Skip to content

Commit 8107c65

Browse files
committed
Test generated Netlify upload functions
1 parent 10c663a commit 8107c65

2 files changed

Lines changed: 203 additions & 0 deletions

File tree

test/fixtures/fake-foc-cli.mjs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env node
2+
3+
const payload = {
4+
ok: true,
5+
result: {
6+
pieceCid: 'bafkqaaaafakecidfornetlifyuploadtest',
7+
size: 321,
8+
copyResults: [
9+
{
10+
url: 'https://calibration.example.invalid/piece/bafkqaaaafakecidfornetlifyuploadtest'
11+
}
12+
]
13+
}
14+
};
15+
16+
process.stdout.write(JSON.stringify(payload));
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { expect } from 'chai';
2+
import fs from 'fs';
3+
import os from 'os';
4+
import path from 'path';
5+
import { pathToFileURL } from 'url';
6+
import { spawnSync } from 'child_process';
7+
8+
function writeJson(filePath, value) {
9+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
10+
fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
11+
}
12+
13+
function runTh(args, cwd) {
14+
return spawnSync('node', [path.resolve('packages/cli/dist/index.js'), ...args], {
15+
cwd,
16+
encoding: 'utf-8'
17+
});
18+
}
19+
20+
function minimalSchema(overrides = {}) {
21+
return {
22+
thsVersion: '2025-12',
23+
schemaVersion: '0.0.1',
24+
app: {
25+
name: 'Netlify Upload Function Test',
26+
slug: 'netlify-upload-function-test',
27+
features: { uploads: true, onChainIndexing: true },
28+
deploy: {
29+
netlify: {
30+
uploads: {
31+
provider: 'filecoin_onchain_cloud',
32+
runner: 'background-function'
33+
}
34+
}
35+
}
36+
},
37+
collections: [
38+
{
39+
name: 'Item',
40+
fields: [{ name: 'image', type: 'image' }],
41+
createRules: { required: [], access: 'public' },
42+
visibilityRules: { gets: ['image'], access: 'public' },
43+
updateRules: { mutable: ['image'], access: 'owner' },
44+
deleteRules: { softDelete: true, access: 'owner' },
45+
indexes: { unique: [], index: [] }
46+
}
47+
],
48+
...overrides
49+
};
50+
}
51+
52+
function installNetlifyBlobsStub(outDir) {
53+
const pkgDir = path.join(outDir, 'node_modules', '@netlify', 'blobs');
54+
fs.mkdirSync(pkgDir, { recursive: true });
55+
fs.writeFileSync(
56+
path.join(pkgDir, 'package.json'),
57+
JSON.stringify(
58+
{
59+
name: '@netlify/blobs',
60+
type: 'module',
61+
exports: './index.js'
62+
},
63+
null,
64+
2
65+
)
66+
);
67+
fs.writeFileSync(
68+
path.join(pkgDir, 'index.js'),
69+
`const stores = globalThis.__tokenhostNetlifyBlobStores ?? (globalThis.__tokenhostNetlifyBlobStores = new Map());
70+
71+
export function getStore(name) {
72+
if (!stores.has(name)) stores.set(name, new Map());
73+
const store = stores.get(name);
74+
return {
75+
async setJSON(key, value) {
76+
store.set(key, JSON.parse(JSON.stringify(value)));
77+
},
78+
async get(key, options = {}) {
79+
if (!store.has(key)) return null;
80+
const value = store.get(key);
81+
return options.type === 'json' ? JSON.parse(JSON.stringify(value)) : value;
82+
},
83+
async delete(key) {
84+
store.delete(key);
85+
}
86+
};
87+
}
88+
`
89+
);
90+
}
91+
92+
async function readJsonResponse(response) {
93+
const text = await response.text();
94+
return text ? JSON.parse(text) : null;
95+
}
96+
97+
describe('Generated Netlify upload functions', function () {
98+
it('processes an async upload job end to end with generated functions', async function () {
99+
this.timeout(180000);
100+
101+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'th-netlify-functions-'));
102+
const schemaPath = path.join(dir, 'schema.json');
103+
const outDir = path.join(dir, 'out');
104+
writeJson(schemaPath, minimalSchema());
105+
106+
const build = runTh(['build', schemaPath, '--out', outDir], process.cwd());
107+
expect(build.status, build.stderr || build.stdout).to.equal(0);
108+
109+
installNetlifyBlobsStub(outDir);
110+
111+
const startPath = pathToFileURL(path.join(outDir, 'netlify', 'functions', 'tokenhost-upload-start.mjs')).href;
112+
const statusPath = pathToFileURL(path.join(outDir, 'netlify', 'functions', 'tokenhost-upload-status.mjs')).href;
113+
const workerPath = pathToFileURL(path.join(outDir, 'netlify', 'functions', 'tokenhost-upload-worker-background.mjs')).href;
114+
115+
const previousFetch = globalThis.fetch;
116+
const previousPrivateKey = process.env.TH_UPLOAD_FOC_PRIVATE_KEY;
117+
const previousCommand = process.env.TH_UPLOAD_FOC_COMMAND;
118+
119+
process.env.TH_UPLOAD_FOC_PRIVATE_KEY = '0x1234';
120+
process.env.TH_UPLOAD_FOC_COMMAND = `node ${path.resolve('test/fixtures/fake-foc-cli.mjs')}`;
121+
122+
try {
123+
const workerModule = await import(workerPath);
124+
globalThis.fetch = async (url, init = {}) => {
125+
if (String(url).includes('/.netlify/functions/tokenhost-upload-worker-background')) {
126+
return await workerModule.default(
127+
new Request(String(url), {
128+
method: init.method || 'POST',
129+
headers: init.headers,
130+
body: init.body
131+
})
132+
);
133+
}
134+
throw new Error(`Unexpected fetch in test: ${String(url)}`);
135+
};
136+
137+
const startModule = await import(startPath);
138+
const statusModule = await import(statusPath);
139+
140+
const health = await startModule.default(new Request('https://example.net/__tokenhost/upload', { method: 'GET' }));
141+
const healthJson = await readJsonResponse(health);
142+
expect(health.status).to.equal(200);
143+
expect(healthJson?.enabled).to.equal(true);
144+
expect(healthJson?.provider).to.equal('filecoin_onchain_cloud');
145+
146+
const payload = Buffer.from('tokenhost-netlify-upload-test', 'utf-8');
147+
const startRes = await startModule.default(
148+
new Request('https://example.net/__tokenhost/upload', {
149+
method: 'POST',
150+
headers: {
151+
'content-type': 'image/png',
152+
'x-tokenhost-upload-filename': 'test.png',
153+
'x-tokenhost-upload-size': String(payload.length)
154+
},
155+
body: payload
156+
})
157+
);
158+
const startJson = await readJsonResponse(startRes);
159+
160+
expect(startRes.status).to.equal(202);
161+
expect(startJson?.ok).to.equal(true);
162+
expect(startJson?.pending).to.equal(true);
163+
expect(startJson?.jobId).to.be.a('string');
164+
165+
const pollRes = await statusModule.default(
166+
new Request(`https://example.net/__tokenhost/upload-status?jobId=${encodeURIComponent(startJson.jobId)}`, {
167+
method: 'GET'
168+
})
169+
);
170+
const pollJson = await readJsonResponse(pollRes);
171+
172+
expect(pollRes.status).to.equal(200);
173+
expect(pollJson?.ok).to.equal(true);
174+
expect(pollJson?.pending).to.equal(false);
175+
expect(pollJson?.upload?.url).to.equal('https://calibration.example.invalid/piece/bafkqaaaafakecidfornetlifyuploadtest');
176+
expect(pollJson?.upload?.cid).to.equal('bafkqaaaafakecidfornetlifyuploadtest');
177+
expect(pollJson?.upload?.provider).to.equal('filecoin_onchain_cloud');
178+
expect(pollJson?.upload?.runnerMode).to.equal('netlify-background');
179+
} finally {
180+
globalThis.fetch = previousFetch;
181+
if (previousPrivateKey === undefined) delete process.env.TH_UPLOAD_FOC_PRIVATE_KEY;
182+
else process.env.TH_UPLOAD_FOC_PRIVATE_KEY = previousPrivateKey;
183+
if (previousCommand === undefined) delete process.env.TH_UPLOAD_FOC_COMMAND;
184+
else process.env.TH_UPLOAD_FOC_COMMAND = previousCommand;
185+
}
186+
});
187+
});

0 commit comments

Comments
 (0)