#!/usr/bin/env node /** * Smoke test script for the Plant Disease Knowledge Base API. * Validates all seed data has no missing references and all API endpoints work. * * Usage: * # With dev server running: * node scripts/smoke-test.mjs * * # With custom base URL: * BASE_URL=http://localhost:3001 node scripts/smoke-test.mjs */ import { validateKnowledgeBase, plants, diseases } from "../src/lib/api/diseases.ts"; const BASE_URL = process.env.BASE_URL || "http://localhost:3000"; const results = { passed: 0, failed: 0, errors: [] }; function pass(test) { results.passed++; console.log(` āœ… ${test}`); } function fail(test, message) { results.failed++; results.errors.push({ test, message }); console.log(` āŒ ${test}: ${message}`); } async function fetchJSON(path) { const res = await fetch(`${BASE_URL}${path}`); const data = await res.json(); return { status: res.status, data, headers: Object.fromEntries(res.headers) }; } console.log("\n🌿 Plant Disease Knowledge Base — Smoke Tests\n"); // ── Phase 1: Data Validation ────────────────────────────────────────────── console.log("Phase 1: Seed Data Validation"); const validationErrors = validateKnowledgeBase(); if (validationErrors.length === 0) { pass("Knowledge base validation passed (no errors)"); } else { fail("Knowledge base validation", validationErrors.join("; ")); } if (plants.length >= 20) { pass(`Plant count: ${plants.length} (≄20)`); } else { fail("Plant count", `Only ${plants.length} plants (need ≄20)`); } if (diseases.length >= 80) { pass(`Disease count: ${diseases.length} (≄80)`); } else { fail("Disease count", `Only ${diseases.length} diseases (need ≄80)`); } const uniquePlantIds = new Set(diseases.map((d) => d.plantId)); if (uniquePlantIds.size >= 20) { pass(`Diseases span ${uniquePlantIds.size} plants (≄20)`); } else { fail("Disease plant coverage", `Only ${uniquePlantIds.size} plants have diseases`); } const causalTypes = new Set(diseases.map((d) => d.causalAgentType)); if (causalTypes.size === 4) { pass(`All 4 causal agent types present: ${[...causalTypes].join(", ")}`); } else { fail("Causal agent types", `Only ${causalTypes.size}/4 types present`); } // ── Phase 2: API Endpoint Tests ─────────────────────────────────────────── console.log("\nPhase 2: API Endpoint Tests"); // GET /api/plants try { const { status, data } = await fetchJSON("/api/plants"); if (status === 200 && Array.isArray(data.plants) && data.plants.length >= 20) { pass(`GET /api/plants returns 200 with ${data.plants.length} plants`); } else { fail("GET /api/plants", `Status ${status}, plants: ${data.plants?.length ?? "N/A"}`); } } catch (e) { fail("GET /api/plants", e.message); } // GET /api/plants?search=tomato try { const { status, data } = await fetchJSON("/api/plants?search=tomato"); if (status === 200 && data.plants.length > 0) { pass(`GET /api/plants?search=tomato returns ${data.plants.length} results`); } else { fail("GET /api/plants?search=tomato", `Status ${status}`); } } catch (e) { fail("GET /api/plants?search=tomato", e.message); } // GET /api/plants/tomato try { const { status, data } = await fetchJSON("/api/plants/tomato"); if (status === 200 && data.plant?.id === "tomato" && data.diseases?.length >= 3) { pass(`GET /api/plants/tomato returns 200 with ${data.diseases.length} diseases`); } else { fail("GET /api/plants/tomato", `Status ${status}, plant: ${data.plant?.id ?? "N/A"}`); } } catch (e) { fail("GET /api/plants/tomato", e.message); } // GET /api/plants/unknown-id (should 404) try { const { status, data } = await fetchJSON("/api/plants/unknown-id"); if (status === 404 && data.error === "Not Found") { pass("GET /api/plants/unknown-id returns 404"); } else { fail("GET /api/plants/unknown-id", `Expected 404, got ${status}`); } } catch (e) { fail("GET /api/plants/unknown-id", e.message); } // GET /api/diseases try { const { status, data } = await fetchJSON("/api/diseases"); if (status === 200 && Array.isArray(data.diseases) && data.diseases.length >= 80) { pass(`GET /api/diseases returns 200 with ${data.diseases.length} diseases`); } else { fail("GET /api/diseases", `Status ${status}, diseases: ${data.diseases?.length ?? "N/A"}`); } } catch (e) { fail("GET /api/diseases", e.message); } // GET /api/diseases?plantId=tomato try { const { status, data } = await fetchJSON("/api/diseases?plantId=tomato"); if (status === 200 && data.diseases.length >= 3 && data.diseases.every((d) => d.plantId === "tomato")) { pass(`GET /api/diseases?plantId=tomato returns ${data.diseases.length} tomato diseases`); } else { fail("GET /api/diseases?plantId=tomato", `Status ${status}, count: ${data.diseases?.length ?? "N/A"}`); } } catch (e) { fail("GET /api/diseases?plantId=tomato", e.message); } // GET /api/diseases?search=blight try { const { status, data } = await fetchJSON("/api/diseases?search=blight"); if (status === 200 && data.diseases.length >= 2) { pass(`GET /api/diseases?search=blight returns ${data.diseases.length} results (≄2)`); } else { fail("GET /api/diseases?search=blight", `Status ${status}, count: ${data.diseases?.length ?? "N/A"}`); } } catch (e) { fail("GET /api/diseases?search=blight", e.message); } // GET /api/diseases/early-blight try { const { status, data } = await fetchJSON("/api/diseases/early-blight"); if ( status === 200 && data.disease?.id === "early-blight" && data.plant?.id === "tomato" && Array.isArray(data.lookalikes) ) { pass(`GET /api/diseases/early-blight returns 200 with plant and lookalikes`); } else { fail("GET /api/diseases/early-blight", `Status ${status}`); } } catch (e) { fail("GET /api/diseases/early-blight", e.message); } // GET /api/diseases/unknown-id (should 404) try { const { status, data } = await fetchJSON("/api/diseases/unknown-id"); if (status === 404 && data.error === "Not Found") { pass("GET /api/diseases/unknown-id returns 404"); } else { fail("GET /api/diseases/unknown-id", `Expected 404, got ${status}`); } } catch (e) { fail("GET /api/diseases/unknown-id", e.message); } // ── Phase 3: Response Headers ───────────────────────────────────────────── console.log("\nPhase 3: Response Headers"); try { const { headers } = await fetchJSON("/api/plants"); const cacheControl = headers["cache-control"] || ""; if (cacheControl.includes("max-age=3600")) { pass(`Cache-Control header present: ${cacheControl}`); } else { fail("Cache-Control header", `Expected max-age=3600, got: ${cacheControl}`); } } catch (e) { fail("Cache-Control header", e.message); } // ── Summary ─────────────────────────────────────────────────────────────── console.log("\n" + "─".repeat(50)); console.log(`Results: ${results.passed} passed, ${results.failed} failed`); if (results.failed > 0) { console.log("\nFailed tests:"); for (const { test, message } of results.errors) { console.log(` • ${test}: ${message}`); } process.exit(1); } else { console.log("\nšŸŽ‰ All smoke tests passed!\n"); process.exit(0); }