| // Copyright 2015 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package testutil |
| |
| import ( |
| "bytes" |
| "fmt" |
| "io/ioutil" |
| "os" |
| "reflect" |
| "runtime/debug" |
| "strings" |
| "testing" |
| |
| "v.io/v23" |
| "v.io/v23/context" |
| "v.io/v23/security" |
| "v.io/v23/security/access" |
| wire "v.io/v23/services/syncbase" |
| "v.io/v23/services/watch" |
| "v.io/v23/syncbase" |
| "v.io/v23/syncbase/util" |
| "v.io/v23/vdl" |
| "v.io/v23/verror" |
| "v.io/v23/vom" |
| "v.io/x/lib/vlog" |
| idiscovery "v.io/x/ref/lib/discovery" |
| fdiscovery "v.io/x/ref/lib/discovery/factory" |
| "v.io/x/ref/lib/discovery/plugins/mock" |
| "v.io/x/ref/services/syncbase/server" |
| "v.io/x/ref/services/syncbase/store" |
| "v.io/x/ref/test" |
| tsecurity "v.io/x/ref/test/testutil" |
| ) |
| |
| func Fatal(t testing.TB, args ...interface{}) { |
| debug.PrintStack() |
| t.Fatal(args...) |
| } |
| |
| func Fatalf(t testing.TB, format string, args ...interface{}) { |
| debug.PrintStack() |
| t.Fatalf(format, args...) |
| } |
| |
| // TODO(sadovsky): Standardize on a small set of constants and helper functions |
| // to share across all Syncbase tests. Currently, our 'featuretests' tests use a |
| // different set of helpers from our other unit tests. |
| func CreateDatabase(t testing.TB, ctx *context.T, s syncbase.Service, name string, perms access.Permissions) syncbase.Database { |
| d := s.Database(ctx, name, nil) |
| if err := d.Create(ctx, perms); err != nil { |
| Fatalf(t, "d.Create() failed: %v", err) |
| } |
| return d |
| } |
| |
| func CreateCollection(t testing.TB, ctx *context.T, d syncbase.Database, name string) syncbase.Collection { |
| c := d.Collection(ctx, name) |
| if err := c.Create(ctx, nil); err != nil { |
| Fatalf(t, "c.Create() failed: %v", err) |
| } |
| return c |
| } |
| |
| func CreateSyncgroup(t testing.TB, ctx *context.T, d syncbase.Database, c syncbase.Collection, name, description string) syncbase.Syncgroup { |
| sg := d.Syncgroup(ctx, name) |
| sgSpec := wire.SyncgroupSpec{ |
| Description: description, |
| Collections: []wire.Id{c.Id()}, |
| Perms: DefaultPerms(wire.AllSyncgroupTags, string(security.AllPrincipals)), |
| } |
| sgMembership := wire.SyncgroupMemberInfo{} |
| if err := sg.Create(ctx, sgSpec, sgMembership); err != nil { |
| Fatalf(t, "sg.Create() failed: %v", err) |
| } |
| return sg |
| } |
| |
| // TODO(sadovsky): Drop the 'perms' argument. The only client that passes |
| // non-nil, syncgroup_test.go, should use SetupOrDieCustom instead. |
| func SetupOrDie(perms access.Permissions) (clientCtx *context.T, serverName string, cleanup func()) { |
| _, clientCtx, serverName, _, cleanup = SetupOrDieCustom(strings.Join([]string{"o", "app", "client"}, security.ChainSeparator), "server", perms) |
| return |
| } |
| |
| // TODO(sadovsky): Switch unit tests to v23test.Shell, then delete this. |
| func SetupOrDieCustom(clientSuffix, serverSuffix string, perms access.Permissions) (ctx, clientCtx *context.T, serverName string, rootp security.Principal, cleanup func()) { |
| ctx, shutdown := test.V23Init() |
| df, _ := idiscovery.NewFactory(ctx, mock.New()) |
| fdiscovery.InjectFactory(df) |
| rootp = tsecurity.NewPrincipal("root") |
| clientCtx, serverCtx := NewCtx(ctx, rootp, clientSuffix), NewCtx(ctx, rootp, serverSuffix) |
| |
| if perms == nil { |
| perms = DefaultPerms(access.AllTypicalTags(), "root"+security.ChainSeparator+clientSuffix) |
| } |
| serverName, stopServer := newServer(serverCtx, perms) |
| cleanup = func() { |
| stopServer() |
| shutdown() |
| } |
| return |
| } |
| |
| func DefaultPerms(tags []access.Tag, patterns ...string) access.Permissions { |
| perms := access.Permissions{} |
| for _, pattern := range patterns { |
| perms.Add(security.BlessingPattern(pattern), access.TagStrings(tags...)...) |
| } |
| return perms |
| } |
| |
| func ScanMatches(ctx *context.T, c syncbase.Collection, r syncbase.RowRange, wantKeys []string, wantValues []interface{}) error { |
| if len(wantKeys) != len(wantValues) { |
| return fmt.Errorf("bad input args") |
| } |
| it := c.Scan(ctx, r) |
| gotKeys := []string{} |
| for it.Advance() { |
| gotKey := it.Key() |
| gotKeys = append(gotKeys, gotKey) |
| i := len(gotKeys) - 1 |
| if i >= len(wantKeys) { |
| continue |
| } |
| // Check key. |
| wantKey := wantKeys[i] |
| if gotKey != wantKey { |
| return fmt.Errorf("Keys do not match: got %q, want %q", gotKey, wantKey) |
| } |
| // Check value. |
| wantValue := wantValues[i] |
| gotValue := reflect.Zero(reflect.TypeOf(wantValue)).Interface() |
| if err := it.Value(&gotValue); err != nil { |
| return fmt.Errorf("it.Value() failed: %v", err) |
| } |
| if !reflect.DeepEqual(gotValue, wantValue) { |
| return fmt.Errorf("Values do not match: got %v, want %v", gotValue, wantValue) |
| } |
| } |
| if err := it.Err(); err != nil { |
| return fmt.Errorf("c.Scan() failed: %v", err) |
| } |
| if len(gotKeys) != len(wantKeys) { |
| return fmt.Errorf("Unmatched keys: got %v, want %v", gotKeys, wantKeys) |
| } |
| return nil |
| } |
| |
| func CheckScan(t testing.TB, ctx *context.T, c syncbase.Collection, r syncbase.RowRange, wantKeys []string, wantValues []interface{}) { |
| if err := ScanMatches(ctx, c, r, wantKeys, wantValues); err != nil { |
| Fatalf(t, err.Error()) |
| } |
| } |
| |
| func CheckExec(t testing.TB, ctx *context.T, db syncbase.DatabaseHandle, q string, wantHeaders []string, wantResults [][]*vom.RawBytes) { |
| gotHeaders, it, err := db.Exec(ctx, q) |
| if err != nil { |
| t.Errorf("query %q: got %v, want nil", q, err) |
| } |
| if !reflect.DeepEqual(gotHeaders, wantHeaders) { |
| t.Errorf("query %q: got %v, want %v", q, gotHeaders, wantHeaders) |
| } |
| gotResults := [][]interface{}{} |
| for it.Advance() { |
| n := it.ResultCount() |
| gotResult := make([]interface{}, n) |
| for i := 0; i != n; i++ { |
| if err := it.Result(i, &gotResult[i]); err != nil { |
| t.Errorf("error decoding result: %v", err) |
| } |
| } |
| gotResults = append(gotResults, gotResult) |
| } |
| if it.Err() != nil { |
| t.Errorf("query %q: got %v, want nil", q, it.Err()) |
| } |
| if got, want := vdl.ValueOf(gotResults), vdl.ValueOf(wantResults); !vdl.EqualValue(got, want) { |
| t.Errorf("query %q: got %v, want %v", q, got, want) |
| } |
| } |
| |
| func CheckExecError(t testing.TB, ctx *context.T, db syncbase.DatabaseHandle, q string, wantErrorID verror.ID) { |
| _, rs, err := db.Exec(ctx, q) |
| if err == nil { |
| if rs.Advance() { |
| t.Errorf("query %q: got true, want false", q) |
| } |
| err = rs.Err() |
| } |
| if verror.ErrorID(err) != wantErrorID { |
| t.Errorf("%q", verror.DebugString(err)) |
| t.Errorf("query %q: got %v, want: %v", q, verror.ErrorID(err), wantErrorID) |
| } |
| } |
| |
| // A WatchChangeTest is a syncbase.WatchChange that has a public ValueBytes |
| // and CollectionInfo field, to allow tests to set them. |
| type WatchChangeTest struct { |
| syncbase.WatchChange |
| ValueBytes *vom.RawBytes |
| CollectionInfo *wire.StoreChangeCollectionInfo |
| } |
| |
| func WatchChangeTestRootPut(resumeMarker watch.ResumeMarker) WatchChangeTest { |
| return WatchChangeTest{ |
| WatchChange: syncbase.WatchChange{ |
| EntityType: syncbase.EntityRoot, |
| ChangeType: syncbase.PutChange, |
| ResumeMarker: resumeMarker, |
| Continued: (resumeMarker == nil), |
| }, |
| } |
| } |
| |
| func WatchChangeTestCollectionPut(cxId wire.Id, allowedTags []access.Tag, perms access.Permissions, resumeMarker watch.ResumeMarker) WatchChangeTest { |
| allowedTagsSet := make(map[access.Tag]struct{}) |
| if len(allowedTags) == 0 { |
| allowedTagsSet = nil |
| } else { |
| for _, tag := range allowedTags { |
| allowedTagsSet[tag] = struct{}{} |
| } |
| } |
| return WatchChangeTest{ |
| WatchChange: syncbase.WatchChange{ |
| EntityType: syncbase.EntityCollection, |
| Collection: cxId, |
| ChangeType: syncbase.PutChange, |
| ResumeMarker: resumeMarker, |
| Continued: (resumeMarker == nil), |
| }, |
| CollectionInfo: &wire.StoreChangeCollectionInfo{ |
| Allowed: allowedTagsSet, |
| Perms: perms, |
| }, |
| } |
| } |
| |
| func WatchChangeTestCollectionDelete(cxId wire.Id, resumeMarker watch.ResumeMarker) WatchChangeTest { |
| return WatchChangeTest{ |
| WatchChange: syncbase.WatchChange{ |
| EntityType: syncbase.EntityCollection, |
| Collection: cxId, |
| ChangeType: syncbase.DeleteChange, |
| ResumeMarker: resumeMarker, |
| Continued: (resumeMarker == nil), |
| }, |
| } |
| } |
| |
| func WatchChangeTestRowPut(cxId wire.Id, rowKey string, value interface{}, resumeMarker watch.ResumeMarker) WatchChangeTest { |
| return WatchChangeTest{ |
| WatchChange: syncbase.WatchChange{ |
| EntityType: syncbase.EntityRow, |
| Collection: cxId, |
| Row: rowKey, |
| ChangeType: syncbase.PutChange, |
| ResumeMarker: resumeMarker, |
| Continued: (resumeMarker == nil), |
| }, |
| ValueBytes: vom.RawBytesOf(value), |
| } |
| } |
| |
| func WatchChangeTestRowDelete(cxId wire.Id, rowKey string, resumeMarker watch.ResumeMarker) WatchChangeTest { |
| return WatchChangeTest{ |
| WatchChange: syncbase.WatchChange{ |
| EntityType: syncbase.EntityRow, |
| Collection: cxId, |
| Row: rowKey, |
| ChangeType: syncbase.DeleteChange, |
| ResumeMarker: resumeMarker, |
| Continued: (resumeMarker == nil), |
| }, |
| } |
| } |
| |
| // WatchChangeEq returns whether *want and *got represent the same value. |
| func WatchChangeEq(got *syncbase.WatchChange, want *WatchChangeTest) (eq bool) { |
| if want.EntityType == got.EntityType && |
| want.Collection == got.Collection && |
| want.Row == got.Row && |
| want.ChangeType == got.ChangeType && |
| bytes.Equal(want.ResumeMarker, got.ResumeMarker) && |
| want.FromSync == got.FromSync && |
| want.Continued == got.Continued { |
| |
| if want.ChangeType == syncbase.DeleteChange { |
| eq = true |
| } else { |
| switch want.EntityType { |
| case syncbase.EntityRow: |
| var wantValue interface{} |
| var gotValue interface{} |
| gotErr := got.Value(&gotValue) |
| wantErr := want.ValueBytes.ToValue(&wantValue) |
| eq = ((gotErr == nil) == (wantErr == nil)) && |
| reflect.DeepEqual(gotValue, wantValue) |
| case syncbase.EntityCollection: |
| eq = reflect.DeepEqual(got.CollectionInfo(), want.CollectionInfo) |
| default: |
| eq = true |
| } |
| } |
| } |
| return eq |
| } |
| |
| // CheckWatch checks that the sequence of elements from the watch stream starts |
| // with the given slice of watch changes. |
| func CheckWatch(t testing.TB, wstream syncbase.WatchStream, changes []WatchChangeTest) { |
| for _, want := range changes { |
| if !wstream.Advance() { |
| Fatalf(t, "wstream.Advance() reached the end: %v", wstream.Err()) |
| } |
| if got := wstream.Change(); !WatchChangeEq(&got, &want) { |
| if got.ChangeType == syncbase.PutChange && got.EntityType == syncbase.EntityCollection { |
| Fatalf(t, "unexpected watch change: got %+v, want %+v (cx info: got %+v, want %+v)", got, want, got.CollectionInfo(), want.CollectionInfo) |
| } else { |
| Fatalf(t, "unexpected watch change: got %+v, want %+v", got, want) |
| } |
| } |
| } |
| } |
| |
| func DefaultSchema(version int32) *syncbase.Schema { |
| return &syncbase.Schema{ |
| Metadata: wire.SchemaMetadata{ |
| Version: version, |
| }, |
| } |
| } |
| |
| //////////////////////////////////////// |
| // Internal helpers |
| |
| func getPermsOrDie(t testing.TB, ctx *context.T, ac util.AccessController) access.Permissions { |
| perms, _, err := ac.GetPermissions(ctx) |
| if err != nil { |
| Fatalf(t, "GetPermissions failed: %v", err) |
| } |
| return perms |
| } |
| |
| func newServer(serverCtx *context.T, perms access.Permissions) (string, func()) { |
| if perms == nil { |
| vlog.Fatal("perms must be specified") |
| } |
| rootDir, err := ioutil.TempDir("", "syncbase") |
| if err != nil { |
| vlog.Fatal("ioutil.TempDir() failed: ", err) |
| } |
| serverCtx, cancel := context.WithCancel(serverCtx) |
| service, err := server.NewService(serverCtx, server.ServiceOptions{ |
| Perms: perms, |
| RootDir: rootDir, |
| Engine: store.EngineForTest, |
| DevMode: true, |
| }) |
| if err != nil { |
| vlog.Fatal("server.NewService() failed: ", err) |
| } |
| serverCtx, s, err := v23.WithNewDispatchingServer(serverCtx, "", server.NewDispatcher(service)) |
| if err != nil { |
| vlog.Fatal("v23.WithNewDispatchingServer() failed: ", err) |
| } |
| service.AddNames(serverCtx, s) |
| name := s.Status().Endpoints[0].Name() |
| return name, func() { |
| cancel() |
| <-s.Closed() |
| service.Close() |
| os.RemoveAll(rootDir) |
| } |
| } |
| |
| // Creates a new context object with blessing "root:<suffix>", configured to |
| // present this blessing when acting as a server as well as when acting as a |
| // client and talking to a server that presents a blessing rooted at "root". |
| // TODO(sadovsky): Switch unit tests to v23test.Shell, then delete this. |
| func NewCtx(ctx *context.T, rootp security.Principal, suffix string) *context.T { |
| // Principal for the new context. |
| p := tsecurity.NewPrincipal(suffix) |
| rootb, _ := rootp.BlessingStore().Default() |
| |
| // Bless the new principal as "root:<suffix>". |
| blessings, err := rootp.Bless(p.PublicKey(), rootb, suffix, security.UnconstrainedUse()) |
| if err != nil { |
| vlog.Fatal("rootp.Bless() failed: ", err) |
| } |
| |
| // Make it so users of the new context present their "root:<suffix>" blessing |
| // when talking to servers with blessings rooted at "root". |
| if _, err := p.BlessingStore().Set(blessings, security.BlessingPattern("root")); err != nil { |
| vlog.Fatal("p.BlessingStore().Set() failed: ", err) |
| } |
| |
| // Make it so that when users of the new context act as a server, they present |
| // their "root:<suffix>" blessing. |
| if err := p.BlessingStore().SetDefault(blessings); err != nil { |
| vlog.Fatal("p.BlessingStore().SetDefault() failed: ", err) |
| } |
| |
| // Have users of the prepared context treat root's public key as an authority |
| // on all blessings rooted at "root". |
| if err := security.AddToRoots(p, blessings); err != nil { |
| vlog.Fatal("AddToRoots() failed: ", err) |
| } |
| |
| resCtx, err := v23.WithPrincipal(ctx, p) |
| if err != nil { |
| vlog.Fatal("v23.WithPrincipal() failed: ", err) |
| } |
| |
| return resCtx |
| } |