@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.
- package/dist/cjs/services/PlanService.js +90 -10
- package/dist/cjs/utils/services.js +1 -1
- package/dist/esm/index.js +91 -11
- package/dist/esm/services/PlanService.js +90 -10
- package/dist/esm/services/index.js +90 -10
- package/dist/esm/utils/services.js +1 -1
- package/dist/node/services/PlanService.js +90 -10
- package/dist/node/utils/services.js +1 -1
- package/package.json +7 -6
- package/src/services/PlanService.js +119 -14
- package/src/services/tests/PlanService/createPlan.test.js +92 -0
- package/src/services/tests/PlanService/createPlanWithValidation.test.js +177 -0
- package/src/services/tests/PlanService/deletePlan.test.js +92 -0
- package/src/services/tests/PlanService/getAdminPlans.test.js +84 -0
- package/src/services/tests/PlanService/getPlan.test.js +50 -0
- package/src/services/tests/PlanService/getPlanWithValidation.test.js +85 -0
- package/src/services/tests/PlanService/getPlans.test.js +53 -0
- package/src/services/tests/PlanService/getPlansWithValidation.test.js +48 -0
- package/src/services/tests/PlanService/initializePlans.test.js +75 -0
- package/src/services/tests/PlanService/updatePlan.test.js +111 -0
- package/src/services/tests/PlanService/updatePlanWithValidation.test.js +188 -0
- package/src/utils/services.js +1 -1
|
@@ -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", "
|
|
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 (
|
|
192
|
-
throw new Error("
|
|
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
|
|
210
|
-
|
|
211
|
-
|
|
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.
|
|
314
|
+
const plans = await this.getPlansWithPricing();
|
|
236
315
|
return plans.filter((plan) => {
|
|
237
|
-
|
|
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.
|
|
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.
|
|
50
|
-
"@domql/utils": "^2.33.
|
|
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.
|
|
54
|
-
"@symbo.ls/socket": "^2.33.
|
|
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": "
|
|
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', '
|
|
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
|
-
//
|
|
209
|
-
if (
|
|
210
|
-
throw new Error('
|
|
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
|
-
//
|
|
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
|
-
//
|
|
233
|
-
if (planData
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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
|
-
|
|
368
|
+
// Use enhanced pricing information from /plans/pricing
|
|
369
|
+
const plans = await this.getPlansWithPricing()
|
|
265
370
|
return plans.filter(plan => {
|
|
266
|
-
const price = plan
|
|
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
|