Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NetStorage client with ListStorageGroups and GetStorageGroup #215

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions pkg/netstorage/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package netstorage

import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"

"github.com/akamai/AkamaiOPEN-edgegrid-golang/v9/pkg/errs"
)

type (
InnerError struct {
Type string `json:"type"`
Title string `json:"title,omitempty"`
Detail string `json:"detail"`
ProblemID string `json:"problemId,omitempty"`
}

// Error is a netstorage error interface
Error struct {
Type string `json:"type"`
Title string `json:"title,omitempty"`
Instance string `json:"instance,omitempty"`
Status int `json:"status,omitempty"`
Detail string `json:"detail"`
ProblemID string `json:"problemId,omitempty"`
Errors []*InnerError `json:"errors,omitempty"`
}
)

// Error parses an error from the response
func (p *netstorage) Error(r *http.Response) error {
var e Error

var body []byte

body, err := ioutil.ReadAll(r.Body)
if err != nil {
p.Log(r.Request.Context()).Errorf("reading error response body: %s", err)
e.Status = r.StatusCode
e.Title = "Failed to read error body"
e.Detail = err.Error()
return &e
}

if err := json.Unmarshal(body, &e); err != nil {
p.Log(r.Request.Context()).Errorf("could not unmarshal API error: %s", err)
e.Title = "Failed to unmarshal error body. NetStorage API failed. Check details for more information."
e.Detail = errs.UnescapeContent(string(body))
}

e.Status = r.StatusCode

return &e
}

func (e *Error) Error() string {
msg, err := json.MarshalIndent(e, "", "\t")
if err != nil {
return fmt.Sprintf("error marshaling API error: %s", err)
}
return fmt.Sprintf("API error: \n%s", msg)
}

// Is handles error comparisons
func (e *Error) Is(target error) bool {
if errors.Is(target, ErrNotFound) {
return e.isErrNotFound()
}

var t *Error
if !errors.As(target, &t) {
return false
}

if e == t {
return true
}

if e.Status != t.Status {
return false
}

return e.Error() == t.Error()
}

func (e *Error) isErrNotFound() bool {
if e.Status == http.StatusBadRequest {
for _, innerErr := range e.Errors {
if innerErr.Detail == "Unable to find the given storage group." {
return true
}
}
}
return false
}
131 changes: 131 additions & 0 deletions pkg/netstorage/errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package netstorage

import (
"context"
"io/ioutil"
"net/http"
"strings"
"testing"

"github.com/akamai/AkamaiOPEN-edgegrid-golang/v9/pkg/session"
"github.com/stretchr/testify/require"
"github.com/tj/assert"
)

func TestNewError(t *testing.T) {
sess, err := session.New()
require.NoError(t, err)

req, err := http.NewRequestWithContext(
context.TODO(),
http.MethodHead,
"/",
nil)
require.NoError(t, err)

tests := map[string]struct {
response *http.Response
expected *Error
}{
"valid response, status code 500": {
response: &http.Response{
Status: "Internal Server Error",
StatusCode: http.StatusInternalServerError,
Body: ioutil.NopCloser(strings.NewReader(
`{"type":"a","title":"b","detail":"c"}`),
),
Request: req,
},
expected: &Error{
Type: "a",
Title: "b",
Detail: "c",
Status: http.StatusInternalServerError,
},
},
"invalid response body, assign status code": {
response: &http.Response{
Status: "Internal Server Error",
StatusCode: http.StatusInternalServerError,
Body: ioutil.NopCloser(strings.NewReader(
`test`),
),
Request: req,
},
expected: &Error{
Title: "Failed to unmarshal error body. NetStorage API failed. Check details for more information.",
Detail: "test",
Status: http.StatusInternalServerError,
},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
res := Client(sess).(*netstorage).Error(test.response)
assert.Equal(t, test.expected, res)
})
}
}

func TestJsonErrorUnmarshalling(t *testing.T) {
req, err := http.NewRequestWithContext(
context.TODO(),
http.MethodHead,
"/",
nil)
require.NoError(t, err)
tests := map[string]struct {
input *http.Response
expected *Error
}{
"API failure with HTML response": {
input: &http.Response{
Request: req,
Status: "OK",
StatusCode: http.StatusServiceUnavailable,
Body: ioutil.NopCloser(strings.NewReader(`<HTML><HEAD>...</HEAD><BODY>...</BODY></HTML>`))},
expected: &Error{
Type: "",
Title: "Failed to unmarshal error body. NetStorage API failed. Check details for more information.",
Detail: "<HTML><HEAD>...</HEAD><BODY>...</BODY></HTML>",
Status: http.StatusServiceUnavailable,
},
},
"API failure with plain text response": {
input: &http.Response{
Request: req,
Status: "OK",
StatusCode: http.StatusServiceUnavailable,
Body: ioutil.NopCloser(strings.NewReader("Your request did not succeed as this operation has reached the limit for your account. Please try after 2024-01-16T15:20:55.945Z"))},
expected: &Error{
Type: "",
Status: http.StatusServiceUnavailable,
Title: "Failed to unmarshal error body. NetStorage API failed. Check details for more information.",
Detail: "Your request did not succeed as this operation has reached the limit for your account. Please try after 2024-01-16T15:20:55.945Z",
},
},
"API failure with XML response": {
input: &http.Response{
Request: req,
Status: "OK",
StatusCode: http.StatusServiceUnavailable,
Body: ioutil.NopCloser(strings.NewReader(`<Root><Item id="1" name="Example" /></Root>`))},
expected: &Error{
Type: "",
Status: http.StatusServiceUnavailable,
Title: "Failed to unmarshal error body. NetStorage API failed. Check details for more information.",
Detail: "<Root><Item id=\"1\" name=\"Example\" /></Root>",
},
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
sess, _ := session.New()
p := netstorage{
Session: sess,
}
assert.Equal(t, test.expected, p.Error(test.input))
})
}
}
62 changes: 62 additions & 0 deletions pkg/netstorage/netstorage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Package netstorage provides access to the Akamai Net Storage APIs
package netstorage

import (
"context"
"errors"
"net/http"

"github.com/akamai/AkamaiOPEN-edgegrid-golang/v9/pkg/session"
)

var (
// ErrStructValidation is returned when given struct validation failed
ErrStructValidation = errors.New("struct validation")

// ErrNotFound is returned when requested resource was not found
ErrNotFound = errors.New("resource not found")
)

type (
// NetStorage is the Net Storage api interface
NetStorage interface {
// Groups

// ListStorageGroups Get a list of all of the storage groups in your NetStorage instance.
//
// See https://techdocs.akamai.com/netstorage/reference/get-storage-groups
ListStorageGroups(ctx context.Context, params ListStorageGroupsRequest) (*ListStorageGroupsResponse, error)

// GetStorageGroup Get a storage group in your NetStorage instance.
//
// See https://techdocs.akamai.com/netstorage/reference/get-storage-group
GetStorageGroup(ctx context.Context, params GetStorageGroupRequest) (*GetStorageGroupResponse, error)
}

netstorage struct {
session.Session
}

// Option defines a NetStorage option
Option func(*netstorage)

// ClientFunc is a netstorage client new method, this can used for mocking
ClientFunc func(sess session.Session, opts ...Option) NetStorage
)

// Client returns a new netstorage Client instance with the specified controller
func Client(sess session.Session, opts ...Option) NetStorage {
p := &netstorage{
Session: sess,
}

for _, opt := range opts {
opt(p)
}
return p
}

// Exec overrides the session.Exec to add netstorage options
func (p *netstorage) Exec(r *http.Request, out interface{}, in ...interface{}) (*http.Response, error) {
return p.Session.Exec(r, out, in...)
}
32 changes: 32 additions & 0 deletions pkg/netstorage/netstorage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package netstorage

import (
"crypto/tls"
"crypto/x509"
"net/http"
"net/http/httptest"
"net/url"
"testing"

"github.com/akamai/AkamaiOPEN-edgegrid-golang/v9/pkg/edgegrid"
"github.com/akamai/AkamaiOPEN-edgegrid-golang/v9/pkg/session"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func mockAPIClient(t *testing.T, mockServer *httptest.Server) NetStorage {
serverURL, err := url.Parse(mockServer.URL)
require.NoError(t, err)
certPool := x509.NewCertPool()
certPool.AddCert(mockServer.Certificate())
httpClient := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: certPool,
},
},
}
s, err := session.New(session.WithClient(httpClient), session.WithSigner(&edgegrid.Config{Host: serverURL.Host}))
assert.NoError(t, err)
return Client(s)
}
Loading