Files
meowlib/client/server_test.go

641 lines
22 KiB
Go
Raw Normal View History

2026-02-04 20:04:19 +01:00
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)
}