initial testing infrastructure

This commit is contained in:
DSeeLP 2025-03-05 13:42:06 +01:00
parent 02e1ffca4d
commit 456c507c10
12 changed files with 213 additions and 28 deletions

View File

@ -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 <nixpkgs> { 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:

2
.gitignore vendored
View File

@ -3,3 +3,5 @@
/result
/schemas
openapi.json
/tests/report

View File

@ -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" = { };
};

View File

@ -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

View File

@ -322,7 +322,7 @@ pub type State = axum::extract::State<Arc<AppState>>;
pub fn router() -> Router<Arc<AppState>> {
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())

View File

@ -23,8 +23,7 @@ pub(super) fn router() -> Router<Arc<AppState>> {
.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<Json<User
let result = Account::list_for_user(&conn, user).await?;
Ok(Json(UserAccounts { accounts: result }))
}
#[cfg(test)]
mod tests {
use uuid::Uuid;
use super::UserTarget;
#[test]
fn user_target() {
let json = serde_json::to_string(&UserTarget::Me).unwrap();
assert_eq!(json, r#""@me""#);
let json = serde_json::to_string(&Uuid::nil()).unwrap();
assert_eq!(json, r#""00000000-0000-0000-0000-000000000000""#);
}
}

View File

@ -75,10 +75,15 @@ impl Account {
name: &str,
) -> Result<Uuid, tokio_postgres::Error> {
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)
}

View File

@ -50,12 +50,13 @@ impl User {
name: &str,
password: &str,
) -> Result<Uuid, Error> {
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)
}
}

85
tests/ci.sh Executable file
View File

@ -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

36
tests/dummy-users.hurl Normal file
View File

@ -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

View File

@ -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

2
tests/reset.sh Executable file
View File

@ -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