diff --git a/src/backend/src/filesystem/hl_operations/hl_move.js b/src/backend/src/filesystem/hl_operations/hl_move.js index 340fe089..ecbe6594 100644 --- a/src/backend/src/filesystem/hl_operations/hl_move.js +++ b/src/backend/src/filesystem/hl_operations/hl_move.js @@ -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'), }); } diff --git a/src/backend/src/filesystem/hl_operations/hl_stat.js b/src/backend/src/filesystem/hl_operations/hl_stat.js index 809620c4..5fa321e1 100644 --- a/src/backend/src/filesystem/hl_operations/hl_stat.js +++ b/src/backend/src/filesystem/hl_operations/hl_stat.js @@ -55,7 +55,10 @@ 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_permissions) { + subject.entry.permissions = []; + await subject.fetchShares(); + } if (return_versions) await subject.fetchVersions(); await subject.fetchIsEmpty(); diff --git a/tools/api-tester/README.md b/tools/api-tester/README.md index 31616b04..95d4eaeb 100644 --- a/tools/api-tester/README.md +++ b/tools/api-tester/README.md @@ -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). diff --git a/tools/api-tester/apitest.js b/tools/api-tester/apitest.js index 46759541..b5dcb9d7 100644 --- a/tools/api-tester/apitest.js +++ b/tools/api-tester/apitest.js @@ -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({ @@ -23,6 +23,7 @@ try { onlycase: { type: 'string' }, bench: { type: 'boolean' }, unit: { type: 'boolean' }, + suite: { type: 'string' }, }, allowPositionals: true, }); @@ -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( @@ -44,6 +48,7 @@ try { 'Options:\n' + ' --config= (required) Path to configuration file\n' + ' --report= (optional) Output file for full test results\n' + + ' --suite= (optional) Run only tests with matching suite name\n' + '' ); process.exit(1); @@ -56,6 +61,7 @@ const main = async () => { const context = { options: { onlycase, + suite: suiteName, } }; const ts = new TestSDK(conf, context); @@ -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 { diff --git a/tools/api-tester/lib/TestRegistry.js b/tools/api-tester/lib/TestRegistry.js index d336e2cd..5091286a 100644 --- a/tools/api-tester/lib/TestRegistry.js +++ b/tools/api-tester/lib/TestRegistry.js @@ -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); } diff --git a/tools/api-tester/lib/TestSDK.js b/tools/api-tester/lib/TestSDK.js index e7fef94a..57ccafaa 100644 --- a/tools/api-tester/lib/TestSDK.js +++ b/tools/api-tester/lib/TestSDK.js @@ -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(); }