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
2 changes: 1 addition & 1 deletion docs/coverage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ code does not implement. Machine-readable: [`coverage.json`](./coverage.json).
| `aoss` | [AOSS](./aws/aoss.md) | - | - | - | 18 |
| `apigateway` | [APIGateway](./aws/apigateway.md) | - | - | - | 29 |
| `apigatewaygcp` | - | - | [APIGateway](./gcp/apigateway.md) | - | 16 |
| `apigatewayv2` | [APIGatewayV2](./aws/apigatewayv2.md) | - | - | - | 20 |
| `apigatewayv2` | [APIGatewayV2](./aws/apigatewayv2.md) | - | - | - | 28 |
| `appconfiguration` | - | [AppConfiguration](./azure/appconfiguration.md) | - | - | 9 |
| `appflow` | [AppFlow](./aws/appflow.md) | - | - | - | 14 |
| `appinsights` | - | [Appinsights](./azure/appinsights.md) | - | - | 6 |
Expand Down
2 changes: 1 addition & 1 deletion docs/coverage/aws/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Services cloudemu emulates for AWS, by native name. Back to the [cross-provider
| [ACM](./acm.md) | `acm` | 17 |
| [AOSS](./aoss.md) | `aoss` | 18 |
| [APIGateway](./apigateway.md) | `apigateway` | 29 |
| [APIGatewayV2](./apigatewayv2.md) | `apigatewayv2` | 20 |
| [APIGatewayV2](./apigatewayv2.md) | `apigatewayv2` | 28 |
| [APS](./aps.md) | `aps` | 21 |
| [AppFlow](./appflow.md) | `appflow` | 14 |
| [AppRunner](./apprunner.md) | `apprunner` | 29 |
Expand Down
10 changes: 9 additions & 1 deletion docs/coverage/aws/apigatewayv2.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,35 @@

AWS's `apigatewayv2` service · portable interface `driver.APIGatewayV2` · [AWS index](./README.md)

## Operations (20)
## Operations (28)

| Operation | Description |
| --- | --- |
| `CreateAPI` | |
| `CreateDeployment` | |
| `CreateIntegration` | |
| `CreateRoute` | |
| `CreateStage` | |
| `DeleteAPI` | |
| `DeleteDeployment` | |
| `DeleteIntegration` | |
| `DeleteRoute` | |
| `DeleteStage` | |
| `GetAPI` | |
| `GetAPIs` | |
| `GetDeployment` | |
| `GetDeployments` | |
| `GetIntegration` | |
| `GetIntegrations` | |
| `GetRoute` | |
| `GetRoutes` | |
| `GetStage` | |
| `GetStages` | |
| `GetTags` | |
| `TagResource` | |
| `UntagResource` | |
| `UpdateAPI` | |
| `UpdateDeployment` | |
| `UpdateIntegration` | |
| `UpdateRoute` | |
| `UpdateStage` | |
Expand Down
24 changes: 24 additions & 0 deletions docs/coverage/coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,9 @@
{
"name": "CreateAPI"
},
{
"name": "CreateDeployment"
},
{
"name": "CreateIntegration"
},
Expand All @@ -444,6 +447,9 @@
{
"name": "DeleteAPI"
},
{
"name": "DeleteDeployment"
},
{
"name": "DeleteIntegration"
},
Expand All @@ -459,6 +465,12 @@
{
"name": "GetAPIs"
},
{
"name": "GetDeployment"
},
{
"name": "GetDeployments"
},
{
"name": "GetIntegration"
},
Expand All @@ -477,9 +489,21 @@
{
"name": "GetStages"
},
{
"name": "GetTags"
},
{
"name": "TagResource"
},
{
"name": "UntagResource"
},
{
"name": "UpdateAPI"
},
{
"name": "UpdateDeployment"
},
{
"name": "UpdateIntegration"
},
Expand Down
74 changes: 58 additions & 16 deletions providers/aws/apigatewayv2/apigatewayv2.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Package apigatewayv2 is an in-memory mock of Amazon API Gateway v2 (HTTP and
// WebSocket APIs). It models the control plane only: an API and its Route,
// Integration and Stage sub-collections, reachable over the apigatewayv2
// REST/JSON protocol. It is a separate service from API Gateway REST v1
// Integration, Stage and Deployment sub-collections plus resource tags,
// reachable over the apigatewayv2 REST/JSON protocol. It is a separate service from API Gateway REST v1
// (providers/aws/apigateway), sharing no state or types.
package apigatewayv2

Expand Down Expand Up @@ -52,6 +52,7 @@ type apiData struct {
routes map[string]*driver.Route
integrations map[string]*driver.Integration
stages map[string]*driver.Stage
deployments map[string]*deploymentRecord
}

// Mock is an in-memory implementation of Amazon API Gateway v2.
Expand Down Expand Up @@ -96,14 +97,15 @@ func (m *Mock) getAPI(id string) (*apiData, error) {
}

// CreateAPI creates a new API with defaulted selection expressions and a
// computed execute-api endpoint.
// computed execute-api endpoint. A Target quick-creates the default
// integration, route and auto-deployed $default stage.
func (m *Mock) CreateAPI(_ context.Context, in *driver.CreateAPIInput) (*driver.API, error) {
if in.Name == "" {
return nil, cerrors.New(cerrors.InvalidArgument, "Name is required")
if in.ProtocolType != driver.ProtocolHTTP && in.ProtocolType != driver.ProtocolWebSocket {
return nil, badRequest("Invalid protocol type specified: %s", in.ProtocolType)
}

if in.ProtocolType != driver.ProtocolHTTP && in.ProtocolType != driver.ProtocolWebSocket {
return nil, cerrors.Newf(cerrors.InvalidArgument, "Invalid protocol type specified: %s", in.ProtocolType)
if in.ProtocolType == driver.ProtocolWebSocket && in.RouteSelectionExpression == "" {
return nil, badRequest("RouteSelectionExpression is required for WEBSOCKET protocol")
}

apiID := genID()
Expand All @@ -113,24 +115,54 @@ func (m *Mock) CreateAPI(_ context.Context, in *driver.CreateAPIInput) (*driver.
RouteSelectionExpression: orDefault(in.RouteSelectionExpression, defaultRouteSelectionExpr),
APIKeySelectionExpression: orDefault(in.APIKeySelectionExpression, defaultAPIKeySelectionExpr),
DisableExecuteAPIEndpoint: in.DisableExecuteAPIEndpoint,
APIEndpoint: fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com", apiID, m.region),
APIEndpoint: m.apiEndpoint(apiID, in.ProtocolType),
CreatedDate: m.now(),
Tags: copyStrMap(in.Tags),
CorsConfiguration: copyCors(in.CorsConfiguration),
}

m.apis.Set(apiID, &apiData{
if err := validateAPIFields(&api); err != nil {
return nil, err
}

if err := validateTags(in.Tags); err != nil {
return nil, err
}

if err := checkQuickCreate(in.ProtocolType, in.Target, in.RouteKey, in.CredentialsArn); err != nil {
return nil, err
}

ad := &apiData{
api: api,
routes: map[string]*driver.Route{},
integrations: map[string]*driver.Integration{},
stages: map[string]*driver.Stage{},
})
deployments: map[string]*deploymentRecord{},
}

if in.Target != "" {
m.quickCreate(ad, in.Target, in.RouteKey, in.CredentialsArn)
}

m.apis.Set(apiID, ad)

out := copyAPI(&api)

return &out, nil
}

// apiEndpoint is the execute-api endpoint of an API: https for HTTP APIs and
// wss for WebSocket APIs.
func (m *Mock) apiEndpoint(apiID, protocol string) string {
scheme := "https"
if protocol == driver.ProtocolWebSocket {
scheme = "wss"
}

return fmt.Sprintf("%s://%s.execute-api.%s.amazonaws.com", scheme, apiID, m.region)
}

// GetAPI returns a single API.
func (m *Mock) GetAPI(_ context.Context, apiID string) (*driver.API, error) {
ad, err := m.getAPI(apiID)
Expand All @@ -146,8 +178,8 @@ func (m *Mock) GetAPI(_ context.Context, apiID string) (*driver.API, error) {
return &out, nil
}

// GetAPIs lists all APIs.
func (m *Mock) GetAPIs(_ context.Context) ([]driver.API, error) {
// GetAPIs lists one page of APIs, ordered by id.
func (m *Mock) GetAPIs(_ context.Context, page *driver.PageInput) ([]driver.API, string, error) {
all := m.apis.All()
out := make([]driver.API, 0, len(all))

Expand All @@ -157,10 +189,11 @@ func (m *Mock) GetAPIs(_ context.Context) ([]driver.API, error) {
ad.mu.RUnlock()
}

return out, nil
return pageOf(out, func(a, b driver.API) bool { return a.APIID < b.APIID }, page)
}

// UpdateAPI applies the non-nil fields of in to the stored API (PATCH).
// UpdateAPI applies the non-nil fields of in to the stored API (PATCH). The
// quick-create fields update the managed integration and route.
func (m *Mock) UpdateAPI(_ context.Context, apiID string, in *driver.UpdateAPIInput) (*driver.API, error) {
ad, err := m.getAPI(apiID)
if err != nil {
Expand All @@ -170,7 +203,7 @@ func (m *Mock) UpdateAPI(_ context.Context, apiID string, in *driver.UpdateAPIIn
ad.mu.Lock()
defer ad.mu.Unlock()

a := &ad.api
a := ad.api
setString(&a.Name, in.Name)
setString(&a.Description, in.Description)
setString(&a.Version, in.Version)
Expand All @@ -182,7 +215,16 @@ func (m *Mock) UpdateAPI(_ context.Context, apiID string, in *driver.UpdateAPIIn
a.CorsConfiguration = copyCors(in.CorsConfiguration)
}

out := copyAPI(a)
if err := validateAPIFields(&a); err != nil {
return nil, err
}

if err := m.updateQuickCreate(ad, in); err != nil {
return nil, err
}

ad.api = a
out := copyAPI(&a)

return &out, nil
}
Expand Down
21 changes: 15 additions & 6 deletions providers/aws/apigatewayv2/apigatewayv2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func TestAPICRUD(t *testing.T) {
t.Fatalf("PATCH clobbered untouched fields: %+v", upd)
}

apis, err := m.GetAPIs(ctx())
apis, _, err := m.GetAPIs(ctx(), nil)
if err != nil || len(apis) != 1 {
t.Fatalf("GetAPIs: %v, len=%d", err, len(apis))
}
Expand All @@ -124,13 +124,20 @@ func TestRouteCRUD(t *testing.T) {
t.Fatalf("CreateRoute: %v, %+v", err, rt)
}

target := "integrations/abc"
ig, err := m.CreateIntegration(ctx(), api.APIID, &driver.CreateIntegrationInput{
IntegrationType: driver.IntegrationHTTPProxy, IntegrationURI: "https://example.com", IntegrationMethod: "GET",
})
if err != nil {
t.Fatalf("CreateIntegration: %v", err)
}

target := "integrations/" + ig.IntegrationID
upd, err := m.UpdateRoute(ctx(), api.APIID, rt.RouteID, &driver.UpdateRouteInput{Target: &target})
if err != nil || upd.Target != target || upd.RouteKey != "GET /items" {
t.Fatalf("UpdateRoute: %v, %+v", err, upd)
}

routes, err := m.GetRoutes(ctx(), api.APIID)
routes, _, err := m.GetRoutes(ctx(), api.APIID, nil)
if err != nil || len(routes) != 1 {
t.Fatalf("GetRoutes: %v, len=%d", err, len(routes))
}
Expand Down Expand Up @@ -164,7 +171,9 @@ func TestIntegrationCRUDAndTimeoutDefault(t *testing.T) {
}

// WebSocket API defaults the integration timeout to 29000.
wsAPI, err := m.CreateAPI(ctx(), &driver.CreateAPIInput{Name: "ws", ProtocolType: driver.ProtocolWebSocket})
wsAPI, err := m.CreateAPI(ctx(), &driver.CreateAPIInput{
Name: "ws", ProtocolType: driver.ProtocolWebSocket, RouteSelectionExpression: "$request.body.action",
})
if err != nil {
t.Fatalf("CreateAPI ws: %v", err)
}
Expand Down Expand Up @@ -202,7 +211,7 @@ func TestStageCRUDAndConflict(t *testing.T) {
t.Fatalf("UpdateStage: %v, %+v", err, upd)
}

stages, err := m.GetStages(ctx(), api.APIID)
stages, _, err := m.GetStages(ctx(), api.APIID, nil)
if err != nil || len(stages) != 1 {
t.Fatalf("GetStages: %v, len=%d", err, len(stages))
}
Expand All @@ -219,7 +228,7 @@ func TestStageCRUDAndConflict(t *testing.T) {
func TestSubResourcesOnMissingAPI(t *testing.T) {
m := newMock(t)

if _, err := m.GetRoutes(ctx(), "nope"); !cerrors.IsNotFound(err) {
if _, _, err := m.GetRoutes(ctx(), "nope", nil); !cerrors.IsNotFound(err) {
t.Fatalf("GetRoutes missing api err = %v, want NotFound", err)
}

Expand Down
Loading
Loading