package client import ( "testing" "forge.redroom.link/yves/meowlib" "github.com/stretchr/testify/assert" "google.golang.org/protobuf/proto" ) // makeServerPair creates two Server structs with cross-wired keypairs, // simulating a client and a server. clientSrv encrypts for the server; // serverSrv encrypts for the client. func makeServerPair(t *testing.T) (clientSrv *Server, serverSrv *Server) { t.Helper() clientKp, err := meowlib.NewKeyPair() if err != nil { t.Fatal(err) } serverKp, err := meowlib.NewKeyPair() if err != nil { t.Fatal(err) } clientSrv = &Server{ Name: "client-side", Url: "https://server.example.com/meow", PublicKey: serverKp.Public, UserKp: clientKp, } serverSrv = &Server{ Name: "server-side", Url: "https://server.example.com/meow", PublicKey: clientKp.Public, UserKp: serverKp, } return } // --------------------------------------------------------------------------- // CreateServerFromUrl // --------------------------------------------------------------------------- func TestCreateServerFromUrl(t *testing.T) { srv, err := CreateServerFromUrl("https://example.com/meow") assert.NoError(t, err) assert.Equal(t, "https://example.com/meow", srv.Name) assert.Equal(t, "https://example.com/meow", srv.Url) assert.NotNil(t, srv.UserKp) assert.NotEmpty(t, srv.UserKp.Public) assert.NotEmpty(t, srv.UserKp.Private) } func TestCreateServerFromUrl_UniqueUserKps(t *testing.T) { srv1, _ := CreateServerFromUrl("https://a.example.com/meow") srv2, _ := CreateServerFromUrl("https://a.example.com/meow") assert.NotEqual(t, srv1.UserKp.Public, srv2.UserKp.Public) } // --------------------------------------------------------------------------- // CreateServerFromUid // --------------------------------------------------------------------------- func TestCreateServerFromUid_WithCredentials(t *testing.T) { srv, err := CreateServerFromUid("user:pass@https://example.com/meow") assert.NoError(t, err) assert.Equal(t, "user", srv.Login) assert.Equal(t, "pass", srv.Password) assert.Equal(t, "https://example.com/meow", srv.Url) assert.NotNil(t, srv.UserKp) } func TestCreateServerFromUid_WithoutCredentials(t *testing.T) { srv, err := CreateServerFromUid("https://example.com/meow") assert.NoError(t, err) assert.Empty(t, srv.Login) assert.Empty(t, srv.Password) assert.Equal(t, "https://example.com/meow", srv.Url) } func TestCreateServerFromUid_NameIsFullUid(t *testing.T) { uid := "admin:secret@https://example.com/meow" srv, err := CreateServerFromUid(uid) assert.NoError(t, err) assert.Equal(t, uid, srv.Name) } func TestCreateServerFromUid_PasswordOnly(t *testing.T) { srv, err := CreateServerFromUid(":secret@https://example.com/meow") assert.NoError(t, err) assert.Empty(t, srv.Login) assert.Equal(t, "secret", srv.Password) assert.Equal(t, "https://example.com/meow", srv.Url) } // --------------------------------------------------------------------------- // CreateServerFromMeowUrl // --------------------------------------------------------------------------- func TestCreateServerFromMeowUrl_WithCredentials(t *testing.T) { srv, err := CreateServerFromMeowUrl("meow://user:pass@server.example.com/meow") assert.NoError(t, err) assert.Equal(t, "user", srv.Login) assert.Equal(t, "pass", srv.Password) assert.Equal(t, "server.example.com/meow", srv.Url) } func TestCreateServerFromMeowUrl_NoCredentials(t *testing.T) { srv, err := CreateServerFromMeowUrl("meow://server.example.com/meow") assert.NoError(t, err) assert.Empty(t, srv.Login) assert.Equal(t, "server.example.com/meow", srv.Url) } // --------------------------------------------------------------------------- // CreateServerFromInvitationLink // --------------------------------------------------------------------------- func TestCreateServerFromInvitationLink(t *testing.T) { srv, err := CreateServerFromInvitationLink("meow://user:pass@server.example.com/meow?abc123") assert.NoError(t, err) assert.Equal(t, "user", srv.Login) assert.Equal(t, "pass", srv.Password) assert.Equal(t, "server.example.com/meow", srv.Url) } func TestCreateServerFromInvitationLink_NoQueryParam(t *testing.T) { srv, err := CreateServerFromInvitationLink("meow://server.example.com/meow") assert.NoError(t, err) assert.Equal(t, "server.example.com/meow", srv.Url) } func TestCreateServerFromInvitationLink_MultipleQuestionMarks(t *testing.T) { // Only the first ? splits; everything before it is the server URL srv, err := CreateServerFromInvitationLink("meow://server.example.com/meow?code?extra") assert.NoError(t, err) assert.Equal(t, "server.example.com/meow", srv.Url) } // --------------------------------------------------------------------------- // CreateServerFromServerCard // --------------------------------------------------------------------------- func TestCreateServerFromServerCard(t *testing.T) { card := &meowlib.ServerCard{ Name: "MyServer", PublicKey: "server-pub-key", Description: "A test server", Url: "https://example.com/meow", Login: "admin", Password: "secret", } srv, err := CreateServerFromServerCard(card) assert.NoError(t, err) assert.Equal(t, "MyServer", srv.Name) assert.Equal(t, "server-pub-key", srv.PublicKey) assert.Equal(t, "A test server", srv.Description) assert.Equal(t, "https://example.com/meow", srv.Url) assert.Equal(t, "admin", srv.Login) assert.Equal(t, "secret", srv.Password) assert.NotNil(t, srv.UserKp) } func TestCreateServerFromServerCard_MinimalCard(t *testing.T) { card := &meowlib.ServerCard{Url: "https://minimal.example.com"} srv, err := CreateServerFromServerCard(card) assert.NoError(t, err) assert.Equal(t, "https://minimal.example.com", srv.Url) assert.NotNil(t, srv.UserKp) } // --------------------------------------------------------------------------- // GetServerCard // --------------------------------------------------------------------------- func TestGetServerCard(t *testing.T) { srv := &Server{ Name: "MyServer", PublicKey: "pub123", Description: "desc", Url: "https://example.com/meow", Login: "user", Password: "pw", } card := srv.GetServerCard() assert.Equal(t, srv.Name, card.Name) assert.Equal(t, srv.PublicKey, card.PublicKey) assert.Equal(t, srv.Description, card.Description) assert.Equal(t, srv.Url, card.Url) assert.Equal(t, srv.Login, card.Login) assert.Equal(t, srv.Password, card.Password) } func TestGetServerCard_RoundTrip(t *testing.T) { card := &meowlib.ServerCard{ Name: "RT", PublicKey: "pk", Description: "roundtrip", Url: "https://rt.example.com", Login: "l", Password: "p", } srv, err := CreateServerFromServerCard(card) assert.NoError(t, err) restored := srv.GetServerCard() assert.Equal(t, card.Name, restored.Name) assert.Equal(t, card.PublicKey, restored.PublicKey) assert.Equal(t, card.Description, restored.Description) assert.Equal(t, card.Url, restored.Url) assert.Equal(t, card.Login, restored.Login) assert.Equal(t, card.Password, restored.Password) } // --------------------------------------------------------------------------- // GetUid / GetMeowUrl // --------------------------------------------------------------------------- func TestGetUid_WithCredentials(t *testing.T) { srv := &Server{Login: "user", Password: "pass", Url: "https://example.com/meow"} assert.Equal(t, "user:pass@https://example.com/meow", srv.GetUid()) } func TestGetUid_NoCredentials(t *testing.T) { srv := &Server{Url: "https://example.com/meow"} assert.Equal(t, "https://example.com/meow", srv.GetUid()) } func TestGetUid_PasswordOnly(t *testing.T) { srv := &Server{Password: "pass", Url: "https://example.com/meow"} assert.Equal(t, ":pass@https://example.com/meow", srv.GetUid()) } func TestGetMeowUrl_NoCredentials(t *testing.T) { srv := &Server{Url: "https://example.com/meow"} assert.Equal(t, "meow://https://example.com/meow", srv.GetMeowUrl()) } func TestGetMeowUrl_WithCredentials(t *testing.T) { srv := &Server{Login: "user", Password: "pass", Url: "https://example.com/meow"} // With credentials the meow:// prefix is not added — matches GetUid behaviour assert.Equal(t, srv.GetUid(), srv.GetMeowUrl()) } // --------------------------------------------------------------------------- // AsymEncryptMessage / AsymDecryptMessage // --------------------------------------------------------------------------- func TestServer_AsymEncryptDecrypt_RoundTrip(t *testing.T) { clientSrv, serverSrv := makeServerPair(t) plaintext := []byte("hello from client to server") enc, err := clientSrv.AsymEncryptMessage(plaintext) assert.NoError(t, err) assert.NotEmpty(t, enc.Data) assert.NotEmpty(t, enc.Signature) decrypted, err := serverSrv.AsymDecryptMessage(enc.Data, enc.Signature) assert.NoError(t, err) assert.Equal(t, plaintext, decrypted) } func TestServer_AsymEncryptDecrypt_Bidirectional(t *testing.T) { clientSrv, serverSrv := makeServerPair(t) // Client → Server enc1, err := clientSrv.AsymEncryptMessage([]byte("client msg")) assert.NoError(t, err) dec1, err := serverSrv.AsymDecryptMessage(enc1.Data, enc1.Signature) assert.NoError(t, err) assert.Equal(t, []byte("client msg"), dec1) // Server → Client enc2, err := serverSrv.AsymEncryptMessage([]byte("server msg")) assert.NoError(t, err) dec2, err := clientSrv.AsymDecryptMessage(enc2.Data, enc2.Signature) assert.NoError(t, err) assert.Equal(t, []byte("server msg"), dec2) } func TestServer_AsymEncryptMessage_InvalidKey(t *testing.T) { srv := &Server{ PublicKey: "not-a-valid-key", UserKp: &meowlib.KeyPair{Private: "also-invalid"}, } _, err := srv.AsymEncryptMessage([]byte("test")) assert.Error(t, err) } func TestServer_AsymDecryptMessage_WrongSignatureKey(t *testing.T) { clientSrv, serverSrv := makeServerPair(t) enc, err := clientSrv.AsymEncryptMessage([]byte("hello")) assert.NoError(t, err) // Replace expected sender key with a random one eve, err := meowlib.NewKeyPair() if err != nil { t.Fatal(err) } serverSrv.PublicKey = eve.Public _, err = serverSrv.AsymDecryptMessage(enc.Data, enc.Signature) assert.Error(t, err) } // --------------------------------------------------------------------------- // PackServerMessage / UnPackServerMessage // --------------------------------------------------------------------------- func TestServer_PackUnPack_RoundTrip(t *testing.T) { srv, err := CreateServerFromUrl("https://example.com/meow") assert.NoError(t, err) payload := []byte("test payload") signature := []byte("test sig") packed, err := srv.PackServerMessage(payload, signature) assert.NoError(t, err) assert.NotEmpty(t, packed) gotPayload, gotSig, err := srv.UnPackServerMessage(packed) assert.NoError(t, err) assert.Equal(t, payload, gotPayload) assert.Equal(t, signature, gotSig) } func TestServer_PackServerMessage_SetsFrom(t *testing.T) { srv, err := CreateServerFromUrl("https://example.com/meow") assert.NoError(t, err) packed, err := srv.PackServerMessage([]byte("p"), []byte("s")) assert.NoError(t, err) msg := &meowlib.PackedServerMessage{} err = proto.Unmarshal(packed, msg) assert.NoError(t, err) assert.Equal(t, srv.UserKp.Public, msg.From) } func TestServer_UnPackServerMessage_InvalidData(t *testing.T) { srv := &Server{} _, _, err := srv.UnPackServerMessage([]byte{0xff, 0xff, 0xff, 0xff, 0xff}) assert.Error(t, err) } // --------------------------------------------------------------------------- // BuildToServerMessageFromUserMessage // --------------------------------------------------------------------------- func TestServer_BuildToServerMessageFromUserMessage(t *testing.T) { srv, err := CreateServerFromUrl("https://example.com/meow") assert.NoError(t, err) pum := &meowlib.PackedUserMessage{ Destination: "dest-key", Payload: []byte("encrypted"), Signature: []byte("sig"), } msg := srv.BuildToServerMessageFromUserMessage(pum) assert.NotEmpty(t, msg.Uuid) assert.Equal(t, "1", msg.Type) assert.Equal(t, srv.UserKp.Public, msg.From) assert.Len(t, msg.Messages, 1) assert.Equal(t, pum, msg.Messages[0]) } func TestServer_BuildToServerMessageFromUserMessage_UniqueUuids(t *testing.T) { srv, _ := CreateServerFromUrl("https://example.com/meow") pum := &meowlib.PackedUserMessage{Destination: "d"} msg1 := srv.BuildToServerMessageFromUserMessage(pum) msg2 := srv.BuildToServerMessageFromUserMessage(pum) assert.NotEqual(t, msg1.Uuid, msg2.Uuid) } // --------------------------------------------------------------------------- // BuildMessageSendingMessage // --------------------------------------------------------------------------- func TestServer_BuildMessageSendingMessage(t *testing.T) { srv, err := CreateServerFromUrl("https://example.com/meow") assert.NoError(t, err) pum := &meowlib.PackedUserMessage{ Destination: "dest", Payload: []byte("payload"), } data, err := srv.BuildMessageSendingMessage(pum) assert.NoError(t, err) assert.NotEmpty(t, data) var msg meowlib.ToServerMessage err = proto.Unmarshal(data, &msg) assert.NoError(t, err) assert.Equal(t, "1", msg.Type) assert.Equal(t, srv.UserKp.Public, msg.From) assert.Len(t, msg.Messages, 1) assert.Equal(t, []byte("payload"), msg.Messages[0].Payload) } // --------------------------------------------------------------------------- // BuildMessageRequestMessage // --------------------------------------------------------------------------- func TestServer_BuildMessageRequestMessage(t *testing.T) { srv, err := CreateServerFromUrl("https://example.com/meow") assert.NoError(t, err) data, err := srv.BuildMessageRequestMessage([]string{"key1", "key2"}) assert.NoError(t, err) assert.NotEmpty(t, data) var msg meowlib.ToServerMessage err = proto.Unmarshal(data, &msg) assert.NoError(t, err) assert.Equal(t, "1", msg.Type) assert.Equal(t, srv.UserKp.Public, msg.From) assert.NotEmpty(t, msg.Uuid) // Note: lookupKeys parameter is currently unused in the message body assert.Empty(t, msg.PullRequest) } // --------------------------------------------------------------------------- // BuildVideoRoomRequestMessage // --------------------------------------------------------------------------- func TestServer_BuildVideoRoomRequestMessage(t *testing.T) { srv, err := CreateServerFromUrl("https://example.com/meow") assert.NoError(t, err) users := []string{"alice", "bob", "charlie"} msg, err := srv.BuildVideoRoomRequestMessage(users, 3600) assert.NoError(t, err) assert.NotNil(t, msg) assert.Equal(t, "1", msg.Type) assert.Equal(t, srv.UserKp.Public, msg.From) assert.NotNil(t, msg.VideoData) assert.Len(t, msg.VideoData.Credentials, 3) assert.Equal(t, "alice", msg.VideoData.Credentials[0].Username) assert.Equal(t, "bob", msg.VideoData.Credentials[1].Username) assert.Equal(t, "charlie", msg.VideoData.Credentials[2].Username) } func TestServer_BuildVideoRoomRequestMessage_SingleUser(t *testing.T) { srv, _ := CreateServerFromUrl("https://example.com/meow") msg, err := srv.BuildVideoRoomRequestMessage([]string{"solo"}, 60) assert.NoError(t, err) assert.Len(t, msg.VideoData.Credentials, 1) assert.Equal(t, "solo", msg.VideoData.Credentials[0].Username) } // --------------------------------------------------------------------------- // BuildToServerMessageInvitationCreation // --------------------------------------------------------------------------- func TestServer_BuildToServerMessageInvitationCreation(t *testing.T) { srv, err := CreateServerFromUrl("https://example.com/meow") assert.NoError(t, err) cc := &meowlib.ContactCard{ Name: "Alice", ContactPublicKey: "alice-pub", } msg, err := srv.BuildToServerMessageInvitationCreation(cc, "secret", 300, 8) assert.NoError(t, err) assert.Equal(t, "1", msg.Type) assert.Equal(t, srv.UserKp.Public, msg.From) assert.NotNil(t, msg.Invitation) assert.Equal(t, int32(1), msg.Invitation.Step) assert.Equal(t, "secret", msg.Invitation.Password) assert.Equal(t, int32(300), msg.Invitation.Timeout) assert.Equal(t, int32(8), msg.Invitation.ShortcodeLen) assert.NotEmpty(t, msg.Invitation.Payload) // Payload is a compressed ContactCard — decompress and verify restored, err := meowlib.NewContactCardFromCompressed(msg.Invitation.Payload) assert.NoError(t, err) assert.Equal(t, "Alice", restored.Name) assert.Equal(t, "alice-pub", restored.ContactPublicKey) } func TestServer_BuildToServerMessageInvitationCreation_NoPassword(t *testing.T) { srv, _ := CreateServerFromUrl("https://example.com/meow") cc := &meowlib.ContactCard{Name: "Bob"} msg, err := srv.BuildToServerMessageInvitationCreation(cc, "", 60, 6) assert.NoError(t, err) assert.Empty(t, msg.Invitation.Password) assert.Equal(t, int32(1), msg.Invitation.Step) } // --------------------------------------------------------------------------- // BuildToServerMessageInvitationRequest // --------------------------------------------------------------------------- func TestServer_BuildToServerMessageInvitationRequest(t *testing.T) { srv, err := CreateServerFromUrl("https://example.com/meow") assert.NoError(t, err) msg, err := srv.BuildToServerMessageInvitationRequest("SC1234", "mypassword") assert.NoError(t, err) assert.Equal(t, "1", msg.Type) assert.Equal(t, srv.UserKp.Public, msg.From) assert.NotNil(t, msg.Invitation) assert.Equal(t, int32(2), msg.Invitation.Step) assert.Equal(t, "SC1234", msg.Invitation.Shortcode) assert.Equal(t, "mypassword", msg.Invitation.Password) } func TestServer_BuildToServerMessageInvitationRequest_NoPassword(t *testing.T) { srv, _ := CreateServerFromUrl("https://example.com/meow") msg, err := srv.BuildToServerMessageInvitationRequest("CODE", "") assert.NoError(t, err) assert.Equal(t, "CODE", msg.Invitation.Shortcode) assert.Empty(t, msg.Invitation.Password) } // --------------------------------------------------------------------------- // BuildToServerMessageInvitationAnswer // --------------------------------------------------------------------------- func TestServer_BuildToServerMessageInvitationAnswer(t *testing.T) { srv, err := CreateServerFromUrl("https://example.com/meow") assert.NoError(t, err) pum := &meowlib.PackedUserMessage{ Destination: "dest", Payload: []byte("answer-payload"), Signature: []byte("answer-sig"), } msg, err := srv.BuildToServerMessageInvitationAnswer(pum, "my-pub-key", "inv-uuid-42", 600) assert.NoError(t, err) assert.Equal(t, "1", msg.Type) assert.Equal(t, srv.UserKp.Public, msg.From) assert.NotNil(t, msg.Invitation) assert.Equal(t, int32(3), msg.Invitation.Step) assert.Equal(t, "inv-uuid-42", msg.Invitation.Uuid) assert.Equal(t, "my-pub-key", msg.Invitation.From) assert.NotEmpty(t, msg.Invitation.Payload) // Payload is proto-serialized PackedUserMessage var decoded meowlib.PackedUserMessage err = proto.Unmarshal(msg.Invitation.Payload, &decoded) assert.NoError(t, err) assert.Equal(t, "dest", decoded.Destination) assert.Equal(t, []byte("answer-payload"), decoded.Payload) } // --------------------------------------------------------------------------- // BuildToServerMessageInvitationAnswerRequest // --------------------------------------------------------------------------- func TestServer_BuildToServerMessageInvitationAnswerRequest(t *testing.T) { srv, err := CreateServerFromUrl("https://example.com/meow") assert.NoError(t, err) msg, err := srv.BuildToServerMessageInvitationAnswerRequest("inv-uuid-99") assert.NoError(t, err) assert.Equal(t, "1", msg.Type) assert.Equal(t, srv.UserKp.Public, msg.From) assert.NotNil(t, msg.Invitation) assert.Equal(t, int32(4), msg.Invitation.Step) assert.Equal(t, "inv-uuid-99", msg.Invitation.Uuid) } // --------------------------------------------------------------------------- // ProcessOutboundMessage / ProcessInboundServerResponse (full pipeline) // --------------------------------------------------------------------------- func TestServer_ProcessOutboundMessage(t *testing.T) { clientSrv, serverSrv := makeServerPair(t) original := &meowlib.ToServerMessage{ Uuid: "out-uuid", Type: "1", From: clientSrv.UserKp.Public, } packed, err := clientSrv.ProcessOutboundMessage(original) assert.NoError(t, err) assert.NotEmpty(t, packed) // Verify the server side can unpack and decrypt back to the original payload, sig, err := serverSrv.UnPackServerMessage(packed) assert.NoError(t, err) decrypted, err := serverSrv.AsymDecryptMessage(payload, sig) assert.NoError(t, err) var restored meowlib.ToServerMessage err = proto.Unmarshal(decrypted, &restored) assert.NoError(t, err) assert.Equal(t, "out-uuid", restored.Uuid) } func TestServer_ProcessOutboundMessage_InvalidServerKey(t *testing.T) { srv := &Server{ PublicKey: "bad-key", UserKp: &meowlib.KeyPair{Public: "pub", Private: "bad-priv"}, } msg := &meowlib.ToServerMessage{Type: "1"} _, err := srv.ProcessOutboundMessage(msg) assert.Error(t, err) } func TestServer_ProcessInboundServerResponse(t *testing.T) { clientSrv, serverSrv := makeServerPair(t) original := &meowlib.FromServerMessage{ Chat: []*meowlib.PackedUserMessage{ {Destination: "chat-dest", Payload: []byte("chat-payload")}, }, } originalBytes, err := proto.Marshal(original) assert.NoError(t, err) // Simulate server packing: encrypt for the client, then pack enc, err := serverSrv.AsymEncryptMessage(originalBytes) assert.NoError(t, err) packedMsg, err := serverSrv.PackServerMessage(enc.Data, enc.Signature) assert.NoError(t, err) // Client processes the inbound message received, err := clientSrv.ProcessInboundServerResponse(packedMsg) assert.NoError(t, err) assert.NotNil(t, received) assert.Len(t, received.Chat, 1) assert.Equal(t, "chat-dest", received.Chat[0].Destination) assert.Equal(t, []byte("chat-payload"), received.Chat[0].Payload) } func TestServer_ProcessInboundServerResponse_InvalidData(t *testing.T) { srv := &Server{ UserKp: &meowlib.KeyPair{Private: "invalid"}, } _, err := srv.ProcessInboundServerResponse([]byte{0xff, 0xff}) assert.Error(t, err) }