Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
RSPACE_URL=
RSPACE_API_KEY=
RSPACE_API_KEY=

48 changes: 48 additions & 0 deletions .github/scripts/warmup_sysadmin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import os
import sys
import time

import requests

RSPACE_URL = os.environ["RSPACE_URL"]

# Public, non-secret bootstrap credentials seeded by rspace-web's own "dev-test"
# Liquibase context (initial-seed-run.sql / initial-seed-devtest.sql) for the
# built-in sysadmin1 account.
SYSADMIN_USERNAME = "sysadmin1"
SYSADMIN_PASSWORD = "sysWisc23!"

# A user account that is only ever authenticated via its API key (never a real
# login) has its home folder created lazily on first write - a code path that
# throws on that first attempt (though it leaves the account usable afterward).
# A real login runs the same initialization synchronously and correctly, so a
# single login here avoids ever hitting that first-write bug during the test run.
def warm_up(timeout_seconds=300, interval_seconds=5):
deadline = time.monotonic() + timeout_seconds
session = requests.Session()
last_error = None
while time.monotonic() < deadline:
try:
session.get(f"{RSPACE_URL}/login", timeout=10)
resp = session.post(
f"{RSPACE_URL}/login",
data={"username": SYSADMIN_USERNAME, "password": SYSADMIN_PASSWORD},
timeout=10,
allow_redirects=True,
)
if resp.status_code == 200 and "loginForm" not in resp.text:
return
last_error = f"HTTP {resp.status_code}, login form still present"
except requests.exceptions.RequestException as exc:
last_error = str(exc)
print(f"Login not successful yet ({last_error}); retrying...", file=sys.stderr)
time.sleep(interval_seconds)
raise RuntimeError(f"Could not log in as {SYSADMIN_USERNAME}: {last_error}")


if __name__ == "__main__":
try:
warm_up()
except Exception as exc:
print(f"Error warming up sysadmin1 account: {exc}", file=sys.stderr)
sys.exit(1)
206 changes: 190 additions & 16 deletions .github/workflows/codeql-and-tests.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
name: CodeQL and Unit Test
name: codeQL and Test

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
workflow_dispatch: # allow manual trigger

permissions:
contents: read

jobs:
analyze:
name: CodeQL Analyze
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 20
permissions:
actions: read
contents: read
Expand All @@ -20,35 +26,203 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}

- name: Autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@v4

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4

test:
name: Unit Test
runs-on: ubuntu-latest
name: unit test
runs-on: ubuntu-24.04
timeout-minutes: 30
needs: analyze
permissions:
contents: read
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]

steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Set up Poetry
run: |
curl -sSL https://install.python-poetry.org | python3 -
echo 'export PATH="$HOME/.local/bin:$PATH"' >> $GITHUB_ENV
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: 1.8.4
virtualenvs-create: true
virtualenvs-in-project: true

- name: Install dependencies
run: poetry install --no-interaction

- name: Run unit tests
run: poetry run pytest -m "not integration"

integration-test:
name: integration test
runs-on: ubuntu-24.04
timeout-minutes: 45
needs: test
permissions:
contents: read
env:
RS_FILE_BASE: /tmp/rspace-filestore
services:
db:
image: mariadb:lts-jammy
env:
MARIADB_ROOT_PASSWORD: rspacedbpwd
MARIADB_DATABASE: rspace
MARIADB_USER: rspacedbuser
MARIADB_PASSWORD: rspacedbpwd
ports:
- 3306:3306
options: >-
--tmpfs /var/lib/mysql:rw,noexec,nosuid,size=2g
--health-cmd="healthcheck.sh --connect --innodb_initialized"
--health-interval=5s
--health-timeout=5s
--health-retries=40
--health-start-period=30s
# Required even though these are API-only tests: some backend startup
# paths check that the chemistry service is reachable.
chemistry:
image: rspaceops/oss-chemistry:latest
ports:
- 8090:8090

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: 1.8.4
virtualenvs-create: true
virtualenvs-in-project: true

- name: Install dependencies
run: poetry install
run: poetry install --no-interaction

- name: Determine latest rspace-web release tag
id: rspace_web_release
run: |
set -euo pipefail
latest_tag=$(curl -fsSL --retry 3 --retry-delay 2 https://api.github.com/repos/rspace-os/rspace-web/releases/latest | jq -r '.tag_name')
if [ -z "$latest_tag" ] || [ "$latest_tag" = "null" ]; then
echo "Unable to determine latest release tag for rspace-web"
exit 1
fi
echo "Latest rspace-web tag: $latest_tag"
echo "tag=$latest_tag" >> "$GITHUB_OUTPUT"

- name: Checkout rspace-web
uses: actions/checkout@v4
with:
repository: rspace-os/rspace-web
ref: ${{ steps.rspace_web_release.outputs.tag }}
path: rspace-web
persist-credentials: false

- name: Set up JDK
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
cache: maven
cache-dependency-path: rspace-web/pom.xml

- name: Configure MariaDB
run: |
mysql -h127.0.0.1 -uroot -prspacedbpwd <<'SQL'
SET GLOBAL character_set_server = 'utf8mb4';
SET GLOBAL collation_server = 'utf8mb4_unicode_ci';
SET GLOBAL sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
GRANT CREATE, DROP ON *.* TO 'rspacedbuser'@'%';
FLUSH PRIVILEGES;
SQL

- name: Setup filestore
run: |
rm -rf "$RS_FILE_BASE"
mkdir -p "$RS_FILE_BASE"

# Backend only - no -DgenerateReactDist, so no Node/pnpm/Vite build.
# The Python client only exercises the REST API, not the UI.
- name: Build RSpace backend
working-directory: rspace-web
run: ./mvnw clean package -DskipTests -Dtimestamp=${{ github.run_id }} -Djava-version=17 -Djava-vendor=temurin

- name: Start RSpace and wait for readiness
working-directory: rspace-web
timeout-minutes: 15
run: |
set -euo pipefail
nohup ./mvnw jetty:run-war \
-DskipTests=true -Dmaven.war.skip=true \
-Dtimestamp=${{ github.run_id }} \
-Djava-version=17 -Djava-vendor=temurin \
-Denvironment=drop-recreate-db \
-Dspring.profiles.active=run \
-DreactDevMode=false \
-Dchemistry.provider=indigo \
-Dchemistry.service.url=http://localhost:8090 \
-Djdbc.url=jdbc:mysql://localhost:3306/rspace \
-Djdbc.db.maven=rspace \
-DRS_FILE_BASE="$RS_FILE_BASE" \
> /tmp/jetty.log 2>&1 &
for i in $(seq 1 180); do
if [ "$(curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/login || true)" = "200" ]; then
echo "Ready after ~$((i*5))s"
exit 0
fi
sleep 5
done
echo "RSpace did not become ready in time"
tail -200 /tmp/jetty.log
exit 1

# sysadmin1's account is a raw SQL fixture row - authenticating purely via
# its API key without ever logging in hits a lazy-initialization bug on
# its first write (home folder isn't created yet). One real login avoids
# it entirely by running the same initialization synchronously and correctly.
- name: Warm up sysadmin1 account
timeout-minutes: 5
run: poetry run python .github/scripts/warmup_sysadmin.py
env:
RSPACE_URL: http://localhost:8080

- name: Run integration tests
timeout-minutes: 20
env:
RSPACE_URL: http://localhost:8080
# Bootstrap sysadmin1 API key seeded by rspace-web's own "dev-test"
# Liquibase context (initial-seed-devtest.sql). Public, non-secret
# dev fixture value - not a credential that protects anything
# outside this ephemeral, job-local RSpace instance.
RSPACE_API_KEY: abcdefghijklmnop12
run: poetry run pytest -m integration -v

- name: Run Unit Test
run: poetry run pytest rspace_client/tests
- name: Dump RSpace server log on failure
if: failure()
run: tail -300 /tmp/jetty.log || true
31 changes: 22 additions & 9 deletions DEVELOPING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## Development

Python 3.7 or later is required. We aim to support only active versions of Python.
Python 3.9 or later is required. We aim to support only active versions of Python.

### Setup

Expand All @@ -21,23 +21,36 @@ to install all project dependencies into your virtual environment.

### Running tests

Tests are a mixture of plain unit tests and integration tests making calls to an RSpace server.
To run all tests, set these environment variables,replacing with your own values
Tests are a mixture of plain unit tests and integration tests that make calls to a live RSpace server.

#### Unit tests only

```
bash> export RSPACE_URL=https:/<your-rspace-domain>
bash> export RSPACE_API_KEY=abcdefgh...
poetry run pytest -m "not integration"
```

If these aren't set, integration tests will be skipped.
#### Integration tests

Integration tests require credentials for a live RSpace instance. Create a `.env` file in the project root:

```
RSPACE_URL=https://<your-rspace-domain>
RSPACE_API_KEY=<your-api-key>
```

Tests can be invoked:
Then run:

```
poetry run pytest rspace_client/tests
poetry run pytest -m integration
```

They should be run with a new RSpace account that does not belong to any groups.
Integration tests should be run with a new RSpace account that does not belong to any groups.

#### How CI runs integration tests

CI doesn't use a long-lived RSpace deployment or your credentials. Each run builds `rspace-web` from source and starts it fresh (Maven/Jetty, seeded database). That gives a known built-in `sysadmin1` account and API key (seeded by rspace-web's own dev/test fixtures), but authenticating purely via API key without ever logging in hits a lazy-initialization bug on that account's first write (its home folder isn't created yet). So CI does one plain HTTP login first (`.github/scripts/warmup_sysadmin.py`), which runs the same initialization correctly, then uses the account's API key directly for the whole suite. See `.github/workflows/codeql-and-tests.yml`.

If you want to reproduce this locally against your own from-source RSpace build (rather than any existing account), log in once as `sysadmin1` / `sysWisc23!` (e.g. run `warmup_sysadmin.py` against your instance) before pointing `RSPACE_API_KEY` at `abcdefghijklmnop12` in your `.env` - otherwise the first document-creation call will 500.

### Writing Tests

Expand Down
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,23 @@ For full details of our API specification, please see https://<YOUR_RSPACE_DOMAI
For example, if you are using `https://community.researchspace.com`,
the API documentation is available at `https://community.researchspace.com/public/apiDocs`

See [DEVELOPING.md](DEVELOPING.md) for details of running tests.
### Testing

CI (`.github/workflows/codeql-and-tests.yml`) runs unit tests across supported Python versions, then integration tests against a real RSpace instance: it builds `rspace-web` from source and starts it with a freshly seeded database (no Docker image, no browser automation), logs in once to initialize the built-in test account, then runs the suite against it.

To run tests locally:

```bash
# unit tests only
poetry run pytest -m "not integration"

# integration tests, against your own RSpace instance
export RSPACE_URL=https://<your-rspace-domain>
export RSPACE_API_KEY=<your-api-key>
poetry run pytest -m integration
```

See [DEVELOPING.md](DEVELOPING.md) for more details.

To install rspace-client and its dependencies, run

Expand Down
8 changes: 7 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,14 @@ setuptools = "<82"
[tool.poetry.group.dev.dependencies]
python-dotenv = "^1.1.1"
black = "^21.6b0"
pytest = "^6.2.4"
pytest = "^8.0.0"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

[tool.pytest.ini_options]
testpaths = ["rspace_client/tests"]
markers = [
"integration: marks tests as integration tests requiring a live RSpace server (deselect with '-m not integration')",
]
2 changes: 0 additions & 2 deletions pytest.ini

This file was deleted.

Loading
Loading