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 { t.Fatal("CreateIdentity failed") } id.Save() for i := 1; i < 10; i++ { var p Peer p.Uid = uuid.New().String() p.Name = "test" + strconv.Itoa(i) p.ContactPublicKey = "stringToFind" + strconv.Itoa(i) err := id.Peers.StorePeer(&p) if err != nil { t.Fatal("StorePeer failed") } } p5 := id.Peers.GetFromPublicKey("stringToFind5") assert.Equal(t, p5.Name, "test5") }