mirror of
https://git.dirksys.ovh/dirk/bankserver.git
synced 2025-12-20 02:59:20 +01:00
initial testing infrastructure
This commit is contained in:
parent
02e1ffca4d
commit
456c507c10
@ -20,9 +20,13 @@ jobs:
|
|||||||
ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN }}
|
ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN }}
|
||||||
- name: Build
|
- name: Build
|
||||||
run: nix-fast-build --skip-cached --no-nom --attic-cache $ATTIC_CACHE --flake .#packages
|
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: |
|
run: |
|
||||||
mkdir images
|
mkdir images
|
||||||
|
MANIFEST_TAG="git.dirksys.ovh/dirk/bankserver:latest"
|
||||||
|
manifest_id=$(podman manifest create $MANIFEST_TAG)
|
||||||
|
|
||||||
for file in result-*dockerImage*; do
|
for file in result-*dockerImage*; do
|
||||||
if [ ! -f "$file" ]; then
|
if [ ! -f "$file" ]; then
|
||||||
continue
|
continue
|
||||||
@ -31,9 +35,21 @@ jobs:
|
|||||||
IFS=$'\x1F' read -r hostArch name crossArch <<< "$info"
|
IFS=$'\x1F' read -r hostArch name crossArch <<< "$info"
|
||||||
arch=${crossArch:-$hostArch}
|
arch=${crossArch:-$hostArch}
|
||||||
containerArch=$(arch=$arch nix eval --raw --impure -I nixpkgs=flake:nixpkgs --expr '(import <nixpkgs> { system = builtins.getEnv "arch";}).go.GOARCH')
|
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
|
done
|
||||||
|
echo "manifest-tag=$MANIFEST_TAG" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: Run tests
|
||||||
|
run: just test
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
@ -46,26 +62,14 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
USERNAME: ${{ github.actor }}
|
USERNAME: ${{ github.actor }}
|
||||||
PASSWORD: ${{ secrets.FORGEJO_REGISTRY_TOKEN }}
|
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'
|
if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch'
|
||||||
shell: bash
|
shell: bash
|
||||||
|
env:
|
||||||
|
MANIFEST_TAG: ${{ steps.process-artifacts.outputs.manifest-tag }}
|
||||||
run: |
|
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"
|
echo "Pushing manifest"
|
||||||
podman login --get-login git.dirksys.ovh
|
podman manifest push $MANIFEST_TAG
|
||||||
podman manifest push git.dirksys.ovh/dirk/bankserver:latest
|
|
||||||
- name: Notify server
|
- name: Notify server
|
||||||
if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch'
|
if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch'
|
||||||
env:
|
env:
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,3 +3,5 @@
|
|||||||
/result
|
/result
|
||||||
/schemas
|
/schemas
|
||||||
openapi.json
|
openapi.json
|
||||||
|
|
||||||
|
/tests/report
|
||||||
|
|||||||
12
flake.nix
12
flake.nix
@ -126,10 +126,18 @@
|
|||||||
name = "bankserver";
|
name = "bankserver";
|
||||||
# tag = "latest";
|
# tag = "latest";
|
||||||
created = "now";
|
created = "now";
|
||||||
contents = [ bankserver ];
|
contents = [
|
||||||
|
bankserver
|
||||||
|
pkgs.catatonit
|
||||||
|
];
|
||||||
config = {
|
config = {
|
||||||
Env = [ "HOST=::" ];
|
Env = [ "HOST=::" ];
|
||||||
Entrypoint = [ "/bin/bankserver" ];
|
# Entrypoint = ["/bin/bankserver"];
|
||||||
|
Entrypoint = [
|
||||||
|
"/bin/catatonit"
|
||||||
|
"--"
|
||||||
|
];
|
||||||
|
Cmd = [ "/bin/bankserver" ];
|
||||||
ExposedPorts = {
|
ExposedPorts = {
|
||||||
"3845/tcp" = { };
|
"3845/tcp" = { };
|
||||||
};
|
};
|
||||||
|
|||||||
8
justfile
8
justfile
@ -10,3 +10,11 @@ openapi:
|
|||||||
yq eval-all -n 'load("openapi-def.yaml") *n load("schemas/schemas.json")' > openapi-temp.yaml
|
yq eval-all -n 'load("openapi-def.yaml") *n load("schemas/schemas.json")' > openapi-temp.yaml
|
||||||
redocly bundle openapi-temp.yaml -o openapi.json
|
redocly bundle openapi-temp.yaml -o openapi.json
|
||||||
rm openapi-temp.yaml
|
rm openapi-temp.yaml
|
||||||
|
|
||||||
|
schemas:
|
||||||
|
touch openapi.json
|
||||||
|
cargo run --bin generate-schemas
|
||||||
|
|
||||||
|
test:
|
||||||
|
tree .
|
||||||
|
tests/ci.sh
|
||||||
|
|||||||
@ -322,7 +322,7 @@ pub type State = axum::extract::State<Arc<AppState>>;
|
|||||||
pub fn router() -> Router<Arc<AppState>> {
|
pub fn router() -> Router<Arc<AppState>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.merge(auth::router())
|
.merge(auth::router())
|
||||||
.nest("/user", user::router())
|
.nest("/users", user::router())
|
||||||
.nest("/docs", docs::router())
|
.nest("/docs", docs::router())
|
||||||
.nest("/accounts", account::router())
|
.nest("/accounts", account::router())
|
||||||
.nest("/transactions", transactions::router())
|
.nest("/transactions", transactions::router())
|
||||||
|
|||||||
@ -23,8 +23,7 @@ pub(super) fn router() -> Router<Arc<AppState>> {
|
|||||||
.route("/", get(list_users))
|
.route("/", get(list_users))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum UserTarget {
|
pub enum UserTarget {
|
||||||
#[serde(rename = "@me")]
|
#[serde(rename = "@me")]
|
||||||
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?;
|
let result = Account::list_for_user(&conn, user).await?;
|
||||||
Ok(Json(UserAccounts { accounts: result }))
|
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""#);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -75,10 +75,15 @@ impl Account {
|
|||||||
name: &str,
|
name: &str,
|
||||||
) -> Result<Uuid, tokio_postgres::Error> {
|
) -> Result<Uuid, tokio_postgres::Error> {
|
||||||
let stmt = client
|
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?;
|
.await?;
|
||||||
let id = id.unwrap_or_else(|| Uuid::new_v7(Timestamp::now(NoContext)));
|
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)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -50,12 +50,13 @@ impl User {
|
|||||||
name: &str,
|
name: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
) -> Result<Uuid, Error> {
|
) -> Result<Uuid, Error> {
|
||||||
|
let hash = password_auth::generate_hash(password);
|
||||||
let stmt = client
|
let stmt = client
|
||||||
.prepare_cached("insert into users(id, name, password) values ($1, $2, $3)")
|
.prepare_cached("insert into users(id, name, password) values ($1, $2, $3)")
|
||||||
.await?;
|
.await?;
|
||||||
let mut tx = client.transaction().await?;
|
let mut tx = client.transaction().await?;
|
||||||
let id = Uuid::new_v7(Timestamp::now(NoContext));
|
let id = Uuid::new_v7(Timestamp::now(NoContext));
|
||||||
tx.execute(&stmt, &[&id, &name, &password])
|
tx.execute(&stmt, &[&id, &name, &hash])
|
||||||
.await
|
.await
|
||||||
.map_err(|err| unique_violation(err, conflict_error))?;
|
.map_err(|err| unique_violation(err, conflict_error))?;
|
||||||
Account::create(&mut tx, Some(id), id, name)
|
Account::create(&mut tx, Some(id), id, name)
|
||||||
@ -125,7 +126,7 @@ impl User {
|
|||||||
let stmt = client
|
let stmt = client
|
||||||
.prepare_cached("select id,name from users where id = $1")
|
.prepare_cached("select id,name from users where id = $1")
|
||||||
.await?;
|
.await?;
|
||||||
let info = client.query_opt(&stmt, &[]).await?.map(User::from);
|
let info = client.query_opt(&stmt, &[&id]).await?.map(User::from);
|
||||||
Ok(info)
|
Ok(info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
85
tests/ci.sh
Executable file
85
tests/ci.sh
Executable 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
36
tests/dummy-users.hurl
Normal 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
|
||||||
20
tests/integration/register.hurl
Normal file
20
tests/integration/register.hurl
Normal 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
2
tests/reset.sh
Executable 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
|
||||||
Loading…
x
Reference in New Issue
Block a user