Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
93010caee4
|
|||
|
ded42ccd03
|
|||
|
9255f9b052
|
|||
|
6e22dcdda5
|
|||
|
ef1e42bcbb
|
|||
|
0912c338e3
|
|||
|
0aff62d050
|
|||
|
15bcd258af
|
|||
|
7599093d90
|
|||
|
b495a67bb8
|
|||
|
df8b20edfb
|
|||
|
671e38429c
|
|||
|
ce8165271d
|
|||
|
56570e7b00
|
@@ -1,4 +1,4 @@
|
|||||||
name: Build Debian Package
|
name: Build and Test Debian Package
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -10,7 +10,9 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
deb_filename: ${{ steps.get_deb_name.outputs.filename }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -23,9 +25,38 @@ jobs:
|
|||||||
- name: Build Debian Package
|
- name: Build Debian Package
|
||||||
run: ./release/build-docker.sh
|
run: ./release/build-docker.sh
|
||||||
|
|
||||||
|
- name: Get Deb Filename
|
||||||
|
id: get_deb_name
|
||||||
|
run: |
|
||||||
|
DEB_FILENAME=$(ls output/*.deb | xargs basename)
|
||||||
|
echo "filename=$DEB_FILENAME" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Upload Build Artifact
|
- name: Upload Build Artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: navidrome-uploader-deb
|
name: ${{ steps.get_deb_name.outputs.filename }}
|
||||||
path: output/*.deb
|
path: output/${{ steps.get_deb_name.outputs.filename }}
|
||||||
retention-days: 5
|
retention-days: 5
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build # only run tests if the build job succeeded
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Create output directory
|
||||||
|
run: mkdir -p output
|
||||||
|
|
||||||
|
- name: Download Build Artifact
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ needs.build.outputs.deb_filename }}
|
||||||
|
path: output/
|
||||||
|
|
||||||
|
- name: Build Test Docker Image
|
||||||
|
run: docker build -t uploader-tester -f Dockerfile.test .
|
||||||
|
|
||||||
|
- name: Run E2E Test Suite
|
||||||
|
run: docker run --rm uploader-tester
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Navidome-Uploader Dockerfile for testing .deb packages
|
||||||
|
# Arian Nasr
|
||||||
|
# May 31, 2026
|
||||||
|
|
||||||
|
FROM debian:stable
|
||||||
|
|
||||||
|
# Prevent interactive prompts
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl \
|
||||||
|
python3 \
|
||||||
|
python3-venv \
|
||||||
|
python3-pip \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy the built deb package into the container
|
||||||
|
COPY output/*.deb /tmp/navidrome-uploader.deb
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y -f /tmp/navidrome-uploader.deb && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN mkdir -p /opt/navidrome/music && \
|
||||||
|
chown -R navidrome-uploader:navidrome-uploader /opt/navidrome/music
|
||||||
|
|
||||||
|
COPY e2e/ /e2e/
|
||||||
|
|
||||||
|
RUN python3 -m venv /e2e/venv && \
|
||||||
|
/e2e/venv/bin/pip install --no-cache-dir -r /e2e/requirements.txt
|
||||||
|
|
||||||
|
RUN chmod +x /e2e/test-entrypoint.sh
|
||||||
|
|
||||||
|
WORKDIR /e2e
|
||||||
|
|
||||||
|
ENTRYPOINT ["/e2e/test-entrypoint.sh"]
|
||||||
Vendored
+17
@@ -1,3 +1,20 @@
|
|||||||
|
navidrome-uploader (0.3.0) unstable; urgency=medium
|
||||||
|
|
||||||
|
* E2E Testing:
|
||||||
|
- Add Dockerfile and entrypoint script for testing .deb packages
|
||||||
|
- Implement initial API functional tests with pytest configuration
|
||||||
|
- Update CI workflow to run E2E tests automatically after package build
|
||||||
|
* Packaging:
|
||||||
|
- Change architecture from 'all' to 'amd64'
|
||||||
|
* CI/Build:
|
||||||
|
- Implement automated Gitea workflow for .deb building
|
||||||
|
* Refactoring:
|
||||||
|
- Remove unused templates
|
||||||
|
* Maintenance:
|
||||||
|
- Update Dropzone.js upstream source
|
||||||
|
|
||||||
|
-- Arian Nasr <arian@2ari.ca> Sun, 31 May 2026 08:43:00 -0400
|
||||||
|
|
||||||
navidrome-uploader (0.2.0) unstable; urgency=medium
|
navidrome-uploader (0.2.0) unstable; urgency=medium
|
||||||
|
|
||||||
* Packaging:
|
* Packaging:
|
||||||
|
|||||||
Vendored
+1
-1
@@ -7,6 +7,6 @@ Standards-Version: 4.7.0
|
|||||||
Rules-Requires-Root: no
|
Rules-Requires-Root: no
|
||||||
|
|
||||||
Package: navidrome-uploader
|
Package: navidrome-uploader
|
||||||
Architecture: all
|
Architecture: amd64
|
||||||
Depends: ${misc:Depends}, adduser, python3, python3-venv, python3-pip, python3-wheel
|
Depends: ${misc:Depends}, adduser, python3, python3-venv, python3-pip, python3-wheel
|
||||||
Description: Navidrome Web Upload Utility
|
Description: Navidrome Web Upload Utility
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Werkzeug==3.1.8
|
||||||
|
pytest==9.0.3
|
||||||
|
requests==2.34.2
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
export NAVIDROME_MUSIC_FOLDER="/opt/navidrome/music"
|
||||||
|
export BASE_URL="http://127.0.0.1:5001"
|
||||||
|
|
||||||
|
# Start the app manually since systemd isn't running in the container
|
||||||
|
cd /opt/navidrome-uploader
|
||||||
|
su -s /bin/sh navidrome-uploader -c "/opt/navidrome-uploader/venv/bin/gunicorn --no-control-socket -c gunicorn.conf.py main:app" &
|
||||||
|
|
||||||
|
APP_PID=$!
|
||||||
|
|
||||||
|
TIMEOUT=30
|
||||||
|
SERVICE_UP=0
|
||||||
|
|
||||||
|
while [ $TIMEOUT -gt 0 ]; do
|
||||||
|
if curl -s $BASE_URL/ping | grep -q "pong"; then
|
||||||
|
SERVICE_UP=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
TIMEOUT=$((TIMEOUT-1))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $SERVICE_UP -eq 0 ]; then
|
||||||
|
echo "Error: Service failed to start within the timeout period"
|
||||||
|
kill $APP_PID
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd /e2e
|
||||||
|
|
||||||
|
/e2e/venv/bin/pytest unit/api/test_api.py
|
||||||
|
|
||||||
|
# Capture the exit code of pytest
|
||||||
|
TEST_EXIT_CODE=$?
|
||||||
|
|
||||||
|
kill $APP_PID
|
||||||
|
|
||||||
|
exit $TEST_EXIT_CODE
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import pytest
|
||||||
|
import requests
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_ping(base_url):
|
||||||
|
response = requests.get(f'{base_url}/ping')
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.text == 'pong'
|
||||||
|
|
||||||
|
def test_api_upload_non_audio_file(base_url, upload_folder):
|
||||||
|
files = {'file': ('test.txt', io.BytesIO(b'not an audio file'))}
|
||||||
|
expected_filename = os.path.join(upload_folder, secure_filename('test.txt'))
|
||||||
|
response = requests.post(f'{base_url}/', files=files)
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert "not allowed" in response.json().get("error", "")
|
||||||
|
assert not os.path.exists(expected_filename)
|
||||||
|
|
||||||
|
if os.path.exists(expected_filename):
|
||||||
|
os.remove(expected_filename)
|
||||||
|
|
||||||
|
def test_api_upload_mp3_file(base_url, upload_folder):
|
||||||
|
files = {'file': ('test.mp3', io.BytesIO(b'fake mp3 content'))}
|
||||||
|
expected_filename = os.path.join(upload_folder, secure_filename('test.mp3'))
|
||||||
|
response = requests.post(f'{base_url}/', files=files)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "uploaded successfully" in response.json().get("message", "")
|
||||||
|
assert os.path.exists(expected_filename)
|
||||||
|
|
||||||
|
if os.path.exists(expected_filename):
|
||||||
|
os.remove(expected_filename)
|
||||||
|
|
||||||
|
def test_api_upload_flac_file(base_url, upload_folder):
|
||||||
|
files = {'file': ('test.flac', io.BytesIO(b'fake flac content'))}
|
||||||
|
expected_filename = os.path.join(upload_folder, secure_filename('test.flac'))
|
||||||
|
response = requests.post(f'{base_url}/', files=files)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "uploaded successfully" in response.json().get("message", "")
|
||||||
|
assert os.path.exists(expected_filename)
|
||||||
|
|
||||||
|
if os.path.exists(expected_filename):
|
||||||
|
os.remove(expected_filename)
|
||||||
|
|
||||||
|
def test_api_upload_m4a_file(base_url, upload_folder):
|
||||||
|
files = {'file': ('test.m4a', io.BytesIO(b'fake m4a content'))}
|
||||||
|
expected_filename = os.path.join(upload_folder, secure_filename('test.m4a'))
|
||||||
|
response = requests.post(f'{base_url}/', files=files)
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert "not allowed" in response.json().get("error", "")
|
||||||
|
assert not os.path.exists(expected_filename)
|
||||||
|
|
||||||
|
if os.path.exists(expected_filename):
|
||||||
|
os.remove(expected_filename)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import pytest
|
||||||
|
import os
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def base_url():
|
||||||
|
return os.getenv('BASE_URL', 'http://127.0.0.1:5001')
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def upload_folder():
|
||||||
|
return os.getenv('NAVIDROME_MUSIC_FOLDER', '/opt/navidrome/music')
|
||||||
+1
-1
@@ -6,5 +6,5 @@ Jinja2==3.1.6
|
|||||||
MarkupSafe==3.0.3
|
MarkupSafe==3.0.3
|
||||||
Werkzeug==3.1.8
|
Werkzeug==3.1.8
|
||||||
gunicorn==26.0.0
|
gunicorn==26.0.0
|
||||||
pip==26.1.1
|
pip==26.1.2
|
||||||
packaging==26.2
|
packaging==26.2
|
||||||
|
|||||||
Reference in New Issue
Block a user