package tests import ( "context" "fmt" "os" "strconv" "strings" "testing" "git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/rpcclient" "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/stackitem" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" ) type testFrostFSIDClientInvoker struct { base *testFrostFSIDInvoker cli *client.Client a awaiter } func initFrostfsIFClientTest(t *testing.T) (clientInvoker *testFrostFSIDClientInvoker, cancelFn func()) { f := newFrostFSIDInvoker(t) wlt := initTmpWallet(t) ctx, cancel := context.WithCancel(context.Background()) address := runRPC(ctx, t, f.e.Chain, wlt) cli, rpc := frostfsidRPCClient(t, address, f.contractHash, f.owner) clientInvoker = &testFrostFSIDClientInvoker{ base: f, cli: cli, a: awaiter{ ctx: ctx, t: t, rpc: rpc, }, } return clientInvoker, func() { cancel() err := os.Remove(wlt) require.NoError(t, err) } } func frostfsidRPCClient(t *testing.T, address string, contractHash util.Uint160, accs ...*wallet.Account) (*client.Client, *rpcclient.Client) { rpcCli, err := rpcclient.New(context.Background(), "http://"+address, rpcclient.Options{}) require.NoError(t, err) var acc *wallet.Account if len(accs) == 0 { acc, err = wallet.NewAccount() require.NoError(t, err) } else { require.Len(t, accs, 1) acc = accs[0] } cli, err := client.New(rpcCli, acc, contractHash, client.Options{}) require.NoError(t, err) return cli, rpcCli } func TestFrostFSID_Client_ContractOwnersManagement(t *testing.T) { ffsid, cancel := initFrostfsIFClientTest(t) defer cancel() committeeInvoker := ffsid.base.CommitteeInvoker() defaultOwnerAddress := ffsid.base.owner.ScriptHash() _, newOwnerAddress := newKey(t) checkAdminClient(t, ffsid.cli, defaultOwnerAddress) _, _, err := ffsid.cli.SetAdmin(newOwnerAddress) require.ErrorContains(t, err, "not witnessed") committeeInvoker.Invoke(t, stackitem.Null{}, setAdminMethod, newOwnerAddress) checkAdminClient(t, ffsid.cli, newOwnerAddress) _, _, err = ffsid.cli.ClearAdmin() require.ErrorContains(t, err, "not witnessed") committeeInvoker.Invoke(t, stackitem.Null{}, clearAdminMethod) checkAdminClient(t, ffsid.cli) } func newKey(t *testing.T) (*keys.PrivateKey, util.Uint160) { key, err := keys.NewPrivateKey() require.NoError(t, err) return key, key.PublicKey().GetScriptHash() } func checkAdminClient(t *testing.T, cli *client.Client, owners ...util.Uint160) { address, isSet, err := cli.GetAdmin() require.NoError(t, err) require.Equal(t, len(owners) > 0, isSet) if isSet { require.Equal(t, owners[0], address) } } func TestFrostFSID_Client_SubjectManagement(t *testing.T) { ffsid, cancel := initFrostfsIFClientTest(t) defer cancel() subjKey, subjAddr := newKey(t) extraKey, _ := newKey(t) subjLogin := "subj-login" iamPathKV := "iam/path" ffsid.a.await(ffsid.cli.CreateSubject(defaultNamespace, subjKey.PublicKey())) ffsid.a.await(ffsid.cli.SetSubjectName(subjAddr, subjLogin)) ffsid.a.await(ffsid.cli.SetSubjectKV(subjAddr, client.IAMPathKey, iamPathKV)) ffsid.a.await(ffsid.cli.AddSubjectKey(subjAddr, extraKey.PublicKey())) subj, err := ffsid.cli.GetSubject(subjAddr) require.NoError(t, err) require.True(t, subjKey.PublicKey().Equal(subj.PrimaryKey)) require.Equal(t, subj.Name, subjLogin) require.Equal(t, map[string]string{client.IAMPathKey: iamPathKV}, subj.KV) require.ElementsMatch(t, []*keys.PublicKey{extraKey.PublicKey()}, subj.AdditionalKeys) ffsid.a.await(ffsid.cli.DeleteSubjectKV(subjAddr, client.IAMPathKey)) subj, err = ffsid.cli.GetSubject(subjAddr) require.NoError(t, err) require.Empty(t, subj.KV) subjects, err := ffsid.cli.ListSubjects() require.NoError(t, err) require.ElementsMatch(t, subjects, []util.Uint160{subjAddr}) subj, err = ffsid.cli.GetSubjectByKey(extraKey.PublicKey()) require.NoError(t, err) require.True(t, subjKey.PublicKey().Equal(subj.PrimaryKey)) ffsid.a.await(ffsid.cli.RemoveSubjectKey(subjAddr, extraKey.PublicKey())) _, err = ffsid.cli.GetSubjectByKey(extraKey.PublicKey()) require.ErrorContains(t, err, "not found") ffsid.a.await(ffsid.cli.DeleteSubject(subjAddr)) _, err = ffsid.cli.GetSubject(subjAddr) require.ErrorContains(t, err, "not found") subjects, err = ffsid.cli.ListSubjects() require.NoError(t, err) require.Empty(t, subjects) } func TestFrostFSID_Client_NamespaceManagement(t *testing.T) { ffsid, cancel := initFrostfsIFClientTest(t) defer cancel() namespace := "namespace" subjKey, subjAddr := newKey(t) ffsid.a.await(ffsid.cli.CreateNamespace(namespace)) _, _, err := ffsid.cli.CreateNamespace(namespace) require.ErrorContains(t, err, "already exists") ffsid.a.await(ffsid.cli.CreateSubject(namespace, subjKey.PublicKey())) ns, err := ffsid.cli.GetNamespace(namespace) require.NoError(t, err) require.Equal(t, namespace, ns.Name) namespaces, err := ffsid.cli.ListNamespaces() require.NoError(t, err) require.ElementsMatch(t, []*client.Namespace{{}, ns}, namespaces) subj, err := ffsid.cli.GetSubject(subjAddr) require.NoError(t, err) require.Equal(t, namespace, subj.Namespace) subjects, err := ffsid.cli.ListNamespaceSubjects(namespace) require.NoError(t, err) require.ElementsMatch(t, []util.Uint160{subjAddr}, subjects) ffsid.a.await(ffsid.cli.DeleteSubject(subjAddr)) subjects, err = ffsid.cli.ListNamespaceSubjects(namespace) require.NoError(t, err) require.Empty(t, subjects) } func TestFrostFSID_Client_DefaultNamespace(t *testing.T) { ffsid, cancel := initFrostfsIFClientTest(t) defer cancel() group := "group" subjKey, subjAddr := newKey(t) ffsid.a.await(ffsid.cli.CreateSubject(defaultNamespace, subjKey.PublicKey())) groupID, err := ffsid.cli.ParseGroupID(ffsid.cli.Wait(ffsid.cli.CreateGroup(defaultNamespace, group))) require.NoError(t, err) ffsid.a.await(ffsid.cli.AddSubjectToGroup(subjAddr, groupID)) namespaces, err := ffsid.cli.ListNamespaces() require.NoError(t, err) require.EqualValues(t, []*client.Namespace{{}}, namespaces) groups, err := ffsid.cli.ListGroups(defaultNamespace) require.NoError(t, err) require.EqualValues(t, []*client.Group{{ID: groupID, Name: group, Namespace: defaultNamespace}}, groups) subjects, err := ffsid.cli.ListNamespaceSubjects(defaultNamespace) require.NoError(t, err) require.EqualValues(t, []util.Uint160{subjAddr}, subjects) subjects, err = ffsid.cli.ListGroupSubjects(defaultNamespace, groupID) require.NoError(t, err) require.EqualValues(t, []util.Uint160{subjAddr}, subjects) subject, err := ffsid.cli.GetSubjectExtended(subjAddr) require.NoError(t, err) require.True(t, subjKey.PublicKey().Equal(subject.PrimaryKey)) require.Equal(t, defaultNamespace, subject.Namespace) require.EqualValues(t, []*client.Group{{ID: groupID, Name: group, Namespace: defaultNamespace}}, subject.Groups) } func TestFrostFSID_Client_GroupManagement(t *testing.T) { ffsid, cancel := initFrostfsIFClientTest(t) defer cancel() namespace := "namespace" subjKey, subjAddr := newKey(t) ffsid.a.await(ffsid.cli.CreateNamespace(namespace)) ffsid.a.await(ffsid.cli.CreateSubject(namespace, subjKey.PublicKey())) groupName := "group" groupID := int64(1) actGroupID, err := ffsid.cli.ParseGroupID(ffsid.cli.Wait(ffsid.cli.CreateGroup(namespace, groupName))) require.NoError(t, err) require.Equal(t, groupID, actGroupID) _, _, err = ffsid.cli.CreateGroup(namespace, groupName) require.ErrorContains(t, err, "is not available") iamARN := "arn" ffsid.a.await(ffsid.cli.SetGroupKV(namespace, groupID, client.IAMARNKey, iamARN)) group, err := ffsid.cli.GetGroup(namespace, groupID) require.NoError(t, err) require.Equal(t, groupName, group.Name) require.Equal(t, map[string]string{client.IAMARNKey: iamARN}, group.KV) ffsid.a.await(ffsid.cli.DeleteGroupKV(namespace, groupID, client.IAMARNKey)) groupExt, err := ffsid.cli.GetGroupExtended(namespace, groupID) require.NoError(t, err) require.Zero(t, groupExt.SubjectsCount) ffsid.a.await(ffsid.cli.AddSubjectToGroup(subjAddr, groupID)) subjExt, err := ffsid.cli.GetSubjectExtended(subjAddr) require.NoError(t, err) require.ElementsMatch(t, []*client.Group{{ID: groupID, Name: groupName, Namespace: namespace, KV: map[string]string{}}}, subjExt.Groups) groupExt, err = ffsid.cli.GetGroupExtended(namespace, groupID) require.NoError(t, err) require.EqualValues(t, 1, groupExt.SubjectsCount) subjects, err := ffsid.cli.ListGroupSubjects(namespace, groupID) require.NoError(t, err) require.ElementsMatch(t, []util.Uint160{subjAddr}, subjects) ffsid.a.await(ffsid.cli.RemoveSubjectFromGroup(subjAddr, groupID)) subjects, err = ffsid.cli.ListGroupSubjects(namespace, groupID) require.NoError(t, err) require.Empty(t, subjects) subjects, err = ffsid.cli.ListNamespaceSubjects(namespace) require.NoError(t, err) require.ElementsMatch(t, []util.Uint160{subjAddr}, subjects) groups, err := ffsid.cli.ListGroups(namespace) require.NoError(t, err) require.ElementsMatch(t, []*client.Group{{ID: groupID, Name: groupName, Namespace: namespace, KV: map[string]string{}}}, groups) ffsid.a.await(ffsid.cli.DeleteGroup(namespace, groupID)) _, err = ffsid.cli.GetGroup(namespace, groupID) require.ErrorContains(t, err, "not found") groups, err = ffsid.cli.ListGroups(namespace) require.NoError(t, err) require.Empty(t, groups) } type testSubject struct { key *keys.PrivateKey addr util.Uint160 } func TestFrostFSID_Client_Lists(t *testing.T) { ffsid, cancel := initFrostfsIFClientTest(t) defer cancel() namespaces := append([]string{defaultNamespace}, createNamespaces(t, ffsid, 3)...) subjects := createSubjectsInNS(t, ffsid, namespaces[1], 3) subjects = append(subjects, createSubjectsInNS(t, ffsid, namespaces[2], 2)...) subjects = append(subjects, createSubjectsInNS(t, ffsid, namespaces[3], 5)...) groups := createGroupsInNS(t, ffsid, namespaces[0], 1) groups = append(groups, createGroupsInNS(t, ffsid, namespaces[1], 2)...) groups = append(groups, createGroupsInNS(t, ffsid, namespaces[2], 1)...) groups = append(groups, createGroupsInNS(t, ffsid, namespaces[3], 2)...) addSubjectsToGroups(t, ffsid, groups, subjects) nsList, err := ffsid.cli.ListNamespaces() require.NoError(t, err) require.ElementsMatch(t, []*client.Namespace{{Name: namespaces[0]}, {Name: namespaces[1]}, {Name: namespaces[2]}, {Name: namespaces[3]}}, nsList) nonEmptyNs, err := ffsid.cli.ListNonEmptyNamespaces() require.NoError(t, err) require.ElementsMatch(t, namespaces[1:], nonEmptyNs) checkNamespaceSubjects(t, ffsid.cli, namespaces[0], subjects, 0, 0) checkNamespaceSubjects(t, ffsid.cli, namespaces[1], subjects, 0, 3) checkNamespaceSubjects(t, ffsid.cli, namespaces[2], subjects, 3, 5) checkNamespaceSubjects(t, ffsid.cli, namespaces[3], subjects, 5, 10) nonEmptyGroups, err := ffsid.cli.ListNonEmptyGroups(namespaces[1]) require.NoError(t, err) require.ElementsMatch(t, []string{groups[2].Name}, nonEmptyGroups) checkGroupSubjects(t, ffsid.cli, groups[0], subjects, 0, 0) checkGroupSubjects(t, ffsid.cli, groups[1], subjects, 0, 0) checkGroupSubjects(t, ffsid.cli, groups[2], subjects, 2, 3) checkGroupSubjects(t, ffsid.cli, groups[3], subjects, 3, 5) checkGroupSubjects(t, ffsid.cli, groups[4], subjects, 6, 8) checkGroupSubjects(t, ffsid.cli, groups[5], subjects, 8, 10) } func createSubjectsInNS(t *testing.T, ffsid *testFrostFSIDClientInvoker, ns string, count int) []testSubject { subjects := make([]testSubject, count) tx := ffsid.cli.StartTx() for i := range subjects { subjects[i].key, subjects[i].addr = newKey(t) err := tx.WrapCall(ffsid.cli.CreateSubjectCall(ns, subjects[i].key.PublicKey())) require.NoError(t, err) } ffsid.a.await(ffsid.cli.SendTx(tx)) return subjects } func createGroupsInNS(t *testing.T, ffsid *testFrostFSIDClientInvoker, ns string, count int) []client.Group { groups := make([]client.Group, count) tx := ffsid.cli.StartTx() for i := range groups { groups[i].Namespace = ns groups[i].Name = ns + "/group" + strconv.Itoa(i+1) err := tx.WrapCall(ffsid.cli.CreateGroupCall(ns, groups[i].Name)) require.NoError(t, err) } res := ffsid.a.await(ffsid.cli.SendTx(tx)) ids, err := unwrap.ArrayOfBigInts(makeValidRes(stackitem.NewArray(res.Stack)), nil) require.NoError(t, err) for i, id := range ids { groups[i].ID = id.Int64() } return groups } func createNamespaces(t *testing.T, ffsid *testFrostFSIDClientInvoker, count int) []string { namespaces := make([]string, count) tx := ffsid.cli.StartTx() for i := range namespaces { namespaces[i] = "ns" + strconv.Itoa(i+1) err := tx.WrapCall(ffsid.cli.CreateNamespaceCall(namespaces[i])) require.NoError(t, err) } ffsid.a.await(ffsid.cli.SendTx(tx)) return namespaces } func addSubjectsToGroups(t *testing.T, ffsid *testFrostFSIDClientInvoker, groups []client.Group, subjects []testSubject) { cli := ffsid.cli tx := cli.StartTx() err := tx.WrapCall(cli.AddSubjectToGroupCall(subjects[2].addr, groups[2].ID)) require.NoError(t, err) err = tx.WrapCall(cli.AddSubjectToGroupCall(subjects[3].addr, groups[3].ID)) require.NoError(t, err) err = tx.WrapCall(cli.AddSubjectToGroupCall(subjects[4].addr, groups[3].ID)) require.NoError(t, err) ffsid.a.await(cli.SendTx(tx)) // we have to start new tx because of insufficient gas / gas limit exceeded error tx = cli.StartTx() err = tx.WrapCall(cli.AddSubjectToGroupCall(subjects[6].addr, groups[4].ID)) require.NoError(t, err) err = tx.WrapCall(cli.AddSubjectToGroupCall(subjects[7].addr, groups[4].ID)) require.NoError(t, err) err = tx.WrapCall(cli.AddSubjectToGroupCall(subjects[8].addr, groups[5].ID)) require.NoError(t, err) err = tx.WrapCall(cli.AddSubjectToGroupCall(subjects[9].addr, groups[5].ID)) require.NoError(t, err) ffsid.a.await(cli.SendTx(tx)) } func checkNamespaceSubjects(t *testing.T, cli *client.Client, ns string, subjects []testSubject, start, end int) { nsSubjects, err := cli.ListNamespaceSubjects(ns) require.NoError(t, err) require.ElementsMatch(t, subjSlice(subjects, start, end), nsSubjects) } func checkGroupSubjects(t *testing.T, cli *client.Client, group client.Group, subjects []testSubject, start, end int) { groupSubjects, err := cli.ListGroupSubjects(group.Namespace, group.ID) require.NoError(t, err) require.ElementsMatch(t, subjSlice(subjects, start, end), groupSubjects) } func subjSlice(subjects []testSubject, start, end int) []util.Uint160 { res := make([]util.Uint160, 0, end-start) for i := start; i < end; i++ { res = append(res, subjects[i].addr) } return res } func TestFrostFSID_Client_UseCaseWithS3GW(t *testing.T) { ffsid, cancel := initFrostfsIFClientTest(t) defer cancel() namespace := "namespace" login := "login" dataUserKey, dataUserAddr := newKey(t) extraDataUserKey, _ := newKey(t) // admin tx := ffsid.cli.StartTx() err := tx.WrapCall(ffsid.cli.CreateNamespaceCall(namespace)) require.NoError(t, err) err = tx.WrapCall(ffsid.cli.CreateSubjectCall(namespace, dataUserKey.PublicKey())) require.NoError(t, err) err = tx.WrapCall(ffsid.cli.SetSubjectNameCall(dataUserAddr, login)) require.NoError(t, err) err = tx.WrapCall(ffsid.cli.AddSubjectKeyCall(dataUserAddr, extraDataUserKey.PublicKey())) require.NoError(t, err) ffsid.a.await(ffsid.cli.SendTx(tx)) // s3-gw subj, err := ffsid.cli.GetSubjectByKey(extraDataUserKey.PublicKey()) require.NoError(t, err) require.Equal(t, login, subj.Name) require.True(t, dataUserKey.PublicKey().Equal(subj.PrimaryKey)) require.Equal(t, namespace, subj.Namespace) } func TestFrostFSID_Client_UseCaseListNSSubjects(t *testing.T) { ffsid, cancel := initFrostfsIFClientTest(t) defer cancel() namespace := "namespace" group := "group" groupID := int64(1) tx := ffsid.cli.StartTx() err := tx.WrapCall(ffsid.cli.CreateNamespaceCall(namespace)) require.NoError(t, err) err = tx.WrapCall(ffsid.cli.CreateGroupCall(namespace, group)) require.NoError(t, err) ffsid.a.await(ffsid.cli.SendTx(tx)) // admin subjects := make([]testSubject, 5) for i := range subjects { tx = ffsid.cli.StartTx() subjects[i].key, subjects[i].addr = newKey(t) err = tx.WrapCall(ffsid.cli.CreateSubjectCall(namespace, subjects[i].key.PublicKey())) require.NoError(t, err) err = tx.WrapCall(ffsid.cli.SetSubjectNameCall(subjects[i].addr, "login"+strconv.Itoa(i))) require.NoError(t, err) if i > len(subjects)/2 { err = tx.WrapCall(ffsid.cli.AddSubjectToGroupCall(subjects[i].addr, groupID)) require.NoError(t, err) } ffsid.a.await(ffsid.cli.SendTx(tx)) } nsSubjects, err := ffsid.cli.ListNamespaceSubjects(namespace) require.NoError(t, err) res := make([]*client.SubjectExtended, len(nsSubjects)) for i, subj := range nsSubjects { res[i], err = ffsid.cli.GetSubjectExtended(subj) require.NoError(t, err) } prettyPrintExtendedSubjects(res) } func TestFrostFSID_Client_GetSubjectByName(t *testing.T) { ffsid, cancel := initFrostfsIFClientTest(t) defer cancel() key, addr := newKey(t) subjName := "name" tx := ffsid.cli.StartTx() err := tx.WrapCall(ffsid.cli.CreateSubjectCall(defaultNamespace, key.PublicKey())) require.NoError(t, err) err = tx.WrapCall(ffsid.cli.SetSubjectNameCall(addr, subjName)) require.NoError(t, err) ffsid.a.await(ffsid.cli.SendTx(tx)) subj, err := ffsid.cli.GetSubjectByName(defaultNamespace, subjName) require.NoError(t, err) require.Equal(t, subjName, subj.Name) require.True(t, key.PublicKey().Equal(subj.PrimaryKey)) require.Equal(t, defaultNamespace, subj.Namespace) } func prettyPrintExtendedSubjects(subjects []*client.SubjectExtended) { for _, subj := range subjects { var sb strings.Builder sb.WriteString(fmt.Sprintf("login: %s, namespace: %v, groups: [ ", subj.Name, subj.Namespace)) for _, group := range subj.Groups { sb.WriteString(group.Namespace + ":" + group.Name + " ") } sb.WriteByte(']') fmt.Println(sb.String()) } }