package client import ( "os" "testing" "forge.redroom.link/yves/meowlib" "github.com/stretchr/testify/assert" ) // setupInvitationTest creates two independent identities with separate storage paths. func setupInvitationTest(t *testing.T) (initiator *Identity, invitee *Identity, cleanup func()) { t.Helper() cfg := GetConfig() cfg.IdentityFile = "inv_test_init.id" cfg.SetMemPass("testpass") initiator, err := CreateIdentity("initiator") if err != nil { t.Fatal(err) } invitee, err = CreateIdentity("invitee") if err != nil { t.Fatal(err) } // Give each identity a pull server UID. srvInit, _ := CreateServerFromUrl("http://init.server/meow/") initiator.MessageServers.StoreServer(srvInit) srvInvitee, _ := CreateServerFromUrl("http://invitee.server/meow/") invitee.MessageServers.StoreServer(srvInvitee) cleanup = func() { os.Remove("inv_test_init.id") os.RemoveAll(cfg.StoragePath + "/" + initiator.Uuid) os.RemoveAll(cfg.StoragePath + "/" + invitee.Uuid) } return initiator, invitee, cleanup } // TestInvitationStep1 verifies that InvitationStep1 creates a minimal peer with only // InvitationKp set (no full keypairs yet) and returns a valid InvitationInitPayload. func TestInvitationStep1(t *testing.T) { initiator, _, cleanup := setupInvitationTest(t) defer cleanup() payload, peer, err := initiator.InvitationStep1("Alice", "Bob", []string{"http://init.server/meow/"}, "Hello Bob!") assert.NoError(t, err) assert.NotNil(t, payload) assert.NotNil(t, peer) assert.NotEmpty(t, payload.Uuid) assert.Equal(t, "Alice", payload.Name) assert.NotEmpty(t, payload.PublicKey) assert.Equal(t, "Hello Bob!", payload.InvitationMessage) // Full keypairs must NOT be set yet. assert.Nil(t, peer.MyIdentity) assert.Nil(t, peer.MyEncryptionKp) assert.Nil(t, peer.MyLookupKp) // Temp keypair must be set. assert.NotNil(t, peer.InvitationKp) assert.Equal(t, payload.PublicKey, peer.InvitationKp.Public) } // TestInvitationStep1PayloadRoundTrip verifies Compress/Decompress of InvitationInitPayload. func TestInvitationStep1PayloadRoundTrip(t *testing.T) { initiator, _, cleanup := setupInvitationTest(t) defer cleanup() payload, _, err := initiator.InvitationStep1("Alice", "Bob", nil, "test msg") assert.NoError(t, err) compressed, err := payload.Compress() assert.NoError(t, err) assert.NotEmpty(t, compressed) restored, err := meowlib.NewInvitationInitPayloadFromCompressed(compressed) assert.NoError(t, err) assert.Equal(t, payload.Uuid, restored.Uuid) assert.Equal(t, payload.Name, restored.Name) assert.Equal(t, payload.PublicKey, restored.PublicKey) assert.Equal(t, payload.InvitationMessage, restored.InvitationMessage) } // TestInvitationStep2 verifies that InvitationStep2 creates a peer with full keypairs and // sets the initiator's temp key as both ContactEncryption and ContactLookupKey. func TestInvitationStep2(t *testing.T) { initiator, invitee, cleanup := setupInvitationTest(t) defer cleanup() payload, _, err := initiator.InvitationStep1("Alice", "Bob", nil, "Hi") assert.NoError(t, err) peer, err := invitee.InvitationStep2("Bob", "Alice", []string{"http://invitee.server/meow/"}, payload) assert.NoError(t, err) assert.NotNil(t, peer) // Full keypairs must be set on invitee's peer. assert.NotNil(t, peer.MyIdentity) assert.NotNil(t, peer.MyEncryptionKp) assert.NotNil(t, peer.MyLookupKp) // Contact fields must point to initiator's temp key. assert.Equal(t, payload.PublicKey, peer.ContactEncryption) assert.Equal(t, payload.PublicKey, peer.ContactLookupKey) assert.Equal(t, payload.Uuid, peer.InvitationId) } // TestInvitationFullFlow exercises the complete 4-step invitation handshake end-to-end, // verifying that both peers end up with each other's full contact information. func TestInvitationFullFlow(t *testing.T) { initiator, invitee, cleanup := setupInvitationTest(t) defer cleanup() // STEP_1: initiator creates init payload. payload, initPeer, err := initiator.InvitationStep1("Alice", "Bob", []string{"http://init.server/meow/"}, "Hello!") assert.NoError(t, err) assert.NotNil(t, initPeer.InvitationKp) assert.Nil(t, initPeer.MyIdentity) // STEP_2: invitee creates their peer from the payload. srvCard := &meowlib.ServerCard{Name: "InviteeServer", Url: "http://invitee.server/meow/"} inviteePeer, err := invitee.InvitationStep2("Bob", "Alice", []string{"http://invitee.server/meow/"}, payload) assert.NoError(t, err) inviteeCC := inviteePeer.GetMyContact() inviteeCC.PullServers = append(inviteeCC.PullServers, srvCard) // STEP_3: initiator receives invitee's CC, generates full keypairs. myCC, _, err := initiator.InvitationStep3(inviteeCC) assert.NoError(t, err) assert.NotNil(t, myCC) assert.NotEmpty(t, myCC.ContactPublicKey) assert.NotEmpty(t, myCC.EncryptionPublicKey) assert.NotEmpty(t, myCC.LookupPublicKey) assert.NotEmpty(t, myCC.DrRootKey) assert.NotEmpty(t, myCC.DrPublicKey) // After step 3, initiator's peer must have full keypairs and invitee's contact info. updatedInitPeer := initiator.Peers.GetFromInvitationId(payload.Uuid) assert.NotNil(t, updatedInitPeer.MyIdentity) assert.NotNil(t, updatedInitPeer.MyEncryptionKp) assert.NotNil(t, updatedInitPeer.MyLookupKp) assert.Equal(t, inviteePeer.MyIdentity.Public, updatedInitPeer.ContactPublicKey) assert.Equal(t, inviteePeer.MyEncryptionKp.Public, updatedInitPeer.ContactEncryption) assert.Nil(t, updatedInitPeer.InvitationKp) // temp key must be cleared // STEP_4: invitee finalizes from initiator's full CC. srvCardInit := &meowlib.ServerCard{Name: "InitServer", Url: "http://init.server/meow/"} myCC.PullServers = append(myCC.PullServers, srvCardInit) err = invitee.InvitationStep4(myCC) assert.NoError(t, err) // Both peers must now be fully finalized (ContactPublicKey set → not pending). finalInitPeer := initiator.Peers.GetFromInvitationId(payload.Uuid) assert.False(t, finalInitPeer.InvitationPending()) finalInviteePeer := invitee.Peers.GetFromInvitationId(payload.Uuid) assert.False(t, finalInviteePeer.InvitationPending()) assert.Equal(t, updatedInitPeer.MyIdentity.Public, finalInviteePeer.ContactPublicKey) assert.Equal(t, updatedInitPeer.MyEncryptionKp.Public, finalInviteePeer.ContactEncryption) assert.Equal(t, updatedInitPeer.MyLookupKp.Public, finalInviteePeer.ContactLookupKey) assert.NotEmpty(t, finalInviteePeer.DrRootKey) } // TestInvitationStep3NotFound verifies that InvitationStep3 returns an error when no // pending peer exists for the given invitation ID. func TestInvitationStep3NotFound(t *testing.T) { initiator, _, cleanup := setupInvitationTest(t) defer cleanup() cc := &meowlib.ContactCard{InvitationId: "nonexistent-uuid", ContactPublicKey: "pub"} _, _, err := initiator.InvitationStep3(cc) assert.Error(t, err) } // TestGetRequestJobsPendingPeer verifies that pending (step-1 only) peers contribute // their InvitationKp to GetRequestJobs instead of MyLookupKp. func TestGetRequestJobsPendingPeer(t *testing.T) { cfg := GetConfig() cfg.SetMemPass("testpass") id, err := CreateIdentity("testjobs") if err != nil { t.Fatal(err) } defer os.RemoveAll(cfg.StoragePath + "/" + id.Uuid) cfg.SetIdentity(id) id.MessageServers = ServerStorage{DbFile: "testjobs.db"} defer os.RemoveAll("testjobs.db") srv, _ := CreateServerFromUrl("http://srv1.test/meow/") id.MessageServers.StoreServer(srv) // Create a step-1 pending peer. _, _, err = id.InvitationStep1("Me", "Friend", []string{"http://srv1.test/meow/"}, "Hi") assert.NoError(t, err) jobs := id.GetRequestJobs() // At least one job should have a lookup key (the InvitationKp). total := 0 for _, j := range jobs { total += len(j.LookupKeys) } assert.Greater(t, total, 0) }