author: Karlos Helton Braga
Before running the project for the first time, copy the environment variables template:
cp env/.env.example env/.envEdit env/.env with your values. The default values in the file work out of the box for local development.
# Build and start (blocking terminal)
docker compose up --build
# Build and start in background
docker compose up --build -d
# Start without rebuilding (after first build)
docker compose up -d
# View logs of all services (when running in background)
docker compose logs -f
# View logs of a specific service
docker compose logs -f web
# Stop containers
docker compose stop
# Stop and remove containers, images and volumes
docker compose down --rmi all --volumesThe project uses Docker volumes for live code reloading during development:
./study_shoes_store→/var/www/study_shoes_store(app code)./env→/var/www/env(environment configuration)
This enables:
- ✅ Instant code reloading without rebuild
- ✅ Direct access to generated migration files
- ✅ Real-time test execution
- ✅ No file copying needed between container and host
# Build the image
docker build . -f shoes-store.Dockerfile -t study_shoes_store --network=host
# Run a single container in background
docker run --name shoes-store -p 8000:8000 -d study_shoes_store
# Run a single container (blocking terminal)
docker run --name shoes-store -p 8000:8000 -it study_shoes_store
# Stop a single container
docker stop shoes-store
# Restart a stopped container
docker start shoes-store
# Remove a stopped container
docker rm shoes-store
# Force remove a running container
docker rm -f shoes-store
# Remove the image
docker rmi study_shoes_store
# Force remove the image (even if used by a container)
docker rmi -f study_shoes_store# List running containers (name, image, status, ports)
docker ps
# List all containers including stopped ones
docker ps -a
# Show detailed info of a container (network, ports, mounts, etc.)
docker inspect <container_name>
# List networks
docker network ls
# Remove all stopped containers, unused networks and dangling images
docker system prune
# Remove all unused images (not just dangling ones)
docker system prune -a
# Remove all unused images, networks, containers and volumes
docker system prune -a --volumesAll management commands must be run inside the running container:
# Apply pending migrations
docker compose exec web python manage.py migrate
# Generate migrations after changing models
docker compose exec web python manage.py makemigrations shoes_api --name <migration_name>
# Open a Django shell
docker compose exec web python manage.py shell
# Create a superuser manually
docker compose exec web python manage.py createsuperuserNote: With Docker volumes enabled, migration files are generated directly on the host. No copying needed!
All tests are written using Django's TestCase and DRF's APITestCase. Tests cover:
- CPF validation: Valid CPF acceptance, invalid CPF rejection, formatting, uniqueness
- Order calculations: Total value calculation from product values
- API authentication: Endpoint protection and authentication requirements
# Run all tests
docker compose exec web python manage.py test
# Run tests for shoes_api app only
docker compose exec web python manage.py test shoes_api
# Run a specific test class
docker compose exec web python manage.py test shoes_api.tests.ClientSerializerTestCase
# Run a specific test method
docker compose exec web python manage.py test shoes_api.tests.ClientSerializerTestCase.test_valid_cpf_accepted
# Run with verbose output (v=2 shows test method names)
docker compose exec web python manage.py test shoes_api -v 2
# Run tests with coverage report
docker compose exec web sh -lc "cd /var/www/study_shoes_store && coverage run --source='shoes_api' manage.py test shoes_api"
docker compose exec web sh -lc "cd /var/www/study_shoes_store && coverage report"Note:
coverageis versioned instudy_shoes_store/requirements.txt. If the command is not found in a running container, rebuild or refresh dependencies.
Run lint before opening a PR, after refactors, or whenever you change files in shoes_api/.
The commands below are executed inside the running web container and use the same working directory, target, and flags as CI (excluding migrations):
# 1) Auto-fix lint issues (when possible)
docker compose exec web sh -lc "cd /var/www/study_shoes_store && ruff check shoes_api --select=E,W,F --exclude=shoes_api/migrations --fix"
# 2) Apply formatting
docker compose exec web sh -lc "cd /var/www/study_shoes_store && ruff format shoes_api --exclude=shoes_api/migrations"
# 3) Validate lint without changing files
docker compose exec web sh -lc "cd /var/www/study_shoes_store && ruff check shoes_api --select=E,W,F --exclude=shoes_api/migrations"
# 4) Validate formatting without changing files
docker compose exec web sh -lc "cd /var/www/study_shoes_store && ruff format shoes_api --check --exclude=shoes_api/migrations"If CI shows Ruff errors while local lint passes, verify that lint-related changes were committed and pushed before running the workflow, then re-run CI on the latest commit SHA.
Run security scan before opening a PR, especially after changes in serializers, views, and authentication logic.
The commands below are executed inside the running web container and use the same working directory, target, and exclusions as CI:
# 1) Generate JSON report without failing the command
docker compose exec web sh -lc "cd /var/www/study_shoes_store && bandit -r shoes_api --exclude shoes_api/migrations,shoes_api/tests.py -f json -o bandit-report.json || true"
# 2) Display report in terminal (fails if issues are found)
docker compose exec web sh -lc "cd /var/www/study_shoes_store && bandit -r shoes_api --exclude shoes_api/migrations,shoes_api/tests.py"If CI shows Bandit errors while local scan passes, verify that security-related changes were committed and pushed before running the workflow, then re-run CI on the latest commit SHA.
Status: ⏸️ Temporarily paused
A GitHub Actions workflow has been configured at .github/workflows/tests.yml with the following jobs:
- lint: Code style checks (ruff)
- security: Security scanning (bandit) + JSON artifact upload (
bandit-report.json) - test: Test suite + coverage gate + coverage artifact upload + Codecov upload
Each job is implemented as a reusable custom action in .github/actions/.
Current workflow compatibility notes:
- External actions are pinned by SHA in workflow steps (with version comments).
- Setup action (
.github/actions/setup-python-env/action.yml) installs system dependencies and Python dependencies, exportsSECRET_KEY=ci-secret-keyfallback, and caches pip packages withactions/cachepinned by SHA. - Coverage action (
.github/actions/coverage-report/action.yml) enforcescoverage report --fail-under=90, generatescoverage.xml, and uploads to Codecov using OIDC (use_oidc: true) only outside forked pull requests. - Workflow uses explicit permissions (
contents: readglobally,id-token: writeonly in thetestjob) and concurrency cancellation for stale runs. - Jobs include
timeout-minutesand artifact retention (retention-days: 7). pushandpull_requesttriggers use path filters to reduce unnecessary CI runs.- Dependabot updates GitHub Actions weekly via
.github/dependabot.yml.
Local Docker parity note:
shoes-store.Dockerfileinstallsgit,curl, andgnupgso local containers match CI runtime dependencies.
Current limitation: GitHub-hosted runners are disabled for this repository (GitHub Enterprise restriction). To enable CI/CD automation:
- Contact your GitHub Enterprise Administrator
- Request to enable "GitHub-hosted runners" for this repository
- Once enabled, validate workflow execution on
pushandpull_requestevents in.github/workflows/tests.yml
Alternatively, a self-hosted runner can be configured on your own infrastructure.
This project implements systematic code quality improvements:
| # | Improvement | Details |
|---|---|---|
| 1 | Environment Management | python-decouple for secure config from .env |
| 2 | Authentication | Explicit SessionAuthentication + BasicAuthentication |
| 3 | WSGI Server | Production-ready gunicorn (replaces Django runserver) |
| 4 | Static Files | WhiteNoise middleware for CSS/JS serving |
| 5 | CSRF Security | CSRF_TRUSTED_ORIGINS from environment |
| 6 | Monetary Precision | DecimalField for value and total_value |
| 7 | Data Integrity | unique=True constraint on Order.order_number |
| 8 | Code Constants | Product.COLOUR_CHOICES class constant |
| 9 | CPF Formatting | CPF().mask() method for proper formatting |
| 10 | Calculation Logic | sum() generator for Order.total_value |
| 11 | Test Coverage | 15 comprehensive tests across serializers and API |
| 12 | API Documentation | Swagger/OpenAPI with drf-spectacular |
| 13 | CI/CD Pipeline | GitHub Actions with custom actions (setup, lint, security, test) |
Test Results: ✅ All 15 tests passing
- 4 tests: CPF validation, formatting, uniqueness
- 3 tests: Order total_value calculation
- 8 tests: API authentication requirements
The API address is: http://127.0.0.1:8000.
Authentication is required to use the API. The user admin with password admin is created when the Docker container is built, to make it easier to interact with the API.
The authentication type used is Basic Authentication. You can understand how it works at the following link:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization
For the credentials mentioned above, the Authorization header should have the value Basic YWRtaW46YWRtaW4=.
Interactive API documentation is available via Swagger UI:
- Swagger UI: http://127.0.0.1:8000/api/docs/
- ReDoc (alternative): http://127.0.0.1:8000/api/redoc/
- OpenAPI Schema (JSON): http://127.0.0.1:8000/api/schema/
Use Swagger UI to:
- ✅ Browse all endpoints
- ✅ View request/response schemas
- ✅ Test endpoints directly in the browser
- ✅ See authentication requirements
All list endpoints in the API are paginated. They return a JSON object with four fields:
- count: total number of results found in the database
- next: URL of the next page (
nullif there is no next page) - previous: URL of the previous page (
nullif there is no previous page) - results: results of the current page
Path: /lotes_api/
Method: POST
Json Body:
{
"identifier_code": "L1",
"manufacturing_date": "2021-07-11",
"product_qty": "10"
}Response:
{
"id": 1,
"identifier_code": "L1",
"manufacturing_date": "2021-07-11",
"product_qty": 10
}Path: /lotes_api/
Method: GET
Response:
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"identifier_code": "L1",
"manufacturing_date": "2021-07-11",
"product_qty": 10
}
]
}Path: /lotes_api/1/
Method: GET
Response:
{
"id": 1,
"identifier_code": "L1",
"manufacturing_date": "2021-07-11",
"product_qty": 10
}Path: /lotes_api/1/
Method: PUT
Json Body:
{
"identifier_code": "L1",
"manufacturing_date": "2021-07-11",
"product_qty": "10"
}Response:
{
"id": 1,
"identifier_code": "L1",
"manufacturing_date": "2021-07-11",
"product_qty": 10
}Path: /lotes_api/1/
Method: DELETE
Response: empty
Path: /produtos_api/
Method: POST
Json Body:
{
"identifier_code": "P1",
"name": "Tenis",
"batch_number": 1,
"colour": "red",
"description": "Tênis Vermelho",
"value": 199.99
}batch_number deve ser o id do lote onde o produto será cadastrado
Response:
{
"id": 1,
"identifier_code": "P1",
"name": "Tenis",
"batch_number": 1,
"colour": "red",
"description": "Tênis Vermelho",
"value": 199.99
}Path: /produtos_api/
Method: GET
Response:
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"identifier_code": "P1",
"name": "Tenis",
"batch_number": 1,
"colour": "red",
"description": "Tênis Vermelho",
"value": 199.99
}
]
}Path: /produtos_api/1/
Method: GET
Response:
{
"id": 1,
"identifier_code": "P1",
"name": "Tenis",
"batch_number": 1,
"colour": "red",
"description": "Tênis Vermelho",
"value": 199.99
}batch_number é o id do lote onde o produto foi cadastrado
Path: /produtos_api/1/
Method: PUT
Json Body:
{
"identifier_code": "P1",
"name": "Tênis",
"batch_number": 1,
"colour": "blue",
"description": "Tênis Azul",
"value": 99.99
}batch_number deve ser o id do lote para onde o produto será atualizado
Response:
{
"id": 1,
"identifier_code": "P1",
"name": "Tênis",
"batch_number": 1,
"colour": "blue",
"description": "Tênis Azul",
"value": 99.99
}Path: /produtos_api/1/
Method: DELETE
Response: empty
Path: /clientes_api/
Method: POST
Json Body:
{
"name": "Lucas",
"cpf": "123.456.789-09",
"birth_date": "1986-03-27"
}Response:
{
"id": 1,
"name": "Lucas",
"cpf": "123.456.789-09",
"birth_date": "1986-03-27"
}Path: /clientes_api/
Method: GET
Response:
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"name": "Lucas",
"cpf": "123.456.789-09",
"birth_date": "1986-03-27"
}
]
}Path: /clientes_api/1/
Method: GET
Response:
{
"id": 1,
"name": "Lucas",
"cpf": "123.456.789-09",
"birth_date": "1986-03-27"
}Path: /clientes_api/1/
Method: PUT
Json Body:
{
"name": "Luke",
"cpf": "123.456.789-09",
"birth_date": "1986-03-27"
}Response:
{
"id": 1,
"name": "Luke",
"cpf": "123.456.789-09",
"birth_date": "1986-03-27"
}Path: /clientes_api/1/
Method: DELETE
Response: empty
Path: /pedidos_api/
Method: POST
Json Body:
{
"order_number": 1,
"client": 1,
"order_date": "2021-07-11",
"seller": 1,
"products": [ 1 ]
}client deve ser o id do cliente comprador seller deve ser o id do usuário vendedor products deve ser um array com os ids dos produtos comprados
Response:
{
"id": 1,
"order_number": 1,
"client": 1,
"order_date": "2021-07-11",
"seller": 1,
"products": [
1
]
}Path: /pedidos_api/
Method: GET
Response:
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"order_number": 1,
"client": {
"id": 1,
"name": "Luke",
"cpf": "12345678909",
"birth_date": "1986-03-27"
},
"order_date": "2021-07-11",
"seller": "(admin)",
"products": [
{
"id": 1,
"identifier_code": "P1",
"name": "Tênis",
"colour": "blue",
"description": "Tênis Azul",
"value": 99.99,
"batch_number": 1
}
],
"total_value": 99.99
}
]
}Para fazer a ordenação por valor ou data, adicionar na url o parâmetro ordering.
Paths:
/pedidos_api/?ordering=order_date/pedidos_api/?ordering=total_value
Path: /pedidos_api/1/
Method: GET
Response:
{
"id": 1,
"order_number": 1,
"client": {
"id": 1,
"name": "Luke",
"cpf": "12345678909",
"birth_date": "1986-03-27"
},
"order_date": "2021-07-11",
"seller": "(admin)",
"products": [
{
"id": 1,
"identifier_code": "P1",
"name": "Tênis",
"colour": "blue",
"description": "Tênis Azul",
"value": 99.99,
"batch_number": 1
}
],
"total_value": 99.99
}Path: /pedidos_api/1/
Method: PUT
Json Body:
{
"id": 1,
"order_number": 1,
"client": 1,
"order_date": "2021-05-11",
"seller": 1,
"products": [
1
]
}Response:
{
"id": 1,
"order_number": 1,
"client": 1,
"order_date": "2021-05-11",
"seller": 1,
"products": [
1
]
}Path: /pedidos_api/1/
Method: DELETE
Response: empty
To provide login and password for sellers, Django's built-in admin user model was used.
The Rest Framework serializer does not allow entering a CPF with dots and dashes, even though they are stripped before being saved to the database, unless the CPF has 14 characters in the database.
Web framework used to build the API.
https://www.djangoproject.com/
Used to simplify API development.
https://www.django-rest-framework.org/
Used to validate the customer's CPF.
https://pypi.org/project/validate-docbr/
Used to manage environment variables from the env/.env file.
https://pypi.org/project/python-decouple/
WSGI server used to serve the application in the Docker container.
Used to serve static files (admin CSS/JS) directly from the WSGI application without a separate web server.