diff --git a/.gitignore b/.gitignore index 9c97bbd..767828f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules +coverage dist .env diff --git a/package-lock.json b/package-lock.json index ca5239c..a5e7823 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,8 @@ "@types/node": "^20.11.0", "@types/pg": "^8.10.9", "@types/supertest": "^6.0.2", + "@vitest/coverage-v8": "^1.6.0", + "@vitest/ui": "^1.6.0", "dotenv": "^16.3.1", "esbuild": "^0.19.11", "nodemon": "^3.0.2", @@ -368,18 +370,18 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.6.tgz", + "integrity": "sha512-WdJjwMEkmBicq5T9fm/cHND3+UlFa2Yj8ALLgmoSQAJZysYbBjw+azChSGPN4DSPLXOcooGRvDwZWMcF/mLO2Q==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz", + "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==", "dev": true, "engines": { "node": ">=6.9.0" @@ -473,9 +475,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", - "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.6.tgz", + "integrity": "sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -761,19 +763,25 @@ "dev": true }, "node_modules/@babel/types": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", - "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.6.tgz", + "integrity": "sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-string-parser": "^7.24.6", + "@babel/helper-validator-identifier": "^7.24.6", "to-fast-properties": "^2.0.0" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, "node_modules/@cashu/cashu-ts": { "version": "0.8.2-rc.7", "resolved": "https://registry.npmjs.org/@cashu/cashu-ts/-/cashu-ts-0.8.2-rc.7.tgz", @@ -1602,9 +1610,9 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.22", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", - "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1686,6 +1694,12 @@ "node": ">=14" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.25", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", + "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==", + "dev": true + }, "node_modules/@react-spring/animated": { "version": "9.7.3", "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.3.tgz", @@ -2626,14 +2640,64 @@ "vite": "^4.2.0 || ^5.0.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz", + "integrity": "sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.4", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.4", + "istanbul-reports": "^3.1.6", + "magic-string": "^0.30.5", + "magicast": "^0.3.3", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "test-exclude": "^6.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "1.6.0" + } + }, + "node_modules/@vitest/coverage-v8/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@vitest/coverage-v8/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, "node_modules/@vitest/expect": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz", - "integrity": "sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", + "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", "dev": true, "dependencies": { - "@vitest/spy": "1.3.1", - "@vitest/utils": "1.3.1", + "@vitest/spy": "1.6.0", + "@vitest/utils": "1.6.0", "chai": "^4.3.10" }, "funding": { @@ -2641,12 +2705,12 @@ } }, "node_modules/@vitest/runner": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.1.tgz", - "integrity": "sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", + "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", "dev": true, "dependencies": { - "@vitest/utils": "1.3.1", + "@vitest/utils": "1.6.0", "p-limit": "^5.0.0", "pathe": "^1.1.1" }, @@ -2682,9 +2746,9 @@ } }, "node_modules/@vitest/snapshot": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.1.tgz", - "integrity": "sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", + "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", "dev": true, "dependencies": { "magic-string": "^0.30.5", @@ -2696,9 +2760,9 @@ } }, "node_modules/@vitest/spy": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.1.tgz", - "integrity": "sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", + "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", "dev": true, "dependencies": { "tinyspy": "^2.2.0" @@ -2707,10 +2771,31 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/ui": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-1.6.0.tgz", + "integrity": "sha512-k3Lyo+ONLOgylctiGovRKy7V4+dIN2yxstX3eY5cWFXH6WP+ooVX79YSyi0GagdTQzLmT43BF27T0s6dOIPBXA==", + "dev": true, + "dependencies": { + "@vitest/utils": "1.6.0", + "fast-glob": "^3.3.2", + "fflate": "^0.8.1", + "flatted": "^3.2.9", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "sirv": "^2.0.4" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "1.6.0" + } + }, "node_modules/@vitest/utils": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.1.tgz", - "integrity": "sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", + "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", "dev": true, "dependencies": { "diff-sequences": "^29.6.3", @@ -4236,6 +4321,12 @@ "bser": "2.1.1" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4668,6 +4759,12 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -4887,6 +4984,91 @@ "node": ">=8" } }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.4.tgz", + "integrity": "sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jackspeak": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", @@ -5565,6 +5747,32 @@ "node": ">=12" } }, + "node_modules/magicast": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz", + "integrity": "sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.24.4", + "@babel/types": "^7.24.0", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -5695,6 +5903,15 @@ "ufo": "^1.3.2" } }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -7183,6 +7400,20 @@ "node": ">=10" } }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -7201,9 +7432,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "dev": true, "engines": { "node": ">=0.10.0" @@ -7663,9 +7894,9 @@ "dev": true }, "node_modules/tinypool": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz", - "integrity": "sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==", + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", "dev": true, "engines": { "node": ">=14.0.0" @@ -7715,6 +7946,15 @@ "node": ">=0.6" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", @@ -7956,9 +8196,9 @@ } }, "node_modules/vite-node": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.1.tgz", - "integrity": "sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", + "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -7978,9 +8218,9 @@ } }, "node_modules/vite-node/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dev": true, "dependencies": { "ms": "2.1.2" @@ -8001,16 +8241,16 @@ "dev": true }, "node_modules/vitest": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.1.tgz", - "integrity": "sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", + "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", "dev": true, "dependencies": { - "@vitest/expect": "1.3.1", - "@vitest/runner": "1.3.1", - "@vitest/snapshot": "1.3.1", - "@vitest/spy": "1.3.1", - "@vitest/utils": "1.3.1", + "@vitest/expect": "1.6.0", + "@vitest/runner": "1.6.0", + "@vitest/snapshot": "1.6.0", + "@vitest/spy": "1.6.0", + "@vitest/utils": "1.6.0", "acorn-walk": "^8.3.2", "chai": "^4.3.10", "debug": "^4.3.4", @@ -8022,9 +8262,9 @@ "std-env": "^3.5.0", "strip-literal": "^2.0.0", "tinybench": "^2.5.1", - "tinypool": "^0.8.2", + "tinypool": "^0.8.3", "vite": "^5.0.0", - "vite-node": "1.3.1", + "vite-node": "1.6.0", "why-is-node-running": "^2.2.2" }, "bin": { @@ -8039,8 +8279,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.3.1", - "@vitest/ui": "1.3.1", + "@vitest/browser": "1.6.0", + "@vitest/ui": "1.6.0", "happy-dom": "*", "jsdom": "*" }, diff --git a/package.json b/package.json index 0eb6633..80cef44 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,8 @@ "@types/node": "^20.11.0", "@types/pg": "^8.10.9", "@types/supertest": "^6.0.2", + "@vitest/coverage-v8": "^1.6.0", + "@vitest/ui": "^1.6.0", "dotenv": "^16.3.1", "esbuild": "^0.19.11", "nodemon": "^3.0.2", diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..8eeb4f7 --- /dev/null +++ b/src/app.ts @@ -0,0 +1,22 @@ +import express, { Response } from "express"; +import bodyparser from "body-parser"; +import cors from "cors"; +import compression from "compression"; +import { requireHTTPS } from "./middleware/https"; +import routes from "./routes"; +import path from "path"; + +const app = express(); + +app.use(bodyparser.json()); +app.use(compression()); +app.use(cors()); +app.use(requireHTTPS); + +app.use(routes); +app.use("/", express.static(path.join(__dirname, "../npubcash-website/dist"))); +app.get("*", (_, res: Response) => { + res.sendFile(path.join(__dirname, "../npubcash-website/dist/index.html")); +}); + +export default app; diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..7619cd9 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,13 @@ +import { SimplePool, getPublicKey } from "nostr-tools"; +import { CashuMint, CashuWallet } from "@cashu/cashu-ts"; +import { LightningHandler } from "./utils/lightning"; +import { BlinkProvider } from "./utils/blink"; + +export const wallet = new CashuWallet(new CashuMint(process.env.MINTURL!)); +export const lnProvider = new LightningHandler(new BlinkProvider()); +export const nostrPool = new SimplePool(); + +export let ZAP_PUBKEY: string; +if (process.env.ZAP_SECRET_KEY) { + ZAP_PUBKEY = getPublicKey(Buffer.from(process.env.ZAP_SECRET_KEY, "hex")); +} diff --git a/src/controller/__tests__/infoController.test.ts b/src/controller/__tests__/infoController.test.ts new file mode 100644 index 0000000..809bb30 --- /dev/null +++ b/src/controller/__tests__/infoController.test.ts @@ -0,0 +1,135 @@ +import supertest from "supertest"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import app from "../../app"; +import { User } from "../../models"; + +const pubkey = + "ca9881c70e72981b356353453f4bbfd8153d209acd9b7b5b4200e80c7dec8c7a"; +const npub = "npub1e2vgr3cww2vpkdtr2dzn7jalmq2n6gy6ekdhkk6zqr5qcl0v33aqa87qqk"; + +const mockAuthMiddleware = vi.hoisted(() => + vi.fn((req, res, next) => { + req.authData = { + authorized: true, + data: { pubkey, npub }, + }; + next(); + }), +); + +vi.mock("../../middleware/auth.ts", () => ({ + isAuthMiddleware: (path, method) => { + return mockAuthMiddleware; + }, +})); + +vi.mock("../../models/user.ts"); + +describe("PUT username", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + test("should return 400 if username is missing", async () => { + vi.stubEnv("NODE_ENV", "development"); + const res = await supertest(app) + .put("/api/v1/info/username") + .set("authorization", "validHeader"); + + expect(res.status).toBe(400); + expect(res.body).toEqual({ error: true, message: "Missing parameters" }); + }); + test("should return 400 if username starts with npub", async () => { + vi.stubEnv("NODE_ENV", "development"); + const res = await supertest(app) + .put("/api/v1/info/username") + .send({ username: "npub1234" }) + .set("authorization", "validHeader"); + + expect(res.status).toBe(400); + expect(res.body).toEqual({ error: true, message: "Invalid username" }); + }); + test("should return 400 is username is already taken", async () => { + vi.stubEnv("NODE_ENV", "development"); + vi.mocked(User.checkIfUsernameExists).mockResolvedValueOnce(true); + const res = await supertest(app) + .put("/api/v1/info/username") + .send({ username: "testUser" }) + .set("authorization", "validHeader"); + + expect(res.status).toBe(400); + expect(res.body).toEqual({ + error: true, + message: "This username is already taken", + }); + }); + + test("should return 400 is username is already set", async () => { + vi.stubEnv("NODE_ENV", "development"); + vi.mocked(User.getUserByPubkey, { partial: true }).mockResolvedValueOnce({ + pubkey: pubkey, + name: "username", + }); + const res = await supertest(app) + .put("/api/v1/info/username") + .send({ username: "testUser" }) + .set("authorization", "validHeader"); + + expect(res.status).toBe(400); + expect(res.body).toEqual({ + error: true, + message: "Username already set", + }); + }); +}); + +describe("GET /info ", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + test("should return default info, when user is not set", async () => { + vi.stubEnv("MINTURL", "url"); + const res = await supertest(app) + .get("/api/v1/info") + .set("authorization", "validHeader"); + expect(mockAuthMiddleware).toHaveBeenCalled(); + expect(res.status).toBe(200); + expect(res.body).toEqual({ username: null, npub, mintUrl: "url" }); + }); +}); + +describe("PUT /info/mint", () => { + test("should return 400 if URL is missing", async () => { + const res = await supertest(app) + .put("/api/v1/info/mint") + .set("authorization", "validHeader"); + expect(res.status).toBe(400); + // expect(res.body).toEqual({ error: true, message: "Missing parameters" }); + }); + test("should return 400 if URL is invalid", async () => { + const res = await supertest(app) + .put("/api/v1/info/mint") + .set("authorization", "validHeader") + .send({ mintUrl: "invalid url" }); + expect(res.status).toBe(400); + // expect(res.body).toEqual({ error: true, message: "Invalid URL" }); + }); + + test("should return 500 if db failed", async () => { + vi.mocked(User.upsertMintByPubkey).mockRejectedValueOnce("error"); + const res = await supertest(app) + .put("/api/v1/info/mint") + .set("authorization", "validHeader") + .send({ mintUrl: "https://validurl.com" }); + expect(res.status).toBe(500); + // expect(res.body).toEqual({ error: true, message: "Invalid URL" }); + }); + test("should return 204 if successfull", async () => { + vi.mocked(User.upsertMintByPubkey).mockResolvedValueOnce(); + const res = await supertest(app) + .put("/api/v1/info/mint") + .set("authorization", "validHeader") + .send({ mintUrl: "https://validurl.com" }); + expect(res.status).toBe(204); + // expect(res.body).toEqual({ error: true, message: "Invalid URL" }); + }); +}); diff --git a/src/controller/__tests__/lnurlController.test.ts b/src/controller/__tests__/lnurlController.test.ts new file mode 100644 index 0000000..df62532 --- /dev/null +++ b/src/controller/__tests__/lnurlController.test.ts @@ -0,0 +1,166 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import request from "supertest"; +import { decodeAndValidateZapRequest } from "../../utils/nostr"; +import app from "../../app"; +import { Transaction, User } from "../../models"; +import { createLnurlResponse } from "../../utils/lnurl"; +import { lnProvider, wallet } from "../../config"; + +vi.mock("../../models/user.ts"); +vi.mock("../../models/transaction.ts"); + +vi.mock("../../utils/nostr", () => ({ + decodeAndValidateZapRequest: vi.fn(), +})); + +vi.mock("../../utils/lnurl", () => ({ + createLnurlResponse: vi.fn(), +})); + +vi.mock("../utils/lnurl", async () => { + return { + createLnurlResponse: vi.fn(), + }; +}); + +vi.mock("crypto", () => ({ + createHash: () => ({ + update: () => ({ + digest: vi.fn().mockReturnValue("mockedHash"), + }), + }), +})); + +vi.mock("../utils/lightning", () => ({ + parseInvoice: vi.fn(), +})); + +vi.mock("nostr-tools", () => ({ + SimplePool: vi.fn(), +})); + +vi.mock("../../config.ts", () => ({ + wallet: { + requestMint: vi.fn(), + }, + lnProvider: { + createInvoice: vi.fn(), + }, +})); + +describe("lnurlController", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.unstubAllEnvs(); + process.env.NODE_ENV = "development"; + }); + + it("should return 401 for invalid npub", async () => { + const res = await request(app).get("/.well-known/lnurlp/npubIsInvalid"); + + expect(res.status).toBe(401); + expect(res.body).toEqual({}); + }); + + it("should return 404 if user not found", async () => { + vi.mocked(User.getUserByName).mockResolvedValue(undefined); + + const res = await request(app).get("/.well-known/lnurlp/nonexistentUser"); + + expect(res.status).toBe(404); + expect(res.body).toEqual({}); + }); + + it("should return lnurl response if no amount provided", async () => { + vi.mocked(User.getUserByName, { partial: true }).mockResolvedValue({ + name: "testUser", + mint_url: "https://mint.minibits.cash/Bitcoin", + pubkey: "testPubkey...", + }); + vi.mocked(createLnurlResponse).mockReturnValue({ + callback: "https://npub.cash/.well-known/lnurlp/testUser", + minSendable: 1000, + maxSendable: 100000, + metadata: "", + tag: "pay", + }); + + const res = await request(app).get("/.well-known/lnurlp/testUser"); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ + callback: "https://npub.cash/.well-known/lnurlp/testUser", + minSendable: 1000, + maxSendable: 100000, + metadata: "", + tag: "pay", + }); + }); + + it("should return error for invalid amount", async () => { + vi.stubEnv("LNURL_MIN_AMOUNT", "10"); + vi.stubEnv("LNURL_MAX_AMOUNT", "1000"); + const res = await request(app).get("/.well-known/lnurlp/testUser?amount=5"); + + expect(res.status).toBe(500); + }); + + it("should return error for invalid zap request", async () => { + vi.mocked(decodeAndValidateZapRequest).mockImplementation(() => { + throw new Error("Invalid zap request"); + }); + + const res = await request(app).get( + "/.well-known/lnurlp/testUser?amount=100&nostr=invalidZapRequest", + ); + + expect(res.status).toBe(400); + expect(res.body).toEqual({ error: true, message: "Invalid zap request" }); + }); + + it("should return invoice for valid request without nostr", async () => { + vi.mocked(User.getUserByName, { partial: true }).mockResolvedValue({ + name: "testUser", + mint_url: "https://mint.minibits.cash/Bitcoin", + pubkey: "testPubkey...", + }); + vi.mocked(wallet.requestMint).mockResolvedValue({ + pr: "lnbc15u1p3xnhl2pp5jptserfk3zk4qy42tlucycrfwxhydvlemu9pqr93tuzlv9cc7g3sdqsvfhkcap3xyhx7un8cqzpgxqzjcsp5f8c52y2stc300gl6s4xswtjpc37hrnnr3c9wvtgjfuvqmpm35evq9qyyssqy4lgd8tj637qcjp05rdpxxykjenthxftej7a2zzmwrmrl70fyj9hvj0rewhzj7jfyuwkwcg9g2jpwtk3wkjtwnkdks84hsnu8xps5vsq4gj5hs", + hash: "456", + }); + const lnProviderMock = vi + .mocked(lnProvider.createInvoice, { partial: true }) + .mockResolvedValue({ + paymentRequest: "invoice", + paymentHash: "hash", + }); + vi.mocked(Transaction.createTransaction, { + partial: true, + }).mockResolvedValue({ + mint_pr: "123", + mint_hash: "456", + server_pr: "invoice", + server_hash: "hash", + user: "testUser", + zap_request: undefined, + amount: 21, + fulfilled: false, + }); + + vi.stubEnv("LNURL_MIN_AMOUNT", "10"); + vi.stubEnv("LNURL_MAX_AMOUNT", "1000000"); + + const res = await request(app).get( + "/.well-known/lnurlp/testUser?amount=21000", + ); + + expect(lnProviderMock).toHaveBeenCalledWith( + 1500, + "Cashu Address", + undefined, + ); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ pr: "invoice", routes: [] }); + }); +}); diff --git a/src/controller/infoController.ts b/src/controller/infoController.ts index 40f8500..232d577 100644 --- a/src/controller/infoController.ts +++ b/src/controller/infoController.ts @@ -1,7 +1,7 @@ import { NextFunction, Request, Response } from "express"; import { User } from "../models"; import { sign, verify } from "jsonwebtoken"; -import { lnProvider } from ".."; +import { lnProvider } from "../config"; import { PaymentJWTPayload } from "../types"; import { usernameRegex } from "../constants/regex"; diff --git a/src/controller/lnurlController.ts b/src/controller/lnurlController.ts index b9de346..04e83ad 100644 --- a/src/controller/lnurlController.ts +++ b/src/controller/lnurlController.ts @@ -2,7 +2,7 @@ import { NextFunction, Request, Response } from "express"; import { Event, nip19 } from "nostr-tools"; import { parseInvoice } from ".././utils/lightning"; -import { lnProvider, wallet } from ".."; +import { lnProvider, wallet } from "../config"; import { Transaction, User } from "../models"; import { createLnurlResponse } from "../utils/lnurl"; import { decodeAndValidateZapRequest } from "../utils/nostr"; @@ -74,6 +74,7 @@ export async function lnurlController( const { amount: mintAmount } = parseInvoice(mintPr); + //TODO:)Parse invoice for expiry and pass it to blink try { invoiceRes = await lnProvider.createInvoice( mintAmount / 1000, diff --git a/src/controller/paidController.ts b/src/controller/paidController.ts index 63abf77..b84f19b 100644 --- a/src/controller/paidController.ts +++ b/src/controller/paidController.ts @@ -1,5 +1,5 @@ import { Request, Response } from "express"; -import { lnProvider, nostrPool, wallet } from ".."; +import { lnProvider, nostrPool, wallet } from "../config"; import { Claim, Transaction } from "../models"; import { createZapReceipt, extractZapRequestData } from "../utils/nostr"; diff --git a/src/index.ts b/src/index.ts index 8f2f942..09cfaf4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,45 +1,9 @@ -import express, { Response } from "express"; -import bodyparser from "body-parser"; -import cors from "cors"; -import { CashuMint, CashuWallet } from "@cashu/cashu-ts"; - -import routes from "./routes"; -import { LightningHandler } from "./utils/lightning"; -import { BlinkProvider } from "./utils/blink"; -import { - SimplePool, - getPublicKey, - useWebSocketImplementation, -} from "nostr-tools"; +import { useWebSocketImplementation } from "nostr-tools"; import { checkEnvVars } from "./utils/general"; -import path from "path"; -import { requireHTTPS } from "./middleware/https"; -import compression from "compression"; +import app from "./app"; useWebSocketImplementation(require("ws")); checkEnvVars(["LNURL_MAX_AMOUNT", "LNURL_MIN_AMOUNT", "MINTURL"]); -export const wallet = new CashuWallet(new CashuMint(process.env.MINTURL!)); -export const lnProvider = new LightningHandler(new BlinkProvider()); -export const nostrPool = new SimplePool(); - -export let ZAP_PUBKEY: string; -if (process.env.ZAP_SECRET_KEY) { - ZAP_PUBKEY = getPublicKey(Buffer.from(process.env.ZAP_SECRET_KEY, "hex")); -} - -const app = express(); - -app.use(bodyparser.json()); -app.use(compression()); -app.use(cors()); -app.use(requireHTTPS); - -app.use(routes); -app.use("/", express.static(path.join(__dirname, "../npubcash-website/dist"))); -app.get("*", (_, res: Response) => { - res.sendFile(path.join(__dirname, "../npubcash-website/dist/index.html")); -}); - app.listen(process.env.PORT || 8000); diff --git a/src/middleware/__tests__/middleware.test.ts b/src/middleware/__tests__/middleware.test.ts new file mode 100644 index 0000000..e956e38 --- /dev/null +++ b/src/middleware/__tests__/middleware.test.ts @@ -0,0 +1,115 @@ +import { describe, expect, test, vi } from "vitest"; +import { NextFunction, Request, Response } from "express"; +import { isAuthMiddleware } from "../auth"; +import { finalizeEvent, generateSecretKey } from "nostr-tools"; + +const headerFunc = vi.fn(); +const resStatus = vi.fn(); + +const sk = generateSecretKey(); + +const validAuthEvent = { + content: "", + kind: 27235, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ["u", "https://test.com/test"], + ["method", "GET"], + ], +}; + +describe("Auth Middleware", () => { + test("pass valid requests on", async () => { + vi.mocked(headerFunc).mockImplementation((header: string) => { + if (header === "host") { + return "test.com"; + } + if (header === "X-Forwarded-Proto") { + return "https"; + } + if (header === "Authorization") { + const signed = finalizeEvent(validAuthEvent, sk); + const authHeader = `Nostr ${btoa(JSON.stringify(signed))}`; + console.log(authHeader); + return authHeader; + } + }); + const req = { header: headerFunc } as unknown as Request; + const res = { status: resStatus } as unknown as Response; + const next = vi.fn() as unknown as NextFunction; + const middleware = isAuthMiddleware("/test", "GET"); + await middleware(req, res, next); + + expect(next).toHaveBeenCalledWith(); + }); + test("should return 400 on missing host header", async () => { + vi.mocked(headerFunc).mockImplementation((header: string) => { + if (header === "host") { + return null; + } + if (header === "X-Forwarded-Proto") { + return "https"; + } + if (header === "Authorization") { + const signed = finalizeEvent(validAuthEvent, sk); + const authHeader = `Nostr ${btoa(JSON.stringify(signed))}`; + console.log(authHeader); + return authHeader; + } + }); + const req = { header: headerFunc } as unknown as Request; + const res = { status: resStatus } as unknown as Response; + const next = vi.fn() as unknown as NextFunction; + const middleware = isAuthMiddleware("/test", "GET"); + await middleware(req, res, next); + + expect(resStatus).toHaveBeenCalledWith(400); + expect(next).toHaveBeenCalledWith(new Error("Missing host header")); + }); + test("should return 400 on missing host header", async () => { + vi.mocked(headerFunc).mockImplementation((header: string) => { + if (header === "host") { + return "test.com"; + } + if (header === "X-Forwarded-Proto") { + return "https"; + } + if (header === "Authorization") { + return null; + } + }); + const req = { header: headerFunc } as unknown as Request; + const res = { status: resStatus } as unknown as Response; + const next = vi.fn() as unknown as NextFunction; + const middleware = isAuthMiddleware("/test", "GET"); + await middleware(req, res, next); + + expect(resStatus).toHaveBeenCalledWith(400); + expect(next).toHaveBeenCalledWith( + new Error("Missing Authorization Header"), + ); + }); + test("should return 400 on missing host header", async () => { + vi.mocked(headerFunc).mockImplementation((header: string) => { + if (header === "host") { + return "test.de"; + } + if (header === "X-Forwarded-Proto") { + return "https"; + } + if (header === "Authorization") { + return "invalid"; + } + }); + const req = { header: headerFunc } as unknown as Request; + const res = { status: resStatus } as unknown as Response; + const next = vi.fn() as unknown as NextFunction; + const middleware = isAuthMiddleware("/test", "GET"); + await middleware(req, res, next); + + expect(resStatus).toHaveBeenCalledWith(400); + expect(next).toHaveBeenCalledWith( + new Error("Invalid Authorization Header"), + ); + }); +}); diff --git a/src/models/claim.ts b/src/models/claim.ts index 88a90b2..e92b0a1 100644 --- a/src/models/claim.ts +++ b/src/models/claim.ts @@ -4,7 +4,6 @@ import { createSanitizedValueString, queryWrapper, } from "../utils/database"; -import { ClaimStatus } from "../types"; export class Claim { id: number; diff --git a/src/utils/__tests__/database.test.ts b/src/utils/__tests__/database.test.ts index 699b8fb..644c745 100644 --- a/src/utils/__tests__/database.test.ts +++ b/src/utils/__tests__/database.test.ts @@ -1,8 +1,23 @@ -import { describe, expect, test } from "vitest"; -import { createBulkInsertPayload } from "../database"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { createBulkInsertPayload, createBulkInsertQuery } from "../database"; + +const mockedMethod = vi.hoisted(() => { + return vi.fn(); +}); + +vi.mock("pg", () => { + return { + Pool: vi.fn(() => ({ + query: mockedMethod, + })), + }; +}); describe("Bulk Insert", () => { - test("create buld insert string", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + test("create bulk insert string", () => { const columns = ["name", "age", "gender"]; const values = [ ["Dan", 24, "male"], @@ -15,4 +30,35 @@ describe("Bulk Insert", () => { expect(insertString.flatValues.length).toBe(12); expect(() => createBulkInsertPayload(columns, invalidValues)).toThrow(); }); + test("execute bulk insert query", () => { + mockedMethod.mockReturnValue(Promise.resolve("bla")); + return createBulkInsertQuery( + "test", + ["name", "age", "gender"], + [ + ["Dan", 24, "male"], + ["Anna", 21, "female"], + ["Steve", 36, "male"], + ["Steve", 36, "male"], + ], + ).then(() => { + expect(mockedMethod).toHaveBeenCalledWith( + "INSERT INTO test (name,age,gender) VALUES ($1,$2,$3),($4,$5,$6),($7,$8,$9),($10,$11,$12);", + [ + "Dan", + 24, + "male", + "Anna", + 21, + "female", + "Steve", + 36, + "male", + "Steve", + 36, + "male", + ], + ); + }); + }); }); diff --git a/src/utils/__tests__/general.test.ts b/src/utils/__tests__/general.test.ts new file mode 100644 index 0000000..9ebe4a1 --- /dev/null +++ b/src/utils/__tests__/general.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, test, vi } from "vitest"; +import { checkEnvVars } from "../general"; +import { beforeEach } from "node:test"; + +describe("General utility functions", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + test("should throw if env vars are missing", () => { + const mock = vi.fn().mockImplementation(checkEnvVars); + expect(() => { + mock(["testEnv1", "testEnv2"]); + }).toThrow(); + }); + test("should not throw if env vars are set", () => { + vi.stubEnv("testEnv1", "true"); + vi.stubEnv("testEnv2", "true"); + const mock = vi.fn().mockImplementation(checkEnvVars); + mock(["testEnv1", "testEnv2"]); + expect(mock).toHaveBeenCalled(); + }); +}); diff --git a/src/utils/__tests__/lnurl.test.ts b/src/utils/__tests__/lnurl.test.ts index f100a93..121a2e0 100644 --- a/src/utils/__tests__/lnurl.test.ts +++ b/src/utils/__tests__/lnurl.test.ts @@ -1,11 +1,16 @@ import { afterEach, describe, expect, test, vi } from "vitest"; import { createLnurlResponse } from "../lnurl"; +import { generateSecretKey, getPublicKey } from "nostr-tools"; +import { ZAP_PUBKEY } from "../../config"; -vi.mock("../../index.ts", () => ({ - get ZAP_PUBKEY() { - return "123"; - }, -})); +const sk = generateSecretKey(); + +vi.mock("../../config", async () => { + return { + ZAP_PUBKEY: + "1f81f34debf4577841c3b0f2834f7308eb522d9dd08292f39308bee2f7c07275", + }; +}); describe("Generating LNURL response", () => { const originalEnv = { ...process.env }; @@ -13,8 +18,9 @@ describe("Generating LNURL response", () => { afterEach(() => { process.env = originalEnv; }); - test("Nostr key is set", () => { - process.env.ZAP_SECRET_KEY = "secret"; + test("Nostr key is set", async () => { + process.env.ZAP_SECRET_KEY = + "97b5bf05d654feb9fbc0d615944d94e7139dd0475980952377ba79f799903644"; process.env.LNURL_MAX_AMOUNT = "10"; process.env.LNURL_MIN_AMOUNT = "1"; process.env.HOSTNAME = "https://npub.cash"; @@ -28,7 +34,12 @@ describe("Generating LNURL response", () => { metadata: '[["text/plain","A cashu lightning address... Neat!"]]', minSendable: 1, tag: "payRequest", - nostrPubkey: "123", + nostrPubkey: getPublicKey( + Buffer.from( + "97b5bf05d654feb9fbc0d615944d94e7139dd0475980952377ba79f799903644", + "hex", + ), + ), }); }); test("Nostr key is not set", () => { diff --git a/src/utils/lnurl.ts b/src/utils/lnurl.ts index 97568d4..f4c6286 100644 --- a/src/utils/lnurl.ts +++ b/src/utils/lnurl.ts @@ -1,4 +1,4 @@ -import { ZAP_PUBKEY } from ".."; +import { ZAP_PUBKEY } from "../config"; export function createLnurlResponse(username: string) { if (process.env.ZAP_SECRET_KEY) { diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..da1b0c2 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from "vite"; +import { configDefaults, coverageConfigDefaults } from "vitest/config"; + +export default defineConfig({ + test: { + exclude: [...configDefaults.exclude, "npubcash-website"], + coverage: { + exclude: [ + ...coverageConfigDefaults.exclude, + "npubcash-website", + "build.js", + "migrations", + "**/blink.ts", + ], + }, + }, +});