@anonympins/fingerprint 0.5.2 → 0.5.4

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +1 -1
  3. package/src/js/fingerprint.client.js +1271 -1191
  4. package/src/js/fingerprint.js +202 -108
  5. package/src/js/fingerprint.utils.js +213 -213
  6. package/src/js/library.js +24 -8
  7. package/src/js/mongodb-store.js +165 -79
  8. package/src/js/pow.solver.inline.js +500 -14
  9. package/src/js/pow.solver.js +323 -14
  10. package/src/js/problem-manager.js +1 -1
  11. package/src/js/redis-store.js +114 -42
  12. package/src/js/tests/dns-circuit-breaker.test.js +55 -0
  13. package/src/js/tests/fingerprint.client.init.test.js +143 -143
  14. package/src/js/tests/fingerprint.client.test.js +128 -128
  15. package/src/js/tests/fingerprint.test.js +32 -2
  16. package/src/js/tests/ip-reputation.test.js +1 -1
  17. package/src/js/tests/pow.solver.test.js +5 -2
  18. package/src/js/tests/usefulWorkSolver.test.js +162 -0
  19. package/src/php/Challenge/ChallengeUtils.php +21 -8
  20. package/src/php/FingerprintClient.php +22 -5
  21. package/src/php/FingerprintEngine.php +232 -56
  22. package/src/php/Optimization/Optimization.php +23 -16
  23. package/src/php/Optimization/OptimizationOperators.php +27 -0
  24. package/src/php/ProblemManager.php +1 -1
  25. package/src/php/Store/MongoDbStore.php +184 -104
  26. package/src/php/Store/RedisStore.php +91 -11
  27. package/src/php/Tests/ChallengeUtilsTest.php +285 -285
  28. package/src/php/Tests/DnsCircuitBreakerTest.php +106 -0
  29. package/src/php/Tests/IpReputationTest.php +1 -1
  30. package/src/php/Tests/PowTest.php +1 -1
  31. package/src/php/Tests/RequestUtilsTest.php +29 -0
  32. package/src/php/Tests/TLSClientHelloParserTest.php +177 -117
  33. package/src/php/Utils/BigInt.php +202 -202
  34. package/src/php/Utils/RequestUtils.php +74 -21
  35. package/src/php/Utils/TLSClientHelloParser.php +212 -116
@@ -269,15 +269,17 @@ export async function solveCpuTarget(message, target) {
269
269
  */
270
270
  export async function solveMemory(seed, difficulty) {
271
271
  const wasmModule = typeof window !== 'undefined' ? (window.wasmModule || (window.ClientLibrary && window.ClientLibrary.wasmModule)) : null;
272
+ let solution;
273
+ let useWasm = false;
272
274
  if (wasmModule && typeof wasmModule._solve_memory_challenge === 'function') {
273
275
  const encoder = new TextEncoder();
274
276
  const seedBytes = encoder.encode(seed);
275
277
  const ptr = wasmModule._malloc(seedBytes.length + 1);
276
278
  wasmModule.HEAPU8.set(seedBytes, ptr);
277
279
  wasmModule.HEAPU8[ptr + seedBytes.length] = 0; // Null-terminator
278
- const solution = wasmModule._solve_memory_challenge(ptr, difficulty);
280
+ solution = wasmModule._solve_memory_challenge(ptr, difficulty);
279
281
  wasmModule._free(ptr);
280
- return solution;
282
+ useWasm = true;
281
283
  }
282
284
 
283
285
  const size = difficulty * 1024 * 1024;
@@ -325,19 +327,21 @@ export async function solveMemory(seed, difficulty) {
325
327
  }
326
328
  const merkleRoot = tree[tree.length - 1][0];
327
329
 
328
- function readBuffer(blocks, addr) {
329
- const blockIdx = Math.floor(addr / 1024);
330
- const elementIdx = addr % 1024;
331
- return blocks[blockIdx][elementIdx];
332
- }
330
+ if (!useWasm) {
331
+ function readBuffer(blocks, addr) {
332
+ const blockIdx = Math.floor(addr / 1024);
333
+ const elementIdx = addr % 1024;
334
+ return blocks[blockIdx][elementIdx];
335
+ }
333
336
 
334
- const totalElements = numBlocks * 1024;
335
- let addr = totalElements > 0 ? readBuffer(blocks, 0) % totalElements : 0;
336
- let solution = 0;
337
- const iterations = 1024;
338
- for (let i = 0; i < iterations; i++) {
339
- addr = readBuffer(blocks, addr) % totalElements;
340
- solution ^= addr;
337
+ const totalElements = numBlocks * 1024;
338
+ let addr = totalElements > 0 ? readBuffer(blocks, 0) % totalElements : 0;
339
+ solution = 0;
340
+ const iterations = 1024;
341
+ for (let i = 0; i < iterations; i++) {
342
+ addr = readBuffer(blocks, addr) % totalElements;
343
+ solution ^= addr;
344
+ }
341
345
  }
342
346
 
343
347
  const challengedIndices = [];
@@ -430,6 +434,81 @@ export async function solveTsp(cities, targetMaxDistance) {
430
434
 
431
435
  // --- Fonctions d'optimisation copiées/adaptées de library.js pour le client ---
432
436
 
437
+ function paretoDominates(objectivesA, objectivesB) {
438
+ let aIsBetterInOne = false;
439
+ for (let i = 0; i < objectivesA.length; i++) {
440
+ if (objectivesA[i] > objectivesB[i]) {
441
+ return false;
442
+ }
443
+ if (objectivesA[i] < objectivesB[i]) {
444
+ aIsBetterInOne = true;
445
+ }
446
+ }
447
+ return aIsBetterInOne;
448
+ }
449
+
450
+ function nonDominatedSort(populationWithObjectives) {
451
+ const fronts = [[]];
452
+ for (const p1 of populationWithObjectives) {
453
+ p1.dominationCount = 0;
454
+ p1.dominatedSolutions = [];
455
+ for (const p2 of populationWithObjectives) {
456
+ if (p1 === p2) continue;
457
+ if (paretoDominates(p1.objectives, p2.objectives)) {
458
+ p1.dominatedSolutions.push(p2);
459
+ } else if (paretoDominates(p2.objectives, p1.objectives)) {
460
+ p1.dominationCount++;
461
+ }
462
+ }
463
+ if (p1.dominationCount === 0) {
464
+ p1.rank = 0;
465
+ fronts[0].push(p1);
466
+ }
467
+ }
468
+
469
+ let i = 0;
470
+ while (fronts[i] && fronts[i].length > 0) {
471
+ const nextFront = [];
472
+ for (const p1 of fronts[i]) {
473
+ for (const p2 of p1.dominatedSolutions) {
474
+ p2.dominationCount--;
475
+ if (p2.dominationCount === 0) {
476
+ p2.rank = i + 1;
477
+ nextFront.push(p2);
478
+ }
479
+ }
480
+ }
481
+ i++;
482
+ if (nextFront.length > 0) {
483
+ fronts[i] = nextFront;
484
+ }
485
+ }
486
+ return fronts;
487
+ }
488
+
489
+ function calculateCrowdingDistance(front) {
490
+ if (front.length === 0) return;
491
+ front.forEach((p) => (p.crowdingDistance = 0));
492
+ const numObjectives = front[0].objectives.length;
493
+
494
+ for (let i = 0; i < numObjectives; i++) {
495
+ front.sort((a, b) => a.objectives[i] - b.objectives[i]);
496
+ const minObj = front[0].objectives[i];
497
+ const maxObj = front[front.length - 1].objectives[i];
498
+
499
+ front[0].crowdingDistance = Infinity;
500
+ front[front.length - 1].crowdingDistance = Infinity;
501
+
502
+ if (maxObj === minObj) continue;
503
+
504
+ for (let j = 1; j < front.length - 1; j++) {
505
+ front[j].crowdingDistance +=
506
+ (front[j + 1].objectives[i] - front[j - 1].objectives[i]) /
507
+ (maxObj - minObj);
508
+ }
509
+ }
510
+ }
511
+
433
512
  const ClientOptimizers = {
434
513
  simulatedAnnealing(initialSolution, evaluator, neighbor, iterations, temp, cooling) {
435
514
  let currentSolution = initialSolution;
@@ -467,6 +546,236 @@ const ClientOptimizers = {
467
546
  population = newPopulation;
468
547
  }
469
548
  return population;
549
+ },
550
+
551
+ geneticAlgorithmMultiObjective(createIndividual, fitnessFunction, crossover, mutate, options = {}) {
552
+ const generations = options.generations || 50;
553
+ const populationSize = options.populationSize || 40;
554
+ const mutationRate = options.mutationRate !== undefined ? options.mutationRate : 0.1;
555
+
556
+ let population = Array.from({ length: populationSize }, () => ({
557
+ individual: createIndividual(),
558
+ }));
559
+ population.forEach((p) => (p.objectives = fitnessFunction(p.individual)));
560
+
561
+ for (let gen = 0; gen < generations; gen++) {
562
+ const offspring = [];
563
+ for (let i = 0; i < populationSize; i++) {
564
+ const parent1 = population[Math.floor(Math.random() * population.length)];
565
+ const parent2 = population[Math.floor(Math.random() * population.length)];
566
+ let childIndividual = crossover(parent1.individual, parent2.individual);
567
+ if (Math.random() < mutationRate) {
568
+ childIndividual = mutate(childIndividual);
569
+ }
570
+ const child = { individual: childIndividual };
571
+ child.objectives = fitnessFunction(child.individual);
572
+ offspring.push(child);
573
+ }
574
+
575
+ const combinedPopulation = [...population, ...offspring];
576
+ const fronts = nonDominatedSort(combinedPopulation);
577
+ const newPopulation = [];
578
+ for (const front of fronts) {
579
+ if (newPopulation.length + front.length <= populationSize) {
580
+ newPopulation.push(...front);
581
+ } else {
582
+ calculateCrowdingDistance(front);
583
+ front.sort((a, b) => b.crowdingDistance - a.crowdingDistance);
584
+ const remaining = populationSize - newPopulation.length;
585
+ newPopulation.push(...front.slice(0, remaining));
586
+ break;
587
+ }
588
+ }
589
+ population = newPopulation;
590
+ }
591
+
592
+ const finalFronts = nonDominatedSort(population);
593
+ const bestFront = finalFronts.length > 0 ? finalFronts[0] : [];
594
+ const uniqueSolutionsMap = new Map();
595
+ for (const p of bestFront) {
596
+ const key = JSON.stringify(p.objectives);
597
+ if (!uniqueSolutionsMap.has(key)) {
598
+ uniqueSolutionsMap.set(key, {
599
+ solution: p.individual,
600
+ objectives: p.objectives,
601
+ });
602
+ }
603
+ }
604
+ return Array.from(uniqueSolutionsMap.values());
605
+ },
606
+
607
+ 'cpc.solve'(context, options = {}) {
608
+ const optimalBaseCommission = context.platformParams ? context.platformParams.optimalBaseCommission : 0.3;
609
+ const optimalBonusFactor = context.platformParams ? context.platformParams.optimalBonusFactor : 0.1;
610
+ const websiteQualityScore = ((context.website && context.website.relevanceScore) || 50) / 100;
611
+ const effectiveCommissionRate = Math.max(0, optimalBaseCommission - websiteQualityScore * optimalBonusFactor);
612
+
613
+ const advertiserDemand = (cpc) => {
614
+ if (cpc <= 0) return Infinity;
615
+ return (context.advertiser.credits || 0) / cpc;
616
+ };
617
+
618
+ const supply = context.estimatedImpressions || 1;
619
+ const competingAdsCount = context.competingAds ? context.competingAds.length : 0;
620
+ const competitionFactor = Math.min(2.5, 1 + competingAdsCount * 0.1);
621
+
622
+ const fitnessFunction = (cpcMultiplier) => {
623
+ const adjustedCPC = 1.0 * cpcMultiplier * competitionFactor;
624
+ if (adjustedCPC < 0.1) return [Infinity, Infinity, Infinity];
625
+
626
+ const demand = advertiserDemand(adjustedCPC);
627
+ const estimatedClicks = Math.min(demand, supply);
628
+ const platformRevenue = estimatedClicks * adjustedCPC * effectiveCommissionRate;
629
+ const advertiserValue = estimatedClicks;
630
+ const marketImbalance = Math.abs(demand - supply);
631
+
632
+ return [-platformRevenue, -advertiserValue, marketImbalance];
633
+ };
634
+
635
+ const createIndividual = () => 0.5 + Math.random() * 4.5;
636
+ const crossover = (cpc1, cpc2) => (cpc1 + cpc2) / 2;
637
+ const mutate = (cpc) => Math.max(0.1, cpc + (Math.random() - 0.5) * 0.5);
638
+
639
+ const gaOptions = {
640
+ generations: 50,
641
+ populationSize: 40,
642
+ ...options,
643
+ };
644
+
645
+ const paretoFront = ClientOptimizers.geneticAlgorithmMultiObjective(
646
+ createIndividual,
647
+ fitnessFunction,
648
+ crossover,
649
+ mutate,
650
+ gaOptions
651
+ );
652
+
653
+ return {
654
+ paretoFront: paretoFront.map((result) => {
655
+ const cpcMultiplier = result.solution;
656
+ const finalCpc = Math.max(0.1, 1.0 * cpcMultiplier * competitionFactor);
657
+ return {
658
+ ...result,
659
+ solution: finalCpc,
660
+ };
661
+ })
662
+ };
663
+ },
664
+
665
+ 'fraud.solve'(context, options = {}) {
666
+ const legitimateClicks = context.legitimateClicks || [];
667
+ const fraudulentClicks = context.fraudulentClicks || [];
668
+
669
+ const fitnessFunction = (solution) => {
670
+ const [minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents] = solution;
671
+ if (
672
+ minTimeToClick < 100 ||
673
+ minTimeToClick > 5000 ||
674
+ maxClickVariance < 1 ||
675
+ maxClickVariance > 10000 ||
676
+ minMouseEntropy < 0 ||
677
+ minMouseEntropy > 1 ||
678
+ minScrollEvents < 0
679
+ ) {
680
+ return [Infinity, Infinity];
681
+ }
682
+
683
+ const calculateClickVariance = (clicks) => {
684
+ if (!clicks || clicks.length < 2) return 0;
685
+ const meanX = clicks.reduce((sum, c) => sum + c.clickX, 0) / clicks.length;
686
+ const meanY = clicks.reduce((sum, c) => sum + c.clickY, 0) / clicks.length;
687
+ return clicks.reduce((sum, c) => sum + Math.pow(c.clickX - meanX, 2) + Math.pow(c.clickY - meanY, 2), 0) / clicks.length;
688
+ };
689
+
690
+ const getClicksByFingerprint = (clickData) => {
691
+ const grouped = {};
692
+ for (const click of clickData) {
693
+ if (!grouped[click.fingerprint]) grouped[click.fingerprint] = [];
694
+ grouped[click.fingerprint].push(click);
695
+ }
696
+ return grouped;
697
+ };
698
+
699
+ const legitimateGroups = getClicksByFingerprint(legitimateClicks);
700
+ const fraudulentGroups = getClicksByFingerprint(fraudulentClicks);
701
+
702
+ let truePositives = 0;
703
+ let falsePositives = 0;
704
+
705
+ for (const fingerprint in fraudulentGroups) {
706
+ const clicks = fraudulentGroups[fingerprint];
707
+ if (!clicks) continue;
708
+ const variance = calculateClickVariance(clicks);
709
+ const isTooFast = clicks.some((c) => c.timeToClick < minTimeToClick);
710
+ const isTooUniform = variance < maxClickVariance;
711
+ const hasLowEntropy = clicks.some((c) => c.mouseEntropy < minMouseEntropy);
712
+ const hasFewScrolls = clicks.some((c) => c.scrollEvents < minScrollEvents);
713
+
714
+ if (isTooFast || isTooUniform || hasLowEntropy || hasFewScrolls) {
715
+ truePositives++;
716
+ }
717
+ }
718
+
719
+ for (const fingerprint in legitimateGroups) {
720
+ const clicks = legitimateGroups[fingerprint];
721
+ if (!clicks) continue;
722
+ const variance = calculateClickVariance(clicks);
723
+ if (
724
+ clicks.some((c) => c.timeToClick < minTimeToClick || c.mouseEntropy < minMouseEntropy || c.scrollEvents < minScrollEvents) ||
725
+ variance < maxClickVariance
726
+ ) {
727
+ falsePositives++;
728
+ }
729
+ }
730
+
731
+ const totalFraudulent = Object.keys(fraudulentGroups).length || 1;
732
+ const totalLegitimate = Object.keys(legitimateGroups).length || 1;
733
+
734
+ const objective1 = 1 - truePositives / totalFraudulent;
735
+ const objective2 = falsePositives / totalLegitimate;
736
+
737
+ return [objective1, objective2];
738
+ };
739
+
740
+ const createIndividual = () => [
741
+ 100 + Math.random() * 4900,
742
+ 1 + Math.random() * 9999,
743
+ Math.random() * 0.5,
744
+ Math.floor(Math.random() * 10)
745
+ ];
746
+
747
+ const crossover = (s1, s2) => [
748
+ (s1[0] + s2[0]) / 2,
749
+ (s1[1] + s2[1]) / 2,
750
+ (s1[2] + s2[2]) / 2,
751
+ (s1[3] + s2[3]) / 2,
752
+ ];
753
+
754
+ const mutate = (solution) => {
755
+ const newSolution = [...solution];
756
+ const i = Math.floor(Math.random() * 4);
757
+ const mutationFactors = [500, 1000, 0.1, 2];
758
+ newSolution[i] += (Math.random() - 0.5) * mutationFactors[i];
759
+ return newSolution;
760
+ };
761
+
762
+ const gaOptions = {
763
+ generations: 80,
764
+ populationSize: 60,
765
+ ...options,
766
+ };
767
+
768
+ const paretoFront = ClientOptimizers.geneticAlgorithmMultiObjective(
769
+ createIndividual,
770
+ fitnessFunction,
771
+ crossover,
772
+ mutate,
773
+ gaOptions
774
+ );
775
+
776
+ return {
777
+ paretoFront
778
+ };
470
779
  }
471
780
  };
472
781
 
@@ -586,7 +586,7 @@ let managerPromise = null;
586
586
  * @returns {Promise<ProblemManager>} The singleton instance.
587
587
  */
588
588
  export function getProblemManager(options = {}, store) {
589
- const defaultPath = join(__dirname, '..', '..', 'problems.config.json');
589
+ const defaultPath = join(__dirname, '..', '..', 'config', 'problems.config.json');
590
590
  const { configPath = defaultPath, config } = options;
591
591
 
592
592
  const hasConfigChanged = problemManagerInstance && (
@@ -1,43 +1,115 @@
1
- /**
2
- * @file Creates a store adapter for ioredis.
3
- * This adapter handles the serialization and deserialization of complex objects,
4
- * including the conversion of Set objects to arrays for storage in Redis.
5
- */
6
-
7
- /**
8
- * Creates a store adapter for an ioredis client.
9
- * @param {import('ioredis').Redis} redisClient - An instance of the ioredis client.
10
- * @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
11
- */
12
- export function createRedisStore(redisClient) {
13
- return {
14
- async get(key) {
15
- const value = await redisClient.get(key);
16
- if (!value) return null;
17
- // Use a reviver to convert arrays back to Sets for specific keys like 'ips'.
18
- return JSON.parse(value, (k, v) => {
19
- if (k === 'ips' && Array.isArray(v)) {
20
- return new Set(v);
21
- }
22
- return v;
23
- });
24
- },
25
- async set(key, value, ttl) {
26
- // Use a replacer to convert Set objects into arrays before serialization.
27
- const stringValue = JSON.stringify(value, (k, v) => {
28
- if (v instanceof Set) {
29
- return Array.from(v);
30
- }
31
- return v;
32
- });
33
-
34
- if (ttl && ttl > 0) {
35
- await redisClient.set(key, stringValue, 'EX', ttl);
36
- } else {
37
- await redisClient.set(key, stringValue);
38
- }
39
- },
40
- async has(key) { return (await redisClient.exists(key)) === 1; },
41
- async delete(key) { await redisClient.del(key); },
42
- };
1
+ /**
2
+ * @file Creates a store adapter for ioredis.
3
+ * This adapter handles the serialization and deserialization of complex objects,
4
+ * including the conversion of Set objects to arrays for storage in Redis.
5
+ */
6
+
7
+ /**
8
+ * Creates a store adapter for an ioredis client.
9
+ * @param {import('ioredis').Redis} redisClient - An instance of the ioredis client.
10
+ * @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
11
+ */
12
+ export function createRedisStore(redisClient) {
13
+ const replacer = (k, v) => (v instanceof Set ? Array.from(v) : v);
14
+ const reviver = (k, v) => (k === 'ips' && Array.isArray(v) ? new Set(v) : v);
15
+
16
+ const localStore = new Map();
17
+ const localTimeouts = new Map();
18
+ let isDown = false;
19
+ let reconnecting = false;
20
+
21
+ const triggerFallback = () => {
22
+ if (!isDown) {
23
+ isDown = true;
24
+ attemptReconnection();
25
+ }
26
+ };
27
+
28
+ const attemptReconnection = () => {
29
+ if (reconnecting) return;
30
+ reconnecting = true;
31
+ const interval = setInterval(async () => {
32
+ try {
33
+ await redisClient.ping();
34
+ isDown = false;
35
+ reconnecting = false;
36
+ clearInterval(interval);
37
+ } catch (err) {
38
+ // Continue de tenter la reconnexion toutes les 5 secondes
39
+ }
40
+ }, 5000);
41
+ if (interval.unref) interval.unref();
42
+ };
43
+
44
+ const setLocal = (key, value, ttl) => {
45
+ localStore.set(key, value);
46
+ if (localTimeouts.has(key)) {
47
+ clearTimeout(localTimeouts.get(key));
48
+ }
49
+ if (ttl && ttl > 0) {
50
+ const timeout = setTimeout(() => {
51
+ localStore.delete(key);
52
+ localTimeouts.delete(key);
53
+ }, ttl * 1000);
54
+ if (timeout.unref) timeout.unref();
55
+ localTimeouts.set(key, timeout);
56
+ }
57
+ };
58
+
59
+ return {
60
+ async get(key) {
61
+ if (isDown) {
62
+ return localStore.get(key) || null;
63
+ }
64
+ try {
65
+ const value = await redisClient.get(key);
66
+ if (!value) return null;
67
+ return JSON.parse(value, reviver);
68
+ } catch (e) {
69
+ triggerFallback();
70
+ return localStore.get(key) || null;
71
+ }
72
+ },
73
+ async set(key, value, ttl) {
74
+ if (isDown) {
75
+ setLocal(key, value, ttl);
76
+ return;
77
+ }
78
+ try {
79
+ const stringValue = JSON.stringify(value, replacer);
80
+ if (ttl && ttl > 0) {
81
+ await redisClient.set(key, stringValue, 'EX', ttl);
82
+ } else {
83
+ await redisClient.set(key, stringValue);
84
+ }
85
+ } catch (e) {
86
+ triggerFallback();
87
+ setLocal(key, value, ttl);
88
+ }
89
+ },
90
+ async has(key) {
91
+ if (isDown) {
92
+ return localStore.has(key);
93
+ }
94
+ try {
95
+ return (await redisClient.exists(key)) === 1;
96
+ } catch (e) {
97
+ triggerFallback();
98
+ return localStore.has(key);
99
+ }
100
+ },
101
+ async delete(key) {
102
+ if (localTimeouts.has(key)) {
103
+ clearTimeout(localTimeouts.get(key));
104
+ localTimeouts.delete(key);
105
+ }
106
+ localStore.delete(key);
107
+ if (isDown) return;
108
+ try {
109
+ await redisClient.del(key);
110
+ } catch (e) {
111
+ triggerFallback();
112
+ }
113
+ },
114
+ };
43
115
  }
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it, beforeEach } from 'vitest';
2
+ import { __internal } from '../fingerprint.js';
3
+
4
+ const { dnsCircuitBreaker, recordDnsSuccess, recordDnsFailure, canAttemptDns } = __internal;
5
+
6
+ describe('DNS Circuit Breaker', () => {
7
+ beforeEach(() => {
8
+ dnsCircuitBreaker.state = 'CLOSED';
9
+ dnsCircuitBreaker.failureCount = 0;
10
+ dnsCircuitBreaker.lastStateChange = 0;
11
+ dnsCircuitBreaker.threshold = 5;
12
+ dnsCircuitBreaker.cooldownMs = 1000; // 1s de cooldown pour accélérer le test unitaire
13
+ });
14
+
15
+ it('devrait démarrer à l\'état CLOSED et autoriser les résolutions', () => {
16
+ expect(canAttemptDns()).toBe(true);
17
+ expect(dnsCircuitBreaker.state).toBe('CLOSED');
18
+ });
19
+
20
+ it('devrait s\'ouvrir après avoir atteint le seuil d\'échecs (5)', () => {
21
+ for (let i = 0; i < 4; i++) {
22
+ recordDnsFailure();
23
+ expect(canAttemptDns()).toBe(true);
24
+ expect(dnsCircuitBreaker.state).toBe('CLOSED');
25
+ }
26
+
27
+ recordDnsFailure();
28
+ expect(canAttemptDns()).toBe(false);
29
+ expect(dnsCircuitBreaker.state).toBe('OPEN');
30
+ });
31
+
32
+ it('devrait passer en HALF-OPEN après expiration du cooldown', async () => {
33
+ for (let i = 0; i < 5; i++) {
34
+ recordDnsFailure();
35
+ }
36
+ expect(canAttemptDns()).toBe(false);
37
+
38
+ await new Promise((resolve) => setTimeout(resolve, 1100));
39
+
40
+ expect(canAttemptDns()).toBe(true);
41
+ expect(dnsCircuitBreaker.state).toBe('HALF-OPEN');
42
+ });
43
+
44
+ it('devrait se refermer (CLOSED) après un succès en HALF-OPEN', async () => {
45
+ for (let i = 0; i < 5; i++) {
46
+ recordDnsFailure();
47
+ }
48
+ await new Promise((resolve) => setTimeout(resolve, 1100));
49
+ expect(canAttemptDns()).toBe(true); // Passage en HALF-OPEN
50
+
51
+ recordDnsSuccess();
52
+ expect(dnsCircuitBreaker.state).toBe('CLOSED');
53
+ expect(dnsCircuitBreaker.failureCount).toBe(0);
54
+ });
55
+ });