//go:build integration package tests import ( "os" "testing" "time" "github.com/gogo/protobuf/types" "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anyproto/anytype-heart/pb" "github.com/anyproto/anytype-heart/pkg/lib/bundle" "github.com/anyproto/anytype-heart/pkg/lib/pb/model" "github.com/anyproto/anytype-heart/tests/blockbuilder" "github.com/anyproto/anytype-heart/util/pbtypes" ) const ( migrationMnemonicKey = "migration_mnemonic" migrationAccounIDKey = "migration_account_id" ) func newImportSession(t *testing.T, port string) *testSession { var s testSession c, err := newClient(port) require.NoError(t, err) s.ClientCommandsClient = c mnemonic, err := readStringFromCache(migrationMnemonicKey) require.NoError(t, err) t.Log("your mnemonic:", mnemonic) cctx := s.newCallCtx(t) _ = call(cctx, s.WalletRecover, &pb.RpcWalletRecoverRequest{ Mnemonic: mnemonic, RootPath: rootPath, }) cctx, s.eventReceiver = s.openClientSession(t, mnemonic) accountID, err := readStringFromCache(migrationAccounIDKey) require.NoError(t, err) t.Log("your account ID:", accountID) return &s } func fetchObjects(t *testing.T, s *testSession, ids []string) map[string]*model.ObjectView { cctx := s.newCallCtx(t) res := map[string]*model.ObjectView{} for _, id := range ids { resp := call(cctx, s.ObjectShow, &pb.RpcObjectShowRequest{ ObjectId: id, }) res[id] = resp.ObjectView } return res } func createAndExportAccount(t *testing.T) (string, map[string]*model.ObjectView) { exportPort := os.Getenv("ANYTYPE_OLD_TEST_GRPC_PORT") if exportPort == "" { t.Fatal("you must specify ANYTYPE_OLD_TEST_GRPC_PORT env variable") } exportSession := newTestSession(t, exportPort, migrationMnemonicKey, migrationAccounIDKey) cctx := exportSession.newCallCtx(t) resp := call(cctx, exportSession.ObjectSearch, &pb.RpcObjectSearchRequest{ Keys: []string{bundle.RelationKeyId.String()}, }) oldObjectIDs := lo.Map(resp.Records, func(r *types.Struct, _ int) string { return r.Fields[bundle.RelationKeyId.String()].GetStringValue() }) oldObjects := fetchObjects(t, exportSession, oldObjectIDs) exportResp := call(cctx, exportSession.ObjectListExport, &pb.RpcObjectListExportRequest{ Path: "/var/anytype_old/", Format: pb.RpcObjectListExport_Protobuf, Zip: true, IncludeArchived: true, IncludeFiles: true, IncludeNested: true, }) call(cctx, exportSession.AccountStop, &pb.RpcAccountStopRequest{ RemoveData: false, }) return exportResp.Path, oldObjects } func TestMigration(t *testing.T) { _ = os.RemoveAll(cacheFilename(migrationMnemonicKey)) _ = os.RemoveAll(cacheFilename(migrationAccounIDKey)) exportPath, oldObjects := createAndExportAccount(t) importSession := newImportSession(t, os.Getenv("ANYTYPE_TEST_GRPC_PORT")) cctx := importSession.newCallCtx(t) call(cctx, importSession.AccountRecoverFromLegacyExport, &pb.RpcAccountRecoverFromLegacyExportRequest{ Path: exportPath, RootPath: rootPath + "_new", }) call(cctx, importSession.ObjectImport, &pb.RpcObjectImportRequest{ Params: &pb.RpcObjectImportRequestParamsOfPbParams{ PbParams: &pb.RpcObjectImportRequestPbParams{ Path: []string{exportPath}, }, }, Type: pb.RpcObjectImportRequest_Pb, }) time.Sleep(1 * time.Minute) resp := call(cctx, importSession.ObjectSearch, &pb.RpcObjectSearchRequest{ Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeyOldAnytypeID.String()}, }) filtered := lo.Filter(resp.Records, func(r *types.Struct, _ int) bool { return r.Fields[bundle.RelationKeyOldAnytypeID.String()].GetStringValue() != "" }) newIDtoOldID := lo.SliceToMap(filtered, func(r *types.Struct) (string, string) { return r.Fields[bundle.RelationKeyId.String()].GetStringValue(), r.Fields[bundle.RelationKeyOldAnytypeID.String()].GetStringValue() }) newObjectIDs := lo.Map(filtered, func(r *types.Struct, _ int) string { return r.Fields[bundle.RelationKeyId.String()].GetStringValue() }) newObjects := fetchObjects(t, importSession, newObjectIDs) for id, newObject := range newObjects { t.Run("details for "+id, func(t *testing.T) { oldID := newIDtoOldID[id] oldObject := oldObjects[oldID] oldDetails := normalizeDetails(oldObject.Details[0].Details) newDetails := normalizeDetails(substituteLinksInDetails(newObject.Details[0].Details, newIDtoOldID)) assertDetails(t, oldDetails, newDetails) blockbuilder.AssertPagesEqualWithLinks(t, oldObject.Blocks, newObject.Blocks, newIDtoOldID) }) } } func assertDetails(t *testing.T, wantdetails *domain.Details, gotdetails *domain.Details) { for key, want := range wantDetails.Fields { got := gotDetails.Fields[key] assert.Equal(t, want, got, key) } } func substituteLinksInDetails(d *types.Struct, idsMap map[string]string) *types.Struct { for k := range d.Fields { if id := pbtypes.GetString(d, k); id != "" { if newID, ok := idsMap[id]; ok { d.Fields[k] = pbtypes.String(newID) } } else if ids := pbtypes.GetStringList(d, k); len(ids) > 0 { newIDs := lo.Map(ids, func(newID string, _ int) string { if oldID, ok := idsMap[newID]; ok { return oldID } else { return newID } }) d.Fields[k] = pbtypes.StringList(newIDs) } } return d } func normalizeDetails(d *types.Struct) *types.Struct { delete(d.Fields, bundle.RelationKeyId.String()) delete(d.Fields, bundle.RelationKeyLastModifiedBy.String()) delete(d.Fields, bundle.RelationKeyLastModifiedDate.String()) delete(d.Fields, bundle.RelationKeyCreatedDate.String()) delete(d.Fields, bundle.RelationKeyLinks.String()) delete(d.Fields, bundle.RelationKeyOldAnytypeID.String()) delete(d.Fields, bundle.RelationKeyCreator.String()) delete(d.Fields, bundle.RelationKeySource.String()) return d }