Files
meowlib/client/helpers/messageHelper_test.go

303 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.
2026-03-01 21:15:17 +01:00
// The outbox file is named {dbFile}_{dbId} so that ProcessSentMessages can
// recover the message DB location from the filename, matching the convention
// used by CreateUserMessageAndSendJob.
2026-02-28 21:04:13 +01:00
func pushAndMarkSent(t *testing.T, dir string, peer *client.Peer, srv client.Server) *client.SendJob {
t.Helper()
2026-03-01 21:15:17 +01:00
dbFile := peer.LastMessage.Dbfile
dbId := peer.LastMessage.Dbid
2026-02-28 21:04:13 +01:00
2026-03-01 21:15:17 +01:00
outboxDir := filepath.Join(dir, "outbox")
require.NoError(t, os.MkdirAll(outboxDir, 0700))
msgFile := filepath.Join(outboxDir, fmt.Sprintf("%s_%d", dbFile, dbId))
2026-02-28 21:04:13 +01:00
require.NoError(t, os.WriteFile(msgFile, []byte("packed-server-message"), 0600))
require.NoError(t, client.PushSendJob(dir, &client.SendJob{
2026-03-01 21:15:17 +01:00
Queue: peer.Uid,
File: msgFile,
Servers: []client.Server{srv},
Timeout: 60,
2026-02-28 21:04:13 +01:00
}))
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")
2026-02-28 21:22:15 +01:00
2026-03-01 21:15:17 +01:00
dbFile := peer.LastMessage.Dbfile
dbId := peer.LastMessage.Dbid
2026-02-28 21:04:13 +01:00
2026-03-01 21:15:17 +01:00
outboxDir := filepath.Join(dir, "outbox")
require.NoError(t, os.MkdirAll(outboxDir, 0700))
msgFile := filepath.Join(outboxDir, fmt.Sprintf("%s_%d", dbFile, dbId))
2026-02-28 21:04:13 +01:00
require.NoError(t, os.WriteFile(msgFile, []byte("packed"), 0600))
require.NoError(t, client.PushSendJob(dir, &client.SendJob{
2026-03-01 21:15:17 +01:00
Queue: peer.Uid,
File: msgFile,
Servers: []client.Server{{Url: "http://test-server.example"}},
Timeout: 60,
2026-02-28 21:04:13 +01:00
}))
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")
}
2026-03-01 21:15:17 +01:00
// TestProcessSentMessages_UnparseableFilename verifies that a job whose filename
// does not follow the {dbFile}_{dbId} convention is skipped with a logged error
// and not counted as updated.
func TestProcessSentMessages_UnparseableFilename(t *testing.T) {
2026-02-28 21:04:13 +01:00
dir, id := setupMsgHelperConfig(t)
peer := newFullyKeyedPeer(t, "peer-uid-nodbinfo")
require.NoError(t, id.Peers.StorePeer(peer))
storeTestMessage(t, peer, "the real message")
2026-03-01 21:15:17 +01:00
// A filename with no underscore cannot be parsed as {dbFile}_{dbId}.
msgFile := filepath.Join(dir, "badname.bin")
2026-02-28 21:04:13 +01:00
require.NoError(t, os.WriteFile(msgFile, []byte("packed"), 0600))
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")
}
}