diff --git a/.gitattributes b/.gitattributes index 35833a7..738e196 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,2 @@ test_client.html linguist-documentation=true -static/index.html linguist-documentation=true -static/script.js linguist-documentation=true -static/style.css linguist-documentation=true +docs/* linguist-documentation=true \ No newline at end of file diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..cad901b --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,33 @@ +name: CI + +on: + workflow_dispatch: + push: + branches-ignore: [ master ] + pull_request: + branches-ignore: [ master ] + +jobs: + test: + name: Run tests and collect coverage and linting + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run tests + run: pytest --cov=app tests/ --cov-report=xml --junitxml=junit.xml -o junit_family=legacy --cov-report=term-missing + diff --git a/.github/workflows/GH-pages.yml b/.github/workflows/GH-pages.yml new file mode 100644 index 0000000..d9d6f5c --- /dev/null +++ b/.github/workflows/GH-pages.yml @@ -0,0 +1,43 @@ +# Simple workflow for deploying static content to GitHub Pages +name: Deploy static content to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["master"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + # Upload entire repository + path: './docs' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml deleted file mode 100644 index bc28452..0000000 --- a/.github/workflows/ci-cd.yml +++ /dev/null @@ -1,152 +0,0 @@ -name: CI/CD - -on: - push: - branches: [ master, pre-release ] - pull_request: - branches: [ master, pre-release ] - -jobs: - get-version: - name: Get versions (main-stage-devVersion) - runs-on: ubuntu-latest - - outputs: - versionMain: ${{ steps.get_version.outputs.versionMain }} - versionDevStage: ${{ steps.get_version.outputs.versionDevStage }} - versionDevVersion: ${{ steps.get_version.outputs.versionDevVersion }} - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Get version - id: get_version - run: | - versionMain=$(grep -oP "version=\"\K[0-9.]+" app/main.py || echo "NO_MAIN_VERSION") - versionDevStage=$(grep -oP 'version=\"\K[0-9.]+-\K[a-zA-Z]+' app/main.py || echo "NO_DEV_STAGE") - versionDevVersion=$(grep -oP 'version=\"\K[0-9.]+-\K[a-zA-Z]+-\K[a-zA-Z0-9.]+' app/main.py || echo "NO_DEV_VERSION") - echo "versionMain=$versionMain" >> $GITHUB_OUTPUT - echo "versionDevStage=$versionDevStage" >> $GITHUB_OUTPUT - echo "versionDevVersion=$versionDevVersion" >> $GITHUB_OUTPUT - - - name: Check Main Version - run: | - if [ "${{ steps.get_version.outputs.versionMain }}" == "NO_MAIN_VERSION" ]; then - echo "Fail: invalid main version (missing)" - exit 1 - fi - - - name: Check Version for Production - if: github.event.pull_request.base.ref == 'master' - run: | - changelogVersion=$(grep "\[${{ steps.get_version.outputs.versionMain }}\]" CHANGES.md || echo "NO_VERSION") - if [ "$changelogVersion" == "NO_VERSION" ]; then - echo "Version ${{ steps.get_version.outputs.versionMain }} not found in changelog" - exit 1 - fi - if [ "${{ steps.get_version.outputs.versionDevStage }}" != "NO_DEV_STAGE" ]; then - echo "No development stage allowed in production" - exit 1 - fi - if [ "${{ steps.get_version.outputs.versionDevVersion }}" != "NO_DEV_VERSION" ]; then - echo "No development version allowed in production" - exit 1 - fi - - - name: Check Dev Stage - if: github.event.pull_request.base.ref == 'pre-release' - run: | - if [ "${{ steps.get_version.outputs.versionDevStage }}" == "NO_DEV_STAGE" ]; then - echo "Fail: invalid development stage (missing)" - exit 1 - fi - - test: - name: Run tests and collect coverage and linting - needs: get-version - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.13' - cache: 'pip' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - - - name: Run tests - run: pytest --cov=app tests/ --cov-report=xml --junitxml=junit.xml -o junit_family=legacy --cov-report=term-missing - - - name: Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - - - name: Upload results to Codecov - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} - - deploy: - needs: [get-version, test] - if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/pre-release' - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} - - - name: Build and push Docker images (master) - if: github.ref == 'refs/heads/master' && needs.get-version.outputs.versionDevStage == 'NO_DEV_STAGE' && needs.get-version.outputs.versionDevVersion == 'NO_DEV_VERSION' - uses: docker/build-push-action@v6 - with: - push: true - tags: | - ${{ secrets.DOCKER_USERNAME }}/kofi-websocket:latest - ${{ secrets.DOCKER_USERNAME }}/kofi-websocket:${{ needs.get-version.outputs.versionMain }} - - - name: Build and push Docker images (pre-release with subversion) - if: github.ref == 'refs/heads/pre-release' && needs.get-version.outputs.versionDevStage != 'NO_DEV_STAGE' && needs.get-version.outputs.versionDevVersion != 'NO_DEV_VERSION' - uses: docker/build-push-action@v6 - with: - push: true - tags: | - ${{ secrets.DOCKER_USERNAME }}/kofi-websocket:dev - ${{ secrets.DOCKER_USERNAME }}/kofi-websocket:dev-${{ needs.get-version.outputs.versionMain }} - ${{ secrets.DOCKER_USERNAME }}/kofi-websocket:dev-${{ needs.get-version.outputs.versionMain }}-${{ needs.get-version.outputs.versionDevStage }} - ${{ secrets.DOCKER_USERNAME }}/kofi-websocket:dev-${{ needs.get-version.outputs.versionMain }}-${{ needs.get-version.outputs.versionDevStage }}-${{ needs.get-version.outputs.versionDevVersion }} - - - name: Build and push Docker images (pre-release without subversion) - if: github.ref == 'refs/heads/pre-release' && needs.get-version.outputs.versionDevStage != 'NO_DEV_STAGE' && needs.get-version.outputs.versionDevVersion == 'NO_DEV_VERSION' - uses: docker/build-push-action@v6 - with: - push: true - tags: | - ${{ secrets.DOCKER_USERNAME }}/kofi-websocket:dev - ${{ secrets.DOCKER_USERNAME }}/kofi-websocket:dev-${{ needs.get-version.outputs.versionMain }} - ${{ secrets.DOCKER_USERNAME }}/kofi-websocket:dev-${{ needs.get-version.outputs.versionMain }}-${{ needs.get-version.outputs.versionDevStage }} \ No newline at end of file diff --git a/.github/workflows/publish-ghcr.yml b/.github/workflows/publish-ghcr.yml new file mode 100644 index 0000000..4d9096f --- /dev/null +++ b/.github/workflows/publish-ghcr.yml @@ -0,0 +1,107 @@ +name: Docker Image ghcr.io Publish + +# on: [push] + +on: + workflow_dispatch: + push: + branches: + - 'master' + tags: + - 'v*' + pull_request: + branches: + - 'master' + +env: + REGISTRY: ghcr.io + USERNAME: ${{ github.actor }} + IMAGE_NAME: ${{ github.repository }} + +jobs: + tests: + name: Run tests and collect coverage and linting + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run tests + run: pytest --cov=app tests/ --cov-report=xml --junitxml=junit.xml -o junit_family=legacy --cov-report=term-missing + + - name: Upload test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/test-results-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + + - name: Upload results to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + + build_and_publish: + runs-on: ubuntu-latest + needs: tests + + permissions: + contents: read + packages: write + attestations: write + id-token: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + # with: + # fetch-depth: 0 + + # - name: Set up QEMU + # uses: docker/setup-qemu-action@v3 + + # - name: Set up Docker Buildx + # uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ env.USERNAME }} + password: ${{ secrets.GH_PAT_GHCR }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + + - name: Build and push Docker image + id: push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v2 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true diff --git a/.vscode/settings.json b/.vscode/settings.json index 3480667..43cc7ee 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,21 +1,10 @@ { - "files.exclude": { - "**/.git": true, - "**/.svn": true, - "**/.hg": true, - "**/CVS": true, - "**/.DS_Store": true, - "**/Thumbs.db": true - }, - "hide-files.files": [], - "python.testing.pytestArgs": [ - "tests" - ], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true, - "cSpell.words": [ - "Iconify", - "kofi", - "pytest" - ] -} \ No newline at end of file + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/.DS_Store": true, + "**/Thumbs.db": true + }, + "hide-files.files": [] +} diff --git a/CHANGES.md b/CHANGES.md index 13d922a..07b85b7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.2.0] - 2025-05-15 + +### Changes in 1.2.0 + +- Moved the documentation to the `docs` folder, to be served by github pages. +- Image Repository is now ghcr.io instead of dockerhub + +- CI/CD: + - Reduction of automated builds of images (ref. [here](https://github.com/docker/metadata-action?tab=readme-ov-file#basic)) + - The tags are now automatically generated + ## [1.1.0] - 2025-02-20 ### Changes in 1.1.0 diff --git a/DockerHub_ReadMe.md b/DockerHub_ReadMe.md deleted file mode 100644 index 1698fdb..0000000 --- a/DockerHub_ReadMe.md +++ /dev/null @@ -1,88 +0,0 @@ -# KoFi WebSocket Bridge - -A FastAPI application that bridges KoFi webhooks to WebSocket connections, enabling real-time notifications of KoFi events. - -[![CI/CD](https://github.com/EventKit-Stream/KoFi-WebSocket/actions/workflows/ci-cd.yml/badge.svg?branch=master)](https://github.com/EventKit-Stream/KoFi-WebSocket/actions/workflows/ci-cd.yml) -[![codecov](https://codecov.io/gh/EventKit-Stream/KoFi-WebSocket/graph/badge.svg?token=6MEX17B6J5)](https://codecov.io/gh/EventKit-Stream/KoFi-WebSocket) - -## Features - -- Webhook endpoint for KoFi notifications -- WebSocket endpoint for real-time updates -- Verification token-based security -- Multiple connections with the same token - -## Usage - -When the application is running, you can connect to the WebSocket endpoint using a `verification_token`. The application will forward any webhook data to connected WebSocket clients. This only happens if the provided `verification_token` matches the one in the webhook's data object. - -### WebSocket Endpoint - -Connect to the WebSocket endpoint using a verification token: - -```http -ws://:8000/ws/{verification_token} -``` - -### Webhook Endpoint - -Send a POST request to the webhook endpoint with the required data: - -```http -POST /webhook -Content-Type: application/x-www-form-urlencoded - -{ - "data": { - "verification_token": "your_token", - "some_key": "some_value" - "another_key": "another_value" - } -} -``` - -### The Home Page - -If you navigate to the root of the application, you will see a simple page explaining how to use the application with a built-in websocket client where you can connect to the WebSocket endpoint with any verification token, and test it by sending a test message. - -## How to use this image - -**Using** `docker run` - -```sh -docker run -d --name kofi-websocket -p 8000:8000 --restart unless-stopped lordlumineer/kofi-websocket:latest -``` - -or using `docker-compose`: - -```yaml -services: - api: - container_name: kofi-websocket - image: lordlumineer/kofi-websocket:latest - restart: unless-stopped - ports: - - "8000:8000" -``` - -### Image Variants - -The `lordlumineer/kofi-websocket` images come in multiple variants: - -- Production ready images: - - `:latest`, this is the latest stable release - - `:`, you can specify a version to use a specific release -- Development images: - The development images are not production-ready and should not be used in production environments. They start with `dev` suffix. - - `:dev`, this is the latest development version - - `:dev-`, you can specify a version to use the latest development release for a specific main release - - `:dev--`, by specifying a stage (`alpha`, `beta`, `rc`, or `dev`), you can use a specific development stage for a specific main release - - `:dev---`, by specifying a version, you can use a specific development version for a specific development stage of a specific main release - -## License - -This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. - -## Contact - -For any inquiries or support, please contact [lordlumineer@eventkit.stream](mailto:lordlumineer@eventkit.stream). diff --git a/README.md b/README.md index 183e1ae..ffb0566 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A FastAPI application that bridges KoFi webhooks to WebSocket connections, enabling real-time notifications of KoFi events. -[![CI/CD](https://github.com/EventKit-Stream/KoFi-WebSocket/actions/workflows/ci-cd.yml/badge.svg?branch=master)](https://github.com/EventKit-Stream/KoFi-WebSocket/actions/workflows/ci-cd.yml) +[![CI/CD](https://github.com/EventKit-Stream/KoFi-WebSocket/actions/workflows/publish-ghcr.yml/badge.svg?branch=master)](https://github.com/EventKit-Stream/KoFi-WebSocket/actions/workflows/publish-ghcr.yml) [![codecov](https://codecov.io/gh/EventKit-Stream/KoFi-WebSocket/graph/badge.svg?token=6MEX17B6J5)](https://codecov.io/gh/EventKit-Stream/KoFi-WebSocket) ## Features diff --git a/app/main.py b/app/main.py index 84048f7..07dd2cb 100644 --- a/app/main.py +++ b/app/main.py @@ -24,13 +24,12 @@ import json from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Form from fastapi.exceptions import HTTPException -from fastapi.staticfiles import StaticFiles from starlette.middleware.cors import CORSMiddleware active_connections: dict[str, set[WebSocket]] = defaultdict(set) app = FastAPI( - version="1.1.0", + version="1.2.0", docs_url=None, # Disable Swagger UI redoc_url=None # Disable ReDoc ) @@ -43,13 +42,12 @@ allow_headers=["*"], ) -@app.get("/ping") -async def _ping(): - return {"message": "pong"} - @app.get("/version") async def _version(): + """ + Returns the current version of the FastAPI application as a JSON object. + """ return {"version": app.version} @@ -102,15 +100,9 @@ async def ko_fi_webhook(data: str = Form(...)): @app.websocket("/ws/{verification_token}") async def websocket_endpoint(websocket: WebSocket, verification_token: str): """ - Establishes a WebSocket connection with the client and forwards incoming - Ko-fi webhooks to the corresponding connection. - - The endpoint expects a verification token as a path parameter, which is used - to identify the connection. The endpoint will keep the connection alive by - sending a "pong" response to the "ping" message sent by the client. - - If the connection is closed, the endpoint will remove the connection from the - active connections dictionary. + Handles a WebSocket connection for receiving Ko-fi webhook notifications. + + Establishes a WebSocket connection associated with the provided verification token, maintains connection health via ping/pong messages, and ensures proper cleanup when the connection is closed. """ await websocket.accept() active_connections[verification_token].add(websocket) @@ -124,6 +116,3 @@ async def websocket_endpoint(websocket: WebSocket, verification_token: str): active_connections[verification_token].discard(websocket) if not active_connections[verification_token]: del active_connections[verification_token] - -# Keep it at the end to prevent routing issues -app.mount("/", StaticFiles(directory="static", html=True), name="static") diff --git a/docker-compose.yml b/docker-compose.yml index 28ce3c0..2f20c31 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: api: container_name: kofi-websocket - #image: lordlumineer/kofi-websocket:latest + #image: ghcr.io/eventkit-stream/kofi-websocket:latest build: context: . restart: unless-stopped diff --git a/static/favicon.svg b/docs/assets/favicon.svg similarity index 100% rename from static/favicon.svg rename to docs/assets/favicon.svg diff --git a/static/og_image.png b/docs/images/banner.png similarity index 100% rename from static/og_image.png rename to docs/images/banner.png diff --git a/static/favicon.ico b/docs/images/favicon.ico similarity index 100% rename from static/favicon.ico rename to docs/images/favicon.ico diff --git a/static/icon.png b/docs/images/icon.png similarity index 100% rename from static/icon.png rename to docs/images/icon.png diff --git a/static/logo.png b/docs/images/logo.png similarity index 100% rename from static/logo.png rename to docs/images/logo.png diff --git a/static/index.html b/docs/index.html similarity index 94% rename from static/index.html rename to docs/index.html index f0b7aad..72dc01c 100644 --- a/static/index.html +++ b/docs/index.html @@ -5,7 +5,7 @@ Ko-fi WebSocket - + - - + + + - - - - - - - - + value="Get real-time Ko-fi notifications with WebSockets. Perfect for streamers and content creators." /> + + + + + + + - + value="Get real-time Ko-fi notifications with WebSockets. Perfect for streamers and content creators." /> + @@ -333,6 +333,8 @@

Ko-fi WebSocket Demo

Directly try Ko-fi WebSocket in your browser.

+
diff --git a/static/script.js b/docs/script.js similarity index 95% rename from static/script.js rename to docs/script.js index 484caae..a15f0f1 100644 --- a/static/script.js +++ b/docs/script.js @@ -5,10 +5,12 @@ const CONFIG = { }; // Codes -const ws_code = `${window.location.protocol.replace('http', 'ws')}//${window.location.host}/ws/YOUR_VERIFICATION_TOKEN`; - -const webhook_code = `${window.location.protocol}//${window.location.host}/webhook` +const http_protocol = 'https:'; // window.location.protocol; +const ws_protocol = 'wss:'; // window.location.protocol.replace('http', 'ws'); +const host = 'api-ko-fi.eventkit.stream'; // window.location.host; +const ws_code = `${ws_protocol}//${host}/ws/YOUR_VERIFICATION_TOKEN`; +const webhook_code = `${http_protocol}//${host}/webhook` const ws_connect_code = `const ws = new WebSocket('${ws_code}');` const handle_events_code = ` @@ -41,7 +43,7 @@ const implementation_code = ` } connect() { - this.ws = new WebSocket(\`${window.location.protocol.replace('http', 'ws')}//${window.location.host}/ws/\${this.token}\`); + this.ws = new WebSocket(\`${ws_protocol}//${host}/ws/\${this.token}\`); this.ws.onopen = () => { console.log('Connected to Ko-fi WebSocket'); @@ -232,14 +234,14 @@ class WebSocketDemo { } connectTestSocket() { + const wsUrlBase = document.getElementById('server-input').value.trim(); const token = document.getElementById('token-input').value.trim(); if (!token) { this.writeToTerminal('Please enter a verification token', 'error'); return; } - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${protocol}//${window.location.host}/ws/${token}`; + const wsUrl = `${wsUrlBase}/ws/${token}`; try { this.ws = new WebSocket(wsUrl); @@ -281,7 +283,8 @@ class WebSocketDemo { }; this.ws.onerror = (error) => { - this.writeToTerminal('Failed to connect. Please check your token and try again.', 'error'); + console.error('WebSocket error:', error); + this.writeToTerminal('Failed to connect. Please check the url and/or your token and try again.', 'error'); this.ws.close(); }; } catch (error) { @@ -376,6 +379,7 @@ class WebSocketDemo { updateButtons(connected) { const connectButton = document.getElementById('connect-button'); document.getElementById('send-test-button').disabled = !connected; + document.getElementById('server-input').disabled = connected; document.getElementById('token-input').disabled = connected; connectButton.textContent = connected ? 'Disconnect' : 'Connect'; @@ -461,7 +465,7 @@ class VersionBadgeManager { async init() { try { - const response = await fetch('/version'); + const response = await fetch(`${http_protocol}//${host}/version`); const data = await response.json(); document.getElementById('version-badge').textContent = 'v' + data.version; } catch (error) { diff --git a/static/style.css b/docs/style.css similarity index 100% rename from static/style.css rename to docs/style.css diff --git a/tests/test_main.py b/tests/test_main.py index 387dcce..60ee6f0 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -146,7 +146,11 @@ async def mock_send_json(*args, **kwargs): @pytest.mark.asyncio async def test_webhook_websocket_disconnect(client, sample_webhook_data): - """Test webhook WebSocket disconnect.""" + """ + Tests that when a WebSocket disconnect occurs during webhook processing, the connection is removed from active connections. + + Opens a WebSocket connection, mocks the send_json method to raise a disconnect, posts webhook data, and verifies the connection is cleaned up. + """ async def mock_send_json(*args, **kwargs): raise WebSocketDisconnect() @@ -166,15 +170,13 @@ async def mock_send_json(*args, **kwargs): WebSocket.send_json = original_send_json -def test_ping_endpoint(client): - """Test the ping endpoint.""" - response = client.get("/ping") - assert response.status_code == 200 - assert response.json() == {"message": "pong"} - - def test_version_endpoint(client): - """Test the version endpoint.""" + """ + Tests that the /version endpoint returns the correct application version. + + Sends a GET request to /version and asserts the response status is 200 and the JSON + payload contains the expected version string. + """ response = client.get("/version") assert response.status_code == 200 assert response.json() == {"version": app.version}