mirror of
https://github.com/HeyPuter/puter.git
synced 2025-02-02 14:18:43 +08:00
move: api-tester to monorepo
This commit is contained in:
parent
44ad3c5781
commit
405d9b35aa
1
tools/api-tester/.gitignore
vendored
Normal file
1
tools/api-tester/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
config.yml
|
9
tools/api-tester/README.md
Normal file
9
tools/api-tester/README.md
Normal 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
112
tools/api-tester/apitest.js
Normal 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();
|
121
tools/api-tester/benches/simple.js
Normal file
121
tools/api-tester/benches/simple.js
Normal 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 });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
16
tools/api-tester/coverage_models/copy.js
Normal file
16
tools/api-tester/coverage_models/copy.js
Normal 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'],
|
||||||
|
});
|
15
tools/api-tester/coverage_models/move.js
Normal file
15
tools/api-tester/coverage_models/move.js
Normal 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']
|
||||||
|
});
|
16
tools/api-tester/coverage_models/write.js
Normal file
16
tools/api-tester/coverage_models/write.js
Normal 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'],
|
||||||
|
});
|
83
tools/api-tester/doc/cartesian.md
Normal file
83
tools/api-tester/doc/cartesian.md
Normal 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.
|
3
tools/api-tester/example_config.yml
Normal file
3
tools/api-tester/example_config.yml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
url: http://puter.localhost:4001/
|
||||||
|
username: lavender_hat_6100
|
||||||
|
token: ---
|
11
tools/api-tester/lib/Assert.js
Normal file
11
tools/api-tester/lib/Assert.js
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
71
tools/api-tester/lib/CoverageModel.js
Normal file
71
tools/api-tester/lib/CoverageModel.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
3
tools/api-tester/lib/ReportGenerator.js
Normal file
3
tools/api-tester/lib/ReportGenerator.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module.exports = class ReportGenerator {
|
||||||
|
//
|
||||||
|
}
|
27
tools/api-tester/lib/TestFactory.js
Normal file
27
tools/api-tester/lib/TestFactory.js
Normal 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_,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
68
tools/api-tester/lib/TestRegistry.js
Normal file
68
tools/api-tester/lib/TestRegistry.js
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
378
tools/api-tester/lib/TestSDK.js
Normal file
378
tools/api-tester/lib/TestSDK.js
Normal 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',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
35
tools/api-tester/lib/log_error.js
Normal file
35
tools/api-tester/lib/log_error.js
Normal 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;
|
5
tools/api-tester/lib/sleep.js
Normal file
5
tools/api-tester/lib/sleep.js
Normal 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
204
tools/api-tester/package-lock.json
generated
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
tools/api-tester/package.json
Normal file
17
tools/api-tester/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
23
tools/api-tester/test_sdks/puter-rest.js
Normal file
23
tools/api-tester/test_sdks/puter-rest.js
Normal 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);
|
13
tools/api-tester/tests/__entry__.js
Normal file
13
tools/api-tester/tests/__entry__.js
Normal 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'));
|
||||||
|
};
|
226
tools/api-tester/tests/batch.js
Normal file
226
tools/api-tester/tests/batch.js
Normal 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)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
131
tools/api-tester/tests/copy_cart.js
Normal file
131
tools/api-tester/tests/copy_cart.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
100
tools/api-tester/tests/delete.js
Normal file
100
tools/api-tester/tests/delete.js
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
78
tools/api-tester/tests/fsentry.js
Normal file
78
tools/api-tester/tests/fsentry.js
Normal 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,
|
||||||
|
};
|
69
tools/api-tester/tests/mkdir.js
Normal file
69
tools/api-tester/tests/mkdir.js
Normal 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})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
94
tools/api-tester/tests/move.js
Normal file
94
tools/api-tester/tests/move.js
Normal 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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
103
tools/api-tester/tests/move_cart.js
Normal file
103
tools/api-tester/tests/move_cart.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
39
tools/api-tester/tests/readdir.js
Normal file
39
tools/api-tester/tests/readdir.js
Normal 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('..');
|
||||||
|
}
|
||||||
|
};
|
100
tools/api-tester/tests/stat.js
Normal file
100
tools/api-tester/tests/stat.js
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
14
tools/api-tester/tests/telem_write.js
Normal file
14
tools/api-tester/tests/telem_write.js
Normal 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 });
|
||||||
|
}
|
||||||
|
};
|
69
tools/api-tester/tests/write_and_read.js
Normal file
69
tools/api-tester/tests/write_and_read.js
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
91
tools/api-tester/tests/write_cart.js
Normal file
91
tools/api-tester/tests/write_cart.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
109
tools/api-tester/tools/readdir_profile.js
Normal file
109
tools/api-tester/tools/readdir_profile.js
Normal 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();
|
73
tools/api-tester/tools/test_read.js
Normal file
73
tools/api-tester/tools/test_read.js
Normal 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();
|
||||||
|
|
8
tools/api-tester/toxiproxy/toxiproxy.json
Normal file
8
tools/api-tester/toxiproxy/toxiproxy.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "mysql",
|
||||||
|
"listen": "[::]:8888",
|
||||||
|
"upstream": "localhost:8889",
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
]
|
8
tools/api-tester/toxiproxy/toxiproxy_control.json
Normal file
8
tools/api-tester/toxiproxy/toxiproxy_control.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "mysql",
|
||||||
|
"listen": "[::]:8888",
|
||||||
|
"upstream": "localhost:8889",
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
]
|
Loading…
Reference in New Issue
Block a user