@yrpri/api 9.0.220 → 9.0.222

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 (55) hide show
  1. package/agents/managers/subscriptionManager.js +2 -2
  2. package/app.js +11 -8
  3. package/controllers/communities.cjs +3 -1
  4. package/controllers/groups.cjs +1 -1
  5. package/controllers/images.cjs +12 -1
  6. package/models/domain.cjs +1 -1
  7. package/models/image.cjs +90 -24
  8. package/package.json +83 -63
  9. package/scripts/cloning/clearUsersForCommunitiesFromUrl.js +1 -1
  10. package/scripts/cloning/cloneFromUrlScript.js +1 -1
  11. package/scripts/cloning/cloneWBFromUrlScriptAndCreateLinks.js +1 -1
  12. package/scripts/cloning/cloneWBFromUrlScriptNoUsersOrPoints.js +1 -1
  13. package/scripts/cloning/cloneWBSerbianFromUrlScriptAndCreateLinks.js +1 -1
  14. package/scripts/cloning/copyCommunityConfigAndTranslationsFromURL.js +1 -1
  15. package/scripts/cloning/copyGroupConfigAndTranslationsFromURL.js +1 -1
  16. package/scripts/cloning/copyPostVideosFromURL.js +1 -1
  17. package/scripts/cloning/deepCloneSerbianWBFromUrlScriptAndCreateLinks.js +1 -1
  18. package/scripts/cloning/deepCloneWBFromUrlScriptAndCreateLinks.js +1 -1
  19. package/scripts/cloning/setAdminsFromURL.js +1 -1
  20. package/scripts/cloning/setExternalIdsFromURL.js +1 -1
  21. package/scripts/endorsementFraudDetection/bulkDeleteDuplicateEndorsmentsFromUrl.js +1 -1
  22. package/scripts/landUseGame/export3Ddata.js +1 -1
  23. package/scripts/movePostsToGroupsRecountGroupFromUrl.js +1 -1
  24. package/scripts/recountALLCommunityGroupCounts.js +1 -1
  25. package/scripts/recountCommunitesFromUrl.js +1 -1
  26. package/scripts/recountCommunity.js +1 -1
  27. package/scripts/setLanguageOnGroupCommunitesFromUrl.js +1 -1
  28. package/services/engine/allOurIdeas/aiHelper.d.ts +1 -2
  29. package/services/engine/analytics/manager.cjs +1 -1
  30. package/services/engine/analytics/plausible/manager.cjs +1 -1
  31. package/services/engine/analytics/utils.cjs +1 -1
  32. package/services/engine/notifications/emails_utils.cjs +10 -1
  33. package/services/engine/recommendations/events_manager.cjs +1 -1
  34. package/services/engine/reports/common_utils.cjs +1 -1
  35. package/services/llms/baseChatBot.d.ts +1 -2
  36. package/services/llms/imageGeneration/s3Service.js +72 -11
  37. package/services/scripts/translation_replace_text_from_url.js +1 -1
  38. package/services/utils/redisConnection.cjs +2 -3
  39. package/services/utils/translation_cloning.cjs +1 -1
  40. package/services/utils/translation_helpers.cjs +1 -1
  41. package/tests/emails_utils.test.cjs +130 -0
  42. package/tests/emails_utils.test.d.cts +1 -0
  43. package/tests/imageModel.test.cjs +373 -0
  44. package/tests/imageModel.test.d.cts +1 -0
  45. package/tests/multerSharpS3Compat.test.cjs +229 -0
  46. package/tests/multerSharpS3Compat.test.d.cts +1 -0
  47. package/tests/requestCompat.test.cjs +288 -0
  48. package/tests/requestCompat.test.d.cts +1 -0
  49. package/utils/multerSharpS3Compat.cjs +230 -0
  50. package/utils/multerSharpS3Compat.d.cts +22 -0
  51. package/utils/passportSsoCompat.cjs +15 -0
  52. package/utils/passportSsoCompat.d.cts +2 -0
  53. package/utils/recount_utils.cjs +1 -1
  54. package/utils/requestCompat.cjs +180 -0
  55. package/utils/requestCompat.d.cts +9 -0
@@ -2,7 +2,7 @@ const models = require('../../models/index.cjs');
2
2
  const async = require('async');
3
3
  const _ = require('lodash');
4
4
  const fs = require('fs');
5
- const request = require('request');
5
+ const request = require("../../utils/requestCompat.cjs");
6
6
  const recountCommunity = require('../../utils/recount_utils.cjs').recountCommunity;
7
7
  const recountPost = require('../../utils/recount_utils.cjs').recountPost;
8
8
  const communityId = process.argv[2];
@@ -1,4 +1,4 @@
1
- const { post } = require("request");
1
+ const { post } = require("../../utils/requestCompat.cjs");
2
2
  const queue = require("../../services/workers/queue.cjs");
3
3
  const models = require("../../models/index.cjs");
4
4
  const fs = require("fs");
@@ -2,7 +2,7 @@ const models = require('../models/index.cjs');
2
2
  const async = require('async');
3
3
  const _ = require('lodash');
4
4
  const fs = require('fs');
5
- const request = require('request');
5
+ const request = require("../utils/requestCompat.cjs");
6
6
  /*
7
7
  const urlToConfig = "https://yrpri-eu-direct-assets.s3-eu-west-1.amazonaws.com/WBMoveIdeas160221.csv"//process.argv[1];
8
8
  const urlToAddAddFront = "https://kyrgyz-aris.yrpri.org/"; // process.argv[2];
@@ -2,7 +2,7 @@ const models = require('../models/index.cjs');
2
2
  const async = require('async');
3
3
  const _ = require('lodash');
4
4
  const fs = require('fs');
5
- const request = require('request');
5
+ const request = require("../utils/requestCompat.cjs");
6
6
  models.Community.findAll({
7
7
  attributes: ['id', 'counter_groups']
8
8
  }).then(allCommunities => {
@@ -2,7 +2,7 @@ const models = require('../models/index.cjs');
2
2
  const async = require('async');
3
3
  const _ = require('lodash');
4
4
  const fs = require('fs');
5
- const request = require('request');
5
+ const request = require("../utils/requestCompat.cjs");
6
6
  const recountCommunity = require('../utils/recount_utils.cjs').recountCommunity;
7
7
  /*
8
8
  const urlToConfig = "https://yrpri-eu-direct-assets.s3-eu-west-1.amazonaws.com/WBMoveIdeas160221.csv"//process.argv[1];
@@ -2,7 +2,7 @@ const models = require('../models/index.cjs');
2
2
  const async = require('async');
3
3
  const _ = require('lodash');
4
4
  const fs = require('fs');
5
- const request = require('request');
5
+ const request = require("../utils/requestCompat.cjs");
6
6
  const recountCommunity = require('../utils/recount_utils.cjs').recountCommunity;
7
7
  /*
8
8
  const urlToConfig = "https://yrpri-eu-direct-assets.s3-eu-west-1.amazonaws.com/WBMoveIdeas160221.csv"//process.argv[1];
@@ -2,7 +2,7 @@ const models = require('../models/index.cjs');
2
2
  const async = require('async');
3
3
  const _ = require('lodash');
4
4
  const fs = require('fs');
5
- const request = require('request');
5
+ const request = require("../utils/requestCompat.cjs");
6
6
  /*
7
7
  const urlToConfig = "https://yrpri-eu-direct-assets.s3-eu-west-1.amazonaws.com/WB_ResetLanguageOnCommuntiesList_16_02_21.csv"//process.argv[1];
8
8
  const toLanguage = "ru"; // process.argv[2];
@@ -1,5 +1,4 @@
1
1
  import { OpenAI } from "openai";
2
- import { Stream } from "openai/streaming";
3
2
  import { WebSocket } from "ws";
4
3
  export declare class AiHelper {
5
4
  openaiClient: OpenAI;
@@ -16,7 +15,7 @@ export declare class AiHelper {
16
15
  getModerationResponse: (instructions: string, question: string, answerToModerate: string) => Promise<boolean>;
17
16
  streamChatCompletions(messages: any[]): Promise<void>;
18
17
  sendToClient(sender: string, message: string, type?: string): void;
19
- streamWebSocketResponses(stream: Stream<OpenAI.Chat.Completions.ChatCompletionChunk>): Promise<void>;
18
+ streamWebSocketResponses(stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>): Promise<void>;
20
19
  getAnswerIdeas(question: string, previousIdeas: string[] | null, firstMessage: string | null): Promise<string | null | undefined>;
21
20
  getAiAnalysis(questionId: number, contextPrompt: string, answers: AoiChoiceData[], cacheKeyForFullResponse: string, redisClient: any, locale: string, topOrBottomIdeasText: string, typeOfAnalysisText: string): Promise<string | null | undefined>;
22
21
  }
@@ -6,7 +6,7 @@ const importCommunity = require('./utils.cjs').importCommunity;
6
6
  const importGroup = require('./utils.cjs').importGroup;
7
7
  const importPost = require('./utils.cjs').importPost;
8
8
  const importPoint = require('./utils.cjs').importPoint;
9
- const request = require('request');
9
+ const request = require("../../../utils/requestCompat.cjs");
10
10
  const updateDomain = (domainId, done) => {
11
11
  log.info('updateDomain');
12
12
  models.Domain.unscoped().findOne({
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  const models = require("../../../../models/index.cjs");
3
3
  const log = require("../../../../utils/logger.cjs");
4
- const request = require("request");
4
+ const request = require("../../../../utils/requestCompat.cjs");
5
5
  const moment = require("moment");
6
6
  // This SQL is needed to allow the site API
7
7
  // UPDATE api_keys SET scopes = '{sites:provision:*}' WHERE name = 'Development';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  const _ = require('lodash');
3
- const request = require('request');
3
+ const request = require("../../../utils/requestCompat.cjs");
4
4
  const models = require('../../../models/index.cjs');
5
5
  const log = require('../../../utils/logger.cjs');
6
6
  const convertToString = (integer, type) => {
@@ -242,7 +242,9 @@ const filterNotificationForDelivery = function (notification, user, template, su
242
242
  }
243
243
  else {
244
244
  const redisKey = `${SUPPRESSION_KEYBASE}${user.id}`;
245
- redisConnection.get(redisKey, (error, found) => {
245
+ redisConnection
246
+ .get(redisKey)
247
+ .then((found) => {
246
248
  if (found) {
247
249
  log.info(`Suppressing emails for user ${user.email} settings ${LIMIT_EMAILS_FOR_SECONDS}`);
248
250
  callback();
@@ -250,6 +252,13 @@ const filterNotificationForDelivery = function (notification, user, template, su
250
252
  else {
251
253
  processNotification(notification, user, template, subject, callback);
252
254
  }
255
+ })
256
+ .catch((error) => {
257
+ log.warn("Redis suppression lookup failed, continuing delivery", {
258
+ err: error,
259
+ userId: user.id,
260
+ });
261
+ processNotification(notification, user, template, subject, callback);
253
262
  });
254
263
  }
255
264
  });
@@ -3,7 +3,7 @@ const models = require('../../../models/index.cjs');
3
3
  const _ = require('lodash');
4
4
  const async = require('async');
5
5
  const log = require('../../../utils/logger.cjs');
6
- const request = require('request');
6
+ const request = require("../../../utils/requestCompat.cjs");
7
7
  let airbrake = null;
8
8
  if (process.env.AIRBRAKE_PROJECT_ID) {
9
9
  airbrake = require('../../utils/airbrake.cjs');
@@ -6,7 +6,7 @@ const moment = require('moment');
6
6
  const skipEmail = false;
7
7
  const aws = require('aws-sdk');
8
8
  const log = require('../../../utils/logger.cjs');
9
- const request = require('request');
9
+ const request = require("../../../utils/requestCompat.cjs");
10
10
  const fs = require('fs');
11
11
  const downloadImage = (uri, filename, callback) => {
12
12
  request.head(uri, (err, res, body) => {
@@ -1,5 +1,4 @@
1
1
  import { OpenAI } from "openai";
2
- import { Stream } from "openai/streaming.js";
3
2
  import WebSocket from "ws";
4
3
  import ioredis from "ioredis";
5
4
  export declare class YpBaseChatBot {
@@ -26,7 +25,7 @@ export declare class YpBaseChatBot {
26
25
  sendAgentCompleted(name: string, lastAgent?: boolean, error?: string | undefined): void;
27
26
  sendAgentUpdate(message: string): void;
28
27
  sendToClient(sender: YpSenderType, message: string, type?: YpAssistantMessageType, uniqueToken?: string | undefined, hiddenContextMessage?: boolean): void;
29
- streamWebSocketResponses(stream: Stream<OpenAI.Chat.Completions.ChatCompletionChunk>): Promise<void>;
28
+ streamWebSocketResponses(stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>): Promise<void>;
30
29
  saveMemoryIfNeeded(): Promise<void>;
31
30
  setChatLog(chatLog: YpSimpleChatLog[]): Promise<void>;
32
31
  conversation(chatLog: YpSimpleChatLog[]): Promise<void>;
@@ -2,6 +2,49 @@ import AWS from "aws-sdk";
2
2
  import fs from "fs";
3
3
  import axios from "axios";
4
4
  import log from "../../../utils/loggerTs.js";
5
+ const normalizeEndpointHost = (endpoint) => {
6
+ if (!endpoint) {
7
+ return null;
8
+ }
9
+ try {
10
+ const normalizedUrl = new URL(endpoint.includes("://") ? endpoint : `https://${endpoint}`);
11
+ return {
12
+ host: normalizedUrl.host,
13
+ hostname: normalizedUrl.hostname,
14
+ };
15
+ }
16
+ catch {
17
+ return {
18
+ host: endpoint,
19
+ hostname: endpoint,
20
+ };
21
+ }
22
+ };
23
+ const getBucketFromVirtualHost = (parsedUrl, normalizedEndpoint) => {
24
+ if (!normalizedEndpoint) {
25
+ return null;
26
+ }
27
+ const hostSuffix = `.${normalizedEndpoint.host}`;
28
+ const hostnameSuffix = `.${normalizedEndpoint.hostname}`;
29
+ if (parsedUrl.host.endsWith(hostSuffix)) {
30
+ return parsedUrl.host.slice(0, -hostSuffix.length) || null;
31
+ }
32
+ if (parsedUrl.hostname.endsWith(hostnameSuffix)) {
33
+ return parsedUrl.hostname.slice(0, -hostnameSuffix.length) || null;
34
+ }
35
+ return null;
36
+ };
37
+ const getAwsBucketFromHostname = (hostname) => {
38
+ const s3MarkerIndex = hostname.indexOf(".s3.");
39
+ if (s3MarkerIndex > 0) {
40
+ return hostname.slice(0, s3MarkerIndex) || null;
41
+ }
42
+ const legacyS3MarkerIndex = hostname.indexOf(".s3-");
43
+ if (legacyS3MarkerIndex > 0) {
44
+ return hostname.slice(0, legacyS3MarkerIndex) || null;
45
+ }
46
+ return null;
47
+ };
5
48
  export class S3Service {
6
49
  constructor(cloudflareApiKey, cloudflareZoneId) {
7
50
  this.cloudflareApiKey = cloudflareApiKey;
@@ -37,13 +80,17 @@ export class S3Service {
37
80
  const params = {
38
81
  Bucket: bucket,
39
82
  Key: key,
40
- ACL: "private",
41
83
  };
42
84
  log.info(`Disabling/Deleting Key from S3: ${JSON.stringify(params)}`);
43
85
  return new Promise((resolve, reject) => {
44
- s3.putObjectAcl(params, (err, data) => {
86
+ s3.deleteObject(params, (err, data) => {
45
87
  if (err) {
46
- log.error(`Error deleting image from S3: ${err}`);
88
+ log.error("Error deleting image from S3", {
89
+ imageUrl,
90
+ bucket,
91
+ key,
92
+ err,
93
+ });
47
94
  reject(err);
48
95
  }
49
96
  else {
@@ -87,25 +134,39 @@ export class S3Service {
87
134
  let bucket, key;
88
135
  const cfImageProxyDomain = process.env.CLOUDFLARE_IMAGE_PROXY_DOMAIN;
89
136
  const s3Bucket = process.env.S3_BUCKET;
137
+ const normalizedEndpoint = normalizeEndpointHost(process.env.S3_ENDPOINT);
138
+ const parsedUrl = new URL(imageUrl);
90
139
  if (cfImageProxyDomain && imageUrl.includes(cfImageProxyDomain)) {
91
- const urlPath = new URL(imageUrl).pathname;
92
- const [, ...pathParts] = urlPath.split("/");
140
+ const [, ...pathParts] = parsedUrl.pathname.split("/");
93
141
  bucket = s3Bucket;
94
142
  key = pathParts.join("/");
95
143
  }
144
+ else if (normalizedEndpoint &&
145
+ (parsedUrl.hostname === normalizedEndpoint.hostname ||
146
+ parsedUrl.host === normalizedEndpoint.host)) {
147
+ const [, maybeBucket, ...pathParts] = parsedUrl.pathname.split("/");
148
+ bucket = process.env.MINIO_ROOT_USER ? maybeBucket || s3Bucket : s3Bucket;
149
+ key = process.env.MINIO_ROOT_USER ? pathParts.join("/") : parsedUrl.pathname.slice(1);
150
+ }
96
151
  else {
97
- const match = imageUrl.match(/https:\/\/(.+?)\.s3\.amazonaws\.com\/(.+)/);
98
- if (match) {
99
- bucket = match[1];
100
- key = match[2];
152
+ const virtualHostBucket = getBucketFromVirtualHost(parsedUrl, normalizedEndpoint) ||
153
+ getAwsBucketFromHostname(parsedUrl.hostname);
154
+ if (virtualHostBucket) {
155
+ bucket = virtualHostBucket;
156
+ key = parsedUrl.pathname.replace(/^\/+/, "");
101
157
  }
102
158
  }
103
159
  return { bucket, key };
104
160
  }
105
161
  async deleteMediaFormatsUrls(formats) {
106
162
  for (const url of formats) {
107
- await this.deleteS3Url(url);
108
- log.info(`Deleted image from S3: ${url}`);
163
+ try {
164
+ await this.deleteS3Url(url);
165
+ log.info(`Deleted image from S3: ${url}`);
166
+ }
167
+ catch (error) {
168
+ log.warn("Best-effort image cleanup failed", { url, error });
169
+ }
109
170
  }
110
171
  }
111
172
  }
@@ -2,7 +2,7 @@ const models = require('../../models/index.cjs');
2
2
  const async = require('async');
3
3
  const _ = require('lodash');
4
4
  const fs = require('fs');
5
- const request = require('request');
5
+ const request = require("../../utils/requestCompat.cjs");
6
6
  const farmhash = require('farmhash');
7
7
  //const communityId = "1264"; //process.argv[2];
8
8
  //const communityId = "1402"; //process.argv[2];
@@ -9,18 +9,17 @@ if (process.env.REDIS_URL) {
9
9
  }
10
10
  if (redisUrl.includes("rediss://")) {
11
11
  redisClient = redis.createClient({
12
- legacyMode: false,
13
12
  url: redisUrl,
14
13
  pingInterval: 10000,
15
14
  socket: { tls: true, rejectUnauthorized: false },
16
15
  });
17
16
  }
18
17
  else {
19
- redisClient = redis.createClient({ legacyMode: true, url: redisUrl });
18
+ redisClient = redis.createClient({ url: redisUrl });
20
19
  }
21
20
  }
22
21
  else {
23
- redisClient = redis.createClient({ legacyMode: true });
22
+ redisClient = redis.createClient();
24
23
  }
25
24
  redisClient.on("error", (err) => log.error("Backend Redis client error", err));
26
25
  redisClient.on("connect", () => log.info("Backend Redis client is connect"));
@@ -3,7 +3,7 @@ const models = require('../../models/index.cjs');
3
3
  const async = require('async');
4
4
  const _ = require('lodash');
5
5
  const fs = require('fs');
6
- const request = require('request');
6
+ const request = require("../../utils/requestCompat.cjs");
7
7
  const farmhash = require('farmhash');
8
8
  const fixTargetLocale = require('./translation_helpers.cjs').fixTargetLocale;
9
9
  // For post get translations in all locales (that exists)
@@ -3,7 +3,7 @@ const models = require("../../models/index.cjs");
3
3
  const async = require("async");
4
4
  const _ = require("lodash");
5
5
  const fs = require("fs");
6
- const request = require("request");
6
+ const request = require("../../utils/requestCompat.cjs");
7
7
  const farmhash = require("farmhash");
8
8
  const log = require('../../utils/logger.cjs');
9
9
  const fixTargetLocale = (itemTargetLocale) => {
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ const test = require("node:test");
3
+ const assert = require("node:assert/strict");
4
+ const fs = require("node:fs");
5
+ const path = require("node:path");
6
+ const vm = require("node:vm");
7
+ const Module = require("node:module");
8
+ const emailsUtilsPath = path.resolve(__dirname, "../services/engine/notifications/emails_utils.cjs");
9
+ const loadEmailsUtils = ({ logger, redisConnection, queue, models }) => {
10
+ const source = fs.readFileSync(emailsUtilsPath, "utf8");
11
+ const module = { exports: {} };
12
+ const baseRequire = Module.createRequire(emailsUtilsPath);
13
+ const mockedRequire = (request) => {
14
+ switch (request) {
15
+ case "../../../utils/logger.cjs":
16
+ return logger;
17
+ case "../../utils/redisConnection.cjs":
18
+ return redisConnection;
19
+ case "../../workers/queue.cjs":
20
+ return queue;
21
+ case "../../../models/index.cjs":
22
+ return models;
23
+ case "nodemailer":
24
+ return {
25
+ createTransport() {
26
+ return {
27
+ verify(callback) {
28
+ if (callback) {
29
+ callback(null, true);
30
+ }
31
+ },
32
+ };
33
+ },
34
+ };
35
+ case "../../utils/i18n.cjs":
36
+ return {
37
+ t(value) {
38
+ return value;
39
+ },
40
+ };
41
+ default:
42
+ return baseRequire(request);
43
+ }
44
+ };
45
+ const wrapped = Module.wrap(source);
46
+ const compiled = vm.runInThisContext(wrapped, {
47
+ filename: emailsUtilsPath,
48
+ });
49
+ compiled(module.exports, mockedRequire, module, emailsUtilsPath, path.dirname(emailsUtilsPath));
50
+ return module.exports;
51
+ };
52
+ test("filterNotificationForDelivery falls back to delivery when Redis suppression lookup fails", async () => {
53
+ const queuedEmails = [];
54
+ const redisSetExCalls = [];
55
+ const warnings = [];
56
+ const emailsUtils = loadEmailsUtils({
57
+ logger: {
58
+ info() { },
59
+ error() { },
60
+ warn(message, metadata) {
61
+ warnings.push({ message, metadata });
62
+ },
63
+ },
64
+ redisConnection: {
65
+ async get() {
66
+ throw new Error("redis down");
67
+ },
68
+ setEx(...args) {
69
+ redisSetExCalls.push(args);
70
+ },
71
+ },
72
+ queue: {
73
+ add(...args) {
74
+ queuedEmails.push(args);
75
+ },
76
+ },
77
+ models: {
78
+ AcNotification: {
79
+ METHOD_MUTED: "muted",
80
+ FREQUENCY_AS_IT_HAPPENS: "as_it_happens",
81
+ },
82
+ },
83
+ });
84
+ const group = {
85
+ name: "visible-group",
86
+ async hasGroupAdmins() {
87
+ return false;
88
+ },
89
+ };
90
+ const user = {
91
+ id: 42,
92
+ email: "person@example.com",
93
+ notifications_settings: {
94
+ point_activity: {
95
+ method: "email",
96
+ frequency: "as_it_happens",
97
+ },
98
+ },
99
+ };
100
+ const notification = {
101
+ from_notification_setting: "point_activity",
102
+ AcActivities: [
103
+ {
104
+ Domain: { id: 1 },
105
+ Community: null,
106
+ Group: group,
107
+ Point: {
108
+ Group: group,
109
+ },
110
+ Post: null,
111
+ },
112
+ ],
113
+ };
114
+ await new Promise((resolve, reject) => {
115
+ emailsUtils.filterNotificationForDelivery(notification, user, "point_activity", "A subject", (error) => {
116
+ if (error) {
117
+ reject(error);
118
+ }
119
+ else {
120
+ resolve();
121
+ }
122
+ });
123
+ });
124
+ assert.equal(warnings.length, 1);
125
+ assert.equal(warnings[0].message, "Redis suppression lookup failed, continuing delivery");
126
+ assert.equal(warnings[0].metadata.userId, 42);
127
+ assert.equal(queuedEmails.length, 1);
128
+ assert.equal(queuedEmails[0][0], "send-one-email");
129
+ assert.equal(redisSetExCalls.length, 1);
130
+ });
@@ -0,0 +1 @@
1
+ export {};