Files
plant-disease-id/scripts/smoke-test.mjs
2026-06-08 16:42:04 -04:00

219 lines
7.4 KiB
JavaScript

#!/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);
}