fix: add database migrations at container startup and deployment tests
Browse files- Add startup script that runs migrations before gunicorn
- Switch Dockerfile to Alpine Python for nginx compatibility
- Add comprehensive deployment tests (12 passing tests)
- Expand ALLOWED_HOSTS for Docker networking
- Add .cache/ to .gitignore
- Add docker-compose.yml for local testing
- Remove tracked __pycache__ file
- .gitignore +1 -0
- Dockerfile +16 -9
- config/settings/__pycache__/base.cpython-312.pyc +0 -0
- config/settings/base.py +7 -1
- docker-compose.yml +6 -0
- pytest.ini +1 -0
- start-django.sh +10 -0
- tests/api/__init__.py +1 -0
- tests/api/test_deployment.py +247 -0
.gitignore
CHANGED
|
@@ -8,6 +8,7 @@ frontend/dist/
|
|
| 8 |
.coverage
|
| 9 |
__pycache__/
|
| 10 |
*.pyc
|
|
|
|
| 11 |
|
| 12 |
# Database
|
| 13 |
db.sqlite3
|
|
|
|
| 8 |
.coverage
|
| 9 |
__pycache__/
|
| 10 |
*.pyc
|
| 11 |
+
.cache/
|
| 12 |
|
| 13 |
# Database
|
| 14 |
db.sqlite3
|
Dockerfile
CHANGED
|
@@ -16,10 +16,13 @@ COPY frontend/ ./
|
|
| 16 |
RUN npm run build
|
| 17 |
|
| 18 |
# Stage 2: Setup Python/Django backend
|
| 19 |
-
FROM python:3.12-
|
| 20 |
|
| 21 |
WORKDIR /app
|
| 22 |
|
|
|
|
|
|
|
|
|
|
| 23 |
# Install uv package manager
|
| 24 |
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
| 25 |
|
|
@@ -32,15 +35,11 @@ COPY pyproject.toml ./
|
|
| 32 |
COPY uv.lock ./
|
| 33 |
|
| 34 |
# Install dependencies using uv
|
| 35 |
-
RUN
|
| 36 |
-
uv sync --frozen
|
| 37 |
|
| 38 |
# Copy application code
|
| 39 |
COPY . .
|
| 40 |
|
| 41 |
-
# Run migrations
|
| 42 |
-
RUN /app/.venv/bin/python manage.py migrate
|
| 43 |
-
|
| 44 |
# Stage 3: Final image with nginx
|
| 45 |
FROM nginx:alpine
|
| 46 |
|
|
@@ -57,15 +56,23 @@ COPY --from=frontend-builder /frontend/dist /usr/share/nginx/html
|
|
| 57 |
# Copy nginx configuration
|
| 58 |
COPY nginx.conf /etc/nginx/nginx.conf
|
| 59 |
|
| 60 |
-
#
|
| 61 |
-
RUN
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
echo 'nodaemon=true' >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 63 |
echo '' >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 64 |
echo '[program:django]' >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 65 |
-
echo 'command=/app/
|
| 66 |
echo 'directory=/app' >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 67 |
echo 'autostart=true' >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 68 |
echo 'autorestart=true' >> /etc/supervisor/conf.d/supervisord.conf && \
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
echo '' >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 70 |
echo '[program:nginx]' >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 71 |
echo 'command=nginx -g "daemon off;"' >> /etc/supervisor/conf.d/supervisord.conf && \
|
|
|
|
| 16 |
RUN npm run build
|
| 17 |
|
| 18 |
# Stage 2: Setup Python/Django backend
|
| 19 |
+
FROM python:3.12-alpine AS backend
|
| 20 |
|
| 21 |
WORKDIR /app
|
| 22 |
|
| 23 |
+
# Install build dependencies for Python packages
|
| 24 |
+
RUN apk add --no-cache gcc musl-dev linux-headers
|
| 25 |
+
|
| 26 |
# Install uv package manager
|
| 27 |
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
| 28 |
|
|
|
|
| 35 |
COPY uv.lock ./
|
| 36 |
|
| 37 |
# Install dependencies using uv
|
| 38 |
+
RUN uv sync --frozen
|
|
|
|
| 39 |
|
| 40 |
# Copy application code
|
| 41 |
COPY . .
|
| 42 |
|
|
|
|
|
|
|
|
|
|
| 43 |
# Stage 3: Final image with nginx
|
| 44 |
FROM nginx:alpine
|
| 45 |
|
|
|
|
| 56 |
# Copy nginx configuration
|
| 57 |
COPY nginx.conf /etc/nginx/nginx.conf
|
| 58 |
|
| 59 |
+
# Make startup script executable
|
| 60 |
+
RUN chmod +x /app/start-django.sh
|
| 61 |
+
|
| 62 |
+
# Create supervisor config directory and file
|
| 63 |
+
RUN mkdir -p /etc/supervisor/conf.d && \
|
| 64 |
+
echo '[supervisord]' > /etc/supervisor/conf.d/supervisord.conf && \
|
| 65 |
echo 'nodaemon=true' >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 66 |
echo '' >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 67 |
echo '[program:django]' >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 68 |
+
echo 'command=/app/start-django.sh' >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 69 |
echo 'directory=/app' >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 70 |
echo 'autostart=true' >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 71 |
echo 'autorestart=true' >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 72 |
+
echo 'stdout_logfile=/dev/stdout' >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 73 |
+
echo 'stdout_logfile_maxbytes=0' >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 74 |
+
echo 'stderr_logfile=/dev/stderr' >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 75 |
+
echo 'stderr_logfile_maxbytes=0' >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 76 |
echo '' >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 77 |
echo '[program:nginx]' >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 78 |
echo 'command=nginx -g "daemon off;"' >> /etc/supervisor/conf.d/supervisord.conf && \
|
config/settings/__pycache__/base.cpython-312.pyc
DELETED
|
Binary file (2.38 kB)
|
|
|
config/settings/base.py
CHANGED
|
@@ -11,7 +11,13 @@ SECRET_KEY = "django-insecure-demo-key-replace-in-production"
|
|
| 11 |
|
| 12 |
DEBUG = True
|
| 13 |
|
| 14 |
-
ALLOWED_HOSTS: list[str] = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
INSTALLED_APPS = [
|
| 17 |
"django.contrib.auth",
|
|
|
|
| 11 |
|
| 12 |
DEBUG = True
|
| 13 |
|
| 14 |
+
ALLOWED_HOSTS: list[str] = [
|
| 15 |
+
"aivlad-demo.hf.space",
|
| 16 |
+
"localhost",
|
| 17 |
+
"127.0.0.1",
|
| 18 |
+
"192.168.50.66",
|
| 19 |
+
"wall-api-demo",
|
| 20 |
+
]
|
| 21 |
|
| 22 |
INSTALLED_APPS = [
|
| 23 |
"django.contrib.auth",
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
services:
|
| 2 |
+
demo:
|
| 3 |
+
image: wall-api:latest
|
| 4 |
+
ports:
|
| 5 |
+
- "7860:7860"
|
| 6 |
+
container_name: wall-api-demo
|
pytest.ini
CHANGED
|
@@ -20,6 +20,7 @@ markers =
|
|
| 20 |
integration: Integration tests (API + DB)
|
| 21 |
e2e: End-to-end tests (full workflows)
|
| 22 |
slow: Slow tests (run separately)
|
|
|
|
| 23 |
|
| 24 |
testpaths = tests
|
| 25 |
|
|
|
|
| 20 |
integration: Integration tests (API + DB)
|
| 21 |
e2e: End-to-end tests (full workflows)
|
| 22 |
slow: Slow tests (run separately)
|
| 23 |
+
deployment: Deployment verification tests
|
| 24 |
|
| 25 |
testpaths = tests
|
| 26 |
|
start-django.sh
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/sh
|
| 2 |
+
set -e
|
| 3 |
+
|
| 4 |
+
cd /app
|
| 5 |
+
|
| 6 |
+
echo "Running database migrations..."
|
| 7 |
+
/app/.venv/bin/python manage.py migrate --noinput
|
| 8 |
+
|
| 9 |
+
echo "Starting Gunicorn..."
|
| 10 |
+
exec /app/.venv/bin/gunicorn config.wsgi:application --bind 127.0.0.1:8000 --workers 2
|
tests/api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""API deployment and integration tests."""
|
tests/api/test_deployment.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Deployment tests for Wall Construction API.
|
| 2 |
+
|
| 3 |
+
Tests that verify the full stack is properly deployed and functional:
|
| 4 |
+
- Database migrations have run successfully
|
| 5 |
+
- API endpoints are accessible and return correct status codes
|
| 6 |
+
- CRUD operations work end-to-end
|
| 7 |
+
- Data persistence works correctly
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import pytest
|
| 13 |
+
from django.urls import reverse
|
| 14 |
+
from rest_framework import status
|
| 15 |
+
from rest_framework.test import APIClient
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@pytest.mark.django_db
|
| 19 |
+
@pytest.mark.deployment
|
| 20 |
+
class TestDeploymentHealth:
|
| 21 |
+
"""Test deployment health and basic API functionality."""
|
| 22 |
+
|
| 23 |
+
def test_api_root_accessible(self, api_client: APIClient) -> None:
|
| 24 |
+
"""Test API root endpoint is accessible."""
|
| 25 |
+
url = "/"
|
| 26 |
+
|
| 27 |
+
response = api_client.get(url)
|
| 28 |
+
|
| 29 |
+
assert response.status_code in (
|
| 30 |
+
status.HTTP_200_OK,
|
| 31 |
+
status.HTTP_301_MOVED_PERMANENTLY,
|
| 32 |
+
status.HTTP_302_FOUND,
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
def test_profiles_endpoint_accessible(self, api_client: APIClient) -> None:
|
| 36 |
+
"""Test profiles list endpoint returns 200 with empty database."""
|
| 37 |
+
url = reverse("profile-list")
|
| 38 |
+
|
| 39 |
+
response = api_client.get(url)
|
| 40 |
+
|
| 41 |
+
assert response.status_code == status.HTTP_200_OK
|
| 42 |
+
assert "results" in response.data
|
| 43 |
+
assert "count" in response.data
|
| 44 |
+
assert isinstance(response.data["results"], list)
|
| 45 |
+
|
| 46 |
+
def test_wallsections_endpoint_accessible(self, api_client: APIClient) -> None:
|
| 47 |
+
"""Test wallsections list endpoint returns 200 with empty database."""
|
| 48 |
+
url = reverse("wallsection-list")
|
| 49 |
+
|
| 50 |
+
response = api_client.get(url)
|
| 51 |
+
|
| 52 |
+
assert response.status_code == status.HTTP_200_OK
|
| 53 |
+
assert "results" in response.data
|
| 54 |
+
assert "count" in response.data
|
| 55 |
+
assert isinstance(response.data["results"], list)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@pytest.mark.django_db
|
| 59 |
+
@pytest.mark.deployment
|
| 60 |
+
class TestDeploymentEndToEnd:
|
| 61 |
+
"""Test end-to-end workflows to verify deployment integrity."""
|
| 62 |
+
|
| 63 |
+
def test_create_profile_and_retrieve(self, api_client: APIClient) -> None:
|
| 64 |
+
"""Test creating a profile and retrieving it verifies database works."""
|
| 65 |
+
list_url = reverse("profile-list")
|
| 66 |
+
|
| 67 |
+
# Create profile
|
| 68 |
+
create_payload = {
|
| 69 |
+
"name": "Deployment Test Profile",
|
| 70 |
+
"team_lead": "Test Lead",
|
| 71 |
+
"is_active": True,
|
| 72 |
+
}
|
| 73 |
+
create_response = api_client.post(list_url, create_payload, format="json")
|
| 74 |
+
|
| 75 |
+
assert create_response.status_code == status.HTTP_201_CREATED
|
| 76 |
+
assert create_response.data["name"] == "Deployment Test Profile"
|
| 77 |
+
assert "id" in create_response.data
|
| 78 |
+
|
| 79 |
+
profile_id = create_response.data["id"]
|
| 80 |
+
|
| 81 |
+
# Retrieve profile
|
| 82 |
+
detail_url = reverse("profile-detail", kwargs={"pk": profile_id})
|
| 83 |
+
retrieve_response = api_client.get(detail_url)
|
| 84 |
+
|
| 85 |
+
assert retrieve_response.status_code == status.HTTP_200_OK
|
| 86 |
+
assert retrieve_response.data["id"] == profile_id
|
| 87 |
+
assert retrieve_response.data["name"] == "Deployment Test Profile"
|
| 88 |
+
assert retrieve_response.data["team_lead"] == "Test Lead"
|
| 89 |
+
|
| 90 |
+
def test_create_wallsection_with_profile(self, api_client: APIClient) -> None:
|
| 91 |
+
"""Test creating wallsection with profile verifies foreign keys work."""
|
| 92 |
+
# Create profile first
|
| 93 |
+
profile_url = reverse("profile-list")
|
| 94 |
+
profile_payload = {"name": "Wall Builder", "team_lead": "Builder Lead"}
|
| 95 |
+
profile_response = api_client.post(
|
| 96 |
+
profile_url,
|
| 97 |
+
profile_payload,
|
| 98 |
+
format="json",
|
| 99 |
+
)
|
| 100 |
+
assert profile_response.status_code == status.HTTP_201_CREATED
|
| 101 |
+
profile_id = profile_response.data["id"]
|
| 102 |
+
|
| 103 |
+
# Create wallsection
|
| 104 |
+
wallsection_url = reverse("wallsection-list")
|
| 105 |
+
wallsection_payload = {
|
| 106 |
+
"profile": profile_id,
|
| 107 |
+
"section_name": "Section 1",
|
| 108 |
+
}
|
| 109 |
+
wallsection_response = api_client.post(
|
| 110 |
+
wallsection_url,
|
| 111 |
+
wallsection_payload,
|
| 112 |
+
format="json",
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
assert wallsection_response.status_code == status.HTTP_201_CREATED
|
| 116 |
+
assert wallsection_response.data["profile"] == profile_id
|
| 117 |
+
assert wallsection_response.data["section_name"] == "Section 1"
|
| 118 |
+
assert "id" in wallsection_response.data
|
| 119 |
+
|
| 120 |
+
def test_list_profiles_after_creation(self, api_client: APIClient) -> None:
|
| 121 |
+
"""Test listing profiles returns created profiles."""
|
| 122 |
+
url = reverse("profile-list")
|
| 123 |
+
|
| 124 |
+
# Create multiple profiles
|
| 125 |
+
for i in range(3):
|
| 126 |
+
payload = {
|
| 127 |
+
"name": f"Profile {i}",
|
| 128 |
+
"team_lead": f"Lead {i}",
|
| 129 |
+
}
|
| 130 |
+
create_response = api_client.post(url, payload, format="json")
|
| 131 |
+
assert create_response.status_code == status.HTTP_201_CREATED
|
| 132 |
+
|
| 133 |
+
# List all profiles
|
| 134 |
+
list_response = api_client.get(url)
|
| 135 |
+
|
| 136 |
+
assert list_response.status_code == status.HTTP_200_OK
|
| 137 |
+
assert list_response.data["count"] == 3
|
| 138 |
+
assert len(list_response.data["results"]) == 3
|
| 139 |
+
|
| 140 |
+
def test_update_profile(self, api_client: APIClient) -> None:
|
| 141 |
+
"""Test updating a profile verifies write operations work."""
|
| 142 |
+
# Create profile
|
| 143 |
+
list_url = reverse("profile-list")
|
| 144 |
+
create_payload = {"name": "Original Name", "team_lead": "Original Lead"}
|
| 145 |
+
create_response = api_client.post(list_url, create_payload, format="json")
|
| 146 |
+
assert create_response.status_code == status.HTTP_201_CREATED
|
| 147 |
+
profile_id = create_response.data["id"]
|
| 148 |
+
|
| 149 |
+
# Update profile
|
| 150 |
+
detail_url = reverse("profile-detail", kwargs={"pk": profile_id})
|
| 151 |
+
update_payload = {
|
| 152 |
+
"name": "Updated Name",
|
| 153 |
+
"team_lead": "Updated Lead",
|
| 154 |
+
"is_active": False,
|
| 155 |
+
}
|
| 156 |
+
update_response = api_client.put(detail_url, update_payload, format="json")
|
| 157 |
+
|
| 158 |
+
assert update_response.status_code == status.HTTP_200_OK
|
| 159 |
+
assert update_response.data["name"] == "Updated Name"
|
| 160 |
+
assert update_response.data["team_lead"] == "Updated Lead"
|
| 161 |
+
assert update_response.data["is_active"] is False
|
| 162 |
+
|
| 163 |
+
def test_delete_profile(self, api_client: APIClient) -> None:
|
| 164 |
+
"""Test deleting a profile verifies delete operations work."""
|
| 165 |
+
# Create profile
|
| 166 |
+
list_url = reverse("profile-list")
|
| 167 |
+
create_payload = {"name": "To Delete", "team_lead": "Delete Lead"}
|
| 168 |
+
create_response = api_client.post(list_url, create_payload, format="json")
|
| 169 |
+
assert create_response.status_code == status.HTTP_201_CREATED
|
| 170 |
+
profile_id = create_response.data["id"]
|
| 171 |
+
|
| 172 |
+
# Delete profile
|
| 173 |
+
detail_url = reverse("profile-detail", kwargs={"pk": profile_id})
|
| 174 |
+
delete_response = api_client.delete(detail_url)
|
| 175 |
+
|
| 176 |
+
assert delete_response.status_code == status.HTTP_204_NO_CONTENT
|
| 177 |
+
|
| 178 |
+
# Verify deletion
|
| 179 |
+
retrieve_response = api_client.get(detail_url)
|
| 180 |
+
assert retrieve_response.status_code == status.HTTP_404_NOT_FOUND
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
@pytest.mark.django_db
|
| 184 |
+
@pytest.mark.deployment
|
| 185 |
+
class TestDatabaseMigrations:
|
| 186 |
+
"""Test that database migrations have been applied correctly."""
|
| 187 |
+
|
| 188 |
+
def test_profile_model_exists(self, api_client: APIClient) -> None:
|
| 189 |
+
"""Test Profile model table exists and is accessible."""
|
| 190 |
+
url = reverse("profile-list")
|
| 191 |
+
|
| 192 |
+
response = api_client.get(url)
|
| 193 |
+
|
| 194 |
+
assert response.status_code == status.HTTP_200_OK
|
| 195 |
+
|
| 196 |
+
def test_wallsection_model_exists(self, api_client: APIClient) -> None:
|
| 197 |
+
"""Test WallSection model table exists and is accessible."""
|
| 198 |
+
url = reverse("wallsection-list")
|
| 199 |
+
|
| 200 |
+
response = api_client.get(url)
|
| 201 |
+
|
| 202 |
+
assert response.status_code == status.HTTP_200_OK
|
| 203 |
+
|
| 204 |
+
def test_profile_fields_exist(self, api_client: APIClient) -> None:
|
| 205 |
+
"""Test Profile model has all required fields."""
|
| 206 |
+
url = reverse("profile-list")
|
| 207 |
+
payload = {
|
| 208 |
+
"name": "Field Test",
|
| 209 |
+
"team_lead": "Test Lead",
|
| 210 |
+
"is_active": True,
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
response = api_client.post(url, payload, format="json")
|
| 214 |
+
|
| 215 |
+
assert response.status_code == status.HTTP_201_CREATED
|
| 216 |
+
assert "id" in response.data
|
| 217 |
+
assert "name" in response.data
|
| 218 |
+
assert "team_lead" in response.data
|
| 219 |
+
assert "is_active" in response.data
|
| 220 |
+
assert "created_at" in response.data
|
| 221 |
+
assert "updated_at" in response.data
|
| 222 |
+
|
| 223 |
+
def test_wallsection_fields_exist(self, api_client: APIClient) -> None:
|
| 224 |
+
"""Test WallSection model has all required fields."""
|
| 225 |
+
# Create profile first
|
| 226 |
+
profile_url = reverse("profile-list")
|
| 227 |
+
profile_response = api_client.post(
|
| 228 |
+
profile_url,
|
| 229 |
+
{"name": "Test", "team_lead": "Lead"},
|
| 230 |
+
format="json",
|
| 231 |
+
)
|
| 232 |
+
profile_id = profile_response.data["id"]
|
| 233 |
+
|
| 234 |
+
# Create wallsection
|
| 235 |
+
url = reverse("wallsection-list")
|
| 236 |
+
payload = {
|
| 237 |
+
"profile": profile_id,
|
| 238 |
+
"section_name": "Test Section",
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
response = api_client.post(url, payload, format="json")
|
| 242 |
+
|
| 243 |
+
assert response.status_code == status.HTTP_201_CREATED
|
| 244 |
+
assert "id" in response.data
|
| 245 |
+
assert "profile" in response.data
|
| 246 |
+
assert "section_name" in response.data
|
| 247 |
+
assert "created_at" in response.data
|