Introduction
La page des objets trouvés a maintenant besoin de recherches pertinentes. Une personne cherchant un sac bleu ne doit recevoir ni tous les objets bleus, ni tous les sacs. Un objet absent, une catégorie invalide et une recherche valide sans résultat nécessitent également des réponses différentes.
Cet espace indépendant commence avec le gestionnaire de liste et de détail du laboratoire guidé précédent. Vous devez en connaître les notions HTTP, JSON et httptest, ou posséder une expérience équivalente. Vous combinerez les filtres, ajouterez des erreurs explicites et utiliserez la page réelle. Prévoyez environ 55 minutes, lecture et erreurs courantes comprises ; il s’agit d’une estimation de l’auteur.
Combiner les filtres de nom et de catégorie
Dans cette étape, appliquez les deux conditions de recherche à la même fiche. Commencez dans Workspace. Rétablissez le répertoire de travail et ajoutez l’installation Go préparée au chemin de recherche de ce terminal. export ne concerne que le shell actuel ; $PATH conserve ses entrées existantes.
cd /home/labex/project/lost-found
export PATH=/usr/local/go/bin:$PATH
Utilisez d’abord la liste fonctionnelle et lisez ses données :
curl -i 'http://127.0.0.1:8080/api/items'
cat items.json
Vous devez obtenir les six fiches. Passez à Lost and Found, saisissez blue, choisissez bags, puis Search collection. Le gestionnaire actuel ignore ces entrées et affiche toujours les six objets. Revenez à Workspace pour ajouter la décision manquante.
La chaîne de requête d’une URL suit ?. category=bags&q=blue fournit deux paramètres nommés séparés par &. La recherche dans la liste de l’API n’accepte que category et q, chacun au plus une fois. Cette étape implémente d’abord les recherches valides ; la suivante traite les entrées invalides.
Le contrat est précis : une catégorie absente ou vide signifie toutes les catégories. Sinon, son orthographe et sa casse doivent correspondre à une catégorie présente dans items.json. Le mot-clé q recherche une sous-chaîne du nom sans distinguer les majuscules et minuscules ; les espaces blancs à ses extrémités sont ignorés. Un mot-clé absent, vide ou constitué uniquement d’espaces blancs correspond à tous les noms. Si les deux filtres sont fournis, les deux doivent correspondre (AND). Conservez tous les champs de chaque ID correspondant, sans doublon ; l’ordre des résultats est libre. Une recherche valide sans correspondance renvoie HTTP 200 et [].
r.URL.Query().Get lit un paramètre décodé. strings.TrimSpace retire les espaces blancs aux extrémités, strings.ToLower applique la conversion Unicode en minuscules et strings.Contains teste une sous-chaîne. Il s’agit d’une simple comparaison en minuscules, sans suppression des accents ni moteur de recherche adapté aux règles d’une langue. make([]Item, 0) crée un slice vide non nil, afin que JSON encode l’absence de correspondances par []. && combine les deux conditions obligatoires ; utiliser || ici inclurait à tort les fiches n’en satisfaisant qu’une seule.
Avant de modifier le code, prévoyez trois résultats à partir de items.json : le mot-clé blue seul, la catégorie bags seule et les deux ensemble. Seul LF-106 appartient à leur intersection. Les deux parapluies montrent pourquoi renvoyer les correspondances de l’une ou l’autre condition serait incorrect.
Conservez le code de détail hérité. Utilisez la procédure nano du premier lab : Ctrl+W trouve le texte, Ctrl+O puis Entrée enregistre sous le nom existant, et Ctrl+X quitte. Une sauvegarde doit avoir une extension autre que .go pour que Go ne compile pas un second gestionnaire.
cp handler.go handler.go.backup
LC_ALL=C.UTF-8 nano handler.go
Trouvez la branche de collection /api/items et remplacez uniquement sa logique de sélection de collection. Conservez son en-tête de réponse, la boucle de détail et la réponse d’ID absent. Implémentez ces décisions en utilisant l’argument items :
- Lisez les paramètres décodés
categoryetqavecr.URL.Query().Get. Gardez la catégorie exactement telle qu’elle est fournie. Appliquezstrings.TrimSpace, puisstrings.ToLowerau mot-clé avant la boucle. - Commencez un slice de résultats avec
make([]Item, 0). Ne réutilisez pas le slice complet d’origine comme résultat, car cela conserverait les fiches non correspondantes. - Parcourez les fiches avec
for _, item := range items. Déterminez si la catégorie est absente/vide ou égale àitem.Category, et séparément si le nom en minuscules contient le mot-clé normalisé. Combinez ces deux décisions avec&&. Ajoutez l’itemcorrespondant entier avecappend, pas seulement son nom ni un ID pris dans une autre fiche. - Encodez ce slice de résultats dans la branche de collection et revenez. Gardez les détails sélectionnés par ID ; n’appliquez pas les filtres de collection à une requête de détail.
À titre de comparaison, une condition de catégorie peut s’écrire category == "" || item.Category == category : le || représente ici soit l’absence de restriction, soit la catégorie demandée. Il ne remplace pas le AND entre les conditions de catégorie et de nom. Choisissez des noms de variables locales qui rendent ces décisions distinctes lisibles.
Enregistrez, quittez et examinez le fichier effectivement enregistré :
cat handler.go
Formatez et recompilez. -w permet à gofmt de modifier le fichier. serve.py compile votre code et ne remplace le serveur enregistré de l’espace qu’après une compilation réussie. Une erreur de compilation laisse l’ancienne version active ; corrigez la ligne indiquée et relancez les deux commandes.
gofmt -w handler.go
python3 serve.py
Envoyez maintenant une recherche combinée. Les apostrophes maintiennent & dans l’URL au lieu de le traiter comme un opérateur du shell :
curl -i 'http://127.0.0.1:8080/api/items?category=bags&q=blue'
Vous devez obtenir uniquement LF-106, Blue sports bag, à Sports hall. Renvoyer aussi les parapluies signifierait qu’OR a été utilisé. Un mot-clé valide sans correspondance doit produire un tableau vide :
curl -i 'http://127.0.0.1:8080/api/items?q=not-in-the-collection'
Pour un texte contenant des espaces, + ou des caractères non ASCII, laissez curl l’encoder. -G envoie les données fournies dans une chaîne de requête GET et --data-urlencode applique un encodage en pourcentage au paramètre. Sans encodage, un signe plus littéral dans les données de requête peut être décodé en espace.
curl -G -i --data-urlencode 'q= CAFÉ + ' 'http://127.0.0.1:8080/api/items'
Vous devez obtenir LF-104, Café + Map, à Welcome booth. Le mot-clé en majuscules et les espaces à ses extrémités sont normalisés, tandis que le signe plus littéral reste dans le nom. Si vous avez mal saisi un chemin, rétablissez cd /home/labex/project/lost-found ; si le service est indisponible, lisez .work/server.log avec cat et recompilez après correction de l’erreur.
Distinguer les requêtes invalides des recherches sans résultat
Dans cette étape, rendez les erreurs observables à la fois par le statut et par JSON, puis utilisez la page terminée. Commencez dans Workspace, dans le même répertoire.
Utilisez 400 pour une requête de collection contenant une catégorie inconnue, un paramètre inconnu ou un paramètre répété. La catégorie doit être écrite exactement ; Bags est différent de bags. Des valeurs vides pour category et q restent valides. Utilisez 404 pour un ID d’objet ou un chemin d’API inconnu. Utilisez 405 pour les méthodes autres que GET sur /api/items et /api/items/<id>, avec Allow: GET. Les corps d’erreur sont des objets contenant une chaîne error non vide ; le message est libre. Une recherche valide sans correspondance conserve 200 avec []. Les requêtes de détail sélectionnent par ID ; les filtres de collection ne s’appliquent pas aux détails. La page d’accueil préparée / reste accessible.
url.ParseQuery(r.URL.RawQuery) analyse la requête et renvoie les valeurs ainsi qu’une erreur : un encodage de requête mal formé peut ainsi être rejeté. Chaque clé correspond à un slice de valeurs ; sa longueur révèle les paramètres répétés. Cet exemple vérifie une catégorie par rapport aux fiches sources actuelles, et non à une liste fixe, afin qu’un autre bureau puisse réutiliser le gestionnaire.
Définissez les en-têtes avant d’écrire le corps. Les retours anticipés empêchent une erreur de se poursuivre vers un résultat normal réussi. Avant de modifier le code, comparez la recherche valide mais vide de l’étape précédente aux requêtes rejetées ci-dessous. Prévoyez pourquoi leurs statuts diffèrent. Un ID d’objet absent constitue un échec différent d’une requête de collection mal formée. Gardez les décisions de recherche de l’étape précédente ; cette modification ajoute des contrôles autour.
LC_ALL=C.UTF-8 nano handler.go
Utilisez Ctrl+W pour trouver la fonction de gestionnaire renvoyée. Ajoutez net/url aux imports pour url.ParseQuery. Implémentez ces contrôles sans remplacer le fichier entier :
D’abord, vérifiez le chemin en premier. Le chemin de collection est exactement /api/items ; un détail commence par /api/items/. Tout autre chemin d’API doit renvoyer 404, sans se poursuivre vers la page ou la liste normale. Vérifiez ensuite la méthode sur ces routes reconnues : si ce n’est pas GET, définissez Allow à GET et renvoyez une erreur JSON 405.
Ensuite, réutilisez si cela vous aide une fonction locale d’écriture d’erreur. Elle peut accepter un statut entier et un message chaîne, appeler w.WriteHeader(status) et encoder map[string]string{"error": message}. L’en-tête de type de contenu reste défini en premier. Choisissez des messages non vides ; leur formulation n’est pas évaluée.
Puis laissez la sélection de détail avant l’analyse de la requête de collection afin que les filtres ne modifient pas une recherche par ID. Un détail absent renvoie toujours 404 avec une erreur JSON.
Après cela, dans la branche de collection, analysez r.URL.RawQuery avec url.ParseQuery. Vérifiez l’erreur renvoyée avant d’utiliser les valeurs ; un encodage mal formé doit renvoyer 400. Une séquence d’échappement en pourcentage nécessite deux chiffres hexadécimaux ; /api/items?q=%ZZ est donc une entrée mal formée concrète à essayer dans un test. Parcourez chaque clé et son slice de valeurs. Refusez les clés autres que category ou q, ainsi qu’un slice dont la longueur n’est pas un. Revenez immédiatement après chaque rejet.
Ensuite, lisez les paramètres acceptés dans les valeurs analysées. Pour une catégorie non vide, parcourez les fiches fournies afin de déterminer si cette catégorie exacte existe ; sinon, refusez-la avec 400. Ne copiez pas dans le programme une liste fixe de catégories de items.json. Un autre bureau peut avoir des catégories entièrement nouvelles.
Enfin, une fois ces contrôles réussis, exécutez votre recherche AND existante et encodez le tableau de résultats. Les valeurs vides de catégorie et de mot-clé restent valides. Conservez [] pour l’absence de correspondance au lieu d’en faire une erreur.
Enregistrez avec Ctrl+O et Entrée, puis quittez avec Ctrl+X. Si l’éditeur affiche un nom de fichier inattendu, annulez l’invite avec Ctrl+C et vérifiez le chemin. Préservez le code de recherche et de détail déjà fonctionnel pendant la correction des erreurs de compilation.
gofmt -w handler.go
python3 serve.py
Les requêtes suivantes sont volontairement rejetées. Attendez respectivement 400, 400, 404 et 405. -X POST choisit la méthode POST ; ce service est en lecture seule et ne doit rien créer.
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'
Chaque réponse contient un champ JSON error ; la réponse POST inclut aussi Allow: GET. Recevoir ces réponses avec curl est différent d’un échec de connexion, où aucun statut HTTP ni corps JSON n’arrive. Ne réinitialisez pas l’espace pour des erreurs HTTP attendues.
Utilisez une table de cas de test pour rassembler les attentes liées. Cela reprend la technique d’enregistreur et de requête du lab précédent. url.Values.Encode() encode la requête Unicode/avec signe plus dans Go. Décodez une réponse de liste dans []Item et vérifiez l’erreur renvoyée par json.Unmarshal avant d’en examiner le contenu. Pour un résultat vide, un slice non nil de longueur zéro signifie [] ; un slice nil signifierait null. La recherche réelle déjà exécutée vérifie aussi le contenu effectif des objets.
Créez votre propre test piloté par une table, avec des fiches indépendantes de la collection réelle. Choisissez un petit jeu comprenant le même nom dans deux catégories et un troisième objet non correspondant. Prévoyez d’abord l’ensemble exact des ID d’une recherche combinée ; cela détecte une implémentation OR qu’une assertion portant uniquement sur le statut manquerait.
LC_ALL=C.UTF-8 nano handler_test.go
Comme dans le premier lab, commencez par package main, importez les paquets utilisés et placez le test dans une fonction Test acceptant *testing.T. Utilisez net/http/httptest pour les requêtes et enregistreurs, encoding/json pour le décodage, testing pour les assertions et net/url si vous encodez une requête. Construisez une seule fois le gestionnaire à partir de votre jeu dans le test.
Représentez les requêtes liées par un slice de structures anonymes. Par exemple, les champs name string, path string et status int donnent à chaque cas un libellé, un chemin et un statut attendu. Fournissez vos propres lignes : incluez une recherche combinée réussie, une recherche vide, une catégorie invalide, une clé répétée, un encodage mal formé et un ID absent. Une boucle for _, tc := range cases applique la requête de chaque cas à un nouvel enregistreur. Utilisez tc.path dans httptest.NewRequest("GET", tc.path, nil) ; comparez recorder.Code à tc.status et incluez le nom du cas dans le message d’échec. Réutiliser un enregistreur mélangerait les corps de réponse ; créez-en donc un nouveau pour chaque cas.
Le t.Run(tc.name, func(t *testing.T) { ... }) de Go peut donner à chaque ligne un sous-test nommé. Remplacez les points par vos instructions d’enregistreur, de requête et d’assertion. Un t.Fatalf en échec doit indiquer le statut attendu et celui observé.
Le statut seul ne peut pas prouver la recherche. Pour le cas combiné réussi, décodez dans []Item, vérifiez l’erreur de décodage et comparez les ID et les champs renvoyés aux fiches correspondantes du jeu. Pour le cas vide, déclarez var got []Item, vérifiez l’erreur de décodage, puis exigez got != nil et len(got) == 0. Cela distingue un tableau vide de null sans convertir les types. Pour les cas rejetés, décodez dans map[string]string et exigez un error non vide. Ajoutez une requête POST séparée pour une route reconnue et vérifiez à la fois 405 et recorder.Header().Get("Allow") == "GET". Ces cas utilisent la même technique d’enregistreur avec des résultats attendus différents.
Vous décidez du jeu, des noms de cas et des assertions exactes. Écrivez au moins une assertion qui échouerait si le résultat contenait un objet supplémentaire de même nom. Enregistrez avec Ctrl+O et Entrée, puis Ctrl+X.
gofmt -w handler_test.go
go test -v .
Vous devez obtenir PASS. Si un test échoue, comparez le chemin et le statut affichés au contrat ci-dessus. Corrigez le gestionnaire, relancez les tests et recompilez avec python3 serve.py pour que le navigateur utilise le code actuel. Conservez les fichiers de support main.go, go.mod, items.json, serve.py et web/ sans changement. Vous pouvez modifier handler.go et ajouter ou modifier vos propres sources et tests Go sans remplacer la collection source ni la page.
Passez à Lost and Found et rechargez. Recherchez blue dans la catégorie bags ; vous devez obtenir une carte, de référence LF-106. Ouvrez View item, lisez Sports hall, puis fermez avec Back to collection. Recherchez not-in-the-collection ; vous devez voir 0 items found et No matching items. Effacez le mot-clé, choisissez All categories et relancez la recherche ; les six fiches doivent revenir. Utilisez Tab pour atteindre les champs et boutons ; Entrée soumet le formulaire et Échap ferme un dialogue de détail ouvert.

Observez la catégorie sélectionnée, le compteur indiquant un seul résultat et la référence correspondante. La page utilise votre API pour chaque recherche et détail. Son message d’indisponibilité traduit un véritable problème de requête ; elle ne remplace jamais la réponse par une collection intégrée.
Résumé
Vous avez combiné un nom normalisé et une catégorie exacte, conservé l’identité des objets, renvoyé [] pour les recherches valides sans résultat et distingué les erreurs 400, 404 et 405. Votre test piloté par une table et la page réelle exercent le gestionnaire. Le défi transpose ce même contrat aux données d’un autre bureau.



