diff --git a/pkg/platformhubpolicies/policy.go b/pkg/platformhubpolicies/policy.go new file mode 100644 index 00000000..5efa2ea3 --- /dev/null +++ b/pkg/platformhubpolicies/policy.go @@ -0,0 +1,151 @@ +package platformhubpolicies + +import ( + "sync" + "time" + + "github.com/go-playground/validator/v10" + "github.com/go-playground/validator/v10/non-standard/validators" +) + +// PolicyDraft represents a set of information to create new Hub policy. +type PolicyDraft struct { + GitRef string + Slug string + Name string + Description string + ScopeRego string + ConditionsRego string + ViolationAction string + ViolationReason string +} + +// Policy represents a set of information to create new Hub policy. +type Policy interface { + GetName() string + SetName(string) + + GetDescription() string + SetDescription(string) + + GetScopeRego() string + SetScopeRego(string) + + GetConditionsRego() string + SetConditionsRego(string) + + GetViolationAction() string + SetViolationAction(string) + + GetViolationReason() string + SetViolationReason(string) + + Validate() error + + PolicyKey +} + +type PolicyKey interface { + GetGitRef() string + GetSlug() string +} + +// PublishedPolicy represents a read-only view of a published Platform Hub policy version. +type PublishedPolicy interface { + GetID() string + GetPublishedDate() time.Time + GetGitRef() string + GetGitCommit() string + GetName() string + GetDescription() string + GetViolationReason() string + GetViolationAction() string + GetScopeRego() string + GetConditionsRego() string + IsActivated() bool + + PublishedPolicyKey +} + +type PublishedPolicyKey interface { + GetSlug() string + GetVersion() string +} + +type persistedPolicy struct { + GitRef string `json:"GitRef" validate:"required,notblank"` + Slug string `json:"Slug" validate:"required,notblank"` + Name string `json:"Name" validate:"required,notblank"` + Description string `json:"Description,omitempty"` + ScopeRego string `json:"ScopeRego" validate:"required,notblank"` + ConditionsRego string `json:"ConditionsRego" validate:"required,notblank"` + ViolationAction string `json:"ViolationAction" validate:"required,notblank"` + ViolationReason string `json:"ViolationReason,omitempty"` +} + +func (p *persistedPolicy) GetName() string { return p.Name } +func (p *persistedPolicy) SetName(name string) { p.Name = name } +func (p *persistedPolicy) GetGitRef() string { return p.GitRef } +func (p *persistedPolicy) GetSlug() string { return p.Slug } +func (p *persistedPolicy) GetDescription() string { return p.Description } +func (p *persistedPolicy) SetDescription(d string) { p.Description = d } +func (p *persistedPolicy) GetScopeRego() string { return p.ScopeRego } +func (p *persistedPolicy) SetScopeRego(s string) { p.ScopeRego = s } +func (p *persistedPolicy) GetConditionsRego() string { return p.ConditionsRego } +func (p *persistedPolicy) SetConditionsRego(c string) { p.ConditionsRego = c } +func (p *persistedPolicy) GetViolationReason() string { return p.ViolationReason } +func (p *persistedPolicy) SetViolationReason(r string) { p.ViolationReason = r } +func (p *persistedPolicy) GetViolationAction() string { return p.ViolationAction } +func (p *persistedPolicy) SetViolationAction(a string) { p.ViolationAction = a } + +// Validate checks the state of the policy and returns an error if invalid. +func (p *persistedPolicy) Validate() error { + validate, err := getValidator() + if err != nil { + return err + } + + return validate.Struct(p) +} + +var getValidator = sync.OnceValues(buildValidator) + +func buildValidator() (*validator.Validate, error) { + v := validator.New() + err := v.RegisterValidation("notblank", validators.NotBlank) + if err != nil { + return nil, err + } + + return v, nil +} + +type publishedPolicyVersion struct { + ID string `json:"Id"` + Slug string `json:"Slug"` + Version string `json:"Version"` + PublishedDate time.Time `json:"PublishedDate"` + GitRef string `json:"GitRef"` + GitCommit string `json:"GitCommit"` + Name string `json:"Name"` + Description string `json:"Description,omitempty"` + ViolationReason string `json:"ViolationReason,omitempty"` + ViolationAction string `json:"ViolationAction"` + RegoScope string `json:"RegoScope"` + RegoConditions string `json:"RegoConditions"` + IsActive bool `json:"IsActive"` +} + +func (v *publishedPolicyVersion) GetID() string { return v.ID } +func (v *publishedPolicyVersion) GetSlug() string { return v.Slug } +func (v *publishedPolicyVersion) GetVersion() string { return v.Version } +func (v *publishedPolicyVersion) GetPublishedDate() time.Time { return v.PublishedDate } +func (v *publishedPolicyVersion) GetGitRef() string { return v.GitRef } +func (v *publishedPolicyVersion) GetGitCommit() string { return v.GitCommit } +func (v *publishedPolicyVersion) GetName() string { return v.Name } +func (v *publishedPolicyVersion) GetDescription() string { return v.Description } +func (v *publishedPolicyVersion) GetViolationReason() string { return v.ViolationReason } +func (v *publishedPolicyVersion) GetViolationAction() string { return v.ViolationAction } +func (v *publishedPolicyVersion) GetScopeRego() string { return v.RegoScope } +func (v *publishedPolicyVersion) GetConditionsRego() string { return v.RegoConditions } +func (v *publishedPolicyVersion) IsActivated() bool { return v.IsActive } diff --git a/pkg/platformhubpolicies/policy_test.go b/pkg/platformhubpolicies/policy_test.go new file mode 100644 index 00000000..4fdc226a --- /dev/null +++ b/pkg/platformhubpolicies/policy_test.go @@ -0,0 +1,205 @@ +package platformhubpolicies + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPolicyValidate_Name(t *testing.T) { + policy := newPolicyBuilder().Build() + + // Valid + policy.SetName("Valid Name") + require.NoError(t, policy.Validate()) + + // Invalid + policy.SetName("") + require.ErrorContains(t, policy.Validate(), "Name") + + policy.SetName(" ") + require.ErrorContains(t, policy.Validate(), "Name") +} + +func TestPolicyValidate_GitRef(t *testing.T) { + policy := newPolicyBuilder().WithGitRef("main").Build() + + // Valid + require.NoError(t, policy.Validate()) + + // Invalid + policy = newPolicyBuilder().WithGitRef("").Build() + require.ErrorContains(t, policy.Validate(), "GitRef") + + policy = newPolicyBuilder().WithGitRef(" ").Build() + require.ErrorContains(t, policy.Validate(), "GitRef") +} + +func TestPolicyValidate_Slug(t *testing.T) { + policy := newPolicyBuilder().WithSlug("valid_slug").Build() + + // Valid + require.NoError(t, policy.Validate()) + + // Invalid + policy = newPolicyBuilder().WithSlug("").Build() + require.ErrorContains(t, policy.Validate(), "Slug") + + policy = newPolicyBuilder().WithSlug(" ").Build() + require.ErrorContains(t, policy.Validate(), "Slug") +} + +func TestPolicyValidate_Description(t *testing.T) { + policy := newPolicyBuilder().Build() + + // Description is optional + policy.SetDescription("Description") + require.NoError(t, policy.Validate()) + + policy.SetDescription("") + require.NoError(t, policy.Validate()) +} + +func TestPolicyValidate_ScopeRego(t *testing.T) { + policy := newPolicyBuilder().Build() + + // Valid + policy.SetScopeRego("package scope") + require.NoError(t, policy.Validate()) + + // Invalid + policy.SetScopeRego("") + require.ErrorContains(t, policy.Validate(), "ScopeRego") + + policy.SetScopeRego(" ") + require.ErrorContains(t, policy.Validate(), "ScopeRego") +} + +func TestPolicyValidate_ConditionsRego(t *testing.T) { + policy := newPolicyBuilder().Build() + + // Valid + policy.SetConditionsRego("package conditions") + require.NoError(t, policy.Validate()) + + // Invalid + policy.SetConditionsRego("") + require.ErrorContains(t, policy.Validate(), "ConditionsRego") + + policy.SetConditionsRego(" ") + require.ErrorContains(t, policy.Validate(), "ConditionsRego") +} + +func TestPolicyValidate_ViolationAction(t *testing.T) { + policy := newPolicyBuilder().Build() + + // Valid + policy.SetViolationAction("block") + require.NoError(t, policy.Validate()) + + // Invalid + policy.SetViolationAction("") + require.ErrorContains(t, policy.Validate(), "ViolationAction") + + policy.SetViolationAction(" ") + require.ErrorContains(t, policy.Validate(), "ViolationAction") +} + +func TestPolicyValidate_ViolationReason(t *testing.T) { + policy := newPolicyBuilder().Build() + + // ViolationReason is optional + policy.SetViolationReason("Some reason") + require.NoError(t, policy.Validate()) + + policy.SetViolationReason("") + require.NoError(t, policy.Validate()) +} + +type policyBuilder struct { + name string + gitRef string + slug string + description string + scopeRego string + conditionsRego string + violationReason string + violationAction string +} + +func newPolicyBuilder() *policyBuilder { + return &policyBuilder{ + name: "Dummy", + gitRef: "main", + slug: "dummy", + scopeRego: "package dummy", + conditionsRego: "package dummy", + violationAction: "block", + } +} + +func (b *policyBuilder) WithName(name string) *policyBuilder { + b.name = name + return b +} + +func (b *policyBuilder) WithGitRef(gitRef string) *policyBuilder { + b.gitRef = gitRef + return b +} + +func (b *policyBuilder) WithSlug(slug string) *policyBuilder { + b.slug = slug + return b +} + +func (b *policyBuilder) WithDescription(description string) *policyBuilder { + b.description = description + return b +} + +func (b *policyBuilder) WithScopeRego(scopeRego string) *policyBuilder { + b.scopeRego = scopeRego + return b +} + +func (b *policyBuilder) WithConditionsRego(conditionsRego string) *policyBuilder { + b.conditionsRego = conditionsRego + return b +} + +func (b *policyBuilder) WithViolationReason(violationReason string) *policyBuilder { + b.violationReason = violationReason + return b +} + +func (b *policyBuilder) WithViolationAction(violationAction string) *policyBuilder { + b.violationAction = violationAction + return b +} + +func (b *policyBuilder) Build() Policy { + return &persistedPolicy{ + Name: b.name, + GitRef: b.gitRef, + Slug: b.slug, + ScopeRego: b.scopeRego, + ConditionsRego: b.conditionsRego, + ViolationAction: b.violationAction, + Description: b.description, + ViolationReason: b.violationReason, + } +} + +func (b *policyBuilder) BuildDraft() *PolicyDraft { + return &PolicyDraft{ + Name: b.name, + GitRef: b.gitRef, + Slug: b.slug, + ScopeRego: b.scopeRego, + ConditionsRego: b.conditionsRego, + ViolationAction: b.violationAction, + Description: b.description, + ViolationReason: b.violationReason, + } +} diff --git a/pkg/platformhubpolicies/service.go b/pkg/platformhubpolicies/service.go new file mode 100644 index 00000000..d1b5db0d --- /dev/null +++ b/pkg/platformhubpolicies/service.go @@ -0,0 +1,290 @@ +package platformhubpolicies + +import ( + "encoding/json" + + "github.com/OctopusDeploy/go-octopusdeploy/v2/internal" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/newclient" +) + +const template = "/api/platformhub/{gitRef}/policies{/slug}{?skip,take,partialName}" + +// Add creates and stores new policy in Platform Hub. +func Add(client newclient.Client, policy PolicyDraft, commitMessage string) (Policy, error) { + command, path, commandError := buildAddCommand(client, policy, commitMessage) + if commandError != nil { + return nil, commandError + } + + createdPolicy, addError := newclient.Post[persistedPolicy](client.HttpSession(), path, command) + if addError != nil { + return nil, addError + } + + return createdPolicy, nil +} + +// List returns a paginated collection of Platform Hub policies based on the provided query. +func List(client newclient.Client, query PoliciesQuery) (*PoliciesQueryResult, error) { + path, pathError := client.URITemplateCache().Expand(template, query) + if pathError != nil { + return nil, pathError + } + + result, resultError := newclient.Get[PoliciesQueryResult](client.HttpSession(), path) + if resultError != nil { + return nil, resultError + } + + return result, nil +} + +// GetBySlug returns the Platform Hub policy that matches given policy key. +func GetBySlug(client newclient.Client, gitRef string, slug string) (Policy, error) { + path, pathError := client.URITemplateCache().Expand(template, map[string]any{"gitRef": gitRef, "slug": slug}) + if pathError != nil { + return nil, pathError + } + + policy, err := newclient.Get[persistedPolicy](client.HttpSession(), path) + if err != nil { + return nil, err + } + + return policy, nil +} + +// Update stores modified policy in Platform Hub. +func Update(client newclient.Client, policy Policy, commitMessage string) (Policy, error) { + command, path, commandError := buildUpdateCommand(client, policy, commitMessage) + if commandError != nil { + return nil, commandError + } + + updatedPolicy, updateError := newclient.Put[persistedPolicy](client.HttpSession(), path, command) + if updateError != nil { + return nil, updateError + } + + return updatedPolicy, nil +} + +// Publish publishes a Platform Hub policy version. +func Publish(client newclient.Client, policy PolicyKey, version string) (PublishedPolicy, error) { + command, path, err := buildPublishCommand(client, policy, version) + if err != nil { + return nil, err + } + + publishedVersion, postErr := newclient.Post[publishedPolicyVersion](client.HttpSession(), path, command) + if postErr != nil { + return nil, postErr + } + + return publishedVersion, nil +} + +// ListVersions returns published versions of a Platform Hub policy. +func ListVersions(client newclient.Client, query PublishedPoliciesQuery) (*PublishedPoliciesQueryResult, error) { + path, pathError := buildGetVersionsPath(client, query) + if pathError != nil { + return nil, pathError + } + + result, err := newclient.Get[PublishedPoliciesQueryResult](client.HttpSession(), path) + if err != nil { + return nil, err + } + + return result, nil +} + +// ActivateVersion activates a published Platform Hub policy version. +func ActivateVersion(client newclient.Client, policy PublishedPolicyKey) (PublishedPolicy, error) { + return modifyVersionStatus(client, policy, true) +} + +// DeactivateVersion deactivates a published Platform Hub policy version. +func DeactivateVersion(client newclient.Client, policy PublishedPolicyKey) (PublishedPolicy, error) { + return modifyVersionStatus(client, policy, false) +} + +// PoliciesQuery represents query parameters for listing policies. +type PoliciesQuery struct { + GitRef string `uri:"gitRef" json:"gitRef"` + PartialName string `uri:"partialName,omitempty" json:"partialName,omitempty"` + Skip int `uri:"skip,omitempty" json:"skip,omitempty"` + Take int `uri:"take,omitempty" json:"take,omitempty"` +} + +// PoliciesQueryResult paginated collection of policies +type PoliciesQueryResult struct { + Policies []Policy + ItemsPerPage int + FilteredItemsCount int + TotalItemsCount int +} + +func (r *PoliciesQueryResult) UnmarshalJSON(data []byte) error { + var raw struct { + Policies []persistedPolicy `json:"Policies"` + ItemsPerPage int `json:"ItemsPerPage"` + FilteredItemsCount int `json:"FilteredItemsCount"` + TotalItemsCount int `json:"TotalItemsCount"` + } + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + r.ItemsPerPage = raw.ItemsPerPage + r.FilteredItemsCount = raw.FilteredItemsCount + r.TotalItemsCount = raw.TotalItemsCount + r.Policies = make([]Policy, len(raw.Policies)) + + for i := range raw.Policies { + r.Policies[i] = &raw.Policies[i] + } + + return nil +} + +// PublishedPoliciesQuery query parameters for listing published policy versions. +type PublishedPoliciesQuery struct { + Slug string `uri:"slug"` + Skip int `uri:"skip,omitempty"` + Take int `uri:"take,omitempty"` +} + +// PublishedPoliciesQueryResult paginated collection of published policy versions +type PublishedPoliciesQueryResult struct { + Items []PublishedPolicy + ItemsPerPage int + TotalResults int +} + +func (r *PublishedPoliciesQueryResult) UnmarshalJSON(data []byte) error { + var raw struct { + Items []publishedPolicyVersion `json:"Items"` + ItemsPerPage int `json:"ItemsPerPage"` + TotalResults int `json:"TotalResults"` + } + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + r.ItemsPerPage = raw.ItemsPerPage + r.TotalResults = raw.TotalResults + r.Items = make([]PublishedPolicy, len(raw.Items)) + + for i := range raw.Items { + r.Items[i] = &raw.Items[i] + } + + return nil +} + +func buildAddCommand(client newclient.Client, candidate PolicyDraft, commitMessage string) (platformHubPolicyUpsertCommand, string, error) { + policy := persistedPolicy{ + GitRef: candidate.GitRef, + Slug: candidate.Slug, + Name: candidate.Name, + Description: candidate.Description, + ScopeRego: candidate.ScopeRego, + ConditionsRego: candidate.ConditionsRego, + ViolationAction: candidate.ViolationAction, + ViolationReason: candidate.ViolationReason, + } + + if validationError := policy.Validate(); validationError != nil { + return platformHubPolicyUpsertCommand{}, "", internal.CreateValidationFailureError("Add", validationError) + } + + path, pathError := client.URITemplateCache().Expand(template, map[string]any{"gitRef": policy.GitRef}) + if pathError != nil { + return platformHubPolicyUpsertCommand{}, "", pathError + } + + command := platformHubPolicyUpsertCommand{ + ChangeDescription: commitMessage, + persistedPolicy: policy, + } + return command, path, nil +} + +func buildUpdateCommand(client newclient.Client, policy Policy, commitMessage string) (platformHubPolicyUpsertCommand, string, error) { + if validationError := policy.Validate(); validationError != nil { + return platformHubPolicyUpsertCommand{}, "", internal.CreateValidationFailureError("Update", validationError) + } + + path, pathError := client.URITemplateCache().Expand(template, map[string]any{"gitRef": policy.GetGitRef(), "slug": policy.GetSlug()}) + if pathError != nil { + return platformHubPolicyUpsertCommand{}, "", pathError + } + + command := platformHubPolicyUpsertCommand{ + ChangeDescription: commitMessage, + persistedPolicy: persistedPolicy{ + GitRef: policy.GetGitRef(), + Slug: policy.GetSlug(), + Name: policy.GetName(), + Description: policy.GetDescription(), + ScopeRego: policy.GetScopeRego(), + ConditionsRego: policy.GetConditionsRego(), + ViolationAction: policy.GetViolationAction(), + ViolationReason: policy.GetViolationReason(), + }, + } + return command, path, nil +} + +type platformHubPolicyUpsertCommand struct { + ChangeDescription string `json:"ChangeDescription,omitempty"` + + persistedPolicy +} + +type publishCommand struct { + Version string `json:"Version"` +} + +func buildPublishCommand(client newclient.Client, policy PolicyKey, version string) (publishCommand, string, error) { + parameters := map[string]any{"gitRef": policy.GetGitRef(), "slug": policy.GetSlug()} + path, pathError := client.URITemplateCache().Expand("/api/platformhub/{gitRef}/policies/{slug}/publish", parameters) + if pathError != nil { + return publishCommand{}, "", pathError + } + + return publishCommand{Version: version}, path, nil +} + +func buildGetVersionsPath(client newclient.Client, query PublishedPoliciesQuery) (string, error) { + return client.URITemplateCache().Expand("/api/platformhub/policies/{slug}/versions/v2{?skip,take}", query) +} + +type modifyVersionStatusCommand struct { + IsActive bool `json:"IsActive"` +} + +func modifyVersionStatus(client newclient.Client, policy PublishedPolicyKey, isActive bool) (PublishedPolicy, error) { + command, path, err := buildModifyVersionStatusCommand(client, policy, isActive) + if err != nil { + return nil, err + } + + modifiedVersion, postErr := newclient.Post[publishedPolicyVersion](client.HttpSession(), path, command) + if postErr != nil { + return nil, postErr + } + + return modifiedVersion, nil +} + +func buildModifyVersionStatusCommand(client newclient.Client, policy PublishedPolicyKey, isActive bool) (modifyVersionStatusCommand, string, error) { + parameters := map[string]any{"slug": policy.GetSlug(), "version": policy.GetVersion()} + path, pathError := client.URITemplateCache().Expand("/api/platformhub/policies/{slug}/versions/{version}/modify-status", parameters) + if pathError != nil { + return modifyVersionStatusCommand{}, "", pathError + } + + return modifyVersionStatusCommand{IsActive: isActive}, path, nil +} diff --git a/pkg/platformhubpolicies/service_test.go b/pkg/platformhubpolicies/service_test.go new file mode 100644 index 00000000..65389730 --- /dev/null +++ b/pkg/platformhubpolicies/service_test.go @@ -0,0 +1,310 @@ +package platformhubpolicies + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/newclient" + "github.com/kinbiko/jsonassert" + "github.com/stretchr/testify/require" +) + +func TestPolicy_BuildAddCommand_Valid(t *testing.T) { + var client = newclient.NewClient(&newclient.HttpSession{}) + + expectedPolicy := newPolicyBuilder(). + WithName("ValidPolicyName"). + WithGitRef("refs/heads/test/branch"). + WithDescription("Testing policy"). + WithViolationReason("Missing manual intervention") + urlEncodedGitRef := "refs%2Fheads%2Ftest%2Fbranch" + commitMessage := "Create new policy" + + // Act + policy := expectedPolicy.BuildDraft() + command, path, commandError := buildAddCommand(client, *policy, commitMessage) + + require.NoError(t, commandError) + require.Equal(t, fmt.Sprintf("/api/platformhub/%s/policies", urlEncodedGitRef), path) + + testAssertUpsertCommand(t, command, expectedPolicy, commitMessage) + testAssertUpsertCommandMarshalJSON(t, command, expectedPolicy, commitMessage) +} + +func TestPolicy_BuildAddCommand_Invalid(t *testing.T) { + var client = newclient.NewClient(&newclient.HttpSession{}) + + invalidPolicy := newPolicyBuilder().WithName("InvalidPolicyName").WithConditionsRego("").BuildDraft() + + _, _, invalidCommandError := buildAddCommand(client, *invalidPolicy, "commit invalid command") + + require.ErrorContains(t, invalidCommandError, "ConditionsRego") +} + +func TestPolicy_BuildUpdateCommand_Valid(t *testing.T) { + var client = newclient.NewClient(&newclient.HttpSession{}) + + expectedPolicy := newPolicyBuilder().WithName("Valid Policy"). + WithGitRef("refs/heads/main"). + WithSlug("valid_policy"). + WithDescription("Ok"). + WithViolationReason("None") + urlEncodedGitRef := "refs%2Fheads%2Fmain" + commitMessage := "Update valid policy" + + // Act + newPolicy := expectedPolicy.Build() + command, path, commandError := buildUpdateCommand(client, newPolicy, commitMessage) + + require.NoError(t, commandError) + require.Equal(t, fmt.Sprintf("/api/platformhub/%s/policies/%s", urlEncodedGitRef, expectedPolicy.slug), path) + + testAssertUpsertCommand(t, command, expectedPolicy, commitMessage) + testAssertUpsertCommandMarshalJSON(t, command, expectedPolicy, commitMessage) +} + +func TestPolicy_BuildUpdateCommand_Invalid(t *testing.T) { + var client = newclient.NewClient(&newclient.HttpSession{}) + + invalidPolicy := newPolicyBuilder().WithName("Invalid Action").WithViolationAction("").Build() + _, _, invalidCommandError := buildUpdateCommand(client, invalidPolicy, "commit invalid command") + + require.ErrorContains(t, invalidCommandError, "ViolationAction") +} + +func TestPolicy_BuildPublishCommand(t *testing.T) { + var client = newclient.NewClient(&newclient.HttpSession{}) + + gitRef := "refs/heads/main" + slug := "my_policy" + version := "1.0.1" + urlEncodedGitRef := "refs%2Fheads%2Fmain" + + policy := newPolicyBuilder().WithGitRef(gitRef).WithSlug(slug).Build() + + // Act + command, path, commandError := buildPublishCommand(client, policy, version) + + require.NoError(t, commandError) + require.Equal(t, fmt.Sprintf("/api/platformhub/%s/policies/%s/publish", urlEncodedGitRef, slug), path) + require.Equal(t, version, command.Version) + + // Verify JSON serialization + jsonBytes, jsonErr := json.Marshal(command) + require.NoError(t, jsonErr) + require.JSONEq(t, fmt.Sprintf(`{"Version":"%s"}`, version), string(jsonBytes)) +} + +func TestPolicy_BuildActivateCommand(t *testing.T) { + version := publishedPolicyVersion{ + Slug: "my_policy", + Version: "1.0.0", + } + + assertModifyVersionStatusCommand(t, version, true) +} + +func TestPolicy_BuildDeactivateCommand(t *testing.T) { + version := publishedPolicyVersion{ + Slug: "my_policy", + Version: "2.0.0", + } + + assertModifyVersionStatusCommand(t, version, false) +} + +func assertModifyVersionStatusCommand(t *testing.T, version publishedPolicyVersion, activated bool) { + var client = newclient.NewClient(&newclient.HttpSession{}) + + // Act + command, path, commandError := buildModifyVersionStatusCommand(client, &version, activated) + + // Assert + require.NoError(t, commandError) + require.Equal(t, fmt.Sprintf("/api/platformhub/policies/%s/versions/%s/modify-status", version.Slug, version.Version), path) + require.Equal(t, command.IsActive, activated) + + // Verify JSON serialization + jsonBytes, jsonErr := json.Marshal(command) + require.NoError(t, jsonErr) + require.JSONEq(t, fmt.Sprintf(`{"IsActive":%t}`, activated), string(jsonBytes)) +} + +func TestPolicy_BuildGetVersionsPath_All(t *testing.T) { + var client = newclient.NewClient(&newclient.HttpSession{}) + + // All parameters + query := PublishedPoliciesQuery{ + Slug: "my_policy", + Skip: 10, + Take: 5, + } + + path, err := buildGetVersionsPath(client, query) + + require.NoError(t, err) + require.Equal(t, "/api/platformhub/policies/my_policy/versions/v2?skip=10&take=5", path) +} + +func TestPolicy_BuildGetVersionsPath_OnlySlug(t *testing.T) { + var client = newclient.NewClient(&newclient.HttpSession{}) + + query := PublishedPoliciesQuery{ + Slug: "my_policy", + } + + // Act + path, err := buildGetVersionsPath(client, query) + + require.NoError(t, err) + require.Equal(t, "/api/platformhub/policies/my_policy/versions/v2", path) +} + +func testAssertUpsertCommand(t *testing.T, command platformHubPolicyUpsertCommand, expected *policyBuilder, expectedCommit string) { + require.NotNil(t, command) + require.Equal(t, expected.name, command.GetName()) + require.Equal(t, expected.gitRef, command.GetGitRef()) + require.Equal(t, expected.slug, command.GetSlug()) + require.Equal(t, expected.scopeRego, command.GetScopeRego()) + require.Equal(t, expected.conditionsRego, command.GetConditionsRego()) + require.Equal(t, expected.violationAction, command.GetViolationAction()) + require.Equal(t, expectedCommit, command.ChangeDescription) +} + +func testAssertUpsertCommandMarshalJSON(t *testing.T, command platformHubPolicyUpsertCommand, expected *policyBuilder, expectedCommit string) { + jsonCommand, jsonError := json.Marshal(command) + require.NoError(t, jsonError) + require.NotNil(t, jsonCommand) + + expectedJson := fmt.Sprintf(`{ + "ChangeDescription": "%s", + "Name": "%s", + "GitRef": "%s", + "Slug": "%s", + "Description": "%s", + "ScopeRego": "%s", + "ConditionsRego": "%s", + "ViolationReason": "%s", + "ViolationAction": "%s" + }`, expectedCommit, expected.name, expected.gitRef, expected.slug, expected.description, expected.scopeRego, expected.conditionsRego, expected.violationReason, expected.violationAction) + + jsonassert.New(t).Assertf(expectedJson, string(jsonCommand)) +} + +func TestPoliciesQueryResult_UnmarshalJSON(t *testing.T) { + inputJSON := `{ + "Policies": [ + { + "GitRef": "refs/heads/main", + "Slug": "policy_one", + "Name": "Policy One", + "Description": "First policy", + "ScopeRego": "package scope1", + "ConditionsRego": "package cond1", + "ViolationAction": "block", + "ViolationReason": "reason1" + }, + { + "GitRef": "refs/heads/main", + "Slug": "policy_two", + "Name": "Policy Two", + "ScopeRego": "package scope2", + "ConditionsRego": "package cond2", + "ViolationAction": "warn" + } + ], + "ItemsPerPage": 30, + "FilteredItemsCount": 2, + "TotalItemsCount": 5 + }` + + var result PoliciesQueryResult + err := json.Unmarshal([]byte(inputJSON), &result) + + require.NoError(t, err) + require.Equal(t, 30, result.ItemsPerPage) + require.Equal(t, 2, result.FilteredItemsCount) + require.Equal(t, 5, result.TotalItemsCount) + require.Len(t, result.Policies, 2) + + require.Equal(t, "Policy One", result.Policies[0].GetName()) + require.Equal(t, "policy_one", result.Policies[0].GetSlug()) + require.Equal(t, "refs/heads/main", result.Policies[0].GetGitRef()) + require.Equal(t, "First policy", result.Policies[0].GetDescription()) + require.Equal(t, "block", result.Policies[0].GetViolationAction()) + require.Equal(t, "reason1", result.Policies[0].GetViolationReason()) + + require.Equal(t, "Policy Two", result.Policies[1].GetName()) + require.Equal(t, "policy_two", result.Policies[1].GetSlug()) + require.Equal(t, "warn", result.Policies[1].GetViolationAction()) + require.Equal(t, "", result.Policies[1].GetDescription()) +} + +func TestPublishedPoliciesQueryResult_UnmarshalJSON(t *testing.T) { + inputJSON := `{ + "Items": [ + { + "Id": "pv-001", + "Slug": "policy_one", + "Version": "1.0.0", + "PublishedDate": "2026-03-15T10:30:00Z", + "GitRef": "refs/heads/main", + "GitCommit": "abc123", + "Name": "Policy One", + "Description": "First version", + "ViolationReason": "Missing step", + "ViolationAction": "block", + "RegoScope": "package scope1", + "RegoConditions": "package cond1", + "IsActive": true + }, + { + "Id": "pv-002", + "Slug": "policy_one", + "Version": "2.0.0", + "PublishedDate": "2026-03-18T14:00:00Z", + "GitRef": "refs/heads/main", + "GitCommit": "def456", + "Name": "Policy One v2", + "ViolationAction": "warn", + "RegoScope": "package scope2", + "RegoConditions": "package cond2", + "IsActive": false + } + ], + "ItemsPerPage": 20, + "TotalResults": 2 + }` + + var result PublishedPoliciesQueryResult + err := json.Unmarshal([]byte(inputJSON), &result) + + require.NoError(t, err) + require.Equal(t, 20, result.ItemsPerPage) + require.Equal(t, 2, result.TotalResults) + require.Len(t, result.Items, 2) + + v1 := result.Items[0] + require.Equal(t, "pv-001", v1.GetID()) + require.Equal(t, "policy_one", v1.GetSlug()) + require.Equal(t, "1.0.0", v1.GetVersion()) + require.Equal(t, "abc123", v1.GetGitCommit()) + require.Equal(t, "refs/heads/main", v1.GetGitRef()) + require.Equal(t, "Policy One", v1.GetName()) + require.Equal(t, "First version", v1.GetDescription()) + require.Equal(t, "Missing step", v1.GetViolationReason()) + require.Equal(t, "block", v1.GetViolationAction()) + require.Equal(t, "package scope1", v1.GetScopeRego()) + require.Equal(t, "package cond1", v1.GetConditionsRego()) + require.True(t, v1.IsActivated()) + + v2 := result.Items[1] + require.Equal(t, "pv-002", v2.GetID()) + require.Equal(t, "2.0.0", v2.GetVersion()) + require.Equal(t, "def456", v2.GetGitCommit()) + require.Equal(t, "Policy One v2", v2.GetName()) + require.Equal(t, "warn", v2.GetViolationAction()) + require.Equal(t, "", v2.GetDescription()) + require.False(t, v2.IsActivated()) +}