@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.
Files changed (74) hide show
  1. package/README.md +22 -2
  2. package/dist/service/eventbridge/cfn/bus/sim-cfn-event-bus-creator.js +1 -0
  3. package/dist/service/eventbridge/cfn/bus/sim-cfn-event-bus-properties.d.ts +4 -0
  4. package/dist/service/eventbridge/cfn/bus/sim-cfn-event-bus-properties.js +26 -1
  5. package/dist/service/eventbridge/cfn/rule/sim-cfn-event-rule-creator.js +3 -1
  6. package/dist/service/eventbridge/cfn/rule/sim-cfn-event-rule-properties.d.ts +0 -7
  7. package/dist/service/eventbridge/cfn/rule/sim-cfn-event-rule-properties.js +0 -20
  8. package/dist/service/eventbridge/cfn/rule/sim-cfn-event-rule-unsimulated-properties.d.ts +13 -0
  9. package/dist/service/eventbridge/cfn/rule/sim-cfn-event-rule-unsimulated-properties.js +59 -0
  10. package/dist/service/eventbridge/cfn/sim-cfn-event-bridge-resource-error.d.ts +1 -1
  11. package/dist/service/eventbridge/cfn/sim-cfn-event-bridge-resource-error.js +1 -1
  12. package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-properties.js +1 -1
  13. package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-property-rules.d.ts +3 -2
  14. package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-property-rules.js +25 -3
  15. package/dist/service/ses/command/authorize/sim-ses-authorizer.d.ts +9 -0
  16. package/dist/service/ses/command/authorize/sim-ses-authorizer.js +18 -2
  17. package/dist/service/ses/command/send/sim-ses-send-email.js +2 -1
  18. package/dist/service/sns/cfn/topic/sim-cfn-sns-topic-creator.js +1 -0
  19. package/dist/service/sns/cfn/topic/sim-cfn-sns-topic-properties.d.ts +9 -4
  20. package/dist/service/sns/cfn/topic/sim-cfn-sns-topic-properties.js +18 -6
  21. package/dist/service/sns/cfn/topic/sim-cfn-sns-topic-property-names.d.ts +10 -0
  22. package/dist/service/sns/cfn/topic/sim-cfn-sns-topic-property-names.js +16 -1
  23. package/dist/service/ssm/cfn/parameter/sim-cfn-ssm-parameter-creator.js +1 -1
  24. package/dist/service/ssm/cfn/parameter/sim-cfn-ssm-parameter-properties.d.ts +11 -6
  25. package/dist/service/ssm/cfn/parameter/sim-cfn-ssm-parameter-properties.js +18 -9
  26. package/docs/README.md +40 -4
  27. package/docs/ai-skill/README.md +57 -54
  28. package/docs/cli/README.md +84 -94
  29. package/docs/factories/README.md +42 -54
  30. package/docs/lint/README.md +41 -67
  31. package/docs/non-aws-dependencies/README.md +72 -168
  32. package/docs/sdk/README.md +109 -95
  33. package/docs/serve/README.md +192 -898
  34. package/docs/services/acm/README.md +24 -40
  35. package/docs/services/apigateway/README.md +52 -71
  36. package/docs/services/apigatewayv2/README.md +55 -74
  37. package/docs/services/athena/README.md +17 -26
  38. package/docs/services/backup/README.md +29 -39
  39. package/docs/services/bedrock/README.md +38 -52
  40. package/docs/services/cloudformation/README.md +43 -55
  41. package/docs/services/cloudfront/README.md +69 -95
  42. package/docs/services/cloudwatch/README.md +40 -54
  43. package/docs/services/cognito/README.md +30 -45
  44. package/docs/services/dynamodb/README.md +34 -51
  45. package/docs/services/ecr/README.md +36 -77
  46. package/docs/services/ecs/README.md +26 -46
  47. package/docs/services/elbv2/README.md +19 -29
  48. package/docs/services/eventbridge/README.md +24 -20
  49. package/docs/services/firehose/README.md +24 -32
  50. package/docs/services/glue/README.md +41 -78
  51. package/docs/services/iam/README.md +13 -15
  52. package/docs/services/kinesis/README.md +53 -93
  53. package/docs/services/kms/README.md +22 -32
  54. package/docs/services/lambda/README.md +60 -80
  55. package/docs/services/logs/README.md +41 -50
  56. package/docs/services/organizations/README.md +50 -85
  57. package/docs/services/personalize/README.md +28 -44
  58. package/docs/services/rekognition/README.md +26 -38
  59. package/docs/services/route53/README.md +17 -17
  60. package/docs/services/s3/README.md +47 -51
  61. package/docs/services/scheduler/README.md +41 -52
  62. package/docs/services/secretsmanager/README.md +27 -42
  63. package/docs/services/ses/README.md +24 -34
  64. package/docs/services/sns/README.md +31 -33
  65. package/docs/services/sqs/README.md +14 -14
  66. package/docs/services/ssm/README.md +19 -21
  67. package/docs/services/stepfunctions/README.md +18 -20
  68. package/docs/services/sts/README.md +32 -45
  69. package/docs/services/wafv2/README.md +12 -17
  70. package/docs/terraform/README.md +108 -126
  71. package/docs/testing/README.md +228 -0
  72. package/docs/time/README.md +87 -117
  73. package/llms.txt +2 -1
  74. package/package.json +1 -1
@@ -1,16 +1,11 @@
1
1
  # Simulated WAFv2
2
2
 
3
- Yulin includes a simulated AWS WAFv2 for tests and local development. It holds web ACLs, IP sets and
4
- regex pattern sets, and it evaluates a request against a web ACL's rules to reach a decision. A test
5
- can assert that a request to `/admin` is blocked and one to `/` is allowed, without an AWS account
6
- and without a distribution in front of anything.
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
- A web ACL can also go in front of what serves the requests. A simulated API Gateway REST API stage
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 in ascending `Priority` and not in the order the list was written. The first
95
- rule that matches and carries a terminating action (`Allow` or `Block`) decides the request.
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 lets the next rule have a look. That is how a rule is staged
98
- before it is turned on, and `countedRuleNames` is what a test asserts against.
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 reads one part of the request, applies the rule's text transformations to it, and tests
158
- what comes out.
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
- ## Simulated commands
1369
+ ## Supported operations
1375
1370
 
1376
1371
  `CreateWebACL`, `GetWebACL`, `UpdateWebACL`, `ListWebACLs`, `DeleteWebACL`, `CreateIPSet`,
1377
1372
  `GetIPSet`, `UpdateIPSet`, `ListIPSets`, `DeleteIPSet`, `CreateRegexPatternSet`,
@@ -1,27 +1,29 @@
1
- # Terraform
1
+ # Deploy Terraform plans
2
2
 
3
- A team whose infrastructure is written in Terraform can deploy it into simulated AWS without
4
- hand-writing a second CloudFormation template describing the same infrastructure. `TerraformAdapter`
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
- ## Deploying a plan
10
+ ## Create the plan JSON
13
11
 
14
- Write the JSON first. `terraform plan` produces the saved plan in Terraform's own binary format, and
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
- A plan carries no code for its functions. It points at a zip on disk, an S3 object or a container
23
- image, and none of the three is a handler Yulin can run. A binding matched on the function name the
24
- plan declares is where the behaviour comes from.
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 Stack is named after the plan file when the deployment does not name one, so
54
- `orders.tfplan.json` deploys as `orders`. Pass `stackName` to name it yourself, and pass the path on
55
- its own where nothing else needs saying.
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
- ## What comes back
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
- `deployPlan` answers with the Stack and a report of what reading the plan made of it.
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 Stack is an ordinary simulated CloudFormation Stack, and everything the [CloudFormation
79
- docs](https://yulinsim.dev/services/cloudformation/ "Simulated CloudFormation usage docs") describe applies to it.
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
- ## What the adapter reads
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 resource of its own. Resources declared inside a module are reached through the
87
- module path they were declared under, however many modules deep.
129
+ instance becomes a separate simulated resource. The adapter also reads resources from nested
130
+ modules.
88
131
 
89
- An attribute Terraform could not resolve at plan time is absent from the plan's values. The plan
90
- keeps the reference behind it, and that reference becomes the link a hand-written template would
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
- Ordering is rebuilt. Terraform resolves what it can before writing the plan, so a Lambda permission
94
- naming a function the same plan creates carries the function's name as a plain string. The value is
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 resolves nothing inside a value it could not build. A Lambda `environment.variables` map
101
- holding one reference to a queue of the same plan arrives unknown in its entirety, and the variable
102
- names go with it. An `aws_iam_role_policy` written with `jsonencode` around an ARN of the same plan
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
- Those two cost more than their count suggests (four attributes out of 154 on a hand-written
106
- application configuration). A handler reads its configuration out of environment variables, and
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 gap. Where Terraform resolved the value, the plan wins, and environment
164
- variables are merged one variable at a time. A configuration that stops collapsing a value stops
165
- needing the override written for it.
166
-
167
- A role whose policy no override supplies is created allowing everything, and `policy` is named on
168
- the report's `lost`. Simulated IAM evaluates authorization, and a role holding no policy would deny
169
- what the configuration allowed and fail the resources using it (an event source mapping is refused
170
- outright when its execution role cannot poll the queue). Supplying the policy takes that default
171
- off. The document is evaluated as it stands, and one omitting `sqs:ReceiveMessage` fails the mapping
172
- the way AWS fails it.
173
-
174
- ## What it maps
175
-
176
- The adapter maps 24 Terraform resource types and folds 11 more into the resource they configure. The
177
- set covers API Gateway, CloudWatch (log groups, metric alarms and EventBridge rules), Cognito,
178
- DynamoDB, ECR, IAM, KMS, Lambda, S3, Secrets Manager, SNS, SQS and SSM Parameter Store.
179
-
180
- A hand-written application configuration of 46 resources deploys whole. A configuration built out of
181
- published `terraform-aws-modules` modules reaches 21 of its 25. Of the four it leaves, the Lambda
182
- module uses `null_resource` and `local_file` to package a zip, and an integration and a route read
183
- their values through a `for_each` hop the adapter steps over.
184
-
185
- ## What the report says
186
-
187
- A type with no mapping, and a resource from a provider other than AWS, are recorded and stepped over
188
- rather than failing the deployment.
189
-
190
- ```typescript terraform-plan-report
191
- /**
192
- * Reading what a Terraform plan import made of the plan.
193
- */
194
-
195
- import { SimAws } from "@kensio/yulin";
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.