move: api-tester to monorepo

This commit is contained in:
KernelDeimos 2025-01-09 15:51:50 -05:00
parent 44ad3c5781
commit 405d9b35aa
37 changed files with 2543 additions and 0 deletions

1
tools/api-tester/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
config.yml

View File

@ -0,0 +1,9 @@
## It takes 3 steps to run the tests :)
1. run `npm install`
2. copy `example_config.yml` and add the correct values
3. run `node apitest.js --config=your_config_file.yml`
## Here's what it looks like when it's working
![image](https://github.com/HeyPuter/puter-api-test/assets/7225168/115aca70-02ea-4ce1-9d5c-1568feb1f851)

112
tools/api-tester/apitest.js Normal file
View File

@ -0,0 +1,112 @@
const YAML = require('yaml');
const TestSDK = require('./lib/TestSDK');
const log_error = require('./lib/log_error');
const TestRegistry = require('./lib/TestRegistry');
const fs = require('node:fs');
const { parseArgs } = require('node:util');
const args = process.argv.slice(2);
let config, report;
try {
({ values: {
config,
report,
bench,
unit,
}, positionals: [id] } = parseArgs({
options: {
config: {
type: 'string',
},
report: {
type: 'string',
},
bench: { type: 'boolean' },
unit: { type: 'boolean' },
},
allowPositionals: true,
}));
} catch (e) {
if ( args.length < 1 ) {
console.error(
'Usage: apitest [OPTIONS]\n' +
'\n' +
'Options:\n' +
' --config=<path> (required) Path to configuration file\n' +
' --report=<path> (optional) Output file for full test results\n' +
''
);
process.exit(1);
}
}
const conf = YAML.parse(fs.readFileSync(config).toString());
const main = async () => {
const ts = new TestSDK(conf);
try {
await ts.delete('api_test', { recursive: true });
} catch (e) {
}
await ts.mkdir('api_test', { overwrite: true });
ts.cd('api_test');
const registry = new TestRegistry(ts);
registry.add_test_sdk('puter-rest.v1', require('./test_sdks/puter-rest')({
config: conf,
}));
require('./tests/__entry__.js')(registry);
require('./benches/simple.js')(registry);
if ( id ) {
if ( unit ) {
await registry.run_test(id);
} else if ( bench ) {
await registry.run_bench(id);
} else {
await registry.run(id);
}
return;
}
if ( unit ) {
await registry.run_all_tests();
} else if ( bench ) {
await registry.run_all_benches();
} else {
await registry.run_all();
}
// await ts.runTestPackage(require('./tests/write_cart'));
// await ts.runTestPackage(require('./tests/move_cart'));
// await ts.runTestPackage(require('./tests/copy_cart'));
// await ts.runTestPackage(require('./tests/write_and_read'));
// await ts.runTestPackage(require('./tests/move'));
// await ts.runTestPackage(require('./tests/stat'));
// await ts.runTestPackage(require('./tests/readdir'));
// await ts.runTestPackage(require('./tests/mkdir'));
// await ts.runTestPackage(require('./tests/batch'));
// await ts.runTestPackage(require('./tests/delete'));
const all = unit && bench;
if ( all || unit ) ts.printTestResults();
if ( all || bench ) ts.printBenchmarkResults();
}
const main_e = async () => {
try {
await main();
} catch (e) {
log_error(e);
}
}
main_e();

View File

@ -0,0 +1,121 @@
const log_error = require("../lib/log_error");
module.exports = registry => {
registry.add_bench('write.tiny', {
name: 'write 30 tiny files',
do: async t => {
for ( let i=0 ; i < 30 ; i++ ) {
await t.write(`tiny_${i}.txt`, 'example\n', { overwrite: true });
}
}
});
registry.add_bench('batch.mkdir-and-write', {
name: 'make directories and write',
do: async t => {
const batch = [];
for ( let i=0 ; i < 30 ; i++ ) {
batch.push({
op: 'mkdir',
path: t.resolve(`dir_${i}`),
});
batch.push({
op: 'write',
path: t.resolve(`tiny_${i}.txt`),
});
}
await t.batch('batch', batch, Array(30).fill('example\n'));
}
});
registry.add_bench('batch.mkdir-deps.1', {
name: 'make directories and write',
do: async t => {
const batch = [];
const blobs = [];
for ( let j=0 ; j < 3 ; j++ ) {
batch.push({
op: 'mkdir',
path: t.resolve('dir_root'),
as: 'root',
})
for ( let i=0 ; i < 10 ; i++ ) {
batch.push({
op: 'write',
path: `$root/test_${i}.txt`
});
blobs.push('example\n');
}
}
await t.batch('batch', batch, blobs);
}
});
// TODO: write explicit test for multiple directories with the same name
// in a batch so that batch can eventually resolve this situation and not
// do something incredibly silly.
registry.add_bench('batch.mkdir-deps.2', {
name: 'make directories and write',
do: async t => {
const batch = [];
const blobs = [];
for ( let j=0 ; j < 3 ; j++ ) {
batch.push({
op: 'mkdir',
path: t.resolve(`dir_${j}`),
as: `dir_${j}`,
})
for ( let k=0 ; k < 3 ; k++ ) {
batch.push({
op: 'mkdir',
parent: `$dir_${j}`,
path: `subdir_${k}`,
as: `subdir_${j}-${k}`,
})
for ( let i=0 ; i < 5 ; i++ ) {
batch.push({
op: 'write',
path: `$subdir_${j}-${k}/test_${i}.txt`
});
blobs.push('example\n');
}
}
}
try {
const response = await t.batch('batch', batch, blobs);
console.log('response?', response);
} catch (e) {
log_error(e);
}
}
});
registry.add_bench('write.batch.tiny', {
name: 'Write 30 tiny files in a batch',
do: async t => {
const batch = [];
for ( let i=0 ; i < 30 ; i++ ) {
batch.push({
op: 'write',
path: t.resolve(`tiny_${i}.txt`),
});
}
await t.batch('batch', batch, Array(30).fill('example\n'));
}
});
// const fiftyMB = Array(50 * 1024 * 1024).map(() =>
// String.fromCharCode(
// Math.floor(Math.random() * 26) + 97
// ));
// registry.add_bench('files.mb50', {
// name: 'write 10 50MB files',
// do: async t => {
// for ( let i=0 ; i < 10 ; i++ ) {
// await t.write(`mb50_${i}.txt`, 'example\n', { overwrite: true });
// }
// }
// });
};

View File

@ -0,0 +1,16 @@
const CoverageModel = require("../lib/CoverageModel");
module.exports = new CoverageModel({
subject: ['file', 'directory-full', 'directory-empty'],
source: {
format: ['path', 'uid'],
},
destination: {
format: ['path', 'uid'],
},
name: ['default', 'specified'],
conditions: {
destinationIsFile: []
},
overwrite: [false, 'overwrite', 'dedupe_name'],
});

View File

@ -0,0 +1,15 @@
const CoverageModel = require("../lib/CoverageModel");
module.exports = new CoverageModel({
source: {
format: ['path', 'uid'],
},
destination: {
format: ['path', 'uid'],
},
name: ['default', 'specified'],
conditions: {
destinationIsFile: []
},
overwrite: [false, 'overwrite', 'dedupe_name']
});

View File

@ -0,0 +1,16 @@
const CoverageModel = require("../lib/CoverageModel");
// ?? What's a coverage model ??
//
// See doc/cartesian.md
module.exports = new CoverageModel({
path: {
format: ['path', 'uid'],
},
name: ['default', 'specified'],
conditions: {
destinationIsFile: []
},
overwrite: [false, 'overwrite', 'dedupe_name'],
});

View File

@ -0,0 +1,83 @@
# Cartesian Tests
A cartesian test is a test the tries every combination of possible
inputs based on some model. It's called this because the set of
possible states is mathematically the cartesian product of the
list of sets of options.
## Coverage Model
A coverage model is what defines all the variables and their
possible value. The coverage model implies the set of all
possible states (cartesian product).
The following is an example of a coverage model for testing
the `/write` API method for Puter's filesystem:
```javascript
module.exports = new CoverageModel({
path: {
format: ['path', 'uid'],
},
name: ['default', 'specified'],
conditions: {
destinationIsFile: []
},
overwrite: [],
});
```
The object will first be flattened. `format` inside `path` will
become a single key: `path.format`,
just as `{ a: { x: 1, y: 2 }, b: { z: 3 } }`
becomes `{ "a.x": 1, "a.y": 2, "b.z": 3 }`
Then, each possible state will be generated to use in tests.
For example, this is one arbitrary state:
```json
{
"path.format": "path",
"name": "specified",
"conditions.destinationIsFile": true,
"overwrite": false
}
```
Wherever an empty list is specified for the list of possible values,
it will be assumed to be `[false, true]`.
## Finding the Culprit
When a cartesian test fails, if you know the _index_ of the test which
failed you can determine what the state was just by looking at the
coverage model.
For example, if tests are failing at indices `1` and `5`
(starting from `0`, of course) for the `/write` example above,
the failures are likely related and occur when the default
filename is used and the destination (`path`) parameter points
to an existing file.
```
destination is file: 0 1 0 1 0 1 0 1
name is the default: 0 0 1 1 0 0 1 1
test results: P F P P P F P P
```
### Interesting note about the anme
I didn't know what this type of test was called at first. I simply knew
I wanted to try all the combinations of possible inputs, and I knew what
the algorithm to do this looked like. I then asked Chat GPT the following
question:
> What do you call the act of choosing one item from each set in a list of sets?
which it answered with:
> The act of choosing one item from each set in a list of sets is typically called the Cartesian Product.
Then after a bit of searching, it turns out neither Chat GPT nor I are the
first to use this term to describe the same thing in automated testing for
software.

View File

@ -0,0 +1,3 @@
url: http://puter.localhost:4001/
username: lavender_hat_6100
token: ---

View File

@ -0,0 +1,11 @@
module.exports = class Assert {
equal (expected, actual) {
this.assert(expected === actual);
}
assert (b) {
if ( ! b ) {
throw new Error('assertion failed');
}
}
}

View File

@ -0,0 +1,71 @@
const cartesianProduct = (obj) => {
// Get array of keys
let keys = Object.keys(obj);
// Generate the Cartesian Product
return keys.reduce((acc, key) => {
let appendArrays = Array.isArray(obj[key]) ? obj[key] : [obj[key]];
let newAcc = [];
acc.forEach(arr => {
appendArrays.forEach(item => {
newAcc.push([...arr, item]);
});
});
return newAcc;
}, [[]]); // start with the "empty product"
}
let obj = {
a: [1, 2],
b: ["a", "b"]
};
console.log(cartesianProduct(obj));
module.exports = class CoverageModel {
constructor (spec) {
const flat = {};
const flatten = (object, prefix) => {
for ( const k in object ) {
let targetKey = k;
if ( prefix ) {
targetKey = prefix + '.' + k;
}
let type = typeof object[k];
if ( Array.isArray(object[k]) ) type = 'array';
if ( type === 'object' ) {
flatten(object[k], targetKey);
continue;
}
if ( object[k].length == 0 ) {
object[k] = [false, true];
}
flat[targetKey] = object[k];
}
};
flatten(spec);
this.flat = flat;
const states = cartesianProduct(flat).map(
values => {
const o = {};
const keys = Object.keys(flat);
for ( let i=0 ; i < keys.length ; i++ ) {
o[keys[i]] = values[i];
}
return o;
}
);
this.states = states;
this.covered = Array(this.states.length).fill(false);
}
}

View File

@ -0,0 +1,3 @@
module.exports = class ReportGenerator {
//
}

View File

@ -0,0 +1,27 @@
module.exports = class TestFactory {
static cartesian (
name,
coverageModel,
{ each, init }
) {
const do_ = async t => {
const states = coverageModel.states;
if ( init ) await init(t);
for ( let i=0 ; i < states.length ; i++ ) {
const state = states[i];
await t.case(`case ${i}`, async () => {
console.log('state', state);
await each(t, state, i);
})
}
};
return {
name,
do: do_,
};
}
}

View File

@ -0,0 +1,68 @@
module.exports = class TestRegistry {
constructor (t) {
this.t = t;
this.sdks = {};
this.tests = {};
this.benches = {};
}
add_test_sdk (id, instance) {
this.t.sdks[id] = instance;
}
add_test (id, testDefinition) {
this.tests[id] = testDefinition;
}
add_bench (id, benchDefinition) {
this.benches[id] = benchDefinition;
}
async run_all_tests () {
for ( const id in this.tests ) {
const testDefinition = this.tests[id];
await this.t.runTestPackage(testDefinition);
}
}
// copilot was able to write everything below this line
// and I think that's pretty cool
async run_all_benches () {
for ( const id in this.benches ) {
const benchDefinition = this.benches[id];
await this.t.runBenchmark(benchDefinition);
}
}
async run_all () {
await this.run_all_tests();
await this.run_all_benches();
}
async run_test (id) {
const testDefinition = this.tests[id];
if ( ! testDefinition ) {
throw new Error(`Test not found: ${id}`);
}
await this.t.runTestPackage(testDefinition);
}
async run_bench (id) {
const benchDefinition = this.benches[id];
if ( ! benchDefinition ) {
throw new Error(`Bench not found: ${id}`);
}
await this.t.runBenchmark(benchDefinition);
}
async run (id) {
if ( this.tests[id] ) {
await this.run_test(id);
} else if ( this.benches[id] ) {
await this.run_bench(id);
} else {
throw new Error(`Test or bench not found: ${id}`);
}
}
}

View File

@ -0,0 +1,378 @@
const axios = require('axios');
const YAML = require('yaml');
const fs = require('node:fs');
const path_ = require('node:path');
const url = require('node:url');
const https = require('node:https');
const Assert = require('./Assert');
const log_error = require('./log_error');
module.exports = class TestSDK {
constructor (conf) {
this.conf = conf;
this.cwd = `/${conf.username}`;
this.httpsAgent = new https.Agent({
rejectUnauthorized: false
})
const url_origin = new url.URL(conf.url).origin;
this.headers_ = {
'Origin': url_origin,
'Authorization': `Bearer ${conf.token}`
};
this.installAPIMethodShorthands_();
this.assert = new Assert();
this.sdks = {};
this.results = [];
this.failCount = 0;
this.caseCount = 0;
this.nameStack = [];
this.packageResults = [];
this.benchmarkResults = [];
}
async get_sdk (name) {
return await this.sdks[name].create();
}
// === test related methods ===
async runTestPackage (testDefinition) {
this.nameStack.push(testDefinition.name);
this.packageResults.push({
name: testDefinition.name,
failCount: 0,
caseCount: 0,
});
const imported = {};
for ( const key of Object.keys(testDefinition.import ?? {}) ) {
imported[key] = this.sdks[key];
}
await testDefinition.do(this, imported);
this.nameStack.pop();
}
async runBenchmark (benchDefinition) {
const strid = '' +
'\x1B[35;1m[bench]\x1B[0m' +
this.nameStack.join(` \x1B[36;1m->\x1B[0m `);
process.stdout.write(strid + ' ... \n');
this.nameStack.push(benchDefinition.name);
let results;
this.benchmarkResults.push(results = {
name: benchDefinition.name,
start: Date.now(),
});
try {
await benchDefinition.do(this);
} catch (e) {
results.error = e;
} finally {
results.end = Date.now();
const dur = results.end - results.start;
process.stdout.write(`...\x1B[32;1m[${dur}]\x1B[0m\n`);
}
}
recordResult (result) {
const pkg = this.packageResults[this.packageResults.length - 1];
this.caseCount++;
pkg.caseCount++;
if ( ! result.success ) {
this.failCount++;
pkg.failCount++;
}
this.results.push(result);
}
async case (id, fn) {
this.nameStack.push(id);
const tabs = Array(this.nameStack.length - 2).fill(' ').join('');
const strid = tabs + this.nameStack.join(` \x1B[36;1m->\x1B[0m `);
process.stdout.write(strid + ' ... \n');
try {
await fn();
} catch (e) {
process.stdout.write(`${tabs}...\x1B[31;1m[FAIL]\x1B[0m\n`);
this.recordResult({
strid,
e,
success: false,
});
log_error(e);
return;
} finally {
this.nameStack.pop();
}
process.stdout.write(`${tabs}...\x1B[32;1m[PASS]\x1B[0m\n`);
this.recordResult({
strid,
success: true
});
}
quirk (msg) {
console.log(`\x1B[33;1mignoring known quirk: ${msg}\x1B[0m`);
}
// === information display methods ===
printTestResults () {
console.log(`\n\x1B[33;1m=== Test Results ===\x1B[0m`);
let tbl = {};
for ( const pkg of this.packageResults ) {
tbl[pkg.name] = {
passed: pkg.caseCount - pkg.failCount,
failed: pkg.failCount,
total: pkg.caseCount,
}
}
console.table(tbl);
process.stdout.write(`\x1B[36;1m${this.caseCount} tests were run\x1B[0m - `);
if ( this.failCount > 0 ) {
console.log(`\x1B[31;1m✖ ${this.failCount} tests failed!\x1B[0m`);
} else {
console.log(`\x1B[32;1m✔ All tests passed!\x1B[0m`)
}
}
printBenchmarkResults () {
console.log(`\n\x1B[33;1m=== Benchmark Results ===\x1B[0m`);
let tbl = {};
for ( const bench of this.benchmarkResults ) {
tbl[bench.name] = {
time: bench.end - bench.start,
error: bench.error ? bench.error.message : '',
}
}
console.table(tbl);
}
// === path related methods ===
cd (path) {
this.cwd = path_.posix.join(this.cwd, path);
}
resolve (path) {
if ( path.startsWith('$') ) return path;
if ( path.startsWith('/') ) return path;
return path_.posix.join(this.cwd, path);
}
// === API calls ===
installAPIMethodShorthands_ () {
const p = this.resolve.bind(this);
this.read = async path => {
const res = await this.get('read', { path: p(path) });
return res.data;
}
this.mkdir = async (path, opts) => {
const res = await this.post('mkdir', {
path: p(path),
...(opts ?? {})
});
return res.data;
};
this.write = async (path, bin, params) => {
path = p(path);
params = params ?? {};
let mime = 'text/plain';
if ( params.hasOwnProperty('mime') ) {
mime = params.mime;
delete params.mime;
}
let name = path_.posix.basename(path);
path = path_.posix.dirname(path);
params.path = path;
const res = await this.upload('write', name, mime, bin, params);
return res.data;
}
this.stat = async (path, params) => {
path = p(path);
const res = await this.post('stat', { ...params, path });
return res.data;
}
this.statu = async (uid, params) => {
const res = await this.post('stat', { ...params, uid });
return res.data;
}
this.readdir = async (path, params) => {
path = p(path);
const res = await this.post('readdir', {
...params,
path
})
return res.data;
}
this.delete = async (path, params) => {
path = p(path);
const res = await this.post('delete', {
...params,
paths: [path]
});
return res.data;
}
this.move = async (src, dst, params = {}) => {
src = p(src);
dst = p(dst);
const destination = path_.dirname(dst);
const source = src;
const new_name = path_.basename(dst);
console.log('move', { destination, source, new_name });
const res = await this.post('move', {
...params,
destination,
source,
new_name,
});
return res.data;
}
}
getURL (...path) {
const apiURL = new url.URL(this.conf.url);
apiURL.pathname = path_.posix.join(
apiURL.pathname,
...path
);
return apiURL.href;
};
// === HTTP methods ===
get (ep, params) {
return axios.request({
httpsAgent: this.httpsAgent,
method: 'get',
url: this.getURL(ep),
params,
headers: {
...this.headers_
}
});
}
post (ep, params) {
return axios.request({
httpsAgent: this.httpsAgent,
method: 'post',
url: this.getURL(ep),
data: params,
headers: {
...this.headers_,
'Content-Type': 'application/json',
}
})
}
upload (ep, name, mime, bin, params) {
const adapt_file = (bin, mime) => {
if ( typeof bin === 'string' ) {
return new Blob([bin], { type: mime });
}
return bin;
};
const fd = new FormData();
for ( const k in params ) fd.append(k, params[k]);
const blob = adapt_file(bin, mime);
fd.append('size', blob.size);
fd.append('file', adapt_file(bin, mime), name)
return axios.request({
httpsAgent: this.httpsAgent,
method: 'post',
url: this.getURL(ep),
data: fd,
headers: {
...this.headers_,
'Content-Type': 'multipart/form-data'
},
});
}
async batch (ep, ops, bins) {
const adapt_file = (bin, mime) => {
if ( typeof bin === 'string' ) {
return new Blob([bin], { type: mime });
}
return bin;
};
const fd = new FormData();
fd.append('original_client_socket_id', '');
fd.append('socket_id', '');
fd.append('operation_id', '');
let fileI = 0;
for ( let i=0 ; i < ops.length ; i++ ) {
const op = ops[i];
fd.append('operation', JSON.stringify(op));
}
const files = [];
for ( let i=0 ; i < ops.length ; i++ ) {
const op = ops[i];
if ( op.op === 'mkdir' ) continue;
if ( op.op === 'mktree' ) continue;
let mime = op.mime ?? 'text/plain';
const file = adapt_file(bins[fileI++], mime);
fd.append('fileinfo', JSON.stringify({
size: file.size,
name: op.name,
mime,
}));
files.push({
op, file,
})
delete op.name;
}
for ( const file of files ) {
const { op, file: blob } = file;
fd.append('file', blob, op.name);
}
const res = await axios.request({
httpsAgent: this.httpsAgent,
method: 'post',
url: this.getURL(ep),
data: fd,
headers: {
...this.headers_,
'Content-Type': 'multipart/form-data'
},
});
return res.data.results;
}
batch_json (ep, ops, bins) {
return axios.request({
httpsAgent: this.httpsAgent,
method: 'post',
url: this.getURL(ep),
data: ops,
headers: {
...this.headers_,
'Content-Type': 'application/json',
},
});
}
}

View File

@ -0,0 +1,35 @@
const log_http_error = e => {
console.log('\x1B[31;1m' + e.message + '\x1B[0m');
console.log('HTTP Method: ', e.config.method.toUpperCase());
console.log('URL: ', e.config.url);
if (e.config.params) {
console.log('URL Parameters: ', e.config.params);
}
if (e.config.method.toLowerCase() === 'post' && e.config.data) {
console.log('Post body: ', e.config.data);
}
console.log('Request Headers: ', JSON.stringify(e.config.headers, null, 2));
if (e.response) {
console.log('Response Status: ', e.response.status);
console.log('Response Headers: ', JSON.stringify(e.response.headers, null, 2));
console.log('Response body: ', e.response.data);
}
console.log('\x1B[31;1m' + e.message + '\x1B[0m');
};
const log_error = e => {
if ( e.request ) {
log_http_error(e);
return;
}
console.error(e);
};
module.exports = log_error;

View File

@ -0,0 +1,5 @@
module.exports = async function sleep (ms) {
await new Promise(rslv => {
setTimeout(rslv, ms);
})
}

204
tools/api-tester/package-lock.json generated Normal file
View File

@ -0,0 +1,204 @@
{
"name": "puter-api-test",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "puter-api-test",
"version": "0.1.0",
"license": "UNLICENSED",
"dependencies": {
"axios": "^1.4.0",
"chai": "^4.3.7",
"chai-as-promised": "^7.1.1",
"yaml": "^2.3.1"
}
},
"node_modules/assertion-error": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
"integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
"engines": {
"node": "*"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/chai": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz",
"integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==",
"dependencies": {
"assertion-error": "^1.1.0",
"check-error": "^1.0.2",
"deep-eql": "^4.1.2",
"get-func-name": "^2.0.0",
"loupe": "^2.3.1",
"pathval": "^1.1.1",
"type-detect": "^4.0.5"
},
"engines": {
"node": ">=4"
}
},
"node_modules/chai-as-promised": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz",
"integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==",
"dependencies": {
"check-error": "^1.0.2"
},
"peerDependencies": {
"chai": ">= 2.1.2 < 5"
}
},
"node_modules/check-error": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
"integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==",
"engines": {
"node": "*"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/deep-eql": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
"integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==",
"dependencies": {
"type-detect": "^4.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/get-func-name": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
"integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==",
"engines": {
"node": "*"
}
},
"node_modules/loupe": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz",
"integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==",
"dependencies": {
"get-func-name": "^2.0.0"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/pathval": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
"integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
"engines": {
"node": "*"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
"engines": {
"node": ">=4"
}
},
"node_modules/yaml": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz",
"integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==",
"engines": {
"node": ">= 14"
}
}
}
}

View File

@ -0,0 +1,17 @@
{
"name": "@heyputer/puter-api-test",
"version": "0.1.0",
"description": "",
"main": "apitest.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Puter Technologies Inc.",
"license": "UNLICENSED",
"dependencies": {
"axios": "^1.4.0",
"chai": "^4.3.7",
"chai-as-promised": "^7.1.1",
"yaml": "^2.3.1"
}
}

View File

@ -0,0 +1,23 @@
const axios = require('axios');
class PuterRestTestSDK {
constructor (config) {
this.config = config;
}
async create() {
const conf = this.config;
const axiosInstance = axios.create({
httpsAgent: new https.Agent({
rejectUnauthorized: false,
}),
baseURL: conf.url,
headers: {
'Authorization': `Bearer ${conf.token}`, // common headers
//... other headers
}
});
return axiosInstance;
}
}
module.exports = ({ config }) => new PuterRestTestSDK(config);

View File

@ -0,0 +1,13 @@
module.exports = registry => {
registry.add_test('write_cart', require('./write_cart'));
registry.add_test('move_cart', require('./move_cart'));
registry.add_test('copy_cart', require('./copy_cart'));
registry.add_test('write_and_read', require('./write_and_read'));
registry.add_test('move', require('./move'));
registry.add_test('stat', require('./stat'));
registry.add_test('readdir', require('./readdir'));
registry.add_test('mkdir', require('./mkdir'));
registry.add_test('batch', require('./batch'));
registry.add_test('delete', require('./delete'));
registry.add_test('telem_write', require('./telem_write'));
};

View File

@ -0,0 +1,226 @@
const { expect } = require("chai");
const { verify_fsentry } = require("./fsentry");
module.exports = {
name: 'batch',
do: async t => {
let results;
/*
await t.case('batch write', async () => {
results = null;
results = await t.batch('/batch/write', [
{
path: t.resolve('test_1.txt'),
overwrite: true,
},
{
path: t.resolve('test_3.txt'),
}
], [
'first file',
'second file',
])
console.log('results?', results)
expect(results.length).equal(2);
for ( const result of results ) {
await verify_fsentry(t, result)
}
});
t.case('batch mkdir', async () => {
results = null;
results = await t.batch_json('batch/mkdir', [
{
path: t.resolve('test_1_dir'),
overwrite: true,
},
{
path: t.resolve('test_3_dir'),
}
])
expect(results.length).equal(2);
for ( const result of results ) {
await verify_fsentry(t, result)
}
});
*/
await t.case('3-3 nested directores', async () => {
results = null;
results = await t.batch('batch', [
{
op: 'mktree',
parent: t.cwd,
tree: [
'a/b/c',
[
'a/b/c',
['a/b/c'],
['d/e/f'],
['g/h/i'],
['j/k/l'],
],
[
'd/e/f',
['a/b/c'],
['d/e/f'],
['g/h/i'],
['j/k/l'],
],
[
'g/h/i',
['a/b/c'],
['d/e/f'],
['g/h/i'],
['j/k/l'],
],
[
'j/k/l',
['a/b/c'],
['d/e/f'],
['g/h/i'],
['j/k/l'],
],
]
}
], []);
});
await t.case('path reference resolution', async () => {
results = null;
results = await t.batch('batch', [
{
op: 'mkdir',
as: 'dest_1',
path: t.resolve('q/w'),
create_missing_parents: true,
},
{
op: 'mkdir',
as: 'dest_2',
path: t.resolve('q/w'), // "q/w (1)"
dedupe_name: true,
create_missing_parents: true,
},
{
op: 'write',
path: t.resolve('$dest_1/file_1.txt'),
},
{
op: 'write',
path: t.resolve('$dest_2/file_2.txt'),
},
], [
'file 1 contents',
'file 2 contents',
]);
console.log('res?', results)
expect(results.length).equal(4);
expect(results[0].name).equal('w');
expect(results[1].name).equal('w (1)');
expect(results[2].path).equal(t.resolve('q/w/file_1.txt'));
expect(results[3].path).equal(t.resolve('q/w (1)/file_2.txt'));
});
await t.case('batch mkdir and write', async () => {
results = null;
results = await t.batch('batch', [
{
op: 'mkdir',
path: t.resolve('test_x_1_dir'),
overwrite: true,
},
{
op: 'write',
path: t.resolve('test_x_1.txt'),
},
{
op: 'mkdir',
path: t.resolve('test_x_2_dir'),
},
{
op: 'write',
path: t.resolve('test_x_2.txt'),
}
], [
'first file',
'second file',
]);
console.log('res?', results)
expect(results.length).equal(4);
for ( const result of results ) {
// await verify_fsentry(t, result)
}
});
await t.case('path reference resolution (without dedupe)', async () => {
results = null;
results = await t.batch('batch', [
{
op: 'mkdir',
as: 'dest_1',
path: t.resolve('q/w'),
create_missing_parents: true,
},
{
op: 'write',
path: t.resolve('$dest_1/file_1.txt'),
},
], [
'file 1 contents',
]);
console.log('res?', results)
expect(results.length).equal(2);
expect(results[0].name).equal('w');
expect(results[1].path).equal(t.resolve('q/w/file_1.txt'));
});
// Test for path reference resolution
await t.case('path reference resolution', async () => {
results = null;
results = await t.batch('batch', [
{
op: 'mkdir',
as: 'dest_1',
path: t.resolve('q/w'),
create_missing_parents: true,
},
{
op: 'mkdir',
as: 'dest_2',
path: t.resolve('q/w'), // "q/w (1)"
dedupe_name: true,
create_missing_parents: true,
},
{
op: 'write',
path: t.resolve('$dest_1/file_1.txt'),
},
{
op: 'write',
path: t.resolve('$dest_2/file_2.txt'),
},
], [
'file 1 contents',
'file 2 contents',
]);
console.log('res?', results)
expect(results.length).equal(4);
expect(results[0].name).equal('w');
expect(results[1].name).equal('w (1)');
expect(results[2].path).equal(t.resolve('q/w/file_1.txt'));
expect(results[3].path).equal(t.resolve('q/w (1)/file_2.txt'));
});
// Test for a single write
await t.case('single write', async () => {
results = null;
results = await t.batch('batch', [
{
op: 'write',
path: t.resolve('just_one_file.txt'),
},
], [
'file 1 contents',
]);
console.log('res?', results)
});
}
};

View File

@ -0,0 +1,131 @@
const { default: axios } = require("axios");
const { expect } = require("chai");
const copy = require("../coverage_models/copy");
const TestFactory = require("../lib/TestFactory");
/*
CARTESIAN TEST FOR /copy
NOTE: This test is very similar to the test for /move,
but DRYing it would add too much complexity.
It is best to have both tests open side-by-side
when making changes to either one.
*/
const PREFIX = 'copy_cart_';
module.exports = TestFactory.cartesian('Cartesian Test for /copy', copy, {
each: async (t, state, i) => {
// 1. Common setup for all states
await t.mkdir(`${PREFIX}${i}`);
const dir = `/${t.cwd}/${PREFIX}${i}`;
await t.mkdir(`${PREFIX}${i}/a`);
let pathOfThingToCopy = '';
if ( state.subject === 'file' ) {
await t.write(`${PREFIX}${i}/a/a_file.txt`, 'file a contents\n');
pathOfThingToCopy = `/a/a_file.txt`;
} else {
await t.mkdir(`${PREFIX}${i}/a/a_directory`);
pathOfThingToCopy = `/a/a_directory`;
// for test purposes, a "full" directory has each of three classes:
// - a file
// - an empty directory
// - a directory with a file in it
if ( state.subject === 'directory-full' ) {
// add a file
await t.write(`${PREFIX}${i}/a/a_directory/a_file.txt`, 'file a contents\n');
// add a directory with a file inside of it
await t.mkdir(`${PREFIX}${i}/a/a_directory/b_directory`);
await t.write(`${PREFIX}${i}/a/a_directory/b_directory/b_file.txt`, 'file a contents\n');
// add an empty directory
await t.mkdir(`${PREFIX}${i}/a/a_directory/c_directory`);
}
}
// 2. Situation setup for this state
if ( state['conditions.destinationIsFile'] ) {
await t.write(`${PREFIX}${i}/b`, 'placeholder\n');
} else {
await t.mkdir(`${PREFIX}${i}/b`);
await t.write(`${PREFIX}${i}/b/b_file.txt`, 'file b contents\n');
}
const srcUID = (await t.stat(`${PREFIX}${i}${pathOfThingToCopy}`)).uid;
const dstUID = (await t.stat(`${PREFIX}${i}/b`)).uid;
// 3. Parameter setup for this state
const data = {};
data.source = state['source.format'] === 'uid'
? srcUID : `${dir}${pathOfThingToCopy}` ;
data.destination = state['destination.format'] === 'uid'
? dstUID : `${dir}/b` ;
if ( state.name === 'specified' ) {
data.new_name = 'x_renamed';
}
if ( state.overwrite ) {
data[state.overwrite] = true;
}
// 4. Request
let e = null;
let resp;
try {
resp = await axios.request({
method: 'post',
httpsAgent: t.httpsAgent,
url: t.getURL('copy'),
data,
headers: {
...t.headers_,
'Content-Type': 'application/json'
}
});
} catch (e_) {
e = e_;
}
// 5. Check Response
let error_expected = null;
if (
state['conditions.destinationIsFile'] &&
state.name === 'specified'
) {
error_expected = {
code: 'dest_is_not_a_directory',
message: `Destination must be a directory.`,
};
}
else if (
state['conditions.destinationIsFile'] &&
! state.overwrite &&
! state.dedupe_name
) {
console.log('AN ERROR IS EXPECTED');
error_expected = {
code: 'item_with_same_name_exists',
message: 'An item with name `b` already exists.',
entry_name: 'b',
}
}
if ( error_expected ) {
expect(e).to.exist;
const data = e.response.data;
expect(data).deep.equal(error_expected);
} else {
if ( e ) throw e;
}
}
})

View File

@ -0,0 +1,100 @@
const { expect } = require("chai");
const sleep = require("../lib/sleep");
module.exports = {
name: 'delete',
do: async t => {
await t.case('delete for normal file', async () => {
await t.write('test_delete.txt', 'delete test\n', { overwrite: true });
await t.delete('test_delete.txt');
let threw = false;
try {
await t.stat('test_delete.txt');
} catch (e) {
expect(e.response.status).equal(404);
threw = true;
}
expect(threw).true;
});
await t.case('error for non-existing file', async () => {
let threw = false;
try {
await t.delete('test_delete.txt');
} catch (e) {
expect(e.response.status).equal(404);
threw = true;
}
expect(threw).true;
});
await t.case('delete for directory', async () => {
await t.mkdir('test_delete_dir', { overwrite: true });
await t.delete('test_delete_dir');
let threw = false;
try {
await t.stat('test_delete_dir');
} catch (e) {
expect(e.response.status).equal(404);
threw = true;
}
expect(threw).true;
});
await t.case('delete for non-empty directory', async () => {
await t.mkdir('test_delete_dir', { overwrite: true });
await t.write('test_delete_dir/test.txt', 'delete test\n', { overwrite: true });
let threw = false;
try {
await t.delete('test_delete_dir');
} catch (e) {
expect(e.response.status).equal(400);
threw = true;
}
expect(threw).true;
});
await t.case('delete for non-empty directory with recursive=true', async () => {
await t.mkdir('test_delete_dir', { overwrite: true });
await t.write('test_delete_dir/test.txt', 'delete test\n', { overwrite: true });
await t.delete('test_delete_dir', { recursive: true });
let threw = false;
await sleep(500);
try {
await t.stat('test_delete_dir');
} catch (e) {
expect(e.response.status).equal(404);
threw = true;
}
expect(threw).true;
});
await t.case('non-empty deep recursion', async () => {
await t.mkdir('del/a/b/c/d', {
create_missing_parents: true,
});
await t.write('del/a/b/c/d/test.txt', 'delete test\n');
await t.delete('del', {
recursive: true,
descendants_only: true,
});
let threw = false;
t.quirk('delete too asynchronous');
await new Promise(rslv => setTimeout(rslv, 500));
try {
await t.stat('del/a/b/c/d/test.txt');
} catch (e) {
expect(e.response.status).equal(404);
threw = true;
}
expect(threw).true;
threw = false;
try {
await t.stat('del/a');
} catch (e) {
expect(e.response.status).equal(404);
threw = true;
}
expect(threw).true;
await t.case('parent directory still exists', async () => {
const stat = await t.stat('del');
expect(stat.name).equal('del');
});
});
}
};

View File

@ -0,0 +1,78 @@
const { expect } = require("chai");
const _bitBooleans = [
'immutable',
'is_shortcut',
'is_symlink',
'is_dir',
];
const _integers = [
'created',
'accessed',
'modified',
];
const _strings = [
'id', 'uid', 'parent_id', 'name',
]
const verify_fsentry = async (t, o) => {
await t.case('fsentry is valid', async () => {
for ( const k of _strings ) {
await t.case(`${k} is a string`, () => {
expect(typeof o[k]).equal('string');
});
}
if ( o.is_dir ) {
await t.case(`type is null for directories`, () => {
expect(o.type).equal(null);
});
}
if ( ! o.is_dir ) {
await t.case(`type is a string for files`, () => {
expect(typeof o.type).equal('string');
});
}
await t.case('id === uid', () => {
expect(o.id).equal(o.uid);
});
await t.case('uid is string', () => {
expect(typeof o.uid).equal('string');
});
for ( const k of _bitBooleans ) {
await t.case(`${k} is 0 or 1`, () => {
expect(o[k]).oneOf([0, 1], `${k} should be 0 or 1`);
});
}
t.quirk('is_shared is not populated currently');
// expect(o.is_shared).oneOf([true, false]);
for ( const k of _integers ) {
if ( o.is_dir && k === 'accessed' ) {
t.quirk('accessed is null for new directories');
continue;
}
await t.case(`${k} is numeric type`, () => {
expect(typeof o[k]).equal('number');
});
await t.case(`${k} has no fractional component`, () => {
expect(Number.isInteger(o[k])).true;
});
}
await t.case('symlink_path is null or string', () => {
expect(
o.symlink_path === null ||
typeof o.symlink_path === 'string'
).true;
});
await t.case('owner object has expected properties', () => {
expect(o.owner).to.haveOwnProperty('username');
expect(o.owner).to.haveOwnProperty('email');
});
})
}
module.exports = {
verify_fsentry,
};

View File

@ -0,0 +1,69 @@
const { expect } = require("chai");
const { verify_fsentry } = require("./fsentry");
module.exports = {
name: 'mkdir',
do: async t => {
await t.case('recursive mkdir', async () => {
// Can create a chain of directories
const path = 'a/b/c/d/e/f/g';
let result;
await t.case('no exception thrown', async () => {
result = await t.mkdir(path, {
create_missing_parents: true,
});
console.log('result?', result)
});
// Returns the last directory in the chain
// await verify_fsentry(t, result);
await t.case('filename is correct', () => {
expect(result.name).equal('g');
});
await t.case('can stat the directory', async () => {
const stat = await t.stat(path);
// await verify_fsentry(t, stat);
await t.case('filename is correct', () => {
expect(stat.name).equal('g');
});
});
// can stat the first directory in the chain
await t.case('can stat the first directory in the chain', async () => {
const stat = await t.stat('a');
// await verify_fsentry(t, stat);
await t.case('filename is correct', () => {
expect(stat.name).equal('a');
});
});
});
// NOTE: It looks like we removed this behavior and we always create missing parents
// await t.case('fails with missing parent', async () => {
// let threw = false;
// try {
// const result = await t.mkdir('a/b/x/g');
// console.log('unexpected result', result);
// } catch (e) {
// expect(e.response.status).equal(422);
// console.log('response?', e.response.data)
// expect(e.response.data).deep.equal({
// code: 'dest_does_not_exist',
// message: 'Destination was not found.',
// });
// threw = true;
// }
// expect(threw).true;
// });
await t.case('mkdir dedupe name', async () => {
for ( let i = 1; i <= 3; i++ ) {
await t.mkdir('a', { dedupe_name: true });
const stat = await t.stat(`a (${i})`);
expect(stat.name).equal(`a (${i})`);
}
});
}
};

View File

@ -0,0 +1,94 @@
const { expect } = require("chai");
const fs = require('fs');
module.exports = {
name: 'move',
do: async t => {
// setup conditions for tests
await t.mkdir('dir_with_contents');
await t.write('dir_with_contents/a.txt', 'move test\n');
await t.write('dir_with_contents/b.txt', 'move test\n');
await t.write('dir_with_contents/c.txt', 'move test\n');
await t.mkdir('dir_with_contents/q');
await t.mkdir('dir_with_contents/w');
await t.mkdir('dir_with_contents/e');
await t.mkdir('dir_no_contents');
await t.write('just_a_file.txt', 'move test\n');
await t.case('move file', async () => {
await t.move('just_a_file.txt', 'just_a_file_moved.txt');
const moved = await t.stat('just_a_file_moved.txt');
let threw = false;
try {
await t.stat('just_a_file.txt');
} catch (e) {
expect(e.response.status).equal(404);
threw = true;
}
expect(threw).true;
expect(moved.name).equal('just_a_file_moved.txt');
});
await t.case('move file to existing file', async () => {
await t.write('just_a_file.txt', 'move test\n');
let threw = false;
try {
await t.move('just_a_file.txt', 'dir_with_contents/a.txt');
} catch (e) {
expect(e.response.status).equal(409);
threw = true;
}
expect(threw).true;
});
/*
await t.case('move file to existing directory', async () => {
await t.move('just_a_file.txt', 'dir_with_contents');
const moved = await t.stat('dir_with_contents/just_a_file.txt');
let threw = false;
try {
await t.stat('just_a_file.txt');
} catch (e) {
expect(e.response.status).equal(404);
threw = true;
}
expect(threw).true;
expect(moved.name).equal('just_a_file.txt');
});
*/
await t.case('move directory', async () => {
await t.move('dir_no_contents', 'dir_no_contents_moved');
const moved = await t.stat('dir_no_contents_moved');
let threw = false;
try {
await t.stat('dir_no_contents');
} catch (e) {
expect(e.response.status).equal(404);
threw = true;
}
expect(threw).true;
expect(moved.name).equal('dir_no_contents_moved');
});
await t.case('move file and create parents', async () => {
await t.write('just_a_file.txt', 'move test\n', { overwrite: true });
const res = await t.move(
'just_a_file.txt',
'dir_with_contents/q/w/e/just_a_file.txt',
{ create_missing_parents: true }
);
expect(res.parent_dirs_created).length(2);
const moved = await t.stat('dir_with_contents/q/w/e/just_a_file.txt');
let threw = false;
try {
await t.stat('just_a_file.txt');
} catch (e) {
expect(e.response.status).equal(404);
threw = true;
}
expect(threw).true;
expect(moved.name).equal('just_a_file.txt');
});
}
};

View File

@ -0,0 +1,103 @@
const { default: axios } = require("axios");
const { expect } = require("chai");
const move = require("../coverage_models/move");
const TestFactory = require("../lib/TestFactory");
/*
CARTESIAN TEST FOR /move
NOTE: This test is very similar to the test for /copy,
but DRYing it would add too much complexity.
It is best to have both tests open side-by-side
when making changes to either one.
*/
const PREFIX = 'move_cart_';
module.exports = TestFactory.cartesian('Cartesian Test for /move', move, {
each: async (t, state, i) => {
// 1. Common setup for all states
await t.mkdir(`${PREFIX}${i}`);
const dir = `/${t.cwd}/${PREFIX}${i}`;
await t.mkdir(`${PREFIX}${i}/a`);
await t.write(`${PREFIX}${i}/a/a_file.txt`, 'file a contents\n');
// 2. Situation setup for this state
if ( state['conditions.destinationIsFile'] ) {
await t.write(`${PREFIX}${i}/b`, 'placeholder\n');
} else {
await t.mkdir(`${PREFIX}${i}/b`);
await t.write(`${PREFIX}${i}/b/b_file.txt`, 'file b contents\n');
}
const srcUID = (await t.stat(`${PREFIX}${i}/a/a_file.txt`)).uid;
const dstUID = (await t.stat(`${PREFIX}${i}/b`)).uid;
// 3. Parameter setup for this state
const data = {};
data.source = state['source.format'] === 'uid'
? srcUID : `${dir}/a/a_file.txt` ;
data.destination = state['destination.format'] === 'uid'
? dstUID : `${dir}/b` ;
if ( state.name === 'specified' ) {
data.new_name = 'x_file.txt';
}
if ( state.overwrite ) {
data[state.overwrite] = true;
}
// 4. Request
let e = null;
let resp;
try {
resp = await axios.request({
method: 'post',
httpsAgent: t.httpsAgent,
url: t.getURL('move'),
data,
headers: {
...t.headers_,
'Content-Type': 'application/json'
}
});
} catch (e_) {
e = e_;
}
// 5. Check Response
let error_expected = null;
if (
state['conditions.destinationIsFile'] &&
state.name === 'specified'
) {
error_expected = {
code: 'dest_is_not_a_directory',
message: `Destination must be a directory.`,
};
}
else if (
state['conditions.destinationIsFile'] &&
! state.overwrite
) {
error_expected = {
code: 'item_with_same_name_exists',
message: 'An item with name `b` already exists.',
entry_name: 'b',
}
}
if ( error_expected ) {
expect(e).to.exist;
const data = e.response.data;
expect(data).deep.equal(error_expected);
} else {
if ( e ) throw e;
}
}
})

View File

@ -0,0 +1,39 @@
const { verify_fsentry } = require("./fsentry");
const { expect } = require("chai");
module.exports = {
name: 'readdir',
do: async t => {
// let result;
await t.mkdir('test_readdir', { overwrite: true });
t.cd('test_readdir');
const files = ['a.txt', 'b.txt', 'c.txt'];
const dirs = ['q', 'w', 'e'];
for ( const file of files ) {
await t.write(file, 'readdir test\n', { overwrite: true });
}
for ( const dir of dirs ) {
await t.mkdir(dir, { overwrite: true });
}
for ( const file of files ) {
const result = await t.stat(file);
await verify_fsentry(t, result);
}
for ( const dir of dirs ) {
const result = await t.stat(dir);
await verify_fsentry(t, result);
}
await t.case('readdir of root shouldn\'t return everything', async () => {
const result = await t.readdir('/', { recursive: true });
console.log('result?', result)
})
// t.cd('..');
}
};

View File

@ -0,0 +1,100 @@
const { verify_fsentry } = require("./fsentry");
const { expect } = require("chai");
module.exports = {
name: 'stat',
do: async t => {
let result;
const TEST_FILENAME = 'test_stat.txt';
let recorded_uid = null;
await t.case('stat with path (no flags)', async () => {
await t.write(TEST_FILENAME, 'stat test\n', { overwrite: true });
result = await t.stat(TEST_FILENAME);
await verify_fsentry(t, result);
recorded_uid = result.uid;
await t.case('filename is correct', () => {
expect(result.name).equal('test_stat.txt');
});
})
await t.case('stat with uid (no flags)', async () => {
result = await t.statu(recorded_uid);
await verify_fsentry(t, result);
await t.case('filename is correct', () => {
expect(result.name).equal('test_stat.txt');
});
})
await t.case('stat with no path or uid provided fails', async () => {
let threw = false;
try {
const res = await t.get('stat', {});
} catch (e) {
expect(e.response.status).equal(400);
expect(e.response.data).deep.equal({
code: 'field_missing',
message: 'Field `subject` is required.',
key: 'subject',
});
threw = true;
}
expect(threw).true;
});
const flags = ['permissions', 'versions'];
for ( const flag of flags ) {
await t.case('stat with ' + flag, async () => {
result = await t.stat(TEST_FILENAME, {
['return_' + flag]: true,
});
await verify_fsentry(t, result);
await t.case('filename is correct', () => {
expect(result.name).equal(`test_stat.txt`);
});
await t.case(`result has ${flag} array`, () => {
expect(Array.isArray(result[flag])).true;
});
})
}
await t.mkdir('test_stat_subdomains', { overwrite: true });
await t.case('stat with subdomains', async () => {
result = await t.stat('test_stat_subdomains', {
return_subdomains: true,
});
await verify_fsentry(t, result);
await t.case('directory name is correct', () => {
expect(result.name).equal(`test_stat_subdomains`);
});
await t.case(`result has subdomains array`, () => {
expect(Array.isArray(result.subdomains)).true;
});
console.log('RESULT', result);
})
{
const flag = 'size';
await t.case('stat with ' + flag, async () => {
result = await t.stat(TEST_FILENAME, {
['return_' + flag]: true,
});
await verify_fsentry(t, result);
await t.case('filename is correct', () => {
expect(result.name).equal(`test_stat.txt`);
});
console.log('RESULT', result);
})
}
// console.log('result?', result);
}
};

View File

@ -0,0 +1,14 @@
const chai = require('chai');
chai.use(require('chai-as-promised'))
const expect = chai.expect;
module.exports = {
name: 'single write for trace and span',
do: async t => {
let result;
const TEST_FILENAME = 'test_telem.txt';
await t.write(TEST_FILENAME, 'example\n', { overwrite: true });
}
};

View File

@ -0,0 +1,69 @@
const chai = require('chai');
chai.use(require('chai-as-promised'))
const expect = chai.expect;
module.exports = {
name: 'write and read',
do: async t => {
let result;
const TEST_FILENAME = 'test_rw.txt';
await t.write(TEST_FILENAME, 'example\n', { overwrite: true });
await t.case('read matches what was written', async () => {
result = await t.read(TEST_FILENAME);
expect(result).equal('example\n');
});
await t.case('write throws for overwrite=false', () => {
expect(
t.write(TEST_FILENAME, 'no-change\n')
).rejectedWith(Error);
});
await t.case('write updates for overwrite=true', async () => {
await t.write(TEST_FILENAME, 'yes-change\n', {
overwrite: true,
});
result = await t.read(TEST_FILENAME);
expect(result).equal('yes-change\n');
});
await t.case('write updates for overwrite=true', async () => {
await t.write(TEST_FILENAME, 'yes-change\n', {
overwrite: true,
});
result = await t.read(TEST_FILENAME, { version_id: '1' });
expect(result).equal('yes-change\n');
});
await t.case('read with no path or uid provided fails', async () => {
let threw = false;
try {
const res = await t.get('read', {});
} catch (e) {
expect(e.response.status).equal(400);
expect(e.response.data).deep.equal({
message: 'Field \`file\` is required.',
code: 'field_missing',
key: 'file',
});
threw = true;
}
expect(threw).true;
});
await t.case('read for non-existing path fails', async () => {
let threw = false;
try {
await t.read('i-do-not-exist.txt');
} catch (e) {
expect(e.response.status).equal(404);
expect(e.response.data).deep.equal({ message: 'Path not found.' });
threw = true;
}
expect(threw).true;
});
}
};

View File

@ -0,0 +1,91 @@
const { default: axios } = require("axios");
const write = require("../coverage_models/write");
const TestFactory = require("../lib/TestFactory");
const chai = require('chai');
chai.use(require('chai-as-promised'))
const expect = chai.expect;
module.exports = TestFactory.cartesian('Cartesian Test for /write', write, {
each: async (t, state, i) => {
if ( state['conditions.destinationIsFile'] ) {
await t.write('write_cart_' + i, 'placeholder\n');
} else {
await t.mkdir('write_cart_' + i);
}
const dir = `/${t.cwd}/write_cart_` + i;
const dirUID = (await t.stat('write_cart_' + i)).uid;
const contents = new Blob(
[`case ${i}\n`],
{ type: 'text/plain' },
);
console.log('DIR UID', dirUID)
const fd = new FormData();
if ( state.name === 'specified' ) {
fd.append('name', 'specified_name.txt');
}
if ( state.overwrite ) {
fd.append(state.overwrite, true);
}
fd.append('path', state.format === 'path' ? dir : dirUID);
fd.append('size', contents.size),
fd.append('file', contents, 'uploaded_name.txt');
let e = null;
let resp;
try {
resp = await axios.request({
method: 'post',
httpsAgent: t.httpsAgent,
url: t.getURL('write'),
data: fd,
headers: {
...t.headers_,
'Content-Type': 'multipart/form-data'
}
})
} catch (e_) {
e = e_;
}
let error_expected = null;
// Error conditions
if (
state['conditions.destinationIsFile'] &&
state.name === 'specified'
) {
error_expected = {
code: 'dest_is_not_a_directory',
message: `Destination must be a directory.`,
};
}
if (
state['conditions.destinationIsFile'] &&
state.name === 'default' &&
! state.overwrite
) {
error_expected = {
code: 'item_with_same_name_exists',
message: 'An item with name `write_cart_'+i+'` already exists.',
entry_name: 'write_cart_' + i,
};
}
if ( error_expected ) {
expect(e).to.exist;
const data = e.response.data;
expect(data).deep.equal(error_expected);
} else {
if ( e ) throw e;
}
}
})

View File

@ -0,0 +1,109 @@
const axios = require('axios');
const YAML = require('yaml');
const https = require('node:https');
const { parseArgs } = require('node:util');
const url = require('node:url');
const path_ = require('path');
const fs = require('fs');
let config;
try {
({ values: {
config,
}, positionals: [id] } = parseArgs({
options: {
config: {
type: 'string',
},
},
allowPositionals: true,
}));
} catch (e) {
if ( args.length < 1 ) {
console.error(
'Usage: readdir_profile [OPTIONS]\n' +
'\n' +
'Options:\n' +
' --config=<path> (required) Path to configuration file\n' +
''
);
process.exit(1);
}
}
const conf = YAML.parse(fs.readFileSync(config).toString());
const dir = `/${conf.username}/readdir_test`
// process.on('SIGINT', async () => {
// process.exit(0);
// });
const httpsAgent = new https.Agent({
rejectUnauthorized: false
})
const getURL = (...path) => {
const apiURL = new url.URL(conf.url);
apiURL.pathname = path_.posix.join(
apiURL.pathname,
...path
);
return apiURL.href;
};
const epoch = Date.now();
const TIME_BEFORE_TEST = 20 * 1000; // 10 seconds
const NOOP = () => {};
let check = () => {
if ( Date.now() - epoch >= TIME_BEFORE_TEST ) {
console.log(
`\x1B[36;1m !!! START THE TEST !!! \x1B[0m`
);
check = NOOP;
}
};
const measure_readdir = async () => {
const ts_start = Date.now();
await axios.request({
httpsAgent,
method: 'post',
url: getURL('readdir'),
data: {
path: dir,
},
headers: {
'Authorization': `Bearer ${conf.token}`,
'Content-Type': 'application/json'
}
})
const ts_end = Date.now();
const diff = ts_end - ts_start;
await fs.promises.appendFile(
`readdir_profile.txt`,
`${Date.now()},${diff}\n`
)
check();
await new Promise(rslv => {
setTimeout(rslv, 5);
});
}
const main = async () => {
while (true) {
await measure_readdir();
}
}
main();

View File

@ -0,0 +1,73 @@
const axios = require('axios');
const YAML = require('yaml');
const https = require('node:https');
const { parseArgs } = require('node:util');
const url = require('node:url');
const path_ = require('path');
const fs = require('fs');
let config;
try {
({ values: {
config,
}, positionals: [id] } = parseArgs({
options: {
config: {
type: 'string',
},
},
allowPositionals: true,
}));
} catch (e) {
if ( args.length < 1 ) {
console.error(
'Usage: readdir_profile [OPTIONS]\n' +
'\n' +
'Options:\n' +
' --config=<path> (required) Path to configuration file\n' +
''
);
process.exit(1);
}
}
const conf = YAML.parse(fs.readFileSync(config).toString());
const entry = `/${conf.username}/read_test.txt`;
// process.on('SIGINT', async () => {
// process.exit(0);
// });
const httpsAgent = new https.Agent({
rejectUnauthorized: false
})
const getURL = (...path) => {
const apiURL = new url.URL(conf.url);
apiURL.pathname = path_.posix.join(
apiURL.pathname,
...path
);
return apiURL.href;
};
const main = async () => {
const resp = await axios.request({
httpsAgent,
method: 'get',
url: getURL('read'),
params: {
file: entry,
},
headers: {
'Authorization': `Bearer ${conf.token}`,
}
})
console.log(resp.data);
}
main();

View File

@ -0,0 +1,8 @@
[
{
"name": "mysql",
"listen": "[::]:8888",
"upstream": "localhost:8889",
"enabled": true
}
]

View File

@ -0,0 +1,8 @@
[
{
"name": "mysql",
"listen": "[::]:8888",
"upstream": "localhost:8889",
"enabled": true
}
]