diff options
| author | Stefan Majewsky <majewsky@gmx.net> | 2018-02-19 21:30:33 +0100 |
|---|---|---|
| committer | Stefan Majewsky <majewsky@gmx.net> | 2018-02-19 21:33:49 +0100 |
| commit | 60d4779889baedc44972d4749daa073efca3b25c (patch) | |
| tree | d47746971f659d6f7e3affe428f239b289954f5b | |
| parent | 8f777460661bbbcbe42730979140f525b382110e (diff) | |
| download | go-schwift-60d4779889baedc44972d4749daa073efca3b25c.tar.gz | |
reorganize code
* Gophercloud dependencies move into subpackage gopherschwift.
* Tests move into subpackage tests (to avoid import cycles).
+ Rename "Client" to "Backend".
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | README.md | 12 | ||||
| -rw-r--r-- | account.go | 37 | ||||
| -rw-r--r-- | backend.go | 43 | ||||
| -rw-r--r-- | client.go | 94 | ||||
| -rw-r--r-- | container.go | 10 | ||||
| -rw-r--r-- | doc.go | 8 | ||||
| -rw-r--r-- | gopherschwift/package.go | 105 | ||||
| -rw-r--r-- | iterator.go | 4 | ||||
| -rw-r--r-- | object.go | 10 | ||||
| -rw-r--r-- | request.go | 12 | ||||
| -rw-r--r-- | tests/account_test.go (renamed from account_test.go) | 14 | ||||
| -rw-r--r-- | tests/backend_test.go | 43 | ||||
| -rw-r--r-- | tests/container_iterator_test.go (renamed from container_iterator_test.go) | 38 | ||||
| -rw-r--r-- | tests/container_test.go (renamed from container_test.go) | 14 | ||||
| -rw-r--r-- | tests/field_test.go (renamed from field_test.go) | 18 | ||||
| -rw-r--r-- | tests/headers_test.go (renamed from headers_test.go) | 19 | ||||
| -rw-r--r-- | tests/object_iterator_test.go (renamed from object_iterator_test.go) | 38 | ||||
| -rw-r--r-- | tests/object_test.go (renamed from object_test.go) | 22 | ||||
| -rw-r--r-- | tests/shared_test.go (renamed from shared_test.go) | 23 |
20 files changed, 345 insertions, 221 deletions
@@ -25,7 +25,7 @@ static-tests: FORCE cover.out: FORCE @echo '>> go test...' - @go test -covermode count -coverpkg github.com/majewsky/schwift/... -coverprofile $@ + @go test -covermode count -coverpkg github.com/majewsky/schwift/... -coverprofile $@ github.com/majewsky/schwift/tests cover.html: cover.out @echo '>> rendering cover.html...' @go tool cover -html=$< -o $@ @@ -14,7 +14,7 @@ You can get this with `go get github.com/majewsky/schwift`. When using this in a ## Usage -This library uses [Gophercloud](https://github.com/gophercloud/gophercloud) to handle authentication, so to use Schwift, you have to first build a `gophercloud.ServiceClient` and then pass that to `schwift.Account()` to get a handle on the Swift account. +This library uses [Gophercloud](https://github.com/gophercloud/gophercloud) to handle authentication, so to use Schwift, you have to first build a `gophercloud.ServiceClient` and then pass that to `gopherschwift.Wrap()` to get a handle on the Swift account. For example, to connect to Swift using OpenStack Keystone authentication: @@ -22,14 +22,14 @@ For example, to connect to Swift using OpenStack Keystone authentication: import ( "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack" - "github.com/majewsky/schwift" + "github.com/majewsky/schwift/gopherschwift" ) authOptions, err := openstack.AuthOptionsFromEnv() provider, err := openstack.AuthenticatedClient(authOptions) client, err := openstack.NewObjectStorageV1(provider, gophercloud.EndpointOpts{}) -account, err := schwift.AccountFromGophercloud(client) +account, err := gopherschwift.Wrap(client) ``` To connect to Swift using Swift's built-in authentication: @@ -38,7 +38,7 @@ To connect to Swift using Swift's built-in authentication: import ( "github.com/gophercloud/gophercloud/openstack" "github.com/gophercloud/gophercloud/openstack/objectstore/v1/swauth" - "github.com/majewsky/schwift" + "github.com/majewsky/schwift/gopherschwift" ) provider, err := openstack.NewClient("http://swift.example.com:8080") @@ -47,7 +47,7 @@ client, err := swauth.NewObjectStorageV1(provider, swauth.AuthOpts { Key: "password", }) -account, err := schwift.AccountFromGophercloud(client) +account, err := gopherschwift.Wrap(client) ``` From this point, follow the [API documentation](https://godoc.org/github.com/majewsky/schwift) for what you can do with @@ -77,7 +77,7 @@ Schwift improves on ncw/swift by: ### What about Gophercloud? Schwift uses Gophercloud for authentication. That solves one problem that ncw/swift has, namely that you cannot -use the Keystone token that ncw/swift fetches for talking to other OpenStack services +use the Keystone token that ncw/swift fetches for talking to other OpenStack services. But besides the auth code, Schwift avoids all other parts of Gophercloud. Gophercloud, like many other OpenStack client libraries, is modeled frankly around the "JSON-in, JSON-out" request-response-based design that all OpenStack APIs @@ -21,13 +21,11 @@ package schwift import ( "fmt" "regexp" - - "github.com/gophercloud/gophercloud" ) //Account represents a Swift account. type Account struct { - client Client + backend Backend //URL parts baseURL string name string @@ -37,28 +35,21 @@ type Account struct { var endpointURLRegexp = regexp.MustCompile(`^(.*/)v1/(.*)/$`) -//AccountFromClient takes something that implements the Client interface, and +//InitializeAccount takes something that implements the Backend interface, and //returns the Account instance corresponding to the account/project that this -//client is connected to. -func AccountFromClient(client Client) (*Account, error) { - match := endpointURLRegexp.FindStringSubmatch(client.EndpointURL()) +//backend is connected to. +func InitializeAccount(backend Backend) (*Account, error) { + match := endpointURLRegexp.FindStringSubmatch(backend.EndpointURL()) if match == nil { - return nil, fmt.Errorf(`schwift.AccountFromClient(): invalid Swift endpoint URL: cannot find "/v1/" in %q`, client.EndpointURL()) + return nil, fmt.Errorf(`schwift.AccountFromClient(): invalid Swift endpoint URL: cannot find "/v1/" in %q`, backend.EndpointURL()) } return &Account{ - client: client, + backend: backend, baseURL: match[1], name: match[2], }, nil } -//AccountFromGophercloud takes a gophercloud.ServiceClient which wraps a Swift -//endpoint, and returns the Account instance corresponding to the account or -//project that this client is connected to. -func AccountFromGophercloud(client *gophercloud.ServiceClient) (*Account, error) { - return AccountFromClient(&gophercloudClient{client}) -} - //SwitchAccount returns a handle on a different account on the same server. Note //that you need reseller permissions to access accounts other than that where //you originally authenticated. This method does not check whether the account @@ -69,7 +60,7 @@ func AccountFromGophercloud(client *gophercloud.ServiceClient) (*Account, error) func (a *Account) SwitchAccount(accountName string) *Account { newEndpointURL := a.baseURL + "v1/" + accountName + "/" return &Account{ - client: a.client.Clone(newEndpointURL), + backend: a.backend.Clone(newEndpointURL), baseURL: a.baseURL, name: accountName, } @@ -81,10 +72,10 @@ func (a *Account) Name() string { return a.name } -//Client returns the Client which is used to make requests against this +//Backend returns the backend which is used to make requests against this //account. -func (a *Account) Client() Client { - return a.client +func (a *Account) Backend() Backend { + return a.backend } //Headers returns the AccountHeaders for this account. If the AccountHeaders @@ -99,7 +90,7 @@ func (a *Account) Headers() (AccountHeaders, error) { resp, err := Request{ Method: "HEAD", ExpectStatusCodes: []int{204}, - }.Do(a.client) + }.Do(a.backend) if err != nil { return AccountHeaders{}, err } @@ -129,7 +120,7 @@ func (a *Account) Update(headers AccountHeaders, opts *RequestOptions) error { Headers: headersToHTTP(headers), Options: opts, ExpectStatusCodes: []int{204}, - }.Do(a.client) + }.Do(a.backend) if err == nil { a.Invalidate() } @@ -150,7 +141,7 @@ func (a *Account) Create(headers AccountHeaders, opts *RequestOptions) error { Options: opts, ExpectStatusCodes: []int{201, 202}, DrainResponseBody: true, - }.Do(a.client) + }.Do(a.backend) if err == nil { a.Invalidate() } diff --git a/backend.go b/backend.go new file mode 100644 index 0000000..30013b1 --- /dev/null +++ b/backend.go @@ -0,0 +1,43 @@ +/****************************************************************************** +* +* Copyright 2018 Stefan Majewsky <majewsky@gmx.net> +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +******************************************************************************/ + +package schwift + +import ( + "net/http" +) + +//Backend is the interface between Schwift and the libraries providing +//authentication for it. +// +//TODO list implementations +type Backend interface { + //EndpointURL returns the endpoint URL from the Keystone catalog for the + //Swift account that this backend operates on. It should look like + //`http://domain.tld/v1/AUTH_projectid/`. + EndpointURL() string + //Clone returns a deep clone of this backend with the endpoint URL changed to + //the given URL. + Clone(newEndpointURL string) Backend + //Do executes the given HTTP request after adding to it the X-Auth-Token + //header containing the backend's current Keystone (or Swift auth) token. It + //may also set other headers, such as User-Agent. If the status code returned + //is 401, it shall attempt to acquire a new auth token and restart the + //request with the new token. + Do(req *http.Request) (*http.Response, error) +} diff --git a/client.go b/client.go deleted file mode 100644 index 5f1192b..0000000 --- a/client.go +++ /dev/null @@ -1,94 +0,0 @@ -/****************************************************************************** -* -* Copyright 2018 Stefan Majewsky <majewsky@gmx.net> -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -* -******************************************************************************/ - -package schwift - -import ( - "net/http" - - "github.com/gophercloud/gophercloud" -) - -//Client is the interface between Schwift and the libraries providing -//authentication for it. Schwift can wrap gophercloud.ServiceClient to provide -//this interface, so if you have a gophercloud.ServiceClient, use the -//AccountFromGophercloud method to obtain the corresponding schwift.Account -//instance. -type Client interface { - //EndpointURL returns the endpoint URL from the Keystone catalog for the - //Swift account that this client operates on. It should look like - //`http://domain.tld/v1/AUTH_projectid/`. - EndpointURL() string - //Clone returns a deep clone of this client with the endpoint URL changed to - //the given URL. - Clone(newEndpointURL string) Client - //Do executes the given request after adding to it the X-Auth-Token header - //containing the client's current Keystone (or Swift auth) token. It may - //also set other headers, such as User-Agent. If the status code returned is - //401, it shall attempt to acquire a new auth token and restart the request - //with the new token. - Do(req *http.Request) (*http.Response, error) -} - -type gophercloudClient struct { - c *gophercloud.ServiceClient -} - -func (g *gophercloudClient) EndpointURL() string { - return g.c.Endpoint -} - -func (g *gophercloudClient) Clone(newEndpointURL string) Client { - clonedClient := *g.c - clonedClient.Endpoint = newEndpointURL - return &gophercloudClient{&clonedClient} -} - -func (g *gophercloudClient) Do(req *http.Request) (*http.Response, error) { - return g.do(req, false) -} - -func (g *gophercloudClient) do(req *http.Request, afterReauth bool) (*http.Response, error) { - provider := g.c.ProviderClient - - req.Header.Set("User-Agent", provider.UserAgent.Join()) - for key, value := range provider.AuthenticatedHeaders() { - req.Header.Set(key, value) - } - - resp, err := provider.HTTPClient.Do(req) - if err != nil { - return nil, err - } - - //detect expired token - if resp.StatusCode == http.StatusUnauthorized && !afterReauth { - err := drainResponseBody(resp) - if err != nil { - return nil, err - } - err = provider.Reauthenticate(resp.Request.Header.Get("X-Auth-Token")) - if err != nil { - return nil, err - } - //restart request with new token - return g.do(req, true) - } - - return resp, nil -} diff --git a/container.go b/container.go index 1e30eff..741350e 100644 --- a/container.go +++ b/container.go @@ -76,7 +76,7 @@ func (c *Container) Headers() (ContainerHeaders, error) { Method: "HEAD", ContainerName: c.name, ExpectStatusCodes: []int{204}, - }.Do(c.a.client) + }.Do(c.a.backend) if err != nil { return ContainerHeaders{}, err } @@ -103,7 +103,7 @@ func (c *Container) Update(headers ContainerHeaders, opts *RequestOptions) error Headers: headersToHTTP(headers), Options: opts, ExpectStatusCodes: []int{204}, - }.Do(c.a.client) + }.Do(c.a.backend) if err == nil { c.Invalidate() } @@ -124,7 +124,7 @@ func (c *Container) Create(headers ContainerHeaders, opts *RequestOptions) error Options: opts, ExpectStatusCodes: []int{201, 202}, DrainResponseBody: true, - }.Do(c.a.client) + }.Do(c.a.backend) if err == nil { c.Invalidate() } @@ -146,7 +146,7 @@ func (c *Container) Delete(headers ContainerHeaders, opts *RequestOptions) error Headers: headersToHTTP(headers), Options: opts, ExpectStatusCodes: []int{204}, - }.Do(c.a.client) + }.Do(c.a.backend) if err == nil { c.Invalidate() } @@ -172,7 +172,7 @@ func (c *Container) EnsureExists() (*Container, error) { ContainerName: c.name, ExpectStatusCodes: []int{201, 202}, DrainResponseBody: true, - }.Do(c.a.client) + }.Do(c.a.backend) return c, err } @@ -21,6 +21,8 @@ Package schwift is a client library for OpenStack Swift (https://github.com/openstack/swift, https://openstack.org). +TODO update doc for changed auth workflow + It uses Gophercloud (https://github.com/gophercloud/gophercloud) for authentication, so you usually start by obtaining a gophercloud.ServiceClient for Swift like so: @@ -37,9 +39,9 @@ Or, if you use Swift's built-in authentication instead of Keystone: Key: "password", }) -Then, in both cases, you use schwift.AccountFromGophercloud() to obtain a -schwift.Account instance, from which point you have access to all of schwift's -API. +Then, in both cases, you use Wrap() from the subpackage gopherschwift to obtain +a schwift.Account instance, from which point you have access to all of +schwift's API. Caching diff --git a/gopherschwift/package.go b/gopherschwift/package.go new file mode 100644 index 0000000..7c66b16 --- /dev/null +++ b/gopherschwift/package.go @@ -0,0 +1,105 @@ +/****************************************************************************** +* +* Copyright 2018 Stefan Majewsky <majewsky@gmx.net> +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +******************************************************************************/ + +/* + +Package gopherschwift contains a Gophercloud backend for Schwift. + +If your application uses Gophercloud (https://github.com/gophercloud/gophercloud), +you can use the Wrap() function in this package as an entrypoint to Schwift. +A schwift.Account created this way will re-use Gophercloud's authentication code, +so you only need to obtain a client token once using Gophercloud. For example: + + authOptions, err := openstack.AuthOptionsFromEnv() // or build a gophercloud.AuthOptions instance yourself + provider, err := openstack.AuthenticatedClient(authOptions) + client, err := openstack.NewObjectStorageV1(provider, gophercloud.EndpointOpts{}) + + account, err := gopherschwift.Wrap(client) + +Using this schwift.Account instance, you have access to all of schwift's API. + +*/ +package gopherschwift + +import ( + "io" + "io/ioutil" + "net/http" + + "github.com/gophercloud/gophercloud" + "github.com/majewsky/schwift" +) + +//Wrap creates a schwift.Account that uses the given service client as its +//backend. The service client must refer to a Swift endpoint, i.e. it should +//have been created by openstack.NewObjectStorageV1(). +func Wrap(client *gophercloud.ServiceClient) (*schwift.Account, error) { + return schwift.InitializeAccount(&backend{client}) +} + +type backend struct { + c *gophercloud.ServiceClient +} + +func (g *backend) EndpointURL() string { + return g.c.Endpoint +} + +func (g *backend) Clone(newEndpointURL string) schwift.Backend { + clonedClient := *g.c + clonedClient.Endpoint = newEndpointURL + return &backend{&clonedClient} +} + +func (g *backend) Do(req *http.Request) (*http.Response, error) { + return g.do(req, false) +} + +func (g *backend) do(req *http.Request, afterReauth bool) (*http.Response, error) { + provider := g.c.ProviderClient + + req.Header.Set("User-Agent", provider.UserAgent.Join()) + for key, value := range provider.AuthenticatedHeaders() { + req.Header.Set(key, value) + } + + resp, err := provider.HTTPClient.Do(req) + if err != nil { + return nil, err + } + + //detect expired token + if resp.StatusCode == http.StatusUnauthorized && !afterReauth { + _, err := io.Copy(ioutil.Discard, resp.Body) + if err != nil { + return nil, err + } + err = resp.Body.Close() + if err != nil { + return nil, err + } + err = provider.Reauthenticate(resp.Request.Header.Get("X-Auth-Token")) + if err != nil { + return nil, err + } + //restart request with new token + return g.do(req, true) + } + + return resp, nil +} diff --git a/iterator.go b/iterator.go index acbc449..e460edd 100644 --- a/iterator.go +++ b/iterator.go @@ -116,7 +116,7 @@ func (b *iteratorBase) nextPage(limit int) ([]string, error) { if b.eof { return nil, nil } - resp, err := b.request(limit, false).Do(b.i.getAccount().client) + resp, err := b.request(limit, false).Do(b.i.getAccount().backend) if err != nil { return nil, err } @@ -145,7 +145,7 @@ func (b *iteratorBase) nextPageDetailed(limit int, data interface{}) error { if b.eof { return nil } - resp, err := b.request(limit, true).Do(b.i.getAccount().client) + resp, err := b.request(limit, true).Do(b.i.getAccount().backend) if err != nil { return err } @@ -98,7 +98,7 @@ func (o *Object) Headers() (ObjectHeaders, error) { //since Openstack LOVES to be inconsistent with everything (incl. itself), //this returns 200 instead of 204 ExpectStatusCodes: []int{200}, - }.Do(o.c.a.client) + }.Do(o.c.a.backend) if err != nil { return ObjectHeaders{}, err } @@ -126,7 +126,7 @@ func (o *Object) Update(headers ObjectHeaders, opts *RequestOptions) error { Headers: headersToHTTP(headers), Options: opts, ExpectStatusCodes: []int{202}, - }.Do(o.c.a.client) + }.Do(o.c.a.backend) if err == nil { o.Invalidate() } @@ -189,7 +189,7 @@ func (o *Object) Upload(content io.Reader, headers ObjectHeaders, opts *RequestO Body: content, ExpectStatusCodes: []int{201}, DrainResponseBody: true, - }.Do(o.c.a.client) + }.Do(o.c.a.backend) if err != nil { return err } @@ -284,7 +284,7 @@ func (o *Object) Delete(headers ObjectHeaders, opts *RequestOptions) error { Headers: headersToHTTP(headers), Options: opts, ExpectStatusCodes: []int{204}, - }.Do(o.c.a.client) + }.Do(o.c.a.backend) if err == nil { o.c.Invalidate() } @@ -316,7 +316,7 @@ func (o *Object) Download(headers ObjectHeaders, opts *RequestOptions) Downloade Headers: headersToHTTP(headers), Options: opts, ExpectStatusCodes: []int{200}, - }.Do(o.c.a.client) + }.Do(o.c.a.backend) if err == nil { headers := ObjectHeaders(headersFromHTTP(resp.Header)) err = headers.Validate() @@ -60,8 +60,8 @@ type Request struct { } //URL returns the full URL for this request. -func (r Request) URL(client Client, values url.Values) (string, error) { - uri, err := url.Parse(client.EndpointURL()) +func (r Request) URL(backend Backend, values url.Values) (string, error) { + uri, err := url.Parse(backend.EndpointURL()) if err != nil { return "", err } @@ -84,14 +84,14 @@ func (r Request) URL(client Client, values url.Values) (string, error) { return uri.String(), nil } -//Do executes this request on the given Client. -func (r Request) Do(client Client) (*http.Response, error) { +//Do executes this request on the given Backend. +func (r Request) Do(backend Backend) (*http.Response, error) { //build URL var values url.Values if r.Options != nil { values = r.Options.Values } - uri, err := r.URL(client, values) + uri, err := r.URL(backend, values) if err != nil { return nil, err } @@ -109,7 +109,7 @@ func (r Request) Do(client Client) (*http.Response, error) { req.Header.Set("Expect", "100-continue") } - resp, err := client.Do(req) + resp, err := backend.Do(req) if err != nil { return nil, err } diff --git a/account_test.go b/tests/account_test.go index 3425f3f..310d4ce 100644 --- a/account_test.go +++ b/tests/account_test.go @@ -16,14 +16,16 @@ * ******************************************************************************/ -package schwift +package tests import ( "testing" + + "github.com/majewsky/schwift" ) func TestAccountBasic(t *testing.T) { - testWithAccount(t, func(a *Account) { + testWithAccount(t, func(a *schwift.Account) { hdr, err := a.Headers() if !expectSuccess(t, err) { t.FailNow() @@ -37,9 +39,9 @@ func TestAccountBasic(t *testing.T) { } func TestAccountMetadata(t *testing.T) { - testWithAccount(t, func(a *Account) { + testWithAccount(t, func(a *schwift.Account) { //test creating some metadata - hdr := make(AccountHeaders) + hdr := make(schwift.AccountHeaders) hdr.Metadata().Set("schwift-test1", "first") hdr.Metadata().Set("schwift-test2", "second") err := a.Update(hdr, nil) @@ -55,7 +57,7 @@ func TestAccountMetadata(t *testing.T) { expectString(t, hdr.Metadata().Get("schwift-test2"), "second") //test deleting some metadata - hdr = make(AccountHeaders) + hdr = make(schwift.AccountHeaders) hdr.Metadata().Clear("schwift-test1") err = a.Update(hdr, nil) if !expectSuccess(t, err) { @@ -70,7 +72,7 @@ func TestAccountMetadata(t *testing.T) { expectString(t, hdr.Metadata().Get("schwift-test2"), "second") //test updating some metadata - hdr = make(AccountHeaders) + hdr = make(schwift.AccountHeaders) hdr.Metadata().Set("schwift-test1", "will not be set") hdr.Metadata().Del("schwift-test1") hdr.Metadata().Set("schwift-test2", "changed") diff --git a/tests/backend_test.go b/tests/backend_test.go new file mode 100644 index 0000000..eca7232 --- /dev/null +++ b/tests/backend_test.go @@ -0,0 +1,43 @@ +/****************************************************************************** +* +* Copyright 2018 Stefan Majewsky <majewsky@gmx.net> +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +******************************************************************************/ + +package tests + +import ( + "net/http" + + "github.com/majewsky/schwift" +) + +type RequestCountingBackend struct { + Inner schwift.Backend + Count int +} + +func (b *RequestCountingBackend) EndpointURL() string { + return b.Inner.EndpointURL() +} + +func (b *RequestCountingBackend) Clone(newEndpointURL string) schwift.Backend { + return &RequestCountingBackend{Inner: b.Inner.Clone(newEndpointURL)} +} + +func (b *RequestCountingBackend) Do(req *http.Request) (*http.Response, error) { + b.Count++ + return b.Inner.Do(req) +} diff --git a/container_iterator_test.go b/tests/container_iterator_test.go index 70620ad..b9813db 100644 --- a/container_iterator_test.go +++ b/tests/container_iterator_test.go @@ -16,15 +16,17 @@ * ******************************************************************************/ -package schwift +package tests import ( "fmt" "testing" + + "github.com/majewsky/schwift" ) func TestContainerIterator(t *testing.T) { - testWithAccount(t, func(a *Account) { + testWithAccount(t, func(a *schwift.Account) { cname := func(idx int) string { return fmt.Sprintf("schwift-test-listing%d", idx) } @@ -85,32 +87,26 @@ func TestContainerIterator(t *testing.T) { iter = a.Containers() iter.Prefix = "schwift-test-listing" idx := 0 - expectSuccess(t, iter.Foreach(func(c *Container) error { + expectSuccess(t, iter.Foreach(func(c *schwift.Container) error { idx++ expectString(t, c.Name(), cname(idx)) return nil })) expectInt(t, idx, 4) - - if a.headers == nil { - t.Error("ContainerIterator.Foreach did not initialize Account.Headers") - } + expectAccountHeadersCached(t, a) //test ForeachDetailed a.Invalidate() iter = a.Containers() iter.Prefix = "schwift-test-listing" idx = 0 - expectSuccess(t, iter.ForeachDetailed(func(info ContainerInfo) error { + expectSuccess(t, iter.ForeachDetailed(func(info schwift.ContainerInfo) error { idx++ expectString(t, info.Container.Name(), cname(idx)) return nil })) expectInt(t, idx, 4) - - if a.headers == nil { - t.Error("ContainerIterator.ForeachDetailed did not initialize Account.Headers") - } + expectAccountHeadersCached(t, a) //test Collect iter = a.Containers() @@ -129,13 +125,25 @@ func TestContainerIterator(t *testing.T) { //cleanup iter = a.Containers() iter.Prefix = "schwift-test-listing" - expectSuccess(t, iter.Foreach(func(c *Container) error { + expectSuccess(t, iter.Foreach(func(c *schwift.Container) error { return c.Delete(nil, nil) })) }) } -func expectContainerNames(t *testing.T, actualContainers []*Container, expectedNames ...string) { +func expectAccountHeadersCached(t *testing.T, a *schwift.Account) { + requestCountBefore := a.Backend().(*RequestCountingBackend).Count + _, err := a.Headers() + expectSuccess(t, err) + requestCountAfter := a.Backend().(*RequestCountingBackend).Count + + t.Helper() + if requestCountBefore != requestCountAfter { + t.Error("Account.Headers was expected to use cache, but issued HEAD request") + } +} + +func expectContainerNames(t *testing.T, actualContainers []*schwift.Container, expectedNames ...string) { t.Helper() if len(actualContainers) != len(expectedNames) { t.Errorf("expected %d containers, got %d containers", @@ -150,7 +158,7 @@ func expectContainerNames(t *testing.T, actualContainers []*Container, expectedN } } -func expectContainerInfos(t *testing.T, actualInfos []ContainerInfo, expectedNames ...string) { +func expectContainerInfos(t *testing.T, actualInfos []schwift.ContainerInfo, expectedNames ...string) { t.Helper() if len(actualInfos) != len(expectedNames) { t.Errorf("expected %d containers, got %d containers", diff --git a/container_test.go b/tests/container_test.go index 654ca84..5a0046b 100644 --- a/container_test.go +++ b/tests/container_test.go @@ -16,15 +16,17 @@ * ******************************************************************************/ -package schwift +package tests import ( "net/http" "testing" + + "github.com/majewsky/schwift" ) func TestContainerLifecycle(t *testing.T) { - testWithAccount(t, func(a *Account) { + testWithAccount(t, func(a *schwift.Account) { containerName := getRandomName() c := a.Container(containerName) @@ -39,8 +41,8 @@ func TestContainerLifecycle(t *testing.T) { _, err = c.Headers() expectError(t, err, "expected 204 response, got 404 instead") - expectBool(t, Is(err, http.StatusNotFound), true) - expectBool(t, Is(err, http.StatusNoContent), false) + expectBool(t, schwift.Is(err, http.StatusNotFound), true) + expectBool(t, schwift.Is(err, http.StatusNoContent), false) //DELETE should be idempotent and not return success on non-existence, but //OpenStack LOVES to be inconsistent with everything (including, notably, itself) @@ -60,14 +62,14 @@ func TestContainerLifecycle(t *testing.T) { } func TestContainerUpdate(t *testing.T) { - testWithContainer(t, func(c *Container) { + testWithContainer(t, func(c *schwift.Container) { hdr, err := c.Headers() expectSuccess(t, err) expectBool(t, hdr.ObjectCount().Exists(), true) expectUint64(t, hdr.ObjectCount().Get(), 0) - hdr = make(ContainerHeaders) + hdr = make(schwift.ContainerHeaders) hdr.ObjectCountQuota().Set(23) hdr.BytesUsedQuota().Set(42) diff --git a/field_test.go b/tests/field_test.go index 539ed74..f3c03ca 100644 --- a/field_test.go +++ b/tests/field_test.go @@ -16,16 +16,18 @@ * ******************************************************************************/ -package schwift +package tests import ( "net/http" "strconv" "testing" + + "github.com/majewsky/schwift" ) func TestFieldString(t *testing.T) { - hdr := make(AccountHeaders) + hdr := make(schwift.AccountHeaders) expectBool(t, hdr.TempURLKey().Exists(), false) expectString(t, hdr.TempURLKey().Get(), "") expectSuccess(t, hdr.Validate()) @@ -59,7 +61,7 @@ func TestFieldString(t *testing.T) { //////////////////////////////////////////////////////////////////////////////// func TestFieldTimestamp(t *testing.T) { - testWithAccount(t, func(a *Account) { + testWithAccount(t, func(a *schwift.Account) { hdr, err := a.Headers() if !expectSuccess(t, err) { return @@ -72,7 +74,7 @@ func TestFieldTimestamp(t *testing.T) { expectFloat64(t, actual, expected) }) - hdr := make(AccountHeaders) + hdr := make(schwift.AccountHeaders) expectBool(t, hdr.CreatedAt().Exists(), false) expectBool(t, hdr.CreatedAt().Get().IsZero(), true) expectSuccess(t, hdr.Validate()) @@ -84,7 +86,7 @@ func TestFieldTimestamp(t *testing.T) { } func TestFieldHTTPTimestamp(t *testing.T) { - testWithContainer(t, func(c *Container) { + testWithContainer(t, func(c *schwift.Container) { obj := c.Object("test") err := obj.Upload(nil, nil, nil) if !expectSuccess(t, err) { @@ -102,7 +104,7 @@ func TestFieldHTTPTimestamp(t *testing.T) { expectInt64(t, actual.Unix(), expected.Unix()) }) - hdr := make(ObjectHeaders) + hdr := make(schwift.ObjectHeaders) expectBool(t, hdr.UpdatedAt().Exists(), false) expectBool(t, hdr.UpdatedAt().Get().IsZero(), true) expectSuccess(t, hdr.Validate()) @@ -116,7 +118,7 @@ func TestFieldHTTPTimestamp(t *testing.T) { //////////////////////////////////////////////////////////////////////////////// func TestFieldUint64(t *testing.T) { - hdr := make(AccountHeaders) + hdr := make(schwift.AccountHeaders) expectBool(t, hdr.BytesUsedQuota().Exists(), false) expectUint64(t, hdr.BytesUsedQuota().Get(), 0) expectSuccess(t, hdr.Validate()) @@ -148,7 +150,7 @@ func TestFieldUint64(t *testing.T) { } func TestFieldUint64Readonly(t *testing.T) { - hdr := make(AccountHeaders) + hdr := make(schwift.AccountHeaders) expectBool(t, hdr.BytesUsed().Exists(), false) expectUint64(t, hdr.BytesUsed().Get(), 0) expectSuccess(t, hdr.Validate()) diff --git a/headers_test.go b/tests/headers_test.go index cb99571..8067ef4 100644 --- a/headers_test.go +++ b/tests/headers_test.go @@ -16,21 +16,22 @@ * ******************************************************************************/ -package schwift +package tests import ( - "net/http" "testing" + + "github.com/majewsky/schwift" ) func TestParseAccountHeadersSuccess(t *testing.T) { - headers := AccountHeaders(headersFromHTTP(http.Header{ - "X-Account-Bytes-Used": {"1234"}, - "X-Account-Object-Count": {"42"}, - "X-Account-Container-Count": {"23"}, - "X-Account-Meta-Quota-Bytes": {"1048576"}, - "X-Account-Meta-foo": {"bar"}, - })) + headers := schwift.AccountHeaders{ + "X-Account-Bytes-Used": "1234", + "X-Account-Object-Count": "42", + "X-Account-Container-Count": "23", + "X-Account-Meta-Quota-Bytes": "1048576", + "X-Account-Meta-Foo": "bar", + } expectSuccess(t, headers.Validate()) expectUint64(t, headers.BytesUsed().Get(), 1234) diff --git a/object_iterator_test.go b/tests/object_iterator_test.go index 92fe4b1..5112324 100644 --- a/object_iterator_test.go +++ b/tests/object_iterator_test.go @@ -16,26 +16,28 @@ * ******************************************************************************/ -package schwift +package tests import ( "bytes" "fmt" "testing" + + "github.com/majewsky/schwift" ) var objectExampleContent = []byte(`{"message":"Hello World!"}`) var objectExampleContentEtag = etagOf(objectExampleContent) func TestObjectIterator(t *testing.T) { - testWithContainer(t, func(c *Container) { + testWithContainer(t, func(c *schwift.Container) { oname := func(idx int) string { return fmt.Sprintf("schwift-test-listing%d", idx) } //create test objects that can be listed for idx := 1; idx <= 4; idx++ { - hdr := make(ObjectHeaders) + hdr := make(schwift.ObjectHeaders) hdr.ContentType().Set("application/json") err := c.Object(oname(idx)).Upload(bytes.NewReader(objectExampleContent), hdr, nil) expectSuccess(t, err) @@ -91,32 +93,26 @@ func TestObjectIterator(t *testing.T) { iter = c.Objects() iter.Prefix = "schwift-test-listing" idx := 0 - expectSuccess(t, iter.Foreach(func(o *Object) error { + expectSuccess(t, iter.Foreach(func(o *schwift.Object) error { idx++ expectString(t, o.Name(), oname(idx)) return nil })) expectInt(t, idx, 4) - - if c.headers == nil { - t.Error("ObjectIterator.Foreach did not initialize Container.Headers") - } + expectContainerHeadersCached(t, c) //test ForeachDetailed c.Invalidate() iter = c.Objects() iter.Prefix = "schwift-test-listing" idx = 0 - expectSuccess(t, iter.ForeachDetailed(func(info ObjectInfo) error { + expectSuccess(t, iter.ForeachDetailed(func(info schwift.ObjectInfo) error { idx++ expectString(t, info.Object.Name(), oname(idx)) return nil })) expectInt(t, idx, 4) - - if c.headers == nil { - t.Error("ObjectIterator.ForeachDetailed did not initialize Container.Headers") - } + expectContainerHeadersCached(t, c) //test Collect iter = c.Objects() @@ -134,7 +130,19 @@ func TestObjectIterator(t *testing.T) { }) } -func expectObjectNames(t *testing.T, actualObjects []*Object, expectedNames ...string) { +func expectContainerHeadersCached(t *testing.T, c *schwift.Container) { + requestCountBefore := c.Account().Backend().(*RequestCountingBackend).Count + _, err := c.Headers() + expectSuccess(t, err) + requestCountAfter := c.Account().Backend().(*RequestCountingBackend).Count + + t.Helper() + if requestCountBefore != requestCountAfter { + t.Error("Container.Headers() was expected to use cache, but issued HEAD request") + } +} + +func expectObjectNames(t *testing.T, actualObjects []*schwift.Object, expectedNames ...string) { t.Helper() if len(actualObjects) != len(expectedNames) { t.Errorf("expected %d objects, got %d objects", @@ -149,7 +157,7 @@ func expectObjectNames(t *testing.T, actualObjects []*Object, expectedNames ...s } } -func expectObjectInfos(t *testing.T, actualInfos []ObjectInfo, expectedNames ...string) { +func expectObjectInfos(t *testing.T, actualInfos []schwift.ObjectInfo, expectedNames ...string) { t.Helper() if len(actualInfos) != len(expectedNames) { t.Errorf("expected %d objects, got %d objects", diff --git a/object_test.go b/tests/object_test.go index 73463ad..740c3f2 100644 --- a/object_test.go +++ b/tests/object_test.go @@ -16,7 +16,7 @@ * ******************************************************************************/ -package schwift +package tests import ( "bytes" @@ -24,10 +24,12 @@ import ( "io/ioutil" "net/http" "testing" + + "github.com/majewsky/schwift" ) func TestObjectLifecycle(t *testing.T) { - testWithContainer(t, func(c *Container) { + testWithContainer(t, func(c *schwift.Container) { objectName := getRandomName() o := c.Object(objectName) @@ -43,8 +45,8 @@ func TestObjectLifecycle(t *testing.T) { _, err = o.Headers() expectError(t, err, "expected 200 response, got 404 instead") - expectBool(t, Is(err, http.StatusNotFound), true) - expectBool(t, Is(err, http.StatusNoContent), false) + expectBool(t, schwift.Is(err, http.StatusNotFound), true) + expectBool(t, schwift.Is(err, http.StatusNoContent), false) //DELETE should be idempotent and not return success on non-existence, but //OpenStack LOVES to be inconsistent with everything (including, notably, itself) @@ -64,8 +66,8 @@ func TestObjectLifecycle(t *testing.T) { } func TestObjectUpload(t *testing.T) { - testWithContainer(t, func(c *Container) { - validateUploadedFile := func(obj *Object, expected []byte) { + testWithContainer(t, func(c *schwift.Container) { + validateUploadedFile := func(obj *schwift.Object, expected []byte) { str, err := obj.Download(nil, nil).AsString() expectSuccess(t, err) expectString(t, str, string(expected)) @@ -131,7 +133,7 @@ func (r opaqueReader) Read(buf []byte) (int, error) { } func TestObjectDownload(t *testing.T) { - testWithContainer(t, func(c *Container) { + testWithContainer(t, func(c *schwift.Container) { //upload example object obj := c.Object("example") err := obj.Upload(bytes.NewReader(objectExampleContent), nil, nil) @@ -164,14 +166,14 @@ func TestObjectDownload(t *testing.T) { } func TestObjectUpdate(t *testing.T) { - testWithContainer(t, func(c *Container) { + testWithContainer(t, func(c *schwift.Container) { obj := c.Object("example") //test that metadata update fails for non-existing object - newHeaders := make(ObjectHeaders) + newHeaders := make(schwift.ObjectHeaders) newHeaders.ContentType().Set("application/json") err := obj.Update(newHeaders, nil) - expectBool(t, Is(err, http.StatusNotFound), true) + expectBool(t, schwift.Is(err, http.StatusNotFound), true) expectError(t, err, "expected 202 response, got 404 instead: <html><h1>Not Found</h1><p>The resource could not be found.</p></html>") //create object diff --git a/shared_test.go b/tests/shared_test.go index 16113e2..187d0b4 100644 --- a/shared_test.go +++ b/tests/shared_test.go @@ -16,7 +16,7 @@ * ******************************************************************************/ -package schwift +package tests import ( "crypto/md5" @@ -29,9 +29,11 @@ import ( "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack" "github.com/gophercloud/gophercloud/openstack/objectstorage/v1/swauth" + "github.com/majewsky/schwift" + "github.com/majewsky/schwift/gopherschwift" ) -func testWithAccount(t *testing.T, testCode func(a *Account)) { +func testWithAccount(t *testing.T, testCode func(a *schwift.Account)) { stAuth := os.Getenv("ST_AUTH") stUser := os.Getenv("ST_USER") stKey := os.Getenv("ST_KEY") @@ -69,16 +71,23 @@ func testWithAccount(t *testing.T, testCode func(a *Account)) { } } - account, err := AccountFromGophercloud(client) + account, err := gopherschwift.Wrap(client) if err != nil { - t.Errorf("schwift.AccountFromGophercloud returned: " + err.Error()) + t.Error(err.Error()) + return + } + account, err = schwift.InitializeAccount( + &RequestCountingBackend{Inner: account.Backend()}, + ) + if err != nil { + t.Error(err.Error()) return } testCode(account) } -func testWithContainer(t *testing.T, testCode func(c *Container)) { - testWithAccount(t, func(a *Account) { +func testWithContainer(t *testing.T, testCode func(c *schwift.Container)) { + testWithAccount(t, func(a *schwift.Account) { containerName := getRandomName() container, err := a.Container(containerName).EnsureExists() expectSuccess(t, err) @@ -89,7 +98,7 @@ func testWithContainer(t *testing.T, testCode func(c *Container)) { exists, err := container.Exists() expectSuccess(t, err) if exists { - expectSuccess(t, container.Objects().Foreach(func(o *Object) error { + expectSuccess(t, container.Objects().Foreach(func(o *schwift.Object) error { return o.Delete(nil, nil) })) err = container.Delete(nil, nil) |
