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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/backend/src/filesystem/hl_operations/hl_move.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ class HLMove extends HLFilesystemOperation {
if ( await dest.exists() ) {
if ( ! values.overwrite && ! values.dedupe_name ) {
throw APIError.create('item_with_same_name_exists', null, {
entry_name: target_name,
entry_name: await dest.get('name'),
});
}

Expand Down
7 changes: 5 additions & 2 deletions src/backend/src/filesystem/hl_operations/hl_stat.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ class HLStat extends HLFilesystemOperation {
const {
subject, user,
return_subdomains,
return_permissions,
return_permissions, // Deprecated: kept for backwards compatiable with `return_shares`
return_shares,
return_versions,
return_size,
} = this.values;
Expand All @@ -55,7 +56,9 @@ class HLStat extends HLFilesystemOperation {

if (return_size) await subject.fetchSize(user);
if (return_subdomains) await subject.fetchSubdomains(user)
if (return_permissions) await subject.fetchShares();
if (return_shares || return_permissions) {
await subject.fetchShares();
}
if (return_versions) await subject.fetchVersions();

await subject.fetchIsEmpty();
Expand Down
1 change: 1 addition & 0 deletions src/backend/src/routers/filesystem_api/stat.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ module.exports = eggspress('/stat', {
user: req.user,
return_subdomains: req.body.return_subdomains,
return_permissions: req.body.return_permissions,
return_shares: req.body.return_shares,
return_versions: req.body.return_versions,
return_size: req.body.return_size,
});
Expand Down
133 changes: 129 additions & 4 deletions tools/api-tester/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,130 @@
## It takes 3 steps to run the tests :)
# API Tester

1. run `npm install`
2. copy `example_config.yml` and add the correct values
3. run `node apitest.js --config=your_config_file.yml`
A test framework for testing the backend API of puter.

## Table of Contents

- [API Tester](#api-tester)
- [How to use](#how-to-use)
- [Workflow](#workflow)
- [Shorthands](#shorthands)
- [Basic Concepts](#basic-concepts)
- [Behaviors](#behaviors)
- [Isolation of `t.cwd`](#isolation-of-t-cwd)
- [Implementation](#implementation)
- [TODO](#todo)

## How to use

### Workflow

All commands below should be run from the root directory of puter.

1. (Optional) Start a backend server:

```bash
npm start
```

2. Copy `example_config.yml` and add the correct values:

```bash
cp ./tools/api-tester/example_config.yml ./tools/api-tester/config.yml
```

Fields:
- url: The endpoint of the backend server. (default: http://api.puter.localhost:4100/)
- username: The username of the admin user. (e.g. admin)
- token: The token of the user. (can be obtained by typing `puter.authToken` in Developer Tools's console)

3. Run the tests:

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

### Shorthands

- Run unit tests only:

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

- Filter tests by suite name:

```bash
node ./tools/api-tester/apitest.js --config=./tools/api-tester/config.yml --unit --suite=mkdir
```

- Rerun failed tests in the last run:

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

## Basic Concepts

A *test case* is a function that tests a specific behavior of the backend API. Test cases can be nested:

```js
await t.case('normal mkdir', async () => {
const result = await t.mkdir_v2('foo');
expect(result.name).equal('foo');

await t.case('can stat the created directory', async () => {
const stat = await t.stat('foo');
expect(stat.name).equal('foo');
});
});
```

A *test suite* is a collection of test cases. A `.js` file should contain exactly one test suite.

```js
module.exports = {
name: 'mkdir',
do: async t => {
await t.case('normal mkdir', async () => {
...
});

await t.case('recursive mkdir', async () => {
...
});
}
};
```

## Behaviors

### Isolation of `t.cwd`

- `t.cwd` is reset at the beginning of each test suite, since a test suite usually doesn't want to be affected by other test suites.
- `t.cwd` will be inherited from the cases in the same test suite, since a leaf case might want to share the context with its parent/sibling cases.

```js
module.exports = {
name: 'readdir',
do: async t => {
// t.cwd is reset to /admin/api_test

await t.case('normal mkdir', async () => {
// inherits cwd from parent/sibling cases

await t.case('mkdir in subdir', async () => {
// inherits cwd from parent/sibling cases
});
});
}
};
```

## Implementation

- Test suites are registered in `tools/api-tester/tests/__entry__.js`.

## TODO

- [ ] Update usage of apitest.js. (Is it possible to generate the usage automatically?)
- [ ] Integrate it into CI, optionally running it only in specific scenarios (e.g., when backend code changes).
10 changes: 8 additions & 2 deletions tools/api-tester/apitest.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const { parseArgs } = require('node:util');

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

let config, report;
let config, report, suiteName;

try {
const parsed = parseArgs({
Expand All @@ -23,6 +23,7 @@ try {
onlycase: { type: 'string' },
bench: { type: 'boolean' },
unit: { type: 'boolean' },
suite: { type: 'string' },
},
allowPositionals: true,
});
Expand All @@ -33,9 +34,12 @@ try {
onlycase,
bench,
unit,
suite: suiteName,
}, positionals: [id] } = parsed);

onlycase = onlycase !== undefined ? Number.parseInt(onlycase) : undefined;
// Ensure suiteName is a string or undefined
suiteName = suiteName || undefined;
} catch (e) {
console.error(e);
console.error(
Expand All @@ -44,6 +48,7 @@ try {
'Options:\n' +
' --config=<path> (required) Path to configuration file\n' +
' --report=<path> (optional) Output file for full test results\n' +
' --suite=<name> (optional) Run only tests with matching suite name\n' +
''
);
process.exit(1);
Expand All @@ -56,6 +61,7 @@ const main = async () => {
const context = {
options: {
onlycase,
suite: suiteName,
}
};
const ts = new TestSDK(conf, context);
Expand Down Expand Up @@ -87,7 +93,7 @@ const main = async () => {
}

if ( unit ) {
await registry.run_all_tests();
await registry.run_all_tests(suiteName);
} else if ( bench ) {
await registry.run_all_benches();
} else {
Expand Down
6 changes: 5 additions & 1 deletion tools/api-tester/lib/TestRegistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ module.exports = class TestRegistry {
this.benches[id] = benchDefinition;
}

async run_all_tests () {
async run_all_tests(suiteName) {
for ( const id in this.tests ) {
if (suiteName && id !== suiteName) {
continue;
}

const testDefinition = this.tests[id];
await this.t.runTestPackage(testDefinition);
}
Expand Down
4 changes: 2 additions & 2 deletions tools/api-tester/lib/TestSDK.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ module.exports = class TestSDK {
async case (id, fn) {
this.nameStack.push(id);

// Always reset cwd for top-level cases to prevent them from affecting
// each other.
// Always reset cwd at the beginning of a test suite to prevent it
// from affected by others.
if (this.nameStack.length === 1) {
this.resetCwd();
}
Expand Down
24 changes: 21 additions & 3 deletions tools/api-tester/tests/stat.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,23 @@ module.exports = {
expect(threw).true;
});

const flags = ['permissions', 'versions'];
await t.case('stat with versions', async () => {
result = await t.stat(TEST_FILENAME, {
return_versions: true,
});

await verify_fsentry(t, result);
await t.case('filename is correct', () => {
expect(result.name).equal(`test_stat.txt`);
});
await t.case(`result has versions array`, () => {
expect(Array.isArray(result.versions)).true;
});
})

// Backend should return 'shares' field when 'return_shares' is true. And
// the backwards compatiable of `return_permissions` is also tested here.
const flags = ['shares', 'permissions'];
for ( const flag of flags ) {
await t.case('stat with ' + flag, async () => {
result = await t.stat(TEST_FILENAME, {
Expand All @@ -57,8 +73,10 @@ module.exports = {
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.case(`result has shares (apps and users)`, () => {
expect('shares' in result).true;
expect(Array.isArray(result['shares']['users'])).true;
expect(Array.isArray(result['shares']['apps'])).true;
});
})
}
Expand Down