diff --git a/.github/workflows/semantic.yml b/.github/workflows/semantic.yml index 3833e09..20ec155 100644 --- a/.github/workflows/semantic.yml +++ b/.github/workflows/semantic.yml @@ -34,6 +34,11 @@ jobs: - name: Prepare Db run: npm run prepare-test-db + # - name: Docker build + # run: npm run docker:build + # - name: Docker run + # run: npm run docker:run + - name: Test uses: paambaati/codeclimate-action@v9.0.0 env: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6fe8d9f..9c77d7e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,6 +31,11 @@ jobs: - name: Prepare Db run: npm run prepare-test-db + # - name: Docker build + # run: npm run docker:build + # - name: Docker run + # run: npm run docker:run + - name: Test uses: paambaati/codeclimate-action@v9.0.0 env: diff --git a/Dockerfile b/Dockerfile index 971d0bd..70c6398 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,4 +12,6 @@ FROM mcr.microsoft.com/dotnet/aspnet:8.0 WORKDIR /app COPY --from=build /app ./ +EXPOSE 3000 + ENTRYPOINT ["dotnet", "Codibre.GrpcSqlProxy.Api.dll"] \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 054f842..32e8935 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,35 @@ "devDependencies": { "@types/lint-staged": "^13.3.0", "husky": "^9.0.11", - "lint-staged": "^15.2.10" + "lint-staged": "^15.2.10", + "run-container": "^2.0.12" + } + }, + "node_modules/@balena/dockerignore": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", + "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", + "dev": true + }, + "node_modules/@types/docker-modem": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", + "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/dockerode": { + "version": "3.3.31", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.31.tgz", + "integrity": "sha512-42R9eoVqJDSvVspV89g7RwRqfNExgievLNWoHkg7NoWIqAmavIbgQBb4oc0qRtHkxE+I3Xxvqv7qVXFABKPBTg==", + "dev": true, + "dependencies": { + "@types/docker-modem": "*", + "@types/node": "*", + "@types/ssh2": "*" } }, "node_modules/@types/lint-staged": { @@ -21,6 +49,39 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.8.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.1.tgz", + "integrity": "sha512-k6Gi8Yyo8EtrNtkHXutUu2corfDf9su95VYVP10aGYMMROM6SAItZi0w1XszA6RtWTHSVp5OeFof37w0IEqCQg==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@types/ssh2": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.1.tgz", + "integrity": "sha512-ZIbEqKAsi5gj35y4P4vkJYly642wIbY6PqoN0xiyQGshKUGXR9WQjF/iF9mXBQ8uBKy3ezfsCkcoHKhd0BzuDA==", + "dev": true, + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.59", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.59.tgz", + "integrity": "sha512-vizm2EqwV/7Zay+A6J3tGl9Lhr7CjZe2HmWS988sefiEmsyP9CeXEleho6i4hJk/8UtZAo0bWN4QPZZr83RxvQ==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "node_modules/ansi-escapes": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", @@ -63,6 +124,55 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -76,6 +186,40 @@ "node": ">=8" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buildcheck": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", + "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/chalk": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", @@ -88,6 +232,12 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -137,6 +287,21 @@ "node": ">=18" } }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -169,6 +334,35 @@ } } }, + "node_modules/docker-modem": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz", + "integrity": "sha512-89zhop5YVhcPEt5FpUFGr3cDyceGhq/F9J+ZndQ4KfqNvfbJpPMfgeixFgUj5OjCYAboElqODxY5Z1EBsSa6sg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^1.15.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/dockerode": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.2.tgz", + "integrity": "sha512-9wM1BVpVMFr2Pw3eJNXrYYt6DT9k0xMcsSCjtPvyQ+xa1iPg/Mo3T/gUcwI0B2cczqCeCYRPF8yFYDwtFXT0+w==", + "dev": true, + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "docker-modem": "^5.0.3", + "tar-fs": "~2.0.1" + }, + "engines": { + "node": ">= 8.0" + } + }, "node_modules/emoji-regex": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", @@ -176,6 +370,15 @@ "dev": true, "license": "MIT" }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -232,6 +435,12 @@ "node": ">=8" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, "node_modules/get-east-asian-width": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", @@ -281,6 +490,32 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, "node_modules/is-fullwidth-code-point": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", @@ -479,6 +714,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -486,6 +727,13 @@ "dev": true, "license": "MIT" }, + "node_modules/nan": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", + "dev": true, + "optional": true + }, "node_modules/npm-run-path": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", @@ -513,6 +761,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, "node_modules/onetime": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", @@ -562,6 +819,30 @@ "node": ">=0.10" } }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -602,6 +883,150 @@ "dev": true, "license": "MIT" }, + "node_modules/run-container": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/run-container/-/run-container-2.0.12.tgz", + "integrity": "sha512-edsmzIQx1/T5eb89qiTxz6Yvm3j+5NUONCItadUcNeQoHf/1zm3xO+x0rs429lNLUXWqTzKouIDCtT9AOp5O0Q==", + "dev": true, + "dependencies": { + "@types/dockerode": "^3.3.19", + "dockerode": "^4.0.0", + "execa": "^5.0.0" + } + }, + "node_modules/run-container/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/run-container/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-container/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/run-container/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-container/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/run-container/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/run-container/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-container/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/run-container/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -652,6 +1077,39 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", + "dev": true + }, + "node_modules/ssh2": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz", + "integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.20.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -707,6 +1165,34 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tar-fs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", + "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==", + "dev": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.0.0" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -720,6 +1206,24 @@ "node": ">=8.0" } }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -753,6 +1257,12 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, "node_modules/yaml": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", diff --git a/package.json b/package.json index e9f04bb..f85138b 100644 --- a/package.json +++ b/package.json @@ -13,12 +13,15 @@ "test": "dotnet test", "prepare-test-db": "cd devops && sh prepare-db.sh && cd ..", "unload-test-db": "cd devops && docker compose down && cd ..", + "docker:build": "docker build . -t test-grpc-client", + "docker:run": "docker run -d --network=host test-grpc-client", "test:coverage": "dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=lcov /p:CoverletOutput=./coverage/lcov.info" }, "devDependencies": { "@types/lint-staged": "^13.3.0", "husky": "^9.0.11", - "lint-staged": "^15.2.10" + "lint-staged": "^15.2.10", + "run-container": "^2.0.12" }, "lint-staged": { "*.cs": "dotnet format --include" diff --git a/run-tests.sh b/run-tests.sh new file mode 100644 index 0000000..eec0bef --- /dev/null +++ b/run-tests.sh @@ -0,0 +1,4 @@ +npm run docker:build +docker_id=$(docker run -d --network=host test-grpc-client) +npm run test:coverage +docker kill $docker_id \ No newline at end of file diff --git a/src/Codibre.GrpcSqlProxy.Api/Program.cs b/src/Codibre.GrpcSqlProxy.Api/Program.cs index a7b23ef..2c3d7b2 100644 --- a/src/Codibre.GrpcSqlProxy.Api/Program.cs +++ b/src/Codibre.GrpcSqlProxy.Api/Program.cs @@ -7,6 +7,7 @@ public class Program public static async Task Main(string[] args) { var app = GetApp(args); + Console.WriteLine($"Listening to {string.Join(",", app.Urls)}"); await app.RunAsync(); } @@ -26,7 +27,8 @@ public static WebApplication GetApp(string[] args) // Configure the HTTP request pipeline. app.MapGrpcService(); app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); - app.Urls.Add($"http://localhost:{args.FirstOrDefault() ?? app.Configuration.GetSection("PORT").Value ?? "3000"}"); + var port = args.FirstOrDefault() ?? app.Configuration.GetSection("PORT").Value ?? "3000"; + app.Urls.Add($"http://0.0.0.0:{port}"); return app; } } \ No newline at end of file diff --git a/src/Codibre.GrpcSqlProxy.Api/Services/SqlProxyService.cs b/src/Codibre.GrpcSqlProxy.Api/Services/SqlProxyService.cs index dd3dd57..30ef000 100644 --- a/src/Codibre.GrpcSqlProxy.Api/Services/SqlProxyService.cs +++ b/src/Codibre.GrpcSqlProxy.Api/Services/SqlProxyService.cs @@ -23,6 +23,11 @@ ServerCallContext context while (await requestStream.MoveNext()) { var request = requestStream.Current; + if (request.Query == "NOOP") + { + await responseStream.WriteEmpty(request.Id); + continue; + } await responseStream.Catch(request.Id, async () => { var connString = request.ConnString; @@ -30,8 +35,12 @@ await responseStream.Catch(request.Id, async () => proxyContext ??= await ProxyContext.GetConnection(responseStream, request); if (proxyContext is not null) { - if (request.PacketSize <= 0) request.PacketSize = 1000; - responseStream.PipeResponse(request, proxyContext); + if (request.Query == "CONNECT") await responseStream.WriteEmpty(request.Id); + else + { + if (request.PacketSize <= 0) request.PacketSize = 1000; + responseStream.PipeResponse(request, proxyContext); + } } }, proxyContext); } diff --git a/src/Codibre.GrpcSqlProxy.Api/Utils/ProxyContext.cs b/src/Codibre.GrpcSqlProxy.Api/Utils/ProxyContext.cs index 203ee29..6cb4686 100644 --- a/src/Codibre.GrpcSqlProxy.Api/Utils/ProxyContext.cs +++ b/src/Codibre.GrpcSqlProxy.Api/Utils/ProxyContext.cs @@ -7,7 +7,7 @@ namespace Codibre.GrpcSqlProxy.Api.Utils; -internal sealed class ProxyContext(string _connectionString) : IAsyncDisposable +internal sealed class ProxyContext(string _connectionString) : IAsyncDisposable, IDisposable { public string ConnectionString { get; set; } = _connectionString; private readonly SqlConnection _connection = new(_connectionString); @@ -79,6 +79,8 @@ public async ValueTask DisposeAsync() // Ignore if error occurs as no transaction were there } } - if (_connection is not null) await _connection.CloseAsync(); + if (_connection.State != System.Data.ConnectionState.Closed) await _connection.CloseAsync(); } + + public void Dispose() => _ = DisposeAsync(); } \ No newline at end of file diff --git a/src/Codibre.GrpcSqlProxy.Api/Utils/ResponsStreamExtensions.cs b/src/Codibre.GrpcSqlProxy.Api/Utils/ResponsStreamExtensions.cs index 22dbb22..8a14463 100644 --- a/src/Codibre.GrpcSqlProxy.Api/Utils/ResponsStreamExtensions.cs +++ b/src/Codibre.GrpcSqlProxy.Api/Utils/ResponsStreamExtensions.cs @@ -8,21 +8,29 @@ namespace Codibre.GrpcSqlProxy.Api.Utils; public static class ResponseStreamExtensions { + public static async Task WriteEmpty(this IServerStreamWriter responseStream, string id) + { + using var semaphore = await AsyncLock.Lock(responseStream); + await responseStream.WriteAsync(SqlResponseEx.CreateEmpty(id)); + } + public static async Task WriteSqlResponse(this IServerStreamWriter responseStream, SqlResponse response) { using var semaphore = await AsyncLock.Lock(responseStream); await responseStream.WriteAsync(response); } - internal static async Task Catch(this IServerStreamWriter responseStream, string id, Func callback, ProxyContext? context) + internal static async Task Catch(this IServerStreamWriter responseStream, string id, Func callback, ProxyContext? context) { try { await callback(); + return true; } catch (Exception ex) { responseStream.WriteError(id, ex.Message, context?.Index ?? 0); + return false; } } diff --git a/src/Codibre.GrpcSqlProxy.Api/Utils/SqlResponseEx.cs b/src/Codibre.GrpcSqlProxy.Api/Utils/SqlResponseEx.cs index 4a6f414..9340ea9 100644 --- a/src/Codibre.GrpcSqlProxy.Api/Utils/SqlResponseEx.cs +++ b/src/Codibre.GrpcSqlProxy.Api/Utils/SqlResponseEx.cs @@ -14,6 +14,16 @@ public static class SqlResponseEx Index = packet.Index, }; + public static SqlResponse CreateEmpty(string id) => new() + { + Id = id, + Result = ByteString.Empty, + Error = "", + Last = LastEnum.Last, + Compressed = false, + Index = 0, + }; + public static SqlResponse CreateError(string id, string error, int index) => new() { Id = id, diff --git a/src/Codibre.GrpcSqlProxy.Client/ISqlProxyClient.cs b/src/Codibre.GrpcSqlProxy.Client/ISqlProxyClient.cs index 3b4c080..fdff96a 100644 --- a/src/Codibre.GrpcSqlProxy.Client/ISqlProxyClient.cs +++ b/src/Codibre.GrpcSqlProxy.Client/ISqlProxyClient.cs @@ -4,4 +4,5 @@ public interface ISqlProxyClient { ISqlProxyClientTunnel Channel { get; } ISqlProxyClientTunnel CreateChannel(); + ValueTask Initialize(); } \ No newline at end of file diff --git a/src/Codibre.GrpcSqlProxy.Client/ISqlProxyClientTunnel.cs b/src/Codibre.GrpcSqlProxy.Client/ISqlProxyClientTunnel.cs index ecf5947..b774281 100644 --- a/src/Codibre.GrpcSqlProxy.Client/ISqlProxyClientTunnel.cs +++ b/src/Codibre.GrpcSqlProxy.Client/ISqlProxyClientTunnel.cs @@ -5,7 +5,10 @@ namespace Codibre.GrpcSqlProxy.Client; public interface ISqlProxyClientTunnel : IDisposable { ISqlProxyBatchQuery Batch { get; } + bool Disposed { get; } void Start(); + ValueTask Noop(); + ValueTask Connect(); ValueTask BeginTransaction(); ValueTask Commit(); ValueTask Rollback(); diff --git a/src/Codibre.GrpcSqlProxy.Client/Impl/ContextInfo.cs b/src/Codibre.GrpcSqlProxy.Client/Impl/ContextInfo.cs index 822a7c7..3761498 100644 --- a/src/Codibre.GrpcSqlProxy.Client/Impl/ContextInfo.cs +++ b/src/Codibre.GrpcSqlProxy.Client/Impl/ContextInfo.cs @@ -25,7 +25,6 @@ Action clear public void Dispose() { Monitor.Dispose(); - Stream.Dispose(); Disposed = true; if (_executionContext is not null) ExecutionContext.Run(_executionContext, (_) => _clear(), null); } diff --git a/src/Codibre.GrpcSqlProxy.Client/Impl/GrpcSqlProxyClient.cs b/src/Codibre.GrpcSqlProxy.Client/Impl/GrpcSqlProxyClient.cs index 62b9072..064e2dd 100644 --- a/src/Codibre.GrpcSqlProxy.Client/Impl/GrpcSqlProxyClient.cs +++ b/src/Codibre.GrpcSqlProxy.Client/Impl/GrpcSqlProxyClient.cs @@ -8,7 +8,20 @@ public class GrpcSqlProxyClient(SqlProxyClientOptions options) : ISqlProxyClient private readonly AsyncLocal _asyncLocal = new(); private readonly SqlProxy.SqlProxyClient _client = new(GrpcChannel.ForAddress(options.Url)); - public ISqlProxyClientTunnel Channel => _asyncLocal.Value ??= CreateChannel(); + public ISqlProxyClientTunnel Channel + { + get + { + if (_asyncLocal.Value is null || _asyncLocal.Value.Disposed) return _asyncLocal.Value = CreateChannel(); + return _asyncLocal.Value; + } + } public ISqlProxyClientTunnel CreateChannel() => new SqlProxyClientTunnel(() => _client.Run(), options); + + public async ValueTask Initialize() + { + using var channel = CreateChannel(); + await channel.Noop(); + } } \ No newline at end of file diff --git a/src/Codibre.GrpcSqlProxy.Client/Impl/SqlProxyClientTunnel.cs b/src/Codibre.GrpcSqlProxy.Client/Impl/SqlProxyClientTunnel.cs index fa2ec83..11ee585 100644 --- a/src/Codibre.GrpcSqlProxy.Client/Impl/SqlProxyClientTunnel.cs +++ b/src/Codibre.GrpcSqlProxy.Client/Impl/SqlProxyClientTunnel.cs @@ -31,6 +31,8 @@ private ContextInfo Context public ISqlProxyBatchQuery Batch { get; } + public bool Disposed { get; private set; } = false; + internal SqlProxyClientTunnel( Func> getStream, SqlProxyClientOptions clientOptions @@ -171,6 +173,7 @@ public void Dispose() { _context.Value?.Dispose(); _context.Value = null; + Disposed = true; } public ValueTask BeginTransaction() @@ -201,4 +204,7 @@ public void OnError(ErrorHandlerEvent handler) } public void Start() => _ = Context; + + public ValueTask Noop() => Execute("NOOP"); + public ValueTask Connect() => Execute("CONNECT"); } \ No newline at end of file diff --git a/test/Codibre.GrpcSqlProxy.Test/Entities.cs b/test/Codibre.GrpcSqlProxy.Test/Entities.cs new file mode 100644 index 0000000..f14ed6a --- /dev/null +++ b/test/Codibre.GrpcSqlProxy.Test/Entities.cs @@ -0,0 +1,14 @@ +namespace Codibre.GrpcSqlProxy.Test; +public class TB_PRODUTO +{ + public int CD_PRODUTO { get; set; } +} +public class TB_PEDIDO +{ + public int CD_PEDIDO { get; set; } +} + +public class TB_PESSOA +{ + public int CD_PESSOA { get; set; } +} \ No newline at end of file diff --git a/test/Codibre.GrpcSqlProxy.Test/GrpcSqlProxyClientAsyncLocalTest.cs b/test/Codibre.GrpcSqlProxy.Test/GrpcSqlProxyClientAsyncLocalTest.cs index 67a1701..44825f8 100644 --- a/test/Codibre.GrpcSqlProxy.Test/GrpcSqlProxyClientAsyncLocalTest.cs +++ b/test/Codibre.GrpcSqlProxy.Test/GrpcSqlProxyClientAsyncLocalTest.cs @@ -1,37 +1,40 @@ -using Codibre.GrpcSqlProxy.Api; +using System.Diagnostics; +using Codibre.GrpcSqlProxy.Api; using Codibre.GrpcSqlProxy.Client; using Codibre.GrpcSqlProxy.Client.Impl; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Xunit.Abstractions; namespace Codibre.GrpcSqlProxy.Test; -public class GrpcSqlProxyClientAsyncLocalTest +[Collection("TestServerCollection")] +public class GrpcSqlProxyClientAsyncLocalTest(ITestOutputHelper _testOutputHelper, TestServer _testServer) { [Fact] public async Task Should_Keep_Transaction_Opened() { // Arrange - var server = await TestServer.Get(); - var client = new GrpcSqlProxyClient( - new SqlProxyClientOptions( - server.Url, - server.Config.GetConnectionString("SqlConnection") ?? throw new Exception("No connection string") - ) - { - Compress = false - } - ); + var watch = Stopwatch.StartNew(); + var client = await _testServer.GetClient(_testOutputHelper); // Act + _testOutputHelper.WriteLine(watch.Elapsed.ToString()); await client.Channel.Execute("DELETE FROM TB_PEDIDO WHERE CD_PEDIDO = 400001"); + _testOutputHelper.WriteLine(watch.Elapsed.ToString()); await client.Channel.BeginTransaction(); + _testOutputHelper.WriteLine(watch.Elapsed.ToString()); await client.Channel.Execute("INSERT INTO TB_PEDIDO (CD_PEDIDO) VALUES (400001)"); + _testOutputHelper.WriteLine(watch.Elapsed.ToString()); var result1 = await client.Channel.QueryFirstOrDefault("SELECT * FROM TB_PEDIDO WHERE CD_PEDIDO = 400001"); + _testOutputHelper.WriteLine(watch.Elapsed.ToString()); await client.Channel.Rollback(); + _testOutputHelper.WriteLine(watch.Elapsed.ToString()); var result2 = await client.Channel.Query("SELECT * FROM TB_PEDIDO WHERE CD_PEDIDO = 400001").ToArrayAsync(); + _testOutputHelper.WriteLine(watch.Elapsed.ToString()); + watch.Stop(); // Assert result1.Should().BeOfType(); @@ -44,9 +47,9 @@ public async Task Should_Keep_Transaction_Opened() public async Task Should_Inject_SqlProxy_Properly() { // Arrange - var server = await TestServer.Get(); + await _testServer.GetClient(_testOutputHelper); var builder = Host.CreateApplicationBuilder([]); - builder.Configuration.GetSection("GrpcSqlProxy").GetSection("Url").Value = server.Url; + builder.Configuration.GetSection("GrpcSqlProxy").GetSection("Url").Value = _testServer.Url; builder.Configuration.GetSection("GrpcSqlProxy").GetSection("Compress").Value = "False"; builder.Configuration.GetSection("GrpcSqlProxy").GetSection("PacketSize").Value = "2000"; builder.Services.AddGrpcSqlProxy(); @@ -72,16 +75,7 @@ public async Task Should_Inject_SqlProxy_Properly() public async Task Should_Use_Compression() { // Arrange - var server = await TestServer.Get(); - var client = new GrpcSqlProxyClient( - new SqlProxyClientOptions( - server.Url, - server.Config.GetConnectionString("SqlConnection") ?? throw new Exception("No connection string") - ) - { - Compress = true - } - ); + var client = await _testServer.GetClient(_testOutputHelper); // Act await client.Channel.BeginTransaction(); @@ -101,16 +95,7 @@ public async Task Should_Use_Compression() public async Task Should_Throw_Error_For_Invalid_Queries() { // Arrange - var server = await TestServer.Get(); - var client = new GrpcSqlProxyClient( - new SqlProxyClientOptions( - server.Url, - server.Config.GetConnectionString("SqlConnection") ?? throw new Exception("No connection string") - ) - { - Compress = false - } - ); + var client = await _testServer.GetClient(_testOutputHelper); Exception? thrownException = null; // Act @@ -132,16 +117,7 @@ public async Task Should_Throw_Error_For_Invalid_Queries() public async Task Should_Keep_Parallel_Transaction_Opened() { // Arrange - var server = await TestServer.Get(); - var client = new GrpcSqlProxyClient( - new SqlProxyClientOptions( - server.Url, - server.Config.GetConnectionString("SqlConnection") ?? throw new Exception("No connection string") - ) - { - Compress = false - } - ); + var client = await _testServer.GetClient(_testOutputHelper); // Act using var channel2 = client.CreateChannel(); diff --git a/test/Codibre.GrpcSqlProxy.Test/GrpcSqlProxyClientBatchTest.cs b/test/Codibre.GrpcSqlProxy.Test/GrpcSqlProxyClientBatchTest.cs index 7e5c53a..df8af84 100644 --- a/test/Codibre.GrpcSqlProxy.Test/GrpcSqlProxyClientBatchTest.cs +++ b/test/Codibre.GrpcSqlProxy.Test/GrpcSqlProxyClientBatchTest.cs @@ -3,10 +3,12 @@ using Codibre.GrpcSqlProxy.Client.Impl; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; +using Xunit.Abstractions; namespace Codibre.GrpcSqlProxy.Test; -public class GrpcSqlProxyClientBatchTest +[Collection("TestServerCollection")] +public class GrpcSqlProxyClientBatchTest(ITestOutputHelper _testOutputHelper, TestServer _testServer) { private IEnumerable GetList() { @@ -17,16 +19,7 @@ private IEnumerable GetList() public async Task Should_Run_Transaction_In_Batch() { // Arrange - var server = await TestServer.Get(); - var client = new GrpcSqlProxyClient( - new SqlProxyClientOptions( - server.Url, - server.Config.GetConnectionString("SqlConnection") ?? throw new Exception("No connection string") - ) - { - Compress = false - } - ); + var client = await _testServer.GetClient(_testOutputHelper); // Act using var channel = client.CreateChannel(); @@ -47,16 +40,7 @@ await channel.Batch.RunInTransaction(() => public async Task Should_Run_Transaction_In_One_RoundTrip() { // Arrange - var server = await TestServer.Get(); - var client = new GrpcSqlProxyClient( - new SqlProxyClientOptions( - server.Url, - server.Config.GetConnectionString("SqlConnection") ?? throw new Exception("No connection string") - ) - { - Compress = false - } - ); + var client = await _testServer.GetClient(_testOutputHelper); // Act using var channel = client.CreateChannel(); @@ -82,16 +66,7 @@ await channel.Batch.RunInTransaction(() => public async Task Should_Run_Query_Batch() { // Arrange - var server = await TestServer.Get(); - var client = new GrpcSqlProxyClient( - new SqlProxyClientOptions( - server.Url, - server.Config.GetConnectionString("SqlConnection") ?? throw new Exception("No connection string") - ) - { - Compress = false - } - ); + var client = await _testServer.GetClient(_testOutputHelper); // Act using var channel = client.CreateChannel(); @@ -125,16 +100,7 @@ public async Task Should_Run_Query_Batch() public async Task Should_Run_Query_Batch_Using_CustomOptions() { // Arrange - var server = await TestServer.Get(); - var client = new GrpcSqlProxyClient( - new SqlProxyClientOptions( - server.Url, - server.Config.GetConnectionString("SqlConnection") ?? throw new Exception("No connection string") - ) - { - Compress = false - } - ); + var client = await _testServer.GetClient(_testOutputHelper); // Act using var channel = client.CreateChannel(); @@ -172,16 +138,7 @@ await channel.Batch.RunQueries(new() public async Task Should_Deal_With_Parameter_Limitation() { // Arrange - var server = await TestServer.Get(); - var client = new GrpcSqlProxyClient( - new SqlProxyClientOptions( - server.Url, - server.Config.GetConnectionString("SqlConnection") ?? throw new Exception("No connection string") - ) - { - Compress = false - } - ); + var client = await _testServer.GetClient(_testOutputHelper); // Act using var channel = client.CreateChannel(); @@ -219,16 +176,7 @@ FROM TB_PEDIDO public async Task Should_Run_PrepareBatchQuery_WithAsyncCallback() { // Arrange - var server = await TestServer.Get(); - var client = new GrpcSqlProxyClient( - new SqlProxyClientOptions( - server.Url, - server.Config.GetConnectionString("SqlConnection") ?? throw new Exception("No connection string") - ) - { - Compress = false - } - ); + var client = await _testServer.GetClient(_testOutputHelper); // Act using var channel = client.CreateChannel(); diff --git a/test/Codibre.GrpcSqlProxy.Test/GrpcSqlProxyClientErrorTest.cs b/test/Codibre.GrpcSqlProxy.Test/GrpcSqlProxyClientErrorTest.cs index 180cad2..4230838 100644 --- a/test/Codibre.GrpcSqlProxy.Test/GrpcSqlProxyClientErrorTest.cs +++ b/test/Codibre.GrpcSqlProxy.Test/GrpcSqlProxyClientErrorTest.cs @@ -3,25 +3,18 @@ using Codibre.GrpcSqlProxy.Client.Impl; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; +using Xunit.Abstractions; namespace Codibre.GrpcSqlProxy.Test; -public class GrpcSqlProxyClientErrorTest +[Collection("TestServerCollection")] +public class GrpcSqlProxyClientErrorTest(ITestOutputHelper _testOutputHelper, TestServer _testServer) { [Fact] public async Task Should_Throw_An_Error_For_InvalidTable() { // Arrange - var server = await TestServer.Get(); - var client = new GrpcSqlProxyClient( - new SqlProxyClientOptions( - server.Url, - server.Config.GetConnectionString("SqlConnection") ?? throw new Exception("No connection string") - ) - { - Compress = false - } - ); + var client = await _testServer.GetClient(_testOutputHelper); Exception? thrownError = null; // Act @@ -43,16 +36,7 @@ public async Task Should_Throw_An_Error_For_InvalidTable() public async Task Should_Throw_An_Error_For_InvalidSyntax() { // Arrange - var server = await TestServer.Get(); - var client = new GrpcSqlProxyClient( - new SqlProxyClientOptions( - server.Url, - server.Config.GetConnectionString("SqlConnection") ?? throw new Exception("No connection string") - ) - { - Compress = false - } - ); + var client = await _testServer.GetClient(_testOutputHelper); Exception? thrownError = null; // Act @@ -73,16 +57,7 @@ public async Task Should_Throw_An_Error_For_InvalidSyntax() public async Task Should_Throw_An_Error_For_InvalidTable_OnBatch() { // Arrange - var server = await TestServer.Get(); - var client = new GrpcSqlProxyClient( - new SqlProxyClientOptions( - server.Url, - server.Config.GetConnectionString("SqlConnection") ?? throw new Exception("No connection string") - ) - { - Compress = false - } - ); + var client = await _testServer.GetClient(_testOutputHelper); Exception? thrownError = null; // Act @@ -105,16 +80,7 @@ public async Task Should_Throw_An_Error_For_InvalidTable_OnBatch() public async Task Should_Throw_An_Error_For_InvalidSyntax_OnBatch() { // Arrange - var server = await TestServer.Get(); - var client = new GrpcSqlProxyClient( - new SqlProxyClientOptions( - server.Url, - server.Config.GetConnectionString("SqlConnection") ?? throw new Exception("No connection string") - ) - { - Compress = false - } - ); + var client = await _testServer.GetClient(_testOutputHelper); Exception? thrownError = null; // Act diff --git a/test/Codibre.GrpcSqlProxy.Test/GrpcSqlProxyClientTest.cs b/test/Codibre.GrpcSqlProxy.Test/GrpcSqlProxyClientTest.cs index 47af7e1..90526a4 100644 --- a/test/Codibre.GrpcSqlProxy.Test/GrpcSqlProxyClientTest.cs +++ b/test/Codibre.GrpcSqlProxy.Test/GrpcSqlProxyClientTest.cs @@ -1,38 +1,54 @@ -using Codibre.GrpcSqlProxy.Api; +using System.Diagnostics; +using Codibre.GrpcSqlProxy.Api; using Codibre.GrpcSqlProxy.Client; using Codibre.GrpcSqlProxy.Client.Impl; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Xunit.Abstractions; namespace Codibre.GrpcSqlProxy.Test; -public class GrpcSqlProxyClientTest +[Collection("TestServerCollection")] +public class GrpcSqlProxyClientTest(ITestOutputHelper _testOutputHelper, TestServer _testServer) { [Fact] public async Task Should_Keep_Transaction_Opened() { // Arrange - var server = await TestServer.Get(); - var client = new GrpcSqlProxyClient( - new SqlProxyClientOptions( - server.Url, - server.Config.GetConnectionString("SqlConnection") ?? throw new Exception("No connection string") - ) - { - Compress = false - } - ); + var client = await _testServer.GetClient(_testOutputHelper); // Act + await Task.WhenAll( + RunTest(client), + RunTest(client), + RunTest(client) + ); + } + + private async Task RunTest(GrpcSqlProxyClient client) + { + var watch = Stopwatch.StartNew(); + _testOutputHelper.WriteLine($"First {watch.Elapsed}"); using var channel = client.CreateChannel(); + _testOutputHelper.WriteLine($"Create {watch.Elapsed}"); + await channel.Noop(); + _testOutputHelper.WriteLine($"Noop {watch.Elapsed}"); + await channel.Connect(); + _testOutputHelper.WriteLine($"Connect {watch.Elapsed}"); await channel.Execute("DELETE FROM TB_PEDIDO WHERE CD_PEDIDO = 600001"); + _testOutputHelper.WriteLine(watch.Elapsed.ToString()); await channel.BeginTransaction(); + _testOutputHelper.WriteLine(watch.Elapsed.ToString()); await channel.Execute("INSERT INTO TB_PEDIDO (CD_PEDIDO) VALUES (600001)"); + _testOutputHelper.WriteLine(watch.Elapsed.ToString()); var result1 = await channel.QueryFirstOrDefault("SELECT * FROM TB_PEDIDO WHERE CD_PEDIDO = 600001"); + _testOutputHelper.WriteLine(watch.Elapsed.ToString()); await channel.Rollback(); + _testOutputHelper.WriteLine(watch.Elapsed.ToString()); var result2 = await channel.Query("SELECT * FROM TB_PEDIDO WHERE CD_PEDIDO = 600001").ToArrayAsync(); + _testOutputHelper.WriteLine(watch.Elapsed.ToString()); // Assert result1.Should().BeOfType(); @@ -45,9 +61,9 @@ public async Task Should_Keep_Transaction_Opened() public async Task Should_Inject_SqlProxy_Properly() { // Arrange - var server = await TestServer.Get(); + await _testServer.GetClient(_testOutputHelper); var builder = Host.CreateApplicationBuilder([]); - builder.Configuration.GetSection("GrpcSqlProxy").GetSection("Url").Value = server.Url; + builder.Configuration.GetSection("GrpcSqlProxy").GetSection("Url").Value = _testServer.Url; builder.Configuration.GetSection("GrpcSqlProxy").GetSection("Compress").Value = "False"; builder.Configuration.GetSection("GrpcSqlProxy").GetSection("PacketSize").Value = "2000"; builder.Services.AddGrpcSqlProxy(); @@ -74,16 +90,7 @@ public async Task Should_Inject_SqlProxy_Properly() public async Task Should_Use_Compression() { // Arrange - var server = await TestServer.Get(); - var client = new GrpcSqlProxyClient( - new SqlProxyClientOptions( - server.Url, - server.Config.GetConnectionString("SqlConnection") ?? throw new Exception("No connection string") - ) - { - Compress = true - } - ); + var client = await _testServer.GetClient(_testOutputHelper); // Act using var channel = client.CreateChannel(); @@ -104,16 +111,7 @@ public async Task Should_Use_Compression() public async Task Should_Throw_Error_For_Invalid_Queries() { // Arrange - var server = await TestServer.Get(); - var client = new GrpcSqlProxyClient( - new SqlProxyClientOptions( - server.Url, - server.Config.GetConnectionString("SqlConnection") ?? throw new Exception("No connection string") - ) - { - Compress = false - } - ); + var client = await _testServer.GetClient(_testOutputHelper); Exception? thrownException = null; // Act @@ -135,16 +133,7 @@ public async Task Should_Throw_Error_For_Invalid_Queries() public async Task Should_Keep_Parallel_Transaction_Opened() { // Arrange - var server = await TestServer.Get(); - var client = new GrpcSqlProxyClient( - new SqlProxyClientOptions( - server.Url, - server.Config.GetConnectionString("SqlConnection") ?? throw new Exception("No connection string") - ) - { - Compress = false - } - ); + var client = await _testServer.GetClient(_testOutputHelper); // Act using var channel1 = client.CreateChannel(); diff --git a/test/Codibre.GrpcSqlProxy.Test/TestServer.cs b/test/Codibre.GrpcSqlProxy.Test/TestServer.cs index 98d9b87..5bd67b0 100644 --- a/test/Codibre.GrpcSqlProxy.Test/TestServer.cs +++ b/test/Codibre.GrpcSqlProxy.Test/TestServer.cs @@ -4,64 +4,69 @@ using Codibre.GrpcSqlProxy.Client.Impl; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; using Microsoft.Identity.Client; +using Xunit.Abstractions; namespace Codibre.GrpcSqlProxy.Test; -public class TB_PRODUTO +public sealed class TestServer : IDisposable { - public int CD_PRODUTO { get; set; } -} -public class TB_PEDIDO -{ - public int CD_PEDIDO { get; set; } -} + private WebApplication? _app = null; + private Task? _start = null; + private ValueTask? _noop = null; + private GrpcSqlProxyClient? _client = null; -public class TB_PESSOA -{ - public int CD_PESSOA { get; set; } -} + private string? _port; -public class TestServer -{ - private readonly WebApplication _app; - private static Task? _run = null; - private static TestServer? _instance = null; + private string GetPort() + { + _port = RandomNumberGenerator.GetInt32(3000, 4000).ToString(); + return _port; + } - private static readonly string _port = RandomNumberGenerator.GetInt32(3000, 4000).ToString(); - public string Url { get; } = $"http://localhost:{_port}"; + public string Url => $"http://localhost:{_port}"; public IConfigurationRoot Config { get; } = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", true) .AddEnvironmentVariables() .Build(); - private TestServer() + public async Task GetClient(ITestOutputHelper _testOutputHelper) { - _app = Program.GetApp([_port]); - _run ??= StartApp(_app); + if (_app is null || _start is null) + { + _start = StartRun(_testOutputHelper); + } + await _start; + if (_client is null || _noop is null) + { + _client = new GrpcSqlProxyClient( + new SqlProxyClientOptions( + Url, + Config.GetConnectionString("SqlConnection") ?? throw new Exception("No connection string") + ) + { + Compress = false + } + ); + _noop = _client.Initialize(); + } + await _noop.Value; + return _client; } - private async Task StartApp(WebApplication app) + private async Task StartRun(ITestOutputHelper logger) { - _ = Task.Run(async () => - { - try - { - await app.RunAsync(); - } - catch - { - // Ignore - } - }); - await Task.Delay(1000); + logger.WriteLine("Starting server"); + _app = Program.GetApp([GetPort()]); + await _app.StartAsync(); + logger.WriteLine("Server started"); } - public static async Task Get() + public void Dispose() { - _instance ??= new TestServer(); - if (_run is not null) await _run; - return _instance; + _client?.Channel.Dispose(); + _app?.StopAsync().ConfigureAwait(false).GetAwaiter().GetResult(); } } \ No newline at end of file diff --git a/test/Codibre.GrpcSqlProxy.Test/TestServerCollection.cs b/test/Codibre.GrpcSqlProxy.Test/TestServerCollection.cs new file mode 100644 index 0000000..00bd409 --- /dev/null +++ b/test/Codibre.GrpcSqlProxy.Test/TestServerCollection.cs @@ -0,0 +1,7 @@ +namespace Codibre.GrpcSqlProxy.Test; + +[CollectionDefinition("TestServerCollection")] +public class TestServerCollection(TestServer testServer) : ICollectionFixture +{ + public TestServer TestServer { get; } = testServer; +} \ No newline at end of file