219 lines
7.4 KiB
JavaScript
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);
|
|
}
|