## Summary Closes #5. - Composer can **Send now** without a schedule time (Schedule and Save draft still available) - Account names wrap and show the platform id so a bad OAuth label is still readable - Social accounts page can rename the display name; re-auth keeps a custom name instead of overwriting it with a placeholder - Worker polls `dispatch_due` every 15s so scheduled campaigns and social posts fire without a manual command; local `docker compose up` now starts the worker ## Test plan - [ ] Open Social → Compose: confirm **Send now**, **Schedule**, and **Save draft** buttons (no combined Save / publish) - [ ] Send now with a connected account and caption → post queues immediately, no datetime required - [ ] Schedule with a datetime → post is scheduled; Schedule with a blank time → validation error - [ ] Composer account list shows full name plus external id, wrapping if the name is long - [ ] Social accounts → rename a messy label → new name shows in composer without reconnecting - [ ] Re-authorize an account that was renamed → custom name is kept - [ ] Rebuild and start compose (`docker compose up --build`) so the **worker** service is running - [ ] Schedule a social post ~2 minutes ahead → after due time, worker logs `Enqueued N due item(s).` and the post leaves Scheduled (published or failed) - [ ] `docker compose logs -f worker` shows `Polling due scheduled work every 15s.` Reviewed-on: #6
183 lines
6.4 KiB
Python
183 lines
6.4 KiB
Python
from unittest.mock import patch
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.test import Client, TestCase
|
|
from django.urls import reverse
|
|
|
|
from social.models import (
|
|
Platform,
|
|
SocialAccount,
|
|
SocialPost,
|
|
is_generic_account_label,
|
|
)
|
|
|
|
|
|
class AccountLabelTests(TestCase):
|
|
def test_generic_placeholders(self):
|
|
self.assertTrue(is_generic_account_label(""))
|
|
self.assertTrue(is_generic_account_label("LinkedIn member"))
|
|
self.assertTrue(is_generic_account_label("Instagram account"))
|
|
self.assertTrue(is_generic_account_label("Page 123456"))
|
|
self.assertTrue(is_generic_account_label("urn:li:person:abc"))
|
|
self.assertFalse(is_generic_account_label("Monica Dhillon"))
|
|
self.assertFalse(is_generic_account_label("@mkdrealtor"))
|
|
|
|
def test_resolve_keeps_custom_name(self):
|
|
existing = SocialAccount(
|
|
platform=Platform.LINKEDIN,
|
|
label="Monica (personal)",
|
|
external_id="urn:li:person:abc",
|
|
)
|
|
self.assertEqual(
|
|
SocialAccount.resolve_label(existing, "LinkedIn member"),
|
|
"Monica (personal)",
|
|
)
|
|
|
|
def test_resolve_replaces_generic(self):
|
|
existing = SocialAccount(
|
|
platform=Platform.LINKEDIN,
|
|
label="LinkedIn member",
|
|
external_id="urn:li:person:abc",
|
|
)
|
|
self.assertEqual(
|
|
SocialAccount.resolve_label(existing, "Monica Dhillon"),
|
|
"Monica Dhillon",
|
|
)
|
|
|
|
|
|
class SocialComposerTests(TestCase):
|
|
def setUp(self):
|
|
User = get_user_model()
|
|
self.user = User.objects.create_user(
|
|
username="composer", password="test-pass-123"
|
|
)
|
|
self.client = Client()
|
|
self.client.login(username="composer", password="test-pass-123")
|
|
self.account = SocialAccount.objects.create(
|
|
platform=Platform.LINKEDIN,
|
|
label="LinkedIn member",
|
|
external_id="urn:li:person:abc123",
|
|
is_active=True,
|
|
owner=self.user,
|
|
)
|
|
|
|
def test_composer_shows_send_now_and_full_name(self):
|
|
response = self.client.get(reverse("social:composer"))
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "Send now")
|
|
self.assertContains(response, "Schedule")
|
|
self.assertContains(response, "LinkedIn member")
|
|
self.assertContains(response, "urn:li:person:abc123")
|
|
self.assertNotContains(response, "Save / publish")
|
|
|
|
def test_send_now_queues_without_schedule(self):
|
|
mock_task = patch("social.views.publish_social_post").start()
|
|
self.addCleanup(patch.stopall)
|
|
response = self.client.post(
|
|
reverse("social:composer"),
|
|
{
|
|
"body": "Open house Saturday",
|
|
"account_ids": [str(self.account.pk)],
|
|
"action": "send_now",
|
|
},
|
|
)
|
|
post = SocialPost.objects.get()
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertEqual(
|
|
response.url, reverse("social:post_detail", kwargs={"pk": post.pk})
|
|
)
|
|
self.assertEqual(post.status, SocialPost.Status.QUEUED)
|
|
self.assertIsNotNone(post.scheduled_for)
|
|
self.assertEqual(post.targets.count(), 1)
|
|
mock_task.enqueue.assert_called_once_with(post_id=str(post.pk))
|
|
|
|
def test_schedule_requires_datetime(self):
|
|
response = self.client.post(
|
|
reverse("social:composer"),
|
|
{
|
|
"body": "Open house Saturday",
|
|
"account_ids": [str(self.account.pk)],
|
|
"action": "schedule",
|
|
},
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "Pick a schedule date/time, or choose Send now.")
|
|
self.assertFalse(SocialPost.objects.exists())
|
|
|
|
def test_schedule_with_datetime(self):
|
|
response = self.client.post(
|
|
reverse("social:composer"),
|
|
{
|
|
"body": "Open house Saturday",
|
|
"account_ids": [str(self.account.pk)],
|
|
"action": "schedule",
|
|
"scheduled_for": "2026-08-10T09:30",
|
|
},
|
|
)
|
|
post = SocialPost.objects.get()
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertEqual(post.status, SocialPost.Status.SCHEDULED)
|
|
self.assertIsNotNone(post.scheduled_for)
|
|
|
|
def test_save_draft(self):
|
|
response = self.client.post(
|
|
reverse("social:composer"),
|
|
{
|
|
"body": "Draft caption",
|
|
"action": "save",
|
|
},
|
|
)
|
|
post = SocialPost.objects.get()
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertEqual(post.status, SocialPost.Status.DRAFT)
|
|
|
|
|
|
class SocialAccountRenameTests(TestCase):
|
|
def setUp(self):
|
|
User = get_user_model()
|
|
self.user = User.objects.create_user(
|
|
username="renamer", password="test-pass-123"
|
|
)
|
|
self.client = Client()
|
|
self.client.login(username="renamer", password="test-pass-123")
|
|
self.account = SocialAccount.objects.create(
|
|
platform=Platform.FACEBOOK,
|
|
label="Page 999",
|
|
external_id="999",
|
|
is_active=True,
|
|
owner=self.user,
|
|
)
|
|
|
|
def test_accounts_page_shows_rename_field(self):
|
|
response = self.client.get(reverse("social:account_list"))
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, 'name="label"')
|
|
self.assertContains(response, "Page 999")
|
|
self.assertContains(response, "Rename")
|
|
|
|
def test_rename_updates_label(self):
|
|
response = self.client.post(
|
|
reverse("social:account_list"),
|
|
{
|
|
"action": "rename",
|
|
"account_id": str(self.account.pk),
|
|
"label": "Monica Dhillon Realty",
|
|
},
|
|
)
|
|
self.assertEqual(response.status_code, 302)
|
|
self.account.refresh_from_db()
|
|
self.assertEqual(self.account.label, "Monica Dhillon Realty")
|
|
|
|
def test_rename_rejects_blank(self):
|
|
response = self.client.post(
|
|
reverse("social:account_list"),
|
|
{
|
|
"action": "rename",
|
|
"account_id": str(self.account.pk),
|
|
"label": " ",
|
|
},
|
|
)
|
|
self.assertEqual(response.status_code, 302)
|
|
self.account.refresh_from_db()
|
|
self.assertEqual(self.account.label, "Page 999")
|