@symbo.ls/sdk 2.33.26 → 2.33.27

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.
@@ -18,6 +18,23 @@ class PlanService extends BaseService {
18
18
  throw new Error(`Failed to get plans: ${error.message}`, { cause: error });
19
19
  }
20
20
  }
21
+ /**
22
+ * Get list of public plans with enhanced pricing information (no authentication required)
23
+ */
24
+ async getPlansWithPricing() {
25
+ try {
26
+ const response = await this._request("/plans/pricing", {
27
+ method: "GET",
28
+ methodName: "getPlansWithPricing"
29
+ });
30
+ if (response.success) {
31
+ return response.data;
32
+ }
33
+ throw new Error(response.message);
34
+ } catch (error) {
35
+ throw new Error(`Failed to get plans with pricing: ${error.message}`, { cause: error });
36
+ }
37
+ }
21
38
  /**
22
39
  * Get a specific plan by ID (no authentication required)
23
40
  */
@@ -182,16 +199,48 @@ class PlanService extends BaseService {
182
199
  if (!planData || typeof planData !== "object") {
183
200
  throw new Error("Plan data must be a valid object");
184
201
  }
185
- const requiredFields = ["name", "key", "price"];
202
+ const requiredFields = ["name", "description"];
186
203
  for (const field of requiredFields) {
187
204
  if (!planData[field]) {
188
205
  throw new Error(`Required field '${field}' is missing`);
189
206
  }
190
207
  }
191
- if (typeof planData.price !== "number" || planData.price < 0) {
192
- throw new Error("Price must be a positive number");
208
+ if (Object.hasOwn(planData, "price")) {
209
+ throw new Error('Field "price" is no longer supported. Use unified "pricingOptions" with "amount" instead.');
210
+ }
211
+ if (planData.pricingOptions != null) {
212
+ if (!Array.isArray(planData.pricingOptions) || planData.pricingOptions.length === 0) {
213
+ throw new Error("pricingOptions must be a non-empty array when provided");
214
+ }
215
+ const allowedIntervals = /* @__PURE__ */ new Set(["month", "year", "week", "day", null]);
216
+ planData.pricingOptions.forEach((option, index) => {
217
+ if (!option || typeof option !== "object") {
218
+ throw new Error(`Pricing option at index ${index} must be an object`);
219
+ }
220
+ const { key, displayName, amount, interval, lookupKey } = option;
221
+ if (!key || typeof key !== "string") {
222
+ throw new Error(`Pricing option at index ${index} is missing required field 'key'`);
223
+ }
224
+ if (!/^[a-z0-9-]+$/u.test(key)) {
225
+ throw new Error(`Pricing option key '${key}' must contain only lowercase letters, numbers, and hyphens`);
226
+ }
227
+ if (!displayName || typeof displayName !== "string") {
228
+ throw new Error(`Pricing option '${key}' is missing required field 'displayName'`);
229
+ }
230
+ if (typeof amount !== "number" || amount < 0) {
231
+ throw new Error(`Pricing option '${key}' must have a non-negative numeric 'amount'`);
232
+ }
233
+ if (interval != null && !allowedIntervals.has(interval)) {
234
+ throw new Error(
235
+ `Pricing option '${key}' has invalid interval '${interval}'. Allowed: month, year, week, day or null`
236
+ );
237
+ }
238
+ if (!lookupKey || typeof lookupKey !== "string") {
239
+ throw new Error(`Pricing option '${key}' is missing required field 'lookupKey'`);
240
+ }
241
+ });
193
242
  }
194
- if (!/^[a-z0-9-]+$/u.test(planData.key)) {
243
+ if (planData.key && !/^[a-z0-9-]+$/u.test(planData.key)) {
195
244
  throw new Error("Plan key must contain only lowercase letters, numbers, and hyphens");
196
245
  }
197
246
  return await this.createPlan(planData);
@@ -206,10 +255,40 @@ class PlanService extends BaseService {
206
255
  if (!planData || typeof planData !== "object") {
207
256
  throw new Error("Plan data must be a valid object");
208
257
  }
209
- if (planData.price != null) {
210
- if (typeof planData.price !== "number" || planData.price < 0) {
211
- throw new Error("Price must be a positive number");
258
+ if (Object.hasOwn(planData, "price")) {
259
+ throw new Error('Field "price" is no longer supported. Use unified "pricingOptions" with "amount" instead.');
260
+ }
261
+ if (planData.pricingOptions != null) {
262
+ if (!Array.isArray(planData.pricingOptions) || planData.pricingOptions.length === 0) {
263
+ throw new Error("pricingOptions must be a non-empty array when provided");
212
264
  }
265
+ const allowedIntervals = /* @__PURE__ */ new Set(["month", "year", "week", "day", null]);
266
+ planData.pricingOptions.forEach((option, index) => {
267
+ if (!option || typeof option !== "object") {
268
+ throw new Error(`Pricing option at index ${index} must be an object`);
269
+ }
270
+ const { key, displayName, amount, interval, lookupKey } = option;
271
+ if (!key || typeof key !== "string") {
272
+ throw new Error(`Pricing option at index ${index} is missing required field 'key'`);
273
+ }
274
+ if (!/^[a-z0-9-]+$/u.test(key)) {
275
+ throw new Error(`Pricing option key '${key}' must contain only lowercase letters, numbers, and hyphens`);
276
+ }
277
+ if (!displayName || typeof displayName !== "string") {
278
+ throw new Error(`Pricing option '${key}' is missing required field 'displayName'`);
279
+ }
280
+ if (typeof amount !== "number" || amount < 0) {
281
+ throw new Error(`Pricing option '${key}' must have a non-negative numeric 'amount'`);
282
+ }
283
+ if (interval != null && !allowedIntervals.has(interval)) {
284
+ throw new Error(
285
+ `Pricing option '${key}' has invalid interval '${interval}'. Allowed: month, year, week, day or null`
286
+ );
287
+ }
288
+ if (!lookupKey || typeof lookupKey !== "string") {
289
+ throw new Error(`Pricing option '${key}' is missing required field 'lookupKey'`);
290
+ }
291
+ });
213
292
  }
214
293
  if (planData.key && !/^[a-z0-9-]+$/u.test(planData.key)) {
215
294
  throw new Error("Plan key must contain only lowercase letters, numbers, and hyphens");
@@ -222,7 +301,7 @@ class PlanService extends BaseService {
222
301
  async getActivePlans() {
223
302
  try {
224
303
  const plans = await this.getPlans();
225
- return plans.filter((plan) => plan.active !== false);
304
+ return plans.filter((plan) => plan.status === "active" && plan.isVisible !== false);
226
305
  } catch (error) {
227
306
  throw new Error(`Failed to get active plans: ${error.message}`, { cause: error });
228
307
  }
@@ -232,9 +311,10 @@ class PlanService extends BaseService {
232
311
  */
233
312
  async getPlansByPriceRange(minPrice = 0, maxPrice = Infinity) {
234
313
  try {
235
- const plans = await this.getPlans();
314
+ const plans = await this.getPlansWithPricing();
236
315
  return plans.filter((plan) => {
237
- const price = plan.price || 0;
316
+ var _a, _b;
317
+ const price = ((_b = (_a = plan == null ? void 0 : plan.pricing) == null ? void 0 : _a.bestPrice) == null ? void 0 : _b.amount) ?? 0;
238
318
  return price >= minPrice && price <= maxPrice;
239
319
  });
240
320
  } catch (error) {
@@ -115,6 +115,7 @@ const SERVICE_METHODS = {
115
115
  getActivePlans: "plan",
116
116
  getPlansByPriceRange: "plan",
117
117
  getPlanByKey: "plan",
118
+ getPlansWithPricing: "plan",
118
119
  // Subscription methods (moved to subscription service)
119
120
  createSubscription: "subscription",
120
121
  getProjectStatus: "subscription",
@@ -164,7 +165,6 @@ const SERVICE_METHODS = {
164
165
  createDnsRecordWithValidation: "dns",
165
166
  getDnsRecordWithValidation: "dns",
166
167
  removeDnsRecordWithValidation: "dns",
167
- setProjectDomainsWithValidation: "dns",
168
168
  addProjectCustomDomainsWithValidation: "dns",
169
169
  isDomainAvailable: "dns",
170
170
  getDomainStatus: "dns",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbo.ls/sdk",
3
- "version": "2.33.26",
3
+ "version": "2.33.27",
4
4
  "type": "module",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -46,12 +46,12 @@
46
46
  "test:user": "cross-env NODE_ENV=$NODE_ENV npx tape integration-tests/index.js integration-tests/user/*.test.js | tap-spec"
47
47
  },
48
48
  "dependencies": {
49
- "@domql/element": "^2.33.26",
50
- "@domql/utils": "^2.33.26",
49
+ "@domql/element": "^2.33.27",
50
+ "@domql/utils": "^2.33.27",
51
51
  "@grafana/faro-web-sdk": "^1.19.0",
52
52
  "@grafana/faro-web-tracing": "^1.19.0",
53
- "@symbo.ls/router": "^2.33.26",
54
- "@symbo.ls/socket": "^2.33.26",
53
+ "@symbo.ls/router": "^2.33.27",
54
+ "@symbo.ls/socket": "^2.33.27",
55
55
  "acorn": "^8.14.0",
56
56
  "acorn-walk": "^8.3.4",
57
57
  "dexie": "^4.0.11",
@@ -69,9 +69,10 @@
69
69
  "esbuild-plugins-node-modules-polyfill": "^1.6.8",
70
70
  "glob": "^11.0.0",
71
71
  "jsdom": "^26.1.0",
72
+ "sinon": "^21.0.0",
72
73
  "tap-parser": "^18.0.0",
73
74
  "tap-spec": "^5.0.0",
74
75
  "tape": "^5.9.0"
75
76
  },
76
- "gitHead": "60d701cca225200ddb1e15d6a3a31da25dfc3bc0"
77
+ "gitHead": "305c78c0cd824b365d3e464dc3ede6b250c8baf1"
77
78
  }
@@ -21,6 +21,24 @@ export class PlanService extends BaseService {
21
21
  }
22
22
  }
23
23
 
24
+ /**
25
+ * Get list of public plans with enhanced pricing information (no authentication required)
26
+ */
27
+ async getPlansWithPricing () {
28
+ try {
29
+ const response = await this._request('/plans/pricing', {
30
+ method: 'GET',
31
+ methodName: 'getPlansWithPricing'
32
+ })
33
+ if (response.success) {
34
+ return response.data
35
+ }
36
+ throw new Error(response.message)
37
+ } catch (error) {
38
+ throw new Error(`Failed to get plans with pricing: ${error.message}`, { cause: error })
39
+ }
40
+ }
41
+
24
42
  /**
25
43
  * Get a specific plan by ID (no authentication required)
26
44
  */
@@ -197,21 +215,64 @@ export class PlanService extends BaseService {
197
215
  throw new Error('Plan data must be a valid object')
198
216
  }
199
217
 
200
- // Basic validation for required fields
201
- const requiredFields = ['name', 'key', 'price']
218
+ // Basic validation for required fields to match server model
219
+ const requiredFields = ['name', 'description']
202
220
  for (const field of requiredFields) {
203
221
  if (!planData[field]) {
204
222
  throw new Error(`Required field '${field}' is missing`)
205
223
  }
206
224
  }
207
225
 
208
- // Validate price is a positive number
209
- if (typeof planData.price !== 'number' || planData.price < 0) {
210
- throw new Error('Price must be a positive number')
226
+ // Legacy field guard: bare "price" is no longer supported on the server
227
+ if (Object.hasOwn(planData, 'price')) {
228
+ throw new Error('Field "price" is no longer supported. Use unified "pricingOptions" with "amount" instead.')
229
+ }
230
+
231
+ // Validate unified pricingOptions structure if provided
232
+ if (planData.pricingOptions != null) {
233
+ if (!Array.isArray(planData.pricingOptions) || planData.pricingOptions.length === 0) {
234
+ throw new Error('pricingOptions must be a non-empty array when provided')
235
+ }
236
+
237
+ const allowedIntervals = new Set(['month', 'year', 'week', 'day', null])
238
+ planData.pricingOptions.forEach((option, index) => {
239
+ if (!option || typeof option !== 'object') {
240
+ throw new Error(`Pricing option at index ${index} must be an object`)
241
+ }
242
+
243
+ const { key, displayName, amount, interval, lookupKey } = option
244
+
245
+ if (!key || typeof key !== 'string') {
246
+ throw new Error(`Pricing option at index ${index} is missing required field 'key'`)
247
+ }
248
+
249
+ // Validate key format (alphanumeric and hyphens only)
250
+ if (!/^[a-z0-9-]+$/u.test(key)) {
251
+ throw new Error(`Pricing option key '${key}' must contain only lowercase letters, numbers, and hyphens`)
252
+ }
253
+
254
+ if (!displayName || typeof displayName !== 'string') {
255
+ throw new Error(`Pricing option '${key}' is missing required field 'displayName'`)
256
+ }
257
+
258
+ if (typeof amount !== 'number' || amount < 0) {
259
+ throw new Error(`Pricing option '${key}' must have a non-negative numeric 'amount'`)
260
+ }
261
+
262
+ if (interval != null && !allowedIntervals.has(interval)) {
263
+ throw new Error(
264
+ `Pricing option '${key}' has invalid interval '${interval}'. Allowed: month, year, week, day or null`
265
+ )
266
+ }
267
+
268
+ if (!lookupKey || typeof lookupKey !== 'string') {
269
+ throw new Error(`Pricing option '${key}' is missing required field 'lookupKey'`)
270
+ }
271
+ })
211
272
  }
212
273
 
213
- // Validate key format (alphanumeric and hyphens only)
214
- if (!/^[a-z0-9-]+$/u.test(planData.key)) {
274
+ // Optional: validate top-level key if provided (legacy support)
275
+ if (planData.key && !/^[a-z0-9-]+$/u.test(planData.key)) {
215
276
  throw new Error('Plan key must contain only lowercase letters, numbers, and hyphens')
216
277
  }
217
278
 
@@ -229,11 +290,52 @@ export class PlanService extends BaseService {
229
290
  throw new Error('Plan data must be a valid object')
230
291
  }
231
292
 
232
- // Validate price if provided
233
- if (planData.price != null) {
234
- if (typeof planData.price !== 'number' || planData.price < 0) {
235
- throw new Error('Price must be a positive number')
293
+ // Legacy field guard: bare "price" is no longer supported on the server
294
+ if (Object.hasOwn(planData, 'price')) {
295
+ throw new Error('Field "price" is no longer supported. Use unified "pricingOptions" with "amount" instead.')
296
+ }
297
+
298
+ // Validate unified pricingOptions structure if provided
299
+ if (planData.pricingOptions != null) {
300
+ if (!Array.isArray(planData.pricingOptions) || planData.pricingOptions.length === 0) {
301
+ throw new Error('pricingOptions must be a non-empty array when provided')
236
302
  }
303
+
304
+ const allowedIntervals = new Set(['month', 'year', 'week', 'day', null])
305
+ planData.pricingOptions.forEach((option, index) => {
306
+ if (!option || typeof option !== 'object') {
307
+ throw new Error(`Pricing option at index ${index} must be an object`)
308
+ }
309
+
310
+ const { key, displayName, amount, interval, lookupKey } = option
311
+
312
+ if (!key || typeof key !== 'string') {
313
+ throw new Error(`Pricing option at index ${index} is missing required field 'key'`)
314
+ }
315
+
316
+ // Validate key format (alphanumeric and hyphens only)
317
+ if (!/^[a-z0-9-]+$/u.test(key)) {
318
+ throw new Error(`Pricing option key '${key}' must contain only lowercase letters, numbers, and hyphens`)
319
+ }
320
+
321
+ if (!displayName || typeof displayName !== 'string') {
322
+ throw new Error(`Pricing option '${key}' is missing required field 'displayName'`)
323
+ }
324
+
325
+ if (typeof amount !== 'number' || amount < 0) {
326
+ throw new Error(`Pricing option '${key}' must have a non-negative numeric 'amount'`)
327
+ }
328
+
329
+ if (interval != null && !allowedIntervals.has(interval)) {
330
+ throw new Error(
331
+ `Pricing option '${key}' has invalid interval '${interval}'. Allowed: month, year, week, day or null`
332
+ )
333
+ }
334
+
335
+ if (!lookupKey || typeof lookupKey !== 'string') {
336
+ throw new Error(`Pricing option '${key}' is missing required field 'lookupKey'`)
337
+ }
338
+ })
237
339
  }
238
340
 
239
341
  // Validate key format if provided
@@ -250,7 +352,9 @@ export class PlanService extends BaseService {
250
352
  async getActivePlans () {
251
353
  try {
252
354
  const plans = await this.getPlans()
253
- return plans.filter(plan => plan.active !== false)
355
+ // Server already returns only active & visible plans for /plans,
356
+ // but we keep this helper aligned with the model fields.
357
+ return plans.filter(plan => plan.status === 'active' && plan.isVisible !== false)
254
358
  } catch (error) {
255
359
  throw new Error(`Failed to get active plans: ${error.message}`, { cause: error })
256
360
  }
@@ -261,9 +365,10 @@ export class PlanService extends BaseService {
261
365
  */
262
366
  async getPlansByPriceRange (minPrice = 0, maxPrice = Infinity) {
263
367
  try {
264
- const plans = await this.getPlans()
368
+ // Use enhanced pricing information from /plans/pricing
369
+ const plans = await this.getPlansWithPricing()
265
370
  return plans.filter(plan => {
266
- const price = plan.price || 0
371
+ const price = plan?.pricing?.bestPrice?.amount ?? 0
267
372
  return price >= minPrice && price <= maxPrice
268
373
  })
269
374
  } catch (error) {
@@ -0,0 +1,92 @@
1
+ import test from 'tape'
2
+ import sinon from 'sinon'
3
+ import { PlanService } from '../../PlanService.js'
4
+
5
+ // #region Setup
6
+ const sandbox = sinon.createSandbox()
7
+ // #endregion
8
+
9
+ // #region Tests
10
+ test('createPlan should return response data', async t => {
11
+ t.plan(1)
12
+
13
+ const planDataStub = {
14
+ success: true,
15
+ data: sandbox.stub()
16
+ }
17
+ const planServiceStub = new PlanService()
18
+ sandbox.stub(planServiceStub, '_request').resolves(planDataStub)
19
+ sandbox.stub(planServiceStub, '_requireReady').resolves()
20
+ const response = await planServiceStub.createPlan([])
21
+ t.equal(response, planDataStub.data, 'Response data returned')
22
+
23
+ sandbox.restore()
24
+ t.end()
25
+ })
26
+
27
+ test('createPlan should return an error - Plan data is required', async t => {
28
+ t.plan(1)
29
+ const planServiceStub = new PlanService()
30
+ sandbox.stub(planServiceStub, '_requireReady').resolves()
31
+
32
+ try {
33
+ await planServiceStub.createPlan(false)
34
+ t.fail('createPlan should have failed')
35
+ } catch (err) {
36
+ t.equal(
37
+ err.toString(),
38
+ 'Error: Plan data is required',
39
+ 'Error correctly returned'
40
+ )
41
+ }
42
+ sandbox.restore()
43
+ t.end()
44
+ })
45
+
46
+ test('createPlan should return an error - Failed to parse URL', async t => {
47
+ t.plan(1)
48
+ const planServiceStub = new PlanService()
49
+ sandbox.stub(planServiceStub, '_requireReady').resolves()
50
+
51
+ try {
52
+ await planServiceStub.createPlan([])
53
+ t.fail('createPlan should have failed')
54
+ } catch (err) {
55
+ t.equal(
56
+ err.toString(),
57
+ 'Error: Failed to create plan: Request failed: Failed to parse URL from null/core/admin/plans',
58
+ 'Error correctly returned'
59
+ )
60
+ }
61
+ sandbox.restore()
62
+ t.end()
63
+ })
64
+
65
+ test('createPlan should fail the requireReady', async t => {
66
+ t.plan(1)
67
+ const planServiceStub = new PlanService()
68
+ sandbox
69
+ .stub(planServiceStub, '_requireReady')
70
+ .throws(() => new Error('Service not initialized for method: createPlan'))
71
+
72
+ try {
73
+ await planServiceStub.createPlan()
74
+ t.fail('createPlan should have failed')
75
+ } catch (err) {
76
+ t.equal(
77
+ err.toString(),
78
+ 'Error: Service not initialized for method: createPlan',
79
+ 'Error correctly returned'
80
+ )
81
+ }
82
+ sandbox.restore()
83
+ t.end()
84
+ })
85
+ // #endregion
86
+
87
+ // #region Cleanup
88
+ test('teardown', t => {
89
+ sandbox.restore()
90
+ t.end()
91
+ })
92
+ // #endregion
@@ -0,0 +1,177 @@
1
+ /* eslint-disable no-useless-escape */
2
+ /* eslint-disable no-undefined */
3
+ import test from 'tape'
4
+ import sinon from 'sinon'
5
+ import { PlanService } from '../../PlanService.js'
6
+
7
+ // #region Setup
8
+ const sandbox = sinon.createSandbox()
9
+ // #endregion
10
+
11
+ // #region Tests
12
+ test('createPlanWithValidation should return response data', async t => {
13
+ t.plan(1)
14
+ const responseStub = [sandbox.stub()]
15
+ const planData = {
16
+ name: 'testName',
17
+ key: 'testkey',
18
+ price: 1.0
19
+ }
20
+ const planServiceStub = new PlanService()
21
+ sandbox.stub(planServiceStub, 'createPlan').resolves(responseStub)
22
+ const response = await planServiceStub.createPlanWithValidation(planData)
23
+ t.ok(response, 'Response returned successfully')
24
+
25
+ sandbox.restore()
26
+ t.end()
27
+ })
28
+
29
+ function planDataEmptyOrNotAnObject () {
30
+ // Data test object
31
+ const badData = {
32
+ planDataUndefined: {
33
+ name: 'planData is undefined',
34
+ testValue: undefined
35
+ },
36
+ planDataNotAnObject: {
37
+ name: 'planData is not an object',
38
+ testValue: 'Not An Object'
39
+ }
40
+ }
41
+
42
+ Object.keys(badData).forEach(key => {
43
+ test(`createPlanWithValidation should return an error - ${badData[key].name}`, async t => {
44
+ t.plan(1)
45
+ const planData = badData[key].testValue
46
+ const planServiceStub = new PlanService()
47
+ try {
48
+ await planServiceStub.createPlanWithValidation(planData)
49
+ t.fail('createPlanWithValidation should have failed')
50
+ } catch (err) {
51
+ t.equal(
52
+ err.toString(),
53
+ 'Error: Plan data must be a valid object',
54
+ `${badData[key].name} successfully caused an error`
55
+ )
56
+ }
57
+ sandbox.restore()
58
+ t.end()
59
+ })
60
+ })
61
+ }
62
+
63
+ function requiredFieldsMissing () {
64
+ // Data test object
65
+ const planData = {
66
+ name: 'testName',
67
+ key: 'testkey',
68
+ price: 1.0
69
+ }
70
+ Object.keys(planData).forEach(field => {
71
+ test('Basic validation for required fields should return an error - string causes Invalid plan data received', async t => {
72
+ t.plan(1)
73
+ const { ...testData } = planData
74
+ delete testData[field]
75
+ const responseStub = [sandbox.stub()]
76
+ const planServiceStub = new PlanService()
77
+ sandbox.stub(planServiceStub, 'createPlan').resolves(responseStub)
78
+ try {
79
+ await planServiceStub.createPlanWithValidation(testData)
80
+ t.fail('createPlanWithValidation failed - missing required field')
81
+ } catch (err) {
82
+ t.equal(
83
+ err.toString(),
84
+ `Error: Required field \'${field}\' is missing`,
85
+ `Validation failed on required field: ${field} successfully`
86
+ )
87
+ }
88
+ sandbox.restore()
89
+ t.end()
90
+ })
91
+ })
92
+ }
93
+
94
+ function priceValidation () {
95
+ const badPriceData = [null, undefined, 'A string', -10, {}]
96
+ for (let ii = 0; ii < badPriceData.length; ii++) {
97
+ test(`Price validation should throw an error checking for: ${badPriceData[ii]}`, async t => {
98
+ t.plan(1)
99
+ const planData = {
100
+ name: 'testName',
101
+ key: 'test-key'
102
+ }
103
+ planData.price = badPriceData[ii]
104
+ const responseStub = [sandbox.stub()]
105
+ const planServiceStub = new PlanService()
106
+ sandbox.stub(planServiceStub, 'createPlan').resolves(responseStub)
107
+ try {
108
+ await planServiceStub.createPlanWithValidation(planData)
109
+ t.fail('Price validation successfully threw an error')
110
+ } catch (err) {
111
+ if (planData.price !== null && planData.price !== undefined) {
112
+ t.equal(
113
+ err.toString(),
114
+ 'Error: Price must be a positive number',
115
+ `Price validation detected bad price data: ${planData.price}`
116
+ )
117
+ } else {
118
+ t.equal(
119
+ err.toString(),
120
+ "Error: Required field 'price' is missing",
121
+ `Price validation detected bad price data: ${planData.price}`
122
+ )
123
+ }
124
+ }
125
+ sandbox.restore()
126
+ t.end()
127
+ })
128
+ }
129
+ }
130
+
131
+ function keyValidation () {
132
+ const badKeyData = [
133
+ 'CAPITALLETTERS',
134
+ 'Special @ Character',
135
+ 'under_score',
136
+ 'syntax!'
137
+ ]
138
+ for (let ii = 0; ii < badKeyData.length; ii++) {
139
+ test(`Key validation should throw an error checking for: ${badKeyData[ii]}`, async t => {
140
+ t.plan(1)
141
+ const planData = {
142
+ name: 'testName',
143
+ key: '',
144
+ price: 1.0
145
+ }
146
+ planData.key += badKeyData[ii]
147
+ const responseStub = [sandbox.stub()]
148
+ const planServiceStub = new PlanService()
149
+ sandbox.stub(planServiceStub, 'createPlan').resolves(responseStub)
150
+ try {
151
+ await planServiceStub.createPlanWithValidation(planData)
152
+ t.fail('Key validation successfully threw an error')
153
+ } catch (err) {
154
+ t.equal(
155
+ err.toString(),
156
+ 'Error: Plan key must contain only lowercase letters, numbers, and hyphens',
157
+ `Key validation detected bad price data: ${planData.key}`
158
+ )
159
+ }
160
+ sandbox.restore()
161
+ t.end()
162
+ })
163
+ }
164
+ }
165
+
166
+ keyValidation()
167
+ priceValidation()
168
+ planDataEmptyOrNotAnObject()
169
+ requiredFieldsMissing()
170
+ // #endregion
171
+
172
+ // #region Cleanup
173
+ test('teardown', t => {
174
+ sandbox.restore()
175
+ t.end()
176
+ })
177
+ // #endregion