From f1a77cbbf5da24a8f841b1b4652f0ce59ce2c631 Mon Sep 17 00:00:00 2001 From: Mark McDonnell Date: Fri, 17 Feb 2023 15:19:55 +0000 Subject: [PATCH] feat(product_enablement): implement API (#399) --- fastly/errors.go | 4 + .../fixtures/product_enablement/disable.yaml | 43 +++++++ .../fixtures/product_enablement/enable.yaml | 52 ++++++++ .../product_enablement/get-disabled.yaml | 42 ++++++ fastly/fixtures/product_enablement/get.yaml | 42 ++++++ fastly/product_enablement.go | 116 +++++++++++++++++ fastly/product_enablement_test.go | 121 ++++++++++++++++++ 7 files changed, 420 insertions(+) create mode 100644 fastly/fixtures/product_enablement/disable.yaml create mode 100644 fastly/fixtures/product_enablement/enable.yaml create mode 100644 fastly/fixtures/product_enablement/get-disabled.yaml create mode 100644 fastly/fixtures/product_enablement/get.yaml create mode 100644 fastly/product_enablement.go create mode 100644 fastly/product_enablement_test.go diff --git a/fastly/errors.go b/fastly/errors.go index 5c89173fb..62d8496d8 100644 --- a/fastly/errors.go +++ b/fastly/errors.go @@ -291,6 +291,10 @@ var ErrManagedLoggingEnabled = errors.New("managed logging already enabled") // requires a "Token" key, but one was not set. var ErrMissingToken = NewFieldError("Token") +// ErrMissingProductID is an error that is returned when an input struct +// requires a "ProductID" key, but one was not set. +var ErrMissingProductID = NewFieldError("ProductID") + // Ensure HTTPError is, in fact, an error. var _ error = (*HTTPError)(nil) diff --git a/fastly/fixtures/product_enablement/disable.yaml b/fastly/fixtures/product_enablement/disable.yaml new file mode 100644 index 000000000..30c5f23a2 --- /dev/null +++ b/fastly/fixtures/product_enablement/disable.yaml @@ -0,0 +1,43 @@ +--- +version: 1 +interactions: +- request: + body: "" + form: {} + headers: + User-Agent: + - FastlyGo/7.2.0 (+github.com/fastly/go-fastly; go1.18.5) + url: https://api.fastly.com/enabled-products/brotli_compression/services/7i6HN3TK9wS159v2gPAZ8A + method: DELETE + response: + body: "" + headers: + Accept-Ranges: + - bytes + Content-Type: + - application/json + Date: + - Fri, 17 Feb 2023 15:08:02 GMT + Fastly-Ratelimit-Remaining: + - "998" + Fastly-Ratelimit-Reset: + - "1676649600" + Status: + - 204 No Content + Strict-Transport-Security: + - max-age=31536000 + Vary: + - Accept-Encoding + Via: + - 1.1 varnish, 1.1 varnish + X-Cache: + - MISS, MISS + X-Cache-Hits: + - 0, 0 + X-Served-By: + - cache-control-cp-aws-us-east-2-prod-7-CONTROL-AWS-UE2, cache-lhr7368-LHR + X-Timer: + - S1676646482.821386,VS0,VE327 + status: 204 No Content + code: 204 + duration: "" diff --git a/fastly/fixtures/product_enablement/enable.yaml b/fastly/fixtures/product_enablement/enable.yaml new file mode 100644 index 000000000..82f6dff55 --- /dev/null +++ b/fastly/fixtures/product_enablement/enable.yaml @@ -0,0 +1,52 @@ +--- +version: 1 +interactions: +- request: + body: ProductID=brotli_compression&ServiceID=7i6HN3TK9wS159v2gPAZ8A + form: + ProductID: + - brotli_compression + ServiceID: + - 7i6HN3TK9wS159v2gPAZ8A + headers: + Content-Type: + - application/x-www-form-urlencoded + User-Agent: + - FastlyGo/7.2.0 (+github.com/fastly/go-fastly; go1.18.5) + url: https://api.fastly.com/enabled-products/brotli_compression/services/7i6HN3TK9wS159v2gPAZ8A + method: PUT + response: + body: | + {"product":{"id":"brotli_compression","object":"product"},"service":{"id":"7i6HN3TK9wS159v2gPAZ8A","object":"service"},"_links":{"self":"/enabled-products/brotli_compression/services/7i6HN3TK9wS159v2gPAZ8A"}} + headers: + Accept-Ranges: + - bytes + Content-Length: + - "209" + Content-Type: + - application/json + Date: + - Fri, 17 Feb 2023 15:08:01 GMT + Fastly-Ratelimit-Remaining: + - "999" + Fastly-Ratelimit-Reset: + - "1676649600" + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000 + Vary: + - Accept-Encoding + Via: + - 1.1 varnish, 1.1 varnish + X-Cache: + - MISS, MISS + X-Cache-Hits: + - 0, 0 + X-Served-By: + - cache-control-cp-aws-us-east-2-prod-1-CONTROL-AWS-UE2, cache-lhr7368-LHR + X-Timer: + - S1676646481.093534,VS0,VE491 + status: 200 OK + code: 200 + duration: "" diff --git a/fastly/fixtures/product_enablement/get-disabled.yaml b/fastly/fixtures/product_enablement/get-disabled.yaml new file mode 100644 index 000000000..89c5deb72 --- /dev/null +++ b/fastly/fixtures/product_enablement/get-disabled.yaml @@ -0,0 +1,42 @@ +--- +version: 1 +interactions: +- request: + body: "" + form: {} + headers: + User-Agent: + - FastlyGo/7.2.0 (+github.com/fastly/go-fastly; go1.18.5) + url: https://api.fastly.com/enabled-products/brotli_compression/services/7i6HN3TK9wS159v2gPAZ8A + method: GET + response: + body: | + {"type":"","title":"no product on service","status":400,"errors":null,"detail":""} + headers: + Accept-Ranges: + - bytes + Content-Length: + - "83" + Content-Type: + - application/json + Date: + - Fri, 17 Feb 2023 15:08:02 GMT + Status: + - 400 Bad Request + Strict-Transport-Security: + - max-age=31536000 + Vary: + - Accept-Encoding + Via: + - 1.1 varnish, 1.1 varnish + X-Cache: + - MISS, MISS + X-Cache-Hits: + - 0, 0 + X-Served-By: + - cache-control-cp-aws-us-east-2-prod-1-CONTROL-AWS-UE2, cache-lhr7368-LHR + X-Timer: + - S1676646482.180149,VS0,VE194 + status: 400 Bad Request + code: 400 + duration: "" diff --git a/fastly/fixtures/product_enablement/get.yaml b/fastly/fixtures/product_enablement/get.yaml new file mode 100644 index 000000000..629b6ded4 --- /dev/null +++ b/fastly/fixtures/product_enablement/get.yaml @@ -0,0 +1,42 @@ +--- +version: 1 +interactions: +- request: + body: "" + form: {} + headers: + User-Agent: + - FastlyGo/7.2.0 (+github.com/fastly/go-fastly; go1.18.5) + url: https://api.fastly.com/enabled-products/brotli_compression/services/7i6HN3TK9wS159v2gPAZ8A + method: GET + response: + body: | + {"product":{"id":"brotli_compression","object":"product"},"service":{"id":"7i6HN3TK9wS159v2gPAZ8A","object":"service"},"_links":{"self":"/enabled-products/brotli_compression/services/7i6HN3TK9wS159v2gPAZ8A"}} + headers: + Accept-Ranges: + - bytes + Content-Length: + - "209" + Content-Type: + - application/json + Date: + - Fri, 17 Feb 2023 15:08:01 GMT + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000 + Vary: + - Accept-Encoding + Via: + - 1.1 varnish, 1.1 varnish + X-Cache: + - MISS, MISS + X-Cache-Hits: + - 0, 0 + X-Served-By: + - cache-control-cp-aws-us-east-2-prod-2-CONTROL-AWS-UE2, cache-lhr7368-LHR + X-Timer: + - S1676646482.611546,VS0,VE190 + status: 200 OK + code: 200 + duration: "" diff --git a/fastly/product_enablement.go b/fastly/product_enablement.go new file mode 100644 index 000000000..04975b81f --- /dev/null +++ b/fastly/product_enablement.go @@ -0,0 +1,116 @@ +package fastly + +import ( + "fmt" +) + +// ProductEnablement represents a response from the Fastly API. +type ProductEnablement struct { + Product ProductEnablementNested `mapstructure:"product"` + Service ProductEnablementNested `mapstructure:"service"` +} + +type ProductEnablementNested struct { + ID string `mapstructure:"id,omitempty"` + Object string `mapstructure:"object,omitempty"` +} + +// Product is a base for the different product variants. +type Product int64 + +func (p Product) String() string { + switch p { + case ProductBrotliCompression: + return "brotli_compression" + case ProductDomainInspector: + return "domain_inspector" + case ProductFanout: + return "fanout" + case ProductImageOptimizer: + return "image_optimizer" + case ProductOriginInspector: + return "origin_inspector" + case ProductWebSockets: + return "websockets" + } + return "unknown" +} + +const ( + ProductUndefined Product = iota + ProductBrotliCompression + ProductDomainInspector + ProductFanout + ProductImageOptimizer + ProductOriginInspector + ProductWebSockets +) + +// ProductEnablementInput is used as input to the various product API functions. +type ProductEnablementInput struct { + // ProductID is the ID of the product and is constrained by the Product type (required). + ProductID Product + // ServiceID is the ID of the service (required). + ServiceID string +} + +// GetProduct retrieves the details of the product enabled on the service. +func (c *Client) GetProduct(i *ProductEnablementInput) (*ProductEnablement, error) { + if i.ProductID == ProductUndefined { + return nil, ErrMissingProductID + } + if i.ServiceID == "" { + return nil, ErrMissingServiceID + } + + path := fmt.Sprintf("/enabled-products/%s/services/%s", i.ProductID, i.ServiceID) + resp, err := c.Get(path, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var h *ProductEnablement + if err := decodeBodyMap(resp.Body, &h); err != nil { + return nil, err + } + + return h, nil +} + +// EnableProduct enables the specified product on the service. +func (c *Client) EnableProduct(i *ProductEnablementInput) (*ProductEnablement, error) { + if i.ProductID == ProductUndefined { + return nil, ErrMissingProductID + } + if i.ServiceID == "" { + return nil, ErrMissingServiceID + } + + path := fmt.Sprintf("/enabled-products/%s/services/%s", i.ProductID, i.ServiceID) + resp, err := c.PutForm(path, i, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var http3 *ProductEnablement + if err := decodeBodyMap(resp.Body, &http3); err != nil { + return nil, err + } + return http3, nil +} + +// DisableProduct disables the specified product on the service. +func (c *Client) DisableProduct(i *ProductEnablementInput) error { + if i.ProductID == ProductUndefined { + return ErrMissingProductID + } + if i.ServiceID == "" { + return ErrMissingServiceID + } + + path := fmt.Sprintf("/enabled-products/%s/services/%s", i.ProductID, i.ServiceID) + _, err := c.Delete(path, nil) + return err +} diff --git a/fastly/product_enablement_test.go b/fastly/product_enablement_test.go new file mode 100644 index 000000000..28acbd40f --- /dev/null +++ b/fastly/product_enablement_test.go @@ -0,0 +1,121 @@ +package fastly + +import ( + "testing" +) + +func TestClient_ProductEnablement(t *testing.T) { + t.Parallel() + + var err error + + // Enable Product + var pe *ProductEnablement + record(t, "product_enablement/enable", func(c *Client) { + pe, err = c.EnableProduct(&ProductEnablementInput{ + ProductID: ProductBrotliCompression, + ServiceID: testServiceID, + }) + }) + if err != nil { + t.Fatal(err) + } + + if pe.Product.ID != ProductBrotliCompression.String() { + t.Errorf("bad feature_revision: %s", pe.Product.ID) + } + + // Get Product status + var gpe *ProductEnablement + record(t, "product_enablement/get", func(c *Client) { + gpe, err = c.GetProduct(&ProductEnablementInput{ + ProductID: ProductBrotliCompression, + ServiceID: testServiceID, + }) + }) + if err != nil { + t.Fatal(err) + } + + if gpe.Product.ID != ProductBrotliCompression.String() { + t.Errorf("bad feature_revision: %s", gpe.Product.ID) + } + + // Disable Product + record(t, "product_enablement/disable", func(c *Client) { + err = c.DisableProduct(&ProductEnablementInput{ + ProductID: ProductBrotliCompression, + ServiceID: testServiceID, + }) + }) + if err != nil { + t.Fatal(err) + } + + // Get Product status again to check disabled + record(t, "product_enablement/get-disabled", func(c *Client) { + gpe, err = c.GetProduct(&ProductEnablementInput{ + ProductID: ProductBrotliCompression, + ServiceID: testServiceID, + }) + }) + + // The API returns a 400 if Product is not enabled. + // The API client returns an error if a non-2xx is returned from the API. + if err == nil { + t.Fatal("expected a 400 from the API but got a 2xx") + } +} + +func TestClient_GetProduct_validation(t *testing.T) { + var err error + + _, err = testClient.GetProduct(&ProductEnablementInput{ + ProductID: ProductBrotliCompression, + }) + if err != ErrMissingServiceID { + t.Errorf("bad error: %s", err) + } + + _, err = testClient.GetProduct(&ProductEnablementInput{ + ServiceID: "foo", + }) + if err != ErrMissingProductID { + t.Errorf("bad error: %s", err) + } +} + +func TestClient_EnableProduct_validation(t *testing.T) { + var err error + _, err = testClient.EnableProduct(&ProductEnablementInput{ + ProductID: ProductBrotliCompression, + }) + if err != ErrMissingServiceID { + t.Errorf("bad error: %s", err) + } + + _, err = testClient.EnableProduct(&ProductEnablementInput{ + ServiceID: "foo", + }) + if err != ErrMissingProductID { + t.Errorf("bad error: %s", err) + } +} + +func TestClient_DisableProduct_validation(t *testing.T) { + var err error + + err = testClient.DisableProduct(&ProductEnablementInput{ + ProductID: ProductBrotliCompression, + }) + if err != ErrMissingServiceID { + t.Errorf("bad error: %s", err) + } + + err = testClient.DisableProduct(&ProductEnablementInput{ + ServiceID: "foo", + }) + if err != ErrMissingProductID { + t.Errorf("bad error: %s", err) + } +}