diff --git a/tests/frostfsid_test.go b/tests/frostfsid_test.go index 46ef33e..2ae6099 100644 --- a/tests/frostfsid_test.go +++ b/tests/frostfsid_test.go @@ -1,110 +1,715 @@ package tests import ( - "bytes" + "errors" "path" - "sort" "testing" - "git.frostfs.info/TrueCloudLab/frostfs-contract/container" + "git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client" + "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" + "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/nspcc-dev/neo-go/pkg/vm/vmstate" + "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" ) const frostfsidPath = "../frostfsid" -func deployFrostFSIDContract(t *testing.T, e *neotest.Executor, addrNetmap, addrContainer util.Uint160) util.Uint160 { - args := make([]any, 2) - args[0] = addrNetmap - args[1] = addrContainer +const ( + addOwnerMethod = "addOwner" + deleteOwnerMethod = "deleteOwner" + listOwnersMethod = "listOwners" + + createSubjectMethod = "createSubject" + getSubjectMethod = "getSubject" + listSubjectsMethod = "listSubjects" + addSubjectKeyMethod = "addSubjectKey" + removeSubjectKeyMethod = "removeSubjectKey" + getSubjectByKeyMethod = "getSubjectByKey" + getSubjectKeyByNameMethod = "getSubjectKeyByName" + setSubjectNameMethod = "setSubjectName" + setSubjectKVMethod = "setSubjectKV" + deleteSubjectKVMethod = "deleteSubjectKV" + deleteSubjectMethod = "deleteSubject" + + createNamespaceMethod = "createNamespace" + getNamespaceMethod = "getNamespace" + getNamespaceExtendedMethod = "getNamespaceExtended" + listNamespacesMethod = "listNamespaces" + addSubjectToNamespaceMethod = "addSubjectToNamespace" + removeSubjectFromNamespaceMethod = "removeSubjectFromNamespace" + listNamespaceSubjectsMethod = "listNamespaceSubjects" + + createGroupMethod = "createGroup" + getGroupMethod = "getGroup" + getGroupExtendedMethod = "getGroupExtended" + listGroupsMethod = "listGroups" + addSubjectToGroupMethod = "addSubjectToGroup" + removeSubjectFromGroupMethod = "removeSubjectFromGroup" + listGroupSubjectsMethod = "listGroupSubjects" + deleteGroupMethod = "deleteGroup" +) + +const notWitnessedError = "not witnessed" + +type testFrostFSIDInvoker struct { + e *neotest.Executor + contractHash util.Uint160 + owner *wallet.Account +} + +func (f *testFrostFSIDInvoker) OwnerInvoker() *neotest.ContractInvoker { + return f.e.NewInvoker(f.contractHash, neotest.NewSingleSigner(f.owner)) +} + +func (f *testFrostFSIDInvoker) CommitteeInvoker() *neotest.ContractInvoker { + return f.e.CommitteeInvoker(f.contractHash) +} + +func (f *testFrostFSIDInvoker) AnonInvoker(t *testing.T) *neotest.ContractInvoker { + acc, err := wallet.NewAccount() + require.NoError(t, err) + + return f.e.NewInvoker(f.contractHash, newSigner(t, f.CommitteeInvoker(), acc)) +} + +func newSigner(t *testing.T, c *neotest.ContractInvoker, acc *wallet.Account) neotest.Signer { + amount := int64(100_0000_0000) + + tx := c.NewTx(t, []neotest.Signer{c.Validator}, + c.NativeHash(t, nativenames.Gas), "transfer", + c.Validator.ScriptHash(), acc.Contract.ScriptHash(), amount, nil) + c.AddNewBlock(t, tx) + c.CheckHalt(t, tx.Hash()) + return neotest.NewSingleSigner(acc) +} + +func deployFrostFSIDContract(t *testing.T, e *neotest.Executor, contractOwner util.Uint160) util.Uint160 { + args := make([]any, 5) + args[0] = []any{contractOwner} c := neotest.CompileFile(t, e.CommitteeHash, frostfsidPath, path.Join(frostfsidPath, "config.yml")) e.DeployContract(t, c, args) return c.Hash } -func newFrostFSIDInvoker(t *testing.T) *neotest.ContractInvoker { +func newFrostFSIDInvoker(t *testing.T) *testFrostFSIDInvoker { e := newExecutor(t) - ctrNNS := neotest.CompileFile(t, e.CommitteeHash, nnsPath, path.Join(nnsPath, "config.yml")) - ctrNetmap := neotest.CompileFile(t, e.CommitteeHash, netmapPath, path.Join(netmapPath, "config.yml")) - ctrBalance := neotest.CompileFile(t, e.CommitteeHash, balancePath, path.Join(balancePath, "config.yml")) - ctrContainer := neotest.CompileFile(t, e.CommitteeHash, containerPath, path.Join(containerPath, "config.yml")) + acc, err := wallet.NewAccount() + require.NoError(t, err) - e.DeployContract(t, ctrNNS, nil) - deployNetmapContract(t, e, ctrBalance.Hash, ctrContainer.Hash, - container.RegistrationFeeKey, int64(containerFee), - container.AliasFeeKey, int64(containerAliasFee)) - deployBalanceContract(t, e, ctrNetmap.Hash, ctrContainer.Hash) - deployContainerContract(t, e, ctrNetmap.Hash, ctrBalance.Hash, ctrNNS.Hash) - h := deployFrostFSIDContract(t, e, ctrNetmap.Hash, ctrContainer.Hash) - return e.CommitteeInvoker(h) + h := deployFrostFSIDContract(t, e, acc.ScriptHash()) + + newSigner(t, e.CommitteeInvoker(h), acc) + + return &testFrostFSIDInvoker{ + e: e, + contractHash: h, + owner: acc, + } } -func TestFrostFSID_AddKey(t *testing.T) { - e := newFrostFSIDInvoker(t) +func TestFrostFSID_ContractOwnersManagement(t *testing.T) { + f := newFrostFSIDInvoker(t) - pubs := make([][]byte, 6) - for i := range pubs { - p, err := keys.NewPrivateKey() + anonInvoker := f.AnonInvoker(t) + anonInvokerHash := anonInvoker.Signers[0].ScriptHash() + invoker := f.OwnerInvoker() + invokerHash := invoker.Signers[0].ScriptHash() + committeeInvoker := f.CommitteeInvoker() + + checkListOwners(t, anonInvoker, invokerHash) + + anonInvoker.InvokeFail(t, notWitnessedError, createNamespaceMethod, "namespace") + invoker.Invoke(t, stackitem.Null{}, createNamespaceMethod, "namespace") + + invoker.InvokeFail(t, notWitnessedError, addOwnerMethod, anonInvokerHash) + committeeInvoker.Invoke(t, stackitem.Null{}, addOwnerMethod, anonInvokerHash) + anonInvoker.Invoke(t, stackitem.Null{}, createNamespaceMethod, "namespace2") + + checkListOwners(t, anonInvoker, invokerHash, anonInvokerHash) + + anonInvoker.InvokeFail(t, notWitnessedError, deleteOwnerMethod, anonInvokerHash) + committeeInvoker.Invoke(t, stackitem.Null{}, deleteOwnerMethod, anonInvokerHash) + anonInvoker.InvokeFail(t, notWitnessedError, createNamespaceMethod, "namespace3") + + checkListOwners(t, anonInvoker, invokerHash) +} + +func checkListOwners(t *testing.T, invoker *neotest.ContractInvoker, expectedAddresses ...util.Uint160) { + s, err := invoker.TestInvoke(t, listOwnersMethod) + require.NoError(t, err) + addresses, err := unwrap.ArrayOfUint160(makeValidRes(stackitem.NewArray(readIteratorAll(s))), nil) + require.NoError(t, err) + require.ElementsMatch(t, addresses, expectedAddresses) +} + +func TestFrostFSID_SubjectManagement(t *testing.T) { + f := newFrostFSIDInvoker(t) + + subjKey, err := keys.NewPrivateKey() + require.NoError(t, err) + subjKeyAddr := subjKey.PublicKey().GetScriptHash() + + anonInvoker := f.AnonInvoker(t) + invoker := f.OwnerInvoker() + + anonInvoker.InvokeFail(t, notWitnessedError, createSubjectMethod, subjKey.PublicKey().Bytes()) + invoker.Invoke(t, stackitem.Null{}, createSubjectMethod, subjKey.PublicKey().Bytes()) + invoker.InvokeFail(t, "already exists", createSubjectMethod, subjKey.PublicKey().Bytes()) + + s, err := anonInvoker.TestInvoke(t, getSubjectMethod, subjKeyAddr) + require.NoError(t, err) + + subj := parseSubject(t, s) + require.True(t, subjKey.PublicKey().Equal(&subj.PrimaryKey)) + + t.Run("add subject key", func(t *testing.T) { + subjNewKey, err := keys.NewPrivateKey() require.NoError(t, err) - pubs[i] = p.PublicKey().Bytes() - } - acc := e.NewAccount(t) - owner := signerToOwner(acc) - e.Invoke(t, stackitem.Null{}, "addKey", owner, - []any{pubs[0], pubs[1]}) - sort.Slice(pubs[:2], func(i, j int) bool { - return bytes.Compare(pubs[i], pubs[j]) == -1 - }) - arr := []stackitem.Item{ - stackitem.NewBuffer(pubs[0]), - stackitem.NewBuffer(pubs[1]), - } - e.Invoke(t, stackitem.NewArray(arr), "key", owner) + anonInvoker.InvokeFail(t, notWitnessedError, addSubjectKeyMethod, subjKeyAddr, subjNewKey.PublicKey().Bytes()) + invoker.Invoke(t, stackitem.Null{}, addSubjectKeyMethod, subjKeyAddr, subjNewKey.PublicKey().Bytes()) - t.Run("multiple addKey per block", func(t *testing.T) { - tx1 := e.PrepareInvoke(t, "addKey", owner, []any{pubs[2]}) - tx2 := e.PrepareInvoke(t, "addKey", owner, []any{pubs[3], pubs[4]}) - e.AddNewBlock(t, tx1, tx2) - e.CheckHalt(t, tx1.Hash(), stackitem.Null{}) - e.CheckHalt(t, tx2.Hash(), stackitem.Null{}) + s, err = anonInvoker.TestInvoke(t, getSubjectMethod, subjKeyAddr) + require.NoError(t, err) + subj := parseSubject(t, s) + require.Len(t, subj.AdditionalKeys, 1) + require.True(t, subjNewKey.PublicKey().Equal(subj.AdditionalKeys[0])) - sort.Slice(pubs[:5], func(i, j int) bool { - return bytes.Compare(pubs[i], pubs[j]) == -1 + t.Run("get subject by additional key", func(t *testing.T) { + s, err = anonInvoker.TestInvoke(t, getSubjectByKeyMethod, subjNewKey.PublicKey().Bytes()) + require.NoError(t, err) + subj := parseSubject(t, s) + require.True(t, subjKey.PublicKey().Equal(&subj.PrimaryKey), "keys must be the same") + + s, err = anonInvoker.TestInvoke(t, getSubjectByKeyMethod, subjKey.PublicKey().Bytes()) + require.NoError(t, err) + subj = parseSubject(t, s) + require.True(t, subjKey.PublicKey().Equal(&subj.PrimaryKey), "keys must be the same") + + t.Run("remove subject key", func(t *testing.T) { + anonInvoker.InvokeFail(t, notWitnessedError, removeSubjectKeyMethod, subjKeyAddr, subjNewKey.PublicKey().Bytes()) + invoker.Invoke(t, stackitem.Null{}, removeSubjectKeyMethod, subjKeyAddr, subjNewKey.PublicKey().Bytes()) + + anonInvoker.InvokeFail(t, "not found", getSubjectByKeyMethod, subjNewKey.PublicKey().Bytes()) + }) }) - arr = []stackitem.Item{ - stackitem.NewBuffer(pubs[0]), - stackitem.NewBuffer(pubs[1]), - stackitem.NewBuffer(pubs[2]), - stackitem.NewBuffer(pubs[3]), - stackitem.NewBuffer(pubs[4]), - } - e.Invoke(t, stackitem.NewArray(arr), "key", owner) }) - e.Invoke(t, stackitem.Null{}, "removeKey", owner, - []any{pubs[1], pubs[5]}) - arr = []stackitem.Item{ - stackitem.NewBuffer(pubs[0]), - stackitem.NewBuffer(pubs[2]), - stackitem.NewBuffer(pubs[3]), - stackitem.NewBuffer(pubs[4]), - } - e.Invoke(t, stackitem.NewArray(arr), "key", owner) + t.Run("set subject name", func(t *testing.T) { + login := "some-login" - t.Run("multiple removeKey per block", func(t *testing.T) { - tx1 := e.PrepareInvoke(t, "removeKey", owner, []any{pubs[2]}) - tx2 := e.PrepareInvoke(t, "removeKey", owner, []any{pubs[0], pubs[4]}) - e.AddNewBlock(t, tx1, tx2) - e.CheckHalt(t, tx1.Hash(), stackitem.Null{}) - e.CheckHalt(t, tx2.Hash(), stackitem.Null{}) + anonInvoker.InvokeFail(t, notWitnessedError, setSubjectNameMethod, subjKeyAddr, login) + invoker.Invoke(t, stackitem.Null{}, setSubjectNameMethod, subjKeyAddr, login) - arr = []stackitem.Item{stackitem.NewBuffer(pubs[3])} - e.Invoke(t, stackitem.NewArray(arr), "key", owner) + s, err = anonInvoker.TestInvoke(t, getSubjectMethod, subjKeyAddr) + require.NoError(t, err) + subj = parseSubject(t, s) + require.Equal(t, login, subj.Name) + }) + + t.Run("set subject KVs", func(t *testing.T) { + iamPath := "iam/path" + + anonInvoker.InvokeFail(t, notWitnessedError, setSubjectKVMethod, subjKeyAddr, client.SubjectIAMPathKey, iamPath) + invoker.Invoke(t, stackitem.Null{}, setSubjectKVMethod, subjKeyAddr, client.SubjectIAMPathKey, iamPath) + + s, err = anonInvoker.TestInvoke(t, getSubjectMethod, subjKeyAddr) + require.NoError(t, err) + subj = parseSubject(t, s) + require.Equal(t, iamPath, subj.KV[client.SubjectIAMPathKey]) + + anonInvoker.InvokeFail(t, notWitnessedError, deleteSubjectKVMethod, subjKeyAddr, client.SubjectIAMPathKey) + invoker.Invoke(t, stackitem.Null{}, deleteSubjectKVMethod, subjKeyAddr, client.SubjectIAMPathKey) + + s, err = anonInvoker.TestInvoke(t, getSubjectMethod, subjKeyAddr) + require.NoError(t, err) + subj = parseSubject(t, s) + require.Empty(t, subj.KV) + }) + + t.Run("list subjects", func(t *testing.T) { + newSubjKey, err := keys.NewPrivateKey() + require.NoError(t, err) + + invoker.Invoke(t, stackitem.Null{}, createSubjectMethod, newSubjKey.PublicKey().Bytes()) + + s, err = anonInvoker.TestInvoke(t, listSubjectsMethod) + require.NoError(t, err) + + addresses, err := unwrap.ArrayOfUint160(makeValidRes(stackitem.NewArray(readIteratorAll(s))), nil) + require.NoError(t, err) + require.Len(t, addresses, 2) + require.ElementsMatch(t, addresses, []util.Uint160{subjKeyAddr, newSubjKey.PublicKey().GetScriptHash()}) + + }) + + anonInvoker.InvokeFail(t, notWitnessedError, deleteSubjectMethod, subjKeyAddr) + invoker.Invoke(t, stackitem.Null{}, deleteSubjectMethod, subjKeyAddr) + + anonInvoker.InvokeFail(t, "subject not found", getSubjectMethod, subjKeyAddr) +} + +func TestFrostFSIS_SubjectNameRelatedInvariants(t *testing.T) { + f := newFrostFSIDInvoker(t) + + subjName1 := "subj1" + subjKey1, err := keys.NewPrivateKey() + require.NoError(t, err) + subjKeyAddr1 := subjKey1.PublicKey().GetScriptHash() + + subjName2 := "subj2" + subjKey2, err := keys.NewPrivateKey() + require.NoError(t, err) + subjKeyAddr2 := subjKey2.PublicKey().GetScriptHash() + + invoker := f.OwnerInvoker() + + ns1, ns2 := "ns1", "ns2" + + // Create two subject (one of them with name) + // Create two namespace. + // Add these subjects to ns1 + invoker.Invoke(t, stackitem.Null{}, createSubjectMethod, subjKey1.PublicKey().Bytes()) + invoker.Invoke(t, stackitem.Null{}, setSubjectNameMethod, subjKeyAddr1, subjName1) + invoker.Invoke(t, stackitem.Null{}, createSubjectMethod, subjKey2.PublicKey().Bytes()) + invoker.Invoke(t, stackitem.Null{}, createNamespaceMethod, ns1) + invoker.Invoke(t, stackitem.Null{}, createNamespaceMethod, ns2) + invoker.Invoke(t, stackitem.Null{}, addSubjectToNamespaceMethod, subjKeyAddr1, ns1) + invoker.Invoke(t, stackitem.Null{}, addSubjectToNamespaceMethod, subjKeyAddr2, ns1) + + // Check that we can find public key by name for subj1 (with name) + // and cannot find key for subj2 (without name) + s, err := invoker.TestInvoke(t, getSubjectKeyByNameMethod, ns1, subjName1) + checkPublicKeyResult(t, s, err, subjKey1) + s, err = invoker.TestInvoke(t, getSubjectKeyByNameMethod, ns1, subjName2) + checkPublicKeyResult(t, s, err, nil) + + // Check that we can find public key for by name for subj2 when we set its name + invoker.Invoke(t, stackitem.Null{}, setSubjectNameMethod, subjKeyAddr2, subjName2) + s, err = invoker.TestInvoke(t, getSubjectKeyByNameMethod, ns1, subjName2) + checkPublicKeyResult(t, s, err, subjKey2) + + // Check that we cannot set for second subject name that the first subject has already taken + invoker.InvokeFail(t, "not available", setSubjectNameMethod, subjKeyAddr2, subjName1) + + // Check that we cannot move subject from one namespace to another + invoker.InvokeFail(t, "cannot be moved", addSubjectToNamespaceMethod, subjKeyAddr2, ns2) + + // Check that we cannot find public key by name for subject that was removed from namespace + invoker.Invoke(t, stackitem.Null{}, removeSubjectFromNamespaceMethod, subjKeyAddr2) + s, err = invoker.TestInvoke(t, getSubjectKeyByNameMethod, ns1, subjName2) + checkPublicKeyResult(t, s, err, nil) + + // Check that we can find public key by name for subject in new namespace + invoker.Invoke(t, stackitem.Null{}, addSubjectToNamespaceMethod, subjKeyAddr2, ns2) + s, err = invoker.TestInvoke(t, getSubjectKeyByNameMethod, ns2, subjName2) + checkPublicKeyResult(t, s, err, subjKey2) + + // Check that subj2 can have the same name as subj1 if they belong to different namespaces + // Also check that after subject renaming its key cannot be found by old name + invoker.Invoke(t, stackitem.Null{}, setSubjectNameMethod, subjKeyAddr2, subjName1) + s, err = invoker.TestInvoke(t, getSubjectKeyByNameMethod, ns2, subjName1) + checkPublicKeyResult(t, s, err, subjKey2) + s, err = invoker.TestInvoke(t, getSubjectKeyByNameMethod, ns2, subjName2) + checkPublicKeyResult(t, s, err, nil) +} + +func TestFrostFSID_NamespaceManagement(t *testing.T) { + f := newFrostFSIDInvoker(t) + + anonInvoker := f.AnonInvoker(t) + invoker := f.OwnerInvoker() + + namespace := "some-namespace" + + anonInvoker.InvokeFail(t, notWitnessedError, createNamespaceMethod, namespace) + invoker.Invoke(t, stackitem.Null{}, createNamespaceMethod, namespace) + invoker.InvokeFail(t, "already exists", createNamespaceMethod, namespace) + + s, err := anonInvoker.TestInvoke(t, getNamespaceMethod, namespace) + require.NoError(t, err) + + ns := parseNamespace(t, s.Pop().Item()) + require.Equal(t, namespace, ns.Name) + + t.Run("add user to namespace", func(t *testing.T) { + subjKey, err := keys.NewPrivateKey() + require.NoError(t, err) + invoker.Invoke(t, stackitem.Null{}, createSubjectMethod, subjKey.PublicKey().Bytes()) + + subjName := "name" + subjAddress := subjKey.PublicKey().GetScriptHash() + invoker.Invoke(t, stackitem.Null{}, setSubjectNameMethod, subjAddress, subjName) + + anonInvoker.InvokeFail(t, notWitnessedError, addSubjectToNamespaceMethod, subjAddress, namespace) + invoker.Invoke(t, stackitem.Null{}, addSubjectToNamespaceMethod, subjAddress, namespace) + invoker.InvokeFail(t, "already added", addSubjectToNamespaceMethod, subjAddress, namespace) + + s, err := anonInvoker.TestInvoke(t, getSubjectMethod, subjAddress) + require.NoError(t, err) + subj := parseSubject(t, s) + require.Equal(t, namespace, subj.Namespace) + + t.Run("list namespace subjects", func(t *testing.T) { + s, err := anonInvoker.TestInvoke(t, listNamespaceSubjectsMethod, namespace) + require.NoError(t, err) + + addresses, err := unwrap.ArrayOfUint160(makeValidRes(stackitem.NewArray(readIteratorAll(s))), nil) + require.NoError(t, err) + require.ElementsMatch(t, addresses, []util.Uint160{subjAddress}) + }) + + t.Run("get subject key by name", func(t *testing.T) { + s, err := anonInvoker.TestInvoke(t, getSubjectKeyByNameMethod, namespace, subjName) + require.NoError(t, err) + + foundKey, err := unwrap.PublicKey(makeValidRes(s.Pop().Item()), nil) + require.NoError(t, err) + require.Equal(t, subjKey.PublicKey(), foundKey) + }) + + t.Run("list namespaces", func(t *testing.T) { + namespace2 := "some-namespace2" + invoker.Invoke(t, stackitem.Null{}, createNamespaceMethod, namespace2) + + s, err := anonInvoker.TestInvoke(t, listNamespacesMethod) + require.NoError(t, err) + + namespaces := parseNamespaces(t, readIteratorAll(s)) + require.NoError(t, err) + require.ElementsMatch(t, namespaces, []Namespace{{Name: namespace}, {Name: namespace2}}) + + t.Run("find namespaces with some subjects", func(t *testing.T) { + for _, ns := range namespaces { + s, err := anonInvoker.TestInvoke(t, getNamespaceExtendedMethod, ns.Name) + require.NoError(t, err) + + nsExt := parseNamespaceExtended(t, s.Pop().Item()) + if nsExt.SubjectsCount > 0 { + require.Equal(t, namespace, nsExt.Name) + } + } + + t.Run("remove subject from namespace", func(t *testing.T) { + anonInvoker.InvokeFail(t, notWitnessedError, removeSubjectFromNamespaceMethod, subjAddress) + invoker.Invoke(t, stackitem.Null{}, removeSubjectFromNamespaceMethod, subjAddress) + + s, err := anonInvoker.TestInvoke(t, getSubjectMethod, subjAddress) + require.NoError(t, err) + subj := parseSubject(t, s) + require.Empty(t, subj.Namespace) + + s, err = anonInvoker.TestInvoke(t, getNamespaceExtendedMethod, namespace) + require.NoError(t, err) + nsExt := parseNamespaceExtended(t, s.Pop().Item()) + require.Zero(t, nsExt.SubjectsCount) + }) + }) + }) }) } + +func TestFrostFSID_GroupManagement(t *testing.T) { + f := newFrostFSIDInvoker(t) + + anonInvoker := f.AnonInvoker(t) + invoker := f.OwnerInvoker() + + nsName := "namespace" + invoker.Invoke(t, stackitem.Null{}, createNamespaceMethod, nsName) + + groupName := "group" + anonInvoker.InvokeFail(t, notWitnessedError, createGroupMethod, nsName, groupName) + invoker.Invoke(t, stackitem.Null{}, createGroupMethod, nsName, groupName) + + s, err := anonInvoker.TestInvoke(t, getGroupMethod, nsName, groupName) + require.NoError(t, err) + group := parseGroup(t, s.Pop().Item()) + require.Equal(t, groupName, group.Name) + + s, err = anonInvoker.TestInvoke(t, listGroupsMethod, nsName) + require.NoError(t, err) + groups := parseGroups(t, readIteratorAll(s)) + require.ElementsMatch(t, groups, []Group{{Name: groupName, Namespace: nsName}}) + + t.Run("add subjects to group", func(t *testing.T) { + subjKey, err := keys.NewPrivateKey() + require.NoError(t, err) + invoker.Invoke(t, stackitem.Null{}, createSubjectMethod, subjKey.PublicKey().Bytes()) + + subjAddress := subjKey.PublicKey().GetScriptHash() + invoker.Invoke(t, stackitem.Null{}, addSubjectToNamespaceMethod, subjAddress, nsName) + anonInvoker.InvokeFail(t, "not witnessed", addSubjectToGroupMethod, subjAddress, groupName) + invoker.Invoke(t, stackitem.Null{}, addSubjectToGroupMethod, subjAddress, groupName) + + t.Run("list group subjects", func(t *testing.T) { + s, err = anonInvoker.TestInvoke(t, listGroupSubjectsMethod, nsName, groupName) + require.NoError(t, err) + + addresses, err := unwrap.ArrayOfUint160(makeValidRes(stackitem.NewArray(readIteratorAll(s))), nil) + require.NoError(t, err) + require.ElementsMatch(t, addresses, []util.Uint160{subjAddress}) + + anonInvoker.InvokeFail(t, "not witnessed", removeSubjectFromGroupMethod, subjAddress, nsName, groupName) + invoker.Invoke(t, stackitem.Null{}, removeSubjectFromGroupMethod, subjAddress, nsName, groupName) + + s, err = anonInvoker.TestInvoke(t, listGroupSubjectsMethod, nsName, groupName) + require.NoError(t, err) + + addresses, err = unwrap.ArrayOfUint160(makeValidRes(stackitem.NewArray(readIteratorAll(s))), nil) + require.NoError(t, err) + require.Empty(t, addresses) + + t.Run("get group extended", func(t *testing.T) { + subjectsCount := 10 + for i := 0; i < subjectsCount; i++ { + subjKey, err := keys.NewPrivateKey() + require.NoError(t, err) + invoker.Invoke(t, stackitem.Null{}, createSubjectMethod, subjKey.PublicKey().Bytes()) + invoker.Invoke(t, stackitem.Null{}, addSubjectToNamespaceMethod, subjKey.PublicKey().GetScriptHash(), nsName) + invoker.Invoke(t, stackitem.Null{}, addSubjectToGroupMethod, subjKey.PublicKey().GetScriptHash(), groupName) + } + + s, err = anonInvoker.TestInvoke(t, getGroupExtendedMethod, nsName, groupName) + require.NoError(t, err) + groupExt := parseGroupExtended(t, s.Pop().Item()) + require.Equal(t, groupName, groupExt.Name) + require.EqualValues(t, subjectsCount, groupExt.SubjectsCount) + }) + }) + }) + + t.Run("delete group", func(t *testing.T) { + anonInvoker.InvokeFail(t, notWitnessedError, deleteGroupMethod, nsName, groupName) + invoker.Invoke(t, stackitem.Null{}, deleteGroupMethod, nsName, groupName) + + anonInvoker.InvokeFail(t, "group not found", getGroupMethod, nsName, groupName) + + s, err = anonInvoker.TestInvoke(t, listGroupsMethod, nsName) + require.NoError(t, err) + groups := parseGroups(t, readIteratorAll(s)) + require.Empty(t, groups) + }) +} + +func checkPublicKeyResult(t *testing.T, s *vm.Stack, err error, key *keys.PrivateKey) { + if key == nil { + require.ErrorContains(t, err, "not found") + return + } + + require.NoError(t, err) + foundKey, err := unwrap.PublicKey(makeValidRes(s.Pop().Item()), nil) + require.NoError(t, err) + require.Equal(t, key.PublicKey(), foundKey) +} + +func readIteratorAll(s *vm.Stack) []stackitem.Item { + iter := s.Pop().Value().(*storage.Iterator) + + stackItems := make([]stackitem.Item, 0) + for iter.Next() { + stackItems = append(stackItems, iter.Value()) + } + + return stackItems +} + +type Subject struct { + PrimaryKey keys.PublicKey + AdditionalKeys keys.PublicKeys + Namespace string + Name string + KV map[string]string +} + +func parseSubject(t *testing.T, s *vm.Stack) Subject { + var subj Subject + + subjStruct := s.Pop().Array() + require.Len(t, subjStruct, 5) + + pkBytes, err := subjStruct[0].TryBytes() + require.NoError(t, err) + err = subj.PrimaryKey.DecodeBytes(pkBytes) + require.NoError(t, err) + + if !subjStruct[1].Equals(stackitem.Null{}) { + subj.AdditionalKeys, err = unwrap.ArrayOfPublicKeys(makeValidRes(subjStruct[1]), nil) + require.NoError(t, err) + } + + nsBytes, err := subjStruct[2].TryBytes() + require.NoError(t, err) + subj.Namespace = string(nsBytes) + + nameBytes, err := subjStruct[3].TryBytes() + require.NoError(t, err) + subj.Name = string(nameBytes) + + subj.KV, err = parseMap(subjStruct[4]) + require.NoError(t, err) + + return subj +} + +func parseMap(item stackitem.Item) (map[string]string, error) { + if item.Equals(stackitem.Null{}) { + return nil, nil + } + + metaMap, err := unwrap.Map(makeValidRes(item), nil) + if err != nil { + return nil, err + } + + meta, ok := metaMap.Value().([]stackitem.MapElement) + if !ok { + return nil, errors.New("invalid map type") + } + + res := make(map[string]string, len(meta)) + for _, element := range meta { + key, err := element.Key.TryBytes() + if err != nil { + return nil, err + } + val, err := element.Value.TryBytes() + if err != nil { + return nil, err + } + res[string(key)] = string(val) + } + + return res, nil +} + +type Namespace struct { + Name string +} + +type NamespaceExtended struct { + Name string + SubjectsCount int64 + GroupsCount int64 +} + +func parseNamespace(t *testing.T, item stackitem.Item) Namespace { + var ns Namespace + + subjStruct := item.Value().([]stackitem.Item) + require.Len(t, subjStruct, 1) + + nameBytes, err := subjStruct[0].TryBytes() + require.NoError(t, err) + ns.Name = string(nameBytes) + + return ns +} + +func parseNamespaceExtended(t *testing.T, item stackitem.Item) NamespaceExtended { + var ns NamespaceExtended + + subjStruct := item.Value().([]stackitem.Item) + require.Len(t, subjStruct, 3) + + nameBytes, err := subjStruct[0].TryBytes() + require.NoError(t, err) + ns.Name = string(nameBytes) + + groupCountInt, err := subjStruct[1].TryInteger() + require.NoError(t, err) + ns.GroupsCount = groupCountInt.Int64() + + subjectsCountInt, err := subjStruct[2].TryInteger() + require.NoError(t, err) + ns.SubjectsCount = subjectsCountInt.Int64() + + return ns +} + +func parseNamespaces(t *testing.T, items []stackitem.Item) []Namespace { + res := make([]Namespace, len(items)) + + for i := 0; i < len(items); i++ { + res[i] = parseNamespace(t, items[i]) + } + + return res +} + +type Group struct { + Name string + Namespace string +} + +type GroupExtended struct { + Name string + Namespace string + SubjectsCount int64 +} + +func parseGroup(t *testing.T, item stackitem.Item) Group { + var group Group + + subjStruct := item.Value().([]stackitem.Item) + require.Len(t, subjStruct, 2) + + nameBytes, err := subjStruct[0].TryBytes() + require.NoError(t, err) + group.Name = string(nameBytes) + + namespaceBytes, err := subjStruct[1].TryBytes() + require.NoError(t, err) + group.Namespace = string(namespaceBytes) + + return group +} + +func parseGroupExtended(t *testing.T, item stackitem.Item) GroupExtended { + var gr GroupExtended + + subjStruct := item.Value().([]stackitem.Item) + require.Len(t, subjStruct, 3) + + nameBytes, err := subjStruct[0].TryBytes() + require.NoError(t, err) + gr.Name = string(nameBytes) + + namespaceBytes, err := subjStruct[1].TryBytes() + require.NoError(t, err) + gr.Namespace = string(namespaceBytes) + + subjectsCountInt, err := subjStruct[2].TryInteger() + require.NoError(t, err) + gr.SubjectsCount = subjectsCountInt.Int64() + + return gr +} + +func parseGroups(t *testing.T, items []stackitem.Item) []Group { + res := make([]Group, len(items)) + + for i := 0; i < len(items); i++ { + res[i] = parseGroup(t, items[i]) + } + + return res +} + +func makeValidRes(item stackitem.Item) *result.Invoke { + return &result.Invoke{ + Stack: []stackitem.Item{item}, + State: vmstate.Halt.String(), + } +}