# `MG-8 QA-1` — Mapa de cobertura da migração

**Story:** `docs/stories/MG-8-paridade.md` · **Data:** 07/08/2026
**Gerado por:** `python3 docs/cobertura.py` (rode antes de acreditar nesta tabela)

O risco escrito na própria story é este: *"é a story mais fácil de fingir que foi
feita"*. Por isso o mapa é **script**, não tabela escrita à mão — tabela à mão
envelhece em silêncio, e quem lê não tem como saber.

O critério do script é rigoroso de propósito: um caso conta como verificado
quando **o id dele aparece** num teste da stack nova (rule, Function, widget ou
device) ou como origem declarada de um caso `MG-*`. Citação de id é o que permite
comparar linha a linha com a execução do NestJS.

## Onde estamos

| | casos | |
| -- | -- | -- |
| verificados com o id na stack nova | **281** | 66% |
| a verificar | 138 | 33% |
| cancelados | 1 | — |
| **total nos planos da fase 1** | **420** | |

**66% não é "66% do produto funciona".** É quanto do plano está *provado do jeito
que esta story exige*. Os 138 restantes se dividem em três grupos bem diferentes,
e tratá-los como uma coisa só seria o erro que a story avisa:

### Grupo A — teste existe, etiqueta não

Casos de tela cobertos pelos **244 testes de widget**, que continuam passando e
não dependem do backend: eles falam com repositório falso. O que falta é o id no
nome do caso. Não há risco de regressão silenciosa aqui; há risco de contabilidade
errada — que é o que este mapa existe para evitar.

Em 07/08 esse grupo encolheu bastante: `PP-3` (contagem de prazo), `PP-4`
(rascunho), `AC-1` (tela de cadastro) e `AC-3` (cold start) tinham teste passando
**sem o id no nome**. Etiquetar não é trabalho de fachada: sem o id, ninguém
consegue dizer se o caso do plano está coberto ou se o teste que existe é sobre
outra coisa — e a resposta muda a decisão de desligar o NestJS.

### Grupo B — o mecanismo deixou de existir

Casos que testavam coisa que o Firebase Auth passou a fazer. O critério da story é
explícito: **reescrever e registrar, nunca descartar em silêncio**.

| Caso | O que testava | O que vale agora |
| -- | -- | -- |
| `AC-3-01`, `AC-3-05`, `AC-3-06`, `AC-3-10` | rotação e revogação de refresh token nossos | o SDK do Firebase renova e revoga sozinho; não há token nosso para rodar |
| `AC-3-07`, `AC-3-08` | logout invalidando o access token | `signOut()` local + a sessão do Auth; o caso vira "cold start depois do logout" |
| `AC-2-03`, `AC-2-04` | limite de tentativas de login | passa a ser do Auth (`too-many-requests`), com política do Google, não nossa |
| `AC-2-09` | ausência de recuperação de senha | **deixa de valer**: o Auth dá recuperação de graça, e `AC-4` volta ao escopo |
| `AC-1-02` | senha e hash nunca voltam na resposta | não existe resposta nossa com senha: a credencial nunca passa pelo nosso servidor |
| `PP-10-11` | trava de sincronização concorrente | virou lock em documento (`MG-6-03`/`MG-6-03b`) |
| `PR-3-11` | ranking em uma consulta | passa a medir **leituras**, e a medição está abaixo |

### Grupo C — falta reexecutar

O resto: casos de regra que existem igual nos dois backends e ainda não foram
reexecutados com o id na stack nova. É o trabalho de `QA-3`, e é onde ficam os
piores riscos por story:

- 🔴 `PP-8` (lembrete de prazo) segue em **zero**, e vai continuar: ele depende de
  `MG-6 BE-3`/`BE-4`, que não existem ainda.
- ✅ `CV-2` (7/9) e `PR-6` (10/10) saíram do zero em 07/08. Em `CV-2` só sobraram
  os dois casos de tela. **Elas não eram difíceis: eram as que ninguém tinha
  olhado**, que é exatamente o que o mapa existe para revelar.
- ✅ `PP-3` (10/11), `CV-5` (9/10) e `PR-6` (10/10) fechados ou quase, em 07/08.
  O que sobrou em cada um é caso de tela sem teste correspondente.
- 🟡 `CC-6`, `CC-7`, `CV-3`, `CV-4` e `PP-4` são os próximos maiores.
- ✅ `PR-1` (cálculo do ponto) em 100% — portada com os 14 casos.

## O que já foi reexecutado com o id original

O arquivo `paridade-seguranca.spec.ts` traz **34 casos** de sigilo, prazo, papel,
autorização e convite mantendo o id de origem: `PP-7-05` continua sendo
`PP-7-05`. Foi o grupo escolhido primeiro de propósito — é o que a story aponta
como o primeiro a sumir quando "os testes passam" basta como prova.

Duas coisas mudaram de **meio** e estão registradas caso a caso: onde o NestJS
recusava com um `if` num service, hoje recusa **o banco** (rule) ou **a callable**.
Para quem chama a API direto, a diferença é grande: não existe mais resposta para
filtrar.

## `QA-5` — custo medido, não estimado

Medido com um contador em volta do Firestore, rodando os fluxos de verdade contra
o emulador (`custo.spec.ts`). O Firestore cobra por **documento**, então é
documento que se conta.

| fluxo | leituras | escritas |
| -- | -- | -- |
| criar bolão (10 jogos) | 13 | 2 |
| entrar por convite | 14 | 1 |
| abrir a rodada no app (10 jogos) | 11 | 0 |
| palpitar na rodada (10 jogos) | 0 | 10 |
| ranking geral (página de 50, bolão de 60) | 50 | 0 |
| ranking da rodada (60 participantes) | 72 | 0 |
| extrato da rodada | 22 | 0 |
| quem não palpitou (60 participantes) | 82 | 0 |
| encerrar o bolão | 70 | 1 |

**O requisito não funcional está cumprido:** o ranking geral custa uma leitura por
**linha da página**, não por participante nem por palpite. Somar na leitura seria
60 × 10 × 38 documentos por abertura de tela — o free tier acabaria antes do
returno do campeonato. É o que justifica a classificação materializada.

O que **cresce com o bolão** é a cobrança da rodada (82 para 60 participantes) e o
encerramento (70). São ações raras — o admin abre a cobrança uma vez por rodada, e
encerra uma vez por temporada. Com o free tier em 50 mil leituras/dia, um bolão de
60 pessoas gasta por rodada, no pior caso, algo como:

- 60 pessoas × (11 abrir a rodada + 22 extrato + 50 ranking) ≈ **5.000 leituras**
- \+ 82 da cobrança + 72 do ranking da rodada ≈ 5.150

Uma rodada por semana, com folga de 10× para o participante que abre o app várias
vezes ao dia. **Cabe.**

## As duas decisões do dono, respondidas em 07/08

| Decisão | Resposta | O que muda |
| -- | -- | -- |
| *Email enumeration protection* | **já estava ligada** | `AC-2-02` sai de bloqueado. O login não distingue senha errada de e-mail inexistente, e o app já tratava os dois com a mesma mensagem — `traduzirAuth` mapeia `user-not-found`, `wrong-password` e `invalid-credential` para o mesmo texto. |
| Identity Platform | **ligado** | O mínimo de 8 caracteres passa a ser do **servidor**, não só do app. `AC-1-05` deixa de ser lacuna conhecida. |

**As duas se verificam por configuração, não por teste automatizado.** O emulador
do Auth não aplica política de senha nem proteção de enumeração — ele aceita
qualquer coisa de propósito, para o teste não depender de nuvem. Verificar isso na
suíte exigiria falar com o projeto real, que é justamente o que a story proíbe.
Ficam registradas aqui, com data, e conferidas na verificação em device.

## O que falta para `MG-8` fechar

- `QA-3`: reexecutar o resto do grupo C. `PP-3`, `CV-5` e `PP-4` são os maiores
  buracos; `PP-8` fica bloqueado até `MG-6 BE-3`/`BE-4`.
- `QA-4`: **uma divergência registrada** — a primeira, abaixo.

## `QA-4` — divergências encontradas

### `CV-4-01` — quem saía continuava no ranking

**Encontrada em 08/08, ao reexecutar o caso. Corrigida no mesmo dia.**

No NestJS o ranking era **somado na leitura** a partir de quem estava ativo: sair
do bolão tirava a pessoa da lista na hora. No Firestore ele é agregado
materializado, e a classificação só era reescrita na **apuração** — quem saía
continuava aparecendo até o próximo jogo terminar, o que pode ser semana que vem
ou nunca (bolão encerrado não tem próximo jogo).

Eram dois defeitos empilhados, e o segundo é o que dá o susto:

1. A mudança de vínculo não disparava recálculo. Resolvido no gatilho que já
   cuidava do índice — mudou o vínculo, recalcula.
2. **O recálculo não apagava linha de quem saiu.** Ele reescrevia a de cada
   participante ativo, e o que não é reescrito **fica**. Agregado materializado só
   está correto quando alguém cuida do que sobra.

É exatamente o tipo de coisa que a migração introduz sem avisar: a regra não
mudou, o lugar mudou, e o novo lugar tem uma propriedade que o antigo não tinha —
memória.
