@kensio/yulin 1.21.1 → 1.21.3

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 (50) hide show
  1. package/dist/service/cloudformation/resource/factory/sim-cfn-resource-factory.type.d.ts +9 -0
  2. package/dist/service/cloudformation/resource/sim-cfn-resource.type.d.ts +11 -0
  3. package/dist/service/cloudformation/resource/update/sim-cfn-resource-update-validator.d.ts +26 -0
  4. package/dist/service/cloudformation/resource/update/sim-cfn-resource-update-validator.js +55 -0
  5. package/dist/service/cloudformation/stack/sim-cfn-stack-resource-operations.d.ts +5 -0
  6. package/dist/service/cloudformation/stack/sim-cfn-stack-resource-operations.js +17 -0
  7. package/dist/service/cloudformation/stack/update/sim-cfn-stack-update-plan.d.ts +7 -1
  8. package/dist/service/cloudformation/stack/update/sim-cfn-stack-update-plan.js +13 -0
  9. package/dist/service/cloudformation/stack/update/sim-cfn-stack-updater.js +1 -0
  10. package/dist/service/dynamodb/cfn/sim-cfn-dynamodb-resource-factory.d.ts +5 -0
  11. package/dist/service/dynamodb/cfn/sim-cfn-dynamodb-resource-factory.js +15 -0
  12. package/dist/service/dynamodb/cfn/table/sim-cfn-dynamodb-table-update-validator.d.ts +24 -0
  13. package/dist/service/dynamodb/cfn/table/sim-cfn-dynamodb-table-update-validator.js +38 -0
  14. package/dist/service/dynamodb/table/sim-dynamodb-index-update.js +1 -2
  15. package/dist/service/ses/command/configuration-set/sim-ses-configuration-set-commands.js +13 -5
  16. package/dist/service/ses/command/configuration-set/sim-ses-configuration-set-options.d.ts +1 -1
  17. package/dist/service/ses/command/configuration-set/sim-ses-configuration-set-options.js +9 -9
  18. package/dist/service/ses/command/send/send.command.d.ts +10 -1
  19. package/dist/service/ses/command/send/sim-ses-read-content.d.ts +2 -1
  20. package/dist/service/ses/command/send/sim-ses-send-email.js +1 -0
  21. package/dist/service/ses/command/send/sim-ses-service-send.js +1 -0
  22. package/dist/service/ses/command/send/sim-ses-simple-content.js +26 -5
  23. package/dist/service/ses/command/send/sim-ses-template-send.js +1 -0
  24. package/dist/service/ses/command/suppression/suppression.command.d.ts +3 -2
  25. package/dist/service/ses/configuration-set/sim-ses-configuration-set.d.ts +9 -10
  26. package/dist/service/ses/configuration-set/sim-ses-configuration-set.js +7 -8
  27. package/dist/service/ses/email/sim-ses-sent-email-store.d.ts +8 -0
  28. package/dist/service/ses/email/sim-ses-sent-email-store.js +17 -0
  29. package/dist/service/ses/email/sim-ses-sent-email.d.ts +18 -0
  30. package/dist/service/ses/email/sim-ses-sent-email.js +3 -0
  31. package/dist/service/ses/error/sim-ses.error.d.ts +10 -0
  32. package/dist/service/ses/error/sim-ses.error.js +10 -0
  33. package/dist/service/ses/feedback/sim-ses-feedback-recorder.d.ts +34 -0
  34. package/dist/service/ses/feedback/sim-ses-feedback-recorder.js +55 -0
  35. package/dist/service/ses/index.d.ts +3 -2
  36. package/dist/service/ses/index.js +2 -1
  37. package/dist/service/ses/sim-ses-commands.d.ts +2 -0
  38. package/dist/service/ses/sim-ses-commands.js +9 -0
  39. package/dist/service/ses/sim-ses-configuration-sets.d.ts +4 -4
  40. package/dist/service/ses/sim-ses-configuration-sets.js +4 -4
  41. package/dist/service/ses/sim-ses-suppression.d.ts +3 -2
  42. package/dist/service/ses/sim-ses-suppression.js +3 -2
  43. package/dist/service/ses/sim-ses-v2.d.ts +13 -0
  44. package/dist/service/ses/sim-ses-v2.js +13 -0
  45. package/dist/service/ses/suppression/sim-ses-suppressed-destination.d.ts +3 -4
  46. package/dist/service/ses/suppression/sim-ses-suppressed-destination.js +3 -4
  47. package/dist/service/ses/suppression/sim-ses-suppression-list.d.ts +4 -4
  48. package/dist/service/ses/suppression/sim-ses-suppression-list.js +4 -4
  49. package/docs/services/ses/README.md +164 -41
  50. package/package.json +2 -2
@@ -14,7 +14,7 @@ SES specific types are imported from the `@kensio/yulin/ses` subpath.
14
14
  ## Asserting on a message that was sent
15
15
 
16
16
  `sentEmails()` hands over the record. Each message carries who it was from, the three recipient
17
- lists, the subject, the body and the message id SES answered with.
17
+ lists, the subject, the body, its attachments and the message id SES answered with.
18
18
 
19
19
  ```typescript sim-ses-send-and-assert
20
20
  /**
@@ -59,6 +59,60 @@ read. The three recipient lists stay apart, leaving a test free to assert that a
59
59
  `body` keeps `text` and `html` apart too. A message sent with only an HTML body reports `undefined`
60
60
  for its text, and never the markup.
61
61
 
62
+ ### Asserting on an attachment
63
+
64
+ A simple message may carry structured attachments. The record keeps them in request order.
65
+ `rawContent` contains a copy of the bytes from the request, and the other fields contain the supplied
66
+ SES attachment metadata.
67
+
68
+ ```typescript sim-ses-attachments
69
+ /**
70
+ * Sending a generated CSV and reading it from the simulated SES record.
71
+ */
72
+
73
+ import { SendEmailCommand } from "@aws-sdk/client-sesv2";
74
+
75
+ import { SimAws } from "@kensio/yulin";
76
+
77
+ const simAws = new SimAws();
78
+ const ses = simAws.sesV2();
79
+
80
+ ses.verifyIdentity("hello@example.com");
81
+ ses.verifyIdentity("someone@example.org");
82
+
83
+ const csv = new TextEncoder().encode("word,meaning\n你好,hello\n");
84
+
85
+ await ses.sendEmail(
86
+ new SendEmailCommand({
87
+ FromEmailAddress: "hello@example.com",
88
+ Destination: { ToAddresses: ["someone@example.org"] },
89
+ Content: {
90
+ Simple: {
91
+ Subject: { Data: "Your vocabulary backup" },
92
+ Body: { Text: { Data: "Your backup is attached." } },
93
+ Attachments: [
94
+ {
95
+ RawContent: csv,
96
+ FileName: "vocabulary.csv",
97
+ ContentType: "text/csv; charset=utf-8",
98
+ ContentDisposition: "ATTACHMENT",
99
+ },
100
+ ],
101
+ },
102
+ },
103
+ }),
104
+ );
105
+
106
+ const [email] = ses.sentEmails();
107
+ const [attachment] = email?.attachments ?? [];
108
+
109
+ // vocabulary.csv "word,meaning\n你好,hello\n"
110
+ console.log(
111
+ attachment?.fileName,
112
+ new TextDecoder().decode(attachment?.rawContent),
113
+ );
114
+ ```
115
+
62
116
  ## Verifying identities
63
117
 
64
118
  Real SES verifies an email address by emailing it a link and a domain by looking for DNS records.
@@ -303,8 +357,9 @@ sending switch, the delivery options and the reputation switch are all declared
303
357
  SES holds a set as state. A test can then assert what a stack declared, with no AWS account to read
304
358
  it back from.
305
359
 
306
- A set is attached to an identity, or named on a send. `SendingEnabled` is the one option a send acts
307
- on. The rest are held for a test to read back.
360
+ A set is attached to an identity, or named on a send. `SendingEnabled` acts during acceptance.
361
+ Suppression options decide whether explicit bounce or complaint feedback adds the recipient to the
362
+ account suppression list. The rest are held for a test to read back.
308
363
 
309
364
  ```typescript sim-ses-configuration-sets
310
365
  /**
@@ -350,8 +405,13 @@ console.log(ses.allConfigurationSets().map((set) => set.configurationSetName));
350
405
  ```
351
406
 
352
407
  A set declaring only its name gets the defaults real SES applies. Sending is on, TLS is optional,
353
- reputation metrics are off and no reason is suppressed. `GetConfigurationSet` reports those defaults
354
- back rather than leaving the groups out of the answer.
408
+ reputation metrics are off and suppression falls back to the account reasons.
409
+ `GetConfigurationSet` reports the sending, delivery and reputation defaults. It leaves suppression
410
+ options absent where the set has no override.
411
+
412
+ An explicit empty override is different. `SuppressionOptions: { SuppressedReasons: [] }` disables
413
+ suppression for feedback on messages sent through that set. Simulated SES retains the empty list so
414
+ it does not fall back to the account reasons.
355
415
 
356
416
  `findConfigurationSet` reaches one set by name and `allConfigurationSets` hands over every set in
357
417
  the scope, oldest first.
@@ -436,9 +496,9 @@ set at a time.
436
496
  domain and the events that report a click. The Virtual Deliverability Manager reports on engagement,
437
497
  which this simulation never measures.
438
498
 
439
- A set's suppression reasons are held and read back, and that is all they do. On real SES they decide
440
- what a bounce or a complaint would add to the account suppression list. Both are absent here. The
441
- list itself is a separate thing, filled by hand.
499
+ A set's suppression reasons override the account reasons when `recordFeedback` records a hard bounce
500
+ or complaint for a message sent through it. A set with no suppression options falls back to the
501
+ account reasons. The feedback entry goes onto the account suppression list.
442
502
 
443
503
  `TlsPolicy` accepts `REQUIRE` and `OPTIONAL` and refuses anything else, and a `SuppressedReasons`
444
504
  entry that is neither `BOUNCE` nor `COMPLAINT` is refused too. `MaxDeliverySeconds` takes whole
@@ -644,9 +704,9 @@ review is beyond what a test can assert on anyway.
644
704
  ## The suppression list
645
705
 
646
706
  Real SES holds an account-level suppression list and fills it from hard bounces and complaints.
647
- Nothing bounces here, so every address on this one was put there by a caller. That is what makes it
648
- worth having in a test. The support tool that lists suppressed addresses, the form that removes one
649
- and the script that seeds the list all have somewhere to run.
707
+ Tests supply that feedback explicitly with `recordFeedback`. Suppression commands manage the same
708
+ list. The support tool that lists suppressed addresses, the form that removes one and the script
709
+ that seeds the list all have somewhere to run.
650
710
 
651
711
  `PutSuppressedDestination`, `GetSuppressedDestination`, `ListSuppressedDestinations` and
652
712
  `DeleteSuppressedDestination` manage it.
@@ -705,6 +765,70 @@ carries the answer. `suppressedRecipients` names who was held back and why,
705
765
  and `isFullySuppressed` is the narrower question of whether the message reached nobody. A message to
706
766
  two recipients with one of them suppressed went to the other.
707
767
 
768
+ ### Recording a hard bounce or complaint
769
+
770
+ `recordFeedback` takes the message id of an accepted message, one of its recipient addresses and a
771
+ `BOUNCE` or `COMPLAINT` reason. Active feedback adds or updates the account suppression entry at the
772
+ current simulated time. The existing suppression commands and `suppressedDestinations()` read the
773
+ result.
774
+
775
+ ```typescript sim-ses-feedback-suppression
776
+ /**
777
+ * Recording a hard bounce and observing the next send being suppressed.
778
+ */
779
+
780
+ import { SendEmailCommand } from "@aws-sdk/client-sesv2";
781
+
782
+ import { SimAws, SimFixedClock } from "@kensio/yulin";
783
+
784
+ const simAws = new SimAws({
785
+ clock: new SimFixedClock(new Date("2026-08-31T09:00:00.000Z")),
786
+ });
787
+ const ses = simAws.sesV2();
788
+
789
+ ses.verifyIdentity("hello@example.com");
790
+ ses.verifyIdentity("someone@example.org");
791
+
792
+ const message = new SendEmailCommand({
793
+ FromEmailAddress: "hello@example.com",
794
+ Destination: { ToAddresses: ["someone@example.org"] },
795
+ Content: {
796
+ Simple: {
797
+ Subject: { Data: "Welcome" },
798
+ Body: { Text: { Data: "Hi there" } },
799
+ },
800
+ },
801
+ });
802
+
803
+ const accepted = await ses.sendEmail(message);
804
+
805
+ ses.recordFeedback({
806
+ messageId: accepted.MessageId!,
807
+ emailAddress: "someone@example.org",
808
+ reason: "BOUNCE",
809
+ });
810
+
811
+ await ses.sendEmail(message);
812
+
813
+ const suppressed = ses.suppressedDestinations()[0];
814
+ const later = ses.sentEmails()[1];
815
+
816
+ // "BOUNCE" "2026-08-31T09:00:00.000Z" true
817
+ console.log(
818
+ suppressed?.reason,
819
+ suppressed?.lastUpdateTime.toISOString(),
820
+ later?.isFullySuppressed,
821
+ );
822
+ ```
823
+
824
+ The operation refuses a message id this SES scope did not issue and an address that was not a
825
+ recipient of that message. Recipient matching ignores case and a display name, while the suppression
826
+ entry keeps the address spelling from the accepted message.
827
+
828
+ Without a configuration-set override, the account's `SuppressedReasons` decide whether feedback is
829
+ active. A configuration set with suppression options replaces those reasons for messages sent
830
+ through it. An empty override leaves the suppression list unchanged.
831
+
708
832
  `ListSuppressedDestinations` pages with `PageSize` and `NextToken`, and narrows with `Reasons`,
709
833
  `StartDate` and `EndDate`. Removing an address that was never on the list succeeds, so a form that
710
834
  removes one twice has no failure to handle.
@@ -938,29 +1062,29 @@ time forward past the window sees the count fall the way an account's would.
938
1062
 
939
1063
  ## Simulated commands
940
1064
 
941
- | Command | Notes |
942
- | --------------------------------- | --------------------------------------------------------------------------------------------------------- |
943
- | `SendEmail` | `Content.Simple` and `Content.Template`. Recorded rather than delivered. |
944
- | `CreateEmailIdentity` | Starts unverified. `Tags`, `DkimSigningAttributes` and `ConfigurationSetName` are held and reported back. |
945
- | `GetEmailIdentity` | Reports the DKIM, MAIL FROM, feedback, configuration set and tag settings the identity holds. |
946
- | `ListEmailIdentities` | Paged with `PageSize` and `NextToken`. |
947
- | `DeleteEmailIdentity` | |
948
- | `CreateEmailTemplate` | Substitution only. `Tags` are refused. |
949
- | `GetEmailTemplate` | Reports the wording with its placeholders unrendered. |
950
- | `UpdateEmailTemplate` | Replaces the wording outright, keeping the creation time. |
951
- | `ListEmailTemplates` | Names and creation times only, paged. |
952
- | `DeleteEmailTemplate` | |
953
- | `CreateConfigurationSet` | `TrackingOptions`, `VdmOptions` and `Tags` are refused. |
954
- | `GetConfigurationSet` | Reports the defaults it applied as well as what was declared. |
955
- | `ListConfigurationSets` | Names only, paged with `PageSize` and `NextToken`. |
956
- | `DeleteConfigurationSet` | |
957
- | `GetAccount` | Reports `SuppressionAttributes` alongside the quota. |
958
- | `PutAccountDetails` | `MailType` and `WebsiteURL` are required, as on real SES. |
959
- | `PutAccountSuppressionAttributes` | No reasons at all turns the suppression list off. |
960
- | `PutSuppressedDestination` | Accepted in the sandbox, which real SES refuses. |
961
- | `GetSuppressedDestination` | |
962
- | `ListSuppressedDestinations` | Paged, and narrowed by `Reasons`, `StartDate` and `EndDate`. |
963
- | `DeleteSuppressedDestination` | Removing an address that is not on the list succeeds. |
1065
+ | Command | Notes |
1066
+ | --------------------------------- | ----------------------------------------------------------------------------------------------------------- |
1067
+ | `SendEmail` | `Content.Simple`, including structured attachments, and `Content.Template`. Recorded rather than delivered. |
1068
+ | `CreateEmailIdentity` | Starts unverified. `Tags`, `DkimSigningAttributes` and `ConfigurationSetName` are held and reported back. |
1069
+ | `GetEmailIdentity` | Reports the DKIM, MAIL FROM, feedback, configuration set and tag settings the identity holds. |
1070
+ | `ListEmailIdentities` | Paged with `PageSize` and `NextToken`. |
1071
+ | `DeleteEmailIdentity` | |
1072
+ | `CreateEmailTemplate` | Substitution only. `Tags` are refused. |
1073
+ | `GetEmailTemplate` | Reports the wording with its placeholders unrendered. |
1074
+ | `UpdateEmailTemplate` | Replaces the wording outright, keeping the creation time. |
1075
+ | `ListEmailTemplates` | Names and creation times only, paged. |
1076
+ | `DeleteEmailTemplate` | |
1077
+ | `CreateConfigurationSet` | `TrackingOptions`, `VdmOptions` and `Tags` are refused. |
1078
+ | `GetConfigurationSet` | Reports applied defaults and preserves whether suppression options were absent. |
1079
+ | `ListConfigurationSets` | Names only, paged with `PageSize` and `NextToken`. |
1080
+ | `DeleteConfigurationSet` | |
1081
+ | `GetAccount` | Reports `SuppressionAttributes` alongside the quota. |
1082
+ | `PutAccountDetails` | `MailType` and `WebsiteURL` are required, as on real SES. |
1083
+ | `PutAccountSuppressionAttributes` | No reasons at all turns the suppression list off. |
1084
+ | `PutSuppressedDestination` | Accepted in the sandbox, which real SES refuses. |
1085
+ | `GetSuppressedDestination` | |
1086
+ | `ListSuppressedDestinations` | Paged, and narrowed by `Reasons`, `StartDate` and `EndDate`. |
1087
+ | `DeleteSuppressedDestination` | Removing an address that is not on the list succeeds. |
964
1088
 
965
1089
  Anything else refuses on send with `SimSdkUnsupportedCommandError`.
966
1090
 
@@ -978,19 +1102,18 @@ Anything else refuses on send with `SimSdkUnsupportedCommandError`.
978
1102
  the template. Template data holding an object where the template wants a value is refused too,
979
1103
  where real Handlebars would render `[object Object]`.
980
1104
  - **`SendBulkEmail` is absent**, along with its per-recipient replacement data.
981
- - **Nothing is delivered, and nothing bounces.** There are no bounce or complaint events and no
982
- event destinations.
1105
+ - **Nothing is delivered.** A test supplies a hard bounce or complaint through `recordFeedback`.
1106
+ Feedback is never generated automatically, and there are no event destinations.
983
1107
  - **A configuration set acts on one send.** `SendingEnabled` refuses a send made through the set.
984
- The suppression reasons, delivery options and reputation switch are held and read back, and
985
- nothing acts on those.
1108
+ Suppression reasons act on explicit feedback. Delivery options and the reputation switch are held
1109
+ and read back.
986
1110
  - **A set name nothing created is still accepted.** Real SES refuses one on an identity and on a
987
1111
  send. Both stand here and the name is recorded, because a test failing over a set missing from a
988
1112
  local setup fails for a reason unrelated to what it asserts.
989
1113
  - **A configuration set holds what it was created with.** The `Put` commands that change one group
990
1114
  of options are absent. A set cannot be changed once it exists.
991
- - **The suppression list fills only by hand.** Every address on it was put there by a caller,
992
- because no message here ever bounces. A configuration set's `SuppressedReasons` name what a bounce
993
- or a complaint would suppress for, and both are absent.
1115
+ - **Feedback is explicit.** Hard bounces and complaints update the suppression list only when a test
1116
+ calls `recordFeedback`. Soft bounces are absent.
994
1117
  - **Tenant-level suppression lists are left out.** A suppression command carrying `TenantName` is
995
1118
  refused rather than answered from the account-level list.
996
1119
  - **`PutSuppressedDestination` works in the sandbox.** Real SES refuses it until an account has
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kensio/yulin",
3
- "version": "1.21.1",
3
+ "version": "1.21.3",
4
4
  "description": "AWS system behaviour simulation for isolated unit testing",
5
5
  "repository": "https://github.com/KensioSoftware/yulin",
6
6
  "homepage": "https://yulinsim.dev/",
@@ -297,7 +297,7 @@
297
297
  "execa": "^10.0.0",
298
298
  "fta-cli": "^3.0.0",
299
299
  "node-sql-parser": "^5.4.0",
300
- "oxfmt": "^0.64.0",
300
+ "oxfmt": "^0.65.0",
301
301
  "oxlint": "^1.77.0",
302
302
  "oxlint-tsgolint": "^7.0.2001",
303
303
  "semantic-release": "25.0.9",