text:
package godo

import (
	"encoding/json"
	"fmt"
	"net/http"
	"reflect"
	"testing"
)

func TestStoragesActions_Attach(t *testing.T) {
	setup()
	defer teardown()
	const (
		volumeID  = "98d414c6-295e-4e3a-ac58-eb9456c1e1d1"
		dropletID = 12345
	)

	attachRequest := &ActionRequest{
		"type":       "attach",
		"droplet_id": float64(dropletID), // encoding/json decodes numbers as floats
	}

	mux.HandleFunc("/v2/volumes/"+volumeID+"/actions", func(w http.ResponseWriter, r *http.Request) {
		v := new(ActionRequest)
		err := json.NewDecoder(r.Body).Decode(v)
		if err != nil {
			t.Fatalf("decode json: %v", err)
		}

		testMethod(t, r, http.MethodPost)
		if !reflect.DeepEqual(v, attachRequest) {
			t.Errorf("want=%#v", attachRequest)
			t.Errorf("got=%#v", v)
		}
		fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
	})

	_, _, err := client.StorageActions.Attach(ctx, volumeID, dropletID)
	if err != nil {
		t.Errorf("StoragesActions.Attach returned error: %v", err)
	}
}

func TestStoragesActions_DetachByDropletID(t *testing.T) {
	setup()
	defer teardown()
	volumeID := "98d414c6-295e-4e3a-ac58-eb9456c1e1d1"
	dropletID := 123456

	detachByDropletIDRequest := &ActionRequest{
		"type":       "detach",
		"droplet_id": float64(dropletID), // encoding/json decodes numbers as floats
	}

	mux.HandleFunc("/v2/volumes/"+volumeID+"/actions", func(w http.ResponseWriter, r *http.Request) {
		v := new(ActionRequest)
		err := json.NewDecoder(r.Body).Decode(v)
		if err != nil {
			t.Fatalf("decode json: %v", err)
		}

		testMethod(t, r, http.MethodPost)
		if !reflect.DeepEqual(v, detachByDropletIDRequest) {
			t.Errorf("want=%#v", detachByDropletIDRequest)
			t.Errorf("got=%#v", v)
		}
		fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
	})

	_, _, err := client.StorageActions.DetachByDropletID(ctx, volumeID, dropletID)
	if err != nil {
		t.Errorf("StoragesActions.DetachByDropletID returned error: %v", err)
	}
}

func TestStorageActions_Get(t *testing.T) {
	setup()
	defer teardown()
	volumeID := "98d414c6-295e-4e3a-ac58-eb9456c1e1d1"

	mux.HandleFunc("/v2/volumes/"+volumeID+"/actions/456", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
	})

	action, _, err := client.StorageActions.Get(ctx, volumeID, 456)
	if err != nil {
		t.Errorf("StorageActions.Get returned error: %v", err)
	}

	expected := &Action{Status: "in-progress"}
	if !reflect.DeepEqual(action, expected) {
		t.Errorf("StorageActions.Get returned %+v, expected %+v", action, expected)
	}
}

func TestStorageActions_List(t *testing.T) {
	setup()
	defer teardown()
	volumeID := "98d414c6-295e-4e3a-ac58-eb9456c1e1d1"

	mux.HandleFunc("/v2/volumes/"+volumeID+"/actions", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprintf(w, `{"actions":[{"status":"in-progress"}]}`)
	})

	actions, _, err := client.StorageActions.List(ctx, volumeID, nil)
	if err != nil {
		t.Errorf("StorageActions.List returned error: %v", err)
	}

	expected := []Action{{Status: "in-progress"}}
	if !reflect.DeepEqual(actions, expected) {
		t.Errorf("StorageActions.List returned %+v, expected %+v", actions, expected)
	}
}

func TestStoragesActions_Resize(t *testing.T) {
	setup()
	defer teardown()
	volumeID := "98d414c6-295e-4e3a-ac58-eb9456c1e1d1"

	resizeRequest := &ActionRequest{
		"type":           "resize",
		"size_gigabytes": float64(500),
		"region":         "nyc1",
	}

	mux.HandleFunc("/v2/volumes/"+volumeID+"/actions", func(w http.ResponseWriter, r *http.Request) {
		v := new(ActionRequest)
		err := json.NewDecoder(r.Body).Decode(v)
		if err != nil {
			t.Fatalf("decode json: %v", err)
		}

		testMethod(t, r, http.MethodPost)
		if !reflect.DeepEqual(v, resizeRequest) {
			t.Errorf("want=%#v", resizeRequest)
			t.Errorf("got=%#v", v)
		}
		fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
	})

	_, _, err := client.StorageActions.Resize(ctx, volumeID, 500, "nyc1")
	if err != nil {
		t.Errorf("StoragesActions.Resize returned error: %v", err)
	}
}


text:
package godo

import (
	"fmt"
	"net/http"

	"github.com/digitalocean/godo/context"
)

const snapshotBasePath = "v2/snapshots"

// SnapshotsService is an interface for interfacing with the snapshots
// endpoints of the DigitalOcean API
// See: https://developers.digitalocean.com/documentation/v2#snapshots
type SnapshotsService interface {
	List(context.Context, *ListOptions) ([]Snapshot, *Response, error)
	ListVolume(context.Context, *ListOptions) ([]Snapshot, *Response, error)
	ListDroplet(context.Context, *ListOptions) ([]Snapshot, *Response, error)
	Get(context.Context, string) (*Snapshot, *Response, error)
	Delete(context.Context, string) (*Response, error)
}

// SnapshotsServiceOp handles communication with the snapshot related methods of the
// DigitalOcean API.
type SnapshotsServiceOp struct {
	client *Client
}

var _ SnapshotsService = &SnapshotsServiceOp{}

// Snapshot represents a DigitalOcean Snapshot
type Snapshot struct {
	ID            string   `json:"id,omitempty"`
	Name          string   `json:"name,omitempty"`
	ResourceID    string   `json:"resource_id,omitempty"`
	ResourceType  string   `json:"resource_type,omitempty"`
	Regions       []string `json:"regions,omitempty"`
	MinDiskSize   int      `json:"min_disk_size,omitempty"`
	SizeGigaBytes float64  `json:"size_gigabytes,omitempty"`
	Created       string   `json:"created_at,omitempty"`
}

type snapshotRoot struct {
	Snapshot *Snapshot `json:"snapshot"`
}

type snapshotsRoot struct {
	Snapshots []Snapshot `json:"snapshots"`
	Links     *Links     `json:"links,omitempty"`
}

type listSnapshotOptions struct {
	ResourceType string `url:"resource_type,omitempty"`
}

func (s Snapshot) String() string {
	return Stringify(s)
}

// List lists all the snapshots available.
func (s *SnapshotsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Snapshot, *Response, error) {
	return s.list(ctx, opt, nil)
}

// ListDroplet lists all the Droplet snapshots.
func (s *SnapshotsServiceOp) ListDroplet(ctx context.Context, opt *ListOptions) ([]Snapshot, *Response, error) {
	listOpt := listSnapshotOptions{ResourceType: "droplet"}
	return s.list(ctx, opt, &listOpt)
}

// ListVolume lists all the volume snapshots.
func (s *SnapshotsServiceOp) ListVolume(ctx context.Context, opt *ListOptions) ([]Snapshot, *Response, error) {
	listOpt := listSnapshotOptions{ResourceType: "volume"}
	return s.list(ctx, opt, &listOpt)
}

// Get retrieves an snapshot by id.
func (s *SnapshotsServiceOp) Get(ctx context.Context, snapshotID string) (*Snapshot, *Response, error) {
	return s.get(ctx, snapshotID)
}

// Delete an snapshot.
func (s *SnapshotsServiceOp) Delete(ctx context.Context, snapshotID string) (*Response, error) {
	path := fmt.Sprintf("%s/%s", snapshotBasePath, snapshotID)

	req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
	if err != nil {
		return nil, err
	}

	resp, err := s.client.Do(ctx, req, nil)

	return resp, err
}

// Helper method for getting an individual snapshot
func (s *SnapshotsServiceOp) get(ctx context.Context, ID string) (*Snapshot, *Response, error) {
	path := fmt.Sprintf("%s/%s", snapshotBasePath, ID)

	req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
	if err != nil {
		return nil, nil, err
	}

	root := new(snapshotRoot)
	resp, err := s.client.Do(ctx, req, root)
	if err != nil {
		return nil, resp, err
	}

	return root.Snapshot, resp, err
}

// Helper method for listing snapshots
func (s *SnapshotsServiceOp) list(ctx context.Context, opt *ListOptions, listOpt *listSnapshotOptions) ([]Snapshot, *Response, error) {
	path := snapshotBasePath
	path, err := addOptions(path, opt)
	if err != nil {
		return nil, nil, err
	}
	path, err = addOptions(path, listOpt)
	if err != nil {
		return nil, nil, err
	}

	req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
	if err != nil {
		return nil, nil, err
	}

	root := new(snapshotsRoot)
	resp, err := s.client.Do(ctx, req, root)
	if err != nil {
		return nil, resp, err
	}
	if l := root.Links; l != nil {
		resp.Links = l
	}

	return root.Snapshots, resp, err
}

repo_name: dan-v/dosxvpn

repo_language: Go

mime_type: text/x-shellscript


text:
package godo

import (
	"encoding/json"
	"testing"
)

var (
	firstPageLinksJSONBlob = []byte(`{
		"links": {
			"pages": {
				"last": "https://api.digitalocean.com/v2/droplets/?page=3",
				"next": "https://api.digitalocean.com/v2/droplets/?page=2"
			}
		}
	}`)
	otherPageLinksJSONBlob = []byte(`{
		"links": {
			"pages": {
				"first": "https://api.digitalocean.com/v2/droplets/?page=1",
				"prev": "https://api.digitalocean.com/v2/droplets/?page=1",
				"last": "https://api.digitalocean.com/v2/droplets/?page=3",
				"next": "https://api.digitalocean.com/v2/droplets/?page=3"
			}
		}
	}`)
	lastPageLinksJSONBlob = []byte(`{
		"links": {
			"pages": {
				"first": "https://api.digitalocean.com/v2/droplets/?page=1",
				"prev": "https://api.digitalocean.com/v2/droplets/?page=2"
			}
		}
	}`)

	missingLinksJSONBlob = []byte(`{ }`)
)

type godoList struct {
	Links Links `json:"links"`
}

func loadLinksJSON(t *testing.T, j []byte) Links {
	var list godoList
	err := json.Unmarshal(j, &list)
	if err != nil {
		t.Fatal(err)
	}

	return list.Links
}

func TestLinks_ParseFirst(t *testing.T) {
	links := loadLinksJSON(t, firstPageLinksJSONBlob)
	_, err := links.CurrentPage()
	if err != nil {
		t.Fatal(err)
	}

	r := &Response{Links: &links}
	checkCurrentPage(t, r, 1)

	if links.IsLastPage() {
		t.Fatalf("shouldn't be last page")
	}
}

func TestLinks_ParseMiddle(t *testing.T) {
	links := loadLinksJSON(t, otherPageLinksJSONBlob)
	_, err := links.CurrentPage()
	if err != nil {
		t.Fatal(err)
	}

	r := &Response{Links: &links}
	checkCurrentPage(t, r, 2)

	if links.IsLastPage() {
		t.Fatalf("shouldn't be last page")
	}
}

func TestLinks_ParseLast(t *testing.T) {
	links := loadLinksJSON(t, lastPageLinksJSONBlob)
	_, err := links.CurrentPage()
	if err != nil {
		t.Fatal(err)
	}

	r := &Response{Links: &links}
	checkCurrentPage(t, r, 3)
	if !links.IsLastPage() {
		t.Fatalf("expected last page")
	}
}

func TestLinks_ParseMissing(t *testing.T) {
	links := loadLinksJSON(t, missingLinksJSONBlob)
	_, err := links.CurrentPage()
	if err != nil {
		t.Fatal(err)
	}

	r := &Response{Links: &links}
	checkCurrentPage(t, r, 1)
}

func TestLinks_ParseURL(t *testing.T) {
	type linkTest struct {
		name, url string
		expected  int
	}

	linkTests := []linkTest{
		{
			name:     "prev",
			url:      "https://api.digitalocean.com/v2/droplets/?page=1",
			expected: 1,
		},
		{
			name:     "last",
			url:      "https://api.digitalocean.com/v2/droplets/?page=5",
			expected: 5,
		},
		{
			name:     "nexta",
			url:      "https://api.digitalocean.com/v2/droplets/?page=2",
			expected: 2,
		},
	}

	for _, lT := range linkTests {
		p, err := pageForURL(lT.url)
		if err != nil {
			t.Fatal(err)
		}

		if p != lT.expected {
			t.Errorf("expected page for '%s' to be '%d', was '%d'",
				lT.url, lT.expected, p)
		}
	}

}

func TestLinks_ParseEmptyString(t *testing.T) {
	type linkTest struct {
		name, url string
		expected  int
	}

	linkTests := []linkTest{
		{
			name:     "none",
			url:      "http://example.com",
			expected: 0,
		},
		{
			name:     "bad",
			url:      "no url",
			expected: 0,
		},
		{
			name:     "empty",
			url:      "",
			expected: 0,
		},
	}

	for _, lT := range linkTests {
		_, err := pageForURL(lT.url)
		if err == nil {
			t.Fatalf("expected error for test '%s', but received none", lT.name)
		}
	}
}


text:
package godo

import (
	"encoding/json"
	"fmt"
	"testing"
	"time"
)

const (
	emptyTimeStr         = `"0001-01-01T00:00:00Z"`
	referenceTimeStr     = `"2006-01-02T15:04:05Z"`
	referenceUnixTimeStr = `1136214245`
)

var (
	referenceTime = time.Date(2006, 01, 02, 15, 04, 05, 0, time.UTC)
	unixOrigin    = time.Unix(0, 0).In(time.UTC)
)

func TestTimestamp_Marshal(t *testing.T) {
	testCases := []struct {
		desc    string
		data    Timestamp
		want    string
		wantErr bool
		equal   bool
	}{
		{"Reference", Timestamp{referenceTime}, referenceTimeStr, false, true},
		{"Empty", Timestamp{}, emptyTimeStr, false, true},
		{"Mismatch", Timestamp{}, referenceTimeStr, false, false},
	}
	for _, tc := range testCases {
		out, err := json.Marshal(tc.data)
		if gotErr := (err != nil); gotErr != tc.wantErr {
			t.Errorf("%s: gotErr=%v, wantErr=%v, err=%v", tc.desc, gotErr, tc.wantErr, err)
		}
		got := string(out)
		equal := got == tc.want
		if (got == tc.want) != tc.equal {
			t.Errorf("%s: got=%s, want=%s, equal=%v, want=%v", tc.desc, got, tc.want, equal, tc.equal)
		}
	}
}

func TestTimestamp_Unmarshal(t *testing.T) {
	testCases := []struct {
		desc    string
		data    string
		want    Timestamp
		wantErr bool
		equal   bool
	}{
		{"Reference", referenceTimeStr, Timestamp{referenceTime}, false, true},
		{"ReferenceUnix", `1136214245`, Timestamp{referenceTime}, false, true},
		{"Empty", emptyTimeStr, Timestamp{}, false, true},
		{"UnixStart", `0`, Timestamp{unixOrigin}, false, true},
		{"Mismatch", referenceTimeStr, Timestamp{}, false, false},
		{"MismatchUnix", `0`, Timestamp{}, false, false},
		{"Invalid", `"asdf"`, Timestamp{referenceTime}, true, false},
	}
	for _, tc := range testCases {
		var got Timestamp
		err := json.Unmarshal([]byte(tc.data), &got)
		if gotErr := err != nil; gotErr != tc.wantErr {
			t.Errorf("%s: gotErr=%v, wantErr=%v, err=%v", tc.desc, gotErr, tc.wantErr, err)
			continue
		}
		equal := got.Equal(tc.want)
		if equal != tc.equal {
			t.Errorf("%s: got=%#v, want=%#v, equal=%v, want=%v", tc.desc, got, tc.want, equal, tc.equal)
		}
	}
}

func TestTimstamp_MarshalReflexivity(t *testing.T) {
	testCases := []struct {
		desc string
		data Timestamp
	}{
		{"Reference", Timestamp{referenceTime}},
		{"Empty", Timestamp{}},
	}
	for _, tc := range testCases {
		data, err := json.Marshal(tc.data)
		if err != nil {
			t.Errorf("%s: Marshal err=%v", tc.desc, err)
		}
		var got Timestamp
		err = json.Unmarshal(data, &got)
		if err != nil {
			t.Errorf("%s: Unmarshal err=%v", data, err)
		}
		if !got.Equal(tc.data) {
			t.Errorf("%s: %+v != %+v", tc.desc, got, data)
		}
	}
}

type WrappedTimestamp struct {
	A    int
	Time Timestamp
}

func TestWrappedTimstamp_Marshal(t *testing.T) {
	testCases := []struct {
		desc    string
		data    WrappedTimestamp
		want    string
		wantErr bool
		equal   bool
	}{
		{"Reference", WrappedTimestamp{0, Timestamp{referenceTime}}, fmt.Sprintf(`{"A":0,"Time":%s}`, referenceTimeStr), false, true},
		{"Empty", WrappedTimestamp{}, fmt.Sprintf(`{"A":0,"Time":%s}`, emptyTimeStr), false, true},
		{"Mismatch", WrappedTimestamp{}, fmt.Sprintf(`{"A":0,"Time":%s}`, referenceTimeStr), false, false},
	}
	for _, tc := range testCases {
		out, err := json.Marshal(tc.data)
		if gotErr := err != nil; gotErr != tc.wantErr {
			t.Errorf("%s: gotErr=%v, wantErr=%v, err=%v", tc.desc, gotErr, tc.wantErr, err)
		}
		got := string(out)
		equal := got == tc.want
		if equal != tc.equal {
			t.Errorf("%s: got=%s, want=%s, equal=%v, want=%v", tc.desc, got, tc.want, equal, tc.equal)
		}
	}
}

func TestWrappedTimestamp_Unmarshal(t *testing.T) {
	testCases := []struct {
		desc    string
		data    string
		want    WrappedTimestamp
		wantErr bool
		equal   bool
	}{
		{"Reference", referenceTimeStr, WrappedTimestamp{0, Timestamp{referenceTime}}, false, true},
		{"ReferenceUnix", referenceUnixTimeStr, WrappedTimestamp{0, Timestamp{referenceTime}}, false, true},
		{"Empty", emptyTimeStr, WrappedTimestamp{0, Timestamp{}}, false, true},
		{"UnixStart", `0`, WrappedTimestamp{0, Timestamp{unixOrigin}}, false, true},
		{"Mismatch", referenceTimeStr, WrappedTimestamp{0, Timestamp{}}, false, false},
		{"MismatchUnix", `0`, WrappedTimestamp{0, Timestamp{}}, false, false},
		{"Invalid", `"asdf"`, WrappedTimestamp{0, Timestamp{referenceTime}}, true, false},
	}
	for _, tc := range testCases {
		var got Timestamp
		err := json.Unmarshal([]byte(tc.data), &got)
		if gotErr := err != nil; gotErr != tc.wantErr {
			t.Errorf("%s: gotErr=%v, wantErr=%v, err=%v", tc.desc, gotErr, tc.wantErr, err)
			continue
		}
		equal := got.Time.Equal(tc.want.Time.Time)
		if equal != tc.equal {
			t.Errorf("%s: got=%#v, want=%#v, equal=%v, want=%v", tc.desc, got, tc.want, equal, tc.equal)
		}
	}
}

func TestWrappedTimestamp_MarshalReflexivity(t *testing.T) {
	testCases := []struct {
		desc string
		data WrappedTimestamp
	}{
		{"Reference", WrappedTimestamp{0, Timestamp{referenceTime}}},
		{"Empty", WrappedTimestamp{0, Timestamp{}}},
	}
	for _, tc := range testCases {
		bytes, err := json.Marshal(tc.data)
		if err != nil {
			t.Errorf("%s: Marshal err=%v", tc.desc, err)
		}
		var got WrappedTimestamp
		err = json.Unmarshal(bytes, &got)
		if err != nil {
			t.Errorf("%s: Unmarshal err=%v", bytes, err)
		}
		if !got.Time.Equal(tc.data.Time) {
			t.Errorf("%s: %+v != %+v", tc.desc, got, tc.data)
		}
	}
}


text:
package godo

import (
	"fmt"
	"net/http"
	"reflect"
	"testing"
)

func TestAccountGet(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/v2/account", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)

		response := `
		{ "account": {
			"droplet_limit": 25,
			"floating_ip_limit": 25,
			"email": "sammy@digitalocean.com",
			"uuid": "b6fr89dbf6d9156cace5f3c78dc9851d957381ef",
			"email_verified": true
			}
		}`

		fmt.Fprint(w, response)
	})

	acct, _, err := client.Account.Get(ctx)
	if err != nil {
		t.Errorf("Account.Get returned error: %v", err)
	}

	expected := &Account{DropletLimit: 25, FloatingIPLimit: 25, Email: "sammy@digitalocean.com",
		UUID: "b6fr89dbf6d9156cace5f3c78dc9851d957381ef", EmailVerified: true}
	if !reflect.DeepEqual(acct, expected) {
		t.Errorf("Account.Get returned %+v, expected %+v", acct, expected)
	}
}

func TestAccountString(t *testing.T) {
	acct := &Account{
		DropletLimit:    25,
		FloatingIPLimit: 25,
		Email:           "sammy@digitalocean.com",
		UUID:            "b6fr89dbf6d9156cace5f3c78dc9851d957381ef",
		EmailVerified:   true,
		Status:          "active",
		StatusMessage:   "message",
	}

	stringified := acct.String()
	expected := `godo.Account{DropletLimit:25, FloatingIPLimit:25, Email:"sammy@digitalocean.com", UUID:"b6fr89dbf6d9156cace5f3c78dc9851d957381ef", EmailVerified:true, Status:"active", StatusMessage:"message"}`
	if expected != stringified {
		t.Errorf("Account.String returned %+v, expected %+v", stringified, expected)
	}

}


text:
package godo

import (
	"encoding/json"
	"fmt"
	"net/http"
	"reflect"
	"testing"
)

func TestDroplets_ListDroplets(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, `{"droplets": [{"id":1},{"id":2}]}`)
	})

	droplets, _, err := client.Droplets.List(ctx, nil)
	if err != nil {
		t.Errorf("Droplets.List returned error: %v", err)
	}

	expected := []Droplet{{ID: 1}, {ID: 2}}
	if !reflect.DeepEqual(droplets, expected) {
		t.Errorf("Droplets.List\n got=%#v\nwant=%#v", droplets, expected)
	}
}

func TestDroplets_ListDropletsByTag(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Query().Get("tag_name") != "testing-1" {
			t.Errorf("Droplets.ListByTag did not request with a tag parameter")
		}

		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, `{"droplets": [{"id":1},{"id":2}]}`)
	})

	droplets, _, err := client.Droplets.ListByTag(ctx, "testing-1", nil)
	if err != nil {
		t.Errorf("Droplets.ListByTag returned error: %v", err)
	}

	expected := []Droplet{{ID: 1}, {ID: 2}}
	if !reflect.DeepEqual(droplets, expected) {
		t.Errorf("Droplets.ListByTag returned %+v, expected %+v", droplets, expected)
	}
}

func TestDroplets_ListDropletsMultiplePages(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)

		dr := dropletsRoot{
			Droplets: []Droplet{
				{ID: 1},
				{ID: 2},
			},
			Links: &Links{
				Pages: &Pages{Next: "http://example.com/v2/droplets/?page=2"},
			},
		}

		b, err := json.Marshal(dr)
		if err != nil {
			t.Fatal(err)
		}

		fmt.Fprint(w, string(b))
	})

	_, resp, err := client.Droplets.List(ctx, nil)
	if err != nil {
		t.Fatal(err)
	}

	checkCurrentPage(t, resp, 1)
}

func TestDroplets_RetrievePageByNumber(t *testing.T) {
	setup()
	defer teardown()

	jBlob := `
	{
		"droplets": [{"id":1},{"id":2}],
		"links":{
			"pages":{
				"next":"http://example.com/v2/droplets/?page=3",
				"prev":"http://example.com/v2/droplets/?page=1",
				"last":"http://example.com/v2/droplets/?page=3",
				"first":"http://example.com/v2/droplets/?page=1"
			}
		}
	}`

	mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, jBlob)
	})

	opt := &ListOptions{Page: 2}
	_, resp, err := client.Droplets.List(ctx, opt)
	if err != nil {
		t.Fatal(err)
	}

	checkCurrentPage(t, resp, 2)
}

func TestDroplets_GetDroplet(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/v2/droplets/12345", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, `{"droplet":{"id":12345}}`)
	})

	droplets, _, err := client.Droplets.Get(ctx, 12345)
	if err != nil {
		t.Errorf("Droplet.Get returned error: %v", err)
	}

	expected := &Droplet{ID: 12345}
	if !reflect.DeepEqual(droplets, expected) {
		t.Errorf("Droplets.Get\n got=%#v\nwant=%#v", droplets, expected)
	}
}

func TestDroplets_Create(t *testing.T) {
	setup()
	defer teardown()

	createRequest := &DropletCreateRequest{
		Name:   "name",
		Region: "region",
		Size:   "size",
		Image: DropletCreateImage{
			ID: 1,
		},
		Volumes: []DropletCreateVolume{
			{Name: "hello-im-a-volume"},
			{ID: "hello-im-another-volume"},
			{Name: "hello-im-still-a-volume", ID: "should be ignored due to Name"},
		},
		Tags: []string{"one", "two"},
	}

	mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) {
		expected := map[string]interface{}{
			"name":               "name",
			"region":             "region",
			"size":               "size",
			"image":              float64(1),
			"ssh_keys":           nil,
			"backups":            false,
			"ipv6":               false,
			"private_networking": false,
			"monitoring":         false,
			"volumes": []interface{}{
				map[string]interface{}{"name": "hello-im-a-volume"},
				map[string]interface{}{"id": "hello-im-another-volume"},
				map[string]interface{}{"name": "hello-im-still-a-volume"},
			},
			"tags": []interface{}{"one", "two"},
		}

		var v map[string]interface{}
		err := json.NewDecoder(r.Body).Decode(&v)
		if err != nil {
			t.Fatalf("decode json: %v", err)
		}

		if !reflect.DeepEqual(v, expected) {
			t.Errorf("Request body\n got=%#v\nwant=%#v", v, expected)
		}

		fmt.Fprintf(w, `{"droplet":{"id":1}, "links":{"actions": [{"id": 1, "href": "http://example.com", "rel": "create"}]}}`)
	})

	droplet, resp, err := client.Droplets.Create(ctx, createRequest)
	if err != nil {
		t.Errorf("Droplets.Create returned error: %v", err)
	}

	if id := droplet.ID; id != 1 {
		t.Errorf("expected id '%d', received '%d'", 1, id)
	}

	if a := resp.Links.Actions[0]; a.ID != 1 {
		t.Errorf("expected action id '%d', received '%d'", 1, a.ID)
	}
}

func TestDroplets_CreateMultiple(t *testing.T) {
	setup()
	defer teardown()

	createRequest := &DropletMultiCreateRequest{
		Names:  []string{"name1", "name2"},
		Region: "region",
		Size:   "size",
		Image: DropletCreateImage{
			ID: 1,
		},
		Tags: []string{"one", "two"},
	}

	mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) {
		expected := map[string]interface{}{
			"names":              []interface{}{"name1", "name2"},
			"region":             "region",
			"size":               "size",
			"image":              float64(1),
			"ssh_keys":           nil,
			"backups":            false,
			"ipv6":               false,
			"private_networking": false,
			"monitoring":         false,
			"tags":               []interface{}{"one", "two"},
		}

		var v map[string]interface{}
		err := json.NewDecoder(r.Body).Decode(&v)
		if err != nil {
			t.Fatalf("decode json: %v", err)
		}

		if !reflect.DeepEqual(v, expected) {
			t.Errorf("Request body = %#v, expected %#v", v, expected)
		}

		fmt.Fprintf(w, `{"droplets":[{"id":1},{"id":2}], "links":{"actions": [{"id": 1, "href": "http://example.com", "rel": "multiple_create"}]}}`)
	})

	droplets, resp, err := client.Droplets.CreateMultiple(ctx, createRequest)
	if err != nil {
		t.Errorf("Droplets.CreateMultiple returned error: %v", err)
	}

	if id := droplets[0].ID; id != 1 {
		t.Errorf("expected id '%d', received '%d'", 1, id)
	}

	if id := droplets[1].ID; id != 2 {
		t.Errorf("expected id '%d', received '%d'", 1, id)
	}

	if a := resp.Links.Actions[0]; a.ID != 1 {
		t.Errorf("expected action id '%d', received '%d'", 1, a.ID)
	}
}

func TestDroplets_Destroy(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/v2/droplets/12345", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodDelete)
	})

	_, err := client.Droplets.Delete(ctx, 12345)
	if err != nil {
		t.Errorf("Droplet.Delete returned error: %v", err)
	}
}

func TestDroplets_DestroyByTag(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Query().Get("tag_name") != "testing-1" {
			t.Errorf("Droplets.DeleteByTag did not request with a tag parameter")
		}

		testMethod(t, r, http.MethodDelete)
	})

	_, err := client.Droplets.DeleteByTag(ctx, "testing-1")
	if err != nil {
		t.Errorf("Droplet.Delete returned error: %v", err)
	}
}

func TestDroplets_Kernels(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/v2/droplets/12345/kernels", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, `{"kernels": [{"id":1},{"id":2}]}`)
	})

	opt := &ListOptions{Page: 2}
	kernels, _, err := client.Droplets.Kernels(ctx, 12345, opt)
	if err != nil {
		t.Errorf("Droplets.Kernels returned error: %v", err)
	}

	expected := []Kernel{{ID: 1}, {ID: 2}}
	if !reflect.DeepEqual(kernels, expected) {
		t.Errorf("Droplets.Kernels\n got=%#v\nwant=%#v", kernels, expected)
	}
}

func TestDroplets_Snapshots(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/v2/droplets/12345/snapshots", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, `{"snapshots": [{"id":1},{"id":2}]}`)
	})

	opt := &ListOptions{Page: 2}
	snapshots, _, err := client.Droplets.Snapshots(ctx, 12345, opt)
	if err != nil {
		t.Errorf("Droplets.Snapshots returned error: %v", err)
	}

	expected := []Image{{ID: 1}, {ID: 2}}
	if !reflect.DeepEqual(snapshots, expected) {
		t.Errorf("Droplets.Snapshots\n got=%#v\nwant=%#v", snapshots, expected)
	}
}

func TestDroplets_Backups(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/v2/droplets/12345/backups", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, `{"backups": [{"id":1},{"id":2}]}`)
	})

	opt := &ListOptions{Page: 2}
	backups, _, err := client.Droplets.Backups(ctx, 12345, opt)
	if err != nil {
		t.Errorf("Droplets.Backups returned error: %v", err)
	}

	expected := []Image{{ID: 1}, {ID: 2}}
	if !reflect.DeepEqual(backups, expected) {
		t.Errorf("Droplets.Backups\n got=%#v\nwant=%#v", backups, expected)
	}
}

func TestDroplets_Actions(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/v2/droplets/12345/actions", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, `{"actions": [{"id":1},{"id":2}]}`)
	})

	opt := &ListOptions{Page: 2}
	actions, _, err := client.Droplets.Actions(ctx, 12345, opt)
	if err != nil {
		t.Errorf("Droplets.Actions returned error: %v", err)
	}

	expected := []Action{{ID: 1}, {ID: 2}}
	if !reflect.DeepEqual(actions, expected) {
		t.Errorf("Droplets.Actions\n got=%#v\nwant=%#v", actions, expected)
	}
}

func TestDroplets_Neighbors(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/v2/droplets/12345/neighbors", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, `{"droplets": [{"id":1},{"id":2}]}`)
	})

	neighbors, _, err := client.Droplets.Neighbors(ctx, 12345)
	if err != nil {
		t.Errorf("Droplets.Neighbors returned error: %v", err)
	}

	expected := []Droplet{{ID: 1}, {ID: 2}}
	if !reflect.DeepEqual(neighbors, expected) {
		t.Errorf("Droplets.Neighbors\n got=%#v\nwant=%#v", neighbors, expected)
	}
}

func TestNetworkV4_String(t *testing.T) {
	network := &NetworkV4{
		IPAddress: "192.168.1.2",
		Netmask:   "255.255.255.0",
		Gateway:   "192.168.1.1",
	}

	stringified := network.String()
	expected := `godo.NetworkV4{IPAddress:"192.168.1.2", Netmask:"255.255.255.0", Gateway:"192.168.1.1", Type:""}`
	if expected != stringified {
		t.Errorf("NetworkV4.String\n got=%#v\nwant=%#v", stringified, expected)
	}

}

func TestNetworkV6_String(t *testing.T) {
	network := &NetworkV6{
		IPAddress: "2604:A880:0800:0010:0000:0000:02DD:4001",
		Netmask:   64,
		Gateway:   "2604:A880:0800:0010:0000:0000:0000:0001",
	}
	stringified := network.String()
	expected := `godo.NetworkV6{IPAddress:"2604:A880:0800:0010:0000:0000:02DD:4001", Netmask:64, Gateway:"2604:A880:0800:0010:0000:0000:0000:0001", Type:""}`
	if expected != stringified {
		t.Errorf("NetworkV6.String\n got=%#v\nwant=%#v", stringified, expected)
	}
}

func TestDroplets_IPMethods(t *testing.T) {
	var d Droplet

	ipv6 := "1000:1000:1000:1000:0000:0000:004D:B001"

	d.Networks = &Networks{
		V4: []NetworkV4{
			{IPAddress: "192.168.0.1", Type: "public"},
			{IPAddress: "10.0.0.1", Type: "private"},
		},
		V6: []NetworkV6{
			{IPAddress: ipv6, Type: "public"},
		},
	}

	ip, err := d.PublicIPv4()
	if err != nil {
		t.Errorf("unknown error")
	}

	if got, expected := ip, "192.168.0.1"; got != expected {
		t.Errorf("Droplet.PublicIPv4 returned %s; expected %s", got, expected)
	}

	ip, err = d.PrivateIPv4()
	if err != nil {
		t.Errorf("unknown error")
	}

	if got, expected := ip, "10.0.0.1"; got != expected {
		t.Errorf("Droplet.PrivateIPv4 returned %s; expected %s", got, expected)
	}

	ip, err = d.PublicIPv6()
	if err != nil {
		t.Errorf("unknown error")
	}

	if got, expected := ip, ipv6; got != expected {
		t.Errorf("Droplet.PublicIPv6 returned %s; expected %s", got, expected)
	}
}


text:
package godo

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"net/http/httputil"
	"net/url"
	"reflect"
	"strings"
	"testing"
	"time"

	"github.com/digitalocean/godo/context"
)

var (
	mux *http.ServeMux

	ctx = context.TODO()

	client *Client

	server *httptest.Server
)

func setup() {
	mux = http.NewServeMux()
	server = httptest.NewServer(mux)

	client = NewClient(nil)
	url, _ := url.Parse(server.URL)
	client.BaseURL = url
}

func teardown() {
	server.Close()
}

func testMethod(t *testing.T, r *http.Request, expected string) {
	if expected != r.Method {
		t.Errorf("Request method = %v, expected %v", r.Method, expected)
	}
}

type values map[string]string

func testFormValues(t *testing.T, r *http.Request, values values) {
	expected := url.Values{}
	for k, v := range values {
		expected.Add(k, v)
	}

	err := r.ParseForm()
	if err != nil {
		t.Fatalf("parseForm(): %v", err)
	}

	if !reflect.DeepEqual(expected, r.Form) {
		t.Errorf("Request parameters = %v, expected %v", r.Form, expected)
	}
}

func testURLParseError(t *testing.T, err error) {
	if err == nil {
		t.Errorf("Expected error to be returned")
	}
	if err, ok := err.(*url.Error); !ok || err.Op != "parse" {
		t.Errorf("Expected URL parse error, got %+v", err)
	}
}

func testClientServices(t *testing.T, c *Client) {
	services := []string{
		"Account",
		"Actions",
		"Domains",
		"Droplets",
		"DropletActions",
		"Images",
		"ImageActions",
		"Keys",
		"Regions",
		"Sizes",
		"FloatingIPs",
		"FloatingIPActions",
		"Tags",
	}

	cp := reflect.ValueOf(c)
	cv := reflect.Indirect(cp)

	for _, s := range services {
		if cv.FieldByName(s).IsNil() {
			t.Errorf("c.%s shouldn't be nil", s)
		}
	}
}

func testClientDefaultBaseURL(t *testing.T, c *Client) {
	if c.BaseURL == nil || c.BaseURL.String() != defaultBaseURL {
		t.Errorf("NewClient BaseURL = %v, expected %v", c.BaseURL, defaultBaseURL)
	}
}

func testClientDefaultUserAgent(t *testing.T, c *Client) {
	if c.UserAgent != userAgent {
		t.Errorf("NewClient UserAgent = %v, expected %v", c.UserAgent, userAgent)
	}
}

func testClientDefaults(t *testing.T, c *Client) {
	testClientDefaultBaseURL(t, c)
	testClientDefaultUserAgent(t, c)
	testClientServices(t, c)
}

func TestNewClient(t *testing.T) {
	c := NewClient(nil)
	testClientDefaults(t, c)
}

func TestNew(t *testing.T) {
	c, err := New(nil)

	if err != nil {
		t.Fatalf("New(): %v", err)
	}
	testClientDefaults(t, c)
}

func TestNewRequest(t *testing.T) {
	c := NewClient(nil)

	inURL, outURL := "/foo", defaultBaseURL+"foo"
	inBody, outBody := &DropletCreateRequest{Name: "l"},
		`{"name":"l","region":"","size":"","image":0,`+
			`"ssh_keys":null,"backups":false,"ipv6":false,`+
			`"private_networking":false,"monitoring":false,"tags":null}`+"\n"
	req, _ := c.NewRequest(ctx, http.MethodGet, inURL, inBody)

	// test relative URL was expanded
	if req.URL.String() != outURL {
		t.Errorf("NewRequest(%v) URL = %v, expected %v", inURL, req.URL, outURL)
	}

	// test body was JSON encoded
	body, _ := ioutil.ReadAll(req.Body)
	if string(body) != outBody {
		t.Errorf("NewRequest(%v)Body = %v, expected %v", inBody, string(body), outBody)
	}

	// test default user-agent is attached to the request
	userAgent := req.Header.Get("User-Agent")
	if c.UserAgent != userAgent {
		t.Errorf("NewRequest() User-Agent = %v, expected %v", userAgent, c.UserAgent)
	}
}

func TestNewRequest_withUserData(t *testing.T) {
	c := NewClient(nil)

	inURL, outURL := "/foo", defaultBaseURL+"foo"
	inBody, outBody := &DropletCreateRequest{Name: "l", UserData: "u"},
		`{"name":"l","region":"","size":"","image":0,`+
			`"ssh_keys":null,"backups":false,"ipv6":false,`+
			`"private_networking":false,"monitoring":false,"user_data":"u","tags":null}`+"\n"
	req, _ := c.NewRequest(ctx, http.MethodGet, inURL, inBody)

	// test relative URL was expanded
	if req.URL.String() != outURL {
		t.Errorf("NewRequest(%v) URL = %v, expected %v", inURL, req.URL, outURL)
	}

	// test body was JSON encoded
	body, _ := ioutil.ReadAll(req.Body)
	if string(body) != outBody {
		t.Errorf("NewRequest(%v)Body = %v, expected %v", inBody, string(body), outBody)
	}

	// test default user-agent is attached to the request
	userAgent := req.Header.Get("User-Agent")
	if c.UserAgent != userAgent {
		t.Errorf("NewRequest() User-Agent = %v, expected %v", userAgent, c.UserAgent)
	}
}

func TestNewRequest_badURL(t *testing.T) {
	c := NewClient(nil)
	_, err := c.NewRequest(ctx, http.MethodGet, ":", nil)
	testURLParseError(t, err)
}

func TestNewRequest_withCustomUserAgent(t *testing.T) {
	ua := "testing/0.0.1"
	c, err := New(nil, SetUserAgent(ua))

	if err != nil {
		t.Fatalf("New() unexpected error: %v", err)
	}

	req, _ := c.NewRequest(ctx, http.MethodGet, "/foo", nil)

	expected := fmt.Sprintf("%s %s", ua, userAgent)
	if got := req.Header.Get("User-Agent"); got != expected {
		t.Errorf("New() UserAgent = %s; expected %s", got, expected)
	}
}

func TestDo(t *testing.T) {
	setup()
	defer teardown()

	type foo struct {
		A string
	}

	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		if m := http.MethodGet; m != r.Method {
			t.Errorf("Request method = %v, expected %v", r.Method, m)
		}
		fmt.Fprint(w, `{"A":"a"}`)
	})

	req, _ := client.NewRequest(ctx, http.MethodGet, "/", nil)
	body := new(foo)
	_, err := client.Do(context.Background(), req, body)
	if err != nil {
		t.Fatalf("Do(): %v", err)
	}

	expected := &foo{"a"}
	if !reflect.DeepEqual(body, expected) {
		t.Errorf("Response body = %v, expected %v", body, expected)
	}
}

func TestDo_httpError(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		http.Error(w, "Bad Request", 400)
	})

	req, _ := client.NewRequest(ctx, http.MethodGet, "/", nil)
	_, err := client.Do(context.Background(), req, nil)

	if err == nil {
		t.Error("Expected HTTP 400 error.")
	}
}

// Test handling of an error caused by the internal http client's Do()
// function.
func TestDo_redirectLoop(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, "/", http.StatusFound)
	})

	req, _ := client.NewRequest(ctx, http.MethodGet, "/", nil)
	_, err := client.Do(context.Background(), req, nil)

	if err == nil {
		t.Error("Expected error to be returned.")
	}
	if err, ok := err.(*url.Error); !ok {
		t.Errorf("Expected a URL error; got %#v.", err)
	}
}

func TestCheckResponse(t *testing.T) {
	res := &http.Response{
		Request:    &http.Request{},
		StatusCode: http.StatusBadRequest,
		Body: ioutil.NopCloser(strings.NewReader(`{"message":"m",
			"errors": [{"resource": "r", "field": "f", "code": "c"}]}`)),
	}
	err := CheckResponse(res).(*ErrorResponse)

	if err == nil {
		t.Fatalf("Expected error response.")
	}

	expected := &ErrorResponse{
		Response: res,
		Message:  "m",
	}
	if !reflect.DeepEqual(err, expected) {
		t.Errorf("Error = %#v, expected %#v", err, expected)
	}
}

// ensure that we properly handle API errors that do not contain a response
// body
func TestCheckResponse_noBody(t *testing.T) {
	res := &http.Response{
		Request:    &http.Request{},
		StatusCode: http.StatusBadRequest,
		Body:       ioutil.NopCloser(strings.NewReader("")),
	}
	err := CheckResponse(res).(*ErrorResponse)

	if err == nil {
		t.Errorf("Expected error response.")
	}

	expected := &ErrorResponse{
		Response: res,
	}
	if !reflect.DeepEqual(err, expected) {
		t.Errorf("Error = %#v, expected %#v", err, expected)
	}
}

func TestErrorResponse_Error(t *testing.T) {
	res := &http.Response{Request: &http.Request{}}
	err := ErrorResponse{Message: "m", Response: res}
	if err.Error() == "" {
		t.Errorf("Expected non-empty ErrorResponse.Error()")
	}
}

func TestDo_rateLimit(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add(headerRateLimit, "60")
		w.Header().Add(headerRateRemaining, "59")
		w.Header().Add(headerRateReset, "1372700873")
	})

	var expected int

	if expected = 0; client.Rate.Limit != expected {
		t.Errorf("Client rate limit = %v, expected %v", client.Rate.Limit, expected)
	}
	if expected = 0; client.Rate.Remaining != expected {
		t.Errorf("Client rate remaining = %v, got %v", client.Rate.Remaining, expected)
	}
	if !client.Rate.Reset.IsZero() {
		t.Errorf("Client rate reset not initialized to zero value")
	}

	req, _ := client.NewRequest(ctx, http.MethodGet, "/", nil)
	_, err := client.Do(context.Background(), req, nil)
	if err != nil {
		t.Fatalf("Do(): %v", err)
	}

	if expected = 60; client.Rate.Limit != expected {
		t.Errorf("Client rate limit = %v, expected %v", client.Rate.Limit, expected)
	}
	if expected = 59; client.Rate.Remaining != expected {
		t.Errorf("Client rate remaining = %v, expected %v", client.Rate.Remaining, expected)
	}
	reset := time.Date(2013, 7, 1, 17, 47, 53, 0, time.UTC)
	if client.Rate.Reset.UTC() != reset {
		t.Errorf("Client rate reset = %v, expected %v", client.Rate.Reset, reset)
	}
}

func TestDo_rateLimit_errorResponse(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add(headerRateLimit, "60")
		w.Header().Add(headerRateRemaining, "59")
		w.Header().Add(headerRateReset, "1372700873")
		http.Error(w, `{"message":"bad request"}`, 400)
	})

	var expected int

	req, _ := client.NewRequest(ctx, http.MethodGet, "/", nil)
	_, _ = client.Do(context.Background(), req, nil)

	if expected = 60; client.Rate.Limit != expected {
		t.Errorf("Client rate limit = %v, expected %v", client.Rate.Limit, expected)
	}
	if expected = 59; client.Rate.Remaining != expected {
		t.Errorf("Client rate remaining = %v, expected %v", client.Rate.Remaining, expected)
	}
	reset := time.Date(2013, 7, 1, 17, 47, 53, 0, time.UTC)
	if client.Rate.Reset.UTC() != reset {
		t.Errorf("Client rate reset = %v, expected %v", client.Rate.Reset, reset)
	}
}

func checkCurrentPage(t *testing.T, resp *Response, expectedPage int) {
	links := resp.Links
	p, err := links.CurrentPage()
	if err != nil {
		t.Fatal(err)
	}

	if p != expectedPage {
		t.Fatalf("expected current page to be '%d', was '%d'", expectedPage, p)
	}
}

func TestDo_completion_callback(t *testing.T) {
	setup()
	defer teardown()

	type foo struct {
		A string
	}

	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		if m := http.MethodGet; m != r.Method {
			t.Errorf("Request method = %v, expected %v", r.Method, m)
		}
		fmt.Fprint(w, `{"A":"a"}`)
	})

	req, _ := client.NewRequest(ctx, http.MethodGet, "/", nil)
	body := new(foo)
	var completedReq *http.Request
	var completedResp string
	client.OnRequestCompleted(func(req *http.Request, resp *http.Response) {
		completedReq = req
		b, err := httputil.DumpResponse(resp, true)
		if err != nil {
			t.Errorf("Failed to dump response: %s", err)
		}
		completedResp = string(b)
	})
	_, err := client.Do(context.Background(), req, body)
	if err != nil {
		t.Fatalf("Do(): %v", err)
	}
	if !reflect.DeepEqual(req, completedReq) {
		t.Errorf("Completed request = %v, expected %v", completedReq, req)
	}
	expected := `{"A":"a"}`
	if !strings.Contains(completedResp, expected) {
		t.Errorf("expected response to contain %v, Response = %v", expected, completedResp)
	}
}

func TestAddOptions(t *testing.T) {
	cases := []struct {
		name     string
		path     string
		expected string
		opts     *ListOptions
		isErr    bool
	}{
		{
			name:     "add options",
			path:     "/action",
			expected: "/action?page=1",
			opts:     &ListOptions{Page: 1},
			isErr:    false,
		},
		{
			name:     "add options with existing parameters",
			path:     "/action?scope=all",
			expected: "/action?page=1&scope=all",
			opts:     &ListOptions{Page: 1},
			isErr:    false,
		},
	}

	for _, c := range cases {
		got, err := addOptions(c.path, c.opts)
		if c.isErr && err == nil {
			t.Errorf("%q expected error but none was encountered", c.name)
			continue
		}

		if !c.isErr && err != nil {
			t.Errorf("%q unexpected error: %v", c.name, err)
			continue
		}

		gotURL, err := url.Parse(got)
		if err != nil {
			t.Errorf("%q unable to parse returned URL", c.name)
			continue
		}

		expectedURL, err := url.Parse(c.expected)
		if err != nil {
			t.Errorf("%q unable to parse expected URL", c.name)
			continue
		}

		if g, e := gotURL.Path, expectedURL.Path; g != e {
			t.Errorf("%q path = %q; expected %q", c.name, g, e)
			continue
		}

		if g, e := gotURL.Query(), expectedURL.Query(); !reflect.DeepEqual(g, e) {
			t.Errorf("%q query = %#v; expected %#v", c.name, g, e)
			continue
		}
	}
}

func TestCustomUserAgent(t *testing.T) {
	ua := "testing/0.0.1"
	c, err := New(nil, SetUserAgent(ua))

	if err != nil {
		t.Fatalf("New() unexpected error: %v", err)
	}

	expected := fmt.Sprintf("%s %s", ua, userAgent)
	if got := c.UserAgent; got != expected {
		t.Errorf("New() UserAgent = %s; expected %s", got, expected)
	}
}

func TestCustomBaseURL(t *testing.T) {
	baseURL := "http://localhost/foo"
	c, err := New(nil, SetBaseURL(baseURL))

	if err != nil {
		t.Fatalf("New() unexpected error: %v", err)
	}

	expected := baseURL
	if got := c.BaseURL.String(); got != expected {
		t.Errorf("New() BaseURL = %s; expected %s", got, expected)
	}
}

func TestCustomBaseURL_badURL(t *testing.T) {
	baseURL := ":"
	_, err := New(nil, SetBaseURL(baseURL))

	testURLParseError(t, err)
}


text:
package godo

import (
	"encoding/json"
	"fmt"
	"net/http"
	"reflect"
	"testing"
)

var (
	listEmptyJSON = `
	{
		"tags": [
		],
		"meta": {
			"total": 0
		}
	}
	`

	listJSON = `
	{
		"tags": [
		{
			"name": "testing-1",
			"resources": {
				"droplets": {
					"count": 0,
					"last_tagged": null
				}
			}
		},
		{
			"name": "testing-2",
			"resources": {
				"droplets": {
					"count": 0,
					"last_tagged": null
				}
			}
		}
		],
		"links": {
			"pages":{
				"next":"http://example.com/v2/tags/?page=3",
				"prev":"http://example.com/v2/tags/?page=1",
				"last":"http://example.com/v2/tags/?page=3",
				"first":"http://example.com/v2/tags/?page=1"
			}
		},
		"meta": {
			"total": 2
		}
	}
	`

	createJSON = `
	{
		"tag": {
			"name": "testing-1",
			"resources": {
				"droplets": {
					"count": 0,
					"last_tagged": null
				}
			}
		}
	}
	`

	getJSON = `
	{
		"tag": {
			"name": "testing-1",
			"resources": {
				"droplets": {
					"count": 1,
					"last_tagged": {
						"id": 1,
						"name": "test.example.com",
						"memory": 1024,
						"vcpus": 2,
						"disk": 20,
						"region": {
							"slug": "nyc1",
							"name": "New York",
							"sizes": [
							"1024mb",
							"512mb"
							],
							"available": true,
							"features": [
							"virtio",
							"private_networking",
							"backups",
							"ipv6"
							]
						},
						"image": {
							"id": 119192817,
							"name": "Ubuntu 13.04",
							"distribution": "ubuntu",
							"slug": "ubuntu1304",
							"public": true,
							"regions": [
							"nyc1"
							],
							"created_at": "2014-07-29T14:35:37Z"
						},
						"size_slug": "1024mb",
						"locked": false,
						"status": "active",
						"networks": {
							"v4": [
							{
								"ip_address": "10.0.0.19",
								"netmask": "255.255.0.0",
								"gateway": "10.0.0.1",
								"type": "private"
							},
							{
								"ip_address": "127.0.0.19",
								"netmask": "255.255.255.0",
								"gateway": "127.0.0.20",
								"type": "public"
							}
							],
							"v6": [
							{
								"ip_address": "2001::13",
								"cidr": 124,
								"gateway": "2400:6180:0000:00D0:0000:0000:0009:7000",
								"type": "public"
							}
							]
						},
						"kernel": {
							"id": 485432985,
							"name": "DO-recovery-static-fsck",
							"version": "3.8.0-25-generic"
						},
						"created_at": "2014-07-29T14:35:37Z",
						"features": [
						"ipv6"
						],
						"backup_ids": [
						449676382
						],
						"snapshot_ids": [
						449676383
						],
						"action_ids": [
						],
						"tags": [
						"tag-1",
						"tag-2"
						]
					}
				}
			}
		}
	}
	`
)

func TestTags_List(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/v2/tags", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, listJSON)
	})

	tags, _, err := client.Tags.List(ctx, nil)
	if err != nil {
		t.Errorf("Tags.List returned error: %v", err)
	}

	expected := []Tag{{Name: "testing-1", Resources: &TaggedResources{Droplets: &TaggedDropletsResources{Count: 0, LastTagged: nil}}},
		{Name: "testing-2", Resources: &TaggedResources{Droplets: &TaggedDropletsResources{Count: 0, LastTagged: nil}}}}
	if !reflect.DeepEqual(tags, expected) {
		t.Errorf("Tags.List returned %+v, expected %+v", tags, expected)
	}
}

func TestTags_ListEmpty(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/v2/tags", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, listEmptyJSON)
	})

	tags, _, err := client.Tags.List(ctx, nil)
	if err != nil {
		t.Errorf("Tags.List returned error: %v", err)
	}

	expected := []Tag{}
	if !reflect.DeepEqual(tags, expected) {
		t.Errorf("Tags.List returned %+v, expected %+v", tags, expected)
	}
}

func TestTags_ListPaging(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/v2/tags", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, listJSON)
	})

	_, resp, err := client.Tags.List(ctx, nil)
	if err != nil {
		t.Errorf("Tags.List returned error: %v", err)
	}
	checkCurrentPage(t, resp, 2)
}

func TestTags_Get(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/v2/tags/testing-1", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, getJSON)
	})

	tag, _, err := client.Tags.Get(ctx, "testing-1")
	if err != nil {
		t.Errorf("Tags.Get returned error: %v", err)
	}

	if tag.Name != "testing-1" {
		t.Errorf("Tags.Get return an incorrect name, got %+v, expected %+v", tag.Name, "testing-1")
	}

	if tag.Resources.Droplets.Count != 1 {
		t.Errorf("Tags.Get return an incorrect droplet resource count, got %+v, expected %+v", tag.Resources.Droplets.Count, 1)
	}

	if tag.Resources.Droplets.LastTagged.ID != 1 {
		t.Errorf("Tags.Get return an incorrect last tagged droplet %+v, expected %+v", tag.Resources.Droplets.LastTagged.ID, 1)
	}
}

func TestTags_Create(t *testing.T) {
	setup()
	defer teardown()

	createRequest := &TagCreateRequest{
		Name: "testing-1",
	}

	mux.HandleFunc("/v2/tags", func(w http.ResponseWriter, r *http.Request) {
		v := new(TagCreateRequest)
		err := json.NewDecoder(r.Body).Decode(v)
		if err != nil {
			t.Fatalf("decode json: %v", err)
		}

		testMethod(t, r, http.MethodPost)
		if !reflect.DeepEqual(v, createRequest) {
			t.Errorf("Request body = %+v, expected %+v", v, createRequest)
		}

		fmt.Fprintf(w, createJSON)
	})

	tag, _, err := client.Tags.Create(ctx, createRequest)
	if err != nil {
		t.Errorf("Tags.Create returned error: %v", err)
	}

	expected := &Tag{Name: "testing-1", Resources: &TaggedResources{Droplets: &TaggedDropletsResources{Count: 0, LastTagged: nil}}}
	if !reflect.DeepEqual(tag, expected) {
		t.Errorf("Tags.Create returned %+v, expected %+v", tag, expected)
	}
}

func TestTags_Delete(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/v2/tags/testing-1", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodDelete)
	})

	_, err := client.Tags.Delete(ctx, "testing-1")
	if err != nil {
		t.Errorf("Tags.Delete returned error: %v", err)
	}
}

func TestTags_TagResource(t *testing.T) {
	setup()
	defer teardown()

	tagResourcesRequest := &TagResourcesRequest{
		Resources: []Resource{{ID: "1", Type: DropletResourceType}},
	}

	mux.HandleFunc("/v2/tags/testing-1/resources", func(w http.ResponseWriter, r *http.Request) {
		v := new(TagResourcesRequest)

		err := json.NewDecoder(r.Body).Decode(v)
		if err != nil {
			t.Fatalf("decode json: %v", err)
		}

		testMethod(t, r, http.MethodPost)
		if !reflect.DeepEqual(v, tagResourcesRequest) {
			t.Errorf("Request body = %+v, expected %+v", v, tagResourcesRequest)
		}

	})

	_, err := client.Tags.TagResources(ctx, "testing-1", tagResourcesRequest)
	if err != nil {
		t.Errorf("Tags.TagResources returned error: %v", err)
	}
}

func TestTags_UntagResource(t *testing.T) {
	setup()
	defer teardown()

	untagResourcesRequest := &UntagResourcesRequest{
		Resources: []Resource{{ID: "1", Type: DropletResourceType}},
	}

	mux.HandleFunc("/v2/tags/testing-1/resources", func(w http.ResponseWriter, r *http.Request) {
		v := new(UntagResourcesRequest)

		err := json.NewDecoder(r.Body).Decode(v)
		if err != nil {
			t.Fatalf("decode json: %v", err)
		}

		testMethod(t, r, http.MethodDelete)
		if !reflect.DeepEqual(v, untagResourcesRequest) {
			t.Errorf("Request body = %+v, expected %+v", v, untagResourcesRequest)
		}

	})

	_, err := client.Tags.UntagResources(ctx, "testing-1", untagResourcesRequest)
	if err != nil {
		t.Errorf("Tags.UntagResources returned error: %v", err)
	}
}


text:
package godo

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
	"reflect"
	"strconv"
	"time"

	"github.com/google/go-querystring/query"
	headerLink "github.com/tent/http-link-go"

	"github.com/digitalocean/godo/context"
)

const (
	libraryVersion = "1.1.0"
	defaultBaseURL = "https://api.digitalocean.com/"
	userAgent      = "godo/" + libraryVersion
	mediaType      = "application/json"

	headerRateLimit     = "RateLimit-Limit"
	headerRateRemaining = "RateLimit-Remaining"
	headerRateReset     = "RateLimit-Reset"
)

// Client manages communication with DigitalOcean V2 API.
type Client struct {
	// HTTP client used to communicate with the DO API.
	client *http.Client

	// Base URL for API requests.
	BaseURL *url.URL

	// User agent for client
	UserAgent string

	// Rate contains the current rate limit for the client as determined by the most recent
	// API call.
	Rate Rate

	// Services used for communicating with the API
	Account           AccountService
	Actions           ActionsService
	Domains           DomainsService
	Droplets          DropletsService
	DropletActions    DropletActionsService
	Images            ImagesService
	ImageActions      ImageActionsService
	Keys              KeysService
	Regions           RegionsService
	Sizes             SizesService
	FloatingIPs       FloatingIPsService
	FloatingIPActions FloatingIPActionsService
	Snapshots         SnapshotsService
	Storage           StorageService
	StorageActions    StorageActionsService
	Tags              TagsService
	LoadBalancers     LoadBalancersService
	Certificates      CertificatesService
	Firewalls         FirewallsService

	// Optional function called after every successful request made to the DO APIs
	onRequestCompleted RequestCompletionCallback
}

// RequestCompletionCallback defines the type of the request callback function
type RequestCompletionCallback func(*http.Request, *http.Response)

// ListOptions specifies the optional parameters to various List methods that
// support pagination.
type ListOptions struct {
	// For paginated result sets, page of results to retrieve.
	Page int `url:"page,omitempty"`

	// For paginated result sets, the number of results to include per page.
	PerPage int `url:"per_page,omitempty"`
}

// Response is a DigitalOcean response. This wraps the standard http.Response returned from DigitalOcean.
type Response struct {
	*http.Response

	// Links that were returned with the response. These are parsed from
	// request body and not the header.
	Links *Links

	// Monitoring URI
	Monitor string

	Rate
}

// An ErrorResponse reports the error caused by an API request
type ErrorResponse struct {
	// HTTP response that caused this error
	Response *http.Response

	// Error message
	Message string `json:"message"`

	// RequestID returned from the API, useful to contact support.
	RequestID string `json:"request_id"`
}

// Rate contains the rate limit for the current client.
type Rate struct {
	// The number of request per hour the client is currently limited to.
	Limit int `json:"limit"`

	// The number of remaining requests the client can make this hour.
	Remaining int `json:"remaining"`

	// The time at which the current rate limit will reset.
	Reset Timestamp `json:"reset"`
}

func addOptions(s string, opt interface{}) (string, error) {
	v := reflect.ValueOf(opt)

	if v.Kind() == reflect.Ptr && v.IsNil() {
		return s, nil
	}

	origURL, err := url.Parse(s)
	if err != nil {
		return s, err
	}

	origValues := origURL.Query()

	newValues, err := query.Values(opt)
	if err != nil {
		return s, err
	}

	for k, v := range newValues {
		origValues[k] = v
	}

	origURL.RawQuery = origValues.Encode()
	return origURL.String(), nil
}

// NewClient returns a new DigitalOcean API client.
func NewClient(httpClient *http.Client) *Client {
	if httpClient == nil {
		httpClient = http.DefaultClient
	}

	baseURL, _ := url.Parse(defaultBaseURL)

	c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent}
	c.Account = &AccountServiceOp{client: c}
	c.Actions = &ActionsServiceOp{client: c}
	c.Domains = &DomainsServiceOp{client: c}
	c.Droplets = &DropletsServiceOp{client: c}
	c.DropletActions = &DropletActionsServiceOp{client: c}
	c.FloatingIPs = &FloatingIPsServiceOp{client: c}
	c.FloatingIPActions = &FloatingIPActionsServiceOp{client: c}
	c.Images = &ImagesServiceOp{client: c}
	c.ImageActions = &ImageActionsServiceOp{client: c}
	c.Keys = &KeysServiceOp{client: c}
	c.Regions = &RegionsServiceOp{client: c}
	c.Snapshots = &SnapshotsServiceOp{client: c}
	c.Sizes = &SizesServiceOp{client: c}
	c.Storage = &StorageServiceOp{client: c}
	c.StorageActions = &StorageActionsServiceOp{client: c}
	c.Tags = &TagsServiceOp{client: c}
	c.LoadBalancers = &LoadBalancersServiceOp{client: c}
	c.Certificates = &CertificatesServiceOp{client: c}
	c.Firewalls = &FirewallsServiceOp{client: c}

	return c
}

// ClientOpt are options for New.
type ClientOpt func(*Client) error

// New returns a new DIgitalOcean API client instance.
func New(httpClient *http.Client, opts ...ClientOpt) (*Client, error) {
	c := NewClient(httpClient)
	for _, opt := range opts {
		if err := opt(c); err != nil {
			return nil, err
		}
	}

	return c, nil
}

// SetBaseURL is a client option for setting the base URL.
func SetBaseURL(bu string) ClientOpt {
	return func(c *Client) error {
		u, err := url.Parse(bu)
		if err != nil {
			return err
		}

		c.BaseURL = u
		return nil
	}
}

// SetUserAgent is a client option for setting the user agent.
func SetUserAgent(ua string) ClientOpt {
	return func(c *Client) error {
		c.UserAgent = fmt.Sprintf("%s %s", ua, c.UserAgent)
		return nil
	}
}

// NewRequest creates an API request. A relative URL can be provided in urlStr, which will be resolved to the
// BaseURL of the Client. Relative URLS should always be specified without a preceding slash. If specified, the
// value pointed to by body is JSON encoded and included in as the request body.
func (c *Client) NewRequest(ctx context.Context, method, urlStr string, body interface{}) (*http.Request, error) {
	rel, err := url.Parse(urlStr)
	if err != nil {
		return nil, err
	}

	u := c.BaseURL.ResolveReference(rel)

	buf := new(bytes.Buffer)
	if body != nil {
		err = json.NewEncoder(buf).Encode(body)
		if err != nil {
			return nil, err
		}
	}

	req, err := http.NewRequest(method, u.String(), buf)
	if err != nil {
		return nil, err
	}

	req.Header.Add("Content-Type", mediaType)
	req.Header.Add("Accept", mediaType)
	req.Header.Add("User-Agent", c.UserAgent)
	return req, nil
}

// OnRequestCompleted sets the DO API request completion callback
func (c *Client) OnRequestCompleted(rc RequestCompletionCallback) {
	c.onRequestCompleted = rc
}

// newResponse creates a new Response for the provided http.Response
func newResponse(r *http.Response) *Response {
	response := Response{Response: r}
	response.populateRate()

	return &response
}

func (r *Response) links() (map[string]headerLink.Link, error) {
	if linkText, ok := r.Response.Header["Link"]; ok {
		links, err := headerLink.Parse(linkText[0])

		if err != nil {
			return nil, err
		}

		linkMap := map[string]headerLink.Link{}
		for _, link := range links {
			linkMap[link.Rel] = link
		}

		return linkMap, nil
	}

	return map[string]headerLink.Link{}, nil
}

// populateRate parses the rate related headers and populates the response Rate.
func (r *Response) populateRate() {
	if limit := r.Header.Get(headerRateLimit); limit != "" {
		r.Rate.Limit, _ = strconv.Atoi(limit)
	}
	if remaining := r.Header.Get(headerRateRemaining); remaining != "" {
		r.Rate.Remaining, _ = strconv.Atoi(remaining)
	}
	if reset := r.Header.Get(headerRateReset); reset != "" {
		if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 {
			r.Rate.Reset = Timestamp{time.Unix(v, 0)}
		}
	}
}

// Do sends an API request and returns the API response. The API response is JSON decoded and stored in the value
// pointed to by v, or returned as an error if an API error has occurred. If v implements the io.Writer interface,
// the raw response will be written to v, without attempting to decode it.
func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) {
	resp, err := context.DoRequestWithClient(ctx, c.client, req)
	if err != nil {
		return nil, err
	}
	if c.onRequestCompleted != nil {
		c.onRequestCompleted(req, resp)
	}

	defer func() {
		if rerr := resp.Body.Close(); err == nil {
			err = rerr
		}
	}()

	response := newResponse(resp)
	c.Rate = response.Rate

	err = CheckResponse(resp)
	if err != nil {
		return response, err
	}

	if v != nil {
		if w, ok := v.(io.Writer); ok {
			_, err = io.Copy(w, resp.Body)
			if err != nil {
				return nil, err
			}
		} else {
			err = json.NewDecoder(resp.Body).Decode(v)
			if err != nil {
				return nil, err
			}
		}
	}

	return response, err
}
func (r *ErrorResponse) Error() string {
	if r.RequestID != "" {
		return fmt.Sprintf("%v %v: %d (request %q) %v",
			r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.RequestID, r.Message)
	}
	return fmt.Sprintf("%v %v: %d %v",
		r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.Message)
}

// CheckResponse checks the API response for errors, and returns them if present. A response is considered an
// error if it has a status code outside the 200 range. API error responses are expected to have either no response
// body, or a JSON response body that maps to ErrorResponse. Any other response body will be silently ignored.
func CheckResponse(r *http.Response) error {
	if c := r.StatusCode; c >= 200 && c <= 299 {
		return nil
	}

	errorResponse := &ErrorResponse{Response: r}
	data, err := ioutil.ReadAll(r.Body)
	if err == nil && len(data) > 0 {
		err := json.Unmarshal(data, errorResponse)
		if err != nil {
			errorResponse.Message = string(data)
		}
	}

	return errorResponse
}

func (r Rate) String() string {
	return Stringify(r)
}

// String is a helper routine that allocates a new string value
// to store v and returns a pointer to it.
func String(v string) *string {
	p := new(string)
	*p = v
	return p
}

// Int is a helper routine that allocates a new int32 value
// to store v and returns a pointer to it, but unlike Int32
// its argument value is an int.
func Int(v int) *int {
	p := new(int)
	*p = v
	return p
}

// Bool is a helper routine that allocates a new bool value
// to store v and returns a pointer to it.
func Bool(v bool) *bool {
	p := new(bool)
	*p = v
	return p
}

// StreamToString converts a reader to a string
func StreamToString(stream io.Reader) string {
	buf := new(bytes.Buffer)
	_, _ = buf.ReadFrom(stream)
	return buf.String()
}


text:
package godo

import (
	"fmt"
	"net/http"

	"github.com/digitalocean/godo/context"
)

const imageBasePath = "v2/images"

// ImagesService is an interface for interfacing with the images
// endpoints of the DigitalOcean API
// See: https://developers.digitalocean.com/documentation/v2#images
type ImagesService interface {
	List(context.Context, *ListOptions) ([]Image, *Response, error)
	ListDistribution(ctx context.Context, opt *ListOptions) ([]Image, *Response, error)
	ListApplication(ctx context.Context, opt *ListOptions) ([]Image, *Response, error)
	ListUser(ctx context.Context, opt *ListOptions) ([]Image, *Response, error)
	GetByID(context.Context, int) (*Image, *Response, error)
	GetBySlug(context.Context, string) (*Image, *Response, error)
	Update(context.Context, int, *ImageUpdateRequest) (*Image, *Response, error)
	Delete(context.Context, int) (*Response, error)
}

// ImagesServiceOp handles communication with the image related methods of the
// DigitalOcean API.
type ImagesServiceOp struct {
	client *Client
}

var _ ImagesService = &ImagesServiceOp{}

// Image represents a DigitalOcean Image
type Image struct {
	ID           int      `json:"id,float64,omitempty"`
	Name         string   `json:"name,omitempty"`
	Type         string   `json:"type,omitempty"`
	Distribution string   `json:"distribution,omitempty"`
	Slug         string   `json:"slug,omitempty"`
	Public       bool     `json:"public,omitempty"`
	Regions      []string `json:"regions,omitempty"`
	MinDiskSize  int      `json:"min_disk_size,omitempty"`
	Created      string   `json:"created_at,omitempty"`
}

// ImageUpdateRequest represents a request to update an image.
type ImageUpdateRequest struct {
	Name string `json:"name"`
}

type imageRoot struct {
	Image *Image
}

type imagesRoot struct {
	Images []Image
	Links  *Links `json:"links"`
}

type listImageOptions struct {
	Private bool   `url:"private,omitempty"`
	Type    string `url:"type,omitempty"`
}

func (i Image) String() string {
	return Stringify(i)
}

// List lists all the images available.
func (s *ImagesServiceOp) List(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) {
	return s.list(ctx, opt, nil)
}

// ListDistribution lists all the distribution images.
func (s *ImagesServiceOp) ListDistribution(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) {
	listOpt := listImageOptions{Type: "distribution"}
	return s.list(ctx, opt, &listOpt)
}

// ListApplication lists all the application images.
func (s *ImagesServiceOp) ListApplication(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) {
	listOpt := listImageOptions{Type: "application"}
	return s.list(ctx, opt, &listOpt)
}

// ListUser lists all the user images.
func (s *ImagesServiceOp) ListUser(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) {
	listOpt := listImageOptions{Private: true}
	return s.list(ctx, opt, &listOpt)
}

// GetByID retrieves an image by id.
func (s *ImagesServiceOp) GetByID(ctx context.Context, imageID int) (*Image, *Response, error) {
	if imageID < 1 {
		return nil, nil, NewArgError("imageID", "cannot be less than 1")
	}

	return s.get(ctx, interface{}(imageID))
}

// GetBySlug retrieves an image by slug.
func (s *ImagesServiceOp) GetBySlug(ctx context.Context, slug string) (*Image, *Response, error) {
	if len(slug) < 1 {
		return nil, nil, NewArgError("slug", "cannot be blank")
	}

	return s.get(ctx, interface{}(slug))
}

// Update an image name.
func (s *ImagesServiceOp) Update(ctx context.Context, imageID int, updateRequest *ImageUpdateRequest) (*Image, *Response, error) {
	if imageID < 1 {
		return nil, nil, NewArgError("imageID", "cannot be less than 1")
	}

	if updateRequest == nil {
		return nil, nil, NewArgError("updateRequest", "cannot be nil")
	}

	path := fmt.Sprintf("%s/%d", imageBasePath, imageID)
	req, err := s.client.NewRequest(ctx, "PUT", path, updateRequest)
	if err != nil {
		return nil, nil, err
	}

	root := new(imageRoot)
	resp, err := s.client.Do(ctx, req, root)
	if err != nil {
		return nil, resp, err
	}

	return root.Image, resp, err
}

// Delete an image.
func (s *ImagesServiceOp) Delete(ctx context.Context, imageID int) (*Response, error) {
	if imageID < 1 {
		return nil, NewArgError("imageID", "cannot be less than 1")
	}

	path := fmt.Sprintf("%s/%d", imageBasePath, imageID)

	req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
	if err != nil {
		return nil, err
	}

	resp, err := s.client.Do(ctx, req, nil)

	return resp, err
}

// Helper method for getting an individual image
func (s *ImagesServiceOp) get(ctx context.Context, ID interface{}) (*Image, *Response, error) {
	path := fmt.Sprintf("%s/%v", imageBasePath, ID)

	req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
	if err != nil {
		return nil, nil, err
	}

	root := new(imageRoot)
	resp, err := s.client.Do(ctx, req, root)
	if err != nil {
		return nil, resp, err
	}

	return root.Image, resp, err
}

// Helper method for listing images
func (s *ImagesServiceOp) list(ctx context.Context, opt *ListOptions, listOpt *listImageOptions) ([]Image, *Response, error) {
	path := imageBasePath
	path, err := addOptions(path, opt)
	if err != nil {
		return nil, nil, err
	}
	path, err = addOptions(path, listOpt)
	if err != nil {
		return nil, nil, err
	}

	req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
	if err != nil {
		return nil, nil, err
	}

	root := new(imagesRoot)
	resp, err := s.client.Do(ctx, req, root)
	if err != nil {
		return nil, resp, err
	}
	if l := root.Links; l != nil {
		resp.Links = l
	}

	return root.Images, resp, err
}


text:
package godo

import (
	"encoding/json"
	"fmt"
	"net/http"
	"reflect"
	"testing"
	"time"
)

func TestStorageVolumes_ListStorageVolumes(t *testing.T) {
	setup()
	defer teardown()

	jBlob := `
	{
		"volumes": [
			{
				"user_id": 42,
				"region": {"slug": "nyc3"},
				"id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
				"name": "my volume",
				"description": "my description",
				"size_gigabytes": 100,
				"droplet_ids": [10],
				"created_at": "2002-10-02T15:00:00.05Z"
			},
			{
				"user_id": 42,
				"region": {"slug": "nyc3"},
				"id": "96d414c6-295e-4e3a-ac59-eb9456c1e1d1",
				"name": "my other volume",
				"description": "my other description",
				"size_gigabytes": 100,
				"created_at": "2012-10-03T15:00:01.05Z"
			}
		],
		"links": {
	    "pages": {
	      "last": "https://api.digitalocean.com/v2/volumes?page=2",
	      "next": "https://api.digitalocean.com/v2/volumes?page=2"
	    }
	  },
	  "meta": {
	    "total": 28
	  }
	}`

	mux.HandleFunc("/v2/volumes/", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, jBlob)
	})

	volumes, _, err := client.Storage.ListVolumes(ctx, nil)
	if err != nil {
		t.Errorf("Storage.ListVolumes returned error: %v", err)
	}

	expected := []Volume{
		{
			Region:        &Region{Slug: "nyc3"},
			ID:            "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
			Name:          "my volume",
			Description:   "my description",
			SizeGigaBytes: 100,
			DropletIDs:    []int{10},
			CreatedAt:     time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC),
		},
		{
			Region:        &Region{Slug: "nyc3"},
			ID:            "96d414c6-295e-4e3a-ac59-eb9456c1e1d1",
			Name:          "my other volume",
			Description:   "my other description",
			SizeGigaBytes: 100,
			CreatedAt:     time.Date(2012, 10, 03, 15, 00, 01, 50000000, time.UTC),
		},
	}
	if !reflect.DeepEqual(volumes, expected) {
		t.Errorf("Storage.ListVolumes returned %+v, expected %+v", volumes, expected)
	}
}

func TestStorageVolumes_Get(t *testing.T) {
	setup()
	defer teardown()
	want := &Volume{
		Region:        &Region{Slug: "nyc3"},
		ID:            "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
		Name:          "my volume",
		Description:   "my description",
		SizeGigaBytes: 100,
		CreatedAt:     time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC),
	}
	jBlob := `{
		"volume":{
			"region": {"slug":"nyc3"},
			"attached_to_droplet": null,
			"id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
			"name": "my volume",
			"description": "my description",
			"size_gigabytes": 100,
			"created_at": "2002-10-02T15:00:00.05Z"
		},
		"links": {
	    "pages": {
	      "last": "https://api.digitalocean.com/v2/volumes?page=2",
	      "next": "https://api.digitalocean.com/v2/volumes?page=2"
	    }
	  },
	  "meta": {
	    "total": 28
	  }
	}`

	mux.HandleFunc("/v2/volumes/80d414c6-295e-4e3a-ac58-eb9456c1e1d1", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, jBlob)
	})

	got, _, err := client.Storage.GetVolume(ctx, "80d414c6-295e-4e3a-ac58-eb9456c1e1d1")
	if err != nil {
		t.Errorf("Storage.GetVolume returned error: %v", err)
	}
	if !reflect.DeepEqual(got, want) {
		t.Errorf("Storage.GetVolume returned %+v, want %+v", got, want)
	}
}

func TestStorageVolumes_ListVolumesByName(t *testing.T) {
	setup()
	defer teardown()

	jBlob :=
		`{
			"volumes": [
				{
					"region": {"slug": "nyc3"},
					"id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
					"name": "myvolume",
					"description": "my description",
					"size_gigabytes": 100,
					"droplet_ids": [10],
					"created_at": "2002-10-02T15:00:00.05Z"
				}
			],
			"links": {},
		  "meta": {
				"total": 1
			}
		}`

	expected := []Volume{
		{
			Region:        &Region{Slug: "nyc3"},
			ID:            "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
			Name:          "myvolume",
			Description:   "my description",
			SizeGigaBytes: 100,
			DropletIDs:    []int{10},
			CreatedAt:     time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC),
		},
	}

	mux.HandleFunc("/v2/volumes", func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Query().Get("name") != "myvolume" || r.URL.Query().Get("region") != "" {
			t.Errorf("Storage.ListVolumeByName did not request the correct name or region")
		}
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, jBlob)
	})

	options := &ListVolumeParams{
		Name: "myvolume",
	}
	volumes, _, err := client.Storage.ListVolumes(ctx, options)
	if err != nil {
		t.Errorf("Storage.ListVolumeByName returned error: %v", err)
	}

	if !reflect.DeepEqual(volumes, expected) {
		t.Errorf("Storage.ListVolumeByName returned %+v, expected %+v", volumes, expected)
	}
}

func TestStorageVolumes_ListVolumesByRegion(t *testing.T) {
	setup()
	defer teardown()

	jBlob :=
		`{
			"volumes": [
				{
					"region": {"slug": "nyc3"},
					"id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
					"name": "myvolume",
					"description": "my description",
					"size_gigabytes": 100,
					"droplet_ids": [10],
					"created_at": "2002-10-02T15:00:00.05Z"
				}
			],
			"links": {},
		  "meta": {
				"total": 1
			}
		}`

	expected := []Volume{
		{
			Region:        &Region{Slug: "nyc3"},
			ID:            "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
			Name:          "myvolume",
			Description:   "my description",
			SizeGigaBytes: 100,
			DropletIDs:    []int{10},
			CreatedAt:     time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC),
		},
	}

	mux.HandleFunc("/v2/volumes", func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Query().Get("region") != "nyc3" || r.URL.Query().Get("name") != "" {
			t.Errorf("Storage.ListVolumeByName did not request the correct name or region")
		}
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, jBlob)
	})

	options := &ListVolumeParams{
		Region: "nyc3",
	}
	volumes, _, err := client.Storage.ListVolumes(ctx, options)
	if err != nil {
		t.Errorf("Storage.ListVolumeByName returned error: %v", err)
	}

	if !reflect.DeepEqual(volumes, expected) {
		t.Errorf("Storage.ListVolumeByName returned %+v, expected %+v", volumes, expected)
	}
}

func TestStorageVolumes_ListVolumesByNameAndRegion(t *testing.T) {
	setup()
	defer teardown()

	jBlob :=
		`{
			"volumes": [
				{
					"region": {"slug": "nyc3"},
					"id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
					"name": "myvolume",
					"description": "my description",
					"size_gigabytes": 100,
					"droplet_ids": [10],
					"created_at": "2002-10-02T15:00:00.05Z"
				}
			],
			"links": {},
		  "meta": {
				"total": 1
			}
		}`

	expected := []Volume{
		{
			Region:        &Region{Slug: "nyc3"},
			ID:            "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
			Name:          "myvolume",
			Description:   "my description",
			SizeGigaBytes: 100,
			DropletIDs:    []int{10},
			CreatedAt:     time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC),
		},
	}

	mux.HandleFunc("/v2/volumes", func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Query().Get("region") != "nyc3" || r.URL.Query().Get("name") != "myvolume" {
			t.Errorf("Storage.ListVolumeByName did not request the correct name or region")
		}
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, jBlob)
	})

	options := &ListVolumeParams{
		Region: "nyc3",
		Name:   "myvolume",
	}
	volumes, _, err := client.Storage.ListVolumes(ctx, options)
	if err != nil {
		t.Errorf("Storage.ListVolumeByName returned error: %v", err)
	}

	if !reflect.DeepEqual(volumes, expected) {
		t.Errorf("Storage.ListVolumeByName returned %+v, expected %+v", volumes, expected)
	}
}

func TestStorageVolumes_Create(t *testing.T) {
	setup()
	defer teardown()

	createRequest := &VolumeCreateRequest{
		Region:        "nyc3",
		Name:          "my volume",
		Description:   "my description",
		SizeGigaBytes: 100,
	}

	want := &Volume{
		Region:        &Region{Slug: "nyc3"},
		ID:            "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
		Name:          "my volume",
		Description:   "my description",
		SizeGigaBytes: 100,
		CreatedAt:     time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC),
	}
	jBlob := `{
		"volume":{
			"region": {"slug":"nyc3"},
			"id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
			"name": "my volume",
			"description": "my description",
			"size_gigabytes": 100,
			"created_at": "2002-10-02T15:00:00.05Z"
		},
		"links": {}
	}`

	mux.HandleFunc("/v2/volumes", func(w http.ResponseWriter, r *http.Request) {
		v := new(VolumeCreateRequest)
		err := json.NewDecoder(r.Body).Decode(v)
		if err != nil {
			t.Fatal(err)
		}

		testMethod(t, r, http.MethodPost)
		if !reflect.DeepEqual(v, createRequest) {
			t.Errorf("Request body = %+v, expected %+v", v, createRequest)
		}

		fmt.Fprint(w, jBlob)
	})

	got, _, err := client.Storage.CreateVolume(ctx, createRequest)
	if err != nil {
		t.Errorf("Storage.CreateVolume returned error: %v", err)
	}
	if !reflect.DeepEqual(got, want) {
		t.Errorf("Storage.CreateVolume returned %+v, want %+v", got, want)
	}
}

func TestStorageVolumes_CreateFromSnapshot(t *testing.T) {
	setup()
	defer teardown()

	createRequest := &VolumeCreateRequest{
		Name:          "my-volume-from-a-snapshot",
		Description:   "my description",
		SizeGigaBytes: 100,
		SnapshotID:    "0d165eff-0b4c-11e7-9093-0242ac110207",
	}

	want := &Volume{
		Region:        &Region{Slug: "nyc3"},
		ID:            "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
		Name:          "my-volume-from-a-snapshot",
		Description:   "my description",
		SizeGigaBytes: 100,
		CreatedAt:     time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC),
	}
	jBlob := `{
		"volume":{
			"region": {"slug":"nyc3"},
			"id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
			"name": "my-volume-from-a-snapshot",
			"description": "my description",
			"size_gigabytes": 100,
			"created_at": "2002-10-02T15:00:00.05Z"
		},
		"links": {}
	}`

	mux.HandleFunc("/v2/volumes", func(w http.ResponseWriter, r *http.Request) {
		v := new(VolumeCreateRequest)
		err := json.NewDecoder(r.Body).Decode(v)
		if err != nil {
			t.Fatal(err)
		}

		testMethod(t, r, http.MethodPost)
		if !reflect.DeepEqual(v, createRequest) {
			t.Errorf("Request body = %+v, expected %+v", v, createRequest)
		}

		fmt.Fprint(w, jBlob)
	})

	got, _, err := client.Storage.CreateVolume(ctx, createRequest)
	if err != nil {
		t.Errorf("Storage.CreateVolume returned error: %v", err)
	}
	if !reflect.DeepEqual(got, want) {
		t.Errorf("Storage.CreateVolume returned %+v, want %+v", got, want)
	}
}

func TestStorageVolumes_Destroy(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/v2/volumes/80d414c6-295e-4e3a-ac58-eb9456c1e1d1", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodDelete)
	})

	_, err := client.Storage.DeleteVolume(ctx, "80d414c6-295e-4e3a-ac58-eb9456c1e1d1")
	if err != nil {
		t.Errorf("Storage.DeleteVolume returned error: %v", err)
	}
}

func TestStorageSnapshots_ListStorageSnapshots(t *testing.T) {
	setup()
	defer teardown()

	jBlob := `
	{
		"snapshots": [
			{
				"regions": ["nyc3"],
				"id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
				"name": "my snapshot",
				"size_gigabytes": 100,
				"created_at": "2002-10-02T15:00:00.05Z"
			},
			{
				"regions": ["nyc3"],
				"id": "96d414c6-295e-4e3a-ac59-eb9456c1e1d1",
				"name": "my other snapshot",
				"size_gigabytes": 100,
				"created_at": "2012-10-03T15:00:01.05Z"
			}
		],
		"links": {
	    "pages": {
	      "last": "https://api.digitalocean.com/v2/volumes?page=2",
	      "next": "https://api.digitalocean.com/v2/volumes?page=2"
	    }
	  },
	  "meta": {
	    "total": 28
	  }
	}`

	mux.HandleFunc("/v2/volumes/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, jBlob)
	})

	volumes, _, err := client.Storage.ListSnapshots(ctx, "98d414c6-295e-4e3a-ac58-eb9456c1e1d1", nil)
	if err != nil {
		t.Errorf("Storage.ListSnapshots returned error: %v", err)
	}

	expected := []Snapshot{
		{
			Regions:       []string{"nyc3"},
			ID:            "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
			Name:          "my snapshot",
			SizeGigaBytes: 100,
			Created:       "2002-10-02T15:00:00.05Z",
		},
		{
			Regions:       []string{"nyc3"},
			ID:            "96d414c6-295e-4e3a-ac59-eb9456c1e1d1",
			Name:          "my other snapshot",
			SizeGigaBytes: 100,
			Created:       "2012-10-03T15:00:01.05Z",
		},
	}
	if !reflect.DeepEqual(volumes, expected) {
		t.Errorf("Storage.ListSnapshots returned %+v, expected %+v", volumes, expected)
	}
}

func TestStorageSnapshots_Get(t *testing.T) {
	setup()
	defer teardown()
	want := &Snapshot{
		Regions:       []string{"nyc3"},
		ID:            "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
		Name:          "my snapshot",
		SizeGigaBytes: 100,
		Created:       "2002-10-02T15:00:00.05Z",
	}
	jBlob := `{
		"snapshot":{
			"regions": ["nyc3"],
			"id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
			"name": "my snapshot",
			"size_gigabytes": 100,
			"created_at": "2002-10-02T15:00:00.05Z"
		},
		"links": {
	    "pages": {
				"last": "https://api.digitalocean.com/v2/volumes/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots?page=2",
				"next": "https://api.digitalocean.com/v2/volumes/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots?page=2"
	    }
	  },
	  "meta": {
	    "total": 28
	  }
	}`

	mux.HandleFunc("/v2/snapshots/80d414c6-295e-4e3a-ac58-eb9456c1e1d1", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, jBlob)
	})

	got, _, err := client.Storage.GetSnapshot(ctx, "80d414c6-295e-4e3a-ac58-eb9456c1e1d1")
	if err != nil {
		t.Errorf("Storage.GetSnapshot returned error: %v", err)
	}
	if !reflect.DeepEqual(got, want) {
		t.Errorf("Storage.GetSnapshot returned %+v, want %+v", got, want)
	}
}

func TestStorageSnapshots_Create(t *testing.T) {
	setup()
	defer teardown()

	createRequest := &SnapshotCreateRequest{
		VolumeID:    "98d414c6-295e-4e3a-ac58-eb9456c1e1d1",
		Name:        "my snapshot",
		Description: "my description",
	}

	want := &Snapshot{
		Regions:       []string{"nyc3"},
		ID:            "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
		Name:          "my snapshot",
		SizeGigaBytes: 100,
		Created:       "2002-10-02T15:00:00.05Z",
	}
	jBlob := `{
		"snapshot":{
			"regions": ["nyc3"],
			"id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
			"name": "my snapshot",
			"description": "my description",
			"size_gigabytes": 100,
			"created_at": "2002-10-02T15:00:00.05Z"
		},
		"links": {
	    "pages": {
	      "last": "https://api.digitalocean.com/v2/volumes/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots?page=2",
	      "next": "https://api.digitalocean.com/v2/volumes/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots?page=2"
	    }
	  },
	  "meta": {
	    "total": 28
	  }
	}`

	mux.HandleFunc("/v2/volumes/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots", func(w http.ResponseWriter, r *http.Request) {
		v := new(SnapshotCreateRequest)
		err := json.NewDecoder(r.Body).Decode(v)
		if err != nil {
			t.Fatal(err)
		}

		testMethod(t, r, http.MethodPost)
		if !reflect.DeepEqual(v, createRequest) {
			t.Errorf("Request body = %+v, expected %+v", v, createRequest)
		}

		fmt.Fprint(w, jBlob)
	})

	got, _, err := client.Storage.CreateSnapshot(ctx, createRequest)
	if err != nil {
		t.Errorf("Storage.CreateSnapshot returned error: %v", err)
	}
	if !reflect.DeepEqual(got, want) {
		t.Errorf("Storage.CreateSnapshot returned %+v, want %+v", got, want)
	}
}

func TestStorageSnapshots_Destroy(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/v2/snapshots/80d414c6-295e-4e3a-ac58-eb9456c1e1d1", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodDelete)
	})

	_, err := client.Storage.DeleteSnapshot(ctx, "80d414c6-295e-4e3a-ac58-eb9456c1e1d1")
	if err != nil {
		t.Errorf("Storage.DeleteSnapshot returned error: %v", err)
	}
}


text:
package godo

import (
	"fmt"
	"net/http"
	"reflect"
	"testing"

	"github.com/digitalocean/godo/context"
)

func TestSnapshots_List(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/v2/snapshots", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, `{"snapshots":[{"id":"1"},{"id":"2", "size_gigabytes": 4.84}]}`)
	})
	ctx := context.Background()
	snapshots, _, err := client.Snapshots.List(ctx, nil)
	if err != nil {
		t.Errorf("Snapshots.List returned error: %v", err)
	}

	expected := []Snapshot{{ID: "1"}, {ID: "2", SizeGigaBytes: 4.84}}
	if !reflect.DeepEqual(snapshots, expected) {
		t.Errorf("Snapshots.List returned %+v, expected %+v", snapshots, expected)
	}
}

func TestSnapshots_ListVolume(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/v2/snapshots", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		expected := "volume"
		actual := r.URL.Query().Get("resource_type")
		if actual != expected {
			t.Errorf("'type' query = %v, expected %v", actual, expected)
		}
		fmt.Fprint(w, `{"snapshots":[{"id":"1"},{"id":"2"}]}`)
	})

	ctx := context.Background()
	snapshots, _, err := client.Snapshots.ListVolume(ctx, nil)
	if err != nil {
		t.Errorf("Snapshots.ListVolume returned error: %v", err)
	}

	expected := []Snapshot{{ID: "1"}, {ID: "2"}}
	if !reflect.DeepEqual(snapshots, expected) {
		t.Errorf("Snapshots.ListVolume returned %+v, expected %+v", snapshots, expected)
	}
}

func TestSnapshots_ListDroplet(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/v2/snapshots", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		expected := "droplet"
		actual := r.URL.Query().Get("resource_type")
		if actual != expected {
			t.Errorf("'resource_type' query = %v, expected %v", actual, expected)
		}

		fmt.Fprint(w, `{"snapshots":[{"id":"1"},{"id":"2", "size_gigabytes": 4.84}]}`)
	})

	ctx := context.Background()
	snapshots, _, err := client.Snapshots.ListDroplet(ctx, nil)
	if err != nil {
		t.Errorf("Snapshots.ListDroplet returned error: %v", err)
	}

	expected := []Snapshot{{ID: "1"}, {ID: "2", SizeGigaBytes: 4.84}}
	if !reflect.DeepEqual(snapshots, expected) {
		t.Errorf("Snapshots.ListDroplet returned %+v, expected %+v", snapshots, expected)
	}
}

func TestSnapshots_ListSnapshotsMultiplePages(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/v2/snapshots", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, `{"snapshots": [{"id":"1"},{"id":"2"}], "links":{"pages":{"next":"http://example.com/v2/snapshots/?page=2"}}}`)
	})

	ctx := context.Background()
	_, resp, err := client.Snapshots.List(ctx, &ListOptions{Page: 2})
	if err != nil {
		t.Fatal(err)
	}
	checkCurrentPage(t, resp, 1)
}

func TestSnapshots_RetrievePageByNumber(t *testing.T) {
	setup()
	defer teardown()

	jBlob := `
    {
        "snapshots": [{"id":"1"},{"id":"2"}],
        "links":{
            "pages":{
                "next":"http://example.com/v2/snapshots/?page=3",
                "prev":"http://example.com/v2/snapshots/?page=1",
                "last":"http://example.com/v2/snapshots/?page=3",
                "first":"http://example.com/v2/snapshots/?page=1"
            }
        }
    }`

	mux.HandleFunc("/v2/snapshots", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, jBlob)
	})

	ctx := context.Background()
	opt := &ListOptions{Page: 2}
	_, resp, err := client.Snapshots.List(ctx, opt)
	if err != nil {
		t.Fatal(err)
	}

	checkCurrentPage(t, resp, 2)
}

func TestSnapshots_GetSnapshotByID(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/v2/snapshots/12345", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, `{"snapshot":{"id":"12345"}}`)
	})

	ctx := context.Background()
	snapshots, _, err := client.Snapshots.Get(ctx, "12345")
	if err != nil {
		t.Errorf("Snapshot.GetByID returned error: %v", err)
	}

	expected := &Snapshot{ID: "12345"}
	if !reflect.DeepEqual(snapshots, expected) {
		t.Errorf("Snapshots.GetByID returned %+v, expected %+v", snapshots, expected)
	}
}

func TestSnapshots_Destroy(t *testing.T) {
	setup()
	defer teardown()

	mux.HandleFunc("/v2/snapshots/12345", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodDelete)
	})

	ctx := context.Background()
	_, err := client.Snapshots.Delete(ctx, "12345")
	if err != nil {
		t.Errorf("Snapshot.Delete returned error: %v", err)
	}
}

func TestSnapshot_String(t *testing.T) {
	snapshot := &Snapshot{
		ID:            "1",
		Name:          "Snapsh176ot",
		ResourceID:    "0",
		ResourceType:  "droplet",
		Regions:       []string{"one"},
		MinDiskSize:   20,
		SizeGigaBytes: 4.84,
		Created:       "2013-11-27T09:24:55Z",
	}

	stringified := snapshot.String()
	expected := `godo.Snapshot{ID:"1", Name:"Snapsh176ot", ResourceID:"0", ResourceType:"droplet", Regions:["one"], MinDiskSize:20, SizeGigaBytes:4.84, Created:"2013-11-27T09:24:55Z"}`
	if expected != stringified {
		t.Errorf("Snapshot.String returned %+v, expected %+v", stringified, expected)
	}
}


text:
package godo

import (
	"net/http"
	"path"

	"github.com/digitalocean/godo/context"
)

const certificatesBasePath = "/v2/certificates"

// CertificatesService is an interface for managing certificates with the DigitalOcean API.
// See: https://developers.digitalocean.com/documentation/v2/#certificates
type CertificatesService interface {
	Get(context.Context, string) (*Certificate, *Response, error)
	List(context.Context, *ListOptions) ([]Certificate, *Response, error)
	Create(context.Context, *CertificateRequest) (*Certificate, *Response, error)
	Delete(context.Context, string) (*Response, error)
}

// Certificate represents a DigitalOcean certificate configuration.
type Certificate struct {
	ID              string `json:"id,omitempty"`
	Name            string `json:"name,omitempty"`
	NotAfter        string `json:"not_after,omitempty"`
	SHA1Fingerprint string `json:"sha1_fingerprint,omitempty"`
	Created         string `json:"created_at,omitempty"`
}

// CertificateRequest represents configuration for a new certificate.
type CertificateRequest struct {
	Name             string `json:"name,omitempty"`
	PrivateKey       string `json:"private_key,omitempty"`
	LeafCertificate  string `json:"leaf_certificate,omitempty"`
	CertificateChain string `json:"certificate_chain,omitempty"`
}

type certificateRoot struct {
	Certificate *Certificate `json:"certificate"`
}

type certificatesRoot struct {
	Certificates []Certificate `json:"certificates"`
	Links        *Links        `json:"links"`
}

// CertificatesServiceOp handles communication with certificates methods of the DigitalOcean API.
type CertificatesServiceOp struct {
	client *Client
}

var _ CertificatesService = &CertificatesServiceOp{}

// Get an existing certificate by its identifier.
func (c *CertificatesServiceOp) Get(ctx context.Context, cID string) (*Certificate, *Response, error) {
	urlStr := path.Join(certificatesBasePath, cID)

	req, err := c.client.NewRequest(ctx, http.MethodGet, urlStr, nil)
	if err != nil {
		return nil, nil, err
	}

	root := new(certificateRoot)
	resp, err := c.client.Do(ctx, req, root)
	if err != nil {
		return nil, resp, err
	}

	return root.Certificate, resp, nil
}

// List all certificates.
func (c *CertificatesServiceOp) List(ctx context.Context, opt *ListOptions) ([]Certificate, *Response, error) {
	urlStr, err := addOptions(certificatesBasePath, opt)
	if err != nil {
		return nil, nil, err
	}

	req, err := c.client.NewRequest(ctx, http.MethodGet, urlStr, nil)
	if err != nil {
		return nil, nil, err
	}

	root := new(certificatesRoot)
	resp, err := c.client.Do(ctx, req, root)
	if err != nil {
		return nil, resp, err
	}
	if l := root.Links; l != nil {
		resp.Links = l
	}

	return root.Certificates, resp, nil
}

// Create a new certificate with provided configuration.
func (c *CertificatesServiceOp) Create(ctx context.Context, cr *CertificateRequest) (*Certificate, *Response, error) {
	req, err := c.client.NewRequest(ctx, http.MethodPost, certificatesBasePath, cr)
	if err != nil {
		return nil, nil, err
	}

	root := new(certificateRoot)
	resp, err := c.client.Do(ctx, req, root)
	if err != nil {
		return nil, resp, err
	}

	return root.Certificate, resp, nil
}

// Delete a certificate by its identifier.
func (c *CertificatesServiceOp) Delete(ctx context.Context, cID string) (*Response, error) {
	urlStr := path.Join(certificatesBasePath, cID)

	req, err := c.client.NewRequest(ctx, http.MethodDelete, urlStr, nil)
	if err != nil {
		return nil, err
	}

	return c.client.Do(ctx, req, nil)
}


text:
package godo

import (
	"fmt"
	"net/http"

	"github.com/digitalocean/godo/context"
)

const keysBasePath = "v2/account/keys"

// KeysService is an interface for interfacing with the keys
// endpoints of the DigitalOcean API
// See: https://developers.digitalocean.com/documentation/v2#keys
type KeysService interface {
	List(context.Context, *ListOptions) ([]Key, *Response, error)
	GetByID(context.Context, int) (*Key, *Response, error)
	GetByFingerprint(context.Context, string) (*Key, *Response, error)
	Create(context.Context, *KeyCreateRequest) (*Key, *Response, error)
	UpdateByID(context.Context, int, *KeyUpdateRequest) (*Key, *Response, error)
	UpdateByFingerprint(context.Context, string, *KeyUpdateRequest) (*Key, *Response, error)
	DeleteByID(context.Context, int) (*Response, error)
	DeleteByFingerprint(context.Context, string) (*Response, error)
}

// KeysServiceOp handles communication with key related method of the
// DigitalOcean API.
type KeysServiceOp struct {
	client *Client
}

var _ KeysService = &KeysServiceOp{}

// Key represents a DigitalOcean Key.
type Key struct {
	ID          int    `json:"id,float64,omitempty"`
	Name        string `json:"name,omitempty"`
	Fingerprint string `json:"fingerprint,omitempty"`
	PublicKey   string `json:"public_key,omitempty"`
}

// KeyUpdateRequest represents a request to update a DigitalOcean key.
type KeyUpdateRequest struct {
	Name string `json:"name"`
}

type keysRoot struct {
	SSHKeys []Key  `json:"ssh_keys"`
	Links   *Links `json:"links"`
}

type keyRoot struct {
	SSHKey *Key `json:"ssh_key"`
}

func (s Key) String() string {
	return Stringify(s)
}

// KeyCreateRequest represents a request to create a new key.
type KeyCreateRequest struct {
	Name      string `json:"name"`
	PublicKey string `json:"public_key"`
}

// List all keys
func (s *KeysServiceOp) List(ctx context.Context, opt *ListOptions) ([]Key, *Response, error) {
	path := keysBasePath
	path, err := addOptions(path, opt)
	if err != nil {
		return nil, nil, err
	}

	req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
	if err != nil {
		return nil, nil, err
	}

	root := new(keysRoot)
	resp, err := s.client.Do(ctx, req, root)
	if err != nil {
		return nil, resp, err
	}
	if l := root.Links; l != nil {
		resp.Links = l
	}

	return root.SSHKeys, resp, err
}

// Performs a get given a path
func (s *KeysServiceOp) get(ctx context.Context, path string) (*Key, *Response, error) {
	req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
	if err != nil {
		return nil, nil, err
	}

	root := new(keyRoot)
	resp, err := s.client.Do(ctx, req, root)
	if err != nil {
		return nil, resp, err
	}

	return root.SSHKey, resp, err
}

// GetByID gets a Key by id
func (s *KeysServiceOp) GetByID(ctx context.Context, keyID int) (*Key, *Response, error) {
	if keyID < 1 {
		return nil, nil, NewArgError("keyID", "cannot be less than 1")
	}

	path := fmt.Sprintf("%s/%d", keysBasePath, keyID)
	return s.get(ctx, path)
}

// GetByFingerprint gets a Key by by fingerprint
func (s *KeysServiceOp) GetByFingerprint(ctx context.Context, fingerprint string) (*Key, *Response, error) {
	if len(fingerprint) < 1 {
		return nil, nil, NewArgError("fingerprint", "cannot not be empty")
	}

	path := fmt.Sprintf("%s/%s", keysBasePath, fingerprint)
	return s.get(ctx, path)
}

// Create a key using a KeyCreateRequest
func (s *KeysServiceOp) Create(ctx context.Context, createRequest *KeyCreateRequest) (*Key, *Response, error) {
	if createRequest == nil {
		return nil, nil, NewArgError("createRequest", "cannot be nil")
	}

	req, err := s.client.NewRequest(ctx, http.MethodPost, keysBasePath, createRequest)
	if err != nil {
		return nil, nil, err
	}

	root := new(keyRoot)
	resp, err := s.client.Do(ctx, req, root)
	if err != nil {
		return nil, resp, err
	}

	return root.SSHKey, resp, err
}

// UpdateByID updates a key name by ID.
func (s *KeysServiceOp) UpdateByID(ctx context.Context, keyID int, updateRequest *KeyUpdateRequest) (*Key, *Response, error) {
	if keyID < 1 {
		return nil, nil, NewArgError("keyID", "cannot be less than 1")
	}

	if updateRequest == nil {
		return nil, nil, NewArgError("updateRequest", "cannot be nil")
	}

	path := fmt.Sprintf("%s/%d", keysBasePath, keyID)
	req, err := s.client.NewRequest(ctx, "PUT", path, updateRequest)
	if err != nil {
		return nil, nil, err
	}

	root := new(keyRoot)
	resp, err := s.client.Do(ctx, req, root)
	if err != nil {
		return nil, resp, err
	}

	return root.SSHKey, resp, err
}

// UpdateByFingerprint updates a key name by fingerprint.
func (s *KeysServiceOp) UpdateByFingerprint(ctx context.Context, fingerprint string, updateRequest *KeyUpdateRequest) (*Key, *Response, error) {
	if len(fingerprint) < 1 {
		return nil, nil, NewArgError("fingerprint", "cannot be empty")
	}

	if updateRequest == nil {
		return nil, nil, NewArgError("updateRequest", "cannot be nil")
	}

	path := fmt.Sprintf("%s/%s", keysBasePath, fingerprint)
	req, err := s.client.NewRequest(ctx, "PUT", path, updateRequest)
	if err != nil {
		return nil, nil, err
	}

	root := new(keyRoot)
	resp, err := s.client.Do(ctx, req, root)
	if err != nil {
		return nil, resp, err
	}

	return root.SSHKey, resp, err
}

// Delete key using a path
func (s *KeysServiceOp) delete(ctx context.Context, path string) (*Response, error) {
	req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
	if err != nil {
		return nil, err
	}

	resp, err := s.client.Do(ctx, req, nil)

	return resp, err
}

// DeleteByID deletes a key by its id
func (s *KeysServiceOp) DeleteByID(ctx context.Context, keyID int) (*Response, error) {
	if keyID < 1 {
		return nil, NewArgError("keyID", "cannot be less than 1")
	}

	path := fmt.Sprintf("%s/%d", keysBasePath, keyID)
	return s.delete(ctx, path)
}

// DeleteByFingerprint deletes a key by its fingerprint
func (s *KeysServiceOp) DeleteByFingerprint(ctx context.Context, fingerprint string) (*Response, error) {
	if len(fingerprint) < 1 {
		return nil, NewArgError("fingerprint", "cannot be empty")
	}

	path := fmt.Sprintf("%s/%s", keysBasePath, fingerprint)
	return s.delete(ctx, path)
}