diff options
| author | Stefan Majewsky <majewsky@gmx.net> | 2018-02-19 21:30:33 +0100 |
|---|---|---|
| committer | Stefan Majewsky <majewsky@gmx.net> | 2018-02-19 21:33:49 +0100 |
| commit | 60d4779889baedc44972d4749daa073efca3b25c (patch) | |
| tree | d47746971f659d6f7e3affe428f239b289954f5b /tests | |
| parent | 8f777460661bbbcbe42730979140f525b382110e (diff) | |
| download | go-schwift-60d4779889baedc44972d4749daa073efca3b25c.tar.gz | |
reorganize code
* Gophercloud dependencies move into subpackage gopherschwift.
* Tests move into subpackage tests (to avoid import cycles).
+ Rename "Client" to "Backend".
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/account_test.go | 92 | ||||
| -rw-r--r-- | tests/backend_test.go | 43 | ||||
| -rw-r--r-- | tests/container_iterator_test.go | 187 | ||||
| -rw-r--r-- | tests/container_test.go | 85 | ||||
| -rw-r--r-- | tests/field_test.go | 167 | ||||
| -rw-r--r-- | tests/headers_test.go | 47 | ||||
| -rw-r--r-- | tests/object_iterator_test.go | 188 | ||||
| -rw-r--r-- | tests/object_test.go | 195 | ||||
| -rw-r--r-- | tests/shared_test.go | 216 |
9 files changed, 1220 insertions, 0 deletions
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 <majewsky@gmx.net> +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +******************************************************************************/ + +package tests + +import ( + "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 <majewsky@gmx.net> +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +******************************************************************************/ + +package tests + +import ( + "net/http" + + "github.com/majewsky/schwift" +) + +type RequestCountingBackend struct { + Inner schwift.Backend + Count int +} + +func (b *RequestCountingBackend) EndpointURL() string { + return b.Inner.EndpointURL() +} + +func (b *RequestCountingBackend) Clone(newEndpointURL string) schwift.Backend { + return &RequestCountingBackend{Inner: b.Inner.Clone(newEndpointURL)} +} + +func (b *RequestCountingBackend) Do(req *http.Request) (*http.Response, error) { + b.Count++ + return b.Inner.Do(req) +} diff --git a/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 <majewsky@gmx.net> +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +******************************************************************************/ + +package tests + +import ( + "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 <majewsky@gmx.net> +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +******************************************************************************/ + +package tests + +import ( + "net/http" + "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: <html><h1>Not Found</h1><p>The resource could not be found.</p></html>") + + 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 <majewsky@gmx.net> +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +******************************************************************************/ + +package tests + +import ( + "net/http" + "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 <majewsky@gmx.net> +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +******************************************************************************/ + +package tests + +import ( + "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 <majewsky@gmx.net> +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +******************************************************************************/ + +package tests + +import ( + "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 <majewsky@gmx.net> +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +******************************************************************************/ + +package tests + +import ( + "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: <html><h1>Not Found</h1><p>The resource could not be found.</p></html>") + + 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: <html><h1>Not Found</h1><p>The resource could not be found.</p></html>") + + //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 <majewsky@gmx.net> +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +******************************************************************************/ + +package tests + +import ( + "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 = "<not set>" + } + 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 = "<not set>" + } + if av != ev && !reported[k] { + t.Errorf(`expected "%s: %s", got "%s: %s" instead`, k, ev, k, av) + } + } +} |
