diff --git a/internal/restapi/v1/integrations/put.go b/internal/restapi/v1/integrations/put.go index 0a45ba1..2f20ebe 100644 --- a/internal/restapi/v1/integrations/put.go +++ b/internal/restapi/v1/integrations/put.go @@ -10,7 +10,6 @@ import ( ) type UpdateIntegrationRequest struct { - // ToDo: convert to UUID ID uuid.UUID `path:"id"` Name *string `json:"name"` Type *string `json:"type"` diff --git a/internal/restapi/v1/policies/delete_test.go b/internal/restapi/v1/policies/delete_test.go new file mode 100644 index 0000000..ba9e73d --- /dev/null +++ b/internal/restapi/v1/policies/delete_test.go @@ -0,0 +1,58 @@ +package policies_test + +import ( + "context" + "github.com/google/uuid" + _ "github.com/mattn/go-sqlite3" + "github.com/shinobistack/gokakashi/ent/enttest" + policyent "github.com/shinobistack/gokakashi/ent/policies" + "github.com/shinobistack/gokakashi/ent/schema" + "github.com/shinobistack/gokakashi/internal/restapi/v1/policies" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestDeletePolicy_ValidDeletion(t *testing.T) { + client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") + defer client.Close() + + // Create a policy + policy := client.Policies.Create(). + SetName("to-be-deleted-test-policy"). + SetImage(schema.Image{Registry: "example-registry", Name: "example-name", Tags: []string{"v1.0"}}). + SaveX(context.Background()) + req := policies.DeletePolicyRequest{ID: policy.ID} + res := &policies.DeletePolicyResponse{} + err := policies.DeletePolicy(client)(context.Background(), req, res) + + assert.NoError(t, err) + assert.Equal(t, policy.ID, res.ID) + + // Verify deletion + exists := client.Policies.Query().Where(policyent.ID(policy.ID)).ExistX(context.Background()) + assert.False(t, exists) +} + +func TestDeletePolicy_InvalidUUID(t *testing.T) { + client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") + defer client.Close() + + req := policies.DeletePolicyRequest{ID: uuid.Nil} + res := &policies.DeletePolicyResponse{} + err := policies.DeletePolicy(client)(context.Background(), req, res) + + assert.Error(t, err) +} + +func TestDeletePolicy_NonExistentID(t *testing.T) { + client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") + defer client.Close() + + req := policies.DeletePolicyRequest{ID: uuid.New()} + res := &policies.DeletePolicyResponse{} + err := policies.DeletePolicy(client)(context.Background(), req, res) + + assert.Error(t, err) +} + +// ToDo: TestDeletePolicy_WithDependentRecords diff --git a/internal/restapi/v1/policies/get_test.go b/internal/restapi/v1/policies/get_test.go new file mode 100644 index 0000000..1e39892 --- /dev/null +++ b/internal/restapi/v1/policies/get_test.go @@ -0,0 +1,86 @@ +package policies_test + +import ( + "context" + "github.com/google/uuid" + _ "github.com/mattn/go-sqlite3" + "github.com/shinobistack/gokakashi/ent/enttest" + "github.com/shinobistack/gokakashi/ent/schema" + "github.com/shinobistack/gokakashi/internal/restapi/v1/policies" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestListPolicies_EmptyDatabase(t *testing.T) { + client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") + defer client.Close() + + req := policies.ListPoliciesRequest{} + res := &[]policies.GetPolicyResponse{} + err := policies.ListPolicies(client)(context.Background(), req, res) + + assert.NoError(t, err) + assert.Equal(t, 0, len(*res)) +} + +func TestListPolicies(t *testing.T) { + client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") + defer client.Close() + + // Create test data + client.Policies.Create(). + SetName("test-policy1"). + SetImage(schema.Image{Registry: "example-registry", Name: "example-name", Tags: []string{"v1.0"}}). + Save(context.Background()) + client.Policies.Create(). + SetName("test-policy2"). + SetImage(schema.Image{Registry: "example-registry", Name: "example-name", Tags: []string{"v1.0"}}). + Save(context.Background()) + + req := policies.ListPoliciesRequest{} + res := &[]policies.GetPolicyResponse{} + err := policies.ListPolicies(client)(context.Background(), req, res) + + assert.NoError(t, err) + assert.Equal(t, 2, len(*res)) +} + +func TestGetPolicy_ValidID(t *testing.T) { + client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") + defer client.Close() + + // Create a policy + policy := client.Policies.Create(). + SetName("test-policy"). + SetImage(schema.Image{Registry: "example-registry", Name: "example-name", Tags: []string{"v1.0"}}). + SaveX(context.Background()) + + req := policies.GetPolicyRequests{ID: policy.ID} + res := &policies.GetPolicyResponse{} + err := policies.GetPolicy(client)(context.Background(), req, res) + + assert.NoError(t, err) + assert.Equal(t, policy.Name, res.Name) +} + +func TestGetPolicy_InvalidUUID(t *testing.T) { + client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") + defer client.Close() + + req := policies.GetPolicyRequests{ID: uuid.Nil} + res := &policies.GetPolicyResponse{} + err := policies.GetPolicy(client)(context.Background(), req, res) + + assert.Error(t, err) +} + +func TestGetPolicy_NonExistentID(t *testing.T) { + client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") + defer client.Close() + + req := policies.GetPolicyRequests{ID: uuid.New()} + res := &policies.GetPolicyResponse{} + err := policies.GetPolicy(client)(context.Background(), req, res) + + assert.Error(t, err) +} diff --git a/internal/restapi/v1/policies/post.go b/internal/restapi/v1/policies/post.go index 92bf5d6..c963b2a 100644 --- a/internal/restapi/v1/policies/post.go +++ b/internal/restapi/v1/policies/post.go @@ -6,8 +6,11 @@ import ( "fmt" "github.com/google/uuid" "github.com/shinobistack/gokakashi/ent" + "github.com/shinobistack/gokakashi/ent/policies" "github.com/shinobistack/gokakashi/ent/schema" "github.com/swaggest/usecase/status" + "regexp" + "strings" ) type CreatePolicyRequest struct { @@ -31,6 +34,21 @@ func CreatePolicy(client *ent.Client) func(ctx context.Context, req CreatePolicy return status.Wrap(errors.New("invalid image: missing required fields"), status.InvalidArgument) } + if !isValidID(req.Name) { + return status.Wrap(errors.New("invalid id format; must be lowercase, alphanumeric, or dashes"), status.InvalidArgument) + } + + // Check for duplicate name + exists, err := client.Policies.Query(). + Where(policies.Name(req.Name)). + Exist(ctx) + if err != nil { + return status.Wrap(fmt.Errorf("failed to check for duplicate policies name: %v", err), status.Internal) + } + if exists { + return status.Wrap(errors.New("policy with the same name already exists"), status.AlreadyExists) + } + // Validate trigger // ToDo: Valid cron for type: cron @@ -59,3 +77,14 @@ func CreatePolicy(client *ent.Client) func(ctx context.Context, req CreatePolicy return nil } } + +// isValidID validates the ID format: +// - All lowercase letters. +// - Multiple words separated by dashes (`-`). +// - No spaces at the beginning or end. +// - No special characters other than hyphen. +func isValidID(id string) bool { + id = strings.TrimSpace(id) + regex := regexp.MustCompile(`^[a-z]+(-[a-z]+)*$`) + return regex.MatchString(id) +} diff --git a/internal/restapi/v1/policies/post_test.go b/internal/restapi/v1/policies/post_test.go new file mode 100644 index 0000000..5bab017 --- /dev/null +++ b/internal/restapi/v1/policies/post_test.go @@ -0,0 +1,124 @@ +package policies_test + +import ( + "context" + _ "github.com/mattn/go-sqlite3" + "github.com/shinobistack/gokakashi/ent/enttest" + "github.com/shinobistack/gokakashi/ent/schema" + "github.com/shinobistack/gokakashi/internal/restapi/v1/policies" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCreatepolicy_InvalidPolicyNameFormat(t *testing.T) { + client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") + defer client.Close() + + req := policies.CreatePolicyRequest{ + Name: "tTest-policy", + Image: schema.Image{ + Registry: "example-registry", + Name: "example-name", + Tags: []string{"v1.0"}, + }, + Trigger: map[string]interface{}{"type": "cron", "schedule": "0 0 * * *"}, + Check: &schema.Check{ + Condition: "sev.high > 0", + Notify: []string{"team-slack"}, + }, + } + res := &policies.CreatePolicyResponse{} + err := policies.CreatePolicy(client)(context.Background(), req, res) + assert.Error(t, err) + + req = policies.CreatePolicyRequest{ + Name: "#test policy", + Image: schema.Image{ + Registry: "example-registry", + Name: "example-name", + Tags: []string{"v1.0"}, + }, + Trigger: map[string]interface{}{"type": "cron", "schedule": "0 0 * * *"}, + Check: &schema.Check{ + Condition: "sev.high > 0", + Notify: []string{"team-slack"}, + }, + } + err = policies.CreatePolicy(client)(context.Background(), req, res) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid id format") +} + +func TestCreatePolicy_ValidInput(t *testing.T) { + client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") + defer client.Close() + + req := policies.CreatePolicyRequest{ + Name: "test-policy", + Image: schema.Image{ + Registry: "example-registry", + Name: "example-name", + Tags: []string{"v1.0"}, + }, + Trigger: map[string]interface{}{"type": "cron", "schedule": "0 0 * * *"}, + Check: &schema.Check{ + Condition: "sev.high > 0", + Notify: []string{"team-slack"}, + }, + } + res := &policies.CreatePolicyResponse{} + err := policies.CreatePolicy(client)(context.Background(), req, res) + + assert.NoError(t, err) + assert.NotNil(t, res.ID) + assert.Equal(t, "created", res.Status) +} + +func TestCreatePolicy_MissingFields(t *testing.T) { + client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") + defer client.Close() + + req := policies.CreatePolicyRequest{ + Name: "incomplete-policy", + Image: schema.Image{ + Registry: "", + Name: "example-name", + Tags: []string{}, + }, + Trigger: nil, + Check: nil, + } + res := &policies.CreatePolicyResponse{} + err := policies.CreatePolicy(client)(context.Background(), req, res) + + assert.Error(t, err) +} + +func TestCreatePolicy_DuplicateName(t *testing.T) { + client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") + defer client.Close() + + // Seed database + req := policies.CreatePolicyRequest{ + Name: "duplicate-policy", + Image: schema.Image{ + Registry: "example-registry", + Name: "example-name", + Tags: []string{"v1.0"}, + }, + Trigger: map[string]interface{}{"type": "cron", "schedule": "0 0 * * *"}, + Check: &schema.Check{ + Condition: "sev.high > 0", + Notify: []string{"team-slack"}, + }, + } + res := &policies.CreatePolicyResponse{} + handler := policies.CreatePolicy(client) + err := handler(context.Background(), req, res) + assert.NoError(t, err) + + // Test case: Duplicate name + err = handler(context.Background(), req, res) + assert.Error(t, err) + assert.Contains(t, err.Error(), "policy with the same name already exists") +} diff --git a/internal/restapi/v1/policies/put_test.go b/internal/restapi/v1/policies/put_test.go new file mode 100644 index 0000000..36e40bd --- /dev/null +++ b/internal/restapi/v1/policies/put_test.go @@ -0,0 +1,88 @@ +package policies_test + +import ( + "context" + "github.com/google/uuid" + _ "github.com/mattn/go-sqlite3" + "github.com/shinobistack/gokakashi/ent/enttest" + "github.com/shinobistack/gokakashi/ent/schema" + "github.com/shinobistack/gokakashi/internal/restapi/v1/policies" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestUpdatePolicy_ValidUpdate(t *testing.T) { + client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") + defer client.Close() + + // Create a policy + policy := client.Policies.Create(). + SetName("test-policy"). + SetImage(schema.Image{Registry: "example-registry", Name: "example-name", Tags: []string{"v1.0"}}). + SaveX(context.Background()) + + req := policies.UpdatePolicyRequest{ + ID: policy.ID, + Name: strPtr("updated-test-policy"), + } + var res policies.GetPolicyResponse + err := policies.UpdatePolicy(client)(context.Background(), req, &res) + + assert.NoError(t, err) + assert.Equal(t, policy.ID, res.ID) + + // Verify update + updatedPolicy := client.Policies.GetX(context.Background(), policy.ID) + assert.Equal(t, "updated-test-policy", updatedPolicy.Name) +} + +func TestUpdatePolicy_InvalidUUID(t *testing.T) { + client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") + defer client.Close() + + req := policies.UpdatePolicyRequest{ + ID: uuid.Nil, + } + var res policies.GetPolicyResponse + err := policies.UpdatePolicy(client)(context.Background(), req, &res) + + assert.Error(t, err) +} + +func TestUpdatePolicy_NonExistentID(t *testing.T) { + client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") + defer client.Close() + + req := policies.UpdatePolicyRequest{ + ID: uuid.New(), + } + var res policies.GetPolicyResponse + err := policies.UpdatePolicy(client)(context.Background(), req, &res) + + assert.Error(t, err) +} + +// ToDo: implement no changes in policy when no to be changed objects are passed +//func TestUpdatePolicy_NoChanges(t *testing.T) { +// client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") +// defer client.Close() +// +// // Create a policy +// policy := client.Policies.Create(). +// SetName("test-policy"). +// SetImage(schema.Image{Registry: "example-registry", Name: "example-name", Tags: []string{"v1.0"}}). +// SaveX(context.Background()) +// +// req := policies.UpdatePolicyRequest{ +// ID: policy.ID, +// } +// var res policies.GetPolicyResponse +// err := policies.UpdatePolicy(client)(context.Background(), req, &res) +// +// assert.NoError(t, err) +// assert.Equal(t, "No Change Policy", client.Policies.GetX(context.Background(), policy.ID).Name) +//} + +func strPtr(s string) *string { + return &s +}