added more support for supabase-js
This commit is contained in:
423
tests/integration/functions.test.ts
Normal file
423
tests/integration/functions.test.ts
Normal file
@@ -0,0 +1,423 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createMockedFunction } from './test-utils';
|
||||
|
||||
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_ANON_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_ANON_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_ANON_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_ANON_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_ANON_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_ANON_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_ANON_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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user