Epic refactoring to improve testability.
It all started when I wanted to write ListServers(), but doing that
meant acquiring the test context, so that I could intercept server-side
communications. However, that couldn't be easily acquired with the old
software configuration. In fact, it was patently impossible to do,
without breaking a lot of encapsulation and conflating concerns.
Additionally, I knew I didn't want to make ComputeApi() a method of an
AccessProvider interface; considering how similar the OpenStack APIs
are, I was banking on that design decision causing a lot of duplicate
code, even if said code was simple. Not only that, but it conflated
concerns; again, something I wanted to avoid.
So, I needed to do a couple of things.
1) Realize that module-global functions are delegators to the global
context. My original implementation of ComputeApi() wasn't, which meant
that I had zero access to any contexts created in unit testing.
2) Realize that the Context interface is the true Gophercloud global
API. This meant I had to make a ComputeApi() method on the Context
interface, and implement it. This proved very convenient -- it granted
me access automatically to all test contexts.
As a checklist bullet point, whenever adding a new global-level function
to gophercloud, do it in at least these steps: a) add the function as a
method on Context. Seriously -- this is the only real way to make it
testable. b) Add a very dumb delegator function in global_context.go
which dispatches to its eponymously-named method on globalContext.
3) Making this simple change was sufficient to start to test an
implementation of ListServers(). However, invoking "c := TestContext();
c.foo(); c.bar();" was becoming repetitive and error-prone. So, I
refactored that into a Java-style DSL. These things aren't terribly Go
idiomatic, but for my needs here, they work nicely.
4) I refactored the two different implementations of custom transports
into a single "transport-double" type. This type will supports both
canned responses and some basic request validation. It's expandable by
simply adding more configuration fields and DSL methods of its own.
5) api.go is no more -- it previously served two separate purposes, each
of which has its own source file now. interfaces.go holds the
definition of all publicly visible APIs, while global_context.go
provides the default global Context, its initialization, and the
module-global entry points that delegate to the global context.
With these changes having been made, *now* I'm ready to actually start
testing ListServers() development! It only took 24 hours and 4
refreshes of the feature branch just to get this far. :-)
The nice thing is, though, that going forward, these changes should
contribute to making future endpoint binding implementations
significantly easier than what I had to do before.
diff --git a/api.go b/api.go
deleted file mode 100644
index 069d4c9..0000000
--- a/api.go
+++ /dev/null
@@ -1,155 +0,0 @@
-package gophercloud
-
-import "strings"
-
-// globalContext is the, well, "global context."
-// Most of this SDK is written in a manner to facilitate easier testing,
-// which doesn't require all the configuration a real-world application would require.
-// However, for real-world deployments, applications should be able to rely on a consistent configuration of providers, etc.
-var globalContext *Context
-
-// providers is the set of supported providers.
-var providers = map[string]Provider{
- "rackspace-us": Provider{
- AuthEndpoint: "https://identity.api.rackspacecloud.com/v2.0/tokens",
- },
- "rackspace-uk": Provider{
- AuthEndpoint: "https://lon.identity.api.rackspacecloud.com/v2.0/tokens",
- },
-}
-
-// Initialize the global context to sane configuration.
-// The Go runtime ensures this function is called before main(),
-// thus guaranteeing proper configuration before your application ever runs.
-func init() {
- globalContext = TestContext()
- for name, descriptor := range providers {
- globalContext.RegisterProvider(name, descriptor)
- }
-}
-
-// Authenticate() grants access to the OpenStack-compatible provider API.
-//
-// Providers are identified through a unique key string.
-// Specifying an unsupported provider will result in an ErrProvider error.
-//
-// The supplied AuthOptions instance allows the client to specify only those credentials
-// relevant for the authentication request. At present, support exists for OpenStack
-// Identity V2 API only; support for V3 will become available as soon as documentation for it
-// becomes readily available.
-//
-// For Identity V2 API requirements, you must provide at least the Username and Password
-// options. The TenantId field is optional, and defaults to "".
-func Authenticate(provider string, options AuthOptions) (*Access, error) {
- return globalContext.Authenticate(provider, options)
-}
-
-// ApiCriteria provides one or more criteria for the SDK to look for appropriate endpoints.
-// Fields left unspecified or otherwise set to their zero-values are assumed to not be
-// relevant, and do not participate in the endpoint search.
-type ApiCriteria struct {
- // Name specifies the desired service catalog entry name.
- Name string
-
- // Region specifies the desired endpoint region.
- Region string
-
- // VersionId specifies the desired version of the endpoint.
- // Note that this field is matched exactly, and is (at present)
- // opaque to Gophercloud. Thus, requesting a version 2
- // endpoint will _not_ match a version 3 endpoint.
- VersionId string
-
- // The UrlChoice field inidicates whether or not gophercloud
- // should use the public or internal endpoint URL if a
- // candidate endpoint is found.
- UrlChoice int
-}
-
-// The choices available for UrlChoice. See the ApiCriteria structure for details.
-const (
- PublicURL = iota
- InternalURL
-)
-
-// ComputeProvider instances encapsulate a Cloud Servers API, should one exist in the service catalog
-// for your provider.
-type ComputeProvider interface {
- ListServers() ([]Server, error)
-}
-
-// AccessProvider instances encapsulate a Keystone authentication interface.
-type AccessProvider interface {
- // FirstEndpointUrlByCriteria searches through the service catalog for the first
- // matching entry endpoint fulfilling the provided criteria. If nothing found,
- // return "". Otherwise, return either the public or internal URL for the
- // endpoint, depending on both its existence and the setting of the ApiCriteria.UrlChoice
- // field.
- FirstEndpointUrlByCriteria(ApiCriteria) string
-}
-
-// genericCloudProvider structures provide the implementation for generic OpenStack-compatible
-// ComputeProvider interfaces.
-type genericCloudProvider struct {
- // endpoint refers to the provider's API endpoint base URL. This will be used to construct
- // and issue queries.
- endpoint string
-}
-
-func ComputeApi(acc AccessProvider, criteria ApiCriteria) (ComputeProvider, error) {
- url := acc.FirstEndpointUrlByCriteria(criteria)
- if url == "" {
- return nil, ErrEndpoint
- }
-
- gcp := &genericCloudProvider{
- endpoint: url,
- }
-
- return gcp, nil
-}
-
-// See AccessProvider interface definition for details.
-func (a *Access) FirstEndpointUrlByCriteria(ac ApiCriteria) string {
- ep := FindFirstEndpointByCriteria(a.ServiceCatalog, ac)
- urls := []string{ep.PublicURL, ep.InternalURL}
- return urls[ac.UrlChoice]
-}
-
-// Given a set of criteria to match on, locate the first candidate endpoint
-// in the provided service catalog.
-//
-// If nothing found, the result will be a zero-valued EntryEndpoint (all URLs
-// set to "").
-func FindFirstEndpointByCriteria(entries []CatalogEntry, ac ApiCriteria) EntryEndpoint {
- rgn := strings.ToUpper(ac.Region)
-
- for _, entry := range entries {
- if (ac.Name != "") && (ac.Name != entry.Name) {
- continue
- }
-
- for _, endpoint := range entry.Endpoints {
- if (ac.Region != "") && (rgn != strings.ToUpper(endpoint.Region)) {
- continue
- }
-
- if (ac.VersionId != "") && (ac.VersionId != endpoint.VersionId) {
- continue
- }
-
- return endpoint
- }
- }
- return EntryEndpoint{}
-}
-
-// See the ComputeProvider interface for details.
-func (gcp *genericCloudProvider) ListServers() ([]Server, error) {
- return nil, nil
-}
-
-// Server structures provide data about a server running in your provider's cloud.
-type Server struct {
- Id string
-}
diff --git a/authenticate.go b/authenticate.go
index 836c21b..d460673 100644
--- a/authenticate.go
+++ b/authenticate.go
@@ -39,9 +39,7 @@
}
// Access encapsulates the API token and its relevant fields, as well as the
-// services catalog that Identity API returns once authenticated. You'll probably
-// rarely use this record directly, unless you intend on marshalling or unmarshalling
-// Identity API JSON records yourself.
+// services catalog that Identity API returns once authenticated.
type Access struct {
Token Token
ServiceCatalog []CatalogEntry
@@ -129,3 +127,10 @@
})
return access, err
}
+
+// See AccessProvider interface definition for details.
+func (a *Access) FirstEndpointUrlByCriteria(ac ApiCriteria) string {
+ ep := FindFirstEndpointByCriteria(a.ServiceCatalog, ac)
+ urls := []string{ep.PublicURL, ep.InternalURL}
+ return urls[ac.UrlChoice]
+}
diff --git a/authenticate_test.go b/authenticate_test.go
index f077dab..a19fec4 100644
--- a/authenticate_test.go
+++ b/authenticate_test.go
@@ -1,10 +1,7 @@
package gophercloud
import (
- "encoding/json"
- "io/ioutil"
"net/http"
- "strings"
"testing"
)
@@ -55,85 +52,9 @@
}
`
-type testTransport struct {
- called int
- response string
-}
-
-func (t *testTransport) RoundTrip(req *http.Request) (rsp *http.Response, err error) {
- t.called++
-
- headers := make(http.Header)
- headers.Add("Content-Type", "application/xml; charset=UTF-8")
-
- body := ioutil.NopCloser(strings.NewReader(t.response))
-
- rsp = &http.Response{
- Status: "200 OK",
- StatusCode: 200,
- Proto: "HTTP/1.1",
- ProtoMajor: 1,
- ProtoMinor: 1,
- Header: headers,
- Body: body,
- ContentLength: -1,
- TransferEncoding: nil,
- Close: true,
- Trailer: nil,
- Request: req,
- }
- return
-}
-
-type tenantIdCheckTransport struct {
- expectTenantId bool
- tenantIdFound bool
-}
-
-func (t *tenantIdCheckTransport) RoundTrip(req *http.Request) (rsp *http.Response, err error) {
- var authContainer *AuthContainer
-
- headers := make(http.Header)
- headers.Add("Content-Type", "application/xml; charset=UTF-8")
-
- body := ioutil.NopCloser(strings.NewReader("t.response"))
-
- rsp = &http.Response{
- Status: "200 OK",
- StatusCode: 200,
- Proto: "HTTP/1.1",
- ProtoMajor: 1,
- ProtoMinor: 1,
- Header: headers,
- Body: body,
- ContentLength: -1,
- TransferEncoding: nil,
- Close: true,
- Trailer: nil,
- Request: req,
- }
-
- bytes, err := ioutil.ReadAll(req.Body)
- if err != nil {
- return nil, err
- }
- err = json.Unmarshal(bytes, &authContainer)
- if err != nil {
- return nil, err
- }
- t.tenantIdFound = (authContainer.Auth.TenantId != "")
-
- if t.tenantIdFound != t.expectTenantId {
- rsp.Status = "500 Internal Server Error"
- rsp.StatusCode = 500
- }
- return
-}
-
func TestAuthProvider(t *testing.T) {
- c := TestContext()
- tt := &testTransport{}
- c.UseCustomClient(&http.Client{
+ tt := newTransport()
+ c := TestContext().UseCustomClient(&http.Client{
Transport: tt,
})
@@ -165,14 +86,14 @@
}
func TestTenantIdEncoding(t *testing.T) {
- c := TestContext()
- tt := &tenantIdCheckTransport{}
- c.UseCustomClient(&http.Client{
+ tt := newTransport()
+ c := TestContext().
+ UseCustomClient(&http.Client{
Transport: tt,
- })
- c.RegisterProvider("provider", Provider{AuthEndpoint: "/"})
+ }).
+ WithProvider("provider", Provider{AuthEndpoint: "/"})
- tt.expectTenantId = false
+ tt.IgnoreTenantId()
_, err := c.Authenticate("provider", AuthOptions{
Username: "u",
Password: "p",
@@ -186,7 +107,7 @@
return
}
- tt.expectTenantId = true
+ tt.ExpectTenantId()
_, err = c.Authenticate("provider", AuthOptions{
Username: "u",
Password: "p",
@@ -203,9 +124,9 @@
}
func TestUserNameAndPassword(t *testing.T) {
- c := TestContext()
- c.UseCustomClient(&http.Client{Transport: &testTransport{}})
- c.RegisterProvider("provider", Provider{AuthEndpoint: "http://localhost/"})
+ c := TestContext().
+ WithProvider("provider", Provider{AuthEndpoint: "http://localhost/"}).
+ UseCustomClient(&http.Client{Transport: newTransport()})
credentials := []AuthOptions{
AuthOptions{},
@@ -228,11 +149,9 @@
}
func TestTokenAcquisition(t *testing.T) {
- c := TestContext()
- tt := &testTransport{}
- tt.response = SUCCESSFUL_RESPONSE
- c.UseCustomClient(&http.Client{Transport: tt})
- c.RegisterProvider("provider", Provider{AuthEndpoint: "http://localhost"})
+ c := TestContext().
+ UseCustomClient(&http.Client{Transport: newTransport().WithResponse(SUCCESSFUL_RESPONSE)}).
+ WithProvider("provider", Provider{AuthEndpoint: "http://localhost/"})
acc, err := c.Authenticate("provider", AuthOptions{Username: "u", Password: "p"})
if err != nil {
@@ -248,11 +167,9 @@
}
func TestServiceCatalogAcquisition(t *testing.T) {
- c := TestContext()
- tt := &testTransport{}
- tt.response = SUCCESSFUL_RESPONSE
- c.UseCustomClient(&http.Client{Transport: tt})
- c.RegisterProvider("provider", Provider{AuthEndpoint: "http://localhost"})
+ c := TestContext().
+ UseCustomClient(&http.Client{Transport: newTransport().WithResponse(SUCCESSFUL_RESPONSE)}).
+ WithProvider("provider", Provider{AuthEndpoint: "http://localhost/"})
acc, err := c.Authenticate("provider", AuthOptions{Username: "u", Password: "p"})
if err != nil {
@@ -279,11 +196,9 @@
}
func TestUserAcquisition(t *testing.T) {
- c := TestContext()
- tt := &testTransport{}
- tt.response = SUCCESSFUL_RESPONSE
- c.UseCustomClient(&http.Client{Transport: tt})
- c.RegisterProvider("provider", Provider{AuthEndpoint: "http://localhost"})
+ c := TestContext().
+ UseCustomClient(&http.Client{Transport: newTransport().WithResponse(SUCCESSFUL_RESPONSE)}).
+ WithProvider("provider", Provider{AuthEndpoint: "http://localhost/"})
acc, err := c.Authenticate("provider", AuthOptions{Username: "u", Password: "p"})
if err != nil {
diff --git a/context.go b/context.go
index daa5001..71ff757 100644
--- a/context.go
+++ b/context.go
@@ -4,6 +4,14 @@
"net/http"
)
+// Provider structures exist for each tangible provider of OpenStack service.
+// For example, Rackspace, Hewlett-Packard, and NASA might have their own instance of this structure.
+//
+// At a minimum, a provider must expose an authentication endpoint.
+type Provider struct {
+ AuthEndpoint string
+}
+
// Context structures encapsulate Gophercloud-global state in a manner which
// facilitates easier unit testing. As a user of this SDK, you'll never
// have to use this structure, except when contributing new code to the SDK.
@@ -32,6 +40,54 @@
// UseCustomClient configures the context to use a customized HTTP client
// instance. By default, TestContext() will return a Context which uses
// the net/http package's default client instance.
-func (c *Context) UseCustomClient(hc *http.Client) {
+func (c *Context) UseCustomClient(hc *http.Client) *Context {
c.httpClient = hc
+ return c
+}
+
+// RegisterProvider allows a unit test to register a mythical provider convenient for testing.
+// If the provider structure lacks adequate configuration, or the configuration given has some
+// detectable error, an ErrConfiguration error will result.
+func (c *Context) RegisterProvider(name string, p Provider) error {
+ if p.AuthEndpoint == "" {
+ return ErrConfiguration
+ }
+
+ c.providerMap[name] = p
+ return nil
+}
+
+// WithProvider offers convenience for unit tests.
+func (c *Context) WithProvider(name string, p Provider) *Context {
+ err := c.RegisterProvider(name, p)
+ if err != nil {
+ panic(err)
+ }
+ return c
+}
+
+// ProviderByName will locate a provider amongst those previously registered, if it exists.
+// If the named provider has not been registered, an ErrProvider error will result.
+func (c *Context) ProviderByName(name string) (p Provider, err error) {
+ for provider, descriptor := range c.providerMap {
+ if name == provider {
+ return descriptor, nil
+ }
+ }
+ return Provider{}, ErrProvider
+}
+
+// Instantiates a Cloud Servers object for the provider given.
+func (c *Context) ComputeApi(acc AccessProvider, criteria ApiCriteria) (ComputeProvider, error) {
+ url := acc.FirstEndpointUrlByCriteria(criteria)
+ if url == "" {
+ return nil, ErrEndpoint
+ }
+
+ gcp := &genericCloudProvider{
+ endpoint: url,
+ context: c,
+ }
+
+ return gcp, nil
}
diff --git a/provider_test.go b/context_test.go
similarity index 100%
rename from provider_test.go
rename to context_test.go
diff --git a/global_context.go b/global_context.go
new file mode 100644
index 0000000..b071c09
--- /dev/null
+++ b/global_context.go
@@ -0,0 +1,48 @@
+package gophercloud
+
+// globalContext is the, well, "global context."
+// Most of this SDK is written in a manner to facilitate easier testing,
+// which doesn't require all the configuration a real-world application would require.
+// However, for real-world deployments, applications should be able to rely on a consistent configuration of providers, etc.
+var globalContext *Context
+
+// providers is the set of supported providers.
+var providers = map[string]Provider{
+ "rackspace-us": Provider{
+ AuthEndpoint: "https://identity.api.rackspacecloud.com/v2.0/tokens",
+ },
+ "rackspace-uk": Provider{
+ AuthEndpoint: "https://lon.identity.api.rackspacecloud.com/v2.0/tokens",
+ },
+}
+
+// Initialize the global context to sane configuration.
+// The Go runtime ensures this function is called before main(),
+// thus guaranteeing proper configuration before your application ever runs.
+func init() {
+ globalContext = TestContext()
+ for name, descriptor := range providers {
+ globalContext.RegisterProvider(name, descriptor)
+ }
+}
+
+// Authenticate() grants access to the OpenStack-compatible provider API.
+//
+// Providers are identified through a unique key string.
+// Specifying an unsupported provider will result in an ErrProvider error.
+//
+// The supplied AuthOptions instance allows the client to specify only those credentials
+// relevant for the authentication request. At present, support exists for OpenStack
+// Identity V2 API only; support for V3 will become available as soon as documentation for it
+// becomes readily available.
+//
+// For Identity V2 API requirements, you must provide at least the Username and Password
+// options. The TenantId field is optional, and defaults to "".
+func Authenticate(provider string, options AuthOptions) (*Access, error) {
+ return globalContext.Authenticate(provider, options)
+}
+
+// Instantiates a Cloud Servers object for the provider given.
+func ComputeApi(acc AccessProvider, criteria ApiCriteria) (ComputeProvider, error) {
+ return globalContext.ComputeApi(acc, criteria)
+}
diff --git a/interfaces.go b/interfaces.go
new file mode 100644
index 0000000..671fa1a
--- /dev/null
+++ b/interfaces.go
@@ -0,0 +1,17 @@
+package gophercloud
+
+// AccessProvider instances encapsulate a Keystone authentication interface.
+type AccessProvider interface {
+ // FirstEndpointUrlByCriteria searches through the service catalog for the first
+ // matching entry endpoint fulfilling the provided criteria. If nothing found,
+ // return "". Otherwise, return either the public or internal URL for the
+ // endpoint, depending on both its existence and the setting of the ApiCriteria.UrlChoice
+ // field.
+ FirstEndpointUrlByCriteria(ApiCriteria) string
+}
+
+// ComputeProvider instances encapsulate a Cloud Servers API, should one exist in the service catalog
+// for your provider.
+type ComputeProvider interface {
+ ListServers() ([]Server, error)
+}
diff --git a/provider.go b/provider.go
deleted file mode 100644
index c741c4c..0000000
--- a/provider.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package gophercloud
-
-// Provider structures exist for each tangible provider of OpenStack service.
-// For example, Rackspace, Hewlett-Packard, and NASA might have their own instance of this structure.
-//
-// At a minimum, a provider must expose an authentication endpoint.
-type Provider struct {
- AuthEndpoint string
-}
-
-// RegisterProvider allows a unit test to register a mythical provider convenient for testing.
-// If the provider structure lacks adequate configuration, or the configuration given has some
-// detectable error, an ErrConfiguration error will result.
-func (c *Context) RegisterProvider(name string, p Provider) error {
- if p.AuthEndpoint == "" {
- return ErrConfiguration
- }
-
- c.providerMap[name] = p
- return nil
-}
-
-// ProviderByName will locate a provider amongst those previously registered, if it exists.
-// If the named provider has not been registered, an ErrProvider error will result.
-func (c *Context) ProviderByName(name string) (p Provider, err error) {
- for provider, descriptor := range c.providerMap {
- if name == provider {
- return descriptor, nil
- }
- }
- return Provider{}, ErrProvider
-}
diff --git a/servers.go b/servers.go
new file mode 100644
index 0000000..f6214c0
--- /dev/null
+++ b/servers.go
@@ -0,0 +1,22 @@
+package gophercloud
+
+// genericCloudProvider structures provide the implementation for generic OpenStack-compatible
+// ComputeProvider interfaces.
+type genericCloudProvider struct {
+ // endpoint refers to the provider's API endpoint base URL. This will be used to construct
+ // and issue queries.
+ endpoint string
+
+ // Test context (if any) in which to issue requests.
+ context *Context
+}
+
+// See the ComputeProvider interface for details.
+func (gcp *genericCloudProvider) ListServers() ([]Server, error) {
+ return nil, nil
+}
+
+// Server structures provide data about a server running in your provider's cloud.
+type Server struct {
+ Id string
+}
diff --git a/servers_test.go b/servers_test.go
new file mode 100644
index 0000000..206290f
--- /dev/null
+++ b/servers_test.go
@@ -0,0 +1,42 @@
+package gophercloud
+
+import (
+ "net/http"
+ "testing"
+)
+
+type testAccess struct {
+ public, internal string
+ calledFirstEndpointByCriteria int
+}
+
+func (ta *testAccess) FirstEndpointUrlByCriteria(ac ApiCriteria) string {
+ ta.calledFirstEndpointByCriteria++
+ urls := []string{ta.public, ta.internal}
+ return urls[ac.UrlChoice]
+}
+
+func TestGetServersApi(t *testing.T) {
+ c := TestContext().UseCustomClient(&http.Client{Transport: newTransport().WithResponse("Hello")})
+
+ acc := &testAccess{
+ public: "http://localhost:8080",
+ internal: "http://localhost:8086",
+ }
+
+ _, err := c.ComputeApi(acc, ApiCriteria{
+ Name: "cloudComputeOpenStack",
+ Region: "dfw",
+ VersionId: "2",
+ })
+
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ if acc.calledFirstEndpointByCriteria != 1 {
+ t.Error("Expected FirstEndpointByCriteria to be called")
+ return
+ }
+}
diff --git a/service_catalog.go b/service_catalog.go
new file mode 100644
index 0000000..326f653
--- /dev/null
+++ b/service_catalog.go
@@ -0,0 +1,61 @@
+package gophercloud
+
+import (
+ "strings"
+)
+
+// ApiCriteria provides one or more criteria for the SDK to look for appropriate endpoints.
+// Fields left unspecified or otherwise set to their zero-values are assumed to not be
+// relevant, and do not participate in the endpoint search.
+type ApiCriteria struct {
+ // Name specifies the desired service catalog entry name.
+ Name string
+
+ // Region specifies the desired endpoint region.
+ Region string
+
+ // VersionId specifies the desired version of the endpoint.
+ // Note that this field is matched exactly, and is (at present)
+ // opaque to Gophercloud. Thus, requesting a version 2
+ // endpoint will _not_ match a version 3 endpoint.
+ VersionId string
+
+ // The UrlChoice field inidicates whether or not gophercloud
+ // should use the public or internal endpoint URL if a
+ // candidate endpoint is found.
+ UrlChoice int
+}
+
+// The choices available for UrlChoice. See the ApiCriteria structure for details.
+const (
+ PublicURL = iota
+ InternalURL
+)
+
+// Given a set of criteria to match on, locate the first candidate endpoint
+// in the provided service catalog.
+//
+// If nothing found, the result will be a zero-valued EntryEndpoint (all URLs
+// set to "").
+func FindFirstEndpointByCriteria(entries []CatalogEntry, ac ApiCriteria) EntryEndpoint {
+ rgn := strings.ToUpper(ac.Region)
+
+ for _, entry := range entries {
+ if (ac.Name != "") && (ac.Name != entry.Name) {
+ continue
+ }
+
+ for _, endpoint := range entry.Endpoints {
+ if (ac.Region != "") && (rgn != strings.ToUpper(endpoint.Region)) {
+ continue
+ }
+
+ if (ac.VersionId != "") && (ac.VersionId != endpoint.VersionId) {
+ continue
+ }
+
+ return endpoint
+ }
+ }
+ return EntryEndpoint{}
+}
diff --git a/api_test.go b/service_catalog_test.go
similarity index 78%
rename from api_test.go
rename to service_catalog_test.go
index 4c0bc25..4420366 100644
--- a/api_test.go
+++ b/service_catalog_test.go
@@ -4,40 +4,6 @@
"testing"
)
-type testAccess struct {
- public, internal string
- calledFirstEndpointByCriteria int
-}
-
-func (ta *testAccess) FirstEndpointUrlByCriteria(ac ApiCriteria) string {
- ta.calledFirstEndpointByCriteria++
- urls := []string{ta.public, ta.internal}
- return urls[ac.UrlChoice]
-}
-
-func TestGettingComputeApi(t *testing.T) {
- acc := &testAccess{
- public: "http://localhost:8080",
- internal: "http://localhost:8086",
- }
-
- _, err := ComputeApi(acc, ApiCriteria{
- Name: "cloudComputeOpenStack",
- Region: "dfw",
- VersionId: "2",
- })
-
- if err != nil {
- t.Error(err)
- return
- }
-
- if acc.calledFirstEndpointByCriteria != 1 {
- t.Error("Expected FirstEndpointByCriteria to be called")
- return
- }
-}
-
func TestFindFirstEndpointByCriteria(t *testing.T) {
endpoint := FindFirstEndpointByCriteria([]CatalogEntry{}, ApiCriteria{Name: "test"})
if endpoint.PublicURL != "" {
diff --git a/transport_double.go b/transport_double.go
new file mode 100644
index 0000000..b65d5db
--- /dev/null
+++ b/transport_double.go
@@ -0,0 +1,76 @@
+package gophercloud
+
+import (
+ "encoding/json"
+ "io/ioutil"
+ "net/http"
+ "strings"
+)
+
+type transport struct {
+ called int
+ response string
+ expectTenantId bool
+ tenantIdFound bool
+}
+
+func (t *transport) RoundTrip(req *http.Request) (rsp *http.Response, err error) {
+ var authContainer *AuthContainer
+
+ t.called++
+
+ headers := make(http.Header)
+ headers.Add("Content-Type", "application/xml; charset=UTF-8")
+
+ body := ioutil.NopCloser(strings.NewReader(t.response))
+
+ rsp = &http.Response{
+ Status: "200 OK",
+ StatusCode: 200,
+ Proto: "HTTP/1.1",
+ ProtoMajor: 1,
+ ProtoMinor: 1,
+ Header: headers,
+ Body: body,
+ ContentLength: -1,
+ TransferEncoding: nil,
+ Close: true,
+ Trailer: nil,
+ Request: req,
+ }
+
+ bytes, err := ioutil.ReadAll(req.Body)
+ if err != nil {
+ return nil, err
+ }
+ err = json.Unmarshal(bytes, &authContainer)
+ if err != nil {
+ return nil, err
+ }
+ t.tenantIdFound = (authContainer.Auth.TenantId != "")
+
+ if t.tenantIdFound != t.expectTenantId {
+ rsp.Status = "500 Internal Server Error"
+ rsp.StatusCode = 500
+ }
+ return
+}
+
+func newTransport() *transport {
+ return &transport{}
+}
+
+func (t *transport) IgnoreTenantId() *transport {
+ t.expectTenantId = false
+ return t
+}
+
+func (t *transport) ExpectTenantId() *transport {
+ t.expectTenantId = true
+ return t
+}
+
+func (t *transport) WithResponse(r string) *transport {
+ t.response = r
+ return t
+}