Skip to content

Commit d2f0054

Browse files
jtopjianjrperritt
authored andcommitted
Identity v3: Users Create (#392)
* Identity v3: Users Create * Identity v3: Users Create unit tests * Identity v3: Users Create acceptance tests * Identity v3 Users: code review updates
1 parent 35bca3f commit d2f0054

7 files changed

Lines changed: 298 additions & 2 deletions

File tree

‎acceptance/openstack/identity/v3/identity.go‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/gophercloud/gophercloud"
77
"github.com/gophercloud/gophercloud/acceptance/tools"
88
"github.com/gophercloud/gophercloud/openstack/identity/v3/projects"
9+
"github.com/gophercloud/gophercloud/openstack/identity/v3/users"
910
)
1011

1112
// CreateProject will create a project with a random name.
@@ -35,6 +36,33 @@ func CreateProject(t *testing.T, client *gophercloud.ServiceClient, c *projects.
3536
return project, nil
3637
}
3738

39+
// CreateUser will create a project with a random name.
40+
// It takes an optional createOpts parameter since creating a user
41+
// has so many options. An error will be returned if the user was
42+
// unable to be created.
43+
func CreateUser(t *testing.T, client *gophercloud.ServiceClient, c *users.CreateOpts) (*users.User, error) {
44+
name := tools.RandomString("ACPTTEST", 8)
45+
t.Logf("Attempting to create user: %s", name)
46+
47+
var createOpts users.CreateOpts
48+
if c != nil {
49+
createOpts = *c
50+
} else {
51+
createOpts = users.CreateOpts{}
52+
}
53+
54+
createOpts.Name = name
55+
56+
user, err := users.Create(client, createOpts).Extract()
57+
if err != nil {
58+
return user, err
59+
}
60+
61+
t.Logf("Successfully created user %s with ID %s", name, user.ID)
62+
63+
return user, nil
64+
}
65+
3866
// DeleteProject will delete a project by ID. A fatal error will occur if
3967
// the project ID failed to be deleted. This works best when using it as
4068
// a deferred function.

‎acceptance/openstack/identity/v3/users_test.go‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,38 @@ func TestUsersGet(t *testing.T) {
6161

6262
tools.PrintResource(t, p)
6363
}
64+
65+
func TestUserCRUD(t *testing.T) {
66+
client, err := clients.NewIdentityV3Client()
67+
if err != nil {
68+
t.Fatalf("Unable to obtain an identity client: %v", err)
69+
}
70+
71+
project, err := CreateProject(t, client, nil)
72+
if err != nil {
73+
t.Fatalf("Unable to create project: %v", err)
74+
}
75+
defer DeleteProject(t, client, project.ID)
76+
77+
tools.PrintResource(t, project)
78+
79+
createOpts := users.CreateOpts{
80+
DefaultProjectID: project.ID,
81+
Password: "foobar",
82+
DomainID: "default",
83+
Options: map[users.Option]interface{}{
84+
users.IgnorePasswordExpiry: true,
85+
},
86+
Extra: map[string]interface{}{
87+
"email": "[email protected]",
88+
},
89+
}
90+
91+
user, err := CreateUser(t, client, &createOpts)
92+
if err != nil {
93+
t.Fatalf("Unable to create user: %v", err)
94+
}
95+
96+
tools.PrintResource(t, user)
97+
tools.PrintResource(t, user.Extra)
98+
}

‎openstack/identity/v3/users/requests.go‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@ import (
55
"github.com/gophercloud/gophercloud/pagination"
66
)
77

8+
// Option is a specific option defined at the API to enable features
9+
// on a user account.
10+
type Option string
11+
12+
const (
13+
IgnoreChangePasswordUponFirstUse Option = "ignore_change_password_upon_first_use"
14+
IgnorePasswordExpiry Option = "ignore_password_expiry"
15+
IgnoreLockoutFailureAttempts Option = "ignore_lockout_failure_attempts"
16+
MultiFactorAuthRules Option = "multi_factor_auth_rules"
17+
MultiFactorAuthEnabled Option = "multi_factor_auth_enabled"
18+
)
19+
820
// ListOptsBuilder allows extensions to add additional parameters to
921
// the List request
1022
type ListOptsBuilder interface {
@@ -61,3 +73,67 @@ func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
6173
_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
6274
return
6375
}
76+
77+
// CreateOptsBuilder allows extensions to add additional parameters to
78+
// the Create request.
79+
type CreateOptsBuilder interface {
80+
ToUserCreateMap() (map[string]interface{}, error)
81+
}
82+
83+
// CreateOpts implements CreateOptsBuilder
84+
type CreateOpts struct {
85+
// Name is the name of the new user.
86+
Name string `json:"name" required:"true"`
87+
88+
// DefaultProjectID is the ID of the default project of the user.
89+
DefaultProjectID string `json:"default_project_id,omitempty"`
90+
91+
// Description is a description of the user.
92+
Description string `json:"description,omitempty"`
93+
94+
// DomainID is the ID of the domain the user belongs to.
95+
DomainID string `json:"domain_id,omitempty"`
96+
97+
// Enabled sets the user status to enabled or disabled.
98+
Enabled *bool `json:"enabled,omitempty"`
99+
100+
// Extra is free-form extra key/value pairs to describe the user.
101+
Extra map[string]interface{} `json:"-"`
102+
103+
// Options are defined options in the API to enable certain features.
104+
Options map[Option]interface{} `json:"options,omitempty"`
105+
106+
// Password is the password of the new user.
107+
Password string `json:"password,omitempty"`
108+
}
109+
110+
// ToUserCreateMap formats a CreateOpts into a create request.
111+
func (opts CreateOpts) ToUserCreateMap() (map[string]interface{}, error) {
112+
b, err := gophercloud.BuildRequestBody(opts, "user")
113+
if err != nil {
114+
return nil, err
115+
}
116+
117+
if opts.Extra != nil {
118+
if v, ok := b["user"].(map[string]interface{}); ok {
119+
for key, value := range opts.Extra {
120+
v[key] = value
121+
}
122+
}
123+
}
124+
125+
return b, nil
126+
}
127+
128+
// Create creates a new User.
129+
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
130+
b, err := opts.ToUserCreateMap()
131+
if err != nil {
132+
r.Err = err
133+
return
134+
}
135+
_, r.Err = client.Post(createURL(client), &b, &r.Body, &gophercloud.RequestOpts{
136+
OkCodes: []int{201},
137+
})
138+
return
139+
}

‎openstack/identity/v3/users/results.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ type GetResult struct {
8585
userResult
8686
}
8787

88+
// CreateResult temporarily contains the response from the Create call.
89+
type CreateResult struct {
90+
userResult
91+
}
92+
8893
// UserPage is a single page of User results.
8994
type UserPage struct {
9095
pagination.LinkedPageBase

‎openstack/identity/v3/users/testing/fixtures.go‎

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ const ListOutput = `
4747
"password_expires_at": "2016-11-06T15:32:17.000000",
4848
"email": "[email protected]",
4949
"options": {
50-
"ignore_password_expiry": true
50+
"ignore_password_expiry": true,
51+
"multi_factor_auth_rules": [["password", "totp"], ["password", "custom-auth-method"]]
5152
}
5253
}
5354
]
@@ -69,12 +70,63 @@ const GetOutput = `
6970
"password_expires_at": "2016-11-06T15:32:17.000000",
7071
"email": "[email protected]",
7172
"options": {
72-
"ignore_password_expiry": true
73+
"ignore_password_expiry": true,
74+
"multi_factor_auth_rules": [["password", "totp"], ["password", "custom-auth-method"]]
7375
}
7476
}
7577
}
7678
`
7779

80+
// GetOutputNoOptions provides a Get result of a user with no options.
81+
const GetOutputNoOptions = `
82+
{
83+
"user": {
84+
"default_project_id": "263fd9",
85+
"domain_id": "1789d1",
86+
"enabled": true,
87+
"id": "9fe1d3",
88+
"links": {
89+
"self": "https://example.com/identity/v3/users/9fe1d3"
90+
},
91+
"name": "jsmith",
92+
"password_expires_at": "2016-11-06T15:32:17.000000",
93+
"email": "[email protected]"
94+
}
95+
}
96+
`
97+
98+
// CreateRequest provides the input to a Create request.
99+
const CreateRequest = `
100+
{
101+
"user": {
102+
"default_project_id": "263fd9",
103+
"domain_id": "1789d1",
104+
"enabled": true,
105+
"name": "jsmith",
106+
"password": "secretsecret",
107+
"email": "[email protected]",
108+
"options": {
109+
"ignore_password_expiry": true,
110+
"multi_factor_auth_rules": [["password", "totp"], ["password", "custom-auth-method"]]
111+
}
112+
}
113+
}
114+
`
115+
116+
// CreateNoOptionsRequest provides the input to a Create request with no Options.
117+
const CreateNoOptionsRequest = `
118+
{
119+
"user": {
120+
"default_project_id": "263fd9",
121+
"domain_id": "1789d1",
122+
"enabled": true,
123+
"name": "jsmith",
124+
"password": "secretsecret",
125+
"email": "[email protected]"
126+
}
127+
}
128+
`
129+
78130
// FirstUser is the first user in the List request.
79131
var nilTime time.Time
80132
var FirstUser = users.User{
@@ -109,6 +161,25 @@ var SecondUser = users.User{
109161
},
110162
Options: map[string]interface{}{
111163
"ignore_password_expiry": true,
164+
"multi_factor_auth_rules": []interface{}{
165+
[]string{"password", "totp"},
166+
[]string{"password", "custom-auth-method"},
167+
},
168+
},
169+
}
170+
171+
var SecondUserNoOptions = users.User{
172+
DefaultProjectID: "263fd9",
173+
DomainID: "1789d1",
174+
Enabled: true,
175+
ID: "9fe1d3",
176+
Links: map[string]interface{}{
177+
"self": "https://example.com/identity/v3/users/9fe1d3",
178+
},
179+
Name: "jsmith",
180+
PasswordExpiresAt: SecondUserPasswordExpiresAt,
181+
Extra: map[string]interface{}{
182+
"email": "[email protected]",
112183
},
113184
}
114185

@@ -142,3 +213,29 @@ func HandleGetUserSuccessfully(t *testing.T) {
142213
fmt.Fprintf(w, GetOutput)
143214
})
144215
}
216+
217+
// HandleCreateUserSuccessfully creates an HTTP handler at `/users` on the
218+
// test handler mux that tests project creation.
219+
func HandleCreateUserSuccessfully(t *testing.T) {
220+
th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
221+
th.TestMethod(t, r, "POST")
222+
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
223+
th.TestJSONRequest(t, r, CreateRequest)
224+
225+
w.WriteHeader(http.StatusCreated)
226+
fmt.Fprintf(w, GetOutput)
227+
})
228+
}
229+
230+
// HandleCreateNoOptionsUserSuccessfully creates an HTTP handler at `/users` on the
231+
// test handler mux that tests project creation.
232+
func HandleCreateNoOptionsUserSuccessfully(t *testing.T) {
233+
th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
234+
th.TestMethod(t, r, "POST")
235+
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
236+
th.TestJSONRequest(t, r, CreateNoOptionsRequest)
237+
238+
w.WriteHeader(http.StatusCreated)
239+
fmt.Fprintf(w, GetOutputNoOptions)
240+
})
241+
}

‎openstack/identity/v3/users/testing/requests_test.go‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,54 @@ func TestGetUser(t *testing.T) {
5353
th.CheckDeepEquals(t, SecondUser, *actual)
5454
th.AssertEquals(t, SecondUser.Extra["email"], "[email protected]")
5555
}
56+
57+
func TestCreateUser(t *testing.T) {
58+
th.SetupHTTP()
59+
defer th.TeardownHTTP()
60+
HandleCreateUserSuccessfully(t)
61+
62+
iTrue := true
63+
createOpts := users.CreateOpts{
64+
Name: "jsmith",
65+
DomainID: "1789d1",
66+
Enabled: &iTrue,
67+
Password: "secretsecret",
68+
DefaultProjectID: "263fd9",
69+
Options: map[users.Option]interface{}{
70+
users.IgnorePasswordExpiry: true,
71+
users.MultiFactorAuthRules: []interface{}{
72+
[]string{"password", "totp"},
73+
[]string{"password", "custom-auth-method"},
74+
},
75+
},
76+
Extra: map[string]interface{}{
77+
"email": "[email protected]",
78+
},
79+
}
80+
81+
actual, err := users.Create(client.ServiceClient(), createOpts).Extract()
82+
th.AssertNoErr(t, err)
83+
th.CheckDeepEquals(t, SecondUser, *actual)
84+
}
85+
86+
func TestCreateNoOptionsUser(t *testing.T) {
87+
th.SetupHTTP()
88+
defer th.TeardownHTTP()
89+
HandleCreateNoOptionsUserSuccessfully(t)
90+
91+
iTrue := true
92+
createOpts := users.CreateOpts{
93+
Name: "jsmith",
94+
DomainID: "1789d1",
95+
Enabled: &iTrue,
96+
Password: "secretsecret",
97+
DefaultProjectID: "263fd9",
98+
Extra: map[string]interface{}{
99+
"email": "[email protected]",
100+
},
101+
}
102+
103+
actual, err := users.Create(client.ServiceClient(), createOpts).Extract()
104+
th.AssertNoErr(t, err)
105+
th.CheckDeepEquals(t, SecondUserNoOptions, *actual)
106+
}

‎openstack/identity/v3/users/urls.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@ func listURL(client *gophercloud.ServiceClient) string {
99
func getURL(client *gophercloud.ServiceClient, userID string) string {
1010
return client.ServiceURL("users", userID)
1111
}
12+
13+
func createURL(client *gophercloud.ServiceClient) string {
14+
return client.ServiceURL("users")
15+
}

0 commit comments

Comments
 (0)