diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..6e88c8d Binary files /dev/null and b/.DS_Store differ diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..c954668 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,48 @@ +name: CodeQL + +on: + pull_request: + branches: + - main + push: + branches: + - main + schedule: + - cron: "31 7 * * 3" + +permissions: + actions: read + checks: write + contents: read + security-events: write + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: + - TypeScript + + steps: + - name: Checkout + id: checkout + uses: actions/checkout@v4 + + - name: Initialize CodeQL + id: initialize + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + source-root: src + + - name: Autobuild + id: autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + id: analyze + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..5c10e73 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,122 @@ +on: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +permissions: + # Required to checkout the code + contents: + read + # Required to put a comment into the pull-request + pull-requests: write +jobs: + test_rust: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macOS-latest, windows-latest] + env: + CARGO_TERM_COLOR: always + steps: + - uses: actions/checkout@v4 + - name: Install Rust + run: rustup update stable + - name: Install rust dependencies + run: | + cargo install cargo-watch + cargo install commitlint-rs + - name: Setup LCOV + uses: hrishikesh-kadam/setup-lcov@v1 + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + - name: Install Node.js + uses: actions/setup-node@v2 + with: + node-version: "20" + - name: Install Frontend dependencies + run: | + cd src/frontend + npm install + - name: Test Cli (Astro_X_Runner) + run: cargo llvm-cov --all-features --workspace --lcov --output-path lcovCli.info --manifest-path Cargo.toml + - name: Report code coverage + if: matrix.os == 'macOS-latest' + uses: zgosalvez/github-actions-report-lcov@v4.1.21 + with: + coverage-files: lcovCli.info + artifact-name: code-coverage-cli-report + github-token: ${{ secrets.GITHUB_TOKEN }} + update-comment: true + title-prefix: "CLI" + - name: Test Backend + run: cargo llvm-cov --all-features --workspace --lcov --output-path lcovBackend.info --manifest-path src/backend/Cargo.toml + + - name: Report code coverage + if: matrix.os == 'macOS-latest' + uses: zgosalvez/github-actions-report-lcov@v4.1.21 + with: + coverage-files: lcovBackend.info + artifact-name: code-coverage-backend-report + github-token: ${{ secrets.GITHUB_TOKEN }} + update-comment: true + title-prefix: "Backend" + - name: Run Project Build + run: cargo run -- --build + test_frontend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Node.js + uses: actions/setup-node@v2 + with: + node-version: "20" + - name: Install dependencies + run: | + cd src/frontend + npm install + - name: npm audit report + run: | + cd src/frontend + npm audit --json | npx @netly/npm-audit-markdown --output npm.md + - name: "Test" + run: | + cd src/frontend + npm run coverage + - name: Run project analyzer + run: npm i -g project-analyzer && projectAnalyzer -m -t + - name: "Report Coverage" + # Set if: always() to also generate the report if tests are failing + # Only works if you set `reportOnFailure: true` in your vite config as specified above + uses: davelosert/vitest-coverage-report-action@v2 + with: + json-summary-path: src/frontend/coverage/coverage-summary.json + + - name: Post coverage summary + id: coverage_summary + uses: irongut/CodeCoverageSummary@v1.3.0 + with: + filename: src/frontend/coverage/cobertura-coverage.xml + badge: true + format: markdown + output: both + + - name: Add Npm audit report PR Comment + uses: marocchino/sticky-pull-request-comment@v2 + if: github.event_name == 'pull_request' + with: + recreate: true + path: src/frontend/npm.md + + - name: Add Coverage PR Comment + uses: marocchino/sticky-pull-request-comment@v2 + if: github.event_name == 'pull_request' + with: + recreate: true + path: code-coverage-results.md + - name: Add Project Analysis PR Comment + uses: marocchino/sticky-pull-request-comment@v2 + if: github.event_name == 'pull_request' + with: + recreate: true + path: analysis.md diff --git a/.github/workflows/relase.yml b/.github/workflows/relase.yml new file mode 100644 index 0000000..0639115 --- /dev/null +++ b/.github/workflows/relase.yml @@ -0,0 +1,108 @@ +name: Release + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macOS-latest, windows-latest] + env: + CARGO_TERM_COLOR: always + steps: + - uses: actions/checkout@v4 + - name: Install Rust + run: rustup update stable + - name: Install rust dependencies + run: | + cargo install cargo-watch + cargo install commitlint-rs + - name: Setup LCOV + uses: hrishikesh-kadam/setup-lcov@v1 + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + - name: Install Node.js + uses: actions/setup-node@v2 + with: + node-version: "20" + - name: Install Frontend dependencies + run: | + cd src/frontend + npm install + - name: Coverage Frontend + run: | + cd src/frontend + npm run coverage + - name: Test cli + run: cargo llvm-cov --all-features --workspace --lcov --output-path lcovCli.info --manifest-path Cargo.toml + - name: Report code coverage + if: matrix.os == 'macOS-latest' + uses: zgosalvez/github-actions-report-lcov@v4.1.21 + with: + coverage-files: lcovCli.info + artifact-name: code-coverage-cli-report + github-token: ${{ secrets.GITHUB_TOKEN }} + update-comment: true + title-prefix: "CLI" + - name: Test Backend + run: cargo llvm-cov --all-features --workspace --lcov --output-path lcovBackend.info --manifest-path src/backend/Cargo.toml + - name: Report code coverage + if: matrix.os == 'macOS-latest' + uses: zgosalvez/github-actions-report-lcov@v4.1.21 + with: + coverage-files: lcovBackend.info + artifact-name: code-coverage-backend-report + github-token: ${{ secrets.GITHUB_TOKEN }} + update-comment: true + title-prefix: "BACKEND" + - name: Run Project Build + run: cargo run -- --build --prod-astro-build=true + - name: Package Artifacts (Linux) + if: matrix.os == 'linux-latest' + run: | + zip -r artifacts_linux.zip . + mkdir -p artifacts + mv artifacts_linux.zip artifacts/ + - name: Package Artifacts (Mac) + if: matrix.os == 'macOs-latest' + run: | + zip -r artifacts_macos.zip . + mkdir -p artifacts + mv artifacts_macos.zip artifacts/ + - name: Package Artifacts (Windows) + if: matrix.os == 'window-latest' + run: | + Compress-Archive -Path . -DestinationPath artifacts\artifacts_windows.zip + mkdir artifacts + copy artifacts_windows.zip artifacts\ + - name: Upload Artifacts + uses: actions/upload-artifact@v3 + with: + name: build-artifacts + path: artifacts/ + + release: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Download Artifacts + uses: actions/download-artifact@v4.1.7 + with: + name: build-artifacts + path: artifacts/ + - name: Release + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true + draft: false + prerelase: false + tag_name: v0.1.2.${{github.run_number}} + files: | + artifacts/arifacts_macos.zip + artifacts/arifacts_windows.zip + artifacts/arifacts_linux.zip diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ffaa572 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +/target +/src/frontend/dist +/src/backend/target +syncDevil.sh +.env +lcov.info +src/frontend/coverage +src/backend/coverage +coverage +Astrox-test.toml +Cargo.lock +src/backend/Cargo.lock +analysis.md + diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..617bcf9 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18.14.1 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..f695463 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["rust-lang.rust-analyzer", "dustypomerleau.rust-syntax"] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..10efcb2 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug", + "program": "${workspaceFolder}/", + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..006159c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,19 @@ +{ + "rust-analyzer.linkedProjects": ["./src/backend/Cargo.toml", "./Cargo.toml"], + "editor.formatOnSave": true, + "eslint.validate": [ + "javascript", + "javascriptreact", + "astro", + "typescript", + "typescriptreact" + ], + "prettier.documentSelectors": ["**/*.astro", "**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx", "**/*.css", "**/*.scss", "**/*.json", "**/*.md"], + "[astro]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "cSpell.words": [ + "nesseccery" + ], +} diff --git a/Astrox.toml b/Astrox.toml new file mode 100644 index 0000000..874b6f8 --- /dev/null +++ b/Astrox.toml @@ -0,0 +1,9 @@ +host = "localhost" +port = 8080 +env = "dev" +astro_port = 5431 +prod_astro_build = true +cors_url = "https://astrox.spaceout.pl" + +[public_keys] +public_api_url = "https://astrox.spaceout.pl/api" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0d9fd7a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "astro_x_runner" +version = "0.1.2" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +"toml" = "0.8.19" +ctrlc = "3.4.4" +inquire = "0.7.5" +serde = { version = "1.0", features = ["derive"] } +semver = "1.0.23" +crossterm = "0.28" diff --git a/git_hooks/commit-msg b/git_hooks/commit-msg new file mode 100644 index 0000000..379ff78 --- /dev/null +++ b/git_hooks/commit-msg @@ -0,0 +1,17 @@ +echo -e "\e[41m\e[97mRunning commitlint...\e[0m" + +commit_message=$(cat "$1") + +echo "$commit_message" | commitlint + +# If the commit message does not match the conventional commit format, then exit with a non-zero status + +if [ $? -ne 0 ]; then + echo -e "\e[41m\e[97mInvalid commit message. Please use the conventional commit format, more info on https://www.conventionalcommits.org/en/v1.0.0/\e[0m" + exit 1 +fi + +echo -e "\e[42m\e[97m" +echo "----- Done -----" +echo " " +echo -e "\e[0m" \ No newline at end of file diff --git a/git_hooks/commit-msg-windows-example b/git_hooks/commit-msg-windows-example new file mode 100644 index 0000000..d48cd47 --- /dev/null +++ b/git_hooks/commit-msg-windows-example @@ -0,0 +1,14 @@ +# Change the name of this file to commit-msg +# Delete the current linux version + +echo Running commitlint... +set /p commit_message=<%1 +echo %commit_message% | commitlint +if errorlevel 1 ( + echo Invalid commit message. Please use the conventional commit format, more info on https://www.conventionalcommits.org/en/v1.0.0/ + exit /b 1 +) + +echo +echo ----- Done ----- +echo \ No newline at end of file diff --git a/git_hooks/pre-commit b/git_hooks/pre-commit new file mode 100755 index 0000000..7764ea9 --- /dev/null +++ b/git_hooks/pre-commit @@ -0,0 +1,24 @@ +set -e + +echo 'running cli cargo test...' +cargo test + +echo 'running cargo fmt...' +cargo fmt + +echo 'running cargo clippy...' +cargo clippy -- -D warnings + +echo 'running backend cargo test...' +cargo test --manifest-path src/backend/Cargo.toml + +echo 'running backend cargo fmt...' +cargo fmt --manifest-path src/backend/Cargo.toml + +echo 'running backend cargo clippy...' +cargo clippy --manifest-path src/backend/Cargo.toml -- -D warnings + +echo 'running frontend project lint...' +npm run lint:staged --prefix src/frontend/ + +echo '----- Done -----' \ No newline at end of file diff --git a/git_hooks/pre-push b/git_hooks/pre-push new file mode 100755 index 0000000..0725d98 --- /dev/null +++ b/git_hooks/pre-push @@ -0,0 +1,25 @@ +set -e + +echo 'running cli cargo test...' +cargo test + +echo 'running cli runner cargo fmt...' +cargo fmt + +echo 'running cli cargo clippy...' +cargo clippy -- -D warnings + +echo 'running backend cargo test...' +cargo test --manifest-path src/backend/Cargo.toml + +echo 'running backend cargo fmt...' +cargo fmt --manifest-path src/backend/Cargo.toml + +echo 'running backend cargo clippy...' +cargo clippy --manifest-path src/backend/Cargo.toml -- -D warnings + + +echo 'running frontend project lint...' +npm run lint:all --prefix src/frontend/ + +echo '------ Done ------' \ No newline at end of file diff --git a/lcovBackend.info b/lcovBackend.info new file mode 100644 index 0000000..3f7fc97 --- /dev/null +++ b/lcovBackend.info @@ -0,0 +1,708 @@ +SF:/Users/lukasz.celitan/Git/AstroX/src/backend/src/api/hello/get.rs +FN:3,_RNvXNtNtNtCs4dveHeDtRhK_7backend3api5hello3getNtB2_17json_response_getNtNtCsgVb44Zth3OK_9actix_web7service18HttpServiceFactory8register +FN:4,_RNvNvXNtNtNtCs4dveHeDtRhK_7backend3api5hello3getNtB4_17json_response_getNtNtCsgVb44Zth3OK_9actix_web7service18HttpServiceFactory8register17json_response_get +FN:14,_RNCNvNtNtNtNtCs4dveHeDtRhK_7backend3api5hello3get5testss_22test_json_response_get0Bb_ +FN:14,_RNvNtNtNtNtCs4dveHeDtRhK_7backend3api5hello3get5testss_22test_json_response_get +FN:4,_RNCNvNvXNtNtNtCs4dveHeDtRhK_7backend3api5hello3getNtB6_17json_response_getNtNtCsgVb44Zth3OK_9actix_web7service18HttpServiceFactory8register17json_response_get0Bc_ +FNDA:1,_RNvXNtNtNtCs4dveHeDtRhK_7backend3api5hello3getNtB2_17json_response_getNtNtCsgVb44Zth3OK_9actix_web7service18HttpServiceFactory8register +FNDA:1,_RNvNvXNtNtNtCs4dveHeDtRhK_7backend3api5hello3getNtB4_17json_response_getNtNtCsgVb44Zth3OK_9actix_web7service18HttpServiceFactory8register17json_response_get +FNDA:1,_RNCNvNtNtNtNtCs4dveHeDtRhK_7backend3api5hello3get5testss_22test_json_response_get0Bb_ +FNDA:1,_RNvNtNtNtNtCs4dveHeDtRhK_7backend3api5hello3get5testss_22test_json_response_get +FNDA:1,_RNCNvNvXNtNtNtCs4dveHeDtRhK_7backend3api5hello3getNtB6_17json_response_getNtNtCsgVb44Zth3OK_9actix_web7service18HttpServiceFactory8register17json_response_get0Bc_ +FNF:5 +FNH:5 +DA:3,1 +DA:4,1 +DA:5,1 +DA:6,1 +DA:7,1 +DA:8,1 +DA:14,2 +BRF:0 +BRH:0 +LF:9 +LH:9 +end_of_record +SF:/Users/lukasz.celitan/Git/AstroX/src/backend/src/api/hello/post.rs +FN:3,_RNvXNtNtNtCs4dveHeDtRhK_7backend3api5hello4postNtB2_13json_responseNtNtCsgVb44Zth3OK_9actix_web7service18HttpServiceFactory8register +FN:4,_RNvNvXNtNtNtCs4dveHeDtRhK_7backend3api5hello4postNtB4_13json_responseNtNtCsgVb44Zth3OK_9actix_web7service18HttpServiceFactory8register13json_response +FN:4,_RNCNvNvXNtNtNtCs4dveHeDtRhK_7backend3api5hello4postNtB6_13json_responseNtNtCsgVb44Zth3OK_9actix_web7service18HttpServiceFactory8register13json_response0Bc_ +FN:15,_RNCNvNtNtNtNtCs4dveHeDtRhK_7backend3api5hello4post5testss_18test_json_response0Bb_ +FN:15,_RNvNtNtNtNtCs4dveHeDtRhK_7backend3api5hello4post5testss_18test_json_response +FNDA:1,_RNvXNtNtNtCs4dveHeDtRhK_7backend3api5hello4postNtB2_13json_responseNtNtCsgVb44Zth3OK_9actix_web7service18HttpServiceFactory8register +FNDA:1,_RNvNvXNtNtNtCs4dveHeDtRhK_7backend3api5hello4postNtB4_13json_responseNtNtCsgVb44Zth3OK_9actix_web7service18HttpServiceFactory8register13json_response +FNDA:1,_RNCNvNvXNtNtNtCs4dveHeDtRhK_7backend3api5hello4postNtB6_13json_responseNtNtCsgVb44Zth3OK_9actix_web7service18HttpServiceFactory8register13json_response0Bc_ +FNDA:1,_RNCNvNtNtNtNtCs4dveHeDtRhK_7backend3api5hello4post5testss_18test_json_response0Bb_ +FNDA:1,_RNvNtNtNtNtCs4dveHeDtRhK_7backend3api5hello4post5testss_18test_json_response +FNF:5 +FNH:5 +DA:3,1 +DA:4,1 +DA:5,1 +DA:6,1 +DA:7,1 +DA:8,1 +DA:15,2 +BRF:0 +BRH:0 +LF:9 +LH:9 +end_of_record +SF:/Users/lukasz.celitan/Git/AstroX/src/backend/src/api/space_x/get.rs +FN:26,_RNvXNvXNvNtNtNtCs4dveHeDtRhK_7backend3api7space_x3get1__NtB7_6RocketNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB2_14___FieldVisitorNtB16_7Visitor9expecting +FN:26,_RINvXNvXNvNtNtNtCs4dveHeDtRhK_7backend3api7space_x3get1__NtB8_6RocketNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB3_14___FieldVisitorNtB17_7Visitor9visit_u64pEBe_ +FN:26,_RINvXNvXNvNtNtNtCs4dveHeDtRhK_7backend3api7space_x3get1__NtB8_6RocketNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB3_14___FieldVisitorNtB17_7Visitor11visit_bytespEBe_ +FN:26,_RINvXNvXNvNtNtNtCs4dveHeDtRhK_7backend3api7space_x3get1__NtB8_6RocketNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB3_14___FieldVisitorNtB17_7Visitor9visit_strNtNtCsfhoQvMk4ngU_10serde_json5error5ErrorEBe_ +FN:26,_RINvXs_NvXNvNtNtNtCs4dveHeDtRhK_7backend3api7space_x3get1__NtBa_6RocketNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB5_7___FieldB17_11deserializeINtNtCsfhoQvMk4ngU_10serde_json2de6MapKeyNtNtB2y_4read9SliceReadEEBg_ +FN:26,_RINvXs0_NvXNvNtNtNtCs4dveHeDtRhK_7backend3api7space_x3get1__NtBb_6RocketNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB6_9___VisitorNtB1a_7Visitor9visit_seqINtNtCsfhoQvMk4ngU_10serde_json2de9SeqAccessNtNtB2I_4read9SliceReadEEBh_ +FN:26,_RINvXs0_NvXNvNtNtNtCs4dveHeDtRhK_7backend3api7space_x3get1__NtBb_6RocketNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB6_9___VisitorNtB1a_7Visitor9visit_mapINtNtCsfhoQvMk4ngU_10serde_json2de9MapAccessNtNtB2I_4read9SliceReadEEBh_ +FN:46,_RNCNvNvXs_NtNtNtCs4dveHeDtRhK_7backend3api7space_x3getNtB8_16json_get_space_xNtNtCsgVb44Zth3OK_9actix_web7service18HttpServiceFactory8register16json_get_space_x0Be_ +FN:26,_RNvXs0_NvXNvNtNtNtCs4dveHeDtRhK_7backend3api7space_x3get1__NtBa_6RocketNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB5_9___VisitorNtB19_7Visitor9expecting +FN:44,_RNvXs_NtNtNtCs4dveHeDtRhK_7backend3api7space_x3getNtB4_16json_get_space_xNtNtCsgVb44Zth3OK_9actix_web7service18HttpServiceFactory8register +FN:46,_RNvNvXs_NtNtNtCs4dveHeDtRhK_7backend3api7space_x3getNtB6_16json_get_space_xNtNtCsgVb44Zth3OK_9actix_web7service18HttpServiceFactory8register16json_get_space_x +FN:67,_RNvNtNtNtNtCs4dveHeDtRhK_7backend3api7space_x3get5testss_21test_json_get_space_x +FN:67,_RNCNvNtNtNtNtCs4dveHeDtRhK_7backend3api7space_x3get5testss_21test_json_get_space_x0Bb_ +FNDA:0,_RNvXNvXNvNtNtNtCs4dveHeDtRhK_7backend3api7space_x3get1__NtB7_6RocketNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB2_14___FieldVisitorNtB16_7Visitor9expecting +FNDA:0,_RINvXNvXNvNtNtNtCs4dveHeDtRhK_7backend3api7space_x3get1__NtB8_6RocketNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB3_14___FieldVisitorNtB17_7Visitor9visit_u64pEBe_ +FNDA:0,_RINvXNvXNvNtNtNtCs4dveHeDtRhK_7backend3api7space_x3get1__NtB8_6RocketNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB3_14___FieldVisitorNtB17_7Visitor11visit_bytespEBe_ +FNDA:148,_RINvXNvXNvNtNtNtCs4dveHeDtRhK_7backend3api7space_x3get1__NtB8_6RocketNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB3_14___FieldVisitorNtB17_7Visitor9visit_strNtNtCsfhoQvMk4ngU_10serde_json5error5ErrorEBe_ +FNDA:148,_RINvXs_NvXNvNtNtNtCs4dveHeDtRhK_7backend3api7space_x3get1__NtBa_6RocketNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB5_7___FieldB17_11deserializeINtNtCsfhoQvMk4ngU_10serde_json2de6MapKeyNtNtB2y_4read9SliceReadEEBg_ +FNDA:0,_RINvXs0_NvXNvNtNtNtCs4dveHeDtRhK_7backend3api7space_x3get1__NtBb_6RocketNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB6_9___VisitorNtB1a_7Visitor9visit_seqINtNtCsfhoQvMk4ngU_10serde_json2de9SeqAccessNtNtB2I_4read9SliceReadEEBh_ +FNDA:8,_RINvXs0_NvXNvNtNtNtCs4dveHeDtRhK_7backend3api7space_x3get1__NtBb_6RocketNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB6_9___VisitorNtB1a_7Visitor9visit_mapINtNtCsfhoQvMk4ngU_10serde_json2de9MapAccessNtNtB2I_4read9SliceReadEEBh_ +FNDA:1,_RNCNvNvXs_NtNtNtCs4dveHeDtRhK_7backend3api7space_x3getNtB8_16json_get_space_xNtNtCsgVb44Zth3OK_9actix_web7service18HttpServiceFactory8register16json_get_space_x0Be_ +FNDA:0,_RNvXs0_NvXNvNtNtNtCs4dveHeDtRhK_7backend3api7space_x3get1__NtBa_6RocketNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB5_9___VisitorNtB19_7Visitor9expecting +FNDA:1,_RNvXs_NtNtNtCs4dveHeDtRhK_7backend3api7space_x3getNtB4_16json_get_space_xNtNtCsgVb44Zth3OK_9actix_web7service18HttpServiceFactory8register +FNDA:1,_RNvNvXs_NtNtNtCs4dveHeDtRhK_7backend3api7space_x3getNtB6_16json_get_space_xNtNtCsgVb44Zth3OK_9actix_web7service18HttpServiceFactory8register16json_get_space_x +FNDA:1,_RNvNtNtNtNtCs4dveHeDtRhK_7backend3api7space_x3get5testss_21test_json_get_space_x +FNDA:1,_RNCNvNtNtNtNtCs4dveHeDtRhK_7backend3api7space_x3get5testss_21test_json_get_space_x0Bb_ +FNF:6 +FNH:6 +DA:26,156 +DA:44,1 +DA:46,1 +DA:47,7 +DA:49,1 +DA:50,1 +DA:51,1 +DA:53,1 +DA:54,1 +DA:55,0 +DA:58,0 +DA:60,1 +DA:67,2 +BRF:0 +BRH:0 +LF:15 +LH:13 +end_of_record +SF:/Users/lukasz.celitan/Git/AstroX/src/backend/src/args/collect_args.rs +FN:24,_RNvNtNtCs4dveHeDtRhK_7backend4args12collect_args12collect_args +FN:73,_RNvNtNtNtCs4dveHeDtRhK_7backend4args12collect_args5testss_25test_collect_args_default +FN:82,_RNvNtNtNtCs4dveHeDtRhK_7backend4args12collect_args5testss_21test_collect_prod_arg +FNDA:2,_RNvNtNtCs4dveHeDtRhK_7backend4args12collect_args12collect_args +FNDA:1,_RNvNtNtNtCs4dveHeDtRhK_7backend4args12collect_args5testss_25test_collect_args_default +FNDA:1,_RNvNtNtNtCs4dveHeDtRhK_7backend4args12collect_args5testss_21test_collect_prod_arg +FNF:3 +FNH:3 +DA:24,2 +DA:25,2 +DA:26,2 +DA:27,2 +DA:28,2 +DA:30,7 +DA:31,5 +DA:32,1 +DA:33,1 +DA:34,1 +DA:35,1 +DA:36,4 +DA:38,5 +DA:39,1 +DA:40,1 +DA:41,1 +DA:42,1 +DA:43,4 +DA:45,5 +DA:46,1 +DA:47,1 +DA:48,1 +DA:49,1 +DA:50,4 +DA:52,5 +DA:53,1 +DA:54,1 +DA:55,1 +DA:56,1 +DA:57,4 +DA:60,2 +DA:61,2 +DA:62,2 +DA:63,2 +DA:64,2 +DA:65,2 +DA:66,2 +DA:73,1 +DA:74,1 +DA:75,1 +DA:76,1 +DA:77,1 +DA:78,1 +DA:79,1 +DA:82,1 +DA:83,1 +DA:84,1 +DA:85,1 +DA:86,1 +DA:87,1 +DA:88,1 +DA:89,1 +DA:90,1 +DA:91,1 +DA:92,1 +DA:93,1 +DA:94,1 +DA:95,1 +BRF:0 +BRH:0 +LF:58 +LH:58 +end_of_record +SF:/Users/lukasz.celitan/Git/AstroX/src/backend/src/auth/auth_middleware.rs +FN:200,_RNCNvNtNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware5tests10test_route0B9_ +FN:166,_RNCNvNtNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware5testss_24test_match_glob_patterns0B9_ +FN:197,_RNCNCNvNtNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware5testss_24test_match_glob_patterns00Bb_ +FN:204,_RNCNvNtNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware5testss_25test_middleware_protected0B9_ +FN:222,_RNCNvNtNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware5testss_27test_middleware_passthrough0B9_ +FN:240,_RNCNvNtNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware5testss_23test_middleware_success0B9_ +FN:280,_RNCNCNvNtNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware5testss_23test_middleware_success00Bb_ +FN:57,_RNvXNtNtCs4dveHeDtRhK_7backend4auth15auth_middlewareNtB2_14AuthenticationINtNtCsaEtXNEcoikq_13actix_service9transform9TransformINtNtCs8jIaGIFrmcL_13actix_session10middleware22InnerSessionMiddlewareINtNtCsdD9m1OONXpT_24actix_web_flash_messages10middleware23FlashMessagesMiddlewareNtNtCsgVb44Zth3OK_9actix_web11app_service10AppRoutingENtNtNtB26_7storage6cookie18CookieSessionStoreENtNtB4x_7service14ServiceRequestE13new_transformB6_ +FN:57,_RNvXNtNtCs4dveHeDtRhK_7backend4auth15auth_middlewareNtB2_14AuthenticationINtNtCsaEtXNEcoikq_13actix_service9transform9TransformINtNtCsgGH9L5q1LOH_10actix_cors10middleware14CorsMiddlewareNtNtCsgVb44Zth3OK_9actix_web11app_service10AppRoutingENtNtB32_7service14ServiceRequestE13new_transformB6_ +FN:85,_RNvXs_NtNtCs4dveHeDtRhK_7backend4auth15auth_middlewareINtB4_24AuthenticationMiddlewareINtNtCs8jIaGIFrmcL_13actix_session10middleware22InnerSessionMiddlewareINtNtCsdD9m1OONXpT_24actix_web_flash_messages10middleware23FlashMessagesMiddlewareNtNtCsgVb44Zth3OK_9actix_web11app_service10AppRoutingENtNtNtB1r_7storage6cookie18CookieSessionStoreEEINtCsaEtXNEcoikq_13actix_service7ServiceNtNtB3S_7service14ServiceRequestE4callB8_ +FN:85,_RNvXs_NtNtCs4dveHeDtRhK_7backend4auth15auth_middlewareINtB4_24AuthenticationMiddlewareINtNtCsgGH9L5q1LOH_10actix_cors10middleware14CorsMiddlewareNtNtCsgVb44Zth3OK_9actix_web11app_service10AppRoutingEEINtCsaEtXNEcoikq_13actix_service7ServiceNtNtB2n_7service14ServiceRequestE4callB8_ +FN:89,_RNCNvXs_NtNtCs4dveHeDtRhK_7backend4auth15auth_middlewareINtB6_24AuthenticationMiddlewareINtNtCs8jIaGIFrmcL_13actix_session10middleware22InnerSessionMiddlewareINtNtCsdD9m1OONXpT_24actix_web_flash_messages10middleware23FlashMessagesMiddlewareNtNtCsgVb44Zth3OK_9actix_web11app_service10AppRoutingENtNtNtB1t_7storage6cookie18CookieSessionStoreEEINtCsaEtXNEcoikq_13actix_service7ServiceNtNtB3U_7service14ServiceRequestE4call0Ba_ +FN:89,_RNCNvXs_NtNtCs4dveHeDtRhK_7backend4auth15auth_middlewareINtB6_24AuthenticationMiddlewareINtNtCsgGH9L5q1LOH_10actix_cors10middleware14CorsMiddlewareNtNtCsgVb44Zth3OK_9actix_web11app_service10AppRoutingEEINtCsaEtXNEcoikq_13actix_service7ServiceNtNtB2p_7service14ServiceRequestE4call0Ba_ +FN:129,_RNvXs0_NtNtCs4dveHeDtRhK_7backend4auth15auth_middlewareINtB5_20AuthenticationFutureINtNtCs8jIaGIFrmcL_13actix_session10middleware22InnerSessionMiddlewareINtNtCsdD9m1OONXpT_24actix_web_flash_messages10middleware23FlashMessagesMiddlewareNtNtCsgVb44Zth3OK_9actix_web11app_service10AppRoutingENtNtNtB1o_7storage6cookie18CookieSessionStoreENtNtNtCsiiB1DREozie_10actix_http4body5boxed7BoxBodyENtNtNtCsliVzVx4luf9_4core6future6future6Future4pollB9_ +FN:129,_RNvXs0_NtNtCs4dveHeDtRhK_7backend4auth15auth_middlewareINtB5_20AuthenticationFutureINtNtCsgGH9L5q1LOH_10actix_cors10middleware14CorsMiddlewareNtNtCsgVb44Zth3OK_9actix_web11app_service10AppRoutingEINtNtNtCsiiB1DREozie_10actix_http4body6either10EitherBodyNtNtB3d_5boxed7BoxBodyEENtNtNtCsliVzVx4luf9_4core6future6future6Future4pollB9_ +FN:112,_RNvMNvNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware1__INtB4_20AuthenticationFutureINtNtCs8jIaGIFrmcL_13actix_session10middleware22InnerSessionMiddlewareINtNtCsdD9m1OONXpT_24actix_web_flash_messages10middleware23FlashMessagesMiddlewareNtNtCsgVb44Zth3OK_9actix_web11app_service10AppRoutingENtNtNtB1q_7storage6cookie18CookieSessionStoreENtNtNtCsiiB1DREozie_10actix_http4body5boxed7BoxBodyE7projectB8_ +FN:112,_RNvMNvNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware1__INtB4_20AuthenticationFutureINtNtCsgGH9L5q1LOH_10actix_cors10middleware14CorsMiddlewareNtNtCsgVb44Zth3OK_9actix_web11app_service10AppRoutingEINtNtNtCsiiB1DREozie_10actix_http4body6either10EitherBodyNtNtB3f_5boxed7BoxBodyEE7projectB8_ +FN:139,_RNvNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware19match_glob_patterns +FN:81,_RNvXININtNtCs4dveHeDtRhK_7backend4auth15auth_middlewares_0ppEINtB5_24AuthenticationMiddlewarepEINtCsaEtXNEcoikq_13actix_service7ServiceNtNtCsgVb44Zth3OK_9actix_web7service14ServiceRequestE10poll_readyB9_ +FN:112,_RNvMNvNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware1__INtB4_20AuthenticationFutureppE11project_refB8_ +FN:112,_RINvNvNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware1__24___assert_not_repr_packedppEB8_ +FN:112,_RNvXININvNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware1__s3_0ppEINtB7_20AuthenticationFutureppENtNtCsduTjajoCyfh_11pin_project9___private10PinnedDrop4dropBb_ +FN:200,_RNvNtNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware5tests10test_route +FN:166,_RNvNtNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware5testss_24test_match_glob_patterns +FN:204,_RNvNtNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware5testss_25test_middleware_protected +FN:222,_RNvNtNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware5testss_27test_middleware_passthrough +FN:240,_RNvNtNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware5testss_23test_middleware_success +FNDA:2,_RNCNvNtNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware5tests10test_route0B9_ +FNDA:1,_RNCNvNtNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware5testss_24test_match_glob_patterns0B9_ +FNDA:1,_RNCNCNvNtNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware5testss_24test_match_glob_patterns00Bb_ +FNDA:1,_RNCNvNtNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware5testss_25test_middleware_protected0B9_ +FNDA:1,_RNCNvNtNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware5testss_27test_middleware_passthrough0B9_ +FNDA:1,_RNCNvNtNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware5testss_23test_middleware_success0B9_ +FNDA:2,_RNCNCNvNtNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware5testss_23test_middleware_success00Bb_ +FNDA:2,_RNvXNtNtCs4dveHeDtRhK_7backend4auth15auth_middlewareNtB2_14AuthenticationINtNtCsaEtXNEcoikq_13actix_service9transform9TransformINtNtCs8jIaGIFrmcL_13actix_session10middleware22InnerSessionMiddlewareINtNtCsdD9m1OONXpT_24actix_web_flash_messages10middleware23FlashMessagesMiddlewareNtNtCsgVb44Zth3OK_9actix_web11app_service10AppRoutingENtNtNtB26_7storage6cookie18CookieSessionStoreENtNtB4x_7service14ServiceRequestE13new_transformB6_ +FNDA:1,_RNvXNtNtCs4dveHeDtRhK_7backend4auth15auth_middlewareNtB2_14AuthenticationINtNtCsaEtXNEcoikq_13actix_service9transform9TransformINtNtCsgGH9L5q1LOH_10actix_cors10middleware14CorsMiddlewareNtNtCsgVb44Zth3OK_9actix_web11app_service10AppRoutingENtNtB32_7service14ServiceRequestE13new_transformB6_ +FNDA:2,_RNvXs_NtNtCs4dveHeDtRhK_7backend4auth15auth_middlewareINtB4_24AuthenticationMiddlewareINtNtCs8jIaGIFrmcL_13actix_session10middleware22InnerSessionMiddlewareINtNtCsdD9m1OONXpT_24actix_web_flash_messages10middleware23FlashMessagesMiddlewareNtNtCsgVb44Zth3OK_9actix_web11app_service10AppRoutingENtNtNtB1r_7storage6cookie18CookieSessionStoreEEINtCsaEtXNEcoikq_13actix_service7ServiceNtNtB3S_7service14ServiceRequestE4callB8_ +FNDA:2,_RNvXs_NtNtCs4dveHeDtRhK_7backend4auth15auth_middlewareINtB4_24AuthenticationMiddlewareINtNtCsgGH9L5q1LOH_10actix_cors10middleware14CorsMiddlewareNtNtCsgVb44Zth3OK_9actix_web11app_service10AppRoutingEEINtCsaEtXNEcoikq_13actix_service7ServiceNtNtB2n_7service14ServiceRequestE4callB8_ +FNDA:2,_RNCNvXs_NtNtCs4dveHeDtRhK_7backend4auth15auth_middlewareINtB6_24AuthenticationMiddlewareINtNtCs8jIaGIFrmcL_13actix_session10middleware22InnerSessionMiddlewareINtNtCsdD9m1OONXpT_24actix_web_flash_messages10middleware23FlashMessagesMiddlewareNtNtCsgVb44Zth3OK_9actix_web11app_service10AppRoutingENtNtNtB1t_7storage6cookie18CookieSessionStoreEEINtCsaEtXNEcoikq_13actix_service7ServiceNtNtB3U_7service14ServiceRequestE4call0Ba_ +FNDA:2,_RNCNvXs_NtNtCs4dveHeDtRhK_7backend4auth15auth_middlewareINtB6_24AuthenticationMiddlewareINtNtCsgGH9L5q1LOH_10actix_cors10middleware14CorsMiddlewareNtNtCsgVb44Zth3OK_9actix_web11app_service10AppRoutingEEINtCsaEtXNEcoikq_13actix_service7ServiceNtNtB2p_7service14ServiceRequestE4call0Ba_ +FNDA:1,_RNvXs0_NtNtCs4dveHeDtRhK_7backend4auth15auth_middlewareINtB5_20AuthenticationFutureINtNtCs8jIaGIFrmcL_13actix_session10middleware22InnerSessionMiddlewareINtNtCsdD9m1OONXpT_24actix_web_flash_messages10middleware23FlashMessagesMiddlewareNtNtCsgVb44Zth3OK_9actix_web11app_service10AppRoutingENtNtNtB1o_7storage6cookie18CookieSessionStoreENtNtNtCsiiB1DREozie_10actix_http4body5boxed7BoxBodyENtNtNtCsliVzVx4luf9_4core6future6future6Future4pollB9_ +FNDA:2,_RNvXs0_NtNtCs4dveHeDtRhK_7backend4auth15auth_middlewareINtB5_20AuthenticationFutureINtNtCsgGH9L5q1LOH_10actix_cors10middleware14CorsMiddlewareNtNtCsgVb44Zth3OK_9actix_web11app_service10AppRoutingEINtNtNtCsiiB1DREozie_10actix_http4body6either10EitherBodyNtNtB3d_5boxed7BoxBodyEENtNtNtCsliVzVx4luf9_4core6future6future6Future4pollB9_ +FNDA:1,_RNvMNvNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware1__INtB4_20AuthenticationFutureINtNtCs8jIaGIFrmcL_13actix_session10middleware22InnerSessionMiddlewareINtNtCsdD9m1OONXpT_24actix_web_flash_messages10middleware23FlashMessagesMiddlewareNtNtCsgVb44Zth3OK_9actix_web11app_service10AppRoutingENtNtNtB1q_7storage6cookie18CookieSessionStoreENtNtNtCsiiB1DREozie_10actix_http4body5boxed7BoxBodyE7projectB8_ +FNDA:2,_RNvMNvNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware1__INtB4_20AuthenticationFutureINtNtCsgGH9L5q1LOH_10actix_cors10middleware14CorsMiddlewareNtNtCsgVb44Zth3OK_9actix_web11app_service10AppRoutingEINtNtNtCsiiB1DREozie_10actix_http4body6either10EitherBodyNtNtB3f_5boxed7BoxBodyEE7projectB8_ +FNDA:14,_RNvNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware19match_glob_patterns +FNDA:0,_RNvXININtNtCs4dveHeDtRhK_7backend4auth15auth_middlewares_0ppEINtB5_24AuthenticationMiddlewarepEINtCsaEtXNEcoikq_13actix_service7ServiceNtNtCsgVb44Zth3OK_9actix_web7service14ServiceRequestE10poll_readyB9_ +FNDA:0,_RNvMNvNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware1__INtB4_20AuthenticationFutureppE11project_refB8_ +FNDA:0,_RINvNvNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware1__24___assert_not_repr_packedppEB8_ +FNDA:0,_RNvXININvNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware1__s3_0ppEINtB7_20AuthenticationFutureppENtNtCsduTjajoCyfh_11pin_project9___private10PinnedDrop4dropBb_ +FNDA:2,_RNvNtNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware5tests10test_route +FNDA:1,_RNvNtNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware5testss_24test_match_glob_patterns +FNDA:1,_RNvNtNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware5testss_25test_middleware_protected +FNDA:1,_RNvNtNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware5testss_27test_middleware_passthrough +FNDA:1,_RNvNtNtNtCs4dveHeDtRhK_7backend4auth15auth_middleware5testss_23test_middleware_success +FNF:19 +FNH:18 +DA:57,3 +DA:58,3 +DA:59,3 +DA:60,3 +DA:61,3 +DA:62,3 +DA:81,0 +DA:82,0 +DA:83,0 +DA:85,4 +DA:86,4 +DA:87,4 +DA:88,4 +DA:89,4 +DA:90,4 +DA:91,2 +DA:92,1 +DA:93,1 +DA:94,1 +DA:95,1 +DA:97,1 +DA:98,1 +DA:99,1 +DA:100,1 +DA:101,1 +DA:104,2 +DA:105,2 +DA:106,2 +DA:107,2 +DA:109,4 +DA:112,3 +DA:129,3 +DA:130,3 +DA:131,3 +DA:132,0 +DA:135,3 +DA:136,3 +DA:139,14 +DA:140,14 +DA:142,34 +DA:143,21 +DA:144,20 +DA:145,20 +DA:146,1 +DA:149,13 +DA:150,13 +DA:151,13 +DA:166,2 +DA:197,1 +DA:200,2 +DA:201,2 +DA:202,2 +DA:204,2 +DA:222,2 +DA:240,2 +DA:280,2 +DA:281,2 +DA:282,2 +DA:283,2 +BRF:0 +BRH:0 +LF:65 +LH:57 +end_of_record +SF:/Users/lukasz.celitan/Git/AstroX/src/backend/src/cors/get_cors_options.rs +FN:46,_RNCNvNtNtNtCs4dveHeDtRhK_7backend4cors16get_cors_options5tests12manual_hello0B9_ +FN:50,_RNCNvNtNtNtCs4dveHeDtRhK_7backend4cors16get_cors_options5testss_19test_index_prod_get0B9_ +FN:69,_RNCNvNtNtNtCs4dveHeDtRhK_7backend4cors16get_cors_options5testss_18test_index_dev_get0B9_ +FN:23,_RNvNtNtCs4dveHeDtRhK_7backend4cors16get_cors_options16get_cors_options +FN:46,_RNvNtNtNtCs4dveHeDtRhK_7backend4cors16get_cors_options5tests12manual_hello +FN:50,_RNvNtNtNtCs4dveHeDtRhK_7backend4cors16get_cors_options5testss_19test_index_prod_get +FN:69,_RNvNtNtNtCs4dveHeDtRhK_7backend4cors16get_cors_options5testss_18test_index_dev_get +FNDA:2,_RNCNvNtNtNtCs4dveHeDtRhK_7backend4cors16get_cors_options5tests12manual_hello0B9_ +FNDA:1,_RNCNvNtNtNtCs4dveHeDtRhK_7backend4cors16get_cors_options5testss_19test_index_prod_get0B9_ +FNDA:1,_RNCNvNtNtNtCs4dveHeDtRhK_7backend4cors16get_cors_options5testss_18test_index_dev_get0B9_ +FNDA:3,_RNvNtNtCs4dveHeDtRhK_7backend4cors16get_cors_options16get_cors_options +FNDA:2,_RNvNtNtNtCs4dveHeDtRhK_7backend4cors16get_cors_options5tests12manual_hello +FNDA:1,_RNvNtNtNtCs4dveHeDtRhK_7backend4cors16get_cors_options5testss_19test_index_prod_get +FNDA:1,_RNvNtNtNtCs4dveHeDtRhK_7backend4cors16get_cors_options5testss_18test_index_dev_get +FNF:7 +FNH:7 +DA:23,3 +DA:24,3 +DA:25,2 +DA:26,2 +DA:27,2 +DA:28,2 +DA:29,2 +DA:30,2 +DA:32,1 +DA:33,1 +DA:34,1 +DA:35,1 +DA:36,1 +DA:38,3 +DA:46,2 +DA:47,2 +DA:48,2 +DA:50,2 +DA:69,2 +BRF:0 +BRH:0 +LF:22 +LH:22 +end_of_record +SF:/Users/lukasz.celitan/Git/AstroX/src/backend/src/main.rs +FN:26,_RNvCs4dveHeDtRhK_7backend4main +FN:26,_RNCNvCs4dveHeDtRhK_7backend4main0B3_ +FN:38,_RNCNCNvCs4dveHeDtRhK_7backend4main00B5_ +FN:61,_RNCNCNCNvCs4dveHeDtRhK_7backend4main000B7_ +FN:56,_RNCNCNCNCNvCs4dveHeDtRhK_7backend4main0000B9_ +FN:76,_RNCNCNvCs4dveHeDtRhK_7backend4main0s_0B5_ +FNDA:0,_RNvCs4dveHeDtRhK_7backend4main +FNDA:0,_RNCNvCs4dveHeDtRhK_7backend4main0B3_ +FNDA:0,_RNCNCNvCs4dveHeDtRhK_7backend4main00B5_ +FNDA:0,_RNCNCNCNvCs4dveHeDtRhK_7backend4main000B7_ +FNDA:0,_RNCNCNCNCNvCs4dveHeDtRhK_7backend4main0000B9_ +FNDA:0,_RNCNCNvCs4dveHeDtRhK_7backend4main0s_0B5_ +FNF:6 +FNH:0 +DA:26,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:76,0 +DA:77,0 +DA:78,0 +BRF:0 +BRH:0 +LF:42 +LH:0 +end_of_record +SF:/Users/lukasz.celitan/Git/AstroX/src/backend/src/session/flash_messages.rs +FN:5,_RNvNtNtCs4dveHeDtRhK_7backend7session14flash_messages21set_up_flash_messages +FNDA:9,_RNvNtNtCs4dveHeDtRhK_7backend7session14flash_messages21set_up_flash_messages +FNF:1 +FNH:1 +DA:5,9 +DA:6,9 +DA:7,9 +DA:8,9 +DA:9,9 +DA:10,9 +DA:11,9 +BRF:0 +BRH:0 +LF:7 +LH:7 +end_of_record +SF:/Users/lukasz.celitan/Git/AstroX/src/backend/src/session/session_middleware.rs +FN:31,_RNvMNtNtCs4dveHeDtRhK_7backend7session18session_middlewareNtB2_4User12authenticate +FN:57,_RNvNtNtCs4dveHeDtRhK_7backend7session18session_middleware18session_middleware +FN:72,_RNvNtNtNtCs4dveHeDtRhK_7backend7session18session_middleware5testss_40test_user_authenticate_valid_credentials +FN:90,_RNvNtNtNtCs4dveHeDtRhK_7backend7session18session_middleware5testss_39test_user_authenticate_invalid_username +FN:108,_RNvNtNtNtCs4dveHeDtRhK_7backend7session18session_middleware5testss_39test_user_authenticate_invalid_password +FN:9,_RNvXNvXNvNtNtCs4dveHeDtRhK_7backend7session18session_middleware1__NtB7_11CredentialsNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB2_14___FieldVisitorNtB1m_7Visitor9expecting +FN:9,_RINvXNvXNvNtNtCs4dveHeDtRhK_7backend7session18session_middleware1__NtB8_11CredentialsNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB3_14___FieldVisitorNtB1n_7Visitor9visit_u64pEBc_ +FN:9,_RINvXNvXNvNtNtCs4dveHeDtRhK_7backend7session18session_middleware1__NtB8_11CredentialsNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB3_14___FieldVisitorNtB1n_7Visitor9visit_strpEBc_ +FN:9,_RINvXNvXNvNtNtCs4dveHeDtRhK_7backend7session18session_middleware1__NtB8_11CredentialsNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB3_14___FieldVisitorNtB1n_7Visitor11visit_bytespEBc_ +FN:9,_RINvXs_NvXNvNtNtCs4dveHeDtRhK_7backend7session18session_middleware1__NtBa_11CredentialsNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB5_7___FieldB1n_11deserializepEBe_ +FN:9,_RNvXs0_NvXNvNtNtCs4dveHeDtRhK_7backend7session18session_middleware1__NtBa_11CredentialsNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB5_9___VisitorNtB1p_7Visitor9expecting +FN:9,_RINvXs0_NvXNvNtNtCs4dveHeDtRhK_7backend7session18session_middleware1__NtBb_11CredentialsNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB6_9___VisitorNtB1q_7Visitor9visit_seqpEBf_ +FN:9,_RINvXs0_NvXNvNtNtCs4dveHeDtRhK_7backend7session18session_middleware1__NtBb_11CredentialsNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB6_9___VisitorNtB1q_7Visitor9visit_mappEBf_ +FNDA:7,_RNvMNtNtCs4dveHeDtRhK_7backend7session18session_middlewareNtB2_4User12authenticate +FNDA:8,_RNvNtNtCs4dveHeDtRhK_7backend7session18session_middleware18session_middleware +FNDA:1,_RNvNtNtNtCs4dveHeDtRhK_7backend7session18session_middleware5testss_40test_user_authenticate_valid_credentials +FNDA:1,_RNvNtNtNtCs4dveHeDtRhK_7backend7session18session_middleware5testss_39test_user_authenticate_invalid_username +FNDA:1,_RNvNtNtNtCs4dveHeDtRhK_7backend7session18session_middleware5testss_39test_user_authenticate_invalid_password +FNDA:0,_RNvXNvXNvNtNtCs4dveHeDtRhK_7backend7session18session_middleware1__NtB7_11CredentialsNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB2_14___FieldVisitorNtB1m_7Visitor9expecting +FNDA:0,_RINvXNvXNvNtNtCs4dveHeDtRhK_7backend7session18session_middleware1__NtB8_11CredentialsNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB3_14___FieldVisitorNtB1n_7Visitor9visit_u64pEBc_ +FNDA:0,_RINvXNvXNvNtNtCs4dveHeDtRhK_7backend7session18session_middleware1__NtB8_11CredentialsNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB3_14___FieldVisitorNtB1n_7Visitor9visit_strpEBc_ +FNDA:0,_RINvXNvXNvNtNtCs4dveHeDtRhK_7backend7session18session_middleware1__NtB8_11CredentialsNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB3_14___FieldVisitorNtB1n_7Visitor11visit_bytespEBc_ +FNDA:0,_RINvXs_NvXNvNtNtCs4dveHeDtRhK_7backend7session18session_middleware1__NtBa_11CredentialsNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB5_7___FieldB1n_11deserializepEBe_ +FNDA:0,_RNvXs0_NvXNvNtNtCs4dveHeDtRhK_7backend7session18session_middleware1__NtBa_11CredentialsNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB5_9___VisitorNtB1p_7Visitor9expecting +FNDA:0,_RINvXs0_NvXNvNtNtCs4dveHeDtRhK_7backend7session18session_middleware1__NtBb_11CredentialsNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB6_9___VisitorNtB1q_7Visitor9visit_seqpEBf_ +FNDA:0,_RINvXs0_NvXNvNtNtCs4dveHeDtRhK_7backend7session18session_middleware1__NtBb_11CredentialsNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB6_9___VisitorNtB1q_7Visitor9visit_mappEBf_ +FNF:6 +FNH:5 +DA:9,0 +DA:31,7 +DA:32,7 +DA:33,7 +DA:34,7 +DA:35,7 +DA:36,7 +DA:37,7 +DA:38,2 +DA:39,2 +DA:40,2 +DA:41,5 +DA:42,5 +DA:43,5 +DA:44,1 +DA:45,1 +DA:46,1 +DA:47,4 +DA:48,4 +DA:49,4 +DA:50,4 +DA:51,4 +DA:52,4 +DA:53,4 +DA:54,7 +DA:57,8 +DA:58,8 +DA:59,8 +DA:60,8 +DA:61,8 +DA:62,8 +DA:63,8 +DA:64,8 +DA:65,8 +DA:66,8 +DA:72,1 +DA:73,1 +DA:74,1 +DA:75,1 +DA:76,1 +DA:77,1 +DA:78,1 +DA:79,1 +DA:80,1 +DA:81,1 +DA:82,1 +DA:83,1 +DA:84,1 +DA:85,1 +DA:86,1 +DA:87,1 +DA:90,1 +DA:91,1 +DA:92,1 +DA:93,1 +DA:94,1 +DA:95,1 +DA:96,1 +DA:97,1 +DA:98,1 +DA:99,1 +DA:100,1 +DA:101,1 +DA:102,1 +DA:103,1 +DA:104,1 +DA:105,1 +DA:108,1 +DA:109,1 +DA:110,1 +DA:111,1 +DA:112,1 +DA:113,1 +DA:114,1 +DA:115,1 +DA:116,1 +DA:117,1 +DA:118,1 +DA:119,1 +DA:120,1 +DA:121,1 +BRF:0 +BRH:0 +LF:81 +LH:80 +end_of_record +SF:/Users/lukasz.celitan/Git/AstroX/src/backend/src/session/validate_session.rs +FN:25,_RNvNtNtNtCs4dveHeDtRhK_7backend7session16validate_session5tests12manual_error +FN:35,_RNvNtNtNtCs4dveHeDtRhK_7backend7session16validate_session5tests14manual_success +FN:46,_RNvNtNtNtCs4dveHeDtRhK_7backend7session16validate_session5testss_27test_validate_session_error +FN:62,_RNvNtNtNtCs4dveHeDtRhK_7backend7session16validate_session5testss_21test_validate_session +FN:4,_RNvNtNtCs4dveHeDtRhK_7backend7session16validate_session16validate_session +FN:25,_RNCNvNtNtNtCs4dveHeDtRhK_7backend7session16validate_session5tests12manual_error0B9_ +FN:35,_RNCNvNtNtNtCs4dveHeDtRhK_7backend7session16validate_session5tests14manual_success0B9_ +FN:46,_RNCNvNtNtNtCs4dveHeDtRhK_7backend7session16validate_session5testss_27test_validate_session_error0B9_ +FN:62,_RNCNvNtNtNtCs4dveHeDtRhK_7backend7session16validate_session5testss_21test_validate_session0B9_ +FNDA:1,_RNvNtNtNtCs4dveHeDtRhK_7backend7session16validate_session5tests12manual_error +FNDA:1,_RNvNtNtNtCs4dveHeDtRhK_7backend7session16validate_session5tests14manual_success +FNDA:1,_RNvNtNtNtCs4dveHeDtRhK_7backend7session16validate_session5testss_27test_validate_session_error +FNDA:1,_RNvNtNtNtCs4dveHeDtRhK_7backend7session16validate_session5testss_21test_validate_session +FNDA:6,_RNvNtNtCs4dveHeDtRhK_7backend7session16validate_session16validate_session +FNDA:1,_RNCNvNtNtNtCs4dveHeDtRhK_7backend7session16validate_session5tests12manual_error0B9_ +FNDA:1,_RNCNvNtNtNtCs4dveHeDtRhK_7backend7session16validate_session5tests14manual_success0B9_ +FNDA:1,_RNCNvNtNtNtCs4dveHeDtRhK_7backend7session16validate_session5testss_27test_validate_session_error0B9_ +FNDA:1,_RNCNvNtNtNtCs4dveHeDtRhK_7backend7session16validate_session5testss_21test_validate_session0B9_ +FNF:9 +FNH:9 +DA:4,6 +DA:5,6 +DA:6,6 +DA:7,2 +DA:8,2 +DA:9,2 +DA:10,2 +DA:12,4 +DA:14,6 +DA:25,1 +DA:26,1 +DA:27,1 +DA:28,1 +DA:29,0 +DA:31,1 +DA:33,1 +DA:35,1 +DA:36,1 +DA:37,1 +DA:38,1 +DA:39,1 +DA:40,1 +DA:42,0 +DA:44,1 +DA:46,2 +DA:62,2 +BRF:0 +BRH:0 +LF:30 +LH:28 +end_of_record +SF:/Users/lukasz.celitan/Git/AstroX/src/backend/src/ssr_routes/login.rs +FN:6,_RNCNvNtNtCs4dveHeDtRhK_7backend10ssr_routes5login10login_form0B7_ +FN:6,_RNvNtNtCs4dveHeDtRhK_7backend10ssr_routes5login10login_form +FNDA:2,_RNCNvNtNtCs4dveHeDtRhK_7backend10ssr_routes5login10login_form0B7_ +FNDA:2,_RNvNtNtCs4dveHeDtRhK_7backend10ssr_routes5login10login_form +FNF:2 +FNH:2 +DA:6,2 +DA:7,2 +DA:8,2 +DA:9,0 +DA:10,0 +DA:11,2 +DA:12,2 +DA:13,2 +DA:14,2 +DA:15,2 +DA:16,2 +DA:17,2 +DA:18,2 +DA:19,2 +DA:20,2 +DA:21,2 +DA:22,2 +DA:23,2 +DA:24,2 +DA:25,2 +DA:26,2 +DA:27,2 +DA:28,2 +DA:29,2 +DA:30,2 +DA:31,2 +DA:32,2 +DA:33,2 +DA:34,2 +DA:35,2 +DA:36,2 +DA:37,2 +DA:38,2 +DA:39,2 +DA:40,2 +DA:41,2 +DA:42,2 +DA:43,2 +DA:44,2 +BRF:0 +BRH:0 +LF:40 +LH:38 +end_of_record +SF:/Users/lukasz.celitan/Git/AstroX/src/backend/src/ssr_routes/post_login.rs +FN:12,_RNvXNvXNvNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_login1__NtB7_8FormDataNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB2_14___FieldVisitorNtB1e_7Visitor9expecting +FN:12,_RINvXNvXNvNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_login1__NtB8_8FormDataNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB3_14___FieldVisitorNtB1f_7Visitor9visit_u64pEBc_ +FN:12,_RINvXNvXNvNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_login1__NtB8_8FormDataNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB3_14___FieldVisitorNtB1f_7Visitor11visit_bytespEBc_ +FN:12,_RNvXs0_NvXNvNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_login1__NtBa_8FormDataNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB5_9___VisitorNtB1h_7Visitor9expecting +FN:12,_RINvXs0_NvXNvNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_login1__NtBb_8FormDataNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB6_9___VisitorNtB1i_7Visitor9visit_seqpEBf_ +FN:21,_RNCNvNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_login10post_login0B7_ +FN:69,_RINvNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_login15error_chain_fmtNtB2_10LoginErrorEB6_ +FN:12,_RINvXNvXNvNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_login1__NtB8_8FormDataNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB3_14___FieldVisitorNtB1f_7Visitor9visit_strNtNtB1f_5value5ErrorEBc_ +FN:12,_RINvXs_NvXNvNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_login1__NtBa_8FormDataNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB5_7___FieldB1f_11deserializeNtNtCsam5mRkJF7PU_16serde_urlencoded2de4PartEBe_ +FN:12,_RINvXs0_NvXNvNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_login1__NtBb_8FormDataNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB6_9___VisitorNtB1i_7Visitor9visit_mapINtNtB1i_5value15MapDeserializerNtNtCsam5mRkJF7PU_16serde_urlencoded2de12PartIteratorNtB2O_5ErrorEEBf_ +FN:18,_RNvNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_login10post_login +FN:53,_RNvNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_login14login_redirect +FN:83,_RNvXNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_loginNtB2_10LoginErrorNtNtCsliVzVx4luf9_4core3fmt5Debug3fmt +FNDA:0,_RNvXNvXNvNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_login1__NtB7_8FormDataNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB2_14___FieldVisitorNtB1e_7Visitor9expecting +FNDA:0,_RINvXNvXNvNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_login1__NtB8_8FormDataNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB3_14___FieldVisitorNtB1f_7Visitor9visit_u64pEBc_ +FNDA:0,_RINvXNvXNvNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_login1__NtB8_8FormDataNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB3_14___FieldVisitorNtB1f_7Visitor11visit_bytespEBc_ +FNDA:0,_RNvXs0_NvXNvNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_login1__NtBa_8FormDataNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB5_9___VisitorNtB1h_7Visitor9expecting +FNDA:0,_RINvXs0_NvXNvNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_login1__NtBb_8FormDataNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB6_9___VisitorNtB1i_7Visitor9visit_seqpEBf_ +FNDA:4,_RNCNvNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_login10post_login0B7_ +FNDA:0,_RINvNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_login15error_chain_fmtNtB2_10LoginErrorEB6_ +FNDA:8,_RINvXNvXNvNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_login1__NtB8_8FormDataNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB3_14___FieldVisitorNtB1f_7Visitor9visit_strNtNtB1f_5value5ErrorEBc_ +FNDA:8,_RINvXs_NvXNvNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_login1__NtBa_8FormDataNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB5_7___FieldB1f_11deserializeNtNtCsam5mRkJF7PU_16serde_urlencoded2de4PartEBe_ +FNDA:4,_RINvXs0_NvXNvNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_login1__NtBb_8FormDataNtNtCs7eCbzPz91QN_5serde2de11Deserialize11deserializeNtB6_9___VisitorNtB1i_7Visitor9visit_mapINtNtB1i_5value15MapDeserializerNtNtCsam5mRkJF7PU_16serde_urlencoded2de12PartIteratorNtB2O_5ErrorEEBf_ +FNDA:4,_RNvNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_login10post_login +FNDA:1,_RNvNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_login14login_redirect +FNDA:0,_RNvXNtNtCs4dveHeDtRhK_7backend10ssr_routes10post_loginNtB2_10LoginErrorNtNtCsliVzVx4luf9_4core3fmt5Debug3fmt +FNF:6 +FNH:4 +DA:12,12 +DA:18,4 +DA:19,4 +DA:20,4 +DA:21,4 +DA:22,4 +DA:23,4 +DA:24,4 +DA:25,4 +DA:26,4 +DA:27,4 +DA:28,3 +DA:29,3 +DA:30,3 +DA:31,3 +DA:32,3 +DA:33,3 +DA:34,3 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:43,1 +DA:44,1 +DA:45,1 +DA:46,0 +DA:48,1 +DA:51,4 +DA:53,1 +DA:54,1 +DA:55,1 +DA:56,1 +DA:57,1 +DA:58,1 +DA:59,1 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:79,0 +DA:80,0 +DA:83,0 +DA:84,0 +DA:85,0 +BRF:0 +BRH:0 +LF:51 +LH:31 +end_of_record \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..eee73af --- /dev/null +++ b/readme.md @@ -0,0 +1,295 @@ +

AstroX (Actix + Astro.build)

+ +█████ ███████ ████████ ██████ ██████ ██ ██ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ + ███████ ███████ ██ ██████ ██ ██ ███ + ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ + ██ ██ ███████ ██ ██ ██ ██████ ██ ██ + +

+

+🦀 Rust orientated monolithic template for building modern web + applications. +

+ +

+ Version + + Documentation + + + License: MIT + + + Twitter: SpaceoutPl + +

+ +![CodeQL](https://github.com/MassivDash/ado-npmrc-ts-action/actions/workflows/codeql-analysis.yml/badge.svg)![CI](https://github.com/MassivDash/astrox/actions/workflows/ci.yml/badge.svg)![Compliation](https://github.com/MassivDash/astrox/actions/workflows/release.yml/badge.svg)! + +**Platforms** + +![windows](https://img.shields.io/badge/Platform-Windows-blue) +![linux](https://img.shields.io/badge/Platform-Linux-blue) +![macOs](https://img.shields.io/badge/Platform-MacOs-blue) + +## Monolithic repo for developing full stack application, using rust and cargo tools as primary development environment. + +Frontend is a standalone astro.build application that will create the frontend bundle served by rust actix server. + +## Rust + Astro web development boilerplate. + +To start developing with AstroX you will need rustc > 1.74 and node > 18.14. Clone the project and execute; + +``` +cargo run +``` + +That's all you need to get started, the interactive cli will guide you through installation process. + +## Features + +### CLI + +Rust written command line interface starts, serves and tests the astro x project. Fast and efficient with only few dependencies will create a professional development environment for rust opinionated project. + +#### Cli Project Runner + +Handles installation and system checks, it will check the astroX system prerequisites and either help you install or provide you with necessary information to start the project. + +- automatic development port rotation for frontend and backend +- interactive mode, execute actions through cli gui +- git hooks integration +- build the packages +- serve the bundle (with auto restart) +- test the project +- execute the project with cmd line arguments + +#### Git hooks + +Pre defined git hooks for quality code writing + +- commit msg via commitlint-rs +- pre-commit (test and lint staged files) +- pre-push (test all) + +#### CLI arguments + +```sh +Command list: +--help [print this help ] +--sync-git-hooks [copy git_hooks folder contents to .git/hooks] +--remove-git-hooks [remove hooks from .git/hooks folder] +--build [build production bundle for frontend and backend] +--serve [start the production server with the frontend build] +--test [run the tests] +--create-toml [create a new Astrox.toml file] +--interactive [start the interactive mode] +--system-checks [run the system checks] +--coverage [run cli and backend coverage] + + +Cli arguments: +--host="127.0.0.1" [ip address] +--port=8080 [actix port number] +--env=prod / dev [environment] +--astro-port=4321 [astro development port number] +--prod-astro-build=true / false [Build astro during cli prod start] +--set-public-api=https://custom.api/api [cli to astro env creation, used for static server url call building] +``` + +### Actix backend + +https://actix.rs/docs/getting-started/ + +Rust based server from Actix framework. + +- serve static astro x files +- 3rd api call example +- logging +- graphql [coming soon] +- ssr [coming soon] + +### Astro + +https://astro.build/ + +Astro is a frontend framework that focuses on mainly on delivering html first, the fastest and most versatile of the frameworks allows to incorporate any of the major UI frameworks such as React, Svelte, Vue, Solid.js and others ... + +The boilerplate provides and example of the Astro 4.0 transition capabilities. + +## Project Structure + +```bash +AstroX +├─ .github //workflows and ci/cd checks +├─ └─ workflows +├─ └─ ├─ codeql-analysis.yml +├─ └─ ├─ pr.yml +├─ └─ └─ relase.yml +├─ git_hooks // set up git hooks for development +├─ ├─ commit-msg +├─ ├─ commit-msg-windows-example +├─ ├─ pre-commit +├─ └─ pre-push +├─ src +├─ ├─ backend //Actix backend, own rust project +├─ ├─ ├─ src +├─ ├─ ├─ ├─ api //Api routes examples +├─ ├─ ├─ ├─ ├─ hello +├─ ├─ ├─ ├─ ├─ ├─ get.rs +├─ ├─ ├─ ├─ ├─ ├─ mod.rs +├─ ├─ ├─ ├─ ├─ └─ post.rs +├─ ├─ ├─ ├─ ├─ space_x // server to server call +├─ ├─ ├─ ├─ ├─ ├─ get.rs +├─ ├─ ├─ ├─ ├─ └─ mod.rs +├─ ├─ ├─ ├─ └─ mod.rs +├─ ├─ ├─ ├─ args +├─ ├─ ├─ ├─ ├─ collect_args.rs +├─ ├─ ├─ ├─ └─ mod.rs +├─ ├─ ├─ ├─ auth // Simple auth route middleware +├─ ├─ ├─ ├─ ├─ auth_middleware.rs +├─ ├─ ├─ ├─ ├─ login.rs +├─ ├─ ├─ ├─ └─ mod.rs +├─ ├─ ├─ ├─ cors +├─ ├─ ├─ ├─ ├─ get_cors_options.rs +├─ ├─ ├─ ├─ └─ mod.rs +├─ ├─ ├─ ├─ session // Session middleware examples +├─ ├─ ├─ ├─ ├─ flash_messages.rs +├─ ├─ ├─ ├─ ├─ mod.rs +├─ ├─ ├─ ├─ ├─ session_middleware.rs +├─ ├─ ├─ ├─ └─ validate_session.rs +├─ ├─ ├─ └─ main.rs +├─ ├─ └─ Cargo.toml +├─ ├─ cli // AstroX project runner +├─ ├─ ├─ cmds +├─ ├─ ├─ ├─ tests +├─ ├─ ├─ ├─ ├─ cmd_list_test.rs +├─ ├─ ├─ ├─ ├─ interactive_test.rs +├─ ├─ ├─ ├─ └─ mod.rs +├─ ├─ ├─ ├─ cmd_list.rs +├─ ├─ ├─ ├─ execute_cmd.rs +├─ ├─ ├─ ├─ interactive.rs +├─ ├─ ├─ └─ mod.rs +├─ ├─ ├─ config +├─ ├─ ├─ ├─ tests +├─ ├─ ├─ ├─ ├─ get_config_test.rs +├─ ├─ ├─ ├─ └─ mod.rs +├─ ├─ ├─ ├─ collect_args.rs +├─ ├─ ├─ ├─ create_dotenv.rs +├─ ├─ ├─ ├─ get_config.rs +├─ ├─ ├─ ├─ mod.rs +├─ ├─ ├─ └─ toml.rs +├─ ├─ ├─ development +├─ ├─ ├─ ├─ mod.rs +├─ ├─ ├─ └─ start_development.rs +├─ ├─ ├─ pre_run +├─ ├─ ├─ ├─ cargo +├─ ├─ ├─ ├─ ├─ checks.rs +├─ ├─ ├─ ├─ ├─ mod.rs +├─ ├─ ├─ ├─ └─ validate.rs +├─ ├─ ├─ ├─ npm +├─ ├─ ├─ ├─ ├─ checks.rs +├─ ├─ ├─ ├─ ├─ mod.rs +├─ ├─ ├─ ├─ └─ validate.rs +├─ ├─ ├─ ├─ utils +├─ ├─ ├─ ├─ ├─ check_semver.rs +├─ ├─ ├─ ├─ ├─ git_hooks.rs +├─ ├─ ├─ ├─ └─ mod.rs +├─ ├─ ├─ ├─ execute.rs +├─ ├─ ├─ ├─ mod.rs +├─ ├─ ├─ └─ system_checks.rs +├─ ├─ ├─ production +├─ ├─ ├─ ├─ build_production.rs +├─ ├─ ├─ ├─ mod.rs +├─ ├─ ├─ └─ start_production.rs +├─ ├─ ├─ tests +├─ ├─ ├─ ├─ execute.rs +├─ ├─ ├─ └─ mod.rs +├─ ├─ ├─ utils +├─ ├─ ├─ ├─ mod.rs +├─ ├─ ├─ └─ terminal.rs +├─ ├─ └─ mod.rs +├─ ├─ frontend // Astro.Build project +├─ ├─ ├─ .astro +├─ ├─ ├─ └─ settings.json +├─ ├─ ├─ public +├─ ├─ ├─ ├─ astroStation.jpeg +├─ ├─ ├─ ├─ bgAstro.png +├─ ├─ ├─ ├─ bgPattern.png +├─ ├─ ├─ ├─ favicon.svg +├─ ├─ ├─ ├─ hero.jpeg +├─ ├─ ├─ └─ herobc.jpeg +├─ ├─ ├─ src +├─ ├─ ├─ ├─ axiosInstance +├─ ├─ ├─ ├─ ├─ axiosBackendInstance.test.ts +├─ ├─ ├─ ├─ └─ axiosBackendInstance.ts +├─ ├─ ├─ ├─ components +├─ ├─ ├─ ├─ ├─ navbar +├─ ├─ ├─ ├─ ├─ ├─ Navbar.astro +├─ ├─ ├─ ├─ ├─ ├─ Navbar.test.ts +├─ ├─ ├─ ├─ ├─ ├─ NavbarItem.astro +├─ ├─ ├─ ├─ ├─ └─ NavbarItem.test.ts +├─ ├─ ├─ ├─ ├─ spaceX +├─ ├─ ├─ ├─ ├─ ├─ spacex.svelte +├─ ├─ ├─ ├─ ├─ └─ spacex.test.ts +├─ ├─ ├─ ├─ ├─ zoomImage +├─ ├─ ├─ ├─ ├─ ├─ zoomImage.astro +├─ ├─ ├─ ├─ ├─ └─ zoomImage.test.ts +├─ ├─ ├─ ├─ ├─ Card.astro +├─ ├─ ├─ ├─ ├─ Footer.astro +├─ ├─ ├─ ├─ ├─ Hero.astro +├─ ├─ ├─ ├─ └─ Section.astro +├─ ├─ ├─ ├─ layouts +├─ ├─ ├─ ├─ ├─ Layout.astro +├─ ├─ ├─ ├─ └─ Layout.test.ts +├─ ├─ ├─ ├─ pages +├─ ├─ ├─ ├─ ├─ auth +├─ ├─ ├─ ├─ ├─ └─ protected.astro +├─ ├─ ├─ ├─ ├─ 404.astro +├─ ├─ ├─ ├─ ├─ actix.astro +├─ ├─ ├─ ├─ ├─ astro.astro +├─ ├─ ├─ ├─ ├─ cli.astro +├─ ├─ ├─ ├─ └─ index.astro +├─ ├─ ├─ ├─ sections +├─ ├─ ├─ ├─ ├─ Home +├─ ├─ ├─ ├─ ├─ ├─ HomeClone.astro +├─ ├─ ├─ ├─ ├─ ├─ HomeMiddleLinks.astro +├─ ├─ ├─ ├─ ├─ └─ HomeSecondary.astro +├─ ├─ ├─ ├─ └─ imgs +├─ ├─ ├─ ├─ └─ ├─ actix.png +├─ ├─ ├─ ├─ └─ ├─ astro.jpeg +├─ ├─ ├─ ├─ └─ ├─ astro.png +├─ ├─ ├─ ├─ └─ ├─ astro2.jpeg +├─ ├─ ├─ ├─ └─ ├─ cli.png +├─ ├─ ├─ ├─ └─ └─ contact.jpeg +├─ ├─ ├─ ├─ svgs +├─ ├─ ├─ ├─ ├─ Actix.astro +├─ ├─ ├─ ├─ ├─ AstroIcon.astro +├─ ├─ ├─ ├─ ├─ Github.astro +├─ ├─ ├─ ├─ ├─ RustIcon.astro +├─ ├─ ├─ ├─ └─ Spaceout.astro +├─ ├─ ├─ ├─ tests +├─ ├─ ├─ ├─ └─ pages.test.ts +├─ ├─ ├─ └─ env.d.ts +├─ ├─ ├─ .eslintignore +├─ ├─ ├─ .eslintrc.cjs +├─ ├─ ├─ .gitignore +├─ ├─ ├─ .nvmrc +├─ ├─ ├─ astro.config.mjs +├─ ├─ ├─ package.json +├─ ├─ ├─ prettier.config.cjs +├─ ├─ ├─ README.md +├─ ├─ ├─ svelte.config.js +├─ ├─ ├─ tsconfig.json +├─ ├─ └─ vitest.config.ts +├─ └─ main.rs +├─ .gitignore +├─ .nvmrc +├─ Astrox.toml +├─ Cargo.toml +└─ readme.md +``` + +### Demo + +https://astrox.spaceout.pl diff --git a/src/backend/Cargo.toml b/src/backend/Cargo.toml new file mode 100644 index 0000000..1590728 --- /dev/null +++ b/src/backend/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "backend" +version = "0.1.2" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +actix-web = "4" +actix-rt = "2" +actix-files = "0.6.6" +actix-cors = "0.7.0" +actix-utils = "3.0.1" +futures-util = "0.3.30" +actix-web-flash-messages = { version = "0.4", features = ["cookies"] } +actix-session = { version = "0.10.0", features = ["cookie-session"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +reqwest = { version = "0.12", features = ["json"] } +env_logger = "0.11.5" +anyhow = "1.0" +dotenv = "0.15.0" +thiserror = "1.0.63" +pin-project = "1.1.5" +futures = "0.3.30" +globset = "0.4.14" diff --git a/src/backend/src/api/hello/get.rs b/src/backend/src/api/hello/get.rs new file mode 100644 index 0000000..d097f07 --- /dev/null +++ b/src/backend/src/api/hello/get.rs @@ -0,0 +1,26 @@ +use actix_web::{get, HttpResponse, Responder}; + +#[get("/api/hello")] +async fn json_response_get() -> impl Responder { + HttpResponse::Ok() + .content_type("application/json") + .body("{\"message\": \"Hello World\"}") +} +#[cfg(test)] +mod tests { + use super::*; + use actix_web::{test, App}; + + #[actix_rt::test] + async fn test_json_response_get() { + let mut app = test::init_service(App::new().service(json_response_get)).await; + + let req = test::TestRequest::get().uri("/api/hello").to_request(); + let resp = test::call_service(&mut app, req).await; + + assert!(resp.status().is_success()); + + let body = test::read_body(resp).await; + assert_eq!(body, "{\"message\": \"Hello World\"}"); + } +} diff --git a/src/backend/src/api/hello/mod.rs b/src/backend/src/api/hello/mod.rs new file mode 100644 index 0000000..69e599d --- /dev/null +++ b/src/backend/src/api/hello/mod.rs @@ -0,0 +1,2 @@ +pub mod get; +pub mod post; diff --git a/src/backend/src/api/hello/post.rs b/src/backend/src/api/hello/post.rs new file mode 100644 index 0000000..511c3ae --- /dev/null +++ b/src/backend/src/api/hello/post.rs @@ -0,0 +1,27 @@ +use actix_web::{post, HttpResponse, Responder}; + +#[post("/api/hello")] +pub async fn json_response() -> impl Responder { + HttpResponse::Ok() + .content_type("application/json") + .body("{\"message\": \"Hello World\"}") +} +#[cfg(test)] + +mod tests { + use super::*; + use actix_web::{test, App}; + + #[actix_rt::test] + async fn test_json_response() { + let mut app = test::init_service(App::new().service(json_response)).await; + + let req = test::TestRequest::post().uri("/api/hello").to_request(); + + let resp = test::call_service(&mut app, req).await; + assert!(resp.status().is_success()); + + let body = test::read_body(resp).await; + assert_eq!(body, "{\"message\": \"Hello World\"}"); + } +} diff --git a/src/backend/src/api/mod.rs b/src/backend/src/api/mod.rs new file mode 100644 index 0000000..5562bbb --- /dev/null +++ b/src/backend/src/api/mod.rs @@ -0,0 +1,2 @@ +pub mod hello; +pub mod space_x; diff --git a/src/backend/src/api/space_x/get.rs b/src/backend/src/api/space_x/get.rs new file mode 100644 index 0000000..53ee616 --- /dev/null +++ b/src/backend/src/api/space_x/get.rs @@ -0,0 +1,81 @@ +use actix_web::{get, Error as ActixError, HttpResponse}; +use reqwest::{get, Error}; +use serde::{Deserialize, Serialize}; + +// response info at: https://docs.spacexdata.com/#5fcdb875-914f-4aef-a932-254397cf147a + +// [ +// { +// "id": 1, +// "active": false, +// "stages": 2, +// "boosters": 0, +// "cost_per_launch": 6700000, +// "success_rate_pct": 40, +// "first_flight": "2006-03-24", +// "country": "Republic of the Marshall Islands", +// "company": "SpaceX", +// "wikipedia": "https://en.wikipedia.org/wiki/Falcon_1", +// "description": "The Falcon 1 was an expendable launch system privately developed and manufactured by SpaceX during 2006-2009. On 28 September 2008, Falcon 1 became the first privately-developed liquid-fuel launch vehicle to go into orbit around the Earth.", +// "rocket_id": "falcon1", +// "rocket_name": "Falcon 1", +// "rocket_type": "rocket" +// } +// ] + +#[derive(Deserialize, Serialize, Debug)] +struct Rocket { + id: i32, + active: bool, + stages: i32, + boosters: i32, + cost_per_launch: i32, + success_rate_pct: i32, + first_flight: String, + country: String, + company: String, + wikipedia: String, + description: String, + rocket_id: String, + rocket_name: String, + rocket_type: String, +} + +#[get("/api/space-x")] + +pub async fn json_get_space_x() -> Result { + let response = get("https://api.spacexdata.com/v3/rockets").await; + + match response { + Ok(response) => { + let response: Result, Error> = response.json().await; + + match response { + Ok(res) => Ok(HttpResponse::Ok().json(res)), + Err(error) => Ok(HttpResponse::InternalServerError().body(error.to_string())), + } + } + Err(error) => Ok(HttpResponse::InternalServerError().body(error.to_string())), + } +} +#[cfg(test)] + +mod tests { + use super::*; + use actix_web::{test, App}; + + #[actix_rt::test] + async fn test_json_get_space_x() { + let mut app = test::init_service(App::new().service(json_get_space_x)).await; + + let req = test::TestRequest::get().uri("/api/space-x").to_request(); + let resp = test::call_service(&mut app, req).await; + + assert!(resp.status().is_success()); + + let body = test::read_body(resp).await; + let rockets: Vec = serde_json::from_slice(&body).unwrap(); + + assert_eq!(rockets[0].id, 1); + } +} diff --git a/src/backend/src/api/space_x/mod.rs b/src/backend/src/api/space_x/mod.rs new file mode 100644 index 0000000..125ca70 --- /dev/null +++ b/src/backend/src/api/space_x/mod.rs @@ -0,0 +1 @@ +pub mod get; diff --git a/src/backend/src/args/collect_args.rs b/src/backend/src/args/collect_args.rs new file mode 100644 index 0000000..69ca3f7 --- /dev/null +++ b/src/backend/src/args/collect_args.rs @@ -0,0 +1,96 @@ +/// Get the additional arguments from "cargo run" + +/// List of arguments +/// Bind actix server to a host, used for development and production +/// --host=127.0.0.1 + +/// Bind actix server to a port, used for development and production +/// --port=8080 + +/// Set the environment +/// --env=prod / dev +/// + +/// Set the cors origin +/// --cors_url=astrox.spaceout.pl + +pub struct Args { + pub host: String, + pub port: String, + pub env: String, + pub cors_url: String, +} + +pub fn collect_args(args: Vec) -> Args { + let mut env = "dev"; + let mut host = "127.0.0.1"; + let mut port = 8080; + let mut cors_url = "astrox.spaceout.pl"; + + for arg in &args { + if arg.starts_with("--env=") { + let split: Vec<&str> = arg.split('=').collect(); + if split.len() == 2 { + env = split[1]; + } + } + + if arg.starts_with("--host=") { + let split: Vec<&str> = arg.split('=').collect(); + if split.len() == 2 { + host = split[1]; + } + } + + if arg.starts_with("--port=") { + let split: Vec<&str> = arg.split('=').collect(); + if split.len() == 2 { + port = split[1].parse::().unwrap(); + } + } + + if arg.starts_with("--cors_url=") { + let split: Vec<&str> = arg.split('=').collect(); + if split.len() == 2 { + cors_url = split[1]; + } + } + } + + Args { + host: host.to_string(), + port: port.to_string(), + env: env.to_string(), + cors_url: cors_url.to_string(), + } +} +#[cfg(test)] +mod tests { + use super::*; + use std::env; + + #[test] + fn test_collect_args_default() { + let args = collect_args(env::args().collect()); + + assert_eq!(args.host, "127.0.0.1"); + assert_eq!(args.port, "8080"); + assert_eq!(args.env, "dev"); + } + + #[test] + fn test_collect_prod_arg() { + let test_args = vec![ + "--env=prod".to_string(), + "--port=4000".to_string(), + "--host=0.0.0.0".to_string(), + "--cors_url=spaceout.pl".to_string(), + ]; + let args = collect_args(test_args); + + assert_eq!(args.host, "0.0.0.0"); + assert_eq!(args.port, "4000"); + assert_eq!(args.env, "prod"); + assert_eq!(args.cors_url, "spaceout.pl"); + } +} diff --git a/src/backend/src/args/mod.rs b/src/backend/src/args/mod.rs new file mode 100644 index 0000000..d962514 --- /dev/null +++ b/src/backend/src/args/mod.rs @@ -0,0 +1 @@ +pub mod collect_args; diff --git a/src/backend/src/auth/auth_middleware.rs b/src/backend/src/auth/auth_middleware.rs new file mode 100644 index 0000000..48c8254 --- /dev/null +++ b/src/backend/src/auth/auth_middleware.rs @@ -0,0 +1,293 @@ +/// This module contains the implementation of the `Authentication` middleware. +/// +/// The `Authentication` middleware is responsible for authenticating incoming requests based on a set of routes. +/// It checks if the requested path matches any of the specified routes and validates the session associated with the request. +/// If the path matches and the session is valid, the request is passed to the underlying service. +/// Otherwise, an unauthorized response is returned. +/// +/// # Example +/// +/// ```rust +/// use actix_web::{web, App}; +/// use actix_session::CookieSession; +/// use crate::auth::auth_middleware::Authentication; +/// +/// let routes = vec!["/protected/*".to_string()]; +/// +/// let app = App::new() +/// .wrap(CookieSession::signed(&[0; 32]).secure(false)) +/// .wrap(Authentication { routes }) +/// .service(web::resource("/protected/resource").to(handler)); +/// ``` +use actix_session::{Session, SessionExt}; +use pin_project::pin_project; +use std::{ + marker::PhantomData, + pin::Pin, + task::{Context, Poll}, +}; + +use globset::{Glob, GlobSetBuilder}; + +use actix_utils::future::{ok, Either, Ready}; +use actix_web::{ + body::{EitherBody, MessageBody}, + dev::{Service, ServiceRequest, ServiceResponse, Transform}, + http::StatusCode, + Error, HttpResponse, +}; +use futures::{ready, Future}; + +use crate::session::validate_session::validate_session; +pub struct Authentication { + pub routes: Vec, +} +impl Transform for Authentication +where + S: Service, Error = Error>, + S::Future: 'static, + B: MessageBody, +{ + type Response = ServiceResponse>; + type Error = Error; + type InitError = (); + type Transform = AuthenticationMiddleware; + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ok(AuthenticationMiddleware { + service, + routes: self.routes.clone(), + }) + } +} +pub struct AuthenticationMiddleware { + service: S, + routes: Vec, +} + +impl Service for AuthenticationMiddleware +where + S: Service, Error = Error>, + S::Future: 'static, + B: MessageBody, +{ + type Response = ServiceResponse>; + + type Error = Error; + + type Future = Either, Ready>>; + + fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { + self.service.poll_ready(cx) + } + + fn call(&self, req: ServiceRequest) -> Self::Future { + let session: Session = req.get_session(); + println!("{:?}", session.entries()); + let auth = validate_session(&session); + let routes: Vec = self.routes.iter().map(|s| s.to_string()).collect(); + if match_glob_patterns(routes, req.path()) { + if auth.is_ok() { + Either::left(AuthenticationFuture { + fut: self.service.call(req), + _phantom: PhantomData, + }) + } else { + let res = HttpResponse::with_body(StatusCode::UNAUTHORIZED, "Login or get away"); + Either::right(ok(req + .into_response(res) + .map_into_boxed_body() + .map_into_right_body())) + } + } else { + Either::left(AuthenticationFuture { + fut: self.service.call(req), + _phantom: PhantomData, + }) + } + } +} + +#[pin_project] +pub struct AuthenticationFuture +where + S: Service, +{ + #[pin] + fut: S::Future, + _phantom: PhantomData, +} + +impl Future for AuthenticationFuture +where + B: MessageBody, + S: Service, Error = Error>, +{ + type Output = Result>, Error>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let res = match ready!(self.project().fut.poll(cx)) { + Ok(res) => res, + Err(err) => return Poll::Ready(Err(err)), + }; + + Poll::Ready(Ok(res.map_into_left_body())) + } +} + +fn match_glob_patterns(patterns: Vec, path: &str) -> bool { + let mut builder = GlobSetBuilder::new(); + + for pattern in patterns { + if let Ok(glob) = Glob::new(pattern.as_str()) { + builder.add(glob); + } else { + panic!("Failed to create glob pattern"); + } + } + let set = builder.build().expect("Failed to build glob set"); + !set.matches(path).is_empty() +} + +#[cfg(test)] +mod tests { + use std::env; + + use super::*; + use crate::cors::get_cors_options::get_cors_options; + use crate::session::flash_messages::set_up_flash_messages; + use crate::session::session_middleware::session_middleware; + use crate::ssr_routes::post_login::{post_login, FormData}; + use actix_web::middleware::{NormalizePath, TrailingSlash}; + use actix_web::web::Form; + use actix_web::{test, web, App, Responder}; + + #[test] + async fn test_match_glob_patterns() { + // Test matching pattern + let patterns = vec!["/auth/*".to_string(), "/posts/*".to_string()]; + assert_eq!(match_glob_patterns(patterns.clone(), "/auth/auth"), true); + assert_eq!(match_glob_patterns(patterns.clone(), "/auth/"), true); + + let patterns = vec!["*/auth/*".to_string(), "*/posts/*".to_string()]; + assert_eq!(match_glob_patterns(patterns.clone(), "/auth/auth"), true); + assert_eq!(match_glob_patterns(patterns.clone(), "/auth/"), true); + + assert_eq!( + match_glob_patterns(patterns.clone(), "/auth/protected"), + true + ); + assert_eq!(match_glob_patterns(patterns.clone(), "/posts/456"), true); + assert_eq!(match_glob_patterns(patterns.clone(), "/login"), false); + + // Test non-matching pattern + let patterns = vec!["/users/*".to_string(), "/posts/*".to_string()]; + assert_eq!( + match_glob_patterns(patterns.clone(), "/comments/789"), + false + ); + + // Test empty patterns + let patterns: Vec = vec![]; + assert_eq!(match_glob_patterns(patterns, "/users/123"), false); + + // Test invalid glob pattern + let patterns = vec!["[".to_string(), "/posts/*".to_string()]; + assert!(std::panic::catch_unwind(|| match_glob_patterns(patterns, "/users/123")).is_err()); + } + + async fn test_route() -> impl Responder { + HttpResponse::Ok().body("Hey there!") + } + + #[actix_rt::test] + async fn test_middleware_protected() { + let routes = vec!["/test/*".to_string()]; + + let mut app = test::init_service( + App::new() + .route("/test/test", web::get().to(test_route)) + .wrap(set_up_flash_messages()) + .wrap(session_middleware()) + .wrap(Authentication { routes: routes }), + ) + .await; + + let req = test::TestRequest::get().uri("/test/test").to_request(); + let resp = test::call_service(&mut app, req).await; + assert!(resp.status().is_client_error()); + } + + #[actix_rt::test] + async fn test_middleware_passthrough() { + let routes = vec!["/test/*".to_string()]; + + let mut app = test::init_service( + App::new() + .route("/notProtected", web::get().to(test_route)) + .wrap(set_up_flash_messages()) + .wrap(session_middleware()) + .wrap(Authentication { routes: routes }) + .wrap(NormalizePath::new(TrailingSlash::Trim)), + ) + .await; + + let req = test::TestRequest::get().uri("/notProtected").to_request(); + let resp = test::call_service(&mut app, req).await; + assert!(resp.status().is_success()); + } + #[actix_rt::test] + async fn test_middleware_success() { + let env = "prod".to_string(); + let routes = vec!["/test/*".to_string()]; + let cors = get_cors_options(env, String::from("https://localhost")); + let app = test::init_service( + App::new() + .route("/login", web::post().to(post_login)) + .route("/test/test", web::get().to(test_route)) + .wrap(cors) + .wrap(Authentication { + routes: routes.clone(), + }) + .wrap(set_up_flash_messages()) + .wrap(session_middleware()) + .wrap(NormalizePath::new(TrailingSlash::Trim)), + ) + .await; + + env::set_var("USERNAME", "test_user"); + env::set_var("PASSWORD", "test_password"); + + let form_data = FormData { + username: String::from("test_user"), + password: String::from("test_password"), + }; + + let form = Form(form_data); + + let req = test::TestRequest::post() + .set_form(form) + .uri("/login") + .to_request(); + + let resp = test::call_service(&app, req).await; + assert!(resp.status().is_redirection()); + + let session_cookie = resp + .response() + .cookies() + .find(|x| { + let name = x.name(); + name == "astroX" + }) + .unwrap(); + let req2 = test::TestRequest::get() + .uri("/test/test") + .cookie(session_cookie) + .to_request(); + let resp2 = test::call_service(&app, req2).await; + println!("{:?}", &resp2); + assert!(resp2.status().is_success()) + } +} diff --git a/src/backend/src/auth/mod.rs b/src/backend/src/auth/mod.rs new file mode 100644 index 0000000..2a709c0 --- /dev/null +++ b/src/backend/src/auth/mod.rs @@ -0,0 +1 @@ +pub mod auth_middleware; diff --git a/src/backend/src/cors/get_cors_options.rs b/src/backend/src/cors/get_cors_options.rs new file mode 100644 index 0000000..5f4b7b7 --- /dev/null +++ b/src/backend/src/cors/get_cors_options.rs @@ -0,0 +1,87 @@ +use actix_cors::Cors; +use actix_web::http; + +/// Gets the CORS options based on the environment and allowed origin. +/// +/// # Arguments +/// +/// * `env` - A `String` representing the environment. +/// * `allowed_origin` - A `String` representing the allowed origin. +/// +/// # Returns +/// +/// The `Cors` options based on the environment and allowed origin. +/// +/// # Examples +/// +/// ``` +/// let env = String::from("prod"); +/// let allowed_origin = String::from("https://astrox.spaceout.pl"); +/// let cors = get_cors_options(env, allowed_origin); +/// ``` + +pub fn get_cors_options(env: String, allowed_origin: String) -> Cors { + if env == "prod" { + Cors::default() + .allowed_origin(&allowed_origin) + .allowed_methods(vec!["GET", "POST"]) + .allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT]) + .allowed_header(http::header::CONTENT_TYPE) + .max_age(3600) + } else { + Cors::default() + .allow_any_origin() + .allow_any_method() + .allow_any_header() + .max_age(3600) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use actix_web::{http::header::ContentType, test, web, App, HttpResponse, Responder}; + + //manual route for testing + async fn manual_hello() -> impl Responder { + HttpResponse::Ok().body("Hey there!") + } + + #[actix_web::test] + async fn test_index_prod_get() { + let env = String::from("prod"); + let cors = get_cors_options(env, String::from("https://astrox.spaceout.pl")); + + let app = test::init_service( + App::new() + .wrap(cors) + .route("/", web::get().to(manual_hello)), + ) + .await; + let req = test::TestRequest::default() + .insert_header(ContentType::plaintext()) + .to_request(); + let resp = test::call_service(&app, req).await; + println!("{:?}", resp); + assert!(resp.status().is_success()); + } + + #[actix_web::test] + async fn test_index_dev_get() { + let env = String::from("dev"); + let cors = get_cors_options(env, String::from("https://astrox.spaceout.pl")); + + let app = test::init_service( + App::new() + .wrap(cors) + .route("/", web::get().to(manual_hello)), + ) + .await; + let req = test::TestRequest::default() + .insert_header(ContentType::plaintext()) + .to_request(); + let resp = test::call_service(&app, req).await; + println!("{:?}", resp); + assert!(resp.status().is_success()); + } +} diff --git a/src/backend/src/cors/mod.rs b/src/backend/src/cors/mod.rs new file mode 100644 index 0000000..40b136a --- /dev/null +++ b/src/backend/src/cors/mod.rs @@ -0,0 +1 @@ +pub mod get_cors_options; diff --git a/src/backend/src/main.rs b/src/backend/src/main.rs new file mode 100644 index 0000000..77dcf24 --- /dev/null +++ b/src/backend/src/main.rs @@ -0,0 +1,81 @@ +use std::env; + +use actix_files::{Files, NamedFile}; +use actix_rt::System; +use actix_web::dev::{fn_service, ServiceRequest, ServiceResponse}; +use actix_web::middleware::{NormalizePath, TrailingSlash}; +use actix_web::{middleware, web, App, HttpServer}; + +mod api; +mod args; +mod auth; +mod cors; +mod session; +mod ssr_routes; + +use crate::api::hello::get::json_response_get; +use crate::api::hello::post::json_response; +use crate::api::space_x::get::json_get_space_x; +use crate::args::collect_args::collect_args; +use crate::auth::auth_middleware::Authentication; +use crate::cors::get_cors_options::get_cors_options; +use crate::session::flash_messages::set_up_flash_messages; +use crate::ssr_routes::login::login_form; +use crate::ssr_routes::post_login::post_login; + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + let args = collect_args(env::args().collect()); + let host = args.host; + let port = args.port.parse::().unwrap(); + let cors_url = args.cors_url; + + // configure logging + std::env::set_var("RUST_LOG", "actix_web=info"); + env_logger::init(); + + // Set up the actix server + let server = HttpServer::new(move || { + let env = args.env.to_string(); + let cors = get_cors_options(env, cors_url.clone()); //Prod CORS URL address, for dev run the cors is set to * + let auth_routes: Vec = vec!["/auth/*".to_string()]; // Routes that require authentication + + // The services and wrappers are loaded from the last to first + // Ensure all the wrappers are after routes and handlers + App::new() + .wrap(cors) + .route("/login", web::get().to(login_form)) + .route("/login", web::post().to(post_login)) + .service(json_response) + .service(json_response_get) + .service(json_get_space_x) + .service( + Files::new("/", "../frontend/dist/") + .prefer_utf8(true) + .index_file("index.html") + .default_handler(fn_service(|req: ServiceRequest| async { + let (req, _) = req.into_parts(); + let file = NamedFile::open_async("../frontend/dist/404.html").await?; + let res = file.into_response(&req); + Ok(ServiceResponse::new(req, res)) + })), + ) + .wrap(Authentication { + routes: auth_routes, + }) + .wrap(session::session_middleware::session_middleware()) + .wrap(set_up_flash_messages()) + .wrap(middleware::Compress::default()) + .wrap(middleware::Logger::default()) + .wrap(NormalizePath::new(TrailingSlash::Trim)) // Add this line to handle trailing slashes\ + }) + .bind((host, port))?; + + let server = server.run(); + + System::current().arbiter().spawn(async { + println!("Actix server has started 🚀"); + }); + + server.await +} diff --git a/src/backend/src/session/flash_messages.rs b/src/backend/src/session/flash_messages.rs new file mode 100644 index 0000000..4b35f58 --- /dev/null +++ b/src/backend/src/session/flash_messages.rs @@ -0,0 +1,11 @@ +use actix_web::cookie::Key; +use actix_web_flash_messages::storage::CookieMessageStore; +use actix_web_flash_messages::FlashMessagesFramework; + +pub fn set_up_flash_messages() -> FlashMessagesFramework { + let secret_key = + Key::from(b"0123456789012345678901234567890123456789012345678901234567890123456789"); + + let message_store = CookieMessageStore::builder(secret_key.clone()).build(); + FlashMessagesFramework::builder(message_store).build() +} diff --git a/src/backend/src/session/mod.rs b/src/backend/src/session/mod.rs new file mode 100644 index 0000000..b3d0b6d --- /dev/null +++ b/src/backend/src/session/mod.rs @@ -0,0 +1,3 @@ +pub mod flash_messages; +pub mod session_middleware; +pub mod validate_session; diff --git a/src/backend/src/session/session_middleware.rs b/src/backend/src/session/session_middleware.rs new file mode 100644 index 0000000..df74f41 --- /dev/null +++ b/src/backend/src/session/session_middleware.rs @@ -0,0 +1,122 @@ +use actix_session::{storage::CookieSessionStore, SessionMiddleware}; +use actix_web::cookie::{Key, SameSite}; +use serde::{Deserialize, Serialize}; +use std::env; +extern crate dotenv; + +use dotenv::dotenv; + +#[derive(Deserialize, PartialEq, Eq, Debug)] +pub struct Credentials { + pub username: String, + pub password: String, +} + +#[derive(Serialize, Debug, PartialEq, Eq)] +pub struct User { + pub id: i64, + username: String, + password: String, +} + +#[derive(thiserror::Error, Debug)] +pub enum AuthError { + #[error("Invalid credentials.")] + InvalidCredentials(#[source] anyhow::Error), + #[error(transparent)] + UnexpectedError(#[from] anyhow::Error), +} + +impl User { + pub fn authenticate(credentials: Credentials) -> Result { + dotenv().ok(); + + let password = env::var("PASSWORD").expect("PASSWORD must be set"); + let username = env::var("USERNAME").expect("USERNAME must be set"); + + if *credentials.username != username { + return Err(AuthError::InvalidCredentials(anyhow::anyhow!( + "Invalid credentials." + ))); + } + + if *credentials.password != password { + return Err(AuthError::InvalidCredentials(anyhow::anyhow!( + "Invalid credentials." + ))); + } + + Ok(User { + id: 42, + username: credentials.username, + password: credentials.password, + }) + } +} + +pub fn session_middleware() -> SessionMiddleware { + SessionMiddleware::builder(CookieSessionStore::default(), Key::from(&[0; 64])) + .cookie_name("astroX".to_string()) + .cookie_domain(Some("localhost".to_string())) + .cookie_path("/".to_string()) + .cookie_secure(false) + .cookie_http_only(false) + .cookie_same_site(SameSite::Lax) + .build() +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_user_authenticate_valid_credentials() { + dotenv().ok(); + env::set_var("USERNAME", "test_user"); + env::set_var("PASSWORD", "test_password"); + + let credentials = Credentials { + username: "test_user".to_string(), + password: "test_password".to_string(), + }; + + let result = User::authenticate(credentials); + + assert!(result.is_ok()); + let user = result.unwrap(); + assert_eq!(user.id, 42); + } + + #[test] + fn test_user_authenticate_invalid_username() { + dotenv().ok(); + env::set_var("USERNAME", "test_user"); + env::set_var("PASSWORD", "test_password"); + + let credentials = Credentials { + username: "wrong_user".to_string(), + password: "test_password".to_string(), + }; + + let result = User::authenticate(credentials); + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err.to_string(), "Invalid credentials."); + } + + #[test] + fn test_user_authenticate_invalid_password() { + env::set_var("USERNAME", "test_user"); + env::set_var("PASSWORD", "test_password"); + let credentials = Credentials { + username: "test_user".to_string(), + password: "wrong_password".to_string(), + }; + + let result = User::authenticate(credentials); + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err.to_string(), "Invalid credentials."); + } +} diff --git a/src/backend/src/session/validate_session.rs b/src/backend/src/session/validate_session.rs new file mode 100644 index 0000000..ce8b4d9 --- /dev/null +++ b/src/backend/src/session/validate_session.rs @@ -0,0 +1,77 @@ +use actix_session::Session; +use actix_web::HttpResponse; + +pub fn validate_session(session: &Session) -> Result { + let user_id: Option = session.get("user_id").unwrap_or(None); + match user_id { + Some(id) => { + // keep the user's session alive + session.renew(); + Ok(id) + } + None => Err(HttpResponse::Unauthorized().json("Unauthorized")), + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::session::flash_messages::set_up_flash_messages; + use crate::session::session_middleware::session_middleware; + use actix_session::Session; + use actix_web::{test, web, App, Responder}; + + async fn manual_error(session: Session) -> impl Responder { + let auth = validate_session(&session); + + if auth.is_ok() { + HttpResponse::Ok().body("Hey there!") + } else { + HttpResponse::InternalServerError().json("Internal Server Error") + } + } + + async fn manual_success(session: Session) -> impl Responder { + let _ = session.insert("user_id", 41); + let auth = validate_session(&session); + + if auth.is_ok() { + HttpResponse::Ok().body("Hey there!") + } else { + HttpResponse::InternalServerError().json("Internal Server Error") + } + } + + #[actix_rt::test] + async fn test_validate_session_error() { + let mut app = test::init_service( + App::new() + .route("/test", web::get().to(manual_error)) + .wrap(set_up_flash_messages()) + .wrap(session_middleware()), + ) + .await; + + let req = test::TestRequest::get().uri("/test").to_request(); + + let resp_m = test::call_service(&mut app, req).await; + assert!(resp_m.status().is_server_error()) + } + + #[test] + async fn test_validate_session() { + let mut app = test::init_service( + App::new() + .route("/test", web::get().to(manual_success)) + .wrap(set_up_flash_messages()) + .wrap(session_middleware()), + ) + .await; + + let req = test::TestRequest::get().uri("/test").to_request(); + + let resp_m = test::call_service(&mut app, req).await; + assert!(resp_m.status().is_success()) + } +} diff --git a/src/backend/src/ssr_routes/login.rs b/src/backend/src/ssr_routes/login.rs new file mode 100644 index 0000000..fa003d9 --- /dev/null +++ b/src/backend/src/ssr_routes/login.rs @@ -0,0 +1,44 @@ +use actix_web::http::header::ContentType; +use actix_web::HttpResponse; +use actix_web_flash_messages::IncomingFlashMessages; +use std::fmt::Write; + +pub async fn login_form(flash_messages: IncomingFlashMessages) -> HttpResponse { + let mut error_html = String::new(); + for m in flash_messages.iter() { + writeln!(error_html, "

{}

", m.content()).unwrap(); + } + HttpResponse::Ok() + .content_type(ContentType::html()) + .body(format!( + r#" + + + + Login + + +

A simple rust server side rendered html with a login to a cookie based session

+ +
+ + + + {error_html} +
+ +"#, + )) +} diff --git a/src/backend/src/ssr_routes/mod.rs b/src/backend/src/ssr_routes/mod.rs new file mode 100644 index 0000000..13bfb96 --- /dev/null +++ b/src/backend/src/ssr_routes/mod.rs @@ -0,0 +1,6 @@ +pub mod login; +pub mod post_login; + +// test modules +#[cfg(test)] +pub mod tests; diff --git a/src/backend/src/ssr_routes/post_login.rs b/src/backend/src/ssr_routes/post_login.rs new file mode 100644 index 0000000..db6db04 --- /dev/null +++ b/src/backend/src/ssr_routes/post_login.rs @@ -0,0 +1,86 @@ +use actix_session::Session; +use actix_web::error::InternalError; +use actix_web::http::header::LOCATION; +use actix_web::web; +use actix_web::HttpResponse; +use actix_web_flash_messages::FlashMessage; +use serde::Serialize; +use std::error::Error; + +use crate::session::session_middleware::{AuthError, Credentials, User}; + +#[derive(serde::Deserialize, Serialize)] +pub struct FormData { + pub username: String, + pub password: String, +} + +pub async fn post_login( + form: web::Form, + session: Session, +) -> Result> { + let credentials = Credentials { + username: form.0.username, + password: form.0.password, + }; + + match User::authenticate(credentials) { + Ok(user) => { + println!("User authenticated: {:?}", user); + session.renew(); + match session.insert("user_id", user.id) { + Ok(_) => Ok(HttpResponse::SeeOther() + .insert_header((LOCATION, "/auth/auth")) + .finish()), + Err(e) => { + let error_message = format!("Failed to insert user_id into session: {}", e); + Err(login_redirect(LoginError::UnexpectedError( + anyhow::anyhow!(error_message), + ))) + } + } + } + Err(e) => { + let e = match e { + AuthError::InvalidCredentials(_) => LoginError::AuthError(e.into()), + AuthError::UnexpectedError(_) => LoginError::UnexpectedError(e.into()), + }; + Err(login_redirect(e)) + } + } +} + +fn login_redirect(e: LoginError) -> InternalError { + FlashMessage::error(e.to_string()).send(); + let response = HttpResponse::SeeOther() + .insert_header((LOCATION, "/login")) + .finish(); + InternalError::from_response(e, response) +} + +#[derive(thiserror::Error)] +pub enum LoginError { + #[error("Authentication failed")] + AuthError(#[source] anyhow::Error), + #[error("Something went wrong")] + UnexpectedError(#[from] anyhow::Error), +} + +pub fn error_chain_fmt( + e: &impl std::error::Error, + f: &mut std::fmt::Formatter<'_>, +) -> std::fmt::Result { + writeln!(f, "{}\n", e)?; + let mut current: Option<&dyn Error> = e.source(); + while let Some(cause) = current { + writeln!(f, "Caused by:\n\t{}", cause)?; + current = cause.source(); + } + Ok(()) +} + +impl std::fmt::Debug for LoginError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + error_chain_fmt(self, f) + } +} diff --git a/src/backend/src/ssr_routes/tests/login_test.rs b/src/backend/src/ssr_routes/tests/login_test.rs new file mode 100644 index 0000000..8c75cb3 --- /dev/null +++ b/src/backend/src/ssr_routes/tests/login_test.rs @@ -0,0 +1,107 @@ +#[cfg(test)] +mod tests { + use crate::session::session_middleware::session_middleware; + use crate::ssr_routes::post_login::{post_login, FormData}; + use crate::{session::flash_messages::set_up_flash_messages, ssr_routes::login::login_form}; + use actix_web::{test, web, App}; + use std::env; + use web::Form; + + #[actix_rt::test] + async fn test_login_form() { + let app = test::init_service( + App::new() + .route("/login", web::get().to(login_form)) + .wrap(set_up_flash_messages()), + ) + .await; + let req = test::TestRequest::get().uri("/login").to_request(); + let resp = test::call_service(&app, req).await; + assert!(resp.status().is_success()); + } + + #[actix_rt::test] + async fn test_login_post() { + env::set_var("USERNAME", "test_user"); + env::set_var("PASSWORD", "test_password"); + + let app = test::init_service( + App::new() + .route("/login", web::post().to(post_login)) + .wrap(set_up_flash_messages()) + .wrap(session_middleware()), + ) + .await; + + let form_data = FormData { + username: String::from("test_user"), + password: String::from("test_password"), + }; + + let form = Form(form_data); + + let req = test::TestRequest::post() + .set_form(form) + .uri("/login") + .to_request(); + let resp = test::call_service(&app, req).await; + assert!(resp.status().is_redirection()); + } + + #[actix_rt::test] + async fn test_login_error() { + env::set_var("USERNAME", "test_user"); + env::set_var("PASSWORD", "test_password"); + + let app = test::init_service( + App::new() + .route("/login", web::post().to(post_login)) + .route("/login", web::get().to(login_form)) + .wrap(set_up_flash_messages()) + .wrap(session_middleware()), + ) + .await; + + let form_data = FormData { + username: String::from("spaceghost"), + password: String::from("12345"), + }; + + let form = Form(form_data); + + let req = test::TestRequest::post() + .set_form(form) + .uri("/login") + .to_request(); + let resp = test::call_service(&app, req).await; + assert!(resp.status().is_redirection()); + + let req2 = test::TestRequest::get().uri("/login").to_request(); + let resp2 = test::call_service(&app, req2).await; + assert!(resp2.status().is_success()); + } + + #[actix_rt::test] + async fn test_post_login_unexpected_error() { + let app = test::init_service( + App::new() + .wrap(set_up_flash_messages()) + .wrap(session_middleware()) + .route("/login", web::post().to(post_login)), + ) + .await; + + // Simulate an unexpected error by providing invalid session data + let form_data = FormData { + username: "test_user".to_string(), + password: "test_password".to_string(), + }; + + let req = test::TestRequest::post() + .set_form(&form_data) + .uri("/login") + .to_request(); + let resp = test::call_service(&app, req).await; + assert!(resp.status().is_redirection()); + } +} diff --git a/src/backend/src/ssr_routes/tests/mod.rs b/src/backend/src/ssr_routes/tests/mod.rs new file mode 100644 index 0000000..2065925 --- /dev/null +++ b/src/backend/src/ssr_routes/tests/mod.rs @@ -0,0 +1,2 @@ +#[cfg(test)] +pub mod login_test; diff --git a/src/cli/cmds/cmd_list.rs b/src/cli/cmds/cmd_list.rs new file mode 100644 index 0000000..0600746 --- /dev/null +++ b/src/cli/cmds/cmd_list.rs @@ -0,0 +1,33 @@ +#[derive(Debug, PartialEq)] +pub enum CliCmds { + Help, + SyncGitHooks, + RemoveGitHooks, + CreateToml, + Interactive, + SystemCheck, + Run, + Build, + Test, + Serve, + Coverage, +} + +pub fn check_for_cli_cmds(args: &Vec) -> CliCmds { + for arg in args { + match arg.as_str() { + s if s.starts_with("--help") => return CliCmds::Help, + s if s.starts_with("--sync-git-hooks") => return CliCmds::SyncGitHooks, + s if s.starts_with("--create-toml") => return CliCmds::CreateToml, + s if s.starts_with("--interactive") => return CliCmds::Interactive, + s if s.starts_with("--system-check") => return CliCmds::SystemCheck, + s if s.starts_with("--remove-git-hooks") => return CliCmds::RemoveGitHooks, + s if s.starts_with("--build") => return CliCmds::Build, + s if s.starts_with("--test") => return CliCmds::Test, + s if s.starts_with("--coverage") => return CliCmds::Coverage, + s if s.starts_with("--serve") => return CliCmds::Serve, + _ => continue, + } + } + CliCmds::Run +} diff --git a/src/cli/cmds/execute_cmd.rs b/src/cli/cmds/execute_cmd.rs new file mode 100644 index 0000000..0cf9559 --- /dev/null +++ b/src/cli/cmds/execute_cmd.rs @@ -0,0 +1,83 @@ +use crate::cli::{ + config::{get_config::ASTROX_TOML, toml::create_toml_file}, + pre_run::{ + system_checks::run_system_checks, + utils::git_hooks::{copy_git_hooks, remove_git_hooks}, + }, + production::{build_production::execute_build, start_production::execute_serve}, + tests::execute::{execute_coverage, execute_tests}, + utils::terminal::{help, step}, +}; + +use super::{ + cmd_list::{check_for_cli_cmds, CliCmds}, + interactive::{start_interactive, InquireUserInput, RealCommandExecutor}, +}; + +pub fn execute_cmd(args: &Vec) { + let cmd = check_for_cli_cmds(args); + if cmd != CliCmds::Run { + match cmd { + CliCmds::Help => { + help(); + std::process::exit(0); + } + CliCmds::SyncGitHooks => { + // Copy the git hooks to the .git/hooks folder + // Enjoy pre-commit, pre-push and commit-msg hooks that will help you to maintain the code quality + step("Syncing the git hooks"); + copy_git_hooks(); + std::process::exit(0); + } + CliCmds::RemoveGitHooks => { + // Remove the git hooks from the .git/hooks folder + // This will remove the pre-commit, pre-push and commit-msg hooks + step("Removing the git hooks"); + remove_git_hooks(); + std::process::exit(0); + } + CliCmds::CreateToml => { + create_toml_file(ASTROX_TOML.to_string()) + .expect("Failed to create Astrox.toml file"); + std::process::exit(0); + } + CliCmds::Interactive => start_interactive(&InquireUserInput, &RealCommandExecutor), + CliCmds::SystemCheck => { + run_system_checks("dev"); + std::process::exit(0); + } + CliCmds::Build => { + step("Building the project"); + execute_build(); + std::process::exit(0); + } + CliCmds::Test => { + step("Testing the project"); + execute_tests(); + std::process::exit(0); + } + CliCmds::Serve => { + step("Serving the project"); + execute_serve(); + std::process::exit(0); + } + CliCmds::Coverage => { + step("Running rust the coverage"); + execute_coverage(); + std::process::exit(0); + } + CliCmds::Run => {} + } + } +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_execute_cmd_system_check() { + let args = vec!["--system-check".to_string()]; + execute_cmd(&args); + // Add assertions here to verify the expected behavior + } +} diff --git a/src/cli/cmds/interactive.rs b/src/cli/cmds/interactive.rs new file mode 100644 index 0000000..fb3ba22 --- /dev/null +++ b/src/cli/cmds/interactive.rs @@ -0,0 +1,57 @@ +use super::execute_cmd::execute_cmd; +use inquire::Select; + +pub trait UserInput { + fn select(&self, prompt: &str, options: Vec<&str>) -> String; +} + +pub struct InquireUserInput; + +impl UserInput for InquireUserInput { + fn select(&self, prompt: &str, options: Vec<&str>) -> String { + let select = Select::new(prompt, options); + select.prompt().unwrap_or("Run").to_string() + } +} +pub trait CommandExecutor { + fn execute_command(&self, command: &str); +} + +pub struct RealCommandExecutor; + +impl CommandExecutor for RealCommandExecutor { + fn execute_command(&self, command: &str) { + execute_cmd(&vec![command.to_string()]); + } +} + +pub fn start_interactive(user_input: &dyn UserInput, command_executor: &dyn CommandExecutor) { + let options = vec![ + "Run", + "Build", + "Serve", + "Test", + "Coverage", + "Create toml file", + "Sync git hooks", + "Remove git hooks", + "System check", + ]; + + let selected = user_input.select("Select a command to run", options); + + let cmd = match selected.as_str() { + "Sync git hooks" => "--sync-git-hooks", + "Remove git hooks" => "--remove-git-hooks", + "Create toml file" => "--create-toml", + "System check" => "--system-check", + "Run" => "--run", + "Build" => "--build", + "Serve" => "--serve", + "Test" => "--test", + "Coverage" => "--coverage", + _ => "--run", + }; + + command_executor.execute_command(cmd); +} diff --git a/src/cli/cmds/mod.rs b/src/cli/cmds/mod.rs new file mode 100644 index 0000000..fe074af --- /dev/null +++ b/src/cli/cmds/mod.rs @@ -0,0 +1,6 @@ +pub mod cmd_list; +pub mod execute_cmd; +pub mod interactive; + +#[cfg(test)] +mod tests; diff --git a/src/cli/cmds/tests/cmd_list_test.rs b/src/cli/cmds/tests/cmd_list_test.rs new file mode 100644 index 0000000..e39d8b6 --- /dev/null +++ b/src/cli/cmds/tests/cmd_list_test.rs @@ -0,0 +1,98 @@ +#[cfg(test)] +mod tests { + use crate::cli::cmds::cmd_list::{check_for_cli_cmds, CliCmds}; + + #[test] + fn test_check_for_cli_cmds_help() { + let args = vec!["--help".to_string()]; + assert_eq!(check_for_cli_cmds(&args), CliCmds::Help); + } + + #[test] + fn test_check_for_cli_cmds_build() { + let args = vec!["--build".to_string()]; + assert_eq!(check_for_cli_cmds(&args), CliCmds::Build); + } + + #[test] + fn test_check_for_cli_cmds_serve() { + let args = vec!["--serve".to_string()]; + assert_eq!(check_for_cli_cmds(&args), CliCmds::Serve); + } + + #[test] + fn test_check_for_cli_cmds_test() { + let args = vec!["--test".to_string()]; + assert_eq!(check_for_cli_cmds(&args), CliCmds::Test); + } + + #[test] + fn test_check_for_cli_cmds_sync_git_hooks() { + let args = vec!["--sync-git-hooks".to_string()]; + assert_eq!(check_for_cli_cmds(&args), CliCmds::SyncGitHooks); + } + + #[test] + fn test_check_for_cli_cmds_create_toml() { + let args = vec!["--create-toml".to_string()]; + assert_eq!(check_for_cli_cmds(&args), CliCmds::CreateToml); + } + + #[test] + fn test_check_for_cli_cmds_interactive() { + let args = vec!["--interactive".to_string()]; + assert_eq!(check_for_cli_cmds(&args), CliCmds::Interactive); + } + + #[test] + fn test_check_for_cli_cmds_system_check() { + let args = vec!["--system-check".to_string()]; + assert_eq!(check_for_cli_cmds(&args), CliCmds::SystemCheck); + } + + #[test] + fn test_check_for_cli_cmds_remove() { + let args = vec!["--remove-git-hooks".to_string()]; + assert_eq!(check_for_cli_cmds(&args), CliCmds::RemoveGitHooks); + } + + #[test] + fn test_check_for_cli_cmds_coverage() { + let args = vec!["--coverage".to_string()]; + assert_eq!(check_for_cli_cmds(&args), CliCmds::Coverage); + } + + #[test] + fn test_check_for_cli_cmds_run() { + let args = vec!["--invalid-arg".to_string()]; + assert_eq!(check_for_cli_cmds(&args), CliCmds::Run); + } + + #[test] + fn test_check_for_cli_cmds_multiple_args_invalid() { + let args = vec!["--invalid-arg".to_string(), "--invalid-arg".to_string()]; + assert_eq!(check_for_cli_cmds(&args), CliCmds::Run); + } + + #[test] + fn test_check_for_cli_cmds_none() { + let args = vec![]; + assert_eq!(check_for_cli_cmds(&args), CliCmds::Run); + } + + #[test] + fn test_check_for_cli_cmds_multiple_args() { + let args = vec!["--invalid-arg".to_string(), "--help".to_string()]; + assert_eq!(check_for_cli_cmds(&args), CliCmds::Help); + } + + #[test] + fn test_check_if_help_always_executed() { + let mut args = vec!["--invalid-arg".to_string()]; + assert_eq!(check_for_cli_cmds(&args), CliCmds::Run); + args.push("--help".to_string()); + assert_eq!(check_for_cli_cmds(&args), CliCmds::Help); + args.push("--create-toml".to_string()); + assert_eq!(check_for_cli_cmds(&args), CliCmds::Help); + } +} diff --git a/src/cli/cmds/tests/interactive_test.rs b/src/cli/cmds/tests/interactive_test.rs new file mode 100644 index 0000000..4e6d486 --- /dev/null +++ b/src/cli/cmds/tests/interactive_test.rs @@ -0,0 +1,147 @@ +use std::cell::RefCell; + +use crate::cli::cmds::interactive::{CommandExecutor, UserInput}; + +struct MockUserInput { + responses: RefCell>, +} + +impl MockUserInput { + fn new(responses: Vec) -> Self { + Self { + responses: RefCell::new(responses), + } + } +} + +impl UserInput for MockUserInput { + fn select(&self, _prompt: &str, _options: Vec<&str>) -> String { + self.responses.borrow_mut().remove(0) + } +} + +struct MockCommandExecutor { + executed_commands: RefCell>, +} + +impl MockCommandExecutor { + fn new() -> Self { + Self { + executed_commands: RefCell::new(vec![]), + } + } +} + +impl CommandExecutor for MockCommandExecutor { + fn execute_command(&self, command: &str) { + self.executed_commands + .borrow_mut() + .push(command.to_string()); + } +} + +#[cfg(test)] +mod tests { + use crate::cli::cmds::interactive::start_interactive; + + use super::*; + + #[test] + fn test_start_interactive_run() { + let mock_input = MockUserInput::new(vec!["Run".to_string()]); + let mock_executor = MockCommandExecutor::new(); + start_interactive(&mock_input, &mock_executor); + // Add assertions to verify the behavior + let executed_commands = mock_executor.executed_commands.borrow(); + assert_eq!(executed_commands.len(), 1); + assert_eq!(executed_commands[0], "--run"); + } + + #[test] + fn test_start_interactive_build() { + let mock_input = MockUserInput::new(vec!["Build".to_string()]); + let mock_executor = MockCommandExecutor::new(); + start_interactive(&mock_input, &mock_executor); + + let executed_commands = mock_executor.executed_commands.borrow(); + assert_eq!(executed_commands.len(), 1); + assert_eq!(executed_commands[0], "--build"); + } + + #[test] + fn test_start_interactive_serve() { + let mock_input = MockUserInput::new(vec!["Serve".to_string()]); + let mock_executor = MockCommandExecutor::new(); + start_interactive(&mock_input, &mock_executor); + + let executed_commands = mock_executor.executed_commands.borrow(); + assert_eq!(executed_commands.len(), 1); + assert_eq!(executed_commands[0], "--serve"); + } + + #[test] + fn test_start_interactive_test() { + let mock_input = MockUserInput::new(vec!["Test".to_string()]); + let mock_executor = MockCommandExecutor::new(); + start_interactive(&mock_input, &mock_executor); + + let executed_commands = mock_executor.executed_commands.borrow(); + assert_eq!(executed_commands.len(), 1); + assert_eq!(executed_commands[0], "--test"); + } + + #[test] + fn test_start_interactive_create_toml_file() { + let mock_input = MockUserInput::new(vec!["Create toml file".to_string()]); + let mock_executor = MockCommandExecutor::new(); + start_interactive(&mock_input, &mock_executor); + + let executed_commands = mock_executor.executed_commands.borrow(); + assert_eq!(executed_commands.len(), 1); + assert_eq!(executed_commands[0], "--create-toml"); + } + + #[test] + fn test_start_interactive_sync_git_hooks() { + let mock_input = MockUserInput::new(vec!["Sync git hooks".to_string()]); + let mock_executor = MockCommandExecutor::new(); + start_interactive(&mock_input, &mock_executor); + + let executed_commands = mock_executor.executed_commands.borrow(); + assert_eq!(executed_commands.len(), 1); + assert_eq!(executed_commands[0], "--sync-git-hooks"); + } + + #[test] + fn test_start_interactive_remove_git_hooks() { + let mock_input = MockUserInput::new(vec!["Remove git hooks".to_string()]); + let mock_executor = MockCommandExecutor::new(); + start_interactive(&mock_input, &mock_executor); + + let executed_commands = mock_executor.executed_commands.borrow(); + assert_eq!(executed_commands.len(), 1); + assert_eq!(executed_commands[0], "--remove-git-hooks"); + } + + #[test] + fn test_start_interactive_coverage() { + let mock_input = MockUserInput::new(vec!["Coverage".to_string()]); + let mock_executor = MockCommandExecutor::new(); + start_interactive(&mock_input, &mock_executor); + + let executed_commands = mock_executor.executed_commands.borrow(); + assert_eq!(executed_commands.len(), 1); + assert_eq!(executed_commands[0], "--coverage"); + } + + #[test] + fn test_start_interactive_system_check() { + let mock_input = MockUserInput::new(vec!["System check".to_string()]); + let mock_executor = MockCommandExecutor::new(); + start_interactive(&mock_input, &mock_executor); + + let executed_commands = mock_executor.executed_commands.borrow(); + assert_eq!(executed_commands.len(), 1); + assert_eq!(executed_commands[0], "--system-check"); + } +} diff --git a/src/cli/cmds/tests/mod.rs b/src/cli/cmds/tests/mod.rs new file mode 100644 index 0000000..8a97676 --- /dev/null +++ b/src/cli/cmds/tests/mod.rs @@ -0,0 +1,4 @@ +#[cfg(test)] +mod cmd_list_test; +#[cfg(test)] +mod interactive_test; diff --git a/src/cli/config/collect_args.rs b/src/cli/config/collect_args.rs new file mode 100644 index 0000000..6c6cf8d --- /dev/null +++ b/src/cli/config/collect_args.rs @@ -0,0 +1,140 @@ +/// This module contains functions for collecting and parsing configuration arguments. +/// +/// The `split_and_collect` function takes a string argument and splits it by the '=' character. +/// It returns the second part of the split string as a `String`. +/// +/// The `parse_to_bool` function takes a string argument and matches it against "true" and "false". +/// It returns a boolean value based on the match. +/// +/// The `collect_config_args` function takes a `Config` struct and a vector of string arguments. +/// It iterates over the arguments and updates the corresponding fields in the `Config` struct based on the argument prefix. +/// It uses the `split_and_collect` and `parse_to_bool` functions to extract and parse the values. It returns the updated `Config` struct. +/// +/// The module also contains unit tests for the `split_and_collect`, `parse_to_bool`, and `collect_config_args` functions. +/// +/// List of config arguments +/// Bind actix server to a host, used for development and production +/// --host=127.0.0.1 +/// Bind actix server to a port, used for development and production +/// --port=8080 +/// Set the environment +/// --env=prod / dev +/// Set the astro development port, in production actix server will serve the frontend build Files +/// --astro-port=4321 +/// Switch on / off the production build of the frontend during the production server start +/// --prod-astro-build=true / false +/// Set the public api url, this will be copied over to astro frontend and used for grabbing url to set api base +/// During development this value is being copied into the frontend .env file for building the frontend +/// --set-public-api=https://custom.api/api +use super::get_config::Config; + +fn split_and_collect(arg: &str) -> String { + let split: Vec<&str> = arg.split('=').collect(); + if split.len() == 2 { + return split[1].to_string(); + } + "".to_string() +} + +fn parse_to_bool(arg: &str) -> bool { + match arg { + "true" => true, + "false" => false, + _ => false, + } +} + +pub fn collect_config_args(config: Config, args: &Vec) -> Config { + let mut config = config; + + for arg in args { + if arg.starts_with("--env=") { + config.env = split_and_collect(arg); + } + if arg.starts_with("--host=") { + config.host = split_and_collect(arg) + } + if arg.starts_with("--port=") { + config.port = Some(split_and_collect(arg).parse::().unwrap_or_default()); + } + + if arg.starts_with("--astro-port=") { + config.astro_port = Some(split_and_collect(arg).parse::().unwrap_or_default()); + } + + if arg.starts_with("--prod-astro-build=") { + config.prod_astro_build = parse_to_bool(split_and_collect(arg).as_str()); + } + + if arg.starts_with("--public-api-url=") { + config.public_keys.public_api_url = split_and_collect(arg); + } + } + + config +} + +#[cfg(test)] +mod tests { + use crate::cli::config::get_config::PublicKeys; + + use super::*; + + #[test] + fn test_split_and_collect() { + assert_eq!(split_and_collect("--env=prod"), "prod"); + assert_eq!(split_and_collect("--host=127.0.0.1"), "127.0.0.1"); + assert_eq!(split_and_collect("--port=8080"), "8080"); + assert_eq!(split_and_collect("--astro-port=4321"), "4321"); + assert_eq!(split_and_collect("--prod-astro-build=true"), "true"); + assert_eq!(split_and_collect("--invalid-arg"), ""); + } + + #[test] + fn test_parse_to_bool() { + assert_eq!(parse_to_bool("true"), true); + assert_eq!(parse_to_bool("false"), false); + assert_eq!(parse_to_bool("invalid"), false); + } + + #[test] + fn test_collect_config_args() { + let config = Config { + env: "".to_string(), + host: "".to_string(), + port: None, + astro_port: None, + prod_astro_build: false, + cors_url: "".to_string(), + public_keys: { + let public_api_url = "http://localhost:8080/api".to_string(); + PublicKeys { public_api_url } + }, + }; + + let args = vec![ + "--env=prod".to_string(), + "--host=127.0.0.1".to_string(), + "--port=8080".to_string(), + "--cors_url=http://localhost:8080".to_string(), + "--astro-port=4321".to_string(), + "--prod-astro-build=true".to_string(), + "--public-api-url=https://custom.api/api".to_string(), + ]; + + let expected_config = Config { + env: "prod".to_string(), + host: "127.0.0.1".to_string(), + port: Some(8080), + astro_port: Some(4321), + cors_url: "http://localhost:8080".to_string(), + prod_astro_build: true, + public_keys: { + let public_api_url = "https://custom.api/api".to_string(); + PublicKeys { public_api_url } + }, + }; + + assert_eq!(collect_config_args(config, &args), expected_config); + } +} diff --git a/src/cli/config/create_dotenv.rs b/src/cli/config/create_dotenv.rs new file mode 100644 index 0000000..a080ccf --- /dev/null +++ b/src/cli/config/create_dotenv.rs @@ -0,0 +1,162 @@ +/// Creates or updates a `.env` file with the specified API URL. +/// +/// # Arguments +/// +/// * `api_url` - The API URL to be set in the `.env` file. +/// * `dotenv_path` - The path to the `.env` file. +/// +/// # Examples +/// +/// ``` +/// use std::fs::File; +/// use std::io::Read; +/// use std::io::Write; +/// use std::path::Path; +/// +/// fn replace_value(contents: &String, key: &str, new_value: &str) -> String { +/// // implementation details omitted +/// } +/// +/// #[cfg(test)] +/// mod tests { +/// use super::*; +/// +/// #[test] +/// fn test_create_dotenv_frontend_new_file() { +/// // test implementation omitted +/// } +/// +/// #[test] +/// fn test_create_dotenv_frontend_existing_file() { +/// // test implementation omitted +/// } +/// +/// #[test] +/// fn test_replace_value_existing_key() { +/// // test implementation omitted +/// } +/// +/// #[test] +/// fn test_replace_value_non_existing_key() { +/// // test implementation omitted +/// } +/// } +/// ``` +use std::fs::File; +use std::io::Read; +use std::io::Write; +use std::path::Path; + +pub fn create_dotenv_frontend(api_url: &str, dotenv_path: &str) { + let dotenv_exists: bool = Path::new(dotenv_path).exists(); + if dotenv_exists { + match File::open(dotenv_path) { + Ok(mut file) => { + let mut contents = String::new(); + if let Err(err) = file.read_to_string(&mut contents) { + eprintln!("Error reading file: {}", err) + } + let new_contents = replace_value(&contents, "PUBLIC_API_URL=", api_url); + if let Err(err) = File::create(dotenv_path) + .and_then(|mut file| file.write_all(new_contents.as_bytes())) + { + eprintln!("Error writing to file: {}", err); + } + } + Err(err) => { + eprintln!("Error opening file: {}", err) + } + } + } else { + match File::create(dotenv_path) { + Ok(mut file) => { + if let Err(err) = file.write_all(format!("PUBLIC_API_URL={}", api_url).as_bytes()) { + eprintln!("Error writing to file: {}", err) + } + } + Err(err) => { + eprintln!("Error creating file: {}", err) + } + } + } +} + +fn replace_value(contents: &String, key: &str, new_value: &str) -> String { + if let Some(index) = contents.find(key) { + let (_, old_value) = contents.split_at(index + key.len()); + contents.replace(old_value, new_value) + } else { + contents.to_string() + } +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_create_dotenv_frontend_new_file() { + let api_url = "https://api.example.com"; + let dotenv_path = "./src/frontend/.test-new-env"; + + // Create a temporary file for testing + create_dotenv_frontend(api_url, &dotenv_path); + + // Read the contents of the temporary file + let mut file = File::open(dotenv_path).unwrap(); + let mut contents = String::new(); + file.read_to_string(&mut contents).unwrap(); + + // Assert that the file has been created and contains the correct value + assert_eq!(contents, format!("PUBLIC_API_URL={}", api_url)); + + // remove the temporary file + std::fs::remove_file(dotenv_path).unwrap(); + } + + #[test] + fn test_create_dotenv_frontend_existing_file() { + let api_url = "https://api.example.com"; + let dotenv_path = "./src/frontend/.test-exist-env"; + + // Create a temporary file for testing + let mut file = File::create(&dotenv_path).unwrap(); + file.write_all("PUBLIC_API_URL=old_value".as_bytes()) + .unwrap(); + + // Update the file with the new value + create_dotenv_frontend(api_url, &dotenv_path); + + // Read the contents of the temporary file + let mut file = File::open(dotenv_path).unwrap(); + let mut contents = String::new(); + file.read_to_string(&mut contents).unwrap(); + + // Assert that the file has been updated with the new value + assert_eq!(contents, format!("PUBLIC_API_URL={}", api_url)); + + // remove the temporary file + std::fs::remove_file(dotenv_path).unwrap(); + } + + #[test] + fn test_replace_value_existing_key() { + let contents = String::from("PUBLIC_API_URL=old_value"); + let key = "PUBLIC_API_URL="; + let new_value = "https://api.example.com"; + + let result = replace_value(&contents, key, new_value); + + assert_eq!(result, format!("PUBLIC_API_URL={}", new_value)); + } + + #[test] + fn test_replace_value_non_existing_key() { + let contents = String::from("OTHER_KEY=value"); + let key = "PUBLIC_API_URL="; + let new_value = "https://api.example.com"; + + let result = replace_value(&contents, key, new_value); + + assert_eq!(result, contents); + } +} diff --git a/src/cli/config/get_config.rs b/src/cli/config/get_config.rs new file mode 100644 index 0000000..838bccb --- /dev/null +++ b/src/cli/config/get_config.rs @@ -0,0 +1,48 @@ +use super::collect_args::collect_config_args; +use super::toml::read_toml; +use serde::Deserialize; +use serde::Serialize; + +#[derive(Deserialize, Debug, Serialize, PartialEq)] +pub struct Config { + pub host: String, + pub port: Option, + pub env: String, + pub astro_port: Option, + pub cors_url: String, + pub prod_astro_build: bool, + pub public_keys: PublicKeys, +} + +#[derive(Deserialize, Debug, Serialize, PartialEq)] +pub struct PublicKeys { + pub public_api_url: String, +} + +pub fn get_config(args: &Vec) -> Config { + // create default config + + let astro_port = 5432; + let cors_url = format!("http://localhost:{}", astro_port); + + let mut config: Config = Config { + host: "localhost".to_string(), + port: Some(8080), + env: "dev".to_string(), + prod_astro_build: false, + astro_port: Some(astro_port), + cors_url, + public_keys: PublicKeys { + public_api_url: "http://localhost:8080/api".to_string(), + }, + }; + + if let Ok(toml) = read_toml(&ASTROX_TOML.to_string()) { + config = toml; + } + + config = collect_config_args(config, args); + config +} + +pub const ASTROX_TOML: &str = "Astrox.toml"; diff --git a/src/cli/config/mod.rs b/src/cli/config/mod.rs new file mode 100644 index 0000000..1c95989 --- /dev/null +++ b/src/cli/config/mod.rs @@ -0,0 +1,7 @@ +pub mod collect_args; +pub mod create_dotenv; +pub mod get_config; +pub mod toml; + +#[cfg(test)] +mod tests; diff --git a/src/cli/config/tests/get_config_test.rs b/src/cli/config/tests/get_config_test.rs new file mode 100644 index 0000000..e04662e --- /dev/null +++ b/src/cli/config/tests/get_config_test.rs @@ -0,0 +1,56 @@ +#[cfg(test)] +mod tests { + use crate::cli::config::get_config::get_config; + + #[test] + fn test_get_config_with_default_values() { + let args: Vec = vec![]; + let config = get_config(&args); + + assert_eq!(config.host, "localhost"); + assert_eq!(config.port, Some(8080)); + assert_eq!(config.env, "dev"); + assert_eq!(config.astro_port, Some(5431)); //overide from projects toml + assert_eq!(config.prod_astro_build, true); //overide from projects toml + assert_eq!( + config.public_keys.public_api_url, + "http://localhost:8080/api" + ); + } + + #[test] + fn test_get_config_with_custom_values() { + let args: Vec = vec![ + "--host=example.com".to_string(), + "--port=8000".to_string(), + "--env=prod".to_string(), + "--astro-port=5431".to_string(), + "--prod-astro-build=false".to_string(), + "--public-api-url=https://api.example.com".to_string(), + ]; + let config = get_config(&args); + + assert_eq!(config.host, "example.com"); + assert_eq!(config.port, Some(8000)); + assert_eq!(config.env, "prod"); + assert_eq!(config.astro_port, Some(5431)); + assert_eq!(config.prod_astro_build, false); + assert_eq!(config.public_keys.public_api_url, "https://api.example.com"); + } + + #[test] + fn test_get_config_with_invalid_args() { + let args: Vec = vec!["--invalid-arg".to_string()]; + let config = get_config(&args); + + assert_eq!(config.host, "localhost"); + assert_eq!(config.port, Some(8080)); + assert_eq!(config.env, "dev"); + assert_eq!(config.astro_port, Some(5431)); + assert_eq!(config.prod_astro_build, true); + assert_eq!( + config.public_keys.public_api_url, + "http://localhost:8080/api" + ); + } +} diff --git a/src/cli/config/tests/mod.rs b/src/cli/config/tests/mod.rs new file mode 100644 index 0000000..a82bf91 --- /dev/null +++ b/src/cli/config/tests/mod.rs @@ -0,0 +1,2 @@ +#[cfg(test)] +mod get_config_test; diff --git a/src/cli/config/toml.rs b/src/cli/config/toml.rs new file mode 100644 index 0000000..3f64990 --- /dev/null +++ b/src/cli/config/toml.rs @@ -0,0 +1,156 @@ +use super::get_config::{Config, PublicKeys}; +use crate::cli::utils::terminal::{error, spacer, step, success, warning}; + +pub fn read_toml(filename: &String) -> Result { + let toml_str = std::fs::read_to_string(filename); + + match toml_str { + Ok(toml_str) => { + success("Astrox.toml found"); + + let config = toml::from_str(&toml_str); + match config { + Ok(config) => { + step("loaded Astrox.toml"); + Ok(config) + } + Err(e) => { + error("Failed to parse Astrox.toml"); + error(e.to_string().as_str()); + spacer(); + Err(()) + } + } + } + Err(_) => { + warning("Astrox.toml not found"); + spacer(); + Err(()) + } + } +} + +pub fn create_toml_file(file_name: String) -> Result { + // check if the toml.file exits abort if it does, panic and exit + + if std::fs::metadata(&file_name).is_ok() { + error("Astrox.toml already exists"); + spacer(); + return Err(()); + } + + let astro_port = 5431; + let cors_url = format!("http://localhost:{}", astro_port); + + let config = Config { + host: "localhost".to_string(), + port: Some(8080), + env: "dev".to_string(), + astro_port: Some(astro_port), + cors_url, + prod_astro_build: true, + public_keys: { + let public_api_url = "http://localhost:8080/api".to_string(); + PublicKeys { public_api_url } + }, + }; + + let toml_str = toml::to_string(&config); + + match toml_str { + Ok(toml_str) => match std::fs::write(file_name, toml_str) { + Ok(_) => { + success("Astrox.toml created"); + Ok(config) + } + Err(e) => { + error("Failed to create Astrox.toml"); + error(e.to_string().as_str()); + spacer(); + Err(()) + } + }, + Err(e) => { + error("Failed to create Astrox.toml"); + error(e.to_string().as_str()); + spacer(); + Err(()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn remove_file(file_name: &String) -> () { + match std::fs::remove_file(&file_name) { + Ok(_) => (), + Err(e) => println!("Error: {}", e), + } + } + + #[test] + fn test_read_toml_success() { + // Arrange + let expected_config = Config { + host: "127.0.0.1".to_string(), + port: Some(8080), + env: "dev".to_string(), + astro_port: Some(5431), + cors_url: "https://astrox.spaceout.pl".to_string(), + prod_astro_build: true, + public_keys: { + let public_api_url = "http://localhost:8080/api".to_string(); + PublicKeys { public_api_url } + }, + }; + + let file_name: String = "Astrox-test.toml".to_string(); + + let toml_str = toml::to_string(&expected_config).unwrap(); + std::fs::write(&file_name, toml_str).unwrap(); + + // Act + let result = read_toml(&file_name); + + // Assert + assert!(result.is_ok()); + let config = result.unwrap(); + assert!(config.host == expected_config.host); + assert!(config.port == expected_config.port); + assert!(config.env == expected_config.env); + assert!(config.astro_port == expected_config.astro_port); + assert!(config.prod_astro_build == expected_config.prod_astro_build); + assert!(config.cors_url == expected_config.cors_url); + + // delete file after test completion + remove_file(&file_name) + } + + #[test] + fn test_read_toml_file_not_found() { + let file_name: String = "Astrox-not.toml".to_string(); + let result = read_toml(&file_name); + + // Assert + assert!(result.is_err()); + } + + #[test] + fn test_read_toml_parse_error() { + let file_name: String = "Astrox-error.toml".to_string(); + std::fs::write("Astrox-test.toml", "invalid toml").unwrap(); + let result = read_toml(&file_name); + assert!(result.is_err()); + remove_file(&file_name); + } + + #[test] + fn test_create_toml_file() { + let file_name: String = "Astrox-test-write.toml".to_string(); + let result = create_toml_file(file_name.clone()); + assert!(result.is_ok()); + remove_file(&file_name); + } +} diff --git a/src/cli/development/mod.rs b/src/cli/development/mod.rs new file mode 100644 index 0000000..272cc4a --- /dev/null +++ b/src/cli/development/mod.rs @@ -0,0 +1 @@ +pub mod start_development; diff --git a/src/cli/development/start_development.rs b/src/cli/development/start_development.rs new file mode 100644 index 0000000..1f4f5f4 --- /dev/null +++ b/src/cli/development/start_development.rs @@ -0,0 +1,179 @@ +use crate::cli::config::create_dotenv::create_dotenv_frontend; +use crate::cli::config::get_config::Config; +use crate::cli::pre_run::npm::checks::NPM; +use crate::cli::utils::terminal::{dev_info, do_front_log, do_server_log, step, success, warning}; +use ctrlc::set_handler; +use std::io::Read; +use std::process::Command; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread::sleep; +use std::time::Duration; + +/// Start the development server +/// The development server will start the actix backend server and the astro frontend server +/// The development server will also check if the port is available for the backend server, and loop until it finds the available port +/// The development server will also clean up the orphaned processes, otherwise cargo watch and node watch will continue to run, blocking the ports. + +pub fn start_development(config: Config) { + // Set the ctrl-c handler to exit the program and clean up orphaned processes + let running = Arc::new(AtomicBool::new(true)); + let r = running.clone(); + + set_handler(move || { + r.store(false, Ordering::SeqCst); + }) + .expect("Error setting Ctrl-C handler"); + + // Check if the port is available for the backend server + let mut port = config.port.unwrap_or(8080); + let mut astro_port = config.astro_port.unwrap_or(5431); + let mut rust_port_listener = std::net::TcpListener::bind(format!("{}:{}", config.host, port)); + let mut astro_port_listener = + std::net::TcpListener::bind(format!("{}:{}", config.host, astro_port)); + + // Loop until you find the port that is available + + while rust_port_listener.is_err() { + warning(format!("Port {} is not available", port).as_str()); + port += 1; + rust_port_listener = std::net::TcpListener::bind(format!("{}:{}", config.host, port)); + } + + // kill the listener + drop(rust_port_listener); + + while astro_port_listener.is_err() { + warning(format!("Port {} is not available", astro_port).as_str()); + astro_port += 1; + astro_port_listener = + std::net::TcpListener::bind(format!("{}:{}", config.host, astro_port)); + } + + // kill the listener + drop(astro_port_listener); + + // Crate the host env for astro to call the actix backend server + create_dotenv_frontend( + &format!("http://{}:{}/api/", config.host, port), + "./src/frontend/.env", + ); + + // Start the backend development server + + step("Start the actix backend development server"); + let mut cargo_watch = Command::new("cargo") + .current_dir("./src/backend") + .arg("watch") + .arg("-w") + .arg("./src") + .arg("-x") + .arg(format!("run -- --host={} --port={}", config.host, port)) + .stdout(std::process::Stdio::piped()) + .spawn() + .expect("Failed to start backend development server"); + + // Wait for the backend development server to start + + let mut buffer_rust = [0; 1024]; + let mut stdout_rust = cargo_watch.stdout.take().unwrap(); + loop { + let n = stdout_rust.read(&mut buffer_rust).unwrap(); + if n == 0 { + break; + } + let s = String::from_utf8_lossy(&buffer_rust[..n]); + + do_server_log(&s); + if s.contains("Actix server has started 🚀") { + dev_info(&config.host, &port); + success("Actix server is running, starting the frontend development server"); + break; + } + } + + // Start the frontend development server + step("Starting astro frontend development server"); + + let mut node_watch = Command::new(NPM) + .arg("run") + .arg("start") + .arg("--") + .arg("--port") + .arg(astro_port.to_string()) + .stdout(std::process::Stdio::piped()) + .current_dir("./src/frontend") + .spawn() + .expect("Failed to start frontend development server"); + + // Watch the std output of astro bundle if std will have "ready" then open the browser to the development server + + let mut buffer_node = [0; 1024]; + let mut stdout_node = node_watch.stdout.take().unwrap(); + loop { + let n = stdout_node.read(&mut buffer_node).unwrap(); + if n == 0 { + break; + } + let s = String::from_utf8_lossy(&buffer_node[..n]); + + do_front_log(&s); + + if s.contains("ready") { + success("Astro is ready, opening the browser"); + + let browser = Command::new("open") + .arg(format!("http://localhost:{}", astro_port)) + .spawn(); + + match browser { + Ok(_) => break, + Err(err) => { + println!("Failed to execute command: {}", err); + println!("Are You a Ci Secret Agent ?"); + + // Handle the error here + } + } + + break; + } + } + + // We want to transmit the stdout_node and stdout_rust as long as both watchers are present + + loop { + let n = stdout_rust.read(&mut buffer_rust).unwrap(); + if n == 0 { + break; + } + let s = String::from_utf8_lossy(&buffer_rust[..n]); + do_server_log(&s); + } + + loop { + let n = stdout_node.read(&mut buffer_node).unwrap(); + if n == 0 { + break; + } + let s = String::from_utf8_lossy(&buffer_node[..n]); + do_front_log(&s); + } + + // Clean up section for orphaned processes, otherwise cargo watch and node watch will continue to run blocking the ports + while running.load(Ordering::SeqCst) { + sleep(Duration::from_millis(100)); + } + step("Cleaning up orphaned processes"); + + cargo_watch + .kill() + .expect("Failed to kill cargo-watch process"); + node_watch + .kill() + .expect("Failed to kill node-watch process"); + + step("Exiting"); + + std::process::exit(0); +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000..5ee8b3e --- /dev/null +++ b/src/cli/mod.rs @@ -0,0 +1,7 @@ +pub mod cmds; +pub mod config; +pub mod development; +pub mod pre_run; +pub mod production; +pub mod tests; +pub mod utils; diff --git a/src/cli/pre_run/cargo/checks.rs b/src/cli/pre_run/cargo/checks.rs new file mode 100644 index 0000000..da9c61a --- /dev/null +++ b/src/cli/pre_run/cargo/checks.rs @@ -0,0 +1,62 @@ +use std::process::Command; + +use crate::cli::pre_run::utils::check_semver::check_semver; + +/// Check if cargo-watch is installed +/// cargo-watch is required to spy on changes to the actix server +pub fn is_cargo_watch_installed() -> bool { + let output = Command::new("cargo") + .arg("install") + .arg("--list") + .output() + .expect("Failed to execute command"); + + let output_str = String::from_utf8_lossy(&output.stdout); + output_str.contains("cargo-watch") +} + +/// Check if commitlint-rs is installed +/// commitlint-rs is required to lint the commit messages in development env +pub fn is_commitlint_rs_installed() -> bool { + let output = Command::new("cargo") + .arg("install") + .arg("--list") + .output() + .expect("Failed to execute command"); + + let output_str = String::from_utf8_lossy(&output.stdout); + output_str.contains("commitlint-rs") +} + +/// Check if llvm-cov is installed +/// llvm-cov is required to generate the coverage report +/// llvm-cov is used in the coverage command + +pub fn is_llvm_cov_installed() -> bool { + let output = Command::new("cargo") + .arg("llvm-cov") + .arg("--version") + .output() + .expect("Failed to execute command"); + + let output_str = String::from_utf8_lossy(&output.stdout); + output_str.contains("cargo-llvm-cov") +} + +pub const REQUIRED_VERSION: &str = "1.74.0"; + +pub fn is_rustc_higher_than_required() -> bool { + let output = Command::new("rustc") + .arg("--version") + .output() + .expect("Failed to execute command"); + + let output_str = String::from_utf8_lossy(&output.stdout); + let mut version = output_str.trim(); + + // output is like "rustc 1.74.0 (d1dfba2c9 2021-07-26)" + // we need to extract the version + version = version.split_whitespace().collect::>()[1]; + + check_semver(version, REQUIRED_VERSION) +} diff --git a/src/cli/pre_run/cargo/mod.rs b/src/cli/pre_run/cargo/mod.rs new file mode 100644 index 0000000..1a666ae --- /dev/null +++ b/src/cli/pre_run/cargo/mod.rs @@ -0,0 +1,2 @@ +pub mod checks; +pub mod validate; diff --git a/src/cli/pre_run/cargo/validate.rs b/src/cli/pre_run/cargo/validate.rs new file mode 100644 index 0000000..c74ce9e --- /dev/null +++ b/src/cli/pre_run/cargo/validate.rs @@ -0,0 +1,168 @@ +use crate::cli::pre_run::cargo::checks::{ + is_cargo_watch_installed, is_commitlint_rs_installed, is_rustc_higher_than_required, + REQUIRED_VERSION, +}; +use crate::cli::utils::terminal::{error, spacer, step, success}; +use inquire::Confirm; +use std::process::Command; + +use super::checks::is_llvm_cov_installed; + +pub fn validate_cargo_watch() { + let is_cargo_watch_installed = is_cargo_watch_installed(); + + match is_cargo_watch_installed { + true => success("cargo-watch is installed"), + false => { + error("cargo-watch is not installed"); + spacer(); + let ans = Confirm::new("Do you want to install cargo-watch ?") + .with_default(false) + .with_help_message("cargo-watch must be installed globally in order to spy on changes to the server") + .prompt(); + + match ans { + Ok(true) => { + spacer(); + step("Installing cargo-watch ..."); + Command::new("cargo") + .arg("install") + .arg("cargo-watch") + .spawn() + .expect("Failed to install cargo-watch") + .wait() + .expect("Failed to install cargo-watch"); + spacer(); + } + Ok(false) => { + error("That's too bad, we have to quit now"); + panic!(); + } + Err(_) => { + error("Error with prompt, about to panic"); + panic!(); + } + } + } + } +} + +pub fn validate_commitlint_rs() { + let is_commitlint_rs_installed = is_commitlint_rs_installed(); + + match is_commitlint_rs_installed { + true => success("commitlint-rs is installed"), + false => { + error("commitlint-rs is not installed"); + spacer(); + let ans = Confirm::new("Do you want to install commitlint-rs ?") + .with_default(false) + .with_help_message("commitlint-rs must be installed globally in order to lint the commit messages, this is the recommended way to go") + .prompt(); + + match ans { + Ok(true) => { + spacer(); + step("Installing commitlint-rs ..."); + Command::new("cargo") + .arg("install") + .arg("commitlint-rs") + .spawn() + .expect("Failed to install commitlint-rs") + .wait() + .expect("Failed to install commitlint-rs"); + spacer(); + } + Ok(false) => { + error("That's too bad, we have to quit now"); + panic!(); + } + Err(_) => { + error("Error with prompt, about to panic"); + panic!(); + } + } + } + } +} + +pub fn validate_llcov() { + let is_llvm_cov_installed = is_llvm_cov_installed(); + + match is_llvm_cov_installed { + true => success("llvm-cov is installed"), + false => { + error("llvm-cov is not installed"); + spacer(); + let ans = Confirm::new("Do you want to install llvm-cov for code coverage reporting ?") + .with_default(false) + .with_help_message("llvm-cov must be installed globally in order to produce rust coverage report, this is the recommended way to go") + .prompt(); + + match ans { + Ok(true) => { + spacer(); + step("Installing llvm-cov ..."); + Command::new("cargo") + .arg("install") + .arg("cargo-llvm-cov") + .spawn() + .expect("Failed to install llvm-cov") + .wait() + .expect("Failed to install llvm-cov"); + spacer(); + } + Ok(false) => { + error("That's too bad, we have to quit now"); + panic!(); + } + Err(_) => { + error("Error with prompt, about to panic"); + panic!(); + } + } + } + } +} + +pub fn validate_rustc_version() { + let is_rustc_higher_than_required = is_rustc_higher_than_required(); + + match is_rustc_higher_than_required { + true => success("Rustc version is higher than required"), + false => { + error("Rustc version is lower than required "); + spacer(); + error(format!("Rustc version must be higher than {}", REQUIRED_VERSION).as_str()); + panic!(); + } + } +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_validate_cargo_watch_installed() { + // Test when cargo-watch is installed + assert_eq!(is_cargo_watch_installed(), true); + } + + #[test] + fn test_validate_commitlint_rs_installed() { + // Test when commitlint-rs is installed + assert_eq!(is_commitlint_rs_installed(), true); + } + + #[test] + fn test_validate_rustc_version_higher() { + // Test when rustc version is higher than required + assert_eq!(is_rustc_higher_than_required(), true); + } + + #[test] + fn test_validate_llvm_cov_installed() { + // Test when llvm-cov is installed + assert_eq!(is_llvm_cov_installed(), true); + } +} diff --git a/src/cli/pre_run/execute.rs b/src/cli/pre_run/execute.rs new file mode 100644 index 0000000..f89af88 --- /dev/null +++ b/src/cli/pre_run/execute.rs @@ -0,0 +1,24 @@ +use super::{system_checks::run_system_checks, utils::git_hooks}; + +pub fn execute(env: &str) { + if !git_hooks::check_if_git_hooks_are_installed() { + git_hooks::copy_git_hooks(); + } + + run_system_checks(env); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_execute_with_installed_git_hooks() { + // Arrange + let env = "dev"; + // Act + execute(env); + // Assert + // Add your assertions here to verify the expected behavior + } +} diff --git a/src/cli/pre_run/mod.rs b/src/cli/pre_run/mod.rs new file mode 100644 index 0000000..9eaf624 --- /dev/null +++ b/src/cli/pre_run/mod.rs @@ -0,0 +1,5 @@ +pub mod cargo; +pub mod execute; +pub mod npm; +pub mod system_checks; +pub mod utils; diff --git a/src/cli/pre_run/npm/checks.rs b/src/cli/pre_run/npm/checks.rs new file mode 100644 index 0000000..519e412 --- /dev/null +++ b/src/cli/pre_run/npm/checks.rs @@ -0,0 +1,58 @@ +use std::process::Command; + +use crate::cli::pre_run::utils::check_semver::check_semver; +/// The name of the npm executable. +/// On windows it is npm.cmd +/// On linux and mac it is npm + +#[cfg(windows)] +pub const NPM: &'static str = "npm.cmd"; + +#[cfg(not(windows))] +pub const NPM: &str = "npm"; + +const REQUIRED_VERSION: &str = "18.14.1"; + +/// check if node is installed +/// check if the node version is above 18.14.1 +/// versions below will panic on astro commands +pub fn is_node_installed() -> bool { + let output = Command::new("node") + .arg("--version") + .output() + .expect("Failed to execute command"); + + let output_str = String::from_utf8_lossy(&output.stdout); + let mut version = output_str.trim(); + + // remove the v from v20.10.0 + if version.starts_with('v') { + version = &version[1..]; + } + + check_semver(version, REQUIRED_VERSION) +} + +/// Check if the frontend project is installed +/// The frontend project is required to run the astro commands + +pub fn is_frontend_project_installed() -> bool { + let project = std::path::Path::new("./src/frontend/package.json").exists(); + let installed = std::path::Path::new("./src/frontend/node_modules").exists(); + project && installed +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_node_installed() { + // Test when node is installed and version is above required version + assert_eq!(is_node_installed(), true); + } + + #[test] + fn test_is_frontend_project_installed() { + assert_eq!(is_frontend_project_installed(), true); + } +} diff --git a/src/cli/pre_run/npm/mod.rs b/src/cli/pre_run/npm/mod.rs new file mode 100644 index 0000000..1a666ae --- /dev/null +++ b/src/cli/pre_run/npm/mod.rs @@ -0,0 +1,2 @@ +pub mod checks; +pub mod validate; diff --git a/src/cli/pre_run/npm/validate.rs b/src/cli/pre_run/npm/validate.rs new file mode 100644 index 0000000..ebe584e --- /dev/null +++ b/src/cli/pre_run/npm/validate.rs @@ -0,0 +1,75 @@ +use std::process::Command; + +use inquire::Confirm; + +use crate::cli::utils::terminal::{error, spacer, step, success}; + +use super::checks::{is_frontend_project_installed, is_node_installed, NPM}; + +pub fn validate_node() { + let is_node_installed = is_node_installed(); + + match is_node_installed { + true => success("node is installed and its version is higher than 18.14.1"), + false => { + error("node is not installed, or its version is below 18.14.1 please install it and try again. Panicking..."); + panic!() + } + } +} + +pub fn validate_frontend_project() { + let project = is_frontend_project_installed(); + + match project { + true => success("astro framework is installed"), + false => { + error("Astro framework is not installed"); + let ans = Confirm::new("Do you want to install astro framework ?") + .with_default(false) + .prompt(); + + match ans { + Ok(true) => { + spacer(); + step("Installing the astro framework ..."); + Command::new(NPM) + .arg("install") + .current_dir("./src/frontend") + .spawn() + .expect("Failed to install the frontend project") + .wait() + .expect("Failed to install the frontend project"); + + spacer(); + success("Astro framework installed successfully") + } + Ok(false) => { + error("That's too bad, we have to quit now"); + panic!(); + } + Err(_) => { + error("Error with prompt, about to panic"); + panic!(); + } + } + } + } +} +#[cfg(test)] +mod tests { + + #[test] + fn test_validate_node_installed() { + // Simulate node being installed + let is_node_installed = true; + assert_eq!(is_node_installed, true); + } + + #[test] + fn test_validate_frontend_project_installed() { + // Simulate frontend project being installed + let project_installed = true; + assert_eq!(project_installed, true); + } +} diff --git a/src/cli/pre_run/system_checks.rs b/src/cli/pre_run/system_checks.rs new file mode 100644 index 0000000..58cfe68 --- /dev/null +++ b/src/cli/pre_run/system_checks.rs @@ -0,0 +1,62 @@ +use crate::cli::utils::terminal::{error, spacer, step, success}; + +use super::{ + cargo::validate::{ + validate_cargo_watch, validate_commitlint_rs, validate_llcov, validate_rustc_version, + }, + npm::validate::{validate_frontend_project, validate_node}, +}; + +/// Run system checks before starting the development or production server +/// The system checks will check if the required tools are installed +/// The system checks will also check if the required projects are installed + +pub fn run_system_checks(env: &str) { + spacer(); + step("Running system checks ..."); + + match env { + "dev" => { + validate_rustc_version(); + validate_cargo_watch(); + validate_commitlint_rs(); + validate_node(); + validate_frontend_project(); + validate_llcov(); + } + "prod" => { + validate_rustc_version(); + validate_node(); + validate_frontend_project(); + } + _ => { + error("Invalid environment, about to panic"); + panic!(); + } + } + + success("System checks passed successfully"); +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_run_system_checks_dev() { + run_system_checks("dev"); + // Add assertions here to verify the expected behavior + } + + #[test] + fn test_run_system_checks_prod() { + run_system_checks("prod"); + // Add assertions here to verify the expected behavior + } + + #[test] + #[should_panic] + fn test_run_system_checks_invalid_env() { + run_system_checks("invalid"); + // This test should panic, so no assertions are needed + } +} diff --git a/src/cli/pre_run/utils/check_semver.rs b/src/cli/pre_run/utils/check_semver.rs new file mode 100644 index 0000000..ef66458 --- /dev/null +++ b/src/cli/pre_run/utils/check_semver.rs @@ -0,0 +1,33 @@ +// Check if the provided version is higher than the required version + +pub fn check_semver(version: &str, required_version: &str) -> bool { + let version = semver::Version::parse(version); + let required_version = semver::Version::parse(required_version); + match version { + Ok(version) => match required_version { + Ok(required_version) => version >= required_version, + Err(_) => false, + }, + Err(_) => false, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_check_semver() { + // Test case: version is higher than required version + assert_eq!(check_semver("1.2.3", "1.0.0"), true); + + // Test case: version is equal to required version + assert_eq!(check_semver("1.2.3", "1.2.3"), true); + + // Test case: version is lower than required version + assert_eq!(check_semver("1.0.0", "1.2.3"), false); + + // Test case: invalid version format + assert_eq!(check_semver("1.2.3.4", "1.0.0"), false); + } +} diff --git a/src/cli/pre_run/utils/git_hooks.rs b/src/cli/pre_run/utils/git_hooks.rs new file mode 100644 index 0000000..4dfbc41 --- /dev/null +++ b/src/cli/pre_run/utils/git_hooks.rs @@ -0,0 +1,136 @@ +use std::fs; + +/// This function will copy the git hooks from the git_hooks folder to .git/hooks +/// This will allow the user to use the pre-commit, pre-push and commit-msg hooks +/// The hooks will help the user to maintain the code quality +/// Alter the hooks in the git_hooks folder to fit your needs + +pub fn copy_git_hooks() { + // Get the list of hooks from the git_hooks folder + let hooks = fs::read_dir("git_hooks").unwrap(); + + // For each hook, copy the file to .git/hooks + + for hook in hooks { + let hook = hook.unwrap(); + let hook_name = hook.file_name(); + let hook_name = hook_name.to_str().unwrap(); + let hook_path = hook.path(); + let hook_path = hook_path.to_str().unwrap(); + + let git_hook_path = format!(".git/hooks/{}", hook_name); + let git_hook_path = git_hook_path.as_str(); + + // Copy the hook to .git/hooks + match fs::copy(hook_path, git_hook_path) { + Ok(_) => { + println!("{} copied to {}", hook_name, git_hook_path); + } + Err(e) => { + println!("Error: {}", e); + } + } + + // Make the hook executable for all systems (linux, mac, windows) + + #[cfg(not(windows))] + { + let output = std::process::Command::new("chmod") + .arg("+x") + .arg(git_hook_path) + .output() + .expect("Failed to execute command"); + let output_str = String::from_utf8_lossy(&output.stdout); + println!("{}", output_str); + } + + #[cfg(windows)] + { + // No need to make the file executable on windows + } + } +} + +// Try and find match between git_hooks and .git/hooks +pub fn check_if_git_hooks_are_installed() -> bool { + // Get the list of hooks from the .git/hooks folder + let hooks = fs::read_dir(".git/hooks"); + if hooks.is_err() { + return false; + } + let hooks = hooks.unwrap(); + // Check if the folder is empty + if hooks.count() == 0 { + return false; + } + true +} + +pub fn remove_git_hooks() { + // Get the list of hooks from the git_hooks folder + let hooks = fs::read_dir("git_hooks").unwrap(); + + // For each hook, copy the file to .git/hooks + + for hook in hooks { + let hook = hook.unwrap(); + let hook_name = hook.file_name(); + let hook_name = hook_name.to_str().unwrap(); + let git_hook_path = format!(".git/hooks/{}", hook_name); + let git_hook_path = git_hook_path.as_str(); + + // Remove the hook from .git/hooks + match fs::remove_file(git_hook_path) { + Ok(_) => { + println!("{} removed from {}", hook_name, git_hook_path); + } + Err(e) => { + println!("Error: {}", e); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn read_the_hooks() -> Vec { + let hooks = fs::read_dir(".git/hooks").unwrap(); + let mut hooks_list = Vec::new(); + for hook in hooks { + let hook = hook.unwrap(); + let hook_name = hook.file_name(); + let hook_name = hook_name.to_str().unwrap(); + hooks_list.push(hook_name.to_string()); + } + println!("{:?}", hooks_list); + hooks_list + } + + #[test] + fn test_copy_git_hooks() { + let check = check_if_git_hooks_are_installed(); + assert!(check == true); + + copy_git_hooks(); + + // Assert + // Verify that the hooks are copied to .git/hooks + assert!(read_the_hooks() + .iter() + .any(|hook| hook == "pre-commit" || hook == "pre-push" || hook == "commit-msg")); + } + + #[test] + fn test_remove_git_hooks() { + // Act + remove_git_hooks(); + let hooks = read_the_hooks(); + // Assert + // Verify that the hooks are removed from .git/hooks + assert!(hooks.iter().any(|hook| hook == "pre-commit") == false); + + copy_git_hooks(); //return the githooks back after tests + } +} diff --git a/src/cli/pre_run/utils/mod.rs b/src/cli/pre_run/utils/mod.rs new file mode 100644 index 0000000..f4a271e --- /dev/null +++ b/src/cli/pre_run/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod check_semver; +pub mod git_hooks; diff --git a/src/cli/production/build_production.rs b/src/cli/production/build_production.rs new file mode 100644 index 0000000..2837834 --- /dev/null +++ b/src/cli/production/build_production.rs @@ -0,0 +1,68 @@ +use crate::cli::config::create_dotenv::create_dotenv_frontend; +use crate::cli::config::get_config::{get_config, Config, ASTROX_TOML}; +use crate::cli::config::toml::read_toml; +use crate::cli::pre_run::npm::checks::NPM; +use crate::cli::utils::terminal::step; +use std::process::Command; + +pub fn build_production(config: Config) { + // Bundle the frontend and wait for the process to finish + // if the astro build is set to true + // start the build process + + if config.prod_astro_build { + // take production build url from config + let prod_build_url = config.public_keys.public_api_url; + + create_dotenv_frontend(&prod_build_url, "./src/frontend/.env"); + + step("Building the frontend package"); + + let bundle = Command::new(NPM) + .arg("run") + .arg("build") + .current_dir("./src/frontend") + .spawn() + .expect("Failed to bundle the frontend") + .wait() + .expect("Failed to bundle the frontend"); + + match bundle.success() { + true => step("Frontend bundled successfully"), + false => panic!("Failed to bundle the frontend"), + } + } + + // Start the backend production server + + step("Building cargo backend production server"); + + let cargo_server = Command::new("cargo") + .current_dir("./src/backend") + .arg("build") + .arg("--release") + .spawn() + .expect("Failed to start backend production server") + .wait() + .expect("Failed to start backend production server"); + + match cargo_server.success() { + true => step("Backend built successfully"), + false => panic!("Failed to build the backend"), + } +} + +pub fn execute_build() { + let config = read_toml(&ASTROX_TOML.to_string()); + match config { + Ok(mut config) => { + config.env = "prod".to_string(); + build_production(config) + } + Err(_) => { + let mut config = get_config(&vec![]); + config.env = "prod".to_string(); + build_production(config); + } + } +} diff --git a/src/cli/production/mod.rs b/src/cli/production/mod.rs new file mode 100644 index 0000000..49c6c08 --- /dev/null +++ b/src/cli/production/mod.rs @@ -0,0 +1,2 @@ +pub mod build_production; +pub mod start_production; diff --git a/src/cli/production/start_production.rs b/src/cli/production/start_production.rs new file mode 100644 index 0000000..f21d279 --- /dev/null +++ b/src/cli/production/start_production.rs @@ -0,0 +1,112 @@ +use crate::cli::config::create_dotenv::create_dotenv_frontend; +use crate::cli::config::get_config::{get_config, Config, ASTROX_TOML}; +use crate::cli::config::toml::read_toml; +use crate::cli::pre_run::npm::checks::NPM; +use crate::cli::utils::terminal::step; +use ctrlc::set_handler; +use std::process::Command; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +/// Start the production server +/// The production server will start the actix backend server +/// The production server will also bundle the frontend + +pub fn start_production(config: Config) { + // Bundle the frontend and wait for the process to finish + // if the astro build is set to true + // start the build process + + let running = Arc::new(AtomicBool::new(true)); + let r = running.clone(); + + set_handler(move || { + r.store(false, Ordering::SeqCst); + }) + .expect("Error setting Ctrl-C handler"); + + if config.prod_astro_build { + // take production build url from config + let prod_build_url = config.public_keys.public_api_url; + + create_dotenv_frontend(&prod_build_url, "./src/frontend/.env"); + + step("Bundling the frontend"); + + let bundle = Command::new(NPM) + .arg("run") + .arg("build") + .current_dir("./src/frontend") + .spawn() + .expect("Failed to bundle the frontend") + .wait() + .expect("Failed to bundle the frontend"); + + match bundle.success() { + true => step("Frontend bundled successfully"), + false => panic!("Failed to bundle the frontend"), + } + } + + // Start the backend production server + + step("Starting cargo backend production server"); + + while running.load(Ordering::SeqCst) { + let mut cargo_server = Command::new("cargo") + .current_dir("./src/backend") + .arg("run") + .arg("--release") + .arg("--") + .arg(format!("--host={}", config.host)) + .arg(format!("--port={}", config.port.unwrap_or(8080))) + .arg(format!("--env={}", config.env)) + .arg(format!("--cors_url={}", config.cors_url)) + .spawn() + .expect("Failed to start backend production server"); + + match cargo_server.wait() { + Ok(status) if status.success() => { + step("Backend production server exited successfully"); + break; + } + Ok(status) => { + step(&format!( + "Backend production server exited with status: {}", + status + )); + } + Err(e) => { + step(&format!( + "Failed to wait on backend production server: {}", + e + )); + } + } + + if running.load(Ordering::SeqCst) { + step("Restarting cargo backend production server"); + } + } + + step("Cleaning up orphaned processes"); + + step("Exiting"); + + std::process::exit(0); +} + +pub fn execute_serve() { + let config = read_toml(&ASTROX_TOML.to_string()); + match config { + Ok(mut config) => { + config.env = "prod".to_string(); + start_production(config); + } + Err(_) => { + let mut config = get_config(&vec![]); + config.env = "prod".to_string(); + start_production(config); + } + } +} diff --git a/src/cli/tests/execute.rs b/src/cli/tests/execute.rs new file mode 100644 index 0000000..d85efb9 --- /dev/null +++ b/src/cli/tests/execute.rs @@ -0,0 +1,89 @@ +use std::process::Command; + +use crate::cli::{ + pre_run::npm::checks::NPM, + utils::terminal::{step, success}, +}; + +pub fn execute_backend_tests() { + step("Executing backend tests"); + + let cargo_test = Command::new("cargo") + .current_dir("./src/backend") + .arg("test") + .spawn() + .expect("Failed to execute backend tests") + .wait() + .expect("Failed to execute backend tests"); + + match cargo_test.success() { + true => step("Backend tests executed successfully"), + false => panic!("Failed to execute backend tests"), + } +} + +pub fn execute_cli_coverage() { + step("Executing CLI coverage"); + + let cargo_coverage = Command::new("cargo") + .current_dir("./") + .arg("llvm-cov") + .arg("--all-features") + .arg("--workspace") + .spawn() + .expect("Failed to execute CLI coverage") + .wait() + .expect("Failed to execute CLI coverage"); + + match cargo_coverage.success() { + true => success("CLI coverage executed successfully"), + false => panic!("Failed to execute CLI coverage"), + } +} + +pub fn execute_backend_coverage() { + step("Executing CLI coverage"); + + let cargo_coverage = Command::new("cargo") + .current_dir("./src/backend/") + .arg("llvm-cov") + .arg("--all-features") + .arg("--workspace") + .spawn() + .expect("Failed to execute CLI coverage") + .wait() + .expect("Failed to execute CLI coverage"); + + match cargo_coverage.success() { + true => success("CLI coverage executed successfully"), + false => panic!("Failed to execute CLI coverage"), + } +} + +pub fn execute_frontend_tests() { + step("Executing frontend tests"); + + let npm_test = Command::new(NPM) + .arg("run") + .arg("test") + .current_dir("./src/frontend") + .spawn() + .expect("Failed to execute frontend tests") + .wait() + .expect("Failed to execute frontend tests"); + + match npm_test.success() { + true => step("Frontend tests executed successfully"), + false => panic!("Failed to execute frontend tests"), + } +} + +pub fn execute_tests() { + execute_backend_tests(); + execute_frontend_tests(); +} + +pub fn execute_coverage() { + execute_cli_coverage(); + execute_backend_coverage(); +} diff --git a/src/cli/tests/mod.rs b/src/cli/tests/mod.rs new file mode 100644 index 0000000..2e8bddd --- /dev/null +++ b/src/cli/tests/mod.rs @@ -0,0 +1 @@ +pub mod execute; diff --git a/src/cli/utils/mod.rs b/src/cli/utils/mod.rs new file mode 100644 index 0000000..a566381 --- /dev/null +++ b/src/cli/utils/mod.rs @@ -0,0 +1 @@ +pub mod terminal; diff --git a/src/cli/utils/terminal.rs b/src/cli/utils/terminal.rs new file mode 100644 index 0000000..12c1321 --- /dev/null +++ b/src/cli/utils/terminal.rs @@ -0,0 +1,257 @@ +use crossterm::execute; +use crossterm::style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor}; +use std::io::stdout; + +// Read the Cargo.toml file and get the version +pub fn get_version() -> String { + let cargo_toml = std::fs::read_to_string("Cargo.toml").expect("Failed to read Cargo.toml"); + let version = cargo_toml + .split('\n') + .find(|line| line.contains("version")) + .unwrap() + .split('=') + .collect::>()[1] + .trim() + .replace('\"', ""); + + version +} + +pub fn do_server_log(string: &str) { + spacer(); + execute!( + stdout(), + SetForegroundColor(Color::White), + SetBackgroundColor(Color::DarkRed), + Print("| Actix |"), + ResetColor, + Print(" "), + Print(string), + ) + .unwrap(); + spacer(); +} + +pub fn do_front_log(string: &str) { + spacer(); + execute!( + stdout(), + SetForegroundColor(Color::White), + SetBackgroundColor(Color::DarkMagenta), + Print("| Astro |"), + ResetColor, + Print(" "), + Print(string), + ) + .unwrap(); + spacer(); +} + +pub fn do_splash() { + spacer(); + execute!( + stdout(), + SetForegroundColor(Color::Magenta), + Print( + " + █████ ███████ ████████ ██████ ██████ ██ ██ + ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ + ███████ ███████ ██ ██████ ██ ██ ███ + ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ + ██ ██ ███████ ██ ██ ██ ██████ ██ ██ +" + ), + ResetColor + ) + .unwrap(); + spacer(); + execute!( + stdout(), + Print(format!( + "{} astro_x: version {} author: @spaceout.pl", + ResetColor, + get_version() + )), + ResetColor + ) + .unwrap(); + spacer(); + hr(); + spacer(); +} + +pub fn hr() { + execute!( + stdout(), + SetForegroundColor(Color::Magenta), + Print( + "==============================================================================================================================================" + ), + ResetColor + ).unwrap(); +} + +pub fn spacer() { + execute!(stdout(), ResetColor, Print("\n\n"), ResetColor).unwrap(); +} + +pub fn step(string: &str) { + execute!( + stdout(), + SetForegroundColor(Color::Magenta), + Print(format!("🏁 Action: {}\n", string)), + ResetColor + ) + .unwrap(); +} + +pub fn success(string: &str) { + execute!( + stdout(), + SetForegroundColor(Color::Green), + Print(format!("✅ Success: {}\n", string)), + ResetColor + ) + .unwrap(); +} + +pub fn warning(string: &str) { + execute!( + stdout(), + SetForegroundColor(Color::Yellow), + Print(format!("☢️ Warning: {}\n", string)), + ResetColor + ) + .unwrap(); +} + +pub fn dev_info(host: &String, port: &u16) { + execute!( + stdout(), + SetForegroundColor(Color::Green), + Print("| Local development backend server running at:\n"), + Print(format!("| http://{}:{}\n", host, port)), + Print("|\n"), + ResetColor + ) + .unwrap(); +} + +pub fn error(string: &str) { + execute!( + stdout(), + SetForegroundColor(Color::Red), + Print(format!("❗ Error: {}\n", string)), + ResetColor + ) + .unwrap(); +} + +pub fn help() { + execute!( + stdout(), + SetForegroundColor(Color::Magenta), + Print(format!("v{} --- Astrox CLI\n", get_version())), + ResetColor + ) + .unwrap(); + spacer(); + execute!( + stdout(), + SetForegroundColor(Color::Blue), + Print("Command list:\n"), + Print("--help [print this help ]\n"), + Print("--sync-git-hooks [copy git_hooks folder contents to .git/hooks]\n"), + Print("--remove-git-hooks [remove hooks from .git/hooks folder]\n"), + Print("--build [build production bundle for frontend and backend]\n"), + Print("--serve [start the production server with the frontend build]\n"), + Print("--test [run the tests]\n"), + Print("--coverage [run the tests and generate coverage report]\n"), + Print("--create-toml [create a new Astrox.toml file]\n"), + Print("--interactive [start the interactive mode]\n"), + Print("--system-checks [run the system checks]\n"), + ResetColor + ) + .unwrap(); + spacer(); + execute!( + stdout(), + SetForegroundColor(Color::Blue), + Print("Cli arguments:\n"), + Print("--host=127.0.0.1 [ip address]\n"), + Print("--port=8080 [actix port number]\n"), + Print("--env=prod / dev [environment]\n"), + Print("--astro-port=4321 [astro development port number]\n"), + Print("--prod-astro-build=true / false [Build astro during cli prod start]\n"), + ResetColor + ) + .unwrap(); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_version() { + // Test when Cargo.toml contains version + let version = get_version(); + assert_eq!(version, "0.1.2"); + } + + #[test] + fn test_do_splash() { + // Test the output of do_splash function + do_splash(); + } + + #[test] + fn test_hr() { + // Test the output of hr function + hr(); + } + + #[test] + fn test_spacer() { + // Test the output of spacer function + spacer(); + } + + #[test] + fn test_step() { + // Test the output of step function + step("Test Step"); + } + + #[test] + fn test_success() { + // Test the output of success function + success("Test Success"); + } + + #[test] + fn test_warning() { + // Test the output of warning function + warning("Test Warning"); + } + + #[test] + fn test_dev_info() { + // Test the output of dev_info function + let host = String::from("localhost"); + let port = 8080; + dev_info(&host, &port); + } + + #[test] + fn test_error() { + // Test the output of error function + error("Test Error"); + } + + #[test] + fn test_help() { + // Test the output of help function + help(); + } +} diff --git a/src/frontend/.eslintignore b/src/frontend/.eslintignore new file mode 100644 index 0000000..f8140e5 --- /dev/null +++ b/src/frontend/.eslintignore @@ -0,0 +1 @@ +env.d.ts \ No newline at end of file diff --git a/src/frontend/.eslintrc.cjs b/src/frontend/.eslintrc.cjs new file mode 100644 index 0000000..4a866b5 --- /dev/null +++ b/src/frontend/.eslintrc.cjs @@ -0,0 +1,61 @@ +module.exports = { + root: true, + env: { + browser: true, + es2021: true, + node: true + }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:astro/recommended', + 'plugin:astro/jsx-a11y-recommended', + 'plugin:svelte/recommended', + ], + overrides: [ + { + env: { + node: true + }, + files: ['.eslintrc.{js,cjs}'], + parserOptions: { + sourceType: 'script' + } + }, + { + files: ['*.svelte'], + parser: 'svelte-eslint-parser', + // Parse the ` + + + + diff --git a/src/frontend/src/components/navbar/Navbar.test.ts b/src/frontend/src/components/navbar/Navbar.test.ts new file mode 100644 index 0000000..edab0eb --- /dev/null +++ b/src/frontend/src/components/navbar/Navbar.test.ts @@ -0,0 +1,12 @@ +import { experimental_AstroContainer as AstroContainer } from 'astro/container' +import { expect, test } from 'vitest' +import Navbar from './Navbar.astro' + +test('Navbar', async () => { + const container = await AstroContainer.create() + const result = await container.renderToString(Navbar) + + expect(result).toContain('astro') + expect(result).toContain('X') + expect(result).toContain('Actix') +}) diff --git a/src/frontend/src/components/navbar/NavbarItem.astro b/src/frontend/src/components/navbar/NavbarItem.astro new file mode 100644 index 0000000..b596c13 --- /dev/null +++ b/src/frontend/src/components/navbar/NavbarItem.astro @@ -0,0 +1,141 @@ +--- +interface Props { + id: string + href: string + external?: boolean +} + +const { id, href, external } = Astro.props +--- + + + + + + + + diff --git a/src/frontend/src/components/navbar/NavbarItem.test.ts b/src/frontend/src/components/navbar/NavbarItem.test.ts new file mode 100644 index 0000000..6f725de --- /dev/null +++ b/src/frontend/src/components/navbar/NavbarItem.test.ts @@ -0,0 +1,39 @@ +import { experimental_AstroContainer as AstroContainer } from 'astro/container' +import { expect, test } from 'vitest' +import NavbarItem from './NavbarItem.astro' + +test('Navbar Item internal', async () => { + const container = await AstroContainer.create() + const result = await container.renderToString(NavbarItem, { + props: { + id: 'home', + href: '/', + external: false + }, + slots: { + default: 'Home' + } + }) + + expect(result).toContain('Home') + expect(result).toContain('href="/') + expect(result).toContain('_self') +}) + +test('Navbar Item internal', async () => { + const container = await AstroContainer.create() + const result = await container.renderToString(NavbarItem, { + props: { + id: 'home', + href: '/', + external: true + }, + slots: { + default: 'Home' + } + }) + + expect(result).toContain('Home') + expect(result).toContain('href="/') + expect(result).toContain('_blank') +}) diff --git a/src/frontend/src/components/spaceX/spacex.svelte b/src/frontend/src/components/spaceX/spacex.svelte new file mode 100644 index 0000000..f86d36d --- /dev/null +++ b/src/frontend/src/components/spaceX/spacex.svelte @@ -0,0 +1,82 @@ + + + + +
+

Svelte Component (Client side call example)

+ {#each data as rocket} +
    +
  • +

    {rocket.rocket_name}

    +

    {rocket.description}

    +
  • +
+ {/each} +
+ + diff --git a/src/frontend/src/components/spaceX/spacex.test.ts b/src/frontend/src/components/spaceX/spacex.test.ts new file mode 100644 index 0000000..8fe1f09 --- /dev/null +++ b/src/frontend/src/components/spaceX/spacex.test.ts @@ -0,0 +1,25 @@ +import { experimental_AstroContainer as AstroContainer } from 'astro/container' +import { expect, test } from 'vitest' +// @eslint-disable-next-line import/no-unresolved +// @ts-expect-error wrong types, but this combo works +import ssr from '@astrojs/svelte/server.js' + +import SpaceX from './spacex.svelte' +import type { AstroComponentFactory } from 'astro/runtime/server/index.js' + +test('Astro Page', async () => { + const container = await AstroContainer.create() + container.addServerRenderer({ + name: '@astrojs/svelte', + // @ts-expect-error wrong types, but this combo works + renderer: ssr + }) + container.addClientRenderer({ + name: '@astrojs/svelte', + entrypoint: '@astrojs/svelte/client-v5.js' + }) + const result = await container.renderToString( + SpaceX as unknown as AstroComponentFactory + ) // This works but astro still have not pinned down the types correctly + expect(result).toContain('Svelte Component (Client side call example)') +}) diff --git a/src/frontend/src/components/zoomImage/zoomImage.astro b/src/frontend/src/components/zoomImage/zoomImage.astro new file mode 100644 index 0000000..e201238 --- /dev/null +++ b/src/frontend/src/components/zoomImage/zoomImage.astro @@ -0,0 +1,83 @@ +--- +import { Image } from 'astro:assets' +import type { ImageMetadata } from 'astro' + +interface Props { + src: ImageMetadata + alt: string + borderFlat: 'left' | 'right' +} + +const { src, alt, borderFlat } = Astro.props +--- + +
+ {alt} +
+ + diff --git a/src/frontend/src/components/zoomImage/zoomImage.test.ts b/src/frontend/src/components/zoomImage/zoomImage.test.ts new file mode 100644 index 0000000..608cbfb --- /dev/null +++ b/src/frontend/src/components/zoomImage/zoomImage.test.ts @@ -0,0 +1,17 @@ +import { experimental_AstroContainer as AstroContainer } from 'astro/container' +import { expect, test } from 'vitest' +import AstroImage from '../../sections/imgs/astro.jpeg' +import ZoomImage from './zoomImage.astro' + +test('ZoomImage', async () => { + const container = await AstroContainer.create() + const result = await container.renderToString(ZoomImage, { + props: { + src: AstroImage, + alt: 'An example image', + borderFlat: 'left' + } + }) + + expect(result).toContain('An example image') +}) diff --git a/src/frontend/src/env.d.ts b/src/frontend/src/env.d.ts new file mode 100644 index 0000000..e815b35 --- /dev/null +++ b/src/frontend/src/env.d.ts @@ -0,0 +1,3 @@ +/* eslint-disable @typescript-eslint/triple-slash-reference */ +/// +/// diff --git a/src/frontend/src/layouts/Layout.astro b/src/frontend/src/layouts/Layout.astro new file mode 100644 index 0000000..e0458de --- /dev/null +++ b/src/frontend/src/layouts/Layout.astro @@ -0,0 +1,68 @@ +--- +import { ClientRouter } from 'astro:transitions' +import Navbar from '../components/navbar/Navbar.astro' +import Footer from '../components/Footer.astro' + +interface Props { + title: string +} + +const { title } = Astro.props +--- + + + + + + + + + + {title} + + + + + + + + +