Skip to content

Commit

Permalink
api: adds CRUD integration types and test cases (#46)
Browse files Browse the repository at this point in the history
* adds api for integration types

* adds api for integration types

* adds test cases

* adds test cases

* fix lint

* restructure

* fix case

* fix the naming pattern
  • Loading branch information
ashwiniag authored Dec 23, 2024
1 parent 477ca73 commit d5b9a3c
Show file tree
Hide file tree
Showing 10 changed files with 423 additions and 3 deletions.
2 changes: 1 addition & 1 deletion ent/integrationtype.go

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

2 changes: 1 addition & 1 deletion ent/schema/integrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func (Integrations) Fields() []ent.Field {
// Edges of the Integrations.
func (Integrations) Edges() []ent.Edge {
// Many to one
edge.From("id", Integrations.Type).
edge.From("integrations_type", Integrations.Type).
// Reference the "integrations" edge in IntegrationType
Ref("integrations").
// Use as foreign key
Expand Down
2 changes: 1 addition & 1 deletion ent/schema/integrationtype.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type IntegrationType struct {
func (IntegrationType) Fields() []ent.Field {
return []ent.Field{
field.String("id").Unique().Immutable(),
field.String("display_name").NotEmpty().Unique().Comment("Types of Integration e.g., linear,jira"),
field.String("display_name").NotEmpty().Unique().Comment("Human-readable name for the integration type"),
}
}

Expand Down
53 changes: 53 additions & 0 deletions internal/restapi/v1/integrationtype/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package integrationtype

import (
"context"
"errors"
"github.com/shinobistack/gokakashi/ent"
"github.com/swaggest/usecase/status"
)

type GetIntegrationTypeRequests struct {
ID string `path:"id"`
}

type GetIntegrationTypeResponse struct {
ID string `json:"id"`
DisplayName string `json:"display_name"`
}

func GetIntegrationType(client *ent.Client) func(ctx context.Context, req GetIntegrationTypeRequests, res *GetIntegrationTypeResponse) error {
return func(ctx context.Context, req GetIntegrationTypeRequests, res *GetIntegrationTypeResponse) error {
it, err := client.IntegrationType.Get(ctx, req.ID)
if err != nil {
if ent.IsNotFound(err) {
return status.Wrap(errors.New("integration type not found"), status.NotFound)
}
return status.Wrap(err, status.Internal)
}

res.ID = it.ID
res.DisplayName = it.DisplayName
return nil

}

}

func ListIntegrationType(client *ent.Client) func(ctx context.Context, req struct{}, res *[]GetIntegrationTypeResponse) error {
return func(ctx context.Context, req struct{}, res *[]GetIntegrationTypeResponse) error {
its, err := client.IntegrationType.Query().All(ctx)
if err != nil {
return status.Wrap(errors.New("failed to fetch integration types"), status.Internal)
}

*res = make([]GetIntegrationTypeResponse, len(its))
for i, it := range its {
(*res)[i] = GetIntegrationTypeResponse{
ID: it.ID,
DisplayName: it.DisplayName,
}
}
return nil
}
}
99 changes: 99 additions & 0 deletions internal/restapi/v1/integrationtype/get_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package integrationtype_test

import (
"context"
_ "github.com/mattn/go-sqlite3"
"github.com/shinobistack/gokakashi/ent/enttest"
"github.com/shinobistack/gokakashi/internal/restapi/v1/integrationtype"
"github.com/stretchr/testify/assert"
"testing"
)

func TestGetIntegrationType_ValidID(t *testing.T) {
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
defer client.Close()

// Seed data
_, _ = client.IntegrationType.Create().
SetID("linear").
SetDisplayName("Linear Integration").
Save(context.Background())

// Test case: Valid ID
req := integrationtype.GetIntegrationTypeRequests{ID: "linear"}
res := &integrationtype.GetIntegrationTypeResponse{}
handler := integrationtype.GetIntegrationType(client)
err := handler(context.Background(), req, res)

assert.NoError(t, err)
assert.Equal(t, "linear", res.ID)
assert.Equal(t, "Linear Integration", res.DisplayName)
}

func TestGetIntegrationType_NonExistentID(t *testing.T) {
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
defer client.Close()

req := integrationtype.GetIntegrationTypeRequests{ID: "nonexistent"}
res := &integrationtype.GetIntegrationTypeResponse{}
handler := integrationtype.GetIntegrationType(client)
err := handler(context.Background(), req, res)

assert.Error(t, err)
assert.Contains(t, err.Error(), "integration type not found")
}

func TestGetIntegrationType_InvalidIDFormat(t *testing.T) {
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
defer client.Close()

req := integrationtype.GetIntegrationTypeRequests{ID: "Invalid ID!"}
res := &integrationtype.GetIntegrationTypeResponse{}
handler := integrationtype.GetIntegrationType(client)
err := handler(context.Background(), req, res)
assert.Error(t, err)

req = integrationtype.GetIntegrationTypeRequests{ID: "Inv*ID"}
handler = integrationtype.GetIntegrationType(client)
err = handler(context.Background(), req, res)
assert.Error(t, err)
}

func TestListIntegrationTypes_ValidRequest(t *testing.T) {
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
defer client.Close()

// Seed data
_, _ = client.IntegrationType.Create().
SetID("linear").
SetDisplayName("Linear Integration").
Save(context.Background())
_, _ = client.IntegrationType.Create().
SetID("jira").
SetDisplayName("Jira Integration").
Save(context.Background())

req := struct{}{}
var res []integrationtype.GetIntegrationTypeResponse
handler := integrationtype.ListIntegrationType(client)
err := handler(context.Background(), req, &res)

assert.NoError(t, err)
assert.Len(t, res, 2)
}

func TestListIntegrations_EmptyDB(t *testing.T) {
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
defer client.Close()

// Prepare response
var res []integrationtype.GetIntegrationTypeResponse

// Execute ListIntegrations handler
handler := integrationtype.ListIntegrationType(client)
err := handler(context.Background(), struct{}{}, &res)

// Validate response
assert.NoError(t, err)
assert.Len(t, res, 0)
}
56 changes: 56 additions & 0 deletions internal/restapi/v1/integrationtype/post.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package integrationtype

import (
"context"
"errors"
"fmt"
"github.com/shinobistack/gokakashi/ent"
"github.com/swaggest/usecase/status"
"regexp"
"strings"
)

type CreateIntegrationTypeRequest struct {
ID string `json:"id"`
DisplayName string `json:"display_name"`
}

func CreateIntegrationType(client *ent.Client) func(ctx context.Context, req CreateIntegrationTypeRequest, res *GetIntegrationTypeResponse) error {
return func(ctx context.Context, req CreateIntegrationTypeRequest, res *GetIntegrationTypeResponse) error {
// Validate fields
if req.ID == "" || req.DisplayName == "" {
return status.Wrap(errors.New("missing required fields: id and/or display_name"), status.InvalidArgument)
}

if !isValidID(req.ID) {
return status.Wrap(errors.New("invalid id format; must be lowercase, alphanumeric, or dashes"), status.InvalidArgument) // 400 Bad Request
}

// Create integration type
it, err := client.IntegrationType.Create().
SetID(req.ID).
SetDisplayName(req.DisplayName).
Save(ctx)
if err != nil {
if ent.IsConstraintError(err) {
return status.Wrap(errors.New("integration type already exists"), status.AlreadyExists)
}
return status.Wrap(fmt.Errorf("failed to create integration type: %v", err), status.Internal)
}

res.ID = it.ID
res.DisplayName = it.DisplayName
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)
}
84 changes: 84 additions & 0 deletions internal/restapi/v1/integrationtype/post_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package integrationtype_test

import (
"context"
"github.com/shinobistack/gokakashi/internal/restapi/v1/integrationtype"
"testing"

"github.com/shinobistack/gokakashi/ent/enttest"
"github.com/stretchr/testify/assert"
)

func TestCreateIntegrationType_ValidInput(t *testing.T) {
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
defer client.Close()

req := integrationtype.CreateIntegrationTypeRequest{
ID: "linear",
DisplayName: "Linear Integration",
}
res := &integrationtype.GetIntegrationTypeResponse{}
handler := integrationtype.CreateIntegrationType(client)
err := handler(context.Background(), req, res)

assert.NoError(t, err)
assert.Equal(t, "linear", res.ID)
assert.Equal(t, "Linear Integration", res.DisplayName)
}

func TestCreateIntegrationType_MissingFields(t *testing.T) {
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
defer client.Close()

req := integrationtype.CreateIntegrationTypeRequest{}
res := &integrationtype.GetIntegrationTypeResponse{}
handler := integrationtype.CreateIntegrationType(client)
err := handler(context.Background(), req, res)

assert.Error(t, err)
assert.Contains(t, err.Error(), "missing required fields")
}

func TestCreateIntegrationType_InvalidIDFormat(t *testing.T) {
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
defer client.Close()

req := integrationtype.CreateIntegrationTypeRequest{
ID: "Invalid ID!",
DisplayName: "Valid Format ",
}
res := &integrationtype.GetIntegrationTypeResponse{}
handler := integrationtype.CreateIntegrationType(client)
err := handler(context.Background(), req, res)
assert.Error(t, err)

req = integrationtype.CreateIntegrationTypeRequest{
ID: "invalid*#",
DisplayName: "Valid Format ",
}
handler = integrationtype.CreateIntegrationType(client)
err = handler(context.Background(), req, res)
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid id format")
}

func TestCreateIntegrationType_DuplicateID(t *testing.T) {
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
defer client.Close()

_, _ = client.IntegrationType.Create().
SetID("linear").
SetDisplayName("Linear Integration").
Save(context.Background())

req := integrationtype.CreateIntegrationTypeRequest{
ID: "linear",
DisplayName: "Linear Integration",
}
res := &integrationtype.GetIntegrationTypeResponse{}
handler := integrationtype.CreateIntegrationType(client)
err := handler(context.Background(), req, res)

assert.Error(t, err)
assert.Contains(t, err.Error(), "integration type already exists")
}
39 changes: 39 additions & 0 deletions internal/restapi/v1/integrationtype/put.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package integrationtype

import (
"context"
"errors"
"fmt"
"github.com/shinobistack/gokakashi/ent"
"github.com/swaggest/usecase/status"
)

type UpdateIntegrationTypeRequest struct {
ID string `path:"id"`
DisplayName *string `json:"display_name"`
}

func UpdateIntegrationType(client *ent.Client) func(ctx context.Context, req UpdateIntegrationTypeRequest, res *GetIntegrationTypeResponse) error {
return func(ctx context.Context, req UpdateIntegrationTypeRequest, res *GetIntegrationTypeResponse) error {
if !isValidID(req.ID) {
return status.Wrap(errors.New("invalid id format"), status.InvalidArgument) // 400 Bad Request
}

update := client.IntegrationType.UpdateOneID(req.ID)
if req.DisplayName != nil {
update = update.SetDisplayName(*req.DisplayName)
}

it, err := update.Save(ctx)
if err != nil {
if ent.IsNotFound(err) {
return status.Wrap(errors.New("integration type not found"), status.NotFound)
}
return status.Wrap(fmt.Errorf("failed to update integration type: %v", err), status.Internal)
}

res.ID = it.ID
res.DisplayName = it.DisplayName
return nil
}
}
Loading

0 comments on commit d5b9a3c

Please sign in to comment.