Add NextSnap PWA with photo gallery viewer and continuous capture

Offline-first photo capture app for Nextcloud with:
- Camera capture with continuous mode (auto-reopens after each photo)
- File browser with fullscreen image gallery, swipe navigation, and rename
- Upload queue with background sync engine
- Admin panel for Nextcloud user management
- Service worker for offline-first caching (v13)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-07 04:53:13 -06:00
commit cad4118f72
55 changed files with 9038 additions and 0 deletions

1
tests/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Tests package

133
tests/test_auth.py Normal file
View File

@@ -0,0 +1,133 @@
import pytest
from app import create_app
from unittest.mock import patch, MagicMock
import tempfile
import os
@pytest.fixture
def app():
"""Create application for testing."""
app = create_app('development')
app.config['TESTING'] = True
# Use simple session for testing
app.config['SECRET_KEY'] = 'test-secret-key'
app.config['SESSION_TYPE'] = 'null' # Disable server-side sessions for testing
return app
@pytest.fixture
def client(app):
"""Create test client with session support."""
with app.test_client() as client:
with client.session_transaction() as sess:
# Initialize session
pass
yield client
def test_login_missing_credentials(client):
"""Test login with missing credentials."""
response = client.post('/api/auth/login', json={})
assert response.status_code == 400
data = response.get_json()
assert 'error' in data
def test_login_empty_credentials(client):
"""Test login with empty credentials."""
response = client.post('/api/auth/login', json={'username': '', 'password': ''})
assert response.status_code == 400
data = response.get_json()
assert 'error' in data
@patch('app.routes.auth.NextcloudClient')
def test_login_invalid_credentials(mock_nc_client, client):
"""Test login with invalid credentials."""
# Mock NextcloudClient to return False for verify_credentials
mock_instance = MagicMock()
mock_instance.verify_credentials.return_value = False
mock_nc_client.return_value = mock_instance
response = client.post('/api/auth/login', json={'username': 'testuser', 'password': 'wrongpass'})
assert response.status_code == 401
data = response.get_json()
assert 'error' in data
@patch('app.routes.auth.NextcloudClient')
def test_login_success(mock_nc_client, client):
"""Test successful login."""
# Mock NextcloudClient to return True for verify_credentials
mock_instance = MagicMock()
mock_instance.verify_credentials.return_value = True
mock_instance.check_admin.return_value = False
mock_nc_client.return_value = mock_instance
response = client.post('/api/auth/login', json={'username': 'testuser', 'password': 'testpass'})
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
assert data['username'] == 'testuser'
assert data['is_admin'] is False
@patch('app.routes.auth.NextcloudClient')
def test_login_admin_user(mock_nc_client, client):
"""Test login with admin user."""
mock_instance = MagicMock()
mock_instance.verify_credentials.return_value = True
mock_instance.check_admin.return_value = True
mock_nc_client.return_value = mock_instance
response = client.post('/api/auth/login', json={'username': 'admin', 'password': 'adminpass'})
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
assert data['is_admin'] is True
def test_status_not_authenticated(client):
"""Test status endpoint when not authenticated."""
response = client.get('/api/auth/status')
assert response.status_code == 200
data = response.get_json()
assert data['authenticated'] is False
@patch('app.routes.auth.NextcloudClient')
def test_status_authenticated(mock_nc_client, client):
"""Test status endpoint when authenticated."""
# First login
mock_instance = MagicMock()
mock_instance.verify_credentials.return_value = True
mock_instance.check_admin.return_value = False
mock_nc_client.return_value = mock_instance
client.post('/api/auth/login', json={'username': 'testuser', 'password': 'testpass'})
# Check status
response = client.get('/api/auth/status')
assert response.status_code == 200
data = response.get_json()
assert data['authenticated'] is True
assert data['username'] == 'testuser'
@patch('app.routes.auth.NextcloudClient')
def test_logout(mock_nc_client, client):
"""Test logout functionality."""
# First login
mock_instance = MagicMock()
mock_instance.verify_credentials.return_value = True
mock_instance.check_admin.return_value = False
mock_nc_client.return_value = mock_instance
client.post('/api/auth/login', json={'username': 'testuser', 'password': 'testpass'})
# Verify logged in
response = client.get('/api/auth/status')
data = response.get_json()
assert data['authenticated'] is True
# Logout
response = client.post('/api/auth/logout')
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
# Verify logged out
response = client.get('/api/auth/status')
data = response.get_json()
assert data['authenticated'] is False

22
tests/test_health.py Normal file
View File

@@ -0,0 +1,22 @@
import pytest
from app import create_app
@pytest.fixture
def app():
"""Create application for testing."""
app = create_app('development')
app.config['TESTING'] = True
return app
@pytest.fixture
def client(app):
"""Create test client."""
return app.test_client()
def test_health_endpoint(client):
"""Test the health check endpoint."""
response = client.get('/api/health')
assert response.status_code == 200
data = response.get_json()
assert data['status'] == 'healthy'
assert data['service'] == 'nextsnap'

195
tests/test_webdav_proxy.py Normal file
View File

@@ -0,0 +1,195 @@
import pytest
from app import create_app
from unittest.mock import patch, MagicMock
import base64
@pytest.fixture
def app():
"""Create application for testing."""
app = create_app('development')
app.config['TESTING'] = True
app.config['SECRET_KEY'] = 'test-secret-key'
app.config['SESSION_TYPE'] = 'null'
return app
@pytest.fixture
def client(app):
"""Create test client with authenticated session."""
with app.test_client() as client:
# Set up authenticated session
with client.session_transaction() as sess:
sess['username'] = 'testuser'
sess['password'] = base64.b64encode(b'testpass').decode()
sess['is_admin'] = False
yield client
def test_list_files_not_authenticated(app):
"""Test list files without authentication."""
with app.test_client() as client:
response = client.get('/api/files/list?path=/')
assert response.status_code == 401
@patch('app.routes.webdav_proxy.NextcloudClient')
def test_list_files_success(mock_nc_client, client):
"""Test successful file listing."""
mock_instance = MagicMock()
mock_instance.propfind.return_value = {
'items': [
{'name': 'Documents', 'path': '/Documents', 'type': 'directory', 'size': 0},
{'name': 'photo.jpg', 'path': '/photo.jpg', 'type': 'file', 'size': 12345}
],
'path': '/',
'count': 2
}
mock_nc_client.return_value = mock_instance
response = client.get('/api/files/list?path=/')
assert response.status_code == 200
data = response.get_json()
assert 'items' in data
assert len(data['items']) == 2
assert data['items'][0]['name'] == 'Documents'
assert data['items'][1]['name'] == 'photo.jpg'
@patch('app.routes.webdav_proxy.NextcloudClient')
def test_upload_file_success(mock_nc_client, client):
"""Test successful file upload."""
mock_instance = MagicMock()
mock_instance.put_file.return_value = {
'success': True,
'url': 'http://nextcloud/remote.php/dav/files/testuser/photo.jpg'
}
mock_nc_client.return_value = mock_instance
# Test with form data
response = client.post(
'/api/files/upload?path=/uploads',
data={
'path': '/uploads',
'file': (BytesIO(b'fake image data'), 'photo.jpg')
},
content_type='multipart/form-data'
)
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
assert 'photo.jpg' in data['path']
def test_upload_file_no_path(client):
"""Test upload without target path."""
response = client.post('/api/files/upload')
assert response.status_code == 400
data = response.get_json()
assert 'error' in data
@patch('app.routes.webdav_proxy.NextcloudClient')
def test_verify_file_exists(mock_nc_client, client):
"""Test file verification - file exists."""
mock_instance = MagicMock()
mock_instance.head.return_value = True
mock_nc_client.return_value = mock_instance
response = client.get('/api/files/verify?path=/photo.jpg')
assert response.status_code == 200
data = response.get_json()
assert data['exists'] is True
@patch('app.routes.webdav_proxy.NextcloudClient')
def test_verify_file_not_exists(mock_nc_client, client):
"""Test file verification - file does not exist."""
mock_instance = MagicMock()
mock_instance.head.return_value = False
mock_nc_client.return_value = mock_instance
response = client.get('/api/files/verify?path=/nonexistent.jpg')
assert response.status_code == 404
data = response.get_json()
assert data['exists'] is False
def test_verify_file_no_path(client):
"""Test verification without path."""
response = client.get('/api/files/verify')
assert response.status_code == 400
data = response.get_json()
assert 'error' in data
@patch('app.routes.webdav_proxy.NextcloudClient')
def test_mkdir_success(mock_nc_client, client):
"""Test successful directory creation."""
mock_instance = MagicMock()
mock_instance.mkcol.return_value = True
mock_nc_client.return_value = mock_instance
response = client.post(
'/api/files/mkdir',
json={'path': '/NewFolder'}
)
assert response.status_code == 201
data = response.get_json()
assert data['success'] is True
assert data['path'] == '/NewFolder'
def test_mkdir_no_path(client):
"""Test mkdir without path."""
response = client.post('/api/files/mkdir', json={})
assert response.status_code == 400
data = response.get_json()
assert 'error' in data
@patch('app.routes.webdav_proxy.NextcloudClient')
def test_rename_file_success(mock_nc_client, client):
"""Test successful file rename."""
mock_instance = MagicMock()
mock_instance.move.return_value = True
mock_nc_client.return_value = mock_instance
response = client.post(
'/api/files/rename',
json={
'source': '/old_name.jpg',
'destination': '/new_name.jpg'
}
)
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
assert data['source'] == '/old_name.jpg'
assert data['destination'] == '/new_name.jpg'
def test_rename_file_missing_params(client):
"""Test rename without required parameters."""
response = client.post(
'/api/files/rename',
json={'source': '/old_name.jpg'}
)
assert response.status_code == 400
data = response.get_json()
assert 'error' in data
@patch('app.routes.webdav_proxy.NextcloudClient')
@patch('app.routes.webdav_proxy.Image')
def test_thumbnail_success(mock_image, mock_nc_client, client):
"""Test successful thumbnail generation."""
# Mock Nextcloud client
mock_instance = MagicMock()
mock_instance.get_file.return_value = b'fake image data'
mock_nc_client.return_value = mock_instance
# Mock PIL Image
mock_img = MagicMock()
mock_img.mode = 'RGB'
mock_image.open.return_value = mock_img
response = client.get('/api/files/thumbnail?path=/photo.jpg&size=256')
assert response.status_code == 200
assert response.content_type == 'image/jpeg'
def test_thumbnail_no_path(client):
"""Test thumbnail without path."""
response = client.get('/api/files/thumbnail')
assert response.status_code == 400
data = response.get_json()
assert 'error' in data
# Import BytesIO for upload test
from io import BytesIO