User Acceptance Test Plan — Server Infrastructure
Test Series: 1 to 4
Environment: AWS EC2 — Ubuntu 24.04.4 LTS, 1 GB RAM, 60 GB Storage
1. Purpose & Scope
This UAT plan validates that the server infrastructure supporting the Django/DRF application is correctly provisioned, secured, and stable enough for acceptance sign-off. It covers OS/environment setup, service configuration, security controls, resource constraints, background processing, third-party integrations, and recovery procedures.
It does not cover application business logic — that should be a separate functional UAT plan.
2. Environment Under Test
| Component |
Detail |
| Cloud provider |
AWS EC2 |
| OS |
Ubuntu 24.04.4 LTS |
| RAM |
1 GB |
| Storage |
60 GB |
| App framework |
Django 5.2.3 / DRF 3.15.1 |
| Web/WSGI server |
Apache2 + mod_wsgi |
| Task queue |
Celery 5.3.6 + Redis 5.0.3 |
| Database |
MongoDB (pymongo 4.13.2) |
| Auth |
SimpleJWT 5.3.1, Argon2 (argon2-cffi) |
| Static files |
WhiteNoise 6.6.0 |
| Scheduled jobs |
django-crontab 0.7.1 |
| Object storage |
AWS S3 (boto3) |
| Push/notifications |
Firebase Admin |
3. Entry Criteria
- Application code deployed to EC2 instance
- Environment variables /
.env configured (python-decouple / django-environ)
- Domain/SSL (if applicable) pointed to instance
- All services (Apache2, Redis, cron) enabled
4. Test Cases
4.1 Environment & Deployment Verification
| ID |
Test Case |
Steps |
Expected Result |
Pass/Fail |
| ENV-01 |
OS version confirmed |
lsb_release -a |
Reports Ubuntu 24.04.4 LTS |
|
| ENV-02 |
Python version matches project requirement |
python3 --version |
Matches version pinned in project docs |
|
| ENV-03 |
Virtual environment isolated |
Activate venv, pip freeze |
Matches requirements.txt exactly, no stray global packages |
|
| ENV-04 |
/Config/config/config.py / secrets not committed to repo |
Inspect .gitignore, repo history |
No secrets in version control |
|
| ENV-05 |
DEBUG=False in production settings |
Check settings/env var |
Confirmed False |
|
| ENV-06 |
ALLOWED_HOSTS restricted |
Check settings |
Only intended domain(s)/IP present, no * |
|
| ENV-07 |
Disk usage healthy |
df -h |
Used space < 80% of 60 GB |
|
| ENV-08 |
System packages up to date |
apt list --upgradable |
No critical security patches pending |
|
4.2 Service Configuration
| ID |
Test Case |
Steps |
Expected Result |
Pass/Fail |
| SVC-01 |
Apache2 running as systemd service |
systemctl status apache2 |
Active (running), enabled on boot |
|
| SVC-02 |
mod_wsgi module loaded and configured |
apache2ctl -M \| grep wsgi |
wsgi_module listed |
|
| SVC-03 |
Apache VirtualHost routes correctly to WSGI app |
Hit public URL |
Serves Django app, not default Apache placeholder page |
|
| SVC-04 |
Apache recovers from worker/process crash |
Kill a wsgi daemon process (if using daemon mode) |
Apache respawns process automatically |
|
| SVC-05 |
Apache config syntax valid |
apache2ctl configtest |
Returns "Syntax OK" |
|
| SVC-06 |
Redis service active |
systemctl status redis / redis-cli ping |
Returns PONG |
|
| SVC-07 |
Celery worker running |
systemctl status celery or celery -A app status |
Worker online |
|
| SVC-08 |
Celery task executes end-to-end |
Trigger a known async task |
Task completes, result/logs confirm success |
|
| SVC-09 |
MongoDB reachable |
pymongo connection test from Django shell |
Connects without timeout/auth error |
|
| SVC-10 |
WhiteNoise/Apache serving static files correctly |
Request a static asset URL directly |
Returns 200 with correct content-type, cache headers |
|
| SVC-11 |
django-crontab jobs registered |
python manage.py crontab show |
Expected jobs listed |
|
| SVC-12 |
Cron job executes on schedule |
Wait for/simulate scheduled time |
Job runs, expected side effect occurs (log entry, DB update, etc.) |
|
4.3 Security Configuration
| ID |
Test Case |
Steps |
Expected Result |
Pass/Fail |
| SEC-01 |
HTTPS enforced |
Request via http:// |
Redirects to https:// (if SSL configured) |
|
| SEC-02 |
JWT auth required on protected endpoints |
Hit protected API without token |
Returns 401 |
|
| SEC-03 |
JWT expiry enforced |
Use expired token |
Returns 401, not silently accepted |
|
| SEC-04 |
Password hashing uses Argon2 |
Inspect a stored password hash |
Prefixed argon2$... |
|
| SEC-05 |
django-axes lockout works |
Attempt failed logins past threshold |
Account/IP locked per configured limit |
|
| SEC-06 |
Axes lockout resets correctly |
Wait cooldown period or admin reset |
Access restored |
|
| SEC-07 |
CORS restricted to intended origins |
Request from disallowed origin |
Blocked by CORS policy |
|
| SEC-08 |
CORS allows intended frontend origin |
Request from approved origin |
Succeeds |
|
| SEC-09 |
Security headers present |
Inspect response headers (CSP, X-Frame-Options, HSTS if applicable) |
Present per configuration |
|
| SEC-10 |
EC2 Security Group least-privilege |
Review AWS console/CLI |
Only required ports open (22 restricted to known IP, 80/443 public, DB port not public) |
|
| SEC-11 |
SSH key-based auth only |
Check sshd_config |
Password auth disabled |
|
| SEC-12 |
Firewall (ufw) active |
ufw status |
Enabled with expected rules |
|
| SEC-13 |
django-watchman health checks pass |
Hit /watchman/ endpoint |
All checks (DB, cache, storage) report OK |
|
4.4 Resource Constraints (1 GB RAM specific)
Given only 1 GB RAM, this tier needs explicit validation — it's the most likely failure point.
| ID |
Test Case |
Steps |
Expected Result |
Pass/Fail |
| RES-01 |
Swap configured |
swapon --show, free -h |
Swap space present (recommended if not already) to avoid OOM kills |
|
| RES-02 |
mod_wsgi process/thread count appropriate |
Check WSGIDaemonProcess directive (processes/threads) |
Sized conservatively for 1 GB RAM (e.g. 2 processes × 2–5 threads, not left at high defaults) |
|
| RES-03 |
No OOM kills under normal load |
dmesg | grep -i "out of memory" after test traffic |
No OOM kill events logged |
|
| RES-04 |
Memory usage under idle |
free -h at rest |
Headroom exists (not >85% used at idle) |
|
| RES-05 |
Memory usage under concurrent requests |
Load test with realistic concurrent users (e.g. 10–20) |
App remains responsive, no crash/restart loop |
|
| RES-06 |
Celery worker memory footprint acceptable |
Monitor top/htop during task processing |
No runaway memory growth |
|
| RES-07 |
Redis memory limit configured |
Check maxmemory in redis.conf |
Set explicitly, not left unbounded on a 1GB host |
|
| RES-08 |
CPU load sustainable |
uptime during load test |
Load average stays reasonable for available vCPUs |
|
| RES-09 |
Apache MPM appropriate for RAM budget |
apache2ctl -M \| grep mpm and review MPM config |
event/worker MPM with mod_wsgi daemon mode preferred over prefork on 1 GB; process/thread limits explicitly capped |
|
| RES-10 |
mod_wsgi daemon mode in use (not embedded) |
Check VirtualHost/wsgi.conf for WSGIDaemonProcess |
Daemon mode configured, isolating app processes from Apache's own workers |
|
4.5 Logging & Monitoring
| ID |
Test Case |
Steps |
Expected Result |
Pass/Fail |
| LOG-01 |
Request IDs present in logs |
Make a request, check logs |
Unique request ID (django-log-request-id) traceable across log lines |
|
| LOG-02 |
Application errors logged, not exposed to user |
Trigger a 500 error |
User sees generic error page, stack trace only in server logs |
|
| LOG-03 |
Log rotation configured |
Check logrotate config or app-level rotation |
Logs don't fill disk over time |
|
| LOG-04 |
Apache access/error logs writing correctly |
Inspect /var/log/apache2/access.log and error.log |
Populated, timestamps current |
|
4.6 Third-Party Integrations
| ID |
Test Case |
Steps |
Expected Result |
Pass/Fail |
| INT-01 |
S3 upload works |
Trigger a file upload flow |
File appears in target S3 bucket |
|
| INT-02 |
S3 bucket permissions correct |
Attempt public access to a private object URL |
Access denied unless intentionally public |
|
| INT-03 |
Firebase Admin SDK initializes |
Trigger a Firebase-dependent action (e.g. push notification) |
No credential/init errors, action succeeds |
|
| INT-04 |
IAM role / credentials scoped correctly |
Review AWS IAM policy attached to EC2 instance or service account |
Least-privilege, no wildcard * resource access |
|
4.7 Backup & Recovery
| ID |
Test Case |
Steps |
Expected Result |
Pass/Fail |
| BAK-01 |
MongoDB backup process exists |
Run backup script/verify managed backup (if Atlas) |
Backup completes and is restorable |
|
| BAK-02 |
Restore from backup tested |
Restore to a staging instance |
Data integrity confirmed |
|
| BAK-03 |
EC2 instance recovery plan |
Review AMI snapshot schedule or IaC redeploy process |
Instance can be rebuilt within acceptable RTO |
|
| BAK-04 |
Environment variables recoverable |
Confirm secrets stored in a secrets manager/secure vault, not only on the instance |
Can redeploy without data loss |
|
4.8 Deployment & Rollback
| ID |
Test Case |
Steps |
Expected Result |
Pass/Fail |
| DEP-01 |
Deployment process documented and repeatable |
Follow deployment steps from scratch (on staging) |
App comes up successfully following documented steps |
|
| DEP-02 |
Rollback to previous version possible |
Simulate rollback |
Previous stable version restored without data loss |
|
| DEP-03 |
Zero/minimal downtime on deploy |
Deploy during monitored window |
Downtime within acceptable threshold (define SLA) |
|
5. Exit Criteria
- All Critical/High severity test cases (SEC, RES, BAK) pass
- Any failed Medium/Low cases have documented remediation plan with owner and date
- Sign-off obtained from [stakeholder/role]
6. Sign-off
| Role |
Name |
Signature |
Date |
| Infrastructure Owner |
|
|
|
| Project Lead |
|
|
|
Notes on Your Specific Setup
- Apache's MPM matters. Check which MPM is active (
apache2ctl -M | grep mpm) — prefork spawns a full process per connection and is heavy on 1 GB; event or worker MPM paired with mod_wsgi daemon mode is lighter. This is worth its own test case (call it RES-09) if you haven't settled on an MPM yet.
- Swap is worth confirming — without it, an OOM spike (e.g. a large file upload processed via python-magic, or a burst of Celery tasks) can kill Apache/mod_wsgi processes outright rather than degrading gracefully.
- Redis on the same 1 GB box competing with Apache/mod_wsgi/Celery for memory is a common bottleneck — RES-07 (maxmemory + eviction policy) matters more than usual here.
- Static files: with Apache in front, you can serve static assets directly via an Apache
Alias (bypassing Django/WhiteNoise entirely for those requests), which is lighter on the 1 GB instance than routing static files through the WSGI app. Worth deciding which approach you're using and testing accordingly in SVC-10.