Skip to content

Commit

Permalink
feature: #279 Confirmed basic concepts work by adding integration tests
Browse files Browse the repository at this point in the history
* Split up modules so that it's easier to setup the integration test without running the app
* Removed the health endpoint from the api spec since it's added automatically
* Removed the result group tag because it didn't seem to work (at least in the integration test)
* Added fixes for getting environment variables into the file as well as for the routes to be setup correctly
* Fixed reference to extension since they no longer come back as a RawMessage
* Changed the sequence number to int64
* Fixed issue where event handlers were not being triggered
* Added api config to the middleware params
* Updated the command dispatcher / handler signature so that the response is value based (not a pointer)
  • Loading branch information
akeemphilbert committed Apr 4, 2024
1 parent 6052d5a commit ff33ef8
Show file tree
Hide file tree
Showing 16 changed files with 220 additions and 80 deletions.
9 changes: 0 additions & 9 deletions v2/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -183,15 +183,6 @@ paths:
x-file: "./controllers/rest/fixtures/staticF/index"
404:
description: file not found
/health:
summary: Health Check
get:
x-controller: HealthCheck
responses:
200:
description: Health Response
500:
description: API Internal Error
/api/json:
get:
operationId: Get API Details
Expand Down
8 changes: 6 additions & 2 deletions v2/rest/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func registerHooks(lifecycle fx.Lifecycle, e *echo.Echo) {
})
}

var API = fx.Module("rest",
var Core = fx.Module("weos-basic",
fx.Provide(
WeOSConfig,
Config,
Expand All @@ -37,5 +37,9 @@ var API = fx.Module("rest",
NewGORMProjection,
NewSecurityConfiguration,
),
fx.Invoke(RouteInitializer, registerHooks),
fx.Invoke(RouteInitializer),
)

var API = fx.Module("rest",
Core,
fx.Invoke(registerHooks))
8 changes: 4 additions & 4 deletions v2/rest/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ type SecurityConfiguration struct {
AuthEnforcer *casbin.Enforcer
}

func NewSecurityConfiguration(p SecurityParams) (result *SecurityConfiguration, err error) {
result = &SecurityConfiguration{
func NewSecurityConfiguration(p SecurityParams) (result SecurityConfiguration, err error) {
result = SecurityConfiguration{
SecuritySchemes: make(map[string]Validator),
}
for name, schema := range p.Config.Components.SecuritySchemes {
Expand All @@ -63,15 +63,15 @@ func NewSecurityConfiguration(p SecurityParams) (result *SecurityConfiguration,
result.SecuritySchemes[name], err = new(OpenIDConnect).FromSchema(ctxt, schema.Value, p.HttpClient)
default:
err = fmt.Errorf("unsupported security scheme '%s'", name)
return nil, err
return result, err
}
}
}

//setup casbin enforcer
adapter, err := gormadapter.NewAdapterByDB(p.GORMDB)
if err != nil {
return nil, err
return result, err
}

//default REST permission model
Expand Down
6 changes: 3 additions & 3 deletions v2/rest/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const DELETE_COMMAND = "delete"

type CommandDispatcherParams struct {
fx.In
CommandConfigs []CommandConfig `group:"commandHandlers"`
CommandConfigs []CommandConfig
Logger Log
}

Expand Down Expand Up @@ -43,7 +43,7 @@ type DefaultCommandDispatcher struct {
dispatch sync.Mutex
}

func (e *DefaultCommandDispatcher) Dispatch(ctx context.Context, command *Command, logger Log, options *CommandOptions) (response *CommandResponse, err error) {
func (e *DefaultCommandDispatcher) Dispatch(ctx context.Context, command *Command, logger Log, options *CommandOptions) (response CommandResponse, err error) {
var wg sync.WaitGroup
var allHandlers []CommandHandler
//first preference is handlers for specific command type and entity type
Expand Down Expand Up @@ -73,7 +73,7 @@ func (e *DefaultCommandDispatcher) Dispatch(ctx context.Context, command *Comman
}
wg.Done()
}()
response, err = handler(ctx, command, options.ResourceRepository, logger)
response, err = handler(ctx, command, logger, options)
}()
}

Expand Down
14 changes: 13 additions & 1 deletion v2/rest/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"go.uber.org/fx"
"net/url"
"os"
"strings"
)

// Config loads the OpenAPI spec from the environment
Expand All @@ -17,7 +18,18 @@ func Config() (*openapi3.T, error) {
if err == nil {
return openapi3.NewLoader().LoadFromURI(turl)
} else {
return openapi3.NewLoader().LoadFromFile(spec)
//read the file
content, err := os.ReadFile(spec)
if err != nil {
return nil, err
}
//change the $ref to another marker so that it doesn't get considered an environment variable WECON-1
tempFile := strings.ReplaceAll(string(content), "$ref", "__ref__")
//replace environment variables in file
tempFile = os.ExpandEnv(string(tempFile))
tempFile = strings.ReplaceAll(string(tempFile), "__ref__", "$ref")
content = []byte(tempFile)
return openapi3.NewLoader().LoadFromData(content)

}
}
Expand Down
15 changes: 6 additions & 9 deletions v2/rest/controllers.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ type ControllerParams struct {

// DefaultWriteController handles the write operations (create, update, delete)
func DefaultWriteController(p *ControllerParams) echo.HandlerFunc {

var err error
var commandName string
var resourceType string
for method, toperation := range p.Operation {
Expand All @@ -44,11 +42,9 @@ func DefaultWriteController(p *ControllerParams) echo.HandlerFunc {
}
}
//If there is a x-command extension then dispatch that command by default
if rawCommand, ok := toperation.Extensions["x-command"].(json.RawMessage); ok {
err := json.Unmarshal(rawCommand, &commandName)
if err != nil {
p.Logger.Fatalf("error unmarshalling command: %s", err)
}
var ok bool
if commandName, ok = toperation.Extensions["x-command"].(string); ok {
p.Logger.Debugf("command configured: %s", commandName)
}
//If there is a x-command-name extension then dispatch that command by default otherwise use the default command based on the operation type
if commandName == "" {
Expand All @@ -67,16 +63,17 @@ func DefaultWriteController(p *ControllerParams) echo.HandlerFunc {

return func(ctxt echo.Context) error {
var sequenceNo string
var seq int
var seq int64

//getting etag from context
etag := ctxt.Request().Header.Get("If-Match")
if etag != "" {
_, sequenceNo = SplitEtag(etag)
seq, err = strconv.Atoi(sequenceNo)
tseq, err := strconv.Atoi(sequenceNo)
if err != nil {
return NewControllerError("unexpected error updating content type. invalid sequence number", err, http.StatusBadRequest)
}
seq = int64(tseq)
}

body, err := io.ReadAll(ctxt.Request().Body)
Expand Down
6 changes: 4 additions & 2 deletions v2/rest/controllers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,10 @@ func TestDefaultWriteController(t *testing.T) {
}
repository := result.Repository
commandDispatcher := &CommandDispatcherMock{
DispatchFunc: func(ctx context.Context, command *rest.Command, logger rest.Log, options *rest.CommandOptions) (*rest.CommandResponse, error) {
return nil, nil
DispatchFunc: func(ctx context.Context, command *rest.Command, logger rest.Log, options *rest.CommandOptions) (rest.CommandResponse, error) {
return rest.CommandResponse{
Code: 200,
}, nil
},
}
controller := rest.DefaultWriteController(&rest.ControllerParams{
Expand Down
2 changes: 1 addition & 1 deletion v2/rest/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func (e *Event) GetType() string {
panic("implement me")
}

func (e *Event) GetSequenceNo() int {
func (e *Event) GetSequenceNo() int64 {
//TODO implement me
panic("implement me")
}
Expand Down
5 changes: 1 addition & 4 deletions v2/rest/fixtures/blog.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -306,9 +306,6 @@ paths:
post:
operationId: Add Blog
summary: Create Blog
x-projection: Default
x-event-dispatcher: Default
x-command-disptacher: Default
requestBody:
description: Blog info that is submitted
required: true
Expand Down Expand Up @@ -339,7 +336,6 @@ paths:
type: integer
- in: query
name: l
x-alias: limit
schema:
type: integer
- in: query
Expand Down Expand Up @@ -475,6 +471,7 @@ paths:
format: double
summary: Update blog details
operationId: Update Blog
x-command: CreateBlog
requestBody:
required: true
content:
Expand Down
66 changes: 37 additions & 29 deletions v2/rest/gorm.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ func NewGORMProjection(p GORMProjectionParams) (result GORMProjectionResult, err
return result, err
}
}
//add handlers for create, update and delete

result = GORMProjectionResult{
Dispatcher: dispatcher,
DefaultProjection: dispatcher,
Expand All @@ -224,39 +226,38 @@ func (e *GORMProjection) Dispatch(ctx context.Context, logger Log, event *Event)
//mutex helps keep state between routines
var errors []error
var wg sync.WaitGroup
var handlers []EventHandler
var ok bool
if globalHandlers := e.handlers[""]; globalHandlers != nil {
if handlers, ok = globalHandlers[event.Type]; ok {

}
}
if resourceTypeHandlers, ok := e.handlers[event.Meta.ResourceType]; ok {
if thandlers, ok := resourceTypeHandlers[event.Type]; ok {
handlers = append(handlers, thandlers...)
}
}

if handlers, ok := resourceTypeHandlers[event.Type]; ok {
//check to see if there were handlers registered for the event type that is not specific to a resource type
if event.Meta.ResourceType != "" {
if eventTypeHandlers, ok := e.handlers[""]; ok {
if ehandlers, ok := eventTypeHandlers[event.Type]; ok {
handlers = append(handlers, ehandlers...)
}
for i := 0; i < len(handlers); i++ {
handler := handlers[i]
wg.Add(1)
go func() {
defer func() {
if r := recover(); r != nil {
logger.Errorf("handler panicked %s", r)
}
}
for i := 0; i < len(handlers); i++ {
handler := handlers[i]
wg.Add(1)
go func() {
defer func() {
if r := recover(); r != nil {
logger.Errorf("handler panicked %s", r)
}
wg.Done()
}()

err := handler(ctx, logger, event)
if err != nil {
errors = append(errors, err)
}
wg.Done()
}()

}()
err := handler(ctx, logger, event)
if err != nil {
errors = append(errors, err)
}
wg.Wait()
}

}()
}
wg.Wait()

return errors
}
Expand Down Expand Up @@ -287,8 +288,16 @@ func (e *GORMProjection) GetSubscribers(resourceType string) map[string][]EventH
}

func (e *GORMProjection) GetByURI(ctxt context.Context, logger Log, uri string) (Resource, error) {
//TODO implement me
panic("implement me")
resource := new(BasicResource)
result := e.gormDB.Where("id = ?", uri).First(resource)
if result.Error != nil {
if !errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, result.Error
} else {
return nil, nil
}
}
return resource, nil
}

func (e *GORMProjection) GetByKey(ctxt context.Context, identifiers map[string]interface{}) (Resource, error) {
Expand All @@ -308,7 +317,6 @@ func (e *GORMProjection) GetByProperties(ctxt context.Context, identifiers map[s

// Persist persists the events to the database
func (e *GORMProjection) Persist(ctxt context.Context, logger Log, resources []Resource) (errs []error) {
//TODO not sure this casting is needed
var events []*Event
for _, resource := range resources {
if event, ok := resource.(*Event); ok {
Expand Down
11 changes: 9 additions & 2 deletions v2/rest/initializers.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,15 @@ func RouteInitializer(p RouteParams) (err error) {
CommandDispatcher: p.CommandDispatcher,
ResourceRepository: p.ResourceRepository,
Schema: p.Config,
APIConfig: p.APIConfig,
}))
//set up the security middleware if there is a config setup
if len(p.Config.Security) > 0 {
pathMiddleware = append(pathMiddleware, SecurityMiddleware(&MiddlewareParams{
Logger: p.Logger,
SecuritySchemes: p.SecuritySchemes,
Schema: p.Config,
APIConfig: p.APIConfig,
}))

}
Expand All @@ -116,9 +118,9 @@ func RouteInitializer(p RouteParams) (err error) {
}
}

var handler echo.HandlerFunc
var methodsFound []string
for method, operation := range pathItem.Operations() {
var handler echo.HandlerFunc
methodsFound = append(methodsFound, method)
if controller, ok := operation.Extensions["x-controller"].(string); ok {
if c, ok := controllers[controller]; ok {
Expand All @@ -127,6 +129,7 @@ func RouteInitializer(p RouteParams) (err error) {
CommandDispatcher: p.CommandDispatcher,
ResourceRepository: p.ResourceRepository,
Schema: p.Config,
APIConfig: p.APIConfig,
PathMap: map[string]*openapi3.PathItem{
path: pathItem,
},
Expand All @@ -146,6 +149,7 @@ func RouteInitializer(p RouteParams) (err error) {
CommandDispatcher: p.CommandDispatcher,
ResourceRepository: p.ResourceRepository,
Schema: p.Config,
APIConfig: p.APIConfig,
PathMap: map[string]*openapi3.PathItem{
path: pathItem,
},
Expand All @@ -159,6 +163,7 @@ func RouteInitializer(p RouteParams) (err error) {
CommandDispatcher: p.CommandDispatcher,
ResourceRepository: p.ResourceRepository,
Schema: p.Config,
APIConfig: p.APIConfig,
PathMap: map[string]*openapi3.PathItem{
path: pathItem,
},
Expand Down Expand Up @@ -194,7 +199,9 @@ func RouteInitializer(p RouteParams) (err error) {
allMiddleware = append(allMiddleware, pathMiddleware...)
allMiddleware = append(allMiddleware, operationMiddleware...)
// Add the middleware to the routes
p.Echo.Add(method, p.APIConfig.BasePath+path, handler, allMiddleware...)
re := regexp.MustCompile(`\{([a-zA-Z0-9\-_]+?)\}`)
echoPath := re.ReplaceAllString(path, `:$1`)
p.Echo.Add(method, p.APIConfig.BasePath+echoPath, handler, allMiddleware...)

//setup security enforcer
if authRaw, ok := operation.Extensions[AuthorizationConfigExtension]; ok {
Expand Down
Loading

0 comments on commit ff33ef8

Please sign in to comment.