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
10 changes: 5 additions & 5 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '22'
cache: 'npm'
cache-dependency-path: package-lock.json

Expand Down Expand Up @@ -97,7 +97,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '22'
cache: 'npm'
cache-dependency-path: package-lock.json

Expand Down Expand Up @@ -136,7 +136,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '22'
cache: 'npm'
cache-dependency-path: package-lock.json

Expand Down Expand Up @@ -218,7 +218,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '22'
cache: 'npm'
cache-dependency-path: package-lock.json

Expand Down Expand Up @@ -279,7 +279,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '22'
cache: 'npm'
cache-dependency-path: package-lock.json

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/commitlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '22'

- name: Install commitlint
run: |
Expand Down
42 changes: 40 additions & 2 deletions .github/workflows/sonar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,49 @@ on:
- main
pull_request:
types: [opened, synchronize, reopened]
schedule:
- cron: '0 6 * * 1' # Every Monday 6:00 UTC
jobs:
sonarqube:
name: SonarQube
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
fetch-depth: 0

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'

- name: Generate backend coverage
run: |
pip install -e apps/backend
pip install -r apps/backend/requirements-dev.txt
cd apps/backend
pytest --cov=opencloudtouch --cov-report=xml:../../coverage-backend.xml -q --tb=no
env:
OCT_MOCK_MODE: "true"
OCT_HAS_DEVICES: "false"
CI: "true"

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: package-lock.json

- name: Generate frontend coverage
run: |
npm ci
cd apps/frontend
npx vitest run --coverage --reporter=dot
env:
CI: "true"

- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@v7
with:
Expand All @@ -22,6 +57,9 @@ jobs:
-Dsonar.qualitygate.wait=false
-Dsonar.projectName=opencloudtouch
-Dsonar.sources=apps/backend/src,apps/frontend/src
-Dsonar.exclusions=**/tests/**,**/__tests__/**,**/*.test.*,**/pytest.ini,**/sonar-project.properties
-Dsonar.tests=apps/backend/tests,apps/frontend/tests
-Dsonar.exclusions=**/tests/**,**/__tests__/**,**/*.test.*
-Dsonar.python.coverage.reportPaths=coverage-backend.xml
-Dsonar.javascript.lcov.reportPaths=.out/coverage/frontend/lcov.info
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
4 changes: 2 additions & 2 deletions apps/backend/requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
-r requirements.txt
pytest==8.3.3
pytest-asyncio==0.24.0
pytest==9.0.3
pytest-asyncio==1.3.0
pytest-cov==7.1.0
pytest-timeout==2.4.0
pytest-xdist==3.8.0
Expand Down
6 changes: 6 additions & 0 deletions apps/backend/src/opencloudtouch/bmx/tunein.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import logging
import os
import re
from xml.etree import ElementTree

import httpx
Expand All @@ -17,6 +18,8 @@
TUNEIN_DESCRIBE_URL = "https://opml.radiotime.com/describe.ashx?id=%s"
TUNEIN_STREAM_URL = "http://opml.radiotime.com/Tune.ashx?id=%s&formats=mp3,aac,ogg"

_STATION_ID_RE = re.compile(r"^[a-zA-Z0-9_-]+$")


def get_oct_base_url() -> str:
"""Get OCT backend URL from environment.
Expand Down Expand Up @@ -83,6 +86,9 @@ async def resolve_tunein_station(station_id: str) -> BmxPlaybackResponse:
"""
logger.info(f"[BMX TUNEIN] Resolving station: {station_id}")

if not _STATION_ID_RE.match(station_id):
raise ValueError(f"Invalid station ID format: {station_id}")

try:
async with httpx.AsyncClient(timeout=10.0) as client:
describe_resp = await client.get(TUNEIN_DESCRIBE_URL % station_id)
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/opencloudtouch/devices/health_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ async def stop(self) -> None:
try:
await self._task
except asyncio.CancelledError:
pass
logger.debug("Health-check task cancelled")
self._task = None
logger.info("Device health-check stopped")

Expand All @@ -65,7 +65,7 @@ async def _run(self) -> None:
self._last_ssh_verify = now

except asyncio.CancelledError:
break
raise
except Exception:
logger.exception("Health-check cycle failed")

Expand Down
13 changes: 9 additions & 4 deletions apps/backend/src/opencloudtouch/setup/wizard_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,16 @@ async def wizard_detect_strategy(request: Request) -> DetectStrategyResponse:


def _check_port_443(hostname: str) -> bool:
"""Try an SSL handshake on port 443 to detect a reverse proxy."""
"""Try an SSL handshake on port 443 to detect a reverse proxy.

SSL verification is intentionally disabled: this function only checks
whether *any* service responds on 443, without sending sensitive data.
The server may use a self-signed certificate.
"""
try:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) # NOSONAR - intentional
ctx.check_hostname = False # NOSONAR - port detection only
ctx.verify_mode = ssl.CERT_NONE # NOSONAR - no sensitive data sent
with socket.create_connection((hostname, 443), timeout=3) as sock:
with ctx.wrap_socket(sock, server_hostname=hostname):
return True
Expand Down
Loading
Loading