443 lines
13 KiB
Go
443 lines
13 KiB
Go
|
package protocol
|
||
|
|
||
|
import (
|
||
|
"encoding/hex"
|
||
|
"strings"
|
||
|
"testing"
|
||
|
"unicode"
|
||
|
|
||
|
"github.com/ethereum/go-ethereum/crypto"
|
||
|
protocrypto "github.com/status-im/status-go/protocol/crypto"
|
||
|
"github.com/stretchr/testify/assert"
|
||
|
"github.com/stretchr/testify/require"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
testMembershipUpdateMessageBytes = []byte(`["~#g5",["072ea460-84d3-53c5-9979-1ca36fb5d1020x0424a68f89ba5fcd5e0640c1e1f591d561fa4125ca4e2a43592bc4123eca10ce064e522c254bb83079ba404327f6eafc01ec90a1444331fe769d3f3a7f90b0dde1",["~#list",[["^ ","~:chat-id","072ea460-84d3-53c5-9979-1ca36fb5d1020x0424a68f89ba5fcd5e0640c1e1f591d561fa4125ca4e2a43592bc4123eca10ce064e522c254bb83079ba404327f6eafc01ec90a1444331fe769d3f3a7f90b0dde1","~:events",[["^ ","~:type","chat-created","~:name","thathata","~:clock-value",156897373998501],["^ ","^4","members-added","^6",156897373998502,"~:members",["~#set",["0x04aebe2bb01a988abe7d978662f21de7760486119876c680e5a559e38e086a2df6dad41c4e4d9079c03db3bced6cb70fca76afc5650e50ea19b81572046a813534"]]]],"~:signature","7fca3d614cf55bc6cdf9c17fd1e65d1688673322bf1f004c58c78e0927edefea3d1053bf6a9d2e058ae88079f588105dccf2a2f9f330f6035cd47c715ee5950601"]]],null]]`)
|
||
|
testMembershipUpdateMessageStruct = MembershipUpdateMessage{
|
||
|
ChatID: "072ea460-84d3-53c5-9979-1ca36fb5d1020x0424a68f89ba5fcd5e0640c1e1f591d561fa4125ca4e2a43592bc4123eca10ce064e522c254bb83079ba404327f6eafc01ec90a1444331fe769d3f3a7f90b0dde1",
|
||
|
Updates: []MembershipUpdate{
|
||
|
{
|
||
|
ChatID: "072ea460-84d3-53c5-9979-1ca36fb5d1020x0424a68f89ba5fcd5e0640c1e1f591d561fa4125ca4e2a43592bc4123eca10ce064e522c254bb83079ba404327f6eafc01ec90a1444331fe769d3f3a7f90b0dde1",
|
||
|
Signature: "7fca3d614cf55bc6cdf9c17fd1e65d1688673322bf1f004c58c78e0927edefea3d1053bf6a9d2e058ae88079f588105dccf2a2f9f330f6035cd47c715ee5950601",
|
||
|
Events: []MembershipUpdateEvent{
|
||
|
{
|
||
|
Type: MembershipUpdateChatCreated,
|
||
|
Name: "thathata",
|
||
|
ClockValue: 156897373998501,
|
||
|
},
|
||
|
{
|
||
|
Type: MembershipUpdateMembersAdded,
|
||
|
Members: []string{"0x04aebe2bb01a988abe7d978662f21de7760486119876c680e5a559e38e086a2df6dad41c4e4d9079c03db3bced6cb70fca76afc5650e50ea19b81572046a813534"},
|
||
|
ClockValue: 156897373998502,
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
Message: nil,
|
||
|
}
|
||
|
)
|
||
|
|
||
|
func TestDecodeMembershipUpdateMessage(t *testing.T) {
|
||
|
val, err := decodeTransitMessage(testMembershipUpdateMessageBytes)
|
||
|
require.NoError(t, err)
|
||
|
require.EqualValues(t, testMembershipUpdateMessageStruct, val)
|
||
|
}
|
||
|
|
||
|
func TestEncodeMembershipUpdateMessage(t *testing.T) {
|
||
|
data, err := EncodeMembershipUpdateMessage(testMembershipUpdateMessageStruct)
|
||
|
require.NoError(t, err)
|
||
|
// Decode it back to a struct and compare. Comparing bytes is not an option because,
|
||
|
// for example, map encoding is non-deterministic.
|
||
|
val, err := decodeTransitMessage(data)
|
||
|
require.NoError(t, err)
|
||
|
require.EqualValues(t, testMembershipUpdateMessageStruct, val)
|
||
|
}
|
||
|
|
||
|
func TestTupleMembershipUpdateEvent(t *testing.T) {
|
||
|
event1 := testMembershipUpdateMessageStruct.Updates[0].Events[0]
|
||
|
result1 := tupleMembershipUpdateEvent(event1)
|
||
|
require.EqualValues(t, [][]interface{}{
|
||
|
{"clock-value", event1.ClockValue},
|
||
|
{"name", "thathata"},
|
||
|
{"type", "chat-created"},
|
||
|
}, result1)
|
||
|
|
||
|
event2 := testMembershipUpdateMessageStruct.Updates[0].Events[1]
|
||
|
result2 := tupleMembershipUpdateEvent(event2)
|
||
|
require.EqualValues(t, [][]interface{}{
|
||
|
{"clock-value", event2.ClockValue},
|
||
|
{"members", event2.Members},
|
||
|
{"type", "members-added"},
|
||
|
}, result2)
|
||
|
}
|
||
|
|
||
|
func TestSignMembershipUpdate(t *testing.T) {
|
||
|
key, err := crypto.HexToECDSA("838fbdd1b670209a258b90af25653a018bc582c44c56e6290a973eebbeb15732")
|
||
|
require.NoError(t, err)
|
||
|
update := testMembershipUpdateMessageStruct.Updates[0]
|
||
|
err = update.Sign(key)
|
||
|
require.NoError(t, err)
|
||
|
expected, err := protocrypto.SignStringAsHex(
|
||
|
strings.Map(func(r rune) rune {
|
||
|
if unicode.IsSpace(r) {
|
||
|
return -1
|
||
|
}
|
||
|
return r
|
||
|
}, `
|
||
|
[
|
||
|
[
|
||
|
[
|
||
|
["clock-value", 156897373998501],
|
||
|
["name", "thathata"],
|
||
|
["type", "chat-created"]
|
||
|
],
|
||
|
[
|
||
|
["clock-value", 156897373998502],
|
||
|
["members", ["0x04aebe2bb01a988abe7d978662f21de7760486119876c680e5a559e38e086a2df6dad41c4e4d9079c03db3bced6cb70fca76afc5650e50ea19b81572046a813534"]],
|
||
|
["type", "members-added"]
|
||
|
]
|
||
|
],
|
||
|
"072ea460-84d3-53c5-9979-1ca36fb5d1020x0424a68f89ba5fcd5e0640c1e1f591d561fa4125ca4e2a43592bc4123eca10ce064e522c254bb83079ba404327f6eafc01ec90a1444331fe769d3f3a7f90b0dde1"
|
||
|
]
|
||
|
`),
|
||
|
key,
|
||
|
)
|
||
|
require.NoError(t, err)
|
||
|
require.Equal(t, expected, update.Signature)
|
||
|
}
|
||
|
|
||
|
func TestGroupCreator(t *testing.T) {
|
||
|
key, err := crypto.GenerateKey()
|
||
|
require.NoError(t, err)
|
||
|
g, err := NewGroupWithCreator("abc", key)
|
||
|
require.NoError(t, err)
|
||
|
creator, err := g.creator()
|
||
|
require.NoError(t, err)
|
||
|
require.Equal(t, publicKeyToString(&key.PublicKey), creator)
|
||
|
}
|
||
|
|
||
|
func TestGroupProcessEvent(t *testing.T) {
|
||
|
createGroup := func(admins, members []string, name string) Group {
|
||
|
return Group{
|
||
|
name: name,
|
||
|
admins: newStringSetFromSlice(admins),
|
||
|
members: newStringSetFromSlice(members),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
testCases := []struct {
|
||
|
Name string
|
||
|
Group Group
|
||
|
Result Group
|
||
|
From string
|
||
|
Event MembershipUpdateEvent
|
||
|
}{
|
||
|
{
|
||
|
Name: "chat-created event",
|
||
|
Group: createGroup(nil, nil, ""),
|
||
|
Result: createGroup([]string{"0xabc"}, []string{"0xabc"}, "some-name"),
|
||
|
From: "0xabc",
|
||
|
Event: NewChatCreatedEvent("some-name", "0xabc", 0),
|
||
|
},
|
||
|
{
|
||
|
Name: "name-changed event",
|
||
|
Group: createGroup(nil, nil, ""),
|
||
|
Result: createGroup(nil, nil, "some-name"),
|
||
|
From: "0xabc",
|
||
|
Event: NewNameChangedEvent("some-name", 0),
|
||
|
},
|
||
|
{
|
||
|
Name: "admins-added event",
|
||
|
Group: createGroup(nil, nil, ""),
|
||
|
Result: createGroup([]string{"0xabc", "0x123"}, nil, ""),
|
||
|
From: "0xabc",
|
||
|
Event: NewAdminsAddedEvent([]string{"0xabc", "0x123"}, 0),
|
||
|
},
|
||
|
{
|
||
|
Name: "admin-removed event",
|
||
|
Group: createGroup([]string{"0xabc", "0xdef"}, nil, ""),
|
||
|
Result: createGroup([]string{"0xdef"}, nil, ""),
|
||
|
From: "0xabc",
|
||
|
Event: NewAdminRemovedEvent("0xabc", 0),
|
||
|
},
|
||
|
{
|
||
|
Name: "members-added event",
|
||
|
Group: createGroup(nil, nil, ""),
|
||
|
Result: createGroup(nil, []string{"0xabc", "0xdef"}, ""),
|
||
|
From: "0xabc",
|
||
|
Event: NewMembersAddedEvent([]string{"0xabc", "0xdef"}, 0),
|
||
|
},
|
||
|
{
|
||
|
Name: "member-removed event",
|
||
|
Group: createGroup(nil, []string{"0xabc", "0xdef"}, ""),
|
||
|
Result: createGroup(nil, []string{"0xdef"}, ""),
|
||
|
From: "0xabc",
|
||
|
Event: NewMemberRemovedEvent("0xabc", 0),
|
||
|
},
|
||
|
{
|
||
|
Name: "member-joined event",
|
||
|
Group: createGroup(nil, []string{"0xabc"}, ""),
|
||
|
Result: createGroup(nil, []string{"0xabc", "0xdef"}, ""),
|
||
|
From: "0xabc",
|
||
|
Event: NewMemberJoinedEvent("0xdef", 0),
|
||
|
},
|
||
|
}
|
||
|
|
||
|
for _, tc := range testCases {
|
||
|
t.Run(tc.Name, func(t *testing.T) {
|
||
|
g := tc.Group
|
||
|
g.processEvent(tc.From, tc.Event)
|
||
|
require.EqualValues(t, tc.Result, g)
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestGroupValidateEvent(t *testing.T) {
|
||
|
createGroup := func(admins, members []string) Group {
|
||
|
return Group{
|
||
|
admins: newStringSetFromSlice(admins),
|
||
|
members: newStringSetFromSlice(members),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
testCases := []struct {
|
||
|
Name string
|
||
|
From string
|
||
|
Group Group
|
||
|
Event MembershipUpdateEvent
|
||
|
Result bool
|
||
|
}{
|
||
|
{
|
||
|
Name: "chat-created with empty admins and members",
|
||
|
Group: createGroup(nil, nil),
|
||
|
Event: NewChatCreatedEvent("test", "0xabc", 0),
|
||
|
Result: true,
|
||
|
},
|
||
|
{
|
||
|
Name: "chat-created with existing admins",
|
||
|
Group: createGroup([]string{"0xabc"}, nil),
|
||
|
Event: NewChatCreatedEvent("test", "0xabc", 0),
|
||
|
Result: false,
|
||
|
},
|
||
|
{
|
||
|
Name: "chat-created with existing members",
|
||
|
Group: createGroup(nil, []string{"0xabc"}),
|
||
|
Event: NewChatCreatedEvent("test", "0xabc", 0),
|
||
|
Result: false,
|
||
|
},
|
||
|
{
|
||
|
Name: "name-changed allowed because from is admin",
|
||
|
From: "0xabc",
|
||
|
Group: createGroup([]string{"0xabc"}, nil),
|
||
|
Event: NewNameChangedEvent("new-name", 0),
|
||
|
Result: true,
|
||
|
},
|
||
|
{
|
||
|
Name: "name-changed not allowed for non-admins",
|
||
|
From: "0xabc",
|
||
|
Group: createGroup(nil, nil),
|
||
|
Event: NewNameChangedEvent("new-name", 0),
|
||
|
Result: false,
|
||
|
},
|
||
|
{
|
||
|
Name: "members-added allowed because from is admin",
|
||
|
From: "0xabc",
|
||
|
Group: createGroup([]string{"0xabc"}, nil),
|
||
|
Event: NewMembersAddedEvent([]string{"0x123"}, 0),
|
||
|
Result: true,
|
||
|
},
|
||
|
{
|
||
|
Name: "members-added not allowed for non-admins",
|
||
|
From: "0xabc",
|
||
|
Group: createGroup(nil, nil),
|
||
|
Event: NewMembersAddedEvent([]string{"0x123"}, 0),
|
||
|
Result: false,
|
||
|
},
|
||
|
{
|
||
|
Name: "member-removed allowed because removing themselves",
|
||
|
From: "0xabc",
|
||
|
Group: createGroup(nil, nil),
|
||
|
Event: NewMemberRemovedEvent("0xabc", 0),
|
||
|
Result: true,
|
||
|
},
|
||
|
{
|
||
|
Name: "member-removed allowed because from is admin",
|
||
|
From: "0xabc",
|
||
|
Group: createGroup([]string{"0xabc"}, nil),
|
||
|
Event: NewMemberRemovedEvent("0x123", 0),
|
||
|
Result: true,
|
||
|
},
|
||
|
{
|
||
|
Name: "member-removed not allowed for non-admins",
|
||
|
From: "0xabc",
|
||
|
Group: createGroup(nil, nil),
|
||
|
Event: NewMemberRemovedEvent("0x123", 0),
|
||
|
Result: false,
|
||
|
},
|
||
|
{
|
||
|
Name: "member-joined must be in members",
|
||
|
From: "0xabc",
|
||
|
Group: createGroup(nil, []string{"0xabc"}),
|
||
|
Event: NewMemberJoinedEvent("0xabc", 0),
|
||
|
Result: true,
|
||
|
},
|
||
|
{
|
||
|
Name: "member-joined not valid because not in members",
|
||
|
From: "0xabc",
|
||
|
Group: createGroup(nil, nil),
|
||
|
Event: NewMemberJoinedEvent("0xabc", 0),
|
||
|
Result: false,
|
||
|
},
|
||
|
{
|
||
|
Name: "member-joined not valid because from differs from the event",
|
||
|
From: "0xdef",
|
||
|
Group: createGroup(nil, nil),
|
||
|
Event: NewMemberJoinedEvent("0xabc", 0),
|
||
|
Result: false,
|
||
|
},
|
||
|
{
|
||
|
Name: "admins-added allowed because originating from other admin",
|
||
|
From: "0xabc",
|
||
|
Group: createGroup([]string{"0xabc", "0x123"}, []string{"0xdef", "0xghi"}),
|
||
|
Event: NewAdminsAddedEvent([]string{"0xdef"}, 0),
|
||
|
Result: true,
|
||
|
},
|
||
|
{
|
||
|
Name: "admins-added not allowed because not from admin",
|
||
|
From: "0xabc",
|
||
|
Group: createGroup([]string{"0x123"}, []string{"0xdef", "0xghi"}),
|
||
|
Event: NewAdminsAddedEvent([]string{"0xdef"}, 0),
|
||
|
Result: false,
|
||
|
},
|
||
|
{
|
||
|
Name: "admins-added not allowed because not in members",
|
||
|
From: "0xabc",
|
||
|
Group: createGroup([]string{"0xabc", "0x123"}, []string{"0xghi"}),
|
||
|
Event: NewAdminsAddedEvent([]string{"0xdef"}, 0),
|
||
|
Result: false,
|
||
|
},
|
||
|
{
|
||
|
Name: "admin-removed allowed because is admin and removes themselves",
|
||
|
From: "0xabc",
|
||
|
Group: createGroup([]string{"0xabc"}, nil),
|
||
|
Event: NewAdminRemovedEvent("0xabc", 0),
|
||
|
Result: true,
|
||
|
},
|
||
|
{
|
||
|
Name: "admin-removed not allowed because not themselves",
|
||
|
From: "0xabc",
|
||
|
Group: createGroup([]string{"0xabc", "0xdef"}, nil),
|
||
|
Event: NewAdminRemovedEvent("0xdef", 0),
|
||
|
Result: false,
|
||
|
},
|
||
|
{
|
||
|
Name: "admin-removed not allowed because not admin",
|
||
|
From: "0xdef",
|
||
|
Group: createGroup([]string{"0xabc"}, nil),
|
||
|
Event: NewAdminRemovedEvent("0xabc", 0),
|
||
|
Result: false,
|
||
|
},
|
||
|
}
|
||
|
|
||
|
for _, tc := range testCases {
|
||
|
t.Run(tc.Name, func(t *testing.T) {
|
||
|
result := tc.Group.validateEvent(tc.From, tc.Event)
|
||
|
assert.Equal(t, tc.Result, result)
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestMembershipUpdateMessageProcess(t *testing.T) {
|
||
|
key, err := crypto.GenerateKey()
|
||
|
require.NoError(t, err)
|
||
|
updates := []MembershipUpdate{
|
||
|
{
|
||
|
ChatID: "some-chat",
|
||
|
Events: []MembershipUpdateEvent{
|
||
|
NewChatCreatedEvent("some-name", "0xabc", 0),
|
||
|
},
|
||
|
},
|
||
|
}
|
||
|
err = updates[0].Sign(key)
|
||
|
require.NoError(t, err)
|
||
|
require.NotEmpty(t, updates[0].Signature)
|
||
|
|
||
|
message := MembershipUpdateMessage{
|
||
|
ChatID: "some-chat",
|
||
|
Updates: updates,
|
||
|
}
|
||
|
err = message.Verify()
|
||
|
require.NoError(t, err)
|
||
|
require.EqualValues(t, "0x"+hex.EncodeToString(crypto.FromECDSAPub(&key.PublicKey)), updates[0].From)
|
||
|
}
|
||
|
|
||
|
func TestMembershipUpdateEventEqual(t *testing.T) {
|
||
|
u1 := MembershipUpdateEvent{
|
||
|
Type: MembershipUpdateChatCreated,
|
||
|
ClockValue: 1,
|
||
|
Member: "0xabc",
|
||
|
Members: []string{"0xabc"},
|
||
|
Name: "abc",
|
||
|
}
|
||
|
require.True(t, u1.Equal(u1))
|
||
|
|
||
|
// Verify equality breaking.
|
||
|
u2 := u1
|
||
|
u2.Members = append(u2.Members, "0xdef")
|
||
|
require.False(t, u1.Equal(u2))
|
||
|
u2 = u1
|
||
|
u2.Type = MembershipUpdateMembersAdded
|
||
|
require.False(t, u1.Equal(u2))
|
||
|
u2 = u1
|
||
|
u2.ClockValue = 2
|
||
|
require.False(t, u1.Equal(u2))
|
||
|
u2 = u1
|
||
|
u2.Member = "0xdef"
|
||
|
require.False(t, u1.Equal(u2))
|
||
|
u2 = u1
|
||
|
u2.Name = "def"
|
||
|
require.False(t, u1.Equal(u2))
|
||
|
}
|
||
|
|
||
|
func TestMembershipUpdateFlatEqual(t *testing.T) {
|
||
|
u1 := MembershipUpdateFlat{
|
||
|
ChatID: "abc",
|
||
|
Signature: "abc",
|
||
|
From: "0xabc",
|
||
|
}
|
||
|
require.True(t, u1.Equal(u1))
|
||
|
|
||
|
// Verify equality breaking.
|
||
|
u2 := u1
|
||
|
u2.ChatID = "def"
|
||
|
require.False(t, u1.Equal(u2))
|
||
|
u2 = u1
|
||
|
u2.Signature = "def"
|
||
|
require.False(t, u1.Equal(u2))
|
||
|
u2 = u1
|
||
|
u2.From = "0xdef"
|
||
|
require.False(t, u1.Equal(u2))
|
||
|
}
|
||
|
|
||
|
func TestMergeFlatMembershipUpdates(t *testing.T) {
|
||
|
u1 := []MembershipUpdateFlat{
|
||
|
{
|
||
|
ChatID: "abc",
|
||
|
Signature: "abc",
|
||
|
From: "0xabc",
|
||
|
},
|
||
|
}
|
||
|
u2 := []MembershipUpdateFlat{
|
||
|
{
|
||
|
ChatID: "abc",
|
||
|
Signature: "def",
|
||
|
From: "0xdef",
|
||
|
},
|
||
|
}
|
||
|
result := MergeFlatMembershipUpdates(u1, u1)
|
||
|
require.EqualValues(t, u1, result)
|
||
|
result = MergeFlatMembershipUpdates(u1, u2)
|
||
|
require.EqualValues(t, append(u1, u2...), result)
|
||
|
}
|