Moving calls to client helper while I'm at it
diff --git a/_site/openstack/identity/v3/tokens/doc.go b/_site/openstack/identity/v3/tokens/doc.go
new file mode 100644
index 0000000..02fce0d
--- /dev/null
+++ b/_site/openstack/identity/v3/tokens/doc.go
@@ -0,0 +1,6 @@
+/*
+Package tokens defines operations performed on the token resource.
+
+Documentation: http://developer.openstack.org/api-ref-identity-v3.html#tokens-v3
+*/
+package tokens
diff --git a/_site/openstack/identity/v3/tokens/errors.go b/_site/openstack/identity/v3/tokens/errors.go
new file mode 100644
index 0000000..4476109
--- /dev/null
+++ b/_site/openstack/identity/v3/tokens/errors.go
@@ -0,0 +1,72 @@
+package tokens
+
+import (
+ "errors"
+ "fmt"
+)
+
+func unacceptedAttributeErr(attribute string) error {
+ return fmt.Errorf("The base Identity V3 API does not accept authentication by %s", attribute)
+}
+
+func redundantWithTokenErr(attribute string) error {
+ return fmt.Errorf("%s may not be provided when authenticating with a TokenID", attribute)
+}
+
+func redundantWithUserID(attribute string) error {
+ return fmt.Errorf("%s may not be provided when authenticating with a UserID", attribute)
+}
+
+var (
+ // ErrAPIKeyProvided indicates that an APIKey was provided but can't be used.
+ ErrAPIKeyProvided = unacceptedAttributeErr("APIKey")
+
+ // ErrTenantIDProvided indicates that a TenantID was provided but can't be used.
+ ErrTenantIDProvided = unacceptedAttributeErr("TenantID")
+
+ // ErrTenantNameProvided indicates that a TenantName was provided but can't be used.
+ ErrTenantNameProvided = unacceptedAttributeErr("TenantName")
+
+ // ErrUsernameWithToken indicates that a Username was provided, but token authentication is being used instead.
+ ErrUsernameWithToken = redundantWithTokenErr("Username")
+
+ // ErrUserIDWithToken indicates that a UserID was provided, but token authentication is being used instead.
+ ErrUserIDWithToken = redundantWithTokenErr("UserID")
+
+ // ErrDomainIDWithToken indicates that a DomainID was provided, but token authentication is being used instead.
+ ErrDomainIDWithToken = redundantWithTokenErr("DomainID")
+
+ // ErrDomainNameWithToken indicates that a DomainName was provided, but token authentication is being used instead.s
+ ErrDomainNameWithToken = redundantWithTokenErr("DomainName")
+
+ // ErrUsernameOrUserID indicates that neither username nor userID are specified, or both are at once.
+ ErrUsernameOrUserID = errors.New("Exactly one of Username and UserID must be provided for password authentication")
+
+ // ErrDomainIDWithUserID indicates that a DomainID was provided, but unnecessary because a UserID is being used.
+ ErrDomainIDWithUserID = redundantWithUserID("DomainID")
+
+ // ErrDomainNameWithUserID indicates that a DomainName was provided, but unnecessary because a UserID is being used.
+ ErrDomainNameWithUserID = redundantWithUserID("DomainName")
+
+ // ErrDomainIDOrDomainName indicates that a username was provided, but no domain to scope it.
+ // It may also indicate that both a DomainID and a DomainName were provided at once.
+ ErrDomainIDOrDomainName = errors.New("You must provide exactly one of DomainID or DomainName to authenticate by Username")
+
+ // ErrMissingPassword indicates that no password was provided and no token is available.
+ ErrMissingPassword = errors.New("You must provide a password to authenticate")
+
+ // ErrScopeDomainIDOrDomainName indicates that a domain ID or Name was required in a Scope, but not present.
+ ErrScopeDomainIDOrDomainName = errors.New("You must provide exactly one of DomainID or DomainName in a Scope with ProjectName")
+
+ // ErrScopeProjectIDOrProjectName indicates that both a ProjectID and a ProjectName were provided in a Scope.
+ ErrScopeProjectIDOrProjectName = errors.New("You must provide at most one of ProjectID or ProjectName in a Scope")
+
+ // ErrScopeProjectIDAlone indicates that a ProjectID was provided with other constraints in a Scope.
+ ErrScopeProjectIDAlone = errors.New("ProjectID must be supplied alone in a Scope")
+
+ // ErrScopeDomainName indicates that a DomainName was provided alone in a Scope.
+ ErrScopeDomainName = errors.New("DomainName must be supplied with a ProjectName or ProjectID in a Scope.")
+
+ // ErrScopeEmpty indicates that no credentials were provided in a Scope.
+ ErrScopeEmpty = errors.New("You must provide either a Project or Domain in a Scope")
+)
diff --git a/_site/openstack/identity/v3/tokens/requests.go b/_site/openstack/identity/v3/tokens/requests.go
new file mode 100644
index 0000000..c8587b6
--- /dev/null
+++ b/_site/openstack/identity/v3/tokens/requests.go
@@ -0,0 +1,285 @@
+package tokens
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+)
+
+// Scope allows a created token to be limited to a specific domain or project.
+type Scope struct {
+ ProjectID string
+ ProjectName string
+ DomainID string
+ DomainName string
+}
+
+func subjectTokenHeaders(c *gophercloud.ServiceClient, subjectToken string) map[string]string {
+ h := c.Provider.AuthenticatedHeaders()
+ h["X-Subject-Token"] = subjectToken
+ return h
+}
+
+// Create authenticates and either generates a new token, or changes the Scope of an existing token.
+func Create(c *gophercloud.ServiceClient, options gophercloud.AuthOptions, scope *Scope) CreateResult {
+ type domainReq struct {
+ ID *string `json:"id,omitempty"`
+ Name *string `json:"name,omitempty"`
+ }
+
+ type projectReq struct {
+ Domain *domainReq `json:"domain,omitempty"`
+ Name *string `json:"name,omitempty"`
+ ID *string `json:"id,omitempty"`
+ }
+
+ type userReq struct {
+ ID *string `json:"id,omitempty"`
+ Name *string `json:"name,omitempty"`
+ Password string `json:"password"`
+ Domain *domainReq `json:"domain,omitempty"`
+ }
+
+ type passwordReq struct {
+ User userReq `json:"user"`
+ }
+
+ type tokenReq struct {
+ ID string `json:"id"`
+ }
+
+ type identityReq struct {
+ Methods []string `json:"methods"`
+ Password *passwordReq `json:"password,omitempty"`
+ Token *tokenReq `json:"token,omitempty"`
+ }
+
+ type scopeReq struct {
+ Domain *domainReq `json:"domain,omitempty"`
+ Project *projectReq `json:"project,omitempty"`
+ }
+
+ type authReq struct {
+ Identity identityReq `json:"identity"`
+ Scope *scopeReq `json:"scope,omitempty"`
+ }
+
+ type request struct {
+ Auth authReq `json:"auth"`
+ }
+
+ // Populate the request structure based on the provided arguments. Create and return an error
+ // if insufficient or incompatible information is present.
+ var req request
+
+ // Test first for unrecognized arguments.
+ if options.APIKey != "" {
+ return createErr(ErrAPIKeyProvided)
+ }
+ if options.TenantID != "" {
+ return createErr(ErrTenantIDProvided)
+ }
+ if options.TenantName != "" {
+ return createErr(ErrTenantNameProvided)
+ }
+
+ if options.Password == "" {
+ if c.Provider.TokenID != "" {
+ // Because we aren't using password authentication, it's an error to also provide any of the user-based authentication
+ // parameters.
+ if options.Username != "" {
+ return createErr(ErrUsernameWithToken)
+ }
+ if options.UserID != "" {
+ return createErr(ErrUserIDWithToken)
+ }
+ if options.DomainID != "" {
+ return createErr(ErrDomainIDWithToken)
+ }
+ if options.DomainName != "" {
+ return createErr(ErrDomainNameWithToken)
+ }
+
+ // Configure the request for Token authentication.
+ req.Auth.Identity.Methods = []string{"token"}
+ req.Auth.Identity.Token = &tokenReq{
+ ID: c.Provider.TokenID,
+ }
+ } else {
+ // If no password or token ID are available, authentication can't continue.
+ return createErr(ErrMissingPassword)
+ }
+ } else {
+ // Password authentication.
+ req.Auth.Identity.Methods = []string{"password"}
+
+ // At least one of Username and UserID must be specified.
+ if options.Username == "" && options.UserID == "" {
+ return createErr(ErrUsernameOrUserID)
+ }
+
+ if options.Username != "" {
+ // If Username is provided, UserID may not be provided.
+ if options.UserID != "" {
+ return createErr(ErrUsernameOrUserID)
+ }
+
+ // Either DomainID or DomainName must also be specified.
+ if options.DomainID == "" && options.DomainName == "" {
+ return createErr(ErrDomainIDOrDomainName)
+ }
+
+ if options.DomainID != "" {
+ if options.DomainName != "" {
+ return createErr(ErrDomainIDOrDomainName)
+ }
+
+ // Configure the request for Username and Password authentication with a DomainID.
+ req.Auth.Identity.Password = &passwordReq{
+ User: userReq{
+ Name: &options.Username,
+ Password: options.Password,
+ Domain: &domainReq{ID: &options.DomainID},
+ },
+ }
+ }
+
+ if options.DomainName != "" {
+ // Configure the request for Username and Password authentication with a DomainName.
+ req.Auth.Identity.Password = &passwordReq{
+ User: userReq{
+ Name: &options.Username,
+ Password: options.Password,
+ Domain: &domainReq{Name: &options.DomainName},
+ },
+ }
+ }
+ }
+
+ if options.UserID != "" {
+ // If UserID is specified, neither DomainID nor DomainName may be.
+ if options.DomainID != "" {
+ return createErr(ErrDomainIDWithUserID)
+ }
+ if options.DomainName != "" {
+ return createErr(ErrDomainNameWithUserID)
+ }
+
+ // Configure the request for UserID and Password authentication.
+ req.Auth.Identity.Password = &passwordReq{
+ User: userReq{ID: &options.UserID, Password: options.Password},
+ }
+ }
+ }
+
+ // Add a "scope" element if a Scope has been provided.
+ if scope != nil {
+ if scope.ProjectName != "" {
+ // ProjectName provided: either DomainID or DomainName must also be supplied.
+ // ProjectID may not be supplied.
+ if scope.DomainID == "" && scope.DomainName == "" {
+ return createErr(ErrScopeDomainIDOrDomainName)
+ }
+ if scope.ProjectID != "" {
+ return createErr(ErrScopeProjectIDOrProjectName)
+ }
+
+ if scope.DomainID != "" {
+ // ProjectName + DomainID
+ req.Auth.Scope = &scopeReq{
+ Project: &projectReq{
+ Name: &scope.ProjectName,
+ Domain: &domainReq{ID: &scope.DomainID},
+ },
+ }
+ }
+
+ if scope.DomainName != "" {
+ // ProjectName + DomainName
+ req.Auth.Scope = &scopeReq{
+ Project: &projectReq{
+ Name: &scope.ProjectName,
+ Domain: &domainReq{Name: &scope.DomainName},
+ },
+ }
+ }
+ } else if scope.ProjectID != "" {
+ // ProjectID provided. ProjectName, DomainID, and DomainName may not be provided.
+ if scope.DomainID != "" {
+ return createErr(ErrScopeProjectIDAlone)
+ }
+ if scope.DomainName != "" {
+ return createErr(ErrScopeProjectIDAlone)
+ }
+
+ // ProjectID
+ req.Auth.Scope = &scopeReq{
+ Project: &projectReq{ID: &scope.ProjectID},
+ }
+ } else if scope.DomainID != "" {
+ // DomainID provided. ProjectID, ProjectName, and DomainName may not be provided.
+ if scope.DomainName != "" {
+ return createErr(ErrScopeDomainIDOrDomainName)
+ }
+
+ // DomainID
+ req.Auth.Scope = &scopeReq{
+ Domain: &domainReq{ID: &scope.DomainID},
+ }
+ } else if scope.DomainName != "" {
+ return createErr(ErrScopeDomainName)
+ } else {
+ return createErr(ErrScopeEmpty)
+ }
+ }
+
+ var result CreateResult
+ var response *perigee.Response
+ response, result.Err = perigee.Request("POST", tokenURL(c), perigee.Options{
+ ReqBody: &req,
+ Results: &result.Resp,
+ OkCodes: []int{201},
+ })
+ if result.Err != nil {
+ return result
+ }
+ result.header = response.HttpResponse.Header
+ return result
+}
+
+// Get validates and retrieves information about another token.
+func Get(c *gophercloud.ServiceClient, token string) GetResult {
+ var result GetResult
+ var response *perigee.Response
+ response, result.Err = perigee.Request("GET", tokenURL(c), perigee.Options{
+ MoreHeaders: subjectTokenHeaders(c, token),
+ Results: &result.Resp,
+ OkCodes: []int{200, 203},
+ })
+ if result.Err != nil {
+ return result
+ }
+ result.header = response.HttpResponse.Header
+ return result
+}
+
+// Validate determines if a specified token is valid or not.
+func Validate(c *gophercloud.ServiceClient, token string) (bool, error) {
+ response, err := perigee.Request("HEAD", tokenURL(c), perigee.Options{
+ MoreHeaders: subjectTokenHeaders(c, token),
+ OkCodes: []int{204, 404},
+ })
+ if err != nil {
+ return false, err
+ }
+
+ return response.StatusCode == 204, nil
+}
+
+// Revoke immediately makes specified token invalid.
+func Revoke(c *gophercloud.ServiceClient, token string) error {
+ _, err := perigee.Request("DELETE", tokenURL(c), perigee.Options{
+ MoreHeaders: subjectTokenHeaders(c, token),
+ OkCodes: []int{204},
+ })
+ return err
+}
diff --git a/_site/openstack/identity/v3/tokens/requests_test.go b/_site/openstack/identity/v3/tokens/requests_test.go
new file mode 100644
index 0000000..367c73c
--- /dev/null
+++ b/_site/openstack/identity/v3/tokens/requests_test.go
@@ -0,0 +1,516 @@
+package tokens
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+// authTokenPost verifies that providing certain AuthOptions and Scope results in an expected JSON structure.
+func authTokenPost(t *testing.T, options gophercloud.AuthOptions, scope *Scope, requestJSON string) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ client := gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: "12345abcdef",
+ },
+ Endpoint: testhelper.Endpoint(),
+ }
+
+ testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "Content-Type", "application/json")
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ testhelper.TestJSONRequest(t, r, requestJSON)
+
+ w.WriteHeader(http.StatusCreated)
+ fmt.Fprintf(w, `{
+ "token": {
+ "expires_at": "2014-10-02T13:45:00.000000Z"
+ }
+ }`)
+ })
+
+ _, err := Create(&client, options, scope).Extract()
+ if err != nil {
+ t.Errorf("Create returned an error: %v", err)
+ }
+}
+
+func authTokenPostErr(t *testing.T, options gophercloud.AuthOptions, scope *Scope, includeToken bool, expectedErr error) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ client := gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{},
+ Endpoint: testhelper.Endpoint(),
+ }
+ if includeToken {
+ client.Provider.TokenID = "abcdef123456"
+ }
+
+ _, err := Create(&client, options, scope).Extract()
+ if err == nil {
+ t.Errorf("Create did NOT return an error")
+ }
+ if err != expectedErr {
+ t.Errorf("Create returned an unexpected error: wanted %v, got %v", expectedErr, err)
+ }
+}
+
+func TestCreateUserIDAndPassword(t *testing.T) {
+ authTokenPost(t, gophercloud.AuthOptions{UserID: "me", Password: "squirrel!"}, nil, `
+ {
+ "auth": {
+ "identity": {
+ "methods": ["password"],
+ "password": {
+ "user": { "id": "me", "password": "squirrel!" }
+ }
+ }
+ }
+ }
+ `)
+}
+
+func TestCreateUsernameDomainIDPassword(t *testing.T) {
+ authTokenPost(t, gophercloud.AuthOptions{Username: "fakey", Password: "notpassword", DomainID: "abc123"}, nil, `
+ {
+ "auth": {
+ "identity": {
+ "methods": ["password"],
+ "password": {
+ "user": {
+ "domain": {
+ "id": "abc123"
+ },
+ "name": "fakey",
+ "password": "notpassword"
+ }
+ }
+ }
+ }
+ }
+ `)
+}
+
+func TestCreateUsernameDomainNamePassword(t *testing.T) {
+ authTokenPost(t, gophercloud.AuthOptions{Username: "frank", Password: "swordfish", DomainName: "spork.net"}, nil, `
+ {
+ "auth": {
+ "identity": {
+ "methods": ["password"],
+ "password": {
+ "user": {
+ "domain": {
+ "name": "spork.net"
+ },
+ "name": "frank",
+ "password": "swordfish"
+ }
+ }
+ }
+ }
+ }
+ `)
+}
+
+func TestCreateTokenID(t *testing.T) {
+ authTokenPost(t, gophercloud.AuthOptions{}, nil, `
+ {
+ "auth": {
+ "identity": {
+ "methods": ["token"],
+ "token": {
+ "id": "12345abcdef"
+ }
+ }
+ }
+ }
+ `)
+}
+
+func TestCreateProjectIDScope(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "fenris", Password: "g0t0h311"}
+ scope := &Scope{ProjectID: "123456"}
+ authTokenPost(t, options, scope, `
+ {
+ "auth": {
+ "identity": {
+ "methods": ["password"],
+ "password": {
+ "user": {
+ "id": "fenris",
+ "password": "g0t0h311"
+ }
+ }
+ },
+ "scope": {
+ "project": {
+ "id": "123456"
+ }
+ }
+ }
+ }
+ `)
+}
+
+func TestCreateDomainIDScope(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "fenris", Password: "g0t0h311"}
+ scope := &Scope{DomainID: "1000"}
+ authTokenPost(t, options, scope, `
+ {
+ "auth": {
+ "identity": {
+ "methods": ["password"],
+ "password": {
+ "user": {
+ "id": "fenris",
+ "password": "g0t0h311"
+ }
+ }
+ },
+ "scope": {
+ "domain": {
+ "id": "1000"
+ }
+ }
+ }
+ }
+ `)
+}
+
+func TestCreateProjectNameAndDomainIDScope(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "fenris", Password: "g0t0h311"}
+ scope := &Scope{ProjectName: "world-domination", DomainID: "1000"}
+ authTokenPost(t, options, scope, `
+ {
+ "auth": {
+ "identity": {
+ "methods": ["password"],
+ "password": {
+ "user": {
+ "id": "fenris",
+ "password": "g0t0h311"
+ }
+ }
+ },
+ "scope": {
+ "project": {
+ "domain": {
+ "id": "1000"
+ },
+ "name": "world-domination"
+ }
+ }
+ }
+ }
+ `)
+}
+
+func TestCreateProjectNameAndDomainNameScope(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "fenris", Password: "g0t0h311"}
+ scope := &Scope{ProjectName: "world-domination", DomainName: "evil-plans"}
+ authTokenPost(t, options, scope, `
+ {
+ "auth": {
+ "identity": {
+ "methods": ["password"],
+ "password": {
+ "user": {
+ "id": "fenris",
+ "password": "g0t0h311"
+ }
+ }
+ },
+ "scope": {
+ "project": {
+ "domain": {
+ "name": "evil-plans"
+ },
+ "name": "world-domination"
+ }
+ }
+ }
+ }
+ `)
+}
+
+func TestCreateExtractsTokenFromResponse(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ client := gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{},
+ Endpoint: testhelper.Endpoint(),
+ }
+
+ testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("X-Subject-Token", "aaa111")
+
+ w.WriteHeader(http.StatusCreated)
+ fmt.Fprintf(w, `{
+ "token": {
+ "expires_at": "2014-10-02T13:45:00.000000Z"
+ }
+ }`)
+ })
+
+ options := gophercloud.AuthOptions{UserID: "me", Password: "shhh"}
+ token, err := Create(&client, options, nil).Extract()
+ if err != nil {
+ t.Fatalf("Create returned an error: %v", err)
+ }
+
+ if token.ID != "aaa111" {
+ t.Errorf("Expected token to be aaa111, but was %s", token.ID)
+ }
+}
+
+func TestCreateFailureEmptyAuth(t *testing.T) {
+ authTokenPostErr(t, gophercloud.AuthOptions{}, nil, false, ErrMissingPassword)
+}
+
+func TestCreateFailureAPIKey(t *testing.T) {
+ authTokenPostErr(t, gophercloud.AuthOptions{APIKey: "something"}, nil, false, ErrAPIKeyProvided)
+}
+
+func TestCreateFailureTenantID(t *testing.T) {
+ authTokenPostErr(t, gophercloud.AuthOptions{TenantID: "something"}, nil, false, ErrTenantIDProvided)
+}
+
+func TestCreateFailureTenantName(t *testing.T) {
+ authTokenPostErr(t, gophercloud.AuthOptions{TenantName: "something"}, nil, false, ErrTenantNameProvided)
+}
+
+func TestCreateFailureTokenIDUsername(t *testing.T) {
+ authTokenPostErr(t, gophercloud.AuthOptions{Username: "something"}, nil, true, ErrUsernameWithToken)
+}
+
+func TestCreateFailureTokenIDUserID(t *testing.T) {
+ authTokenPostErr(t, gophercloud.AuthOptions{UserID: "something"}, nil, true, ErrUserIDWithToken)
+}
+
+func TestCreateFailureTokenIDDomainID(t *testing.T) {
+ authTokenPostErr(t, gophercloud.AuthOptions{DomainID: "something"}, nil, true, ErrDomainIDWithToken)
+}
+
+func TestCreateFailureTokenIDDomainName(t *testing.T) {
+ authTokenPostErr(t, gophercloud.AuthOptions{DomainName: "something"}, nil, true, ErrDomainNameWithToken)
+}
+
+func TestCreateFailureMissingUser(t *testing.T) {
+ options := gophercloud.AuthOptions{Password: "supersecure"}
+ authTokenPostErr(t, options, nil, false, ErrUsernameOrUserID)
+}
+
+func TestCreateFailureBothUser(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Password: "supersecure",
+ Username: "oops",
+ UserID: "redundancy",
+ }
+ authTokenPostErr(t, options, nil, false, ErrUsernameOrUserID)
+}
+
+func TestCreateFailureMissingDomain(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Password: "supersecure",
+ Username: "notuniqueenough",
+ }
+ authTokenPostErr(t, options, nil, false, ErrDomainIDOrDomainName)
+}
+
+func TestCreateFailureBothDomain(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Password: "supersecure",
+ Username: "someone",
+ DomainID: "hurf",
+ DomainName: "durf",
+ }
+ authTokenPostErr(t, options, nil, false, ErrDomainIDOrDomainName)
+}
+
+func TestCreateFailureUserIDDomainID(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ UserID: "100",
+ Password: "stuff",
+ DomainID: "oops",
+ }
+ authTokenPostErr(t, options, nil, false, ErrDomainIDWithUserID)
+}
+
+func TestCreateFailureUserIDDomainName(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ UserID: "100",
+ Password: "sssh",
+ DomainName: "oops",
+ }
+ authTokenPostErr(t, options, nil, false, ErrDomainNameWithUserID)
+}
+
+func TestCreateFailureScopeProjectNameAlone(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+ scope := &Scope{ProjectName: "notenough"}
+ authTokenPostErr(t, options, scope, false, ErrScopeDomainIDOrDomainName)
+}
+
+func TestCreateFailureScopeProjectNameAndID(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+ scope := &Scope{ProjectName: "whoops", ProjectID: "toomuch", DomainID: "1234"}
+ authTokenPostErr(t, options, scope, false, ErrScopeProjectIDOrProjectName)
+}
+
+func TestCreateFailureScopeProjectIDAndDomainID(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+ scope := &Scope{ProjectID: "toomuch", DomainID: "notneeded"}
+ authTokenPostErr(t, options, scope, false, ErrScopeProjectIDAlone)
+}
+
+func TestCreateFailureScopeProjectIDAndDomainNAme(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+ scope := &Scope{ProjectID: "toomuch", DomainName: "notneeded"}
+ authTokenPostErr(t, options, scope, false, ErrScopeProjectIDAlone)
+}
+
+func TestCreateFailureScopeDomainIDAndDomainName(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+ scope := &Scope{DomainID: "toomuch", DomainName: "notneeded"}
+ authTokenPostErr(t, options, scope, false, ErrScopeDomainIDOrDomainName)
+}
+
+func TestCreateFailureScopeDomainNameAlone(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+ scope := &Scope{DomainName: "notenough"}
+ authTokenPostErr(t, options, scope, false, ErrScopeDomainName)
+}
+
+func TestCreateFailureEmptyScope(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+ scope := &Scope{}
+ authTokenPostErr(t, options, scope, false, ErrScopeEmpty)
+}
+
+func TestGetRequest(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ client := gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: "12345abcdef",
+ },
+ Endpoint: testhelper.Endpoint(),
+ }
+
+ testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "Content-Type", "")
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ testhelper.TestHeader(t, r, "X-Auth-Token", "12345abcdef")
+ testhelper.TestHeader(t, r, "X-Subject-Token", "abcdef12345")
+
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+ { "token": { "expires_at": "2014-08-29T13:10:01.000000Z" } }
+ `)
+ })
+
+ token, err := Get(&client, "abcdef12345").Extract()
+ if err != nil {
+ t.Errorf("Info returned an error: %v", err)
+ }
+
+ expected, _ := time.Parse(time.UnixDate, "Fri Aug 29 13:10:01 UTC 2014")
+ if token.ExpiresAt != expected {
+ t.Errorf("Expected expiration time %s, but was %s", expected.Format(time.UnixDate), token.ExpiresAt.Format(time.UnixDate))
+ }
+}
+
+func prepareAuthTokenHandler(t *testing.T, expectedMethod string, status int) gophercloud.ServiceClient {
+ client := gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: "12345abcdef",
+ },
+ Endpoint: testhelper.Endpoint(),
+ }
+
+ testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, expectedMethod)
+ testhelper.TestHeader(t, r, "Content-Type", "")
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ testhelper.TestHeader(t, r, "X-Auth-Token", "12345abcdef")
+ testhelper.TestHeader(t, r, "X-Subject-Token", "abcdef12345")
+
+ w.WriteHeader(status)
+ })
+
+ return client
+}
+
+func TestValidateRequestSuccessful(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+ client := prepareAuthTokenHandler(t, "HEAD", http.StatusNoContent)
+
+ ok, err := Validate(&client, "abcdef12345")
+ if err != nil {
+ t.Errorf("Unexpected error from Validate: %v", err)
+ }
+
+ if !ok {
+ t.Errorf("Validate returned false for a valid token")
+ }
+}
+
+func TestValidateRequestFailure(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+ client := prepareAuthTokenHandler(t, "HEAD", http.StatusNotFound)
+
+ ok, err := Validate(&client, "abcdef12345")
+ if err != nil {
+ t.Errorf("Unexpected error from Validate: %v", err)
+ }
+
+ if ok {
+ t.Errorf("Validate returned true for an invalid token")
+ }
+}
+
+func TestValidateRequestError(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+ client := prepareAuthTokenHandler(t, "HEAD", http.StatusUnauthorized)
+
+ _, err := Validate(&client, "abcdef12345")
+ if err == nil {
+ t.Errorf("Missing expected error from Validate")
+ }
+}
+
+func TestRevokeRequestSuccessful(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+ client := prepareAuthTokenHandler(t, "DELETE", http.StatusNoContent)
+
+ err := Revoke(&client, "abcdef12345")
+ if err != nil {
+ t.Errorf("Unexpected error from Revoke: %v", err)
+ }
+}
+
+func TestRevokeRequestError(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+ client := prepareAuthTokenHandler(t, "DELETE", http.StatusNotFound)
+
+ err := Revoke(&client, "abcdef12345")
+ if err == nil {
+ t.Errorf("Missing expected error from Revoke")
+ }
+}
diff --git a/_site/openstack/identity/v3/tokens/results.go b/_site/openstack/identity/v3/tokens/results.go
new file mode 100644
index 0000000..1be98cb
--- /dev/null
+++ b/_site/openstack/identity/v3/tokens/results.go
@@ -0,0 +1,75 @@
+package tokens
+
+import (
+ "net/http"
+ "time"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+)
+
+// commonResult is the deferred result of a Create or a Get call.
+type commonResult struct {
+ gophercloud.CommonResult
+
+ // header stores the headers from the original HTTP response because token responses are returned in an X-Subject-Token header.
+ header http.Header
+}
+
+// Extract interprets a commonResult as a Token.
+func (r commonResult) Extract() (*Token, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ Token struct {
+ ExpiresAt string `mapstructure:"expires_at"`
+ } `mapstructure:"token"`
+ }
+
+ var token Token
+
+ // Parse the token itself from the stored headers.
+ token.ID = r.header.Get("X-Subject-Token")
+
+ err := mapstructure.Decode(r.Resp, &response)
+ if err != nil {
+ return nil, err
+ }
+
+ // Attempt to parse the timestamp.
+ token.ExpiresAt, err = time.Parse(gophercloud.RFC3339Milli, response.Token.ExpiresAt)
+
+ return &token, err
+}
+
+// CreateResult is the deferred response from a Create call.
+type CreateResult struct {
+ commonResult
+}
+
+// createErr quickly creates a CreateResult that reports an error.
+func createErr(err error) CreateResult {
+ return CreateResult{
+ commonResult: commonResult{
+ CommonResult: gophercloud.CommonResult{Err: err},
+ header: nil,
+ },
+ }
+}
+
+// GetResult is the deferred response from a Get call.
+type GetResult struct {
+ commonResult
+}
+
+// Token is a string that grants a user access to a controlled set of services in an OpenStack provider.
+// Each Token is valid for a set length of time.
+type Token struct {
+ // ID is the issued token.
+ ID string
+
+ // ExpiresAt is the timestamp at which this token will no longer be accepted.
+ ExpiresAt time.Time
+}
diff --git a/_site/openstack/identity/v3/tokens/urls.go b/_site/openstack/identity/v3/tokens/urls.go
new file mode 100644
index 0000000..360b60a
--- /dev/null
+++ b/_site/openstack/identity/v3/tokens/urls.go
@@ -0,0 +1,7 @@
+package tokens
+
+import "github.com/rackspace/gophercloud"
+
+func tokenURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("auth", "tokens")
+}
diff --git a/_site/openstack/identity/v3/tokens/urls_test.go b/_site/openstack/identity/v3/tokens/urls_test.go
new file mode 100644
index 0000000..549c398
--- /dev/null
+++ b/_site/openstack/identity/v3/tokens/urls_test.go
@@ -0,0 +1,21 @@
+package tokens
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestTokenURL(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ client := gophercloud.ServiceClient{Endpoint: testhelper.Endpoint()}
+
+ expected := testhelper.Endpoint() + "auth/tokens"
+ actual := tokenURL(&client)
+ if actual != expected {
+ t.Errorf("Expected URL %s, but was %s", expected, actual)
+ }
+}