428 lines
15 KiB
TypeScript
428 lines
15 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { createMockedFunction } from './test-utils';
|
|
import dotenv from 'dotenv';
|
|
import path from 'path';
|
|
|
|
dotenv.config({ path: path.resolve(process.cwd(), '../../.env') });
|
|
|
|
describe('Edge Functions', () => {
|
|
const functionName = `hello-world-${Date.now()}`;
|
|
// Simple WASI module that prints "Hello from WASM!" to stdout
|
|
const wat = `
|
|
(module
|
|
(import "wasi_snapshot_preview1" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))
|
|
(memory 1)
|
|
(export "memory" (memory 0))
|
|
(data (i32.const 8) "Hello from WASM!")
|
|
(func $main (export "_start")
|
|
(i32.store (i32.const 0) (i32.const 8)) ;; iov.iov_base
|
|
(i32.store (i32.const 4) (i32.const 16)) ;; iov.iov_len
|
|
|
|
(call $fd_write
|
|
(i32.const 1) ;; stdout
|
|
(i32.const 0) ;; iovs ptr
|
|
(i32.const 1) ;; iovs len
|
|
(i32.const 20) ;; nwritten ptr
|
|
)
|
|
drop
|
|
)
|
|
)
|
|
`;
|
|
|
|
it('should deploy a function', async () => {
|
|
const res = await fetch(`${process.env.MADBASE_URL}/functions/v1`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${process.env.MADBASE_SERVICE_ROLE_KEY}`
|
|
},
|
|
body: JSON.stringify({
|
|
name: functionName,
|
|
code_base64: Buffer.from(wat).toString('base64')
|
|
})
|
|
});
|
|
if (res.status !== 200) {
|
|
console.error('Deploy failed:', await res.text());
|
|
}
|
|
expect(res.status).toBe(200);
|
|
});
|
|
|
|
it('should invoke a function', async () => {
|
|
const res = await fetch(`${process.env.MADBASE_URL}/functions/v1/${functionName}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${process.env.MADBASE_SERVICE_ROLE_KEY}`
|
|
},
|
|
body: JSON.stringify({ payload: { name: 'World' } })
|
|
});
|
|
|
|
if (res.status !== 200) {
|
|
console.error('Invoke failed:', await res.text());
|
|
}
|
|
expect(res.status).toBe(200);
|
|
const data = await res.json();
|
|
console.log('Invoke response:', data);
|
|
expect(data.result).toContain('Hello from WASM!');
|
|
});
|
|
|
|
it('should deploy and invoke a Deno function', async () => {
|
|
const name = `deno-hello-${Date.now()}`;
|
|
// Simple Deno function that uses Deno.serve shim
|
|
const code = `
|
|
Deno.serve(async (req) => {
|
|
const body = await req.json();
|
|
return new Response("Hello " + body.name + " from Deno!");
|
|
});
|
|
`;
|
|
|
|
// Deploy
|
|
const deployRes = await fetch(`${process.env.MADBASE_URL}/functions/v1`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${process.env.MADBASE_SERVICE_ROLE_KEY}`
|
|
},
|
|
body: JSON.stringify({
|
|
name,
|
|
code_base64: Buffer.from(code).toString('base64'),
|
|
runtime: 'deno'
|
|
})
|
|
});
|
|
|
|
if (deployRes.status !== 200) {
|
|
console.error('Deno Deploy failed:', await deployRes.text());
|
|
}
|
|
expect(deployRes.status).toBe(200);
|
|
|
|
// Invoke
|
|
const invokeRes = await fetch(`${process.env.MADBASE_URL}/functions/v1/${name}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${process.env.MADBASE_SERVICE_ROLE_KEY}`
|
|
},
|
|
body: JSON.stringify({ payload: { name: 'World' } })
|
|
});
|
|
|
|
if (invokeRes.status !== 200) {
|
|
console.error('Deno Invoke failed:', await invokeRes.text());
|
|
}
|
|
expect(invokeRes.status).toBe(200);
|
|
const data = await invokeRes.json();
|
|
console.log('Deno Invoke response:', data);
|
|
expect(data.result).toBe('Hello World from Deno!');
|
|
});
|
|
|
|
describe('Unit Tests (Component Logic)', () => {
|
|
it('should handle missing environment variables', async () => {
|
|
const name = `env-check-${Date.now()}`;
|
|
const code = createMockedFunction(`
|
|
Deno.serve(async (req) => {
|
|
const key = Deno.env.get("MY_SECRET_KEY");
|
|
if (!key) {
|
|
return new Response("Missing Key", { status: 500 });
|
|
}
|
|
return new Response("Found Key: " + key);
|
|
});
|
|
`, { env: {} }); // Empty env
|
|
|
|
// Deploy
|
|
await fetch(`${process.env.MADBASE_URL}/functions/v1`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${process.env.MADBASE_SERVICE_ROLE_KEY}`
|
|
},
|
|
body: JSON.stringify({
|
|
name,
|
|
code_base64: Buffer.from(code).toString('base64'),
|
|
runtime: 'deno'
|
|
})
|
|
});
|
|
|
|
// Invoke
|
|
const res = await fetch(`${process.env.MADBASE_URL}/functions/v1/${name}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${process.env.MADBASE_SERVICE_ROLE_KEY}`
|
|
},
|
|
body: JSON.stringify({ payload: {} })
|
|
});
|
|
|
|
const data = await res.json();
|
|
expect(data.result).toBe("Missing Key");
|
|
expect(data.status).toBe(500);
|
|
});
|
|
|
|
it('should validate request body', async () => {
|
|
const name = `body-check-${Date.now()}`;
|
|
const code = createMockedFunction(`
|
|
Deno.serve(async (req) => {
|
|
const body = await req.json();
|
|
if (!body.requiredField) {
|
|
return new Response("Missing Field", { status: 400 });
|
|
}
|
|
return new Response("OK");
|
|
});
|
|
`);
|
|
|
|
// Deploy
|
|
await fetch(`${process.env.MADBASE_URL}/functions/v1`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${process.env.MADBASE_SERVICE_ROLE_KEY}`
|
|
},
|
|
body: JSON.stringify({
|
|
name,
|
|
code_base64: Buffer.from(code).toString('base64'),
|
|
runtime: 'deno'
|
|
})
|
|
});
|
|
|
|
// Invoke (Missing Field)
|
|
const res = await fetch(`${process.env.MADBASE_URL}/functions/v1/${name}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${process.env.MADBASE_SERVICE_ROLE_KEY}`
|
|
},
|
|
body: JSON.stringify({ payload: {} })
|
|
});
|
|
|
|
const data = await res.json();
|
|
expect(data.result).toBe("Missing Field");
|
|
expect(data.status).toBe(400);
|
|
});
|
|
});
|
|
|
|
describe('Integration Tests (System Interactions)', () => {
|
|
it('should handle CORS preflight requests', async () => {
|
|
const name = `cors-check-${Date.now()}`;
|
|
const code = createMockedFunction(`
|
|
const corsHeaders = {
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
};
|
|
Deno.serve(async (req) => {
|
|
if (req.method === "OPTIONS") {
|
|
return new Response("ok", { headers: corsHeaders });
|
|
}
|
|
return new Response("ok", { headers: corsHeaders });
|
|
});
|
|
`);
|
|
|
|
await fetch(`${process.env.MADBASE_URL}/functions/v1`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${process.env.MADBASE_SERVICE_ROLE_KEY}`
|
|
},
|
|
body: JSON.stringify({
|
|
name,
|
|
code_base64: Buffer.from(code).toString('base64'),
|
|
runtime: 'deno'
|
|
})
|
|
});
|
|
|
|
// Invoke with OPTIONS (Note: The Gateway might handle this or pass it through.
|
|
// Our Deno runtime shim creates a request with POST method by default for invocations,
|
|
// so testing OPTIONS strictly via invocation endpoint might need support in the handler/shim.
|
|
// For now, we test that the function *can* set headers in response.)
|
|
|
|
const res = await fetch(`${process.env.MADBASE_URL}/functions/v1/${name}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${process.env.MADBASE_SERVICE_ROLE_KEY}`
|
|
},
|
|
body: JSON.stringify({ payload: {} })
|
|
});
|
|
|
|
const data = await res.json();
|
|
// Check if headers are returned (requires handler update to return headers, which we did)
|
|
expect(data.headers['access-control-allow-origin']).toBe('*');
|
|
});
|
|
});
|
|
|
|
describe('E2E Workflows (User Flows)', () => {
|
|
it('should execute invite staff workflow', async () => {
|
|
const name = `invite-staff-${Date.now()}`;
|
|
const code = createMockedFunction(`
|
|
Deno.serve(async (req) => {
|
|
const { email } = await req.json();
|
|
|
|
// 1. Insert into DB (mocked)
|
|
const supabase = createClient();
|
|
const { error } = await supabase.from('invitations').insert({ email });
|
|
if (error) return new Response("DB Error", { status: 500 });
|
|
|
|
// 2. Send Email (mocked fetch)
|
|
const res = await fetch("https://api.resend.com/emails", {
|
|
method: "POST",
|
|
body: JSON.stringify({ to: email })
|
|
});
|
|
|
|
if (!res.ok) return new Response("Email Error", { status: 502 });
|
|
|
|
return new Response("Invite Sent");
|
|
});
|
|
`, {
|
|
fetch: [{ urlPattern: "api.resend.com", status: 200, response: { id: "email_123" } }],
|
|
supabase: { insertResult: { id: "invite_123" } }
|
|
});
|
|
|
|
await fetch(`${process.env.MADBASE_URL}/functions/v1`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${process.env.MADBASE_SERVICE_ROLE_KEY}`
|
|
},
|
|
body: JSON.stringify({
|
|
name,
|
|
code_base64: Buffer.from(code).toString('base64'),
|
|
runtime: 'deno'
|
|
})
|
|
});
|
|
|
|
const res = await fetch(`${process.env.MADBASE_URL}/functions/v1/${name}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${process.env.MADBASE_SERVICE_ROLE_KEY}`
|
|
},
|
|
body: JSON.stringify({ payload: { email: "newuser@example.com" } })
|
|
});
|
|
|
|
const data = await res.json();
|
|
expect(data.result).toBe("Invite Sent");
|
|
expect(data.status).toBe(200);
|
|
});
|
|
});
|
|
|
|
it('should deploy and invoke a complex Polar Checkout-like function', async () => {
|
|
const name = `polar-checkout-${Date.now()}`;
|
|
const code = createMockedFunction(`
|
|
const corsHeaders = {
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
|
|
};
|
|
|
|
Deno.serve(async (req) => {
|
|
if (req.method === "OPTIONS") {
|
|
return new Response(null, { headers: corsHeaders });
|
|
}
|
|
|
|
try {
|
|
const POLAR_API_KEY = Deno.env.get("POLAR_API_KEY");
|
|
if (!POLAR_API_KEY) throw new Error("POLAR_API_KEY is not configured");
|
|
|
|
// Authenticate user
|
|
const authHeader = req.headers.get("Authorization");
|
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
return new Response(JSON.stringify({ error: "Unauthorized: Missing or invalid token" }), {
|
|
status: 401,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
const supabase = createClient(
|
|
Deno.env.get("SUPABASE_URL"),
|
|
Deno.env.get("SUPABASE_ANON_KEY"),
|
|
{ global: { headers: { Authorization: authHeader } } }
|
|
);
|
|
|
|
const token = authHeader.replace("Bearer ", "");
|
|
const { data: claimsData, error: claimsError } = await supabase.auth.getClaims(token);
|
|
|
|
if (claimsError || !claimsData?.claims) {
|
|
return new Response(JSON.stringify({ error: "Unauthorized: Invalid claims" }), { status: 401 });
|
|
}
|
|
|
|
const { productId, successUrl } = await req.json();
|
|
|
|
// Create Polar checkout session
|
|
const polarRes = await fetch("https://sandbox-api.polar.sh/v1/checkouts/", {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: "Bearer " + POLAR_API_KEY,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
products: [productId],
|
|
success_url: successUrl,
|
|
metadata: { user_id: claimsData.claims.sub }
|
|
}),
|
|
});
|
|
|
|
const polarData = await polarRes.json();
|
|
|
|
if (!polarRes.ok) {
|
|
throw new Error("Polar API error");
|
|
}
|
|
|
|
return new Response(
|
|
JSON.stringify({ url: polarData.url, id: polarData.id }),
|
|
{ status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
|
);
|
|
|
|
} catch (error) {
|
|
return new Response(JSON.stringify({ error: String(error) }), {
|
|
status: 500,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
});
|
|
`, {
|
|
env: {
|
|
"POLAR_API_KEY": "mock_polar_key",
|
|
"SUPABASE_URL": "http://mock-supabase",
|
|
"SUPABASE_ANON_KEY": "mock_anon_key",
|
|
"SUPABASE_SERVICE_ROLE_KEY": "mock_service_key"
|
|
},
|
|
supabase: {
|
|
claims: { sub: "user_123", email: "test@example.com" }
|
|
},
|
|
fetch: [{
|
|
urlPattern: "sandbox-api.polar.sh/v1/checkouts/",
|
|
status: 200,
|
|
response: { url: "https://sandbox.polar.sh/checkout/123", id: "checkout_123" }
|
|
}]
|
|
});
|
|
|
|
// Deploy
|
|
const deployRes = await fetch(`${process.env.MADBASE_URL}/functions/v1`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${process.env.MADBASE_SERVICE_ROLE_KEY}`
|
|
},
|
|
body: JSON.stringify({
|
|
name,
|
|
code_base64: Buffer.from(code).toString('base64'),
|
|
runtime: 'deno'
|
|
})
|
|
});
|
|
|
|
expect(deployRes.status).toBe(200);
|
|
|
|
// Invoke
|
|
const invokeRes = await fetch(`${process.env.MADBASE_URL}/functions/v1/${name}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${process.env.MADBASE_SERVICE_ROLE_KEY}`
|
|
},
|
|
body: JSON.stringify({ payload: { productId: "prod_123", successUrl: "http://example.com" } })
|
|
});
|
|
|
|
expect(invokeRes.status).toBe(200);
|
|
const data = await invokeRes.json();
|
|
console.log('Polar Invoke response:', data);
|
|
const result = JSON.parse(data.result);
|
|
expect(result.url).toBe("https://sandbox.polar.sh/checkout/123");
|
|
});
|
|
});
|