fix: Batch 1 hotfix — S-02 + S-07 + BUG-1 + BUG-2 + OPS-1#40
Merged
Conversation
… prod base.py:150 deixa SIGNING_KEY do SimpleJWT cair em SECRET_KEY como fallback (conveniente em dev). Em prod e divida critica de seguranca: leak do SECRET_KEY compromete sessao E JWT simultaneamente, permitindo forja de access token e impersonacao total (incluindo role dev que e imune a ban por design). Replica padrao F2-B-03 da busca (commit 96cdad5): production.py faz raise ImproperlyConfigured quando JWT_SIGNING_KEY esta vazia OU igual a SECRET_KEY. 3 testes em subprocess isolado (django.setup ja roda na sessao pytest atual; importlib.reload corromperia apps): - JWT_SIGNING_KEY == SECRET_KEY -> rc=2 + mensagem cita JWT_SIGNING_KEY + S-02 - JWT_SIGNING_KEY vazia -> rc=2 (cai no default=SECRET_KEY) - JWT_SIGNING_KEY distinta -> rc=0 (load sucesso) Espelha _base_env do test search/test_settings_production.py para incluir JWT_SIGNING_KEY distinta nao-acidentalmente disparar o guard novo (cada arquivo testa SEU proprio guard). Closes F-01 T01.X1 (Open Question §10 de F-01-autenticacao-jwt-cookie-httponly.md). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…try) services.py:42-58 (antes do fix) envolvia msg.send(fail_silently=False) num try/except Exception: return False. Resultado: falhas SMTP eram silenciosamente engolidas E matavam o autoretry_for=(Exception,) do wrapper Celery send_welcome_email (tasks.py:50-71). Subscriber nunca recebia welcome e ninguem sabia. Comentario original justificava o swallow com "nao quebrar fluxo publico de subscribe", mas grep confirma que views.py:22 SEMPRE chama send_welcome via .delay() (async, Celery). O argumento era historic / falso. Em pratica, swallow so mascarava falhas SMTP da observabilidade. Apos fix: excecoes propagam para Celery autoretry (max_retries=3, backoff exponencial). Falhas SMTP transientes ganham 3 tentativas; falhas permanentes vao pro DLQ + Sentry alerta. 3 testes: - caminho feliz retorna True + confirma fail_silently=False - SMTPServerDisconnected propaga (autoretry pega) - RuntimeError generico propaga (defesa em profundidade) Closes F-40 CA12 (BUG-2). Hotfix candidate critico do CONCERNS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…on email
Template article_notification.html linha 31 (antes do fix) usava
{{ article.cover_image.url }} direto, que retorna /media/articles/... —
caminho relativo do MEDIA_URL Django. Clientes de email (Gmail, Outlook,
Apple Mail) NAO resolvem caminhos relativos contra base alguma. Resultado:
TODOS os subscribers recebiam placeholder broken-image em TODA notificacao
de novo artigo.
Test suite atual nao pegava porque nao havia assert de URL absoluta.
Fix:
- services.py:_dispatch_article_notification_sync monta
cover_image_absolute_url = SITE_URL + article.cover_image.url
e passa no ctx do template
- article_notification.html usa cover_image_absolute_url em vez de
article.cover_image.url
Test em test_services.py:test_article_notification_uses_absolute_cover_url
mocka EmailMultiAlternatives, captura HTML attached e assert que:
- URL absoluta com SITE_URL presente
- URL relativa /media/... NAO esta presente
Closes F-40 CA11 (BUG-1). Junto com BUG-2 do commit anterior, fecha as 2
falhas silenciosas mais serias do newsletter (cover broken + welcome
sem retry).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Antes: POST /api/v1/articles/<slug>/comments/ caía só no throttle global 'user'=1000/hour (base.py:195). Em artigo viral, leitor autenticado podia floodar 1000 comentários numa hora — saturar moderação reativa antes do sinal aparecer. Pattern já vivo em apps/users/views.py:32 (auth scope 10/min via ScopedRateThrottle) só não tinha sido aplicado a comments. Fix: - base.py: novo scope `comments_create` = 10/minute. - development.py: relaxa para 10000/hour em dev (smoke manual sem 429). - CommentListCreateView: throttle_scope='comments_create' + get_throttles() retorna [ScopedRateThrottle()] APENAS em POST. GET list segue defaults. - conftest.py: autouse fixture limpa cache.clear() entre testes (DRF ScopedRateThrottle armazena histórico em cache compartilhado). Tests TDD (2 novos em apps/comments/tests/test_views.py): - test_throttle_comments_create_blocks_flood: 10 POSTs passam (201), 11º retorna 429 com Retry-After header. Monkeypatch direto em ScopedRateThrottle.THROTTLE_RATES (atributo de classe capturado em import-time — override via settings fixture NÃO propaga). - test_throttle_comments_create_does_not_affect_get_list: 20 GETs consecutivos retornam 200 mesmo com rate proposital de 2/min — prova que GET não passa pelo ScopedRateThrottle. Trade-off de 10/min: leitor legítimo posta 1 a cada 6s (mais que humano real). Bot que floodar 11 posts em <60s sinaliza abuso e leva 429 + Retry-After. Operacionalmente preferível a moderação reativa pós-flood. Refs: CONCERNS.md S-07 · F-20 CA12 · ADR pattern users/views.py:32 Test sweep: 334 passed, 0 regressions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Antes: ao mudar status=DRAFT → PUBLISHED no Django admin, o campo `published_at` (null=True, blank=True) ficava None — editor precisava preencher manualmente todo publish, e esquecia quase sempre. Sintomas em produção: - ordering '-published_at' jogava artigos publicados sem timestamp para o final de QuerySets (apareciam por último em listas) - template `published_at|date:"d \d\e F"` renderizava string vazia (Article.tsx mostra " · " em vez de " · 8 de junho de 2026") - SearchView caía no fallback ranking `-created_at`, perdendo sinal editorial Fix em apps/articles/admin.py:save_model: - if status == PUBLISHED AND published_at is None → seta timezone.now() - NÃO sobrescreve published_at existente (respeita agendamento manual e edição de artigos já publicados — invariante preservada) - chama super().save_model() inalterado Tests TDD (3 novos em apps/articles/tests/test_admin.py): - transição DRAFT→PUBLISHED com published_at=None: assert seta now() dentro da janela do save_model (before <= published_at <= after) - DRAFT mantém published_at=None: não polui timestamp prematuro - PUBLISHED com published_at já setado (7 dias atrás): preserva exatamente — diff < 1s (tolerância p/ microssegundos) RequestFactory + FallbackStorage para construir request admin sem subir TestClient completo. AdminSite() vanilla para isolar ModelAdmin sob teste. Refs: CONCERNS.md OPS-1 · F-10 (publicação editorial via admin) Test sweep: 337 passed, 0 regressions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resumo
Batch 1 dos 13 achados levantados pelos 13 agentes brownfield+retroativos (sessão de 9-jun-2026). Cinco hotfixes em TDD red-green-refactor, cada um commit atômico com testes que protegem regressão.
Hotfixes por severidade
8828aabproduction.pyagora fazraise ImproperlyConfiguredseJWT_SIGNING_KEYausente ou igual aSECRET_KEY. Defesa em profundidade — leak de SECRET_KEY não pode comprometer assinatura JWT.b08055csend_welcomeremoveu otry/except Exception: return Falseque silenciava falhas SMTP e matava oautoretry_for=(Exception,)do Celery wrapper. Falhas agora propagam → 3 retries com backoff → Sentry.87f65dccover_image.urlé relativa (/media/...) e clientes de email não resolvem. Service monta URL absoluta comSITE_URLe injeta comocover_image_absolute_urlno ctx; template usa a var nova. Subscribers viam broken-image em TODA notificação.e3f552ecomments_create(10/min em prod, 10000/h em dev).CommentListCreateView.get_throttles()aplicaScopedRateThrottleAPENAS em POST — GET list segue defaults. Antes só caía emuser=1000/h, baixo demais p/ anti-flood em artigo viral.84096ebArticleAdmin.save_modelagora setapublished_at = now()quando status→PUBLISHEDe campo está vazio. NÃO sobrescreve valor existente. Antes editor esquecia o campo → ordering-published_atjogava artigos pro fim + template renderizava data vazia + busca caía no fallback-created_at.Cobertura de testes (TDD em todos)
apps/users/tests/test_settings_production.py(subprocess isolado p/ não tocar settings da sessão atual)apps/newsletter/tests/test_services.py(propaga SMTP, propaga RuntimeError, retorna True em sucesso, cover URL absoluta no HTML, sem URL relativa vazada)apps/comments/tests/test_views.py(11º POST → 429 com Retry-After; 20 GETs com rate 2/min → todos 200). Monkeypatch direto emScopedRateThrottle.THROTTLE_RATESporque o atributo é de classe capturado em import-time — override viasettingsfixture não propaga.apps/articles/tests/test_admin.py(transição seta, DRAFT mantém null, valor existente preservado)Validação
uv run pytest apps/→ 337 passed, 27 skipped, 0 regressions (84 → 89 testes após batch)Coordenação com PR #39 (docs reorg)
Os arquivos
docs/backlog/features/F-NN-*.mdque rastreiam estes fixes vivem apenas emchore/docs-reorg-requirements-backlog(PR #39). Após merge de #39 emdevelop, abrirei umchore(docs): backfill hashes Batch 1 hotfix em F-NNspara fechar a rastreabilidade Task↔commit. Não há bloqueio mútuo de merge.Roadmap Batch 2 (próximo PR)
🔴 S-10 LGPD AuditLog (Art.16 — blocker pre-go-live) · S-04 2FA strategy ADR · OPS-3 ban+JWT blocklist Redis · S-03 CSP report-only → enforce · timing attack newsletter signup · view_count migração Redis.
Test plan
cd backend && uv run pytest apps/— 337 passeduv run python manage.py check(não regrediu)🤖 Generated with Claude Code