Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/puter-js/src/modules/FileSystem/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import upload from "./operations/upload.js";
import read from "./operations/read.js";
import move from "./operations/move.js";
import write from "./operations/write.js";
import share from "./operations/share.js";
import sign from "./operations/sign.js";
import symlink from './operations/symlink.js';
// Why is this called deleteFSEntry instead of just delete? because delete is
Expand All @@ -34,6 +35,7 @@ export class PuterJSFileSystemModule extends AdvancedBase {
sign = sign;
symlink = symlink;
getReadURL = getReadURL;
share = share;

FSItem = FSItem

Expand Down
59 changes: 59 additions & 0 deletions src/puter-js/src/modules/FileSystem/operations/share.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';

const share = async function (targetPath, options = {}) {
// targetPath is required
if (!targetPath) {
throw new Error('No target path provided.');
}

// If targetPath is not provided or it's not starting with a slash, it means it's a relative path
// in that case, we need to prepend the app's root directory to it
targetPath = getAbsolutePathForApp(targetPath);

// Extract options
const recipients = options.recipients || [];
const access = options.access || 'read';

// Validate access level
if (!['read', 'write'].includes(access)) {
throw new Error('Invalid access level. Must be "read" or "write".');
}

// Validate recipients
if (!Array.isArray(recipients) || recipients.length === 0) {
throw new Error('Recipients must be a non-empty array.');
}

// Prepare the share request
const shareData = {
recipients: recipients,
shares: [
{
$: 'fs-share',
path: targetPath,
access: access,
}
]
};

// Make the API call to share the file
console.log(`api origin: ${puter.APIOrigin}`);
const response = await fetch(`${puter.APIOrigin}/share`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${puter.authToken}`
},
body: JSON.stringify(shareData)
});

if (!response.ok) {
const errorText = await response.text();
throw new Error(`Share failed: ${response.status} ${errorText}`);
}

const result = await response.json();
return result;
};

export default share;
32 changes: 30 additions & 2 deletions src/puter-js/src/modules/FileSystem/operations/write.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import path from "../../../lib/path.js"
import path from "../../../lib/path.js";
import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';

const write = async function (targetPath, data, options = {}) {
Expand Down Expand Up @@ -56,7 +56,35 @@ const write = async function (targetPath, data, options = {}) {
}

// perform upload
return this.upload(data, parent, options);
const result = await this.upload(data, parent, options);

if (options.share) {
try {
// Call the share API after successful write
let share_result = await this.share(result.path, {
recipients: options.share.recipients || [],
access: options.share.access || 'read'
});
console.log('share_result', share_result);

// Add share information to the result
result.share = {
status: share_result.status,
recipients: share_result.recipients || [],
shares: share_result.shares || []
};
} catch (error) {
console.error('Failed to share file after write:', error);
// Add error information to the result
result.share = {
status: 'error',
error: error.message,
recipients: []
};
}
}

return result;
}

export default write;
8 changes: 7 additions & 1 deletion tools/api-tester/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# API Tester

A test framework for testing the backend API of puter.
A test framework for testing the API of puter backend and puter-js client.

## Table of Contents

Expand Down Expand Up @@ -43,6 +43,12 @@ All commands below should be run from the root directory of puter.
node ./tools/api-tester/apitest.js --config=./tools/api-tester/config.yml
```

4. (experimental) Run tests against the puter-js client:

```bash
node ./tools/api-tester/apitest.js --config=./tools/api-tester/config.yml --client
```

### Shorthands

- Run unit tests only:
Expand Down
89 changes: 88 additions & 1 deletion tools/api-tester/apitest.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ const { parseArgs } = require('node:util');

const args = process.argv.slice(2);

let config, report, suiteName;
let config, report, suiteName, onlycase, bench, unit, stopOnFailure, id, puterjs;

try {
const parsed = parseArgs({
options: {
config: {
type: 'string',
default: './tools/api-tester/config.yml',
},
report: {
type: 'string',
Expand All @@ -24,6 +25,8 @@ try {
bench: { type: 'boolean' },
unit: { type: 'boolean' },
suite: { type: 'string' },
'stop-on-failure': { type: 'boolean' },
puterjs: { type: 'boolean' },
},
allowPositionals: true,
});
Expand All @@ -35,6 +38,8 @@ try {
bench,
unit,
suite: suiteName,
'stop-on-failure': stopOnFailure,
puterjs,
}, positionals: [id] } = parsed);

onlycase = onlycase !== undefined ? Number.parseInt(onlycase) : undefined;
Expand All @@ -47,6 +52,7 @@ try {
'\n' +
'Options:\n' +
' --config=<path> (required) Path to configuration file\n' +
' --puterjs (optional) Use puter-js puterjs\n' +
' --report=<path> (optional) Output file for full test results\n' +
' --suite=<name> (optional) Run only tests with matching suite name\n' +
''
Expand All @@ -58,6 +64,87 @@ const conf = YAML.parse(fs.readFileSync(config).toString());


const main = async () => {
if (puterjs) {
// const run = require('./puter_js/__entry__.js');

const context = {
mountpoint: {
path: '/',
}
};

const ts = new TestSDK(conf, context, {});
const registry = new TestRegistry(ts);

await require('./puter_js/__entry__.js')(registry);

await registry.run_all_tests();

// await run(conf);
ts.printTestResults();
ts.printBenchmarkResults();
process.exit(0);
return;
}

const unit_test_results = [];
const benchmark_results = [];
for (const mountpoint of conf.mountpoints) {
const { unit_test_results: results, benchmark_results: benchs } = await test({ mountpoint });
unit_test_results.push(...results);
benchmark_results.push(...benchs);
}

// hard-coded identifier for ci script
console.log("==================== nightly build results begin ====================")

// print unit test results
let tbl = {};
for ( const result of unit_test_results ) {
tbl[result.name + ' - ' + result.settings] = {
passed: result.caseCount - result.failCount,
failed: result.failCount,
total: result.caseCount,
'duration (s)': result.duration ? result.duration.toFixed(2) : 'N/A',
}
}
console.table(tbl);

// print benchmark results
if (benchmark_results.length > 0) {
tbl = {};
for ( const result of benchmark_results ) {
const fs_provider = result.fs_provider || 'unknown';
tbl[result.name + ' - ' + fs_provider] = {
'duration (s)': result.duration ? (result.duration / 1000).toFixed(2) : 'N/A',
}
}
console.table(tbl);

// print description of each benchmark since it's too long to fit in the table
const seen = new Set();
for ( const result of benchmark_results ) {
if ( seen.has(result.name) ) continue;
seen.add(result.name);

if ( result.description ) {
console.log(result.name + ': ' + result.description);
}
}
}

// hard-coded identifier for ci script
console.log("==================== nightly build results end ====================")
}

/**
* Run test using the given config, and return the test results
*
* @param {Object} options
* @param {Object} options.mountpoint
* @returns {Promise<Object>}
*/
async function test({ mountpoint }) {
const context = {
options: {
onlycase,
Expand Down
27 changes: 27 additions & 0 deletions tools/api-tester/puter_js/__entry__.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const load_puterjs = require('./load.cjs');

async function run(conf) {
const puter = await load_puterjs();
if (conf.token) {
puter.setAuthToken(conf.token);
} else {
throw new Error('No token found in config file. Please add a "token" field to your config.yaml');
}
return;
};

module.exports = async registry => {
const puter = await load_puterjs();
if (registry.t?.conf?.token) {
puter.setAuthToken(registry.t.conf.token);
} else {
throw new Error('No token found in config file. Please add a "token" field to your config.yaml');
}

registry.t.puter = puter;

console.log('__entry__.js');
require('./filesystem/__entry__.js')(registry);

// registry.add_test('filesystem', require('./filesystem/__entry__.js'));
};
4 changes: 4 additions & 0 deletions tools/api-tester/puter_js/filesystem/__entry__.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = registry => {
console.log('filesystem __entry__.js');
registry.add_test('write', require('./write.js'));
};
40 changes: 40 additions & 0 deletions tools/api-tester/puter_js/filesystem/write.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const chai = require('chai');
chai.use(require('chai-as-promised'))
const expect = chai.expect;

module.exports = {
name: 'write',
description: 'a collection of tests for writing to the filesystem',
do: async t => {
const puter = t.puter;

await t.case('demo (whoami)', async () => {
const result = await puter.auth.whoami();
expect(result.username).to.equal('admin');
});

await t.case('write and share', async () => {
let result = await puter.fs.write('~/test.txt', 'hello');
expect(result.name).to.equal('test.txt');

result = await puter.fs.share('~/test.txt', {
recipients: ['tom', 'jerry'],
access: 'read',
withPermissions: true,
});
console.log('result', result);
expect(result.recipients.length).to.equal(2);
});

await t.case('write with share args', async () => {
let result = await puter.fs.write('~/test.txt', 'hello', {
share: {
recipients: ['tom', 'jerry'],
access: 'read',
},
withPermissions: true,
});
expect(result.share.recipients.length).to.equal(2);
});
}
}
13 changes: 13 additions & 0 deletions tools/api-tester/puter_js/load.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const vm = require('vm');

async function load_puterjs() {
const goodContext = {}
Object.getOwnPropertyNames(globalThis).forEach(name => { try { goodContext[name] = globalThis[name]; } catch { } })
goodContext.globalThis = goodContext
const code = await fetch("http://puter.localhost:4100/puter.js/v2").then(res => res.text());
const context = vm.createContext(goodContext);
const result = vm.runInNewContext(code, context);
return goodContext.puter;
}

module.exports = load_puterjs;