From 0ef62fb4ff802e79c6d839485ba2d5f2e5d9195c Mon Sep 17 00:00:00 2001 From: "Piotrowski, Piotr" Date: Mon, 14 Sep 2020 15:56:04 +0200 Subject: [PATCH 1/5] [TFP-170] Add GET and POST for property versions --- go.sum | 2 +- pkg/papi/papi.go | 12 + pkg/papi/propertyversion.go | 233 ++++++++++++++++ pkg/papi/propertyversion_test.go | 445 +++++++++++++++++++++++++++++++ 4 files changed, 691 insertions(+), 1 deletion(-) create mode 100644 pkg/papi/propertyversion.go create mode 100644 pkg/papi/propertyversion_test.go diff --git a/go.sum b/go.sum index 8418954a..d07f6bf7 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,7 @@ github.com/apex/log v1.9.0/go.mod h1:m82fZlWIuiWzWP04XCTXmnX0xRkYYbCdYn8jbJeLBEA github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= +github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= @@ -14,7 +15,6 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE= github.com/go-ozzo/ozzo-validation/v4 v4.2.2 h1:5uhbQAuRK6taB9orHJXA5GtOCuQbsHktskg8aWciC68= github.com/go-ozzo/ozzo-validation/v4 v4.2.2/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= diff --git a/pkg/papi/papi.go b/pkg/papi/papi.go index 8f9b164f..25405522 100644 --- a/pkg/papi/papi.go +++ b/pkg/papi/papi.go @@ -59,6 +59,18 @@ type ( // CreateEdgeHostname creates a new edge hostname // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#postedgehostnames CreateEdgeHostname(context.Context, CreateEdgeHostnameRequest) (*CreateEdgeHostnameResponse, error) + + // GetPropertyVersions creates a new CP code + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getpropertyversions + GetPropertyVersions(context.Context, GetPropertyVersionsRequest) (*GetPropertyVersionsResponse, error) + + // GetPropertyVersion creates a new CP code + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getpropertyversion + GetPropertyVersion(context.Context, GetPropertyVersionRequest) (*GetPropertyVersionsResponse, error) + + // CreatePropertyVersion creates a new CP code + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#postpropertyversions + CreatePropertyVersion(context.Context, CreatePropertyVersionRequest) (*CreatePropertyVersionResponse, error) } papi struct { diff --git a/pkg/papi/propertyversion.go b/pkg/papi/propertyversion.go new file mode 100644 index 00000000..31027e97 --- /dev/null +++ b/pkg/papi/propertyversion.go @@ -0,0 +1,233 @@ +package papi + +import ( + "context" + "fmt" + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v2/pkg/papi/tools" + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v2/pkg/session" + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/spf13/cast" + "net/http" + "strconv" +) + +type ( + // GetPropertyVersionsRequest contains path and query params used for listing property versions + GetPropertyVersionsRequest struct { + PropertyID string + ContractID string + GroupID string + Limit int + Offset int + } + + // GetPropertyVersionsResponse contains GET response returned while fetching property versions or specific version + GetPropertyVersionsResponse struct { + PropertyID string `json:"propertyId"` + PropertyName string `json:"propertyName"` + AccountID string `json:"accountId"` + ContractID string `json:"contractId"` + GroupID string `json:"groupId"` + AssetID string `json:"assetId"` + Versions PropertyVersionItems `json:"versions"` + } + + // PropertyVersionItems contains collection of property version details + PropertyVersionItems struct { + Items []PropertyVersionGetItem `json:"items"` + } + + // PropertyVersionGetItem contains detailed information about specific property version returned in GET + PropertyVersionGetItem struct { + Etag string `json:"etag"` + Note string `json:"note"` + ProductID string `json:"productId"` + ProductionStatus string `json:"productionStatus"` + PropertyVersion int `json:"propertyVersion"` + RuleFormat string `json:"ruleFormat"` + StagingStatus string `json:"stagingStatus"` + UpdatedByUser string `json:"updatedByUser"` + UpdatedDate string `json:"updatedDate"` + } + + // GetPropertyVersionRequest contains path and query params used for fetching specific property version + GetPropertyVersionRequest struct { + PropertyID string + PropertyVersion int + ContractID string + GroupID string + } + + // CreatePropertyVersionRequest contains path and query params, as well as request body required to execute POST /versions request + CreatePropertyVersionRequest struct { + PropertyID string + ContractID string + GroupID string + Version PropertyVersionCreate + } + + // PropertyVersionCreate contains request body used in POST /versions request + PropertyVersionCreate struct { + CreateFromVersion int `json:"createFromVersion"` + CreateFromVersionEtag string `json:"createFromVersionEtag"` + } + + // CreatePropertyVersionResponse contains a link returned after creating new property version and version number of this version + CreatePropertyVersionResponse struct { + VersionLink string `json:"versionLink"` + PropertyVersion int + } +) + +// Validate validates GetPropertyVersionsRequest +func (v GetPropertyVersionsRequest) Validate() error { + return validation.Errors{ + "PropertyID": validation.Validate(v.PropertyID, validation.Required), + }.Filter() +} + +// Validate validates GetPropertyVersionRequest +func (v GetPropertyVersionRequest) Validate() error { + return validation.Errors{ + "PropertyID": validation.Validate(v.PropertyID, validation.Required), + "PropertyVersion": validation.Validate(v.PropertyVersion, validation.Required), + }.Filter() +} + +// Validate validates CreatePropertyVersionRequest +func (v CreatePropertyVersionRequest) Validate() error { + return validation.Errors{ + "PropertyID": validation.Validate(v.PropertyID, validation.Required), + "Version": validation.Validate(v.Version), + }.Filter() +} + +// Validate validates PropertyVersionCreate +func (v PropertyVersionCreate) Validate() error { + return validation.Errors{ + "CreateFromVersion": validation.Validate(v.CreateFromVersion, validation.Required), + }.Filter() +} + +// GetPropertyVersions returns list of property versions for give propertyID, contractID and groupID +func (p *papi) GetPropertyVersions(ctx context.Context, params GetPropertyVersionsRequest) (*GetPropertyVersionsResponse, error) { + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + logger := p.Log(ctx) + logger.Debug("GetPropertyVersions") + + getURL := fmt.Sprintf( + "/papi/v1/properties/%s/versions?contractId=%s&groupId=%s", + params.PropertyID, + params.ContractID, + params.GroupID, + ) + if params.Limit != 0 { + getURL += fmt.Sprintf("&limit=%d", params.Limit) + } + if params.Offset != 0 { + getURL += fmt.Sprintf("&offset=%d", params.Offset) + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, getURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create getpropertyversions request: %w", err) + } + + req.Header.Set("PAPI-Use-Prefixes", cast.ToString(p.usePrefixes)) + var versions GetPropertyVersionsResponse + resp, err := p.Exec(req, &versions) + if err != nil { + return nil, fmt.Errorf("getpropertyversions request failed: %w", err) + } + + if resp.StatusCode == http.StatusNotFound { + return nil, fmt.Errorf("%w: %s", session.ErrNotFound, getURL) + } + if resp.StatusCode != http.StatusOK { + return nil, session.NewAPIError(resp, logger) + } + + return &versions, nil +} + +// GetPropertyVersion returns property version with provided version number +func (p *papi) GetPropertyVersion(ctx context.Context, params GetPropertyVersionRequest) (*GetPropertyVersionsResponse, error) { + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + logger := p.Log(ctx) + logger.Debug("GetPropertyVersion") + + getURL := fmt.Sprintf( + "/papi/v1/properties/%s/versions/%d?contractId=%s&groupId=%s", + params.PropertyID, + params.PropertyVersion, + params.ContractID, + params.GroupID, + ) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, getURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create getpropertyversion request: %w", err) + } + + req.Header.Set("PAPI-Use-Prefixes", cast.ToString(p.usePrefixes)) + var versions GetPropertyVersionsResponse + resp, err := p.Exec(req, &versions) + if err != nil { + return nil, fmt.Errorf("getpropertyversion request failed: %w", err) + } + + if resp.StatusCode == http.StatusNotFound { + return nil, fmt.Errorf("%w: %s", session.ErrNotFound, getURL) + } + if resp.StatusCode != http.StatusOK { + return nil, session.NewAPIError(resp, logger) + } + + return &versions, nil +} + +// CreatePropertyVersion creates a new property version and returns location and number for the new version +func (p *papi) CreatePropertyVersion(ctx context.Context, request CreatePropertyVersionRequest) (*CreatePropertyVersionResponse, error) { + if err := request.Validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + logger := p.Log(ctx) + logger.Debug("CreatePropertyVersion") + + getURL := fmt.Sprintf( + "/papi/v1/properties/%s/versions?contractId=%s&groupId=%s", + request.PropertyID, + request.ContractID, + request.GroupID, + ) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, getURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create createpropertyversion request: %w", err) + } + + req.Header.Set("PAPI-Use-Prefixes", cast.ToString(p.usePrefixes)) + var version CreatePropertyVersionResponse + resp, err := p.Exec(req, &version) + if err != nil { + return nil, fmt.Errorf("createpropertyversion request failed: %w", err) + } + + if resp.StatusCode != http.StatusCreated { + return nil, session.NewAPIError(resp, logger) + } + propertyVersion, err := tools.FetchIDFromLocation(version.VersionLink) + if err != nil { + return nil, fmt.Errorf("%w: %s", tools.ErrInvalidLocation, err.Error()) + } + versionNumber, err := strconv.Atoi(propertyVersion) + if err != nil { + return nil, err + } + version.PropertyVersion = versionNumber + return &version, nil +} diff --git a/pkg/papi/propertyversion_test.go b/pkg/papi/propertyversion_test.go new file mode 100644 index 00000000..de2e9dbe --- /dev/null +++ b/pkg/papi/propertyversion_test.go @@ -0,0 +1,445 @@ +package papi + +import ( + "context" + "errors" + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v2/pkg/papi/tools" + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v2/pkg/session" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "net/http" + "net/http/httptest" + "testing" +) + +func TestPapi_GetPropertyVersions(t *testing.T) { + tests := map[string]struct { + params GetPropertyVersionsRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *GetPropertyVersionsResponse + withError func(*testing.T, error) + }{ + "200 OK": { + params: GetPropertyVersionsRequest{ + PropertyID: "propertyID", + ContractID: "contract", + GroupID: "group", + Limit: 5, + Offset: 6, + }, + responseStatus: http.StatusOK, + responseBody: ` +{ + "propertyId": "propertyID", + "propertyName": "property_name", + "accountId": "accountID", + "contractId": "contract", + "groupId": "group", + "assetId": "assetID", + "versions": { + "items": [ + { + "propertyVersion": 2, + "updatedByUser": "user", + "updatedDate": "2020-09-14T19:06:13Z", + "productionStatus": "INACTIVE", + "stagingStatus": "ACTIVE", + "etag": "etag", + "productId": "productID", + "note": "version note" + } + ] + } +}`, + expectedPath: "/papi/v1/properties/propertyID/versions?contractId=contract&groupId=group&limit=5&offset=6", + expectedResponse: &GetPropertyVersionsResponse{ + PropertyID: "propertyID", + PropertyName: "property_name", + AccountID: "accountID", + ContractID: "contract", + GroupID: "group", + AssetID: "assetID", + Versions: PropertyVersionItems{ + Items: []PropertyVersionGetItem{ + { + Etag: "etag", + Note: "version note", + ProductID: "productID", + ProductionStatus: "INACTIVE", + PropertyVersion: 2, + StagingStatus: "ACTIVE", + UpdatedByUser: "user", + UpdatedDate: "2020-09-14T19:06:13Z", + }}}, + }, + }, + "404 Not Found": { + params: GetPropertyVersionsRequest{ + PropertyID: "propertyID", + ContractID: "contract", + GroupID: "group", + Limit: 5, + Offset: 6, + }, + responseStatus: http.StatusNotFound, + responseBody: ` +{ + "type": "not_found", + "title": "Not Found", + "detail": "Could not find property versions", + "status": 404 +}`, + expectedPath: "/papi/v1/properties/propertyID/versions?contractId=contract&groupId=group&limit=5&offset=6", + withError: func(t *testing.T, err error) { + want := session.ErrNotFound + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "500 Internal Server Error": { + params: GetPropertyVersionsRequest{ + PropertyID: "propertyID", + ContractID: "contract", + GroupID: "group", + Limit: 5, + Offset: 6, + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` +{ + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error fetching property versions", + "status": 505 +}`, + expectedPath: "/papi/v1/properties/propertyID/versions?contractId=contract&groupId=group&limit=5&offset=6", + withError: func(t *testing.T, err error) { + want := session.APIError{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error fetching property versions", + StatusCode: http.StatusInternalServerError, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "empty property ID": { + params: GetPropertyVersionsRequest{ + PropertyID: "", + ContractID: "contract", + GroupID: "group", + }, + withError: func(t *testing.T, err error) { + want := ErrStructValidation + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + assert.Contains(t, err.Error(), "PropertyID") + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodGet, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + result, err := client.GetPropertyVersions(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func TestPapi_GetPropertyVersion(t *testing.T) { + tests := map[string]struct { + params GetPropertyVersionRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *GetPropertyVersionsResponse + withError func(*testing.T, error) + }{ + "200 OK": { + params: GetPropertyVersionRequest{ + PropertyID: "propertyID", + PropertyVersion: 2, + ContractID: "contract", + GroupID: "group", + }, + responseStatus: http.StatusOK, + responseBody: ` +{ + "propertyId": "propertyID", + "propertyName": "property_name", + "accountId": "accountID", + "contractId": "contract", + "groupId": "group", + "assetId": "assetID", + "versions": { + "items": [ + { + "propertyVersion": 2, + "updatedByUser": "user", + "updatedDate": "2020-09-14T19:06:13Z", + "productionStatus": "INACTIVE", + "stagingStatus": "ACTIVE", + "etag": "etag", + "productId": "productID", + "note": "version note" + } + ] + } +}`, + expectedPath: "/papi/v1/properties/propertyID/versions/2?contractId=contract&groupId=group", + expectedResponse: &GetPropertyVersionsResponse{ + PropertyID: "propertyID", + PropertyName: "property_name", + AccountID: "accountID", + ContractID: "contract", + GroupID: "group", + AssetID: "assetID", + Versions: PropertyVersionItems{ + Items: []PropertyVersionGetItem{ + { + Etag: "etag", + Note: "version note", + ProductID: "productID", + ProductionStatus: "INACTIVE", + PropertyVersion: 2, + StagingStatus: "ACTIVE", + UpdatedByUser: "user", + UpdatedDate: "2020-09-14T19:06:13Z", + }}}, + }, + }, + "404 Not Found": { + params: GetPropertyVersionRequest{ + PropertyID: "propertyID", + PropertyVersion: 2, + ContractID: "contract", + GroupID: "group", + }, + responseStatus: http.StatusNotFound, + responseBody: ` +{ + "type": "not_found", + "title": "Not Found", + "detail": "Could not find property version", + "status": 404 +}`, + expectedPath: "/papi/v1/properties/propertyID/versions/2?contractId=contract&groupId=group", + withError: func(t *testing.T, err error) { + want := session.ErrNotFound + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "500 Internal Server Error": { + params: GetPropertyVersionRequest{ + PropertyID: "propertyID", + PropertyVersion: 2, + ContractID: "contract", + GroupID: "group", + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` +{ + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error fetching property version", + "status": 505 +}`, + expectedPath: "/papi/v1/properties/propertyID/versions/2?contractId=contract&groupId=group", + withError: func(t *testing.T, err error) { + want := session.APIError{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error fetching property version", + StatusCode: http.StatusInternalServerError, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "empty property ID": { + params: GetPropertyVersionRequest{ + PropertyID: "", + PropertyVersion: 2, + ContractID: "contract", + GroupID: "group", + }, + withError: func(t *testing.T, err error) { + want := ErrStructValidation + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + assert.Contains(t, err.Error(), "PropertyID") + }, + }, + "empty property version version": { + params: GetPropertyVersionRequest{ + PropertyID: "propertyID", + ContractID: "contract", + GroupID: "group", + }, + withError: func(t *testing.T, err error) { + want := ErrStructValidation + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + assert.Contains(t, err.Error(), "PropertyVersion") + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodGet, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + result, err := client.GetPropertyVersion(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func TestPapi_CreatePropertyVersion(t *testing.T) { + tests := map[string]struct { + params CreatePropertyVersionRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *CreatePropertyVersionResponse + withError func(*testing.T, error) + }{ + "201 Created": { + params: CreatePropertyVersionRequest{ + PropertyID: "propertyID", + ContractID: "contract", + GroupID: "group", + Version: PropertyVersionCreate{ + CreateFromVersion: 1, + }, + }, + responseStatus: http.StatusCreated, + responseBody: ` +{ + "versionLink": "/papi/v1/properties/propertyID/versions/2?contractId=contract&groupId=group" +}`, + expectedPath: "/papi/v1/properties/propertyID/versions?contractId=contract&groupId=group", + expectedResponse: &CreatePropertyVersionResponse{ + VersionLink: "/papi/v1/properties/propertyID/versions/2?contractId=contract&groupId=group", + PropertyVersion: 2, + }, + }, + "500 Internal Server Error": { + params: CreatePropertyVersionRequest{ + PropertyID: "propertyID", + ContractID: "contract", + GroupID: "group", + Version: PropertyVersionCreate{ + CreateFromVersion: 1, + }, + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` +{ + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error creating property version", + "status": 500 +}`, + expectedPath: "/papi/v1/properties/propertyID/versions?contractId=contract&groupId=group", + withError: func(t *testing.T, err error) { + want := session.APIError{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error creating property version", + StatusCode: http.StatusInternalServerError, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "empty property ID": { + params: CreatePropertyVersionRequest{ + PropertyID: "", + ContractID: "contract", + GroupID: "group", + Version: PropertyVersionCreate{ + CreateFromVersion: 1, + }, + }, + withError: func(t *testing.T, err error) { + want := ErrStructValidation + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + assert.Contains(t, err.Error(), "PropertyID") + }, + }, + "empty CreateFromVersion": { + params: CreatePropertyVersionRequest{ + PropertyID: "propertyID", + ContractID: "contract", + GroupID: "group", + Version: PropertyVersionCreate{}, + }, + withError: func(t *testing.T, err error) { + want := ErrStructValidation + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + assert.Contains(t, err.Error(), "CreateFromVersion") + }, + }, + "invalid location": { + params: CreatePropertyVersionRequest{ + PropertyID: "propertyID", + ContractID: "contract", + GroupID: "group", + Version: PropertyVersionCreate{ + CreateFromVersion: 1, + }, + }, + responseStatus: http.StatusCreated, + responseBody: ` +{ + "versionLink": ":" +}`, + expectedPath: "/papi/v1/properties/propertyID/versions?contractId=contract&groupId=group", + withError: func(t *testing.T, err error) { + want := tools.ErrInvalidLocation + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodPost, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + result, err := client.CreatePropertyVersion(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} From cd063982943b4687e35b4195370fc3ac0d228a92 Mon Sep 17 00:00:00 2001 From: "Piotrowski, Piotr" Date: Tue, 15 Sep 2020 11:05:08 +0200 Subject: [PATCH 2/5] [TFP-170] Add GetLatestVersion --- pkg/papi/papi.go | 2 + pkg/papi/propertyversion.go | 60 +++++++++++- pkg/papi/propertyversion_test.go | 154 +++++++++++++++++++++++++++++-- 3 files changed, 206 insertions(+), 10 deletions(-) diff --git a/pkg/papi/papi.go b/pkg/papi/papi.go index 25405522..1dcbad28 100644 --- a/pkg/papi/papi.go +++ b/pkg/papi/papi.go @@ -71,6 +71,8 @@ type ( // CreatePropertyVersion creates a new CP code // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#postpropertyversions CreatePropertyVersion(context.Context, CreatePropertyVersionRequest) (*CreatePropertyVersionResponse, error) + + GetLatestVersion(context.Context, GetLatestVersionRequest) (*GetPropertyVersionsResponse, error) } papi struct { diff --git a/pkg/papi/propertyversion.go b/pkg/papi/propertyversion.go index 31027e97..0d1f1def 100644 --- a/pkg/papi/propertyversion.go +++ b/pkg/papi/propertyversion.go @@ -77,6 +77,18 @@ type ( VersionLink string `json:"versionLink"` PropertyVersion int } + + GetLatestVersionRequest struct { + PropertyID string + ActivatedOn string + ContractID string + GroupID string + } +) + +const ( + VersionProduction = "PRODUCTION" + VersionStaging = "STAGING" ) // Validate validates GetPropertyVersionsRequest @@ -109,6 +121,13 @@ func (v PropertyVersionCreate) Validate() error { }.Filter() } +func (v GetLatestVersionRequest) Validate() error { + return validation.Errors{ + "PropertyID": validation.Validate(v.PropertyID, validation.Required), + "ActivatedOn": validation.Validate(v.ActivatedOn, validation.In(VersionProduction, VersionStaging)), + }.Filter() +} + // GetPropertyVersions returns list of property versions for give propertyID, contractID and groupID func (p *papi) GetPropertyVersions(ctx context.Context, params GetPropertyVersionsRequest) (*GetPropertyVersionsResponse, error) { if err := params.Validate(); err != nil { @@ -152,6 +171,45 @@ func (p *papi) GetPropertyVersions(ctx context.Context, params GetPropertyVersio return &versions, nil } +// GetLatestVersion returns either the latest property version overall, or the latest ACTIVE version on production or staging network +func (p *papi) GetLatestVersion(ctx context.Context, params GetLatestVersionRequest) (*GetPropertyVersionsResponse, error) { + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + logger := p.Log(ctx) + logger.Debug("GetLatestVersion") + + getURL := fmt.Sprintf( + "/papi/v1/properties/%s/versions/latest?contractId=%s&groupId=%s", + params.PropertyID, + params.ContractID, + params.GroupID, + ) + if params.ActivatedOn != "" { + getURL += fmt.Sprintf("&activatedOn=%s", params.ActivatedOn) + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, getURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create getlatestversion request: %w", err) + } + + req.Header.Set("PAPI-Use-Prefixes", cast.ToString(p.usePrefixes)) + var version GetPropertyVersionsResponse + resp, err := p.Exec(req, &version) + if err != nil { + return nil, fmt.Errorf("getlatestversion request failed: %w", err) + } + + if resp.StatusCode == http.StatusNotFound { + return nil, fmt.Errorf("%w: %s", session.ErrNotFound, getURL) + } + if resp.StatusCode != http.StatusOK { + return nil, session.NewAPIError(resp, logger) + } + return &version, nil +} + // GetPropertyVersion returns property version with provided version number func (p *papi) GetPropertyVersion(ctx context.Context, params GetPropertyVersionRequest) (*GetPropertyVersionsResponse, error) { if err := params.Validate(); err != nil { @@ -226,7 +284,7 @@ func (p *papi) CreatePropertyVersion(ctx context.Context, request CreateProperty } versionNumber, err := strconv.Atoi(propertyVersion) if err != nil { - return nil, err + return nil, fmt.Errorf("%w: %s: %s", tools.ErrInvalidLocation, "version should be a number", propertyVersion) } version.PropertyVersion = versionNumber return &version, nil diff --git a/pkg/papi/propertyversion_test.go b/pkg/papi/propertyversion_test.go index de2e9dbe..851aa3c3 100644 --- a/pkg/papi/propertyversion_test.go +++ b/pkg/papi/propertyversion_test.go @@ -336,9 +336,9 @@ func TestPapi_CreatePropertyVersion(t *testing.T) { }, responseStatus: http.StatusCreated, responseBody: ` -{ - "versionLink": "/papi/v1/properties/propertyID/versions/2?contractId=contract&groupId=group" -}`, + { + "versionLink": "/papi/v1/properties/propertyID/versions/2?contractId=contract&groupId=group" + }`, expectedPath: "/papi/v1/properties/propertyID/versions?contractId=contract&groupId=group", expectedResponse: &CreatePropertyVersionResponse{ VersionLink: "/papi/v1/properties/propertyID/versions/2?contractId=contract&groupId=group", @@ -356,12 +356,12 @@ func TestPapi_CreatePropertyVersion(t *testing.T) { }, responseStatus: http.StatusInternalServerError, responseBody: ` -{ - "type": "internal_error", - "title": "Internal Server Error", - "detail": "Error creating property version", - "status": 500 -}`, + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error creating property version", + "status": 500 + }`, expectedPath: "/papi/v1/properties/propertyID/versions?contractId=contract&groupId=group", withError: func(t *testing.T, err error) { want := session.APIError{ @@ -443,3 +443,139 @@ func TestPapi_CreatePropertyVersion(t *testing.T) { }) } } + +func TestPapi_GetLatestVersion(t *testing.T) { + tests := map[string]struct { + params GetLatestVersionRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *GetPropertyVersionsResponse + withError func(*testing.T, error) + }{ + "200 OK": { + params: GetLatestVersionRequest{ + PropertyID: "propertyID", + ActivatedOn: "STAGING", + ContractID: "contract", + GroupID: "group", + }, + responseStatus: http.StatusOK, + expectedPath: "/papi/v1/properties/propertyID/versions/latest?contractId=contract&groupId=group&activatedOn=STAGING", + responseBody: ` +{ + "propertyId": "propertyID", + "propertyName": "property_name", + "accountId": "accountID", + "contractId": "contract", + "groupId": "group", + "assetId": "assetID", + "versions": { + "items": [ + { + "propertyVersion": 2, + "updatedByUser": "user", + "updatedDate": "2020-09-14T19:06:13Z", + "productionStatus": "INACTIVE", + "stagingStatus": "ACTIVE", + "etag": "etag", + "productId": "productID", + "note": "version note" + } + ] + } +}`, + expectedResponse: &GetPropertyVersionsResponse{ + PropertyID: "propertyID", + PropertyName: "property_name", + AccountID: "accountID", + ContractID: "contract", + GroupID: "group", + AssetID: "assetID", + Versions: PropertyVersionItems{ + Items: []PropertyVersionGetItem{ + { + Etag: "etag", + Note: "version note", + ProductID: "productID", + ProductionStatus: "INACTIVE", + PropertyVersion: 2, + StagingStatus: "ACTIVE", + UpdatedByUser: "user", + UpdatedDate: "2020-09-14T19:06:13Z", + }}}, + }, + }, + "500 Internal Server Error": { + params: GetLatestVersionRequest{ + PropertyID: "propertyID", + ContractID: "contract", + GroupID: "group", + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` +{ + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error fetching latest version", + "status": 500 +}`, + expectedPath: "/papi/v1/properties/propertyID/versions/latest?contractId=contract&groupId=group", + withError: func(t *testing.T, err error) { + want := session.APIError{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error fetching latest version", + StatusCode: http.StatusInternalServerError, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "empty property ID": { + params: GetLatestVersionRequest{ + PropertyID: "", + ActivatedOn: "STAGING", + ContractID: "contract", + GroupID: "group", + }, + withError: func(t *testing.T, err error) { + want := ErrStructValidation + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + assert.Contains(t, err.Error(), "PropertyID") + }, + }, + "invalid ActivatedOn": { + params: GetLatestVersionRequest{ + PropertyID: "propertyID", + ActivatedOn: "test", + ContractID: "contract", + GroupID: "group", + }, + withError: func(t *testing.T, err error) { + want := ErrStructValidation + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + assert.Contains(t, err.Error(), "ActivatedOn") + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodGet, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + result, err := client.GetLatestVersion(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} From d7d3d91b3f0244bb69b22c90494c8bacba58e3c2 Mon Sep 17 00:00:00 2001 From: "Piotrowski, Piotr" Date: Tue, 15 Sep 2020 11:59:08 +0200 Subject: [PATCH 3/5] [TFP-170] Add GetAvailableBehaviors and GetAvailableCriteria --- pkg/papi/papi.go | 16 +- pkg/papi/propertyversion.go | 118 ++++++++++- pkg/papi/propertyversion_test.go | 322 +++++++++++++++++++++++++++++++ 3 files changed, 452 insertions(+), 4 deletions(-) diff --git a/pkg/papi/papi.go b/pkg/papi/papi.go index 1dcbad28..ea36b323 100644 --- a/pkg/papi/papi.go +++ b/pkg/papi/papi.go @@ -60,19 +60,29 @@ type ( // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#postedgehostnames CreateEdgeHostname(context.Context, CreateEdgeHostnameRequest) (*CreateEdgeHostnameResponse, error) - // GetPropertyVersions creates a new CP code + // GetPropertyVersions fetches available property versions // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getpropertyversions GetPropertyVersions(context.Context, GetPropertyVersionsRequest) (*GetPropertyVersionsResponse, error) - // GetPropertyVersion creates a new CP code + // GetPropertyVersion fetches specific property version // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getpropertyversion GetPropertyVersion(context.Context, GetPropertyVersionRequest) (*GetPropertyVersionsResponse, error) - // CreatePropertyVersion creates a new CP code + // CreatePropertyVersion creates a new property version // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#postpropertyversions CreatePropertyVersion(context.Context, CreatePropertyVersionRequest) (*CreatePropertyVersionResponse, error) + // GetLatestVersion fetches latest property version + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getlatestversion GetLatestVersion(context.Context, GetLatestVersionRequest) (*GetPropertyVersionsResponse, error) + + // GetAvailableBehaviors fetches a list of behaviors applied to property version + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getavailablebehaviors + GetAvailableBehaviors(context.Context, GetFeaturesRequest) (*GetFeaturesCriteriaResponse, error) + + // GetAvailableCriteria fetches a list of criteria applied to property version + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getavailablecriteria + GetAvailableCriteria(context.Context, GetFeaturesRequest) (*GetFeaturesCriteriaResponse, error) } papi struct { diff --git a/pkg/papi/propertyversion.go b/pkg/papi/propertyversion.go index 0d1f1def..9c2b7f67 100644 --- a/pkg/papi/propertyversion.go +++ b/pkg/papi/propertyversion.go @@ -78,17 +78,48 @@ type ( PropertyVersion int } + // GetLatestVersionRequest contains path and query params required to fetch latest property version GetLatestVersionRequest struct { PropertyID string ActivatedOn string ContractID string GroupID string } + + // GetFeaturesRequest contains path and query params required to fetch both available behaviors and available criteria for a property + GetFeaturesRequest struct { + PropertyID string + PropertyVersion int + ContractID string + GroupID string + } + + // AvailableFeature represents details of a single feature (behavior or criteria available for selected property version + AvailableFeature struct { + Name string `json:"name"` + SchemaLink string `json:"schemaLink"` + } + + // GetFeaturesCriteriaResponse contains response received when fetching both available behaviors and available criteria for a property + GetFeaturesCriteriaResponse struct { + ContractID string `json:"contractId"` + GroupID string `json:"groupId"` + ProductID string `json:"productId"` + RuleFormat string `json:"ruleFormat"` + AvailableBehaviors AvailableFeatureItems `json:"availableBehaviors"` + } + + // AvailableFeatureItems contains a slice of AvailableFeature items + AvailableFeatureItems struct { + Items []AvailableFeature `json:"items"` + } ) const ( + // VersionProduction const VersionProduction = "PRODUCTION" - VersionStaging = "STAGING" + // VersionStaging const + VersionStaging = "STAGING" ) // Validate validates GetPropertyVersionsRequest @@ -121,6 +152,7 @@ func (v PropertyVersionCreate) Validate() error { }.Filter() } +// Validate validates GetLatestVersionRequest func (v GetLatestVersionRequest) Validate() error { return validation.Errors{ "PropertyID": validation.Validate(v.PropertyID, validation.Required), @@ -128,6 +160,14 @@ func (v GetLatestVersionRequest) Validate() error { }.Filter() } +// Validate validates GetFeaturesRequest +func (v GetFeaturesRequest) Validate() error { + return validation.Errors{ + "PropertyID": validation.Validate(v.PropertyID, validation.Required), + "PropertyVersion": validation.Validate(v.PropertyVersion, validation.Required), + }.Filter() +} + // GetPropertyVersions returns list of property versions for give propertyID, contractID and groupID func (p *papi) GetPropertyVersions(ctx context.Context, params GetPropertyVersionsRequest) (*GetPropertyVersionsResponse, error) { if err := params.Validate(); err != nil { @@ -289,3 +329,79 @@ func (p *papi) CreatePropertyVersion(ctx context.Context, request CreateProperty version.PropertyVersion = versionNumber return &version, nil } + +// GetAvailableBehaviors lists available behaviors for given property version +func (p *papi) GetAvailableBehaviors(ctx context.Context, params GetFeaturesRequest) (*GetFeaturesCriteriaResponse, error) { + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + logger := p.Log(ctx) + logger.Debug("GetAvailableBehaviors") + + getURL := fmt.Sprintf( + "/papi/v1/properties/%s/versions/%d/available-behaviors?contractId=%s&groupId=%s", + params.PropertyID, + params.PropertyVersion, + params.ContractID, + params.GroupID, + ) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, getURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create getavailablebehaviors request: %w", err) + } + + req.Header.Set("PAPI-Use-Prefixes", cast.ToString(p.usePrefixes)) + var versions GetFeaturesCriteriaResponse + resp, err := p.Exec(req, &versions) + if err != nil { + return nil, fmt.Errorf("getavailablebehaviors request failed: %w", err) + } + + if resp.StatusCode == http.StatusNotFound { + return nil, fmt.Errorf("%w: %s", session.ErrNotFound, getURL) + } + if resp.StatusCode != http.StatusOK { + return nil, session.NewAPIError(resp, logger) + } + + return &versions, nil +} + +// GetAvailableCriteria lists available criteria for given property version +func (p *papi) GetAvailableCriteria(ctx context.Context, params GetFeaturesRequest) (*GetFeaturesCriteriaResponse, error) { + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + logger := p.Log(ctx) + logger.Debug("GetAvailableCriteria") + + getURL := fmt.Sprintf( + "/papi/v1/properties/%s/versions/%d/available-criteria?contractId=%s&groupId=%s", + params.PropertyID, + params.PropertyVersion, + params.ContractID, + params.GroupID, + ) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, getURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create getavailablecriteria request: %w", err) + } + + req.Header.Set("PAPI-Use-Prefixes", cast.ToString(p.usePrefixes)) + var versions GetFeaturesCriteriaResponse + resp, err := p.Exec(req, &versions) + if err != nil { + return nil, fmt.Errorf("getavailablecriteria request failed: %w", err) + } + + if resp.StatusCode == http.StatusNotFound { + return nil, fmt.Errorf("%w: %s", session.ErrNotFound, getURL) + } + if resp.StatusCode != http.StatusOK { + return nil, session.NewAPIError(resp, logger) + } + + return &versions, nil +} diff --git a/pkg/papi/propertyversion_test.go b/pkg/papi/propertyversion_test.go index 851aa3c3..782ea6a7 100644 --- a/pkg/papi/propertyversion_test.go +++ b/pkg/papi/propertyversion_test.go @@ -414,6 +414,26 @@ func TestPapi_CreatePropertyVersion(t *testing.T) { responseBody: ` { "versionLink": ":" +}`, + expectedPath: "/papi/v1/properties/propertyID/versions?contractId=contract&groupId=group", + withError: func(t *testing.T, err error) { + want := tools.ErrInvalidLocation + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "invalid version format": { + params: CreatePropertyVersionRequest{ + PropertyID: "propertyID", + ContractID: "contract", + GroupID: "group", + Version: PropertyVersionCreate{ + CreateFromVersion: 1, + }, + }, + responseStatus: http.StatusCreated, + responseBody: ` +{ + "versionLink": "/papi/v1/properties/propertyID/versions/abc?contractId=contract&groupId=group" }`, expectedPath: "/papi/v1/properties/propertyID/versions?contractId=contract&groupId=group", withError: func(t *testing.T, err error) { @@ -506,6 +526,26 @@ func TestPapi_GetLatestVersion(t *testing.T) { }}}, }, }, + "404 Not Found": { + params: GetLatestVersionRequest{ + PropertyID: "propertyID", + ContractID: "contract", + GroupID: "group", + }, + responseStatus: http.StatusNotFound, + responseBody: ` +{ + "type": "not_found", + "title": "Not Found", + "detail": "Could not find latest version", + "status": 404 +}`, + expectedPath: "/papi/v1/properties/propertyID/versions/latest?contractId=contract&groupId=group", + withError: func(t *testing.T, err error) { + want := session.ErrNotFound + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, "500 Internal Server Error": { params: GetLatestVersionRequest{ PropertyID: "propertyID", @@ -579,3 +619,285 @@ func TestPapi_GetLatestVersion(t *testing.T) { }) } } + +func TestPapi_GetAvailableBehaviors(t *testing.T) { + tests := map[string]struct { + params GetFeaturesRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *GetFeaturesCriteriaResponse + withError func(*testing.T, error) + }{ + "200 OK": { + params: GetFeaturesRequest{ + PropertyID: "propertyID", + PropertyVersion: 2, + ContractID: "contract", + GroupID: "group", + }, + responseStatus: http.StatusOK, + expectedPath: "/papi/v1/properties/propertyID/versions/2/available-behaviors?contractId=contract&groupId=group", + responseBody: ` +{ + "contractId": "contract", + "groupId": "group", + "productId": "productID", + "ruleFormat": "v2020-09-15", + "availableBehaviors": { + "items": [ + { + "name": "cpCode", + "schemaLink": "/papi/v1/schemas/products/prd_Alta/latest#/definitions/catalog/behaviors/cpCode" + } + ] + } +}`, + expectedResponse: &GetFeaturesCriteriaResponse{ + ContractID: "contract", + GroupID: "group", + ProductID: "productID", + RuleFormat: "v2020-09-15", + AvailableBehaviors: AvailableFeatureItems{Items: []AvailableFeature{ + { + Name: "cpCode", + SchemaLink: "/papi/v1/schemas/products/prd_Alta/latest#/definitions/catalog/behaviors/cpCode", + }, + }}, + }, + }, + "404 Not Found": { + params: GetFeaturesRequest{ + PropertyID: "propertyID", + PropertyVersion: 2, + ContractID: "contract", + GroupID: "group", + }, + responseStatus: http.StatusNotFound, + responseBody: ` +{ + "type": "not_found", + "title": "Not Found", + "detail": "Could not find available behaviors", + "status": 404 +}`, + expectedPath: "/papi/v1/properties/propertyID/versions/2/available-behaviors?contractId=contract&groupId=group", + withError: func(t *testing.T, err error) { + want := session.ErrNotFound + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "500 Internal Server Error": { + params: GetFeaturesRequest{ + PropertyID: "propertyID", + PropertyVersion: 2, + ContractID: "contract", + GroupID: "group", + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` +{ + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error fetching available behaviors", + "status": 500 +}`, + expectedPath: "/papi/v1/properties/propertyID/versions/2/available-behaviors?contractId=contract&groupId=group", + withError: func(t *testing.T, err error) { + want := session.APIError{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error fetching available behaviors", + StatusCode: http.StatusInternalServerError, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "empty property ID": { + params: GetFeaturesRequest{ + PropertyID: "", + PropertyVersion: 2, + ContractID: "contract", + GroupID: "group", + }, + withError: func(t *testing.T, err error) { + want := ErrStructValidation + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + assert.Contains(t, err.Error(), "PropertyID") + }, + }, + "empty property version": { + params: GetFeaturesRequest{ + PropertyID: "propertyID", + ContractID: "contract", + GroupID: "group", + }, + withError: func(t *testing.T, err error) { + want := ErrStructValidation + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + assert.Contains(t, err.Error(), "PropertyVersion") + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodGet, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + result, err := client.GetAvailableBehaviors(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +func TestPapi_GetAvailableCriteria(t *testing.T) { + tests := map[string]struct { + params GetFeaturesRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse *GetFeaturesCriteriaResponse + withError func(*testing.T, error) + }{ + "200 OK": { + params: GetFeaturesRequest{ + PropertyID: "propertyID", + PropertyVersion: 2, + ContractID: "contract", + GroupID: "group", + }, + responseStatus: http.StatusOK, + expectedPath: "/papi/v1/properties/propertyID/versions/2/available-criteria?contractId=contract&groupId=group", + responseBody: ` +{ + "contractId": "contract", + "groupId": "group", + "productId": "productID", + "ruleFormat": "v2020-09-15", + "availableBehaviors": { + "items": [ + { + "name": "cpCode", + "schemaLink": "/papi/v1/schemas/products/prd_Alta/latest#/definitions/catalog/behaviors/cpCode" + } + ] + } +}`, + expectedResponse: &GetFeaturesCriteriaResponse{ + ContractID: "contract", + GroupID: "group", + ProductID: "productID", + RuleFormat: "v2020-09-15", + AvailableBehaviors: AvailableFeatureItems{Items: []AvailableFeature{ + { + Name: "cpCode", + SchemaLink: "/papi/v1/schemas/products/prd_Alta/latest#/definitions/catalog/behaviors/cpCode", + }, + }}, + }, + }, + "404 Not Found": { + params: GetFeaturesRequest{ + PropertyID: "propertyID", + PropertyVersion: 2, + ContractID: "contract", + GroupID: "group", + }, + responseStatus: http.StatusNotFound, + responseBody: ` +{ + "type": "not_found", + "title": "Not Found", + "detail": "Could not find available criteria", + "status": 404 +}`, + expectedPath: "/papi/v1/properties/propertyID/versions/2/available-criteria?contractId=contract&groupId=group", + withError: func(t *testing.T, err error) { + want := session.ErrNotFound + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "500 Internal Server Error": { + params: GetFeaturesRequest{ + PropertyID: "propertyID", + PropertyVersion: 2, + ContractID: "contract", + GroupID: "group", + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` +{ + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error fetching available behaviors", + "status": 500 +}`, + expectedPath: "/papi/v1/properties/propertyID/versions/2/available-criteria?contractId=contract&groupId=group", + withError: func(t *testing.T, err error) { + want := session.APIError{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error fetching available behaviors", + StatusCode: http.StatusInternalServerError, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "empty property ID": { + params: GetFeaturesRequest{ + PropertyID: "", + PropertyVersion: 2, + ContractID: "contract", + GroupID: "group", + }, + withError: func(t *testing.T, err error) { + want := ErrStructValidation + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + assert.Contains(t, err.Error(), "PropertyID") + }, + }, + "empty property version": { + params: GetFeaturesRequest{ + PropertyID: "propertyID", + ContractID: "contract", + GroupID: "group", + }, + withError: func(t *testing.T, err error) { + want := ErrStructValidation + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + assert.Contains(t, err.Error(), "PropertyVersion") + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodGet, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + result, err := client.GetAvailableCriteria(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} From 1aae209a47852ee010f2aa2cd6ff16da2a0d4ff2 Mon Sep 17 00:00:00 2001 From: "Piotrowski, Piotr" Date: Tue, 15 Sep 2020 12:02:01 +0200 Subject: [PATCH 4/5] [TFP-170] Add interface composition in PAPI --- pkg/papi/activation.go | 16 ++++++++ pkg/papi/contract.go | 8 ++++ pkg/papi/cpcode.go | 16 ++++++++ pkg/papi/group.go | 8 ++++ pkg/papi/papi.go | 73 +++---------------------------------- pkg/papi/propertyversion.go | 28 ++++++++++++++ 6 files changed, 81 insertions(+), 68 deletions(-) diff --git a/pkg/papi/activation.go b/pkg/papi/activation.go index e5aabc0c..9f1fb600 100644 --- a/pkg/papi/activation.go +++ b/pkg/papi/activation.go @@ -11,6 +11,22 @@ import ( ) type ( + // Activations contains operations available on Activation resource + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#propertyactivationsgroup + Activations interface { + // CreateActivation creates a new activation or deactivation request + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#postpropertyactivations + CreateActivation(context.Context, CreateActivationRequest) (*CreateActivationResponse, error) + + // GetActivation gets details about an activation + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getpropertyactivation + GetActivation(context.Context, GetActivationRequest) (*GetActivationResponse, error) + + // CancelActivation allows for canceling an activation while it is still PENDING + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#deletepropertyactivation + CancelActivation(context.Context, CancelActivationRequest) (*CancelActivationResponse, error) + } + // ActivationFallbackInfo encapsulates information about fast fallback, which may allow you to fallback to a previous activation when // POSTing an activation with useFastFallback enabled. ActivationFallbackInfo struct { diff --git a/pkg/papi/contract.go b/pkg/papi/contract.go index 15d3ca5d..591931da 100644 --- a/pkg/papi/contract.go +++ b/pkg/papi/contract.go @@ -10,6 +10,14 @@ import ( ) type ( + // Contracts contains operations available on Contract resource + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#contractsgroup + Contracts interface { + // GetContract provides a read-only list of contract names and identifiers + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getcontracts + GetContracts(context.Context) (*GetContractsResponse, error) + } + // Contract represents a property contract resource Contract struct { ContractID string `json:"contractId"` diff --git a/pkg/papi/cpcode.go b/pkg/papi/cpcode.go index 3d4096c0..c25d04f3 100644 --- a/pkg/papi/cpcode.go +++ b/pkg/papi/cpcode.go @@ -11,6 +11,22 @@ import ( ) type ( + // CPCodes contains operations available on CPCode resource + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#cpcodesgroup + CPCodes interface { + // GetCPCodes lists all available CP codes + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getcpcodes + GetCPCodes(context.Context, GetCPCodesRequest) (*GetCPCodesResponse, error) + + // GetCPCode gets CP code with provided ID + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getcpcode + GetCPCode(context.Context, GetCPCodeRequest) (*GetCPCodesResponse, error) + + // CreateCPCode creates a new CP code + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#postcpcodes + CreateCPCode(context.Context, CreateCPCodeRequest) (*CreateCPCodeResponse, error) + } + // CPCode contains CP code resource data CPCode struct { ID string `json:"cpcodeId"` diff --git a/pkg/papi/group.go b/pkg/papi/group.go index 1a3cd606..cbcf0085 100644 --- a/pkg/papi/group.go +++ b/pkg/papi/group.go @@ -10,6 +10,14 @@ import ( ) type ( + // Groups contains operations available on Group resource + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#groupsgroup + Groups interface { + // GetGroups provides a read-only list of groups, which may contain properties. + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getgroups + GetGroups(context.Context) (*GetGroupsResponse, error) + } + // Group represents a property group resource Group struct { GroupID string `json:"groupId"` diff --git a/pkg/papi/papi.go b/pkg/papi/papi.go index ea36b323..00d47d9f 100644 --- a/pkg/papi/papi.go +++ b/pkg/papi/papi.go @@ -2,7 +2,6 @@ package papi import ( - "context" "errors" "github.com/akamai/AkamaiOPEN-edgegrid-golang/v2/pkg/session" @@ -16,73 +15,11 @@ var ( type ( // PAPI is the papi api interface PAPI interface { - // GetGroups provides a read-only list of groups, which may contain properties. - // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getgroups - GetGroups(context.Context) (*GetGroupsResponse, error) - - // GetContract provides a read-only list of contract names and identifiers - // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getcontracts - GetContracts(context.Context) (*GetContractsResponse, error) - - // CreateActivation creates a new activation or deactivation request - // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#postpropertyactivations - CreateActivation(context.Context, CreateActivationRequest) (*CreateActivationResponse, error) - - // GetActivation gets details about an activation - // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getpropertyactivation - GetActivation(context.Context, GetActivationRequest) (*GetActivationResponse, error) - - // CancelActivation allows for canceling an activation while it is still PENDING - // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#deletepropertyactivation - CancelActivation(context.Context, CancelActivationRequest) (*CancelActivationResponse, error) - - // GetCPCodes lists all available CP codes - // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getcpcodes - GetCPCodes(context.Context, GetCPCodesRequest) (*GetCPCodesResponse, error) - - // GetCPCode gets CP code with provided ID - // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getcpcode - GetCPCode(context.Context, GetCPCodeRequest) (*GetCPCodesResponse, error) - - // CreateCPCode creates a new CP code - // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#postcpcodes - CreateCPCode(context.Context, CreateCPCodeRequest) (*CreateCPCodeResponse, error) - - // GetEdgeHostnames fetches a list of edge hostnames - // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getedgehostnames - GetEdgeHostnames(context.Context, GetEdgeHostnamesRequest) (*GetEdgeHostnamesResponse, error) - - // GetEdgeHostname fetches edge hostname with given ID - // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getedgehostname - GetEdgeHostname(context.Context, GetEdgeHostnameRequest) (*GetEdgeHostnamesResponse, error) - - // CreateEdgeHostname creates a new edge hostname - // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#postedgehostnames - CreateEdgeHostname(context.Context, CreateEdgeHostnameRequest) (*CreateEdgeHostnameResponse, error) - - // GetPropertyVersions fetches available property versions - // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getpropertyversions - GetPropertyVersions(context.Context, GetPropertyVersionsRequest) (*GetPropertyVersionsResponse, error) - - // GetPropertyVersion fetches specific property version - // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getpropertyversion - GetPropertyVersion(context.Context, GetPropertyVersionRequest) (*GetPropertyVersionsResponse, error) - - // CreatePropertyVersion creates a new property version - // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#postpropertyversions - CreatePropertyVersion(context.Context, CreatePropertyVersionRequest) (*CreatePropertyVersionResponse, error) - - // GetLatestVersion fetches latest property version - // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getlatestversion - GetLatestVersion(context.Context, GetLatestVersionRequest) (*GetPropertyVersionsResponse, error) - - // GetAvailableBehaviors fetches a list of behaviors applied to property version - // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getavailablebehaviors - GetAvailableBehaviors(context.Context, GetFeaturesRequest) (*GetFeaturesCriteriaResponse, error) - - // GetAvailableCriteria fetches a list of criteria applied to property version - // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getavailablecriteria - GetAvailableCriteria(context.Context, GetFeaturesRequest) (*GetFeaturesCriteriaResponse, error) + Groups + Contracts + Activations + CPCodes + PropertyVersions } papi struct { diff --git a/pkg/papi/propertyversion.go b/pkg/papi/propertyversion.go index 9c2b7f67..67f35ffa 100644 --- a/pkg/papi/propertyversion.go +++ b/pkg/papi/propertyversion.go @@ -12,6 +12,34 @@ import ( ) type ( + // PropertyVersions contains operations available on PropertyVersions resource + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#propertyversionsgroup + PropertyVersions interface { + // GetPropertyVersions fetches available property versions + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getpropertyversions + GetPropertyVersions(context.Context, GetPropertyVersionsRequest) (*GetPropertyVersionsResponse, error) + + // GetPropertyVersion fetches specific property version + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getpropertyversion + GetPropertyVersion(context.Context, GetPropertyVersionRequest) (*GetPropertyVersionsResponse, error) + + // CreatePropertyVersion creates a new property version + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#postpropertyversions + CreatePropertyVersion(context.Context, CreatePropertyVersionRequest) (*CreatePropertyVersionResponse, error) + + // GetLatestVersion fetches latest property version + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getlatestversion + GetLatestVersion(context.Context, GetLatestVersionRequest) (*GetPropertyVersionsResponse, error) + + // GetAvailableBehaviors fetches a list of behaviors applied to property version + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getavailablebehaviors + GetAvailableBehaviors(context.Context, GetFeaturesRequest) (*GetFeaturesCriteriaResponse, error) + + // GetAvailableCriteria fetches a list of criteria applied to property version + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getavailablecriteria + GetAvailableCriteria(context.Context, GetFeaturesRequest) (*GetFeaturesCriteriaResponse, error) + } + // GetPropertyVersionsRequest contains path and query params used for listing property versions GetPropertyVersionsRequest struct { PropertyID string From 527ad758763074aad208d1ad0ed1b8c62b14f0c1 Mon Sep 17 00:00:00 2001 From: "Piotrowski, Piotr" Date: Tue, 15 Sep 2020 19:12:53 +0200 Subject: [PATCH 5/5] [TFP-170] Add interface composition in PAPI # Conflicts: # pkg/papi/papi.go --- pkg/papi/edgehostname.go | 16 ++++++++++++++++ pkg/papi/papi.go | 1 + 2 files changed, 17 insertions(+) diff --git a/pkg/papi/edgehostname.go b/pkg/papi/edgehostname.go index 83928ed4..4a1fe1a3 100644 --- a/pkg/papi/edgehostname.go +++ b/pkg/papi/edgehostname.go @@ -12,6 +12,22 @@ import ( ) type ( + // EdgeHostnames contains operations available on EdgeHostnames resource + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#edgehostnamesgroup + EdgeHostnames interface { + // GetEdgeHostnames fetches a list of edge hostnames + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getedgehostnames + GetEdgeHostnames(context.Context, GetEdgeHostnamesRequest) (*GetEdgeHostnamesResponse, error) + + // GetEdgeHostname fetches edge hostname with given ID + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#getedgehostname + GetEdgeHostname(context.Context, GetEdgeHostnameRequest) (*GetEdgeHostnamesResponse, error) + + // CreateEdgeHostname creates a new edge hostname + // See: https://developer.akamai.com/api/core_features/property_manager/v1.html#postedgehostnames + CreateEdgeHostname(context.Context, CreateEdgeHostnameRequest) (*CreateEdgeHostnameResponse, error) + } + // GetEdgeHostnamesRequest contains query params used for listing edge hostnames GetEdgeHostnamesRequest struct { ContractID string diff --git a/pkg/papi/papi.go b/pkg/papi/papi.go index 00d47d9f..4670ee0f 100644 --- a/pkg/papi/papi.go +++ b/pkg/papi/papi.go @@ -20,6 +20,7 @@ type ( Activations CPCodes PropertyVersions + EdgeHostnames } papi struct {