| // 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 server_test |
| |
| import ( |
| "fmt" |
| "io/ioutil" |
| "os" |
| "reflect" |
| "runtime/debug" |
| "strings" |
| "testing" |
| |
| "v.io/v23" |
| "v.io/v23/context" |
| "v.io/v23/naming" |
| "v.io/v23/security" |
| "v.io/v23/security/access" |
| "v.io/v23/services/groups" |
| "v.io/v23/verror" |
| _ "v.io/x/ref/runtime/factories/generic" |
| "v.io/x/ref/services/groups/internal/server" |
| "v.io/x/ref/services/groups/internal/store" |
| "v.io/x/ref/services/groups/internal/store/leveldb" |
| "v.io/x/ref/services/groups/internal/store/mem" |
| "v.io/x/ref/test" |
| "v.io/x/ref/test/testutil" |
| ) |
| |
| type backend int |
| |
| const ( |
| leveldbstore backend = iota |
| memstore |
| ) |
| |
| func Fatal(t *testing.T, args ...interface{}) { |
| debug.PrintStack() |
| t.Fatal(args...) |
| } |
| |
| func Fatalf(t *testing.T, format string, args ...interface{}) { |
| debug.PrintStack() |
| t.Fatalf(format, args...) |
| } |
| |
| func getEntriesOrDie(t *testing.T, ctx *context.T, g groups.GroupClientStub) map[groups.BlessingPatternChunk]struct{} { |
| res, _, err := g.Get(ctx, groups.GetRequest{}, "") |
| if err != nil { |
| Fatalf(t, "Get failed: %v", err) |
| } |
| return res.Entries |
| } |
| |
| func getPermsOrDie(t *testing.T, ctx *context.T, g groups.GroupClientStub) access.Permissions { |
| res, _, err := g.GetPermissions(ctx) |
| if err != nil { |
| Fatalf(t, "GetPermissions failed: %v", err) |
| } |
| return res |
| } |
| |
| func getVersionOrDie(t *testing.T, ctx *context.T, g groups.GroupClientStub) string { |
| _, version, err := g.Get(ctx, groups.GetRequest{}, "") |
| if err != nil { |
| Fatalf(t, "Get failed: %v", err) |
| } |
| return version |
| } |
| |
| func bpc(chunk string) groups.BlessingPatternChunk { |
| return groups.BlessingPatternChunk(chunk) |
| } |
| |
| func bpcSet(chunks ...string) map[groups.BlessingPatternChunk]struct{} { |
| res := map[groups.BlessingPatternChunk]struct{}{} |
| for _, chunk := range chunks { |
| res[bpc(chunk)] = struct{}{} |
| } |
| return res |
| } |
| |
| func bpcSlice(chunks ...string) []groups.BlessingPatternChunk { |
| res := []groups.BlessingPatternChunk{} |
| for _, chunk := range chunks { |
| res = append(res, bpc(chunk)) |
| } |
| return res |
| } |
| |
| func entriesEqual(a, b map[groups.BlessingPatternChunk]struct{}) bool { |
| // Unlike DeepEqual, we treat nil and empty maps as equivalent. |
| if len(a) == 0 && len(b) == 0 { |
| return true |
| } |
| return reflect.DeepEqual(a, b) |
| } |
| |
| // security.Authorizer implementation that disallows any operations on a |
| // "reserved*" suffix. Used to test that the provided authorization policy |
| // is applied to group creation operations. |
| type reservedAuthorizer struct{} |
| |
| func (reservedAuthorizer) Authorize(_ *context.T, call security.Call) error { |
| if strings.HasPrefix(call.Suffix(), "reserved") { |
| return fmt.Errorf("operations on %q are reserved and not authorized", call.Suffix()) |
| } |
| return nil |
| } |
| |
| func newServer(ctx *context.T, be backend) (string, func()) { |
| var st store.Store |
| var path string |
| var err error |
| |
| switch be { |
| case memstore: |
| st = mem.New() |
| case leveldbstore: |
| path, err = ioutil.TempDir("", "") |
| if err != nil { |
| ctx.Fatal("ioutil.TempDir() failed: ", err) |
| } |
| st, err = leveldb.Open(path) |
| if err != nil { |
| ctx.Fatal("leveldb.Open() failed: ", err) |
| } |
| default: |
| ctx.Fatal("unknown backend: ", be) |
| } |
| |
| m := server.NewManager(st, reservedAuthorizer{}) |
| |
| ctx, cancel := context.WithCancel(ctx) |
| ctx, server, err := v23.WithNewDispatchingServer(ctx, "", m) |
| if err != nil { |
| ctx.Fatal("NewDispatchingServer() failed: ", err) |
| } |
| |
| name := server.Status().Endpoints[0].Name() |
| return name, func() { |
| cancel() |
| <-server.Closed() |
| if path != "" { |
| os.RemoveAll(path) |
| } |
| } |
| } |
| |
| func setupOrDie(be backend) (clientCtx *context.T, serverName string, cleanup func()) { |
| ctx, shutdown := test.V23Init() |
| serverCtx, err := v23.WithPrincipal(ctx, testutil.NewPrincipal()) |
| if err != nil { |
| ctx.Fatal(err) |
| } |
| if clientCtx, err = v23.WithPrincipal(ctx, testutil.NewPrincipal()); err != nil { |
| ctx.Fatal(err) |
| } |
| |
| // Provide the client and server blessings from a common identity provider. |
| idp := testutil.NewIDProvider("idp") |
| if err := idp.Bless(v23.GetPrincipal(clientCtx), "client"); err != nil { |
| ctx.Fatal(err) |
| } |
| if err := idp.Bless(v23.GetPrincipal(serverCtx), "server"); err != nil { |
| ctx.Fatal(err) |
| } |
| serverName, stopServer := newServer(serverCtx, be) |
| cleanup = func() { |
| stopServer() |
| shutdown() |
| } |
| return |
| } |
| |
| //////////////////////////////////////// |
| // Test cases |
| |
| func TestCreateMemStore(t *testing.T) { |
| testCreateHelper(t, memstore) |
| } |
| |
| func testCreateHelper(t *testing.T, be backend) { |
| ctx, serverName, cleanup := setupOrDie(be) |
| defer cleanup() |
| |
| // Unauthorized creates should fail. |
| // This fails because of the reservedAuthorizer used in setupOrDie. |
| if err := groups.GroupClient(naming.JoinAddressName(serverName, "reservedGroup")).Create(ctx, nil, nil); err == nil { |
| t.Errorf("Creation of reservedGroup succeeded") |
| } |
| // Create a group with a default perms and no entries. |
| g := groups.GroupClient(naming.JoinAddressName(serverName, "grpA")) |
| if err := g.Create(ctx, nil, nil); err != nil { |
| t.Fatalf("Create failed: %v", err) |
| } |
| // Verify perms of created group. |
| perms := access.Permissions{} |
| for _, tag := range access.AllTypicalTags() { |
| perms.Add(security.BlessingPattern("idp:client"), string(tag)) |
| } |
| gotPermissions, wantPermissions := getPermsOrDie(t, ctx, g), perms |
| if !reflect.DeepEqual(gotPermissions, wantPermissions) { |
| t.Errorf("Permissions do not match: got %v, want %v", gotPermissions, wantPermissions) |
| } |
| // Verify entries of created group. |
| got, want := getEntriesOrDie(t, ctx, g), bpcSet() |
| if !entriesEqual(got, want) { |
| t.Errorf("Entries do not match: got %v, want %v", got, want) |
| } |
| |
| // Creating same group again should fail, since the group already exists. |
| g = groups.GroupClient(naming.JoinAddressName(serverName, "grpA")) |
| if err := g.Create(ctx, nil, nil); verror.ErrorID(err) != verror.ErrExist.ID { |
| t.Fatalf("Create should have failed: %v", err) |
| } |
| |
| // Create a group with perms and a few entries, including some redundant ones. |
| g = groups.GroupClient(naming.JoinAddressName(serverName, "grpB")) |
| perms = access.Permissions{} |
| // Allow Admin and Read so that we can call GetPermissions and Get. |
| for _, tag := range []access.Tag{access.Admin, access.Read} { |
| perms.Add(security.BlessingPattern("idp:client"), string(tag)) |
| } |
| if err := g.Create(ctx, perms, bpcSlice("foo", "bar", "foo")); err != nil { |
| t.Fatalf("Create failed: %v", err) |
| } |
| // Verify perms of created group. |
| gotPermissions, wantPermissions = getPermsOrDie(t, ctx, g), perms |
| if !reflect.DeepEqual(gotPermissions, wantPermissions) { |
| t.Errorf("Permissions do not match: got %v, want %v", gotPermissions, wantPermissions) |
| } |
| // Verify entries of created group. |
| got, want = getEntriesOrDie(t, ctx, g), bpcSet("foo", "bar") |
| if !entriesEqual(got, want) { |
| t.Errorf("Entries do not match: got %v, want %v", got, want) |
| } |
| } |
| |
| func TestDeleteMemStore(t *testing.T) { |
| testDeleteHelper(t, memstore) |
| } |
| |
| func testDeleteHelper(t *testing.T, be backend) { |
| ctx, serverName, cleanup := setupOrDie(be) |
| defer cleanup() |
| |
| // Create a group with a default perms and no entries, check that we can |
| // delete it. |
| g := groups.GroupClient(naming.JoinAddressName(serverName, "grpA")) |
| if err := g.Create(ctx, nil, nil); err != nil { |
| t.Fatalf("Create failed: %v", err) |
| } |
| // Delete with bad version should fail. |
| if err := g.Delete(ctx, "20"); verror.ErrorID(err) != verror.ErrBadVersion.ID { |
| t.Fatalf("Delete should have failed with version error: %v", err) |
| } |
| // Delete with correct version should succeed. |
| version := getVersionOrDie(t, ctx, g) |
| if err := g.Delete(ctx, version); err != nil { |
| t.Fatalf("Delete failed: %v", err) |
| } |
| // Check that the group was actually deleted. |
| if _, _, err := g.Get(ctx, groups.GetRequest{}, ""); verror.ErrorID(err) != verror.ErrNoExist.ID { |
| t.Fatal("Group was not deleted") |
| } |
| |
| // Create a group with several entries, check that we can delete it. |
| g = groups.GroupClient(naming.JoinAddressName(serverName, "grpB")) |
| if err := g.Create(ctx, nil, bpcSlice("foo", "bar", "foo")); err != nil { |
| t.Fatalf("Create failed: %v", err) |
| } |
| // Delete with empty version should succeed. |
| if err := g.Delete(ctx, ""); err != nil { |
| t.Fatalf("Delete failed: %v", err) |
| } |
| // Check that the group was actually deleted. |
| if _, _, err := g.Get(ctx, groups.GetRequest{}, ""); verror.ErrorID(err) != verror.ErrNoExist.ID { |
| t.Fatal("Group was not deleted") |
| } |
| // Check that Delete is idempotent. |
| if err := g.Delete(ctx, ""); err != nil { |
| t.Fatalf("Delete failed: %v", err) |
| } |
| // Check that we can recreate a group that was deleted. |
| if err := g.Create(ctx, nil, nil); err != nil { |
| t.Fatalf("Create failed: %v", err) |
| } |
| |
| // Create a group with perms that disallow Delete(), check that Delete() |
| // fails. |
| g = groups.GroupClient(naming.JoinAddressName(serverName, "grpC")) |
| perms := access.Permissions{} |
| perms.Add(security.BlessingPattern("idp:client"), string(access.Admin)) |
| if err := g.Create(ctx, perms, nil); err != nil { |
| t.Fatalf("Create failed: %v", err) |
| } |
| // Delete should fail (no access). |
| if err := g.Delete(ctx, ""); verror.ErrorID(err) != verror.ErrNoAccess.ID { |
| t.Fatalf("Delete should have failed with access error: %v", err) |
| } |
| } |
| |
| func TestPermsMemStore(t *testing.T) { |
| testPermsHelper(t, memstore) |
| } |
| |
| func testPermsHelper(t *testing.T, be backend) { |
| ctx, serverName, cleanup := setupOrDie(be) |
| defer cleanup() |
| |
| // Create a group with a default perms. |
| g := groups.GroupClient(naming.JoinAddressName(serverName, "grpA")) |
| if err := g.Create(ctx, nil, nil); err != nil { |
| t.Fatalf("Create failed: %v", err) |
| } |
| |
| // Use "ac" so the code below can exactly match code in syncbase. |
| // TODO(sadovsky): All Vanadium {Set,Get}Permissions tests ought to share this |
| // test implementation. |
| ac := g |
| |
| // Mirrors syncbase/v23/syncbase/testutil/layer.go. |
| myperms := access.Permissions{} |
| myperms.Add(security.BlessingPattern("idp:client"), string(access.Admin)) |
| // Demonstrate that myperms differs from the current perms. |
| if reflect.DeepEqual(myperms, getPermsOrDie(t, ctx, ac)) { |
| t.Fatalf("Permissions should not match: %v", myperms) |
| } |
| |
| var permsBefore, permsAfter access.Permissions |
| var versionBefore, versionAfter string |
| |
| getPermsAndVersionOrDie := func() (access.Permissions, string) { |
| perms, version, err := ac.GetPermissions(ctx) |
| if err != nil { |
| // Use Fatalf rather than t.Fatalf so we get a stack trace. |
| Fatalf(t, "GetPermissions failed: %v", err) |
| } |
| return perms, version |
| } |
| |
| // SetPermissions with bad version should fail. |
| permsBefore, versionBefore = getPermsAndVersionOrDie() |
| if err := ac.SetPermissions(ctx, myperms, "20"); verror.ErrorID(err) != verror.ErrBadVersion.ID { |
| t.Fatalf("SetPermissions should have failed with version error: %v", err) |
| } |
| // Since SetPermissions failed, perms and version should not have changed. |
| permsAfter, versionAfter = getPermsAndVersionOrDie() |
| if !reflect.DeepEqual(permsAfter, permsBefore) { |
| t.Errorf("Perms do not match: got %v, want %v", permsAfter, permsBefore) |
| } |
| if versionAfter != versionBefore { |
| t.Errorf("Versions do not match: got %v, want %v", versionAfter, versionBefore) |
| } |
| |
| // SetPermissions with correct version should succeed. |
| permsBefore, versionBefore = permsAfter, versionAfter |
| if err := ac.SetPermissions(ctx, myperms, versionBefore); err != nil { |
| t.Fatalf("SetPermissions failed: %v", err) |
| } |
| // Check that perms and version actually changed. |
| permsAfter, versionAfter = getPermsAndVersionOrDie() |
| if !reflect.DeepEqual(permsAfter, myperms) { |
| t.Errorf("Perms do not match: got %v, want %v", permsAfter, myperms) |
| } |
| if versionBefore == versionAfter { |
| t.Errorf("Versions should not match: %v", versionBefore) |
| } |
| |
| // SetPermissions with empty version should succeed. |
| permsBefore, versionBefore = permsAfter, versionAfter |
| myperms.Add(security.BlessingPattern("idp:client"), string(access.Read)) |
| if err := ac.SetPermissions(ctx, myperms, ""); err != nil { |
| t.Fatalf("SetPermissions failed: %v", err) |
| } |
| // Check that perms and version actually changed. |
| permsAfter, versionAfter = getPermsAndVersionOrDie() |
| if !reflect.DeepEqual(permsAfter, myperms) { |
| t.Errorf("Perms do not match: got %v, want %v", permsAfter, myperms) |
| } |
| if versionBefore == versionAfter { |
| t.Errorf("Versions should not match: %v", versionBefore) |
| } |
| |
| // SetPermissions with unchanged perms should succeed, and version should |
| // still change. |
| permsBefore, versionBefore = permsAfter, versionAfter |
| if err := ac.SetPermissions(ctx, myperms, ""); err != nil { |
| t.Fatalf("SetPermissions failed: %v", err) |
| } |
| // Check that perms did not change and version did change. |
| permsAfter, versionAfter = getPermsAndVersionOrDie() |
| if !reflect.DeepEqual(permsAfter, permsBefore) { |
| t.Errorf("Perms do not match: got %v, want %v", permsAfter, permsBefore) |
| } |
| if versionBefore == versionAfter { |
| t.Errorf("Versions should not match: %v", versionBefore) |
| } |
| |
| // Take away our access. SetPermissions and GetPermissions should fail. |
| if err := ac.SetPermissions(ctx, access.Permissions{}, ""); err != nil { |
| t.Fatalf("SetPermissions failed: %v", err) |
| } |
| if _, _, err := ac.GetPermissions(ctx); verror.ErrorID(err) != verror.ErrNoAccess.ID { |
| t.Fatalf("GetPermissions should have failed with access error: %v", err) |
| } |
| if err := ac.SetPermissions(ctx, myperms, ""); verror.ErrorID(err) != verror.ErrNoAccess.ID { |
| t.Fatalf("SetPermissions should have failed with access error: %v", err) |
| } |
| } |
| |
| func TestAddMemStore(t *testing.T) { |
| testAddHelper(t, memstore) |
| } |
| |
| // testAddHelper tests mirror testRemoveHelper tests. |
| func testAddHelper(t *testing.T, be backend) { |
| ctx, serverName, cleanup := setupOrDie(be) |
| defer cleanup() |
| |
| // Create a group with a default perms and no entries. |
| g := groups.GroupClient(naming.JoinAddressName(serverName, "grpA")) |
| if err := g.Create(ctx, nil, nil); err != nil { |
| t.Fatalf("Create failed: %v", err) |
| } |
| // Verify entries of created group. |
| got, want := getEntriesOrDie(t, ctx, g), bpcSet() |
| if !entriesEqual(got, want) { |
| t.Errorf("Entries do not match: got %v, want %v", got, want) |
| } |
| |
| var versionBefore, versionAfter string |
| versionBefore = getVersionOrDie(t, ctx, g) |
| // Add with bad version should fail. |
| if err := g.Add(ctx, bpc("foo"), "20"); verror.ErrorID(err) != verror.ErrBadVersion.ID { |
| t.Fatalf("Add should have failed with version error: %v", err) |
| } |
| // Version should not have changed. |
| versionAfter = getVersionOrDie(t, ctx, g) |
| if versionAfter != versionBefore { |
| t.Errorf("Versions do not match: got %v, want %v", versionAfter, versionBefore) |
| } |
| |
| // Add an entry, verify it was added and the version changed. |
| versionBefore = versionAfter |
| if err := g.Add(ctx, bpc("foo"), versionBefore); err != nil { |
| t.Fatalf("Add failed: %v", err) |
| } |
| got, want = getEntriesOrDie(t, ctx, g), bpcSet("foo") |
| if !entriesEqual(got, want) { |
| t.Errorf("Entries do not match: got %v, want %v", got, want) |
| } |
| versionAfter = getVersionOrDie(t, ctx, g) |
| if versionBefore == versionAfter { |
| t.Errorf("Versions should not match: %v", versionBefore) |
| } |
| |
| // Add another entry, verify it was added and the version changed. |
| versionBefore = versionAfter |
| // Add with empty version should succeed. |
| if err := g.Add(ctx, bpc("bar"), ""); err != nil { |
| t.Fatalf("Add failed: %v", err) |
| } |
| got, want = getEntriesOrDie(t, ctx, g), bpcSet("foo", "bar") |
| if !entriesEqual(got, want) { |
| t.Errorf("Entries do not match: got %v, want %v", got, want) |
| } |
| versionAfter = getVersionOrDie(t, ctx, g) |
| if versionBefore == versionAfter { |
| t.Errorf("Versions should not match: %v", versionBefore) |
| } |
| |
| // Add "bar" again, verify entries are still ["foo", "bar"] and the version |
| // changed. |
| versionBefore = versionAfter |
| if err := g.Add(ctx, bpc("bar"), versionBefore); err != nil { |
| t.Fatalf("Add failed: %v", err) |
| } |
| got, want = getEntriesOrDie(t, ctx, g), bpcSet("foo", "bar") |
| if !entriesEqual(got, want) { |
| t.Errorf("Entries do not match: got %v, want %v", got, want) |
| } |
| versionAfter = getVersionOrDie(t, ctx, g) |
| if versionBefore == versionAfter { |
| t.Errorf("Versions should not match: %v", versionBefore) |
| } |
| |
| // Create a group with perms that disallow Add(), check that Add() fails. |
| g = groups.GroupClient(naming.JoinAddressName(serverName, "grpB")) |
| perms := access.Permissions{} |
| perms.Add(security.BlessingPattern("idp:client"), string(access.Admin)) |
| if err := g.Create(ctx, perms, nil); err != nil { |
| t.Fatalf("Create failed: %v", err) |
| } |
| // Add should fail (no access). |
| if err := g.Add(ctx, bpc("foo"), ""); verror.ErrorID(err) != verror.ErrNoAccess.ID { |
| t.Fatalf("Add should have failed with access error: %v", err) |
| } |
| } |
| |
| func TestRemoveMemStore(t *testing.T) { |
| testRemoveHelper(t, memstore) |
| } |
| |
| // testRemoveHelper tests mirror testAddHelper tests. |
| func testRemoveHelper(t *testing.T, be backend) { |
| ctx, serverName, cleanup := setupOrDie(be) |
| defer cleanup() |
| |
| // Create a group with a default perms and two entries. |
| g := groups.GroupClient(naming.JoinAddressName(serverName, "grpA")) |
| if err := g.Create(ctx, nil, bpcSlice("foo", "bar")); err != nil { |
| t.Fatalf("Create failed: %v", err) |
| } |
| // Verify entries of created group. |
| got, want := getEntriesOrDie(t, ctx, g), bpcSet("foo", "bar") |
| if !entriesEqual(got, want) { |
| t.Errorf("Entries do not match: got %v, want %v", got, want) |
| } |
| |
| var versionBefore, versionAfter string |
| versionBefore = getVersionOrDie(t, ctx, g) |
| // Remove with bad version should fail. |
| if err := g.Remove(ctx, bpc("foo"), "20"); verror.ErrorID(err) != verror.ErrBadVersion.ID { |
| t.Fatalf("Remove should have failed with version error: %v", err) |
| } |
| // Version should not have changed. |
| versionAfter = getVersionOrDie(t, ctx, g) |
| if versionAfter != versionBefore { |
| t.Errorf("Versions do not match: got %v, want %v", versionAfter, versionBefore) |
| } |
| |
| // Remove an entry, verify it was removed and the version changed. |
| versionBefore = versionAfter |
| if err := g.Remove(ctx, bpc("foo"), versionBefore); err != nil { |
| t.Fatalf("Remove failed: %v", err) |
| } |
| got, want = getEntriesOrDie(t, ctx, g), bpcSet("bar") |
| if !entriesEqual(got, want) { |
| t.Errorf("Entries do not match: got %v, want %v", got, want) |
| } |
| versionAfter = getVersionOrDie(t, ctx, g) |
| if versionBefore == versionAfter { |
| t.Errorf("Versions should not match: %v", versionBefore) |
| } |
| |
| // Remove another entry, verify it was removed and the version changed. |
| versionBefore = versionAfter |
| // Remove with empty version should succeed. |
| if err := g.Remove(ctx, bpc("bar"), ""); err != nil { |
| t.Fatalf("Remove failed: %v", err) |
| } |
| got, want = getEntriesOrDie(t, ctx, g), bpcSet() |
| if !entriesEqual(got, want) { |
| t.Errorf("Entries do not match: got %v, want %v", got, want) |
| } |
| versionAfter = getVersionOrDie(t, ctx, g) |
| if versionBefore == versionAfter { |
| t.Errorf("Versions should not match: %v", versionBefore) |
| } |
| |
| // Remove "bar" again, verify entries are still [] and the version changed. |
| versionBefore = versionAfter |
| if err := g.Remove(ctx, bpc("bar"), versionBefore); err != nil { |
| t.Fatalf("Remove failed: %v", err) |
| } |
| got, want = getEntriesOrDie(t, ctx, g), bpcSet() |
| if !entriesEqual(got, want) { |
| t.Errorf("Entries do not match: got %v, want %v", got, want) |
| } |
| versionAfter = getVersionOrDie(t, ctx, g) |
| if versionBefore == versionAfter { |
| t.Errorf("Versions should not match: %v", versionBefore) |
| } |
| |
| // Create a group with perms that disallow Remove(), check that Remove() |
| // fails. |
| g = groups.GroupClient(naming.JoinAddressName(serverName, "grpB")) |
| perms := access.Permissions{} |
| perms.Add(security.BlessingPattern("idp:client"), string(access.Admin)) |
| if err := g.Create(ctx, perms, bpcSlice("foo", "bar")); err != nil { |
| t.Fatalf("Create failed: %v", err) |
| } |
| // Remove should fail (no access). |
| if err := g.Remove(ctx, bpc("foo"), ""); verror.ErrorID(err) != verror.ErrNoAccess.ID { |
| t.Fatalf("Remove should have failed with access error: %v", err) |
| } |
| } |
| |
| func TestGet(t *testing.T) { |
| // TODO(sadovsky): Implement. |
| } |
| |
| func TestRest(t *testing.T) { |
| // TODO(sadovsky): Implement. |
| } |