Merge lp:~thumper/juju-core/user-display-name into lp:~go-bot/juju-core/trunk

Proposed by Tim Penhey
Status: Needs review
Proposed branch: lp:~thumper/juju-core/user-display-name
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 1164 lines (+351/-240)
22 files modified
agent/bootstrap.go (+1/-1)
cmd/juju/authorizedkeys_test.go (+4/-8)
cmd/juju/user_add.go (+43/-28)
cmd/juju/user_add_test.go (+148/-76)
juju/conn_test.go (+1/-1)
juju/testing/conn.go (+6/-0)
provider/dummy/environs.go (+1/-1)
state/api/params/params.go (+12/-2)
state/api/usermanager/client.go (+8/-7)
state/api/usermanager/client_test.go (+18/-5)
state/apiserver/charms_test.go (+2/-5)
state/apiserver/client/api_test.go (+1/-2)
state/apiserver/client/client_test.go (+2/-5)
state/apiserver/login_test.go (+1/-2)
state/apiserver/usermanager/usermanager.go (+9/-4)
state/apiserver/usermanager/usermanager_test.go (+24/-19)
state/compat_test.go (+1/-1)
state/conn_test.go (+1/-1)
state/megawatcher_internal_test.go (+1/-1)
state/state_test.go (+2/-2)
state/user.go (+16/-9)
state/user_test.go (+49/-60)
To merge this branch: bzr merge lp:~thumper/juju-core/user-display-name
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+221823@code.launchpad.net

Description of the change

Add a display name option for new users

This ended up being more work than I hoped as I refactored the apiserver
parts, many tests, and made sure there was backwards compatability with
older client apis.

Also changed how the CLI command was tested so it didn't need a
JujuConnSuite.

https://codereview.appspot.com/102970043/

To post a comment you must log in.
Revision history for this message
Tim Penhey (thumper) wrote :

Reviewers: mp+221823_code.launchpad.net,

Message:
Please take a look.

Description:
Add a display name option for new users

This ended up being more work than I hoped as I refactored the apiserver
parts, many tests, and made sure there was backwards compatability with
older client apis.

Also changed how the CLI command was tested so it didn't need a
JujuConnSuite.

https://code.launchpad.net/~thumper/juju-core/user-display-name/+merge/221823

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/102970043/

Affected files (+353, -240 lines):
   A [revision details]
   M agent/bootstrap.go
   M cmd/juju/authorizedkeys_test.go
   M cmd/juju/user_add.go
   M cmd/juju/user_add_test.go
   M juju/conn_test.go
   M juju/testing/conn.go
   M provider/dummy/environs.go
   M state/api/params/params.go
   M state/api/usermanager/client.go
   M state/api/usermanager/client_test.go
   M state/apiserver/charms_test.go
   M state/apiserver/client/api_test.go
   M state/apiserver/client/client_test.go
   M state/apiserver/login_test.go
   M state/apiserver/usermanager/usermanager.go
   M state/apiserver/usermanager/usermanager_test.go
   M state/compat_test.go
   M state/conn_test.go
   M state/megawatcher_internal_test.go
   M state/state_test.go
   M state/user.go
   M state/user_test.go

Revision history for this message
Menno Finlay-Smits (menno.smits) wrote :

LGTM - only minor comments and praise added in-line. The user add tests
look much better.

One thing: can this actually land without schema migration support?

https://codereview.appspot.com/102970043/diff/1/cmd/juju/user_add.go
File cmd/juju/user_add.go (left):

https://codereview.appspot.com/102970043/diff/1/cmd/juju/user_add.go#oldcode44
cmd/juju/user_add.go:44: Args: "<username> <password>",
Sorry for missing this when I implemented --password

https://codereview.appspot.com/102970043/diff/1/cmd/juju/user_add.go
File cmd/juju/user_add.go (right):

https://codereview.appspot.com/102970043/diff/1/cmd/juju/user_add.go#newcode109
cmd/juju/user_add.go:109: return err
errors.Trace() here too?

https://codereview.appspot.com/102970043/diff/1/state/api/usermanager/client.go
File state/api/usermanager/client.go (left):

https://codereview.appspot.com/102970043/diff/1/state/api/usermanager/client.go#oldcode37
state/api/usermanager/client.go:37: p := params.EntityPasswords{Changes:
[]params.EntityPassword{u}}
These names were terrible! Glad you've fixed it.

https://codereview.appspot.com/102970043/diff/1/state/api/usermanager/client.go
File state/api/usermanager/client.go (right):

https://codereview.appspot.com/102970043/diff/1/state/api/usermanager/client.go#newcode37
state/api/usermanager/client.go:37: Changes:
[]params.ModifyUser{{Username: username, DisplayName: displayName,
Password: password}},
This is much more readable.

https://codereview.appspot.com/102970043/diff/1/state/api/usermanager/client_test.go
File state/api/usermanager/client_test.go (right):

https://codereview.appspot.com/102970043/diff/1/state/api/usermanager/client_test.go#newcode42
state/api/usermanager/client_test.go:42: err :=
s.APIState.Call("UserManager", "", "AddUser", userArgs, results)
Maybe add a comment here about why s.APIState.Call("UserManager",...) is
used here instead of s.usermanager.AddUser()? I understand why but had
to think about it for a bit.

https://codereview.appspot.com/102970043/diff/1/state/apiserver/charms_test.go
File state/apiserver/charms_test.go (left):

https://codereview.appspot.com/102970043/diff/1/state/apiserver/charms_test.go#oldcode39
state/apiserver/charms_test.go:39: password, err :=
utils.RandomPassword()
Play devil's advocate: this change means the user has a fixed password
instead of a random one. Are you sure that doesn't negatively impact any
existing tests? I can't think of why it would but it is a test
functionality change.

https://codereview.appspot.com/102970043/diff/1/state/user_test.go
File state/user_test.go (right):

https://codereview.appspot.com/102970043/diff/1/state/user_test.go#newcode45
state/user_test.go:45: func (s *UserSuite) makeUser(c *gc.C) *state.User
{
addSomeUser might be a better name, indicating that a test/throwaway
user is being created. It also hints at the username being used.

https://codereview.appspot.com/102970043/

Revision history for this message
Jesse Meek (waigani) wrote :
Download full text (3.4 KiB)

On 2014/06/03 05:17:26, menn0 wrote:
> LGTM - only minor comments and praise added in-line. The user add
tests look
> much better.

> One thing: can this actually land without schema migration support?

> https://codereview.appspot.com/102970043/diff/1/cmd/juju/user_add.go
> File cmd/juju/user_add.go (left):

https://codereview.appspot.com/102970043/diff/1/cmd/juju/user_add.go#oldcode44
> cmd/juju/user_add.go:44: Args: "<username> <password>",
> Sorry for missing this when I implemented --password

> https://codereview.appspot.com/102970043/diff/1/cmd/juju/user_add.go
> File cmd/juju/user_add.go (right):

https://codereview.appspot.com/102970043/diff/1/cmd/juju/user_add.go#newcode109
> cmd/juju/user_add.go:109: return err
> errors.Trace() here too?

https://codereview.appspot.com/102970043/diff/1/state/api/usermanager/client.go
> File state/api/usermanager/client.go (left):

https://codereview.appspot.com/102970043/diff/1/state/api/usermanager/client.go#oldcode37
> state/api/usermanager/client.go:37: p :=
params.EntityPasswords{Changes:
> []params.EntityPassword{u}}
> These names were terrible! Glad you've fixed it.

https://codereview.appspot.com/102970043/diff/1/state/api/usermanager/client.go
> File state/api/usermanager/client.go (right):

https://codereview.appspot.com/102970043/diff/1/state/api/usermanager/client.go#newcode37
> state/api/usermanager/client.go:37: Changes:
[]params.ModifyUser{{Username:
> username, DisplayName: displayName, Password: password}},
> This is much more readable.

https://codereview.appspot.com/102970043/diff/1/state/api/usermanager/client_test.go
> File state/api/usermanager/client_test.go (right):

https://codereview.appspot.com/102970043/diff/1/state/api/usermanager/client_test.go#newcode42
> state/api/usermanager/client_test.go:42: err :=
s.APIState.Call("UserManager",
> "", "AddUser", userArgs, results)
> Maybe add a comment here about why s.APIState.Call("UserManager",...)
is used
> here instead of s.usermanager.AddUser()? I understand why but had to
think about
> it for a bit.

https://codereview.appspot.com/102970043/diff/1/state/apiserver/charms_test.go
> File state/apiserver/charms_test.go (left):

https://codereview.appspot.com/102970043/diff/1/state/apiserver/charms_test.go#oldcode39
> state/apiserver/charms_test.go:39: password, err :=
utils.RandomPassword()
> Play devil's advocate: this change means the user has a fixed password
instead
> of a random one. Are you sure that doesn't negatively impact any
existing tests?
> I can't think of why it would but it is a test functionality change.

> https://codereview.appspot.com/102970043/diff/1/state/user_test.go
> File state/user_test.go (right):

https://codereview.appspot.com/102970043/diff/1/state/user_test.go#newcode45
> state/user_test.go:45: func (s *UserSuite) makeUser(c *gc.C)
*state.User {
> addSomeUser might be a better name, indicating that a test/throwaway
user is
> being created. It also hints at the username being used.

LGTM. Just one suggestion inline. The bulk of the changes look to be
updating AddUser to take an extra arg. As to schema migration support, I
think adding fields is fine - its when we change or remove them ...

Read more...

Revision history for this message
Jesse Meek (waigani) wrote :

https://codereview.appspot.com/102970043/diff/1/state/user_test.go
File state/user_test.go (right):

https://codereview.appspot.com/102970043/diff/1/state/user_test.go#newcode75
state/user_test.go:75: user := s.makeUser(c)
It seems you are making a user for every test. Why not makeUser in
SetUpTest?

https://codereview.appspot.com/102970043/

Revision history for this message
Tim Penhey (thumper) wrote :

New branch is moving to github.

https://codereview.appspot.com/102970043/diff/1/state/api/usermanager/client_test.go
File state/api/usermanager/client_test.go (right):

https://codereview.appspot.com/102970043/diff/1/state/api/usermanager/client_test.go#newcode42
state/api/usermanager/client_test.go:42: err :=
s.APIState.Call("UserManager", "", "AddUser", userArgs, results)
On 2014/06/03 05:17:26, menn0 wrote:
> Maybe add a comment here about why s.APIState.Call("UserManager",...)
is used
> here instead of s.usermanager.AddUser()? I understand why but had to
think about
> it for a bit.

Done.

https://codereview.appspot.com/102970043/diff/1/state/apiserver/charms_test.go
File state/apiserver/charms_test.go (left):

https://codereview.appspot.com/102970043/diff/1/state/apiserver/charms_test.go#oldcode39
state/apiserver/charms_test.go:39: password, err :=
utils.RandomPassword()
On 2014/06/03 05:17:26, menn0 wrote:
> Play devil's advocate: this change means the user has a fixed password
instead
> of a random one. Are you sure that doesn't negatively impact any
existing tests?
> I can't think of why it would but it is a test functionality change.

Sure, but the tests all still pass :-)

I'm considering a way to change the way we create test fixture objects.
A cunning plan.

https://codereview.appspot.com/102970043/diff/1/state/user_test.go
File state/user_test.go (right):

https://codereview.appspot.com/102970043/diff/1/state/user_test.go#newcode75
state/user_test.go:75: user := s.makeUser(c)
On 2014/06/03 21:29:26, waigani wrote:
> It seems you are making a user for every test. Why not makeUser in
SetUpTest?

Not quite every test :-)

https://codereview.appspot.com/102970043/

Unmerged revisions

2819. By Tim Penhey

Fix the test.

2818. By Tim Penhey

Lots of test rework.

2817. By Tim Penhey

Merge trunk.

2816. By Tim Penhey

Minor cleanups

2815. By Tim Penhey

Update other test call sites when they don't care about user details.

2814. By Tim Penhey

Update client calls

2813. By Tim Penhey

Fix the state tests.

2812. By Tim Penhey

The state changes for more params.

2811. By Tim Penhey

Extra params for AddUser.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'agent/bootstrap.go'
--- agent/bootstrap.go 2014-05-08 07:04:54 +0000
+++ agent/bootstrap.go 2014-06-03 04:22:37 +0000
@@ -121,7 +121,7 @@
121func initBootstrapUser(st *state.State, passwordHash string) error {121func initBootstrapUser(st *state.State, passwordHash string) error {
122 logger.Debugf("adding admin user")122 logger.Debugf("adding admin user")
123 // Set up initial authentication.123 // Set up initial authentication.
124 u, err := st.AddUser("admin", "")124 u, err := st.AddUser("admin", "", "")
125 if err != nil {125 if err != nil {
126 return err126 return err
127 }127 }
128128
=== modified file 'cmd/juju/authorizedkeys_test.go'
--- cmd/juju/authorizedkeys_test.go 2014-05-22 06:03:06 +0000
+++ cmd/juju/authorizedkeys_test.go 2014-06-03 04:22:37 +0000
@@ -139,8 +139,7 @@
139 key1 := sshtesting.ValidKeyOne.Key + " user@host"139 key1 := sshtesting.ValidKeyOne.Key + " user@host"
140 key2 := sshtesting.ValidKeyTwo.Key + " another@host"140 key2 := sshtesting.ValidKeyTwo.Key + " another@host"
141 s.setAuthorizedKeys(c, key1, key2)141 s.setAuthorizedKeys(c, key1, key2)
142 _, err := s.State.AddUser("fred", "password")142 s.AddUser(c, "fred")
143 c.Assert(err, gc.IsNil)
144143
145 context, err := coretesting.RunCommand(c, envcmd.Wrap(&ListKeysCommand{}), "--user", "fred")144 context, err := coretesting.RunCommand(c, envcmd.Wrap(&ListKeysCommand{}), "--user", "fred")
146 c.Assert(err, gc.IsNil)145 c.Assert(err, gc.IsNil)
@@ -174,8 +173,7 @@
174func (s *AddKeySuite) TestAddKeyNonDefaultUser(c *gc.C) {173func (s *AddKeySuite) TestAddKeyNonDefaultUser(c *gc.C) {
175 key1 := sshtesting.ValidKeyOne.Key + " user@host"174 key1 := sshtesting.ValidKeyOne.Key + " user@host"
176 s.setAuthorizedKeys(c, key1)175 s.setAuthorizedKeys(c, key1)
177 _, err := s.State.AddUser("fred", "password")176 s.AddUser(c, "fred")
178 c.Assert(err, gc.IsNil)
179177
180 key2 := sshtesting.ValidKeyTwo.Key + " another@host"178 key2 := sshtesting.ValidKeyTwo.Key + " another@host"
181 context, err := coretesting.RunCommand(c, envcmd.Wrap(&AddKeysCommand{}), "--user", "fred", key2)179 context, err := coretesting.RunCommand(c, envcmd.Wrap(&AddKeysCommand{}), "--user", "fred", key2)
@@ -206,8 +204,7 @@
206 key1 := sshtesting.ValidKeyOne.Key + " user@host"204 key1 := sshtesting.ValidKeyOne.Key + " user@host"
207 key2 := sshtesting.ValidKeyTwo.Key + " another@host"205 key2 := sshtesting.ValidKeyTwo.Key + " another@host"
208 s.setAuthorizedKeys(c, key1, key2)206 s.setAuthorizedKeys(c, key1, key2)
209 _, err := s.State.AddUser("fred", "password")207 s.AddUser(c, "fred")
210 c.Assert(err, gc.IsNil)
211208
212 context, err := coretesting.RunCommand(c, envcmd.Wrap(&DeleteKeysCommand{}),209 context, err := coretesting.RunCommand(c, envcmd.Wrap(&DeleteKeysCommand{}),
213 "--user", "fred", sshtesting.ValidKeyTwo.Fingerprint)210 "--user", "fred", sshtesting.ValidKeyTwo.Fingerprint)
@@ -240,8 +237,7 @@
240func (s *ImportKeySuite) TestImportKeyNonDefaultUser(c *gc.C) {237func (s *ImportKeySuite) TestImportKeyNonDefaultUser(c *gc.C) {
241 key1 := sshtesting.ValidKeyOne.Key + " user@host"238 key1 := sshtesting.ValidKeyOne.Key + " user@host"
242 s.setAuthorizedKeys(c, key1)239 s.setAuthorizedKeys(c, key1)
243 _, err := s.State.AddUser("fred", "password")240 s.AddUser(c, "fred")
244 c.Assert(err, gc.IsNil)
245241
246 context, err := coretesting.RunCommand(c, envcmd.Wrap(&ImportKeysCommand{}), "--user", "fred", "lp:validuser")242 context, err := coretesting.RunCommand(c, envcmd.Wrap(&ImportKeysCommand{}), "--user", "fred", "lp:validuser")
247 c.Assert(err, gc.IsNil)243 c.Assert(err, gc.IsNil)
248244
=== modified file 'cmd/juju/user_add.go'
--- cmd/juju/user_add.go 2014-05-26 00:06:16 +0000
+++ cmd/juju/user_add.go 2014-06-03 04:22:37 +0000
@@ -8,6 +8,7 @@
8 "os"8 "os"
9 "strings"9 "strings"
1010
11 "github.com/juju/errors"
11 "launchpad.net/gnuflag"12 "launchpad.net/gnuflag"
1213
13 "launchpad.net/juju-core/cmd"14 "launchpad.net/juju-core/cmd"
@@ -33,40 +34,49 @@
3334
34type UserAddCommand struct {35type UserAddCommand struct {
35 envcmd.EnvCommandBase36 envcmd.EnvCommandBase
36 User string37 User string
37 Password string38 DisplayName string
38 outPath string39 Password string
40 outPath string
39}41}
4042
41func (c *UserAddCommand) Info() *cmd.Info {43func (c *UserAddCommand) Info() *cmd.Info {
42 return &cmd.Info{44 return &cmd.Info{
43 Name: "add",45 Name: "add",
44 Args: "<username> <password>",46 Args: "<username> [<display name>]",
45 Purpose: "adds a user",47 Purpose: "adds a user",
46 Doc: userAddCommandDoc,48 Doc: userAddCommandDoc,
47 }49 }
48}50}
4951
50func (c *UserAddCommand) SetFlags(f *gnuflag.FlagSet) {52func (c *UserAddCommand) SetFlags(f *gnuflag.FlagSet) {
51 f.StringVar(&(c.Password), "password", "", "Password for new user")53 f.StringVar(&c.Password, "password", "", "Password for new user")
52 f.StringVar(&(c.outPath), "o", "", "Output an environment file for new user")54 f.StringVar(&c.outPath, "o", "", "Output an environment file for new user")
53 f.StringVar(&(c.outPath), "output", "", "")55 f.StringVar(&c.outPath, "output", "", "")
54}56}
5557
56func (c *UserAddCommand) Init(args []string) error {58func (c *UserAddCommand) Init(args []string) error {
57 switch len(args) {59 if len(args) == 0 {
58 case 0:
59 return fmt.Errorf("no username supplied")60 return fmt.Errorf("no username supplied")
60 case 1:61 }
61 c.User = args[0]62 c.User, args = args[0], args[1:]
62 default:63 if len(args) > 0 {
63 return cmd.CheckEmpty(args[1:])64 c.DisplayName, args = args[0], args[1:]
64 }65 }
65 return nil66 return cmd.CheckEmpty(args)
67}
68
69type addUserAPI interface {
70 AddUser(username, displayname, password string) error
71 Close() error
72}
73
74var getAddUserAPI = func(c *UserAddCommand) (addUserAPI, error) {
75 return juju.NewUserManagerClient(c.EnvName)
66}76}
6777
68func (c *UserAddCommand) Run(ctx *cmd.Context) error {78func (c *UserAddCommand) Run(ctx *cmd.Context) error {
69 client, err := juju.NewUserManagerClient(c.EnvName)79 client, err := getAddUserAPI(c)
70 if err != nil {80 if err != nil {
71 return err81 return err
72 }82 }
@@ -74,19 +84,24 @@
74 if c.Password == "" {84 if c.Password == "" {
75 c.Password, err = utils.RandomPassword()85 c.Password, err = utils.RandomPassword()
76 if err != nil {86 if err != nil {
77 return fmt.Errorf("Failed to generate password: %v", err)87 return errors.Annotate(err, "failed to generate password")
78 }88 }
79 }89 }
8090
81 err = client.AddUser(c.User, c.Password)91 err = client.AddUser(c.User, c.DisplayName, c.Password)
82 if err != nil {92 if err != nil {
83 return err93 return err
84 }94 }
85 fmt.Fprintf(ctx.Stdout, "user \"%s\" added with password \"%s\"\n", c.User, c.Password)95 user := c.User
96 if c.DisplayName != "" {
97 user = fmt.Sprintf("%s (%s)", c.DisplayName, user)
98 }
99
100 fmt.Fprintf(ctx.Stdout, "user %q added with password %q\n", user, c.Password)
86101
87 if c.outPath != "" {102 if c.outPath != "" {
88 outPath := NormaliseJenvPath(ctx, c.outPath)103 outPath := NormaliseJenvPath(ctx, c.outPath)
89 err := GenerateUserJenv(c.EnvName, c.User, c.Password, outPath)104 err = GenerateUserJenv(c.EnvName, c.User, c.Password, outPath)
90 if err == nil {105 if err == nil {
91 fmt.Fprintf(ctx.Stdout, "environment file written to %s\n", outPath)106 fmt.Fprintf(ctx.Stdout, "environment file written to %s\n", outPath)
92 }107 }
@@ -101,14 +116,14 @@
101 return ctx.AbsPath(outPath)116 return ctx.AbsPath(outPath)
102}117}
103118
104func GenerateUserJenv(envName, user, password, outPath string) (err error) {119func GenerateUserJenv(envName, user, password, outPath string) error {
105 store, err := configstore.Default()120 store, err := configstore.Default()
106 if err != nil {121 if err != nil {
107 return122 return errors.Trace(err)
108 }123 }
109 storeInfo, err := store.ReadInfo(envName)124 storeInfo, err := store.ReadInfo(envName)
110 if err != nil {125 if err != nil {
111 return126 return errors.Trace(err)
112 }127 }
113 outputInfo := configstore.EnvironInfoData{}128 outputInfo := configstore.EnvironInfoData{}
114 outputInfo.User = user129 outputInfo.User = user
@@ -117,17 +132,17 @@
117 outputInfo.CACert = storeInfo.APIEndpoint().CACert132 outputInfo.CACert = storeInfo.APIEndpoint().CACert
118 yaml, err := cmd.FormatYaml(outputInfo)133 yaml, err := cmd.FormatYaml(outputInfo)
119 if err != nil {134 if err != nil {
120 return135 return errors.Trace(err)
121 }136 }
122137
123 outFile, err := os.Create(outPath)138 outFile, err := os.Create(outPath)
124 if err != nil {139 if err != nil {
125 return140 return errors.Trace(err)
126 }141 }
127 defer outFile.Close()142 defer outFile.Close()
128 _, err = outFile.Write(yaml)143 outFile.Write(yaml)
129 if err != nil {144 if err != nil {
130 return145 return errors.Trace(err)
131 }146 }
132 return147 return nil
133}148}
134149
=== modified file 'cmd/juju/user_add_test.go'
--- cmd/juju/user_add_test.go 2014-05-26 00:06:16 +0000
+++ cmd/juju/user_add_test.go 2014-06-03 04:22:37 +0000
@@ -4,11 +4,10 @@
4package main4package main
55
6import (6import (
7 "bytes"7 "errors"
8 "fmt"
8 "io/ioutil"9 "io/ioutil"
9 "path/filepath"10 "path/filepath"
10 "regexp"
11 "strings"
1211
13 jc "github.com/juju/testing/checkers"12 jc "github.com/juju/testing/checkers"
14 gc "launchpad.net/gocheck"13 gc "launchpad.net/gocheck"
@@ -16,107 +15,180 @@
1615
17 "launchpad.net/juju-core/cmd"16 "launchpad.net/juju-core/cmd"
18 "launchpad.net/juju-core/cmd/envcmd"17 "launchpad.net/juju-core/cmd/envcmd"
19 jujutesting "launchpad.net/juju-core/juju/testing"18 "launchpad.net/juju-core/environs/configstore"
20 "launchpad.net/juju-core/testing"19 "launchpad.net/juju-core/testing"
21)20)
2221
23// All of the functionality of the AddUser api call is contained elsewhere.22// All of the functionality of the AddUser api call is contained elsewhere.
24// This suite provides basic tests for the "user add" command23// This suite provides basic tests for the "user add" command
25type UserAddCommandSuite struct {24type UserAddCommandSuite struct {
26 jujutesting.JujuConnSuite25 testing.FakeJujuHomeSuite
26 mockAPI *mockAddUserAPI
27}27}
2828
29var _ = gc.Suite(&UserAddCommandSuite{})29var _ = gc.Suite(&UserAddCommandSuite{})
3030
31func (s *UserAddCommandSuite) SetUpTest(c *gc.C) {
32 s.FakeJujuHomeSuite.SetUpTest(c)
33 s.mockAPI = &mockAddUserAPI{}
34 s.PatchValue(&getAddUserAPI, func(c *UserAddCommand) (addUserAPI, error) {
35 return s.mockAPI, nil
36 })
37}
38
31func newUserAddCommand() cmd.Command {39func newUserAddCommand() cmd.Command {
32 return envcmd.Wrap(&UserAddCommand{})40 return envcmd.Wrap(&UserAddCommand{})
33}41}
3442
35func (s *UserAddCommandSuite) TestUserAdd(c *gc.C) {43func (s *UserAddCommandSuite) TestAddUserJustUsername(c *gc.C) {
36 _, err := testing.RunCommand(c, newUserAddCommand(), "foobar")44 context, err := testing.RunCommand(c, newUserAddCommand(), "foobar")
37 c.Assert(err, gc.IsNil)45 c.Assert(err, gc.IsNil)
3846 c.Assert(s.mockAPI.username, gc.Equals, "foobar")
39 _, err = testing.RunCommand(c, newUserAddCommand(), "foobar")47 c.Assert(s.mockAPI.displayname, gc.Equals, "")
40 c.Assert(err, gc.ErrorMatches, "Failed to create user: user already exists")48 // Password is generated
41}49 c.Assert(s.mockAPI.password, gc.Not(gc.Equals), "")
4250 expected := fmt.Sprintf(`user "foobar" added with password %q`, s.mockAPI.password)
43func (s *UserAddCommandSuite) TestTooManyArgs(c *gc.C) {51 c.Assert(testing.Stdout(context), gc.Equals, expected+"\n")
44 _, err := testing.RunCommand(c, newUserAddCommand(), "foobar", "whoops")52}
45 c.Assert(err, gc.ErrorMatches, `unrecognized args: \["whoops"\]`)53
46}54func (s *UserAddCommandSuite) TestAddUserUsernameAndDisplayname(c *gc.C) {
4755 context, err := testing.RunCommand(c, newUserAddCommand(), "foobar", "Foo Bar")
48func (s *UserAddCommandSuite) TestNotEnoughArgs(c *gc.C) {56 c.Assert(err, gc.IsNil)
49 _, err := testing.RunCommand(c, newUserAddCommand())57 c.Assert(s.mockAPI.username, gc.Equals, "foobar")
50 c.Assert(err, gc.ErrorMatches, `no username supplied`)58 c.Assert(s.mockAPI.displayname, gc.Equals, "Foo Bar")
51}59 // Password is generated
5260 c.Assert(s.mockAPI.password, gc.Not(gc.Equals), "")
53func (s *UserAddCommandSuite) TestGeneratePassword(c *gc.C) {61 expected := fmt.Sprintf(`user "Foo Bar (foobar)" added with password %q`, s.mockAPI.password)
54 ctx, err := testing.RunCommand(c, newUserAddCommand(), "foobar")62 c.Assert(testing.Stdout(context), gc.Equals, expected+"\n")
5563}
56 c.Assert(err, gc.IsNil)64
57 user, password, filename := parseUserAddStdout(c, ctx)65func (s *UserAddCommandSuite) TestAddUserUsernameAndDisplaynameWithPassword(c *gc.C) {
58 c.Assert(user, gc.Equals, "foobar")66 context, err := testing.RunCommand(c, newUserAddCommand(), "foobar", "Foo Bar", "--password", "password")
59 // Let's not try to assume too much about the password generation67 c.Assert(err, gc.IsNil)
60 // algorithm other than there will be at least 10 characters.68 c.Assert(s.mockAPI.username, gc.Equals, "foobar")
61 c.Assert(password, gc.Matches, "..........+")69 c.Assert(s.mockAPI.displayname, gc.Equals, "Foo Bar")
62 c.Assert(filename, gc.Equals, "")70 c.Assert(s.mockAPI.password, gc.Equals, "password")
63}71 expected := `user "Foo Bar (foobar)" added with password "password"`
6472 c.Assert(testing.Stdout(context), gc.Equals, expected+"\n")
65func (s *UserAddCommandSuite) TestUserSpecifiedPassword(c *gc.C) {73}
66 ctx, err := testing.RunCommand(c, newUserAddCommand(), "foobar", "--password", "frogdog")74
67 c.Assert(err, gc.IsNil)75func (s *UserAddCommandSuite) TestAddUserErrorResponse(c *gc.C) {
6876 s.mockAPI.failMessage = "failed to create user, chaos ensues"
69 user, password, filename := parseUserAddStdout(c, ctx)77 context, err := testing.RunCommand(c, newUserAddCommand(), "foobar")
70 c.Assert(user, gc.Equals, "foobar")78 c.Assert(err, gc.ErrorMatches, "failed to create user, chaos ensues")
71 c.Assert(password, gc.Equals, "frogdog")79 c.Assert(s.mockAPI.username, gc.Equals, "foobar")
72 c.Assert(filename, gc.Equals, "")80 c.Assert(s.mockAPI.displayname, gc.Equals, "")
81 c.Assert(testing.Stdout(context), gc.Equals, "")
82}
83
84func (s *UserAddCommandSuite) TestInit(c *gc.C) {
85 for i, test := range []struct {
86 args []string
87 user string
88 displayname string
89 password string
90 outPath string
91 errorString string
92 }{
93 {
94 errorString: "no username supplied",
95 }, {
96 args: []string{"foobar"},
97 user: "foobar",
98 }, {
99 args: []string{"foobar", "Foo Bar"},
100 user: "foobar",
101 displayname: "Foo Bar",
102 }, {
103 args: []string{"foobar", "Foo Bar", "extra"},
104 errorString: `unrecognized args: \["extra"\]`,
105 }, {
106 args: []string{"foobar", "--password", "password"},
107 user: "foobar",
108 password: "password",
109 }, {
110 args: []string{"foobar", "--output", "somefile"},
111 user: "foobar",
112 outPath: "somefile",
113 }, {
114 args: []string{"foobar", "-o", "somefile"},
115 user: "foobar",
116 outPath: "somefile",
117 },
118 } {
119 c.Logf("test %d", i)
120 addUserCmd := &UserAddCommand{}
121 err := testing.InitCommand(addUserCmd, test.args)
122 if test.errorString == "" {
123 c.Check(addUserCmd.User, gc.Equals, test.user)
124 c.Check(addUserCmd.DisplayName, gc.Equals, test.displayname)
125 c.Check(addUserCmd.Password, gc.Equals, test.password)
126 c.Check(addUserCmd.outPath, gc.Equals, test.outPath)
127 } else {
128 c.Check(err, gc.ErrorMatches, test.errorString)
129 }
130 }
131}
132
133func fakeBootstrapEnvironment(c *gc.C, envName string) {
134 store, err := configstore.Default()
135 c.Assert(err, gc.IsNil)
136 envInfo, err := store.CreateInfo(envName)
137 c.Assert(err, gc.IsNil)
138 envInfo.SetBootstrapConfig(map[string]interface{}{"random": "extra data"})
139 envInfo.SetAPIEndpoint(configstore.APIEndpoint{
140 Addresses: []string{"localhost:12345"},
141 CACert: testing.CACert,
142 })
143 envInfo.SetAPICredentials(configstore.APICredentials{
144 User: "admin",
145 Password: "password",
146 })
147 err = envInfo.Write()
148 c.Assert(err, gc.IsNil)
73}149}
74150
75func (s *UserAddCommandSuite) TestJenvOutput(c *gc.C) {151func (s *UserAddCommandSuite) TestJenvOutput(c *gc.C) {
152 fakeBootstrapEnvironment(c, "erewhemos")
76 outputName := filepath.Join(c.MkDir(), "output")153 outputName := filepath.Join(c.MkDir(), "output")
77 ctx, err := testing.RunCommand(c, newUserAddCommand(),154 ctx, err := testing.RunCommand(c, newUserAddCommand(),
78 "foobar", "--password", "password", "--output", outputName)155 "foobar", "--password", "password", "--output", outputName)
79 c.Assert(err, gc.IsNil)156 c.Assert(err, gc.IsNil)
80157
81 user, password, filename := parseUserAddStdout(c, ctx)158 expected := fmt.Sprintf(`user "foobar" added with password %q`, s.mockAPI.password)
82 c.Assert(user, gc.Equals, "foobar")159 expected = fmt.Sprintf("%s\nenvironment file written to %s.jenv\n", expected, outputName)
83 c.Assert(password, gc.Equals, "password")160 c.Assert(testing.Stdout(ctx), gc.Equals, expected)
84 c.Assert(filename, gc.Equals, outputName+".jenv")
85161
86 raw, err := ioutil.ReadFile(filename)162 raw, err := ioutil.ReadFile(outputName + ".jenv")
87 c.Assert(err, gc.IsNil)163 c.Assert(err, gc.IsNil)
88 d := map[string]interface{}{}164 d := map[string]interface{}{}
89 err = goyaml.Unmarshal(raw, &d)165 err = goyaml.Unmarshal(raw, &d)
90 c.Assert(err, gc.IsNil)166 c.Assert(err, gc.IsNil)
91 c.Assert(d["user"], gc.Equals, "foobar")167 c.Assert(d["user"], gc.Equals, "foobar")
92 c.Assert(d["password"], gc.Equals, "password")168 c.Assert(d["password"], gc.Equals, "password")
93 _, found := d["state-servers"]169 c.Assert(d["state-servers"], gc.DeepEquals, []interface{}{"localhost:12345"})
94 c.Assert(found, gc.Equals, true)170 c.Assert(d["ca-cert"], gc.DeepEquals, testing.CACert)
95 _, found = d["ca-cert"]171 _, found := d["bootstrap-config"]
96 c.Assert(found, gc.Equals, true)172 c.Assert(found, jc.IsFalse)
97}173}
98174
99// parseUserAddStdout parses the output from the "juju user add"175type mockAddUserAPI struct {
100// command and checks that it has the correct form, returning the176 failMessage string
101// interesting parts. The .jenv filename will be an empty string when177 username string
102// it wasn't included in the output.178 displayname string
103func parseUserAddStdout(c *gc.C, ctx *cmd.Context) (user string, password string, filename string) {179 password string
104 stdout := strings.TrimSpace(ctx.Stdout.(*bytes.Buffer).String())180}
105 lines := strings.Split(stdout, "\n")181
106 c.Assert(len(lines), jc.LessThan, 3)182func (m *mockAddUserAPI) AddUser(username, displayname, password string) error {
107183 m.username = username
108 reLine0 := regexp.MustCompile(`^user "(.+)" added with password "(.+)"$`)184 m.displayname = displayname
109 line0Matches := reLine0.FindStringSubmatch(lines[0])185 m.password = password
110 c.Assert(len(line0Matches), gc.Equals, 3)186 if m.failMessage == "" {
111 user, password = line0Matches[1], line0Matches[2]187 return nil
112
113 if len(lines) == 2 {
114 reLine1 := regexp.MustCompile(`^environment file written to (.+)$`)
115 line1Matches := reLine1.FindStringSubmatch(lines[1])
116 c.Assert(len(line1Matches), gc.Equals, 2)
117 filename = line1Matches[1]
118 } else {
119 filename = ""
120 }188 }
121 return189 return errors.New(m.failMessage)
190}
191
192func (*mockAddUserAPI) Close() error {
193 return nil
122}194}
123195
=== modified file 'juju/conn_test.go'
--- juju/conn_test.go 2014-05-20 04:27:02 +0000
+++ juju/conn_test.go 2014-06-03 04:22:37 +0000
@@ -501,7 +501,7 @@
501501
502func (s *DeployLocalSuite) TestDeployOwnerTag(c *gc.C) {502func (s *DeployLocalSuite) TestDeployOwnerTag(c *gc.C) {
503 usermanager := usermanager.NewClient(s.APIState)503 usermanager := usermanager.NewClient(s.APIState)
504 err := usermanager.AddUser("foobar", "")504 err := usermanager.AddUser("foobar", "", "")
505 c.Assert(err, gc.IsNil)505 c.Assert(err, gc.IsNil)
506 service, err := juju.DeployService(s.State,506 service, err := juju.DeployService(s.State,
507 juju.DeployServiceParams{507 juju.DeployServiceParams{
508508
=== modified file 'juju/testing/conn.go'
--- juju/testing/conn.go 2014-05-26 08:25:47 +0000
+++ juju/testing/conn.go 2014-06-03 04:22:37 +0000
@@ -98,6 +98,12 @@
98 s.setUpConn(c)98 s.setUpConn(c)
99}99}
100100
101func (s *JujuConnSuite) AddUser(c *gc.C, username string) *state.User {
102 user, err := s.State.AddUser(username, "", "password")
103 c.Assert(err, gc.IsNil)
104 return user
105}
106
101func (s *JujuConnSuite) StateInfo(c *gc.C) *state.Info {107func (s *JujuConnSuite) StateInfo(c *gc.C) *state.Info {
102 info, _, err := s.Conn.Environ.StateInfo()108 info, _, err := s.Conn.Environ.StateInfo()
103 c.Assert(err, gc.IsNil)109 c.Assert(err, gc.IsNil)
104110
=== modified file 'provider/dummy/environs.go'
--- provider/dummy/environs.go 2014-05-16 13:03:36 +0000
+++ provider/dummy/environs.go 2014-06-03 04:22:37 +0000
@@ -629,7 +629,7 @@
629 if err := st.SetAdminMongoPassword(utils.UserPasswordHash(password, utils.CompatSalt)); err != nil {629 if err := st.SetAdminMongoPassword(utils.UserPasswordHash(password, utils.CompatSalt)); err != nil {
630 panic(err)630 panic(err)
631 }631 }
632 _, err = st.AddUser("admin", password)632 _, err = st.AddUser("admin", "", password)
633 if err != nil {633 if err != nil {
634 panic(err)634 panic(err)
635 }635 }
636636
=== modified file 'state/api/params/params.go'
--- state/api/params/params.go 2014-05-27 08:30:44 +0000
+++ state/api/params/params.go 2014-06-03 04:22:37 +0000
@@ -382,10 +382,20 @@
382 Keys []string382 Keys []string
383}383}
384384
385// ModifyUsers holds the parameters for making a UserManager Add or Modify calls.
386type ModifyUsers struct {
387 Changes []ModifyUser
388}
389
385// ModifyUser stores the parameters used for a UserManager.Add|Remove call390// ModifyUser stores the parameters used for a UserManager.Add|Remove call
386type ModifyUser struct {391type ModifyUser struct {
387 Tag string392 // Tag is here purely for backwards compatability. Older clients will
388 Password string393 // attempt to use the EntityPassword structure, so we need a Tag here
394 // (which will be treated as Username)
395 Tag string
396 Username string
397 DisplayName string
398 Password string
389}399}
390400
391// MarshalJSON implements json.Marshaler.401// MarshalJSON implements json.Marshaler.
392402
=== modified file 'state/api/usermanager/client.go'
--- state/api/usermanager/client.go 2014-05-23 11:24:54 +0000
+++ state/api/usermanager/client.go 2014-06-03 04:22:37 +0000
@@ -29,14 +29,15 @@
29 return c.st.Close()29 return c.st.Close()
30}30}
3131
32func (c *Client) AddUser(tag, password string) error {32func (c *Client) AddUser(username, displayName, password string) error {
33 if !names.IsUser(tag) {33 if !names.IsUser(username) {
34 return fmt.Errorf("invalid user name %q", tag)34 return fmt.Errorf("invalid user name %q", username)
35 }35 }
36 u := params.EntityPassword{Tag: tag, Password: password}36 userArgs := params.ModifyUsers{
37 p := params.EntityPasswords{Changes: []params.EntityPassword{u}}37 Changes: []params.ModifyUser{{Username: username, DisplayName: displayName, Password: password}},
38 }
38 results := new(params.ErrorResults)39 results := new(params.ErrorResults)
39 err := c.call("AddUser", p, results)40 err := c.call("AddUser", userArgs, results)
40 if err != nil {41 if err != nil {
41 return err42 return err
42 }43 }
4344
=== modified file 'state/api/usermanager/client_test.go'
--- state/api/usermanager/client_test.go 2014-05-23 11:24:54 +0000
+++ state/api/usermanager/client_test.go 2014-06-03 04:22:37 +0000
@@ -8,6 +8,7 @@
88
9 jujutesting "launchpad.net/juju-core/juju/testing"9 jujutesting "launchpad.net/juju-core/juju/testing"
10 "launchpad.net/juju-core/state"10 "launchpad.net/juju-core/state"
11 "launchpad.net/juju-core/state/api/params"
11 "launchpad.net/juju-core/state/api/usermanager"12 "launchpad.net/juju-core/state/api/usermanager"
12)13)
1314
@@ -26,14 +27,26 @@
26}27}
2728
28func (s *usermanagerSuite) TestAddUser(c *gc.C) {29func (s *usermanagerSuite) TestAddUser(c *gc.C) {
29 err := s.usermanager.AddUser("foobar", "password")30 err := s.usermanager.AddUser("foobar", "Foo Bar", "password")
31 c.Assert(err, gc.IsNil)
32 _, err = s.State.User("foobar")
33 c.Assert(err, gc.IsNil)
34}
35
36func (s *usermanagerSuite) TestAddUserOldClient(c *gc.C) {
37 userArgs := params.EntityPasswords{
38 Changes: []params.EntityPassword{{Tag: "foobar", Password: "password"}},
39 }
40 results := new(params.ErrorResults)
41
42 err := s.APIState.Call("UserManager", "", "AddUser", userArgs, results)
30 c.Assert(err, gc.IsNil)43 c.Assert(err, gc.IsNil)
31 _, err = s.State.User("foobar")44 _, err = s.State.User("foobar")
32 c.Assert(err, gc.IsNil)45 c.Assert(err, gc.IsNil)
33}46}
3447
35func (s *usermanagerSuite) TestRemoveUser(c *gc.C) {48func (s *usermanagerSuite) TestRemoveUser(c *gc.C) {
36 err := s.usermanager.AddUser("foobar", "password")49 err := s.usermanager.AddUser("foobar", "Foo Bar", "password")
37 c.Assert(err, gc.IsNil)50 c.Assert(err, gc.IsNil)
38 _, err = s.State.User("foobar")51 _, err = s.State.User("foobar")
39 c.Assert(err, gc.IsNil)52 c.Assert(err, gc.IsNil)
@@ -45,12 +58,12 @@
45}58}
4659
47func (s *usermanagerSuite) TestAddExistingUser(c *gc.C) {60func (s *usermanagerSuite) TestAddExistingUser(c *gc.C) {
48 err := s.usermanager.AddUser("foobar", "password")61 err := s.usermanager.AddUser("foobar", "Foo Bar", "password")
49 c.Assert(err, gc.IsNil)62 c.Assert(err, gc.IsNil)
5063
51 // Try adding again64 // Try adding again
52 err = s.usermanager.AddUser("foobar", "password")65 err = s.usermanager.AddUser("foobar", "Foo Bar", "password")
53 c.Assert(err, gc.ErrorMatches, "Failed to create user: user already exists")66 c.Assert(err, gc.ErrorMatches, "failed to create user: user already exists")
54}67}
5568
56func (s *usermanagerSuite) TestCantRemoveAdminUser(c *gc.C) {69func (s *usermanagerSuite) TestCantRemoveAdminUser(c *gc.C) {
5770
=== modified file 'state/apiserver/charms_test.go'
--- state/apiserver/charms_test.go 2014-03-13 23:30:56 +0000
+++ state/apiserver/charms_test.go 2014-06-03 04:22:37 +0000
@@ -36,12 +36,9 @@
3636
37func (s *authHttpSuite) SetUpTest(c *gc.C) {37func (s *authHttpSuite) SetUpTest(c *gc.C) {
38 s.JujuConnSuite.SetUpTest(c)38 s.JujuConnSuite.SetUpTest(c)
39 password, err := utils.RandomPassword()39 user := s.AddUser(c, "joe")
40 c.Assert(err, gc.IsNil)
41 user, err := s.State.AddUser("joe", password)
42 c.Assert(err, gc.IsNil)
43 s.userTag = user.Tag()40 s.userTag = user.Tag()
44 s.password = password41 s.password = "password"
45}42}
4643
47func (s *authHttpSuite) sendRequest(c *gc.C, tag, password, method, uri, contentType string, body io.Reader) (*http.Response, error) {44func (s *authHttpSuite) sendRequest(c *gc.C, tag, password, method, uri, contentType string, body io.Reader) (*http.Response, error) {
4845
=== modified file 'state/apiserver/client/api_test.go'
--- state/apiserver/client/api_test.go 2014-05-13 04:30:48 +0000
+++ state/apiserver/client/api_test.go 2014-06-03 04:22:37 +0000
@@ -277,8 +277,7 @@
277 setDefaultPassword(c, u)277 setDefaultPassword(c, u)
278 add(u)278 add(u)
279279
280 u, err = s.State.AddUser("other", "password")280 u = s.AddUser(c, "other")
281 c.Assert(err, gc.IsNil)
282 setDefaultPassword(c, u)281 setDefaultPassword(c, u)
283 add(u)282 add(u)
284283
285284
=== modified file 'state/apiserver/client/client_test.go'
--- state/apiserver/client/client_test.go 2014-05-16 02:43:52 +0000
+++ state/apiserver/client/client_test.go 2014-06-03 04:22:37 +0000
@@ -27,7 +27,6 @@
27 "launchpad.net/juju-core/state"27 "launchpad.net/juju-core/state"
28 "launchpad.net/juju-core/state/api"28 "launchpad.net/juju-core/state/api"
29 "launchpad.net/juju-core/state/api/params"29 "launchpad.net/juju-core/state/api/params"
30 "launchpad.net/juju-core/state/api/usermanager"
31 "launchpad.net/juju-core/state/apiserver/client"30 "launchpad.net/juju-core/state/apiserver/client"
32 "launchpad.net/juju-core/state/presence"31 "launchpad.net/juju-core/state/presence"
33 coretesting "launchpad.net/juju-core/testing"32 coretesting "launchpad.net/juju-core/testing"
@@ -834,12 +833,10 @@
834 defer restore()833 defer restore()
835 curl, _ := addCharm(c, store, "dummy")834 curl, _ := addCharm(c, store, "dummy")
836835
837 usermanager := usermanager.NewClient(s.APIState)836 s.AddUser(c, "foobar")
838 err := usermanager.AddUser("foobar", "password")
839 c.Assert(err, gc.IsNil)
840 s.APIState = s.OpenAPIAs(c, "user-foobar", "password")837 s.APIState = s.OpenAPIAs(c, "user-foobar", "password")
841838
842 err = s.APIState.Client().ServiceDeploy(839 err := s.APIState.Client().ServiceDeploy(
843 curl.String(), "service", 3, "", constraints.Value{}, "",840 curl.String(), "service", 3, "", constraints.Value{}, "",
844 )841 )
845 c.Assert(err, gc.IsNil)842 c.Assert(err, gc.IsNil)
846843
=== modified file 'state/apiserver/login_test.go'
--- state/apiserver/login_test.go 2014-04-17 13:14:49 +0000
+++ state/apiserver/login_test.go 2014-06-03 04:22:37 +0000
@@ -130,8 +130,7 @@
130 st, err := api.Open(info, fastDialOpts)130 st, err := api.Open(info, fastDialOpts)
131 c.Assert(err, gc.IsNil)131 c.Assert(err, gc.IsNil)
132 defer st.Close()132 defer st.Close()
133 u, err := s.State.AddUser("inactive", "password")133 u := s.AddUser(c, "inactive")
134 c.Assert(err, gc.IsNil)
135 err = u.Deactivate()134 err = u.Deactivate()
136 c.Assert(err, gc.IsNil)135 c.Assert(err, gc.IsNil)
137136
138137
=== modified file 'state/apiserver/usermanager/usermanager.go'
--- state/apiserver/usermanager/usermanager.go 2014-04-25 13:57:06 +0000
+++ state/apiserver/usermanager/usermanager.go 2014-06-03 04:22:37 +0000
@@ -6,6 +6,7 @@
6import (6import (
7 "fmt"7 "fmt"
88
9 "github.com/juju/errors"
9 "github.com/juju/loggo"10 "github.com/juju/loggo"
1011
11 "launchpad.net/juju-core/state"12 "launchpad.net/juju-core/state"
@@ -17,7 +18,7 @@
1718
18// UserManager defines the methods on the usermanager API end point.19// UserManager defines the methods on the usermanager API end point.
19type UserManager interface {20type UserManager interface {
20 AddUser(arg params.EntityPasswords) (params.ErrorResults, error)21 AddUser(arg params.ModifyUsers) (params.ErrorResults, error)
21 RemoveUser(arg params.Entities) (params.ErrorResults, error)22 RemoveUser(arg params.Entities) (params.ErrorResults, error)
22}23}
2324
@@ -48,7 +49,7 @@
48 nil49 nil
49}50}
5051
51func (api *UserManagerAPI) AddUser(args params.EntityPasswords) (params.ErrorResults, error) {52func (api *UserManagerAPI) AddUser(args params.ModifyUsers) (params.ErrorResults, error) {
52 result := params.ErrorResults{53 result := params.ErrorResults{
53 Results: make([]params.ErrorResult, len(args.Changes)),54 Results: make([]params.ErrorResult, len(args.Changes)),
54 }55 }
@@ -65,9 +66,13 @@
65 result.Results[0].Error = common.ServerError(common.ErrPerm)66 result.Results[0].Error = common.ServerError(common.ErrPerm)
66 continue67 continue
67 }68 }
68 _, err := api.state.AddUser(arg.Tag, arg.Password)69 username := arg.Username
70 if username == "" {
71 username = arg.Tag
72 }
73 _, err := api.state.AddUser(username, arg.DisplayName, arg.Password)
69 if err != nil {74 if err != nil {
70 err = fmt.Errorf("Failed to create user: %v", err)75 err = errors.Annotate(err, "failed to create user")
71 result.Results[i].Error = common.ServerError(err)76 result.Results[i].Error = common.ServerError(err)
72 continue77 continue
73 }78 }
7479
=== modified file 'state/apiserver/usermanager/usermanager_test.go'
--- state/apiserver/usermanager/usermanager_test.go 2014-04-17 12:47:50 +0000
+++ state/apiserver/usermanager/usermanager_test.go 2014-06-03 04:22:37 +0000
@@ -44,32 +44,36 @@
44}44}
4545
46func (s *userManagerSuite) TestAddUser(c *gc.C) {46func (s *userManagerSuite) TestAddUser(c *gc.C) {
47 arg := params.EntityPassword{47 args := params.ModifyUsers{
48 Tag: "foobar",48 Changes: []params.ModifyUser{{
49 Password: "password",49 Username: "foobar",
50 }50 DisplayName: "Foo Bar",
5151 Password: "password",
52 args := params.EntityPasswords{Changes: []params.EntityPassword{arg}}52 }}}
5353
54 result, err := s.usermanager.AddUser(args)54 result, err := s.usermanager.AddUser(args)
55 // Check that the call is succesful55 // Check that the call is succesful
56 c.Assert(err, gc.IsNil)56 c.Assert(err, gc.IsNil)
57 c.Assert(result, gc.DeepEquals, params.ErrorResults{Results: []params.ErrorResult{params.ErrorResult{Error: nil}}})57 c.Assert(result.Results, gc.HasLen, 1)
58 c.Assert(result.Results[0], gc.DeepEquals, params.ErrorResult{Error: nil})
58 // Check that the call results in a new user being created59 // Check that the call results in a new user being created
59 user, err := s.State.User("foobar")60 user, err := s.State.User("foobar")
60 c.Assert(err, gc.IsNil)61 c.Assert(err, gc.IsNil)
61 c.Assert(user, gc.NotNil)62 c.Assert(user, gc.NotNil)
63 c.Assert(user.Name(), gc.Equals, "foobar")
64 c.Assert(user.DisplayName(), gc.Equals, "Foo Bar")
62}65}
6366
64func (s *userManagerSuite) TestRemoveUser(c *gc.C) {67func (s *userManagerSuite) TestRemoveUser(c *gc.C) {
65 arg := params.EntityPassword{68 args := params.ModifyUsers{
66 Tag: "foobar",69 Changes: []params.ModifyUser{{
67 Password: "password",70 Username: "foobar",
68 }71 DisplayName: "Foo Bar",
72 Password: "password",
73 }}}
69 removeArg := params.Entity{74 removeArg := params.Entity{
70 Tag: "foobar",75 Tag: "foobar",
71 }76 }
72 args := params.EntityPasswords{Changes: []params.EntityPassword{arg}}
73 removeArgs := params.Entities{Entities: []params.Entity{removeArg}}77 removeArgs := params.Entities{Entities: []params.Entity{removeArg}}
74 _, err := s.usermanager.AddUser(args)78 _, err := s.usermanager.AddUser(args)
75 c.Assert(err, gc.IsNil)79 c.Assert(err, gc.IsNil)
@@ -83,21 +87,22 @@
83 c.Assert(err, gc.IsNil)87 c.Assert(err, gc.IsNil)
84 // Removal makes the user in active88 // Removal makes the user in active
85 c.Assert(user.IsDeactivated(), gc.Equals, true)89 c.Assert(user.IsDeactivated(), gc.Equals, true)
86 c.Assert(user.PasswordValid(arg.Password), gc.Equals, false)90 c.Assert(user.PasswordValid(args.Changes[0].Password), gc.Equals, false)
87}91}
8892
89// Since removing a user just deacitvates them you cannot add a user93// Since removing a user just deacitvates them you cannot add a user
90// that has been previously been removed94// that has been previously been removed
91// TODO(mattyw) 2014-03-07 bug #128874595// TODO(mattyw) 2014-03-07 bug #1288745
92func (s *userManagerSuite) TestCannotAddRemoveAdd(c *gc.C) {96func (s *userManagerSuite) TestCannotAddRemoveAdd(c *gc.C) {
93 arg := params.EntityPassword{
94 Tag: "addremove",
95 Password: "password",
96 }
97 removeArg := params.Entity{97 removeArg := params.Entity{
98 Tag: "foobar",98 Tag: "foobar",
99 }99 }
100 args := params.EntityPasswords{Changes: []params.EntityPassword{arg}}100 args := params.ModifyUsers{
101 Changes: []params.ModifyUser{{
102 Username: "foobar",
103 DisplayName: "Foo Bar",
104 Password: "password",
105 }}}
101 removeArgs := params.Entities{Entities: []params.Entity{removeArg}}106 removeArgs := params.Entities{Entities: []params.Entity{removeArg}}
102 _, err := s.usermanager.AddUser(args)107 _, err := s.usermanager.AddUser(args)
103 c.Assert(err, gc.IsNil)108 c.Assert(err, gc.IsNil)
@@ -106,7 +111,7 @@
106 c.Assert(err, gc.IsNil)111 c.Assert(err, gc.IsNil)
107 _, err = s.State.User("addremove")112 _, err = s.State.User("addremove")
108 result, err := s.usermanager.AddUser(args)113 result, err := s.usermanager.AddUser(args)
109 expectedError := apiservertesting.ServerError("Failed to create user: user already exists")114 expectedError := apiservertesting.ServerError("failed to create user: user already exists")
110 c.Assert(result, gc.DeepEquals, params.ErrorResults{115 c.Assert(result, gc.DeepEquals, params.ErrorResults{
111 Results: []params.ErrorResult{116 Results: []params.ErrorResult{
112 params.ErrorResult{expectedError}}})117 params.ErrorResult{expectedError}}})
113118
=== modified file 'state/compat_test.go'
--- state/compat_test.go 2014-05-20 04:27:02 +0000
+++ state/compat_test.go 2014-06-03 04:22:37 +0000
@@ -68,7 +68,7 @@
68}68}
6969
70func (s *compatSuite) TestGetServiceWithoutNetworksIsOK(c *gc.C) {70func (s *compatSuite) TestGetServiceWithoutNetworksIsOK(c *gc.C) {
71 _, err := s.state.AddUser(AdminUser, "pass")71 _, err := s.state.AddUser(AdminUser, "", "pass")
72 c.Assert(err, gc.IsNil)72 c.Assert(err, gc.IsNil)
73 charm := addCharm(c, s.state, "quantal", testing.Charms.Dir("mysql"))73 charm := addCharm(c, s.state, "quantal", testing.Charms.Dir("mysql"))
74 service, err := s.state.AddService("mysql", "user-admin", charm, nil, nil)74 service, err := s.state.AddService("mysql", "user-admin", charm, nil, nil)
7575
=== modified file 'state/conn_test.go'
--- state/conn_test.go 2014-05-27 02:47:01 +0000
+++ state/conn_test.go 2014-06-03 04:22:37 +0000
@@ -59,7 +59,7 @@
59 cs.services = cs.MgoSuite.Session.DB("juju").C("services")59 cs.services = cs.MgoSuite.Session.DB("juju").C("services")
60 cs.units = cs.MgoSuite.Session.DB("juju").C("units")60 cs.units = cs.MgoSuite.Session.DB("juju").C("units")
61 cs.stateServers = cs.MgoSuite.Session.DB("juju").C("stateServers")61 cs.stateServers = cs.MgoSuite.Session.DB("juju").C("stateServers")
62 cs.State.AddUser(state.AdminUser, "pass")62 cs.State.AddUser(state.AdminUser, "", "pass")
63}63}
6464
65func (cs *ConnSuite) TearDownTest(c *gc.C) {65func (cs *ConnSuite) TearDownTest(c *gc.C) {
6666
=== modified file 'state/megawatcher_internal_test.go'
--- state/megawatcher_internal_test.go 2014-05-20 04:27:02 +0000
+++ state/megawatcher_internal_test.go 2014-06-03 04:22:37 +0000
@@ -47,7 +47,7 @@
47 s.BaseSuite.SetUpTest(c)47 s.BaseSuite.SetUpTest(c)
48 s.MgoSuite.SetUpTest(c)48 s.MgoSuite.SetUpTest(c)
49 s.State = TestingInitialize(c, nil, Policy(nil))49 s.State = TestingInitialize(c, nil, Policy(nil))
50 s.State.AddUser(AdminUser, "pass")50 s.State.AddUser(AdminUser, "", "pass")
51}51}
5252
53func (s *storeManagerStateSuite) TearDownTest(c *gc.C) {53func (s *storeManagerStateSuite) TearDownTest(c *gc.C) {
5454
=== modified file 'state/state_test.go'
--- state/state_test.go 2014-05-27 06:12:16 +0000
+++ state/state_test.go 2014-06-03 04:22:37 +0000
@@ -2372,7 +2372,7 @@
2372 svc := s.AddTestingService(c, "ser-vice2", s.AddTestingCharm(c, "mysql"))2372 svc := s.AddTestingService(c, "ser-vice2", s.AddTestingCharm(c, "mysql"))
2373 _, err = svc.AddUnit()2373 _, err = svc.AddUnit()
2374 c.Assert(err, gc.IsNil)2374 c.Assert(err, gc.IsNil)
2375 _, err = s.State.AddUser("arble", "pass")2375 _, err = s.State.AddUser("arble", "", "pass")
2376 c.Assert(err, gc.IsNil)2376 c.Assert(err, gc.IsNil)
2377 s.AddTestingService(c, "wordpress", s.AddTestingCharm(c, "wordpress"))2377 s.AddTestingService(c, "wordpress", s.AddTestingCharm(c, "wordpress"))
2378 eps, err := s.State.InferEndpoints([]string{"wordpress", "ser-vice2"})2378 eps, err := s.State.InferEndpoints([]string{"wordpress", "ser-vice2"})
@@ -2464,7 +2464,7 @@
2464 c.Assert(err, gc.IsNil)2464 c.Assert(err, gc.IsNil)
24652465
2466 // Parse a user entity name.2466 // Parse a user entity name.
2467 user, err := s.State.AddUser("arble", "pass")2467 user, err := s.State.AddUser("arble", "", "pass")
2468 c.Assert(err, gc.IsNil)2468 c.Assert(err, gc.IsNil)
2469 coll, id, err = state.ParseTag(s.State, user.Tag())2469 coll, id, err = state.ParseTag(s.State, user.Tag())
2470 c.Assert(coll, gc.Equals, "users")2470 c.Assert(coll, gc.Equals, "users")
24712471
=== modified file 'state/user.go'
--- state/user.go 2014-05-21 03:07:36 +0000
+++ state/user.go 2014-06-03 04:22:37 +0000
@@ -22,9 +22,9 @@
22}22}
2323
24// AddUser adds a user to the state.24// AddUser adds a user to the state.
25func (st *State) AddUser(name, password string) (*User, error) {25func (st *State) AddUser(username, displayName, password string) (*User, error) {
26 if !names.IsUser(name) {26 if !names.IsUser(username) {
27 return nil, fmt.Errorf("invalid user name %q", name)27 return nil, errors.Errorf("invalid user name %q", username)
28 }28 }
29 salt, err := utils.RandomSalt()29 salt, err := utils.RandomSalt()
30 if err != nil {30 if err != nil {
@@ -33,23 +33,24 @@
33 u := &User{33 u := &User{
34 st: st,34 st: st,
35 doc: userDoc{35 doc: userDoc{
36 Name: name,36 Name: username,
37 DisplayName: displayName,
37 PasswordHash: utils.UserPasswordHash(password, salt),38 PasswordHash: utils.UserPasswordHash(password, salt),
38 PasswordSalt: salt,39 PasswordSalt: salt,
39 },40 },
40 }41 }
41 ops := []txn.Op{{42 ops := []txn.Op{{
42 C: st.users.Name,43 C: st.users.Name,
43 Id: name,44 Id: username,
44 Assert: txn.DocMissing,45 Assert: txn.DocMissing,
45 Insert: &u.doc,46 Insert: &u.doc,
46 }}47 }}
47 err = st.runTransaction(ops)48 err = st.runTransaction(ops)
48 if err == txn.ErrAborted {49 if err == txn.ErrAborted {
49 err = fmt.Errorf("user already exists")50 err = errors.New("user already exists")
50 }51 }
51 if err != nil {52 if err != nil {
52 return nil, err53 return nil, errors.Trace(err)
53 }54 }
54 return u, nil55 return u, nil
55}56}
@@ -68,7 +69,7 @@
68func (st *State) User(name string) (*User, error) {69func (st *State) User(name string) (*User, error) {
69 u := &User{st: st}70 u := &User{st: st}
70 if err := st.getUser(name, &u.doc); err != nil {71 if err := st.getUser(name, &u.doc); err != nil {
71 return nil, err72 return nil, errors.Trace(err)
72 }73 }
73 return u, nil74 return u, nil
74}75}
@@ -81,7 +82,8 @@
8182
82type userDoc struct {83type userDoc struct {
83 Name string `bson:"_id_"`84 Name string `bson:"_id_"`
84 Deactivated bool // Removing users means they still exist, but are marked deactivated85 DisplayName string
86 Deactivated bool // Removing users means they still exist, but are marked deactivated
85 PasswordHash string87 PasswordHash string
86 PasswordSalt string88 PasswordSalt string
87}89}
@@ -91,6 +93,11 @@
91 return u.doc.Name93 return u.doc.Name
92}94}
9395
96// DisplayName returns the display name of the user.
97func (u *User) DisplayName() string {
98 return u.doc.DisplayName
99}
100
94// Tag returns the Tag for101// Tag returns the Tag for
95// the user ("user-$username")102// the user ("user-$username")
96func (u *User) Tag() string {103func (u *User) Tag() string {
97104
=== modified file 'state/user_test.go'
--- state/user_test.go 2014-05-22 00:48:08 +0000
+++ state/user_test.go 2014-06-03 04:22:37 +0000
@@ -17,67 +17,69 @@
17 ConnSuite17 ConnSuite
18}18}
1919
20var _ = gc.Suite(&UserSuite{})20var (
21 _ = gc.Suite(&UserSuite{})
22)
2123
22func (s *UserSuite) TestAddUserInvalidNames(c *gc.C) {24func (s *UserSuite) TestAddUserInvalidNames(c *gc.C) {
23 for _, name := range []string{25 for _, name := range []string{
24 "",26 "",
25 "b^b",27 "b^b",
26 } {28 } {
27 u, err := s.State.AddUser(name, "password")29 u, err := s.State.AddUser(name, "ignored", "ignored")
28 c.Assert(err, gc.ErrorMatches, `invalid user name "`+regexp.QuoteMeta(name)+`"`)30 c.Assert(err, gc.ErrorMatches, `invalid user name "`+regexp.QuoteMeta(name)+`"`)
29 c.Assert(u, gc.IsNil)31 c.Assert(u, gc.IsNil)
30 }32 }
31}33}
3234
33func (s *UserSuite) TestAddUserValidName(c *gc.C) {35func (s *UserSuite) addUser(c *gc.C, name, displayName, password string) *state.User {
36 user, err := s.State.AddUser(name, displayName, password)
37 c.Assert(err, gc.IsNil)
38 c.Assert(user, gc.NotNil)
39 c.Assert(user.Name(), gc.Equals, name)
40 c.Assert(user.DisplayName(), gc.Equals, displayName)
41 c.Assert(user.PasswordValid(password), jc.IsTrue)
42 return user
43}
44
45func (s *UserSuite) makeUser(c *gc.C) *state.User {
46 return s.addUser(c, "someuser", "displayName", "a-password")
47}
48
49func (s *UserSuite) TestAddUser(c *gc.C) {
34 name := "f00-Bar.ram77"50 name := "f00-Bar.ram77"
35 u, err := s.State.AddUser(name, "password")51 displayName := "Display"
36 c.Check(u, gc.NotNil)52 password := "password"
37 c.Assert(err, gc.IsNil)53
38 c.Assert(u.Name(), gc.Equals, name)54 s.addUser(c, name, displayName, password)
39}55
4056 user, err := s.State.User(name)
41func (s *UserSuite) TestAddUser(c *gc.C) {57 c.Assert(err, gc.IsNil)
42 u, err := s.State.AddUser("aa", "b")58 c.Check(user, gc.NotNil)
43 c.Check(u, gc.NotNil)59 c.Assert(user.Name(), gc.Equals, name)
44 c.Assert(err, gc.IsNil)60 c.Assert(user.DisplayName(), gc.Equals, displayName)
4561 c.Assert(user.PasswordValid(password), jc.IsTrue)
46 c.Assert(u.Name(), gc.Equals, "aa")
47 c.Assert(u.PasswordValid("b"), jc.IsTrue)
48
49 u1, err := s.State.User("aa")
50 c.Check(u1, gc.NotNil)
51 c.Assert(err, gc.IsNil)
52
53 c.Assert(u1.Name(), gc.Equals, "aa")
54 c.Assert(u1.PasswordValid("b"), jc.IsTrue)
55}62}
5663
57func (s *UserSuite) TestCheckUserExists(c *gc.C) {64func (s *UserSuite) TestCheckUserExists(c *gc.C) {
58 u, err := s.State.AddUser("aa", "b")65 user := s.makeUser(c)
59 c.Check(u, gc.NotNil)66 exists, err := state.CheckUserExists(s.State, user.Name())
60 c.Assert(err, gc.IsNil)67 c.Assert(err, gc.IsNil)
61 e, err := state.CheckUserExists(s.State, "aa")68 c.Assert(exists, jc.IsTrue)
62 c.Assert(err, gc.IsNil)69 exists, err = state.CheckUserExists(s.State, "notAUser")
63 c.Assert(e, gc.Equals, true)70 c.Assert(err, gc.IsNil)
64 e, err = state.CheckUserExists(s.State, "notAUser")71 c.Assert(exists, jc.IsFalse)
65 c.Assert(err, gc.IsNil)
66 c.Assert(e, gc.Equals, false)
67}72}
6873
69func (s *UserSuite) TestSetPassword(c *gc.C) {74func (s *UserSuite) TestSetPassword(c *gc.C) {
70 u, err := s.State.AddUser("someuser", "password")75 user := s.makeUser(c)
71 c.Assert(err, gc.IsNil)
72
73 testSetPassword(c, func() (state.Authenticator, error) {76 testSetPassword(c, func() (state.Authenticator, error) {
74 return s.State.User(u.Name())77 return s.State.User(user.Name())
75 })78 })
76}79}
7780
78func (s *UserSuite) TestAddUserSetsSalt(c *gc.C) {81func (s *UserSuite) TestAddUserSetsSalt(c *gc.C) {
79 u, err := s.State.AddUser("someuser", "a-password")82 u := s.makeUser(c)
80 c.Assert(err, gc.IsNil)
81 salt, hash := state.GetUserPasswordSaltAndHash(u)83 salt, hash := state.GetUserPasswordSaltAndHash(u)
82 c.Check(hash, gc.Not(gc.Equals), "")84 c.Check(hash, gc.Not(gc.Equals), "")
83 c.Check(salt, gc.Not(gc.Equals), "")85 c.Check(salt, gc.Not(gc.Equals), "")
@@ -86,8 +88,7 @@
86}88}
8789
88func (s *UserSuite) TestSetPasswordChangesSalt(c *gc.C) {90func (s *UserSuite) TestSetPasswordChangesSalt(c *gc.C) {
89 u, err := s.State.AddUser("someuser", "a-password")91 u := s.makeUser(c)
90 c.Assert(err, gc.IsNil)
91 origSalt, origHash := state.GetUserPasswordSaltAndHash(u)92 origSalt, origHash := state.GetUserPasswordSaltAndHash(u)
92 c.Check(origSalt, gc.Not(gc.Equals), "")93 c.Check(origSalt, gc.Not(gc.Equals), "")
93 // Even though the password is the same, we take this opportunity to94 // Even though the password is the same, we take this opportunity to
@@ -101,10 +102,9 @@
101}102}
102103
103func (s *UserSuite) TestSetPasswordHash(c *gc.C) {104func (s *UserSuite) TestSetPasswordHash(c *gc.C) {
104 u, err := s.State.AddUser("someuser", "password")105 u := s.makeUser(c)
105 c.Assert(err, gc.IsNil)
106106
107 err = u.SetPasswordHash(utils.UserPasswordHash("foo", utils.CompatSalt), utils.CompatSalt)107 err := u.SetPasswordHash(utils.UserPasswordHash("foo", utils.CompatSalt), utils.CompatSalt)
108 c.Assert(err, gc.IsNil)108 c.Assert(err, gc.IsNil)
109109
110 c.Assert(u.PasswordValid("foo"), jc.IsTrue)110 c.Assert(u.PasswordValid("foo"), jc.IsTrue)
@@ -120,10 +120,9 @@
120}120}
121121
122func (s *UserSuite) TestSetPasswordHashWithSalt(c *gc.C) {122func (s *UserSuite) TestSetPasswordHashWithSalt(c *gc.C) {
123 u, err := s.State.AddUser("someuser", "password")123 u := s.makeUser(c)
124 c.Assert(err, gc.IsNil)
125124
126 err = u.SetPasswordHash(utils.UserPasswordHash("foo", "salted"), "salted")125 err := u.SetPasswordHash(utils.UserPasswordHash("foo", "salted"), "salted")
127 c.Assert(err, gc.IsNil)126 c.Assert(err, gc.IsNil)
128127
129 c.Assert(u.PasswordValid("foo"), jc.IsTrue)128 c.Assert(u.PasswordValid("foo"), jc.IsTrue)
@@ -133,11 +132,10 @@
133}132}
134133
135func (s *UserSuite) TestPasswordValidUpdatesSalt(c *gc.C) {134func (s *UserSuite) TestPasswordValidUpdatesSalt(c *gc.C) {
136 u, err := s.State.AddUser("someuser", "password")135 u := s.makeUser(c)
137 c.Assert(err, gc.IsNil)
138136
139 compatHash := utils.UserPasswordHash("foo", utils.CompatSalt)137 compatHash := utils.UserPasswordHash("foo", utils.CompatSalt)
140 err = u.SetPasswordHash(compatHash, "")138 err := u.SetPasswordHash(compatHash, "")
141 c.Assert(err, gc.IsNil)139 c.Assert(err, gc.IsNil)
142 beforeSalt, beforeHash := state.GetUserPasswordSaltAndHash(u)140 beforeSalt, beforeHash := state.GetUserPasswordSaltAndHash(u)
143 c.Assert(beforeSalt, gc.Equals, "")141 c.Assert(beforeSalt, gc.Equals, "")
@@ -160,20 +158,11 @@
160 c.Assert(lastHash, gc.Equals, afterHash)158 c.Assert(lastHash, gc.Equals, afterHash)
161}159}
162160
163func (s *UserSuite) TestName(c *gc.C) {
164 u, err := s.State.AddUser("someuser", "password")
165 c.Assert(err, gc.IsNil)
166
167 c.Assert(u.Name(), gc.Equals, "someuser")
168 c.Assert(u.Tag(), gc.Equals, "user-someuser")
169}
170
171func (s *UserSuite) TestDeactivate(c *gc.C) {161func (s *UserSuite) TestDeactivate(c *gc.C) {
172 u, err := s.State.AddUser("someuser", "password")162 u := s.makeUser(c)
173 c.Assert(err, gc.IsNil)
174 c.Assert(u.IsDeactivated(), gc.Equals, false)163 c.Assert(u.IsDeactivated(), gc.Equals, false)
175164
176 err = u.Deactivate()165 err := u.Deactivate()
177 c.Assert(err, gc.IsNil)166 c.Assert(err, gc.IsNil)
178 c.Assert(u.IsDeactivated(), gc.Equals, true)167 c.Assert(u.IsDeactivated(), gc.Equals, true)
179 c.Assert(u.PasswordValid(""), gc.Equals, false)168 c.Assert(u.PasswordValid(""), gc.Equals, false)

Subscribers

People subscribed via source and target branches

to status/vote changes: