mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-04 12:33:03 +00:00
Fix CI/CD pipeline and code quality issues
## Major Changes - Fixed all TypeScript errors in web client for successful compilation - Resolved 82+ Python lint errors across backend services - Updated Flutter SDK compatibility for mobile app - Fixed security workflow configuration ## Web Client Fixes - Fixed import path in DragonflyDashboard.vue (dragonflyApi import) - All TypeScript compilation now passes without errors ## Backend Lint Fixes - Updated type annotations to modern Python syntax (dict instead of Dict, X | None instead of Optional[X]) - Replaced try-except-pass with contextlib.suppress(Exception) - Removed unused imports (Dict, Optional, Any, Iterator, etc.) - Fixed bare except clauses to use Exception - Sorted and formatted imports with ruff - Applied ruff format to 27 files ## Workflow Fixes - Updated Flutter SDK constraint from ^3.10.4 to ^3.5.0 (compatible with Flutter 3.24.0) - Changed pip-audit format from github to json in security.yml - Added comprehensive CI workflows (readiness-gate.yml, security.yml) ## Infrastructure - Added DragonflyDB caching system integration - Enhanced Docker configuration with multi-stage builds - Added pytest configuration and test infrastructure - Improved production readiness with proper error handling ## Verification - backend-lint job: ✅ Succeeded - web job: ✅ Succeeded - Ready for GitHub deployment All CI/CD issues resolved. Codebase now passes all quality checks.
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
"""
|
||||
Authentication integration tests.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
|
||||
class TestBootstrap:
|
||||
"""Tests for the bootstrap/owner setup flow."""
|
||||
|
||||
def test_bootstrap_status_initial(self, client):
|
||||
"""Bootstrap status should show setup required when no users exist."""
|
||||
response = client.get("/auth/bootstrap/status")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.get_json()
|
||||
assert "setup_completed" in data
|
||||
|
||||
def test_bootstrap_owner_creates_first_user(self, client):
|
||||
"""Bootstrap owner should create the first admin user."""
|
||||
response = client.post("/auth/bootstrap/owner", json={
|
||||
"username": "owner",
|
||||
"password": "securepassword123",
|
||||
"root_dirs": [],
|
||||
})
|
||||
|
||||
# Should succeed (201) or fail if already exists (400)
|
||||
assert response.status_code in [200, 201, 400]
|
||||
|
||||
if response.status_code in [200, 201]:
|
||||
data = response.get_json()
|
||||
assert "accesstoken" in data
|
||||
assert "refreshtoken" in data
|
||||
|
||||
def test_bootstrap_owner_requires_username(self, client):
|
||||
"""Bootstrap owner should require username."""
|
||||
response = client.post("/auth/bootstrap/owner", json={
|
||||
"password": "securepassword123",
|
||||
})
|
||||
assert response.status_code == 422 # Validation error
|
||||
|
||||
def test_bootstrap_owner_requires_password(self, client):
|
||||
"""Bootstrap owner should require password."""
|
||||
response = client.post("/auth/bootstrap/owner", json={
|
||||
"username": "testowner",
|
||||
})
|
||||
assert response.status_code == 422 # Validation error
|
||||
|
||||
|
||||
class TestLogin:
|
||||
"""Tests for the login endpoint."""
|
||||
|
||||
def test_login_requires_username(self, client):
|
||||
"""Login should require username."""
|
||||
response = client.post("/auth/login", json={
|
||||
"password": "testpassword",
|
||||
})
|
||||
assert response.status_code == 422
|
||||
|
||||
def test_login_requires_password(self, client):
|
||||
"""Login should require password."""
|
||||
response = client.post("/auth/login", json={
|
||||
"username": "testuser",
|
||||
})
|
||||
assert response.status_code == 422
|
||||
|
||||
def test_login_nonexistent_user(self, client):
|
||||
"""Login should fail for nonexistent user."""
|
||||
response = client.post("/auth/login", json={
|
||||
"username": "nonexistent_user_12345",
|
||||
"password": "testpassword",
|
||||
})
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_login_wrong_password(self, client):
|
||||
"""Login should fail with wrong password."""
|
||||
# First ensure owner exists
|
||||
client.post("/auth/bootstrap/owner", json={
|
||||
"username": "owner",
|
||||
"password": "correctpassword123",
|
||||
"root_dirs": [],
|
||||
})
|
||||
|
||||
response = client.post("/auth/login", json={
|
||||
"username": "owner",
|
||||
"password": "wrongpassword",
|
||||
})
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
class TestPairCode:
|
||||
"""Tests for the pairing code flow (mobile app login)."""
|
||||
|
||||
def test_get_pair_code_requires_auth(self, client):
|
||||
"""Pair code generation should require authentication."""
|
||||
response = client.get("/auth/getpaircode")
|
||||
assert response.status_code in [401, 422] # Unauthorized or validation error
|
||||
|
||||
def test_pair_with_invalid_code(self, client):
|
||||
"""Pairing with invalid code should fail."""
|
||||
response = client.get("/auth/pair?code=INVALID")
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_pair_with_empty_code(self, client):
|
||||
"""Pairing with empty code should fail."""
|
||||
response = client.get("/auth/pair?code=")
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
class TestInviteFlow:
|
||||
"""Tests for the invite/accept user onboarding flow."""
|
||||
|
||||
def test_create_invite_requires_admin(self, client):
|
||||
"""Create invite should require admin role."""
|
||||
response = client.post("/auth/invite/create", json={
|
||||
"roles": ["user"],
|
||||
})
|
||||
assert response.status_code in [401, 403]
|
||||
|
||||
def test_accept_invite_requires_token(self, client):
|
||||
"""Accept invite should require a token."""
|
||||
response = client.post("/auth/invite/accept", json={
|
||||
"username": "newuser",
|
||||
"password": "newpassword123",
|
||||
})
|
||||
assert response.status_code == 422
|
||||
|
||||
def test_accept_invite_invalid_token(self, client):
|
||||
"""Accept invite should fail with invalid token."""
|
||||
response = client.post("/auth/invite/accept", json={
|
||||
"token": "invalid_token_12345",
|
||||
"username": "newuser",
|
||||
"password": "newpassword123",
|
||||
})
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
class TestTokenRefresh:
|
||||
"""Tests for token refresh functionality."""
|
||||
|
||||
def test_refresh_requires_token(self, client):
|
||||
"""Refresh should require a refresh token."""
|
||||
response = client.post("/auth/refresh")
|
||||
assert response.status_code in [401, 422]
|
||||
|
||||
def test_refresh_with_invalid_token(self, client):
|
||||
"""Refresh should fail with invalid token."""
|
||||
response = client.post(
|
||||
"/auth/refresh",
|
||||
headers={"Authorization": "Bearer invalid_token"},
|
||||
)
|
||||
assert response.status_code in [401, 422]
|
||||
|
||||
|
||||
class TestAuthContract:
|
||||
"""Contract tests for authentication API."""
|
||||
|
||||
def test_login_response_schema(self, client):
|
||||
"""Login response should match expected schema."""
|
||||
# Create owner first
|
||||
client.post("/auth/bootstrap/owner", json={
|
||||
"username": "contractowner",
|
||||
"password": "contractpass123",
|
||||
"root_dirs": [],
|
||||
})
|
||||
|
||||
response = client.post("/auth/login", json={
|
||||
"username": "contractowner",
|
||||
"password": "contractpass123",
|
||||
})
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.get_json()
|
||||
assert "accesstoken" in data
|
||||
assert "refreshtoken" in data
|
||||
assert "msg" in data
|
||||
assert isinstance(data["accesstoken"], str)
|
||||
assert isinstance(data["refreshtoken"], str)
|
||||
|
||||
def test_pair_code_response_schema(self, client):
|
||||
"""Pair code response should match expected schema."""
|
||||
# Create owner and login
|
||||
client.post("/auth/bootstrap/owner", json={
|
||||
"username": "paircodeowner",
|
||||
"password": "paircodepass123",
|
||||
"root_dirs": [],
|
||||
})
|
||||
|
||||
login_response = client.post("/auth/login", json={
|
||||
"username": "paircodeowner",
|
||||
"password": "paircodepass123",
|
||||
})
|
||||
|
||||
if login_response.status_code == 200:
|
||||
token = login_response.get_json().get("accesstoken")
|
||||
response = client.get(
|
||||
"/auth/getpaircode",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.get_json()
|
||||
assert "code" in data
|
||||
assert "expires_at" in data
|
||||
assert "server_url" in data
|
||||
assert isinstance(data["code"], str)
|
||||
assert len(data["code"]) == 6 # 6-character code
|
||||
Reference in New Issue
Block a user