Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ jobs:
OCTOPUS_SERVER_BASE64_LICENSE: ${{ secrets.OCTOPUS_SERVER_BASE64_LICENSE }}
OCTOPUS__FeatureToggles__EphemeralEnvironmentsManualDeploymentsFeatureToggle: 'true'
OCTOPUS__FeatureToggles__EphemeralEnvironmentsFeatureToggle: 'true'
OCTOPUS__FeatureToggles__RateLimitingV2FeatureToggle: 'true'
ports:
- 8080:8080
# https://github.com/dorny/test-reporter/issues/168
Expand Down
40 changes: 40 additions & 0 deletions pkg/ratelimitingpolicies/rate_limiting_policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package ratelimitingpolicies

type RateLimitingPolicy struct {
ID string `json:"Id"`
IsBuiltIn bool `json:"IsBuiltIn"`
Name string `json:"Name"`
IsEnabled bool `json:"IsEnabled"`
ScopeType RateLimitingPolicyScopeType `json:"ScopeType"`
RequestsPerHour int `json:"RequestsPerHour"`
BurstLimit int `json:"BurstLimit"`
AuditMode bool `json:"AuditMode"`
}

type GetRateLimitingPolicyByIdRequest struct {
ID string `uri:"id"`
}

type ListRateLimitingPoliciesRequest struct {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not 100% sure I'm doing this the right way, as while there are some examples (e.g. live status) that are similar with requests and responses, there seem to be numerous different patterns at play in this repo. This seems the most strongly aligned with our target architecture though, and aligns with the C# client too.

Skip int `uri:"skip,omitempty"`
Take int `uri:"take,omitempty"`
}

type ListRateLimitingPoliciesResponse struct {
ItemType string `json:"ItemType"`
TotalResults int `json:"TotalResults"`
ItemsPerPage int `json:"ItemsPerPage"`
Items []RateLimitingPolicy `json:"Items"`
NumberOfPages int `json:"NumberOfPages"`
LastPageNumber int `json:"LastPageNumber"`
}

type ModifyRateLimitingPolicyCommand struct {
ID string `uri:"id" json:"-"`
Name string `json:"Name"`
IsEnabled bool `json:"IsEnabled"`
ScopeType RateLimitingPolicyScopeType `json:"ScopeType"`
RequestsPerHour int `json:"RequestsPerHour"`
BurstLimit int `json:"BurstLimit"`
AuditMode bool `json:"AuditMode"`
}
9 changes: 9 additions & 0 deletions pkg/ratelimitingpolicies/rate_limiting_policy_scope_type.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ratelimitingpolicies

type RateLimitingPolicyScopeType int

const (
Unauthenticated RateLimitingPolicyScopeType = iota

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checking, this serializes across the JSON API as a string, doesn't it?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep! TestRateLimitingPolicyScopeTypeJsonMarshal goes back and forth to confirm

AuthenticatedHuman
AuthenticatedAgent
)
100 changes: 100 additions & 0 deletions pkg/ratelimitingpolicies/rate_limiting_policy_scope_type_string.go

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL about this enumer code generator

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 61 additions & 0 deletions pkg/ratelimitingpolicies/rate_limiting_policy_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package ratelimitingpolicies

import (
"github.com/OctopusDeploy/go-octopusdeploy/v2/internal"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/newclient"
)

const rateLimitingPoliciesTemplate = "/api/ratelimitingpolicies{/id}{?skip,take}"

// GetByID returns the rate limiting policy that matches the given ID.
func GetByID(client newclient.Client, request GetRateLimitingPolicyByIdRequest) (*RateLimitingPolicy, error) {
if request.ID == "" {
return nil, internal.CreateRequiredParameterIsEmptyError("ID")
}

path, pathError := client.URITemplateCache().Expand(rateLimitingPoliciesTemplate, request)
if pathError != nil {
return nil, pathError
}

result, resultError := newclient.Get[RateLimitingPolicy](client.HttpSession(), path)
if resultError != nil {
return nil, resultError
}

return result, nil
}

// List returns a paginated collection of rate limiting policies.
func List(client newclient.Client, request ListRateLimitingPoliciesRequest) (*ListRateLimitingPoliciesResponse, error) {
path, pathError := client.URITemplateCache().Expand(rateLimitingPoliciesTemplate, request)
if pathError != nil {
return nil, pathError
}

result, resultError := newclient.Get[ListRateLimitingPoliciesResponse](client.HttpSession(), path)
if resultError != nil {
return nil, resultError
}

return result, nil
}

// Modify changes the rate limiting policy that matches the given ID.
func Modify(client newclient.Client, command ModifyRateLimitingPolicyCommand) (*RateLimitingPolicy, error) {
if command.ID == "" {
return nil, internal.CreateRequiredParameterIsEmptyError("ID")
}

path, pathError := client.URITemplateCache().Expand(rateLimitingPoliciesTemplate, command)
if pathError != nil {
return nil, pathError
}

result, resultError := newclient.Put[RateLimitingPolicy](client.HttpSession(), path, command)
if resultError != nil {
return nil, resultError
}

return result, nil
}
128 changes: 128 additions & 0 deletions pkg/ratelimitingpolicies/rate_limiting_policy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package ratelimitingpolicies

import (
"encoding/json"
"testing"

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

func TestRateLimitingPolicyScopeTypeJsonMarshal(t *testing.T) {
cases := map[RateLimitingPolicyScopeType]string{
Unauthenticated: `"Unauthenticated"`,
AuthenticatedHuman: `"AuthenticatedHuman"`,
AuthenticatedAgent: `"AuthenticatedAgent"`,
}
for scope, expected := range cases {
jsonValue, err := json.Marshal(scope)
require.NoError(t, err)
require.JSONEq(t, expected, string(jsonValue))

var enumValue RateLimitingPolicyScopeType
require.NoError(t, json.Unmarshal(jsonValue, &enumValue))
require.Equal(t, scope, enumValue)
}
}

func TestRateLimitingPolicyScopeTypeJsonUnmarshalInvalid(t *testing.T) {
var scope RateLimitingPolicyScopeType
require.Error(t, json.Unmarshal([]byte(`"NotAScope"`), &scope))
}

func TestRateLimitingPolicyMarshalRoundTrip(t *testing.T) {
policy := RateLimitingPolicy{
ID: "RateLimitingPolicies-2",
IsBuiltIn: true,
Name: "Authenticated requests",
IsEnabled: true,
ScopeType: AuthenticatedHuman,
RequestsPerHour: 10_000,
BurstLimit: 5_000,
AuditMode: true,
}

data, err := json.Marshal(policy)
require.NoError(t, err)

expected := `{
"Id": "RateLimitingPolicies-2",
"IsBuiltIn": true,
"Name": "Authenticated requests",
"IsEnabled": true,
"ScopeType": "AuthenticatedHuman",
"RequestsPerHour": 10000,
"BurstLimit": 5000,
"AuditMode": true
}`
require.JSONEq(t, expected, string(data))

var received RateLimitingPolicy
require.NoError(t, json.Unmarshal(data, &received))
require.Equal(t, policy, received)
}

func TestModifyRateLimitingPolicyCommandMarshal(t *testing.T) {
command := ModifyRateLimitingPolicyCommand{
ID: "RateLimitingPolicies-1",
Name: "Changed",
IsEnabled: true,
ScopeType: Unauthenticated,
RequestsPerHour: 123,
BurstLimit: 456,
AuditMode: true,
}

data, err := json.Marshal(command)
require.NoError(t, err)
require.JSONEq(t, `{
"Name": "Changed",
"IsEnabled": true,
"ScopeType": "Unauthenticated",
"RequestsPerHour": 123,
"BurstLimit": 456,
"AuditMode": true
}`, string(data))
}

func TestListRateLimitingPoliciesResponseUnmarshal(t *testing.T) {
payload := `{
"ItemType": "RateLimitingPolicy",
"TotalResults": 2,
"ItemsPerPage": 30,
"NumberOfPages": 1,
"LastPageNumber": 0,
"Items": [
{
"Id": "RateLimitingPolicies-1",
"Name": "Human",
"IsBuiltIn": true,
"ScopeType": "AuthenticatedHuman",
"IsEnabled": true,
"RequestsPerHour": 1000,
"BurstLimit": 50,
"AuditMode": true
},
{
"Id": "RateLimitingPolicies-2",
"Name": "Agent",
"IsBuiltIn": true,
"ScopeType": "AuthenticatedAgent",
"IsEnabled": false,
"RequestsPerHour": 500,
"BurstLimit": 25,
"AuditMode": false
}
]
}`

var response ListRateLimitingPoliciesResponse
require.NoError(t, json.Unmarshal([]byte(payload), &response))

require.Equal(t, 2, response.TotalResults)
require.Len(t, response.Items, 2)
require.Equal(t, AuthenticatedHuman, response.Items[0].ScopeType)
require.Equal(t, AuthenticatedAgent, response.Items[1].ScopeType)
require.False(t, response.Items[1].IsEnabled)
require.True(t, response.Items[0].AuditMode)
require.False(t, response.Items[1].AuditMode)
}
Loading
Loading