-
Notifications
You must be signed in to change notification settings - Fork 0
/
recurlyJsMock.ts
305 lines (273 loc) · 8.88 KB
/
recurlyJsMock.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
import * as merge from "deepmerge";
import fileUrl = require("file-url");
import * as path from "path";
import * as phantom from "phantom";
import * as request from "request";
declare var window: any;
declare var recurly: any;
declare var $: any;
/**
* Can be used to mock Recurly.js functionality during automated testing.
*/
export class RecurlyJsMock {
/**
* Spins up a headless browser instance, creates a Recurly.js compliant form,
* inputs the desired data and returns the Recurly.js token.
*
* @param publicApiKey The public API key of the Recurly account to be used
* @param logLevel The desired log level. Can be debug, info, warn or error.
* @param details The payment information to be used to fill out the form
*/
public static getTokenSingleRun(
publicApiKey: string,
logLevel: LogLevel = LogLevel.Error,
details?: PaymentDetails): Promise<RecurlyToken> {
return new Promise<RecurlyToken>(async (resolve, reject) => {
/*
* The PhantomJs instance has to be run with web security turned off in order
* to allow accessing iframe contents.
*/
const instance = await phantom.create(
["--web-security=no"],
{ logLevel: RecurlyJsMock.getLogLevelConfiguration(logLevel) }
);
const page = await instance.createPage();
/*
* Listen for callbacks from client JS. This is used to detect when Recurly.js
* has finished processing payment information and the token is ready to
* be collected.
*/
await page.on("onCallback", async (data: Callback) => {
switch (data.status) {
case "success":
resolve(data.token);
await instance.exit();
break;
case "error":
reject(data.err);
await instance.exit(1);
break;
default:
reject();
await instance.exit(2);
break;
}
});
/*
* Listen to resource requested events for debugging purposes.
*/
await page.on("onResourceRequested", (requestData) => {
instance.logger.debug("Requesting", requestData.url);
});
/*
* Load the html form.
*/
const status = await page.open(fileUrl(path.resolve(__dirname, "recurlyJsMock.html")));
instance.logger.debug(status);
/*
* Load required JS libraries.
*/
await page.includeJs("http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js");
await page.includeJs("https://js.recurly.com/v4/recurly.js");
/*
* Configure Recurly.js
*/
await page.evaluate((key) => {
recurly.configure({
publicKey: key
});
}, publicApiKey);
const content = await page.property("content");
instance.logger.debug(content);
// Listen for Recurly.js
await page.evaluate(() => {
// On form submit, we stop submission to get the token
$("form").on("submit", function(event) {
// Prevent the form from submitting while we retrieve the token from Recurly
event.preventDefault();
const form = this;
// Now we call recurly.token with the form. It goes to Recurly servers
// to tokenize the credit card information, then injects the token into the
// data-recurly="token" field above
recurly.token(form, (err, token) => {
let result: Callback;
if (err) {
result = { status: "error", err };
} else {
result = { status: "success", token };
}
window.callPhantom(result);
});
});
});
/*
* Fill out form and submit
*/
await page.evaluate((paymentDetails: PaymentDetails) => {
$("#number iframe").first().contents().find("input").first().val(paymentDetails.cardData.number);
$("#month iframe").first().contents().find("input").first().val(paymentDetails.cardData.month);
$("#year iframe").first().contents().find("input").first().val(paymentDetails.cardData.year);
$("#first_name").val(paymentDetails.cardData.firstName);
$("#last_name").val(paymentDetails.cardData.lastName);
$("#address1").val(paymentDetails.address1);
$("#city").val(paymentDetails.city);
$("#country").val(paymentDetails.country);
$("#postal_code").val(paymentDetails.postalCode);
$("form").submit();
}, RecurlyJsMock.mergeWithDefaults(details));
});
}
/**
* Returns the Recurly.js token without running a headless browser.
*
* @param publicApiKey The public API key of the Recurly account to be used
* @param logLevel The desired log level. Can be debug, info, warn or error.
* @param details The payment information to be used
*/
public static getTokenAPICall(
publicApiKey: string,
logLevel: LogLevel = LogLevel.Error,
details?: PaymentDetails): Promise<RecurlyToken> {
if (details == null) {
details = {};
}
return new Promise<RecurlyToken>((resolve, reject) => {
const url = "https://api.recurly.com/js/v1/token";
const data = RecurlyJsMock.mergeWithDefaults(details);
const formData = {
address1: data.address1 ? data.address1 : "",
address2: data.address2 ? data.address2 : "",
city: data.city ? data.city : "",
country: data.country ? data.country : "",
cvv: data.cardData.cvv ? data.cardData.cvv : "",
first_name: data.cardData.firstName ? data.cardData.firstName : "",
key: publicApiKey,
last_name: data.cardData.lastName ? data.cardData.lastName : "",
month: data.cardData.month ? data.cardData.month : "",
number: data.cardData.number ? data.cardData.number : "",
phone: data.phone ? data.phone : "",
postal_code: data.postalCode ? data.postalCode : "",
state: data.state ? data.state : "",
vat_number: data.vatNumber ? data.vatNumber : "",
year: data.cardData.year ? data.cardData.year : ""
};
request.post({ url, formData }, (err, res, body) => {
if (err || res.statusCode !== 200) {
reject(err);
} else {
body = JSON.parse(body);
// Recurly API fails to respond with proper error code
if (body.hasOwnProperty("error")) {
reject(body.error);
} else {
resolve(body);
}
}
});
});
}
private static getLogLevelConfiguration(logLevel: LogLevel): string {
switch (logLevel) {
case LogLevel.Debug:
return "debug";
case LogLevel.Info:
return "info";
case LogLevel.Warn:
return "warn";
case LogLevel.Error:
return "error";
default:
return "error";
}
}
private static mergeWithDefaults(details: PaymentDetails): PaymentDetails {
const defaults: PaymentDetails = {
address1: "Street 1",
cardData: {
cvv: 123,
firstName: "John",
lastName: "Doe",
month: 10,
number: "4111 1111 1111 1111",
year: 20
},
city: "Vienna",
country: "AT",
postalCode: "1080"
};
RecurlyJsMock.deleteUndefinedProperties(details);
if (!details) {
return defaults;
} else {
return merge(defaults, details);
}
}
/**
* Deletes undefined values from given details recursively including nested objects.
*/
private static deleteUndefinedProperties(obj: any) {
Object.keys(obj).forEach((key) => {
if (obj[key] && typeof obj[key] === "object") {
RecurlyJsMock.deleteUndefinedProperties(obj[key]);
} else if (obj[key] == null) {
delete obj[key];
}
});
return obj;
}
public startDaemon(publicApiKey: string, logLevel: LogLevel = LogLevel.Error): Promise<boolean> {
throw new Error("Not implemented.");
}
public stopDaemon(): Promise<boolean> {
throw new Error("Not implemented.");
}
public getToken(details: PaymentDetails): Promise<boolean> {
throw new Error("Not implemented.");
}
}
interface Callback {
status: string;
err?: any;
token?: {
id: string;
};
}
export interface RecurlyToken {
id: string;
}
interface RecurlyError {
name: string;
code: string;
message: string;
fields?: string[];
}
export enum LogLevel {
Debug = 0,
Info = 1,
Warn = 2,
Error = 3
}
/**
* Payment data handled by Recurly.js.
*
* @see https://dev.recurly.com/docs/getting-started-1#section-card-data
* @see https://dev.recurly.com/docs/getting-started-1#section-billing-address
*/
export interface PaymentDetails {
address1?: string;
address2?: string;
cardData?: {
cvv?: number;
firstName?: string;
lastName?: string;
month?: number;
number?: string;
year?: number;
};
city?: string;
country?: string;
phone?: string;
postalCode?: string; // ISO 3166-1 alpha-2 country code
state?: string;
vatNumber?: string;
}