adding basic peer tests

This commit is contained in:
ycc
2026-02-04 19:42:28 +01:00
parent b1ecd04a28
commit 4fe989b5ff
2 changed files with 503 additions and 1 deletions

View File

@@ -1,13 +1,515 @@
package client
import (
"os"
"strconv"
"testing"
"forge.redroom.link/yves/meowlib"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/proto"
)
// makePeerPair creates two peers with properly cross-wired keypairs, simulating
// a completed invitation. Alice's contact keys point to Bob's and vice versa.
func makePeerPair(t *testing.T) (alice *Peer, bob *Peer) {
t.Helper()
aliceIdentity, err := meowlib.NewKeyPair()
if err != nil {
t.Fatal(err)
}
aliceEncryption, err := meowlib.NewKeyPair()
if err != nil {
t.Fatal(err)
}
aliceLookup, err := meowlib.NewKeyPair()
if err != nil {
t.Fatal(err)
}
bobIdentity, err := meowlib.NewKeyPair()
if err != nil {
t.Fatal(err)
}
bobEncryption, err := meowlib.NewKeyPair()
if err != nil {
t.Fatal(err)
}
bobLookup, err := meowlib.NewKeyPair()
if err != nil {
t.Fatal(err)
}
alice = &Peer{
Uid: "alice-uid",
Name: "bob",
MyName: "alice",
MyIdentity: aliceIdentity,
MyEncryptionKp: aliceEncryption,
MyLookupKp: aliceLookup,
ContactPublicKey: bobIdentity.Public,
ContactEncryption: bobEncryption.Public,
ContactLookupKey: bobLookup.Public,
}
bob = &Peer{
Uid: "bob-uid",
Name: "alice",
MyName: "bob",
MyIdentity: bobIdentity,
MyEncryptionKp: bobEncryption,
MyLookupKp: bobLookup,
ContactPublicKey: aliceIdentity.Public,
ContactEncryption: aliceEncryption.Public,
ContactLookupKey: aliceLookup.Public,
}
return
}
// ---------------------------------------------------------------------------
// Invitation state
// ---------------------------------------------------------------------------
func TestInvitationPending_True(t *testing.T) {
p := &Peer{} // ContactPublicKey is empty
assert.True(t, p.InvitationPending())
}
func TestInvitationPending_False(t *testing.T) {
p := &Peer{ContactPublicKey: "some-key"}
assert.False(t, p.InvitationPending())
}
// ---------------------------------------------------------------------------
// BuildSimpleUserMessage
// ---------------------------------------------------------------------------
func TestBuildSimpleUserMessage(t *testing.T) {
p := &Peer{
ContactLookupKey: "dest-lookup-key",
MyIdentity: &meowlib.KeyPair{Public: "my-pub-key"},
}
msg, err := p.BuildSimpleUserMessage([]byte("hello"))
assert.NoError(t, err)
assert.Equal(t, "dest-lookup-key", msg.Destination)
assert.Equal(t, "my-pub-key", msg.From)
assert.Equal(t, []byte("hello"), msg.Data)
assert.Equal(t, "1", msg.Type)
assert.NotNil(t, msg.Status)
assert.NotEmpty(t, msg.Status.Uuid)
}
func TestBuildSimpleUserMessage_EmptyData(t *testing.T) {
p := &Peer{
ContactLookupKey: "dest",
MyIdentity: &meowlib.KeyPair{Public: "pub"},
}
msg, err := p.BuildSimpleUserMessage([]byte{})
assert.NoError(t, err)
assert.Empty(t, msg.Data)
assert.NotEmpty(t, msg.Status.Uuid)
}
func TestBuildSimpleUserMessage_UniqueUuids(t *testing.T) {
p := &Peer{
ContactLookupKey: "dest",
MyIdentity: &meowlib.KeyPair{Public: "pub"},
}
msg1, _ := p.BuildSimpleUserMessage([]byte("a"))
msg2, _ := p.BuildSimpleUserMessage([]byte("b"))
assert.NotEqual(t, msg1.Status.Uuid, msg2.Status.Uuid)
}
// ---------------------------------------------------------------------------
// BuildSingleFileMessage
// ---------------------------------------------------------------------------
func TestBuildSingleFileMessage_FileNotFound(t *testing.T) {
p := &Peer{
ContactLookupKey: "dest",
MyIdentity: &meowlib.KeyPair{Public: "pub"},
}
GetConfig().Chunksize = 1024
_, err := p.BuildSingleFileMessage("/nonexistent/path/file.txt", []byte("msg"))
assert.Error(t, err)
}
func TestBuildSingleFileMessage_SingleChunk(t *testing.T) {
content := []byte("small file content")
tmpFile, err := os.CreateTemp("", "peer_test_*.txt")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tmpFile.Name())
tmpFile.Write(content)
tmpFile.Close()
p := &Peer{
ContactLookupKey: "dest-lookup",
MyIdentity: &meowlib.KeyPair{Public: "my-pub"},
}
GetConfig().Chunksize = 1024 // larger than file
msgs, err := p.BuildSingleFileMessage(tmpFile.Name(), []byte("msg"))
assert.NoError(t, err)
assert.Len(t, msgs, 1)
assert.Equal(t, "dest-lookup", msgs[0].Destination)
assert.Equal(t, "my-pub", msgs[0].From)
assert.Equal(t, "1", msgs[0].Type)
assert.Len(t, msgs[0].Files, 1)
assert.Equal(t, content, msgs[0].Files[0].Data)
assert.Equal(t, uint32(0), msgs[0].Files[0].Chunk)
assert.Equal(t, uint64(len(content)), msgs[0].Files[0].Size)
assert.NotNil(t, msgs[0].Status)
assert.NotEmpty(t, msgs[0].Status.Uuid)
}
func TestBuildSingleFileMessage_MultipleChunks(t *testing.T) {
// 20 bytes with chunksize 7 → chunks of [7, 7, 6], last chunk guaranteed
// to arrive with nil error on os.File before a separate (0, EOF) read.
content := []byte("abcdefghijklmnopqrst")
tmpFile, err := os.CreateTemp("", "peer_test_multi_*.txt")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tmpFile.Name())
tmpFile.Write(content)
tmpFile.Close()
p := &Peer{
ContactLookupKey: "dest-lookup",
MyIdentity: &meowlib.KeyPair{Public: "my-pub"},
}
GetConfig().Chunksize = 7
msgs, err := p.BuildSingleFileMessage(tmpFile.Name(), []byte("msg"))
assert.NoError(t, err)
assert.Len(t, msgs, 3)
// Verify chunk indices and reassembly
var reassembled []byte
for i, m := range msgs {
assert.Equal(t, uint32(i), m.Files[0].Chunk)
assert.Equal(t, uint64(len(content)), m.Files[0].Size)
reassembled = append(reassembled, m.Files[0].Data...)
}
assert.Equal(t, content, reassembled)
// Only the first chunk carries a status UUID
assert.NotNil(t, msgs[0].Status)
assert.NotEmpty(t, msgs[0].Status.Uuid)
assert.Nil(t, msgs[1].Status)
assert.Nil(t, msgs[2].Status)
}
func TestBuildSingleFileMessage_EmptyFile(t *testing.T) {
tmpFile, err := os.CreateTemp("", "peer_test_empty_*.txt")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tmpFile.Name())
tmpFile.Close() // 0 bytes
p := &Peer{
ContactLookupKey: "dest",
MyIdentity: &meowlib.KeyPair{Public: "pub"},
}
GetConfig().Chunksize = 1024
msgs, err := p.BuildSingleFileMessage(tmpFile.Name(), []byte("msg"))
assert.NoError(t, err)
assert.Empty(t, msgs)
}
// ---------------------------------------------------------------------------
// BuildInvitationAnswerMessage
// ---------------------------------------------------------------------------
func TestBuildInvitationAnswerMessage(t *testing.T) {
p := &Peer{
ContactLookupKey: "dest-lookup",
MyIdentity: &meowlib.KeyPair{Public: "my-pub"},
InvitationId: "inv-uuid-123",
}
contactCard := &meowlib.ContactCard{
Name: "Alice",
ContactPublicKey: "alice-pub",
}
msg, err := p.BuildInvitationAnswerMessage(contactCard)
assert.NoError(t, err)
assert.Equal(t, "dest-lookup", msg.Destination)
assert.Equal(t, "my-pub", msg.From)
assert.Equal(t, "1", msg.Type)
assert.NotNil(t, msg.Invitation)
assert.Equal(t, int32(3), msg.Invitation.Step)
assert.Equal(t, "inv-uuid-123", msg.Invitation.Uuid)
// Payload is the proto-serialized contact card
var decoded meowlib.ContactCard
err = proto.Unmarshal(msg.Invitation.Payload, &decoded)
assert.NoError(t, err)
assert.Equal(t, "Alice", decoded.Name)
assert.Equal(t, "alice-pub", decoded.ContactPublicKey)
}
// ---------------------------------------------------------------------------
// Serialize / Deserialize
// ---------------------------------------------------------------------------
func TestSerializeDeserializeUserMessage(t *testing.T) {
p := &Peer{}
original := &meowlib.UserMessage{
Destination: "dest-key",
From: "from-key",
Type: "1",
Data: []byte("test payload"),
Status: &meowlib.ConversationStatus{Uuid: "uuid-1"},
}
serialized, err := p.SerializeUserMessage(original)
assert.NoError(t, err)
assert.NotEmpty(t, serialized)
restored, err := p.DeserializeUserMessage(serialized)
assert.NoError(t, err)
assert.Equal(t, original.Destination, restored.Destination)
assert.Equal(t, original.From, restored.From)
assert.Equal(t, original.Type, restored.Type)
assert.Equal(t, original.Data, restored.Data)
assert.Equal(t, original.Status.Uuid, restored.Status.Uuid)
}
func TestDeserializeUserMessage_InvalidData(t *testing.T) {
p := &Peer{}
// tag = field 1 wire type 2 (length-delimited), length = 10, but 0 bytes follow → EOF
_, err := p.DeserializeUserMessage([]byte{0x0a, 0x0a})
assert.Error(t, err)
}
// ---------------------------------------------------------------------------
// AsymEncryptMessage / AsymDecryptMessage
// ---------------------------------------------------------------------------
func TestAsymEncryptDecryptMessage_RoundTrip(t *testing.T) {
alice, bob := makePeerPair(t)
plaintext := []byte("secret message from alice to bob")
enc, err := alice.AsymEncryptMessage(plaintext)
assert.NoError(t, err)
assert.NotEmpty(t, enc.Data)
assert.NotEmpty(t, enc.Signature)
decrypted, err := bob.AsymDecryptMessage(enc.Data, enc.Signature)
assert.NoError(t, err)
assert.Equal(t, plaintext, decrypted)
}
func TestAsymEncryptDecryptMessage_Bidirectional(t *testing.T) {
alice, bob := makePeerPair(t)
// Alice → Bob
enc1, err := alice.AsymEncryptMessage([]byte("alice says hi"))
assert.NoError(t, err)
dec1, err := bob.AsymDecryptMessage(enc1.Data, enc1.Signature)
assert.NoError(t, err)
assert.Equal(t, []byte("alice says hi"), dec1)
// Bob → Alice
enc2, err := bob.AsymEncryptMessage([]byte("bob says hi"))
assert.NoError(t, err)
dec2, err := alice.AsymDecryptMessage(enc2.Data, enc2.Signature)
assert.NoError(t, err)
assert.Equal(t, []byte("bob says hi"), dec2)
}
func TestAsymDecryptMessage_WrongSignatureKey(t *testing.T) {
alice, bob := makePeerPair(t)
enc, err := alice.AsymEncryptMessage([]byte("hello"))
assert.NoError(t, err)
// Bob verifies against a random key instead of Alice's — must fail
eve, err := meowlib.NewKeyPair()
if err != nil {
t.Fatal(err)
}
bobTampered := *bob
bobTampered.ContactPublicKey = eve.Public
_, err = bobTampered.AsymDecryptMessage(enc.Data, enc.Signature)
assert.Error(t, err)
}
func TestAsymEncryptMessage_InvalidKey(t *testing.T) {
p := &Peer{
ContactEncryption: "not-a-valid-key",
MyIdentity: &meowlib.KeyPair{Private: "also-invalid"},
}
_, err := p.AsymEncryptMessage([]byte("hello"))
assert.Error(t, err)
}
// ---------------------------------------------------------------------------
// PackUserMessage / UnPackUserMessage
// ---------------------------------------------------------------------------
func TestPackUserMessage(t *testing.T) {
p := &Peer{
ContactLookupKey: "dest-key",
ServerDeliveryInfo: false,
}
packed := p.PackUserMessage([]byte("payload"), []byte("sig"))
assert.Equal(t, "dest-key", packed.Destination)
assert.Equal(t, []byte("payload"), packed.Payload)
assert.Equal(t, []byte("sig"), packed.Signature)
assert.Empty(t, packed.ServerDeliveryUuid)
}
func TestPackUserMessage_WithDeliveryTracking(t *testing.T) {
p := &Peer{
ContactLookupKey: "dest-key",
ServerDeliveryInfo: true,
}
packed := p.PackUserMessage([]byte("payload"), []byte("sig"))
assert.NotEmpty(t, packed.ServerDeliveryUuid)
// Two calls produce different delivery UUIDs
packed2 := p.PackUserMessage([]byte("payload"), []byte("sig"))
assert.NotEqual(t, packed.ServerDeliveryUuid, packed2.ServerDeliveryUuid)
}
func TestUnPackUserMessage(t *testing.T) {
p := &Peer{}
// UnPackUserMessage unmarshals a PackedServerMessage (fields 2,3 = payload, signature)
original := &meowlib.PackedServerMessage{
From: "sender",
Payload: []byte("the payload"),
Signature: []byte("the signature"),
}
data, err := proto.Marshal(original)
if err != nil {
t.Fatal(err)
}
payload, signature, err := p.UnPackUserMessage(data)
assert.NoError(t, err)
assert.Equal(t, []byte("the payload"), payload)
assert.Equal(t, []byte("the signature"), signature)
}
func TestUnPackUserMessage_InvalidData(t *testing.T) {
p := &Peer{}
// Truncated varint — all continuation bits set, no terminator
_, _, err := p.UnPackUserMessage([]byte{0xff, 0xff, 0xff, 0xff, 0xff})
assert.Error(t, err)
}
// ---------------------------------------------------------------------------
// ProcessOutboundUserMessage / ProcessInboundUserMessage (full pipeline)
// ---------------------------------------------------------------------------
func TestProcessOutboundInbound_RoundTrip(t *testing.T) {
alice, bob := makePeerPair(t)
userMsg, err := alice.BuildSimpleUserMessage([]byte("end to end test"))
assert.NoError(t, err)
packed, err := alice.ProcessOutboundUserMessage(userMsg)
assert.NoError(t, err)
assert.NotEmpty(t, packed.Payload)
assert.NotEmpty(t, packed.Signature)
assert.Equal(t, bob.MyLookupKp.Public, packed.Destination)
received, err := bob.ProcessInboundUserMessage(packed.Payload, packed.Signature)
assert.NoError(t, err)
assert.Equal(t, []byte("end to end test"), received.Data)
assert.Equal(t, alice.MyIdentity.Public, received.From)
}
func TestProcessOutboundInbound_EmptyMessage(t *testing.T) {
alice, bob := makePeerPair(t)
userMsg, err := alice.BuildSimpleUserMessage([]byte{})
assert.NoError(t, err)
packed, err := alice.ProcessOutboundUserMessage(userMsg)
assert.NoError(t, err)
received, err := bob.ProcessInboundUserMessage(packed.Payload, packed.Signature)
assert.NoError(t, err)
assert.Empty(t, received.Data)
}
func TestProcessOutboundUserMessage_InvalidKey(t *testing.T) {
p := &Peer{
ContactLookupKey: "dest",
ContactEncryption: "invalid-key",
MyIdentity: &meowlib.KeyPair{Public: "pub", Private: "invalid-priv"},
}
msg, _ := p.BuildSimpleUserMessage([]byte("test"))
_, err := p.ProcessOutboundUserMessage(msg)
assert.Error(t, err)
}
// ---------------------------------------------------------------------------
// GetConversationRequest
// ---------------------------------------------------------------------------
func TestGetConversationRequest(t *testing.T) {
p := &Peer{}
cr := p.GetConversationRequest()
assert.NotNil(t, cr)
}
// ---------------------------------------------------------------------------
// SetDbPassword / GetDbPassword
// ---------------------------------------------------------------------------
func TestGetDbPassword_NoPasswordSet(t *testing.T) {
p := &Peer{} // no explicit dbPassword
GetConfig().Clean()
_, err := p.GetDbPassword()
assert.Error(t, err)
}
func TestSetGetDbPassword(t *testing.T) {
p := &Peer{}
p.SetDbPassword("my-secret-password")
pw, err := p.GetDbPassword()
assert.NoError(t, err)
assert.Equal(t, "my-secret-password", pw)
}
func TestGetDbPassword_FallbackToMemPass(t *testing.T) {
p := &Peer{} // dbPassword not set → falls back to config
GetConfig().SetMemPass("config-password")
pw, err := p.GetDbPassword()
assert.NoError(t, err)
assert.Equal(t, "config-password", pw)
}
// ---------------------------------------------------------------------------
// Stub / no-op functions
// ---------------------------------------------------------------------------
func TestUpdateMessage_ReturnsNil(t *testing.T) {
p := &Peer{}
err := p.UpdateMessage(InternalUserMessage{})
assert.NoError(t, err)
}
func TestLoadMessage_ReturnsNil(t *testing.T) {
p := &Peer{}
msg, err := p.LoadMessage("some-uid")
assert.NoError(t, err)
assert.Nil(t, msg)
}
// ---------------------------------------------------------------------------
// Original test (retained)
// ---------------------------------------------------------------------------
func TestGetFromPublicKey(t *testing.T) {
id, err := CreateIdentity("test")
if err != nil {