Skip to content

fix: Batch 1 hotfix — S-02 + S-07 + BUG-1 + BUG-2 + OPS-1#40

Merged
GabeMarques-Intetsu merged 5 commits into
developfrom
fix/hotfix-batch-1-bugs
Jun 10, 2026
Merged

fix: Batch 1 hotfix — S-02 + S-07 + BUG-1 + BUG-2 + OPS-1#40
GabeMarques-Intetsu merged 5 commits into
developfrom
fix/hotfix-batch-1-bugs

Conversation

@GabeMarques-Intetsu

Copy link
Copy Markdown
Collaborator

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.

Batch 2 (S-10 LGPD, S-04 2FA, OPS-3 ban+JWT blocklist, S-03 CSP flip, SendGrid timing, view_count Redis) virá em PR separado após este merge — escopo de Batch 2 envolve ADRs novos.

Hotfixes por severidade

ID Sev Commit O quê
S-02 🟠 segurança auth 8828aab production.py agora faz raise ImproperlyConfigured se JWT_SIGNING_KEY ausente ou igual a SECRET_KEY. Defesa em profundidade — leak de SECRET_KEY não pode comprometer assinatura JWT.
BUG-2 🔴 newsletter b08055c send_welcome removeu o try/except Exception: return False que silenciava falhas SMTP e matava o autoretry_for=(Exception,) do Celery wrapper. Falhas agora propagam → 3 retries com backoff → Sentry.
BUG-1 🔴 newsletter 87f65dc cover_image.url é relativa (/media/...) e clientes de email não resolvem. Service monta URL absoluta com SITE_URL e injeta como cover_image_absolute_url no ctx; template usa a var nova. Subscribers viam broken-image em TODA notificação.
S-07 🟡 anti-flood e3f552e Novo scope DRF comments_create (10/min em prod, 10000/h em dev). CommentListCreateView.get_throttles() aplica ScopedRateThrottle APENAS em POST — GET list segue defaults. Antes só caía em user=1000/h, baixo demais p/ anti-flood em artigo viral.
OPS-1 🟠 produto 84096eb ArticleAdmin.save_model agora seta published_at = now() quando status→PUBLISHED e campo está vazio. NÃO sobrescreve valor existente. Antes editor esquecia o campo → ordering -published_at jogava artigos pro fim + template renderizava data vazia + busca caía no fallback -created_at.

Cobertura de testes (TDD em todos)

  • S-02: 3 testes em apps/users/tests/test_settings_production.py (subprocess isolado p/ não tocar settings da sessão atual)
  • BUG-1+BUG-2: 5 testes em apps/newsletter/tests/test_services.py (propaga SMTP, propaga RuntimeError, retorna True em sucesso, cover URL absoluta no HTML, sem URL relativa vazada)
  • S-07: 2 testes em apps/comments/tests/test_views.py (11º POST → 429 com Retry-After; 20 GETs com rate 2/min → todos 200). Monkeypatch direto em ScopedRateThrottle.THROTTLE_RATES porque o atributo é de classe capturado em import-time — override via settings fixture não propaga.
  • OPS-1: 3 testes em 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)
  • ✅ Cada commit roda isoladamente (TDD red verificado em cada um)
  • ✅ Cobertura sobe — política dura §6 INEGOCIÁVEL atendida

Coordenação com PR #39 (docs reorg)

Os arquivos docs/backlog/features/F-NN-*.md que rastreiam estes fixes vivem apenas em chore/docs-reorg-requirements-backlog (PR #39). Após merge de #39 em develop, abrirei um chore(docs): backfill hashes Batch 1 hotfix em F-NNs para 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 passed
  • uv run python manage.py check (não regrediu)
  • Smoke manual pós-merge: subscribe newsletter em dev → confirma SMTP retry pega; publicar artigo via admin sem preencher published_at → confirma timestamp populado.

🤖 Generated with Claude Code

GabeMarques-Intetsu and others added 5 commits June 9, 2026 20:55
… 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>
@GabeMarques-Intetsu GabeMarques-Intetsu merged commit f60bbc5 into develop Jun 10, 2026
6 of 7 checks passed
@GabeMarques-Intetsu GabeMarques-Intetsu deleted the fix/hotfix-batch-1-bugs branch June 10, 2026 00:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant