Repository: hetznercloud/hcloud-go
Pull request #25 (2018-01-10 09:05:05)

Implement location client


Patch:
diff --git a/hcloud/client.go b/hcloud/client.go
index 5cc8023..eb8d5a9 100644
--- a/hcloud/client.go
+++ b/hcloud/client.go
@@ -52,8 +52,10 @@ type Client struct {
 	httpClient  *http.Client
 
 	Action     ActionClient
+	Datacenter DatacenterClient
 	FloatingIP FloatingIPClient
 	Image      ImageClient
+	Location   LocationClient
 	Server     ServerClient
 	ServerType ServerTypeClient
 	SSHKey     SSHKeyClient
@@ -96,8 +98,10 @@ func NewClient(options ...ClientOption) *Client {
 	}
 
 	client.Action = ActionClient{client: client}
+	client.Datacenter = DatacenterClient{client: client}
 	client.FloatingIP = FloatingIPClient{client: client}
 	client.Image = ImageClient{client: client}
+	client.Location = LocationClient{client: client}
 	client.Server = ServerClient{client: client}
 	client.ServerType = ServerTypeClient{client: client}
 	client.SSHKey = SSHKeyClient{client: client}
diff --git a/hcloud/datacenter.go b/hcloud/datacenter.go
index 5ac4d5a..7b4429f 100644
--- a/hcloud/datacenter.go
+++ b/hcloud/datacenter.go
@@ -1,9 +1,124 @@
 package hcloud
 
+import (
+	"context"
+	"fmt"
+	"net/url"
+	"strconv"
+
+	"github.com/hetznercloud/hcloud-go/hcloud/schema"
+)
+
 // Datacenter represents a datacenter in the Hetzner Cloud.
 type Datacenter struct {
 	ID          int
 	Name        string
 	Description string
 	Location    *Location
+	ServerTypes DatacenterServerTypes
+}
+
+// DatacenterServerTypes represents the server types available and supported in a datacenter.
+type DatacenterServerTypes struct {
+	Supported []*ServerType
+	Available []*ServerType
+}
+
+// DatacenterClient is a client for the datacenter API.
+type DatacenterClient struct {
+	client *Client
+}
+
+// GetByID retrieves a datacenter by its ID.
+func (c *DatacenterClient) GetByID(ctx context.Context, id int) (*Datacenter, *Response, error) {
+	req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/datacenters/%d", id), nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var body schema.DatacenterGetResponse
+	resp, err := c.client.Do(req, &body)
+	if err != nil {
+		if IsError(err, ErrorCodeNotFound) {
+			return nil, resp, nil
+		}
+		return nil, resp, err
+	}
+	return DatacenterFromSchema(body.Datacenter), resp, nil
+}
+
+// GetByName retrieves an datacenter by its name.
+func (c *DatacenterClient) GetByName(ctx context.Context, name string) (*Datacenter, *Response, error) {
+	path := "/datacenters?name=" + url.QueryEscape(name)
+	req, err := c.client.NewRequest(ctx, "GET", path, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var body schema.DatacenterListResponse
+	resp, err := c.client.Do(req, &body)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	if len(body.Datacenters) == 0 {
+		return nil, resp, nil
+	}
+	return DatacenterFromSchema(body.Datacenters[0]), resp, nil
+}
+
+// Get retrieves a datacenter by its ID if the input can be parsed as an integer, otherwise it retrieves a datacenter by its name.
+func (c *DatacenterClient) Get(ctx context.Context, idOrName string) (*Datacenter, *Response, error) {
+	if id, err := strconv.Atoi(idOrName); err == nil {
+		return c.GetByID(ctx, int(id))
+	}
+	return c.GetByName(ctx, idOrName)
+}
+
+// DatacenterListOpts specifies options for listing datacenters.
+type DatacenterListOpts struct {
+	ListOpts
+}
+
+// List returns a list of datacenters for a specific page.
+func (c *DatacenterClient) List(ctx context.Context, opts DatacenterListOpts) ([]*Datacenter, *Response, error) {
+	path := "/datacenters?" + valuesForListOpts(opts.ListOpts).Encode()
+	req, err := c.client.NewRequest(ctx, "GET", path, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var body schema.DatacenterListResponse
+	resp, err := c.client.Do(req, &body)
+	if err != nil {
+		return nil, nil, err
+	}
+	datacenters := make([]*Datacenter, 0, len(body.Datacenters))
+	for _, i := range body.Datacenters {
+		datacenters = append(datacenters, DatacenterFromSchema(i))
+	}
+	return datacenters, resp, nil
+}
+
+// All returns all datacenters.
+func (c *DatacenterClient) All(ctx context.Context) ([]*Datacenter, error) {
+	allDatacenters := []*Datacenter{}
+
+	opts := DatacenterListOpts{}
+	opts.PerPage = 50
+
+	_, err := c.client.all(func(page int) (*Response, error) {
+		opts.Page = page
+		datacenters, resp, err := c.List(ctx, opts)
+		if err != nil {
+			return resp, err
+		}
+		allDatacenters = append(allDatacenters, datacenters...)
+		return resp, nil
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return allDatacenters, nil
 }
diff --git a/hcloud/location.go b/hcloud/location.go
index 9aa95cd..4ab55ef 100644
--- a/hcloud/location.go
+++ b/hcloud/location.go
@@ -1,5 +1,14 @@
 package hcloud
 
+import (
+	"context"
+	"fmt"
+	"net/url"
+	"strconv"
+
+	"github.com/hetznercloud/hcloud-go/hcloud/schema"
+)
+
 // Location represents a location in the Hetzner Cloud.
 type Location struct {
 	ID          int
@@ -10,3 +19,102 @@ type Location struct {
 	Latitude    float64
 	Longitude   float64
 }
+
+// LocationClient is a client for the location API.
+type LocationClient struct {
+	client *Client
+}
+
+// GetByID retrieves a location by its ID.
+func (c *LocationClient) GetByID(ctx context.Context, id int) (*Location, *Response, error) {
+	req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/locations/%d", id), nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var body schema.LocationGetResponse
+	resp, err := c.client.Do(req, &body)
+	if err != nil {
+		if IsError(err, ErrorCodeNotFound) {
+			return nil, resp, nil
+		}
+		return nil, resp, err
+	}
+	return LocationFromSchema(body.Location), resp, nil
+}
+
+// GetByName retrieves an location by its name.
+func (c *LocationClient) GetByName(ctx context.Context, name string) (*Location, *Response, error) {
+	path := "/locations?name=" + url.QueryEscape(name)
+	req, err := c.client.NewRequest(ctx, "GET", path, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var body schema.LocationListResponse
+	resp, err := c.client.Do(req, &body)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	if len(body.Locations) == 0 {
+		return nil, resp, nil
+	}
+	return LocationFromSchema(body.Locations[0]), resp, nil
+}
+
+// Get retrieves a location by its ID if the input can be parsed as an integer, otherwise it retrieves a location by its name.
+func (c *LocationClient) Get(ctx context.Context, idOrName string) (*Location, *Response, error) {
+	if id, err := strconv.Atoi(idOrName); err == nil {
+		return c.GetByID(ctx, int(id))
+	}
+	return c.GetByName(ctx, idOrName)
+}
+
+// LocationListOpts specifies options for listing location.
+type LocationListOpts struct {
+	ListOpts
+}
+
+// List returns a list of locations for a specific page.
+func (c *LocationClient) List(ctx context.Context, opts LocationListOpts) ([]*Location, *Response, error) {
+	path := "/locations?" + valuesForListOpts(opts.ListOpts).Encode()
+	req, err := c.client.NewRequest(ctx, "GET", path, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var body schema.LocationListResponse
+	resp, err := c.client.Do(req, &body)
+	if err != nil {
+		return nil, nil, err
+	}
+	locations := make([]*Location, 0, len(body.Locations))
+	for _, i := range body.Locations {
+		locations = append(locations, LocationFromSchema(i))
+	}
+	return locations, resp, nil
+}
+
+// All returns all locations.
+func (c *LocationClient) All(ctx context.Context) ([]*Location, error) {
+	allLocations := []*Location{}
+
+	opts := LocationListOpts{}
+	opts.PerPage = 50
+
+	_, err := c.client.all(func(page int) (*Response, error) {
+		opts.Page = page
+		locations, resp, err := c.List(ctx, opts)
+		if err != nil {
+			return resp, err
+		}
+		allLocations = append(allLocations, locations...)
+		return resp, nil
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return allLocations, nil
+}
diff --git a/hcloud/schema.go b/hcloud/schema.go
index 39f339b..9b6eab8 100644
--- a/hcloud/schema.go
+++ b/hcloud/schema.go
@@ -86,12 +86,23 @@ func LocationFromSchema(s schema.Location) *Location {
 
 // DatacenterFromSchema converts a schema.Datacenter to a Datacenter.
 func DatacenterFromSchema(s schema.Datacenter) *Datacenter {
-	return &Datacenter{
+	d := &Datacenter{
 		ID:          s.ID,
 		Name:        s.Name,
 		Description: s.Description,
 		Location:    LocationFromSchema(s.Location),
+		ServerTypes: DatacenterServerTypes{
+			Available: []*ServerType{},
+			Supported: []*ServerType{},
+		},
 	}
+	for _, t := range s.ServerTypes.Available {
+		d.ServerTypes.Available = append(d.ServerTypes.Available, &ServerType{ID: t})
+	}
+	for _, t := range s.ServerTypes.Supported {
+		d.ServerTypes.Supported = append(d.ServerTypes.Supported, &ServerType{ID: t})
+	}
+	return d
 }
 
 // ServerFromSchema converts a schema.Server to a Server.
diff --git a/hcloud/schema/datacenter.go b/hcloud/schema/datacenter.go
index 0d15080..3e8178e 100644
--- a/hcloud/schema/datacenter.go
+++ b/hcloud/schema/datacenter.go
@@ -6,4 +6,18 @@ type Datacenter struct {
 	Name        string   `json:"name"`
 	Description string   `json:"description"`
 	Location    Location `json:"location"`
+	ServerTypes struct {
+		Supported []int `json:"supported"`
+		Available []int `json:"available"`
+	} `json:"server_types"`
+}
+
+// DatacenterGetResponse defines the schema of the response when retrieving a single datacenter.
+type DatacenterGetResponse struct {
+	Datacenter Datacenter `json:"datacenter"`
+}
+
+// DatacenterListResponse defines the schema of the response when listing datacenters.
+type DatacenterListResponse struct {
+	Datacenters []Datacenter `json:"datacenters"`
 }
diff --git a/hcloud/schema/location.go b/hcloud/schema/location.go
index c9eb7cf..1f030a9 100644
--- a/hcloud/schema/location.go
+++ b/hcloud/schema/location.go
@@ -10,3 +10,13 @@ type Location struct {
 	Latitude    float64 `json:"latitude"`
 	Longitude   float64 `json:"longitude"`
 }
+
+// LocationGetResponse defines the schema of the response when retrieving a single location.
+type LocationGetResponse struct {
+	Location Location `json:"location"`
+}
+
+// LocationListResponse defines the schema of the response when listing locations.
+type LocationListResponse struct {
+	Locations []Location `json:"locations"`
+}


Test patch:
diff --git a/hcloud/datacenter_test.go b/hcloud/datacenter_test.go
new file mode 100644
index 0000000..8b98206
--- /dev/null
+++ b/hcloud/datacenter_test.go
@@ -0,0 +1,212 @@
+package hcloud
+
+import (
+	"context"
+	"encoding/json"
+	"net/http"
+	"testing"
+
+	"github.com/hetznercloud/hcloud-go/hcloud/schema"
+)
+
+func TestDatacenterClient(t *testing.T) {
+	t.Run("GetByID", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/datacenters/1", func(w http.ResponseWriter, r *http.Request) {
+			json.NewEncoder(w).Encode(schema.DatacenterGetResponse{
+				Datacenter: schema.Datacenter{
+					ID: 1,
+				},
+			})
+		})
+
+		ctx := context.Background()
+		datacenter, _, err := env.Client.Datacenter.GetByID(ctx, 1)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if datacenter == nil {
+			t.Fatal("no datacenter")
+		}
+		if datacenter.ID != 1 {
+			t.Errorf("unexpected datacenter ID: %v", datacenter.ID)
+		}
+
+		t.Run("via Get", func(t *testing.T) {
+			datacenter, _, err := env.Client.Datacenter.Get(ctx, "1")
+			if err != nil {
+				t.Fatal(err)
+			}
+			if datacenter == nil {
+				t.Fatal("no datacenter")
+			}
+			if datacenter.ID != 1 {
+				t.Errorf("unexpected datacenter ID: %v", datacenter.ID)
+			}
+		})
+	})
+
+	t.Run("GetByID (not found)", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/datacenters/1", func(w http.ResponseWriter, r *http.Request) {
+			w.Header().Set("Content-Type", "application/json")
+			w.WriteHeader(http.StatusNotFound)
+			json.NewEncoder(w).Encode(schema.ErrorResponse{
+				Error: schema.Error{
+					Code: ErrorCodeNotFound,
+				},
+			})
+		})
+
+		ctx := context.Background()
+		datacenter, _, err := env.Client.Datacenter.GetByID(ctx, 1)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if datacenter != nil {
+			t.Fatal("expected no datacenter")
+		}
+	})
+
+	t.Run("GetByName", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/datacenters", func(w http.ResponseWriter, r *http.Request) {
+			if r.URL.RawQuery != "name=fsn1-dc8" {
+				t.Fatal("missing name query")
+			}
+			json.NewEncoder(w).Encode(schema.DatacenterListResponse{
+				Datacenters: []schema.Datacenter{
+					{
+						ID: 1,
+					},
+				},
+			})
+		})
+
+		ctx := context.Background()
+		datacenter, _, err := env.Client.Datacenter.GetByName(ctx, "fsn1-dc8")
+		if err != nil {
+			t.Fatal(err)
+		}
+		if datacenter == nil {
+			t.Fatal("no datacenter")
+		}
+		if datacenter.ID != 1 {
+			t.Errorf("unexpected datacenter ID: %v", datacenter.ID)
+		}
+
+		t.Run("via Get", func(t *testing.T) {
+			datacenter, _, err := env.Client.Datacenter.Get(ctx, "fsn1-dc8")
+			if err != nil {
+				t.Fatal(err)
+			}
+			if datacenter == nil {
+				t.Fatal("no datacenter")
+			}
+			if datacenter.ID != 1 {
+				t.Errorf("unexpected datacenter ID: %v", datacenter.ID)
+			}
+		})
+	})
+
+	t.Run("GetByName (not found)", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/datacenters", func(w http.ResponseWriter, r *http.Request) {
+			if r.URL.RawQuery != "name=fsn1-dc8" {
+				t.Fatal("missing name query")
+			}
+			json.NewEncoder(w).Encode(schema.DatacenterListResponse{
+				Datacenters: []schema.Datacenter{},
+			})
+		})
+
+		ctx := context.Background()
+		datacenter, _, err := env.Client.Datacenter.GetByName(ctx, "fsn1-dc8")
+		if err != nil {
+			t.Fatal(err)
+		}
+		if datacenter != nil {
+			t.Fatal("unexpected datacenter")
+		}
+	})
+
+	t.Run("List", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/datacenters", func(w http.ResponseWriter, r *http.Request) {
+			if page := r.URL.Query().Get("page"); page != "2" {
+				t.Errorf("expected page 2; got %q", page)
+			}
+			if perPage := r.URL.Query().Get("per_page"); perPage != "50" {
+				t.Errorf("expected per_page 50; got %q", perPage)
+			}
+			json.NewEncoder(w).Encode(schema.DatacenterListResponse{
+				Datacenters: []schema.Datacenter{
+					{ID: 1},
+					{ID: 2},
+				},
+			})
+		})
+
+		opts := DatacenterListOpts{}
+		opts.Page = 2
+		opts.PerPage = 50
+
+		ctx := context.Background()
+		datacenters, _, err := env.Client.Datacenter.List(ctx, opts)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if len(datacenters) != 2 {
+			t.Fatal("expected 2 datacenters")
+		}
+	})
+
+	t.Run("All", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/datacenters", func(w http.ResponseWriter, r *http.Request) {
+			w.Header().Set("Content-Type", "application/json")
+			json.NewEncoder(w).Encode(struct {
+				Datacenters []schema.Datacenter `json:"datacenters"`
+				Meta        schema.Meta         `json:"meta"`
+			}{
+				Datacenters: []schema.Datacenter{
+					{ID: 1},
+					{ID: 2},
+					{ID: 3},
+				},
+				Meta: schema.Meta{
+					Pagination: &schema.MetaPagination{
+						Page:         1,
+						LastPage:     1,
+						PerPage:      3,
+						TotalEntries: 3,
+					},
+				},
+			})
+		})
+
+		ctx := context.Background()
+		datacenters, err := env.Client.Datacenter.All(ctx)
+		if err != nil {
+			t.Fatalf("Datacenter.List failed: %s", err)
+		}
+		if len(datacenters) != 3 {
+			t.Fatalf("expected 3 datacenters; got %d", len(datacenters))
+		}
+		if datacenters[0].ID != 1 || datacenters[1].ID != 2 || datacenters[2].ID != 3 {
+			t.Errorf("unexpected datacenters")
+		}
+	})
+}
diff --git a/hcloud/location_test.go b/hcloud/location_test.go
new file mode 100644
index 0000000..5f8d330
--- /dev/null
+++ b/hcloud/location_test.go
@@ -0,0 +1,212 @@
+package hcloud
+
+import (
+	"context"
+	"encoding/json"
+	"net/http"
+	"testing"
+
+	"github.com/hetznercloud/hcloud-go/hcloud/schema"
+)
+
+func TestLocationClient(t *testing.T) {
+	t.Run("GetByID", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/locations/1", func(w http.ResponseWriter, r *http.Request) {
+			json.NewEncoder(w).Encode(schema.LocationGetResponse{
+				Location: schema.Location{
+					ID: 1,
+				},
+			})
+		})
+
+		ctx := context.Background()
+		location, _, err := env.Client.Location.GetByID(ctx, 1)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if location == nil {
+			t.Fatal("no location")
+		}
+		if location.ID != 1 {
+			t.Errorf("unexpected location ID: %v", location.ID)
+		}
+
+		t.Run("via Get", func(t *testing.T) {
+			location, _, err := env.Client.Location.Get(ctx, "1")
+			if err != nil {
+				t.Fatal(err)
+			}
+			if location == nil {
+				t.Fatal("no location")
+			}
+			if location.ID != 1 {
+				t.Errorf("unexpected location ID: %v", location.ID)
+			}
+		})
+	})
+
+	t.Run("GetByID (not found)", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/locations/1", func(w http.ResponseWriter, r *http.Request) {
+			w.Header().Set("Content-Type", "application/json")
+			w.WriteHeader(http.StatusNotFound)
+			json.NewEncoder(w).Encode(schema.ErrorResponse{
+				Error: schema.Error{
+					Code: ErrorCodeNotFound,
+				},
+			})
+		})
+
+		ctx := context.Background()
+		location, _, err := env.Client.Location.GetByID(ctx, 1)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if location != nil {
+			t.Fatal("expected no location")
+		}
+	})
+
+	t.Run("GetByName", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/locations", func(w http.ResponseWriter, r *http.Request) {
+			if r.URL.RawQuery != "name=fsn1-dc8" {
+				t.Fatal("missing name query")
+			}
+			json.NewEncoder(w).Encode(schema.LocationListResponse{
+				Locations: []schema.Location{
+					{
+						ID: 1,
+					},
+				},
+			})
+		})
+
+		ctx := context.Background()
+		location, _, err := env.Client.Location.GetByName(ctx, "fsn1-dc8")
+		if err != nil {
+			t.Fatal(err)
+		}
+		if location == nil {
+			t.Fatal("no location")
+		}
+		if location.ID != 1 {
+			t.Errorf("unexpected location ID: %v", location.ID)
+		}
+
+		t.Run("via Get", func(t *testing.T) {
+			location, _, err := env.Client.Location.Get(ctx, "fsn1-dc8")
+			if err != nil {
+				t.Fatal(err)
+			}
+			if location == nil {
+				t.Fatal("no location")
+			}
+			if location.ID != 1 {
+				t.Errorf("unexpected location ID: %v", location.ID)
+			}
+		})
+	})
+
+	t.Run("GetByName (not found)", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/locations", func(w http.ResponseWriter, r *http.Request) {
+			if r.URL.RawQuery != "name=fsn1-dc8" {
+				t.Fatal("missing name query")
+			}
+			json.NewEncoder(w).Encode(schema.LocationListResponse{
+				Locations: []schema.Location{},
+			})
+		})
+
+		ctx := context.Background()
+		location, _, err := env.Client.Location.GetByName(ctx, "fsn1-dc8")
+		if err != nil {
+			t.Fatal(err)
+		}
+		if location != nil {
+			t.Fatal("unexpected location")
+		}
+	})
+
+	t.Run("List", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/locations", func(w http.ResponseWriter, r *http.Request) {
+			if page := r.URL.Query().Get("page"); page != "2" {
+				t.Errorf("expected page 2; got %q", page)
+			}
+			if perPage := r.URL.Query().Get("per_page"); perPage != "50" {
+				t.Errorf("expected per_page 50; got %q", perPage)
+			}
+			json.NewEncoder(w).Encode(schema.LocationListResponse{
+				Locations: []schema.Location{
+					{ID: 1},
+					{ID: 2},
+				},
+			})
+		})
+
+		opts := LocationListOpts{}
+		opts.Page = 2
+		opts.PerPage = 50
+
+		ctx := context.Background()
+		locations, _, err := env.Client.Location.List(ctx, opts)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if len(locations) != 2 {
+			t.Fatal("expected 2 locations")
+		}
+	})
+
+	t.Run("All", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/locations", func(w http.ResponseWriter, r *http.Request) {
+			w.Header().Set("Content-Type", "application/json")
+			json.NewEncoder(w).Encode(struct {
+				Locations []schema.Location `json:"locations"`
+				Meta      schema.Meta       `json:"meta"`
+			}{
+				Locations: []schema.Location{
+					{ID: 1},
+					{ID: 2},
+					{ID: 3},
+				},
+				Meta: schema.Meta{
+					Pagination: &schema.MetaPagination{
+						Page:         1,
+						LastPage:     1,
+						PerPage:      3,
+						TotalEntries: 3,
+					},
+				},
+			})
+		})
+
+		ctx := context.Background()
+		locations, err := env.Client.Location.All(ctx)
+		if err != nil {
+			t.Fatalf("Location.List failed: %s", err)
+		}
+		if len(locations) != 3 {
+			t.Fatalf("expected 3 locations; got %d", len(locations))
+		}
+		if locations[0].ID != 1 || locations[1].ID != 2 || locations[2].ID != 3 {
+			t.Errorf("unexpected locations")
+		}
+	})
+}
diff --git a/hcloud/schema_test.go b/hcloud/schema_test.go
index fcefa5f..322d751 100644
--- a/hcloud/schema_test.go
+++ b/hcloud/schema_test.go
@@ -251,6 +251,12 @@ func TestDatacenterFromSchema(t *testing.T) {
 	if datacenter.Location == nil || datacenter.Location.ID != 1 {
 		t.Errorf("unexpected Location: %v", datacenter.Location)
 	}
+	if len(datacenter.ServerTypes.Available) != 4 {
+		t.Errorf("unexpected ServerTypes.Available (should be 4): %v", len(datacenter.ServerTypes.Available))
+	}
+	if len(datacenter.ServerTypes.Supported) != 4 {
+		t.Errorf("unexpected ServerTypes.Supported length (should be 4): %v", len(datacenter.ServerTypes.Supported))
+	}
 }
 
 func TestServerFromSchema(t *testing.T) {