Files
meowlib/client/helpers/messageHelper_test.go

302 lines
9.7 KiB
Go
Raw Normal View History

2026-02-28 21:04:13 +01:00
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")
}
}