Introducción
La página de objetos perdidos necesita ahora búsquedas útiles. Una persona que busca una bolsa azul no debería recibir todos los objetos azules ni todas las bolsas. Un objeto inexistente, una categoría no válida y una búsqueda válida sin coincidencias también necesitan respuestas distintas.
Este espacio independiente comienza con el manejador de listas y detalles del laboratorio guiado anterior. Necesitas sus conceptos de HTTP, JSON y httptest, o experiencia equivalente. Combinarás filtros, introducirás errores explícitos y utilizarás la página real. Calcula unos 55 minutos, incluida la lectura y los errores habituales; es una estimación del autor.
Combinar filtros de nombre y categoría
En este paso, haz que las dos condiciones de búsqueda se apliquen al mismo registro. Comienza en Workspace. Recupera el directorio de trabajo y añade la instalación de Go preparada a la ruta de búsqueda de este terminal. export solo afecta al shell actual; $PATH conserva sus entradas existentes.
cd /home/labex/project/lost-found
export PATH=/usr/local/go/bin:$PATH
Primero utiliza la lista que funciona y lee sus datos:
curl -i 'http://127.0.0.1:8080/api/items'
cat items.json
Espera los seis registros. Cambia a Lost and Found, introduce blue, elige bags y selecciona Search collection. El manejador actual ignora esas entradas y sigue mostrando los seis objetos. Vuelve a Workspace para incorporar la decisión que falta.
La cadena de consulta de una URL aparece después de ?. category=bags&q=blue proporciona dos parámetros con nombre separados por &. La búsqueda de lista de la API solo aceptará category y q, cada uno como máximo una vez. Este paso implementa primero las búsquedas válidas; el siguiente trata las entradas no válidas.
El contrato es preciso: una categoría ausente o vacía significa todas las categorías. En otro caso, su escritura y uso de mayúsculas deben coincidir con una categoría presente en items.json. La palabra clave q busca una subcadena del nombre sin distinguir mayúsculas y minúsculas; se ignoran los espacios en blanco de sus extremos. Una palabra clave ausente, vacía o compuesta solo por espacios en blanco coincide con todos los nombres. Si se proporcionan los dos filtros, ambos deben cumplirse (AND). Conserva todos los campos de cada ID coincidente, sin duplicados; se permite cualquier orden de resultados. Una búsqueda válida sin coincidencias devuelve HTTP 200 y [].
r.URL.Query().Get lee un parámetro decodificado. strings.TrimSpace elimina los espacios en blanco de los extremos, strings.ToLower aplica la conversión Unicode a minúsculas y strings.Contains comprueba una subcadena. Es una comparación simple en minúsculas; no elimina acentos ni implementa un buscador con reglas propias de cada idioma. make([]Item, 0) crea un slice vacío distinto de nil, de modo que JSON codifique la ausencia de coincidencias como []. && une las dos condiciones obligatorias; usar aquí || incluiría incorrectamente registros que solo satisfacen una.
Antes de editar, predice tres resultados a partir de items.json: solo la palabra clave blue, solo la categoría bags y ambas juntas. Solo LF-106 pertenece a su intersección. Los dos paraguas muestran por qué devolver las coincidencias de cualquiera de las dos condiciones sería incorrecto.
Conserva el código de detalles heredado. Usa el procedimiento de nano del primer laboratorio: Ctrl+W localiza texto, Ctrl+O y Enter guardan el nombre existente y Ctrl+X sale. Una copia de respaldo debe tener una extensión distinta de .go para que Go no compile un segundo manejador.
cp handler.go handler.go.backup
LC_ALL=C.UTF-8 nano handler.go
Busca la rama de colección /api/items y reemplaza solo su lógica de selección. Deja intactos la cabecera de respuesta, el bucle de detalles y la respuesta de ID inexistente. Implementa estas decisiones utilizando el argumento items:
- Lee los parámetros decodificados
categoryyqconr.URL.Query().Get. Conserva la categoría exactamente como se proporcionó. Aplicastrings.TrimSpacey despuésstrings.ToLowera la palabra clave antes del bucle. - Inicia un slice de resultados con
make([]Item, 0). No reutilices el slice completo original como resultado, porque conservaría registros no coincidentes. - Recorre los registros con
for _, item := range items. Decide si la categoría está ausente/vacía o es igual aitem.Categoryy, por separado, si el nombre en minúsculas contiene la palabra clave normalizada. Une esas dos decisiones con&&. Añade elitemcoincidente completo conappend, no solo su nombre ni un ID tomado de otro registro. - Codifica ese slice de resultados en la rama de colección y retorna. Mantén la selección de detalles por ID; no apliques filtros de colección a una solicitud de detalle.
Como comparación, una condición de categoría puede expresarse como category == "" || item.Category == category: el || representa aquí que no hay restricción o que se trata de la categoría solicitada. No sustituye el AND entre las condiciones de categoría y nombre. Elige nombres de variables locales que hagan legibles esas decisiones separadas.
Guarda, sal y revisa el archivo que realmente guardaste:
cat handler.go
Formatea y reconstruye. -w permite que gofmt actualice el archivo. serve.py compila tu código y sustituye únicamente el servidor registrado del espacio después de una compilación correcta. Los errores de compilación dejan funcionando la versión anterior; corrige la línea indicada y vuelve a ejecutar ambos comandos.
gofmt -w handler.go
python3 serve.py
Ahora envía una búsqueda combinada. Las comillas mantienen & dentro de la URL en lugar de tratarlo como un operador del shell:
curl -i 'http://127.0.0.1:8080/api/items?category=bags&q=blue'
Espera únicamente LF-106, Blue sports bag, en Sports hall. Si también aparecen los paraguas, se habría usado OR. Una palabra clave válida sin coincidencias debe producir un array vacío:
curl -i 'http://127.0.0.1:8080/api/items?q=not-in-the-collection'
Para texto con espacios, + o caracteres no ASCII, deja que curl lo codifique. -G envía los datos proporcionados en una cadena de consulta GET, y --data-urlencode aplica codificación porcentual a ese parámetro. Sin codificar, un signo más literal de los datos de consulta puede decodificarse como un espacio.
curl -G -i --data-urlencode 'q= CAFÉ + ' 'http://127.0.0.1:8080/api/items'
Espera LF-104, Café + Map, en Welcome booth. Se normalizan las mayúsculas de la palabra clave y los espacios de sus extremos, mientras que el signo más literal sigue formando parte del nombre. Si escribiste mal una ruta, recupera cd /home/labex/project/lost-found; si el servicio no está disponible, lee .work/server.log con cat y reconstruye tras corregir el error.
Distinguir solicitudes no válidas de búsquedas vacías
En este paso, haz que los errores se observen tanto en el estado como en JSON y después utiliza la página terminada. Comienza en Workspace, en el mismo directorio.
Usa 400 para una consulta de colección que contenga una categoría desconocida, un parámetro desconocido o un parámetro repetido. La escritura de la categoría es exacta; Bags es diferente de bags. Los valores vacíos de category y q siguen siendo válidos. Usa 404 para un ID de objeto desconocido o una ruta de API desconocida. Usa 405 para métodos distintos de GET en /api/items y /api/items/<id>, con Allow: GET. Los cuerpos de error son objetos que contienen una cadena error no vacía; el texto puede variar. Una búsqueda válida sin coincidencias mantiene 200 con []. Las solicitudes de detalle seleccionan por ID; los filtros de colección no se aplican a los detalles. La página de inicio preparada / sigue disponible.
url.ParseQuery(r.URL.RawQuery) analiza la consulta y devuelve tanto los valores como un error, lo que permite rechazar una codificación de consulta incorrecta. Cada clave corresponde a un slice de valores: su longitud revela parámetros repetidos. Este ejemplo comprueba la categoría contra los registros de origen actuales, en lugar de usar una lista fija de categorías, para que otro puesto pueda reutilizar el manejador.
Establece las cabeceras antes de escribir el cuerpo. Los retornos anticipados evitan que un error continúe hasta un resultado normal correcto. Antes de editar, compara la búsqueda válida pero vacía del paso anterior con las solicitudes rechazadas de abajo. Predice por qué difieren sus estados. Un ID de objeto inexistente es un fallo distinto de una consulta de colección mal formada. Conserva las decisiones de búsqueda del paso anterior; esta edición añade validaciones a su alrededor.
LC_ALL=C.UTF-8 nano handler.go
Usa Ctrl+W para encontrar la función del manejador devuelta. Añade net/url a las importaciones para usar url.ParseQuery. Implementa estas validaciones sin sustituir todo el archivo:
Primero, comprueba la ruta. La ruta de colección es exactamente /api/items; un detalle comienza por /api/items/. Cualquier otra ruta de API debe devolver 404, sin continuar hasta la página normal ni la lista. Después comprueba el método en esas rutas reconocidas: si no es GET, establece Allow en GET y devuelve un error JSON 405.
Después, reutiliza una función local para escribir errores si te resulta útil. Puede recibir un entero de estado y una cadena de mensaje, llamar a w.WriteHeader(status) y codificar map[string]string{"error": message}. La cabecera de tipo de contenido sigue estableciéndose primero. Elige mensajes no vacíos; su redacción no se evalúa.
A continuación, deja la selección de detalles antes del análisis de la consulta de colección para que los filtros no alteren una búsqueda por ID. Un detalle inexistente sigue devolviendo 404 con un error JSON.
Después, en la rama de colección, analiza r.URL.RawQuery con url.ParseQuery. Comprueba el error devuelto antes de usar los valores; una codificación mal formada debe devolver 400. Una secuencia de escape porcentual requiere dos dígitos hexadecimales, así que /api/items?q=%ZZ es una entrada mal formada concreta que puedes probar en una prueba. Recorre cada clave y su slice de valores. Rechaza las claves distintas de category o q y los slices cuya longitud no sea uno. Retorna inmediatamente después de cada rechazo.
A continuación, lee los parámetros aceptados de los valores analizados. Para una categoría no vacía, recorre los registros suministrados para determinar si existe esa categoría exacta; recházala con 400 si no existe. No copies al programa una lista fija de categorías de items.json. Otro puesto puede tener categorías completamente nuevas.
Por último, cuando estas validaciones se superen, ejecuta tu búsqueda AND existente y codifica el array de resultados. Los valores vacíos de categoría y palabra clave siguen siendo válidos. Mantén el resultado sin coincidencias [], sin convertirlo en un error.
Guarda con Ctrl+O y Enter y sal con Ctrl+X. Si el editor indica un nombre de archivo que no pretendías usar, cancela la solicitud con Ctrl+C y comprueba la ruta. Conserva el código de búsqueda y detalles que ya funciona mientras corriges errores de compilación.
gofmt -w handler.go
python3 serve.py
Las siguientes solicitudes se rechazan deliberadamente. Espera 400, 400, 404 y 405, respectivamente. -X POST elige el método POST; este servicio es de solo lectura y no debe crear 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 respuesta tiene un error JSON; la respuesta POST también incluye Allow: GET. Que curl reciba estas respuestas es distinto de un fallo de conexión, en el que no llega ningún estado HTTP ni respuesta JSON. No conviertas errores HTTP esperados en un motivo para reiniciar el espacio.
Utiliza una tabla de casos de prueba para agrupar expectativas relacionadas. Se repite la técnica de solicitud y grabador del laboratorio anterior. url.Values.Encode() codifica la consulta Unicode/con signo más en Go. Decodifica una respuesta de lista en []Item y comprueba el error devuelto por json.Unmarshal antes de examinar su contenido. Para un resultado vacío, un slice distinto de nil con longitud cero significa []; uno nil significaría null. La búsqueda real que ya ejecutaste también comprueba el contenido de los objetos.
Crea tu propia prueba basada en una tabla, usando registros independientes en lugar de la colección real. Elige unos datos de prueba pequeños con el mismo nombre en dos categorías y un tercer objeto no coincidente. Predice primero el conjunto exacto de ID de una búsqueda combinada; así detectas una implementación OR que una aserción solo del estado no detectaría.
LC_ALL=C.UTF-8 nano handler_test.go
Como en el primer laboratorio, comienza con package main, importa los paquetes que uses y coloca la prueba en una función Test que reciba *testing.T. Usa net/http/httptest para solicitudes y grabadores, encoding/json para decodificar, testing para las aserciones y net/url si codificas una consulta. Construye una vez un manejador a partir de tus datos de prueba dentro de la prueba.
Representa solicitudes relacionadas mediante un slice de estructuras anónimas. Por ejemplo, los campos name string, path string y status int dan a cada caso una etiqueta, una ruta de solicitud y un estado esperado. Proporciona tus propias filas: incluye una búsqueda combinada correcta, una búsqueda vacía, una categoría no válida, una clave repetida, una codificación mal formada y un ID inexistente. Un bucle for _, tc := range cases aplica la solicitud de cada caso a un grabador nuevo. Usa tc.path en httptest.NewRequest("GET", tc.path, nil); compara recorder.Code con tc.status e incluye el nombre del caso en los mensajes de fallo. Reutilizar un grabador mezclaría los cuerpos de respuesta, así que crea uno nuevo para cada caso.
La función t.Run(tc.name, func(t *testing.T) { ... }) de Go permite dar a cada fila una subprueba con nombre. Sustituye los puntos por tus instrucciones de grabador, solicitud y aserciones. Un t.Fatalf que falle debe mostrar tanto el estado esperado como el observado.
El estado por sí solo no demuestra la búsqueda. Para el caso combinado correcto, decodifica en []Item, comprueba el error de decodificación y compara los ID y campos devueltos con los registros coincidentes de tus datos de prueba. Para el caso vacío, declara var got []Item, comprueba el error de decodificación y exige después got != nil y len(got) == 0. Así distingues un array vacío de null sin convertir entre tipos. Para los casos rechazados, decodifica en map[string]string y exige un error no vacío. Añade una solicitud POST separada para una ruta reconocida y comprueba tanto 405 como recorder.Header().Get("Allow") == "GET". Se utiliza la misma técnica de grabador con distintos resultados esperados.
Tú decides los datos de prueba, los nombres de los casos y las aserciones exactas. Incluye al menos una aserción que falle si el resultado contiene un objeto adicional con el mismo nombre. Guarda con Ctrl+O y Enter y después Ctrl+X.
gofmt -w handler_test.go
go test -v .
Espera PASS. Si una prueba falla, compara la ruta y el estado mostrados con el contrato anterior. Corrige el manejador, vuelve a ejecutar las pruebas y reconstruye con python3 serve.py para que el navegador use tu código actual. Conserva sin cambios los archivos auxiliares main.go, go.mod, items.json, serve.py y web/. Puedes editar handler.go y añadir o editar tus propias fuentes y pruebas de Go sin sustituir la colección de origen ni la página.
Cambia a Lost and Found y recarga. Busca blue en la categoría bags; espera una tarjeta con la referencia LF-106. Abre View item y lee Sports hall; después cierra con Back to collection. Busca not-in-the-collection; espera 0 items found y No matching items. Borra la palabra clave, elige All categories y vuelve a buscar; deben regresar los seis registros. Usa Tab para acceder a los campos y botones; Enter envía el formulario y Escape cierra un diálogo de detalle abierto.

Observa la categoría seleccionada, el recuento de un resultado y la referencia correspondiente. La página utiliza tu API en cada búsqueda y detalle. Su mensaje de indisponibilidad representa un problema real de la solicitud; nunca sustituye la respuesta por una colección incorporada.
Resumen
Combinaste filtros por nombre normalizado y categoría exacta, conservaste la identidad de los objetos, devolviste [] para búsquedas válidas sin resultados y distinguiste los errores 400, 404 y 405. La prueba basada en una tabla y la página real ejercitan tu manejador. El desafío transfiere este mismo contrato a los datos de otro puesto.



