mirror of
https://github.com/HeyPuter/puter.git
synced 2026-05-08 08:01:05 -05:00
Merge branch 'main' of https://github.com/HeyPuter/puter
This commit is contained in:
+108
-17
@@ -2,9 +2,9 @@ name: test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
branches: ["main"]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -15,21 +15,44 @@ jobs:
|
||||
node-version: [20.x, 22.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
rm package-lock.json
|
||||
npm install -g npm@latest
|
||||
npm install
|
||||
npm run test
|
||||
- name: Build
|
||||
run: |
|
||||
rm package-lock.json
|
||||
npm install -g npm@latest
|
||||
npm install
|
||||
npm run test
|
||||
|
||||
api-test:
|
||||
name: backend (node env, api-test)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: API Test
|
||||
run: |
|
||||
pip install -r ./tests/ci/requirements.txt
|
||||
./tests/ci/api-test.py
|
||||
|
||||
playwright-test:
|
||||
name: puterjs (browser env, playwright)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
@@ -44,8 +67,76 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: API Test
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
working-directory: ./tests/playwright
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
working-directory: ./tests/playwright
|
||||
|
||||
- name: Playwright Test
|
||||
run: |
|
||||
pip install -r ./tools/api-tester/ci/requirements.txt
|
||||
./tools/api-tester/ci/run.py
|
||||
pip install -r ./tests/ci/requirements.txt
|
||||
./tests/ci/playwright-test.py
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: server-logs
|
||||
path: |
|
||||
/tmp/backend.log
|
||||
/tmp/fs-tree-manager.log
|
||||
retention-days: 3
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: config-files
|
||||
path: |
|
||||
./volatile/config/config.json
|
||||
./src/fs_tree_manager/config.yaml
|
||||
./tests/client-config.yaml
|
||||
retention-days: 3
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
id: playwright-report
|
||||
with:
|
||||
name: playwright-report
|
||||
path: tests/playwright/playwright-report/
|
||||
retention-days: 3
|
||||
|
||||
- name: Get Playwright artifact URL
|
||||
run: |
|
||||
ARTIFACT_URL=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts \
|
||||
--jq '.artifacts[] | select(.name=="playwright-report") | .archive_download_url')
|
||||
echo "url=$ARTIFACT_URL" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Output artifact URL
|
||||
run: echo 'Artifact URL is ${{ steps.playwright-report.outputs.artifact-url }}'
|
||||
|
||||
vitest:
|
||||
name: puterjs (node env, vitest)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Vitest Test
|
||||
run: |
|
||||
pip install -r ./tests/ci/requirements.txt
|
||||
./tests/ci/vitest.py
|
||||
|
||||
+13
-1
@@ -45,10 +45,22 @@ jsconfig.json
|
||||
# the exact tree installed in the node_modules folder
|
||||
package-lock.json
|
||||
|
||||
# ======================================================================
|
||||
# playwright test (currently only test the file-system)
|
||||
# ======================================================================
|
||||
tests/client-config.yaml
|
||||
|
||||
# ======================================================================
|
||||
# python
|
||||
# ======================================================================
|
||||
__pycache__/
|
||||
|
||||
# ======================================================================
|
||||
# other
|
||||
# ======================================================================
|
||||
# AI STUFF
|
||||
AGENTS.md
|
||||
.roo
|
||||
|
||||
|
||||
# source maps
|
||||
*.map
|
||||
@@ -0,0 +1,54 @@
|
||||
## Summary
|
||||
|
||||
Playwright test the puter-js API in browser environment.
|
||||
|
||||
## Motivation
|
||||
|
||||
Some features of the puter-js/puter-GUI only work in the browser environment:
|
||||
|
||||
- file system
|
||||
- naive-cache
|
||||
- client-replica (WIP)
|
||||
- wspush
|
||||
|
||||
## Setup
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```sh
|
||||
cd ./tests/playwright
|
||||
npm install
|
||||
npx playwright install --with-deps
|
||||
```
|
||||
|
||||
Initialize the client config (working directory: `./tests/playwright`):
|
||||
|
||||
1. `cp ../example-client-config.yaml ../client-config.yaml`
|
||||
2. Edit the `client-config.yaml` to set the `auth_token`
|
||||
|
||||
## Run tests
|
||||
|
||||
### CLI
|
||||
|
||||
Working directory: `./tests/playwright`
|
||||
|
||||
```sh
|
||||
# run all tests
|
||||
npx playwright test
|
||||
|
||||
# run a test by name
|
||||
# e.g: npx playwright test -g "mkdir in root directory is prohibited"
|
||||
npx playwright test -g "mkdir in root directory is prohibited"
|
||||
|
||||
# run the tests that failed in the last test run
|
||||
npx playwright test --last-failed
|
||||
|
||||
# open the report of the last test run in the browser
|
||||
npx playwright show-report
|
||||
```
|
||||
|
||||
### VSCode/Cursor
|
||||
|
||||
1. Install the "Playwright Test for VSCode" extension.
|
||||
2. Go to "Testing" tab in the sidebar.
|
||||
3. Click buttons to run tests.
|
||||
@@ -51,6 +51,7 @@ export default defineConfig([
|
||||
// TypeScript support block
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
ignores: ['tests/**/*.ts'],
|
||||
languageOptions: {
|
||||
parser: tseslintParser,
|
||||
parserOptions: {
|
||||
@@ -70,6 +71,28 @@ export default defineConfig([
|
||||
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
|
||||
},
|
||||
},
|
||||
// TypeScript support for tests
|
||||
{
|
||||
files: ['tests/**/*.ts'],
|
||||
languageOptions: {
|
||||
parser: tseslintParser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: './tests/tsconfig.json',
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': tseslintPlugin,
|
||||
},
|
||||
rules: {
|
||||
// Recommended rules for TypeScript
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
'@typescript-eslint/ban-ts-comment': 'warn',
|
||||
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
js,
|
||||
|
||||
Generated
+118
-1
@@ -36,6 +36,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.35.0",
|
||||
"@stylistic/eslint-plugin": "^5.3.1",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.1",
|
||||
"@typescript-eslint/parser": "^8.46.1",
|
||||
"chalk": "^4.1.0",
|
||||
@@ -50,6 +51,7 @@
|
||||
"license-check-and-add": "^4.0.5",
|
||||
"mocha": "^10.6.0",
|
||||
"nodemon": "^3.1.0",
|
||||
"ts-proto": "^2.8.0",
|
||||
"typescript": "^5.4.5",
|
||||
"uglify-js": "^3.17.4",
|
||||
"vite-plugin-static-copy": "^3.1.3",
|
||||
@@ -891,6 +893,7 @@
|
||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
@@ -1146,6 +1149,13 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/protobuf": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.10.0.tgz",
|
||||
"integrity": "sha512-fdRs9PSrBF7QUntpZpq6BTw58fhgGJojgg39m9oFOJGZT+nip9b0so5cYY1oWl5pvemDLr0cPPsH46vwThEbpQ==",
|
||||
"dev": true,
|
||||
"license": "(Apache-2.0 AND BSD-3-Clause)"
|
||||
},
|
||||
"node_modules/@canvas/image-data": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@canvas/image-data/-/image-data-1.0.0.tgz",
|
||||
@@ -3032,6 +3042,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.22.12.tgz",
|
||||
"integrity": "sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jimp/core": "^0.22.12"
|
||||
}
|
||||
@@ -3068,6 +3079,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.22.12.tgz",
|
||||
"integrity": "sha512-xslz2ZoFZOPLY8EZ4dC29m168BtDx95D6K80TzgUi8gqT7LY6CsajWO0FAxDwHz6h0eomHMfyGX0stspBrTKnQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jimp/utils": "^0.22.12"
|
||||
},
|
||||
@@ -3080,6 +3092,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.22.12.tgz",
|
||||
"integrity": "sha512-S0vJADTuh1Q9F+cXAwFPlrKWzDj2F9t/9JAbUvaaDuivpyWuImEKXVz5PUZw2NbpuSHjwssbTpOZ8F13iJX4uw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jimp/utils": "^0.22.12"
|
||||
},
|
||||
@@ -3104,6 +3117,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.22.12.tgz",
|
||||
"integrity": "sha512-xImhTE5BpS8xa+mAN6j4sMRWaUgUDLoaGHhJhpC+r7SKKErYDR0WQV4yCE4gP+N0gozD0F3Ka1LUSaMXrn7ZIA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jimp/utils": "^0.22.12",
|
||||
"tinycolor2": "^1.6.0"
|
||||
@@ -3147,6 +3161,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.22.12.tgz",
|
||||
"integrity": "sha512-FNuUN0OVzRCozx8XSgP9MyLGMxNHHJMFt+LJuFjn1mu3k0VQxrzqbN06yIl46TVejhyAhcq5gLzqmSCHvlcBVw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jimp/utils": "^0.22.12"
|
||||
},
|
||||
@@ -3270,6 +3285,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.22.12.tgz",
|
||||
"integrity": "sha512-3NyTPlPbTnGKDIbaBgQ3HbE6wXbAlFfxHVERmrbqAi8R3r6fQPxpCauA8UVDnieg5eo04D0T8nnnNIX//i/sXg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jimp/utils": "^0.22.12"
|
||||
},
|
||||
@@ -3282,6 +3298,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.22.12.tgz",
|
||||
"integrity": "sha512-9YNEt7BPAFfTls2FGfKBVgwwLUuKqy+E8bDGGEsOqHtbuhbshVGxN2WMZaD4gh5IDWvR+emmmPPWGgaYNYt1gA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jimp/utils": "^0.22.12"
|
||||
},
|
||||
@@ -3297,6 +3314,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.22.12.tgz",
|
||||
"integrity": "sha512-dghs92qM6MhHj0HrV2qAwKPMklQtjNpoYgAB94ysYpsXslhRTiPisueSIELRwZGEr0J0VUxpUY7HgJwlSIgGZw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jimp/utils": "^0.22.12"
|
||||
},
|
||||
@@ -3604,6 +3622,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz",
|
||||
"integrity": "sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
@@ -3613,6 +3632,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.49.1.tgz",
|
||||
"integrity": "sha512-kaNl/T7WzyMUQHQlVq7q0oV4Kev6+0xFwqzofryC66jgGMacd0QH5TwfpbUwSTby+SdAdprAe5UKMvBw4tKS5Q==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.0.0"
|
||||
},
|
||||
@@ -7266,6 +7286,13 @@
|
||||
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz",
|
||||
@@ -7312,6 +7339,7 @@
|
||||
"integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.46.1",
|
||||
"@typescript-eslint/types": "8.46.1",
|
||||
@@ -7854,7 +7882,8 @@
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@xtuc/ieee754": {
|
||||
"version": "1.2.0",
|
||||
@@ -7904,6 +7933,7 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -8584,6 +8614,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.9",
|
||||
"caniuse-lite": "^1.0.30001746",
|
||||
@@ -8802,6 +8833,19 @@
|
||||
"randomstring": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/case-anything": {
|
||||
"version": "2.1.13",
|
||||
"resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.13.tgz",
|
||||
"integrity": "sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.13"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/centra": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/centra/-/centra-2.7.0.tgz",
|
||||
@@ -8816,6 +8860,7 @@
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
|
||||
"integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"assertion-error": "^2.0.1",
|
||||
"check-error": "^2.1.1",
|
||||
@@ -10143,6 +10188,29 @@
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dprint-node": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/dprint-node/-/dprint-node-1.0.8.tgz",
|
||||
"integrity": "sha512-iVKnUtYfGrYcW1ZAlfR/F59cUVL8QIhWoBJoSjkkdua/dkWIgjZfiLMeTjiB06X0ZLkQ0M2C1VbUj/CxkIf1zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-libc": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/dprint-node/node_modules/detect-libc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"detect-libc": "bin/detect-libc.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -10533,6 +10601,7 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz",
|
||||
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -15964,6 +16033,7 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz",
|
||||
"integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -16164,6 +16234,7 @@
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -17664,6 +17735,42 @@
|
||||
"typescript": ">=4.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-poet": {
|
||||
"version": "6.12.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-poet/-/ts-poet-6.12.0.tgz",
|
||||
"integrity": "sha512-xo+iRNMWqyvXpFTaOAvLPA5QAWO6TZrSUs5s4Odaya3epqofBu/fMLHEWl8jPmjhA0s9sgj9sNvF1BmaQlmQkA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"dprint-node": "^1.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-proto": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-2.8.0.tgz",
|
||||
"integrity": "sha512-OtHoiTNYdmtKlkfQZpEVt6wX8wxU2bmHbVNvIopInng0QmzyHapSzLTXKkDToyqJWVNjD18lopERyO64tCBTZQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.0.0",
|
||||
"case-anything": "^2.1.13",
|
||||
"ts-poet": "^6.12.0",
|
||||
"ts-proto-descriptors": "2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"protoc-gen-ts_proto": "protoc-gen-ts_proto"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-proto-descriptors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-proto-descriptors/-/ts-proto-descriptors-2.0.0.tgz",
|
||||
"integrity": "sha512-wHcTH3xIv11jxgkX5OyCSFfw27agpInAd6yh89hKG6zqIXnjW9SYqSER2CVQxdPj4czeOhGagNvZBEbJPy7qkw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
@@ -17753,6 +17860,7 @@
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -17973,6 +18081,7 @@
|
||||
"integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -18238,6 +18347,7 @@
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz",
|
||||
"integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.8",
|
||||
@@ -18287,6 +18397,7 @@
|
||||
"integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@discoveryjs/json-ext": "^0.5.0",
|
||||
"@webpack-cli/configtest": "^2.1.1",
|
||||
@@ -18518,6 +18629,7 @@
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz",
|
||||
"integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@colors/colors": "^1.6.0",
|
||||
"@dabh/diagnostics": "^2.0.8",
|
||||
@@ -18722,6 +18834,7 @@
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
@@ -19055,6 +19168,7 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
@@ -19262,6 +19376,7 @@
|
||||
"version": "3.29.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
@@ -19989,6 +20104,7 @@
|
||||
"version": "3.29.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
@@ -20151,6 +20267,7 @@
|
||||
"version": "3.29.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
|
||||
+4
-1
@@ -13,6 +13,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.35.0",
|
||||
"@stylistic/eslint-plugin": "^5.3.1",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.1",
|
||||
"@typescript-eslint/parser": "^8.46.1",
|
||||
"chalk": "^4.1.0",
|
||||
@@ -27,6 +28,7 @@
|
||||
"license-check-and-add": "^4.0.5",
|
||||
"mocha": "^10.6.0",
|
||||
"nodemon": "^3.1.0",
|
||||
"ts-proto": "^2.8.0",
|
||||
"typescript": "^5.4.5",
|
||||
"uglify-js": "^3.17.4",
|
||||
"vite-plugin-static-copy": "^3.1.3",
|
||||
@@ -44,7 +46,8 @@
|
||||
"check-translations": "node tools/check-translations.js",
|
||||
"prepare": "husky",
|
||||
"build:ts": "tsc",
|
||||
"postinstall": "npm run build:ts"
|
||||
"postinstall": "npm run build:ts",
|
||||
"gen": "./scripts/gen.sh"
|
||||
},
|
||||
"workspaces": [
|
||||
"src/*",
|
||||
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
|
||||
protoc \
|
||||
-I=src/backend/src/filesystem/definitions/proto \
|
||||
--plugin=protoc-gen-ts_proto=$(npm root)/.bin/protoc-gen-ts_proto \
|
||||
--ts_proto_out=src/backend/src/filesystem/definitions/ts \
|
||||
--ts_proto_opt=esModuleInterop=true,outputServices=none,outputJsonMethods=true,useExactTypes=false,snakeToCamel=false \
|
||||
src/backend/src/filesystem/definitions/proto/fsentry.proto
|
||||
@@ -0,0 +1,26 @@
|
||||
syntax = "proto3";
|
||||
|
||||
// The FSEntry from client's (puter-js, http API) perspective, it's used for
|
||||
// - end to end test
|
||||
// - backend logic
|
||||
// - communication between servers
|
||||
message FSEntry {
|
||||
string uuid = 1;
|
||||
// Same as uuid, used for backward compatibility.
|
||||
string uid = 2;
|
||||
|
||||
string name = 3;
|
||||
string path = 4;
|
||||
|
||||
string parent_uuid = 5;
|
||||
// Same as parent_uuid, used for backward compatibility.
|
||||
string parent_uid = 6;
|
||||
// Same as parent_uuid, used for backward compatibility.
|
||||
string parent_id = 7;
|
||||
|
||||
bool is_dir = 8;
|
||||
int64 created = 9;
|
||||
int64 modified = 10;
|
||||
int64 accessed = 11;
|
||||
int64 size = 12;
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
"use strict";
|
||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-ts_proto v2.8.0
|
||||
// protoc v3.21.12
|
||||
// source: fsentry.proto
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.FSEntry = exports.protobufPackage = void 0;
|
||||
/* eslint-disable */
|
||||
const wire_1 = require("@bufbuild/protobuf/wire");
|
||||
exports.protobufPackage = "";
|
||||
function createBaseFSEntry() {
|
||||
return {
|
||||
uuid: "",
|
||||
uid: "",
|
||||
name: "",
|
||||
path: "",
|
||||
parent_uuid: "",
|
||||
parent_uid: "",
|
||||
parent_id: "",
|
||||
is_dir: false,
|
||||
created: 0,
|
||||
modified: 0,
|
||||
accessed: 0,
|
||||
size: 0,
|
||||
};
|
||||
}
|
||||
exports.FSEntry = {
|
||||
encode(message, writer = new wire_1.BinaryWriter()) {
|
||||
if (message.uuid !== "") {
|
||||
writer.uint32(10).string(message.uuid);
|
||||
}
|
||||
if (message.uid !== "") {
|
||||
writer.uint32(18).string(message.uid);
|
||||
}
|
||||
if (message.name !== "") {
|
||||
writer.uint32(26).string(message.name);
|
||||
}
|
||||
if (message.path !== "") {
|
||||
writer.uint32(34).string(message.path);
|
||||
}
|
||||
if (message.parent_uuid !== "") {
|
||||
writer.uint32(42).string(message.parent_uuid);
|
||||
}
|
||||
if (message.parent_uid !== "") {
|
||||
writer.uint32(50).string(message.parent_uid);
|
||||
}
|
||||
if (message.parent_id !== "") {
|
||||
writer.uint32(58).string(message.parent_id);
|
||||
}
|
||||
if (message.is_dir !== false) {
|
||||
writer.uint32(64).bool(message.is_dir);
|
||||
}
|
||||
if (message.created !== 0) {
|
||||
writer.uint32(72).int64(message.created);
|
||||
}
|
||||
if (message.modified !== 0) {
|
||||
writer.uint32(80).int64(message.modified);
|
||||
}
|
||||
if (message.accessed !== 0) {
|
||||
writer.uint32(88).int64(message.accessed);
|
||||
}
|
||||
if (message.size !== 0) {
|
||||
writer.uint32(96).int64(message.size);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
decode(input, length) {
|
||||
const reader = input instanceof wire_1.BinaryReader ? input : new wire_1.BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseFSEntry();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
message.uuid = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
message.uid = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break;
|
||||
}
|
||||
message.name = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 34) {
|
||||
break;
|
||||
}
|
||||
message.path = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 42) {
|
||||
break;
|
||||
}
|
||||
message.parent_uuid = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 6: {
|
||||
if (tag !== 50) {
|
||||
break;
|
||||
}
|
||||
message.parent_uid = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 7: {
|
||||
if (tag !== 58) {
|
||||
break;
|
||||
}
|
||||
message.parent_id = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 8: {
|
||||
if (tag !== 64) {
|
||||
break;
|
||||
}
|
||||
message.is_dir = reader.bool();
|
||||
continue;
|
||||
}
|
||||
case 9: {
|
||||
if (tag !== 72) {
|
||||
break;
|
||||
}
|
||||
message.created = longToNumber(reader.int64());
|
||||
continue;
|
||||
}
|
||||
case 10: {
|
||||
if (tag !== 80) {
|
||||
break;
|
||||
}
|
||||
message.modified = longToNumber(reader.int64());
|
||||
continue;
|
||||
}
|
||||
case 11: {
|
||||
if (tag !== 88) {
|
||||
break;
|
||||
}
|
||||
message.accessed = longToNumber(reader.int64());
|
||||
continue;
|
||||
}
|
||||
case 12: {
|
||||
if (tag !== 96) {
|
||||
break;
|
||||
}
|
||||
message.size = longToNumber(reader.int64());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
fromJSON(object) {
|
||||
return {
|
||||
uuid: isSet(object.uuid) ? globalThis.String(object.uuid) : "",
|
||||
uid: isSet(object.uid) ? globalThis.String(object.uid) : "",
|
||||
name: isSet(object.name) ? globalThis.String(object.name) : "",
|
||||
path: isSet(object.path) ? globalThis.String(object.path) : "",
|
||||
parent_uuid: isSet(object.parent_uuid) ? globalThis.String(object.parent_uuid) : "",
|
||||
parent_uid: isSet(object.parent_uid) ? globalThis.String(object.parent_uid) : "",
|
||||
parent_id: isSet(object.parent_id) ? globalThis.String(object.parent_id) : "",
|
||||
is_dir: isSet(object.is_dir) ? globalThis.Boolean(object.is_dir) : false,
|
||||
created: isSet(object.created) ? globalThis.Number(object.created) : 0,
|
||||
modified: isSet(object.modified) ? globalThis.Number(object.modified) : 0,
|
||||
accessed: isSet(object.accessed) ? globalThis.Number(object.accessed) : 0,
|
||||
size: isSet(object.size) ? globalThis.Number(object.size) : 0,
|
||||
};
|
||||
},
|
||||
toJSON(message) {
|
||||
const obj = {};
|
||||
if (message.uuid !== "") {
|
||||
obj.uuid = message.uuid;
|
||||
}
|
||||
if (message.uid !== "") {
|
||||
obj.uid = message.uid;
|
||||
}
|
||||
if (message.name !== "") {
|
||||
obj.name = message.name;
|
||||
}
|
||||
if (message.path !== "") {
|
||||
obj.path = message.path;
|
||||
}
|
||||
if (message.parent_uuid !== "") {
|
||||
obj.parent_uuid = message.parent_uuid;
|
||||
}
|
||||
if (message.parent_uid !== "") {
|
||||
obj.parent_uid = message.parent_uid;
|
||||
}
|
||||
if (message.parent_id !== "") {
|
||||
obj.parent_id = message.parent_id;
|
||||
}
|
||||
if (message.is_dir !== false) {
|
||||
obj.is_dir = message.is_dir;
|
||||
}
|
||||
if (message.created !== 0) {
|
||||
obj.created = Math.round(message.created);
|
||||
}
|
||||
if (message.modified !== 0) {
|
||||
obj.modified = Math.round(message.modified);
|
||||
}
|
||||
if (message.accessed !== 0) {
|
||||
obj.accessed = Math.round(message.accessed);
|
||||
}
|
||||
if (message.size !== 0) {
|
||||
obj.size = Math.round(message.size);
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
create(base) {
|
||||
return exports.FSEntry.fromPartial(base ?? {});
|
||||
},
|
||||
fromPartial(object) {
|
||||
const message = createBaseFSEntry();
|
||||
message.uuid = object.uuid ?? "";
|
||||
message.uid = object.uid ?? "";
|
||||
message.name = object.name ?? "";
|
||||
message.path = object.path ?? "";
|
||||
message.parent_uuid = object.parent_uuid ?? "";
|
||||
message.parent_uid = object.parent_uid ?? "";
|
||||
message.parent_id = object.parent_id ?? "";
|
||||
message.is_dir = object.is_dir ?? false;
|
||||
message.created = object.created ?? 0;
|
||||
message.modified = object.modified ?? 0;
|
||||
message.accessed = object.accessed ?? 0;
|
||||
message.size = object.size ?? 0;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
function longToNumber(int64) {
|
||||
const num = globalThis.Number(int64.toString());
|
||||
if (num > globalThis.Number.MAX_SAFE_INTEGER) {
|
||||
throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER");
|
||||
}
|
||||
if (num < globalThis.Number.MIN_SAFE_INTEGER) {
|
||||
throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER");
|
||||
}
|
||||
return num;
|
||||
}
|
||||
function isSet(value) {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
//# sourceMappingURL=fsentry.js.map
|
||||
@@ -0,0 +1,315 @@
|
||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-ts_proto v2.8.0
|
||||
// protoc v3.21.12
|
||||
// source: fsentry.proto
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||
|
||||
export const protobufPackage = "";
|
||||
|
||||
/**
|
||||
* The FSEntry from client's (puter-js, http API) perspective, it's used for
|
||||
* - end to end test
|
||||
* - backend logic
|
||||
* - communication between servers
|
||||
*/
|
||||
export interface FSEntry {
|
||||
uuid: string;
|
||||
/** Same as uuid, used for backward compatibility. */
|
||||
uid: string;
|
||||
name: string;
|
||||
path: string;
|
||||
parent_uuid: string;
|
||||
/** Same as parent_uuid, used for backward compatibility. */
|
||||
parent_uid: string;
|
||||
/** Same as parent_uuid, used for backward compatibility. */
|
||||
parent_id: string;
|
||||
is_dir: boolean;
|
||||
created: number;
|
||||
modified: number;
|
||||
accessed: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
function createBaseFSEntry(): FSEntry {
|
||||
return {
|
||||
uuid: "",
|
||||
uid: "",
|
||||
name: "",
|
||||
path: "",
|
||||
parent_uuid: "",
|
||||
parent_uid: "",
|
||||
parent_id: "",
|
||||
is_dir: false,
|
||||
created: 0,
|
||||
modified: 0,
|
||||
accessed: 0,
|
||||
size: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export const FSEntry: MessageFns<FSEntry> = {
|
||||
encode(message: FSEntry, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.uuid !== "") {
|
||||
writer.uint32(10).string(message.uuid);
|
||||
}
|
||||
if (message.uid !== "") {
|
||||
writer.uint32(18).string(message.uid);
|
||||
}
|
||||
if (message.name !== "") {
|
||||
writer.uint32(26).string(message.name);
|
||||
}
|
||||
if (message.path !== "") {
|
||||
writer.uint32(34).string(message.path);
|
||||
}
|
||||
if (message.parent_uuid !== "") {
|
||||
writer.uint32(42).string(message.parent_uuid);
|
||||
}
|
||||
if (message.parent_uid !== "") {
|
||||
writer.uint32(50).string(message.parent_uid);
|
||||
}
|
||||
if (message.parent_id !== "") {
|
||||
writer.uint32(58).string(message.parent_id);
|
||||
}
|
||||
if (message.is_dir !== false) {
|
||||
writer.uint32(64).bool(message.is_dir);
|
||||
}
|
||||
if (message.created !== 0) {
|
||||
writer.uint32(72).int64(message.created);
|
||||
}
|
||||
if (message.modified !== 0) {
|
||||
writer.uint32(80).int64(message.modified);
|
||||
}
|
||||
if (message.accessed !== 0) {
|
||||
writer.uint32(88).int64(message.accessed);
|
||||
}
|
||||
if (message.size !== 0) {
|
||||
writer.uint32(96).int64(message.size);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): FSEntry {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseFSEntry();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.uuid = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.uid = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.name = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 34) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.path = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 42) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.parent_uuid = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 6: {
|
||||
if (tag !== 50) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.parent_uid = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 7: {
|
||||
if (tag !== 58) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.parent_id = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 8: {
|
||||
if (tag !== 64) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.is_dir = reader.bool();
|
||||
continue;
|
||||
}
|
||||
case 9: {
|
||||
if (tag !== 72) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.created = longToNumber(reader.int64());
|
||||
continue;
|
||||
}
|
||||
case 10: {
|
||||
if (tag !== 80) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.modified = longToNumber(reader.int64());
|
||||
continue;
|
||||
}
|
||||
case 11: {
|
||||
if (tag !== 88) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.accessed = longToNumber(reader.int64());
|
||||
continue;
|
||||
}
|
||||
case 12: {
|
||||
if (tag !== 96) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.size = longToNumber(reader.int64());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): FSEntry {
|
||||
return {
|
||||
uuid: isSet(object.uuid) ? globalThis.String(object.uuid) : "",
|
||||
uid: isSet(object.uid) ? globalThis.String(object.uid) : "",
|
||||
name: isSet(object.name) ? globalThis.String(object.name) : "",
|
||||
path: isSet(object.path) ? globalThis.String(object.path) : "",
|
||||
parent_uuid: isSet(object.parent_uuid) ? globalThis.String(object.parent_uuid) : "",
|
||||
parent_uid: isSet(object.parent_uid) ? globalThis.String(object.parent_uid) : "",
|
||||
parent_id: isSet(object.parent_id) ? globalThis.String(object.parent_id) : "",
|
||||
is_dir: isSet(object.is_dir) ? globalThis.Boolean(object.is_dir) : false,
|
||||
created: isSet(object.created) ? globalThis.Number(object.created) : 0,
|
||||
modified: isSet(object.modified) ? globalThis.Number(object.modified) : 0,
|
||||
accessed: isSet(object.accessed) ? globalThis.Number(object.accessed) : 0,
|
||||
size: isSet(object.size) ? globalThis.Number(object.size) : 0,
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: FSEntry): unknown {
|
||||
const obj: any = {};
|
||||
if (message.uuid !== "") {
|
||||
obj.uuid = message.uuid;
|
||||
}
|
||||
if (message.uid !== "") {
|
||||
obj.uid = message.uid;
|
||||
}
|
||||
if (message.name !== "") {
|
||||
obj.name = message.name;
|
||||
}
|
||||
if (message.path !== "") {
|
||||
obj.path = message.path;
|
||||
}
|
||||
if (message.parent_uuid !== "") {
|
||||
obj.parent_uuid = message.parent_uuid;
|
||||
}
|
||||
if (message.parent_uid !== "") {
|
||||
obj.parent_uid = message.parent_uid;
|
||||
}
|
||||
if (message.parent_id !== "") {
|
||||
obj.parent_id = message.parent_id;
|
||||
}
|
||||
if (message.is_dir !== false) {
|
||||
obj.is_dir = message.is_dir;
|
||||
}
|
||||
if (message.created !== 0) {
|
||||
obj.created = Math.round(message.created);
|
||||
}
|
||||
if (message.modified !== 0) {
|
||||
obj.modified = Math.round(message.modified);
|
||||
}
|
||||
if (message.accessed !== 0) {
|
||||
obj.accessed = Math.round(message.accessed);
|
||||
}
|
||||
if (message.size !== 0) {
|
||||
obj.size = Math.round(message.size);
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create(base?: DeepPartial<FSEntry>): FSEntry {
|
||||
return FSEntry.fromPartial(base ?? {});
|
||||
},
|
||||
fromPartial(object: DeepPartial<FSEntry>): FSEntry {
|
||||
const message = createBaseFSEntry();
|
||||
message.uuid = object.uuid ?? "";
|
||||
message.uid = object.uid ?? "";
|
||||
message.name = object.name ?? "";
|
||||
message.path = object.path ?? "";
|
||||
message.parent_uuid = object.parent_uuid ?? "";
|
||||
message.parent_uid = object.parent_uid ?? "";
|
||||
message.parent_id = object.parent_id ?? "";
|
||||
message.is_dir = object.is_dir ?? false;
|
||||
message.created = object.created ?? 0;
|
||||
message.modified = object.modified ?? 0;
|
||||
message.accessed = object.accessed ?? 0;
|
||||
message.size = object.size ?? 0;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||
|
||||
export type DeepPartial<T> = T extends Builtin ? T
|
||||
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
||||
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: Partial<T>;
|
||||
|
||||
function longToNumber(int64: { toString(): string }): number {
|
||||
const num = globalThis.Number(int64.toString());
|
||||
if (num > globalThis.Number.MAX_SAFE_INTEGER) {
|
||||
throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER");
|
||||
}
|
||||
if (num < globalThis.Number.MIN_SAFE_INTEGER) {
|
||||
throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER");
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
function isSet(value: any): boolean {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
export interface MessageFns<T> {
|
||||
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||
fromJSON(object: any): T;
|
||||
toJSON(message: T): unknown;
|
||||
create(base?: DeepPartial<T>): T;
|
||||
fromPartial(object: DeepPartial<T>): T;
|
||||
}
|
||||
Generated
-2863
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "puter-integration-tests",
|
||||
"version": "1.0.0",
|
||||
"description": "Integration tests for Puter",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "mocha captcha/**/*.test.js",
|
||||
"test:auth": "mocha captcha/authentication-flow.test.js",
|
||||
"test:ui": "mocha captcha/ui-behavior.test.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.3.7",
|
||||
"express": "^4.18.2",
|
||||
"jsdom": "^21.1.0",
|
||||
"mocha": "^10.2.0",
|
||||
"sinon": "^15.2.0",
|
||||
"body-parser": "^1.20.2",
|
||||
"supertest": "^6.3.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
## Table of Contents
|
||||
|
||||
- [Summary](#summary)
|
||||
- [How to use](#how-to-use)
|
||||
- [Initialize the Client Config](#initialize-the-client-config)
|
||||
- [Run API-Tester (test http API)](#run-api-tester-test-http-api)
|
||||
- [Run Playwright (test puter-js API with browser environment)](#run-playwright-test-puter-js-api-with-browser-environment)
|
||||
- [Run Vitest (test puter-js API with node environment)](#run-vitest-test-puter-js-api-with-node-environment)
|
||||
|
||||
## Summary
|
||||
|
||||
End-to-end tests for puter-js and http API.
|
||||
|
||||
## How to use
|
||||
|
||||
### Initialize the Client Config
|
||||
|
||||
1. Start a backend server:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
2. Copy `example-client-config.yaml` and edit the `auth_token` field. (`auth_token` can be obtained by logging in on the webpage and typing `puter.authToken` in Developer Tools's console)
|
||||
|
||||
```bash
|
||||
cp ./tests/example-client-config.yaml ./tests/client-config.yaml
|
||||
```
|
||||
|
||||
### Run API-Tester (test http API)
|
||||
|
||||
```bash
|
||||
node ./tests/api-tester/apitest.js --unit --stop-on-failure
|
||||
```
|
||||
|
||||
### Run Playwright (test puter-js API with browser environment)
|
||||
|
||||
```bash
|
||||
cd ./tests/playwright
|
||||
npm install
|
||||
npx playwright install --with-deps
|
||||
npx playwright test
|
||||
```
|
||||
|
||||
### Run Vitest (test puter-js API with node environment)
|
||||
|
||||
```bash
|
||||
npm run test:puterjs-api
|
||||
```
|
||||
@@ -16,7 +16,7 @@ try {
|
||||
options: {
|
||||
config: {
|
||||
type: 'string',
|
||||
default: './tools/api-tester/config.yml',
|
||||
default: './tests/client-config.yaml',
|
||||
},
|
||||
report: {
|
||||
type: 'string',
|
||||
@@ -20,10 +20,10 @@ module.exports = class TestSDK {
|
||||
this.httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false
|
||||
})
|
||||
const url_origin = new url.URL(conf.url).origin;
|
||||
const url_origin = new url.URL(conf.api_url).origin;
|
||||
this.headers_ = {
|
||||
'Origin': url_origin,
|
||||
'Authorization': `Bearer ${conf.token}`
|
||||
'Authorization': `Bearer ${conf.auth_token}`
|
||||
};
|
||||
|
||||
this.installAPIMethodShorthands_();
|
||||
@@ -332,7 +332,7 @@ module.exports = class TestSDK {
|
||||
}
|
||||
|
||||
getURL (...path) {
|
||||
const apiURL = new url.URL(this.conf.url);
|
||||
const apiURL = new url.URL(this.conf.api_url);
|
||||
apiURL.pathname = path_.posix.join(
|
||||
apiURL.pathname,
|
||||
...path
|
||||
Executable
+79
@@ -0,0 +1,79 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# Usage:
|
||||
# ./tools/api-tester/ci/run.py
|
||||
|
||||
import time
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
import yaml
|
||||
|
||||
import cxc_toolkit
|
||||
|
||||
import common
|
||||
|
||||
|
||||
def update_server_config():
|
||||
# Load the config file
|
||||
config_file = f"{os.getcwd()}/volatile/config/config.json"
|
||||
|
||||
with open(config_file, "r") as f:
|
||||
config = json.load(f)
|
||||
|
||||
# Ensure services and mountpoint sections exist
|
||||
if "services" not in config:
|
||||
config["services"] = {}
|
||||
if "mountpoint" not in config["services"]:
|
||||
config["services"]["mountpoint"] = {}
|
||||
if "mountpoints" not in config["services"]["mountpoint"]:
|
||||
config["services"]["mountpoint"]["mountpoints"] = {}
|
||||
|
||||
# Add the mountpoint configuration
|
||||
mountpoint_config = {
|
||||
"/": {"mounter": "puterfs"},
|
||||
"/admin/tmp": {"mounter": "memoryfs"},
|
||||
}
|
||||
|
||||
# Merge mountpoints (overwrite existing ones)
|
||||
config["services"]["mountpoint"]["mountpoints"].update(mountpoint_config)
|
||||
|
||||
# Write the updated config back
|
||||
with open(config_file, "w") as f:
|
||||
json.dump(config, f, indent=2)
|
||||
|
||||
|
||||
def run():
|
||||
# =========================================================================
|
||||
# free the port 4100
|
||||
# =========================================================================
|
||||
cxc_toolkit.exec.run_command("fuser -k 4100/tcp", ignore_failure=True)
|
||||
|
||||
# =========================================================================
|
||||
# config server
|
||||
# =========================================================================
|
||||
cxc_toolkit.exec.run_command("npm install")
|
||||
common.init_backend_config()
|
||||
admin_password = common.get_admin_password()
|
||||
update_server_config()
|
||||
|
||||
# =========================================================================
|
||||
# config client
|
||||
# =========================================================================
|
||||
cxc_toolkit.exec.run_background("npm start")
|
||||
# wait 10s for the server to start
|
||||
time.sleep(10)
|
||||
|
||||
token = common.get_token(admin_password)
|
||||
common.init_client_config(token)
|
||||
|
||||
# =========================================================================
|
||||
# run the test
|
||||
# =========================================================================
|
||||
cxc_toolkit.exec.run_command(
|
||||
"node ./tests/api-tester/apitest.js --unit --stop-on-failure"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -0,0 +1,94 @@
|
||||
import os
|
||||
import time
|
||||
|
||||
import cxc_toolkit
|
||||
import requests
|
||||
import yaml
|
||||
|
||||
|
||||
PUTER_ROOT = os.getcwd()
|
||||
|
||||
|
||||
def init_backend_config():
|
||||
"""
|
||||
Initialize a default config in ./volatile/config/config.json.
|
||||
"""
|
||||
# init config.json
|
||||
server_process = cxc_toolkit.exec.run_background("npm start")
|
||||
|
||||
# wait 10s for the server to start
|
||||
time.sleep(10)
|
||||
server_process.terminate()
|
||||
|
||||
|
||||
# Possible reasons for failure:
|
||||
# - The backend server is not initialized, run "npm start" to initialize it.
|
||||
# - Admin password in the kv service is flushed, have to trigger the creation of the admin user.
|
||||
# 1. sqlite3 ./volatile/runtime/puter-database.sqlite
|
||||
# 2. DELETE FROM user WHERE username = 'admin';
|
||||
def get_admin_password() -> str:
|
||||
"""
|
||||
Get the admin password from the backend server, throw an error if not found.
|
||||
"""
|
||||
LOG_PATH = "/tmp/backend.log"
|
||||
backend_process = cxc_toolkit.exec.run_background("npm start", log_path=LOG_PATH)
|
||||
|
||||
# NB: run_command + kill_on_output may wait indefinitely, use run_background + hard limit instead
|
||||
time.sleep(10)
|
||||
|
||||
backend_process.terminate()
|
||||
|
||||
# read the log file
|
||||
with open(LOG_PATH, "r") as f:
|
||||
lines = f.readlines()
|
||||
for line in lines:
|
||||
if "password for admin" in line:
|
||||
print(f"found password line: ---{line}---")
|
||||
admin_password = line.split("password for admin is:")[1].strip()
|
||||
print(f"Extracted admin password: {admin_password}")
|
||||
return admin_password
|
||||
|
||||
raise RuntimeError(f"no admin password found, check {LOG_PATH} for details")
|
||||
|
||||
|
||||
def get_token(admin_password: str) -> str:
|
||||
"""
|
||||
Get the token from the backend server, throw an error if not found.
|
||||
"""
|
||||
server_url = "http://api.puter.localhost:4100/login"
|
||||
login_data = {"username": "admin", "password": admin_password}
|
||||
response = requests.post(
|
||||
server_url,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"Origin": "http://api.puter.localhost:4100",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
|
||||
},
|
||||
json=login_data,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
response_json = response.json()
|
||||
if "token" not in response_json:
|
||||
raise RuntimeError("No token found")
|
||||
return response_json["token"]
|
||||
|
||||
|
||||
def init_client_config(token: str):
|
||||
"""
|
||||
Initialize a client config in ./tests/client-config.yaml.
|
||||
"""
|
||||
example_config_path = f"{PUTER_ROOT}/tests/example-client-config.yaml"
|
||||
config_path = f"{PUTER_ROOT}/tests/client-config.yaml"
|
||||
|
||||
# load
|
||||
with open(example_config_path, "r") as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
# update
|
||||
config["auth_token"] = token
|
||||
|
||||
# write
|
||||
with open(config_path, "w") as f:
|
||||
yaml.dump(config, f, default_flow_style=False, indent=2)
|
||||
Executable
+143
@@ -0,0 +1,143 @@
|
||||
#! /usr/bin/env python3
|
||||
|
||||
# test the client-replica feature
|
||||
# - need browser environment (following features require browser environment: fs naive-cache, client-replica, wspush)
|
||||
# - test multi-server setup
|
||||
# - test change-propagation-time
|
||||
# - test local read
|
||||
# - test consistency
|
||||
|
||||
# first stage: test in the existing workspace, test single server + multiple sessions
|
||||
# second stage: test from a fresh clone, test single server + multiple sessions
|
||||
# third stage: test in the existing workspace, test multiple servers + multiple sessions
|
||||
# fourth stage: test from a fresh clone, test multiple servers + multiple sessions
|
||||
|
||||
import time
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
import yaml
|
||||
|
||||
import cxc_toolkit
|
||||
|
||||
import common
|
||||
|
||||
ENABLE_FS_TREE_MANAGER = False
|
||||
PUTER_ROOT = common.PUTER_ROOT
|
||||
|
||||
|
||||
def init_backend_config():
|
||||
"""
|
||||
TODO: replace with common.init_backend_config
|
||||
"""
|
||||
# init config.json
|
||||
server_process = cxc_toolkit.exec.run_background("npm start")
|
||||
# wait 10s for the server to start
|
||||
time.sleep(10)
|
||||
server_process.terminate()
|
||||
|
||||
example_config_path = f"{PUTER_ROOT}/volatile/config/config.json"
|
||||
config_path = f"{PUTER_ROOT}/volatile/config/config.json"
|
||||
|
||||
# load
|
||||
with open(example_config_path, "r") as f:
|
||||
config = json.load(f)
|
||||
|
||||
# update
|
||||
if ENABLE_FS_TREE_MANAGER:
|
||||
config["services"]["client-replica"] = {
|
||||
"enabled": True,
|
||||
"fs_tree_manager_url": "localhost:50052",
|
||||
}
|
||||
|
||||
# write
|
||||
with open(config_path, "w") as f:
|
||||
json.dump(config, f, indent=2)
|
||||
|
||||
|
||||
def init_fs_tree_manager_config():
|
||||
example_config_path = f"{PUTER_ROOT}/src/fs_tree_manager/example-config.yaml"
|
||||
config_path = f"{PUTER_ROOT}/src/fs_tree_manager/config.yaml"
|
||||
|
||||
# load
|
||||
with open(example_config_path, "r") as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
# update
|
||||
config["database"]["driver"] = "sqlite3"
|
||||
config["database"]["sqlite3"][
|
||||
"path"
|
||||
] = f"{PUTER_ROOT}/volatile/runtime/puter-database.sqlite"
|
||||
|
||||
# write
|
||||
with open(config_path, "w") as f:
|
||||
yaml.dump(config, f, default_flow_style=False, indent=2)
|
||||
|
||||
print(f"fs-tree-manager config initialized at {config_path}")
|
||||
|
||||
|
||||
def run():
|
||||
# =========================================================================
|
||||
# clean ports
|
||||
# =========================================================================
|
||||
|
||||
# clean port 4100 for backend server
|
||||
cxc_toolkit.exec.run_command("fuser -k 4100/tcp", ignore_failure=True)
|
||||
|
||||
# clean port 50052 for fs-tree-manager server
|
||||
cxc_toolkit.exec.run_command("fuser -k 50052/tcp", ignore_failure=True)
|
||||
|
||||
# =========================================================================
|
||||
# config server
|
||||
# =========================================================================
|
||||
cxc_toolkit.exec.run_command("npm install")
|
||||
init_backend_config()
|
||||
admin_password = common.get_admin_password()
|
||||
|
||||
# =========================================================================
|
||||
# start backend server
|
||||
# =========================================================================
|
||||
cxc_toolkit.exec.run_background(
|
||||
"npm start", work_dir=PUTER_ROOT, log_path="/tmp/backend.log"
|
||||
)
|
||||
# wait 10s for the server to start
|
||||
time.sleep(10)
|
||||
|
||||
# =========================================================================
|
||||
# config client
|
||||
# =========================================================================
|
||||
token = common.get_token(admin_password)
|
||||
common.init_client_config(token)
|
||||
|
||||
# =========================================================================
|
||||
# start fs-tree-manager server
|
||||
# =========================================================================
|
||||
if ENABLE_FS_TREE_MANAGER:
|
||||
init_fs_tree_manager_config()
|
||||
|
||||
cxc_toolkit.exec.run_command(
|
||||
"go mod download",
|
||||
work_dir=f"{PUTER_ROOT}/src/fs_tree_manager",
|
||||
)
|
||||
|
||||
cxc_toolkit.exec.run_background(
|
||||
"go run server.go",
|
||||
work_dir=f"{PUTER_ROOT}/src/fs_tree_manager",
|
||||
log_path="/tmp/fs-tree-manager.log",
|
||||
)
|
||||
|
||||
# NB: "go mod download" and "go run server.go" may take a long time in github
|
||||
# action environment, I don't know why.
|
||||
time.sleep(60)
|
||||
|
||||
# =========================================================================
|
||||
# run the test
|
||||
# =========================================================================
|
||||
cxc_toolkit.exec.run_command(
|
||||
"npx playwright test",
|
||||
work_dir=f"{PUTER_ROOT}/tests/playwright",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -1,3 +1,3 @@
|
||||
cxc-toolkit>=0.9.2
|
||||
cxc-toolkit>=1.0.0
|
||||
requests==2.32.4
|
||||
PyYAML==6.0.2
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
#! /usr/bin/env python3
|
||||
|
||||
import time
|
||||
|
||||
import cxc_toolkit
|
||||
|
||||
import common
|
||||
|
||||
|
||||
def run():
|
||||
# =========================================================================
|
||||
# clean ports
|
||||
# =========================================================================
|
||||
|
||||
# clean port 4100 for backend server
|
||||
cxc_toolkit.exec.run_command("fuser -k 4100/tcp", ignore_failure=True)
|
||||
|
||||
# clean port 50052 for fs-tree-manager server
|
||||
cxc_toolkit.exec.run_command("fuser -k 50052/tcp", ignore_failure=True)
|
||||
|
||||
# =========================================================================
|
||||
# config server
|
||||
# =========================================================================
|
||||
cxc_toolkit.exec.run_command("npm install")
|
||||
common.init_backend_config()
|
||||
admin_password = common.get_admin_password()
|
||||
|
||||
# =========================================================================
|
||||
# start backend server
|
||||
# =========================================================================
|
||||
cxc_toolkit.exec.run_background(
|
||||
"npm start", work_dir=common.PUTER_ROOT, log_path="/tmp/backend.log"
|
||||
)
|
||||
# wait 10s for the server to start
|
||||
time.sleep(10)
|
||||
|
||||
# =========================================================================
|
||||
# config client
|
||||
# =========================================================================
|
||||
token = common.get_token(admin_password)
|
||||
common.init_client_config(token)
|
||||
|
||||
# =========================================================================
|
||||
# run the test
|
||||
# =========================================================================
|
||||
cxc_toolkit.exec.run_command(
|
||||
"npm run test:puterjs-api",
|
||||
work_dir=common.PUTER_ROOT,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -0,0 +1,9 @@
|
||||
api_url: http://api.puter.localhost:4100
|
||||
frontend_url: http://puter.localhost:4100
|
||||
username: admin
|
||||
auth_token: <your-token>
|
||||
mountpoints:
|
||||
- path: /
|
||||
provider: puterfs
|
||||
- path: /admin/tmp
|
||||
provider: memoryfs
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "puter-integration-tests",
|
||||
"version": "1.0.0",
|
||||
"description": "Integration tests for Puter",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "mocha captcha/**/*.test.js",
|
||||
"test:auth": "mocha captcha/authentication-flow.test.js",
|
||||
"test:ui": "mocha captcha/ui-behavior.test.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.3.7",
|
||||
"express": "^4.18.2",
|
||||
"jsdom": "^21.1.0",
|
||||
"mocha": "^10.2.0",
|
||||
"sinon": "^15.2.0",
|
||||
"body-parser": "^1.20.2",
|
||||
"supertest": "^6.3.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Playwright Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
- name: Run Playwright tests
|
||||
run: npx playwright test
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
# Playwright
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/playwright/.auth/
|
||||
@@ -0,0 +1,32 @@
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import * as yaml from 'yaml'
|
||||
|
||||
// Strong-typed configuration interface
|
||||
export interface TestConfig {
|
||||
api_url: string
|
||||
frontend_url: string
|
||||
username: string
|
||||
auth_token: string
|
||||
}
|
||||
|
||||
// Singleton configuration loader - loads config only once
|
||||
let config: TestConfig | null = null
|
||||
|
||||
export function getTestConfig(): TestConfig {
|
||||
if (config === null) {
|
||||
const configPath = path.join(__dirname, '../../client-config.yaml')
|
||||
const rawConfig = yaml.parse(fs.readFileSync(configPath, 'utf8'))
|
||||
|
||||
// Validate required fields
|
||||
if (!rawConfig.api_url || !rawConfig.frontend_url || !rawConfig.username || !rawConfig.auth_token) {
|
||||
throw new Error('Invalid test configuration: missing required fields')
|
||||
}
|
||||
|
||||
config = rawConfig as TestConfig
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
// Export the typed configuration
|
||||
export const testConfig: TestConfig = getTestConfig()
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "playwright",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.56.0",
|
||||
"@types/node": "^24.7.2",
|
||||
"yaml": "^2.4.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// import dotenv from 'dotenv';
|
||||
// import path from 'path';
|
||||
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
|
||||
// Disable parallelism since puter fs doesn't provide concurrent safety.
|
||||
workers: 1,
|
||||
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('')`. */
|
||||
// baseURL: 'http://localhost:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
|
||||
// {
|
||||
// name: 'firefox',
|
||||
// use: { ...devices['Desktop Firefox'] },
|
||||
// },
|
||||
|
||||
// {
|
||||
// name: 'webkit',
|
||||
// use: { ...devices['Desktop Safari'] },
|
||||
// },
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// url: 'http://localhost:3000',
|
||||
// reuseExistingServer: !process.env.CI,
|
||||
// },
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
// puter.fs.batch doesn't work well, add tests later.
|
||||
@@ -0,0 +1,232 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { BASE_PATH, test } from './fixtures';
|
||||
|
||||
test('copy file with path format', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/copy_cart_1`;
|
||||
const sourceFile = `${testPath}/a/a_file.txt`;
|
||||
const destDir = `${testPath}/b`;
|
||||
|
||||
// Setup: create directory structure
|
||||
await page.evaluate(async ({ testPath, sourceFile, destDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(testPath);
|
||||
await puter.fs.mkdir(`${testPath}/a`);
|
||||
await puter.fs.write(sourceFile, 'file a contents\n');
|
||||
await puter.fs.mkdir(destDir);
|
||||
}, { testPath, sourceFile, destDir });
|
||||
|
||||
// Copy file
|
||||
const result = await page.evaluate(async ({ sourceFile, destDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.copy(sourceFile, destDir);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('copy error:', error);
|
||||
return null;
|
||||
}
|
||||
}, { sourceFile, destDir });
|
||||
|
||||
console.log('result: ', result);
|
||||
|
||||
expect(result[0]).toBeTruthy();
|
||||
expect(result[0].copied.name).toBe('a_file.txt');
|
||||
});
|
||||
|
||||
test('copy file with specified name', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/copy_cart_2`;
|
||||
const sourceFile = `${testPath}/a/a_file.txt`;
|
||||
const destDir = `${testPath}/b`;
|
||||
const newName = 'x_renamed';
|
||||
|
||||
// Setup: create directory structure
|
||||
await page.evaluate(async ({ testPath, sourceFile, destDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(testPath);
|
||||
await puter.fs.mkdir(`${testPath}/a`);
|
||||
await puter.fs.write(sourceFile, 'file a contents\n');
|
||||
await puter.fs.mkdir(destDir);
|
||||
}, { testPath, sourceFile, destDir });
|
||||
|
||||
// Copy file with new name
|
||||
const result = await page.evaluate(async ({ sourceFile, destDir, newName }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.copy(sourceFile, destDir, { newName });
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('copy error:', error);
|
||||
return null;
|
||||
}
|
||||
}, { sourceFile, destDir, newName });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result[0].copied.name).toBe(newName);
|
||||
});
|
||||
|
||||
test('copy file with overwrite', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/copy_cart_3`;
|
||||
const sourceFile = `${testPath}/a/a_file.txt`;
|
||||
const destDir = `${testPath}/b`;
|
||||
|
||||
// Setup: create directory structure
|
||||
await page.evaluate(async ({ testPath, sourceFile, destDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(testPath);
|
||||
await puter.fs.mkdir(`${testPath}/a`);
|
||||
await puter.fs.write(sourceFile, 'file a contents\n');
|
||||
await puter.fs.mkdir(destDir);
|
||||
await puter.fs.write(`${destDir}/a_file.txt`, 'existing file\n');
|
||||
}, { testPath, sourceFile, destDir });
|
||||
|
||||
// Copy file with overwrite
|
||||
const result = await page.evaluate(async ({ sourceFile, destDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.copy(sourceFile, destDir, { overwrite: true });
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('copy error:', error);
|
||||
return null;
|
||||
}
|
||||
}, { sourceFile, destDir });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result[0]).toBeTruthy();
|
||||
});
|
||||
|
||||
test('copy file without overwrite to directory with existing file should error', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/copy_cart_4`;
|
||||
const sourceFile = `${testPath}/a/a_file.txt`;
|
||||
const destDir = `${testPath}/b`;
|
||||
|
||||
// Setup: create directory structure
|
||||
await page.evaluate(async ({ testPath, sourceFile, destDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(testPath);
|
||||
await puter.fs.mkdir(`${testPath}/a`);
|
||||
await puter.fs.write(sourceFile, 'file a contents\n');
|
||||
await puter.fs.mkdir(destDir);
|
||||
await puter.fs.write(`${destDir}/a_file.txt`, 'existing file\n');
|
||||
}, { testPath, sourceFile, destDir });
|
||||
|
||||
// Attempt copy without overwrite (should fail)
|
||||
const result = await page.evaluate(async ({ sourceFile, destDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.copy(sourceFile, destDir);
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, code: error.code, entry_name: error.entry_name };
|
||||
}
|
||||
}, { sourceFile, destDir });
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.code).toBeTruthy();
|
||||
});
|
||||
|
||||
test('copy file to file destination should error', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/copy_cart_6`;
|
||||
const sourceFile = `${testPath}/a/a_file.txt`;
|
||||
const destFile = `${testPath}/b`;
|
||||
|
||||
// Setup: create file as destination (not directory)
|
||||
await page.evaluate(async ({ testPath, sourceFile, destFile }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(testPath);
|
||||
await puter.fs.mkdir(`${testPath}/a`);
|
||||
await puter.fs.write(sourceFile, 'file a contents\n');
|
||||
await puter.fs.write(destFile, 'placeholder\n');
|
||||
}, { testPath, sourceFile, destFile });
|
||||
|
||||
// Attempt copy with specified name to file destination (should error)
|
||||
const result = await page.evaluate(async ({ sourceFile, destFile }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.copy(sourceFile, destFile, { newName: 'x_renamed' });
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, code: error.code };
|
||||
}
|
||||
}, { sourceFile, destFile });
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.code).toBe('dest_is_not_a_directory');
|
||||
});
|
||||
|
||||
test('copy empty directory', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/copy_cart_7`;
|
||||
const sourceDir = `${testPath}/a/a_directory`;
|
||||
const destDir = `${testPath}/b`;
|
||||
|
||||
// Setup: create empty directory
|
||||
await page.evaluate(async ({ testPath, sourceDir, destDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(testPath);
|
||||
await puter.fs.mkdir(`${testPath}/a`);
|
||||
await puter.fs.mkdir(sourceDir);
|
||||
await puter.fs.mkdir(destDir);
|
||||
}, { testPath, sourceDir, destDir });
|
||||
|
||||
// Copy directory
|
||||
const result = await page.evaluate(async ({ sourceDir, destDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.copy(sourceDir, destDir);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('copy error:', error);
|
||||
return null;
|
||||
}
|
||||
}, { sourceDir, destDir });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result[0].copied.name).toBe('a_directory');
|
||||
});
|
||||
|
||||
test('copy full directory', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/copy_cart_8`;
|
||||
const sourceDir = `${testPath}/a/a_directory`;
|
||||
const destDir = `${testPath}/b`;
|
||||
|
||||
// Setup: create full directory with file, empty dir, and nested dir
|
||||
await page.evaluate(async ({ testPath, sourceDir, destDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(testPath);
|
||||
await puter.fs.mkdir(`${testPath}/a`);
|
||||
await puter.fs.mkdir(sourceDir);
|
||||
await puter.fs.write(`${sourceDir}/a_file.txt`, 'file a contents\n');
|
||||
await puter.fs.mkdir(`${sourceDir}/b_directory`);
|
||||
await puter.fs.write(`${sourceDir}/b_directory/b_file.txt`, 'file b contents\n');
|
||||
await puter.fs.mkdir(`${sourceDir}/c_directory`);
|
||||
await puter.fs.mkdir(destDir);
|
||||
}, { testPath, sourceDir, destDir });
|
||||
|
||||
// Copy directory
|
||||
const result = await page.evaluate(async ({ sourceDir, destDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.copy(sourceDir, destDir);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('copy error:', error);
|
||||
return null;
|
||||
}
|
||||
}, { sourceDir, destDir });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result[0].copied.name).toBe('a_directory');
|
||||
|
||||
// Verify nested files were copied
|
||||
const nestedFile = await page.evaluate(async ({ destDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.read(`${destDir}/a_directory/a_file.txt`);
|
||||
return result.text();
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}, { destDir });
|
||||
|
||||
expect(nestedFile).toBe('file a contents\n');
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { BASE_PATH, test } from './fixtures';
|
||||
|
||||
test('delete for normal file', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/delete_test_1`;
|
||||
const testFile = `${testPath}/test_delete.txt`;
|
||||
|
||||
await page.evaluate(async ({ testPath, testFile }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(testPath);
|
||||
await puter.fs.write(testFile, 'delete test\n');
|
||||
}, { testPath, testFile });
|
||||
|
||||
await page.evaluate(async ({ testFile }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.delete(testFile);
|
||||
}, { testFile });
|
||||
|
||||
let threw = false;
|
||||
const result = await page.evaluate(async ({ testFile }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.stat(testFile);
|
||||
return { exists: true };
|
||||
} catch (e) {
|
||||
return { exists: false, error: (e as any).code || (e as any).message };
|
||||
}
|
||||
}, { testFile });
|
||||
|
||||
expect(result.exists).toBe(false);
|
||||
});
|
||||
|
||||
test('error for non-existing file', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/delete_test_2`;
|
||||
const testFile = `${testPath}/test_delete.txt`;
|
||||
|
||||
await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(testPath);
|
||||
}, { testPath });
|
||||
|
||||
let threw = false;
|
||||
const result = await page.evaluate(async ({ testFile }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.delete(testFile);
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
return { success: false, error: (e as any).code || (e as any).message };
|
||||
}
|
||||
}, { testFile });
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
test('delete for directory', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/delete_test_3`;
|
||||
const testDir = `${testPath}/test_delete_dir`;
|
||||
|
||||
await page.evaluate(async ({ testPath, testDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(testPath);
|
||||
await puter.fs.mkdir(testDir);
|
||||
}, { testPath, testDir });
|
||||
|
||||
await page.evaluate(async ({ testDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.delete(testDir);
|
||||
}, { testDir });
|
||||
|
||||
const result = await page.evaluate(async ({ testDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.stat(testDir);
|
||||
return { exists: true };
|
||||
} catch (e) {
|
||||
return { exists: false, error: (e as any).code || (e as any).message };
|
||||
}
|
||||
}, { testDir });
|
||||
|
||||
expect(result.exists).toBe(false);
|
||||
});
|
||||
|
||||
test('delete for non-empty directory with recursive=true', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/delete_test_5`;
|
||||
const testDir = `${testPath}/test_delete_dir`;
|
||||
const testFile = `${testDir}/test.txt`;
|
||||
|
||||
await page.evaluate(async ({ testPath, testDir, testFile }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(testPath);
|
||||
await puter.fs.mkdir(testDir);
|
||||
await puter.fs.write(testFile, 'delete test\n');
|
||||
}, { testPath, testDir, testFile });
|
||||
|
||||
await page.evaluate(async ({ testDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.delete(testDir, { recursive: true });
|
||||
}, { testDir });
|
||||
|
||||
// Wait for deletion to complete
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const result = await page.evaluate(async ({ testDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.stat(testDir);
|
||||
return { exists: true };
|
||||
} catch (e) {
|
||||
return { exists: false, error: (e as any).code || (e as any).message };
|
||||
}
|
||||
}, { testDir });
|
||||
|
||||
expect(result.exists).toBe(false);
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
import { test as base, expect, Page } from '@playwright/test';
|
||||
import { validate as isValidUUID } from 'uuid';
|
||||
import { FSEntry } from '../../../../src/backend/src/filesystem/definitions/ts/fsentry';
|
||||
import { testConfig } from '../../config/test-config';
|
||||
|
||||
// The maximum time needed for file-system change to be propagated from
|
||||
// one session to others.
|
||||
export const CHANGE_PROPAGATION_TIME = 0;
|
||||
|
||||
export const BASE_PATH = '/admin/tests';
|
||||
|
||||
export const ERROR_CODES = [
|
||||
'forbidden',
|
||||
'dest_does_not_exist',
|
||||
'subject_does_not_exist',
|
||||
'source_does_not_exist',
|
||||
];
|
||||
|
||||
export const test = base.extend<{ page: Page }>({
|
||||
page: async ({ browser }, use) => {
|
||||
const ctx = await browser.newContext();
|
||||
const page = await ctx.newPage();
|
||||
await bootstrap(page);
|
||||
|
||||
await page.evaluate(async ({ BASE_PATH }) => {
|
||||
const puter = (window as any).puter;
|
||||
|
||||
try {
|
||||
await puter.fs.delete(BASE_PATH, { recursive: true });
|
||||
} catch( error ) {
|
||||
// ignore error
|
||||
console.error('delete error:', error);
|
||||
}
|
||||
|
||||
try {
|
||||
await puter.fs.mkdir(BASE_PATH);
|
||||
} catch( error ) {
|
||||
console.error('mkdir error:', error);
|
||||
throw error;
|
||||
}
|
||||
}, { BASE_PATH });
|
||||
|
||||
await use(page);
|
||||
},
|
||||
});
|
||||
|
||||
// Check the integrity of the FSEntry object.
|
||||
function checkIntegrity(entry: FSEntry): string | null {
|
||||
// check essential fields
|
||||
if ( !entry.uid || !isValidUUID(entry.uid) ) {
|
||||
return `Invalid UID: ${entry.uid}`;
|
||||
}
|
||||
if ( !entry.name || entry.name.trim() === '' ) {
|
||||
return `Invalid name: ${entry.name}`;
|
||||
}
|
||||
if ( !entry.path || entry.path.trim() === '' ) {
|
||||
return `Invalid path: ${entry.path}`;
|
||||
}
|
||||
if ( !entry.parent_id || !isValidUUID(entry.parent_id) ) {
|
||||
return `Invalid parent_id: ${entry.parent_id}`;
|
||||
}
|
||||
if ( entry.size < 0 ) {
|
||||
return `Invalid size: ${entry.size}`;
|
||||
}
|
||||
if ( typeof entry.is_dir !== 'boolean' ) {
|
||||
return `Invalid is_dir type: ${typeof entry.is_dir}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function bootstrap(page: Page) {
|
||||
page.on('pageerror', (e) => console.error('[pageerror]', e));
|
||||
page.on('console', (m) => console.log('[browser]', m.text()));
|
||||
|
||||
await page.goto(testConfig.frontend_url); // establish origin
|
||||
await page.addScriptTag({ url: '/puter.js/v2' }); // load bundle
|
||||
await page.waitForFunction(() => Boolean((window as any).puter), null, { timeout: 10_000 });
|
||||
|
||||
await page.evaluate(async ({ api_url, auth_token }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.setAPIOrigin(api_url);
|
||||
await puter.setAuthToken(auth_token);
|
||||
return;
|
||||
}, { api_url: testConfig.api_url, auth_token: testConfig.auth_token });
|
||||
}
|
||||
|
||||
base('change-propagation - mkdir', async ({ browser }) => {
|
||||
const ctxA = await browser.newContext();
|
||||
const ctxB = await browser.newContext();
|
||||
const pageA = await ctxA.newPage();
|
||||
const pageB = await ctxB.newPage();
|
||||
await Promise.all([bootstrap(pageA), bootstrap(pageB)]);
|
||||
|
||||
// Paths
|
||||
const testPath = `/${testConfig.username}/Desktop`;
|
||||
const dirName = `_test_dir_${Date.now()}`;
|
||||
const dirPath = `${testPath}/${dirName}`;
|
||||
|
||||
// --- Session A: perform the action (mkdir) ---
|
||||
await pageA.evaluate(async ({ dirPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(dirPath);
|
||||
}, { dirPath });
|
||||
|
||||
// Wait for change to be propagated.
|
||||
await pageB.waitForTimeout(CHANGE_PROPAGATION_TIME);
|
||||
|
||||
// --- Session B: observe AFTER mkdir ---
|
||||
const { entry }: { entry: FSEntry } = await pageB.evaluate(async ({ dirPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
|
||||
const entry = await puter.fs.stat(dirPath);
|
||||
return { entry };
|
||||
}, { dirPath });
|
||||
|
||||
// Print the complete FSEntry object
|
||||
console.log('FSEntry object:', JSON.stringify(entry, null, 2));
|
||||
|
||||
const integrityError = checkIntegrity(entry);
|
||||
expect(integrityError).toBeNull();
|
||||
|
||||
await Promise.all([ctxA.close(), ctxB.close()]);
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { BASE_PATH, ERROR_CODES, test } from './fixtures';
|
||||
|
||||
// NB: Don't test "parent + path" api for puter-js, it's only supported on http
|
||||
// api: https://github.com/HeyPuter/puter/blob/9bdb139f7a82ef610e6beb76b91014ac530828a4/src/puter-js/src/modules/FileSystem/operations/mkdir.js#L48-L49
|
||||
|
||||
test('recursive mkdir', async ({ page }) => {
|
||||
// Test recursive mkdir with create_missing_parents
|
||||
const path = `${BASE_PATH}/a/b/c/d/e/f/g`;
|
||||
const result = await page.evaluate(async ({ path }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.mkdir(path, {
|
||||
createMissingParents: true,
|
||||
});
|
||||
console.log('mkdir result?', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('error?', error);
|
||||
return null;
|
||||
}
|
||||
}, { path });
|
||||
|
||||
console.log('result?', result);
|
||||
});
|
||||
|
||||
test('mkdir dedupe name', async ({ page }) => {
|
||||
const basePath = `${BASE_PATH}/dedupe_test`;
|
||||
|
||||
// Create initial directory
|
||||
await page.evaluate(async ({ basePath }) => {
|
||||
const puter = (window as any).puter;
|
||||
|
||||
try {
|
||||
await puter.fs.mkdir(basePath);
|
||||
} catch (error) {
|
||||
console.error('error: ', error);
|
||||
}
|
||||
}, { basePath });
|
||||
|
||||
// Test dedupe functionality
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const result = await page.evaluate(async ({ basePath }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.mkdir(basePath, { dedupeName: true });
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('mkdir error:', error);
|
||||
return null;
|
||||
}
|
||||
}, { basePath });
|
||||
|
||||
if (result) {
|
||||
expect(result.name).toBe(`dedupe_test (${i})`);
|
||||
}
|
||||
|
||||
// Verify the directory exists
|
||||
const stat = await page.evaluate(async ({ basePath, i }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const stat = await puter.fs.stat(`${basePath} (${i})`);
|
||||
return stat;
|
||||
} catch (error) {
|
||||
console.error('stat error:', error);
|
||||
return null;
|
||||
}
|
||||
}, { basePath, i });
|
||||
|
||||
if (stat) {
|
||||
expect(stat.name).toBe(`dedupe_test (${i})`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('mkdir in root directory is prohibited', async ({ page }) => {
|
||||
// Test full path format
|
||||
let error_code = await page.evaluate(async () => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.mkdir('/a');
|
||||
return null;
|
||||
} catch (error: any) {
|
||||
return error.code;
|
||||
}
|
||||
});
|
||||
expect(ERROR_CODES.includes(error_code)).toBe(true);
|
||||
|
||||
// Test parent + path format
|
||||
error_code = await page.evaluate(async () => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.mkdir('a', { parent: '/' });
|
||||
return null;
|
||||
} catch (error: any) {
|
||||
return error.code;
|
||||
}
|
||||
});
|
||||
expect(ERROR_CODES.includes(error_code)).toBe(true);
|
||||
});
|
||||
|
||||
test('full path api with create_missing_parents', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/full_path_api/create_missing_parents_works`;
|
||||
const targetPath = `${testPath}/a/b/c`;
|
||||
|
||||
// Verify parent directory does not exist initially
|
||||
let error_code = await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.stat(`${testPath}/a`);
|
||||
return null;
|
||||
} catch (error: any) {
|
||||
console.error('stat error:', error);
|
||||
return error.code;
|
||||
}
|
||||
}, { testPath });
|
||||
expect(ERROR_CODES.includes(error_code)).toBe(true);
|
||||
|
||||
// Test mkdir with create_missing_parents
|
||||
const result = await page.evaluate(async ({ targetPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.mkdir(targetPath, {
|
||||
createMissingParents: true,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('mkdir error:', error);
|
||||
return null;
|
||||
}
|
||||
}, { targetPath });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.name).toBe('c');
|
||||
|
||||
// Test mkdir without create_missing_parents should fail
|
||||
error_code = await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.mkdir(`${testPath}/x/y/z`);
|
||||
return null;
|
||||
} catch (error: any) {
|
||||
return error.code;
|
||||
}
|
||||
}, { testPath });
|
||||
expect(ERROR_CODES.includes(error_code)).toBe(true);
|
||||
|
||||
// Verify all directories along the path exist
|
||||
const paths = ['a', 'a/b', 'a/b/c'];
|
||||
for (const path of paths) {
|
||||
const stat = await page.evaluate(async ({ testPath, path }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const stat = await puter.fs.stat(`${testPath}/${path}`);
|
||||
return stat;
|
||||
} catch (error) {
|
||||
console.error('stat error:', error);
|
||||
return null;
|
||||
}
|
||||
}, { testPath, path });
|
||||
|
||||
expect(stat).toBeTruthy();
|
||||
expect(stat.name).toBe(path.split('/').pop());
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,205 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { BASE_PATH, ERROR_CODES, test } from './fixtures';
|
||||
|
||||
test('move file', async ({ page }) => {
|
||||
const sourceFile = `${BASE_PATH}/just_a_file.txt`;
|
||||
const targetFile = `${BASE_PATH}/just_a_file_moved.txt`;
|
||||
|
||||
// Create source file
|
||||
await page.evaluate(async ({ sourceFile }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.write(sourceFile, 'move test\n');
|
||||
} catch (error) {
|
||||
console.error('write error:', error);
|
||||
}
|
||||
}, { sourceFile });
|
||||
|
||||
// Move the file
|
||||
const result = await page.evaluate(async ({ sourceFile, targetFile }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.move(sourceFile, targetFile);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('move error:', error);
|
||||
return null;
|
||||
}
|
||||
}, { sourceFile, targetFile });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
|
||||
// Verify target file exists
|
||||
const movedStat = await page.evaluate(async ({ targetFile }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const stat = await puter.fs.stat(targetFile);
|
||||
return stat;
|
||||
} catch (error) {
|
||||
console.error('stat error:', error);
|
||||
return null;
|
||||
}
|
||||
}, { targetFile });
|
||||
|
||||
expect(movedStat).toBeTruthy();
|
||||
expect(movedStat.name).toBe('just_a_file_moved.txt');
|
||||
|
||||
// Verify source file no longer exists
|
||||
const sourceError = await page.evaluate(async ({ sourceFile }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.stat(sourceFile);
|
||||
return null;
|
||||
} catch (error: any) {
|
||||
return error.code;
|
||||
}
|
||||
}, { sourceFile });
|
||||
|
||||
expect(ERROR_CODES.includes(sourceError)).toBe(true);
|
||||
});
|
||||
|
||||
test('move file to existing file', async ({ page }) => {
|
||||
const sourceFile = `${BASE_PATH}/just_a_file.txt`;
|
||||
const targetFile = `${BASE_PATH}/dir_with_contents/a.txt`;
|
||||
|
||||
// Setup: create source file and target file
|
||||
await page.evaluate(async ({ sourceFile, targetFile }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.mkdir(`${BASE_PATH}/dir_with_contents`);
|
||||
await puter.fs.write(sourceFile, 'move test\n');
|
||||
await puter.fs.write(targetFile, 'existing content\n');
|
||||
} catch (error) {
|
||||
console.error('setup error:', error);
|
||||
}
|
||||
}, { sourceFile, targetFile });
|
||||
|
||||
// Attempt to move file to existing file (should fail)
|
||||
const errorCode = await page.evaluate(async ({ sourceFile, targetFile }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.move(sourceFile, targetFile);
|
||||
return null;
|
||||
} catch (error: any) {
|
||||
return error.code;
|
||||
}
|
||||
}, { sourceFile, targetFile });
|
||||
|
||||
expect(ERROR_CODES.includes(errorCode), `unexpected error code: ${errorCode}`).toBe(true);
|
||||
});
|
||||
|
||||
test('move directory', async ({ page }) => {
|
||||
const sourceDir = `${BASE_PATH}/dir_no_contents`;
|
||||
const targetDir = `${BASE_PATH}/dir_no_contents_moved`;
|
||||
|
||||
// Create source directory
|
||||
await page.evaluate(async ({ sourceDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.mkdir(sourceDir);
|
||||
} catch (error) {
|
||||
console.error('mkdir error:', error);
|
||||
}
|
||||
}, { sourceDir });
|
||||
|
||||
// Move the directory
|
||||
const result = await page.evaluate(async ({ sourceDir, targetDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.move(sourceDir, targetDir);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('move error:', error);
|
||||
return null;
|
||||
}
|
||||
}, { sourceDir, targetDir });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
|
||||
// Verify target directory exists
|
||||
const movedStat = await page.evaluate(async ({ targetDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const stat = await puter.fs.stat(targetDir);
|
||||
return stat;
|
||||
} catch (error) {
|
||||
console.error('stat error:', error);
|
||||
return null;
|
||||
}
|
||||
}, { targetDir });
|
||||
|
||||
expect(movedStat).toBeTruthy();
|
||||
expect(movedStat.name).toBe('dir_no_contents_moved');
|
||||
|
||||
// Verify source directory no longer exists
|
||||
const sourceError = await page.evaluate(async ({ sourceDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.stat(sourceDir);
|
||||
return null;
|
||||
} catch (error: any) {
|
||||
return error.code;
|
||||
}
|
||||
}, { sourceDir });
|
||||
|
||||
expect(ERROR_CODES.includes(sourceError)).toBe(true);
|
||||
});
|
||||
|
||||
test('move file and create parents', async ({ page }) => {
|
||||
const sourceFile = `${BASE_PATH}/just_a_file.txt`;
|
||||
const targetFile = `${BASE_PATH}/dir_with_contents/q/w/e/just_a_file.txt`;
|
||||
|
||||
// Setup: create source file and parent directories
|
||||
await page.evaluate(async ({ BASE_PATH, sourceFile }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(`${BASE_PATH}/dir_with_contents`);
|
||||
await puter.fs.mkdir(`${BASE_PATH}/dir_with_contents/q`);
|
||||
await puter.fs.mkdir(`${BASE_PATH}/dir_with_contents/w`);
|
||||
await puter.fs.write(sourceFile, 'move test\n');
|
||||
}, { BASE_PATH, sourceFile });
|
||||
|
||||
// Move file with create_missing_parents
|
||||
const result = await page.evaluate(async ({ sourceFile, targetFile }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.move(sourceFile, targetFile, {
|
||||
createMissingParents: true,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('move error:', error);
|
||||
return null;
|
||||
}
|
||||
}, { sourceFile, targetFile });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.parent_dirs_created.length).toBe(2);
|
||||
|
||||
// Verify target file exists
|
||||
const movedStat = await page.evaluate(async ({ targetFile }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const stat = await puter.fs.stat(targetFile);
|
||||
return stat;
|
||||
} catch (error) {
|
||||
console.error('stat error:', error);
|
||||
return null;
|
||||
}
|
||||
}, { targetFile });
|
||||
|
||||
expect(movedStat).toBeTruthy();
|
||||
expect(movedStat.name).toBe('just_a_file.txt');
|
||||
|
||||
// Verify source file no longer exists
|
||||
const sourceError = await page.evaluate(async ({ sourceFile }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.stat(sourceFile);
|
||||
return null;
|
||||
} catch (error: any) {
|
||||
return error.code;
|
||||
}
|
||||
}, { sourceFile });
|
||||
|
||||
expect(ERROR_CODES.includes(sourceError)).toBe(true);
|
||||
});
|
||||
@@ -0,0 +1,187 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { BASE_PATH, test } from './fixtures';
|
||||
|
||||
test('move file with path format', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/move_cart_1`;
|
||||
const sourceFile = `${testPath}/a/a_file.txt`;
|
||||
const destDir = `${testPath}/b`;
|
||||
|
||||
// Setup: create directory structure
|
||||
await page.evaluate(async ({ testPath, sourceFile, destDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(testPath);
|
||||
await puter.fs.mkdir(`${testPath}/a`);
|
||||
await puter.fs.write(sourceFile, 'file a contents\n');
|
||||
await puter.fs.mkdir(destDir);
|
||||
}, { testPath, sourceFile, destDir });
|
||||
|
||||
// Move file
|
||||
const result = await page.evaluate(async ({ sourceFile, destDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.move(sourceFile, destDir);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('move error:', error);
|
||||
return null;
|
||||
}
|
||||
}, { sourceFile, destDir });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.moved.name).toBe('a_file.txt');
|
||||
});
|
||||
|
||||
test('move file with specified name', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/move_cart_2`;
|
||||
const sourceFile = `${testPath}/a/a_file.txt`;
|
||||
const destDir = `${testPath}/b`;
|
||||
const newName = 'x_file.txt';
|
||||
|
||||
// Setup: create directory structure
|
||||
await page.evaluate(async ({ testPath, sourceFile, destDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(testPath);
|
||||
await puter.fs.mkdir(`${testPath}/a`);
|
||||
await puter.fs.write(sourceFile, 'file a contents\n');
|
||||
await puter.fs.mkdir(destDir);
|
||||
}, { testPath, sourceFile, destDir });
|
||||
|
||||
// Move file with new name
|
||||
const result = await page.evaluate(async ({ sourceFile, destDir, newName }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.move(sourceFile, destDir, { newName });
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('move error:', error);
|
||||
return null;
|
||||
}
|
||||
}, { sourceFile, destDir, newName });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.moved.name).toBe(newName);
|
||||
});
|
||||
|
||||
test('move file with overwrite to directory', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/move_cart_3`;
|
||||
const sourceFile = `${testPath}/a/a_file.txt`;
|
||||
const destDir = `${testPath}/b`;
|
||||
|
||||
// Setup: create directory structure
|
||||
await page.evaluate(async ({ testPath, sourceFile, destDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(testPath);
|
||||
await puter.fs.mkdir(`${testPath}/a`);
|
||||
await puter.fs.write(sourceFile, 'file a contents\n');
|
||||
await puter.fs.mkdir(destDir);
|
||||
await puter.fs.write(`${destDir}/a_file.txt`, 'existing file\n');
|
||||
}, { testPath, sourceFile, destDir });
|
||||
|
||||
// Move file with overwrite
|
||||
const result = await page.evaluate(async ({ sourceFile, destDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.move(sourceFile, destDir, { overwrite: true });
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('move error:', error);
|
||||
return null;
|
||||
}
|
||||
}, { sourceFile, destDir });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
test('move file without overwrite to directory with existing file should error', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/move_cart_4`;
|
||||
const sourceFile = `${testPath}/a/a_file.txt`;
|
||||
const destDir = `${testPath}/b`;
|
||||
|
||||
// Setup: create directory structure
|
||||
await page.evaluate(async ({ testPath, sourceFile, destDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(testPath);
|
||||
await puter.fs.mkdir(`${testPath}/a`);
|
||||
await puter.fs.write(sourceFile, 'file a contents\n');
|
||||
await puter.fs.mkdir(destDir);
|
||||
await puter.fs.write(`${destDir}/a_file.txt`, 'existing file\n');
|
||||
}, { testPath, sourceFile, destDir });
|
||||
|
||||
// Attempt move without overwrite (should fail)
|
||||
const result = await page.evaluate(async ({ sourceFile, destDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.move(sourceFile, destDir);
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, code: error.code };
|
||||
}
|
||||
}, { sourceFile, destDir });
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.code).toBeTruthy();
|
||||
});
|
||||
|
||||
test('move file to file destination should error', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/move_cart_6`;
|
||||
const sourceFile = `${testPath}/a/a_file.txt`;
|
||||
const destFile = `${testPath}/b`;
|
||||
|
||||
// Setup: create file as destination (not directory)
|
||||
await page.evaluate(async ({ testPath, sourceFile, destFile }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(testPath);
|
||||
await puter.fs.mkdir(`${testPath}/a`);
|
||||
await puter.fs.write(sourceFile, 'file a contents\n');
|
||||
await puter.fs.write(destFile, 'placeholder\n');
|
||||
}, { testPath, sourceFile, destFile });
|
||||
|
||||
// Attempt move with specified name to file destination (should error)
|
||||
const result = await page.evaluate(async ({ sourceFile, destFile }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.move(sourceFile, destFile, { newName: 'x_file.txt' });
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, code: error.code };
|
||||
}
|
||||
}, { sourceFile, destFile });
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.code).toBe('dest_is_not_a_directory');
|
||||
});
|
||||
|
||||
test('move file with uid format', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/move_cart_7`;
|
||||
const sourceFile = `${testPath}/a/a_file.txt`;
|
||||
const destDir = `${testPath}/b`;
|
||||
|
||||
// Setup and get UIDs
|
||||
const { sourceUID, destUID } = await page.evaluate(async ({ testPath, sourceFile, destDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(testPath);
|
||||
await puter.fs.mkdir(`${testPath}/a`);
|
||||
await puter.fs.write(sourceFile, 'file a contents\n');
|
||||
await puter.fs.mkdir(destDir);
|
||||
|
||||
const sourceStat = await puter.fs.stat(sourceFile);
|
||||
const destStat = await puter.fs.stat(destDir);
|
||||
return { sourceUID: sourceStat.uid, destUID: destStat.uid };
|
||||
}, { testPath, sourceFile, destDir });
|
||||
|
||||
// Move using UIDs (if supported by puter-js)
|
||||
const result = await page.evaluate(async ({ sourceUID, destUID }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
// Note: puter-js move might not support uid format directly
|
||||
// This would require internal API usage
|
||||
return { sourceUID, destUID };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}, { sourceUID, destUID });
|
||||
|
||||
expect(result.sourceUID).toBeTruthy();
|
||||
expect(result.destUID).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { BASE_PATH, test } from './fixtures';
|
||||
|
||||
test('readdir test', async ({ page }) => {
|
||||
// Create test directory
|
||||
const testDir = `${BASE_PATH}/test_readdir`;
|
||||
|
||||
await page.evaluate(async ({ testDir }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.mkdir(testDir, { overwrite: true });
|
||||
} catch (error) {
|
||||
console.error('mkdir error:', error);
|
||||
throw error;
|
||||
}
|
||||
}, { testDir });
|
||||
|
||||
// Create files
|
||||
const files = ['a.txt', 'b.txt', 'c.txt'];
|
||||
const dirs = ['q', 'w', 'e'];
|
||||
|
||||
for (const file of files) {
|
||||
await page.evaluate(async ({ testDir, file }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.write(`${testDir}/${file}`, 'readdir test\n', { overwrite: true });
|
||||
} catch (error) {
|
||||
console.error(`write error for ${file}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}, { testDir, file });
|
||||
}
|
||||
|
||||
// Create directories
|
||||
for (const dir of dirs) {
|
||||
await page.evaluate(async ({ testDir, dir }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.mkdir(`${testDir}/${dir}`, { overwrite: true });
|
||||
} catch (error) {
|
||||
console.error(`mkdir error for ${dir}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}, { testDir, dir });
|
||||
}
|
||||
|
||||
// Verify files
|
||||
for (const file of files) {
|
||||
const result = await page.evaluate(async ({ testDir, file }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.stat(`${testDir}/${file}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`stat error for ${file}:`, error);
|
||||
return null;
|
||||
}
|
||||
}, { testDir, file });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.name).toBe(file);
|
||||
expect(result.is_dir).toBe(false);
|
||||
}
|
||||
|
||||
// Verify directories
|
||||
for (const dir of dirs) {
|
||||
const result = await page.evaluate(async ({ testDir, dir }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.stat(`${testDir}/${dir}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`stat error for ${dir}:`, error);
|
||||
return null;
|
||||
}
|
||||
}, { testDir, dir });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.name).toBe(dir);
|
||||
expect(result.is_dir).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('readdir of root shouldn\'t return everything', async ({ page }) => {
|
||||
const result = await page.evaluate(async () => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.readdir('/', { recursive: true });
|
||||
console.log('result?', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('readdir error:', error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('result?', result);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { BASE_PATH, test } from './fixtures';
|
||||
|
||||
test('stat with path (no flags)', async ({ page }) => {
|
||||
const TEST_FILENAME = 'test_stat.txt';
|
||||
const testPath = `${BASE_PATH}/${TEST_FILENAME}`;
|
||||
|
||||
// Write the test file
|
||||
await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.write(testPath, 'stat test\n', { overwrite: true });
|
||||
} catch (error) {
|
||||
console.error('write error:', error);
|
||||
throw error;
|
||||
}
|
||||
}, { testPath });
|
||||
|
||||
// Stat the file
|
||||
const result = await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.stat(testPath);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('stat error:', error);
|
||||
return null;
|
||||
}
|
||||
}, { testPath });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.name).toBe('test_stat.txt');
|
||||
expect(result.is_dir).toBe(false);
|
||||
expect(result.uid).toBeDefined();
|
||||
});
|
||||
|
||||
test('stat with uid', async ({ page }) => {
|
||||
const TEST_FILENAME = 'test_stat.txt';
|
||||
const testPath = `${BASE_PATH}/${TEST_FILENAME}`;
|
||||
|
||||
// Write the test file
|
||||
await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.write(testPath, 'stat test\n', { overwrite: true });
|
||||
} catch (error) {
|
||||
console.error('write error:', error);
|
||||
throw error;
|
||||
}
|
||||
}, { testPath });
|
||||
|
||||
// Get uid from first stat
|
||||
const firstStat = await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.stat(testPath);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('stat error:', error);
|
||||
return null;
|
||||
}
|
||||
}, { testPath });
|
||||
|
||||
const uid = firstStat.uid;
|
||||
|
||||
// Stat using uid
|
||||
const result = await page.evaluate(async ({ uid }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.stat(uid);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('statu error:', error);
|
||||
return null;
|
||||
}
|
||||
}, { uid });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.name).toBe('test_stat.txt');
|
||||
expect(result.uid).toBe(uid);
|
||||
});
|
||||
|
||||
test('stat with no path or uid provided fails', async ({ page }) => {
|
||||
const result = await page.evaluate(async () => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.stat('');
|
||||
return { success: true, result };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
test('stat with versions', async ({ page }) => {
|
||||
const TEST_FILENAME = 'test_stat.txt';
|
||||
const testPath = `${BASE_PATH}/${TEST_FILENAME}`;
|
||||
|
||||
// Write the test file
|
||||
await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.write(testPath, 'stat test\n', { overwrite: true });
|
||||
} catch (error) {
|
||||
console.error('write error:', error);
|
||||
throw error;
|
||||
}
|
||||
}, { testPath });
|
||||
|
||||
// Stat with returnVersions flag
|
||||
const result = await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
console.log('STAT WITH VERSIONS', testPath);
|
||||
const result = await puter.fs.stat({
|
||||
path: testPath,
|
||||
returnVersions: true,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('stat error:', error);
|
||||
return null;
|
||||
}
|
||||
}, { testPath });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.name).toBe('test_stat.txt');
|
||||
console.log('RESULT', result);
|
||||
expect(Array.isArray(result.versions)).toBe(true);
|
||||
});
|
||||
|
||||
test('stat with shares', async ({ page }) => {
|
||||
const TEST_FILENAME = 'test_stat.txt';
|
||||
const testPath = `${BASE_PATH}/${TEST_FILENAME}`;
|
||||
|
||||
// Write the test file
|
||||
await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.write(testPath, 'stat test\n', { overwrite: true });
|
||||
} catch (error) {
|
||||
console.error('write error:', error);
|
||||
throw error;
|
||||
}
|
||||
}, { testPath });
|
||||
|
||||
// Stat with returnPermissions flag (returns shares info)
|
||||
const result = await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.stat({
|
||||
path: testPath,
|
||||
returnPermissions: true,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('stat error:', error);
|
||||
return null;
|
||||
}
|
||||
}, { testPath });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.name).toBe('test_stat.txt');
|
||||
// returnPermissions returns shares info
|
||||
expect('shares' in result).toBe(true);
|
||||
expect(Array.isArray(result.shares.users)).toBe(true);
|
||||
expect(Array.isArray(result.shares.apps)).toBe(true);
|
||||
});
|
||||
|
||||
test('stat with subdomains', async ({ page }) => {
|
||||
const dirName = 'test_stat_subdomains';
|
||||
const testPath = `${BASE_PATH}/${dirName}`;
|
||||
|
||||
// Create directory
|
||||
await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.mkdir(testPath, { overwrite: true });
|
||||
} catch (error) {
|
||||
console.error('mkdir error:', error);
|
||||
throw error;
|
||||
}
|
||||
}, { testPath });
|
||||
|
||||
// Stat with returnSubdomains flag
|
||||
const result = await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.stat({
|
||||
path: testPath,
|
||||
returnSubdomains: true,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('stat error:', error);
|
||||
return null;
|
||||
}
|
||||
}, { testPath });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.name).toBe('test_stat_subdomains');
|
||||
expect(Array.isArray(result.subdomains)).toBe(true);
|
||||
console.log('RESULT', result);
|
||||
});
|
||||
|
||||
test('stat with size', async ({ page }) => {
|
||||
const TEST_FILENAME = 'test_stat.txt';
|
||||
const testPath = `${BASE_PATH}/${TEST_FILENAME}`;
|
||||
|
||||
// Write the test file
|
||||
await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.write(testPath, 'stat test\n', { overwrite: true });
|
||||
} catch (error) {
|
||||
console.error('write error:', error);
|
||||
throw error;
|
||||
}
|
||||
}, { testPath });
|
||||
|
||||
// Stat with returnSize flag
|
||||
const result = await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.stat({
|
||||
path: testPath,
|
||||
returnSize: true,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('stat error:', error);
|
||||
return null;
|
||||
}
|
||||
}, { testPath });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.name).toBe('test_stat.txt');
|
||||
console.log('RESULT', result);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { BASE_PATH, test } from './fixtures';
|
||||
|
||||
test('read matches what was written', async ({ page }) => {
|
||||
const fileName = 'test_rw.txt';
|
||||
const testPath = `${BASE_PATH}/${fileName}`;
|
||||
|
||||
// Write file
|
||||
await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.write(testPath, 'example\n');
|
||||
}, { testPath });
|
||||
|
||||
// Read and verify
|
||||
const result = await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
const result = await puter.fs.read(testPath);
|
||||
return await result.text();
|
||||
}, { testPath });
|
||||
|
||||
expect(result).toBe('example\n');
|
||||
});
|
||||
|
||||
test('write without overwrite creates deduped name', async ({ page }) => {
|
||||
const fileName = 'test_rw.txt';
|
||||
const testPath = `${BASE_PATH}/${fileName}`;
|
||||
|
||||
// Write initial file
|
||||
await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.write(testPath, 'example\n');
|
||||
}, { testPath });
|
||||
|
||||
// Write without overwrite - should create deduped name
|
||||
let errorThrown = false;
|
||||
const result = await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.write(testPath, 'no-change\n');
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, code: error.code };
|
||||
}
|
||||
}, { testPath });
|
||||
|
||||
// Note: puter-js behavior might auto-dedupe names
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
test('write with overwrite updates file', async ({ page }) => {
|
||||
const fileName = 'test_rw.txt';
|
||||
const testPath = `${BASE_PATH}/${fileName}`;
|
||||
|
||||
// Write initial file
|
||||
await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.write(testPath, 'example\n');
|
||||
}, { testPath });
|
||||
|
||||
// Write with overwrite
|
||||
await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.write(testPath, 'yes-change\n', { overwrite: true });
|
||||
}, { testPath });
|
||||
|
||||
// Verify content was updated
|
||||
const result = await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
const result = await puter.fs.read(testPath);
|
||||
return await result.text();
|
||||
}, { testPath });
|
||||
|
||||
expect(result).toBe('yes-change\n');
|
||||
});
|
||||
|
||||
test('read with version id', async ({ page }) => {
|
||||
const fileName = 'test_rw.txt';
|
||||
const testPath = `${BASE_PATH}/${fileName}`;
|
||||
|
||||
// Write file
|
||||
await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.write(testPath, 'yes-change\n', { overwrite: true });
|
||||
}, { testPath });
|
||||
|
||||
// Read with version_id
|
||||
const result = await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const result = await puter.fs.read(testPath, { version_id: '1' });
|
||||
const text = await result.text();
|
||||
return { success: true, text };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}, { testPath });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
test('read with no path or uid provided fails', async ({ page }) => {
|
||||
const result = await page.evaluate(async () => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.read('');
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, code: error.code };
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.code).toBeTruthy();
|
||||
});
|
||||
|
||||
test('read non-existing file fails', async ({ page }) => {
|
||||
const result = await page.evaluate(async ({ basePath }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
await puter.fs.read(`${basePath}/i-do-not-exist.txt`);
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, code: error.code };
|
||||
}
|
||||
}, { basePath: BASE_PATH });
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.code).toBe('subject_does_not_exist');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { BASE_PATH, test } from './fixtures';
|
||||
|
||||
test('write to new directory with default name', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/write_test_1`;
|
||||
|
||||
// Create directory
|
||||
await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(testPath);
|
||||
}, { testPath });
|
||||
|
||||
// Write file with default name
|
||||
const result = await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
const contents = new Blob(['test content 1\n'], { type: 'text/plain' });
|
||||
const file = new File([contents], 'uploaded_name.txt', { type: 'text/plain' });
|
||||
|
||||
const result = await puter.fs.write(`${testPath}/uploaded_name.txt`, file);
|
||||
return result;
|
||||
}, { testPath });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.name).toBe('uploaded_name.txt');
|
||||
});
|
||||
|
||||
test('write with specified name', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/write_test_2`;
|
||||
|
||||
// Create directory
|
||||
await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(testPath);
|
||||
}, { testPath });
|
||||
|
||||
// Write file with specified name
|
||||
const result = await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
const contents = new Blob(['test content 2\n'], { type: 'text/plain' });
|
||||
const file = new File([contents], 'uploaded_name.txt', { type: 'text/plain' });
|
||||
|
||||
const result = await puter.fs.write(`${testPath}/uploaded_name.txt`, file);
|
||||
return result;
|
||||
}, { testPath });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
test('write with overwrite option', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/write_test_3`;
|
||||
const fileName = 'test_overwrite.txt';
|
||||
|
||||
// Create directory
|
||||
await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(testPath);
|
||||
}, { testPath });
|
||||
|
||||
// Write initial file
|
||||
await page.evaluate(async ({ testPath, fileName }) => {
|
||||
const puter = (window as any).puter;
|
||||
const contents = new Blob(['initial content\n'], { type: 'text/plain' });
|
||||
const file = new File([contents], fileName, { type: 'text/plain' });
|
||||
await puter.fs.write(`${testPath}/${fileName}`, file);
|
||||
}, { testPath, fileName });
|
||||
|
||||
// Write with overwrite
|
||||
const result = await page.evaluate(async ({ testPath, fileName }) => {
|
||||
const puter = (window as any).puter;
|
||||
const contents = new Blob(['updated content\n'], { type: 'text/plain' });
|
||||
const file = new File([contents], fileName, { type: 'text/plain' });
|
||||
const result = await puter.fs.write(`${testPath}/${fileName}`, file, { overwrite: true });
|
||||
return result;
|
||||
}, { testPath, fileName });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
|
||||
// Verify content was overwritten
|
||||
const readResult = await page.evaluate(async ({ testPath, fileName }) => {
|
||||
const puter = (window as any).puter;
|
||||
const result = await puter.fs.read(`${testPath}/${fileName}`);
|
||||
return result.text();
|
||||
}, { testPath, fileName });
|
||||
|
||||
expect(readResult).toBe('updated content\n');
|
||||
});
|
||||
|
||||
test('write to directory using UID', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/write_test_4`;
|
||||
|
||||
// Create directory and get UID
|
||||
const dirUID = await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(testPath);
|
||||
const stat = await puter.fs.stat(testPath);
|
||||
return stat.uid;
|
||||
}, { testPath });
|
||||
|
||||
// Write file using UID
|
||||
const result = await page.evaluate(async ({ dirUID }) => {
|
||||
const puter = (window as any).puter;
|
||||
const contents = new Blob(['test content with UID\n'], { type: 'text/plain' });
|
||||
const file = new File([contents], 'uid_test.txt', { type: 'text/plain' });
|
||||
|
||||
// Note: puter-js write doesn't directly support UID for destination
|
||||
// This would require using the internal API
|
||||
return { uid: dirUID };
|
||||
}, { dirUID });
|
||||
|
||||
expect(result.uid).toBeTruthy();
|
||||
});
|
||||
|
||||
test('write with dedupe name option', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/write_test_5`;
|
||||
const fileName = 'dedupe_test.txt';
|
||||
|
||||
// Create directory
|
||||
await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(testPath);
|
||||
}, { testPath });
|
||||
|
||||
// Write initial file
|
||||
await page.evaluate(async ({ testPath, fileName }) => {
|
||||
const puter = (window as any).puter;
|
||||
const contents = new Blob(['initial\n'], { type: 'text/plain' });
|
||||
const file = new File([contents], fileName, { type: 'text/plain' });
|
||||
await puter.fs.write(`${testPath}/${fileName}`, file);
|
||||
}, { testPath, fileName });
|
||||
|
||||
// Write with dedupeName (without overwrite)
|
||||
const result = await page.evaluate(async ({ testPath, fileName }) => {
|
||||
const puter = (window as any).puter;
|
||||
const contents = new Blob(['deduped\n'], { type: 'text/plain' });
|
||||
const file = new File([contents], fileName, { type: 'text/plain' });
|
||||
|
||||
try {
|
||||
const result = await puter.fs.write(`${testPath}/${fileName}`, file, {
|
||||
overwrite: false,
|
||||
dedupeName: true
|
||||
});
|
||||
return { success: true, result };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}, { testPath, fileName });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.result.name).toMatch(/dedupe_test \(\d\)\.txt/);
|
||||
});
|
||||
|
||||
test('write string data', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/write_test_6`;
|
||||
|
||||
// Create directory
|
||||
await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(testPath);
|
||||
}, { testPath });
|
||||
|
||||
// Write string data
|
||||
const result = await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
const result = await puter.fs.write(`${testPath}/string_test.txt`, 'Hello World\n');
|
||||
return result;
|
||||
}, { testPath });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.name).toBe('string_test.txt');
|
||||
|
||||
// Verify content
|
||||
const readResult = await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
const result = await puter.fs.read(`${testPath}/string_test.txt`);
|
||||
return result.text();
|
||||
}, { testPath });
|
||||
|
||||
expect(readResult).toBe('Hello World\n');
|
||||
});
|
||||
|
||||
test('write to file instead of directory should error', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/write_test_7`;
|
||||
const fileName = 'destination.txt';
|
||||
|
||||
// Create directory
|
||||
await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(testPath);
|
||||
}, { testPath });
|
||||
|
||||
// Create a file
|
||||
await page.evaluate(async ({ testPath, fileName }) => {
|
||||
const puter = (window as any).puter;
|
||||
const contents = new Blob(['initial content\n'], { type: 'text/plain' });
|
||||
const file = new File([contents], fileName, { type: 'text/plain' });
|
||||
await puter.fs.write(`${testPath}/${fileName}`, file);
|
||||
}, { testPath, fileName });
|
||||
|
||||
// Try to write to a file (should error or create a nested file)
|
||||
const result = await page.evaluate(async ({ testPath, fileName }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const contents = new Blob(['test\n'], { type: 'text/plain' });
|
||||
const file = new File([contents], 'nested.txt', { type: 'text/plain' });
|
||||
const result = await puter.fs.write(`${testPath}/${fileName}`, file);
|
||||
return { success: true, result };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, code: error.code };
|
||||
}
|
||||
}, { testPath, fileName });
|
||||
|
||||
// Note: puter-js behavior might differ from the API tester
|
||||
// The exact behavior depends on implementation
|
||||
expect(result.success !== undefined).toBe(true);
|
||||
});
|
||||
|
||||
test('write without overwrite on existing file should error', async ({ page }) => {
|
||||
const testPath = `${BASE_PATH}/write_test_8`;
|
||||
const fileName = 'existing.txt';
|
||||
const dedupedFileName = 'existing (1).txt';
|
||||
|
||||
// Create directory
|
||||
await page.evaluate(async ({ testPath }) => {
|
||||
const puter = (window as any).puter;
|
||||
await puter.fs.mkdir(testPath);
|
||||
}, { testPath });
|
||||
|
||||
// Create initial file
|
||||
await page.evaluate(async ({ testPath, fileName }) => {
|
||||
const puter = (window as any).puter;
|
||||
const contents = new Blob(['initial\n'], { type: 'text/plain' });
|
||||
const file = new File([contents], fileName, { type: 'text/plain' });
|
||||
await puter.fs.write(`${testPath}/${fileName}`, file);
|
||||
}, { testPath, fileName });
|
||||
|
||||
// Try to write without overwrite - should create deduped name
|
||||
const result = await page.evaluate(async ({ testPath, fileName }) => {
|
||||
const puter = (window as any).puter;
|
||||
try {
|
||||
const contents = new Blob(['second\n'], { type: 'text/plain' });
|
||||
const file = new File([contents], fileName, { type: 'text/plain' });
|
||||
const result = await puter.fs.write(`${testPath}/${fileName}`, file, { overwrite: false });
|
||||
return { success: true, result };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, code: error.code };
|
||||
}
|
||||
}, { testPath, fileName });
|
||||
|
||||
// With overwrite: false, it should create a deduped filename
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.result.name).toBe(dedupedFileName);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { testConfig } from '../config/test-config';
|
||||
|
||||
test('puter.auth.whoami', async ({ page }) => {
|
||||
if ( !testConfig.auth_token ) {
|
||||
throw new Error('authToken is required in client-config.yaml');
|
||||
}
|
||||
|
||||
page.on('pageerror', (err) => console.error('[pageerror]', err));
|
||||
page.on('console', (msg) => console.log('[browser]', msg.text()));
|
||||
|
||||
// 1) Open any page served by your backend to establish same-origin
|
||||
await page.goto(testConfig.frontend_url); // even a 404 page is fine; origin is set
|
||||
|
||||
// 2) Load the real bundle from the same origin
|
||||
await page.addScriptTag({ url: '/puter.js/v2' });
|
||||
|
||||
// 3) Wait for global
|
||||
await page.waitForFunction(() => Boolean((window as any).puter), null, { timeout: 10000 });
|
||||
|
||||
// 4) Call whoami in the browser context
|
||||
const result = await page.evaluate(async (testConfig) => {
|
||||
const puter = (window as any).puter;
|
||||
|
||||
await puter.setAPIOrigin(testConfig.api_url);
|
||||
await puter.setAuthToken(testConfig.auth_token);
|
||||
|
||||
return await puter.auth.whoami();
|
||||
}, testConfig);
|
||||
|
||||
expect(result?.username).toBe(testConfig.username);
|
||||
|
||||
const result2 = await page.evaluate(async () => {
|
||||
const puter = (window as any).puter;
|
||||
return await puter.auth.whoami();
|
||||
});
|
||||
|
||||
expect(result2?.username).toBe(testConfig.username);
|
||||
});
|
||||
|
||||
test('connect to prod puter', async ({ page }) => {
|
||||
page.on('pageerror', (err) => console.error('[pageerror]', err));
|
||||
page.on('console', (msg) => console.log('[browser]', msg.text()));
|
||||
|
||||
const prodURL = 'https://puter.com';
|
||||
|
||||
// Go to production URL
|
||||
await page.goto(prodURL);
|
||||
|
||||
// Wait for 5 seconds then exit
|
||||
await page.waitForTimeout(5000);
|
||||
});
|
||||
@@ -1,16 +1,34 @@
|
||||
// testUtils.ts - Puter.js API test utilities (TypeScript)
|
||||
import type { Puter } from '../../src/puter-js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as yaml from 'yaml';
|
||||
import type { Puter } from '../../src/puter-js/index.js';
|
||||
|
||||
// Create and configure a global puter instance from environment variables
|
||||
// Create and configure a global puter instance from client-config.yaml
|
||||
// Usage: import { puter } from './testUtils'
|
||||
// Environment variables: PUTER_AUTH_TOKEN, PUTER_API_ORIGIN, PUTER_ORIGIN
|
||||
// Configuration is read from tests/client-config.yaml
|
||||
|
||||
// Load configuration from YAML file
|
||||
let config: any;
|
||||
try {
|
||||
const configPath = path.join(__dirname, '../client-config.yaml');
|
||||
config = yaml.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
} catch (error) {
|
||||
console.error('Failed to load client-config.yaml:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const puter: Puter = require('../../src/puter-js/src/index.js').default || globalThis.puter;
|
||||
globalThis.PUTER_ORIGIN = process.env.PUTER_ORIGIN || 'https://puter.com';
|
||||
globalThis.PUTER_API_ORIGIN = process.env.PUTER_API_ORIGIN || 'https://api.puter.com';
|
||||
if (process.env.PUTER_API_ORIGIN) (puter as any).setAPIOrigin(process.env.PUTER_API_ORIGIN);
|
||||
if (process.env.PUTER_ORIGIN) (puter as any).defaultGUIOrigin = process.env.PUTER_ORIGIN;
|
||||
if (process.env.PUTER_AUTH_TOKEN) (puter as any).setAuthToken(process.env.PUTER_AUTH_TOKEN);
|
||||
|
||||
(globalThis as any).PUTER_ORIGIN = config.frontend_url;
|
||||
(globalThis as any).PUTER_API_ORIGIN = config.api_url;
|
||||
|
||||
(puter as any).setAPIOrigin(config.api_url);
|
||||
(puter as any).defaultGUIOrigin = config.frontend_url;
|
||||
|
||||
if (config.auth_token) {
|
||||
(puter as any).setAuthToken(config.auth_token);
|
||||
}
|
||||
|
||||
export { puter };
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".."
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
# API Test
|
||||
|
||||
## Summary
|
||||
|
||||
This script tests the APIs of the Puter backend and `puter-js`.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
pip install -r ./tools/api-tester/ci/requirements.txt
|
||||
./tools/api-tester/ci/run.py
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] Support macOS.
|
||||
@@ -1,187 +0,0 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# Usage:
|
||||
# ./tools/api-tester/ci/run.py
|
||||
|
||||
import argparse
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import datetime
|
||||
import urllib
|
||||
import requests
|
||||
import yaml
|
||||
|
||||
import cxc_toolkit
|
||||
import cxc_toolkit.exec
|
||||
|
||||
|
||||
class Context:
|
||||
def __init__(self):
|
||||
self.ADMIN_PASSWORD = None
|
||||
self.TOKEN = None
|
||||
|
||||
|
||||
CONTEXT = Context()
|
||||
|
||||
|
||||
def get_token():
|
||||
# Send HTTP request to server and print response
|
||||
print("Sending HTTP request to server...")
|
||||
# Assuming the server runs on localhost:4100 (default Puter port)
|
||||
server_url = "http://api.puter.localhost:4100/login"
|
||||
|
||||
# Prepare login data
|
||||
login_data = {"username": "admin", "password": CONTEXT.ADMIN_PASSWORD}
|
||||
|
||||
# Send POST request using requests library
|
||||
response = requests.post(
|
||||
server_url,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"Origin": "http://api.puter.localhost:4100",
|
||||
},
|
||||
json=login_data,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
print(f"Server response status: {response.status_code}")
|
||||
print(f"Server response body: {response.text}")
|
||||
|
||||
response_json = response.json()
|
||||
print(f"Parsed JSON response: {json.dumps(response_json, indent=2)}")
|
||||
print(f"Token: {response_json['token']}")
|
||||
CONTEXT.TOKEN = response_json["token"]
|
||||
|
||||
|
||||
def init_server_config():
|
||||
server_process = cxc_toolkit.exec.run_background("npm start")
|
||||
# wait 10s for the server to start
|
||||
time.sleep(10)
|
||||
server_process.terminate()
|
||||
|
||||
|
||||
# create the admin user and print its password
|
||||
def get_admin_password():
|
||||
# output_bytes, exit_code = cxc_toolkit.exec.run_command(
|
||||
# "npm start",
|
||||
# stream_output=False,
|
||||
# kill_on_output="password for admin",
|
||||
# )
|
||||
|
||||
backend_process = cxc_toolkit.exec.run_background(
|
||||
"npm start", log_path="/tmp/backend.log"
|
||||
)
|
||||
|
||||
# NB: run_command + kill_on_output may wait indefinitely, use run_background + hard limit instead
|
||||
time.sleep(10)
|
||||
|
||||
backend_process.terminate()
|
||||
|
||||
# read the log file
|
||||
with open("/tmp/backend.log", "r") as f:
|
||||
lines = f.readlines()
|
||||
for line in lines:
|
||||
if "password for admin" in line:
|
||||
print(f"found password line: ---{line}---")
|
||||
admin_password = line.split("password for admin is:")[1].strip()
|
||||
print(f"Extracted admin password: {admin_password}")
|
||||
CONTEXT.ADMIN_PASSWORD = admin_password
|
||||
return
|
||||
|
||||
if not CONTEXT.ADMIN_PASSWORD:
|
||||
print("Error: No admin password found")
|
||||
|
||||
# print the log file
|
||||
with open("/tmp/backend.log", "r") as f:
|
||||
print(f.read())
|
||||
|
||||
exit(1)
|
||||
|
||||
|
||||
def update_server_config():
|
||||
# Load the config file
|
||||
config_file = f"{os.getcwd()}/volatile/config/config.json"
|
||||
|
||||
with open(config_file, "r") as f:
|
||||
config = json.load(f)
|
||||
|
||||
# Ensure services and mountpoint sections exist
|
||||
if "services" not in config:
|
||||
config["services"] = {}
|
||||
if "mountpoint" not in config["services"]:
|
||||
config["services"]["mountpoint"] = {}
|
||||
if "mountpoints" not in config["services"]["mountpoint"]:
|
||||
config["services"]["mountpoint"]["mountpoints"] = {}
|
||||
|
||||
# Add the mountpoint configuration
|
||||
mountpoint_config = {
|
||||
"/": {"mounter": "puterfs"},
|
||||
"/admin/tmp": {"mounter": "memoryfs"},
|
||||
}
|
||||
|
||||
# Merge mountpoints (overwrite existing ones)
|
||||
config["services"]["mountpoint"]["mountpoints"].update(mountpoint_config)
|
||||
|
||||
# Write the updated config back
|
||||
with open(config_file, "w") as f:
|
||||
json.dump(config, f, indent=2)
|
||||
|
||||
|
||||
def init_api_test():
|
||||
# Load the example config
|
||||
example_config_path = f"{os.getcwd()}/tools/api-tester/example_config.yml"
|
||||
config_path = f"{os.getcwd()}/tools/api-tester/config.yml"
|
||||
|
||||
with open(example_config_path, "r") as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
# Update the token
|
||||
if not CONTEXT.TOKEN:
|
||||
print("Warning: No token available in CONTEXT")
|
||||
exit(1)
|
||||
|
||||
config["token"] = CONTEXT.TOKEN
|
||||
config["url"] = "http://api.puter.localhost:4100"
|
||||
|
||||
# Write the updated config
|
||||
with open(config_path, "w") as f:
|
||||
yaml.dump(config, f, default_flow_style=False, indent=2)
|
||||
|
||||
|
||||
def run():
|
||||
# =========================================================================
|
||||
# free the port 4100
|
||||
# =========================================================================
|
||||
cxc_toolkit.exec.run_command("fuser -k 4100/tcp", ignore_failure=True)
|
||||
|
||||
# =========================================================================
|
||||
# config server
|
||||
# =========================================================================
|
||||
cxc_toolkit.exec.run_command("npm install")
|
||||
init_server_config()
|
||||
get_admin_password()
|
||||
update_server_config()
|
||||
|
||||
# =========================================================================
|
||||
# config client
|
||||
# =========================================================================
|
||||
cxc_toolkit.exec.run_background("npm start")
|
||||
# wait 10s for the server to start
|
||||
time.sleep(10)
|
||||
|
||||
get_token()
|
||||
init_api_test()
|
||||
|
||||
# =========================================================================
|
||||
# run the test
|
||||
# =========================================================================
|
||||
cxc_toolkit.exec.run_command(
|
||||
"node ./tools/api-tester/apitest.js --unit --stop-on-failure"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -1,8 +0,0 @@
|
||||
url: http://api.puter.localhost:4100/
|
||||
username: admin
|
||||
token: ---
|
||||
mountpoints:
|
||||
- path: /
|
||||
provider: puterfs
|
||||
- path: /admin/tmp
|
||||
provider: memoryfs
|
||||
Generated
-388
@@ -1,388 +0,0 @@
|
||||
{
|
||||
"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.12.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.12.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz",
|
||||
"integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"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/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-func-name": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
|
||||
"integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"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/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user