feat: add MCP server with resources, tools, auth, and Traefik routing

- Add backend/app/mcp_server.py with FastMCP server (hms-cms)
- Resources: design-rules, page-structure, sync-status
- Tools: trigger_sync, get_sync_log, set_rentman_token, read_file,
  write_file, create_page, deploy, git_commit, git_status
- Bearer token auth via MCP_AUTH_TOKEN env var
- Mount MCP at /mcp with streamable HTTP transport
- Integrate session manager in FastAPI lifespan
- Add Traefik labels for external /mcp route on hms.media-on.de
- Add git, docker.io, curl to backend Dockerfile
- Mount repo root + docker socket in backend container
- Add mcp>=1.0.0 to requirements.txt
This commit is contained in:
Agent Zero
2026-07-12 15:58:22 +02:00
parent 00c23bc066
commit cd465e0564
124 changed files with 3309 additions and 225 deletions
@@ -0,0 +1,101 @@
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
describe("Cart Store", () => {
const storePath = resolve(__dirname, "../../stores/cart.ts");
const content = readFileSync(storePath, "utf-8");
it("should define useCartStore with defineStore", () => {
expect(content).toContain("defineStore");
expect(content).toContain("useCartStore");
});
it("should have CartItem interface with required fields", () => {
expect(content).toContain("equipment_id");
expect(content).toContain("name");
expect(content).toContain("quantity");
expect(content).toContain("rental_price");
expect(content).toContain("image_url");
});
it("should have totalCount getter", () => {
expect(content).toContain("totalCount");
expect(content).toContain("reduce");
});
it("should have isEmpty getter", () => {
expect(content).toContain("isEmpty");
});
it("should have hasItems getter", () => {
expect(content).toContain("hasItems");
});
it("should have apiItems getter for rental request payload", () => {
expect(content).toContain("apiItems");
expect(content).toContain("equipment_id");
expect(content).toContain("quantity");
});
it("should have addItem action", () => {
expect(content).toContain("addItem");
expect(content).toContain("existing");
expect(content).toContain("quantity += 1");
});
it("should have addItemByFields action", () => {
expect(content).toContain("addItemByFields");
});
it("should have removeItem action", () => {
expect(content).toContain("removeItem");
expect(content).toContain("splice");
});
it("should have updateQuantity action", () => {
expect(content).toContain("updateQuantity");
expect(content).toContain("quantity <= 0");
});
it("should have incrementQuantity action", () => {
expect(content).toContain("incrementQuantity");
});
it("should have decrementQuantity action", () => {
expect(content).toContain("decrementQuantity");
});
it("should have clearCart action", () => {
expect(content).toContain("clearCart");
expect(content).toContain("items = []");
});
it("should have RentalRequestPayload interface", () => {
expect(content).toContain("RentalRequestPayload");
expect(content).toContain("event_name");
expect(content).toContain("date_start");
expect(content).toContain("date_end");
expect(content).toContain("location");
expect(content).toContain("person_count");
expect(content).toContain("contact_name");
expect(content).toContain("contact_email");
expect(content).toContain("contact_phone");
expect(content).toContain("contact_street");
expect(content).toContain("contact_postalcode");
expect(content).toContain("contact_city");
expect(content).toContain("message");
expect(content).toContain("items");
});
it("should have RentalRequestResponse interface", () => {
expect(content).toContain("RentalRequestResponse");
expect(content).toContain("reference_number");
expect(content).toContain("status");
});
it("should use TypeScript", () => {
expect(content).toContain("import type");
expect(content).toContain("export interface");
});
});
@@ -0,0 +1,106 @@
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
describe("Design Tokens (prototype-style.css)", () => {
const cssPath = resolve(__dirname, "../../assets/css/main.css");
const cssContent = readFileSync(cssPath, "utf-8");
it("should define --bg variable in :root", () => {
expect(cssContent).toContain("--bg: #131313");
});
it("should define --panel variable in :root", () => {
expect(cssContent).toContain("--panel: #1a1a1a");
});
it("should define --color-accent variable in :root", () => {
expect(cssContent).toContain("--color-accent: #EC6925");
});
it("should define --surface variable in :root", () => {
expect(cssContent).toContain("--surface: #212121");
});
it("should define --text variable in :root", () => {
expect(cssContent).toContain("--text: #ffffff");
});
it("should define --text-muted variable in :root", () => {
expect(cssContent).toContain("--text-muted: #d4d4d4");
});
it("should define radius tokens", () => {
expect(cssContent).toContain("--radius-sm: 2px");
expect(cssContent).toContain("--radius-md: 3px");
expect(cssContent).toContain("--radius-lg: 4px");
});
it("should define transition tokens", () => {
expect(cssContent).toContain("--transition-fast: 150ms ease");
});
it("should define hms-* CSS classes", () => {
expect(cssContent).toContain(".hms-header");
expect(cssContent).toContain(".hms-hero");
expect(cssContent).toContain(".hms-card");
expect(cssContent).toContain(".hms-btn");
expect(cssContent).toContain(".hms-btn-primary");
expect(cssContent).toContain(".hms-btn-secondary");
expect(cssContent).toContain(".hms-nav-btn");
expect(cssContent).toContain(".hms-badge");
expect(cssContent).toContain(".hms-input");
expect(cssContent).toContain(".hms-skeleton");
expect(cssContent).toContain(".hms-spinner");
expect(cssContent).toContain(".hms-speaker-grid");
expect(cssContent).toContain(".hms-footer");
expect(cssContent).toContain(".hms-gallery");
expect(cssContent).toContain(".hms-eq-card");
expect(cssContent).toContain(".hms-chip");
});
it("should include Tailwind directives", () => {
expect(cssContent).toContain("@tailwind base");
expect(cssContent).toContain("@tailwind components");
expect(cssContent).toContain("@tailwind utilities");
});
});
describe("Tailwind Config", () => {
const configPath = resolve(__dirname, "../../tailwind.config.ts");
const configContent = readFileSync(configPath, "utf-8");
it("should extend colors with accent", () => {
expect(configContent).toContain("accent");
expect(configContent).toContain("#EC6925");
});
it("should configure Inter font family", () => {
expect(configContent).toContain("Inter");
});
});
describe("Nuxt Config", () => {
const configPath = resolve(__dirname, "../../nuxt.config.ts");
const configContent = readFileSync(configPath, "utf-8");
it("should include noindex robots meta tag", () => {
expect(configContent).toContain("noindex");
expect(configContent).toContain("nofollow");
});
it("should include JSON-LD LocalBusiness script", () => {
expect(configContent).toContain("LocalBusiness");
expect(configContent).toContain("schema.org");
});
it("should include routeRules for SSR/CSR", () => {
expect(configContent).toContain("routeRules");
expect(configContent).toContain("/mietkatalog");
expect(configContent).toContain("/admin");
});
it("should include Tailwind CSS module", () => {
expect(configContent).toContain("@nuxtjs/tailwindcss");
});
});
@@ -0,0 +1,83 @@
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
describe("Equipment Detail Page (prototype port)", () => {
const pagePath = resolve(__dirname, "../../pages/mietkatalog/[id].vue");
const content = readFileSync(pagePath, "utf-8");
it("should have breadcrumb navigation back to mietkatalog", () => {
expect(content).toContain("Zurück zum Katalog");
expect(content).toContain("/mietkatalog");
});
it("should display equipment name", () => {
expect(content).toContain("item.name");
});
it("should display item code", () => {
expect(content).toContain("item.code");
expect(content).toContain("Artikelnummer");
});
it("should display brand", () => {
expect(content).toContain("item.brand");
expect(content).toContain("Marke");
});
it("should display description", () => {
expect(content).toContain("item.description");
});
it("should display specs table", () => {
expect(content).toContain("item.specs");
expect(content).toContain("Technische Daten");
});
it("should display image with placeholder fallback", () => {
expect(content).toContain("item.image");
expect(content).toContain("📦");
expect(content).toContain("Kein Bild verfügbar");
});
it("should have Mietanfrage section", () => {
expect(content).toContain("Mietanfrage");
expect(content).toContain("Mietbeginn");
expect(content).toContain("Mietende");
});
it("should have quantity selector", () => {
expect(content).toContain("quantity");
expect(content).toContain("Anzahl");
});
it("should have add to cart button", () => {
expect(content).toContain("Zur Mietanfrage hinzufügen");
expect(content).toContain("hms-btn-primary");
});
it("should show ErrorState for non-existent ID", () => {
expect(content).toContain("ErrorState");
expect(content).toContain("nicht gefunden");
});
it("should have related items section", () => {
expect(content).toContain("relatedItems");
expect(content).toContain("Ähnliche Geräte");
expect(content).toContain("EquipmentCard");
});
it("should use loading skeleton", () => {
expect(content).toContain("hms-skeleton");
expect(content).toContain("loading");
});
it("should use TypeScript with lang=ts", () => {
expect(content).toContain('<script setup lang="ts">');
});
it("should have 18 equipment items in data", () => {
expect(content).toContain("L-Acoustics K2");
expect(content).toContain("Sennheiser EW-DX 835");
});
});
@@ -0,0 +1,74 @@
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
describe("Home Page (prototype port)", () => {
const pagePath = resolve(__dirname, "../../pages/index.vue");
const content = readFileSync(pagePath, "utf-8");
it("should have hms-hero section", () => {
expect(content).toContain("hms-hero");
expect(content).toContain("hms-hero-bg");
expect(content).toContain("hms-hero-overlay");
});
it("should have hero title with Veranstaltungstechnik text", () => {
expect(content).toContain("Veranstaltungstechnik");
expect(content).toContain("Professionelle Technik");
});
it("should have Mietkatalog button", () => {
expect(content).toContain("Mietkatalog");
expect(content).toContain("hms-btn-primary");
});
it("should have Beratung anfragen button", () => {
expect(content).toContain("Beratung anfragen");
expect(content).toContain("hms-btn-secondary");
});
it("should display all 8 services", () => {
expect(content).toContain("Vermietung");
expect(content).toContain("Verkauf");
expect(content).toContain("Personal");
expect(content).toContain("Transport");
expect(content).toContain("Lagerung");
expect(content).toContain("Werkstatt");
expect(content).toContain("Installation");
expect(content).toContain("Booking");
});
it("should use ServiceCard component", () => {
expect(content).toContain("ServiceCard");
});
it("should have hms-speaker-grid with 6 speakers", () => {
expect(content).toContain("hms-speaker-grid");
expect(content).toContain("hms-speaker-item");
expect(content).toContain("SpeakerIcon");
expect(content).toContain("L-Acoustics K2");
expect(content).toContain("L-Acoustics KS28");
expect(content).toContain("Robe Pointe");
});
it("should have about section with stats", () => {
expect(content).toContain("20+");
expect(content).toContain("1.000+");
expect(content).toContain("Jahre Erfahrung");
expect(content).toContain("Geräte im Lager");
});
it("should have CTA section with Katalog durchsuchen", () => {
expect(content).toContain("Katalog durchsuchen");
expect(content).toContain("Online-Mietkatalog");
});
it("should use TypeScript with lang=ts", () => {
expect(content).toContain('<script setup lang="ts">');
});
it("should use hms-badge hms-badge-primary", () => {
expect(content).toContain("hms-badge");
expect(content).toContain("hms-badge-primary");
});
});
@@ -0,0 +1,84 @@
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
describe("Kontakt Page (prototype port)", () => {
const pagePath = resolve(__dirname, "../../pages/kontakt.vue");
const content = readFileSync(pagePath, "utf-8");
it("should have page title Kontakt", () => {
expect(content).toContain("Kontakt");
});
it("should have office address card with Leipheim", () => {
expect(content).toContain("Grockelhofen 10");
expect(content).toContain("89340 Leipheim");
});
it("should have warehouse address with Ellzee", () => {
expect(content).toContain("Zur Schönhalde 8");
expect(content).toContain("89352 Ellzee");
});
it("should have opening hours", () => {
expect(content).toContain("Öffnungszeiten");
expect(content).toContain("10:00 18:00");
});
it("should have 2 contact persons", () => {
expect(content).toContain("Leopold Hammerschmidt");
expect(content).toContain("Andreas Mössle");
});
it("should have form with name field (required)", () => {
expect(content).toContain('id="name"');
expect(content).toContain("Name");
});
it("should have form with email field (required)", () => {
expect(content).toContain('id="email"');
expect(content).toContain("E-Mail");
});
it("should have form with phone field", () => {
expect(content).toContain('id="phone"');
expect(content).toContain("Telefon");
});
it("should have form with subject select", () => {
expect(content).toContain('id="subject"');
expect(content).toContain("Anliegen");
});
it("should have form with message textarea (required)", () => {
expect(content).toContain('id="message"');
expect(content).toContain("Nachricht");
});
it("should have privacy consent checkbox", () => {
expect(content).toContain('type="checkbox"');
expect(content).toContain("Datenschutzerklärung");
});
it("should have submit button", () => {
expect(content).toContain("Nachricht senden");
expect(content).toContain("hms-btn-primary");
});
it("should have email validation", () => {
expect(content).toContain("validate");
});
it("should have success state", () => {
expect(content).toContain("Nachricht übermittelt");
expect(content).toContain("Vielen Dank");
});
it("should have hms-input class", () => {
expect(content).toContain("hms-input");
});
it("should use TypeScript with lang=ts", () => {
expect(content).toContain('<script setup lang="ts">');
});
});
+167
View File
@@ -0,0 +1,167 @@
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
describe("AppHeader Component (prototype port)", () => {
const componentPath = resolve(__dirname, "../../components/AppHeader.vue");
const content = readFileSync(componentPath, "utf-8");
it("should have hms-header class", () => {
expect(content).toContain("hms-header");
});
it("should have nav items: Home, Referenzen, Mietkatalog, Kontakt", () => {
expect(content).toContain("Home");
expect(content).toContain("Referenzen");
expect(content).toContain("Mietkatalog");
expect(content).toContain("Kontakt");
});
it("should have Warenkorb button with hms-btn-primary", () => {
expect(content).toContain("Warenkorb");
expect(content).toContain("hms-btn-primary");
});
it("should have hms-nav-btn class for nav items", () => {
expect(content).toContain("hms-nav-btn");
});
it("should have mobile burger menu with aria-expanded", () => {
expect(content).toContain("aria-expanded");
expect(content).toContain("mobileMenuOpen");
});
it("should use HmsLogo component", () => {
expect(content).toContain("HmsLogo");
});
it("should have NuxtLink for navigation", () => {
expect(content).toContain("NuxtLink");
});
it("should have aria-current for active route", () => {
expect(content).toContain("aria-current");
});
});
describe("AppFooter Component (prototype port)", () => {
const componentPath = resolve(__dirname, "../../components/AppFooter.vue");
const content = readFileSync(componentPath, "utf-8");
it("should have hms-footer class", () => {
expect(content).toContain("hms-footer");
});
it("should contain address with Leipheim", () => {
expect(content).toContain("89340");
expect(content).toContain("Leipheim");
});
it("should contain phone number", () => {
expect(content).toContain("tel:+498221204433");
expect(content).toContain("204433");
});
it("should contain email link info@hms-licht-ton.de", () => {
expect(content).toContain("mailto:info@hms-licht-ton.de");
});
it("should contain Impressum link", () => {
expect(content).toContain("Impressum");
expect(content).toContain("/impressum");
});
it("should contain DSGVO link", () => {
expect(content).toContain("DSGVO");
expect(content).toContain("/datenschutz");
});
it("should contain AGB Vermietung link", () => {
expect(content).toContain("AGB Vermietung");
expect(content).toContain("/agb-vermietung");
});
it("should contain Admin-Login link", () => {
expect(content).toContain("Admin-Login");
expect(content).toContain("/admin");
});
it("should have 4-column grid layout", () => {
expect(content).toContain("lg:grid-cols-4");
});
it("should include Facebook social icon link", () => {
expect(content).toContain("facebook.com");
});
it("should include Instagram social icon link", () => {
expect(content).toContain("instagram.com");
});
it("should use HmsLogo component", () => {
expect(content).toContain("HmsLogo");
});
});
describe("Error Page (404, prototype port)", () => {
const errorPath = resolve(__dirname, "../../error.vue");
const content = readFileSync(errorPath, "utf-8");
it("should contain 'Seite nicht gefunden' for 404", () => {
expect(content).toContain("Seite nicht gefunden");
});
it("should contain 404 text", () => {
expect(content).toContain("404");
});
it("should contain link to home", () => {
expect(content).toContain('to="/"');
expect(content).toContain("Zur Startseite");
});
it("should include noindex meta tag", () => {
expect(content).toContain("noindex");
expect(content).toContain("nofollow");
});
it("should have hms-btn hms-btn-primary", () => {
expect(content).toContain("hms-btn");
expect(content).toContain("hms-btn-primary");
});
});
describe("HmsLogo Component (prototype port)", () => {
const logoPath = resolve(__dirname, "../../components/HmsLogo.vue");
const content = readFileSync(logoPath, "utf-8");
it("should render SVG element", () => {
expect(content).toContain("<svg");
});
it("should use orange accent color #EC6925", () => {
expect(content).toContain("#EC6925");
});
it("should have hms-logo-svg class", () => {
expect(content).toContain("hms-logo-svg");
});
it("should accept size prop with default 40", () => {
expect(content).toContain("size");
expect(content).toContain("40");
});
});
describe("robots.txt route", () => {
const robotsPath = resolve(__dirname, "../../server/routes/robots.txt.ts");
const content = readFileSync(robotsPath, "utf-8");
it("should contain User-agent: *", () => {
expect(content).toContain("User-agent: *");
});
it("should contain Disallow: /", () => {
expect(content).toContain("Disallow: /");
});
});
@@ -0,0 +1,124 @@
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
describe("Mietkatalog Page (prototype port)", () => {
const pagePath = resolve(__dirname, "../../pages/mietkatalog.vue");
const content = readFileSync(pagePath, "utf-8");
it("should have page title Mietkatalog", () => {
expect(content).toContain("Mietkatalog");
});
it("should have search input with hms-input", () => {
expect(content).toContain('type="search"');
expect(content).toContain("hms-input");
expect(content).toContain("searchQuery");
});
it("should have sort dropdown", () => {
expect(content).toContain("sortBy");
expect(content).toContain("name");
expect(content).toContain("brand");
expect(content).toContain("code");
});
it("should have category filter chips with hms-chip", () => {
expect(content).toContain("hms-chip");
expect(content).toContain("activeCategory");
expect(content).toContain("Tontechnik");
expect(content).toContain("Lichttechnik");
expect(content).toContain("Rigging");
});
it("should show result count", () => {
expect(content).toContain("filteredEquipment.length");
expect(content).toContain("Gerät");
expect(content).toContain("gefunden");
});
it("should use LoadingSkeleton component", () => {
expect(content).toContain("LoadingSkeleton");
});
it("should use ErrorState component", () => {
expect(content).toContain("ErrorState");
});
it("should use EmptyState component", () => {
expect(content).toContain("EmptyState");
});
it("should render EquipmentCard for each item", () => {
expect(content).toContain("EquipmentCard");
expect(content).toContain("filteredEquipment");
});
it("should have 18 equipment items", () => {
expect(content).toContain("L-Acoustics K2");
expect(content).toContain("Shure SM58");
expect(content).toContain("Sennheiser EW-DX 835");
});
it("should have Warenkorb button", () => {
expect(content).toContain("Warenkorb ansehen");
});
it("should use TypeScript with lang=ts", () => {
expect(content).toContain('<script setup lang="ts">');
});
it("should use hms-card for filter panel", () => {
expect(content).toContain("hms-card");
});
});
describe("EquipmentCard Component (prototype port)", () => {
const cardPath = resolve(__dirname, "../../components/EquipmentCard.vue");
const content = readFileSync(cardPath, "utf-8");
it("should have hms-eq-card class", () => {
expect(content).toContain("hms-eq-card");
});
it("should have image with fallback placeholder", () => {
expect(content).toContain("item.image");
expect(content).toContain("📦");
});
it("should show category badge", () => {
expect(content).toContain("hms-badge");
expect(content).toContain("hms-badge-primary");
expect(content).toContain("item.category");
});
it("should display equipment name", () => {
expect(content).toContain("item.name");
});
it("should show truncated description", () => {
expect(content).toContain("item.description");
expect(content).toContain("line-clamp");
});
it("should display item code", () => {
expect(content).toContain("item.code");
});
it("should emit add-to-cart", () => {
expect(content).toContain("add-to-cart");
});
it("should have Hinzufügen button", () => {
expect(content).toContain("Hinzufügen");
expect(content).toContain("hms-btn-ghost");
});
it("should emit click", () => {
expect(content).toContain("click");
});
it("should use TypeScript with lang=ts", () => {
expect(content).toContain('<script setup lang="ts">');
});
});
@@ -0,0 +1,53 @@
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
describe("Referenzen Page (prototype port)", () => {
const pagePath = resolve(__dirname, "../../pages/referenzen.vue");
const content = readFileSync(pagePath, "utf-8");
it("should have page title Referenzen", () => {
expect(content).toContain("Referenzen");
});
it("should have hms-gallery grid", () => {
expect(content).toContain("hms-gallery");
});
it("should have 9 reference images", () => {
expect(content).toContain("ref1.jpg");
expect(content).toContain("ref9.jpg");
});
it("should have category filter chips with hms-chip", () => {
expect(content).toContain("hms-chip");
expect(content).toContain("Alle");
expect(content).toContain("Open-Air");
expect(content).toContain("Indoor");
expect(content).toContain("Rigging");
expect(content).toContain("Event");
});
it("should have filter logic with computed filteredImages", () => {
expect(content).toContain("filteredImages");
expect(content).toContain("filter");
expect(content).toContain("computed");
});
it("should have loading skeleton state", () => {
expect(content).toContain("hms-skeleton");
expect(content).toContain("loading");
});
it("should use ErrorState component", () => {
expect(content).toContain("ErrorState");
});
it("should use EmptyState component", () => {
expect(content).toContain("EmptyState");
});
it("should use TypeScript with lang=ts", () => {
expect(content).toContain('<script setup lang="ts">');
});
});
@@ -0,0 +1,86 @@
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
describe("Warenkorb Page (prototype port)", () => {
const pagePath = resolve(__dirname, "../../pages/warenkorb.vue");
const content = readFileSync(pagePath, "utf-8");
it("should have page title Mietanfrage", () => {
expect(content).toContain("Mietanfrage");
});
it("should use useCart composable", () => {
expect(content).toContain("useCart");
});
it("should have empty state with link to mietkatalog", () => {
expect(content).toContain("EmptyState");
expect(content).toContain("Warenkorb ist leer");
expect(content).toContain("/mietkatalog");
});
it("should have cart item list", () => {
expect(content).toContain("cartItems");
expect(content).toContain("v-for");
});
it("should have quantity stepper with increment and decrement", () => {
expect(content).toContain("incrementQuantity");
expect(content).toContain("decrementQuantity");
});
it("should display item quantity", () => {
expect(content).toContain("item.quantity");
});
it("should have remove button", () => {
expect(content).toContain("removeItem");
});
it("should have clear cart button", () => {
expect(content).toContain("clearCart");
expect(content).toContain("Warenkorb leeren");
});
it("should display total count", () => {
expect(content).toContain("totalCount");
});
it("should have request form with name field", () => {
expect(content).toContain('id="req-name"');
expect(content).toContain("Name");
});
it("should have request form with email field", () => {
expect(content).toContain('id="req-email"');
expect(content).toContain("E-Mail");
});
it("should have request form with event date field", () => {
expect(content).toContain('id="req-date"');
expect(content).toContain("Veranstaltungsdatum");
});
it("should have request form with location field", () => {
expect(content).toContain('id="req-location"');
expect(content).toContain("Veranstaltungsort");
});
it("should have submit button", () => {
expect(content).toContain("Anfrage absenden");
});
it("should have success state", () => {
expect(content).toContain("Mietanfrage übermittelt");
expect(content).toContain("Vielen Dank");
});
it("should have hms-card for items", () => {
expect(content).toContain("hms-card");
});
it("should use TypeScript with lang=ts", () => {
expect(content).toContain('<script setup lang="ts">');
});
});
@@ -0,0 +1,84 @@
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
describe("useApi composable", () => {
const path = resolve(__dirname, "../../composables/useApi.ts");
const content = readFileSync(path, "utf-8");
it("should export useApi function", () => {
expect(content).toContain("export function useApi");
});
it("should use useRuntimeConfig for apiBase", () => {
expect(content).toContain("useRuntimeConfig");
expect(content).toContain("apiBase");
});
it("should create $fetch client with baseURL", () => {
expect(content).toContain("$fetch.create");
expect(content).toContain("baseURL");
});
it("should export get, post, put, del methods", () => {
expect(content).toContain("async function get");
expect(content).toContain("async function post");
expect(content).toContain("async function put");
expect(content).toContain("async function del");
});
});
describe("useEquipment composable", () => {
const path = resolve(__dirname, "../../composables/useEquipment.ts");
const content = readFileSync(path, "utf-8");
it("should export EquipmentItem interface", () => {
expect(content).toContain("export interface EquipmentItem");
});
it("should export EquipmentDetail interface", () => {
expect(content).toContain("export interface EquipmentDetail");
});
it("should export PaginatedEquipment interface", () => {
expect(content).toContain("export interface PaginatedEquipment");
});
it("should export SortOption type", () => {
expect(content).toContain("export type SortOption");
});
it("should use useApi internally", () => {
expect(content).toContain("useApi");
});
it("should have list method with search, category, sort, page params", () => {
expect(content).toContain("async function list");
expect(content).toContain("search");
expect(content).toContain("category");
expect(content).toContain("sort");
expect(content).toContain("page");
});
it("should call /api/equipment for list", () => {
expect(content).toContain("/api/equipment");
expect(content).toContain("/api/equipment/categories");
expect(content).toContain("/api/equipment/${id}");
});
it("should have detail method", () => {
expect(content).toContain("async function detail");
});
it("should have categories method", () => {
expect(content).toContain("async function categories");
expect(content).toContain("/api/equipment/categories");
});
it("should return list, detail, categories from composable", () => {
expect(content).toContain("return");
expect(content).toContain("list");
expect(content).toContain("detail");
expect(content).toContain("categories");
});
});