185 lines
5.5 KiB
TypeScript
185 lines
5.5 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { FunctionService } from '../../src/services/functions/function.service.js';
|
|
|
|
const mockClient = {
|
|
query: vi.fn(),
|
|
release: vi.fn(),
|
|
};
|
|
|
|
const mockPool = {
|
|
query: vi.fn(),
|
|
connect: vi.fn().mockResolvedValue(mockClient),
|
|
};
|
|
|
|
vi.mock('../../src/infra/database/database.manager.js', () => ({
|
|
DatabaseManager: {
|
|
getInstance: () => ({
|
|
getPool: () => mockPool,
|
|
}),
|
|
},
|
|
}));
|
|
|
|
vi.mock('../../src/providers/functions/deno-subhosting.provider.js', () => ({
|
|
DenoSubhostingProvider: {
|
|
getInstance: () => ({
|
|
isConfigured: vi.fn().mockReturnValue(false),
|
|
checkCode: vi.fn(),
|
|
}),
|
|
},
|
|
}));
|
|
|
|
vi.mock('../../src/services/secrets/secret.service.js', () => ({
|
|
SecretService: {
|
|
getInstance: () => ({}),
|
|
},
|
|
}));
|
|
|
|
vi.mock('../../src/utils/logger.js', () => ({
|
|
default: {
|
|
info: vi.fn(),
|
|
error: vi.fn(),
|
|
warn: vi.fn(),
|
|
debug: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
describe('FunctionService Code Validation (Public API)', () => {
|
|
let service: FunctionService;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
service = FunctionService.getInstance();
|
|
});
|
|
|
|
const createTestFunction = (code: string) => {
|
|
return service.createFunction({
|
|
slug: 'test-function',
|
|
name: 'Test Function',
|
|
code,
|
|
status: 'active',
|
|
});
|
|
};
|
|
|
|
const mockSuccessfulCreate = () => {
|
|
mockClient.query.mockResolvedValueOnce({});
|
|
mockClient.query.mockResolvedValueOnce({});
|
|
mockClient.query.mockResolvedValueOnce({ rows: [{ id: '1' }] });
|
|
};
|
|
|
|
describe('Platform contract validation', () => {
|
|
it('should allow valid function code', async () => {
|
|
const validCode = `
|
|
export default async function(req: Request) {
|
|
const data = await req.json();
|
|
return new Response(JSON.stringify({ hello: 'world' }));
|
|
}
|
|
`;
|
|
|
|
mockSuccessfulCreate();
|
|
await expect(createTestFunction(validCode)).resolves.toBeDefined();
|
|
});
|
|
|
|
it('should block Deno.serve because the platform router handles serving', async () => {
|
|
const code = 'Deno.serve((req) => new Response("hi"));';
|
|
|
|
await expect(createTestFunction(code)).rejects.toThrow(/cannot contain Deno\.serve\(\)/i);
|
|
});
|
|
|
|
it('should reject simple Deno.serve examples anywhere in source', async () => {
|
|
const code = `
|
|
// Standalone Deno apps often use Deno.serve(() => {}).
|
|
export default async function(req: Request) {
|
|
const docs = "Deno.serve(() => {}) is not used by InsForge functions";
|
|
return new Response(docs);
|
|
}
|
|
`;
|
|
|
|
await expect(createTestFunction(code)).rejects.toThrow(/cannot contain Deno\.serve\(\)/i);
|
|
});
|
|
|
|
it('should not treat bracket access as an API-layer security boundary', async () => {
|
|
const code = `
|
|
export default async function(req: Request) {
|
|
Deno["serve"](() => new Response("hi"));
|
|
return new Response("ok");
|
|
}
|
|
`;
|
|
|
|
mockSuccessfulCreate();
|
|
await expect(createTestFunction(code)).resolves.toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('Runtime responsibility boundaries', () => {
|
|
it('should not reject dangerous-looking words inside comments', async () => {
|
|
const code = `
|
|
// Require authenticated user to invoke this function.
|
|
/*
|
|
* Documentation can mention process, eval, globalThis, require,
|
|
* Deno.spawn, and Deno.Command without turning prose into code.
|
|
*/
|
|
export default async function(req: Request) {
|
|
return new Response('ok');
|
|
}
|
|
`;
|
|
|
|
mockSuccessfulCreate();
|
|
await expect(createTestFunction(code)).resolves.toBeDefined();
|
|
});
|
|
|
|
it('should not reject code-shaped examples inside comments', async () => {
|
|
const code = `
|
|
// Example only: const fs = require("fs")
|
|
/*
|
|
* Avoid process.env.API_KEY, eval("x"), and Deno.spawn("cmd")
|
|
* unless the runtime/provider explicitly supports that behavior.
|
|
*/
|
|
export default async function(req: Request) {
|
|
return new Response('ok');
|
|
}
|
|
`;
|
|
|
|
mockSuccessfulCreate();
|
|
await expect(createTestFunction(code)).resolves.toBeDefined();
|
|
});
|
|
|
|
it('should not reject runtime-sensitive APIs at the API validation layer', async () => {
|
|
const code = `
|
|
export default async function(req: Request) {
|
|
const name = new URL(req.url).searchParams.get('name') ?? 'world';
|
|
const rendered = eval('"hello " + name');
|
|
return new Response(String(rendered));
|
|
}
|
|
`;
|
|
|
|
mockSuccessfulCreate();
|
|
await expect(createTestFunction(code)).resolves.toBeDefined();
|
|
});
|
|
|
|
it('should not reject dynamic imports or CommonJS-shaped code at the API layer', async () => {
|
|
const code = `
|
|
export default async function(req: Request) {
|
|
const dependency = await import('npm:@insforge/sdk');
|
|
const maybeRequire = 'require("fs") appears only as text here';
|
|
return new Response(JSON.stringify({ dependency: !!dependency, maybeRequire }));
|
|
}
|
|
`;
|
|
|
|
mockSuccessfulCreate();
|
|
await expect(createTestFunction(code)).resolves.toBeDefined();
|
|
});
|
|
|
|
it('should allow semicolon-free static import statements', async () => {
|
|
const code = `
|
|
import { createClient } from 'npm:@insforge/sdk'
|
|
export default async function(req: Request) {
|
|
return new Response(String(Boolean(createClient)))
|
|
}
|
|
`;
|
|
|
|
mockSuccessfulCreate();
|
|
await expect(createTestFunction(code)).resolves.toBeDefined();
|
|
});
|
|
});
|
|
});
|