From 60d4779889baedc44972d4749daa073efca3b25c Mon Sep 17 00:00:00 2001 From: Stefan Majewsky Date: Mon, 19 Feb 2018 21:30:33 +0100 Subject: reorganize code * Gophercloud dependencies move into subpackage gopherschwift. * Tests move into subpackage tests (to avoid import cycles). + Rename "Client" to "Backend". --- Makefile | 2 +- README.md | 12 +-- account.go | 37 +++---- account_test.go | 90 ---------------- backend.go | 43 ++++++++ client.go | 94 ----------------- container.go | 10 +- container_iterator_test.go | 179 -------------------------------- container_test.go | 83 --------------- doc.go | 8 +- field_test.go | 165 ------------------------------ gopherschwift/package.go | 105 +++++++++++++++++++ headers_test.go | 46 --------- iterator.go | 4 +- object.go | 10 +- object_iterator_test.go | 180 -------------------------------- object_test.go | 193 ---------------------------------- request.go | 12 +-- shared_test.go | 207 ------------------------------------- tests/account_test.go | 92 +++++++++++++++++ tests/backend_test.go | 43 ++++++++ tests/container_iterator_test.go | 187 +++++++++++++++++++++++++++++++++ tests/container_test.go | 85 +++++++++++++++ tests/field_test.go | 167 ++++++++++++++++++++++++++++++ tests/headers_test.go | 47 +++++++++ tests/object_iterator_test.go | 188 ++++++++++++++++++++++++++++++++++ tests/object_test.go | 195 +++++++++++++++++++++++++++++++++++ tests/shared_test.go | 216 +++++++++++++++++++++++++++++++++++++++ 28 files changed, 1412 insertions(+), 1288 deletions(-) delete mode 100644 account_test.go create mode 100644 backend.go delete mode 100644 client.go delete mode 100644 container_iterator_test.go delete mode 100644 container_test.go delete mode 100644 field_test.go create mode 100644 gopherschwift/package.go delete mode 100644 headers_test.go delete mode 100644 object_iterator_test.go delete mode 100644 object_test.go delete mode 100644 shared_test.go create mode 100644 tests/account_test.go create mode 100644 tests/backend_test.go create mode 100644 tests/container_iterator_test.go create mode 100644 tests/container_test.go create mode 100644 tests/field_test.go create mode 100644 tests/headers_test.go create mode 100644 tests/object_iterator_test.go create mode 100644 tests/object_test.go create mode 100644 tests/shared_test.go diff --git a/Makefile b/Makefile index 042153b..fa49b20 100644 --- a/Makefile +++ b/Makefile @@ -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 $@ diff --git a/README.md b/README.md index 76f3483..b13b42b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/account.go b/account.go index 6abfaf3..91d1ce5 100644 --- a/account.go +++ b/account.go @@ -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/account_test.go b/account_test.go deleted file mode 100644 index 3425f3f..0000000 --- a/account_test.go +++ /dev/null @@ -1,90 +0,0 @@ -/****************************************************************************** -* -* Copyright 2018 Stefan Majewsky -* -* 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 ( - "testing" -) - -func TestAccountBasic(t *testing.T) { - testWithAccount(t, func(a *Account) { - hdr, err := a.Headers() - if !expectSuccess(t, err) { - t.FailNow() - } - //There are not a lot of things we can test here (besides testing that - //Headers() does not fail, i.e. everything parses correctly), but - //Content-Type is going to be text/plain because GET on an account lists - //the container names as plain text. - expectString(t, hdr.Get("Content-Type"), "text/plain; charset=utf-8") - }) -} - -func TestAccountMetadata(t *testing.T) { - testWithAccount(t, func(a *Account) { - //test creating some metadata - hdr := make(AccountHeaders) - hdr.Metadata().Set("schwift-test1", "first") - hdr.Metadata().Set("schwift-test2", "second") - err := a.Update(hdr, nil) - if !expectSuccess(t, err) { - t.FailNow() - } - - hdr, err = a.Headers() - if !expectSuccess(t, err) { - t.FailNow() - } - expectString(t, hdr.Metadata().Get("schwift-test1"), "first") - expectString(t, hdr.Metadata().Get("schwift-test2"), "second") - - //test deleting some metadata - hdr = make(AccountHeaders) - hdr.Metadata().Clear("schwift-test1") - err = a.Update(hdr, nil) - if !expectSuccess(t, err) { - t.FailNow() - } - - hdr, err = a.Headers() - if !expectSuccess(t, err) { - t.FailNow() - } - expectString(t, hdr.Metadata().Get("schwift-test1"), "") - expectString(t, hdr.Metadata().Get("schwift-test2"), "second") - - //test updating some metadata - hdr = make(AccountHeaders) - hdr.Metadata().Set("schwift-test1", "will not be set") - hdr.Metadata().Del("schwift-test1") - hdr.Metadata().Set("schwift-test2", "changed") - err = a.Update(hdr, nil) - if !expectSuccess(t, err) { - t.FailNow() - } - - hdr, err = a.Headers() - if !expectSuccess(t, err) { - t.FailNow() - } - expectString(t, hdr.Metadata().Get("schwift-test1"), "") - expectString(t, hdr.Metadata().Get("schwift-test2"), "changed") - - }) -} 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 +* +* 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 -* -* 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 } diff --git a/container_iterator_test.go b/container_iterator_test.go deleted file mode 100644 index 70620ad..0000000 --- a/container_iterator_test.go +++ /dev/null @@ -1,179 +0,0 @@ -/****************************************************************************** -* -* Copyright 2018 Stefan Majewsky -* -* 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 ( - "fmt" - "testing" -) - -func TestContainerIterator(t *testing.T) { - testWithAccount(t, func(a *Account) { - cname := func(idx int) string { - return fmt.Sprintf("schwift-test-listing%d", idx) - } - - //create test containers that can be listed - for idx := 1; idx <= 4; idx++ { - _, err := a.Container(cname(idx)).EnsureExists() - expectSuccess(t, err) - } - - //test iteration with empty last page - iter := a.Containers() - iter.Prefix = "schwift-test-listing" - cs, err := iter.NextPage(2) - expectSuccess(t, err) - expectContainerNames(t, cs, cname(1), cname(2)) - cs, err = iter.NextPage(2) - expectSuccess(t, err) - expectContainerNames(t, cs, cname(3), cname(4)) - cs, err = iter.NextPage(2) - expectSuccess(t, err) - expectContainerNames(t, cs) - cs, err = iter.NextPage(2) - expectSuccess(t, err) - expectContainerNames(t, cs) - - //test iteration with partial last page - iter = a.Containers() - iter.Prefix = "schwift-test-listing" - cs, err = iter.NextPage(3) - expectSuccess(t, err) - expectContainerNames(t, cs, cname(1), cname(2), cname(3)) - cs, err = iter.NextPage(3) - expectSuccess(t, err) - expectContainerNames(t, cs, cname(4)) - cs, err = iter.NextPage(4) - expectSuccess(t, err) - expectContainerNames(t, cs) - - //test detailed iteration - iter = a.Containers() - iter.Prefix = "schwift-test-listing" - cis, err := iter.NextPageDetailed(2) - expectSuccess(t, err) - expectContainerInfos(t, cis, cname(1), cname(2)) - cis, err = iter.NextPageDetailed(3) - expectSuccess(t, err) - expectContainerInfos(t, cis, cname(3), cname(4)) - cis, err = iter.NextPageDetailed(3) - expectSuccess(t, err) - expectContainerInfos(t, cis) - cis, err = iter.NextPageDetailed(3) - expectSuccess(t, err) - expectContainerInfos(t, cis) - - //test Foreach - a.Invalidate() - iter = a.Containers() - iter.Prefix = "schwift-test-listing" - idx := 0 - expectSuccess(t, iter.Foreach(func(c *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") - } - - //test ForeachDetailed - a.Invalidate() - iter = a.Containers() - iter.Prefix = "schwift-test-listing" - idx = 0 - expectSuccess(t, iter.ForeachDetailed(func(info 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") - } - - //test Collect - iter = a.Containers() - iter.Prefix = "schwift-test-listing" - cs, err = iter.Collect() - expectSuccess(t, err) - expectContainerNames(t, cs, cname(1), cname(2), cname(3), cname(4)) - - //test CollectDetailed - iter = a.Containers() - iter.Prefix = "schwift-test-listing" - cis, err = iter.CollectDetailed() - expectSuccess(t, err) - expectContainerInfos(t, cis, cname(1), cname(2), cname(3), cname(4)) - - //cleanup - iter = a.Containers() - iter.Prefix = "schwift-test-listing" - expectSuccess(t, iter.Foreach(func(c *Container) error { - return c.Delete(nil, nil) - })) - }) -} - -func expectContainerNames(t *testing.T, actualContainers []*Container, expectedNames ...string) { - t.Helper() - if len(actualContainers) != len(expectedNames) { - t.Errorf("expected %d containers, got %d containers", - len(expectedNames), len(actualContainers)) - return - } - for idx, c := range actualContainers { - if c.Name() != expectedNames[idx] { - t.Errorf("expected containers[%d].Name() == %q, got %q", - idx, expectedNames[idx], c.Name()) - } - } -} - -func expectContainerInfos(t *testing.T, actualInfos []ContainerInfo, expectedNames ...string) { - t.Helper() - if len(actualInfos) != len(expectedNames) { - t.Errorf("expected %d containers, got %d containers", - len(expectedNames), len(actualInfos)) - return - } - for idx, info := range actualInfos { - if info.Container.Name() != expectedNames[idx] { - t.Errorf("expected containers[%d].Name() == %q, got %q", - idx, expectedNames[idx], info.Container.Name()) - } - //TODO: upload test object of defined size to the listed containers to - //check if this zero is not just the default value - if info.BytesUsed != 0 { - t.Errorf("expected containers[%d] bytesUsed == 0, got %d", - idx, info.BytesUsed) - } - if info.ObjectCount != 0 { - t.Errorf("expected containers[%d] objectCount == 0, got %d", - idx, info.ObjectCount) - } - if info.LastModified.IsZero() { - t.Errorf("containers[%d].LastModified is zero", idx) - } - } -} diff --git a/container_test.go b/container_test.go deleted file mode 100644 index 654ca84..0000000 --- a/container_test.go +++ /dev/null @@ -1,83 +0,0 @@ -/****************************************************************************** -* -* Copyright 2018 Stefan Majewsky -* -* 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" - "testing" -) - -func TestContainerLifecycle(t *testing.T) { - testWithAccount(t, func(a *Account) { - containerName := getRandomName() - c := a.Container(containerName) - - expectString(t, c.Name(), containerName) - if c.Account() != a { - t.Errorf("expected c.Account() = %#v, got %#v instead\n", a, c.Account()) - } - - exists, err := c.Exists() - expectSuccess(t, err) - expectBool(t, exists, false) - - _, 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) - - //DELETE should be idempotent and not return success on non-existence, but - //OpenStack LOVES to be inconsistent with everything (including, notably, itself) - err = c.Delete(nil, nil) - expectError(t, err, "expected 204 response, got 404 instead:

Not Found

The resource could not be found.

") - - err = c.Create(nil, nil) - expectSuccess(t, err) - - exists, err = c.Exists() - expectSuccess(t, err) - expectBool(t, exists, true) - - err = c.Delete(nil, nil) - expectSuccess(t, err) - }) -} - -func TestContainerUpdate(t *testing.T) { - testWithContainer(t, func(c *Container) { - - hdr, err := c.Headers() - expectSuccess(t, err) - expectBool(t, hdr.ObjectCount().Exists(), true) - expectUint64(t, hdr.ObjectCount().Get(), 0) - - hdr = make(ContainerHeaders) - hdr.ObjectCountQuota().Set(23) - hdr.BytesUsedQuota().Set(42) - - err = c.Update(hdr, nil) - expectSuccess(t, err) - - hdr, err = c.Headers() - expectSuccess(t, err) - expectUint64(t, hdr.BytesUsedQuota().Get(), 42) - expectUint64(t, hdr.ObjectCountQuota().Get(), 23) - - }) -} diff --git a/doc.go b/doc.go index bf50a20..37ca4f3 100644 --- a/doc.go +++ b/doc.go @@ -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/field_test.go b/field_test.go deleted file mode 100644 index 539ed74..0000000 --- a/field_test.go +++ /dev/null @@ -1,165 +0,0 @@ -/****************************************************************************** -* -* Copyright 2018 Stefan Majewsky -* -* 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" - "strconv" - "testing" -) - -func TestFieldString(t *testing.T) { - hdr := make(AccountHeaders) - expectBool(t, hdr.TempURLKey().Exists(), false) - expectString(t, hdr.TempURLKey().Get(), "") - expectSuccess(t, hdr.Validate()) - - hdr["X-Account-Meta-Temp-Url-Key"] = "" - expectBool(t, hdr.TempURLKey().Exists(), false) - expectString(t, hdr.TempURLKey().Get(), "") - expectSuccess(t, hdr.Validate()) - - hdr["X-Account-Meta-Temp-Url-Key"] = "foo" - expectBool(t, hdr.TempURLKey().Exists(), true) - expectString(t, hdr.TempURLKey().Get(), "foo") - expectSuccess(t, hdr.Validate()) - - hdr.TempURLKey().Set("bar") - expectHeaders(t, hdr, map[string]string{ - "X-Account-Meta-Temp-Url-Key": "bar", - }) - hdr.TempURLKey().Clear() - expectHeaders(t, hdr, map[string]string{ - "X-Account-Meta-Temp-Url-Key": "", - }) - hdr.TempURLKey().Del() - expectHeaders(t, hdr, nil) - hdr.TempURLKey().Clear() - expectHeaders(t, hdr, map[string]string{ - "X-Account-Meta-Temp-Url-Key": "", - }) -} - -//////////////////////////////////////////////////////////////////////////////// - -func TestFieldTimestamp(t *testing.T) { - testWithAccount(t, func(a *Account) { - hdr, err := a.Headers() - if !expectSuccess(t, err) { - return - } - - expectBool(t, hdr.CreatedAt().Exists(), true) - - actual := float64(hdr.CreatedAt().Get().UnixNano()) / 1e9 - expected, _ := strconv.ParseFloat(hdr["X-Timestamp"], 64) - expectFloat64(t, actual, expected) - }) - - hdr := make(AccountHeaders) - expectBool(t, hdr.CreatedAt().Exists(), false) - expectBool(t, hdr.CreatedAt().Get().IsZero(), true) - expectSuccess(t, hdr.Validate()) - - hdr["X-Timestamp"] = "wtf" - expectBool(t, hdr.CreatedAt().Exists(), true) - expectBool(t, hdr.CreatedAt().Get().IsZero(), true) - expectError(t, hdr.Validate(), `Bad header X-Timestamp: strconv.ParseFloat: parsing "wtf": invalid syntax`) -} - -func TestFieldHTTPTimestamp(t *testing.T) { - testWithContainer(t, func(c *Container) { - obj := c.Object("test") - err := obj.Upload(nil, nil, nil) - if !expectSuccess(t, err) { - return - } - - hdr, err := obj.Headers() - if !expectSuccess(t, err) { - return - } - expectBool(t, hdr.UpdatedAt().Exists(), true) - - actual := hdr.UpdatedAt().Get() - expected, _ := http.ParseTime(hdr.Get("Last-Modified")) - expectInt64(t, actual.Unix(), expected.Unix()) - }) - - hdr := make(ObjectHeaders) - expectBool(t, hdr.UpdatedAt().Exists(), false) - expectBool(t, hdr.UpdatedAt().Get().IsZero(), true) - expectSuccess(t, hdr.Validate()) - - hdr["Last-Modified"] = "wtf" - expectBool(t, hdr.UpdatedAt().Exists(), true) - expectBool(t, hdr.UpdatedAt().Get().IsZero(), true) - expectError(t, hdr.Validate(), `Bad header Last-Modified: parsing time "wtf" as "Mon Jan _2 15:04:05 2006": cannot parse "wtf" as "Mon"`) -} - -//////////////////////////////////////////////////////////////////////////////// - -func TestFieldUint64(t *testing.T) { - hdr := make(AccountHeaders) - expectBool(t, hdr.BytesUsedQuota().Exists(), false) - expectUint64(t, hdr.BytesUsedQuota().Get(), 0) - expectSuccess(t, hdr.Validate()) - - hdr["X-Account-Meta-Quota-Bytes"] = "23" - expectBool(t, hdr.BytesUsedQuota().Exists(), true) - expectUint64(t, hdr.BytesUsedQuota().Get(), 23) - expectSuccess(t, hdr.Validate()) - - hdr["X-Account-Meta-Quota-Bytes"] = "-23" - expectBool(t, hdr.BytesUsedQuota().Exists(), true) - expectUint64(t, hdr.BytesUsedQuota().Get(), 0) - expectError(t, hdr.Validate(), `Bad header X-Account-Meta-Quota-Bytes: strconv.ParseUint: parsing "-23": invalid syntax`) - - hdr.BytesUsedQuota().Set(9001) - expectHeaders(t, hdr, map[string]string{ - "X-Account-Meta-Quota-Bytes": "9001", - }) - hdr.BytesUsedQuota().Clear() - expectHeaders(t, hdr, map[string]string{ - "X-Account-Meta-Quota-Bytes": "", - }) - hdr.BytesUsedQuota().Del() - expectHeaders(t, hdr, nil) - hdr.BytesUsedQuota().Clear() - expectHeaders(t, hdr, map[string]string{ - "X-Account-Meta-Quota-Bytes": "", - }) -} - -func TestFieldUint64Readonly(t *testing.T) { - hdr := make(AccountHeaders) - expectBool(t, hdr.BytesUsed().Exists(), false) - expectUint64(t, hdr.BytesUsed().Get(), 0) - expectSuccess(t, hdr.Validate()) - - hdr["X-Account-Bytes-Used"] = "23" - expectBool(t, hdr.BytesUsed().Exists(), true) - expectUint64(t, hdr.BytesUsed().Get(), 23) - expectSuccess(t, hdr.Validate()) - - hdr["X-Account-Bytes-Used"] = "-23" - expectBool(t, hdr.BytesUsed().Exists(), true) - expectUint64(t, hdr.BytesUsed().Get(), 0) - expectError(t, hdr.Validate(), `Bad header X-Account-Bytes-Used: strconv.ParseUint: parsing "-23": invalid syntax`) -} 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 +* +* 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/headers_test.go b/headers_test.go deleted file mode 100644 index cb99571..0000000 --- a/headers_test.go +++ /dev/null @@ -1,46 +0,0 @@ -/****************************************************************************** -* -* Copyright 2018 Stefan Majewsky -* -* 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" - "testing" -) - -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"}, - })) - - expectSuccess(t, headers.Validate()) - expectUint64(t, headers.BytesUsed().Get(), 1234) - expectUint64(t, headers.ContainerCount().Get(), 23) - expectUint64(t, headers.ObjectCount().Get(), 42) - expectUint64(t, headers.BytesUsedQuota().Get(), 1048576) - - expectString(t, headers.Metadata().Get("foo"), "bar") - expectString(t, headers.Metadata().Get("Foo"), "bar") - expectString(t, headers.Metadata().Get("FOO"), "bar") -} - -//TODO TestParseAccountHeadersError 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 } diff --git a/object.go b/object.go index 9da08c5..ca049f4 100644 --- a/object.go +++ b/object.go @@ -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() diff --git a/object_iterator_test.go b/object_iterator_test.go deleted file mode 100644 index 92fe4b1..0000000 --- a/object_iterator_test.go +++ /dev/null @@ -1,180 +0,0 @@ -/****************************************************************************** -* -* Copyright 2018 Stefan Majewsky -* -* 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 ( - "bytes" - "fmt" - "testing" -) - -var objectExampleContent = []byte(`{"message":"Hello World!"}`) -var objectExampleContentEtag = etagOf(objectExampleContent) - -func TestObjectIterator(t *testing.T) { - testWithContainer(t, func(c *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.ContentType().Set("application/json") - err := c.Object(oname(idx)).Upload(bytes.NewReader(objectExampleContent), hdr, nil) - expectSuccess(t, err) - } - - //test iteration with empty last page - iter := c.Objects() - iter.Prefix = "schwift-test-listing" - os, err := iter.NextPage(2) - expectSuccess(t, err) - expectObjectNames(t, os, oname(1), oname(2)) - os, err = iter.NextPage(2) - expectSuccess(t, err) - expectObjectNames(t, os, oname(3), oname(4)) - os, err = iter.NextPage(2) - expectSuccess(t, err) - expectObjectNames(t, os) - os, err = iter.NextPage(2) - expectSuccess(t, err) - expectObjectNames(t, os) - - //test iteration with partial last page - iter = c.Objects() - iter.Prefix = "schwift-test-listing" - os, err = iter.NextPage(3) - expectSuccess(t, err) - expectObjectNames(t, os, oname(1), oname(2), oname(3)) - os, err = iter.NextPage(3) - expectSuccess(t, err) - expectObjectNames(t, os, oname(4)) - os, err = iter.NextPage(4) - expectSuccess(t, err) - expectObjectNames(t, os) - - //test detailed iteration - iter = c.Objects() - iter.Prefix = "schwift-test-listing" - ois, err := iter.NextPageDetailed(2) - expectSuccess(t, err) - expectObjectInfos(t, ois, oname(1), oname(2)) - ois, err = iter.NextPageDetailed(3) - expectSuccess(t, err) - expectObjectInfos(t, ois, oname(3), oname(4)) - ois, err = iter.NextPageDetailed(3) - expectSuccess(t, err) - expectObjectInfos(t, ois) - ois, err = iter.NextPageDetailed(3) - expectSuccess(t, err) - expectObjectInfos(t, ois) - - //test Foreach - c.Invalidate() - iter = c.Objects() - iter.Prefix = "schwift-test-listing" - idx := 0 - expectSuccess(t, iter.Foreach(func(o *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") - } - - //test ForeachDetailed - c.Invalidate() - iter = c.Objects() - iter.Prefix = "schwift-test-listing" - idx = 0 - expectSuccess(t, iter.ForeachDetailed(func(info 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") - } - - //test Collect - iter = c.Objects() - iter.Prefix = "schwift-test-listing" - os, err = iter.Collect() - expectSuccess(t, err) - expectObjectNames(t, os, oname(1), oname(2), oname(3), oname(4)) - - //test CollectDetailed - iter = c.Objects() - iter.Prefix = "schwift-test-listing" - ois, err = iter.CollectDetailed() - expectSuccess(t, err) - expectObjectInfos(t, ois, oname(1), oname(2), oname(3), oname(4)) - }) -} - -func expectObjectNames(t *testing.T, actualObjects []*Object, expectedNames ...string) { - t.Helper() - if len(actualObjects) != len(expectedNames) { - t.Errorf("expected %d objects, got %d objects", - len(expectedNames), len(actualObjects)) - return - } - for idx, c := range actualObjects { - if c.Name() != expectedNames[idx] { - t.Errorf("expected objects[%d].Name() == %q, got %q", - idx, expectedNames[idx], c.Name()) - } - } -} - -func expectObjectInfos(t *testing.T, actualInfos []ObjectInfo, expectedNames ...string) { - t.Helper() - if len(actualInfos) != len(expectedNames) { - t.Errorf("expected %d objects, got %d objects", - len(expectedNames), len(actualInfos)) - return - } - for idx, info := range actualInfos { - if info.Object.Name() != expectedNames[idx] { - t.Errorf("expected objects[%d].Name() == %q, got %q", - idx, expectedNames[idx], info.Object.Name()) - } - if info.SizeBytes != uint64(len(objectExampleContent)) { - t.Errorf("expected objects[%d] sizeBytes == %d, got %d", - idx, len(objectExampleContent), info.SizeBytes) - } - if info.ContentType != "application/json" { - t.Errorf(`expected objects[%d] contentType == "application/json", got %q`, - idx, info.ContentType) - } - if info.Etag != objectExampleContentEtag { - t.Errorf("expected objects[%d] etag == %q, got %q", - idx, objectExampleContentEtag, info.Etag) - } - if info.LastModified.IsZero() { - t.Errorf("objects[%d].LastModified is zero", idx) - } - } -} diff --git a/object_test.go b/object_test.go deleted file mode 100644 index 73463ad..0000000 --- a/object_test.go +++ /dev/null @@ -1,193 +0,0 @@ -/****************************************************************************** -* -* Copyright 2018 Stefan Majewsky -* -* 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 ( - "bytes" - "io" - "io/ioutil" - "net/http" - "testing" -) - -func TestObjectLifecycle(t *testing.T) { - testWithContainer(t, func(c *Container) { - objectName := getRandomName() - o := c.Object(objectName) - - expectString(t, o.Name(), objectName) - expectString(t, o.FullName(), c.Name()+"/"+objectName) - if o.Container() != c { - t.Errorf("expected o.Container() = %#v, got %#v instead\n", c, o.Container()) - } - - exists, err := o.Exists() - expectSuccess(t, err) - expectBool(t, exists, false) - - _, 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) - - //DELETE should be idempotent and not return success on non-existence, but - //OpenStack LOVES to be inconsistent with everything (including, notably, itself) - err = o.Delete(nil, nil) - expectError(t, err, "expected 204 response, got 404 instead:

Not Found

The resource could not be found.

") - - err = o.Upload(bytes.NewReader([]byte("test")), nil, nil) - expectSuccess(t, err) - - exists, err = o.Exists() - expectSuccess(t, err) - expectBool(t, exists, true) - - err = o.Delete(nil, nil) - expectSuccess(t, err) - }) -} - -func TestObjectUpload(t *testing.T) { - testWithContainer(t, func(c *Container) { - validateUploadedFile := func(obj *Object, expected []byte) { - str, err := obj.Download(nil, nil).AsString() - expectSuccess(t, err) - expectString(t, str, string(expected)) - obj.Invalidate() - hdr, err := obj.Headers() - expectSuccess(t, err) - expectString(t, hdr.Etag().Get(), etagOf(expected)) - } - - //test upload with bytes.Reader - obj := c.Object("upload1") - err := obj.Upload(bytes.NewReader(objectExampleContent), nil, nil) - expectSuccess(t, err) - validateUploadedFile(obj, objectExampleContent) - - //test upload with bytes.Buffer - obj = c.Object("upload2") - err = obj.Upload(bytes.NewBuffer(objectExampleContent), nil, nil) - expectSuccess(t, err) - validateUploadedFile(obj, objectExampleContent) - - //test upload with opaque io.Reader - obj = c.Object("upload3") - err = obj.Upload(opaqueReader{bytes.NewReader(objectExampleContent)}, nil, nil) - expectSuccess(t, err) - validateUploadedFile(obj, objectExampleContent) - - //test upload with io.Writer - obj = c.Object("upload4") - err = obj.UploadWithWriter(nil, nil, func(w io.Writer) error { - _, err := w.Write(objectExampleContent) - return err - }) - expectSuccess(t, err) - validateUploadedFile(obj, objectExampleContent) - - //test upload with empty reader (should create zero-byte-sized object) - obj = c.Object("upload5") - err = obj.Upload(eofReader{}, nil, nil) - expectSuccess(t, err) - validateUploadedFile(obj, nil) - - //test upload without reader (should create zero-byte-sized object) - obj = c.Object("upload6") - err = obj.Upload(nil, nil, nil) - expectSuccess(t, err) - validateUploadedFile(obj, nil) - }) -} - -type eofReader struct{} - -func (r eofReader) Read([]byte) (int, error) { - return 0, io.EOF -} - -type opaqueReader struct { - b *bytes.Reader -} - -func (r opaqueReader) Read(buf []byte) (int, error) { - return r.b.Read(buf) -} - -func TestObjectDownload(t *testing.T) { - testWithContainer(t, func(c *Container) { - //upload example object - obj := c.Object("example") - err := obj.Upload(bytes.NewReader(objectExampleContent), nil, nil) - expectSuccess(t, err) - - //test download as string - str, err := obj.Download(nil, nil).AsString() - expectSuccess(t, err) - expectString(t, str, string(objectExampleContent)) - - //test download as byte slice - buf, err := obj.Download(nil, nil).AsByteSlice() - expectSuccess(t, err) - expectString(t, string(buf), string(objectExampleContent)) - - //test download as io.ReadCloser slice - reader, err := obj.Download(nil, nil).AsReadCloser() - expectSuccess(t, err) - buf = make([]byte, 4) - _, err = reader.Read(buf) - expectSuccess(t, err) - expectString(t, string(buf), string(objectExampleContent[0:4])) - _, err = reader.Read(buf) - expectSuccess(t, err) - expectString(t, string(buf), string(objectExampleContent[4:8])) - buf, err = ioutil.ReadAll(reader) - expectSuccess(t, err) - expectString(t, string(buf), string(objectExampleContent[8:])) - }) -} - -func TestObjectUpdate(t *testing.T) { - testWithContainer(t, func(c *Container) { - obj := c.Object("example") - - //test that metadata update fails for non-existing object - newHeaders := make(ObjectHeaders) - newHeaders.ContentType().Set("application/json") - err := obj.Update(newHeaders, nil) - expectBool(t, Is(err, http.StatusNotFound), true) - expectError(t, err, "expected 202 response, got 404 instead:

Not Found

The resource could not be found.

") - - //create object - err = obj.Upload(nil, nil, nil) - expectSuccess(t, err) - - hdr, err := obj.Headers() - expectSuccess(t, err) - expectString(t, hdr.ContentType().Get(), "application/octet-stream") - - //now the metadata update should work - err = obj.Update(newHeaders, nil) - expectSuccess(t, err) - obj.Invalidate() - hdr, err = obj.Headers() - expectSuccess(t, err) - expectString(t, hdr.ContentType().Get(), "application/json") - }) -} diff --git a/request.go b/request.go index 70c30af..b96670b 100644 --- a/request.go +++ b/request.go @@ -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/shared_test.go b/shared_test.go deleted file mode 100644 index 16113e2..0000000 --- a/shared_test.go +++ /dev/null @@ -1,207 +0,0 @@ -/****************************************************************************** -* -* Copyright 2018 Stefan Majewsky -* -* 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 ( - "crypto/md5" - "crypto/rand" - "encoding/hex" - "math" - "os" - "testing" - - "github.com/gophercloud/gophercloud" - "github.com/gophercloud/gophercloud/openstack" - "github.com/gophercloud/gophercloud/openstack/objectstorage/v1/swauth" -) - -func testWithAccount(t *testing.T, testCode func(a *Account)) { - stAuth := os.Getenv("ST_AUTH") - stUser := os.Getenv("ST_USER") - stKey := os.Getenv("ST_KEY") - var client *gophercloud.ServiceClient - - if stAuth == "" && stUser == "" && stKey == "" { - //option 1: Keystone authentication - authOptions, err := openstack.AuthOptionsFromEnv() - if err != nil { - t.Error("missing Swift credentials (need either ST_AUTH, ST_USER, ST_KEY or OS_* variables)") - t.Error("openstack.AuthOptionsFromEnv returned: " + err.Error()) - return - } - provider, err := openstack.AuthenticatedClient(authOptions) - if err != nil { - t.Errorf("openstack.AuthenticatedClient returned: " + err.Error()) - return - } - client, err = openstack.NewObjectStorageV1(provider, gophercloud.EndpointOpts{}) - if err != nil { - t.Errorf("openstack.NewObjectStorageV1 returned: " + err.Error()) - return - } - } else { - //option 2: Swift authentication v1 - provider, err := openstack.NewClient(stAuth) - if err != nil { - t.Errorf("openstack.NewClient returned: " + err.Error()) - return - } - client, err = swauth.NewObjectStorageV1(provider, swauth.AuthOpts{User: stUser, Key: stKey}) - if err != nil { - t.Errorf("swauth.NewObjectStorageV1 returned: " + err.Error()) - return - } - } - - account, err := AccountFromGophercloud(client) - if err != nil { - t.Errorf("schwift.AccountFromGophercloud returned: " + err.Error()) - return - } - testCode(account) -} - -func testWithContainer(t *testing.T, testCode func(c *Container)) { - testWithAccount(t, func(a *Account) { - containerName := getRandomName() - container, err := a.Container(containerName).EnsureExists() - expectSuccess(t, err) - - testCode(container) - - //cleanup - exists, err := container.Exists() - expectSuccess(t, err) - if exists { - expectSuccess(t, container.Objects().Foreach(func(o *Object) error { - return o.Delete(nil, nil) - })) - err = container.Delete(nil, nil) - expectSuccess(t, err) - } - }) -} - -//////////////////////////////////////////////////////////////////////////////// - -func etagOf(buf []byte) string { - hash := md5.Sum(buf) - return hex.EncodeToString(hash[:]) -} - -func getRandomName() string { - var buf [16]byte - _, err := rand.Read(buf[:]) - if err != nil { - panic(err.Error()) - } - return hex.EncodeToString(buf[:]) -} - -//////////////////////////////////////////////////////////////////////////////// - -func expectBool(t *testing.T, actual, expected bool) { - t.Helper() - if actual != expected { - t.Errorf("expected value %#v, got %#v instead\n", expected, actual) - } -} - -func expectFloat64(t *testing.T, actual, expected float64) { - t.Helper() - if math.Abs((actual-expected)/expected) > 1e-8 { - t.Errorf("expected value %g, got %g instead\n", expected, actual) - } -} - -func expectInt(t *testing.T, actual, expected int) { - t.Helper() - if actual != expected { - t.Errorf("expected value %d, got %d instead\n", expected, actual) - } -} - -func expectInt64(t *testing.T, actual, expected int64) { - t.Helper() - if actual != expected { - t.Errorf("expected value %d, got %d instead\n", expected, actual) - } -} - -func expectUint64(t *testing.T, actual, expected uint64) { - t.Helper() - if actual != expected { - t.Errorf("expected value %d, got %d instead\n", expected, actual) - } -} - -func expectString(t *testing.T, actual, expected string) { - t.Helper() - if actual != expected { - t.Errorf("expected value %q, got %q instead\n", expected, actual) - } -} - -func expectError(t *testing.T, actual error, expected string) (ok bool) { - t.Helper() - if actual == nil { - t.Errorf("expected error %q, got no error\n", expected) - return false - } - if expected != actual.Error() { - t.Errorf("expected error %q, got %q instead\n", expected, actual.Error()) - return false - } - return true -} - -func expectSuccess(t *testing.T, actual error) (ok bool) { - t.Helper() - if actual != nil { - t.Errorf("expected success, got error %q instead\n", actual.Error()) - return false - } - return true -} - -func expectHeaders(t *testing.T, actual map[string]string, expected map[string]string) { - t.Helper() - reported := make(map[string]bool) - - for k, av := range actual { - ev, exists := expected[k] - if !exists { - ev = "" - } - if av != ev { - t.Errorf(`expected "%s: %s", got "%s: %s" instead`, k, ev, k, av) - reported[k] = true - } - } - - for k, ev := range expected { - av, exists := actual[k] - if !exists { - av = "" - } - if av != ev && !reported[k] { - t.Errorf(`expected "%s: %s", got "%s: %s" instead`, k, ev, k, av) - } - } -} diff --git a/tests/account_test.go b/tests/account_test.go new file mode 100644 index 0000000..310d4ce --- /dev/null +++ b/tests/account_test.go @@ -0,0 +1,92 @@ +/****************************************************************************** +* +* Copyright 2018 Stefan Majewsky +* +* 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 ( + "testing" + + "github.com/majewsky/schwift" +) + +func TestAccountBasic(t *testing.T) { + testWithAccount(t, func(a *schwift.Account) { + hdr, err := a.Headers() + if !expectSuccess(t, err) { + t.FailNow() + } + //There are not a lot of things we can test here (besides testing that + //Headers() does not fail, i.e. everything parses correctly), but + //Content-Type is going to be text/plain because GET on an account lists + //the container names as plain text. + expectString(t, hdr.Get("Content-Type"), "text/plain; charset=utf-8") + }) +} + +func TestAccountMetadata(t *testing.T) { + testWithAccount(t, func(a *schwift.Account) { + //test creating some metadata + hdr := make(schwift.AccountHeaders) + hdr.Metadata().Set("schwift-test1", "first") + hdr.Metadata().Set("schwift-test2", "second") + err := a.Update(hdr, nil) + if !expectSuccess(t, err) { + t.FailNow() + } + + hdr, err = a.Headers() + if !expectSuccess(t, err) { + t.FailNow() + } + expectString(t, hdr.Metadata().Get("schwift-test1"), "first") + expectString(t, hdr.Metadata().Get("schwift-test2"), "second") + + //test deleting some metadata + hdr = make(schwift.AccountHeaders) + hdr.Metadata().Clear("schwift-test1") + err = a.Update(hdr, nil) + if !expectSuccess(t, err) { + t.FailNow() + } + + hdr, err = a.Headers() + if !expectSuccess(t, err) { + t.FailNow() + } + expectString(t, hdr.Metadata().Get("schwift-test1"), "") + expectString(t, hdr.Metadata().Get("schwift-test2"), "second") + + //test updating some metadata + hdr = make(schwift.AccountHeaders) + hdr.Metadata().Set("schwift-test1", "will not be set") + hdr.Metadata().Del("schwift-test1") + hdr.Metadata().Set("schwift-test2", "changed") + err = a.Update(hdr, nil) + if !expectSuccess(t, err) { + t.FailNow() + } + + hdr, err = a.Headers() + if !expectSuccess(t, err) { + t.FailNow() + } + expectString(t, hdr.Metadata().Get("schwift-test1"), "") + expectString(t, hdr.Metadata().Get("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 +* +* 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/tests/container_iterator_test.go b/tests/container_iterator_test.go new file mode 100644 index 0000000..b9813db --- /dev/null +++ b/tests/container_iterator_test.go @@ -0,0 +1,187 @@ +/****************************************************************************** +* +* Copyright 2018 Stefan Majewsky +* +* 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 ( + "fmt" + "testing" + + "github.com/majewsky/schwift" +) + +func TestContainerIterator(t *testing.T) { + testWithAccount(t, func(a *schwift.Account) { + cname := func(idx int) string { + return fmt.Sprintf("schwift-test-listing%d", idx) + } + + //create test containers that can be listed + for idx := 1; idx <= 4; idx++ { + _, err := a.Container(cname(idx)).EnsureExists() + expectSuccess(t, err) + } + + //test iteration with empty last page + iter := a.Containers() + iter.Prefix = "schwift-test-listing" + cs, err := iter.NextPage(2) + expectSuccess(t, err) + expectContainerNames(t, cs, cname(1), cname(2)) + cs, err = iter.NextPage(2) + expectSuccess(t, err) + expectContainerNames(t, cs, cname(3), cname(4)) + cs, err = iter.NextPage(2) + expectSuccess(t, err) + expectContainerNames(t, cs) + cs, err = iter.NextPage(2) + expectSuccess(t, err) + expectContainerNames(t, cs) + + //test iteration with partial last page + iter = a.Containers() + iter.Prefix = "schwift-test-listing" + cs, err = iter.NextPage(3) + expectSuccess(t, err) + expectContainerNames(t, cs, cname(1), cname(2), cname(3)) + cs, err = iter.NextPage(3) + expectSuccess(t, err) + expectContainerNames(t, cs, cname(4)) + cs, err = iter.NextPage(4) + expectSuccess(t, err) + expectContainerNames(t, cs) + + //test detailed iteration + iter = a.Containers() + iter.Prefix = "schwift-test-listing" + cis, err := iter.NextPageDetailed(2) + expectSuccess(t, err) + expectContainerInfos(t, cis, cname(1), cname(2)) + cis, err = iter.NextPageDetailed(3) + expectSuccess(t, err) + expectContainerInfos(t, cis, cname(3), cname(4)) + cis, err = iter.NextPageDetailed(3) + expectSuccess(t, err) + expectContainerInfos(t, cis) + cis, err = iter.NextPageDetailed(3) + expectSuccess(t, err) + expectContainerInfos(t, cis) + + //test Foreach + a.Invalidate() + iter = a.Containers() + iter.Prefix = "schwift-test-listing" + idx := 0 + expectSuccess(t, iter.Foreach(func(c *schwift.Container) error { + idx++ + expectString(t, c.Name(), cname(idx)) + return nil + })) + expectInt(t, idx, 4) + expectAccountHeadersCached(t, a) + + //test ForeachDetailed + a.Invalidate() + iter = a.Containers() + iter.Prefix = "schwift-test-listing" + idx = 0 + expectSuccess(t, iter.ForeachDetailed(func(info schwift.ContainerInfo) error { + idx++ + expectString(t, info.Container.Name(), cname(idx)) + return nil + })) + expectInt(t, idx, 4) + expectAccountHeadersCached(t, a) + + //test Collect + iter = a.Containers() + iter.Prefix = "schwift-test-listing" + cs, err = iter.Collect() + expectSuccess(t, err) + expectContainerNames(t, cs, cname(1), cname(2), cname(3), cname(4)) + + //test CollectDetailed + iter = a.Containers() + iter.Prefix = "schwift-test-listing" + cis, err = iter.CollectDetailed() + expectSuccess(t, err) + expectContainerInfos(t, cis, cname(1), cname(2), cname(3), cname(4)) + + //cleanup + iter = a.Containers() + iter.Prefix = "schwift-test-listing" + expectSuccess(t, iter.Foreach(func(c *schwift.Container) error { + return c.Delete(nil, nil) + })) + }) +} + +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", + len(expectedNames), len(actualContainers)) + return + } + for idx, c := range actualContainers { + if c.Name() != expectedNames[idx] { + t.Errorf("expected containers[%d].Name() == %q, got %q", + idx, expectedNames[idx], c.Name()) + } + } +} + +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", + len(expectedNames), len(actualInfos)) + return + } + for idx, info := range actualInfos { + if info.Container.Name() != expectedNames[idx] { + t.Errorf("expected containers[%d].Name() == %q, got %q", + idx, expectedNames[idx], info.Container.Name()) + } + //TODO: upload test object of defined size to the listed containers to + //check if this zero is not just the default value + if info.BytesUsed != 0 { + t.Errorf("expected containers[%d] bytesUsed == 0, got %d", + idx, info.BytesUsed) + } + if info.ObjectCount != 0 { + t.Errorf("expected containers[%d] objectCount == 0, got %d", + idx, info.ObjectCount) + } + if info.LastModified.IsZero() { + t.Errorf("containers[%d].LastModified is zero", idx) + } + } +} diff --git a/tests/container_test.go b/tests/container_test.go new file mode 100644 index 0000000..5a0046b --- /dev/null +++ b/tests/container_test.go @@ -0,0 +1,85 @@ +/****************************************************************************** +* +* Copyright 2018 Stefan Majewsky +* +* 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" + "testing" + + "github.com/majewsky/schwift" +) + +func TestContainerLifecycle(t *testing.T) { + testWithAccount(t, func(a *schwift.Account) { + containerName := getRandomName() + c := a.Container(containerName) + + expectString(t, c.Name(), containerName) + if c.Account() != a { + t.Errorf("expected c.Account() = %#v, got %#v instead\n", a, c.Account()) + } + + exists, err := c.Exists() + expectSuccess(t, err) + expectBool(t, exists, false) + + _, err = c.Headers() + expectError(t, err, "expected 204 response, got 404 instead") + 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) + err = c.Delete(nil, nil) + expectError(t, err, "expected 204 response, got 404 instead:

Not Found

The resource could not be found.

") + + err = c.Create(nil, nil) + expectSuccess(t, err) + + exists, err = c.Exists() + expectSuccess(t, err) + expectBool(t, exists, true) + + err = c.Delete(nil, nil) + expectSuccess(t, err) + }) +} + +func TestContainerUpdate(t *testing.T) { + 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(schwift.ContainerHeaders) + hdr.ObjectCountQuota().Set(23) + hdr.BytesUsedQuota().Set(42) + + err = c.Update(hdr, nil) + expectSuccess(t, err) + + hdr, err = c.Headers() + expectSuccess(t, err) + expectUint64(t, hdr.BytesUsedQuota().Get(), 42) + expectUint64(t, hdr.ObjectCountQuota().Get(), 23) + + }) +} diff --git a/tests/field_test.go b/tests/field_test.go new file mode 100644 index 0000000..f3c03ca --- /dev/null +++ b/tests/field_test.go @@ -0,0 +1,167 @@ +/****************************************************************************** +* +* Copyright 2018 Stefan Majewsky +* +* 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" + "strconv" + "testing" + + "github.com/majewsky/schwift" +) + +func TestFieldString(t *testing.T) { + hdr := make(schwift.AccountHeaders) + expectBool(t, hdr.TempURLKey().Exists(), false) + expectString(t, hdr.TempURLKey().Get(), "") + expectSuccess(t, hdr.Validate()) + + hdr["X-Account-Meta-Temp-Url-Key"] = "" + expectBool(t, hdr.TempURLKey().Exists(), false) + expectString(t, hdr.TempURLKey().Get(), "") + expectSuccess(t, hdr.Validate()) + + hdr["X-Account-Meta-Temp-Url-Key"] = "foo" + expectBool(t, hdr.TempURLKey().Exists(), true) + expectString(t, hdr.TempURLKey().Get(), "foo") + expectSuccess(t, hdr.Validate()) + + hdr.TempURLKey().Set("bar") + expectHeaders(t, hdr, map[string]string{ + "X-Account-Meta-Temp-Url-Key": "bar", + }) + hdr.TempURLKey().Clear() + expectHeaders(t, hdr, map[string]string{ + "X-Account-Meta-Temp-Url-Key": "", + }) + hdr.TempURLKey().Del() + expectHeaders(t, hdr, nil) + hdr.TempURLKey().Clear() + expectHeaders(t, hdr, map[string]string{ + "X-Account-Meta-Temp-Url-Key": "", + }) +} + +//////////////////////////////////////////////////////////////////////////////// + +func TestFieldTimestamp(t *testing.T) { + testWithAccount(t, func(a *schwift.Account) { + hdr, err := a.Headers() + if !expectSuccess(t, err) { + return + } + + expectBool(t, hdr.CreatedAt().Exists(), true) + + actual := float64(hdr.CreatedAt().Get().UnixNano()) / 1e9 + expected, _ := strconv.ParseFloat(hdr["X-Timestamp"], 64) + expectFloat64(t, actual, expected) + }) + + hdr := make(schwift.AccountHeaders) + expectBool(t, hdr.CreatedAt().Exists(), false) + expectBool(t, hdr.CreatedAt().Get().IsZero(), true) + expectSuccess(t, hdr.Validate()) + + hdr["X-Timestamp"] = "wtf" + expectBool(t, hdr.CreatedAt().Exists(), true) + expectBool(t, hdr.CreatedAt().Get().IsZero(), true) + expectError(t, hdr.Validate(), `Bad header X-Timestamp: strconv.ParseFloat: parsing "wtf": invalid syntax`) +} + +func TestFieldHTTPTimestamp(t *testing.T) { + testWithContainer(t, func(c *schwift.Container) { + obj := c.Object("test") + err := obj.Upload(nil, nil, nil) + if !expectSuccess(t, err) { + return + } + + hdr, err := obj.Headers() + if !expectSuccess(t, err) { + return + } + expectBool(t, hdr.UpdatedAt().Exists(), true) + + actual := hdr.UpdatedAt().Get() + expected, _ := http.ParseTime(hdr.Get("Last-Modified")) + expectInt64(t, actual.Unix(), expected.Unix()) + }) + + hdr := make(schwift.ObjectHeaders) + expectBool(t, hdr.UpdatedAt().Exists(), false) + expectBool(t, hdr.UpdatedAt().Get().IsZero(), true) + expectSuccess(t, hdr.Validate()) + + hdr["Last-Modified"] = "wtf" + expectBool(t, hdr.UpdatedAt().Exists(), true) + expectBool(t, hdr.UpdatedAt().Get().IsZero(), true) + expectError(t, hdr.Validate(), `Bad header Last-Modified: parsing time "wtf" as "Mon Jan _2 15:04:05 2006": cannot parse "wtf" as "Mon"`) +} + +//////////////////////////////////////////////////////////////////////////////// + +func TestFieldUint64(t *testing.T) { + hdr := make(schwift.AccountHeaders) + expectBool(t, hdr.BytesUsedQuota().Exists(), false) + expectUint64(t, hdr.BytesUsedQuota().Get(), 0) + expectSuccess(t, hdr.Validate()) + + hdr["X-Account-Meta-Quota-Bytes"] = "23" + expectBool(t, hdr.BytesUsedQuota().Exists(), true) + expectUint64(t, hdr.BytesUsedQuota().Get(), 23) + expectSuccess(t, hdr.Validate()) + + hdr["X-Account-Meta-Quota-Bytes"] = "-23" + expectBool(t, hdr.BytesUsedQuota().Exists(), true) + expectUint64(t, hdr.BytesUsedQuota().Get(), 0) + expectError(t, hdr.Validate(), `Bad header X-Account-Meta-Quota-Bytes: strconv.ParseUint: parsing "-23": invalid syntax`) + + hdr.BytesUsedQuota().Set(9001) + expectHeaders(t, hdr, map[string]string{ + "X-Account-Meta-Quota-Bytes": "9001", + }) + hdr.BytesUsedQuota().Clear() + expectHeaders(t, hdr, map[string]string{ + "X-Account-Meta-Quota-Bytes": "", + }) + hdr.BytesUsedQuota().Del() + expectHeaders(t, hdr, nil) + hdr.BytesUsedQuota().Clear() + expectHeaders(t, hdr, map[string]string{ + "X-Account-Meta-Quota-Bytes": "", + }) +} + +func TestFieldUint64Readonly(t *testing.T) { + hdr := make(schwift.AccountHeaders) + expectBool(t, hdr.BytesUsed().Exists(), false) + expectUint64(t, hdr.BytesUsed().Get(), 0) + expectSuccess(t, hdr.Validate()) + + hdr["X-Account-Bytes-Used"] = "23" + expectBool(t, hdr.BytesUsed().Exists(), true) + expectUint64(t, hdr.BytesUsed().Get(), 23) + expectSuccess(t, hdr.Validate()) + + hdr["X-Account-Bytes-Used"] = "-23" + expectBool(t, hdr.BytesUsed().Exists(), true) + expectUint64(t, hdr.BytesUsed().Get(), 0) + expectError(t, hdr.Validate(), `Bad header X-Account-Bytes-Used: strconv.ParseUint: parsing "-23": invalid syntax`) +} diff --git a/tests/headers_test.go b/tests/headers_test.go new file mode 100644 index 0000000..8067ef4 --- /dev/null +++ b/tests/headers_test.go @@ -0,0 +1,47 @@ +/****************************************************************************** +* +* Copyright 2018 Stefan Majewsky +* +* 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 ( + "testing" + + "github.com/majewsky/schwift" +) + +func TestParseAccountHeadersSuccess(t *testing.T) { + 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) + expectUint64(t, headers.ContainerCount().Get(), 23) + expectUint64(t, headers.ObjectCount().Get(), 42) + expectUint64(t, headers.BytesUsedQuota().Get(), 1048576) + + expectString(t, headers.Metadata().Get("foo"), "bar") + expectString(t, headers.Metadata().Get("Foo"), "bar") + expectString(t, headers.Metadata().Get("FOO"), "bar") +} + +//TODO TestParseAccountHeadersError diff --git a/tests/object_iterator_test.go b/tests/object_iterator_test.go new file mode 100644 index 0000000..5112324 --- /dev/null +++ b/tests/object_iterator_test.go @@ -0,0 +1,188 @@ +/****************************************************************************** +* +* Copyright 2018 Stefan Majewsky +* +* 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 ( + "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 *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(schwift.ObjectHeaders) + hdr.ContentType().Set("application/json") + err := c.Object(oname(idx)).Upload(bytes.NewReader(objectExampleContent), hdr, nil) + expectSuccess(t, err) + } + + //test iteration with empty last page + iter := c.Objects() + iter.Prefix = "schwift-test-listing" + os, err := iter.NextPage(2) + expectSuccess(t, err) + expectObjectNames(t, os, oname(1), oname(2)) + os, err = iter.NextPage(2) + expectSuccess(t, err) + expectObjectNames(t, os, oname(3), oname(4)) + os, err = iter.NextPage(2) + expectSuccess(t, err) + expectObjectNames(t, os) + os, err = iter.NextPage(2) + expectSuccess(t, err) + expectObjectNames(t, os) + + //test iteration with partial last page + iter = c.Objects() + iter.Prefix = "schwift-test-listing" + os, err = iter.NextPage(3) + expectSuccess(t, err) + expectObjectNames(t, os, oname(1), oname(2), oname(3)) + os, err = iter.NextPage(3) + expectSuccess(t, err) + expectObjectNames(t, os, oname(4)) + os, err = iter.NextPage(4) + expectSuccess(t, err) + expectObjectNames(t, os) + + //test detailed iteration + iter = c.Objects() + iter.Prefix = "schwift-test-listing" + ois, err := iter.NextPageDetailed(2) + expectSuccess(t, err) + expectObjectInfos(t, ois, oname(1), oname(2)) + ois, err = iter.NextPageDetailed(3) + expectSuccess(t, err) + expectObjectInfos(t, ois, oname(3), oname(4)) + ois, err = iter.NextPageDetailed(3) + expectSuccess(t, err) + expectObjectInfos(t, ois) + ois, err = iter.NextPageDetailed(3) + expectSuccess(t, err) + expectObjectInfos(t, ois) + + //test Foreach + c.Invalidate() + iter = c.Objects() + iter.Prefix = "schwift-test-listing" + idx := 0 + expectSuccess(t, iter.Foreach(func(o *schwift.Object) error { + idx++ + expectString(t, o.Name(), oname(idx)) + return nil + })) + expectInt(t, idx, 4) + expectContainerHeadersCached(t, c) + + //test ForeachDetailed + c.Invalidate() + iter = c.Objects() + iter.Prefix = "schwift-test-listing" + idx = 0 + expectSuccess(t, iter.ForeachDetailed(func(info schwift.ObjectInfo) error { + idx++ + expectString(t, info.Object.Name(), oname(idx)) + return nil + })) + expectInt(t, idx, 4) + expectContainerHeadersCached(t, c) + + //test Collect + iter = c.Objects() + iter.Prefix = "schwift-test-listing" + os, err = iter.Collect() + expectSuccess(t, err) + expectObjectNames(t, os, oname(1), oname(2), oname(3), oname(4)) + + //test CollectDetailed + iter = c.Objects() + iter.Prefix = "schwift-test-listing" + ois, err = iter.CollectDetailed() + expectSuccess(t, err) + expectObjectInfos(t, ois, oname(1), oname(2), oname(3), oname(4)) + }) +} + +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", + len(expectedNames), len(actualObjects)) + return + } + for idx, c := range actualObjects { + if c.Name() != expectedNames[idx] { + t.Errorf("expected objects[%d].Name() == %q, got %q", + idx, expectedNames[idx], c.Name()) + } + } +} + +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", + len(expectedNames), len(actualInfos)) + return + } + for idx, info := range actualInfos { + if info.Object.Name() != expectedNames[idx] { + t.Errorf("expected objects[%d].Name() == %q, got %q", + idx, expectedNames[idx], info.Object.Name()) + } + if info.SizeBytes != uint64(len(objectExampleContent)) { + t.Errorf("expected objects[%d] sizeBytes == %d, got %d", + idx, len(objectExampleContent), info.SizeBytes) + } + if info.ContentType != "application/json" { + t.Errorf(`expected objects[%d] contentType == "application/json", got %q`, + idx, info.ContentType) + } + if info.Etag != objectExampleContentEtag { + t.Errorf("expected objects[%d] etag == %q, got %q", + idx, objectExampleContentEtag, info.Etag) + } + if info.LastModified.IsZero() { + t.Errorf("objects[%d].LastModified is zero", idx) + } + } +} diff --git a/tests/object_test.go b/tests/object_test.go new file mode 100644 index 0000000..740c3f2 --- /dev/null +++ b/tests/object_test.go @@ -0,0 +1,195 @@ +/****************************************************************************** +* +* Copyright 2018 Stefan Majewsky +* +* 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 ( + "bytes" + "io" + "io/ioutil" + "net/http" + "testing" + + "github.com/majewsky/schwift" +) + +func TestObjectLifecycle(t *testing.T) { + testWithContainer(t, func(c *schwift.Container) { + objectName := getRandomName() + o := c.Object(objectName) + + expectString(t, o.Name(), objectName) + expectString(t, o.FullName(), c.Name()+"/"+objectName) + if o.Container() != c { + t.Errorf("expected o.Container() = %#v, got %#v instead\n", c, o.Container()) + } + + exists, err := o.Exists() + expectSuccess(t, err) + expectBool(t, exists, false) + + _, err = o.Headers() + expectError(t, err, "expected 200 response, got 404 instead") + 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) + err = o.Delete(nil, nil) + expectError(t, err, "expected 204 response, got 404 instead:

Not Found

The resource could not be found.

") + + err = o.Upload(bytes.NewReader([]byte("test")), nil, nil) + expectSuccess(t, err) + + exists, err = o.Exists() + expectSuccess(t, err) + expectBool(t, exists, true) + + err = o.Delete(nil, nil) + expectSuccess(t, err) + }) +} + +func TestObjectUpload(t *testing.T) { + 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)) + obj.Invalidate() + hdr, err := obj.Headers() + expectSuccess(t, err) + expectString(t, hdr.Etag().Get(), etagOf(expected)) + } + + //test upload with bytes.Reader + obj := c.Object("upload1") + err := obj.Upload(bytes.NewReader(objectExampleContent), nil, nil) + expectSuccess(t, err) + validateUploadedFile(obj, objectExampleContent) + + //test upload with bytes.Buffer + obj = c.Object("upload2") + err = obj.Upload(bytes.NewBuffer(objectExampleContent), nil, nil) + expectSuccess(t, err) + validateUploadedFile(obj, objectExampleContent) + + //test upload with opaque io.Reader + obj = c.Object("upload3") + err = obj.Upload(opaqueReader{bytes.NewReader(objectExampleContent)}, nil, nil) + expectSuccess(t, err) + validateUploadedFile(obj, objectExampleContent) + + //test upload with io.Writer + obj = c.Object("upload4") + err = obj.UploadWithWriter(nil, nil, func(w io.Writer) error { + _, err := w.Write(objectExampleContent) + return err + }) + expectSuccess(t, err) + validateUploadedFile(obj, objectExampleContent) + + //test upload with empty reader (should create zero-byte-sized object) + obj = c.Object("upload5") + err = obj.Upload(eofReader{}, nil, nil) + expectSuccess(t, err) + validateUploadedFile(obj, nil) + + //test upload without reader (should create zero-byte-sized object) + obj = c.Object("upload6") + err = obj.Upload(nil, nil, nil) + expectSuccess(t, err) + validateUploadedFile(obj, nil) + }) +} + +type eofReader struct{} + +func (r eofReader) Read([]byte) (int, error) { + return 0, io.EOF +} + +type opaqueReader struct { + b *bytes.Reader +} + +func (r opaqueReader) Read(buf []byte) (int, error) { + return r.b.Read(buf) +} + +func TestObjectDownload(t *testing.T) { + testWithContainer(t, func(c *schwift.Container) { + //upload example object + obj := c.Object("example") + err := obj.Upload(bytes.NewReader(objectExampleContent), nil, nil) + expectSuccess(t, err) + + //test download as string + str, err := obj.Download(nil, nil).AsString() + expectSuccess(t, err) + expectString(t, str, string(objectExampleContent)) + + //test download as byte slice + buf, err := obj.Download(nil, nil).AsByteSlice() + expectSuccess(t, err) + expectString(t, string(buf), string(objectExampleContent)) + + //test download as io.ReadCloser slice + reader, err := obj.Download(nil, nil).AsReadCloser() + expectSuccess(t, err) + buf = make([]byte, 4) + _, err = reader.Read(buf) + expectSuccess(t, err) + expectString(t, string(buf), string(objectExampleContent[0:4])) + _, err = reader.Read(buf) + expectSuccess(t, err) + expectString(t, string(buf), string(objectExampleContent[4:8])) + buf, err = ioutil.ReadAll(reader) + expectSuccess(t, err) + expectString(t, string(buf), string(objectExampleContent[8:])) + }) +} + +func TestObjectUpdate(t *testing.T) { + testWithContainer(t, func(c *schwift.Container) { + obj := c.Object("example") + + //test that metadata update fails for non-existing object + newHeaders := make(schwift.ObjectHeaders) + newHeaders.ContentType().Set("application/json") + err := obj.Update(newHeaders, nil) + expectBool(t, schwift.Is(err, http.StatusNotFound), true) + expectError(t, err, "expected 202 response, got 404 instead:

Not Found

The resource could not be found.

") + + //create object + err = obj.Upload(nil, nil, nil) + expectSuccess(t, err) + + hdr, err := obj.Headers() + expectSuccess(t, err) + expectString(t, hdr.ContentType().Get(), "application/octet-stream") + + //now the metadata update should work + err = obj.Update(newHeaders, nil) + expectSuccess(t, err) + obj.Invalidate() + hdr, err = obj.Headers() + expectSuccess(t, err) + expectString(t, hdr.ContentType().Get(), "application/json") + }) +} diff --git a/tests/shared_test.go b/tests/shared_test.go new file mode 100644 index 0000000..187d0b4 --- /dev/null +++ b/tests/shared_test.go @@ -0,0 +1,216 @@ +/****************************************************************************** +* +* Copyright 2018 Stefan Majewsky +* +* 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 ( + "crypto/md5" + "crypto/rand" + "encoding/hex" + "math" + "os" + "testing" + + "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 *schwift.Account)) { + stAuth := os.Getenv("ST_AUTH") + stUser := os.Getenv("ST_USER") + stKey := os.Getenv("ST_KEY") + var client *gophercloud.ServiceClient + + if stAuth == "" && stUser == "" && stKey == "" { + //option 1: Keystone authentication + authOptions, err := openstack.AuthOptionsFromEnv() + if err != nil { + t.Error("missing Swift credentials (need either ST_AUTH, ST_USER, ST_KEY or OS_* variables)") + t.Error("openstack.AuthOptionsFromEnv returned: " + err.Error()) + return + } + provider, err := openstack.AuthenticatedClient(authOptions) + if err != nil { + t.Errorf("openstack.AuthenticatedClient returned: " + err.Error()) + return + } + client, err = openstack.NewObjectStorageV1(provider, gophercloud.EndpointOpts{}) + if err != nil { + t.Errorf("openstack.NewObjectStorageV1 returned: " + err.Error()) + return + } + } else { + //option 2: Swift authentication v1 + provider, err := openstack.NewClient(stAuth) + if err != nil { + t.Errorf("openstack.NewClient returned: " + err.Error()) + return + } + client, err = swauth.NewObjectStorageV1(provider, swauth.AuthOpts{User: stUser, Key: stKey}) + if err != nil { + t.Errorf("swauth.NewObjectStorageV1 returned: " + err.Error()) + return + } + } + + account, err := gopherschwift.Wrap(client) + if err != nil { + 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 *schwift.Container)) { + testWithAccount(t, func(a *schwift.Account) { + containerName := getRandomName() + container, err := a.Container(containerName).EnsureExists() + expectSuccess(t, err) + + testCode(container) + + //cleanup + exists, err := container.Exists() + expectSuccess(t, err) + if exists { + expectSuccess(t, container.Objects().Foreach(func(o *schwift.Object) error { + return o.Delete(nil, nil) + })) + err = container.Delete(nil, nil) + expectSuccess(t, err) + } + }) +} + +//////////////////////////////////////////////////////////////////////////////// + +func etagOf(buf []byte) string { + hash := md5.Sum(buf) + return hex.EncodeToString(hash[:]) +} + +func getRandomName() string { + var buf [16]byte + _, err := rand.Read(buf[:]) + if err != nil { + panic(err.Error()) + } + return hex.EncodeToString(buf[:]) +} + +//////////////////////////////////////////////////////////////////////////////// + +func expectBool(t *testing.T, actual, expected bool) { + t.Helper() + if actual != expected { + t.Errorf("expected value %#v, got %#v instead\n", expected, actual) + } +} + +func expectFloat64(t *testing.T, actual, expected float64) { + t.Helper() + if math.Abs((actual-expected)/expected) > 1e-8 { + t.Errorf("expected value %g, got %g instead\n", expected, actual) + } +} + +func expectInt(t *testing.T, actual, expected int) { + t.Helper() + if actual != expected { + t.Errorf("expected value %d, got %d instead\n", expected, actual) + } +} + +func expectInt64(t *testing.T, actual, expected int64) { + t.Helper() + if actual != expected { + t.Errorf("expected value %d, got %d instead\n", expected, actual) + } +} + +func expectUint64(t *testing.T, actual, expected uint64) { + t.Helper() + if actual != expected { + t.Errorf("expected value %d, got %d instead\n", expected, actual) + } +} + +func expectString(t *testing.T, actual, expected string) { + t.Helper() + if actual != expected { + t.Errorf("expected value %q, got %q instead\n", expected, actual) + } +} + +func expectError(t *testing.T, actual error, expected string) (ok bool) { + t.Helper() + if actual == nil { + t.Errorf("expected error %q, got no error\n", expected) + return false + } + if expected != actual.Error() { + t.Errorf("expected error %q, got %q instead\n", expected, actual.Error()) + return false + } + return true +} + +func expectSuccess(t *testing.T, actual error) (ok bool) { + t.Helper() + if actual != nil { + t.Errorf("expected success, got error %q instead\n", actual.Error()) + return false + } + return true +} + +func expectHeaders(t *testing.T, actual map[string]string, expected map[string]string) { + t.Helper() + reported := make(map[string]bool) + + for k, av := range actual { + ev, exists := expected[k] + if !exists { + ev = "" + } + if av != ev { + t.Errorf(`expected "%s: %s", got "%s: %s" instead`, k, ev, k, av) + reported[k] = true + } + } + + for k, ev := range expected { + av, exists := actual[k] + if !exists { + av = "" + } + if av != ev && !reported[k] { + t.Errorf(`expected "%s: %s", got "%s: %s" instead`, k, ev, k, av) + } + } +} -- cgit v1.2.3