@20syldev/api 4.0.0 → 4.2.0

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.
@@ -0,0 +1,57 @@
1
+ import { test, describe } from 'node:test';
2
+ import { strict as assert } from 'node:assert';
3
+ import dice from '../../src/modules/v4/dice.js';
4
+
5
+ describe('dice', () => {
6
+ test('parses 2d6+3 notation', () => {
7
+ const result = dice('2d6+3');
8
+ assert.equal(result.count, 2);
9
+ assert.equal(result.sides, 6);
10
+ assert.equal(result.modifier, 3);
11
+ assert.equal(result.results.length, 2);
12
+ });
13
+
14
+ test('total includes positive modifier', () => {
15
+ const result = dice('1d2+10');
16
+ assert.equal(result.modifier, 10);
17
+ assert.ok(result.total === 11 || result.total === 12);
18
+ });
19
+
20
+ test('negative modifier', () => {
21
+ const result = dice('1d2-5');
22
+ assert.equal(result.modifier, -5);
23
+ assert.ok(result.total === -4 || result.total === -3);
24
+ });
25
+
26
+ test('default count of 1 when omitted', () => {
27
+ const result = dice('d20');
28
+ assert.equal(result.count, 1);
29
+ assert.equal(result.sides, 20);
30
+ });
31
+
32
+ test('all results are within 1..sides', () => {
33
+ for (let i = 0; i < 50; i++) {
34
+ const result = dice('5d10');
35
+ for (const r of result.results) {
36
+ assert.ok(r >= 1 && r <= 10, `result ${r} out of range`);
37
+ }
38
+ }
39
+ });
40
+
41
+ test('handles space instead of +', () => {
42
+ const result = dice('2d6 3');
43
+ assert.equal(result.modifier, 3);
44
+ });
45
+
46
+ test('throws on invalid notation', () => {
47
+ assert.throws(() => dice('2x6'), /Invalid notation/);
48
+ });
49
+
50
+ test('throws on too many dice', () => {
51
+ assert.throws(() => dice('200d6'), /between 1 and 100/);
52
+ });
53
+
54
+ test('throws on invalid sides', () => {
55
+ assert.throws(() => dice('2d1'), /between 2 and 1000/);
56
+ });
57
+ });
@@ -0,0 +1,47 @@
1
+ import { test, describe } from 'node:test';
2
+ import { strict as assert } from 'node:assert';
3
+ import domain from '../../src/modules/v4/domain.js';
4
+
5
+ describe('domain', () => {
6
+ test('returns expected fields', () => {
7
+ const result = domain();
8
+ assert.ok('domain' in result);
9
+ assert.ok('full_domain' in result);
10
+ assert.ok('ip_address' in result);
11
+ assert.ok('dns_servers' in result);
12
+ assert.ok('country' in result);
13
+ });
14
+
15
+ test('domain has TLD', () => {
16
+ const result = domain();
17
+ assert.match(
18
+ result.domain as string,
19
+ /\.(com|fr|eu|dev|net|org|io|tech|biz|info|co|app|store|online|shop|tv)$/,
20
+ );
21
+ });
22
+
23
+ test('full_domain starts with subdomain', () => {
24
+ const result = domain();
25
+ const full = result.full_domain as string;
26
+ const dom = result.domain as string;
27
+ assert.ok(full.endsWith(dom));
28
+ });
29
+
30
+ test('ip_address is non-empty array', () => {
31
+ const result = domain();
32
+ const ips = result.ip_address as string[];
33
+ assert.ok(Array.isArray(ips));
34
+ assert.ok(ips.length >= 1);
35
+ });
36
+
37
+ test('seo_score in range 0..99', () => {
38
+ const result = domain();
39
+ const score = result.seo_score as number;
40
+ assert.ok(score >= 0 && score < 100);
41
+ });
42
+
43
+ test('ssl_certified is boolean', () => {
44
+ const result = domain();
45
+ assert.equal(typeof result.ssl_certified, 'boolean');
46
+ });
47
+ });
@@ -0,0 +1,108 @@
1
+ import { test, describe } from 'node:test';
2
+ import { strict as assert } from 'node:assert';
3
+ import {
4
+ base64encode,
5
+ base64decode,
6
+ urlencode,
7
+ urldecode,
8
+ morse,
9
+ unmorse,
10
+ rot13,
11
+ caesar,
12
+ binary,
13
+ unbinary,
14
+ } from '../../src/modules/v4/encode.js';
15
+
16
+ describe('encode', () => {
17
+ describe('base64', () => {
18
+ test('encodes ASCII text', () => {
19
+ assert.equal(base64encode('hello'), 'aGVsbG8=');
20
+ });
21
+
22
+ test('round-trip preserves UTF-8', () => {
23
+ assert.equal(base64decode(base64encode('héllo wörld')), 'héllo wörld');
24
+ });
25
+
26
+ test('decode rejects invalid base64', () => {
27
+ assert.throws(() => base64decode('not!valid'), /Invalid Base64/);
28
+ });
29
+ });
30
+
31
+ describe('url', () => {
32
+ test('encodes special chars', () => {
33
+ assert.equal(urlencode('a b&c=d'), 'a%20b%26c%3Dd');
34
+ });
35
+
36
+ test('round-trip', () => {
37
+ assert.equal(urldecode(urlencode('hello world?')), 'hello world?');
38
+ });
39
+
40
+ test('decode rejects malformed sequence', () => {
41
+ assert.throws(() => urldecode('%ZZ'), /Invalid URL/);
42
+ });
43
+ });
44
+
45
+ describe('morse', () => {
46
+ test('encodes SOS', () => {
47
+ assert.equal(morse('SOS'), '... --- ...');
48
+ });
49
+
50
+ test('round-trip with words', () => {
51
+ assert.equal(unmorse(morse('HELLO WORLD')), 'HELLO WORLD');
52
+ });
53
+
54
+ test('throws on unsupported char', () => {
55
+ assert.throws(() => morse('hello#'), /Unsupported character/);
56
+ });
57
+ });
58
+
59
+ describe('rot13', () => {
60
+ test('shifts letters by 13', () => {
61
+ assert.equal(rot13('Hello'), 'Uryyb');
62
+ });
63
+
64
+ test('is its own inverse', () => {
65
+ assert.equal(rot13(rot13('The Quick Brown Fox')), 'The Quick Brown Fox');
66
+ });
67
+
68
+ test('preserves non-alpha chars', () => {
69
+ assert.equal(rot13('abc 123!'), 'nop 123!');
70
+ });
71
+ });
72
+
73
+ describe('caesar', () => {
74
+ test('shift 3 forward', () => {
75
+ assert.equal(caesar('abc', '3'), 'def');
76
+ });
77
+
78
+ test('shift wraps around alphabet', () => {
79
+ assert.equal(caesar('xyz', '3'), 'abc');
80
+ });
81
+
82
+ test('negative shift', () => {
83
+ assert.equal(caesar('def', '-3'), 'abc');
84
+ });
85
+
86
+ test('throws on non-numeric shift', () => {
87
+ assert.throws(() => caesar('abc', 'abc'), /Shift must be a number/);
88
+ });
89
+ });
90
+
91
+ describe('binary', () => {
92
+ test('encodes A as 01000001', () => {
93
+ assert.equal(binary('A'), '01000001');
94
+ });
95
+
96
+ test('round-trip', () => {
97
+ assert.equal(unbinary(binary('Hello!')), 'Hello!');
98
+ });
99
+
100
+ test('rejects malformed binary', () => {
101
+ assert.throws(() => unbinary('012'), /Invalid binary/);
102
+ });
103
+
104
+ test('rejects wrong group size', () => {
105
+ assert.throws(() => unbinary('0100'), /Each binary group must contain 8 bits/);
106
+ });
107
+ });
108
+ });
@@ -0,0 +1,37 @@
1
+ import { test, describe } from 'node:test';
2
+ import { strict as assert } from 'node:assert';
3
+ import hash from '../../src/modules/v4/hash.js';
4
+
5
+ describe('hash', () => {
6
+ test('sha256 of "hello"', () => {
7
+ const result = hash('hello', 'sha256');
8
+ assert.deepEqual(result, {
9
+ method: 'sha256',
10
+ hash: '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824',
11
+ });
12
+ });
13
+
14
+ test('md5 of "hello"', () => {
15
+ const result = hash('hello', 'md5');
16
+ assert.deepEqual(result, {
17
+ method: 'md5',
18
+ hash: '5d41402abc4b2a76b9719d911017c592',
19
+ });
20
+ });
21
+
22
+ test('sha1 of empty string', () => {
23
+ const result = hash('', 'sha1');
24
+ assert.equal((result as { hash: string }).hash, 'da39a3ee5e6b4b0d3255bfef95601890afd80709');
25
+ });
26
+
27
+ test('returns error object on unsupported method', () => {
28
+ const result = hash('hello', 'fakehash');
29
+ assert.ok('error' in result);
30
+ });
31
+
32
+ test('same input + method = same hash (deterministic)', () => {
33
+ const a = hash('test', 'sha256');
34
+ const b = hash('test', 'sha256');
35
+ assert.deepEqual(a, b);
36
+ });
37
+ });
@@ -0,0 +1,77 @@
1
+ import { test, describe, before, after } from 'node:test';
2
+ import { strict as assert } from 'node:assert';
3
+ import hyperplanning from '../../src/modules/v4/hyperplanning.js';
4
+
5
+ const ORIGINAL_FETCH = globalThis.fetch;
6
+
7
+ const sampleIcs = `BEGIN:VCALENDAR
8
+ VERSION:2.0
9
+ PRODID:-//Test//Test//EN
10
+ BEGIN:VEVENT
11
+ UID:event-1@test
12
+ DTSTAMP:20990101T000000Z
13
+ DTSTART:20990101T080000Z
14
+ DTEND:20990101T090000Z
15
+ SUMMARY:Mathematics - CM
16
+ DESCRIPTION:Matière : Mathematics\\nEnseignant : Mr Smith\\nPromotions : G1, G2\\nSalle : Room 101
17
+ END:VEVENT
18
+ END:VCALENDAR`;
19
+
20
+ function mockFetch(body: string, ok = true, contentType = 'text/calendar'): void {
21
+ globalThis.fetch = (async () => ({
22
+ ok,
23
+ headers: { get: (h: string) => (h.toLowerCase() === 'content-type' ? contentType : null) },
24
+ text: async () => body,
25
+ })) as unknown as typeof fetch;
26
+ }
27
+
28
+ describe('hyperplanning', () => {
29
+ after(() => {
30
+ globalThis.fetch = ORIGINAL_FETCH;
31
+ });
32
+
33
+ test('parses valid ICS with default detail', async () => {
34
+ mockFetch(sampleIcs);
35
+ const events = await hyperplanning('https://fake.test/cal.ics');
36
+ assert.equal(events.length, 1);
37
+ assert.equal(events[0]!.summary, 'Mathematics - CM');
38
+ assert.ok(events[0]!.start);
39
+ assert.ok(events[0]!.end);
40
+ });
41
+
42
+ test('detail=list returns split summary', async () => {
43
+ mockFetch(sampleIcs);
44
+ const events = await hyperplanning('https://fake.test/cal.ics', 'list');
45
+ assert.deepEqual(events[0]!.summary, ['Mathematics', 'CM']);
46
+ });
47
+
48
+ test('detail=full extracts subject/teacher/classes', async () => {
49
+ mockFetch(sampleIcs);
50
+ const events = await hyperplanning('https://fake.test/cal.ics', 'full');
51
+ const e = events[0]!;
52
+ assert.equal(e.subject, 'Mathematics');
53
+ assert.equal(e.teacher, 'Mr Smith');
54
+ assert.deepEqual(e.classes, ['G1', 'G2']);
55
+ });
56
+
57
+ test('throws on non-OK response', async () => {
58
+ mockFetch('', false);
59
+ await assert.rejects(() => hyperplanning('https://fake.test/cal.ics'), /Invalid ICS/);
60
+ });
61
+
62
+ test('throws on wrong content-type', async () => {
63
+ mockFetch(sampleIcs, true, 'text/html');
64
+ await assert.rejects(() => hyperplanning('https://fake.test/cal.ics'), /Invalid ICS/);
65
+ });
66
+
67
+ test('filters out past events', async () => {
68
+ const pastIcs = sampleIcs.replaceAll('20990101', '19990101');
69
+ mockFetch(pastIcs);
70
+ const events = await hyperplanning('https://fake.test/cal.ics');
71
+ assert.equal(events.length, 0);
72
+ });
73
+ });
74
+
75
+ before(() => {
76
+ globalThis.fetch = ORIGINAL_FETCH;
77
+ });
@@ -0,0 +1,34 @@
1
+ import { test, describe } from 'node:test';
2
+ import { strict as assert } from 'node:assert';
3
+ import levenshtein from '../../src/modules/v4/levenshtein.js';
4
+
5
+ describe('levenshtein', () => {
6
+ test('identical strings: distance 0', () => {
7
+ const result = levenshtein('hello', 'hello');
8
+ assert.equal(result.distance, 0);
9
+ });
10
+
11
+ test('kitten / sitting: distance 3', () => {
12
+ const result = levenshtein('kitten', 'sitting');
13
+ assert.equal(result.distance, 3);
14
+ });
15
+
16
+ test('saturday / sunday: distance 3', () => {
17
+ const result = levenshtein('saturday', 'sunday');
18
+ assert.equal(result.distance, 3);
19
+ });
20
+
21
+ test('empty vs string: distance = string length', () => {
22
+ const result = levenshtein('a', 'abc');
23
+ assert.equal(result.distance, 2);
24
+ });
25
+
26
+ test('throws on missing first string', () => {
27
+ assert.throws(() => levenshtein('', 'abc'), /first string/);
28
+ });
29
+
30
+ test('throws on string > 1000 chars', () => {
31
+ const big = 'a'.repeat(1001);
32
+ assert.throws(() => levenshtein(big, 'b'), /1000/);
33
+ });
34
+ });
@@ -0,0 +1,65 @@
1
+ import { test, describe } from 'node:test';
2
+ import { strict as assert } from 'node:assert';
3
+ import personal from '../../src/modules/v4/personal.js';
4
+
5
+ describe('personal', () => {
6
+ test('returns expected fields', () => {
7
+ const result = personal();
8
+ const fields = [
9
+ 'name',
10
+ 'email',
11
+ 'phone',
12
+ 'job',
13
+ 'language',
14
+ 'card',
15
+ 'cvc',
16
+ 'expiration',
17
+ 'address',
18
+ 'birthday',
19
+ 'civil_status',
20
+ 'social_profiles',
21
+ 'emergency_contacts',
22
+ 'subscriptions',
23
+ 'pets',
24
+ ];
25
+ for (const f of fields) {
26
+ assert.ok(f in result, `missing field: ${f}`);
27
+ }
28
+ });
29
+
30
+ test('card has 4 groups of 4 digits', () => {
31
+ const result = personal();
32
+ assert.match(result.card as string, /^\d{4} \d{4} \d{4} \d{4}$/);
33
+ });
34
+
35
+ test('cvc is 3 digits', () => {
36
+ const result = personal();
37
+ const cvc = result.cvc as number;
38
+ assert.ok(cvc >= 100 && cvc <= 999);
39
+ });
40
+
41
+ test('expiration MM/YY format', () => {
42
+ const result = personal();
43
+ assert.match(result.expiration as string, /^\d{2}\/\d{2}$/);
44
+ });
45
+
46
+ test('phone has international code', () => {
47
+ const result = personal();
48
+ assert.match(result.phone as string, /^\+\d+ /);
49
+ });
50
+
51
+ test('social_profiles has 4 platforms', () => {
52
+ const result = personal();
53
+ const social = result.social_profiles as Record<string, string>;
54
+ assert.ok('twitter' in social);
55
+ assert.ok('facebook' in social);
56
+ assert.ok('linkedin' in social);
57
+ assert.ok('instagram' in social);
58
+ });
59
+
60
+ test('emergency_contacts non-empty', () => {
61
+ const result = personal();
62
+ const contacts = result.emergency_contacts as unknown[];
63
+ assert.ok(contacts.length >= 1);
64
+ });
65
+ });
@@ -0,0 +1,57 @@
1
+ import { test, describe } from 'node:test';
2
+ import { strict as assert } from 'node:assert';
3
+ import statistics from '../../src/modules/v4/statistics.js';
4
+
5
+ describe('statistics', () => {
6
+ test('basic computation on integers', () => {
7
+ const result = statistics('1,2,3,4,5');
8
+ assert.equal(result.count, 5);
9
+ assert.equal(result.sum, 15);
10
+ assert.equal(result.min, 1);
11
+ assert.equal(result.max, 5);
12
+ assert.equal(result.range, 4);
13
+ assert.equal(result.mean, 3);
14
+ assert.equal(result.median, 3);
15
+ });
16
+
17
+ test('median on even count', () => {
18
+ const result = statistics('1,2,3,4');
19
+ assert.equal(result.median, 2.5);
20
+ });
21
+
22
+ test('mode is empty when all values unique', () => {
23
+ const result = statistics('1,2,3,4,5');
24
+ assert.deepEqual(result.mode, []);
25
+ });
26
+
27
+ test('mode detects most frequent value', () => {
28
+ const result = statistics('1,2,2,3,3,3');
29
+ assert.deepEqual(result.mode, [3]);
30
+ });
31
+
32
+ test('multiple modes', () => {
33
+ const result = statistics('1,1,2,2,3');
34
+ assert.deepEqual(result.mode.sort(), [1, 2]);
35
+ });
36
+
37
+ test('stddev of identical values is 0', () => {
38
+ const result = statistics('5,5,5,5');
39
+ assert.equal(result.stddev, 0);
40
+ assert.equal(result.variance, 0);
41
+ });
42
+
43
+ test('handles negative and decimal values', () => {
44
+ const result = statistics('-1.5,0,1.5');
45
+ assert.equal(result.mean, 0);
46
+ assert.equal(result.min, -1.5);
47
+ assert.equal(result.max, 1.5);
48
+ });
49
+
50
+ test('throws on non-numeric values', () => {
51
+ assert.throws(() => statistics('1,abc,3'), /must contain only numbers/);
52
+ });
53
+
54
+ test('throws on missing values', () => {
55
+ assert.throws(() => statistics(''), /required/);
56
+ });
57
+ });
@@ -0,0 +1,107 @@
1
+ import { test, describe } from 'node:test';
2
+ import { strict as assert } from 'node:assert';
3
+ import { slug, stats, lorem, number } from '../../src/modules/v4/text.js';
4
+
5
+ describe('text', () => {
6
+ describe('slug', () => {
7
+ test('basic slug', () => {
8
+ assert.equal(slug('Hello World'), 'hello-world');
9
+ });
10
+
11
+ test('strips diacritics', () => {
12
+ assert.equal(slug('Crème Brûlée'), 'creme-brulee');
13
+ });
14
+
15
+ test('strips special chars', () => {
16
+ assert.equal(slug('Mon Article #1 !'), 'mon-article-1');
17
+ });
18
+
19
+ test('collapses multiple spaces', () => {
20
+ assert.equal(slug('a b c'), 'a-b-c');
21
+ });
22
+ });
23
+
24
+ describe('stats', () => {
25
+ test('counts characters and words', () => {
26
+ const result = stats('Hello world');
27
+ assert.equal(result.characters, 11);
28
+ assert.equal(result.charactersNoSpaces, 10);
29
+ assert.equal(result.words, 2);
30
+ });
31
+
32
+ test('counts sentences', () => {
33
+ const result = stats('Hi. How are you? Fine!');
34
+ assert.equal(result.sentences, 3);
35
+ });
36
+
37
+ test('reading time format', () => {
38
+ const result = stats('a '.repeat(200).trim());
39
+ assert.match(result.readingTime, /min/);
40
+ });
41
+
42
+ test('short text returns seconds', () => {
43
+ const result = stats('hello world');
44
+ assert.match(result.readingTime, /s$/);
45
+ });
46
+ });
47
+
48
+ describe('lorem', () => {
49
+ test('words count', () => {
50
+ const result = lorem('words', '10');
51
+ assert.equal(result.split(' ').length, 10);
52
+ });
53
+
54
+ test('sentences end with period', () => {
55
+ const result = lorem('sentences', '3');
56
+ const sentences = result.split('. ');
57
+ assert.equal(sentences.length, 3);
58
+ });
59
+
60
+ test('paragraphs separated by double newline', () => {
61
+ const result = lorem('paragraphs', '2');
62
+ assert.equal(result.split('\n\n').length, 2);
63
+ });
64
+
65
+ test('throws on invalid type', () => {
66
+ assert.throws(() => lorem('verses', '5'), /Type must be one of/);
67
+ });
68
+ });
69
+
70
+ describe('number', () => {
71
+ test('English: 42', () => {
72
+ assert.equal(number('42', 'en'), 'forty-two');
73
+ });
74
+
75
+ test('English: 100', () => {
76
+ assert.equal(number('100', 'en'), 'one hundred');
77
+ });
78
+
79
+ test('English: 1234', () => {
80
+ assert.equal(number('1234', 'en'), 'one thousand two hundred thirty-four');
81
+ });
82
+
83
+ test('French: 21', () => {
84
+ assert.equal(number('21', 'fr'), 'vingt et un');
85
+ });
86
+
87
+ test('French: 80', () => {
88
+ assert.equal(number('80', 'fr'), 'quatre-vingts');
89
+ });
90
+
91
+ test('French: 0', () => {
92
+ assert.equal(number('0', 'fr'), 'zéro');
93
+ });
94
+
95
+ test('English: negative', () => {
96
+ assert.equal(number('-5', 'en'), 'minus five');
97
+ });
98
+
99
+ test('throws on invalid lang', () => {
100
+ assert.throws(() => number('42', 'es'), /Lang must be one of/);
101
+ });
102
+
103
+ test('throws on non-integer', () => {
104
+ assert.throws(() => number('3.14', 'en'), /must be an integer/);
105
+ });
106
+ });
107
+ });
@@ -0,0 +1,67 @@
1
+ import { test, describe } from 'node:test';
2
+ import { strict as assert } from 'node:assert';
3
+ import time from '../../src/modules/v4/time.js';
4
+
5
+ describe('time', () => {
6
+ test('live returns full set of formats', () => {
7
+ const result = time('live');
8
+ const fields = [
9
+ 'iso',
10
+ 'utc',
11
+ 'timestamp',
12
+ 'locale',
13
+ 'date',
14
+ 'time',
15
+ 'year',
16
+ 'month',
17
+ 'day',
18
+ 'hour',
19
+ 'minute',
20
+ 'second',
21
+ 'ms',
22
+ 'dayOfWeek',
23
+ 'dayOfYear',
24
+ 'weekNumber',
25
+ 'timezone',
26
+ 'timezoneOffset',
27
+ ];
28
+ for (const f of fields) {
29
+ assert.ok(f in result, `missing field: ${f}`);
30
+ }
31
+ });
32
+
33
+ test('iso is parseable', () => {
34
+ const result = time('live');
35
+ assert.ok(!isNaN(Date.parse(result.iso as string)));
36
+ });
37
+
38
+ test('format=year returns only date', () => {
39
+ const result = time('live', undefined, undefined, 'year');
40
+ assert.ok('date' in result);
41
+ assert.equal(typeof result.date, 'number');
42
+ });
43
+
44
+ test('random within range', () => {
45
+ const result = time('random', '2020-01-01', '2020-12-31');
46
+ const ts = result.timestamp as number;
47
+ assert.ok(ts >= new Date('2020-01-01').getTime());
48
+ assert.ok(ts <= new Date('2020-12-31').getTime());
49
+ });
50
+
51
+ test('throws on invalid type', () => {
52
+ assert.throws(() => time('foo'), /valid type/);
53
+ });
54
+
55
+ test('throws on invalid format', () => {
56
+ assert.throws(() => time('live', undefined, undefined, 'fakeformat'), /valid format/);
57
+ });
58
+
59
+ test('throws on invalid timezone', () => {
60
+ assert.throws(() => time('live', undefined, undefined, undefined, 'Mars/Olympus'), /valid timezone/);
61
+ });
62
+
63
+ test('Europe/Paris timezone', () => {
64
+ const result = time('live', undefined, undefined, undefined, 'Europe/Paris');
65
+ assert.equal(result.timezone, 'Europe/Paris');
66
+ });
67
+ });