Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/util/misc.test.ts
5801 views
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import seedrandom from "seedrandom";
7
import * as misc from "./misc";
8
9
describe("academic domain", () => {
10
const ia = misc.isAcademic;
11
12
test("denies non academics", () => {
13
expect(ia("[email protected]")).toBe(false);
14
expect(ia("[email protected]")).toBe(false);
15
expect(ia("[email protected]")).toBe(false);
16
expect(ia("[email protected]")).toBe(false);
17
expect(ia("[email protected]")).toBe(false);
18
});
19
20
test("detects academics", () => {
21
expect(ia("[email protected]")).toBe(true);
22
expect(ia("[email protected]")).toBe(true);
23
expect(ia("[email protected]")).toBe(true);
24
expect(ia("[email protected]")).toBe(true);
25
expect(ia("[email protected]")).toBe(true);
26
});
27
});
28
29
describe("rpad_html", () => {
30
const rp = misc.rpad_html;
31
const round1 = misc.round1;
32
test("0", () => expect(rp(0, 3)).toEqual("  0"));
33
test("99", () => expect(rp(99, 3)).toEqual(" 99"));
34
test("4444-5", () => expect(rp(4444, 5)).toEqual(" 4444"));
35
test("6666-4", () => expect(rp(6666, 4)).toEqual("6666"));
36
test("1000-4", () => expect(rp(1000, 4)).toEqual("1000"));
37
test("1000-3", () => expect(rp(1000, 3)).toEqual("1000"));
38
test("pi-1", () => expect(rp(3.1415, 4, round1)).toEqual(" 3.1"));
39
});
40
41
describe("path_split", () => {
42
const ps = misc.path_split;
43
44
test("full path", () =>
45
expect(ps("foo/bar")).toEqual({ head: "foo", tail: "bar" }));
46
47
test("filename", () =>
48
expect(ps("foo.bar.baz")).toEqual({ head: "", tail: "foo.bar.baz" }));
49
50
test("dirname", () => expect(ps("foo/")).toEqual({ head: "foo", tail: "" }));
51
52
test("abspath", () =>
53
expect(ps("/HOME/USER/DIR")).toEqual({
54
head: "/HOME/USER",
55
tail: "DIR",
56
}));
57
58
test("ROOT", () => expect(ps("/")).toEqual({ head: "", tail: "" }));
59
});
60
61
describe("contains_url", () => {
62
const cu = misc.contains_url;
63
64
test("normal html is fine", () =>
65
expect(cu("<h2>foo</h2><div>bar</div>")).toBe(false));
66
67
test("detects URLs", () => {
68
expect(cu("<p><a href='http://foo.com'>click me</a></p>")).toBe(true);
69
expect(cu("abc bar.com xyz")).toBe(true);
70
expect(cu("abc www.buy.me xyz")).toBe(true);
71
});
72
});
73
74
describe("date object some time ago", () => {
75
test("roughly 10 mins ago", () => {
76
const res = misc.minutes_ago(10);
77
const diff = new Date().getTime() - res.getTime();
78
expect(diff).toBeLessThan(10 * 60 * 1000 + 100);
79
expect(diff).toBeGreaterThan(10 * 60 * 1000 - 100);
80
});
81
test("2 months ago", () => {
82
const res = misc.months_ago(2);
83
const diff = new Date().getTime() - res.getTime();
84
expect(diff).toBeLessThan(2 * 31 * 24 * 60 * 60 * 1000);
85
expect(diff).toBeGreaterThan(2 * 30 * 24 * 60 * 60 * 1000);
86
});
87
});
88
89
describe("how_long_ago_m", () => {
90
test("10 min ago by Date", () => {
91
const past: Date = misc.minutes_ago(10);
92
const diff = misc.how_long_ago_m(past);
93
expect(diff).toBeLessThan(10.1);
94
expect(diff).toBeGreaterThan(9.9);
95
});
96
97
test("10 min ago by timestamp", () => {
98
const past: number = misc.minutes_ago(10).getTime();
99
const diff = misc.how_long_ago_m(past);
100
expect(diff).toBeLessThan(10.1);
101
expect(diff).toBeGreaterThan(9.9);
102
});
103
});
104
105
describe("json patch test", () => {
106
const j = misc.test_valid_jsonpatch;
107
test("empty array is fine", () => expect(j([])).toBe(true));
108
test("a complete example is fine", () => {
109
// taken from https://jsonpatch.com/
110
const patch = [
111
{ op: "add", path: "/biscuits/1", value: { name: "Ginger Nut" } },
112
{ op: "remove", path: "/biscuits" },
113
{ op: "remove", path: "/biscuits/0" },
114
{ op: "replace", path: "/biscuits/0/name", value: "Chocolate Digestive" },
115
{ op: "copy", from: "/biscuits/0", path: "/best_biscuit" },
116
{ op: "move", from: "/biscuits", path: "/cookies" },
117
{ op: "test", path: "/best_biscuit/name", value: "Choco Leibniz" },
118
];
119
120
expect(j(patch)).toBe(true);
121
});
122
test("fails with broken examples", () => {
123
expect(
124
j({ op: "add", path: "/biscuits/1", value: { name: "Ginger Nut" } }),
125
).toBe(false);
126
expect(j([{ opp: "remove", path: "/biscuits" }])).toBe(false);
127
expect(j([{ path: "/biscuits/0" }])).toBe(false);
128
expect(j([{ op: "replacce", path: "/biscuits/0/name" }])).toBe(false);
129
});
130
});
131
132
test("firstLetterUppercase", () => {
133
const s = misc.firstLetterUppercase;
134
expect(s(undefined)).toBe("");
135
expect(s("")).toBe("");
136
expect(s("a")).toBe("A");
137
expect(s("abc")).toBe("Abc");
138
expect(s("ABC")).toBe("ABC");
139
expect(s("aBC")).toBe("ABC");
140
});
141
142
test("hexColorToRGBA", () => {
143
const c1 = misc.hexColorToRGBA("#000000");
144
expect(c1).toEqual("rgb(0,0,0)");
145
const c2 = misc.hexColorToRGBA("#ffffff", 0.5);
146
expect(c2).toEqual("rgba(255,255,255,0.5)");
147
});
148
149
test("strictMod", () => {
150
const mod = misc.strictMod;
151
expect(mod(0, 3)).toBe(0);
152
expect(mod(1, 3)).toBe(1);
153
expect(mod(-2, 3)).toBe(1);
154
expect(mod(-3, 3)).toBe(0);
155
expect(mod(-1, 10)).toBe(9);
156
});
157
158
test("EDITOR_PREFIX", () => {
159
// don't change it, because codebase is not using the global variable everywhere
160
expect(misc.EDITOR_PREFIX).toBe("editor-");
161
});
162
163
describe("is_bad_latex_filename", () => {
164
test("allows filenames without double spaces or quotes", () => {
165
expect(misc.is_bad_latex_filename("paper.tex")).toBe(false);
166
expect(misc.is_bad_latex_filename("my paper.tex")).toBe(false);
167
expect(misc.is_bad_latex_filename("folder/subfolder/file.tex")).toBe(false);
168
});
169
170
test("rejects filenames with double spaces", () => {
171
expect(misc.is_bad_latex_filename("my paper.tex")).toBe(true);
172
expect(misc.is_bad_latex_filename("folder/bad name/file.tex")).toBe(true);
173
});
174
175
test("rejects filenames with single quotes", () => {
176
expect(misc.is_bad_latex_filename("author's-notes.tex")).toBe(true);
177
expect(misc.is_bad_latex_filename("folder/author's-notes.tex")).toBe(true);
178
});
179
});
180
181
describe("test code for displaying numbers as currency with 2 or sometimes 3 decimals of precision", () => {
182
const { currency } = misc;
183
it("displays 1.23", () => {
184
expect(currency(1.23)).toBe("$1.23");
185
});
186
187
it("displays 0.0094 with 3 digits (not 2), but only because n is less than 0.01", () => {
188
expect(currency(0.0094)).toBe("$0.009");
189
});
190
191
it("displays 0.1941 with 2, because n is not less than 0.01", () => {
192
expect(currency(0.1941)).toBe("$0.19");
193
});
194
it("displays 0.01941 with 2, because n is not less than 0.01", () => {
195
expect(currency(0.01941)).toBe("$0.02");
196
});
197
198
it("displays 0.0941 with 2 digits if second argument specifies that", () => {
199
expect(currency(0.0941, 2)).toBe("$0.09");
200
});
201
202
it("displays 0.086 with 2 digits if second argument specifies that, and it is rounded to nearest", () => {
203
expect(currency(0.086, 2)).toBe("$0.09");
204
});
205
206
it("displays 0.083 with 2 digits if second argument specifies that, and it is rounded to nearest (NOT up)", () => {
207
expect(currency(0.083, 2)).toBe("$0.08");
208
});
209
210
it("always includes at least 2 decimals", () => {
211
expect(currency(10)).toBe("$10.00");
212
});
213
});
214
215
describe("smallIntegerToEnglishWord", () => {
216
it("handles floats", () => {
217
expect(misc.smallIntegerToEnglishWord(1.2)).toBe(1.2);
218
});
219
220
it("handles 0", () => {
221
expect(misc.smallIntegerToEnglishWord(0)).toBe("zero");
222
});
223
224
it("handles 1", () => {
225
expect(misc.smallIntegerToEnglishWord(1)).toBe("one");
226
});
227
228
it("handles 17", () => {
229
expect(misc.smallIntegerToEnglishWord(17)).toBe("seventeen");
230
});
231
232
it("handles negative numbers", () => {
233
expect(misc.smallIntegerToEnglishWord(-1)).toBe(-1);
234
});
235
});
236
237
describe("test round2up and round2down for various inputs", () => {
238
const { round2up, round2down } = misc;
239
it("round2up tests -- uses the decimal representation (not internal binary))", () => {
240
// see https://github.com/sagemathinc/cocalc/issues/7220
241
expect(round2up(20.01)).toBe(20.01);
242
expect(round2up(20.011)).toBe(20.02);
243
expect(round2up(20.01999)).toBe(20.02);
244
expect(round2up(4.73)).toBe(4.73);
245
expect(round2up(4.731)).toBe(4.74);
246
expect(round2up(4.736)).toBe(4.74);
247
});
248
249
it("round2down tests -- uses the decimal representation (not internal binary))", () => {
250
// see https://github.com/sagemathinc/cocalc/issues/7220
251
expect(round2down(20.01)).toBe(20.01);
252
expect(round2down(20.011)).toBe(20.01);
253
expect(round2down(20.019)).toBe(20.01);
254
expect(round2down(4.73)).toBe(4.73);
255
expect(round2down(4.731)).toBe(4.73);
256
expect(round2down(4.736)).toBe(4.73);
257
});
258
259
it("a random test of 1000 cases", () => {
260
let seed = "my-seed";
261
let rng = seedrandom(seed);
262
263
for (let i = 0; i < 1000; i++) {
264
let randomNum = rng(); // Returns a number between 0 and 1
265
// Perform your tests with randomNum
266
// For example:
267
expect(round2up(randomNum)).toBeGreaterThanOrEqual(randomNum);
268
expect(round2up(randomNum)).toBeLessThan(randomNum + 0.01);
269
expect(round2down(randomNum)).toBeLessThanOrEqual(randomNum);
270
expect(round2down(randomNum)).toBeGreaterThan(randomNum - 0.01);
271
}
272
});
273
});
274
275
describe("numToOrdinal", () => {
276
const { numToOrdinal } = misc;
277
it("appends proper suffixes", () => {
278
expect(numToOrdinal(1)).toBe("1st");
279
expect(numToOrdinal(2)).toBe("2nd");
280
expect(numToOrdinal(3)).toBe("3rd");
281
expect(numToOrdinal(4)).toBe("4th");
282
expect(numToOrdinal(5)).toBe("5th");
283
expect(numToOrdinal(6)).toBe("6th");
284
expect(numToOrdinal(7)).toBe("7th");
285
expect(numToOrdinal(8)).toBe("8th");
286
expect(numToOrdinal(9)).toBe("9th");
287
expect(numToOrdinal(10)).toBe("10th");
288
expect(numToOrdinal(11)).toBe("11th");
289
expect(numToOrdinal(12)).toBe("12th");
290
expect(numToOrdinal(13)).toBe("13th");
291
expect(numToOrdinal(21)).toBe("21st");
292
expect(numToOrdinal(22)).toBe("22nd");
293
expect(numToOrdinal(23)).toBe("23rd");
294
expect(numToOrdinal(24)).toBe("24th");
295
expect(numToOrdinal(42)).toBe("42nd");
296
expect(numToOrdinal(101)).toBe("101st");
297
expect(numToOrdinal(202)).toBe("202nd");
298
expect(numToOrdinal(303)).toBe("303rd");
299
expect(numToOrdinal(1000)).toBe("1000th");
300
});
301
it("Falls back in other cases", () => {
302
expect(numToOrdinal(-1)).toBe("-1th");
303
});
304
});
305
306
describe("hoursToTimeIntervalHuman", () => {
307
const { hoursToTimeIntervalHuman } = misc;
308
it("converts nicely", () => {
309
expect(hoursToTimeIntervalHuman(1)).toBe("1 hour");
310
expect(hoursToTimeIntervalHuman(13.333)).toBe("13.3 hours");
311
expect(hoursToTimeIntervalHuman(13.888)).toBe("13.9 hours");
312
expect(hoursToTimeIntervalHuman(24)).toBe("1 day");
313
expect(hoursToTimeIntervalHuman(24 * 7)).toBe("1 week");
314
expect(hoursToTimeIntervalHuman(2)).toBe("2 hours");
315
expect(hoursToTimeIntervalHuman(2 * 24)).toBe("2 days");
316
expect(hoursToTimeIntervalHuman(5 * 7 * 24)).toBe("5 weeks");
317
expect(hoursToTimeIntervalHuman(2.5111 * 24)).toBe("2.5 days");
318
expect(hoursToTimeIntervalHuman(2.5111 * 24 * 7)).toBe("2.5 weeks");
319
});
320
});
321
322
describe("tail", () => {
323
const s = `
324
foo
325
bar
326
baz
327
abc
328
xyz
329
test 123`;
330
const { tail } = misc;
331
it("return the last 3 lines", () => {
332
const t = tail(s, 3);
333
expect(t.split("\n").length).toEqual(3);
334
expect(t.startsWith("abc")).toBe(true);
335
});
336
it("return the last line", () => {
337
const t = tail("foo", 3);
338
expect(t.split("\n").length).toEqual(1);
339
expect(t).toEqual("foo");
340
});
341
});
342
343
describe("suggest_duplicate_filename", () => {
344
const dup = misc.suggest_duplicate_filename;
345
it("works with numbers", () => {
346
expect(dup("filename-1.test")).toBe("filename-2.test");
347
expect(dup("filename-99.test")).toBe("filename-100.test");
348
expect(dup("filename_99.test")).toBe("filename_100.test");
349
});
350
it("handles leading zeros", () => {
351
// handles leading 0's properly: https://github.com/sagemathinc/cocalc/issues/2973
352
expect(dup("filename_001.test")).toBe("filename_002.test");
353
});
354
it("works also without", () => {
355
expect(dup("filename-test")).toBe("filename-test-1");
356
expect(dup("filename-xxx.test")).toBe("filename-xxx-1.test");
357
expect(dup("bla")).toBe("bla-1");
358
expect(dup("foo.bar")).toBe("foo-1.bar");
359
});
360
it("also works with weird corner cases", () => {
361
expect(dup("asdf-")).toBe("asdf--1");
362
});
363
});
364
365
describe("is_valid email_address", () => {
366
const ivea = misc.is_valid_email_address;
367
test("valid", () => {
368
expect(ivea("[email protected]")).toBe(true);
369
expect(ivea("[email protected]")).toBe(true);
370
expect(ivea("[email protected]")).toBe(true);
371
expect(ivea("[email protected]")).toBe(true);
372
expect(ivea("[email protected]")).toBe(true);
373
expect(ivea("[email protected]")).toBe(true);
374
expect(ivea("[email protected]")).toBe(true);
375
expect(ivea("[email protected]")).toBe(true);
376
expect(ivea("[email protected]")).toBe(true);
377
expect(ivea("[email protected]")).toBe(true);
378
expect(ivea("[email protected]")).toBe(true);
379
expect(ivea("[email protected]")).toBe(true);
380
});
381
test("invalid", () => {
382
expect(ivea(123)).toBe(false);
383
expect(ivea({})).toBe(false);
384
expect(ivea([])).toBe(false);
385
expect(ivea(null)).toBe(false);
386
expect(ivea(undefined)).toBe(false);
387
expect(ivea("abc")).toBe(false);
388
expect(ivea("abc@[email protected]")).toBe(false);
389
expect(ivea("foo@bar.")).toBe(false);
390
expect(ivea("[email protected]")).toBe(false);
391
expect(ivea("[email protected]")).toBe(false);
392
expect(ivea("@bar.com")).toBe(false);
393
expect(ivea("foo@")).toBe(false);
394
expect(ivea("foo")).toBe(false);
395
expect(ivea("foo [email protected]")).toBe(false);
396
expect(ivea("foo@[email protected]")).toBe(false);
397
});
398
});
399
400
describe("isValidAnonymousID", () => {
401
const isValid = misc.isValidAnonymousID;
402
403
it("should accept valid IPv4 addresses", () => {
404
expect(isValid("192.168.1.1")).toBe(true);
405
expect(isValid("10.23.66.8")).toBe(true);
406
});
407
408
it("should accept valid IPv6 addresses", () => {
409
expect(isValid("::1")).toBe(true);
410
expect(isValid("2001:db8::1")).toBe(true);
411
expect(isValid("fe80::1")).toBe(true);
412
expect(isValid("2001:0db8:85a3:0000:0000:8a2e:0370:7334")).toBe(true);
413
});
414
415
it("should accept valid UUIDs", () => {
416
expect(isValid("123e4567-e89b-12d3-a456-426614174000")).toBe(true);
417
});
418
419
it("should accept strings with minimum length", () => {
420
expect(isValid("abc")).toBe(true);
421
});
422
423
it("should reject empty strings", () => {
424
expect(isValid("")).toBe(false);
425
});
426
427
it("should reject strings shorter than 3 characters", () => {
428
expect(isValid("ab")).toBe(false);
429
});
430
431
it("should reject null and undefined", () => {
432
expect(isValid(null)).toBe(false);
433
expect(isValid(undefined)).toBe(false);
434
});
435
436
it("should reject non-string types", () => {
437
expect(isValid(123)).toBe(false);
438
expect(isValid(true)).toBe(false);
439
expect(isValid({})).toBe(false);
440
expect(isValid([])).toBe(false);
441
expect(isValid(new Date())).toBe(false);
442
});
443
});
444
445
describe("secure_random_token", () => {
446
const { secure_random_token } = misc;
447
448
it("should return a token with default length of 16", () => {
449
const token = secure_random_token();
450
expect(token.length).toBe(16);
451
});
452
453
it("should return a string with the specified length", () => {
454
expect(secure_random_token(8).length).toBe(8);
455
expect(secure_random_token(32).length).toBe(32);
456
expect(secure_random_token(64).length).toBe(64);
457
});
458
459
it("should return empty string for length 0", () => {
460
const token = secure_random_token(0);
461
expect(token).toBe("");
462
});
463
464
it("should only contain characters from the default BASE58 alphabet", () => {
465
const BASE58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
466
const token = secure_random_token(100);
467
for (const char of token) {
468
expect(BASE58.includes(char)).toBe(true);
469
}
470
});
471
472
it("should only contain characters from a custom alphabet", () => {
473
const customAlphabet = "ABCD";
474
const token = secure_random_token(50, customAlphabet);
475
for (const char of token) {
476
expect(customAlphabet.includes(char)).toBe(true);
477
}
478
});
479
480
it("should have reasonable diversity (at least 3 different chars in 16 chars)", () => {
481
const token = secure_random_token(16);
482
const uniqueChars = new Set(token.split(""));
483
expect(uniqueChars.size).toBeGreaterThanOrEqual(3);
484
});
485
486
it("should throw error when alphabet is empty", () => {
487
expect(() => secure_random_token(10, "")).toThrow(
488
"impossible, since alphabet is empty",
489
);
490
});
491
492
it("should work with single-character alphabet", () => {
493
const token = secure_random_token(10, "X");
494
expect(token).toBe("XXXXXXXXXX");
495
});
496
497
it("should generate different tokens on successive calls", () => {
498
const token1 = secure_random_token(16);
499
const token2 = secure_random_token(16);
500
// With 93 bits of randomness, collision is astronomically unlikely
501
expect(token1).not.toBe(token2);
502
});
503
});
504
505