adding basic peer tests
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user