package messages_test import ( "os" "testing" "forge.redroom.link/yves/meowlib" "forge.redroom.link/yves/meowlib/client" "forge.redroom.link/yves/meowlib/client/invitation/messages" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" ) // setupIdentity creates a fresh identity and sets it as the active config identity. // Returns the identity and a cleanup function. func setupIdentity(t *testing.T, nickname string) (*client.Identity, func()) { t.Helper() cfg := client.GetConfig() cfg.SetMemPass("testpass") //nolint:errcheck id, err := client.CreateIdentity(nickname) require.NoError(t, err) cfg.SetIdentity(id) cleanup := func() { os.RemoveAll(cfg.StoragePath + "/" + id.Uuid) } return id, cleanup } // TestStep1ReturnsBinaryPayload verifies that Step1 returns non-empty bytes that // deserialise to a valid InvitationInitPayload, and that the pending peer is stored // with only a temp keypair (no real identity keys yet). func TestStep1ReturnsBinaryPayload(t *testing.T) { cfg := client.GetConfig() cfg.SetMemPass("testpass") //nolint:errcheck initiator, cleanInit := setupIdentity(t, "alice") defer cleanInit() step1Bytes, err := messages.Step1InitiatorCreatesInviteeAndTempKey("Bob", "Alice", "Hello!", nil) require.NoError(t, err) require.NotEmpty(t, step1Bytes) var payload meowlib.InvitationInitPayload require.NoError(t, proto.Unmarshal(step1Bytes, &payload)) assert.NotEmpty(t, payload.Uuid) assert.NotEmpty(t, payload.PublicKey) assert.Equal(t, "Alice", payload.Name) assert.Equal(t, "Hello!", payload.InvitationMessage) // Peer is saved with temp keypair only — no real identity keys yet. initPeer := initiator.Peers.GetFromName("Bob") require.NotNil(t, initPeer) assert.Nil(t, initPeer.MyIdentity) assert.NotNil(t, initPeer.InvitationKp) } // TestFullInvitationFlow runs all four steps end-to-end, passing the binary output of // each step directly to the next, and verifies that both peers end up with each other's // real keys after the exchange completes. func TestFullInvitationFlow(t *testing.T) { cfg := client.GetConfig() cfg.SetMemPass("testpass") //nolint:errcheck // --- STEP 1: initiator creates temp keypair, gets binary payload --- initiator, cleanInit := setupIdentity(t, "alice2") defer cleanInit() step1Bytes, err := messages.Step1InitiatorCreatesInviteeAndTempKey("Bob", "Alice", "", nil) require.NoError(t, err) require.NotEmpty(t, step1Bytes) // --- STEP 2: invitee creates peer, returns serialized Invitation (step=2) --- invitee, cleanInvitee := setupIdentity(t, "bob2") defer cleanInvitee() step2Bytes, err := messages.Step2InviteeCreatesInitiatorAndEncryptedContactCard(step1Bytes, "Alice", "Bob", nil) require.NoError(t, err) require.NotEmpty(t, step2Bytes, "step2 must return non-empty invitation bytes") // Invitee now has a peer with full keypairs. inviteePeer := invitee.Peers.GetFromName("Alice") require.NotNil(t, inviteePeer) assert.NotNil(t, inviteePeer.MyIdentity) assert.NotNil(t, inviteePeer.MyEncryptionKp) assert.NotNil(t, inviteePeer.MyLookupKp) // The step-2 wire format is a serialized Invitation. var inv2 meowlib.Invitation require.NoError(t, proto.Unmarshal(step2Bytes, &inv2)) assert.EqualValues(t, 2, inv2.Step) assert.NotEmpty(t, inv2.Uuid) assert.Equal(t, inviteePeer.MyIdentity.Public, inv2.From) assert.NotEmpty(t, inv2.Payload) // --- STEP 3: initiator decrypts invitee's card, returns serialized Invitation (step=3) --- cfg.SetIdentity(initiator) step3Bytes, err := messages.Step3InitiatorFinalizesInviteeAndCreatesContactCard(step2Bytes) require.NoError(t, err) require.NotEmpty(t, step3Bytes) // Initiator's peer must now hold invitee's real keys; temp keypair must be gone. initPeer := initiator.Peers.GetFromName("Bob") require.NotNil(t, initPeer) assert.Equal(t, inviteePeer.MyIdentity.Public, initPeer.ContactPublicKey) assert.Equal(t, inviteePeer.MyEncryptionKp.Public, initPeer.ContactEncryption) assert.Equal(t, inviteePeer.MyLookupKp.Public, initPeer.ContactLookupKey) assert.Nil(t, initPeer.InvitationKp, "temp keypair must be cleared after step3") assert.NotEmpty(t, initPeer.DrKpPublic) assert.NotEmpty(t, initPeer.DrRootKey) // The step-3 wire format is a serialized Invitation. var inv3 meowlib.Invitation require.NoError(t, proto.Unmarshal(step3Bytes, &inv3)) assert.EqualValues(t, 3, inv3.Step) assert.NotEmpty(t, inv3.Uuid) assert.NotEmpty(t, inv3.Payload) // --- STEP 4: invitee finalises initiator --- cfg.SetIdentity(invitee) finalPeer, err := messages.Step4InviteeFinalizesInitiator(step3Bytes) require.NoError(t, err) require.NotNil(t, finalPeer) // Invitee's peer must now hold initiator's real keys and the invitation must be complete. assert.Equal(t, initPeer.MyIdentity.Public, finalPeer.ContactPublicKey) assert.Equal(t, initPeer.MyEncryptionKp.Public, finalPeer.ContactEncryption) assert.Equal(t, initPeer.MyLookupKp.Public, finalPeer.ContactLookupKey) assert.False(t, finalPeer.InvitationPending(), "invitation must be fully finalized") assert.NotEmpty(t, finalPeer.DrRootKey) assert.NotEmpty(t, finalPeer.ContactDrPublicKey) }