feat: add internal API test harness

This commit introduces a new internal API test harness built with React and Vite. This harness provides a user interface for testing various API endpoints, including authentication, core integrations, and entity-related APIs.

The harness includes the following features:

- Firebase authentication integration for secure API testing.
- A modular design with separate components for different API categories.
- Form-based input for API parameters, allowing users to easily configure requests.
- JSON-based response display for clear and readable API results.
- Error handling and display for debugging purposes.
- A navigation system for easy access to different API endpoints.
- Environment-specific configuration for testing in different environments.

This harness will enable developers to quickly and efficiently test API endpoints, ensuring the quality and reliability of the KROW backend services.

The following files were added:

- Makefile: Added targets for installing, developing, building, and deploying the API test harness.
- firebase.json: Added hosting configurations for the API test harness in development and staging environments.
- firebase/internal-launchpad/index.html: Updated with accordion styles and navigation for diagrams and documents.
- internal-api-harness/.env.example: Example environment variables for the API test harness.
- internal-api-harness/.gitignore: Git ignore file for the API test harness.
- internal-api-harness/README.md: README file for the API test harness.
- internal-api-harness/components.json: Configuration file for shadcn-ui components.
- internal-api-harness/eslint.config.js: ESLint configuration file.
- internal-api-harness/index.html: Main HTML file for the API test harness.
- internal-api-harness/jsconfig.json: JSConfig file for the API test harness.
- internal-api-harness/package.json: Package file for the API test harness.
- internal-api-harness/postcss.config.js: PostCSS configuration file.
- internal-api-harness/public/logo.svg: Krow logo.
- internal-api-harness/public/vite.svg: Vite logo.
- internal-api-harness/src/App.css: CSS file for the App component.
- internal-api-harness/src/App.jsx: Main App component.
- internal-api-harness/src/api/client.js: API client for making requests to the backend.
- internal-api-harness/src/api/krowSDK.js: SDK for interacting with Krow APIs.
- internal-api-harness/src/assets/react.svg: React logo.
- internal-api-harness/src/components/ApiResponse.jsx: Component for displaying API responses.
- internal-api-harness/src/components/Layout.jsx: Layout component for the API test harness.
- internal-api-harness/src/components/ServiceTester.jsx: Component for testing individual services.
- internal-api-harness/src/components/ui/button.jsx: Button component.
- internal-api-harness/src/components/ui/card.jsx: Card component.
- internal-api-harness/src/components/ui/collapsible.jsx: Collapsible component.
- internal-api-harness/src/components/ui/input.jsx: Input component.
- internal-api-harness/src/components/ui/label.jsx: Label component.
- internal-api-harness/src/components/ui/select.jsx: Select component.
- internal-api-harness/src/components/ui/textarea.jsx: Textarea component.
- internal-api-harness/src/firebase.js: Firebase configuration file.
- internal-api-harness/src/index.css: Main CSS file.
- internal-api-harness/src/lib/utils.js: Utility functions.
- internal-api-harness/src/main.jsx: Main entry point for the React application.
- internal-api-harness/src/pages/ApiPlaceholder.jsx: Placeholder component for unimplemented APIs.
- internal-api-harness/src/pages/EntityTester.jsx: Component for testing entity APIs.
- internal-api-harness/src/pages/GenerateImage.jsx: Component for testing the Generate Image API.
- internal-api-harness/src/pages/Home.jsx: Home page component.
- internal-api-harness/src/pages/Login.jsx: Login page component.
- internal-api-harness/src/pages/auth/GetMe.jsx: Component for testing the Get Me API.
- internal-api-harness/src/pages/core/CreateSignedUrl.jsx: Component for testing the Create Signed URL API.
- internal-api-harness/src/pages/core/InvokeLLM.jsx: Component for testing the Invoke LLM API.
- internal-api-harness/src/pages/core/SendEmail.jsx: Component for testing the Send Email API.
- internal-api-harness/src/pages/core/UploadFile.jsx: Component for testing the Upload File API.
- internal-api-harness/src/pages/core/UploadPrivateFile.jsx: Component for testing the Upload Private File API.
- internal-api-harness/tailwind.config.js: Tailwind CSS configuration file.
- internal-api-harness/vite.config.js: Vite configuration file.
This commit is contained in:
bwnyasse
2025-11-16 21:45:17 -05:00
parent 831570f2e0
commit f7c2027065
47 changed files with 8707 additions and 154 deletions

View File

@@ -0,0 +1,13 @@
const ApiPlaceholder = ({ title }) => {
return (
<div>
<h1 className="text-3xl font-bold text-slate-900 mb-4">{title}</h1>
<div className="bg-slate-100 border border-slate-200 rounded-lg p-8 text-center">
<p className="text-slate-500">This page is a placeholder for the "{title}" API test harness.</p>
<p className="text-slate-500 mt-2">Implementation is pending.</p>
</div>
</div>
);
};
export default ApiPlaceholder;

View File

@@ -0,0 +1,190 @@
import { useState, useMemo } from "react";
import { krowSDK } from "@/api/krowSDK";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import ApiResponse from "@/components/ApiResponse";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
const entityNames = Object.keys(krowSDK.entities).sort();
const getPrettifiedJSON = (entity, method) => {
// Basic placeholder payloads. A more advanced SDK could provide detailed examples.
const payloads = {
get: { id: "some-id" },
create: { data: { property: "value" } },
update: { id: "some-id", data: { property: "new-value" } },
delete: { id: "some-id" },
filter: { where: { property: { _eq: "value" } } },
list: {}
};
return JSON.stringify(payloads[method] || {}, null, 2);
};
const EntityTester = () => {
const [selectedEntity, setSelectedEntity] = useState(null);
const [selectedMethod, setSelectedMethod] = useState(null);
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const [jsonInput, setJsonInput] = useState("");
const [jsonError, setJsonError] = useState(null);
const availableMethods = useMemo(() => {
if (!selectedEntity) return [];
return Object.keys(krowSDK.entities[selectedEntity]);
}, [selectedEntity]);
const handleEntityChange = (entity) => {
setSelectedEntity(entity);
setSelectedMethod(null);
setJsonInput("");
setJsonError(null);
setResponse(null);
setError(null);
};
const handleMethodSelect = (method) => {
setSelectedMethod(method);
setJsonInput(getPrettifiedJSON(selectedEntity, method));
setJsonError(null);
setResponse(null);
setError(null);
};
const handleJsonInputChange = (e) => {
setJsonInput(e.target.value);
try {
JSON.parse(e.target.value);
setJsonError(null);
} catch (err) {
setJsonError("Invalid JSON format");
}
};
const executeApi = async () => {
if (!selectedEntity || !selectedMethod || jsonError) return;
setLoading(true);
setResponse(null);
setError(null);
try {
const params = JSON.parse(jsonInput);
const sdkMethod = krowSDK.entities[selectedEntity][selectedMethod];
const res = await sdkMethod(params);
setResponse(res);
} catch (err) {
setError(err.response?.data || err.message);
} finally {
setLoading(false);
}
};
const renderMethodForm = () => {
if (!selectedMethod) {
return (
<div className="mt-4 p-4 text-center text-slate-500 bg-slate-50 rounded-lg">
<p>Select a method to begin.</p>
</div>
);
}
return (
<div className="space-y-4 mt-6">
<h3 className="text-lg font-semibold text-slate-800">
Parameters for <code className="bg-slate-100 p-1 rounded text-sm">/{selectedEntity}/{selectedMethod}</code>
</h3>
{/*
This is a textarea for JSON input. A more advanced implementation could
dynamically generate a form based on the expected parameters of each
SDK method, but that requires metadata about each method's signature
which is not currently available in the mock client.
*/}
<div className="space-y-2">
<Label htmlFor="params">JSON Payload</Label>
<Textarea
id="params"
name="params"
value={jsonInput}
onChange={handleJsonInputChange}
rows={8}
className={`font-mono text-sm ${jsonError ? 'border-red-500 focus-visible:ring-red-500' : ''}`}
/>
{jsonError && <p className="text-xs text-red-600">{jsonError}</p>}
</div>
<Button onClick={executeApi} disabled={loading || !!jsonError}>
{loading ? "Executing..." : "Execute"}
</Button>
</div>
);
};
return (
<div>
<header className="mb-8">
<h1 className="text-3xl font-bold text-slate-900">Entity API Tester</h1>
<p className="text-slate-600 mt-1">
Select an entity and method, provide the required parameters in JSON format, and execute the API call.
</p>
</header>
<Card className="shadow-sm">
<CardHeader>
<CardTitle>API Configuration</CardTitle>
<CardDescription>Choose a Base44 entity and method to interact with.</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 items-start">
<div className="col-span-1">
<Label>Entity</Label>
<Select onValueChange={handleEntityChange} value={selectedEntity || ""}>
<SelectTrigger>
<SelectValue placeholder="Select an entity" />
</SelectTrigger>
<SelectContent>
{entityNames.map(entity => (
<SelectItem key={entity} value={entity}>{entity}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{selectedEntity && (
<div className="col-span-2">
<Label>Method</Label>
<div className="flex flex-wrap items-center gap-2 pt-2">
{availableMethods.map(method => (
<Button
key={method}
variant={selectedMethod === method ? "default" : "outline"}
size="sm"
onClick={() => handleMethodSelect(method)}
>
{method}
</Button>
))}
</div>
</div>
)}
</div>
{renderMethodForm()}
</CardContent>
</Card>
<ApiResponse response={response} error={error} loading={loading} />
</div>
);
};
export default EntityTester;

View File

@@ -0,0 +1,28 @@
import ServiceTester from "@/components/ServiceTester";
const GenerateImage = () => {
const fields = [
{
name: "prompt",
label: "Prompt",
type: "textarea",
placeholder: "Enter a prompt for the image",
},
{
name: "file",
label: "File",
type: "file",
},
];
return (
<ServiceTester
serviceName="Generate Image"
serviceDescription="Test the Generate Image service"
endpoint="/generate-image"
fields={fields}
/>
);
};
export default GenerateImage;

View File

@@ -0,0 +1,31 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
const Home = () => {
return (
<div className="max-w-4xl mx-auto">
<div className="mb-8">
<h1 className="text-3xl font-bold text-slate-900">Welcome to KROW API Test Harness</h1>
<p className="text-slate-600 mt-1">
Your dedicated tool for rapid and authenticated testing of KROW backend services.
</p>
</div>
<Card>
<CardHeader>
<CardTitle>Get Started</CardTitle>
<CardDescription>
Use the sidebar navigation to select an API category and then choose a specific endpoint to test.
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-slate-700">
This tool automatically handles Firebase authentication, injecting the necessary ID tokens into your API requests.
Simply log in, select an API, provide the required parameters, and execute to see the raw JSON response.
</p>
</CardContent>
</Card>
</div>
);
};
export default Home;

View File

@@ -0,0 +1,37 @@
import { GoogleAuthProvider, signInWithPopup, setPersistence, browserLocalPersistence } from "firebase/auth";
import { useAuthState } from "react-firebase-hooks/auth";
import { Navigate } from "react-router-dom";
import { auth } from "../firebase";
import { Button } from "@/components/ui/button";
const Login = () => {
const [user, loading] = useAuthState(auth);
const handleGoogleLogin = async () => {
const provider = new GoogleAuthProvider();
try {
await setPersistence(auth, browserLocalPersistence);
await signInWithPopup(auth, provider);
} catch (error) {
console.error("Error signing in with Google", error);
}
};
if (loading) {
return <div>Loading...</div>;
}
// If user is logged in, redirect to the home page
if (user) {
return <Navigate to="/" />;
}
// If no user, show the login button
return (
<div className="flex items-center justify-center h-screen">
<Button onClick={handleGoogleLogin}>Sign in with Google</Button>
</div>
);
};
export default Login;

View File

@@ -0,0 +1,49 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import ApiResponse from "@/components/ApiResponse";
import { krowSDK } from "@/api/krowSDK";
const GetMe = () => {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const handleGetMe = async () => {
setLoading(true);
setError(null);
setResponse(null);
try {
const res = await krowSDK.auth.me();
setResponse(res);
} catch (err) {
setError(err.response?.data || err.message);
} finally {
setLoading(false);
}
};
return (
<div>
<h1 className="text-3xl font-bold text-slate-900 mb-4">Get Me</h1>
<p className="text-slate-600 mb-6">Fetches the currently authenticated user's profile.</p>
<Card className="max-w-2xl">
<CardHeader>
<CardTitle>Test `/auth/me`</CardTitle>
</CardHeader>
<CardContent>
<Button onClick={handleGetMe} disabled={loading}>
{loading ? "Loading..." : "Execute"}
</Button>
</CardContent>
</Card>
<ApiResponse response={response} error={error} loading={loading} />
</div>
);
};
// We need to re-import Card components because they are not globally available.
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
export default GetMe;

View File

@@ -0,0 +1,74 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import ApiResponse from "@/components/ApiResponse";
import { krowSDK } from "@/api/krowSDK";
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "@/components/ui/card";
const CreateSignedUrl = () => {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
file_uri: "gs://your-bucket/private-file.pdf",
expires_in: 3600,
});
const handleChange = (e) => {
const { id, value } = e.target;
setFormData(prev => ({ ...prev, [id]: value }));
};
const handleCreateUrl = async (e) => {
e.preventDefault();
setLoading(true);
setError(null);
setResponse(null);
try {
const params = {
...formData,
expires_in: parseInt(formData.expires_in, 10),
};
const res = await krowSDK.integrations.Core.CreateFileSignedUrl(params);
setResponse(res);
} catch (err) {
setError(err.response?.data || err.message);
} finally {
setLoading(false);
}
};
return (
<div>
<h1 className="text-3xl font-bold text-slate-900 mb-4">Create Signed URL</h1>
<p className="text-slate-600 mb-6">Tests the `integrations.Core.CreateFileSignedUrl` endpoint.</p>
<Card className="max-w-2xl">
<CardHeader>
<CardTitle>Test `/createSignedUrl`</CardTitle>
<CardDescription>Creates a temporary access URL for a private file stored in GCS.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleCreateUrl} className="space-y-4">
<div>
<Label htmlFor="file_uri">File URI</Label>
<Input id="file_uri" value={formData.file_uri} onChange={handleChange} />
</div>
<div>
<Label htmlFor="expires_in">Expires In (seconds)</Label>
<Input id="expires_in" type="number" value={formData.expires_in} onChange={handleChange} />
</div>
<Button type="submit" disabled={loading}>
{loading ? "Creating..." : "Create Signed URL"}
</Button>
</form>
</CardContent>
</Card>
<ApiResponse response={response} error={error} loading={loading} />
</div>
);
};
export default CreateSignedUrl;

View File

@@ -0,0 +1,81 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import ApiResponse from "@/components/ApiResponse";
import { krowSDK } from "@/api/krowSDK";
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
const InvokeLLM = () => {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
prompt: "Extract the total amount from the attached invoice.",
response_json_schema: JSON.stringify({ type: "object", properties: { total_amount: { type: "number" } } }, null, 2),
file_urls: "https://example.com/invoice.pdf",
});
const handleChange = (e) => {
const { id, value } = e.target;
setFormData(prev => ({ ...prev, [id]: value }));
};
const handleInvokeLLM = async (e) => {
e.preventDefault();
setLoading(true);
setError(null);
setResponse(null);
try {
const params = {
...formData,
response_json_schema: JSON.parse(formData.response_json_schema),
file_urls: formData.file_urls.split(',').map(url => url.trim()),
};
const res = await krowSDK.integrations.Core.InvokeLLM(params);
setResponse(res);
} catch (err) {
setError(err.response?.data || err.message);
} finally {
setLoading(false);
}
};
return (
<div>
<h1 className="text-3xl font-bold text-slate-900 mb-4">Invoke LLM</h1>
<p className="text-slate-600 mb-6">Tests the `integrations.Core.InvokeLLM` endpoint.</p>
<Card className="max-w-2xl">
<CardHeader>
<CardTitle>Test `/invokeLLM`</CardTitle>
<CardDescription>Calls a large language model (e.g., Vertex AI).</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleInvokeLLM} className="space-y-4">
<div>
<Label htmlFor="prompt">Prompt</Label>
<Textarea id="prompt" value={formData.prompt} onChange={handleChange} rows={4} />
</div>
<div>
<Label htmlFor="response_json_schema">Response JSON Schema</Label>
<Textarea id="response_json_schema" value={formData.response_json_schema} onChange={handleChange} rows={6} className="font-mono" />
</div>
<div>
<Label htmlFor="file_urls">File URLs (comma-separated)</Label>
<Input id="file_urls" value={formData.file_urls} onChange={handleChange} />
</div>
<Button type="submit" disabled={loading}>
{loading ? "Invoking..." : "Invoke LLM"}
</Button>
</form>
</CardContent>
</Card>
<ApiResponse response={response} error={error} loading={loading} />
</div>
);
};
export default InvokeLLM;

View File

@@ -0,0 +1,75 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import ApiResponse from "@/components/ApiResponse";
import { krowSDK } from "@/api/krowSDK";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
const SendEmail = () => {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
to: "test@example.com",
subject: "Test Email from Harness",
body: "This is a test email.",
});
const handleChange = (e) => {
const { id, value } = e.target;
setFormData(prev => ({ ...prev, [id]: value }));
};
const handleSendEmail = async (e) => {
e.preventDefault();
setLoading(true);
setError(null);
setResponse(null);
try {
const res = await krowSDK.integrations.Core.SendEmail(formData);
setResponse(res);
} catch (err) {
setError(err.response?.data || err.message);
} finally {
setLoading(false);
}
};
return (
<div>
<h1 className="text-3xl font-bold text-slate-900 mb-4">Send Email</h1>
<p className="text-slate-600 mb-6">Tests the `integrations.Core.SendEmail` endpoint.</p>
<Card className="max-w-2xl">
<CardHeader>
<CardTitle>Test `/sendEmail`</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSendEmail} className="space-y-4">
<div>
<Label htmlFor="to">To</Label>
<Input id="to" type="email" value={formData.to} onChange={handleChange} />
</div>
<div>
<Label htmlFor="subject">Subject</Label>
<Input id="subject" value={formData.subject} onChange={handleChange} />
</div>
<div>
<Label htmlFor="body">Body</Label>
<Textarea id="body" value={formData.body} onChange={handleChange} rows={5} />
</div>
<Button type="submit" disabled={loading}>
{loading ? "Sending..." : "Send Email"}
</Button>
</form>
</CardContent>
</Card>
<ApiResponse response={response} error={error} loading={loading} />
</div>
);
};
export default SendEmail;

View File

@@ -0,0 +1,67 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import ApiResponse from "@/components/ApiResponse";
import { krowSDK } from "@/api/krowSDK";
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "@/components/ui/card";
const UploadFile = () => {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const [file, setFile] = useState(null);
const handleFileChange = (e) => {
setFile(e.target.files[0]);
};
const handleUpload = async (e) => {
e.preventDefault();
if (!file) {
setError({ message: "Please select a file to upload." });
return;
}
setLoading(true);
setError(null);
setResponse(null);
try {
const res = await krowSDK.integrations.Core.UploadFile({ file });
setResponse(res);
} catch (err) {
setError(err.response?.data || err.message);
} finally {
setLoading(false);
}
};
return (
<div>
<h1 className="text-3xl font-bold text-slate-900 mb-4">Upload Public File</h1>
<p className="text-slate-600 mb-6">Tests the `integrations.Core.UploadFile` endpoint for public files.</p>
<Card className="max-w-2xl">
<CardHeader>
<CardTitle>Test `/uploadFile`</CardTitle>
<CardDescription>Handles multipart/form-data upload to GCS and returns a public URL.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleUpload} className="space-y-4">
<div>
<Label htmlFor="file">File</Label>
<Input id="file" type="file" onChange={handleFileChange} />
</div>
<Button type="submit" disabled={loading || !file}>
{loading ? "Uploading..." : "Upload File"}
</Button>
</form>
</CardContent>
</Card>
<ApiResponse response={response} error={error} loading={loading} />
</div>
);
};
export default UploadFile;

View File

@@ -0,0 +1,67 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import ApiResponse from "@/components/ApiResponse";
import { krowSDK } from "@/api/krowSDK";
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "@/components/ui/card";
const UploadPrivateFile = () => {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const [file, setFile] = useState(null);
const handleFileChange = (e) => {
setFile(e.target.files[0]);
};
const handleUpload = async (e) => {
e.preventDefault();
if (!file) {
setError({ message: "Please select a file to upload." });
return;
}
setLoading(true);
setError(null);
setResponse(null);
try {
const res = await krowSDK.integrations.Core.UploadPrivateFile({ file });
setResponse(res);
} catch (err) {
setError(err.response?.data || err.message);
} finally {
setLoading(false);
}
};
return (
<div>
<h1 className="text-3xl font-bold text-slate-900 mb-4">Upload Private File</h1>
<p className="text-slate-600 mb-6">Tests the `integrations.Core.UploadPrivateFile` endpoint.</p>
<Card className="max-w-2xl">
<CardHeader>
<CardTitle>Test `/uploadPrivateFile`</CardTitle>
<CardDescription>Handles multipart/form-data upload to GCS and returns a secure gs:// URI.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleUpload} className="space-y-4">
<div>
<Label htmlFor="file">File</Label>
<Input id="file" type="file" onChange={handleFileChange} />
</div>
<Button type="submit" disabled={loading || !file}>
{loading ? "Uploading..." : "Upload File"}
</Button>
</form>
</CardContent>
</Card>
<ApiResponse response={response} error={error} loading={loading} />
</div>
);
};
export default UploadPrivateFile;