diff --git a/.forgejo/workflows/ci.yaml b/.forgejo/workflows/ci.yaml index fd70078..59a005e 100644 --- a/.forgejo/workflows/ci.yaml +++ b/.forgejo/workflows/ci.yaml @@ -20,9 +20,13 @@ jobs: ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN }} - name: Build run: nix-fast-build --skip-cached --no-nom --attic-cache $ATTIC_CACHE --flake .#packages - - name: Process results + - name: Process build artifacts + id: process-artifacts run: | mkdir images + MANIFEST_TAG="git.dirksys.ovh/dirk/bankserver:latest" + manifest_id=$(podman manifest create $MANIFEST_TAG) + for file in result-*dockerImage*; do if [ ! -f "$file" ]; then continue @@ -31,9 +35,21 @@ jobs: IFS=$'\x1F' read -r hostArch name crossArch <<< "$info" arch=${crossArch:-$hostArch} containerArch=$(arch=$arch nix eval --raw --impure -I nixpkgs=flake:nixpkgs --expr '(import { system = builtins.getEnv "arch";}).go.GOARCH') - echo "Processed image for $containerArch" - mv $file images/image-$containerArch.tar.gz + + echo "Processed image for $containerArch" + target=images/image-$containerArch.tar.gz + mv $file $target + + echo "Loading $target" + podman image load < $target + image_id=$(podman image ls --format '{{.ID}}' | head -n 2 | tail -1) + podman image untag $image_id + echo "Adding $containerArch image to manifest" + podman manifest add $manifest_id $image_id done + echo "manifest-tag=$MANIFEST_TAG" >> "$GITHUB_OUTPUT" + - name: Run tests + run: just test - name: Upload artifact uses: actions/upload-artifact@v3 with: @@ -46,26 +62,14 @@ jobs: env: USERNAME: ${{ github.actor }} PASSWORD: ${{ secrets.FORGEJO_REGISTRY_TOKEN }} - - name: Push docker images + - name: Push container images if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch' shell: bash + env: + MANIFEST_TAG: ${{ steps.process-artifacts.outputs.manifest-tag }} run: | - manifest_id=$(podman manifest create git.dirksys.ovh/dirk/bankserver:latest) - sleep 1 - for file in $(ls images); do - echo "Loading images/$file" - podman image load < images/$file - image_id=$(podman image ls --format '{{.ID}}' | head -n 2 | tail -1) - architecture=$(podman image inspect $image_id | jq -r ".[0].Architecture") - tag=$(podman image ls --format "{{.Tag}}" | head -n 2 | tail -1) - tag="git.dirksys.ovh/dirk/bankserver:$tag-$architecture" - podman image untag $image_id - echo "Adding $architecture image to manifest" - podman manifest add $manifest_id $image_id - done echo "Pushing manifest" - podman login --get-login git.dirksys.ovh - podman manifest push git.dirksys.ovh/dirk/bankserver:latest + podman manifest push $MANIFEST_TAG - name: Notify server if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch' env: diff --git a/.gitignore b/.gitignore index 13284d1..edf54c3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ /result /schemas openapi.json + +/tests/report diff --git a/flake.nix b/flake.nix index c591038..328ccc6 100644 --- a/flake.nix +++ b/flake.nix @@ -126,10 +126,18 @@ name = "bankserver"; # tag = "latest"; created = "now"; - contents = [ bankserver ]; + contents = [ + bankserver + pkgs.catatonit + ]; config = { Env = [ "HOST=::" ]; - Entrypoint = [ "/bin/bankserver" ]; + # Entrypoint = ["/bin/bankserver"]; + Entrypoint = [ + "/bin/catatonit" + "--" + ]; + Cmd = [ "/bin/bankserver" ]; ExposedPorts = { "3845/tcp" = { }; }; diff --git a/justfile b/justfile index 0be7a8e..ef04adc 100644 --- a/justfile +++ b/justfile @@ -10,3 +10,11 @@ openapi: yq eval-all -n 'load("openapi-def.yaml") *n load("schemas/schemas.json")' > openapi-temp.yaml redocly bundle openapi-temp.yaml -o openapi.json rm openapi-temp.yaml + +schemas: + touch openapi.json + cargo run --bin generate-schemas + +test: + tree . + tests/ci.sh diff --git a/src/api/mod.rs b/src/api/mod.rs index 062f897..87951f3 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -322,7 +322,7 @@ pub type State = axum::extract::State>; pub fn router() -> Router> { Router::new() .merge(auth::router()) - .nest("/user", user::router()) + .nest("/users", user::router()) .nest("/docs", docs::router()) .nest("/accounts", account::router()) .nest("/transactions", transactions::router()) diff --git a/src/api/user.rs b/src/api/user.rs index c68b8b3..b3b6786 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -23,8 +23,7 @@ pub(super) fn router() -> Router> { .route("/", get(list_users)) } -#[derive(Deserialize)] -#[serde(untagged)] +#[derive(Deserialize, Serialize)] pub enum UserTarget { #[serde(rename = "@me")] Me, @@ -104,3 +103,18 @@ pub async fn user_accounts(EState(state): State, auth: Auth) -> Result Result { let stmt = client - .prepare_cached("insert into accounts(id, \"user\", name) values ($1, $2, $3)") + .prepare_cached( + "insert into accounts(id, \"user\", name, balance) values ($1, $2, $3, $4)", + ) .await?; let id = id.unwrap_or_else(|| Uuid::new_v7(Timestamp::now(NoContext))); - client.execute(&stmt, &[&id, &user, &name]).await?; + let balance: i64 = if id == user { 1000 * 100 } else { 0 }; + client + .execute(&stmt, &[&id, &user, &name, &balance]) + .await?; Ok(id) } diff --git a/src/model/user.rs b/src/model/user.rs index d4bacf3..a95e646 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -50,12 +50,13 @@ impl User { name: &str, password: &str, ) -> Result { + let hash = password_auth::generate_hash(password); let stmt = client .prepare_cached("insert into users(id, name, password) values ($1, $2, $3)") .await?; let mut tx = client.transaction().await?; let id = Uuid::new_v7(Timestamp::now(NoContext)); - tx.execute(&stmt, &[&id, &name, &password]) + tx.execute(&stmt, &[&id, &name, &hash]) .await .map_err(|err| unique_violation(err, conflict_error))?; Account::create(&mut tx, Some(id), id, name) @@ -125,7 +126,7 @@ impl User { let stmt = client .prepare_cached("select id,name from users where id = $1") .await?; - let info = client.query_opt(&stmt, &[]).await?.map(User::from); + let info = client.query_opt(&stmt, &[&id]).await?.map(User::from); Ok(info) } } diff --git a/tests/ci.sh b/tests/ci.sh new file mode 100755 index 0000000..ed757ba --- /dev/null +++ b/tests/ci.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +set -eu + +ENV_FILE=".env" +RUN_DIR="/run/postgresql" + +if [ ! -z "${CI:-}" ]; then + echo "Running in CI" +fi + +if ss -tulwn | grep ":5432" -q; then + echo "Postgres running" +else + echo "Postgres not running" + + if [ ! -z "${CI:-}" ]; then + # Create a temporary directory and store its name in a variable. + TEMPD=$(mktemp -d) + + # Exit if the temp directory wasn't created successfully. + if [ ! -e "$TEMPD" ]; then + >&2 echo "Failed to create temp directory" + exit 1 + fi + mkdir "$TEMPD"/run + ENV_FILE="$TEMPD/.env" + echo -e "DATABASE__USER=bankserver\nDATABASE__DBNAME=bankserver\nJWT_KEY=ABCDEFG" > $ENV_FILE + + DB_RUNNING=1 + RUN_DIR="$TEMPD"/run + podman run --name bankserver-db --rm --replace --detach -v "$RUN_DIR":/run/postgresql -e POSTGRES_USER=bankserver -e POSTGRES_HOST_AUTH_METHOD=trust postgres + sleep 5 + fi +fi + +function cleanup { + if [ ! -z "${APP_RUNNING:-}" ]; then + if [ ! -z "${CI:-}" ]; then + echo "Stopping app instance" + podman stop bankserver -t 2 + else + pkill -P $JOB_PID + fi + fi + + if [ ! -z "${DB_RUNNING:-}" ]; then + echo "Stopping database" + podman stop bankserver-db -t 2 + fi + + if [ ! -z "${TEMPD:-}" ]; then + rm -rf "$TEMPD" + fi +} + +trap cleanup EXIT + +HOST="http://[::1]:3845" + +wait_for_url () { + echo "Testing $1..." + printf 'GET %s\nHTTP 404' "$1" | hurl --retry "$2"; + return 0 +} + +echo "Starting application" + +APP_RUNNING="1" +if [ ! -z "${CI:-}" ]; then + echo $RUN_DIR + ls -la $RUN_DIR + podman run --name bankserver --rm --replace --detach --env-file $ENV_FILE --network host -v "$RUN_DIR":/run/postgresql --publish 3845:3845 --pull never bankserver:latest +else + just dev & + JOB_PID=$! +fi + +echo "Waiting for app instance to be ready" +wait_for_url "$HOST" 20 + +echo "Creating dummy users" +hurl --variable host="$HOST" --error-format long tests/dummy-users.hurl + +echo "Running tests" +hurl --variable host="$HOST" --test --error-format long --color --report-html tests/report tests/integration/*.hurl || true diff --git a/tests/dummy-users.hurl b/tests/dummy-users.hurl new file mode 100644 index 0000000..fe83445 --- /dev/null +++ b/tests/dummy-users.hurl @@ -0,0 +1,36 @@ +POST {{host}}/api/register +{ + "name": "user1", + "password": "this-is-a-password" +} +HTTP 201 +POST {{host}}/api/register +{ + "name": "user2", + "password": "this-is-a-password" +} +HTTP 201 +POST {{host}}/api/register +{ + "name": "user3", + "password": "this-is-a-password" +} +HTTP 201 +POST {{host}}/api/register +{ + "name": "user4", + "password": "this-is-a-password" +} +HTTP 201 +POST {{host}}/api/register +{ + "name": "user5", + "password": "this-is-a-password" +} +HTTP 201 +POST {{host}}/api/register +{ + "name": "user6", + "password": "this-is-a-password" +} +HTTP 201 diff --git a/tests/integration/register.hurl b/tests/integration/register.hurl new file mode 100644 index 0000000..d616f02 --- /dev/null +++ b/tests/integration/register.hurl @@ -0,0 +1,20 @@ +POST {{host}}/api/register +{ + "name": "test-user", + "password": "this-is-a-test" +} +HTTP 201 + +[Captures] +token: jsonpath "$.token" + +GET {{host}}/api/users/@me +Authorization: Bearer {{token}} +HTTP 200 + +POST {{host}}/api/login +{ + "name": "test-user", + "password": "this-is-a-test" +} +HTTP 200 diff --git a/tests/reset.sh b/tests/reset.sh new file mode 100755 index 0000000..b0ce01c --- /dev/null +++ b/tests/reset.sh @@ -0,0 +1,2 @@ +#/bin/sh +psql -tac "drop schema public cascade; create schema public; grant all on schema public to $DATABASE__USER;" -d $DATABASE__DBNAME