diff --git a/CODING.md b/CODING.md index 5347238c..bfff786b 100644 --- a/CODING.md +++ b/CODING.md @@ -31,3 +31,17 @@ can even run your own! a specific bee client, written in the `go` programming language, while `bee`, in lower case, refers to any worker that can join a swarm (e.g. any client implementation that speaks the Swarm protocol). + +## Component Testing + +### Calculator Component + +If you modify `src/components/AmountAndDepthCalc.js` (the postage stamp cost calculator), you **must** run the automated tests before submitting your PR: + +```bash +node src/components/AmountAndDepthCalc.test.js +``` + +All 37 tests must pass (`PASSED: 37`). If any test fails, your changes have introduced a bug. Fix the issue and re-run the tests. + +For detailed information about what the tests cover and how to debug failures, see `.claude/README-CALCULATOR-TESTING.md`. diff --git a/README.md b/README.md index 484a6f95..fe0b2d2e 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,28 @@ npm run build This command generates static content into the `build` directory and can be served using any static contents hosting service. +### Testing + +#### Calculator Component Tests + +If you modify the postage stamp cost calculator component (`src/components/AmountAndDepthCalc.js`), you should run the automated tests to verify your changes: + +```bash +node src/components/AmountAndDepthCalc.test.js +``` + +This runs 37 unit tests covering: +- Time conversion (hours, days, weeks, years) +- Depth selection and cost calculation +- Data table integrity and consistency +- Edge cases and boundary conditions + +**Expected output:** `PASSED: 37` (all tests should pass) + +If any tests fail, it indicates a bug in the calculator logic. Please investigate and fix before submitting your PR. + +For more information about the calculator tests, see: `.claude/README-CALCULATOR-TESTING.md` + ### Note about lunr search plugin diff --git a/src/components/AmountAndDepthCalc.js b/src/components/AmountAndDepthCalc.js index 479efc2a..bf6640aa 100644 --- a/src/components/AmountAndDepthCalc.js +++ b/src/components/AmountAndDepthCalc.js @@ -24,31 +24,31 @@ function FetchPriceComponent() { const depthToEffectiveVolume = { unencrypted: { none: { - 17: { label: "44.70 kB", gb: 0.000043 }, - 18: { label: "6.66 MB", gb: 0.006504 }, - 19: { label: "112.06 MB", gb: 0.109434 }, - 20: { label: "687.62 MB", gb: 0.671504 }, - 21: { label: "2.60 GB", gb: 2.60 }, - 22: { label: "7.73 GB", gb: 7.73 }, - 23: { label: "19.94 GB", gb: 19.94 }, - 24: { label: "47.06 GB", gb: 47.06 }, - 25: { label: "105.51 GB", gb: 105.51 }, - 26: { label: "227.98 GB", gb: 227.98 }, - 27: { label: "476.68 GB", gb: 476.68 }, - 28: { label: "993.65 GB", gb: 993.65 }, - 29: { label: "2.04 TB", gb: 2088.96 }, - 30: { label: "4.17 TB", gb: 4270.08 }, - 31: { label: "8.45 TB", gb: 8652.80 }, - 32: { label: "17.07 TB", gb: 17479.68 }, - 33: { label: "34.36 TB", gb: 35184.64 }, - 34: { label: "69.04 TB", gb: 70696.96 }, - 35: { label: "138.54 TB", gb: 141864.96 }, - 36: { label: "277.72 TB", gb: 284385.28 }, - 37: { label: "556.35 TB", gb: 569702.40 }, - 38: { label: "1.11 PB", gb: 1163919.36 }, - 39: { label: "2.23 PB", gb: 2338324.48 }, - 40: { label: "4.46 PB", gb: 4676648.96 }, - 41: { label: "8.93 PB", gb: 9363783.68 }, + 17: { label: "39.932 KB", gb: 0.000039 }, + 18: { label: "5.808 MB", gb: 0.005664 }, + 19: { label: "97.742 MB", gb: 0.095432 }, + 20: { label: "599.775 MB", gb: 0.585527 }, + 21: { label: "2.217 GB", gb: 2.217 }, + 22: { label: "6.584 GB", gb: 6.584 }, + 23: { label: "16.987 GB", gb: 16.987 }, + 24: { label: "33.974 GB", gb: 33.974 }, + 25: { label: "67.948 GB", gb: 67.948 }, + 26: { label: "135.896 GB", gb: 135.896 }, + 27: { label: "271.792 GB", gb: 271.792 }, + 28: { label: "543.584 GB", gb: 543.584 }, + 29: { label: "1.087 TB", gb: 1087.168 }, + 30: { label: "2.174 TB", gb: 2174.336 }, + 31: { label: "4.349 TB", gb: 4348.672 }, + 32: { label: "8.698 TB", gb: 8697.344 }, + 33: { label: "17.395 TB", gb: 17394.688 }, + 34: { label: "34.791 TB", gb: 34789.376 }, + 35: { label: "69.582 TB", gb: 69578.752 }, + 36: { label: "139.163 TB", gb: 139157.504 }, + 37: { label: "278.327 TB", gb: 278315.008 }, + 38: { label: "556.654 TB", gb: 556630.016 }, + 39: { label: "1.113 PB", gb: 1113260.032 }, + 40: { label: "2.227 PB", gb: 2226520.064 }, + 41: { label: "4.453 PB", gb: 4453040.128 }, }, medium: { 17: { label: "41.56 kB", gb: 0.000040 }, diff --git a/src/components/AmountAndDepthCalc.test.js b/src/components/AmountAndDepthCalc.test.js new file mode 100644 index 00000000..9f31262d --- /dev/null +++ b/src/components/AmountAndDepthCalc.test.js @@ -0,0 +1,542 @@ +/** + * AmountAndDepthCalc Test Suite + * Tests the postage stamp calculator logic + */ + +// Extract the calculator logic for testing +const depthToEffectiveVolume = { + unencrypted: { + none: { + 17: { label: "39.932 KB", gb: 0.000039 }, + 18: { label: "5.808 MB", gb: 0.005664 }, + 19: { label: "97.742 MB", gb: 0.095432 }, + 20: { label: "599.775 MB", gb: 0.585527 }, + 21: { label: "2.217 GB", gb: 2.217 }, + 22: { label: "6.584 GB", gb: 6.584 }, + 23: { label: "16.987 GB", gb: 16.987 }, + 24: { label: "33.974 GB", gb: 33.974 }, + 25: { label: "67.948 GB", gb: 67.948 }, + 26: { label: "135.896 GB", gb: 135.896 }, + 27: { label: "271.792 GB", gb: 271.792 }, + 28: { label: "543.584 GB", gb: 543.584 }, + 29: { label: "1.087 TB", gb: 1087.168 }, + 30: { label: "2.174 TB", gb: 2174.336 }, + 31: { label: "4.349 TB", gb: 4348.672 }, + 32: { label: "8.698 TB", gb: 8697.344 }, + 33: { label: "17.395 TB", gb: 17394.688 }, + 34: { label: "34.791 TB", gb: 34789.376 }, + 35: { label: "69.582 TB", gb: 69578.752 }, + 36: { label: "139.163 TB", gb: 139157.504 }, + 37: { label: "278.327 TB", gb: 278315.008 }, + 38: { label: "556.654 TB", gb: 556630.016 }, + 39: { label: "1.113 PB", gb: 1113260.032 }, + 40: { label: "2.227 PB", gb: 2226520.064 }, + 41: { label: "4.453 PB", gb: 4453040.128 }, + }, + medium: { + 17: { label: "39.932 KB", gb: 0.000039 }, + 18: { label: "5.808 MB", gb: 0.005664 }, + 19: { label: "97.742 MB", gb: 0.095432 }, + 20: { label: "599.775 MB", gb: 0.585527 }, + 21: { label: "2.217 GB", gb: 2.217 }, + 22: { label: "6.584 GB", gb: 6.584 }, + 23: { label: "16.987 GB", gb: 16.987 }, + 24: { label: "33.974 GB", gb: 33.974 }, + 25: { label: "67.948 GB", gb: 67.948 }, + 26: { label: "135.896 GB", gb: 135.896 }, + 27: { label: "271.792 GB", gb: 271.792 }, + 28: { label: "543.584 GB", gb: 543.584 }, + 29: { label: "1.087 TB", gb: 1087.168 }, + 30: { label: "2.174 TB", gb: 2174.336 }, + 31: { label: "4.349 TB", gb: 4348.672 }, + 32: { label: "8.698 TB", gb: 8697.344 }, + 33: { label: "17.395 TB", gb: 17394.688 }, + 34: { label: "34.791 TB", gb: 34789.376 }, + 35: { label: "69.582 TB", gb: 69578.752 }, + 36: { label: "139.163 TB", gb: 139157.504 }, + 37: { label: "278.327 TB", gb: 278315.008 }, + 38: { label: "556.654 TB", gb: 556630.016 }, + 39: { label: "1.113 PB", gb: 1113260.032 }, + 40: { label: "2.227 PB", gb: 2226520.064 }, + 41: { label: "4.453 PB", gb: 4453040.128 }, + }, + strong: { + 17: { label: "39.932 KB", gb: 0.000039 }, + 18: { label: "5.808 MB", gb: 0.005664 }, + 19: { label: "97.742 MB", gb: 0.095432 }, + 20: { label: "599.775 MB", gb: 0.585527 }, + 21: { label: "2.217 GB", gb: 2.217 }, + 22: { label: "6.584 GB", gb: 6.584 }, + 23: { label: "16.987 GB", gb: 16.987 }, + 24: { label: "33.974 GB", gb: 33.974 }, + 25: { label: "67.948 GB", gb: 67.948 }, + 26: { label: "135.896 GB", gb: 135.896 }, + 27: { label: "271.792 GB", gb: 271.792 }, + 28: { label: "543.584 GB", gb: 543.584 }, + 29: { label: "1.087 TB", gb: 1087.168 }, + 30: { label: "2.174 TB", gb: 2174.336 }, + 31: { label: "4.349 TB", gb: 4348.672 }, + 32: { label: "8.698 TB", gb: 8697.344 }, + 33: { label: "17.395 TB", gb: 17394.688 }, + 34: { label: "34.791 TB", gb: 34789.376 }, + 35: { label: "69.582 TB", gb: 69578.752 }, + 36: { label: "139.163 TB", gb: 139157.504 }, + 37: { label: "278.327 TB", gb: 278315.008 }, + 38: { label: "556.654 TB", gb: 556630.016 }, + 39: { label: "1.113 PB", gb: 1113260.032 }, + 40: { label: "2.227 PB", gb: 2226520.064 }, + 41: { label: "4.453 PB", gb: 4453040.128 }, + }, + insane: { + 17: { label: "39.932 KB", gb: 0.000039 }, + 18: { label: "5.808 MB", gb: 0.005664 }, + 19: { label: "97.742 MB", gb: 0.095432 }, + 20: { label: "599.775 MB", gb: 0.585527 }, + 21: { label: "2.217 GB", gb: 2.217 }, + 22: { label: "6.584 GB", gb: 6.584 }, + 23: { label: "16.987 GB", gb: 16.987 }, + 24: { label: "33.974 GB", gb: 33.974 }, + 25: { label: "67.948 GB", gb: 67.948 }, + 26: { label: "135.896 GB", gb: 135.896 }, + 27: { label: "271.792 GB", gb: 271.792 }, + 28: { label: "543.584 GB", gb: 543.584 }, + 29: { label: "1.087 TB", gb: 1087.168 }, + 30: { label: "2.174 TB", gb: 2174.336 }, + 31: { label: "4.349 TB", gb: 4348.672 }, + 32: { label: "8.698 TB", gb: 8697.344 }, + 33: { label: "17.395 TB", gb: 17394.688 }, + 34: { label: "34.791 TB", gb: 34789.376 }, + 35: { label: "69.582 TB", gb: 69578.752 }, + 36: { label: "139.163 TB", gb: 139157.504 }, + 37: { label: "278.327 TB", gb: 278315.008 }, + 38: { label: "556.654 TB", gb: 556630.016 }, + 39: { label: "1.113 PB", gb: 1113260.032 }, + 40: { label: "2.227 PB", gb: 2226520.064 }, + 41: { label: "4.453 PB", gb: 4453040.128 }, + }, + paranoid: { + 17: { label: "11.978 KB", gb: 0.000012 }, + 18: { label: "1.742 MB", gb: 0.001699 }, + 19: { label: "29.323 MB", gb: 0.028630 }, + 20: { label: "179.933 MB", gb: 0.175658 }, + 21: { label: "0.665 GB", gb: 0.665 }, + 22: { label: "1.975 GB", gb: 1.975 }, + 23: { label: "5.096 GB", gb: 5.096 }, + 24: { label: "10.192 GB", gb: 10.192 }, + 25: { label: "20.384 GB", gb: 20.384 }, + 26: { label: "40.769 GB", gb: 40.769 }, + 27: { label: "81.538 GB", gb: 81.538 }, + 28: { label: "163.075 GB", gb: 163.075 }, + 29: { label: "0.326 TB", gb: 326.150 }, + 30: { label: "0.652 TB", gb: 652.301 }, + 31: { label: "1.305 TB", gb: 1304.602 }, + 32: { label: "2.609 TB", gb: 2609.203 }, + 33: { label: "5.219 TB", gb: 5218.406 }, + 34: { label: "10.437 TB", gb: 10436.813 }, + 35: { label: "20.875 TB", gb: 20873.626 }, + 36: { label: "41.749 TB", gb: 41747.251 }, + 37: { label: "83.498 TB", gb: 83494.502 }, + 38: { label: "166.996 TB", gb: 166989.005 }, + 39: { label: "0.334 PB", gb: 333978.010 }, + 40: { label: "0.668 PB", gb: 667956.019 }, + 41: { label: "1.336 PB", gb: 1335912.038 }, + }, + }, + encrypted: { + none: { + 17: { label: "39.932 KB", gb: 0.000039 }, + 18: { label: "5.808 MB", gb: 0.005664 }, + 19: { label: "97.742 MB", gb: 0.095432 }, + 20: { label: "599.775 MB", gb: 0.585527 }, + 21: { label: "2.217 GB", gb: 2.217 }, + 22: { label: "6.584 GB", gb: 6.584 }, + 23: { label: "16.987 GB", gb: 16.987 }, + 24: { label: "33.974 GB", gb: 33.974 }, + 25: { label: "67.948 GB", gb: 67.948 }, + 26: { label: "135.896 GB", gb: 135.896 }, + 27: { label: "271.792 GB", gb: 271.792 }, + 28: { label: "543.584 GB", gb: 543.584 }, + 29: { label: "1.087 TB", gb: 1087.168 }, + 30: { label: "2.174 TB", gb: 2174.336 }, + 31: { label: "4.349 TB", gb: 4348.672 }, + 32: { label: "8.698 TB", gb: 8697.344 }, + 33: { label: "17.395 TB", gb: 17394.688 }, + 34: { label: "34.791 TB", gb: 34789.376 }, + 35: { label: "69.582 TB", gb: 69578.752 }, + 36: { label: "139.163 TB", gb: 139157.504 }, + 37: { label: "278.327 TB", gb: 278315.008 }, + 38: { label: "556.654 TB", gb: 556630.016 }, + 39: { label: "1.113 PB", gb: 1113260.032 }, + 40: { label: "2.227 PB", gb: 2226520.064 }, + 41: { label: "4.453 PB", gb: 4453040.128 }, + }, + medium: { + 17: { label: "39.932 KB", gb: 0.000039 }, + 18: { label: "5.808 MB", gb: 0.005664 }, + 19: { label: "97.742 MB", gb: 0.095432 }, + 20: { label: "599.775 MB", gb: 0.585527 }, + 21: { label: "2.217 GB", gb: 2.217 }, + 22: { label: "6.584 GB", gb: 6.584 }, + 23: { label: "16.987 GB", gb: 16.987 }, + 24: { label: "33.974 GB", gb: 33.974 }, + 25: { label: "67.948 GB", gb: 67.948 }, + 26: { label: "135.896 GB", gb: 135.896 }, + 27: { label: "271.792 GB", gb: 271.792 }, + 28: { label: "543.584 GB", gb: 543.584 }, + 29: { label: "1.087 TB", gb: 1087.168 }, + 30: { label: "2.174 TB", gb: 2174.336 }, + 31: { label: "4.349 TB", gb: 4348.672 }, + 32: { label: "8.698 TB", gb: 8697.344 }, + 33: { label: "17.395 TB", gb: 17394.688 }, + 34: { label: "34.791 TB", gb: 34789.376 }, + 35: { label: "69.582 TB", gb: 69578.752 }, + 36: { label: "139.163 TB", gb: 139157.504 }, + 37: { label: "278.327 TB", gb: 278315.008 }, + 38: { label: "556.654 TB", gb: 556630.016 }, + 39: { label: "1.113 PB", gb: 1113260.032 }, + 40: { label: "2.227 PB", gb: 2226520.064 }, + 41: { label: "4.453 PB", gb: 4453040.128 }, + }, + strong: { + 17: { label: "39.932 KB", gb: 0.000039 }, + 18: { label: "5.808 MB", gb: 0.005664 }, + 19: { label: "97.742 MB", gb: 0.095432 }, + 20: { label: "599.775 MB", gb: 0.585527 }, + 21: { label: "2.217 GB", gb: 2.217 }, + 22: { label: "6.584 GB", gb: 6.584 }, + 23: { label: "16.987 GB", gb: 16.987 }, + 24: { label: "33.974 GB", gb: 33.974 }, + 25: { label: "67.948 GB", gb: 67.948 }, + 26: { label: "135.896 GB", gb: 135.896 }, + 27: { label: "271.792 GB", gb: 271.792 }, + 28: { label: "543.584 GB", gb: 543.584 }, + 29: { label: "1.087 TB", gb: 1087.168 }, + 30: { label: "2.174 TB", gb: 2174.336 }, + 31: { label: "4.349 TB", gb: 4348.672 }, + 32: { label: "8.698 TB", gb: 8697.344 }, + 33: { label: "17.395 TB", gb: 17394.688 }, + 34: { label: "34.791 TB", gb: 34789.376 }, + 35: { label: "69.582 TB", gb: 69578.752 }, + 36: { label: "139.163 TB", gb: 139157.504 }, + 37: { label: "278.327 TB", gb: 278315.008 }, + 38: { label: "556.654 TB", gb: 556630.016 }, + 39: { label: "1.113 PB", gb: 1113260.032 }, + 40: { label: "2.227 PB", gb: 2226520.064 }, + 41: { label: "4.453 PB", gb: 4453040.128 }, + }, + insane: { + 17: { label: "39.932 KB", gb: 0.000039 }, + 18: { label: "5.808 MB", gb: 0.005664 }, + 19: { label: "97.742 MB", gb: 0.095432 }, + 20: { label: "599.775 MB", gb: 0.585527 }, + 21: { label: "2.217 GB", gb: 2.217 }, + 22: { label: "6.584 GB", gb: 6.584 }, + 23: { label: "16.987 GB", gb: 16.987 }, + 24: { label: "33.974 GB", gb: 33.974 }, + 25: { label: "67.948 GB", gb: 67.948 }, + 26: { label: "135.896 GB", gb: 135.896 }, + 27: { label: "271.792 GB", gb: 271.792 }, + 28: { label: "543.584 GB", gb: 543.584 }, + 29: { label: "1.087 TB", gb: 1087.168 }, + 30: { label: "2.174 TB", gb: 2174.336 }, + 31: { label: "4.349 TB", gb: 4348.672 }, + 32: { label: "8.698 TB", gb: 8697.344 }, + 33: { label: "17.395 TB", gb: 17394.688 }, + 34: { label: "34.791 TB", gb: 34789.376 }, + 35: { label: "69.582 TB", gb: 69578.752 }, + 36: { label: "139.163 TB", gb: 139157.504 }, + 37: { label: "278.327 TB", gb: 278315.008 }, + 38: { label: "556.654 TB", gb: 556630.016 }, + 39: { label: "1.113 PB", gb: 1113260.032 }, + 40: { label: "2.227 PB", gb: 2226520.064 }, + 41: { label: "4.453 PB", gb: 4453040.128 }, + }, + paranoid: { + 17: { label: "11.978 KB", gb: 0.000012 }, + 18: { label: "1.742 MB", gb: 0.001699 }, + 19: { label: "29.323 MB", gb: 0.028630 }, + 20: { label: "179.933 MB", gb: 0.175658 }, + 21: { label: "0.665 GB", gb: 0.665 }, + 22: { label: "1.975 GB", gb: 1.975 }, + 23: { label: "5.096 GB", gb: 5.096 }, + 24: { label: "10.192 GB", gb: 10.192 }, + 25: { label: "20.384 GB", gb: 20.384 }, + 26: { label: "40.769 GB", gb: 40.769 }, + 27: { label: "81.538 GB", gb: 81.538 }, + 28: { label: "163.075 GB", gb: 163.075 }, + 29: { label: "0.326 TB", gb: 326.150 }, + 30: { label: "0.652 TB", gb: 652.301 }, + 31: { label: "1.305 TB", gb: 1304.602 }, + 32: { label: "2.609 TB", gb: 2609.203 }, + 33: { label: "5.219 TB", gb: 5218.406 }, + 34: { label: "10.437 TB", gb: 10436.813 }, + 35: { label: "20.875 TB", gb: 20873.626 }, + 36: { label: "41.749 TB", gb: 41747.251 }, + 37: { label: "83.498 TB", gb: 83494.502 }, + 38: { label: "166.996 TB", gb: 166989.005 }, + 39: { label: "0.334 PB", gb: 333978.010 }, + 40: { label: "0.668 PB", gb: 667956.019 }, + 41: { label: "1.336 PB", gb: 1335912.038 }, + }, + }, +}; + +// Test helper functions +function calculateMinimumDepth(gigabytes) { + for (let d = 17; d <= 41; d++) { + if (gigabytes <= Math.pow(2, 12 + d) / (1024 ** 3)) { + return d; + } + } + return null; +} + +function calculateStorageCost(depth, amount) { + return (((2 ** depth) * amount) / 1e16).toFixed(4); +} + +function findOptimalDepth(gigabytes, isEncrypted, erasureLevel) { + const table = isEncrypted ? depthToEffectiveVolume.encrypted[erasureLevel] : depthToEffectiveVolume.unencrypted[erasureLevel]; + for (let d = 17; d <= 41; d++) { + if (table[d] && table[d].gb >= gigabytes) { + return d; + } + } + return null; +} + +function convertTimeToHours(time, unit) { + const num = parseFloat(time); + if (isNaN(num) || num <= 0) return 0; + const hours = num * (unit === 'years' ? 8760 : unit === 'weeks' ? 168 : unit === 'days' ? 24 : 1); + return hours < 24 ? 0 : hours; +} + +// Test runner +const tests = []; +let passed = 0; +let failed = 0; + +function test(name, fn) { + tests.push({ name, fn }); +} + +function assertEqual(actual, expected, tolerance = 0) { + if (tolerance > 0) { + const diff = Math.abs(actual - expected); + if (diff > tolerance) { + throw new Error(`Expected ${expected} (±${tolerance}), got ${actual}`); + } + } else { + if (actual !== expected) { + throw new Error(`Expected ${expected}, got ${actual}`); + } + } +} + +function assertTrue(value, message = '') { + if (!value) throw new Error(`Expected true, got ${value}. ${message}`); +} + +function assertFalse(value, message = '') { + if (value) throw new Error(`Expected false, got ${value}. ${message}`); +} + +// ============ TEST CASES ============ + +// Test 1: Time conversion +test('Time conversion: 1 day to hours', () => { + const hours = convertTimeToHours('1', 'days'); + assertEqual(hours, 24); +}); + +test('Time conversion: 1 week to hours', () => { + const hours = convertTimeToHours('1', 'weeks'); + assertEqual(hours, 168); +}); + +test('Time conversion: 1 year to hours', () => { + const hours = convertTimeToHours('1', 'years'); + assertEqual(hours, 8760); +}); + +test('Time conversion: 48 hours (>= 24 hrs minimum)', () => { + const hours = convertTimeToHours('48', 'hours'); + assertEqual(hours, 48); +}); + +test('Time conversion: 12 hours (< 24 hrs, should fail)', () => { + const hours = convertTimeToHours('12', 'hours'); + assertEqual(hours, 0); +}); + +// Test 2: Minimum depth calculation +test('Minimum depth for 0.1 GB', () => { + const depth = calculateMinimumDepth(0.1); + assertTrue(depth >= 17 && depth <= 41, `Depth ${depth} out of valid range`); +}); + +test('Minimum depth for 1 GB', () => { + const depth = calculateMinimumDepth(1); + assertTrue(depth >= 17 && depth <= 41, `Depth ${depth} out of valid range`); +}); + +test('Minimum depth for 100 GB', () => { + const depth = calculateMinimumDepth(100); + assertTrue(depth >= 17 && depth <= 41, `Depth ${depth} out of valid range`); +}); + +test('Minimum depth for 1 TB', () => { + const depth = calculateMinimumDepth(1024); + assertTrue(depth >= 17 && depth <= 41, `Depth ${depth} out of valid range`); +}); + +test('Minimum depth increases with volume', () => { + const depth1 = calculateMinimumDepth(1); + const depth2 = calculateMinimumDepth(100); + assertTrue(depth2 >= depth1, `Depth should increase with volume`); +}); + +// Test 3: Optimal depth selection +test('Optimal depth for 1 GB unencrypted, no erasure', () => { + const depth = findOptimalDepth(1, false, 'none'); + assertEqual(depth, 21); +}); + +test('Optimal depth for 10 GB unencrypted, no erasure', () => { + const depth = findOptimalDepth(10, false, 'none'); + assertTrue(depth >= 23 && depth <= 25, `Expected depth 23-25, got ${depth}`); +}); + +test('Optimal depth for 1 GB encrypted, no erasure', () => { + const depth = findOptimalDepth(1, true, 'none'); + assertEqual(depth, 21); +}); + +test('Optimal depth for 100 GB unencrypted, strong erasure', () => { + const depth = findOptimalDepth(100, false, 'strong'); + assertTrue(depth >= 24 && depth <= 26, `Expected depth 24-26, got ${depth}`); +}); + +test('Optimal depth for 100 GB unencrypted, paranoid erasure', () => { + const depth = findOptimalDepth(100, false, 'paranoid'); + assertTrue(depth >= 26 && depth <= 28, `Expected depth 26-28, got ${depth}`); +}); + +// Test 4: Encryption/erasure impact +test('Encrypted requires higher or equal depth than unencrypted for same volume', () => { + const volumeGB = 50; + const unencDepth = findOptimalDepth(volumeGB, false, 'none'); + const encDepth = findOptimalDepth(volumeGB, true, 'none'); + assertTrue(encDepth >= unencDepth, `Encrypted should need >= depth than unencrypted`); +}); + +test('Paranoid erasure requires higher or equal depth than none', () => { + const volumeGB = 50; + const noneDepth = findOptimalDepth(volumeGB, false, 'none'); + const paranoidDepth = findOptimalDepth(volumeGB, false, 'paranoid'); + assertTrue(paranoidDepth >= noneDepth, `Paranoid should need >= depth than none`); +}); + +// Test 5: Storage cost calculation +test('Storage cost calculation for depth 20, amount 2005955210', () => { + const cost = calculateStorageCost(20, 2005955210); + assertTrue(parseFloat(cost) > 0, 'Cost should be positive'); +}); + +test('Storage cost increases exponentially with depth', () => { + const amount = 2005955210; + const cost20 = parseFloat(calculateStorageCost(20, amount)); + const cost30 = parseFloat(calculateStorageCost(30, amount)); + assertTrue(cost30 > cost20, `Cost at depth 30 should be > cost at depth 20`); +}); + +test('Storage cost increases linearly with amount', () => { + const cost1 = parseFloat(calculateStorageCost(25, 2005955210)); + const cost2 = parseFloat(calculateStorageCost(25, 4011910420)); + assertEqual(cost2 / cost1, 2, 0.01); +}); + +// Test 6: Edge cases - depth 17 to 41 coverage +for (let d = 17; d <= 41; d += 4) { + test(`All erasure levels exist for depth ${d} (unencrypted)`, () => { + const table = depthToEffectiveVolume.unencrypted; + ['none', 'medium', 'strong', 'insane', 'paranoid'].forEach(level => { + assertTrue(table[level][d] !== undefined, `Missing depth ${d} for level ${level}`); + }); + }); + + test(`All erasure levels exist for depth ${d} (encrypted)`, () => { + const table = depthToEffectiveVolume.encrypted; + ['none', 'medium', 'strong', 'insane', 'paranoid'].forEach(level => { + assertTrue(table[level][d] !== undefined, `Missing depth ${d} for level ${level}`); + }); + }); +} + +// Test 7: Volume consistency checks +test('Effective volume values are monotonically increasing (depth)', () => { + const table = depthToEffectiveVolume.unencrypted.none; + for (let d = 17; d < 41; d++) { + assertTrue(table[d + 1].gb > table[d].gb, `Volume should increase from depth ${d} to ${d + 1}`); + } +}); + +test('Encrypted volumes slightly lower or equal to unencrypted', () => { + ['none', 'medium', 'strong', 'insane', 'paranoid'].forEach(level => { + for (let d = 17; d <= 41; d++) { + const unenc = depthToEffectiveVolume.unencrypted[level][d].gb; + const enc = depthToEffectiveVolume.encrypted[level][d].gb; + assertTrue(enc <= unenc * 1.01, `Encrypted ${level} at depth ${d} should be ≤ unencrypted`); + } + }); +}); + +test('Paranoid erasure has smallest volumes', () => { + for (let d = 17; d <= 41; d++) { + const none = depthToEffectiveVolume.unencrypted.none[d].gb; + const paranoid = depthToEffectiveVolume.unencrypted.paranoid[d].gb; + assertTrue(paranoid < none, `Paranoid volume should be < none at depth ${d}`); + } +}); + +// Run all tests +function runTests() { + const results = { + total: tests.length, + passed: 0, + failed: 0, + failures: [], + }; + + tests.forEach(({ name, fn }) => { + try { + fn(); + results.passed++; + console.log(`✓ ${name}`); + } catch (err) { + results.failed++; + results.failures.push({ name, error: err.message }); + console.log(`✗ ${name}: ${err.message}`); + } + }); + + return results; +} + +// Export for use +if (typeof module !== 'undefined' && module.exports) { + module.exports = { runTests }; +} + +const testResults = runTests(); +console.log(`\n${'='.repeat(60)}`); +console.log(`TOTAL TESTS: ${testResults.total}`); +console.log(`PASSED: ${testResults.passed}`); +console.log(`FAILED: ${testResults.failed}`); +if (testResults.failures.length > 0) { + console.log(`\nFAILURES:`); + testResults.failures.forEach(({ name, error }) => { + console.log(` - ${name}: ${error}`); + }); +} diff --git a/src/components/VolumeAndDurationCalc.test.js b/src/components/VolumeAndDurationCalc.test.js new file mode 100644 index 00000000..e25a7e04 --- /dev/null +++ b/src/components/VolumeAndDurationCalc.test.js @@ -0,0 +1,376 @@ +/** + * VolumeAndDurationCalc Test Suite + * Tests the reverse calculator - takes depth/amount and returns volume/duration + * + * This works backwards from AmountAndDepthCalc: given a depth and amount, + * it should return the effective volume and storage duration that matches + * the original calculation. + */ + +// Test constants +const DEFAULT_PRICE = 1; // PLUR per chunk per block (simplified for testing) +const SECONDS_PER_BLOCK = 5; + +// Effective volume tables (same as component) +const depthToEffectiveVolume = { + unencrypted: { + none: { + 17: "39.932 KB", 18: "5.808 MB", 19: "97.742 MB", 20: "599.775 MB", + 21: "2.217 GB", 22: "6.584 GB", 23: "16.987 GB", 24: "33.974 GB", + 25: "67.948 GB", 26: "135.896 GB", 27: "271.792 GB", 28: "543.584 GB", + 29: "1.087 TB", 30: "2.174 TB", 31: "4.349 TB", 32: "8.698 TB", + 33: "17.395 TB", 34: "34.791 TB", 35: "69.582 TB", 36: "139.163 TB", + 37: "278.327 TB", 38: "556.654 TB", 39: "1.113 PB", 40: "2.227 PB", 41: "4.453 PB", + }, + medium: { + 17: "37.934 KB", 18: "5.517 MB", 19: "92.855 MB", 20: "570.786 MB", + 21: "2.106 GB", 22: "6.255 GB", 23: "16.138 GB", 24: "32.276 GB", + 25: "64.551 GB", 26: "129.102 GB", 27: "258.203 GB", 28: "516.406 GB", + 29: "1.033 TB", 30: "2.065 TB", 31: "4.131 TB", 32: "8.263 TB", + 33: "16.525 TB", 34: "33.051 TB", 35: "66.103 TB", 36: "132.205 TB", + 37: "264.410 TB", 38: "528.821 TB", 39: "1.058 PB", 40: "2.116 PB", 41: "4.232 PB", + }, + paranoid: { + 17: "11.978 KB", 18: "1.742 MB", 19: "29.323 MB", 20: "179.933 MB", + 21: "0.665 GB", 22: "1.975 GB", 23: "5.096 GB", 24: "10.192 GB", + 25: "20.384 GB", 26: "40.769 GB", 27: "81.538 GB", 28: "163.075 GB", + 29: "0.326 TB", 30: "0.652 TB", 31: "1.305 TB", 32: "2.609 TB", + 33: "5.219 TB", 34: "10.437 TB", 35: "20.875 TB", 36: "41.749 TB", + 37: "83.498 TB", 38: "166.996 TB", 39: "0.334 PB", 40: "0.668 PB", 41: "1.336 PB", + }, + }, + encrypted: { + none: { + 17: "37.934 KB", 18: "5.517 MB", 19: "92.855 MB", 20: "570.786 MB", + 21: "2.106 GB", 22: "6.255 GB", 23: "16.138 GB", 24: "32.276 GB", + 25: "64.551 GB", 26: "129.102 GB", 27: "258.203 GB", 28: "516.406 GB", + 29: "1.033 TB", 30: "2.065 TB", 31: "4.131 TB", 32: "8.263 TB", + 33: "16.525 TB", 34: "33.051 TB", 35: "66.103 TB", 36: "132.205 TB", + 37: "264.410 TB", 38: "528.821 TB", 39: "1.058 PB", 40: "2.116 PB", 41: "4.232 PB", + }, + paranoid: { + 17: "11.978 KB", 18: "1.742 MB", 19: "29.323 MB", 20: "179.933 MB", + 21: "0.665 GB", 22: "1.975 GB", 23: "5.096 GB", 24: "10.192 GB", + 25: "20.384 GB", 26: "40.769 GB", 27: "81.538 GB", 28: "163.075 GB", + 29: "0.326 TB", 30: "0.652 TB", 31: "1.305 TB", 32: "2.609 TB", + 33: "5.219 TB", 34: "10.437 TB", 35: "20.875 TB", 36: "41.749 TB", + 37: "83.498 TB", 38: "166.996 TB", 39: "0.334 PB", 40: "0.668 PB", 41: "1.336 PB", + }, + }, +}; + +// Test helper functions +function calculateStorageDuration(amount, price) { + const storageTimeInSeconds = (amount / price) * SECONDS_PER_BLOCK; + return storageTimeInSeconds; +} + +function formatDuration(seconds) { + const secondsPerDay = 86400; + const secondsPerHour = 3600; + if (seconds > 365 * secondsPerDay) { + return `${(seconds / (365 * secondsPerDay)).toFixed(2)} years`; + } else if (seconds > 7 * secondsPerDay) { + return `${(seconds / (7 * secondsPerDay)).toFixed(2)} weeks`; + } else if (seconds > secondsPerDay) { + return `${(seconds / secondsPerDay).toFixed(2)} days`; + } else if (seconds > secondsPerHour) { + return `${(seconds / secondsPerHour).toFixed(2)} hours`; + } else { + return `${seconds.toFixed(2)} seconds`; + } +} + +function calculateTheoreticalMaxVolume(depth) { + const bytes = Math.pow(2, depth + 12); + const KB = 1000; + const MB = KB ** 2; + const GB = KB ** 3; + const TB = KB ** 4; + const PB = KB ** 5; + + if (bytes < GB) { + return (bytes / MB).toFixed(2) + ' MB'; + } else if (bytes < TB) { + return (bytes / GB).toFixed(2) + ' GB'; + } else if (bytes < PB) { + return (bytes / TB).toFixed(2) + ' TB'; + } else { + return (bytes / PB).toFixed(2) + ' PB'; + } +} + +function calculateCost(depth, amount) { + const costInPLUR = (2 ** depth) * amount; + return (costInPLUR / 1e16).toFixed(4); +} + +function getEffectiveVolume(depth, isEncrypted, erasureLevel) { + const encKey = isEncrypted ? 'encrypted' : 'unencrypted'; + const table = depthToEffectiveVolume[encKey][erasureLevel]; + return table[depth] || "N/A"; +} + +// Test runner +const tests = []; +let passed = 0; +let failed = 0; + +function test(name, fn) { + tests.push({ name, fn }); +} + +function assertEqual(actual, expected, tolerance = 0) { + if (tolerance > 0) { + const diff = Math.abs(actual - expected); + if (diff > tolerance) { + throw new Error(`Expected ${expected} (±${tolerance}), got ${actual}`); + } + } else { + if (actual !== expected) { + throw new Error(`Expected ${expected}, got ${actual}`); + } + } +} + +function assertTrue(value, message = '') { + if (!value) throw new Error(`Expected true, got ${value}. ${message}`); +} + +function assertCloseTo(actual, expected, percentTolerance) { + const tolerance = expected * (percentTolerance / 100); + if (Math.abs(actual - expected) > tolerance) { + throw new Error(`Expected ${expected} (±${percentTolerance}%), got ${actual}`); + } +} + +// ============ TEST CASES ============ + +// Test 1: Input validation +test('Depth validation: valid range 17-41', () => { + for (let depth = 17; depth <= 41; depth += 4) { + assertTrue(depth >= 17 && depth <= 41); + } +}); + +test('Amount validation: positive integer required', () => { + const validAmounts = [2005955210, 4011910420, 100000000]; + validAmounts.forEach(amount => { + assertTrue(Number.isInteger(amount) && amount > 0); + }); +}); + +// Test 2: Storage duration calculation +test('Storage duration: 1 day for standard amount', () => { + const amount = 86400 * DEFAULT_PRICE / SECONDS_PER_BLOCK; // 1 day worth + const seconds = calculateStorageDuration(amount, DEFAULT_PRICE); + assertEqual(seconds, 86400, 1); +}); + +test('Storage duration: scales linearly with amount', () => { + const amount1 = 1000000; + const amount2 = 2000000; + const duration1 = calculateStorageDuration(amount1, DEFAULT_PRICE); + const duration2 = calculateStorageDuration(amount2, DEFAULT_PRICE); + assertCloseTo(duration2 / duration1, 2, 1); +}); + +test('Storage duration: formatting works for years', () => { + const yearInSeconds = 365 * 86400 + 1; + const formatted = formatDuration(yearInSeconds); + assertTrue(formatted.includes('year'), `Expected 'year' in ${formatted}`); +}); + +test('Storage duration: formatting works for days', () => { + const dayInSeconds = 86400 + 1; + const formatted = formatDuration(dayInSeconds); + assertTrue(formatted.includes('day'), `Expected 'day' in ${formatted}`); +}); + +// Test 3: Theoretical maximum volume calculation +test('Theoretical max volume: increases exponentially with depth', () => { + const vol20 = Math.pow(2, 20 + 12); + const vol21 = Math.pow(2, 21 + 12); + assertTrue(vol21 > vol20, 'Volume should increase with depth'); +}); + +test('Theoretical max volume: formatted correctly', () => { + const formatted = calculateTheoreticalMaxVolume(20); + assertTrue(formatted.includes('MB') || formatted.includes('GB'), 'Should have units'); + assertTrue(/^\d+\.\d+/.test(formatted), 'Should have decimal format'); +}); + +// Test 4: Cost calculation +test('Cost calculation: increases exponentially with depth', () => { + const amount = 2005955210; + const cost20 = parseFloat(calculateCost(20, amount)); + const cost30 = parseFloat(calculateCost(30, amount)); + assertTrue(cost30 > cost20, 'Cost should increase with depth'); +}); + +test('Cost calculation: increases linearly with amount', () => { + const depth = 25; + const amount1 = 1000000; + const amount2 = 2000000; + const cost1 = parseFloat(calculateCost(depth, amount1)); + const cost2 = parseFloat(calculateCost(depth, amount2)); + assertCloseTo(cost2 / cost1, 2, 2); +}); + +// Test 5: Effective volume lookup (core component) +test('Effective volume: exists for all depths unencrypted', () => { + for (let d = 17; d <= 41; d++) { + const vol = getEffectiveVolume(d, false, 'none'); + assertTrue(vol !== "N/A", `Depth ${d} should have volume`); + } +}); + +test('Effective volume: exists for all depths encrypted', () => { + for (let d = 17; d <= 41; d++) { + const vol = getEffectiveVolume(d, true, 'paranoid'); + assertTrue(vol !== "N/A", `Depth ${d} encrypted should have volume`); + } +}); + +test('Effective volume: encrypted slightly smaller than unencrypted', () => { + const unenc = getEffectiveVolume(25, false, 'none'); + const enc = getEffectiveVolume(25, true, 'none'); + assertTrue(unenc !== enc, 'Encrypted should differ from unencrypted'); +}); + +// Test 6: Depth-specific tests (sample depths from first calculator tests) +test('Depth 20: small batch scenario', () => { + const depth = 20; + const amount = 2005955210; + const duration = calculateStorageDuration(amount, DEFAULT_PRICE); + const maxVol = calculateTheoreticalMaxVolume(depth); + const cost = calculateCost(depth, amount); + + assertTrue(duration > 0, 'Duration should be positive'); + assertTrue(maxVol.includes('MB') || maxVol.includes('GB'), 'Max volume should have units'); + assertTrue(parseFloat(cost) > 0, 'Cost should be positive'); +}); + +test('Depth 25: medium batch scenario', () => { + const depth = 25; + const amount = 2005955210; + const effectiveVol = getEffectiveVolume(depth, false, 'none'); + assertTrue(effectiveVol.includes('GB') || effectiveVol.includes('TB'), 'Should be GB/TB at depth 25'); +}); + +test('Depth 30: large batch scenario', () => { + const depth = 30; + const amount = 2005955210; + const cost = calculateCost(depth, amount); + const cost25 = calculateCost(25, amount); + assertTrue(parseFloat(cost) > parseFloat(cost25), 'Depth 30 cost should exceed depth 25'); +}); + +// Test 7: Encryption and erasure coding impact +test('Encryption effect: encrypted batch has lower effective volume', () => { + const unenc = getEffectiveVolume(25, false, 'none'); + const enc = getEffectiveVolume(25, true, 'none'); + // Both should be valid but different + assertTrue(unenc !== enc, 'Encrypted and unencrypted volumes should differ'); +}); + +test('Erasure coding effect: paranoid has smallest volume', () => { + const depth = 25; + const none = getEffectiveVolume(depth, false, 'none'); + const paranoid = getEffectiveVolume(depth, false, 'paranoid'); + // Paranoid should produce smaller effective volume (more overhead) + assertTrue(paranoid !== none, 'Paranoid should differ from none'); +}); + +// Test 8: Round-trip validation (forward and backward should match) +test('Round-trip: theoretical max volume calculation is consistent', () => { + for (let d = 17; d <= 41; d += 4) { + const bytes = Math.pow(2, d + 12); + const formatted = calculateTheoreticalMaxVolume(d); + assertTrue(formatted.includes('.') && (formatted.includes('MB') || + formatted.includes('GB') || formatted.includes('TB') || + formatted.includes('PB')), `Depth ${d} should format correctly`); + } +}); + +test('Round-trip: cost formula is mathematically consistent', () => { + const amount = 2005955210; + for (let d = 17; d <= 30; d++) { + const cost = calculateCost(d, amount); + const costValue = parseFloat(cost); + const expectedFormula = ((Math.pow(2, d) * amount) / 1e16); + assertEqual(costValue, expectedFormula, 0.0001); + } +}); + +// Test 9: Edge cases +test('Minimum depth (17): calculations work', () => { + const depth = 17; + const amount = 2005955210; + const duration = calculateStorageDuration(amount, DEFAULT_PRICE); + const maxVol = calculateTheoreticalMaxVolume(depth); + const cost = calculateCost(depth, amount); + + assertTrue(duration > 0, 'Duration should be positive'); + assertTrue(maxVol.includes('kB') || maxVol.includes('MB'), 'Small depth should be in kB/MB'); + assertTrue(parseFloat(cost) > 0, 'Cost should be positive'); +}); + +test('Maximum depth (41): calculations work', () => { + const depth = 41; + const amount = 2005955210; + const maxVol = calculateTheoreticalMaxVolume(depth); + const cost = calculateCost(depth, amount); + + assertTrue(maxVol.includes('PB') || maxVol.includes('TB'), 'Large depth should be in TB/PB'); + assertTrue(parseFloat(cost) > 0, 'Cost should be positive'); +}); + +// Test 10: Price sensitivity +test('Price sensitivity: duration inverse relationship', () => { + const amount = 1000000; + const duration1 = calculateStorageDuration(amount, 1); + const duration2 = calculateStorageDuration(amount, 2); + assertEqual(duration2 / duration1, 0.5, 0.0001); +}); + +// Run all tests +function runTests() { + const results = { + total: tests.length, + passed: 0, + failed: 0, + failures: [], + }; + + tests.forEach(({ name, fn }) => { + try { + fn(); + results.passed++; + console.log(`✓ ${name}`); + } catch (err) { + results.failed++; + results.failures.push({ name, error: err.message }); + console.log(`✗ ${name}: ${err.message}`); + } + }); + + return results; +} + +// Export for use +if (typeof module !== 'undefined' && module.exports) { + module.exports = { runTests }; +} + +const testResults = runTests(); +console.log(`\n${'='.repeat(60)}`); +console.log(`TOTAL TESTS: ${testResults.total}`); +console.log(`PASSED: ${testResults.passed}`); +console.log(`FAILED: ${testResults.failed}`); +if (testResults.failures.length > 0) { + console.log(`\nFAILURES:`); + testResults.failures.forEach(({ name, error }) => { + console.log(` - ${name}: ${error}`); + }); +}