Version: 2.4.0 Last Updated: 2025-10-12 Maintained By: QA Engineering Team
- Overview
- Getting Started
- Architecture
- Development Guidelines
- Testing Strategy
- Configuration Management
- Authentication System
- Writing Tests
- Troubleshooting Guide
- Best Practices
- Contributing
This is a production-grade API test automation framework for the Spotify Web API. The framework provides comprehensive test coverage across functional, integration, and end-to-end scenarios, with support for both client credentials and OAuth authentication flows.
- Dual Authentication Support: Seamlessly handles both Client Credentials and OAuth 2.0 Authorization Code flows
- Multi-Environment Configuration: QA, Staging, and Production environment support with hierarchical configuration
- Intelligent Test Organization: Marker-based test categorization for flexible execution
- Automatic Token Management: Token refresh and expiration handling built-in
- Comprehensive Error Handling: Retry logic with exponential backoff and rate limiting support
- Production-Ready Reporting: Allure integration with detailed test reports
- Language: Python 3.9+
- Test Framework: Pytest 8.3.2
- HTTP Client: Requests with retry mechanisms
- Configuration: YAML-based hierarchical config
- Reporting: Allure, HTML, JUnit XML
- Security: Environment variable-based credential management
# Python 3.9 or higher
python --version
# pip package manager
pip --version# Clone the repository
git clone <repository-url>
cd spotify_api_automation_enhanced_2_4
# Create virtual environment (recommended)
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install -r requirements.txt- Copy environment template:
cp .env.example .env- Configure credentials in
.env:
SPOTIFY_CLIENT_ID=your_client_id_here
SPOTIFY_CLIENT_SECRET=your_client_secret_here
SPOTIFY_REDIRECT_URI=http://127.0.0.1:8080/callback
ENVIRONMENT=qa- Verify setup:
python verify_config.pyYou should see all configuration checks passing.
# Run functional tests (no OAuth required)
pytest tests/functional/albums/test_album_retrieval.py -v
# Expected output:
# β test_get_album_by_id_success PASSED
# β test_get_multiple_albums_success PASSEDβββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Test Layer β
β (tests/functional, integration, e2e) β
ββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββ
β Pytest Fixtures (conftest.py) β
β - spotify_client (auth flow selection) β
β - test_config (environment configuration) β
β - test_user_data (dynamic user data) β
ββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββ
β SpotifyAPIClient (orchestrator) β
β libs/spotify_client.py β
ββββββ¬βββββββββββ¬βββββββββββ¬βββββββββββ¬ββββββββββββ¬ββββ
β β β β β
ββββββΌβββββ ββββΌββββ ββββββΌβββββ ββββΌβββββ βββββΌβββββ
β Albums β βTracksβ βPlaylistsβ βSearch β β Users β
β Client β βClientβ β Client β βClient β βClient β
ββββββ¬βββββ ββββ¬ββββ ββββββ¬βββββ βββββ¬ββββ βββββ¬βββββ
β β β β β
ββββββββββββ΄βββββββββββ΄ββββββββββββ΄ββββββββββ
β
βββββββββββββββΌββββββββββββββ
β SpotifyBaseClient β
β libs/base_client.py β
β (HTTP operations, retry) β
βββββββββββββββ¬ββββββββββββββ
β
βββββββββββββββΌββββββββββββββ
β SpotifyAuthManager β
β libs/auth.py β
β (authentication bridge) β
βββββββββββββββ¬ββββββββββββββ
β
ββββββββββββββββββ΄βββββββββββββββββ
β β
βββββββββΌββββββββββ βββββββββββββΌβββββββββββ
β Client β β OAuth Handler β
β Credentials β β (authorization_code) β
β Flow β β + Token Refresh β
βββββββββββββββββββ ββββββββββββββββββββββββ
The main entry point for all API interactions. Orchestrates authentication and provides organized access to endpoint-specific clients.
# Initialization
client = SpotifyAPIClient(config, auth_flow='client_credentials')
client.authenticate()
# Access endpoint clients
album = client.albums.get_album("album_id")
tracks = client.search.search("query", ["track"])
profile = client.users.get_current_user_profile()Domain-specific clients that encapsulate endpoint logic:
- AlbumsClient: Album retrieval, multiple albums
- TracksClient: Track details, audio features
- PlaylistsClient: Create, update, delete playlists
- SearchClient: Search across different types
- UsersClient: User profile, playlists
Handles HTTP operations with built-in:
- Retry logic (exponential backoff for 5xx errors)
- Rate limiting (respects Retry-After header)
- Error handling (custom exceptions)
- Request/response logging
Dual authentication architecture:
# SpotifyAuthManager (libs/auth.py)
# - Orchestrates authentication flows
# - Authentication bridge pattern
# - Token validation and refresh
# SpotifyAdvancedAuth (libs/advanced_auth.py)
# - OAuth 2.0 implementation
# - Browser-based authorization
# - Token storage in .auth_cache/Key Innovation: Authentication Bridge
# libs/auth.py:397-439
def _load_tokens_from_advanced_auth(self):
"""Load OAuth tokens from advanced_auth storage."""
# Reads tokens from .auth_cache/tokens_{hash}.json
# Transfers to SpotifyOAuthHandler for unified token managementHierarchical configuration with three layers:
base.yaml (defaults)
β
environment.yaml (qa/staging/prod overrides)
β
Environment variables (runtime overrides)
We follow PEP 8 with these specific conventions:
# Function naming: snake_case
def get_album_by_id(album_id: str) -> Dict[str, Any]:
pass
# Class naming: PascalCase
class SpotifyAPIClient:
pass
# Constants: UPPER_SNAKE_CASE
MAX_RETRIES = 3
DEFAULT_TIMEOUT = 30
# Private methods: _leading_underscore
def _handle_error(self, response):
passAll public functions must include type hints:
from typing import Dict, List, Optional, Any
def search(
self,
query: str,
types: List[str],
limit: int = 20,
offset: int = 0
) -> Dict[str, Any]:
"""Search for items on Spotify.
Args:
query: Search query string
types: List of item types to search
limit: Maximum number of results (1-50)
offset: Index of first result to return
Returns:
Dict containing search results
Raises:
SpotifyAPIError: If search request fails
"""
passWe use Google-style docstrings:
def authenticate(self, flow_type: str = 'client_credentials') -> bool:
"""Authenticate with Spotify API.
This method handles both client credentials and OAuth flows.
It automatically refreshes expired tokens when refresh_token
is available.
Args:
flow_type: Authentication flow type.
- 'client_credentials': Server-to-server auth
- 'authorization_code': User-specific auth with OAuth
Returns:
True if authentication successful, False otherwise
Raises:
SpotifyAuthError: If authentication fails and cannot recover
Example:
>>> client = SpotifyAPIClient(config)
>>> client.authenticate(flow_type='client_credentials')
True
"""
passUse comments to explain "why", not "what":
# Good: Explains reasoning
# Wait for rate limit to reset before retrying
# Respecting Retry-After header prevents further 429 errors
retry_after = int(response.headers.get('Retry-After', 5))
time.sleep(retry_after)
# Bad: States the obvious
# Get retry after header
retry_after = int(response.headers.get('Retry-After', 5))# libs/exceptions.py (if not exists, create)
class SpotifyFrameworkError(Exception):
"""Base exception for framework errors."""
pass
class SpotifyAPIError(SpotifyFrameworkError):
"""API request failed."""
def __init__(self, message: str, status_code: int = None):
self.status_code = status_code
super().__init__(message)
class SpotifyAuthError(SpotifyFrameworkError):
"""Authentication failed."""
pass
class SpotifyConfigError(SpotifyFrameworkError):
"""Configuration invalid or missing."""
passdef make_api_request(self):
"""Standard error handling pattern."""
try:
response = requests.get(url, timeout=30)
response.raise_for_status()
return response.json()
except requests.exceptions.Timeout:
logger.error(f"Request timeout after 30s: {url}")
raise SpotifyAPIError("Request timed out")
except requests.exceptions.HTTPError as e:
if e.response.status_code == 401:
raise SpotifyAuthError("Invalid or expired token")
elif e.response.status_code == 429:
retry_after = e.response.headers.get('Retry-After', 60)
raise SpotifyAPIError(f"Rate limited. Retry after {retry_after}s")
else:
raise SpotifyAPIError(f"HTTP {e.response.status_code}: {e}")
except Exception as e:
logger.exception("Unexpected error in API request")
raise SpotifyAPIError(f"Unexpected error: {e}")from utils.logger import get_logger
logger = get_logger(__name__) # Use module name
# Log levels
logger.debug("Token validation successful") # Verbose, development
logger.info("Authenticating with client_credentials") # General info
logger.warning("Token expires in 60s, consider refresh") # Warnings
logger.error("Authentication failed: invalid credentials") # Errors
logger.critical("Cannot load configuration, exiting") # Critical failures# Good: Structured, actionable information
logger.info(
f"API request: {method} {endpoint} "
f"(status={response.status_code}, duration={duration:.2f}s)"
)
# Good: Include context for debugging
logger.error(
f"Failed to retrieve album: {album_id}",
extra={'user_id': user_id, 'flow': auth_flow}
)
# Bad: Not enough context
logger.info("Request sent")
# Bad: Too verbose, clutters logs
logger.debug(f"Response: {json.dumps(response.json(), indent=2)}")tests/
βββ functional/ # Public API tests (Client Credentials)
β βββ albums/
β β βββ test_album_retrieval.py
β β βββ test_multiple_albums.py
β βββ search/
β β βββ test_search_tracks.py
β βββ tracks/
β βββ test_track_retrieval.py
β
βββ integration/ # User-specific tests (OAuth)
β βββ test_user_playlists.py
β βββ test_user_profile.py
β
βββ e2e/ # Complete workflows (OAuth)
βββ test_playlist_workflow.py
Markers control test execution and authentication:
# pytest.ini
[pytest]
markers =
functional: Functional tests for individual endpoints
integration: Integration tests for workflows
e2e: End-to-end tests for complete scenarios
requires_user_auth: Tests requiring OAuth authentication
client_credentials_only: Tests using client credentials
smoke: Quick smoke tests
slow: Long-running tests# Run all functional tests
pytest -m functional -v
# Run tests that need OAuth
pytest -m requires_user_auth -v
# Run smoke tests only
pytest -m smoke -v
# Combine markers (AND)
pytest -m "functional and not slow" -v
# Combine markers (OR)
pytest -m "integration or e2e" -v
# Exclude marker
pytest -m "not slow" -vUnderstanding fixture scopes is critical for performance:
# Session scope: Loaded once per test session
@pytest.fixture(scope="session")
def test_config():
"""Configuration loaded once and reused."""
return load_config()
# Function scope: Created for each test
@pytest.fixture(scope="function")
def spotify_client(request, test_config):
"""Fresh client per test for isolation."""
# Authentication happens for EVERY test
# Good for isolation, slower execution
return SpotifyAPIClient(test_config)
# Module scope: Created once per test file
@pytest.fixture(scope="module")
def shared_test_data():
"""Loaded once per test module."""
return load_test_data("albums.json")Performance Note: Current implementation uses function-scoped spotify_client, causing ~30-95s per test due to repeated authentication. Consider session-scoped fixtures for read-only tests.
# TestData/test_albums.json
{
"albums": [
{
"id": "4LH4d3cOWNNsVw41Gqt2kv",
"name": "The Dark Side of the Moon",
"artist": "Pink Floyd"
}
]
}
# Load in tests
def test_get_album(spotify_client):
test_data = FileHelper.load_json_file("TestData/test_albums.json")
album_id = test_data['albums'][0]['id']
album = spotify_client.albums.get_album(album_id)
assert album['name'] == test_data['albums'][0]['name']@pytest.fixture
def test_user_data(spotify_client):
"""Get real authenticated user data."""
if spotify_client.auth_flow == 'authorization_code':
profile = spotify_client.users.get_current_user_profile()
return {'user_id': profile['id']}
return {'user_id': None}The ConfigLoader implements a three-layer configuration system:
# Layer 1: Base configuration (config/base.yaml)
api:
base_url: "https://api.spotify.com/v1"
timeout: 30
max_retries: 3
# Layer 2: Environment configuration (config/qa.yaml)
api:
timeout: 45 # Override for QA
# Layer 3: Environment variables
SPOTIFY_API__TIMEOUT=60 # Highest priorityFormat: SPOTIFY_<SECTION>__<KEY>
# Set API base URL
SPOTIFY_API__BASE_URL=https://api.spotify.com/v2
# Set authentication credentials
SPOTIFY_AUTH__CLIENT_ID=your_client_id
SPOTIFY_AUTH__CLIENT_SECRET=your_client_secret
# Set nested values (double underscore for nesting)
SPOTIFY_TEST__CLEANUP_AFTER_TESTS=false# config/base.yaml - Common settings
api:
base_url: "https://api.spotify.com/v1"
timeout: 30
max_retries: 3
retry_backoff_factor: 1.0
auth:
client_id: ${SPOTIFY_CLIENT_ID}
client_secret: ${SPOTIFY_CLIENT_SECRET}
redirect_uri: ${SPOTIFY_REDIRECT_URI}
test:
cleanup_after_tests: true
parallel_execution: false
logging:
level: INFO
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"# config/qa.yaml - QA overrides
api:
timeout: 45 # Longer timeout in QA
max_retries: 5 # More retries in QA
logging:
level: DEBUG # Verbose logging in QA
test:
cleanup_after_tests: true# config/prod.yaml - Production overrides
api:
timeout: 20 # Shorter timeout in prod
max_retries: 1 # Fail fast in prod
logging:
level: WARNING # Minimal logging in prod
test:
cleanup_after_tests: false # Keep data for investigation# Method 1: Set in .env file
echo "ENVIRONMENT=staging" >> .env
pytest tests/
# Method 2: Environment variable
ENVIRONMENT=prod pytest tests/
# Method 3: In code
config = load_config(environment='staging')The framework implements a sophisticated dual authentication system that seamlessly handles both Client Credentials and OAuth flows.
Use Case: Server-to-server authentication, public API access
Flow:
- Send client_id + client_secret to token endpoint
- Receive access_token (valid for ~3600s)
- Use token for API requests
- Get new token when expired (no refresh_token)
Implementation:
# Automatic in conftest.py
@pytest.fixture
def spotify_client(test_config):
# Default flow if no @pytest.mark.requires_user_auth
client = SpotifyAPIClient(test_config, auth_flow='client_credentials')
client.authenticate()
return clientUse Case: User-specific operations requiring permissions
Flow:
- Redirect user to Spotify authorization page
- User grants permissions
- Receive authorization code
- Exchange code for access_token + refresh_token
- Use access_token for API requests
- Refresh token when expired (automatic)
Implementation:
# Triggered by marker
@pytest.mark.requires_user_auth
def test_user_profile(spotify_client):
# Automatically uses authorization_code flow
profile = spotify_client.users.get_current_user_profile()
assert 'id' in profileProblem Solved: How to integrate external OAuth token generation with framework authentication?
Solution: Authentication Bridge Pattern
# libs/auth.py:397-439
class SpotifyAuthManager:
def _load_tokens_from_advanced_auth(self):
"""Bridge to load OAuth tokens from external storage.
This method reads tokens stored by manual_token_setup.py
or oauth_helper.py (using AdvancedSpotifyAuth) and loads
them into SpotifyOAuthHandler for unified token management.
"""
# Find token file in .auth_cache/
token_files = Path('.auth_cache').glob('tokens_*.json')
for token_file in token_files:
token_data = json.loads(token_file.read_text())
# Extract tokens
tokens = token_data.get('tokens', {})
# Transfer to OAuth handler
self.oauth_handler._store_token_data(tokens)
logger.info("Loaded OAuth tokens from advanced_auth")
return True
return FalseFlow Diagram:
Manual Token Setup / OAuth Helper
β
advanced_auth.py
β
.auth_cache/tokens_{hash}.json
β
SpotifyAuthManager._load_tokens_from_advanced_auth()
β
SpotifyOAuthHandler (unified token management)
β
Tests (transparent token access)
# libs/auth.py - SpotifyOAuthHandler
def get_valid_token(self) -> str:
"""Get valid access token, refreshing if necessary."""
# Step 1: Check current token
if self.is_token_valid():
return self.access_token
# Step 2: Token expired, attempt refresh
if self.refresh_token:
logger.info("Token expired, refreshing...")
self.refresh_access_token()
return self.access_token
# Step 3: No refresh token available
raise SpotifyAuthError(
"Token expired and no refresh_token available. "
"Run manual_token_setup.py to re-authenticate."
)
def is_token_valid(self) -> bool:
"""Check if token is still valid (with 60s buffer)."""
if not self.access_token or not self.token_expires_at:
return False
# 60-second buffer prevents mid-request expiration
return time.time() < (self.token_expires_at - 60){
"tokens": {
"access_token": "BQC8X9y...",
"refresh_token": "AQBz...",
"token_type": "Bearer",
"expires_in": 3600,
"token_expires_at": 1728750000.0
},
"metadata": {
"client_id": "7b5dcaac78ea4358b7b9a6f982551df3",
"scopes": ["user-read-private", "user-read-email"],
"created_at": 1728746400.0
},
"stored_at": 1728746400.0,
"client_id_hash": "e4f2a1b..."
}python manual_token_setup.pyInteractive prompts:
Enter access_token: [paste token]
Enter refresh_token (optional): [paste token]
Enter token_type (default: Bearer): [press enter]
Enter expires_in seconds (default: 3600): [press enter]
Token sources:
- Postman OAuth 2.0 flow
- Spotify Developer Dashboard
- curl command with OAuth flow
- Any OAuth 2.0 client
python oauth_helper.pyAutomated flow:
- Opens browser to Spotify authorization page
- User logs in and grants permissions
- Callback handled by local server (port 8080)
- Tokens automatically saved to
.auth_cache/
"""
tests/functional/albums/test_album_operations.py
Functional tests for album retrieval operations.
These tests use client_credentials flow (no OAuth required).
"""
import pytest
from typing import Dict, Any
class TestAlbumRetrieval:
"""Test suite for album retrieval operations."""
def test_get_single_album_success(self, spotify_client):
"""Verify successful retrieval of a single album.
Test validates:
- Album can be retrieved by valid ID
- Response contains all required fields
- Data types are correct
"""
# Arrange
album_id = "4LH4d3cOWNNsVw41Gqt2kv" # Pink Floyd - Dark Side
# Act
album = spotify_client.albums.get_album(album_id)
# Assert
assert album['id'] == album_id
assert 'name' in album
assert 'artists' in album
assert isinstance(album['artists'], list)
assert len(album['artists']) > 0
def test_get_album_invalid_id_returns_404(self, spotify_client):
"""Verify 404 error for invalid album ID.
Negative test to ensure proper error handling
for non-existent resources.
"""
# Arrange
invalid_id = "INVALID_ID_12345"
# Act & Assert
with pytest.raises(SpotifyAPIError) as exc_info:
spotify_client.albums.get_album(invalid_id)
assert exc_info.value.status_code == 404
@pytest.mark.parametrize("album_id,expected_name", [
("4LH4d3cOWNNsVw41Gqt2kv", "The Dark Side of the Moon"),
("1DFixLWuPkv3KT3TnV35m3", "Thriller"),
("6DEjYFkNZh67HP7R9PSZvv", "Abbey Road"),
])
def test_get_album_multiple_ids(
self,
spotify_client,
album_id: str,
expected_name: str
):
"""Verify album retrieval for multiple known albums.
Parametrized test to validate consistent behavior
across different albums.
"""
# Act
album = spotify_client.albums.get_album(album_id)
# Assert
assert album['name'] == expected_name
assert album['type'] == 'album'"""
tests/integration/test_user_playlists.py
Integration tests for user playlist operations.
These tests require OAuth authentication with user context.
"""
import pytest
import time
class TestUserPlaylists:
"""Test suite for user playlist operations."""
@pytest.mark.requires_user_auth # Triggers OAuth flow
def test_create_playlist_success(self, spotify_client, test_user_data):
"""Verify user can create a new playlist.
Prerequisites:
- Valid OAuth token with playlist-modify-public scope
- Authenticated user context
Test validates:
- Playlist creation succeeds
- Created playlist has correct properties
- Playlist is owned by authenticated user
"""
# Arrange
user_id = test_user_data['user_id']
playlist_name = f"Test_Playlist_{int(time.time())}"
# Act
playlist = spotify_client.playlists.create_playlist(
user_id=user_id,
name=playlist_name,
public=False,
description="Created by automated test"
)
# Assert
assert playlist['name'] == playlist_name
assert playlist['public'] is False
assert playlist['owner']['id'] == user_id
# Cleanup
spotify_client.playlists.unfollow_playlist(playlist['id'])
@pytest.mark.requires_user_auth
def test_get_user_playlists(self, spotify_client, test_user_data):
"""Verify user can retrieve their playlists.
Test validates:
- User playlists can be retrieved
- Response contains pagination metadata
- Each playlist has required fields
"""
# Arrange
user_id = test_user_data['user_id']
# Act
response = spotify_client.playlists.get_user_playlists(
user_id=user_id,
limit=20
)
# Assert
assert 'items' in response
assert 'total' in response
assert 'limit' in response
assert response['limit'] == 20
# Validate each playlist
for playlist in response['items']:
assert 'id' in playlist
assert 'name' in playlist
assert 'owner' in playlist"""
tests/e2e/test_playlist_workflow.py
End-to-end test for complete playlist management workflow.
"""
import pytest
import time
@pytest.mark.requires_user_auth
class TestPlaylistWorkflow:
"""Complete workflow tests for playlist operations."""
def test_complete_playlist_lifecycle(self, spotify_client, test_user_data):
"""Test complete playlist lifecycle: create β update β add tracks β delete.
This E2E test validates the entire user workflow for
managing playlists, ensuring all operations work together.
"""
user_id = test_user_data['user_id']
# Step 1: Create playlist
playlist_name = f"E2E_Test_{int(time.time())}"
playlist = spotify_client.playlists.create_playlist(
user_id=user_id,
name=playlist_name,
public=False
)
playlist_id = playlist['id']
assert playlist['name'] == playlist_name
# Step 2: Update playlist details
updated_name = f"{playlist_name}_Updated"
spotify_client.playlists.update_playlist(
playlist_id=playlist_id,
name=updated_name,
description="Updated description"
)
updated_playlist = spotify_client.playlists.get_playlist(playlist_id)
assert updated_playlist['name'] == updated_name
assert updated_playlist['description'] == "Updated description"
# Step 3: Add tracks to playlist
track_uris = [
"spotify:track:3n3Ppam7vgaVa1iaRUc9Lp", # Example track
"spotify:track:0VjIjW4GlUZAMYd2vXMi3b"
]
spotify_client.playlists.add_tracks(
playlist_id=playlist_id,
track_uris=track_uris
)
# Verify tracks added
tracks = spotify_client.playlists.get_playlist_tracks(playlist_id)
assert len(tracks['items']) == 2
# Step 4: Delete playlist (cleanup)
spotify_client.playlists.unfollow_playlist(playlist_id)
# Verify deletion (should return 404)
with pytest.raises(SpotifyAPIError) as exc:
spotify_client.playlists.get_playlist(playlist_id)
assert exc.value.status_code == 404# conftest.py or test file
@pytest.fixture
def sample_album_ids():
"""Provide sample album IDs for testing."""
return [
"4LH4d3cOWNNsVw41Gqt2kv", # Pink Floyd
"1DFixLWuPkv3KT3TnV35m3", # Michael Jackson
"6DEjYFkNZh67HP7R9PSZvv", # The Beatles
]
def test_get_multiple_albums(spotify_client, sample_album_ids):
"""Test using fixture data."""
albums = spotify_client.albums.get_multiple_albums(sample_album_ids)
assert len(albums['albums']) == 3@pytest.fixture
def cleanup_playlists(spotify_client):
"""Automatically cleanup created playlists after test."""
created_playlists = []
# Return function to register playlists for cleanup
def register_playlist(playlist_id: str):
created_playlists.append(playlist_id)
yield register_playlist
# Cleanup after test
for playlist_id in created_playlists:
try:
spotify_client.playlists.unfollow_playlist(playlist_id)
print(f"Cleaned up playlist: {playlist_id}")
except Exception as e:
print(f"Cleanup failed: {e}")
# Usage
@pytest.mark.requires_user_auth
def test_with_cleanup(spotify_client, test_user_data, cleanup_playlists):
playlist = spotify_client.playlists.create_playlist(
test_user_data['user_id'], "Test"
)
cleanup_playlists(playlist['id']) # Register for cleanup
# Test logic...
# Playlist automatically deleted after testSymptom: Integration/E2E tests fail with authentication error
Cause: Missing OAuth tokens for authorization_code flow
Solution:
# Run manual token setup
python manual_token_setup.py
# Or use OAuth helper
python oauth_helper.pyVerify: Check .auth_cache/ directory has tokens_*.json file with refresh_token field
Symptom: Tests fail after some time with token expiration error
Cause: Token file missing refresh_token or token_expires_at field
Solution:
# Re-run token setup with complete token data
python manual_token_setup.py
# Ensure you provide:
# - access_token (required)
# - refresh_token (required for auto-refresh)
# - expires_in (default: 3600)Verify:
# Check token file format
import json
with open('.auth_cache/tokens_<hash>.json') as f:
data = json.load(f)
assert 'refresh_token' in data['tokens']
assert 'token_expires_at' in data['tokens']Symptom: ConfigurationError: Environment config file not found: config/qa.yaml
Cause: Missing environment configuration file
Solution:
# Check ENVIRONMENT setting
echo $ENVIRONMENT # Should be qa, staging, or prod
# Verify config file exists
ls config/qa.yaml
# Create from template if missing
cp config/base.yaml config/qa.yamlSymptom: Each test takes 30+ seconds to run
Cause: Per-test authentication overhead (function-scoped fixture)
Temporary Workaround:
# Run tests in parallel
pytest tests/functional/ -n 4 # 4 workersPermanent Solution (requires code change):
# conftest.py - Add session-scoped fixture
@pytest.fixture(scope="session")
def shared_client(test_config):
"""Shared client for read-only tests."""
client = SpotifyAPIClient(test_config, auth_flow='client_credentials')
client.authenticate()
return client
# Use in read-only tests
def test_get_album(shared_client):
album = shared_client.albums.get_album("album_id")Symptom: Tests fail with rate limiting errors
Cause: Exceeded Spotify API rate limits
Solution:
# Run tests with delay
pytest tests/ --dist loadscope -n 2 # Reduce workers
# Add delay between requests (in code)
import time
time.sleep(1) # Add 1s delay between testsFramework Handles This: Automatic retry with Retry-After header
Symptom: UnicodeEncodeError: 'charmap' codec can't encode character
Cause: Windows console doesn't support Unicode by default
Solution: Already fixed in verify_config.py with UTF-8 wrapper
For other scripts:
import sys
import io
if sys.platform == 'win32':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')# Method 1: Set log level in config
# config/qa.yaml
logging:
level: DEBUG
# Method 2: Environment variable
SPOTIFY_LOG_LEVEL=DEBUG pytest tests/
# Method 3: Pytest flag
pytest tests/ --log-cli-level=DEBUG# Quick script to inspect tokens
import json
from pathlib import Path
token_files = Path('.auth_cache').glob('tokens_*.json')
for token_file in token_files:
print(f"\n{token_file.name}:")
data = json.loads(token_file.read_text())
print(f" Access Token: {'*' * 20} (present)")
print(f" Refresh Token: {data['tokens'].get('refresh_token', 'MISSING')}")
print(f" Expires At: {data['tokens'].get('token_expires_at', 'MISSING')}")# Quick test script
from libs.spotify_client import SpotifyAPIClient
from utils.config_loader import load_config
config = load_config('qa')
client = SpotifyAPIClient(config, auth_flow='client_credentials')
client.authenticate()
# Test specific endpoint
album = client.albums.get_album("4LH4d3cOWNNsVw41Gqt2kv")
print(album['name']) # Should print album name-
Use Descriptive Test Names
# Good def test_get_album_with_valid_id_returns_album_data(spotify_client): pass # Bad def test_album(spotify_client): pass
-
Follow AAA Pattern (Arrange, Act, Assert)
def test_search_tracks(spotify_client): # Arrange - Setup test data query = "Pink Floyd" search_types = ["track"] # Act - Execute the operation results = spotify_client.search.search(query, search_types) # Assert - Verify the outcome assert 'tracks' in results assert len(results['tracks']['items']) > 0
-
Test One Thing Per Test
# Good - Single responsibility def test_album_has_id(spotify_client): album = spotify_client.albums.get_album("album_id") assert 'id' in album def test_album_has_name(spotify_client): album = spotify_client.albums.get_album("album_id") assert 'name' in album # Bad - Multiple assertions for different concerns def test_album(spotify_client): album = spotify_client.albums.get_album("album_id") assert 'id' in album assert 'name' in album assert 'type' in album # If this fails, we don't know about the others
-
Use Fixtures for Common Setup
@pytest.fixture def created_playlist(spotify_client, test_user_data): """Create and return a test playlist.""" playlist = spotify_client.playlists.create_playlist( test_user_data['user_id'], "Test Playlist" ) yield playlist # Cleanup spotify_client.playlists.unfollow_playlist(playlist['id'])
-
Parametrize for Multiple Scenarios
@pytest.mark.parametrize("status_code,exception_type", [ (401, SpotifyAuthError), (403, SpotifyAuthError), (404, SpotifyAPIError), (500, SpotifyAPIError), ]) def test_error_handling(spotify_client, mocker, status_code, exception_type): mocker.patch('requests.request', return_value=Mock(status_code=status_code)) with pytest.raises(exception_type): spotify_client.albums.get_album("test_id")
-
Keep endpoint clients focused
# Good - AlbumsClient only handles album operations class AlbumsClient: def get_album(self, album_id: str) -> Dict: pass def get_multiple_albums(self, album_ids: List[str]) -> Dict: pass # Bad - Mixed responsibilities class AlbumsClient: def get_album(self, album_id: str) -> Dict: pass def search_albums(self, query: str) -> Dict: # Should be in SearchClient pass
-
Use constants for magic strings
# constants.py class SpotifyEndpoints: ALBUMS = "/albums" TRACKS = "/tracks" SEARCH = "/search" class HTTPMethods: GET = "GET" POST = "POST" PUT = "PUT" DELETE = "DELETE" # Usage response = self.make_request(HTTPMethods.GET, SpotifyEndpoints.ALBUMS)
-
Keep configuration DRY
# Good - Base config with environment overrides # base.yaml - Common settings api: timeout: 30 # qa.yaml - QA-specific only api: timeout: 45 # Bad - Duplicate everything # qa.yaml api: timeout: 45 max_retries: 3 # Duplicated from base retry_backoff_factor: 1.0 # Duplicated from base
-
Never commit credentials
# Verify .gitignore .env .auth_cache/ *.key *.pem credentials.json
-
Use environment-specific credentials
# QA credentials SPOTIFY_CLIENT_ID=qa_client_id # Prod credentials (different) SPOTIFY_CLIENT_ID=prod_client_id
-
Mask sensitive data in logs
# Good logger.info(f"Authenticated with client_id: {client_id[:8]}***") # Bad logger.info(f"Authenticated with token: {access_token}")
-
Set file permissions for token cache
# Restrict .auth_cache permissions import os from pathlib import Path cache_dir = Path(".auth_cache") cache_dir.mkdir(mode=0o700, exist_ok=True) # Owner-only access token_file = cache_dir / "tokens.json" os.chmod(token_file, 0o600) # Read/write by owner only
-
Create feature branch
git checkout -b feature/add-artist-endpoint
-
Make changes
- Write code following style guide
- Add tests for new functionality
- Update documentation
-
Run tests locally
# Run all tests pytest tests/ -v # Run specific tests pytest tests/functional/artists/ -v # Check code quality black . flake8 . mypy libs/
-
Commit changes
git add . git commit -m "feat: add artist endpoint client"
-
Push and create PR
git push origin feature/add-artist-endpoint # Create pull request on GitHub
Follow conventional commits format:
<type>(<scope>): <subject>
<body>
<footer>
Types:
feat: New featurefix: Bug fixdocs: Documentation changesstyle: Code style changes (formatting, etc.)refactor: Code refactoringtest: Adding or updating testschore: Maintenance tasks
Examples:
feat(albums): add support for album tracks pagination
fix(auth): correct token expiry calculation
Updated token validation to include 60-second buffer
docs(readme): update installation instructions
test(search): add negative tests for invalid queries
refactor(config): simplify configuration loading logicFor Reviewers:
- Code follows style guide (PEP 8)
- All functions have type hints
- Tests added for new functionality
- Tests pass locally
- Documentation updated (if needed)
- No hardcoded credentials or secrets
- Error handling is comprehensive
- Logging is appropriate (not too verbose)
For Contributors:
- Ran
black .for formatting - Ran
flake8 .with no errors - All tests pass:
pytest tests/ -v - Added docstrings to new functions
- Updated CHANGELOG.md (if applicable)
- No merge conflicts with main branch
Step 1: Create endpoint client file
# libs/endpoints/artists.py
from libs.base_client import SpotifyBaseClient
from typing import Dict, List, Optional
class ArtistsClient:
"""Client for Spotify Artists API endpoints."""
def __init__(self, base_client: SpotifyBaseClient):
"""Initialize Artists client.
Args:
base_client: Base client for HTTP operations
"""
self.base_client = base_client
def get_artist(self, artist_id: str) -> Dict:
"""Get Spotify artist by ID.
Args:
artist_id: Spotify artist ID
Returns:
Dict containing artist data
Raises:
SpotifyAPIError: If request fails
"""
endpoint = f"/artists/{artist_id}"
return self.base_client.make_request("GET", endpoint)
def get_artist_top_tracks(
self,
artist_id: str,
market: str = "US"
) -> Dict:
"""Get artist's top tracks.
Args:
artist_id: Spotify artist ID
market: Market code (e.g., "US", "GB")
Returns:
Dict containing top tracks
Raises:
SpotifyAPIError: If request fails
"""
endpoint = f"/artists/{artist_id}/top-tracks"
params = {"market": market}
return self.base_client.make_request("GET", endpoint, params=params)Step 2: Add to SpotifyAPIClient
# libs/spotify_client.py
from libs.endpoints.artists import ArtistsClient
class SpotifyAPIClient:
def __init__(self, config, auth_flow='client_credentials'):
# ... existing initialization ...
# Add artist client
self.artists = ArtistsClient(self.base_client)Step 3: Create tests
# tests/functional/artists/test_artist_retrieval.py
import pytest
class TestArtistRetrieval:
"""Test suite for artist retrieval operations."""
def test_get_artist_by_id(self, spotify_client):
"""Verify artist can be retrieved by ID."""
artist_id = "0TnOYISbd1XYRBk9myaseg" # Pitbull
artist = spotify_client.artists.get_artist(artist_id)
assert artist['id'] == artist_id
assert 'name' in artist
assert artist['type'] == 'artist'
def test_get_artist_top_tracks(self, spotify_client):
"""Verify artist top tracks retrieval."""
artist_id = "0TnOYISbd1XYRBk9myaseg"
tracks = spotify_client.artists.get_artist_top_tracks(artist_id)
assert 'tracks' in tracks
assert len(tracks['tracks']) > 0Step 4: Update documentation
# README.md
### Artist Operations
```python
# Get artist
artist = client.artists.get_artist("artist_id")
# Get artist top tracks
top_tracks = client.artists.get_artist_top_tracks("artist_id", market="US")
### Pre-commit Hooks (Optional)
```bash
# Install pre-commit
pip install pre-commit
# Create .pre-commit-config.yaml
repos:
- repo: https://github.com/psf/black
rev: 23.12.1
hooks:
- id: black
- repo: https://github.com/pycqa/flake8
rev: 7.0.0
hooks:
- id: flake8
args: ['--max-line-length=100']
# Install hooks
pre-commit install
This section explains the purpose of all files and folders in the root directory, which files are essential vs. optional, and what can be safely removed.
| File | Purpose | Why Essential |
|---|---|---|
| .env | Environment variables for credentials | Contains your Spotify API credentials (client_id, client_secret). Required for authentication. |
| .env.example | Template for .env file | Used to create .env file. Keep for reference when setting up on new machines. |
| pytest.ini | Pytest configuration | Defines test markers, Python paths, and pytest behavior. Required for tests to run correctly. |
| requirements.txt | Python dependencies | Lists all required packages. Essential for installation (pip install -r requirements.txt). |
| conftest.py | Pytest fixtures | Contains global fixtures (spotify_client, test_config, etc.). Tests won't run without this. |
| Script | Purpose | When to Use |
|---|---|---|
| verify_config.py | Configuration verification tool | Run after setup or when troubleshooting. Checks all configuration, dependencies, and authentication. |
| manual_token_setup.py | Manual OAuth token entry | Use when you have OAuth tokens from Postman/other sources and want to manually enter them. |
| oauth_helper.py | Browser-based OAuth setup | Use for automatic OAuth token generation via browser authorization flow. |
| setup.py | Python package setup | Used for installing framework as package (pip install -e .). Keep for development setup. |
| Script | Purpose | Recommendation |
|---|---|---|
| run_tests.bat | Windows batch script for running tests | Optional - Convenience script. You can run pytest directly instead. |
| run_tests.ps1 | PowerShell script for running tests | Optional - Convenience script. You can run pytest directly instead. |
| Directory | Purpose | Why Essential |
|---|---|---|
| config/ | Environment configurations (qa.yaml, auth.yaml, base.yaml) | Required for multi-environment support and configuration loading. |
| libs/ | Core framework code (auth, clients, endpoints) | Contains all framework implementation. Cannot run tests without this. |
| tests/ | Test suites (functional, integration, e2e) | Contains all test cases. Primary purpose of the framework. |
| utils/ | Utility modules (config_loader, logger, helpers) | Shared utilities used throughout framework. |
| TestData/ | Test data files (JSON files with test data) | Test data for data-driven tests. |
| Directory | Purpose | Can Remove? |
|---|---|---|
| .auth_cache/ | OAuth token storage | Yes, but will lose saved tokens. Tokens will need to be re-generated. |
| logs/ | Test execution logs | Yes - Auto-regenerated on test runs. |
| reports/ | Test reports (HTML, Allure, JUnit) | Yes - Auto-regenerated on test runs. |
| .pytest_cache/ | Pytest cache for faster reruns | Yes - Auto-regenerated by pytest. |
| pycache/ | Python bytecode cache | Yes - Auto-regenerated by Python. |
| venv/ | Python virtual environment | Yes, but you'll need to recreate it (python -m venv venv). |
| Directory | Purpose |
|---|---|
| .idea/ | PyCharm/IntelliJ IDE settings |
| File | Purpose | Recommendation |
|---|---|---|
| .gitignore | Git ignore rules | Keep if using git. Prevents committing sensitive files (.env, .auth_cache, etc.). |
| pyproject.toml | Python project metadata and build config | Keep for modern Python packaging. Used by tools like black, pytest, etc. |
| MANIFEST.in | Package manifest for distribution | Keep if you plan to distribute framework as package. Otherwise optional. |
Core Configuration:
- .env
- .env.example
- pytest.ini
- requirements.txt
- conftest.py
- pyproject.toml
Core Scripts:
- verify_config.py
- manual_token_setup.py
- oauth_helper.py
- setup.py
Core Directories:
- config/
- libs/
- tests/
- utils/
- TestData/
Primary Documentation:
- README.md (this file)
- WORKFLOW_DOCUMENTATION.md (interview prep)
- INTERVIEW_QUESTIONS.md (interview prep)
- FRAMEWORK_EVALUATION.md (stakeholder presentations)
# Clean cache and generated files
rm -rf .pytest_cache/ __pycache__/ logs/ reports/
# These will be auto-regenerated on next test runpython verify_config.py
# Should show:
# β
All essential files present
# β
Configuration valid
# β
Framework readyIf you want the absolute minimum files to run tests:
Files (13 essential):
.env
pytest.ini
requirements.txt
conftest.py
verify_config.py
manual_token_setup.py
oauth_helper.py
setup.py
pyproject.toml
README.md
Directories (4 essential):
config/
libs/
tests/
utils/
Everything else is optional, documentation, or auto-generated.
# Testing
pytest tests/ -v # Run all tests
pytest tests/functional/ -v # Functional tests only
pytest -m requires_user_auth -v # OAuth tests only
pytest -k "test_album" -v # Tests matching pattern
pytest tests/ -x # Stop on first failure
pytest tests/ --maxfail=3 # Stop after 3 failures
pytest tests/ -n 4 # Parallel execution (4 workers)
pytest tests/ --lf # Run last failed tests
pytest tests/ --ff # Run failures first
# Reporting
pytest tests/ --html=reports/report.html # HTML report
pytest tests/ --junitxml=reports/junit.xml # JUnit XML
pytest tests/ --alluredir=reports/allure-results # Allure
allure serve reports/allure-results # View Allure report
# Code Quality
black . # Format code
flake8 . # Lint code
mypy libs/ utils/ # Type checking
pylint libs/ utils/ # Static analysis
# Configuration
python verify_config.py # Verify setup
python manual_token_setup.py # Setup OAuth
python oauth_helper.py # Browser OAuth
# Environment
export ENVIRONMENT=qa # Set environment
echo $ENVIRONMENT # Check environmentspotify_api_automation_enhanced_2_4/
βββ .auth_cache/ # OAuth token storage (not in git)
β βββ tokens_*.json
βββ .github/ # GitHub workflows (if CI/CD setup)
β βββ workflows/
β βββ tests.yml
βββ config/ # Configuration files
β βββ base.yaml # Base configuration
β βββ qa.yaml # QA environment
β βββ staging.yaml # Staging environment
β βββ prod.yaml # Production environment
βββ libs/ # Core framework code
β βββ __init__.py
β βββ auth.py # Authentication manager
β βββ advanced_auth.py # OAuth implementation
β βββ spotify_client.py # Main API client
β βββ base_client.py # HTTP client
β βββ endpoints/ # Endpoint clients
β βββ __init__.py
β βββ albums.py
β βββ tracks.py
β βββ playlists.py
β βββ search.py
β βββ users.py
βββ tests/ # Test suites
β βββ conftest.py # Pytest fixtures
β βββ functional/ # Functional tests
β β βββ albums/
β β βββ search/
β β βββ tracks/
β βββ integration/ # Integration tests
β β βββ test_user_playlists.py
β β βββ test_user_profile.py
β βββ e2e/ # E2E tests
β βββ test_playlist_workflow.py
βββ utils/ # Utility modules
β βββ __init__.py
β βββ config_loader.py # Configuration management
β βββ logger.py # Logging setup
β βββ helpers.py # Helper functions
βββ TestData/ # Test data files
β βββ test_albums.json
β βββ test_playlists.json
β βββ test_users.json
βββ reports/ # Test reports (generated)
β βββ allure-results/
β βββ pytest_report.html
βββ logs/ # Log files (generated)
βββ .env # Environment variables (not in git)
βββ .env.example # Environment template
βββ .gitignore # Git ignore rules
βββ pytest.ini # Pytest configuration
βββ requirements.txt # Python dependencies
βββ setup.py # Package setup
βββ MANIFEST.in # Package manifest
βββ README.md # Project overview
βββ WORKFLOW_DOCUMENTATION.md # Workflow guide
βββ INTERVIEW_QUESTIONS.md # Interview Q&A
Internal Team Contacts:
- QA Engineering Lead: [Name]
- DevOps Team: [Email/Slack]
- API Documentation: https://developer.spotify.com/documentation/web-api
Resources:
- Spotify API Documentation: https://developer.spotify.com/documentation/web-api
- Pytest Documentation: https://docs.pytest.org
- Python Requests: https://docs.python-requests.org
Issue Tracking:
- Report bugs: [Issue Tracker URL]
- Feature requests: [Issue Tracker URL]
- Documentation improvements: [Wiki/Confluence URL]
Last Updated: 2025-10-12 Framework Version: 2.4.0 Document Version: 1.0