text:
package godo

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

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

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

	actions, _, err := client.Actions.List(ctx, nil)
	if err != nil {
		t.Fatalf("unexpected error: %s", err)
	}

	expected := []Action{{ID: 1}, {ID: 2}}
	if len(actions) != len(expected) || actions[0].ID != expected[0].ID || actions[1].ID != expected[1].ID {
		t.Fatalf("unexpected response")
	}
}

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

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

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

	checkCurrentPage(t, resp, 1)
}

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

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

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

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

	checkCurrentPage(t, resp, 2)
}

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

	mux.HandleFunc("/v2/actions/12345", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, `{"action": {"id":12345,"region":{"name":"name","slug":"slug","available":true,"sizes":["512mb"],"features":["virtio"]},"region_slug":"slug"}}`)
		testMethod(t, r, http.MethodGet)
	})

	action, _, err := client.Actions.Get(ctx, 12345)
	if err != nil {
		t.Fatalf("unexpected error: %s", err)
	}

	if action.ID != 12345 {
		t.Fatalf("unexpected response")
	}

	region := &Region{
		Name:      "name",
		Slug:      "slug",
		Available: true,
		Sizes:     []string{"512mb"},
		Features:  []string{"virtio"},
	}
	if !reflect.DeepEqual(action.Region, region) {
		t.Fatalf("unexpected response, invalid region")
	}

	if action.RegionSlug != "slug" {
		t.Fatalf("unexpected response, invalid region slug")
	}
}

func TestAction_String(t *testing.T) {
	pt, err := time.Parse(time.RFC3339, "2014-05-08T20:36:47Z")
	if err != nil {
		t.Fatalf("unexpected error: %s", err)
	}

	startedAt := &Timestamp{
		Time: pt,
	}
	action := &Action{
		ID:        1,
		Status:    "in-progress",
		Type:      "transfer",
		StartedAt: startedAt,
	}

	stringified := action.String()
	expected := `godo.Action{ID:1, Status:"in-progress", Type:"transfer", ` +
		`StartedAt:godo.Timestamp{2014-05-08 20:36:47 +0000 UTC}, ` +
		`ResourceID:0, ResourceType:"", RegionSlug:""}`
	if expected != stringified {
		t.Errorf("Action.Stringify returned %+v, expected %+v", stringified, expected)
	}
}


text:
package godo

import (
	"fmt"
	"net/http"

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

// StorageActionsService is an interface for interfacing with the
// storage actions endpoints of the Digital Ocean API.
// See: https://developers.digitalocean.com/documentation/v2#storage-actions
type StorageActionsService interface {
	Attach(ctx context.Context, volumeID string, dropletID int) (*Action, *Response, error)
	DetachByDropletID(ctx context.Context, volumeID string, dropletID int) (*Action, *Response, error)
	Get(ctx context.Context, volumeID string, actionID int) (*Action, *Response, error)
	List(ctx context.Context, volumeID string, opt *ListOptions) ([]Action, *Response, error)
	Resize(ctx context.Context, volumeID string, sizeGigabytes int, regionSlug string) (*Action, *Response, error)
}

// StorageActionsServiceOp handles communication with the storage volumes
// action related methods of the DigitalOcean API.
type StorageActionsServiceOp struct {
	client *Client
}

// StorageAttachment represents the attachement of a block storage
// volume to a specific Droplet under the device name.
type StorageAttachment struct {
	DropletID int `json:"droplet_id"`
}

// Attach a storage volume to a Droplet.
func (s *StorageActionsServiceOp) Attach(ctx context.Context, volumeID string, dropletID int) (*Action, *Response, error) {
	request := &ActionRequest{
		"type":       "attach",
		"droplet_id": dropletID,
	}
	return s.doAction(ctx, volumeID, request)
}

// DetachByDropletID a storage volume from a Droplet by Droplet ID.
func (s *StorageActionsServiceOp) DetachByDropletID(ctx context.Context, volumeID string, dropletID int) (*Action, *Response, error) {
	request := &ActionRequest{
		"type":       "detach",
		"droplet_id": dropletID,
	}
	return s.doAction(ctx, volumeID, request)
}

// Get an action for a particular storage volume by id.
func (s *StorageActionsServiceOp) Get(ctx context.Context, volumeID string, actionID int) (*Action, *Response, error) {
	path := fmt.Sprintf("%s/%d", storageAllocationActionPath(volumeID), actionID)
	return s.get(ctx, path)
}

// List the actions for a particular storage volume.
func (s *StorageActionsServiceOp) List(ctx context.Context, volumeID string, opt *ListOptions) ([]Action, *Response, error) {
	path := storageAllocationActionPath(volumeID)
	path, err := addOptions(path, opt)
	if err != nil {
		return nil, nil, err
	}

	return s.list(ctx, path)
}

// Resize a storage volume.
func (s *StorageActionsServiceOp) Resize(ctx context.Context, volumeID string, sizeGigabytes int, regionSlug string) (*Action, *Response, error) {
	request := &ActionRequest{
		"type":           "resize",
		"size_gigabytes": sizeGigabytes,
		"region":         regionSlug,
	}
	return s.doAction(ctx, volumeID, request)
}

func (s *StorageActionsServiceOp) doAction(ctx context.Context, volumeID string, request *ActionRequest) (*Action, *Response, error) {
	path := storageAllocationActionPath(volumeID)

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

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

	return root.Event, resp, err
}

func (s *StorageActionsServiceOp) get(ctx context.Context, path string) (*Action, *Response, error) {
	req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
	if err != nil {
		return nil, nil, err
	}

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

	return root.Event, resp, err
}

func (s *StorageActionsServiceOp) list(ctx context.Context, path string) ([]Action, *Response, error) {
	req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
	if err != nil {
		return nil, nil, err
	}

	root := new(actionsRoot)
	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.Actions, resp, err
}

func storageAllocationActionPath(volumeID string) string {
	return fmt.Sprintf("%s/%s/actions", storageAllocPath, volumeID)
}

repo_name: dan-v/dosxvpn

repo_language: Go

mime_type: text/x-shellscript


text:
package godo

import (
	"net/http"

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

// RegionsService is an interface for interfacing with the regions
// endpoints of the DigitalOcean API
// See: https://developers.digitalocean.com/documentation/v2#regions
type RegionsService interface {
	List(context.Context, *ListOptions) ([]Region, *Response, error)
}

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

var _ RegionsService = &RegionsServiceOp{}

// Region represents a DigitalOcean Region
type Region struct {
	Slug      string   `json:"slug,omitempty"`
	Name      string   `json:"name,omitempty"`
	Sizes     []string `json:"sizes,omitempty"`
	Available bool     `json:"available,omitempty"`
	Features  []string `json:"features,omitempty"`
}

type regionsRoot struct {
	Regions []Region
	Links   *Links `json:"links"`
}

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

// List all regions
func (s *RegionsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Region, *Response, error) {
	path := "v2/regions"
	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(regionsRoot)
	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.Regions, resp, err
}


text:
package godo

import (
	"net/http"
	"path"
	"strconv"

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

const firewallsBasePath = "/v2/firewalls"

// FirewallsService is an interface for managing Firewalls with the DigitalOcean API.
// See: https://developers.digitalocean.com/documentation/documentation/v2/#firewalls
type FirewallsService interface {
	Get(context.Context, string) (*Firewall, *Response, error)
	Create(context.Context, *FirewallRequest) (*Firewall, *Response, error)
	Update(context.Context, string, *FirewallRequest) (*Firewall, *Response, error)
	Delete(context.Context, string) (*Response, error)
	List(context.Context, *ListOptions) ([]Firewall, *Response, error)
	ListByDroplet(context.Context, int, *ListOptions) ([]Firewall, *Response, error)
	AddDroplets(context.Context, string, ...int) (*Response, error)
	RemoveDroplets(context.Context, string, ...int) (*Response, error)
	AddTags(context.Context, string, ...string) (*Response, error)
	RemoveTags(context.Context, string, ...string) (*Response, error)
	AddRules(context.Context, string, *FirewallRulesRequest) (*Response, error)
	RemoveRules(context.Context, string, *FirewallRulesRequest) (*Response, error)
}

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

// Firewall represents a DigitalOcean Firewall configuration.
type Firewall struct {
	ID             string          `json:"id"`
	Name           string          `json:"name"`
	Status         string          `json:"status"`
	InboundRules   []InboundRule   `json:"inbound_rules"`
	OutboundRules  []OutboundRule  `json:"outbound_rules"`
	DropletIDs     []int           `json:"droplet_ids"`
	Tags           []string        `json:"tags"`
	Created        string          `json:"created_at"`
	PendingChanges []PendingChange `json:"pending_changes"`
}

// String creates a human-readable description of a Firewall.
func (fw Firewall) String() string {
	return Stringify(fw)
}

// FirewallRequest represents the configuration to be applied to an existing or a new Firewall.
type FirewallRequest struct {
	Name          string         `json:"name"`
	InboundRules  []InboundRule  `json:"inbound_rules"`
	OutboundRules []OutboundRule `json:"outbound_rules"`
	DropletIDs    []int          `json:"droplet_ids"`
	Tags          []string       `json:"tags"`
}

// FirewallRulesRequest represents rules configuration to be applied to an existing Firewall.
type FirewallRulesRequest struct {
	InboundRules  []InboundRule  `json:"inbound_rules"`
	OutboundRules []OutboundRule `json:"outbound_rules"`
}

// InboundRule represents a DigitalOcean Firewall inbound rule.
type InboundRule struct {
	Protocol  string   `json:"protocol,omitempty"`
	PortRange string   `json:"ports,omitempty"`
	Sources   *Sources `json:"sources"`
}

// OutboundRule represents a DigitalOcean Firewall outbound rule.
type OutboundRule struct {
	Protocol     string        `json:"protocol,omitempty"`
	PortRange    string        `json:"ports,omitempty"`
	Destinations *Destinations `json:"destinations"`
}

// Sources represents a DigitalOcean Firewall InboundRule sources.
type Sources struct {
	Addresses        []string `json:"addresses,omitempty"`
	Tags             []string `json:"tags,omitempty"`
	DropletIDs       []int    `json:"droplet_ids,omitempty"`
	LoadBalancerUIDs []string `json:"load_balancer_uids,omitempty"`
}

// PendingChange represents a DigitalOcean Firewall status details.
type PendingChange struct {
	DropletID int    `json:"droplet_id,omitempty"`
	Removing  bool   `json:"removing,omitempty"`
	Status    string `json:"status,omitempty"`
}

// Destinations represents a DigitalOcean Firewall OutboundRule destinations.
type Destinations struct {
	Addresses        []string `json:"addresses,omitempty"`
	Tags             []string `json:"tags,omitempty"`
	DropletIDs       []int    `json:"droplet_ids,omitempty"`
	LoadBalancerUIDs []string `json:"load_balancer_uids,omitempty"`
}

var _ FirewallsService = &FirewallsServiceOp{}

// Get an existing Firewall by its identifier.
func (fw *FirewallsServiceOp) Get(ctx context.Context, fID string) (*Firewall, *Response, error) {
	path := path.Join(firewallsBasePath, fID)

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

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

	return root.Firewall, resp, err
}

// Create a new Firewall with a given configuration.
func (fw *FirewallsServiceOp) Create(ctx context.Context, fr *FirewallRequest) (*Firewall, *Response, error) {
	req, err := fw.client.NewRequest(ctx, http.MethodPost, firewallsBasePath, fr)
	if err != nil {
		return nil, nil, err
	}

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

	return root.Firewall, resp, err
}

// Update an existing Firewall with new configuration.
func (fw *FirewallsServiceOp) Update(ctx context.Context, fID string, fr *FirewallRequest) (*Firewall, *Response, error) {
	path := path.Join(firewallsBasePath, fID)

	req, err := fw.client.NewRequest(ctx, "PUT", path, fr)
	if err != nil {
		return nil, nil, err
	}

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

	return root.Firewall, resp, err
}

// Delete a Firewall by its identifier.
func (fw *FirewallsServiceOp) Delete(ctx context.Context, fID string) (*Response, error) {
	path := path.Join(firewallsBasePath, fID)
	return fw.createAndDoReq(ctx, http.MethodDelete, path, nil)
}

// List Firewalls.
func (fw *FirewallsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Firewall, *Response, error) {
	path, err := addOptions(firewallsBasePath, opt)
	if err != nil {
		return nil, nil, err
	}

	return fw.listHelper(ctx, path)
}

// ListByDroplet Firewalls.
func (fw *FirewallsServiceOp) ListByDroplet(ctx context.Context, dID int, opt *ListOptions) ([]Firewall, *Response, error) {
	basePath := path.Join(dropletBasePath, strconv.Itoa(dID), "firewalls")
	path, err := addOptions(basePath, opt)
	if err != nil {
		return nil, nil, err
	}

	return fw.listHelper(ctx, path)
}

// AddDroplets to a Firewall.
func (fw *FirewallsServiceOp) AddDroplets(ctx context.Context, fID string, dropletIDs ...int) (*Response, error) {
	path := path.Join(firewallsBasePath, fID, "droplets")
	return fw.createAndDoReq(ctx, http.MethodPost, path, &dropletsRequest{IDs: dropletIDs})
}

// RemoveDroplets from a Firewall.
func (fw *FirewallsServiceOp) RemoveDroplets(ctx context.Context, fID string, dropletIDs ...int) (*Response, error) {
	path := path.Join(firewallsBasePath, fID, "droplets")
	return fw.createAndDoReq(ctx, http.MethodDelete, path, &dropletsRequest{IDs: dropletIDs})
}

// AddTags to a Firewall.
func (fw *FirewallsServiceOp) AddTags(ctx context.Context, fID string, tags ...string) (*Response, error) {
	path := path.Join(firewallsBasePath, fID, "tags")
	return fw.createAndDoReq(ctx, http.MethodPost, path, &tagsRequest{Tags: tags})
}

// RemoveTags from a Firewall.
func (fw *FirewallsServiceOp) RemoveTags(ctx context.Context, fID string, tags ...string) (*Response, error) {
	path := path.Join(firewallsBasePath, fID, "tags")
	return fw.createAndDoReq(ctx, http.MethodDelete, path, &tagsRequest{Tags: tags})
}

// AddRules to a Firewall.
func (fw *FirewallsServiceOp) AddRules(ctx context.Context, fID string, rr *FirewallRulesRequest) (*Response, error) {
	path := path.Join(firewallsBasePath, fID, "rules")
	return fw.createAndDoReq(ctx, http.MethodPost, path, rr)
}

// RemoveRules from a Firewall.
func (fw *FirewallsServiceOp) RemoveRules(ctx context.Context, fID string, rr *FirewallRulesRequest) (*Response, error) {
	path := path.Join(firewallsBasePath, fID, "rules")
	return fw.createAndDoReq(ctx, http.MethodDelete, path, rr)
}

type dropletsRequest struct {
	IDs []int `json:"droplet_ids"`
}

type tagsRequest struct {
	Tags []string `json:"tags"`
}

type firewallRoot struct {
	Firewall *Firewall `json:"firewall"`
}

type firewallsRoot struct {
	Firewalls []Firewall `json:"firewalls"`
	Links     *Links     `json:"links"`
}

func (fw *FirewallsServiceOp) createAndDoReq(ctx context.Context, method, path string, v interface{}) (*Response, error) {
	req, err := fw.client.NewRequest(ctx, method, path, v)
	if err != nil {
		return nil, err
	}

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

func (fw *FirewallsServiceOp) listHelper(ctx context.Context, path string) ([]Firewall, *Response, error) {
	req, err := fw.client.NewRequest(ctx, http.MethodGet, path, nil)
	if err != nil {
		return nil, nil, err
	}

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

	return root.Firewalls, resp, err
}


text:
package godo

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

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

	mux.HandleFunc("/v2/domains", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, `{"domains": [{"name":"foo.com"},{"name":"bar.com"}]}`)
	})

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

	expected := []Domain{{Name: "foo.com"}, {Name: "bar.com"}}
	if !reflect.DeepEqual(domains, expected) {
		t.Errorf("Domains.List returned %+v, expected %+v", domains, expected)
	}
}

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

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

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

	checkCurrentPage(t, resp, 1)
}

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

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

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

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

	checkCurrentPage(t, resp, 2)
}

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

	mux.HandleFunc("/v2/domains/example.com", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, `{"domain":{"name":"example.com"}}`)
	})

	domains, _, err := client.Domains.Get(ctx, "example.com")
	if err != nil {
		t.Errorf("domain.Get returned error: %v", err)
	}

	expected := &Domain{Name: "example.com"}
	if !reflect.DeepEqual(domains, expected) {
		t.Errorf("domains.Get returned %+v, expected %+v", domains, expected)
	}
}

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

	createRequest := &DomainCreateRequest{
		Name:      "example.com",
		IPAddress: "127.0.0.1",
	}

	mux.HandleFunc("/v2/domains", func(w http.ResponseWriter, r *http.Request) {
		v := new(DomainCreateRequest)
		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, `{"domain":{"name":"example.com"}}`)
	})

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

	expected := &Domain{Name: "example.com"}
	if !reflect.DeepEqual(domain, expected) {
		t.Errorf("Domains.Create returned %+v, expected %+v", domain, expected)
	}
}

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

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

	_, err := client.Domains.Delete(ctx, "example.com")
	if err != nil {
		t.Errorf("Domains.Delete returned error: %v", err)
	}
}

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

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

	records, _, err := client.Domains.Records(ctx, "example.com", nil)
	if err != nil {
		t.Errorf("Domains.List returned error: %v", err)
	}

	expected := []DomainRecord{{ID: 1}, {ID: 2}}
	if !reflect.DeepEqual(records, expected) {
		t.Errorf("Domains.List returned %+v, expected %+v", records, expected)
	}
}

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

	mux.HandleFunc("/v2/domains/example.com/records", func(w http.ResponseWriter, r *http.Request) {
		perPage := r.URL.Query().Get("per_page")
		if perPage != "2" {
			t.Fatalf("expected '2', got '%s'", perPage)
		}

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

	dro := &ListOptions{PerPage: 2}
	records, _, err := client.Domains.Records(ctx, "example.com", dro)
	if err != nil {
		t.Errorf("Domains.List returned error: %v", err)
	}

	expected := []DomainRecord{{ID: 1}, {ID: 2}}
	if !reflect.DeepEqual(records, expected) {
		t.Errorf("Domains.List returned %+v, expected %+v", records, expected)
	}
}

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

	mux.HandleFunc("/v2/domains/example.com/records/1", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, `{"domain_record":{"id":1}}`)
	})

	record, _, err := client.Domains.Record(ctx, "example.com", 1)
	if err != nil {
		t.Errorf("Domains.GetRecord returned error: %v", err)
	}

	expected := &DomainRecord{ID: 1}
	if !reflect.DeepEqual(record, expected) {
		t.Errorf("Domains.GetRecord returned %+v, expected %+v", record, expected)
	}
}

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

	mux.HandleFunc("/v2/domains/example.com/records/1", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodDelete)
	})

	_, err := client.Domains.DeleteRecord(ctx, "example.com", 1)
	if err != nil {
		t.Errorf("Domains.RecordDelete returned error: %v", err)
	}
}

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

	createRequest := &DomainRecordEditRequest{
		Type:     "CNAME",
		Name:     "example",
		Data:     "@",
		Priority: 10,
		Port:     10,
		TTL:      1800,
		Weight:   10,
		Flags:    1,
		Tag:      "test",
	}

	mux.HandleFunc("/v2/domains/example.com/records",
		func(w http.ResponseWriter, r *http.Request) {
			v := new(DomainRecordEditRequest)
			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, `{"domain_record": {"id":1}}`)
		})

	record, _, err := client.Domains.CreateRecord(ctx, "example.com", createRequest)
	if err != nil {
		t.Errorf("Domains.CreateRecord returned error: %v", err)
	}

	expected := &DomainRecord{ID: 1}
	if !reflect.DeepEqual(record, expected) {
		t.Errorf("Domains.CreateRecord returned %+v, expected %+v", record, expected)
	}
}

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

	editRequest := &DomainRecordEditRequest{
		Type:     "CNAME",
		Name:     "example",
		Data:     "@",
		Priority: 10,
		Port:     10,
		TTL:      1800,
		Weight:   10,
		Flags:    1,
		Tag:      "test",
	}

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

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

		fmt.Fprintf(w, `{"id":1}`)
	})

	record, _, err := client.Domains.EditRecord(ctx, "example.com", 1, editRequest)
	if err != nil {
		t.Errorf("Domains.EditRecord returned error: %v", err)
	}

	expected := &DomainRecord{ID: 1}
	if !reflect.DeepEqual(record, expected) {
		t.Errorf("Domains.EditRecord returned %+v, expected %+v", record, expected)
	}
}

func TestDomainRecord_String(t *testing.T) {
	record := &DomainRecord{
		ID:       1,
		Type:     "CNAME",
		Name:     "example",
		Data:     "@",
		Priority: 10,
		Port:     10,
		TTL:      1800,
		Weight:   10,
		Flags:    1,
		Tag:      "test",
	}

	stringified := record.String()
	expected := `godo.DomainRecord{ID:1, Type:"CNAME", Name:"example", Data:"@", Priority:10, Port:10, TTL:1800, Weight:10, Flags:1, Tag:"test"}`
	if expected != stringified {
		t.Errorf("DomainRecord.String returned %+v, expected %+v", stringified, expected)
	}
}

func TestDomainRecordEditRequest_String(t *testing.T) {
	record := &DomainRecordEditRequest{
		Type:     "CNAME",
		Name:     "example",
		Data:     "@",
		Priority: 10,
		Port:     10,
		TTL:      1800,
		Weight:   10,
		Flags:    1,
		Tag:      "test",
	}

	stringified := record.String()
	expected := `godo.DomainRecordEditRequest{Type:"CNAME", Name:"example", Data:"@", Priority:10, Port:10, TTL:1800, Weight:10, Flags:1, Tag:"test"}`
	if expected != stringified {
		t.Errorf("DomainRecordEditRequest.String returned %+v, expected %+v", stringified, expected)
	}
}


text:
package godo

import (
	"fmt"
	"net/http"

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

const floatingBasePath = "v2/floating_ips"

// FloatingIPsService is an interface for interfacing with the floating IPs
// endpoints of the Digital Ocean API.
// See: https://developers.digitalocean.com/documentation/v2#floating-ips
type FloatingIPsService interface {
	List(context.Context, *ListOptions) ([]FloatingIP, *Response, error)
	Get(context.Context, string) (*FloatingIP, *Response, error)
	Create(context.Context, *FloatingIPCreateRequest) (*FloatingIP, *Response, error)
	Delete(context.Context, string) (*Response, error)
}

// FloatingIPsServiceOp handles communication with the floating IPs related methods of the
// DigitalOcean API.
type FloatingIPsServiceOp struct {
	client *Client
}

var _ FloatingIPsService = &FloatingIPsServiceOp{}

// FloatingIP represents a Digital Ocean floating IP.
type FloatingIP struct {
	Region  *Region  `json:"region"`
	Droplet *Droplet `json:"droplet"`
	IP      string   `json:"ip"`
}

func (f FloatingIP) String() string {
	return Stringify(f)
}

type floatingIPsRoot struct {
	FloatingIPs []FloatingIP `json:"floating_ips"`
	Links       *Links       `json:"links"`
}

type floatingIPRoot struct {
	FloatingIP *FloatingIP `json:"floating_ip"`
	Links      *Links      `json:"links,omitempty"`
}

// FloatingIPCreateRequest represents a request to create a floating IP.
// If DropletID is not empty, the floating IP will be assigned to the
// droplet.
type FloatingIPCreateRequest struct {
	Region    string `json:"region"`
	DropletID int    `json:"droplet_id,omitempty"`
}

// List all floating IPs.
func (f *FloatingIPsServiceOp) List(ctx context.Context, opt *ListOptions) ([]FloatingIP, *Response, error) {
	path := floatingBasePath
	path, err := addOptions(path, opt)
	if err != nil {
		return nil, nil, err
	}

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

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

	return root.FloatingIPs, resp, err
}

// Get an individual floating IP.
func (f *FloatingIPsServiceOp) Get(ctx context.Context, ip string) (*FloatingIP, *Response, error) {
	path := fmt.Sprintf("%s/%s", floatingBasePath, ip)

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

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

	return root.FloatingIP, resp, err
}

// Create a floating IP. If the DropletID field of the request is not empty,
// the floating IP will also be assigned to the droplet.
func (f *FloatingIPsServiceOp) Create(ctx context.Context, createRequest *FloatingIPCreateRequest) (*FloatingIP, *Response, error) {
	path := floatingBasePath

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

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

	return root.FloatingIP, resp, err
}

// Delete a floating IP.
func (f *FloatingIPsServiceOp) Delete(ctx context.Context, ip string) (*Response, error) {
	path := fmt.Sprintf("%s/%s", floatingBasePath, ip)

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

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

	return resp, err
}


text:
package godo

import (
	"strconv"
	"time"
)

// Timestamp represents a time that can be unmarshalled from a JSON string
// formatted as either an RFC3339 or Unix timestamp. All
// exported methods of time.Time can be called on Timestamp.
type Timestamp struct {
	time.Time
}

func (t Timestamp) String() string {
	return t.Time.String()
}

// UnmarshalJSON implements the json.Unmarshaler interface.
// Time is expected in RFC3339 or Unix format.
func (t *Timestamp) UnmarshalJSON(data []byte) error {
	str := string(data)
	i, err := strconv.ParseInt(str, 10, 64)
	if err == nil {
		t.Time = time.Unix(i, 0)
	} else {
		t.Time, err = time.Parse(`"`+time.RFC3339+`"`, str)
	}
	return err
}

// Equal reports whether t and u are equal based on time.Equal
func (t Timestamp) Equal(u Timestamp) bool {
	return t.Time.Equal(u.Time)
}


text:
package godo

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

	"github.com/stretchr/testify/assert"
)

var lbListJSONResponse = `
{
	"load_balancers":[
        {
            "id":"37e6be88-01ec-4ec7-9bc6-a514d4719057",
            "name":"example-lb-01",
            "ip":"46.214.185.203",
            "algorithm":"round_robin",
            "status":"active",
            "created_at":"2016-12-15T14:16:36Z",
            "forwarding_rules":[
                {
                    "entry_protocol":"https",
                    "entry_port":443,
                    "target_protocol":"http",
                    "target_port":80,
                    "certificate_id":"a-b-c"
                }
            ],
            "health_check":{
                "protocol":"http",
                "port":80,
                "path":"/index.html",
                "check_interval_seconds":10,
                "response_timeout_seconds":5,
                "healthy_threshold":5,
                "unhealthy_threshold":3
            },
            "sticky_sessions":{
                "type":"cookies",
                "cookie_name":"DO-LB",
                "cookie_ttl_seconds":5
            },
            "region":{
            	"name":"New York 1",
                "slug":"nyc1",
                "sizes":[
                    "512mb",
                    "1gb",
                    "2gb",
                    "4gb",
                    "8gb",
                    "16gb"
                ],
                "features":[
                    "private_networking",
                    "backups",
                    "ipv6",
                    "metadata",
                    "storage"
                ],
                "available":true
            },
            "droplet_ids":[
                2,
                21
            ]
        }
    ],
    "links":{
        "pages":{
            "last":"http://localhost:3001/v2/load_balancers?page=3&per_page=1",
            "next":"http://localhost:3001/v2/load_balancers?page=2&per_page=1"
        }
    },
    "meta":{
        "total":3
    }
}
`

var lbCreateJSONResponse = `
{
    "load_balancer":{
        "id":"8268a81c-fcf5-423e-a337-bbfe95817f23",
        "name":"example-lb-01",
        "ip":"",
        "algorithm":"round_robin",
        "status":"new",
        "created_at":"2016-12-15T14:19:09Z",
        "forwarding_rules":[
            {
                "entry_protocol":"https",
                "entry_port":443,
                "target_protocol":"http",
                "target_port":80,
                "certificate_id":"a-b-c"
            },
            {
                "entry_protocol":"https",
                "entry_port":444,
                "target_protocol":"https",
                "target_port":443,
                "tls_passthrough":true
            }
        ],
        "health_check":{
            "protocol":"http",
            "port":80,
            "path":"/index.html",
            "check_interval_seconds":10,
            "response_timeout_seconds":5,
            "healthy_threshold":5,
            "unhealthy_threshold":3
        },
        "sticky_sessions":{
            "type":"cookies",
            "cookie_name":"DO-LB",
            "cookie_ttl_seconds":5
        },
        "region":{
            "name":"New York 1",
            "slug":"nyc1",
            "sizes":[
                "512mb",
                "1gb",
                "2gb",
                "4gb",
                "8gb",
                "16gb"
            ],
            "features":[
                "private_networking",
                "backups",
                "ipv6",
                "metadata",
                "storage"
            ],
            "available":true
        },
        "droplet_ids":[
            2,
            21
        ],
        "redirect_http_to_https":true
    }
}
`

var lbGetJSONResponse = `
{
    "load_balancer":{
        "id":"37e6be88-01ec-4ec7-9bc6-a514d4719057",
        "name":"example-lb-01",
        "ip":"46.214.185.203",
        "algorithm":"round_robin",
        "status":"active",
        "created_at":"2016-12-15T14:16:36Z",
        "forwarding_rules":[
            {
                "entry_protocol":"https",
                "entry_port":443,
                "target_protocol":"http",
                "target_port":80,
                "certificate_id":"a-b-c"
            }
        ],
        "health_check":{
            "protocol":"http",
            "port":80,
            "path":"/index.html",
            "check_interval_seconds":10,
            "response_timeout_seconds":5,
            "healthy_threshold":5,
            "unhealthy_threshold":3
        },
        "sticky_sessions":{
            "type":"cookies",
            "cookie_name":"DO-LB",
            "cookie_ttl_seconds":5
        },
        "region":{
            "name":"New York 1",
            "slug":"nyc1",
            "sizes":[
                "512mb",
                "1gb",
                "2gb",
                "4gb",
                "8gb",
                "16gb"
            ],
            "features":[
                "private_networking",
                "backups",
                "ipv6",
                "metadata",
                "storage"
            ],
            "available":true
        },
        "droplet_ids":[
            2,
            21
        ]
    }
}
`

var lbUpdateJSONResponse = `
{
    "load_balancer":{
        "id":"8268a81c-fcf5-423e-a337-bbfe95817f23",
        "name":"example-lb-01",
        "ip":"12.34.56.78",
        "algorithm":"least_connections",
        "status":"active",
        "created_at":"2016-12-15T14:19:09Z",
        "forwarding_rules":[
            {
                "entry_protocol":"http",
                "entry_port":80,
                "target_protocol":"http",
                "target_port":80
            },
            {
                "entry_protocol":"https",
                "entry_port":443,
                "target_protocol":"http",
                "target_port":80,
                "certificate_id":"a-b-c"
            }
        ],
        "health_check":{
            "protocol":"tcp",
            "port":80,
            "path":"",
            "check_interval_seconds":10,
            "response_timeout_seconds":5,
            "healthy_threshold":5,
            "unhealthy_threshold":3
        },
        "sticky_sessions":{
            "type":"none"
        },
        "region":{
            "name":"New York 1",
            "slug":"nyc1",
            "sizes":[
                "512mb",
                "1gb",
                "2gb",
                "4gb",
                "8gb",
                "16gb"
            ],
            "features":[
                "private_networking",
                "backups",
                "ipv6",
                "metadata",
                "storage"
            ],
            "available":true
        },
        "droplet_ids":[
            2,
            21
        ]
    }
}
`

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

	path := "/v2/load_balancers"
	loadBalancerId := "37e6be88-01ec-4ec7-9bc6-a514d4719057"
	path = fmt.Sprintf("%s/%s", path, loadBalancerId)
	mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, lbGetJSONResponse)
	})

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

	expected := &LoadBalancer{
		ID:        "37e6be88-01ec-4ec7-9bc6-a514d4719057",
		Name:      "example-lb-01",
		IP:        "46.214.185.203",
		Algorithm: "round_robin",
		Status:    "active",
		Created:   "2016-12-15T14:16:36Z",
		ForwardingRules: []ForwardingRule{
			{
				EntryProtocol:  "https",
				EntryPort:      443,
				TargetProtocol: "http",
				TargetPort:     80,
				CertificateID:  "a-b-c",
				TlsPassthrough: false,
			},
		},
		HealthCheck: &HealthCheck{
			Protocol:               "http",
			Port:                   80,
			Path:                   "/index.html",
			CheckIntervalSeconds:   10,
			ResponseTimeoutSeconds: 5,
			HealthyThreshold:       5,
			UnhealthyThreshold:     3,
		},
		StickySessions: &StickySessions{
			Type:             "cookies",
			CookieName:       "DO-LB",
			CookieTtlSeconds: 5,
		},
		Region: &Region{
			Slug:      "nyc1",
			Name:      "New York 1",
			Sizes:     []string{"512mb", "1gb", "2gb", "4gb", "8gb", "16gb"},
			Available: true,
			Features:  []string{"private_networking", "backups", "ipv6", "metadata", "storage"},
		},
		DropletIDs: []int{2, 21},
	}

	assert.Equal(t, expected, loadBalancer)
}

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

	createRequest := &LoadBalancerRequest{
		Name:      "example-lb-01",
		Algorithm: "round_robin",
		Region:    "nyc1",
		ForwardingRules: []ForwardingRule{
			{
				EntryProtocol:  "https",
				EntryPort:      443,
				TargetProtocol: "http",
				TargetPort:     80,
				CertificateID:  "a-b-c",
			},
		},
		HealthCheck: &HealthCheck{
			Protocol:               "http",
			Port:                   80,
			Path:                   "/index.html",
			CheckIntervalSeconds:   10,
			ResponseTimeoutSeconds: 5,
			UnhealthyThreshold:     3,
			HealthyThreshold:       5,
		},
		StickySessions: &StickySessions{
			Type:             "cookies",
			CookieName:       "DO-LB",
			CookieTtlSeconds: 5,
		},
		Tag:                 "my-tag",
		DropletIDs:          []int{2, 21},
		RedirectHttpToHttps: true,
	}

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

		testMethod(t, r, http.MethodPost)
		assert.Equal(t, createRequest, v)

		fmt.Fprint(w, lbCreateJSONResponse)
	})

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

	expected := &LoadBalancer{
		ID:        "8268a81c-fcf5-423e-a337-bbfe95817f23",
		Name:      "example-lb-01",
		Algorithm: "round_robin",
		Status:    "new",
		Created:   "2016-12-15T14:19:09Z",
		ForwardingRules: []ForwardingRule{
			{
				EntryProtocol:  "https",
				EntryPort:      443,
				TargetProtocol: "http",
				TargetPort:     80,
				CertificateID:  "a-b-c",
				TlsPassthrough: false,
			},
			{
				EntryProtocol:  "https",
				EntryPort:      444,
				TargetProtocol: "https",
				TargetPort:     443,
				CertificateID:  "",
				TlsPassthrough: true,
			},
		},
		HealthCheck: &HealthCheck{
			Protocol:               "http",
			Port:                   80,
			Path:                   "/index.html",
			CheckIntervalSeconds:   10,
			ResponseTimeoutSeconds: 5,
			HealthyThreshold:       5,
			UnhealthyThreshold:     3,
		},
		StickySessions: &StickySessions{
			Type:             "cookies",
			CookieName:       "DO-LB",
			CookieTtlSeconds: 5,
		},
		Region: &Region{
			Slug:      "nyc1",
			Name:      "New York 1",
			Sizes:     []string{"512mb", "1gb", "2gb", "4gb", "8gb", "16gb"},
			Available: true,
			Features:  []string{"private_networking", "backups", "ipv6", "metadata", "storage"},
		},
		DropletIDs:          []int{2, 21},
		RedirectHttpToHttps: true,
	}

	assert.Equal(t, expected, loadBalancer)
}

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

	updateRequest := &LoadBalancerRequest{
		Name:      "example-lb-01",
		Algorithm: "least_connections",
		Region:    "nyc1",
		ForwardingRules: []ForwardingRule{
			{
				EntryProtocol:  "http",
				EntryPort:      80,
				TargetProtocol: "http",
				TargetPort:     80,
			},
			{
				EntryProtocol:  "https",
				EntryPort:      443,
				TargetProtocol: "http",
				TargetPort:     80,
				CertificateID:  "a-b-c",
			},
		},
		HealthCheck: &HealthCheck{
			Protocol:               "tcp",
			Port:                   80,
			Path:                   "",
			CheckIntervalSeconds:   10,
			ResponseTimeoutSeconds: 5,
			UnhealthyThreshold:     3,
			HealthyThreshold:       5,
		},
		StickySessions: &StickySessions{
			Type: "none",
		},
		DropletIDs: []int{2, 21},
	}

	path := "/v2/load_balancers"
	loadBalancerId := "8268a81c-fcf5-423e-a337-bbfe95817f23"
	path = fmt.Sprintf("%s/%s", path, loadBalancerId)

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

		testMethod(t, r, "PUT")
		assert.Equal(t, updateRequest, v)

		fmt.Fprint(w, lbUpdateJSONResponse)
	})

	loadBalancer, _, err := client.LoadBalancers.Update(ctx, loadBalancerId, updateRequest)
	if err != nil {
		t.Errorf("LoadBalancers.Update returned error: %v", err)
	}

	expected := &LoadBalancer{
		ID:        "8268a81c-fcf5-423e-a337-bbfe95817f23",
		Name:      "example-lb-01",
		IP:        "12.34.56.78",
		Algorithm: "least_connections",
		Status:    "active",
		Created:   "2016-12-15T14:19:09Z",
		ForwardingRules: []ForwardingRule{
			{
				EntryProtocol:  "http",
				EntryPort:      80,
				TargetProtocol: "http",
				TargetPort:     80,
			},
			{
				EntryProtocol:  "https",
				EntryPort:      443,
				TargetProtocol: "http",
				TargetPort:     80,
				CertificateID:  "a-b-c",
			},
		},
		HealthCheck: &HealthCheck{
			Protocol:               "tcp",
			Port:                   80,
			Path:                   "",
			CheckIntervalSeconds:   10,
			ResponseTimeoutSeconds: 5,
			UnhealthyThreshold:     3,
			HealthyThreshold:       5,
		},
		StickySessions: &StickySessions{
			Type: "none",
		},
		Region: &Region{
			Slug:      "nyc1",
			Name:      "New York 1",
			Sizes:     []string{"512mb", "1gb", "2gb", "4gb", "8gb", "16gb"},
			Available: true,
			Features:  []string{"private_networking", "backups", "ipv6", "metadata", "storage"},
		},
		DropletIDs: []int{2, 21},
	}

	assert.Equal(t, expected, loadBalancer)
}

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

	path := "/v2/load_balancers"
	mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, lbListJSONResponse)
	})

	loadBalancers, _, err := client.LoadBalancers.List(ctx, nil)

	if err != nil {
		t.Errorf("LoadBalancers.List returned error: %v", err)
	}

	expected := []LoadBalancer{
		{
			ID:        "37e6be88-01ec-4ec7-9bc6-a514d4719057",
			Name:      "example-lb-01",
			IP:        "46.214.185.203",
			Algorithm: "round_robin",
			Status:    "active",
			Created:   "2016-12-15T14:16:36Z",
			ForwardingRules: []ForwardingRule{
				{
					EntryProtocol:  "https",
					EntryPort:      443,
					TargetProtocol: "http",
					TargetPort:     80,
					CertificateID:  "a-b-c",
				},
			},
			HealthCheck: &HealthCheck{
				Protocol:               "http",
				Port:                   80,
				Path:                   "/index.html",
				CheckIntervalSeconds:   10,
				ResponseTimeoutSeconds: 5,
				HealthyThreshold:       5,
				UnhealthyThreshold:     3,
			},
			StickySessions: &StickySessions{
				Type:             "cookies",
				CookieName:       "DO-LB",
				CookieTtlSeconds: 5,
			},
			Region: &Region{
				Slug:      "nyc1",
				Name:      "New York 1",
				Sizes:     []string{"512mb", "1gb", "2gb", "4gb", "8gb", "16gb"},
				Available: true,
				Features:  []string{"private_networking", "backups", "ipv6", "metadata", "storage"},
			},
			DropletIDs: []int{2, 21},
		},
	}

	assert.Equal(t, expected, loadBalancers)
}

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

	path := "/v2/load_balancers"
	mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		testFormValues(t, r, map[string]string{"page": "2"})
		fmt.Fprint(w, lbListJSONResponse)
	})

	opts := &ListOptions{Page: 2}
	_, resp, err := client.LoadBalancers.List(ctx, opts)

	if err != nil {
		t.Errorf("LoadBalancers.List returned error: %v", err)
	}

	assert.Equal(t, "http://localhost:3001/v2/load_balancers?page=2&per_page=1", resp.Links.Pages.Next)
	assert.Equal(t, "http://localhost:3001/v2/load_balancers?page=3&per_page=1", resp.Links.Pages.Last)
}

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

	lbID := "37e6be88-01ec-4ec7-9bc6-a514d4719057"
	path := "/v2/load_balancers"
	path = fmt.Sprintf("%s/%s", path, lbID)
	mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodDelete)
	})

	_, err := client.LoadBalancers.Delete(ctx, lbID)

	if err != nil {
		t.Errorf("LoadBalancers.Delete returned error: %v", err)
	}
}

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

	dropletIdsRequest := &dropletIDsRequest{
		IDs: []int{42, 44},
	}

	lbID := "37e6be88-01ec-4ec7-9bc6-a514d4719057"
	path := fmt.Sprintf("/v2/load_balancers/%s/droplets", lbID)
	mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
		v := new(dropletIDsRequest)
		err := json.NewDecoder(r.Body).Decode(v)
		if err != nil {
			t.Fatal(err)
		}

		testMethod(t, r, http.MethodPost)
		assert.Equal(t, dropletIdsRequest, v)

		fmt.Fprint(w, nil)
	})

	_, err := client.LoadBalancers.AddDroplets(ctx, lbID, dropletIdsRequest.IDs...)

	if err != nil {
		t.Errorf("LoadBalancers.AddDroplets returned error: %v", err)
	}
}

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

	dropletIdsRequest := &dropletIDsRequest{
		IDs: []int{2, 21},
	}

	lbID := "37e6be88-01ec-4ec7-9bc6-a514d4719057"
	path := fmt.Sprintf("/v2/load_balancers/%s/droplets", lbID)
	mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
		v := new(dropletIDsRequest)
		err := json.NewDecoder(r.Body).Decode(v)
		if err != nil {
			t.Fatal(err)
		}

		testMethod(t, r, http.MethodDelete)
		assert.Equal(t, dropletIdsRequest, v)

		fmt.Fprint(w, nil)
	})

	_, err := client.LoadBalancers.RemoveDroplets(ctx, lbID, dropletIdsRequest.IDs...)

	if err != nil {
		t.Errorf("LoadBalancers.RemoveDroplets returned error: %v", err)
	}
}

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

	frr := &forwardingRulesRequest{
		Rules: []ForwardingRule{
			{
				EntryProtocol:  "https",
				EntryPort:      444,
				TargetProtocol: "http",
				TargetPort:     81,
				CertificateID:  "b2abc00f-d3c4-426c-9f0b-b2f7a3ff7527",
			},
			{
				EntryProtocol:  "tcp",
				EntryPort:      8080,
				TargetProtocol: "tcp",
				TargetPort:     8081,
			},
		},
	}

	lbID := "37e6be88-01ec-4ec7-9bc6-a514d4719057"
	path := fmt.Sprintf("/v2/load_balancers/%s/forwarding_rules", lbID)
	mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
		v := new(forwardingRulesRequest)
		err := json.NewDecoder(r.Body).Decode(v)
		if err != nil {
			t.Fatal(err)
		}

		testMethod(t, r, http.MethodPost)
		assert.Equal(t, frr, v)

		fmt.Fprint(w, nil)
	})

	_, err := client.LoadBalancers.AddForwardingRules(ctx, lbID, frr.Rules...)

	if err != nil {
		t.Errorf("LoadBalancers.AddForwardingRules returned error: %v", err)
	}
}

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

	frr := &forwardingRulesRequest{
		Rules: []ForwardingRule{
			{
				EntryProtocol:  "https",
				EntryPort:      444,
				TargetProtocol: "http",
				TargetPort:     81,
			},
			{
				EntryProtocol:  "tcp",
				EntryPort:      8080,
				TargetProtocol: "tcp",
				TargetPort:     8081,
			},
		},
	}

	lbID := "37e6be88-01ec-4ec7-9bc6-a514d4719057"
	path := fmt.Sprintf("/v2/load_balancers/%s/forwarding_rules", lbID)
	mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
		v := new(forwardingRulesRequest)
		err := json.NewDecoder(r.Body).Decode(v)
		if err != nil {
			t.Fatal(err)
		}

		testMethod(t, r, http.MethodDelete)
		assert.Equal(t, frr, v)

		fmt.Fprint(w, nil)
	})

	_, err := client.LoadBalancers.RemoveForwardingRules(ctx, lbID, frr.Rules...)

	if err != nil {
		t.Errorf("LoadBalancers.RemoveForwardingRules returned error: %v", err)
	}
}

func TestLoadBalancers_AsRequest(t *testing.T) {
	lb := &LoadBalancer{
		ID:        "37e6be88-01ec-4ec7-9bc6-a514d4719057",
		Name:      "test-loadbalancer",
		IP:        "10.0.0.1",
		Algorithm: "least_connections",
		Status:    "active",
		Created:   "2011-06-24T12:00:00Z",
		HealthCheck: &HealthCheck{
			Protocol:               "http",
			Port:                   80,
			Path:                   "/ping",
			CheckIntervalSeconds:   30,
			ResponseTimeoutSeconds: 10,
			HealthyThreshold:       3,
			UnhealthyThreshold:     3,
		},
		StickySessions: &StickySessions{
			Type:             "cookies",
			CookieName:       "nomnom",
			CookieTtlSeconds: 32,
		},
		Region: &Region{
			Slug: "lon1",
		},
		RedirectHttpToHttps: true,
	}
	lb.DropletIDs = make([]int, 1, 2)
	lb.DropletIDs[0] = 12345
	lb.ForwardingRules = make([]ForwardingRule, 1, 2)
	lb.ForwardingRules[0] = ForwardingRule{
		EntryProtocol:  "http",
		EntryPort:      80,
		TargetProtocol: "http",
		TargetPort:     80,
	}

	want := &LoadBalancerRequest{
		Name:      "test-loadbalancer",
		Algorithm: "least_connections",
		Region:    "lon1",
		ForwardingRules: []ForwardingRule{ForwardingRule{
			EntryProtocol:  "http",
			EntryPort:      80,
			TargetProtocol: "http",
			TargetPort:     80,
		}},
		HealthCheck: &HealthCheck{
			Protocol:               "http",
			Port:                   80,
			Path:                   "/ping",
			CheckIntervalSeconds:   30,
			ResponseTimeoutSeconds: 10,
			HealthyThreshold:       3,
			UnhealthyThreshold:     3,
		},
		StickySessions: &StickySessions{
			Type:             "cookies",
			CookieName:       "nomnom",
			CookieTtlSeconds: 32,
		},
		DropletIDs:          []int{12345},
		RedirectHttpToHttps: true,
	}

	r := lb.AsRequest()
	assert.Equal(t, want, r)
	assert.False(t, r.HealthCheck == lb.HealthCheck, "HealthCheck points to same struct")
	assert.False(t, r.StickySessions == lb.StickySessions, "StickySessions points to same struct")

	r.DropletIDs = append(r.DropletIDs, 54321)
	r.ForwardingRules = append(r.ForwardingRules, ForwardingRule{
		EntryProtocol:  "https",
		EntryPort:      443,
		TargetProtocol: "https",
		TargetPort:     443,
		TlsPassthrough: true,
	})

	// Check that original LoadBalancer hasn't changed
	lb.DropletIDs = append(lb.DropletIDs, 13579)
	lb.ForwardingRules = append(lb.ForwardingRules, ForwardingRule{
		EntryProtocol:  "tcp",
		EntryPort:      587,
		TargetProtocol: "tcp",
		TargetPort:     587,
	})
	assert.Equal(t, []int{12345, 54321}, r.DropletIDs)
	assert.Equal(t, []ForwardingRule{
		ForwardingRule{
			EntryProtocol:  "http",
			EntryPort:      80,
			TargetProtocol: "http",
			TargetPort:     80,
		},
		ForwardingRule{
			EntryProtocol:  "https",
			EntryPort:      443,
			TargetProtocol: "https",
			TargetPort:     443,
			TlsPassthrough: true,
		},
	}, r.ForwardingRules)
}


text:
package godo

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

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

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

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

	expected := []Key{{ID: 1}, {ID: 2}}
	if !reflect.DeepEqual(keys, expected) {
		t.Errorf("Keys.List returned %+v, expected %+v", keys, expected)
	}
}

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

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

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

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

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

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

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

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

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

	keys, _, err := client.Keys.GetByID(ctx, 12345)
	if err != nil {
		t.Errorf("Keys.GetByID returned error: %v", err)
	}

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

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

	mux.HandleFunc("/v2/account/keys/aa:bb:cc", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, `{"ssh_key": {"fingerprint":"aa:bb:cc"}}`)
	})

	keys, _, err := client.Keys.GetByFingerprint(ctx, "aa:bb:cc")
	if err != nil {
		t.Errorf("Keys.GetByFingerprint returned error: %v", err)
	}

	expected := &Key{Fingerprint: "aa:bb:cc"}
	if !reflect.DeepEqual(keys, expected) {
		t.Errorf("Keys.GetByFingerprint returned %+v, expected %+v", keys, expected)
	}
}

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

	createRequest := &KeyCreateRequest{
		Name:      "name",
		PublicKey: "ssh-rsa longtextandstuff",
	}

	mux.HandleFunc("/v2/account/keys", func(w http.ResponseWriter, r *http.Request) {
		v := new(KeyCreateRequest)
		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, `{"ssh_key":{"id":1}}`)
	})

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

	expected := &Key{ID: 1}
	if !reflect.DeepEqual(key, expected) {
		t.Errorf("Keys.Create returned %+v, expected %+v", key, expected)
	}
}

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

	updateRequest := &KeyUpdateRequest{
		Name: "name",
	}

	mux.HandleFunc("/v2/account/keys/12345", func(w http.ResponseWriter, r *http.Request) {
		expected := map[string]interface{}{
			"name": "name",
		}

		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, `{"ssh_key":{"id":1}}`)
	})

	key, _, err := client.Keys.UpdateByID(ctx, 12345, updateRequest)
	if err != nil {
		t.Errorf("Keys.Update returned error: %v", err)
	} else {
		if id := key.ID; id != 1 {
			t.Errorf("expected id '%d', received '%d'", 1, id)
		}
	}
}

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

	updateRequest := &KeyUpdateRequest{
		Name: "name",
	}

	mux.HandleFunc("/v2/account/keys/3b:16:bf:e4:8b:00:8b:b8:59:8c:a9:d3:f0:19:45:fa", func(w http.ResponseWriter, r *http.Request) {
		expected := map[string]interface{}{
			"name": "name",
		}

		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, `{"ssh_key":{"id":1}}`)
	})

	key, _, err := client.Keys.UpdateByFingerprint(ctx, "3b:16:bf:e4:8b:00:8b:b8:59:8c:a9:d3:f0:19:45:fa", updateRequest)
	if err != nil {
		t.Errorf("Keys.Update returned error: %v", err)
	} else {
		if id := key.ID; id != 1 {
			t.Errorf("expected id '%d', received '%d'", 1, id)
		}
	}
}

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

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

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

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

	mux.HandleFunc("/v2/account/keys/aa:bb:cc", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodDelete)
	})

	_, err := client.Keys.DeleteByFingerprint(ctx, "aa:bb:cc")
	if err != nil {
		t.Errorf("Keys.Delete returned error: %v", err)
	}
}

func TestKey_String(t *testing.T) {
	key := &Key{
		ID:          123,
		Name:        "Key",
		Fingerprint: "fingerprint",
		PublicKey:   "public key",
	}

	stringified := key.String()
	expected := `godo.Key{ID:123, Name:"Key", Fingerprint:"fingerprint", PublicKey:"public key"}`
	if expected != stringified {
		t.Errorf("Key.String returned %+v, expected %+v", stringified, expected)
	}
}


text:
package godo

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

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

	mux.HandleFunc("/v2/floating_ips", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, `{"floating_ips": [{"region":{"slug":"nyc3"},"droplet":{"id":1},"ip":"192.168.0.1"},{"region":{"slug":"nyc3"},"droplet":{"id":2},"ip":"192.168.0.2"}]}`)
	})

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

	expected := []FloatingIP{
		{Region: &Region{Slug: "nyc3"}, Droplet: &Droplet{ID: 1}, IP: "192.168.0.1"},
		{Region: &Region{Slug: "nyc3"}, Droplet: &Droplet{ID: 2}, IP: "192.168.0.2"},
	}
	if !reflect.DeepEqual(floatingIPs, expected) {
		t.Errorf("FloatingIPs.List returned %+v, expected %+v", floatingIPs, expected)
	}
}

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

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

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

	checkCurrentPage(t, resp, 1)
}

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

	jBlob := `
	{
		"floating_ips": [{"region":{"slug":"nyc3"},"droplet":{"id":1},"ip":"192.168.0.1"},{"region":{"slug":"nyc3"},"droplet":{"id":2},"ip":"192.168.0.2"}],
		"links":{
			"pages":{
				"next":"http://example.com/v2/floating_ips/?page=3",
				"prev":"http://example.com/v2/floating_ips/?page=1",
				"last":"http://example.com/v2/floating_ips/?page=3",
				"first":"http://example.com/v2/floating_ips/?page=1"
			}
		}
	}`

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

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

	checkCurrentPage(t, resp, 2)
}

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

	mux.HandleFunc("/v2/floating_ips/192.168.0.1", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, `{"floating_ip":{"region":{"slug":"nyc3"},"droplet":{"id":1},"ip":"192.168.0.1"}}`)
	})

	floatingIP, _, err := client.FloatingIPs.Get(ctx, "192.168.0.1")
	if err != nil {
		t.Errorf("domain.Get returned error: %v", err)
	}

	expected := &FloatingIP{Region: &Region{Slug: "nyc3"}, Droplet: &Droplet{ID: 1}, IP: "192.168.0.1"}
	if !reflect.DeepEqual(floatingIP, expected) {
		t.Errorf("FloatingIPs.Get returned %+v, expected %+v", floatingIP, expected)
	}
}

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

	createRequest := &FloatingIPCreateRequest{
		Region:    "nyc3",
		DropletID: 1,
	}

	mux.HandleFunc("/v2/floating_ips", func(w http.ResponseWriter, r *http.Request) {
		v := new(FloatingIPCreateRequest)
		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, `{"floating_ip":{"region":{"slug":"nyc3"},"droplet":{"id":1},"ip":"192.168.0.1"}}`)
	})

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

	expected := &FloatingIP{Region: &Region{Slug: "nyc3"}, Droplet: &Droplet{ID: 1}, IP: "192.168.0.1"}
	if !reflect.DeepEqual(floatingIP, expected) {
		t.Errorf("FloatingIPs.Create returned %+v, expected %+v", floatingIP, expected)
	}
}

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

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

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


text:
package godo

import (
	"fmt"
	"net/http"

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

const (
	actionsBasePath = "v2/actions"

	// ActionInProgress is an in progress action status
	ActionInProgress = "in-progress"

	//ActionCompleted is a completed action status
	ActionCompleted = "completed"
)

// ActionsService handles communction with action related methods of the
// DigitalOcean API: https://developers.digitalocean.com/documentation/v2#actions
type ActionsService interface {
	List(context.Context, *ListOptions) ([]Action, *Response, error)
	Get(context.Context, int) (*Action, *Response, error)
}

// ActionsServiceOp handles communition with the image action related methods of the
// DigitalOcean API.
type ActionsServiceOp struct {
	client *Client
}

var _ ActionsService = &ActionsServiceOp{}

type actionsRoot struct {
	Actions []Action `json:"actions"`
	Links   *Links   `json:"links"`
}

type actionRoot struct {
	Event *Action `json:"action"`
}

// Action represents a DigitalOcean Action
type Action struct {
	ID           int        `json:"id"`
	Status       string     `json:"status"`
	Type         string     `json:"type"`
	StartedAt    *Timestamp `json:"started_at"`
	CompletedAt  *Timestamp `json:"completed_at"`
	ResourceID   int        `json:"resource_id"`
	ResourceType string     `json:"resource_type"`
	Region       *Region    `json:"region,omitempty"`
	RegionSlug   string     `json:"region_slug,omitempty"`
}

// List all actions
func (s *ActionsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Action, *Response, error) {
	path := actionsBasePath
	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(actionsRoot)
	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.Actions, resp, err
}

// Get an action by ID.
func (s *ActionsServiceOp) Get(ctx context.Context, id int) (*Action, *Response, error) {
	if id < 1 {
		return nil, nil, NewArgError("id", "cannot be less than 1")
	}

	path := fmt.Sprintf("%s/%d", actionsBasePath, id)
	req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
	if err != nil {
		return nil, nil, err
	}

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

	return root.Event, resp, err
}

func (a Action) String() string {
	return Stringify(a)
}


text:
package godo

import (
	"net/http"

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

// SizesService is an interface for interfacing with the size
// endpoints of the DigitalOcean API
// See: https://developers.digitalocean.com/documentation/v2#sizes
type SizesService interface {
	List(context.Context, *ListOptions) ([]Size, *Response, error)
}

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

var _ SizesService = &SizesServiceOp{}

// Size represents a DigitalOcean Size
type Size struct {
	Slug         string   `json:"slug,omitempty"`
	Memory       int      `json:"memory,omitempty"`
	Vcpus        int      `json:"vcpus,omitempty"`
	Disk         int      `json:"disk,omitempty"`
	PriceMonthly float64  `json:"price_monthly,omitempty"`
	PriceHourly  float64  `json:"price_hourly,omitempty"`
	Regions      []string `json:"regions,omitempty"`
	Available    bool     `json:"available,omitempty"`
	Transfer     float64  `json:"transfer,omitempty"`
}

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

type sizesRoot struct {
	Sizes []Size
	Links *Links `json:"links"`
}

// List all images
func (s *SizesServiceOp) List(ctx context.Context, opt *ListOptions) ([]Size, *Response, error) {
	path := "v2/sizes"
	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(sizesRoot)
	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.Sizes, resp, err
}


text:
package godo

import "fmt"

// ArgError is an error that represents an error with an input to godo. It
// identifies the argument and the cause (if possible).
type ArgError struct {
	arg    string
	reason string
}

var _ error = &ArgError{}

// NewArgError creates an InputError.
func NewArgError(arg, reason string) *ArgError {
	return &ArgError{
		arg:    arg,
		reason: reason,
	}
}

func (e *ArgError) Error() string {
	return fmt.Sprintf("%s is invalid because %s", e.arg, e.reason)
}


text:
// Package godo is the DigtalOcean API v2 client for Go
package godo


text:
package godo

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

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

	transferRequest := &ActionRequest{}

	mux.HandleFunc("/v2/images/12345/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, transferRequest) {
			t.Errorf("Request body = %+v, expected %+v", v, transferRequest)
		}

		fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)

	})

	transfer, _, err := client.ImageActions.Transfer(ctx, 12345, transferRequest)
	if err != nil {
		t.Errorf("ImageActions.Transfer returned error: %v", err)
	}

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

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

	convertRequest := &ActionRequest{
		"type": "convert",
	}

	mux.HandleFunc("/v2/images/12345/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, convertRequest) {
			t.Errorf("Request body = %+v, expected %+v", v, convertRequest)
		}

		fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)

	})

	transfer, _, err := client.ImageActions.Convert(ctx, 12345)
	if err != nil {
		t.Errorf("ImageActions.Transfer returned error: %v", err)
	}

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

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

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

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

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


text:
package godo

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

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

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

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

	expected := []Size{{Slug: "1"}, {Slug: "2"}}
	if !reflect.DeepEqual(sizes, expected) {
		t.Errorf("Sizes.List returned %+v, expected %+v", sizes, expected)
	}
}

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

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

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

	checkCurrentPage(t, resp, 1)
}

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

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

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

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

	checkCurrentPage(t, resp, 2)
}

func TestSize_String(t *testing.T) {
	size := &Size{
		Slug:         "slize",
		Memory:       123,
		Vcpus:        456,
		Disk:         789,
		PriceMonthly: 123,
		PriceHourly:  456,
		Regions:      []string{"1", "2"},
		Available:    true,
		Transfer:     789,
	}

	stringified := size.String()
	expected := `godo.Size{Slug:"slize", Memory:123, Vcpus:456, Disk:789, PriceMonthly:123, PriceHourly:456, Regions:["1" "2"], Available:true, Transfer:789}`
	if expected != stringified {
		t.Errorf("Size.String returned %+v, expected %+v", stringified, expected)
	}
}


text:
package godo

import (
	"fmt"
	"net/http"

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

const domainsBasePath = "v2/domains"

// DomainsService is an interface for managing DNS with the DigitalOcean API.
// See: https://developers.digitalocean.com/documentation/v2#domains and
// https://developers.digitalocean.com/documentation/v2#domain-records
type DomainsService interface {
	List(context.Context, *ListOptions) ([]Domain, *Response, error)
	Get(context.Context, string) (*Domain, *Response, error)
	Create(context.Context, *DomainCreateRequest) (*Domain, *Response, error)
	Delete(context.Context, string) (*Response, error)

	Records(context.Context, string, *ListOptions) ([]DomainRecord, *Response, error)
	Record(context.Context, string, int) (*DomainRecord, *Response, error)
	DeleteRecord(context.Context, string, int) (*Response, error)
	EditRecord(context.Context, string, int, *DomainRecordEditRequest) (*DomainRecord, *Response, error)
	CreateRecord(context.Context, string, *DomainRecordEditRequest) (*DomainRecord, *Response, error)
}

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

var _ DomainsService = &DomainsServiceOp{}

// Domain represents a DigitalOcean domain
type Domain struct {
	Name     string `json:"name"`
	TTL      int    `json:"ttl"`
	ZoneFile string `json:"zone_file"`
}

// domainRoot represents a response from the DigitalOcean API
type domainRoot struct {
	Domain *Domain `json:"domain"`
}

type domainsRoot struct {
	Domains []Domain `json:"domains"`
	Links   *Links   `json:"links"`
}

// DomainCreateRequest respresents a request to create a domain.
type DomainCreateRequest struct {
	Name      string `json:"name"`
	IPAddress string `json:"ip_address"`
}

// DomainRecordRoot is the root of an individual Domain Record response
type domainRecordRoot struct {
	DomainRecord *DomainRecord `json:"domain_record"`
}

// DomainRecordsRoot is the root of a group of Domain Record responses
type domainRecordsRoot struct {
	DomainRecords []DomainRecord `json:"domain_records"`
	Links         *Links         `json:"links"`
}

// DomainRecord represents a DigitalOcean DomainRecord
type DomainRecord struct {
	ID       int    `json:"id,float64,omitempty"`
	Type     string `json:"type,omitempty"`
	Name     string `json:"name,omitempty"`
	Data     string `json:"data,omitempty"`
	Priority int    `json:"priority,omitempty"`
	Port     int    `json:"port,omitempty"`
	TTL      int    `json:"ttl,omitempty"`
	Weight   int    `json:"weight,omitempty"`
	Flags    int    `json:"flags"`
	Tag      string `json:"tag,omitempty"`
}

// DomainRecordEditRequest represents a request to update a domain record.
type DomainRecordEditRequest struct {
	Type     string `json:"type,omitempty"`
	Name     string `json:"name,omitempty"`
	Data     string `json:"data,omitempty"`
	Priority int    `json:"priority,omitempty"`
	Port     int    `json:"port,omitempty"`
	TTL      int    `json:"ttl,omitempty"`
	Weight   int    `json:"weight,omitempty"`
	Flags    int    `json:"flags"`
	Tag      string `json:"tag,omitempty"`
}

func (d Domain) String() string {
	return Stringify(d)
}

// List all domains.
func (s DomainsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Domain, *Response, error) {
	path := domainsBasePath
	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(domainsRoot)
	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.Domains, resp, err
}

// Get individual domain. It requires a non-empty domain name.
func (s *DomainsServiceOp) Get(ctx context.Context, name string) (*Domain, *Response, error) {
	if len(name) < 1 {
		return nil, nil, NewArgError("name", "cannot be an empty string")
	}

	path := fmt.Sprintf("%s/%s", domainsBasePath, name)

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

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

	return root.Domain, resp, err
}

// Create a new domain
func (s *DomainsServiceOp) Create(ctx context.Context, createRequest *DomainCreateRequest) (*Domain, *Response, error) {
	if createRequest == nil {
		return nil, nil, NewArgError("createRequest", "cannot be nil")
	}

	path := domainsBasePath

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

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

// Delete domain
func (s *DomainsServiceOp) Delete(ctx context.Context, name string) (*Response, error) {
	if len(name) < 1 {
		return nil, NewArgError("name", "cannot be an empty string")
	}

	path := fmt.Sprintf("%s/%s", domainsBasePath, name)

	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
}

// Converts a DomainRecord to a string.
func (d DomainRecord) String() string {
	return Stringify(d)
}

// Converts a DomainRecordEditRequest to a string.
func (d DomainRecordEditRequest) String() string {
	return Stringify(d)
}

// Records returns a slice of DomainRecords for a domain
func (s *DomainsServiceOp) Records(ctx context.Context, domain string, opt *ListOptions) ([]DomainRecord, *Response, error) {
	if len(domain) < 1 {
		return nil, nil, NewArgError("domain", "cannot be an empty string")
	}

	path := fmt.Sprintf("%s/%s/records", domainsBasePath, domain)
	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(domainRecordsRoot)
	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.DomainRecords, resp, err
}

// Record returns the record id from a domain
func (s *DomainsServiceOp) Record(ctx context.Context, domain string, id int) (*DomainRecord, *Response, error) {
	if len(domain) < 1 {
		return nil, nil, NewArgError("domain", "cannot be an empty string")
	}

	if id < 1 {
		return nil, nil, NewArgError("id", "cannot be less than 1")
	}

	path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id)

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

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

	return record.DomainRecord, resp, err
}

// DeleteRecord deletes a record from a domain identified by id
func (s *DomainsServiceOp) DeleteRecord(ctx context.Context, domain string, id int) (*Response, error) {
	if len(domain) < 1 {
		return nil, NewArgError("domain", "cannot be an empty string")
	}

	if id < 1 {
		return nil, NewArgError("id", "cannot be less than 1")
	}

	path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id)

	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
}

// EditRecord edits a record using a DomainRecordEditRequest
func (s *DomainsServiceOp) EditRecord(ctx context.Context,
	domain string,
	id int,
	editRequest *DomainRecordEditRequest,
) (*DomainRecord, *Response, error) {
	if len(domain) < 1 {
		return nil, nil, NewArgError("domain", "cannot be an empty string")
	}

	if id < 1 {
		return nil, nil, NewArgError("id", "cannot be less than 1")
	}

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

	path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id)

	req, err := s.client.NewRequest(ctx, "PUT", path, editRequest)
	if err != nil {
		return nil, nil, err
	}

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

	return d, resp, err
}

// CreateRecord creates a record using a DomainRecordEditRequest
func (s *DomainsServiceOp) CreateRecord(ctx context.Context,
	domain string,
	createRequest *DomainRecordEditRequest) (*DomainRecord, *Response, error) {
	if len(domain) < 1 {
		return nil, nil, NewArgError("domain", "cannot be empty string")
	}

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

	path := fmt.Sprintf("%s/%s/records", domainsBasePath, domain)
	req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest)

	if err != nil {
		return nil, nil, err
	}

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

	return d.DomainRecord, resp, err
}


text:
package godo

import (
	"fmt"
	"net/http"

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

// FloatingIPActionsService is an interface for interfacing with the
// floating IPs actions endpoints of the Digital Ocean API.
// See: https://developers.digitalocean.com/documentation/v2#floating-ips-action
type FloatingIPActionsService interface {
	Assign(ctx context.Context, ip string, dropletID int) (*Action, *Response, error)
	Unassign(ctx context.Context, ip string) (*Action, *Response, error)
	Get(ctx context.Context, ip string, actionID int) (*Action, *Response, error)
	List(ctx context.Context, ip string, opt *ListOptions) ([]Action, *Response, error)
}

// FloatingIPActionsServiceOp handles communication with the floating IPs
// action related methods of the DigitalOcean API.
type FloatingIPActionsServiceOp struct {
	client *Client
}

// Assign a floating IP to a droplet.
func (s *FloatingIPActionsServiceOp) Assign(ctx context.Context, ip string, dropletID int) (*Action, *Response, error) {
	request := &ActionRequest{
		"type":       "assign",
		"droplet_id": dropletID,
	}
	return s.doAction(ctx, ip, request)
}

// Unassign a floating IP from the droplet it is currently assigned to.
func (s *FloatingIPActionsServiceOp) Unassign(ctx context.Context, ip string) (*Action, *Response, error) {
	request := &ActionRequest{"type": "unassign"}
	return s.doAction(ctx, ip, request)
}

// Get an action for a particular floating IP by id.
func (s *FloatingIPActionsServiceOp) Get(ctx context.Context, ip string, actionID int) (*Action, *Response, error) {
	path := fmt.Sprintf("%s/%d", floatingIPActionPath(ip), actionID)
	return s.get(ctx, path)
}

// List the actions for a particular floating IP.
func (s *FloatingIPActionsServiceOp) List(ctx context.Context, ip string, opt *ListOptions) ([]Action, *Response, error) {
	path := floatingIPActionPath(ip)
	path, err := addOptions(path, opt)
	if err != nil {
		return nil, nil, err
	}

	return s.list(ctx, path)
}

func (s *FloatingIPActionsServiceOp) doAction(ctx context.Context, ip string, request *ActionRequest) (*Action, *Response, error) {
	path := floatingIPActionPath(ip)

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

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

	return root.Event, resp, err
}

func (s *FloatingIPActionsServiceOp) get(ctx context.Context, path string) (*Action, *Response, error) {
	req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
	if err != nil {
		return nil, nil, err
	}

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

	return root.Event, resp, err
}

func (s *FloatingIPActionsServiceOp) list(ctx context.Context, path string) ([]Action, *Response, error) {
	req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
	if err != nil {
		return nil, nil, err
	}

	root := new(actionsRoot)
	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.Actions, resp, err
}

func floatingIPActionPath(ip string) string {
	return fmt.Sprintf("%s/%s/actions", floatingBasePath, ip)
}


text:
package godo

import (
	"bytes"
	"fmt"
	"io"
	"reflect"
)

var timestampType = reflect.TypeOf(Timestamp{})

// Stringify attempts to create a string representation of DigitalOcean types
func Stringify(message interface{}) string {
	var buf bytes.Buffer
	v := reflect.ValueOf(message)
	stringifyValue(&buf, v)
	return buf.String()
}

// stringifyValue was graciously cargoculted from the goprotubuf library
func stringifyValue(w io.Writer, val reflect.Value) {
	if val.Kind() == reflect.Ptr && val.IsNil() {
		_, _ = w.Write([]byte("<nil>"))
		return
	}

	v := reflect.Indirect(val)

	switch v.Kind() {
	case reflect.String:
		fmt.Fprintf(w, `"%s"`, v)
	case reflect.Slice:
		stringifySlice(w, v)
		return
	case reflect.Struct:
		stringifyStruct(w, v)
	default:
		if v.CanInterface() {
			fmt.Fprint(w, v.Interface())
		}
	}
}

func stringifySlice(w io.Writer, v reflect.Value) {
	_, _ = w.Write([]byte{'['})
	for i := 0; i < v.Len(); i++ {
		if i > 0 {
			_, _ = w.Write([]byte{' '})
		}

		stringifyValue(w, v.Index(i))
	}

	_, _ = w.Write([]byte{']'})
}

func stringifyStruct(w io.Writer, v reflect.Value) {
	if v.Type().Name() != "" {
		_, _ = w.Write([]byte(v.Type().String()))
	}

	// special handling of Timestamp values
	if v.Type() == timestampType {
		fmt.Fprintf(w, "{%s}", v.Interface())
		return
	}

	_, _ = w.Write([]byte{'{'})

	var sep bool
	for i := 0; i < v.NumField(); i++ {
		fv := v.Field(i)
		if fv.Kind() == reflect.Ptr && fv.IsNil() {
			continue
		}
		if fv.Kind() == reflect.Slice && fv.IsNil() {
			continue
		}

		if sep {
			_, _ = w.Write([]byte(", "))
		} else {
			sep = true
		}

		_, _ = w.Write([]byte(v.Type().Field(i).Name))
		_, _ = w.Write([]byte{':'})
		stringifyValue(w, fv)
	}

	_, _ = w.Write([]byte{'}'})
}


text:
package godo

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

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

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

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

	expected := []Image{{ID: 1}, {ID: 2}}
	if !reflect.DeepEqual(images, expected) {
		t.Errorf("Images.List returned %+v, expected %+v", images, expected)
	}
}

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

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

	images, _, err := client.Images.ListDistribution(ctx, nil)
	if err != nil {
		t.Errorf("Images.ListDistribution returned error: %v", err)
	}

	expected := []Image{{ID: 1}, {ID: 2}}
	if !reflect.DeepEqual(images, expected) {
		t.Errorf("Images.ListDistribution returned %+v, expected %+v", images, expected)
	}
}

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

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

	images, _, err := client.Images.ListApplication(ctx, nil)
	if err != nil {
		t.Errorf("Images.ListApplication returned error: %v", err)
	}

	expected := []Image{{ID: 1}, {ID: 2}}
	if !reflect.DeepEqual(images, expected) {
		t.Errorf("Images.ListApplication returned %+v, expected %+v", images, expected)
	}
}

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

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

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

	images, _, err := client.Images.ListUser(ctx, nil)
	if err != nil {
		t.Errorf("Images.ListUser returned error: %v", err)
	}

	expected := []Image{{ID: 1}, {ID: 2}}
	if !reflect.DeepEqual(images, expected) {
		t.Errorf("Images.ListUser returned %+v, expected %+v", images, expected)
	}
}

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

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

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

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

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

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

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

	checkCurrentPage(t, resp, 2)
}

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

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

	images, _, err := client.Images.GetByID(ctx, 12345)
	if err != nil {
		t.Errorf("Image.GetByID returned error: %v", err)
	}

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

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

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

	images, _, err := client.Images.GetBySlug(ctx, "ubuntu")
	if err != nil {
		t.Errorf("Image.GetBySlug returned error: %v", err)
	}

	expected := &Image{ID: 12345}
	if !reflect.DeepEqual(images, expected) {
		t.Errorf("Images.Get returned %+v, expected %+v", images, expected)
	}
}

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

	updateRequest := &ImageUpdateRequest{
		Name: "name",
	}

	mux.HandleFunc("/v2/images/12345", func(w http.ResponseWriter, r *http.Request) {
		expected := map[string]interface{}{
			"name": "name",
		}

		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, `{"image":{"id":1}}`)
	})

	image, _, err := client.Images.Update(ctx, 12345, updateRequest)
	if err != nil {
		t.Errorf("Images.Update returned error: %v", err)
	} else {
		if id := image.ID; id != 1 {
			t.Errorf("expected id '%d', received '%d'", 1, id)
		}
	}
}

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

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

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

func TestImage_String(t *testing.T) {
	image := &Image{
		ID:           1,
		Name:         "Image",
		Type:         "snapshot",
		Distribution: "Ubuntu",
		Slug:         "image",
		Public:       true,
		Regions:      []string{"one", "two"},
		MinDiskSize:  20,
		Created:      "2013-11-27T09:24:55Z",
	}

	stringified := image.String()
	expected := `godo.Image{ID:1, Name:"Image", Type:"snapshot", Distribution:"Ubuntu", Slug:"image", Public:true, Regions:["one" "two"], MinDiskSize:20, Created:"2013-11-27T09:24:55Z"}`
	if expected != stringified {
		t.Errorf("Image.String returned %+v, expected %+v", stringified, expected)
	}
}