package helpers import ( "fmt" "os" "path/filepath" "testing" "time" "forge.redroom.link/yves/meowlib" "forge.redroom.link/yves/meowlib/client" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" _ "github.com/mattn/go-sqlite3" ) // setupMsgHelperConfig wires the global client.Config singleton to a fresh // temporary directory and returns it. Original values are restored in t.Cleanup. func setupMsgHelperConfig(t *testing.T) (dir string, id *client.Identity) { t.Helper() dir = t.TempDir() cfg := client.GetConfig() origStorage := cfg.StoragePath origSuffix := cfg.DbSuffix origChunk := cfg.Chunksize cfg.StoragePath = dir cfg.DbSuffix = ".sqlite" cfg.Chunksize = 1024 * 1024 require.NoError(t, cfg.SetMemPass("testpassword")) var err error id, err = client.CreateIdentity("testuser") require.NoError(t, err) t.Cleanup(func() { cfg.StoragePath = origStorage cfg.DbSuffix = origSuffix cfg.Chunksize = origChunk }) return dir, id } // newFullyKeyedPeer returns a Peer with all three keypairs and contact keys set, // ready to store messages. func newFullyKeyedPeer(t *testing.T, uid string) *client.Peer { t.Helper() var err error peer := &client.Peer{ Uid: uid, Name: "TestPeer-" + uid, } peer.MyIdentity, err = meowlib.NewKeyPair() require.NoError(t, err) peer.MyEncryptionKp, err = meowlib.NewKeyPair() require.NoError(t, err) peer.MyLookupKp, err = meowlib.NewKeyPair() require.NoError(t, err) k, err := meowlib.NewKeyPair() require.NoError(t, err) peer.ContactPublicKey = k.Public k, err = meowlib.NewKeyPair() require.NoError(t, err) peer.ContactEncryption = k.Public k, err = meowlib.NewKeyPair() require.NoError(t, err) peer.ContactLookupKey = k.Public return peer } // storeTestMessage stores a single outbound message for peer. func storeTestMessage(t *testing.T, peer *client.Peer, text string) { t.Helper() um := &meowlib.UserMessage{ Data: []byte(text), From: peer.MyIdentity.Public, Status: &meowlib.ConversationStatus{Uuid: "uuid-" + text}, } require.NoError(t, peer.StoreMessage(um, nil)) require.NotNil(t, peer.LastMessage, "StoreMessage must set LastMessage") } // pushAndMarkSent pushes a send job for the given peer and marks it as delivered // by the given server. Returns the job after the status update. // It mirrors the correct app usage: read MessageDbFile/MessageDbId from // peer.LastMessage right after storing (i.e. from CreateAndStoreUserMessage's // dbFile/dbId return values). func pushAndMarkSent(t *testing.T, dir string, peer *client.Peer, srv client.Server) *client.SendJob { t.Helper() require.NotNil(t, peer.LastMessage, "pushAndMarkSent: call storeTestMessage first") dbFile := peer.LastMessage.Dbfile dbId := peer.LastMessage.Dbid msgFile := filepath.Join(dir, fmt.Sprintf("msg-%d.bin", dbId)) require.NoError(t, os.WriteFile(msgFile, []byte("packed-server-message"), 0600)) require.NoError(t, client.PushSendJob(dir, &client.SendJob{ Queue: peer.Uid, File: msgFile, MessageDbFile: dbFile, MessageDbId: dbId, Servers: []client.Server{srv}, Timeout: 60, })) job, _, err := client.PeekSendJob(dir, peer.Uid) require.NoError(t, err) require.NotNil(t, job) sentAt := time.Now() srvIdx := 0 job.Status = client.SendStatusSent job.SentAt = &sentAt job.SuccessfulServer = &srvIdx require.NoError(t, client.UpdateSendJob(dir, peer.Uid, job)) return job } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- // TestProcessSentMessages_UpdatesDeliveryInfo is the main round-trip test. // It verifies that after ProcessSentMessages runs: // - the function returns 1 (one message updated) // - the send job is removed from the queue // - a subsequent LoadMessagesHistory returns ServerDeliveryUuid and // ServerDeliveryTimestamp for the message func TestProcessSentMessages_UpdatesDeliveryInfo(t *testing.T) { dir, id := setupMsgHelperConfig(t) peer := newFullyKeyedPeer(t, "peer-uid-main") require.NoError(t, id.Peers.StorePeer(peer)) storeTestMessage(t, peer, "hello world") srv := client.Server{Url: "http://test-server.example"} job := pushAndMarkSent(t, dir, peer, srv) // --- call under test --- updated := ProcessSentMessages(dir) assert.Equal(t, 1, updated, "exactly one message should be updated") // The job must be removed from the queue after processing. jobAfter, err := client.GetSendJob(dir, peer.Uid, job.ID) require.NoError(t, err) assert.Nil(t, jobAfter, "job should be deleted after processing") // Reload message history and verify delivery metadata was persisted. msgs, err := peer.LoadMessagesHistory(0, 0, 50) require.NoError(t, err) require.Len(t, msgs, 1, "expected exactly one message in history") assert.Equal(t, srv.GetUid(), msgs[0].ServerDeliveryUuid, "ServerDeliveryUuid should match the server that accepted the message") assert.NotZero(t, msgs[0].ServerDeliveryTimestamp, "ServerDeliveryTimestamp should be set after ProcessSentMessages") assert.Equal(t, uint64(job.SentAt.Unix()), msgs[0].ServerDeliveryTimestamp, "ServerDeliveryTimestamp should match job.SentAt") } // TestProcessSentMessages_SkipsJobWithoutDeliveryInfo verifies that a Sent job // missing SentAt or SuccessfulServer is discarded (not counted, not updating // the message DB). func TestProcessSentMessages_SkipsJobWithoutDeliveryInfo(t *testing.T) { dir, id := setupMsgHelperConfig(t) peer := newFullyKeyedPeer(t, "peer-uid-incomplete") require.NoError(t, id.Peers.StorePeer(peer)) storeTestMessage(t, peer, "incomplete job") require.NotNil(t, peer.LastMessage) msgFile := filepath.Join(dir, "msg.bin") require.NoError(t, os.WriteFile(msgFile, []byte("packed"), 0600)) require.NoError(t, client.PushSendJob(dir, &client.SendJob{ Queue: peer.Uid, File: msgFile, MessageDbFile: peer.LastMessage.Dbfile, MessageDbId: peer.LastMessage.Dbid, Servers: []client.Server{{Url: "http://test-server.example"}}, Timeout: 60, })) job, _, err := client.PeekSendJob(dir, peer.Uid) require.NoError(t, err) require.NotNil(t, job) // Mark as Sent but intentionally leave SentAt and SuccessfulServer nil. job.Status = client.SendStatusSent require.NoError(t, client.UpdateSendJob(dir, peer.Uid, job)) updated := ProcessSentMessages(dir) assert.Equal(t, 0, updated, "incomplete job must not be counted as updated") // Message should have no delivery info. msgs, err := peer.LoadMessagesHistory(0, 0, 50) require.NoError(t, err) require.Len(t, msgs, 1) assert.Empty(t, msgs[0].ServerDeliveryUuid, "delivery UUID must not be set") assert.Zero(t, msgs[0].ServerDeliveryTimestamp, "delivery timestamp must not be set") } // TestProcessSentMessages_EmptyQueues verifies that an absent or empty queues // directory results in 0 updates without error. func TestProcessSentMessages_EmptyQueues(t *testing.T) { dir, _ := setupMsgHelperConfig(t) // queues/ directory does not exist yet. updated := ProcessSentMessages(dir) assert.Equal(t, 0, updated, "no queues → 0 updates") // Also test with the directory present but empty. require.NoError(t, os.MkdirAll(filepath.Join(dir, "queues"), 0700)) updated = ProcessSentMessages(dir) assert.Equal(t, 0, updated, "empty queues → 0 updates") } // TestProcessSentMessages_MissingDbInfo verifies that a job with blank // MessageDbFile or zero MessageDbId is rejected with a clear error, not silently // treated as successful. func TestProcessSentMessages_MissingDbInfo(t *testing.T) { dir, id := setupMsgHelperConfig(t) peer := newFullyKeyedPeer(t, "peer-uid-nodbinfo") require.NoError(t, id.Peers.StorePeer(peer)) storeTestMessage(t, peer, "the real message") msgFile := filepath.Join(dir, "msg.bin") require.NoError(t, os.WriteFile(msgFile, []byte("packed"), 0600)) // Push a job WITHOUT MessageDbFile / MessageDbId — the old broken pattern. require.NoError(t, client.PushSendJob(dir, &client.SendJob{ Queue: peer.Uid, File: msgFile, Servers: []client.Server{{Url: "http://test-server.example"}}, Timeout: 60, })) job, _, err := client.PeekSendJob(dir, peer.Uid) require.NoError(t, err) require.NotNil(t, job) sentAt := time.Now() srvIdx := 0 job.Status = client.SendStatusSent job.SentAt = &sentAt job.SuccessfulServer = &srvIdx require.NoError(t, client.UpdateSendJob(dir, peer.Uid, job)) // Must NOT count as updated; the real message row must be untouched. updated := ProcessSentMessages(dir) assert.Equal(t, 0, updated, "job without db info must not be counted as updated") msgs, err := peer.LoadMessagesHistory(0, 0, 50) require.NoError(t, err) require.Len(t, msgs, 1) assert.Empty(t, msgs[0].ServerDeliveryUuid, "delivery UUID must not be set") assert.Zero(t, msgs[0].ServerDeliveryTimestamp, "delivery timestamp must not be set") } // TestProcessSentMessages_MultipleMessages verifies that all jobs in the same // queue are processed and that each message gets its own delivery info. func TestProcessSentMessages_MultipleMessages(t *testing.T) { dir, id := setupMsgHelperConfig(t) peer := newFullyKeyedPeer(t, "peer-uid-multi") require.NoError(t, id.Peers.StorePeer(peer)) srv := client.Server{Url: "http://test-server.example"} const n = 3 for i := range n { storeTestMessage(t, peer, fmt.Sprintf("message-%d", i)) pushAndMarkSent(t, dir, peer, srv) } updated := ProcessSentMessages(dir) assert.Equal(t, n, updated, "all %d messages should be updated", n) msgs, err := peer.LoadMessagesHistory(0, 0, 50) require.NoError(t, err) require.Len(t, msgs, n) for _, m := range msgs { assert.Equal(t, srv.GetUid(), m.ServerDeliveryUuid, "every message should have ServerDeliveryUuid set") assert.NotZero(t, m.ServerDeliveryTimestamp, "every message should have ServerDeliveryTimestamp set") } }