@kensio/yulin 1.21.6 → 1.21.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -2
- package/dist/service/eventbridge/cfn/bus/sim-cfn-event-bus-creator.js +1 -0
- package/dist/service/eventbridge/cfn/bus/sim-cfn-event-bus-properties.d.ts +4 -0
- package/dist/service/eventbridge/cfn/bus/sim-cfn-event-bus-properties.js +26 -1
- package/dist/service/eventbridge/cfn/rule/sim-cfn-event-rule-creator.js +3 -1
- package/dist/service/eventbridge/cfn/rule/sim-cfn-event-rule-properties.d.ts +0 -7
- package/dist/service/eventbridge/cfn/rule/sim-cfn-event-rule-properties.js +0 -20
- package/dist/service/eventbridge/cfn/rule/sim-cfn-event-rule-unsimulated-properties.d.ts +13 -0
- package/dist/service/eventbridge/cfn/rule/sim-cfn-event-rule-unsimulated-properties.js +59 -0
- package/dist/service/eventbridge/cfn/sim-cfn-event-bridge-resource-error.d.ts +1 -1
- package/dist/service/eventbridge/cfn/sim-cfn-event-bridge-resource-error.js +1 -1
- package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-properties.js +1 -1
- package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-property-rules.d.ts +3 -2
- package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-property-rules.js +25 -3
- package/dist/service/ses/command/authorize/sim-ses-authorizer.d.ts +9 -0
- package/dist/service/ses/command/authorize/sim-ses-authorizer.js +18 -2
- package/dist/service/ses/command/send/sim-ses-send-email.js +2 -1
- package/dist/service/sns/cfn/topic/sim-cfn-sns-topic-creator.js +1 -0
- package/dist/service/sns/cfn/topic/sim-cfn-sns-topic-properties.d.ts +9 -4
- package/dist/service/sns/cfn/topic/sim-cfn-sns-topic-properties.js +18 -6
- package/dist/service/sns/cfn/topic/sim-cfn-sns-topic-property-names.d.ts +10 -0
- package/dist/service/sns/cfn/topic/sim-cfn-sns-topic-property-names.js +16 -1
- package/dist/service/ssm/cfn/parameter/sim-cfn-ssm-parameter-creator.js +1 -1
- package/dist/service/ssm/cfn/parameter/sim-cfn-ssm-parameter-properties.d.ts +11 -6
- package/dist/service/ssm/cfn/parameter/sim-cfn-ssm-parameter-properties.js +18 -9
- package/docs/README.md +40 -4
- package/docs/ai-skill/README.md +57 -54
- package/docs/cli/README.md +84 -94
- package/docs/factories/README.md +42 -54
- package/docs/lint/README.md +41 -67
- package/docs/non-aws-dependencies/README.md +72 -168
- package/docs/sdk/README.md +109 -95
- package/docs/serve/README.md +192 -898
- package/docs/services/acm/README.md +24 -40
- package/docs/services/apigateway/README.md +52 -71
- package/docs/services/apigatewayv2/README.md +55 -74
- package/docs/services/athena/README.md +17 -26
- package/docs/services/backup/README.md +29 -39
- package/docs/services/bedrock/README.md +38 -52
- package/docs/services/cloudformation/README.md +43 -55
- package/docs/services/cloudfront/README.md +69 -95
- package/docs/services/cloudwatch/README.md +40 -54
- package/docs/services/cognito/README.md +30 -45
- package/docs/services/dynamodb/README.md +34 -51
- package/docs/services/ecr/README.md +36 -77
- package/docs/services/ecs/README.md +26 -46
- package/docs/services/elbv2/README.md +19 -29
- package/docs/services/eventbridge/README.md +24 -20
- package/docs/services/firehose/README.md +24 -32
- package/docs/services/glue/README.md +41 -78
- package/docs/services/iam/README.md +13 -15
- package/docs/services/kinesis/README.md +53 -93
- package/docs/services/kms/README.md +22 -32
- package/docs/services/lambda/README.md +60 -80
- package/docs/services/logs/README.md +41 -50
- package/docs/services/organizations/README.md +50 -85
- package/docs/services/personalize/README.md +28 -44
- package/docs/services/rekognition/README.md +26 -38
- package/docs/services/route53/README.md +17 -17
- package/docs/services/s3/README.md +47 -51
- package/docs/services/scheduler/README.md +41 -52
- package/docs/services/secretsmanager/README.md +27 -42
- package/docs/services/ses/README.md +24 -34
- package/docs/services/sns/README.md +31 -33
- package/docs/services/sqs/README.md +14 -14
- package/docs/services/ssm/README.md +19 -21
- package/docs/services/stepfunctions/README.md +18 -20
- package/docs/services/sts/README.md +32 -45
- package/docs/services/wafv2/README.md +12 -17
- package/docs/terraform/README.md +108 -126
- package/docs/testing/README.md +228 -0
- package/docs/time/README.md +87 -117
- package/llms.txt +2 -1
- package/package.json +1 -1
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
# Simulated WAFv2
|
|
2
2
|
|
|
3
|
-
Yulin
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Yulin simulates AWS WAFv2 web ACLs, IP sets and regex pattern sets for tests and local development.
|
|
4
|
+
You can evaluate a `Request` directly or associate a web ACL with a simulated API Gateway REST API,
|
|
5
|
+
Cognito user pool or CloudFront distribution. Each request is checked against the web ACL before
|
|
6
|
+
the protected service handles it.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
and a simulated Cognito user pool each take one through `AssociateWebACL`, and a simulated
|
|
10
|
-
CloudFront distribution takes one through its own `WebACLId`. The requests that stage, pool or
|
|
11
|
-
distribution serves are then put through the web ACL's rules.
|
|
12
|
-
|
|
13
|
-
WAFv2 specific types are imported from the `@kensio/yulin/wafv2` subpath.
|
|
8
|
+
Import WAFv2-specific types from `@kensio/yulin/wafv2`.
|
|
14
9
|
|
|
15
10
|
## Deciding what happens to a request
|
|
16
11
|
|
|
@@ -91,11 +86,11 @@ carrying the status, the body and any headers the rule named.
|
|
|
91
86
|
|
|
92
87
|
## Rules run in priority order
|
|
93
88
|
|
|
94
|
-
Rules are evaluated
|
|
95
|
-
rule
|
|
89
|
+
Rules are evaluated by ascending `Priority`, regardless of their order in the input list. The first
|
|
90
|
+
matching rule with an `Allow` or `Block` action decides the request.
|
|
96
91
|
|
|
97
|
-
A `Count` action records the match and
|
|
98
|
-
before
|
|
92
|
+
A `Count` action records the match and continues to the next rule. Tests can inspect
|
|
93
|
+
`countedRuleNames` before changing a rule to `Allow` or `Block`.
|
|
99
94
|
|
|
100
95
|
```typescript sim-wafv2-count
|
|
101
96
|
/**
|
|
@@ -154,8 +149,8 @@ whatever serves the request has usually read it by the time WAF gets a look.
|
|
|
154
149
|
|
|
155
150
|
## What a statement can inspect
|
|
156
151
|
|
|
157
|
-
A statement
|
|
158
|
-
|
|
152
|
+
A statement selects part of the request, applies its text transformations, and then tests the
|
|
153
|
+
result.
|
|
159
154
|
|
|
160
155
|
The parts a statement can be pointed at are `UriPath`, `QueryString`, `SingleQueryArgument`,
|
|
161
156
|
`AllQueryArguments`, `SingleHeader`, `Headers`, `Cookies`, `Method` and `Body`. `Headers` and
|
|
@@ -1371,7 +1366,7 @@ Tags, logging, sampled requests and CloudWatch metrics for a web ACL are not sim
|
|
|
1371
1366
|
`AssociationConfig`, `DataProtectionConfig`, `OnSourceDDoSProtectionConfig` and `ApplicationConfig`
|
|
1372
1367
|
are refused for the same reason, each naming what it would have configured.
|
|
1373
1368
|
|
|
1374
|
-
##
|
|
1369
|
+
## Supported operations
|
|
1375
1370
|
|
|
1376
1371
|
`CreateWebACL`, `GetWebACL`, `UpdateWebACL`, `ListWebACLs`, `DeleteWebACL`, `CreateIPSet`,
|
|
1377
1372
|
`GetIPSet`, `UpdateIPSet`, `ListIPSets`, `DeleteIPSet`, `CreateRegexPatternSet`,
|
package/docs/terraform/README.md
CHANGED
|
@@ -1,27 +1,29 @@
|
|
|
1
|
-
# Terraform
|
|
1
|
+
# Deploy Terraform plans
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
reads the JSON `terraform show -json` writes for a saved plan file and creates the resources the
|
|
6
|
-
plan declares.
|
|
3
|
+
`TerraformAdapter` reads a Terraform plan in JSON form and deploys its AWS resources into a
|
|
4
|
+
`SimAws` instance.
|
|
7
5
|
|
|
8
6
|
```bash
|
|
9
7
|
npm i -D @kensio/yulin
|
|
10
8
|
```
|
|
11
9
|
|
|
12
|
-
##
|
|
10
|
+
## Create the plan JSON
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
`terraform show -json` turns that into the document the adapter reads.
|
|
12
|
+
Save a plan, then convert it to JSON:
|
|
16
13
|
|
|
17
14
|
```bash
|
|
18
15
|
terraform plan -out=orders.tfplan
|
|
19
16
|
terraform show -json orders.tfplan > orders.tfplan.json
|
|
20
17
|
```
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
`TerraformAdapter` expects the JSON file produced by `terraform show -json`. The binary `.tfplan`
|
|
20
|
+
file is only the input to that command.
|
|
21
|
+
|
|
22
|
+
## Deploy the plan
|
|
23
|
+
|
|
24
|
+
Create an adapter for the `SimAws` instance and pass the JSON path to `deployPlan`. A Terraform plan
|
|
25
|
+
records the location of each Lambda deployment package. Use a binding to supply an executable
|
|
26
|
+
handler for the simulated function:
|
|
25
27
|
|
|
26
28
|
```typescript terraform-deploy-plan
|
|
27
29
|
/**
|
|
@@ -50,9 +52,10 @@ console.log(simAws.dynamoDb().findTable("orders-orders")?.tableName);
|
|
|
50
52
|
console.log(stack.status);
|
|
51
53
|
```
|
|
52
54
|
|
|
53
|
-
The
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
The default stack name comes from the plan filename. For example, `orders.tfplan.json` creates a
|
|
56
|
+
stack named `orders`. Pass `stackName` when the stack needs a different name.
|
|
57
|
+
|
|
58
|
+
If the deployment needs no bindings, overrides, or custom stack name, pass the path directly:
|
|
56
59
|
|
|
57
60
|
```typescript terraform-deploy-plan-path
|
|
58
61
|
/**
|
|
@@ -71,43 +74,75 @@ const { stack } = await new TerraformAdapter(simAws).deployPlan(
|
|
|
71
74
|
console.log(stack.stackName);
|
|
72
75
|
```
|
|
73
76
|
|
|
74
|
-
##
|
|
77
|
+
## Read the deployed stack and import report
|
|
78
|
+
|
|
79
|
+
`deployPlan` returns `stack` and `report`.
|
|
80
|
+
|
|
81
|
+
`stack` is a simulated CloudFormation stack. Use `stack.getResource(...)` to read a resource,
|
|
82
|
+
`stack.output(...)` to read an output, and `stack.delete()` to delete it. The
|
|
83
|
+
[CloudFormation guide](https://yulinsim.dev/services/cloudformation/ "Simulated CloudFormation usage docs")
|
|
84
|
+
describes the rest of the stack API.
|
|
85
|
+
|
|
86
|
+
`report` explains how Terraform resources were imported:
|
|
87
|
+
|
|
88
|
+
```typescript terraform-plan-report
|
|
89
|
+
/**
|
|
90
|
+
* Reading what a Terraform plan import made of the plan.
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
import { SimAws } from "@kensio/yulin";
|
|
94
|
+
import { TerraformAdapter } from "@kensio/yulin/terraform";
|
|
95
|
+
|
|
96
|
+
const simAws = new SimAws();
|
|
97
|
+
|
|
98
|
+
const { report } = await new TerraformAdapter(simAws).deployPlan(
|
|
99
|
+
"terraform/orders.tfplan.json",
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// [ { address: 'aws_s3_bucket.uploads', type: 'aws_s3_bucket',
|
|
103
|
+
// cfnType: 'AWS::S3::Bucket', logicalId: 'AwsS3BucketUploads' } ]
|
|
104
|
+
console.log(report.mapped);
|
|
75
105
|
|
|
76
|
-
|
|
106
|
+
// [ { address: 'aws_route53_zone.public', type: 'aws_route53_zone',
|
|
107
|
+
// reason: 'no mapping for resource type' } ]
|
|
108
|
+
console.log(report.skipped);
|
|
77
109
|
|
|
78
|
-
The
|
|
79
|
-
|
|
80
|
-
Resources are read with `stack.getResource(...)`, Outputs with `stack.output(...)`, and the whole
|
|
81
|
-
thing is torn down with `stack.delete()`.
|
|
110
|
+
// The aws_s3_bucket_versioning and friends that became bucket properties.
|
|
111
|
+
console.log(report.folded);
|
|
82
112
|
|
|
83
|
-
|
|
113
|
+
// Attributes a mapping could not carry and no override supplied, such as a
|
|
114
|
+
// Lambda's environment variables, which Terraform collapses whole when one of
|
|
115
|
+
// them is unknown.
|
|
116
|
+
console.log(report.lost);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- `mapped` lists resources that became CloudFormation resources.
|
|
120
|
+
- `folded` lists Terraform resources that became properties of another resource.
|
|
121
|
+
- `skipped` lists resources that were omitted and gives the reason for each omission.
|
|
122
|
+
- `lost` lists attributes that the plan could not supply and no override replaced.
|
|
123
|
+
|
|
124
|
+
The `mapped`, `folded`, and `skipped` lists account for every managed resource in the plan.
|
|
125
|
+
|
|
126
|
+
## How resources and references are imported
|
|
84
127
|
|
|
85
128
|
A resource declared with `count` or `for_each` arrives in the plan already expanded, and each
|
|
86
|
-
instance becomes a
|
|
87
|
-
|
|
129
|
+
instance becomes a separate simulated resource. The adapter also reads resources from nested
|
|
130
|
+
modules.
|
|
88
131
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
have carried.
|
|
132
|
+
When an attribute is unknown at plan time, Terraform omits its value but keeps its references. The
|
|
133
|
+
adapter follows references through module inputs, module outputs, `each.value`, and `each.key`.
|
|
92
134
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
right and the edge CloudFormation orders from has gone with it. Every reference a resource declares
|
|
96
|
-
becomes an ordering edge, whether or not the value resolved.
|
|
135
|
+
The adapter also restores dependency ordering from each resource's references. This matters when
|
|
136
|
+
Terraform has already resolved a reference to a plain string in the planned values.
|
|
97
137
|
|
|
98
138
|
## Supplying environment variables and role policies
|
|
99
139
|
|
|
100
|
-
Terraform
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
arrives without its statements.
|
|
140
|
+
Terraform marks a whole compound value as unknown when any part of it cannot be resolved. This can
|
|
141
|
+
remove every key from a Lambda `environment.variables` map. It can also remove every statement from
|
|
142
|
+
an IAM policy built with `jsonencode`.
|
|
104
143
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
simulated IAM evaluates authorization.
|
|
108
|
-
|
|
109
|
-
`overrides` supplies them, matched on the name the plan carries, the way a binding is matched on a
|
|
110
|
-
function name. An environment is matched on the function's name and an inline policy on the role's.
|
|
144
|
+
Use `overrides` to supply these values. Lambda environment overrides match the function name. IAM
|
|
145
|
+
policy overrides match the role name:
|
|
111
146
|
|
|
112
147
|
```typescript terraform-plan-overrides
|
|
113
148
|
/**
|
|
@@ -160,89 +195,36 @@ const { report } = await new TerraformAdapter(simAws).deployPlan({
|
|
|
160
195
|
console.log(report.lost);
|
|
161
196
|
```
|
|
162
197
|
|
|
163
|
-
An override fills a
|
|
164
|
-
variables are merged
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
import { TerraformAdapter } from "@kensio/yulin/terraform";
|
|
197
|
-
|
|
198
|
-
const simAws = new SimAws();
|
|
199
|
-
|
|
200
|
-
const { report } = await new TerraformAdapter(simAws).deployPlan(
|
|
201
|
-
"terraform/orders.tfplan.json",
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
// [ { address: 'aws_s3_bucket.uploads', type: 'aws_s3_bucket',
|
|
205
|
-
// cfnType: 'AWS::S3::Bucket', logicalId: 'AwsS3BucketUploads' } ]
|
|
206
|
-
console.log(report.mapped);
|
|
207
|
-
|
|
208
|
-
// [ { address: 'aws_route53_zone.public', type: 'aws_route53_zone',
|
|
209
|
-
// reason: 'no mapping for resource type' } ]
|
|
210
|
-
console.log(report.skipped);
|
|
211
|
-
|
|
212
|
-
// The aws_s3_bucket_versioning and friends that became bucket properties.
|
|
213
|
-
console.log(report.folded);
|
|
214
|
-
|
|
215
|
-
// Attributes a mapping could not carry and no override supplied, such as a
|
|
216
|
-
// Lambda's environment variables, which Terraform collapses whole when one of
|
|
217
|
-
// them is unknown.
|
|
218
|
-
console.log(report.lost);
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
`mapped`, `folded` and `skipped` add up to the plan's managed resource count. `lost` names the
|
|
222
|
-
attributes that did not survive the plan and that no override covered, per resource. A Terraform
|
|
223
|
-
value that names a resource of the same plan and sits inside something Terraform builds in one go,
|
|
224
|
-
such as a `jsonencode` document or a `for_each` map, is unknown in its entirety and its contents go
|
|
225
|
-
with it.
|
|
226
|
-
|
|
227
|
-
## What a reference cannot reach
|
|
228
|
-
|
|
229
|
-
A value a plan resolved arrives as a value. A value the plan could not resolve arrives as a
|
|
230
|
-
reference, and the import follows that reference to the resource that will produce it, through
|
|
231
|
-
module outputs, module variables, `each.value` and `each.key`. That covers what a community module
|
|
232
|
-
such as `terraform-aws-modules/apigateway-v2/aws` does with a `routes` map.
|
|
233
|
-
|
|
234
|
-
Two shapes stop it, and both are recorded as `unresolved required attribute` on `report.skipped`.
|
|
235
|
-
|
|
236
|
-
A plan records the references of a whole collection in one list, and the list says what the
|
|
237
|
-
collection was built from without saying which entry holds which. A `routes` map naming one function
|
|
238
|
-
is unambiguous. A `routes` map naming two functions leaves `each.value.uri` able to mean either, and
|
|
239
|
-
the import declines. Setting the value with a resource of its own, or with one module call per
|
|
240
|
-
function, gives each reference a collection to itself.
|
|
241
|
-
|
|
242
|
-
A value reaching a resource through a `local` is out of range whatever it holds. A plan carries the
|
|
243
|
-
locals' effects and none of their definitions, so there is nothing to follow.
|
|
244
|
-
|
|
245
|
-
## The scope of what it reads
|
|
246
|
-
|
|
247
|
-
One plan JSON file, already produced. Reading HCL, reading `terraform.tfstate`, and running
|
|
248
|
-
`terraform` as a subprocess are all outside it.
|
|
198
|
+
An override only fills a missing value. A value resolved by Terraform takes precedence. Environment
|
|
199
|
+
variables are merged by key.
|
|
200
|
+
|
|
201
|
+
If an IAM role's inline policy is missing, Yulin gives the role an allow-all policy so resources can
|
|
202
|
+
still be created. The report records `policy` under `lost`. Supplying a policy override removes that
|
|
203
|
+
fallback, and simulated IAM evaluates the supplied policy normally.
|
|
204
|
+
|
|
205
|
+
## Available functionality
|
|
206
|
+
|
|
207
|
+
- `TerraformAdapter` reads JSON produced by `terraform show -json` for a saved plan.
|
|
208
|
+
- `deployPlan` accepts a path or an object containing `planPath`, `stackName`, `bindings`, and
|
|
209
|
+
`overrides`.
|
|
210
|
+
- The adapter maps 24 Terraform resource types and folds 11 configuration resources into their
|
|
211
|
+
parent resources.
|
|
212
|
+
- Supported resource areas include API Gateway HTTP APIs, CloudWatch, Cognito, DynamoDB, ECR, IAM,
|
|
213
|
+
KMS, Lambda, S3, Secrets Manager, SNS, SQS, and SSM Parameter Store.
|
|
214
|
+
- Resources created with `count` and `for_each` are imported as separate instances.
|
|
215
|
+
- Resources in nested modules are imported.
|
|
216
|
+
- The adapter reports mapped, folded, skipped, and lost data for the plan.
|
|
217
|
+
|
|
218
|
+
## Limitations
|
|
219
|
+
|
|
220
|
+
- The adapter reads one existing plan JSON file. It does not read HCL or Terraform state, and it does
|
|
221
|
+
not run Terraform.
|
|
222
|
+
- Resources from non-AWS providers and Terraform types without a mapping are skipped and recorded in
|
|
223
|
+
`report.skipped`.
|
|
224
|
+
- The adapter cannot follow values through Terraform `local` declarations because a plan contains
|
|
225
|
+
their results but not their definitions.
|
|
226
|
+
- A collection reference can be ambiguous. For example, `each.value.uri` cannot be resolved when the
|
|
227
|
+
source collection contains several possible Lambda function references. The affected resource is
|
|
228
|
+
skipped with the reason `unresolved required attribute`.
|
|
229
|
+
- Some unknown compound values need an override because Terraform omits the whole value from the
|
|
230
|
+
plan.
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Test suite setup
|
|
2
|
+
|
|
3
|
+
Run one Yulin environment for an application's test suite. Create the simulation, deploy the
|
|
4
|
+
application's infrastructure, and install AWS SDK interception once. Every test then interacts with
|
|
5
|
+
the same simulated account and resources.
|
|
6
|
+
|
|
7
|
+
This is the recommended setup. It matches the way a suite uses a shared AWS account or a
|
|
8
|
+
container-based simulator such as LocalStack. Creating a new Yulin environment for every test or
|
|
9
|
+
test file is supported, but it should be reserved for cases that need a blank simulated account.
|
|
10
|
+
|
|
11
|
+
The simulated clock is the main exception. A suite-wide `SimAws` has one clock, so a test that moves
|
|
12
|
+
it changes time for every resource in that environment. Keep the majority of tests in the shared
|
|
13
|
+
environment without changing its clock. Put clock-controlling tests in a smaller isolated group.
|
|
14
|
+
|
|
15
|
+
## Split the Vitest suite
|
|
16
|
+
|
|
17
|
+
Yulin holds state in the process that created it. Vitest must run the Yulin tests in one worker for
|
|
18
|
+
all shared files to reach the same environment. Give that project disabled file parallelism and file
|
|
19
|
+
isolation, then load a setup module before each test file.
|
|
20
|
+
|
|
21
|
+
The second project below matches files ending in `.clock.test.ts`. Those tests do not load the shared
|
|
22
|
+
setup and can create isolated Yulin environments:
|
|
23
|
+
|
|
24
|
+
```typescript testing-vitest-config
|
|
25
|
+
import { defineConfig } from "vitest/config";
|
|
26
|
+
|
|
27
|
+
export default defineConfig({
|
|
28
|
+
test: {
|
|
29
|
+
environment: "node",
|
|
30
|
+
projects: [
|
|
31
|
+
{
|
|
32
|
+
extends: true,
|
|
33
|
+
test: {
|
|
34
|
+
name: "shared Yulin",
|
|
35
|
+
include: ["test/**/*.test.ts"],
|
|
36
|
+
exclude: ["test/**/*.clock.test.ts"],
|
|
37
|
+
fileParallelism: false,
|
|
38
|
+
isolate: false,
|
|
39
|
+
setupFiles: ["./test/setup-yulin.ts"],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
extends: true,
|
|
44
|
+
test: {
|
|
45
|
+
name: "isolated Yulin clock",
|
|
46
|
+
include: ["test/**/*.clock.test.ts"],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Vitest executes a `setupFiles` entry before every test file. With isolation disabled, modules
|
|
55
|
+
imported by that entry stay cached in the worker. Put the Yulin initialization in an imported module
|
|
56
|
+
to make it run once.
|
|
57
|
+
|
|
58
|
+
See Vitest's documentation for [`setupFiles`](https://vitest.dev/config/setupfiles),
|
|
59
|
+
[`fileParallelism`](https://vitest.dev/config/fileparallelism), and
|
|
60
|
+
[`isolate`](https://vitest.dev/config/isolate).
|
|
61
|
+
|
|
62
|
+
## Create and deploy the shared environment
|
|
63
|
+
|
|
64
|
+
Put the suite environment in a module such as `test/yulin-environment.ts`. Intercept client classes
|
|
65
|
+
used by the application and deploy its synthesized CDK cloud assembly:
|
|
66
|
+
|
|
67
|
+
```typescript testing-shared-yulin-environment
|
|
68
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
69
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
70
|
+
import type { SimAws } from "@kensio/yulin";
|
|
71
|
+
import { SimSdk } from "@kensio/yulin/sdk";
|
|
72
|
+
|
|
73
|
+
interface YulinTestEnvironment {
|
|
74
|
+
readonly simAws: SimAws;
|
|
75
|
+
readonly simSdk: SimSdk;
|
|
76
|
+
readonly uploadsBucketName: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type YulinTestGlobal = typeof globalThis & {
|
|
80
|
+
yulinEnvironment?: Promise<YulinTestEnvironment>;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const testGlobal = globalThis as YulinTestGlobal;
|
|
84
|
+
|
|
85
|
+
// oxlint-disable-next-line unicorn-js/prefer-top-level-await -- The shared promise prevents setup from running again before another test file.
|
|
86
|
+
export const yulin = await (testGlobal.yulinEnvironment ??= startYulin());
|
|
87
|
+
|
|
88
|
+
async function startYulin(): Promise<YulinTestEnvironment> {
|
|
89
|
+
const simSdk = new SimSdk();
|
|
90
|
+
simSdk.intercept(DynamoDBClient);
|
|
91
|
+
simSdk.intercept(S3Client);
|
|
92
|
+
|
|
93
|
+
const stacks = await simSdk.simAws.cloudFormation().deployCdkOut({
|
|
94
|
+
directoryPath: "cdk.out",
|
|
95
|
+
stackNames: ["ApplicationStack"],
|
|
96
|
+
});
|
|
97
|
+
const appStack = stacks.get("ApplicationStack");
|
|
98
|
+
|
|
99
|
+
if (appStack === undefined) {
|
|
100
|
+
throw new Error("ApplicationStack was not deployed");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
process.once("exit", () => {
|
|
104
|
+
simSdk.restoreAll();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
simAws: simSdk.simAws,
|
|
109
|
+
simSdk,
|
|
110
|
+
uploadsBucketName: appStack.output("UploadsBucketName"),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Use the same templates that the application deploys. `deployCdkOut(...)` can deploy the whole cloud
|
|
116
|
+
assembly or the named application Stacks. Read generated resource names from stack outputs or
|
|
117
|
+
resource accessors after deployment.
|
|
118
|
+
|
|
119
|
+
The configured setup entry only needs to import that module:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// test/setup-yulin.ts
|
|
123
|
+
import "./yulin-environment.js";
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Do not put the initialization directly in `setup-yulin.ts`. Vitest executes that file for every test
|
|
127
|
+
file, even when isolation is disabled.
|
|
128
|
+
|
|
129
|
+
## Use the environment from every test
|
|
130
|
+
|
|
131
|
+
Application code continues to construct and send through ordinary AWS SDK clients. Class-level
|
|
132
|
+
interception routes all of them to the suite's Yulin environment.
|
|
133
|
+
|
|
134
|
+
A test that needs direct access can import the shared environment:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
import { randomUUID } from "node:crypto";
|
|
138
|
+
|
|
139
|
+
import {
|
|
140
|
+
GetObjectCommand,
|
|
141
|
+
PutObjectCommand,
|
|
142
|
+
S3Client,
|
|
143
|
+
} from "@aws-sdk/client-s3";
|
|
144
|
+
import { expect, it } from "vitest";
|
|
145
|
+
|
|
146
|
+
import { yulin } from "../yulin-environment.js";
|
|
147
|
+
|
|
148
|
+
it("stores an upload", async () => {
|
|
149
|
+
const key = `test-uploads/${randomUUID()}.txt`;
|
|
150
|
+
const s3 = new S3Client({ region: "eu-west-2" });
|
|
151
|
+
|
|
152
|
+
await s3.send(
|
|
153
|
+
new PutObjectCommand({
|
|
154
|
+
Bucket: yulin.uploadsBucketName,
|
|
155
|
+
Key: key,
|
|
156
|
+
Body: "an upload",
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const stored = await yulin.simAws
|
|
161
|
+
.region("eu-west-2")
|
|
162
|
+
.s3()
|
|
163
|
+
.getObject(
|
|
164
|
+
new GetObjectCommand({
|
|
165
|
+
Bucket: yulin.uploadsBucketName,
|
|
166
|
+
Key: key,
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
expect(await stored?.Body?.transformToString()).toBe("an upload");
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The `SimAws` object is mainly useful for preparing input state and reading state back in assertions.
|
|
175
|
+
Exercise the application through its normal interfaces whenever possible.
|
|
176
|
+
|
|
177
|
+
## Keep tests independent in shared state
|
|
178
|
+
|
|
179
|
+
Shared infrastructure does not require tests to depend on one another. Give each test's records,
|
|
180
|
+
object keys, user names, and other mutable data unique values. Read CloudFormation-generated names
|
|
181
|
+
from the deployed stack. Avoid assertions that assume the simulated account contains no other data.
|
|
182
|
+
|
|
183
|
+
Keep `beforeEach` for the records a test needs. A per-file `beforeAll` can prepare data used by every
|
|
184
|
+
test in that file. Leave the suite's stacks and SDK interception in place until the worker exits.
|
|
185
|
+
|
|
186
|
+
Tests run sequentially with `fileParallelism: false`. If a test uses `it.concurrent`, its data still
|
|
187
|
+
needs unique identifiers because those cases share the same environment at the same time.
|
|
188
|
+
|
|
189
|
+
## Give clock-controlling tests their own environment
|
|
190
|
+
|
|
191
|
+
Every service in a `SimAws` reads the same simulated clock. Calling `advanceBy(...)` can expire
|
|
192
|
+
credentials, delete resources whose retention period has passed, and run scheduled work anywhere in
|
|
193
|
+
the environment. Resetting the clock afterwards cannot reverse those changes.
|
|
194
|
+
|
|
195
|
+
Tests in the shared project should treat the clock as read-only. Put a test that calls `freeze()`,
|
|
196
|
+
`setTo(...)`, `advanceBy(...)`, or `resume()` in a `.clock.test.ts` file and create a fresh environment
|
|
197
|
+
inside the test:
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import { SimAws, SimFixedClock } from "@kensio/yulin";
|
|
201
|
+
import { it } from "vitest";
|
|
202
|
+
|
|
203
|
+
it("expires a session", async () => {
|
|
204
|
+
const simAws = new SimAws({
|
|
205
|
+
clock: new SimFixedClock(new Date("2026-09-04T09:00:00.000Z")),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Deploy only the infrastructure this clock-controlling test needs.
|
|
209
|
+
|
|
210
|
+
await simAws.clock().advanceBy({ minutes: 20 });
|
|
211
|
+
|
|
212
|
+
// Assert the behaviour after the time change.
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Create a `SimSdk` around that `SimAws` when application code uses SDK clients. Restore its
|
|
217
|
+
interceptions at the end of the test. The [simulated time guide](https://yulinsim.dev/time/) describes
|
|
218
|
+
what moving the clock runs and changes.
|
|
219
|
+
|
|
220
|
+
## When to create another environment
|
|
221
|
+
|
|
222
|
+
A fresh `SimAws` or `SimSdk` is useful when the empty environment is part of the behaviour under
|
|
223
|
+
test, or when the test needs to control simulated time. Yulin's own service unit tests are another
|
|
224
|
+
example because they test resource creation and account isolation directly.
|
|
225
|
+
|
|
226
|
+
Vitest workers cannot share an in-memory `SimAws`. A suite that keeps file parallelism creates one
|
|
227
|
+
environment per worker. Put Yulin-based application tests in a Vitest project with
|
|
228
|
+
`fileParallelism: false` when the rest of the unit suite should remain parallel.
|