mirror of
https://github.com/ramvignesh-b/pi-ku.git
synced 2026-05-04 08:56:52 +00:00
feat: implement letter encryption fields, lifecycle timestamps, and API endpoints with validation
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 6.0.4 on 2026-04-10 15:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("letters", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="letter",
|
||||
name="encrypted_content",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="letter",
|
||||
name="encrypted_metadata",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 6.0.4 on 2026-04-10 15:47
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("letters", "0002_letter_encrypted_content_letter_encrypted_metadata"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="letter",
|
||||
name="unlock_at",
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 6.0.4 on 2026-04-10 16:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("letters", "0003_letter_unlock_at"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="letter",
|
||||
name="burned_at",
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="letter",
|
||||
name="opened_at",
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="letter",
|
||||
name="sealed_at",
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,6 +1,7 @@
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
|
||||
|
||||
@@ -21,6 +22,18 @@ class Letter(models.Model):
|
||||
status = models.CharField(max_length=10, choices=Status.choices, default=Status.DRAFT)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
encrypted_content = models.TextField(null=True, blank=True)
|
||||
encrypted_metadata = models.TextField(null=True, blank=True)
|
||||
unlock_at = models.DateTimeField(null=True, blank=True)
|
||||
sealed_at = models.DateTimeField(null=True, blank=True)
|
||||
opened_at = models.DateTimeField(null=True, blank=True)
|
||||
burned_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
def clean(self):
|
||||
# custom validation
|
||||
super().clean()
|
||||
if self.type == Letter.Type.VAULT and self.status == Letter.Status.SEALED and not self.unlock_at:
|
||||
raise ValidationError("A sealed VAULT letter must have an unlock_date.")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.type} - {self.status}"
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import Letter
|
||||
|
||||
|
||||
class LetterSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Letter
|
||||
fields = [
|
||||
"public_id",
|
||||
"type",
|
||||
"status",
|
||||
"encrypted_content",
|
||||
"encrypted_metadata",
|
||||
"unlock_at",
|
||||
"sealed_at",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
] # user to be fetched from request
|
||||
read_only_fields = ["public_id", "created_at", "updated_at"]
|
||||
|
||||
def create(self, validated_data):
|
||||
user = self.context["request"].user # get user from access token
|
||||
return Letter.objects.create(user=user, **validated_data)
|
||||
@@ -1,5 +1,6 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from .models import Letter
|
||||
|
||||
@@ -10,7 +11,7 @@ class LetterModelTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(email="test@pi-ku.app", password="password1234", full_name="Test User")
|
||||
|
||||
def test_create_letter_basic(self):
|
||||
def test_create_letter_draft(self):
|
||||
"""create a basic Letter model with required fields"""
|
||||
letter = Letter.objects.create(user=self.user, type="KEPT", status="DRAFT")
|
||||
|
||||
@@ -18,7 +19,43 @@ class LetterModelTest(TestCase):
|
||||
self.assertEqual(letter.type, "KEPT")
|
||||
self.assertEqual(letter.status, "DRAFT")
|
||||
self.assertIsNotNone(letter.public_id)
|
||||
|
||||
self.assertIsNone(letter.unlock_at)
|
||||
self.assertEqual(letter.encrypted_content, None)
|
||||
self.assertEqual(letter.encrypted_metadata, None)
|
||||
self.assertIsNone(letter.sealed_at)
|
||||
self.assertIsNone(letter.opened_at)
|
||||
self.assertIsNone(letter.burned_at)
|
||||
# Verify timestamps are auto-added
|
||||
self.assertIsNotNone(letter.created_at)
|
||||
self.assertIsNotNone(letter.updated_at)
|
||||
|
||||
def test_vault_requires_unlock_date_when_sealed(self):
|
||||
"""a sealed VAULT letter must have an unlock_date"""
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
letter = Letter(
|
||||
user=self.user,
|
||||
type=Letter.Type.VAULT,
|
||||
status=Letter.Status.SEALED,
|
||||
encrypted_content="enc_v1...",
|
||||
)
|
||||
with self.assertRaises(ValidationError):
|
||||
letter.full_clean()
|
||||
|
||||
|
||||
class LetterAPITest(APITestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(email="api@pi-ku.app", password="password1234", full_name="API User")
|
||||
self.client.force_authenticate(user=self.user)
|
||||
self.url = "/api/letters/"
|
||||
|
||||
def test_create_draft_letter_api(self):
|
||||
"""Test API can successfully create a basic draft letter."""
|
||||
payload = {"type": "KEPT", "encrypted_content": "enc_xyz==", "encrypted_metadata": "enc_meta=="}
|
||||
|
||||
response = self.client.post(self.url, payload)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(Letter.objects.count(), 1)
|
||||
self.assertEqual(Letter.objects.get().status, "DRAFT")
|
||||
self.assertEqual(Letter.objects.get().type, "KEPT")
|
||||
self.assertEqual(Letter.objects.get().user, self.user)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import LetterView
|
||||
|
||||
urlpatterns = [
|
||||
path("", LetterView.as_view(), name="letter-list-create"),
|
||||
]
|
||||
@@ -1 +1,15 @@
|
||||
# Create your views here.
|
||||
from rest_framework import generics
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from letters.models import Letter
|
||||
from letters.serializers import LetterSerializer
|
||||
|
||||
|
||||
class LetterView(generics.ListCreateAPIView):
|
||||
serializer_class = LetterSerializer
|
||||
# enforce auth guard
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
"""return only letters of the authenticated user"""
|
||||
return Letter.objects.filter(user=self.request.user)
|
||||
|
||||
Reference in New Issue
Block a user