1 | var proxyquire = require('proxyquire'),
|
2 | Sinon = require('sinon'),
|
3 | https = require('https'),
|
4 | events = require('events'),
|
5 | Mixpanel = require('../lib/mixpanel-node');
|
6 |
|
7 | var mock_now_time = new Date(2016, 1, 1).getTime(),
|
8 | six_days_ago_timestamp = mock_now_time - (1000 * 60 * 60 * 24 * 6);
|
9 |
|
10 | exports.import = {
|
11 | setUp: function(next) {
|
12 | this.mixpanel = Mixpanel.init('token', { secret: 'my api secret' });
|
13 | this.clock = Sinon.useFakeTimers(mock_now_time);
|
14 |
|
15 | Sinon.stub(this.mixpanel, 'send_request');
|
16 |
|
17 | next();
|
18 | },
|
19 |
|
20 | tearDown: function(next) {
|
21 | this.mixpanel.send_request.restore();
|
22 | this.clock.restore();
|
23 |
|
24 | next();
|
25 | },
|
26 |
|
27 | "calls send_request with correct endpoint and data": function(test) {
|
28 | var event = "test",
|
29 | time = six_days_ago_timestamp,
|
30 | props = { key1: 'val1' },
|
31 | expected_endpoint = "/import",
|
32 | expected_data = {
|
33 | event: 'test',
|
34 | properties: {
|
35 | key1: 'val1',
|
36 | token: 'token',
|
37 | time: time
|
38 | }
|
39 | };
|
40 |
|
41 | this.mixpanel.import(event, time, props);
|
42 |
|
43 | test.ok(
|
44 | this.mixpanel.send_request.calledWithMatch({ endpoint: expected_endpoint, data: expected_data }),
|
45 | "import didn't call send_request with correct arguments"
|
46 | );
|
47 |
|
48 | test.done();
|
49 | },
|
50 |
|
51 | "supports a Date instance greater than 5 days old": function(test) {
|
52 | var event = "test",
|
53 | time = new Date(six_days_ago_timestamp),
|
54 | props = { key1: 'val1' },
|
55 | expected_endpoint = "/import",
|
56 | expected_data = {
|
57 | event: 'test',
|
58 | properties: {
|
59 | key1: 'val1',
|
60 | token: 'token',
|
61 | time: six_days_ago_timestamp
|
62 | }
|
63 | };
|
64 |
|
65 | this.mixpanel.import(event, time, props);
|
66 |
|
67 | test.ok(
|
68 | this.mixpanel.send_request.calledWithMatch({ endpoint: expected_endpoint, data: expected_data }),
|
69 | "import didn't call send_request with correct arguments"
|
70 | );
|
71 |
|
72 | test.done();
|
73 | },
|
74 |
|
75 | "supports a Date instance less than 5 days old": function(test) {
|
76 | var event = "test",
|
77 | time = new Date(mock_now_time),
|
78 | props = { key1: 'val1' },
|
79 | expected_endpoint = "/import",
|
80 | expected_data = {
|
81 | event: 'test',
|
82 | properties: {
|
83 | key1: 'val1',
|
84 | token: 'token',
|
85 | time: mock_now_time
|
86 | }
|
87 | };
|
88 |
|
89 | this.mixpanel.import(event, time, props);
|
90 |
|
91 | test.ok(
|
92 | this.mixpanel.send_request.calledWithMatch({ endpoint: expected_endpoint, data: expected_data }),
|
93 | "import didn't call send_request with correct arguments"
|
94 | );
|
95 |
|
96 | test.done();
|
97 | },
|
98 |
|
99 | "supports a unix timestamp": function(test) {
|
100 | var event = "test",
|
101 | time = mock_now_time,
|
102 | props = { key1: 'val1' },
|
103 | expected_endpoint = "/import",
|
104 | expected_data = {
|
105 | event: 'test',
|
106 | properties: {
|
107 | key1: 'val1',
|
108 | token: 'token',
|
109 | time: time
|
110 | }
|
111 | };
|
112 |
|
113 | this.mixpanel.import(event, time, props);
|
114 |
|
115 | test.ok(
|
116 | this.mixpanel.send_request.calledWithMatch({ endpoint: expected_endpoint, data: expected_data }),
|
117 | "import didn't call send_request with correct arguments"
|
118 | );
|
119 |
|
120 | test.done();
|
121 | },
|
122 |
|
123 | "requires the time argument to be a number or Date": function(test) {
|
124 | test.doesNotThrow(this.mixpanel.import.bind(this, 'test', new Date()));
|
125 | test.doesNotThrow(this.mixpanel.import.bind(this, 'test', Date.now()));
|
126 | test.throws(
|
127 | this.mixpanel.import.bind(this, 'test', 'not a number or Date'),
|
128 | /`time` property must be a Date or Unix timestamp/,
|
129 | "import didn't throw an error when time wasn't a number or Date"
|
130 | );
|
131 | test.throws(
|
132 | this.mixpanel.import.bind(this, 'test'),
|
133 | /`time` property must be a Date or Unix timestamp/,
|
134 | "import didn't throw an error when time wasn't specified"
|
135 | );
|
136 |
|
137 | test.done();
|
138 | }
|
139 | };
|
140 |
|
141 | exports.import_batch = {
|
142 | setUp: function(next) {
|
143 | this.mixpanel = Mixpanel.init('token', { secret: 'my api secret' });
|
144 | this.clock = Sinon.useFakeTimers();
|
145 |
|
146 | Sinon.stub(this.mixpanel, 'send_request');
|
147 |
|
148 | next();
|
149 | },
|
150 |
|
151 | tearDown: function(next) {
|
152 | this.mixpanel.send_request.restore();
|
153 | this.clock.restore();
|
154 |
|
155 | next();
|
156 | },
|
157 |
|
158 | "calls send_request with correct endpoint, data, and method": function(test) {
|
159 | var expected_endpoint = "/import",
|
160 | event_list = [
|
161 | {event: 'test', properties: {key1: 'val1', time: 500 }},
|
162 | {event: 'test', properties: {key2: 'val2', time: 1000}},
|
163 | {event: 'test2', properties: {key2: 'val2', time: 1500}}
|
164 | ],
|
165 | expected_data = [
|
166 | {event: 'test', properties: {key1: 'val1', time: 500, token: 'token'}},
|
167 | {event: 'test', properties: {key2: 'val2', time: 1000, token: 'token'}},
|
168 | {event: 'test2', properties: {key2: 'val2', time: 1500, token: 'token'}}
|
169 | ];
|
170 |
|
171 | this.mixpanel.import_batch(event_list);
|
172 |
|
173 | test.ok(
|
174 | this.mixpanel.send_request.calledWithMatch({
|
175 | method: 'POST',
|
176 | endpoint: expected_endpoint,
|
177 | data: expected_data
|
178 | }),
|
179 | "import_batch didn't call send_request with correct arguments"
|
180 | );
|
181 |
|
182 | test.done();
|
183 | },
|
184 |
|
185 | "requires the time argument for every event": function(test) {
|
186 | var event_list = [
|
187 | {event: 'test', properties: {key1: 'val1', time: 500 }},
|
188 | {event: 'test', properties: {key2: 'val2', time: 1000}},
|
189 | {event: 'test2', properties: {key2: 'val2' }}
|
190 | ];
|
191 | test.throws(
|
192 | function() { this.mixpanel.import_batch(event_list); },
|
193 | "Import methods require you to specify the time of the event",
|
194 | "import didn't throw an error when time wasn't specified"
|
195 | );
|
196 |
|
197 | test.done();
|
198 | },
|
199 |
|
200 | "batches 50 events at a time": function(test) {
|
201 | var event_list = [];
|
202 | for (var ei = 0; ei < 130; ei++) {
|
203 | event_list.push({event: 'test', properties: {key1: 'val1', time: 500 + ei }});
|
204 | }
|
205 |
|
206 | this.mixpanel.import_batch(event_list);
|
207 |
|
208 | test.equals(
|
209 | 3, this.mixpanel.send_request.callCount,
|
210 | "import_batch didn't call send_request correct number of times"
|
211 | );
|
212 |
|
213 | test.done();
|
214 | }
|
215 | };
|
216 |
|
217 | exports.import_batch_integration = {
|
218 | setUp: function(next) {
|
219 | this.mixpanel = Mixpanel.init('token', { secret: 'my api secret' });
|
220 | this.clock = Sinon.useFakeTimers();
|
221 |
|
222 | Sinon.stub(https, 'request');
|
223 |
|
224 | this.http_emitter = new events.EventEmitter();
|
225 |
|
226 |
|
227 | this.res = [];
|
228 | for (var ri = 0; ri < 5; ri++) {
|
229 | this.res.push(new events.EventEmitter());
|
230 | https.request.onCall(ri).callsArgWith(1, this.res[ri]);
|
231 | https.request.onCall(ri).returns({
|
232 | write: function () {},
|
233 | end: function () {},
|
234 | on: function(event) {},
|
235 | });
|
236 | }
|
237 |
|
238 | this.event_list = [];
|
239 | for (var ei = 0; ei < 130; ei++) {
|
240 | this.event_list.push({event: 'test', properties: {key1: 'val1', time: 500 + ei }});
|
241 | }
|
242 |
|
243 | next();
|
244 | },
|
245 |
|
246 | tearDown: function(next) {
|
247 | https.request.restore();
|
248 | this.clock.restore();
|
249 |
|
250 | next();
|
251 | },
|
252 |
|
253 | "calls provided callback after all requests finish": function(test) {
|
254 | test.expect(2);
|
255 | this.mixpanel.import_batch(this.event_list, function(error_list) {
|
256 | test.equals(
|
257 | 3, https.request.callCount,
|
258 | "import_batch didn't call send_request correct number of times before callback"
|
259 | );
|
260 | test.equals(
|
261 | null, error_list,
|
262 | "import_batch returned errors in callback unexpectedly"
|
263 | );
|
264 | test.done();
|
265 | });
|
266 | for (var ri = 0; ri < 3; ri++) {
|
267 | this.res[ri].emit('data', '1');
|
268 | this.res[ri].emit('end');
|
269 | }
|
270 | },
|
271 |
|
272 | "passes error list to callback": function(test) {
|
273 | test.expect(1);
|
274 | this.mixpanel.import_batch(this.event_list, function(error_list) {
|
275 | test.equals(
|
276 | 3, error_list.length,
|
277 | "import_batch didn't return errors in callback"
|
278 | );
|
279 | test.done();
|
280 | });
|
281 | for (var ri = 0; ri < 3; ri++) {
|
282 | this.res[ri].emit('data', '0');
|
283 | this.res[ri].emit('end');
|
284 | }
|
285 | },
|
286 |
|
287 | "calls provided callback when options are passed": function(test) {
|
288 | test.expect(2);
|
289 | this.mixpanel.import_batch(this.event_list, {max_batch_size: 100}, function(error_list) {
|
290 | test.equals(
|
291 | 3, https.request.callCount,
|
292 | "import_batch didn't call send_request correct number of times before callback"
|
293 | );
|
294 | test.equals(
|
295 | null, error_list,
|
296 | "import_batch returned errors in callback unexpectedly"
|
297 | );
|
298 | test.done();
|
299 | });
|
300 | for (var ri = 0; ri < 3; ri++) {
|
301 | this.res[ri].emit('data', '1');
|
302 | this.res[ri].emit('end');
|
303 | }
|
304 | },
|
305 |
|
306 | "sends more requests when max_batch_size < 50": function(test) {
|
307 | test.expect(2);
|
308 | this.mixpanel.import_batch(this.event_list, {max_batch_size: 30}, function(error_list) {
|
309 | test.equals(
|
310 | 5, https.request.callCount,
|
311 | "import_batch didn't call send_request correct number of times before callback"
|
312 | );
|
313 | test.equals(
|
314 | null, error_list,
|
315 | "import_batch returned errors in callback unexpectedly"
|
316 | );
|
317 | test.done();
|
318 | });
|
319 | for (var ri = 0; ri < 5; ri++) {
|
320 | this.res[ri].emit('data', '1');
|
321 | this.res[ri].emit('end');
|
322 | }
|
323 | },
|
324 |
|
325 | "can set max concurrent requests": function(test) {
|
326 | var async_all_stub = Sinon.stub();
|
327 | var PatchedMixpanel = proxyquire('../lib/mixpanel-node', {
|
328 | './utils': {async_all: async_all_stub},
|
329 | });
|
330 | async_all_stub.callsArgWith(2, null);
|
331 | this.mixpanel = PatchedMixpanel.init('token', { secret: 'my api secret' });
|
332 |
|
333 | test.expect(2);
|
334 | this.mixpanel.import_batch(this.event_list, {max_batch_size: 30, max_concurrent_requests: 2}, function(error_list) {
|
335 |
|
336 |
|
337 |
|
338 |
|
339 | test.equals(
|
340 | 3, async_all_stub.callCount,
|
341 | "import_batch didn't batch concurrent https requests correctly"
|
342 | );
|
343 | test.equals(
|
344 | null, error_list,
|
345 | "import_batch returned errors in callback unexpectedly"
|
346 | );
|
347 | test.done();
|
348 | });
|
349 | for (var ri = 0; ri < 5; ri++) {
|
350 | this.res[ri].emit('data', '1');
|
351 | this.res[ri].emit('end');
|
352 | }
|
353 | },
|
354 |
|
355 | "behaves well without a callback": function(test) {
|
356 | test.expect(2);
|
357 | this.mixpanel.import_batch(this.event_list);
|
358 | test.equals(
|
359 | 3, https.request.callCount,
|
360 | "import_batch didn't call send_request correct number of times"
|
361 | );
|
362 | this.mixpanel.import_batch(this.event_list, {max_batch_size: 100});
|
363 | test.equals(
|
364 | 5, https.request.callCount,
|
365 | "import_batch didn't call send_request correct number of times"
|
366 | );
|
367 | test.done();
|
368 | }
|
369 | };
|