Merge "ref/services/groups: a command-line client for interacting with the group server"
diff --git a/services/groups/groups/doc.go b/services/groups/groups/doc.go
new file mode 100644
index 0000000..fec9b80
--- /dev/null
+++ b/services/groups/groups/doc.go
@@ -0,0 +1,140 @@
+// 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.
+
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+/*
+Command groups creates and manages Vanadium groups of blessing patterns.
+
+Usage:
+   groups <command>
+
+The groups commands are:
+   create      Creates a blessing pattern group
+   delete      Delete a blessing group
+   add         Adds a blessing pattern to a group
+   remove      Removes a blessing pattern from a group
+   relate      Relate a set of blessing to a group
+   get         Returns entries of a group
+   help        Display help for commands or topics
+
+The global flags are:
+ -v23.metadata=<just specify -v23.metadata to activate>
+   Displays metadata for the program and exits.
+
+Groups create - Creates a blessing pattern group
+
+Creates a blessing pattern group.
+
+Usage:
+   groups create [flags] <von> <patterns...>
+
+<von> is the vanadium object name of the group to create
+
+<patterns...> is a list of blessing pattern chunks
+
+The groups create flags are:
+ -permissions=
+   Path to a permissions file
+
+Groups delete - Delete a blessing group
+
+Delete a blessing group.
+
+Usage:
+   groups delete [flags] <von>
+
+<von> is the vanadium object name of the group
+
+The groups delete flags are:
+ -version=
+   Identifies group version
+
+Groups add - Adds a blessing pattern to a group
+
+Adds a blessing pattern to a group.
+
+Usage:
+   groups add [flags] <von> <pattern>
+
+<von> is the vanadium object name of the group
+
+<pattern> is the blessing pattern chunk to add
+
+The groups add flags are:
+ -version=
+   Identifies group version
+
+Groups remove - Removes a blessing pattern from a group
+
+Removes a blessing pattern from a group.
+
+Usage:
+   groups remove [flags] <von> <pattern>
+
+<von> is the vanadium object name of the group
+
+<pattern> is the blessing pattern chunk to add
+
+The groups remove flags are:
+ -version=
+   Identifies group version
+
+Groups relate - Relate a set of blessing to a group
+
+Relate a set of blessing to a group.
+
+NOTE: This command exists primarily for debugging purposes. In particular,
+invocations of the Relate RPC are expected to be mainly issued by the
+authorization logic.
+
+Usage:
+   groups relate [flags] <von> <blessings>
+
+<von> is the vanadium object name of the group
+
+<blessings...> is a list of blessings
+
+The groups relate flags are:
+ -approximation=under
+   Identifies the type of approximation to use; supported values = (under, over)
+ -version=
+   Identifies group version
+
+Groups get - Returns entries of a group
+
+Returns entries of a group.
+
+Usage:
+   groups get <von>
+
+<von> is the vanadium object name of the group
+
+Groups help - Display help for commands or topics
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+Usage:
+   groups help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The groups help flags are:
+ -style=compact
+   The formatting style for help output:
+      compact - Good for compact cmdline output.
+      full    - Good for cmdline output, shows all global flags.
+      godoc   - Good for godoc processing.
+   Override the default by setting the CMDLINE_STYLE environment variable.
+ -width=<terminal width>
+   Format output to this target width in runes, or unlimited if width < 0.
+   Defaults to the terminal width if available.  Override the default by setting
+   the CMDLINE_WIDTH environment variable.
+*/
+package main
diff --git a/services/groups/groups/main.go b/services/groups/groups/main.go
new file mode 100644
index 0000000..e15b7dc
--- /dev/null
+++ b/services/groups/groups/main.go
@@ -0,0 +1,206 @@
+// 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.
+
+// The following enables go generate to generate the doc.go file.
+//go:generate go run $V23_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go .
+
+package main
+
+import (
+	"fmt"
+	"os"
+
+	"v.io/v23/context"
+	"v.io/v23/security/access"
+	"v.io/v23/services/groups"
+	"v.io/x/lib/cmdline"
+	"v.io/x/ref/lib/v23cmd"
+)
+
+var (
+	flagPermFile      string
+	flagVersion       string
+	flagApproximation string
+
+	cmdCreate = &cmdline.Command{
+		Name:     "create",
+		Short:    "Creates a blessing pattern group",
+		Long:     "Creates a blessing pattern group.",
+		ArgsName: "<von> <patterns...>",
+		ArgsLong: `
+<von> is the vanadium object name of the group to create
+
+<patterns...> is a list of blessing pattern chunks
+`,
+		Runner: v23cmd.RunnerFunc(func(ctx *context.T, env *cmdline.Env, args []string) error {
+			// Process command-line arguments.
+			if want, got := 1, len(args); want > got {
+				return env.UsageErrorf("create: unexpected number of arguments, want at least %d, got %d", want, got)
+			}
+			von := args[0]
+			var patterns []groups.BlessingPatternChunk
+			for _, pattern := range args[1:] {
+				patterns = append(patterns, groups.BlessingPatternChunk(pattern))
+			}
+			// Process command-line flags.
+			var permissions access.Permissions
+			if flagPermFile != "" {
+				file, err := os.Open(flagPermFile)
+				if err != nil {
+					return fmt.Errorf("Open(%v) failed: %v", flagPermFile, err)
+				}
+				permissions, err = access.ReadPermissions(file)
+				if err != nil {
+					return err
+				}
+			}
+			// Invoke the "create" RPC.
+			client := groups.GroupClient(von)
+			return client.Create(ctx, permissions, patterns)
+		}),
+	}
+
+	cmdDelete = &cmdline.Command{
+		Name:     "delete",
+		Short:    "Delete a blessing group",
+		Long:     "Delete a blessing group.",
+		ArgsName: "<von>",
+		ArgsLong: "<von> is the vanadium object name of the group",
+		Runner: v23cmd.RunnerFunc(func(ctx *context.T, env *cmdline.Env, args []string) error {
+			// Process command-line arguments.
+			if want, got := 1, len(args); want != got {
+				return env.UsageErrorf("delete: unexpected number of arguments, want %d, got %d", want, got)
+			}
+			von := args[0]
+			// Invoke the "delete" RPC.
+			client := groups.GroupClient(von)
+			return client.Delete(ctx, flagVersion)
+		}),
+	}
+
+	cmdAdd = &cmdline.Command{
+		Name:     "add",
+		Short:    "Adds a blessing pattern to a group",
+		Long:     "Adds a blessing pattern to a group.",
+		ArgsName: "<von> <pattern>",
+		ArgsLong: `
+<von> is the vanadium object name of the group
+
+<pattern> is the blessing pattern chunk to add
+`,
+		Runner: v23cmd.RunnerFunc(func(ctx *context.T, env *cmdline.Env, args []string) error {
+			// Process command-line arguments.
+			if want, got := 2, len(args); want != got {
+				return env.UsageErrorf("add: unexpected number of arguments, want %d, got %d", want, got)
+			}
+			von, pattern := args[0], args[1]
+			// Invoke the "add" RPC.
+			client := groups.GroupClient(von)
+			return client.Add(ctx, groups.BlessingPatternChunk(pattern), flagVersion)
+		}),
+	}
+
+	cmdRemove = &cmdline.Command{
+		Name:     "remove",
+		Short:    "Removes a blessing pattern from a group",
+		Long:     "Removes a blessing pattern from a group.",
+		ArgsName: "<von> <pattern>",
+		ArgsLong: `
+<von> is the vanadium object name of the group
+
+<pattern> is the blessing pattern chunk to add
+`,
+		Runner: v23cmd.RunnerFunc(func(ctx *context.T, env *cmdline.Env, args []string) error {
+			// Process command-line arguments.
+			if want, got := 2, len(args); want != got {
+				return env.UsageErrorf("remove: unexpected number of arguments, want %d, got %d", want, got)
+			}
+			von, pattern := args[0], args[1]
+			// Invoke the "remove" RPC.
+			client := groups.GroupClient(von)
+			return client.Remove(ctx, groups.BlessingPatternChunk(pattern), flagVersion)
+		}),
+	}
+
+	cmdRelate = &cmdline.Command{
+		Name:  "relate",
+		Short: "Relate a set of blessing to a group",
+		Long: `
+Relate a set of blessing to a group.
+
+NOTE: This command exists primarily for debugging purposes. In
+particular, invocations of the Relate RPC are expected to be mainly
+issued by the authorization logic.
+`,
+		ArgsName: "<von> <blessings>",
+		ArgsLong: `
+<von> is the vanadium object name of the group
+
+<blessings...> is a list of blessings
+`,
+		Runner: v23cmd.RunnerFunc(func(ctx *context.T, env *cmdline.Env, args []string) error {
+			// Process command-line arguments.
+			if want, got := 1, len(args); want > got {
+				return env.UsageErrorf("relate: unexpected number of arguments, want at least %d, got %d", want, got)
+			}
+			von := args[0]
+			blessings := make(map[string]struct{}, len(args[1:]))
+			for _, blessing := range args[1:] {
+				blessings[blessing] = struct{}{}
+			}
+			// Process command-line flags.
+			var hint groups.ApproximationType
+			switch flagApproximation {
+			case "under":
+				hint = groups.ApproximationTypeUnder
+			case "over":
+				hint = groups.ApproximationTypeOver
+			}
+			// Invoke the "relate" RPC.
+			client := groups.GroupClient(von)
+			remainder, approximations, version, err := client.Relate(ctx, blessings, hint, flagVersion, nil)
+			if err != nil {
+				return err
+			}
+			fmt.Fprintf(env.Stdout, "remainder = %v, approximations = %v, version = %v\n", remainder, approximations, version)
+			return nil
+		}),
+	}
+
+	cmdGet = &cmdline.Command{
+		Name:     "get",
+		Short:    "Returns entries of a group",
+		Long:     "Returns entries of a group.",
+		ArgsName: "<von>",
+		ArgsLong: "<von> is the vanadium object name of the group",
+		Runner: v23cmd.RunnerFunc(func(ctx *context.T, env *cmdline.Env, args []string) error {
+			// TODO(jsimsa): Implement once the corresponding server-side
+			// API is designed.
+			return fmt.Errorf(`the "groups get ..." sub-command is not implemented`)
+		}),
+	}
+
+	cmdRoot = &cmdline.Command{
+		Name:     "groups",
+		Short:    "creates and manages Vanadium groups of blessing patterns",
+		Long:     "Command groups creates and manages Vanadium groups of blessing patterns.",
+		Children: []*cmdline.Command{cmdCreate, cmdDelete, cmdAdd, cmdRemove, cmdRelate, cmdGet},
+	}
+)
+
+func init() {
+	cmdCreate.Flags.StringVar(&flagPermFile, "permissions", "", "Path to a permissions file")
+	cmdDelete.Flags.StringVar(&flagVersion, "version", "", "Identifies group version")
+	cmdAdd.Flags.StringVar(&flagVersion, "version", "", "Identifies group version")
+	cmdRemove.Flags.StringVar(&flagVersion, "version", "", "Identifies group version")
+	cmdRelate.Flags.StringVar(&flagVersion, "version", "", "Identifies group version")
+	cmdRelate.Flags.StringVar(&flagApproximation, "approximation", "under",
+		"Identifies the type of approximation to use; supported values = (under, over)",
+	)
+}
+
+func main() {
+	cmdline.HideGlobalFlagsExcept()
+	cmdline.Main(cmdRoot)
+}
diff --git a/services/groups/groups/main_test.go b/services/groups/groups/main_test.go
new file mode 100644
index 0000000..f36de00
--- /dev/null
+++ b/services/groups/groups/main_test.go
@@ -0,0 +1,216 @@
+// 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 main
+
+import (
+	"bytes"
+	"fmt"
+	"strings"
+	"testing"
+	"unicode"
+	"unicode/utf8"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/naming"
+	"v.io/v23/rpc"
+	"v.io/v23/security/access"
+	"v.io/v23/services/groups"
+	"v.io/x/lib/cmdline"
+	"v.io/x/ref/lib/v23cmd"
+	_ "v.io/x/ref/runtime/factories/generic"
+	"v.io/x/ref/test"
+)
+
+var group map[string]struct{}
+var buffer bytes.Buffer
+
+type mock struct{}
+
+func (mock) Create(ctx *context.T, call rpc.ServerCall, perms access.Permissions, entries []groups.BlessingPatternChunk) error {
+	fmt.Fprintf(&buffer, "Create(%v, %v) was called", perms, entries)
+	group = make(map[string]struct{}, len(entries))
+	for _, entry := range entries {
+		group[string(entry)] = struct{}{}
+	}
+	return nil
+}
+
+func (mock) Delete(ctx *context.T, call rpc.ServerCall, version string) error {
+	fmt.Fprintf(&buffer, "Delete(%v) was called", version)
+	group = nil
+	return nil
+}
+
+func (mock) Add(ctx *context.T, call rpc.ServerCall, entry groups.BlessingPatternChunk, version string) error {
+	fmt.Fprintf(&buffer, "Add(%v, %v) was called", entry, version)
+	group[string(entry)] = struct{}{}
+	return nil
+}
+
+func (mock) Remove(ctx *context.T, call rpc.ServerCall, entry groups.BlessingPatternChunk, version string) error {
+	fmt.Fprintf(&buffer, "Remove(%v, %v) was called", entry, version)
+	delete(group, string(entry))
+	return nil
+}
+
+func (mock) Relate(ctx *context.T, call rpc.ServerCall, blessings map[string]struct{}, hint groups.ApproximationType, version string, visitedGroups map[string]struct{}) (map[string]struct{}, []groups.Approximation, string, error) {
+	fmt.Fprintf(&buffer, "Relate(%v, %v, %v, %v) was called", blessings, hint, version, visitedGroups)
+	return nil, nil, "123", nil
+}
+
+func (mock) Get(ctx *context.T, call rpc.ServerCall, req groups.GetRequest, version string) (groups.GetResponse, string, error) {
+	return groups.GetResponse{}, "", nil
+}
+
+func (mock) SetPermissions(ctx *context.T, call rpc.ServerCall, perms access.Permissions, version string) error {
+	return nil
+}
+
+func (mock) GetPermissions(ctx *context.T, call rpc.ServerCall) (access.Permissions, string, error) {
+	return nil, "", nil
+}
+
+func capitalize(s string) string {
+	if s == "" {
+		return ""
+	}
+	rune, size := utf8.DecodeRuneInString(s)
+	return string(unicode.ToUpper(rune)) + s[size:]
+}
+
+func startServer(ctx *context.T, t *testing.T) (rpc.Server, naming.Endpoint) {
+	server, err := v23.NewServer(ctx)
+	if err != nil {
+		t.Fatalf("NewServer failed: %v", err)
+	}
+	l := v23.GetListenSpec(ctx)
+	endpoints, err := server.Listen(l)
+	if err != nil {
+		t.Fatalf("Listen(%s) failed: %v", l, err)
+	}
+	unpublished := ""
+	if err := server.Serve(unpublished, groups.GroupServer(&mock{}), nil); err != nil {
+		t.Fatalf("Serve(%v) failed: %v", unpublished, err)
+	}
+	return server, endpoints[0]
+}
+
+func stopServer(t *testing.T, server rpc.Server) {
+	if err := server.Stop(); err != nil {
+		t.Errorf("Stop() failed: %v", err)
+	}
+}
+
+func TestGroupClient(t *testing.T) {
+	ctx, shutdown := test.V23Init()
+	defer shutdown()
+
+	server, endpoint := startServer(ctx, t)
+	defer stopServer(t, server)
+
+	// Test the "create" command.
+	{
+		var stdout, stderr bytes.Buffer
+		env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr}
+		patterns := []string{"alice", "bob"}
+		args := append([]string{"create", naming.JoinAddressName(endpoint.String(), "")}, patterns...)
+		if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, args); err != nil {
+			t.Fatalf("run failed: %v\n%v", err, stderr.String())
+		}
+		if got, want := strings.TrimSpace(stdout.String()), ""; got != want {
+			t.Errorf("got %q, want %q", got, want)
+		}
+		if got, want := buffer.String(), fmt.Sprintf("Create(%v, %v) was called", access.Permissions{}, patterns); got != want {
+			t.Errorf("got %v, want %v", got, want)
+		}
+		if got, want := len(group), 2; got != want {
+			t.Errorf("got %v, want %v", got, want)
+		}
+		buffer.Reset()
+	}
+
+	// Test the "add" command.
+	{
+		var stdout, stderr bytes.Buffer
+		env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr}
+		pattern, version := "charlie", "123"
+		args := []string{"add", "-version=" + version, naming.JoinAddressName(endpoint.String(), ""), pattern}
+		if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, args); err != nil {
+			t.Fatalf("run failed: %v\n%v", err, stderr.String())
+		}
+		if got, want := strings.TrimSpace(stdout.String()), ""; got != want {
+			t.Errorf("got %q, want %q", got, want)
+		}
+		if got, want := buffer.String(), fmt.Sprintf("Add(%v, %v) was called", pattern, version); got != want {
+			t.Errorf("got %v, want %v", got, want)
+		}
+		if got, want := len(group), 3; got != want {
+			t.Errorf("got %v, want %v", got, want)
+		}
+		buffer.Reset()
+	}
+
+	// Test the "remove" command.
+	{
+		var stdout, stderr bytes.Buffer
+		env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr}
+		pattern, version := "bob", "123"
+		args := []string{"remove", "-version=" + version, naming.JoinAddressName(endpoint.String(), ""), "bob"}
+		if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, args); err != nil {
+			t.Fatalf("run failed: %v\n%v", err, stderr.String())
+		}
+		if got, want := strings.TrimSpace(stdout.String()), ""; got != want {
+			t.Errorf("got %q, want %q", got, want)
+		}
+		if got, want := buffer.String(), fmt.Sprintf("Remove(%v, %v) was called", pattern, version); got != want {
+			t.Errorf("got %v, want %v", got, want)
+		}
+		if got, want := len(group), 2; got != want {
+			t.Errorf("got %v, want %v", got, want)
+		}
+		buffer.Reset()
+	}
+
+	// Test the "delete" command.
+	{
+		var stdout, stderr bytes.Buffer
+		env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr}
+		version := "123"
+		args := []string{"delete", "-version=" + version, naming.JoinAddressName(endpoint.String(), "")}
+		if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, args); err != nil {
+			t.Fatalf("run failed: %v\n%v", err, stderr.String())
+		}
+		if got, want := strings.TrimSpace(stdout.String()), ""; got != want {
+			t.Errorf("got %q, want %q", got, want)
+		}
+		if got, want := buffer.String(), fmt.Sprintf("Delete(%v) was called", version); got != want {
+			t.Errorf("got %v, want %v", got, want)
+		}
+		if got, want := len(group), 0; got != want {
+			t.Errorf("got %v, want %v", got, want)
+		}
+		buffer.Reset()
+	}
+
+	// Test the "relate" command.
+	{
+		var stdout, stderr bytes.Buffer
+		env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr}
+		hint, version := "over", "123"
+		args := []string{"relate", "-approximation=" + hint, "-version=" + version, naming.JoinAddressName(endpoint.String(), "")}
+		if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, args); err != nil {
+			t.Fatalf("run failed: %v\n%v", err, stderr.String())
+		}
+		if got, want := strings.TrimSpace(stdout.String()), "remainder = map[], approximations = [], version = 123"; got != want {
+			t.Errorf("got %q, want %q", got, want)
+		}
+		empty := map[string]struct{}{}
+		if got, want := buffer.String(), fmt.Sprintf("Relate(%v, %v, %v, %v) was called", empty, capitalize(hint), version, empty); got != want {
+			t.Errorf("got %v, want %v", got, want)
+		}
+		buffer.Reset()
+	}
+}