Introdução
A página de achados e perdidos agora precisa de buscas úteis. Um visitante que procura uma bolsa azul não deve receber todos os objetos azuis nem todas as bolsas. Um item inexistente, uma categoria inválida e uma busca válida sem correspondências também precisam de respostas diferentes.
Este ambiente de trabalho independente começa com o handler de lista e detalhes do laboratório guiado anterior. Você precisa dos conceitos de HTTP, JSON e httptest daquele laboratório ou de experiência equivalente. Você combinará filtros, introduzirá erros explícitos e usará a página real. Reserve cerca de 55 minutos, incluindo leitura e erros comuns; esta é uma estimativa do autor.
Combine os filtros de nome e categoria
Nesta etapa, faça as duas condições de busca valerem para o mesmo registro. Comece em Workspace. Restaure o diretório de trabalho e adicione a instalação Go preparada ao caminho de busca deste terminal. export afeta somente o shell atual; $PATH mantém suas entradas existentes.
cd /home/labex/project/lost-found
export PATH=/usr/local/go/bin:$PATH
Primeiro, use a lista funcional e leia seus dados:
curl -i 'http://127.0.0.1:8080/api/items'
cat items.json
Espere todos os seis registros. Mude para Lost and Found, digite blue, escolha bags e selecione Search collection. O handler atual ignora essas entradas e ainda mostra todos os seis itens. Volte para Workspace para acrescentar a decisão que falta.
A string de consulta de uma URL vem depois de ?. category=bags&q=blue fornece dois parâmetros nomeados separados por &. Uma busca na lista da API aceitará apenas category e q, cada um no máximo uma vez. Esta etapa implementa primeiro as buscas válidas; a próxima trata entradas inválidas.
O contrato é preciso: uma categoria ausente ou vazia significa todas as categorias. Caso contrário, sua grafia e capitalização devem corresponder a uma categoria presente em items.json. A palavra-chave q corresponde a uma substring do nome, sem distinguir maiúsculas de minúsculas; os espaços em branco ao redor são ignorados. Uma palavra-chave ausente, vazia ou composta somente por espaços em branco corresponde a todos os nomes. Quando ambos os filtros forem fornecidos, ambos devem corresponder (AND). Mantenha todos os campos de cada ID correspondente, sem duplicatas; qualquer ordem de resultados é permitida. Uma busca válida sem correspondências retorna HTTP 200 e [].
r.URL.Query().Get lê um parâmetro decodificado. strings.TrimSpace remove espaços em branco ao redor, strings.ToLower aplica a conversão Unicode para minúsculas e strings.Contains verifica uma substring. Esta é uma comparação simples em minúsculas, não remoção de acentos nem um mecanismo de busca sensível ao idioma. make([]Item, 0) cria um slice vazio, diferente de nil, para que JSON codifique a ausência de correspondências como []. && une as duas condições necessárias; usar || aqui incluiria incorretamente registros que satisfazem apenas uma delas.
Antes de editar, preveja três resultados de items.json: apenas a palavra-chave blue, apenas a categoria bags e ambas juntas. Somente LF-106 pertence à interseção. Os dois guarda-chuvas mostram por que retornar as correspondências de qualquer uma das condições estaria errado.
Preserve o código herdado de detalhes. Use o fluxo nano do primeiro laboratório: Ctrl+W localiza texto, Ctrl+O e depois Enter salva o nome existente, e Ctrl+X sai. Um backup deve ter uma extensão diferente de .go para que Go não compile um segundo handler.
cp handler.go handler.go.backup
LC_ALL=C.UTF-8 nano handler.go
Encontre o ramo da coleção /api/items e substitua apenas sua lógica de seleção da coleção. Preserve o cabeçalho de resposta, o laço de detalhes e a resposta de ID inexistente. Implemente estas decisões usando o argumento items:
- Leia os parâmetros decodificados
categoryeqcomr.URL.Query().Get. Preserve a categoria exatamente como fornecida. Apliquestrings.TrimSpacee depoisstrings.ToLowerà palavra-chave antes do laço. - Inicie um slice de resultados com
make([]Item, 0). Não reutilize o slice completo original como resultado, pois isso manteria registros não correspondentes. - Percorra os registros com
for _, item := range items. Decida se a categoria está ausente/vazia ou é igual aitem.Categorye, separadamente, se o nome em minúsculas contém a palavra-chave normalizada. Combine essas duas decisões com&&. Acrescente oitemcorrespondente inteiro usandoappend, não apenas seu nome nem um ID de outro registro. - Codifique esse slice de resultados no ramo da coleção e retorne. Preserve a seleção de detalhes por ID; não aplique filtros da coleção a uma requisição de detalhes.
Para comparar, uma condição de categoria pode ser expressa como category == "" || item.Category == category: o || aqui representa ausência de restrição ou a categoria solicitada. Ele não substitui o AND entre as condições de categoria e nome. Escolha nomes de variáveis locais que tornem essas decisões separadas legíveis.
Salve, saia e examine o arquivo que realmente salvou:
cat handler.go
Formate e recompile. -w permite que gofmt atualize o arquivo. serve.py compila seu código e substitui somente o servidor registrado do ambiente de trabalho após uma compilação bem-sucedida. Erros de compilação deixam a versão antiga em execução; corrija a linha indicada e execute ambos os comandos novamente.
gofmt -w handler.go
python3 serve.py
Agora envie uma busca combinada. As aspas mantêm & dentro da URL em vez de tratá-lo como um operador do shell:
curl -i 'http://127.0.0.1:8080/api/items?category=bags&q=blue'
Espere somente LF-106, Blue sports bag, em Sports hall. Retornar os guarda-chuvas também significaria que OR foi usado. Uma palavra-chave válida, mas sem correspondências, deve produzir um array vazio:
curl -i 'http://127.0.0.1:8080/api/items?q=not-in-the-collection'
Para texto com espaços, + ou caracteres fora de ASCII, deixe curl codificá-lo. -G envia os dados fornecidos em uma string de consulta GET, e --data-urlencode aplica codificação percentual a esse parâmetro. Sem codificação, um sinal de mais literal nos dados da consulta pode ser decodificado como espaço.
curl -G -i --data-urlencode 'q= CAFÉ + ' 'http://127.0.0.1:8080/api/items'
Espere LF-104, Café + Map, em Welcome booth. A palavra-chave em maiúsculas e os espaços ao redor são normalizados, enquanto o sinal de mais literal continua pertencendo ao nome. Se um caminho foi digitado incorretamente, restaure cd /home/labex/project/lost-found; se o serviço estiver indisponível, leia .work/server.log com cat e recompile após corrigir o erro.
Diferencie requisições inválidas de buscas vazias
Nesta etapa, torne os erros observáveis pelo status e pelo JSON e depois use a página concluída. Comece em Workspace, no mesmo diretório.
Use 400 para uma consulta de coleção contendo categoria desconhecida, parâmetro desconhecido ou parâmetro repetido. A grafia da categoria é exata; Bags é diferente de bags. category e q vazios continuam válidos. Use 404 para um ID de item desconhecido ou um caminho de API desconhecido. Use 405 para métodos diferentes de GET em /api/items e /api/items/<id>, com Allow: GET. Os corpos de erro são objetos contendo uma string error não vazia; a redação é flexível. Uma busca válida sem correspondências continua sendo 200 com []. Requisições de detalhes selecionam por ID; os filtros de consulta da coleção não se aplicam aos detalhes. A página inicial preparada / continua disponível.
url.ParseQuery(r.URL.RawQuery) analisa a consulta e retorna os valores e um erro, permitindo rejeitar codificação de consulta malformada. Cada chave corresponde a um slice de valores: seu comprimento revela parâmetros repetidos. Este exemplo verifica a categoria nos registros de origem atuais em vez de uma lista fixa de categorias, permitindo que outro balcão reutilize o handler.
Defina os cabeçalhos antes de escrever o corpo. Retornos antecipados impedem que um erro continue até um resultado normal bem-sucedido. Antes de editar, compare a busca válida, mas vazia, da etapa anterior às requisições rejeitadas abaixo. Preveja por que seus status diferem. Um ID de item inexistente é uma falha diferente de uma consulta de coleção malformada. Preserve as decisões de busca da etapa anterior; esta edição acrescenta verificações ao redor delas.
LC_ALL=C.UTF-8 nano handler.go
Use Ctrl+W para encontrar a função de handler retornada. Adicione net/url às importações para usar url.ParseQuery. Implemente estas verificações sem substituir o arquivo inteiro:
Primeiro, verifique o caminho. O caminho da coleção é exatamente /api/items; um detalhe começa com /api/items/. Qualquer outro caminho de API deve retornar 404, não continuar até a página ou lista normal. Depois verifique o método nessas rotas reconhecidas: se não for GET, defina Allow como GET e retorne um erro JSON 405.
Em seguida, reutilize uma função local de escrita de erro, se ajudar. Ela pode receber um status inteiro e uma mensagem string, chamar w.WriteHeader(status) e codificar map[string]string{"error": message}. O cabeçalho de tipo de conteúdo continua sendo definido primeiro. Escolha mensagens não vazias; a redação não é avaliada.
Depois, mantenha a seleção de detalhes antes da análise da consulta da coleção para que os filtros não alterem uma busca por ID. Um detalhe inexistente continua retornando 404 com erro JSON.
Depois disso, no ramo da coleção, analise r.URL.RawQuery com url.ParseQuery. Verifique o erro retornado antes de usar os valores; codificação malformada deve retornar 400. Um escape percentual exige dois dígitos hexadecimais, portanto /api/items?q=%ZZ é uma entrada malformada concreta para experimentar em um teste. Percorra cada chave e seu slice de valores. Rejeite chaves diferentes de category ou q e rejeite um slice cujo comprimento não seja um. Retorne imediatamente após cada rejeição.
Em seguida, leia os parâmetros aceitos dos valores analisados. Para uma categoria não vazia, percorra os registros fornecidos para determinar se essa categoria exata existe; rejeite-a com 400 se não existir. Não copie uma lista fixa de categorias de items.json para o programa. Outro balcão pode ter categorias inteiramente novas.
Por fim, depois dessas verificações passarem, execute sua busca AND existente e codifique o array de resultados. Categoria e palavra-chave vazias continuam válidas. Preserve o resultado sem correspondências [], em vez de transformá-lo em erro.
Salve com Ctrl+O e Enter, depois saia com Ctrl+X. Se o editor informar um nome de arquivo que você não pretendia, cancele a confirmação com Ctrl+C e confira o caminho. Preserve o código de busca e detalhes já funcional ao corrigir erros de compilação.
gofmt -w handler.go
python3 serve.py
As seguintes requisições são rejeitadas deliberadamente. Espere 400, 400, 404 e 405, respectivamente. -X POST escolhe o método POST; este serviço é somente de leitura e não deve criar nada.
curl -i 'http://127.0.0.1:8080/api/items?category=unknown'
curl -i 'http://127.0.0.1:8080/api/items?q=blue&q=bag'
curl -i 'http://127.0.0.1:8080/api/not-a-route'
curl -X POST -i 'http://127.0.0.1:8080/api/items'
Cada resposta tem um campo JSON error; a resposta ao POST também inclui Allow: GET. O recebimento dessas respostas por curl é diferente de uma falha de conexão, na qual não chega status HTTP nem resposta JSON. Não transforme erros HTTP esperados em motivo para reinicializar o ambiente de trabalho.
Use uma tabela de casos de teste para manter expectativas relacionadas juntas. Isso repete a técnica de recorder/requisição do laboratório anterior. url.Values.Encode() codifica a consulta Unicode/com sinal de mais em Go. Decodifique uma resposta de lista em []Item e verifique o erro retornado por json.Unmarshal antes de examinar seu conteúdo. Para um resultado vazio, um slice diferente de nil com comprimento zero significa []; um slice nil significaria null. A busca real já executada também verifica o conteúdo efetivo dos itens.
Crie seu próprio teste orientado por tabela usando registros independentes em vez da coleção real. Escolha uma pequena fixture com o mesmo nome em duas categorias e um terceiro item não correspondente. Primeiro preveja o conjunto exato de IDs de uma busca combinada; isso detecta uma implementação OR que uma verificação apenas de status deixaria passar.
LC_ALL=C.UTF-8 nano handler_test.go
Como no primeiro laboratório, comece com package main, importe os pacotes usados e coloque o teste em uma função Test que recebe *testing.T. Use net/http/httptest para requisições/recorders, encoding/json para decodificação, testing para verificações e net/url se codificar uma consulta. Crie um handler a partir da fixture uma vez dentro do teste.
Represente requisições relacionadas com um slice de estruturas anônimas. Por exemplo, os campos name string, path string e status int dão a cada caso um rótulo, um caminho de requisição e um status esperado. Forneça suas próprias linhas: inclua busca combinada bem-sucedida, busca vazia, categoria inválida, chave repetida, codificação malformada e ID inexistente. Um laço for _, tc := range cases aplica a requisição de cada caso a um recorder novo. Use tc.path em httptest.NewRequest("GET", tc.path, nil); compare recorder.Code a tc.status e inclua o nome do caso na saída de falha. Reutilizar um recorder misturaria corpos de resposta, então crie um novo para cada caso.
O t.Run(tc.name, func(t *testing.T) { ... }) de Go opcionalmente dá a cada linha um subteste nomeado. Substitua os pontos pelas instruções de recorder, requisição e verificações. Um t.Fatalf com falha deve informar tanto o status esperado quanto o observado.
Apenas o status não prova a busca. Para o caso combinado bem-sucedido, decodifique em []Item, verifique o erro de decodificação e compare os IDs e campos retornados aos registros correspondentes da fixture. Para o caso vazio, declare var got []Item, verifique o erro de decodificação e depois exija got != nil e len(got) == 0. Isso distingue um array vazio de null sem converter tipos. Para os casos rejeitados, decodifique em map[string]string e exija um error não vazio. Adicione uma requisição POST separada para uma rota reconhecida e verifique tanto 405 quanto recorder.Header().Get("Allow") == "GET". São usos da mesma técnica de recorder com resultados esperados diferentes.
Você decide a fixture, os nomes dos casos e as verificações exatas. Faça pelo menos uma verificação que falharia se o resultado contivesse um item extra de mesmo nome. Salve com Ctrl+O e Enter, depois Ctrl+X.
gofmt -w handler_test.go
go test -v .
Espere PASS. Se um teste falhar, compare o caminho e o status exibidos com o contrato acima. Corrija o handler, execute os testes novamente e recompile com python3 serve.py para que o navegador use seu código atual. Mantenha os arquivos de apoio main.go, go.mod, items.json, serve.py e web/ inalterados. Você pode editar handler.go e adicionar ou editar seu próprio código Go e testes sem substituir a coleção de origem ou a página.
Mude para Lost and Found e recarregue. Busque blue na categoria bags; espere um cartão com a referência LF-106. Abra View item, leia Sports hall e feche com Back to collection. Busque not-in-the-collection; espere 0 items found e No matching items. Limpe a palavra-chave, escolha All categories e busque novamente; todos os seis registros devem voltar. Use Tab para alcançar campos e botões; Enter envia o formulário e Escape fecha um diálogo de detalhes aberto.

Observe a categoria selecionada, a contagem de um resultado e a referência correspondente. A página usa sua API em cada busca e detalhe. Sua mensagem de indisponibilidade representa um problema real na requisição; ela nunca substitui os dados por uma coleção embutida.
Resumo
Você combinou filtros de nome normalizado e categoria exata, preservou a identidade dos itens, retornou [] para buscas válidas sem correspondências e separou os erros 400, 404 e 405. Seu teste orientado por tabela e a página real exercitam o handler. O desafio transfere esse mesmo contrato para os dados de outro balcão.



