From 617535b61856dbb6d5cbdb50cdb2c8933694d92e Mon Sep 17 00:00:00 2001 From: Mortza81 Date: Sun, 23 Feb 2025 15:31:12 +0330 Subject: [PATCH 01/24] WIP: app create tests --- package-lock.json | 629 ++++++++++++++++++++++++++++- package.json | 7 +- src/base.ts | 4 + src/types/network.ts | 2 + test/fixtures/apps/fixture.ts | 35 ++ test/fixtures/networks/fixture.ts | 20 + test/global.config.spec.ts | 254 ------------ test/got.config.spec.ts | 17 - test/mocha.opts | 5 - test/units/app/create.unit.test.ts | 91 +++++ test/utils.test.ts | 62 --- test/utils/fixture.ts | 3 - test/utils/run.ts | 23 -- 13 files changed, 773 insertions(+), 379 deletions(-) create mode 100644 test/fixtures/apps/fixture.ts create mode 100644 test/fixtures/networks/fixture.ts delete mode 100644 test/global.config.spec.ts delete mode 100644 test/got.config.spec.ts delete mode 100644 test/mocha.opts create mode 100644 test/units/app/create.unit.test.ts delete mode 100644 test/utils.test.ts delete mode 100644 test/utils/fixture.ts delete mode 100644 test/utils/run.ts diff --git a/package-lock.json b/package-lock.json index d906b930..9fe59062 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,8 +44,10 @@ "liara": "bin/run.js" }, "devDependencies": { + "@oclif/test": "^4.1.11", "@types/async-retry": "^1.4.9", "@types/bytes": "^3.1.5", + "@types/chai": "^5.0.1", "@types/fs-extra": "^11.0.4", "@types/inquirer": "^9.0.7", "@types/jest": "^29.5.14", @@ -55,10 +57,13 @@ "@types/ua-parser-js": "^0.7.39", "@types/update-notifier": "^6.0.8", "@types/ws": "^8.5.13", + "chai": "^5.2.0", "eslint": "^8.53.0", "eslint-config-oclif": "^5.2.2", "eslint-config-oclif-typescript": "^3.1.13", "husky": "^9.1.7", + "mocha": "^11.1.0", + "nock": "^14.0.1", "oclif": "^4.14.12", "prettier": "^3.3.3", "pretty-quick": "^4.0.0", @@ -2724,6 +2729,24 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mswjs/interceptors": { + "version": "0.37.6", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.37.6.tgz", + "integrity": "sha512-wK+5pLK5XFmgtH3aQ2YVvA3HohS3xqV/OxuVOdNx9Wpnz7VE/fnC+e1A7ln6LFYeck7gOJ/dsZV6OLplOtAJ2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3437,6 +3460,48 @@ "node": ">=8" } }, + "node_modules/@oclif/test": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@oclif/test/-/test-4.1.11.tgz", + "integrity": "sha512-j689R13E2so1Rj6jJUfQ67yJ4N7u6L5KFzv87cvUfD9AZ79xAtCxGrd34/iOLUDJmv1huFt/0QumBcjKoWUSYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansis": "^3.16.0", + "debug": "^4.4.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@oclif/core": ">= 3.0.0" + } + }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -4320,6 +4385,16 @@ "integrity": "sha512-VgZkrJckypj85YxEsEavcMmmSOIzkUHqWmM4CCyia5dc54YwsXzJ5uT4fYxBQNEXx+oF1krlhgCbvfubXqZYsQ==", "dev": true }, + "node_modules/@types/chai": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.0.1.tgz", + "integrity": "sha512-5T8ajsg3M/FOncpLYW7sdOcD6yf4+722sze/tc4KQV0P8Z2rAr3SAuHCIkYmYpt8VbcQlnz8SxlOlPQYefe4cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, "node_modules/@types/cli-progress": { "version": "3.11.5", "resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.5.tgz", @@ -4334,6 +4409,13 @@ "integrity": "sha512-GUvNiia85zTDDIx0iPrtF3pI8dwrQkfuokEqxqPDE55qxH0U5SZz4awVZjiJLWN2ZZRkXCUqgsMUbygXY+kytA==", "dev": true }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/fs-extra": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", @@ -4837,6 +4919,16 @@ "string-width": "^4.1.0" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-escapes": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", @@ -4876,11 +4968,12 @@ "integrity": "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==" }, "node_modules/ansis": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.3.2.tgz", - "integrity": "sha512-cFthbBlt+Oi0i9Pv/j6YdVWJh54CtjGACaMPCIrEV4Ha7HWsIjXDwseYV79TIL0B4+KfSwD5S70PeQDkPUd1rA==", + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.16.0.tgz", + "integrity": "sha512-sU7d/tfZiYrsIAXbdL/CNZld5bCkruzwT5KmqmadCJYxuLxHAOBjidxD5+iLmN/6xEfjcQq1l7OpsiCBlc4LzA==", + "license": "ISC", "engines": { - "node": ">=15" + "node": ">=14" } }, "node_modules/anymatch": { @@ -4888,7 +4981,6 @@ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, - "peer": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -5032,6 +5124,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -5233,6 +5335,19 @@ } ] }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -5388,6 +5503,13 @@ "node": ">=8" } }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, "node_modules/browserslist": { "version": "4.21.9", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", @@ -5662,6 +5784,23 @@ "cdl": "bin/cdl.js" } }, + "node_modules/chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/chalk": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", @@ -5708,6 +5847,54 @@ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/chrono-node": { "version": "2.7.7", "resolved": "https://registry.npmjs.org/chrono-node/-/chrono-node-2.7.7.tgz", @@ -6092,9 +6279,10 @@ "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==" }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -6107,6 +6295,19 @@ } } }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -6147,6 +6348,16 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -7544,6 +7755,16 @@ "micromatch": "^4.0.2" } }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, "node_modules/flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -7648,7 +7869,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } @@ -8102,6 +8322,16 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/header-case": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", @@ -8547,6 +8777,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-boolean-object": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.1.tgz", @@ -8822,6 +9065,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, "node_modules/is-npm": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.0.0.tgz", @@ -10334,6 +10584,13 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -10499,6 +10756,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/loupe": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "dev": true, + "license": "MIT" + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -10695,6 +10959,208 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mocha": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.1.0.tgz", + "integrity": "sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/mocha/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/mocha/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/mocha/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/mocha/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -10755,6 +11221,21 @@ "tslib": "^2.0.3" } }, + "node_modules/nock": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.1.tgz", + "integrity": "sha512-IJN4O9pturuRdn60NjQ7YkFt6Rwei7ZKaOwb1tvUIIqTgeD0SDDAX3vrqZD4wcXczeEy/AsUXxpGpP/yHqV7xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mswjs/interceptors": "^0.37.3", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">=18.20.0 <20 || >=20.12.1" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -10788,7 +11269,6 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -11316,6 +11796,13 @@ "node": ">=0.10.0" } }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, "node_modules/p-cancelable": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", @@ -11380,6 +11867,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -11502,15 +11996,16 @@ "dev": true }, "node_modules/path-scurry": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", - "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -11532,6 +12027,16 @@ "node": ">=8" } }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -11754,6 +12259,16 @@ "node": ">= 6" } }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -11835,6 +12350,16 @@ "integrity": "sha512-y/M9weqWAH4iopRd7EHDEQQvpFPHj1AA3oHozE9tfITHUtTR7Z9PSlIRRG2l1GuW7sefC1cXFfIcF+cgnShdBA==", "dev": true }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -12010,6 +12535,19 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/redeyed": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", @@ -12419,6 +12957,16 @@ "upper-case-first": "^2.0.2" } }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -12781,6 +13329,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -13871,6 +14426,13 @@ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" }, + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -13998,6 +14560,45 @@ "node": ">=12" } }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index 65efa6f5..f525ee09 100644 --- a/package.json +++ b/package.json @@ -41,8 +41,10 @@ "ws": "8.18.0" }, "devDependencies": { + "@oclif/test": "^4.1.11", "@types/async-retry": "^1.4.9", "@types/bytes": "^3.1.5", + "@types/chai": "^5.0.1", "@types/fs-extra": "^11.0.4", "@types/inquirer": "^9.0.7", "@types/jest": "^29.5.14", @@ -52,10 +54,13 @@ "@types/ua-parser-js": "^0.7.39", "@types/update-notifier": "^6.0.8", "@types/ws": "^8.5.13", + "chai": "^5.2.0", "eslint": "^8.53.0", "eslint-config-oclif": "^5.2.2", "eslint-config-oclif-typescript": "^3.1.13", "husky": "^9.1.7", + "mocha": "^11.1.0", + "nock": "^14.0.1", "oclif": "^4.14.12", "prettier": "^3.3.3", "pretty-quick": "^4.0.0", @@ -169,7 +174,7 @@ "format": "prettier \"**/*.ts\" \"**/*.js\" \"**/*.json\" --ignore-path ./.prettierignore --write", "postpack": "rm -f oclif.manifest.json tsconfig.tsbuildinfo", "prepack": "set -ex; rm -rf lib && tsc -b && oclif manifest && oclif readme", - "test": "NODE_OPTIONS=--experimental-vm-modules jest", + "test": "node --loader ts-node/esm ./node_modules/.bin/mocha --forbid-only --timeout 5000 \"test/**/*.test.ts\"", "version": "oclif readme && git add README.md", "readme": "oclif readme", "prepare": "husky install", diff --git a/src/base.ts b/src/base.ts index 0e13a835..b264c527 100644 --- a/src/base.ts +++ b/src/base.ts @@ -77,6 +77,10 @@ export interface IProject { project_id: string; created_at: string; isDeployed: boolean; + network?: { + _id: string; + name: string; + }; } export interface IGetProjectsResponse { diff --git a/src/types/network.ts b/src/types/network.ts index 2dff0b6f..9de02c7d 100644 --- a/src/types/network.ts +++ b/src/types/network.ts @@ -2,4 +2,6 @@ export default interface INetwork { _id: string; name: string; createdAt: string; + projectCount: number, + databaseCount: number } diff --git a/test/fixtures/apps/fixture.ts b/test/fixtures/apps/fixture.ts new file mode 100644 index 00000000..74ab55ea --- /dev/null +++ b/test/fixtures/apps/fixture.ts @@ -0,0 +1,35 @@ +import {IGetProjectsResponse} from "../../../src/base" +export const projects: IGetProjectsResponse={ + projects: [ + { + _id: "64c7f1a2b3e8c91d0e5f7b2a", + project_id: "proj12345", + type: "web-application", + status: "ACTIVE", + scale: 3, + planID: "standard-plus-g2", + bundlePlanID: "basic", + created_at: "2023-10-05T12:34:56Z", + isDeployed: true, + network: { + _id: "64c7f1a2b3e8c91d0e5f7b2b", + name: "network-abc123" + } + }, + { + _id: "64c7f1a2b3e8c91d0e5f7b2c", + project_id: "proj67890", + type: "backend-service", + status: "INACTIVE", + scale: 1, + planID: "small-g2", + bundlePlanID: "standard", + created_at: "2023-10-01T09:15:30Z", + isDeployed: false, + network: { + _id: "64c7f1a2b3e8c91d0e5f7b2d", + name: "network-xyz789" + } + } + ] +}; \ No newline at end of file diff --git a/test/fixtures/networks/fixture.ts b/test/fixtures/networks/fixture.ts new file mode 100644 index 00000000..2fb04da1 --- /dev/null +++ b/test/fixtures/networks/fixture.ts @@ -0,0 +1,20 @@ +import IGetNetworkResponse from "../../../src/types/get-network-response" +export const networks: IGetNetworkResponse = { + networks: [ + { + _id: "64c7f1a2b3e8c91d0e5f7b2a", + name: "network-abc123", + createdAt: "2023-10-05T12:34:56Z", + projectCount: 1, + databaseCount: 0 + }, + { + _id: "64c7f1a2b3e8c91d0e5f7b2b", + name: "network-xyz789", + createdAt: "2023-10-01T09:15:30Z", + projectCount: 1, + databaseCount: 0 + } + ] +}; + diff --git a/test/global.config.spec.ts b/test/global.config.spec.ts deleted file mode 100644 index afa1ca70..00000000 --- a/test/global.config.spec.ts +++ /dev/null @@ -1,254 +0,0 @@ -import got from 'got'; -import fs from 'fs-extra'; -import { Config } from '@oclif/core'; -import Command, { IConfig } from '../src/base'; -import { - GLOBAL_CONF_PATH, - GLOBAL_CONF_VERSION, - PREVIOUS_GLOBAL_CONF_PATH, -} from '../src/constants'; - -// test users credentials -const newContentCredentials = { - accounts: { - test1: { - email: 'test1@gmail.com', - avatar: '//www.gravatar.com/avatar/b27b143b69933c34caafcce34453f2b3?d=mp', - region: 'germany', - current: true, - fullname: 'test1', - api_token: 'test-api-token', - }, - test2: { - email: 'test2@gmail.com', - avatar: '//www.gravatar.com/avatar/d44cd1682dd31bf9ef8a5a67ca399bc1?d=mp', - region: 'iran', - current: false, - fullname: 'test2', - api_token: 'test2-api-token', - }, - }, - version: '1', -}; - -const oldContentCredentialsLogin = { - api_token: 'test-api-token', - region: 'iran', - current: null, -}; - -const oldContentCredentialsAccounts = { - api_token: 'test-multiaccount-api-token', - region: 'iran', - current: null, - accounts: { - user1: { - email: 'userone@gmail.com', - api_token: 'user1-multiaccount-api-token', - region: 'iran', - }, - user2: { - email: 'usertwo@gmail.com', - api_token: 'user2-multiaccount-api-token', - region: 'germany', - }, - }, -}; - -jest.mock('got'); - -// mocking config (user credentials) change direcotry to /tmp -jest.mock('../src/constants.ts', () => ({ - get GLOBAL_CONF_PATH() { - return '/tmp/.liara-auth.json'; - }, - get PREVIOUS_GLOBAL_CONF_PATH() { - return '/tmp/.liara.json'; - }, - - get GLOBAL_CONF_VERSION(): string { - return '1'; - }, - - REGIONS_API_URL: { - iran: 'https://api.liara.ir', - germany: 'https://api.liara.ir', - }, -})); - -// create files for user credentials in /tmp directory -async function createCredentials(path: string, content: any) { - // 1) create .liara-auth.json file - await fs.writeJSON(path, content); -} - -class TestConfig extends Command { - async run() { - // const {api_token, region} = oldContentCredentialsLogin - // console.log( - // await this.setAxiosConfig({ region: "iran", "api-token": "test" }) - // ); - // console.log(this.axiosConfig); - this.setGotConfig = (config: IConfig): Promise => { - return Promise.resolve(); - }; - - this.got = got; - return this.readGlobalConfig(); - } -} - -class TestGotRequest extends Command { - async run() { - await this.setGotConfig({ region: 'iran', 'api-token': 'test' }); - return this.got; - } -} -beforeAll(async () => { - await createCredentials('/tmp/.liara-auth.json', newContentCredentials); -}); - -describe('reading global configuration', () => { - test('check if new global path exist', async () => { - const content = await new TestConfig([], {} as Config).run(); - - // check if previous global path not exist any more. - const previousConfigExists = await fs.pathExists(PREVIOUS_GLOBAL_CONF_PATH); - expect(previousConfigExists).toBeFalsy(); - - // checking content of new config path (.liara-auth.json) - expect(content.version).toBe(GLOBAL_CONF_VERSION); - expect(content.accounts).toBeDefined(); - - expect(content.accounts.test1.email).toBe( - newContentCredentials.accounts.test1.email, - ); - expect(content.accounts.test1.avatar).toBe( - newContentCredentials.accounts.test1.avatar, - ); - expect(content.accounts.test1.region).toBe( - newContentCredentials.accounts.test1.region, - ); - expect(content.accounts.test1.current).toBe( - newContentCredentials.accounts.test1.current, - ); - expect(content.accounts.test1.fullname).toBe( - newContentCredentials.accounts.test1.fullname, - ); - expect(content.accounts.test1.api_token).toBe( - newContentCredentials.accounts.test1.api_token, - ); - expect(content.accounts.test2.email).toBe( - newContentCredentials.accounts.test2.email, - ); - expect(content.accounts.test2.avatar).toBe( - newContentCredentials.accounts.test2.avatar, - ); - expect(content.accounts.test2.region).toBe( - newContentCredentials.accounts.test2.region, - ); - expect(content.accounts.test2.current).toBe( - newContentCredentials.accounts.test2.current, - ); - expect(content.accounts.test2.fullname).toBe( - newContentCredentials.accounts.test2.fullname, - ); - expect(content.accounts.test2.api_token).toBe( - newContentCredentials.accounts.test2.api_token, - ); - }); - test('not only .liara-auth.json not exists but also .liara.json not exists too', async () => { - // delete both file credentials first - fs.removeSync(PREVIOUS_GLOBAL_CONF_PATH); - fs.removeSync(GLOBAL_CONF_PATH); - - const content = await new TestConfig([], {} as Config).run(); - expect(typeof content.accounts).toBe('object'); - expect(content.accounts).toBeDefined(); - expect(content.version).toBe(GLOBAL_CONF_VERSION); - }); - test('check if only .liara.json exist and user never add any accounts just login', async () => { - await createCredentials('/tmp/.liara.json', oldContentCredentialsLogin); - const data = { - user: { - api_token: 'test-api-token-from-server', - avatar: 'user-avatar', - fullname: 'test-user-name', - email: 'testuser@gmail.com', - }, - }; - //@ts-ignore - - got.get.mockImplementation((path: string, config: GotOptions) => { - return { - json() { - return Promise.resolve({ ...data }); - }, - }; - }); - - const content = await new TestConfig([], {} as Config).run(); - const accountName = `${data.user.email.split('@')[0]}_${ - oldContentCredentialsLogin.region - }`; - - expect(content.version).toBe(GLOBAL_CONF_VERSION); - expect(content.accounts).toBeDefined(); - expect(content.accounts[accountName]).toBeDefined(); - expect(content.accounts[accountName].api_token).toBe( - oldContentCredentialsLogin.api_token, - ); - expect(content.accounts[accountName].region).toBe( - oldContentCredentialsLogin.region, - ); - expect(content.accounts[accountName].fullname).toBe(data.user.fullname); - expect(content.accounts[accountName].email).toBe(data.user.email); - expect(content.accounts[accountName].avatar).toBe(data.user.avatar); - expect(content.accounts[accountName].current).toBe(true); - }); - test('check if only .liara.json exist and user add accounts', async () => { - const data = { - user: { - api_token: 'test-api-token-from-server', - avatar: 'user-avatar', - fullname: 'test-user-name', - email: 'testuser@gmail.com', - }, - }; - //@ts-ignore - got.get.mockImplementation((path: string, config: GotOptions) => { - return { - json() { - return Promise.resolve({ ...data }); - }, - }; - }); - await createCredentials('/tmp/.liara.json', oldContentCredentialsAccounts); - const content = await new TestConfig([], {} as Config).run(); - - expect(content.accounts).toBeDefined(); - expect(content.version).toBe(GLOBAL_CONF_VERSION); - expect(content.accounts['user1']).toBeDefined(); - expect(content.accounts['user2']).toBeDefined(); - expect(content.accounts['user1'].current).toBe(false); - expect(content.accounts['user1'].avatar).toBe(data.user.avatar); - expect(content.accounts['user1'].fullname).toBe(data.user.fullname); - expect(content.accounts['user1'].api_token).toBe( - oldContentCredentialsAccounts.accounts.user1.api_token, - ); - expect(content.accounts['user1'].email).toBe(data.user.email); - expect(content.accounts['user1'].region).toBe( - oldContentCredentialsAccounts.accounts.user1.region, - ); - expect(content.accounts['user2'].current).toBe(false); - expect(content.accounts['user2'].avatar).toBe(data.user.avatar); - expect(content.accounts['user2'].fullname).toBe(data.user.fullname); - expect(content.accounts['user2'].api_token).toBe( - oldContentCredentialsAccounts.accounts.user2.api_token, - ); - expect(content.accounts['user2'].email).toBe(data.user.email); - expect(content.accounts['user2'].region).toBe( - oldContentCredentialsAccounts.accounts.user2.region, - ); - }); -}); diff --git a/test/got.config.spec.ts b/test/got.config.spec.ts deleted file mode 100644 index 5b5c8437..00000000 --- a/test/got.config.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import Command from '../src/base'; -import { Config } from '@oclif/core'; - -class TestGotRequest extends Command { - async run() { - await this.setGotConfig({ region: 'iran', 'api-token': 'test' }); - return this.got; - } -} - -test('http configuration', async () => { - const configs = await new TestGotRequest([], {} as Config).run(); - console.log(configs.defaults.options); - expect(configs.defaults.options.timeout.request).toBe(10000); - expect(configs.defaults.options.headers.authorization).toBe('Bearer test'); - expect(configs.defaults.options.prefixUrl).toBe('https://api.liara.ir/'); -}); diff --git a/test/mocha.opts b/test/mocha.opts deleted file mode 100644 index 73fb8366..00000000 --- a/test/mocha.opts +++ /dev/null @@ -1,5 +0,0 @@ ---require ts-node/register ---watch-extensions ts ---recursive ---reporter spec ---timeout 5000 diff --git a/test/units/app/create.unit.test.ts b/test/units/app/create.unit.test.ts new file mode 100644 index 00000000..eefd1ffb --- /dev/null +++ b/test/units/app/create.unit.test.ts @@ -0,0 +1,91 @@ +import { runCommand } from '@oclif/test'; +import nock from 'nock'; +import { expect } from 'chai'; +import { networks } from "../../fixtures/networks/fixture.ts" +describe('app:create', function () { + let api: ReturnType; + const appName = 'test-app'; + const platform = 'laravel'; + const plan = 'small-g2'; + const bundlePlan = 'standard'; + const network = networks.networks[0].name; + api = nock('https://api.iran.liara.ir'); + + api + .get('/v1/networks') + .query({ teamID: '' }) + .reply(200, networks); + + api + .post('/v1/projects/', { + name: appName, + planID: plan, + platform: platform, + bundlePlanID: bundlePlan, + network: networks.networks[0]._id, + readOnlyRootFilesystem: true, + }) + .query({ teamID: '' }) + .reply(409,{ + statusCode: 409, + error: "Conflict", + message: "Project exists.", + data: null + }); + + afterEach(function () { + api.done(); + nock.cleanAll(); + }); + + it('creates an app with the specified flags', async ()=> { + + const { stdout, stderr } = await runCommand([ + 'app:create', + '--app', + appName, + '--platform', + platform, + '--plan', + plan, + '--feature-plan', + bundlePlan, + '--network', + network, + '--read-only', + 'true', + ]); + expect(stdout).to.equal(`App ${appName} created.\n`); + }); + it.skip('throws an error if app name already exists',async ()=>{ + api + .post('/v1/projects/', { + name: appName, + planID: plan, + platform: platform, + bundlePlanID: bundlePlan, + network: networks.networks[0]._id, + readOnlyRootFilesystem: true, + }) + .query({ teamID: '' }) + .reply(409); + + const { stdout, stderr } = await runCommand([ + 'app:create', + '--app', + appName, + '--platform', + platform, + '--plan', + plan, + '--feature-plan', + bundlePlan, + '--network', + network, + '--read-only', + 'true', + ]); + console.log(stderr); + expect(stderr).to.equal(`The app already exists. Please use a unique name for your app.`); + }) + }); diff --git a/test/utils.test.ts b/test/utils.test.ts deleted file mode 100644 index 3a5683ba..00000000 --- a/test/utils.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { expect } from '@oclif/test'; - -import fixture from './utils/fixture'; -import getFiles from '../src/utils/get-files'; -import validatePort from '../src/utils/validate-port'; -import detectPlatform from '../src/utils/detect-platform'; - -describe('utils', () => { - it('should detect DotNet platform', () => { - expect(detectPlatform(fixture('dotnet-apps/app1'))).to.not.eq('dotnet'); // Too deep - expect(detectPlatform(fixture('dotnet-apps/app3'))).to.not.eq('dotnet'); // No .csproj file - - expect(detectPlatform(fixture('dotnet-apps/app2'))).to.eq('dotnet'); // Max deep - expect(detectPlatform(fixture('dotnet-apps/app4'))).to.eq('dotnet'); - }); - - it('should throw an error for invalid liara.json file', async () => { - expect(validatePort('asdf')).to.contain('number'); - expect(validatePort('3.2')).to.contain('integer'); - expect(validatePort('-3.2')).to.contain('integer'); - expect(validatePort('-80')).to.contain('integer'); - expect(validatePort('80')).to.be.eq(true); - expect(validatePort('5000')).to.be.eq(true); - expect(validatePort(5000)).to.be.eq(true); - expect(validatePort(80)).to.be.eq(true); - }); - - it('should respect .gitignore', async () => { - const { files } = await getFiles(fixture('simple-gitignore')); - expect(files).to.have.length(1); - }); - - it('should respect nested ignore files', async () => { - const { files } = await getFiles(fixture('nested-ignore-files')); - expect(files).to.have.length(2); - }); - - it("should respect ignore files' priority", async () => { - const { files } = await getFiles(fixture('ignore-files-priority')); - expect(files).to.have.length(2); - }); - - it('should ignore default ignore patterns', async () => { - const { files } = await getFiles(fixture('default-ignores')); - expect(files).to.have.length(1); - }); - - it('should override default ignore patterns', async () => { - const { files } = await getFiles(fixture('override-default-ignores')); - expect(files).to.have.length(2); - }); - - it('case sensitive ignore', async () => { - const { files } = await getFiles(fixture('ignore-case-sensitive')); - expect(files).to.have.length(1); - }); - - it('should ignore absolute patterns', async () => { - const { files } = await getFiles(fixture('ignore-absolute-patterns')); - expect(files).to.have.length(1); - }); -}); diff --git a/test/utils/fixture.ts b/test/utils/fixture.ts deleted file mode 100644 index e9655bef..00000000 --- a/test/utils/fixture.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function fixture(name: string): string { - return __dirname + '/../fixtures/' + name; -} diff --git a/test/utils/run.ts b/test/utils/run.ts deleted file mode 100644 index d810537f..00000000 --- a/test/utils/run.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { exec } from 'child_process'; -import * as path from 'path'; -import { Readable } from 'stream'; - -export default async function run(args: string[]) { - args.unshift( - 'node', - path.resolve(path.join(__dirname, '..', '..', 'bin', 'run')) - ); - const result = await exec(args.join(' ')); - return { - stderr: await concatStreamPromise(result.stderr), - stdout: await concatStreamPromise(result.stdout), - }; -} - -function concatStreamPromise(stream: Readable) { - return new Promise((resolve) => { - let result = ''; - stream.on('data', (data) => (result += data)); - stream.on('end', () => resolve(result)); - }); -} From 6b3333add38074b840c48ee7a398aaa698715d76 Mon Sep 17 00:00:00 2001 From: Mortza81 Date: Tue, 25 Feb 2025 15:36:58 +0330 Subject: [PATCH 02/24] chore: add tests for app create command --- test/units/app/create.unit.test.ts | 246 +++++++++++++++++++---------- 1 file changed, 164 insertions(+), 82 deletions(-) diff --git a/test/units/app/create.unit.test.ts b/test/units/app/create.unit.test.ts index eefd1ffb..41a5b16a 100644 --- a/test/units/app/create.unit.test.ts +++ b/test/units/app/create.unit.test.ts @@ -1,91 +1,173 @@ -import { runCommand } from '@oclif/test'; -import nock from 'nock'; -import { expect } from 'chai'; +import { runCommand } from "@oclif/test"; +import nock from "nock"; +import { expect } from "chai"; import { networks } from "../../fixtures/networks/fixture.ts" -describe('app:create', function () { - let api: ReturnType; - const appName = 'test-app'; - const platform = 'laravel'; - const plan = 'small-g2'; - const bundlePlan = 'standard'; - const network = networks.networks[0].name; - api = nock('https://api.iran.liara.ir'); - - api - .get('/v1/networks') - .query({ teamID: '' }) - .reply(200, networks); - - api - .post('/v1/projects/', { - name: appName, - planID: plan, - platform: platform, - bundlePlanID: bundlePlan, - network: networks.networks[0]._id, - readOnlyRootFilesystem: true, - }) - .query({ teamID: '' }) - .reply(409,{ - statusCode: 409, - error: "Conflict", - message: "Project exists.", - data: null + +describe("app:create", function () { + const api = nock("https://api.iran.liara.ir"); + + + + beforeEach(() => { + api + .get("/v1/networks") + .query({ teamID: "" }) + .reply(200, networks); }); - afterEach(function () { + afterEach(() => { api.done(); nock.cleanAll(); }); - it('creates an app with the specified flags', async ()=> { - - const { stdout, stderr } = await runCommand([ - 'app:create', - '--app', - appName, - '--platform', - platform, - '--plan', - plan, - '--feature-plan', - bundlePlan, - '--network', - network, - '--read-only', - 'true', - ]); - expect(stdout).to.equal(`App ${appName} created.\n`); - }); - it.skip('throws an error if app name already exists',async ()=>{ - api - .post('/v1/projects/', { - name: appName, - planID: plan, - platform: platform, - bundlePlanID: bundlePlan, - network: networks.networks[0]._id, - readOnlyRootFilesystem: true, - }) - .query({ teamID: '' }) - .reply(409); - - const { stdout, stderr } = await runCommand([ - 'app:create', - '--app', - appName, - '--platform', - platform, - '--plan', - plan, - '--feature-plan', - bundlePlan, - '--network', - network, - '--read-only', - 'true', + it.skip("creates an app with the specified flags", async () => { + + api + .post("/v1/projects/", { + name: "test-app", + planID: "small-g2", + platform: "laravel", + bundlePlanID: "standard", + network: networks.networks[0]._id, + readOnlyRootFilesystem: true, + }) + .query({ teamID: "" }) + .reply(200); + + const { stdout } = await runCommand([ + "app:create", + "--app", "test-app", + "--platform", "laravel", + "--plan", "small-g2", + "--feature-plan", "standard", + "--network", networks.networks[0].name, + "--read-only", "true", + ]); + + expect(stdout).to.equal(`App test-app created.\n`); + }); + + it.skip("throws an error if app name already exists", async () => { + + api + .post("/v1/projects/", { + name: "test-app", + planID: "small-g2", + platform: "laravel", + bundlePlanID: "standard", + network: networks.networks[0]._id, + readOnlyRootFilesystem: true, + }) + .query({ teamID: "" }) + .reply(409, { + statusCode: 409, + error: "Conflict", + message: "Project exists.", + data: null + }); + + const { error } = await runCommand([ + "app:create", + "--app", "test-app", + "--platform", "laravel", + "--plan", "small-g2", + "--feature-plan", "standard", + "--network", networks.networks[0].name, + "--read-only", "true", ]); - console.log(stderr); - expect(stderr).to.equal(`The app already exists. Please use a unique name for your app.`); - }) + + expect(error?.message).to.equal("The app already exists. Please use a unique name for your app."); }); + + it.skip("throws an error if the network is not found", async () => { + + const { error } = await runCommand([ + "app:create", + "--app", "test-app", + "--platform", "laravel", + "--plan", "small-g2", + "--feature-plan", "standard", + "--network", "not-found", + "--read-only", "true", + ]); + + expect(error?.message).to.equal("Network not-found not found."); + }) + + it.skip("thorws an error if user select a feature plan that is not available for free plan", async () => { + + const { error } = await runCommand([ + "app:create", + "--app", "test-app", + "--platform", "laravel", + "--plan", "free", + "--feature-plan", "standard", + "--network", networks.networks[0].name, + "--read-only", "true", + ]); + + expect(error?.message).to.equal(`Only "free" feature bundle plan is available for free plan.`); + } + ) + + it.skip("throws an error if the user does not have enough balance", async () => { + + api + .post("/v1/projects/", { + name: "test-app", + planID: "small-g2", + platform: "laravel", + bundlePlanID: "standard", + network: networks.networks[0]._id, + readOnlyRootFilesystem: true, + }) + .query({ teamID: "" }) + .reply(402); + + const { error } = await runCommand([ + "app:create", + "--app", "test-app", + "--platform", "laravel", + "--plan", "small-g2", + "--feature-plan", "standard", + "--network", networks.networks[0].name, + "--read-only", "true", + ]); + + expect(error?.message).to.equal("Not enough balance. Please charge your account."); + }) + + it("throws proper error if the app creation fails with status code 500", async () => { + + api + .post("/v1/projects/", { + name: "test-app", + planID: "small-g2", + platform: "laravel", + bundlePlanID: "standard", + network: networks.networks[0]._id, + readOnlyRootFilesystem: true, + }) + .query({ teamID: "" }) + .reply(500); + + const { error } = await runCommand([ + "app:create", + "--app", "test-app", + "--platform", "laravel", + "--plan", "small-g2", + "--feature-plan", "standard", + "--network", networks.networks[0].name, + "--read-only", "true", + ]); + + expect(error?.message).to.equal(`Error: Unable to Create App + Please try the following steps: + 1. Check your internet connection. + 2. Ensure you have enough balance. + 3. Try again later. + 4. If you still have problems, please contact support by submitting a ticket at https://console.liara.ir/tickets`) + }) + + +}); \ No newline at end of file From d4276bf92206e831726e9ccb245ee1b6a3c0a56e Mon Sep 17 00:00:00 2001 From: Mortza81 Date: Sun, 9 Mar 2025 14:49:26 +0330 Subject: [PATCH 03/24] add: login command tests --- package-lock.json | 148 +++++++++++++++++- package.json | 2 + src/commands/login.ts | 6 +- test/fixtures/accounts/fixture.ts | 35 +++++ test/units/app/create.unit.test.ts | 242 ++++++++++++++++------------- test/units/auth/login.unit.test.ts | 143 +++++++++++++++++ 6 files changed, 460 insertions(+), 116 deletions(-) create mode 100644 test/fixtures/accounts/fixture.ts create mode 100644 test/units/auth/login.unit.test.ts diff --git a/package-lock.json b/package-lock.json index 9fe59062..7b6bc94c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "progress": "2.0.3", "semver": "7.6.3", "shamsi-date-converter": "1.0.5", + "sinon": "^19.0.2", "tar": "7.4.3", "ua-parser-js": "1.0.38", "update-notifier": "7.1.0", @@ -54,6 +55,7 @@ "@types/node": "18.15.11", "@types/progress": "^2.0.7", "@types/semver": "^7.5.8", + "@types/sinon": "^17.0.4", "@types/ua-parser-js": "^0.7.39", "@types/update-notifier": "^6.0.8", "@types/ws": "^8.5.13", @@ -3583,11 +3585,10 @@ } }, "node_modules/@sinonjs/commons": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", - "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", - "dev": true, - "peer": true, + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" } @@ -3602,6 +3603,32 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", + "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "lodash.get": "^4.4.2", + "type-detect": "^4.1.0" + } + }, + "node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "license": "(Unlicense OR Apache-2.0)" + }, "node_modules/@smithy/abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.1.tgz", @@ -4547,6 +4574,23 @@ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, + "node_modules/@types/sinon": { + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.4.tgz", + "integrity": "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -10614,6 +10658,12 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "license": "MIT" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -10718,6 +10768,13 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -11211,6 +11268,28 @@ "node": "*" } }, + "node_modules/nise": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", + "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.1", + "@sinonjs/text-encoding": "^0.7.3", + "just-extend": "^6.2.0", + "path-to-regexp": "^8.1.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -12019,6 +12098,15 @@ "node": "14 || >=16.14" } }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -13121,6 +13209,54 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, + "node_modules/sinon": { + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz", + "integrity": "sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.2", + "@sinonjs/samsam": "^8.0.1", + "diff": "^7.0.0", + "nise": "^6.1.1", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -13924,8 +14060,6 @@ "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==", - "dev": true, - "peer": true, "engines": { "node": ">=4" } diff --git a/package.json b/package.json index f525ee09..dd1f2479 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "progress": "2.0.3", "semver": "7.6.3", "shamsi-date-converter": "1.0.5", + "sinon": "^19.0.2", "tar": "7.4.3", "ua-parser-js": "1.0.38", "update-notifier": "7.1.0", @@ -51,6 +52,7 @@ "@types/node": "18.15.11", "@types/progress": "^2.0.7", "@types/semver": "^7.5.8", + "@types/sinon": "^17.0.4", "@types/ua-parser-js": "^0.7.39", "@types/update-notifier": "^6.0.8", "@types/ws": "^8.5.13", diff --git a/src/commands/login.ts b/src/commands/login.ts index 872242c4..bf52be65 100644 --- a/src/commands/login.ts +++ b/src/commands/login.ts @@ -7,6 +7,7 @@ import AccountAdd from './account/add.js'; import AccountUse from './account/use.js'; import { createDebugLogger } from '../utils/output.js'; import { GLOBAL_CONF_PATH, GLOBAL_CONF_VERSION } from '../constants.js'; +import ora from 'ora'; export default class Login extends Command { static description = 'login to your account'; @@ -26,6 +27,7 @@ export default class Login extends Command { }; async run() { + this.spinner = ora(); const { flags } = await this.parse(Login); const debug = createDebugLogger(flags.debug); @@ -75,7 +77,7 @@ export default class Login extends Command { JSON.stringify({ accounts: currentAccounts, version: GLOBAL_CONF_VERSION, - }) + }), ); this.spinner.succeed('You have logged in successfully.'); @@ -93,7 +95,7 @@ export default class Login extends Command { debug(`${error.message}\n`); this.spinner.fail( - 'Cannot open browser. Browser unavailable or lacks permissions.' + 'Cannot open browser. Browser unavailable or lacks permissions.', ); } } diff --git a/test/fixtures/accounts/fixture.ts b/test/fixtures/accounts/fixture.ts new file mode 100644 index 00000000..3d3d0c29 --- /dev/null +++ b/test/fixtures/accounts/fixture.ts @@ -0,0 +1,35 @@ +import IBrowserLogin from '../../../src/types/browser-login'; +export const accounts: IBrowserLogin[] = [ + { + fullname: 'test-name1', + region: 'iran', + avatar: '//www.gravatar.com/avatar/test1?d=mp', + email: 'test2@gmail.com', + token: 'dkmfewfp[ewkfp[kewp[fkef[pewf', + current: true, + }, + { + fullname: 'test-name2', + region: 'iran', + avatar: '//www.gravatar.com/avatar/test2?d=mp', + email: 'test1@gmail.com', + token: 'dfhefiowejhniofjiowejfijewiopf', + current: false, + }, + { + fullname: 'test-name3', + region: 'iran', + avatar: '//www.gravatar.com/avatar/test3?d=mp', + email: 'test3@gmail.com', + token: 'dasdsaddkmfewfp[ewkfp[kewp[fkef[pewf', + current: true, + }, + { + fullname: 'test-name4', + region: 'iran', + avatar: '//www.gravatar.com/avatar/test4?d=mp', + email: 'test4@gmail.com', + token: 'dfhefiowejdsdsadasdhniofjiowejfijewiopf', + current: false, + }, +]; diff --git a/test/units/app/create.unit.test.ts b/test/units/app/create.unit.test.ts index 41a5b16a..c7b657c2 100644 --- a/test/units/app/create.unit.test.ts +++ b/test/units/app/create.unit.test.ts @@ -1,18 +1,13 @@ -import { runCommand } from "@oclif/test"; -import nock from "nock"; -import { expect } from "chai"; -import { networks } from "../../fixtures/networks/fixture.ts" - -describe("app:create", function () { - const api = nock("https://api.iran.liara.ir"); - +import { runCommand } from '@oclif/test'; +import nock from 'nock'; +import { expect } from 'chai'; +import { networks } from '../../fixtures/networks/fixture.ts'; +describe.skip('app:create', function () { + const api = nock('https://api.iran.liara.ir'); beforeEach(() => { - api - .get("/v1/networks") - .query({ teamID: "" }) - .reply(200, networks); + api.get('/v1/networks').query({ teamID: '' }).reply(200, networks); }); afterEach(() => { @@ -20,145 +15,180 @@ describe("app:create", function () { nock.cleanAll(); }); - it.skip("creates an app with the specified flags", async () => { - + it.skip('creates an app with the specified flags', async () => { api - .post("/v1/projects/", { - name: "test-app", - planID: "small-g2", - platform: "laravel", - bundlePlanID: "standard", + .post('/v1/projects/', { + name: 'test-app', + planID: 'small-g2', + platform: 'laravel', + bundlePlanID: 'standard', network: networks.networks[0]._id, readOnlyRootFilesystem: true, }) - .query({ teamID: "" }) + .query({ teamID: '' }) .reply(200); const { stdout } = await runCommand([ - "app:create", - "--app", "test-app", - "--platform", "laravel", - "--plan", "small-g2", - "--feature-plan", "standard", - "--network", networks.networks[0].name, - "--read-only", "true", + 'app:create', + '--app', + 'test-app', + '--platform', + 'laravel', + '--plan', + 'small-g2', + '--feature-plan', + 'standard', + '--network', + networks.networks[0].name, + '--read-only', + 'true', ]); - + expect(stdout).to.equal(`App test-app created.\n`); }); - it.skip("throws an error if app name already exists", async () => { - + it.skip('throws an error if app name already exists', async () => { api - .post("/v1/projects/", { - name: "test-app", - planID: "small-g2", - platform: "laravel", - bundlePlanID: "standard", + .post('/v1/projects/', { + name: 'test-app', + planID: 'small-g2', + platform: 'laravel', + bundlePlanID: 'standard', network: networks.networks[0]._id, readOnlyRootFilesystem: true, }) - .query({ teamID: "" }) + .query({ teamID: '' }) .reply(409, { statusCode: 409, - error: "Conflict", - message: "Project exists.", - data: null + error: 'Conflict', + message: 'Project exists.', + data: null, }); const { error } = await runCommand([ - "app:create", - "--app", "test-app", - "--platform", "laravel", - "--plan", "small-g2", - "--feature-plan", "standard", - "--network", networks.networks[0].name, - "--read-only", "true", + 'app:create', + '--app', + 'test-app', + '--platform', + 'laravel', + '--plan', + 'small-g2', + '--feature-plan', + 'standard', + '--network', + networks.networks[0].name, + '--read-only', + 'true', ]); - expect(error?.message).to.equal("The app already exists. Please use a unique name for your app."); + expect(error?.message).to.equal( + 'The app already exists. Please use a unique name for your app.', + ); }); - it.skip("throws an error if the network is not found", async () => { - + it.skip('throws an error if the network is not found', async () => { const { error } = await runCommand([ - "app:create", - "--app", "test-app", - "--platform", "laravel", - "--plan", "small-g2", - "--feature-plan", "standard", - "--network", "not-found", - "--read-only", "true", + 'app:create', + '--app', + 'test-app', + '--platform', + 'laravel', + '--plan', + 'small-g2', + '--feature-plan', + 'standard', + '--network', + 'not-found', + '--read-only', + 'true', ]); - expect(error?.message).to.equal("Network not-found not found."); - }) - - it.skip("thorws an error if user select a feature plan that is not available for free plan", async () => { + expect(error?.message).to.equal('Network not-found not found.'); + }); + it.skip('thorws an error if user select a feature plan that is not available for free plan', async () => { const { error } = await runCommand([ - "app:create", - "--app", "test-app", - "--platform", "laravel", - "--plan", "free", - "--feature-plan", "standard", - "--network", networks.networks[0].name, - "--read-only", "true", + 'app:create', + '--app', + 'test-app', + '--platform', + 'laravel', + '--plan', + 'free', + '--feature-plan', + 'standard', + '--network', + networks.networks[0].name, + '--read-only', + 'true', ]); - expect(error?.message).to.equal(`Only "free" feature bundle plan is available for free plan.`); - } - ) - - it.skip("throws an error if the user does not have enough balance", async () => { + expect(error?.message).to.equal( + `Only "free" feature bundle plan is available for free plan.`, + ); + }); + it.skip('throws an error if the user does not have enough balance', async () => { api - .post("/v1/projects/", { - name: "test-app", - planID: "small-g2", - platform: "laravel", - bundlePlanID: "standard", + .post('/v1/projects/', { + name: 'test-app', + planID: 'small-g2', + platform: 'laravel', + bundlePlanID: 'standard', network: networks.networks[0]._id, readOnlyRootFilesystem: true, }) - .query({ teamID: "" }) + .query({ teamID: '' }) .reply(402); const { error } = await runCommand([ - "app:create", - "--app", "test-app", - "--platform", "laravel", - "--plan", "small-g2", - "--feature-plan", "standard", - "--network", networks.networks[0].name, - "--read-only", "true", + 'app:create', + '--app', + 'test-app', + '--platform', + 'laravel', + '--plan', + 'small-g2', + '--feature-plan', + 'standard', + '--network', + networks.networks[0].name, + '--read-only', + 'true', ]); - expect(error?.message).to.equal("Not enough balance. Please charge your account."); - }) - - it("throws proper error if the app creation fails with status code 500", async () => { + expect(error?.message).to.equal( + 'Not enough balance. Please charge your account.', + ); + }); + it('throws proper error if the app creation fails with status code 500', async () => { api - .post("/v1/projects/", { - name: "test-app", - planID: "small-g2", - platform: "laravel", - bundlePlanID: "standard", + .post('/v1/projects/', { + name: 'test-app', + planID: 'small-g2', + platform: 'laravel', + bundlePlanID: 'standard', network: networks.networks[0]._id, readOnlyRootFilesystem: true, }) - .query({ teamID: "" }) + .query({ teamID: '' }) .reply(500); const { error } = await runCommand([ - "app:create", - "--app", "test-app", - "--platform", "laravel", - "--plan", "small-g2", - "--feature-plan", "standard", - "--network", networks.networks[0].name, - "--read-only", "true", + 'app:create', + '--app', + 'test-app', + '--platform', + 'laravel', + '--plan', + 'small-g2', + '--feature-plan', + 'standard', + '--network', + networks.networks[0].name, + '--read-only', + 'true', ]); expect(error?.message).to.equal(`Error: Unable to Create App @@ -166,8 +196,6 @@ describe("app:create", function () { 1. Check your internet connection. 2. Ensure you have enough balance. 3. Try again later. - 4. If you still have problems, please contact support by submitting a ticket at https://console.liara.ir/tickets`) - }) - - -}); \ No newline at end of file + 4. If you still have problems, please contact support by submitting a ticket at https://console.liara.ir/tickets`); + }); +}); diff --git a/test/units/auth/login.unit.test.ts b/test/units/auth/login.unit.test.ts new file mode 100644 index 00000000..134acacb --- /dev/null +++ b/test/units/auth/login.unit.test.ts @@ -0,0 +1,143 @@ +import { runCommand } from '@oclif/test'; +import { expect } from 'chai'; +import sinon from 'sinon'; +import { accounts } from '../../fixtures/accounts/fixture.ts'; +import Login from '../../../src/commands/login.ts'; +import fs from 'fs-extra'; +import path from 'path'; +import os from 'node:os'; +import AccountUse from '../../../src/commands/account/use.ts'; +import AccountAdd from '../../../src/commands/account/add.ts'; + +describe('login', async () => { + let fsStub: sinon.SinonStub; + let browserStub: sinon.SinonStub; + let AccountUseStub: sinon.SinonStub; + let AccountAddStub: sinon.SinonStub; + + beforeEach(() => { + AccountUseStub = sinon.stub(AccountUse.prototype, 'run'); + AccountAddStub = sinon.stub(AccountAdd.prototype, 'run'); + browserStub = sinon + .stub(Login.prototype, 'browser') + .resolves(accounts.slice(0, 2)); + fsStub = sinon.stub(fs, 'writeFileSync'); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should write account infos if .liara-auth.json is empty', async () => { + sinon + .stub(Login.prototype, 'readGlobalConfig') + .resolves({ version: '1', accounts: {} }); + await runCommand(['login']); + + const expectedData = JSON.stringify({ + accounts: { + [`${accounts[0].email.split('@')[0]}_${accounts[0].region}`]: { + email: accounts[0].email, + region: accounts[0].region, + avatar: accounts[0].avatar, + api_token: accounts[0].token, + fullname: accounts[0].fullname, + current: false, + }, + [`${accounts[1].email.split('@')[0]}_${accounts[1].region}`]: { + email: accounts[1].email, + region: accounts[1].region, + avatar: accounts[1].avatar, + api_token: accounts[1].token, + fullname: accounts[1].fullname, + current: false, + }, + }, + version: '1', + }); + + expect( + fsStub.calledWithExactly( + path.join(os.homedir(), '.liara-auth.json'), + expectedData, + ), + ).to.be.true; + }); + + it('should write account infos if .liara-auth.json is not empty', async () => { + const accountObj = accounts.slice(2, 4).reduce((acc, account) => { + acc[`${account.email.split('@')[0]}_${account.region}`] = account; + return acc; + }, {}); + + sinon + .stub(Login.prototype, 'readGlobalConfig') + .resolves({ version: '1', accounts: accountObj }); + + await runCommand(['login']); + + const expectedAccounts = { + ...accountObj, + [`${accounts[0].email.split('@')[0]}_${accounts[0].region}`]: { + email: accounts[0].email, + region: accounts[0].region, + avatar: accounts[0].avatar, + api_token: accounts[0].token, + fullname: accounts[0].fullname, + current: false, + }, + [`${accounts[1].email.split('@')[0]}_${accounts[1].region}`]: { + email: accounts[1].email, + region: accounts[1].region, + avatar: accounts[1].avatar, + api_token: accounts[1].token, + fullname: accounts[1].fullname, + current: false, + }, + }; + + const expectedData = JSON.stringify({ + accounts: expectedAccounts, + version: '1', + }); + + expect( + fsStub.calledWithExactly( + path.join(os.homedir(), '.liara-auth.json'), + expectedData, + ), + ).to.be.true; + }); + it('should pass the current account to account use command', async () => { + sinon + .stub(Login.prototype, 'readGlobalConfig') + .resolves({ version: '1', accounts: {} }); + + await runCommand(['login']); + + const currentAccount = accounts.find((data) => data.current); + const currentAccountName = `${currentAccount!.email.split('@')[0]}_${currentAccount!.region}`; + + expect(AccountUseStub.calledWithExactly(currentAccountName)); + }); + it('should pass the flags to account add command if -i flag is used', async () => { + await runCommand([ + 'login', + '-i', + `--email ${accounts[0].email}`, + `--password 123456`, + ]); + + expect( + AccountAddStub.calledWithExactly([ + '--api-token', + '', + '--email', + accounts[0].email, + '--password', + '', + 123456, + ]), + ); + }); +}); From f5b220af1956edad0d02e4c24af842b6a0025421 Mon Sep 17 00:00:00 2001 From: Mortza81 Date: Sun, 9 Mar 2025 15:00:26 +0330 Subject: [PATCH 04/24] fix typo --- test/fixtures/accounts/fixture.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/fixtures/accounts/fixture.ts b/test/fixtures/accounts/fixture.ts index 3d3d0c29..20a3b72f 100644 --- a/test/fixtures/accounts/fixture.ts +++ b/test/fixtures/accounts/fixture.ts @@ -4,7 +4,7 @@ export const accounts: IBrowserLogin[] = [ fullname: 'test-name1', region: 'iran', avatar: '//www.gravatar.com/avatar/test1?d=mp', - email: 'test2@gmail.com', + email: 'test1@gmail.com', token: 'dkmfewfp[ewkfp[kewp[fkef[pewf', current: true, }, @@ -12,7 +12,7 @@ export const accounts: IBrowserLogin[] = [ fullname: 'test-name2', region: 'iran', avatar: '//www.gravatar.com/avatar/test2?d=mp', - email: 'test1@gmail.com', + email: 'test2@gmail.com', token: 'dfhefiowejhniofjiowejfijewiopf', current: false, }, From b9449deda2e01c13a5ad726b78ec624ff593e6d9 Mon Sep 17 00:00:00 2001 From: morteza Date: Mon, 10 Mar 2025 14:39:16 +0330 Subject: [PATCH 05/24] chore: account use command tests --- src/commands/account/use.ts | 7 +-- test/fixtures/accounts/fixture.ts | 20 +++++++- test/units/app/create.unit.test.ts | 12 ++--- test/units/auth/account-use.unit.test.ts | 58 ++++++++++++++++++++++++ test/units/auth/login.unit.test.ts | 18 +++----- 5 files changed, 93 insertions(+), 22 deletions(-) create mode 100644 test/units/auth/account-use.unit.test.ts diff --git a/src/commands/account/use.ts b/src/commands/account/use.ts index ddce1a88..bda840fa 100644 --- a/src/commands/account/use.ts +++ b/src/commands/account/use.ts @@ -16,13 +16,14 @@ export default class AccountUse extends Command { async run() { const { flags } = await this.parse(AccountUse); const liara_json = await this.readGlobalConfig(); + console.log(liara_json); if ( !liara_json || !liara_json.accounts || Object.keys(liara_json.accounts).length === 0 ) { this.error( - "Please add your accounts via 'liara account:add' command, first." + "Please add your accounts via 'liara account:add' command, first.", ); } @@ -30,7 +31,7 @@ export default class AccountUse extends Command { const selectedAccount = liara_json.accounts[name]; !selectedAccount && this.error( - `Could not find any account associated with this name ${name}.` + `Could not find any account associated with this name ${name}.`, ); for (const account of Object.keys(liara_json.accounts)) { @@ -45,7 +46,7 @@ export default class AccountUse extends Command { JSON.stringify({ version: GLOBAL_CONF_VERSION, accounts: liara_json.accounts, - }) + }), ); this.log(chalk.green('> Auth credentials changed.')); } diff --git a/test/fixtures/accounts/fixture.ts b/test/fixtures/accounts/fixture.ts index 20a3b72f..9c7d634a 100644 --- a/test/fixtures/accounts/fixture.ts +++ b/test/fixtures/accounts/fixture.ts @@ -6,7 +6,7 @@ export const accounts: IBrowserLogin[] = [ avatar: '//www.gravatar.com/avatar/test1?d=mp', email: 'test1@gmail.com', token: 'dkmfewfp[ewkfp[kewp[fkef[pewf', - current: true, + current: false, }, { fullname: 'test-name2', @@ -33,3 +33,21 @@ export const accounts: IBrowserLogin[] = [ current: false, }, ]; +export const currentAccounts = { + test3_iran: { + fullname: 'test-name3', + region: 'iran', + avatar: '//www.gravatar.com/avatar/test3?d=mp', + email: 'test3@gmail.com', + token: 'dasdsaddkmfewfp[ewkfp[kewp[fkef[pewf', + current: true, + }, + test4_iran: { + fullname: 'test-name4', + region: 'iran', + avatar: '//www.gravatar.com/avatar/test4?d=mp', + email: 'test4@gmail.com', + token: 'dfhefiowejdsdsadasdhniofjiowejfijewiopf', + current: false, + }, +}; diff --git a/test/units/app/create.unit.test.ts b/test/units/app/create.unit.test.ts index c7b657c2..3ffe890a 100644 --- a/test/units/app/create.unit.test.ts +++ b/test/units/app/create.unit.test.ts @@ -3,7 +3,7 @@ import nock from 'nock'; import { expect } from 'chai'; import { networks } from '../../fixtures/networks/fixture.ts'; -describe.skip('app:create', function () { +describe('app:create', function () { const api = nock('https://api.iran.liara.ir'); beforeEach(() => { @@ -15,7 +15,7 @@ describe.skip('app:create', function () { nock.cleanAll(); }); - it.skip('creates an app with the specified flags', async () => { + it('creates an app with the specified flags', async () => { api .post('/v1/projects/', { name: 'test-app', @@ -47,7 +47,7 @@ describe.skip('app:create', function () { expect(stdout).to.equal(`App test-app created.\n`); }); - it.skip('throws an error if app name already exists', async () => { + it('throws an error if app name already exists', async () => { api .post('/v1/projects/', { name: 'test-app', @@ -86,7 +86,7 @@ describe.skip('app:create', function () { ); }); - it.skip('throws an error if the network is not found', async () => { + it('throws an error if the network is not found', async () => { const { error } = await runCommand([ 'app:create', '--app', @@ -106,7 +106,7 @@ describe.skip('app:create', function () { expect(error?.message).to.equal('Network not-found not found.'); }); - it.skip('thorws an error if user select a feature plan that is not available for free plan', async () => { + it('thorws an error if user select a feature plan that is not available for free plan', async () => { const { error } = await runCommand([ 'app:create', '--app', @@ -128,7 +128,7 @@ describe.skip('app:create', function () { ); }); - it.skip('throws an error if the user does not have enough balance', async () => { + it('throws an error if the user does not have enough balance', async () => { api .post('/v1/projects/', { name: 'test-app', diff --git a/test/units/auth/account-use.unit.test.ts b/test/units/auth/account-use.unit.test.ts new file mode 100644 index 00000000..9505f979 --- /dev/null +++ b/test/units/auth/account-use.unit.test.ts @@ -0,0 +1,58 @@ +import { runCommand } from '@oclif/test'; +import { expect } from 'chai'; +import sinon from 'sinon'; +import { accounts, currentAccounts } from '../../fixtures/accounts/fixture.ts'; +import fs from 'fs-extra'; +import path from 'path'; +import os from 'node:os'; +import AccountUse from '../../../src/commands/account/use.ts'; + +describe('account:use', async () => { + let fsStub: sinon.SinonStub; + let promptAccount: sinon.SinonStub; + let liaraAuthConfigFile: sinon.SinonStub; + + beforeEach(async () => { + fsStub = sinon.stub(fs, 'writeFileSync'); + promptAccount = sinon.stub(AccountUse.prototype, 'promptName'); + liaraAuthConfigFile = sinon + .stub(AccountUse.prototype, 'readGlobalConfig') + .resolves({ version: '1', accounts: currentAccounts }); + }); + + afterEach(async () => { + sinon.restore(); + }); + it('should throw an error when the specified account does not exist', async () => { + const { error } = await runCommand([ + 'account:use', + '--account', + 'test5_iran', + ]); + + expect(error?.message).to.equal( + 'Could not find any account associated with this name test5_iran.', + ); + }); + + it('should switch the current account and persist changes to config', async () => { + const { error } = await runCommand([ + 'account:use', + '--account', + 'test4_iran', + ]); + + const expectedAccounts = { + test3_iran: { ...currentAccounts['test3_iran'], current: false }, + test4_iran: { ...currentAccounts['test4_iran'], current: true }, + }; + + expect( + fsStub.calledWithExactly( + path.join(os.homedir(), '.liara-auth.json'), + JSON.stringify({ version: '1', accounts: expectedAccounts }), + ), + ).to.be.true; + expect(error).to.be.undefined; + }); +}); diff --git a/test/units/auth/login.unit.test.ts b/test/units/auth/login.unit.test.ts index 134acacb..f73b626e 100644 --- a/test/units/auth/login.unit.test.ts +++ b/test/units/auth/login.unit.test.ts @@ -1,7 +1,7 @@ import { runCommand } from '@oclif/test'; import { expect } from 'chai'; import sinon from 'sinon'; -import { accounts } from '../../fixtures/accounts/fixture.ts'; +import { accounts, currentAccounts } from '../../fixtures/accounts/fixture.ts'; import Login from '../../../src/commands/login.ts'; import fs from 'fs-extra'; import path from 'path'; @@ -28,7 +28,7 @@ describe('login', async () => { sinon.restore(); }); - it('should write account infos if .liara-auth.json is empty', async () => { + it('should create a new config file with accounts when none exist', async () => { sinon .stub(Login.prototype, 'readGlobalConfig') .resolves({ version: '1', accounts: {} }); @@ -55,7 +55,6 @@ describe('login', async () => { }, version: '1', }); - expect( fsStub.calledWithExactly( path.join(os.homedir(), '.liara-auth.json'), @@ -64,20 +63,15 @@ describe('login', async () => { ).to.be.true; }); - it('should write account infos if .liara-auth.json is not empty', async () => { - const accountObj = accounts.slice(2, 4).reduce((acc, account) => { - acc[`${account.email.split('@')[0]}_${account.region}`] = account; - return acc; - }, {}); - + it('should merge new accounts into existing config without overwriting', async () => { sinon .stub(Login.prototype, 'readGlobalConfig') - .resolves({ version: '1', accounts: accountObj }); + .resolves({ version: '1', accounts: currentAccounts }); await runCommand(['login']); const expectedAccounts = { - ...accountObj, + ...currentAccounts, [`${accounts[0].email.split('@')[0]}_${accounts[0].region}`]: { email: accounts[0].email, region: accounts[0].region, @@ -120,7 +114,7 @@ describe('login', async () => { expect(AccountUseStub.calledWithExactly(currentAccountName)); }); - it('should pass the flags to account add command if -i flag is used', async () => { + it('should delegate credentials to account:add in interactive mode (-i)', async () => { await runCommand([ 'login', '-i', From 305707671f32c8478fcff445fcd6a92c0b5dc416 Mon Sep 17 00:00:00 2001 From: morteza Date: Mon, 10 Mar 2025 14:41:06 +0330 Subject: [PATCH 06/24] fix typo --- src/commands/account/use.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/account/use.ts b/src/commands/account/use.ts index bda840fa..823459de 100644 --- a/src/commands/account/use.ts +++ b/src/commands/account/use.ts @@ -16,7 +16,7 @@ export default class AccountUse extends Command { async run() { const { flags } = await this.parse(AccountUse); const liara_json = await this.readGlobalConfig(); - console.log(liara_json); + if ( !liara_json || !liara_json.accounts || From 75fa40a461c6d26571580ef47839465106d66dff Mon Sep 17 00:00:00 2001 From: Mortza81 Date: Tue, 11 Mar 2025 15:23:34 +0330 Subject: [PATCH 07/24] chore: complete account use command tests --- test/units/auth/account-use.unit.test.ts | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/test/units/auth/account-use.unit.test.ts b/test/units/auth/account-use.unit.test.ts index 9505f979..cc3ef5e0 100644 --- a/test/units/auth/account-use.unit.test.ts +++ b/test/units/auth/account-use.unit.test.ts @@ -15,15 +15,15 @@ describe('account:use', async () => { beforeEach(async () => { fsStub = sinon.stub(fs, 'writeFileSync'); promptAccount = sinon.stub(AccountUse.prototype, 'promptName'); - liaraAuthConfigFile = sinon - .stub(AccountUse.prototype, 'readGlobalConfig') - .resolves({ version: '1', accounts: currentAccounts }); + liaraAuthConfigFile = sinon.stub(AccountUse.prototype, 'readGlobalConfig'); }); afterEach(async () => { sinon.restore(); }); it('should throw an error when the specified account does not exist', async () => { + liaraAuthConfigFile.resolves({ version: '1', accounts: currentAccounts }); + const { error } = await runCommand([ 'account:use', '--account', @@ -36,6 +36,8 @@ describe('account:use', async () => { }); it('should switch the current account and persist changes to config', async () => { + liaraAuthConfigFile.resolves({ version: '1', accounts: currentAccounts }); + const { error } = await runCommand([ 'account:use', '--account', @@ -55,4 +57,18 @@ describe('account:use', async () => { ).to.be.true; expect(error).to.be.undefined; }); + + it('should throw an error if .liara-auth.json does not exist', async () => { + liaraAuthConfigFile.resolves(undefined); + + const { error } = await runCommand([ + 'account:use', + '--account', + 'test4_iran', + ]); + + expect(error?.message).to.equal( + "Please add your accounts via 'liara account:add' command, first.", + ); + }); }); From 376d7436dc5a738c60a2fa1ff899afe38e878d9d Mon Sep 17 00:00:00 2001 From: Mortza81 Date: Thu, 13 Mar 2025 13:22:18 +0330 Subject: [PATCH 08/24] start deploy command tests --- .../nest1/nest2/nest3/nest4/nest5/Program.cs | 0 .../nest1/nest2/nest3/nest4/nest5/app.csproj | 0 .../app2/nest1/nest2/nest3/nest4/Program.cs | 0 .../app2/nest1/nest2/nest3/nest4/app.csproj | 0 test/fixtures/dotnet-apps/app3/app.csproj | 0 .../dotnet-apps/app3/nest1/Program.cs | 0 test/fixtures/dotnet-apps/app4/Startup.cs | 0 test/fixtures/dotnet-apps/app4/boom.csproj | 0 test/fixtures/nodejs-app/.gitignore | 1 + test/fixtures/nodejs-app/README.md | 28 ++ test/fixtures/nodejs-app/index.html | 19 ++ test/fixtures/nodejs-app/package.json | 13 + .../nodejs-app/public/css/BeautifulPeople.ttf | Bin 0 -> 197192 bytes test/fixtures/nodejs-app/public/css/app.css | 28 ++ .../nodejs-app/public/images/favicon.ico | Bin 0 -> 15406 bytes test/fixtures/nodejs-app/public/js/app.js | 275 ++++++++++++++++++ test/fixtures/nodejs-app/server.js | 14 + test/units/deploy/deploy.unit.test.ts | 49 ++++ 18 files changed, 427 insertions(+) delete mode 100644 test/fixtures/dotnet-apps/app1/nest1/nest2/nest3/nest4/nest5/Program.cs delete mode 100644 test/fixtures/dotnet-apps/app1/nest1/nest2/nest3/nest4/nest5/app.csproj delete mode 100644 test/fixtures/dotnet-apps/app2/nest1/nest2/nest3/nest4/Program.cs delete mode 100644 test/fixtures/dotnet-apps/app2/nest1/nest2/nest3/nest4/app.csproj delete mode 100644 test/fixtures/dotnet-apps/app3/app.csproj delete mode 100644 test/fixtures/dotnet-apps/app3/nest1/Program.cs delete mode 100644 test/fixtures/dotnet-apps/app4/Startup.cs delete mode 100644 test/fixtures/dotnet-apps/app4/boom.csproj create mode 100644 test/fixtures/nodejs-app/.gitignore create mode 100644 test/fixtures/nodejs-app/README.md create mode 100644 test/fixtures/nodejs-app/index.html create mode 100644 test/fixtures/nodejs-app/package.json create mode 100644 test/fixtures/nodejs-app/public/css/BeautifulPeople.ttf create mode 100644 test/fixtures/nodejs-app/public/css/app.css create mode 100644 test/fixtures/nodejs-app/public/images/favicon.ico create mode 100644 test/fixtures/nodejs-app/public/js/app.js create mode 100644 test/fixtures/nodejs-app/server.js create mode 100644 test/units/deploy/deploy.unit.test.ts diff --git a/test/fixtures/dotnet-apps/app1/nest1/nest2/nest3/nest4/nest5/Program.cs b/test/fixtures/dotnet-apps/app1/nest1/nest2/nest3/nest4/nest5/Program.cs deleted file mode 100644 index e69de29b..00000000 diff --git a/test/fixtures/dotnet-apps/app1/nest1/nest2/nest3/nest4/nest5/app.csproj b/test/fixtures/dotnet-apps/app1/nest1/nest2/nest3/nest4/nest5/app.csproj deleted file mode 100644 index e69de29b..00000000 diff --git a/test/fixtures/dotnet-apps/app2/nest1/nest2/nest3/nest4/Program.cs b/test/fixtures/dotnet-apps/app2/nest1/nest2/nest3/nest4/Program.cs deleted file mode 100644 index e69de29b..00000000 diff --git a/test/fixtures/dotnet-apps/app2/nest1/nest2/nest3/nest4/app.csproj b/test/fixtures/dotnet-apps/app2/nest1/nest2/nest3/nest4/app.csproj deleted file mode 100644 index e69de29b..00000000 diff --git a/test/fixtures/dotnet-apps/app3/app.csproj b/test/fixtures/dotnet-apps/app3/app.csproj deleted file mode 100644 index e69de29b..00000000 diff --git a/test/fixtures/dotnet-apps/app3/nest1/Program.cs b/test/fixtures/dotnet-apps/app3/nest1/Program.cs deleted file mode 100644 index e69de29b..00000000 diff --git a/test/fixtures/dotnet-apps/app4/Startup.cs b/test/fixtures/dotnet-apps/app4/Startup.cs deleted file mode 100644 index e69de29b..00000000 diff --git a/test/fixtures/dotnet-apps/app4/boom.csproj b/test/fixtures/dotnet-apps/app4/boom.csproj deleted file mode 100644 index e69de29b..00000000 diff --git a/test/fixtures/nodejs-app/.gitignore b/test/fixtures/nodejs-app/.gitignore new file mode 100644 index 00000000..40b878db --- /dev/null +++ b/test/fixtures/nodejs-app/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/test/fixtures/nodejs-app/README.md b/test/fixtures/nodejs-app/README.md new file mode 100644 index 00000000..a4dc9434 --- /dev/null +++ b/test/fixtures/nodejs-app/README.md @@ -0,0 +1,28 @@ +# Node apps getting started + +Example of how deploy a simple Node project on [liara](https://liara.ir). + +## Deploying + +[Create New Node App](https://console.liara.ir/apps/create) & install the [Liara CLI](https://docs.liara.ir/cli/install) + +```bash +$ git clone https://github.com/liara-cloud/nodejs-getting-started.git # or clone your own fork + +$ cd node-getting-started + +$ liara deploy +``` + +## Availabe Branches + +1. [Adding liara.json file](https://github.com/liara-cloud/nodejs-getting-started/tree/liaraJson) +2. [Disk setup](https://github.com/liara-cloud/nodejs-getting-started/tree/diskSetup) +3. [Email Server in Liara](https://github.com/liara-cloud/nodejs-getting-started/tree/email-server) +4. [Oject Srorage in Liara](https://github.com/liara-cloud/nodejs-getting-started/tree/object-storage) +5. [Headless Chrome Puppeteer](https://github.com/liara-cloud/nodejs-getting-started/tree/headless-chrome-puppeteer) +6. [Prisma](https://github.com/liara-cloud/nodejs-getting-started/tree/prisma) + +## Documentation + +Read more on liara [Node apps documentation](https://docs.liara.ir/app-deploy/nodejs/getting-started) diff --git a/test/fixtures/nodejs-app/index.html b/test/fixtures/nodejs-app/index.html new file mode 100644 index 00000000..841956db --- /dev/null +++ b/test/fixtures/nodejs-app/index.html @@ -0,0 +1,19 @@ + + + + + + + Deployed to Liara + + + + + +

Hooray!

+ + + + + + diff --git a/test/fixtures/nodejs-app/package.json b/test/fixtures/nodejs-app/package.json new file mode 100644 index 00000000..c79771fb --- /dev/null +++ b/test/fixtures/nodejs-app/package.json @@ -0,0 +1,13 @@ +{ + "name": "node-getting-started", + "version": "1.0.0", + "description": "simple web server with expressjs", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "license": "ISC", + "dependencies": { + "express": "^4.17.1" + } +} diff --git a/test/fixtures/nodejs-app/public/css/BeautifulPeople.ttf b/test/fixtures/nodejs-app/public/css/BeautifulPeople.ttf new file mode 100644 index 0000000000000000000000000000000000000000..8746c57477af2faa247d386a37406ed46c7f1f4b GIT binary patch literal 197192 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 1 ? 1 : p; + + this.complete = this._progress === 1; + }, + get progress() { + return this._progress; + }, + draw: function () { + ctx.fillStyle = '#fff'; + ctx.beginPath(); + ctx.arc( + this.x, + this.y, + this.r, + -HALF_PI, + TWO_PI * this._progress - HALF_PI, + ); + ctx.lineTo(this.x, this.y); + ctx.closePath(); + ctx.fill(); + }, +}; + +// pun intended +Exploader = function (x, y) { + this.x = x; + this.y = y; + + this.startRadius = 24; + + this.time = 0; + this.duration = 0.4; + this.progress = 0; + + this.complete = false; +}; + +Exploader.prototype = { + reset: function () { + this.time = 0; + this.progress = 0; + this.complete = false; + }, + update: function () { + this.time = Math.min(this.duration, this.time + timeStep); + this.progress = Ease.inBack(this.time, 0, 1, this.duration); + + this.complete = this.time === this.duration; + }, + draw: function () { + ctx.fillStyle = '#fff'; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.startRadius * (1 - this.progress), 0, TWO_PI); + ctx.fill(); + }, +}; + +var particles = [], + loader, + exploader, + phase = 0; + +function initDrawingCanvas() { + drawingCanvas.width = viewWidth; + drawingCanvas.height = viewHeight; + ctx = drawingCanvas.getContext('2d'); + + createLoader(); + createExploader(); + createParticles(); +} + +function createLoader() { + loader = new Loader(viewWidth * 0.5, viewHeight * 0.5); +} + +function createExploader() { + exploader = new Exploader(viewWidth * 0.5, viewHeight * 0.5); +} + +function createParticles() { + for (var i = 0; i < 128; i++) { + var p0 = new Point(viewWidth * 0.5, viewHeight * 0.5); + var p1 = new Point(Math.random() * viewWidth, Math.random() * viewHeight); + var p2 = new Point(Math.random() * viewWidth, Math.random() * viewHeight); + var p3 = new Point(Math.random() * viewWidth, viewHeight + 64); + + particles.push(new Particle(p0, p1, p2, p3)); + } +} + +function update() { + switch (phase) { + case 0: + loader.progress += 1 / 45; + break; + case 1: + exploader.update(); + break; + case 2: + particles.forEach(function (p) { + p.update(); + }); + break; + } +} + +function draw() { + ctx.clearRect(0, 0, viewWidth, viewHeight); + + switch (phase) { + case 0: + loader.draw(); + break; + case 1: + exploader.draw(); + break; + case 2: + particles.forEach(function (p) { + p.draw(); + }); + break; + } +} + +window.onload = function () { + initDrawingCanvas(); + requestAnimationFrame(loop); +}; + +function loop() { + update(); + draw(); + + if (phase === 0 && loader.complete) { + phase = 1; + } else if (phase === 1 && exploader.complete) { + phase = 2; + } else if (phase === 2 && checkParticlesComplete()) { + // reset + phase = 2; + //loader.reset(); + exploader.reset(); + particles.length = 0; + createParticles(); + } + + requestAnimationFrame(loop); +} + +function checkParticlesComplete() { + for (var i = 0; i < particles.length; i++) { + if (particles[i].complete === false) return false; + } + return true; +} + +// math and stuff + +var Ease = { + inCubic: function (t, b, c, d) { + t /= d; + return c * t * t * t + b; + }, + outCubic: function (t, b, c, d) { + t /= d; + t--; + return c * (t * t * t + 1) + b; + }, + inOutCubic: function (t, b, c, d) { + t /= d / 2; + if (t < 1) return (c / 2) * t * t * t + b; + t -= 2; + return (c / 2) * (t * t * t + 2) + b; + }, + inBack: function (t, b, c, d, s) { + s = s || 1.70158; + return c * (t /= d) * t * ((s + 1) * t - s) + b; + }, +}; + +function cubeBezier(p0, c0, c1, p1, t) { + var p = new Point(); + var nt = 1 - t; + + p.x = + nt * nt * nt * p0.x + + 3 * nt * nt * t * c0.x + + 3 * nt * t * t * c1.x + + t * t * t * p1.x; + p.y = + nt * nt * nt * p0.y + + 3 * nt * nt * t * c0.y + + 3 * nt * t * t * c1.y + + t * t * t * p1.y; + + return p; +} diff --git a/test/fixtures/nodejs-app/server.js b/test/fixtures/nodejs-app/server.js new file mode 100644 index 00000000..416a3b42 --- /dev/null +++ b/test/fixtures/nodejs-app/server.js @@ -0,0 +1,14 @@ +const express = require('express'); +const app = express(); +const path = require('path'); +const LIARA_URL = process.env.LIARA_URL || 'localhost'; + +app.use(express.static('public')); + +app.get('/', function (req, res) { + res.sendFile(path.join(__dirname + '/index.html')); +}); + +app.listen(3005, () => + console.log(`app listening on port 3005 on ${LIARA_URL}`), +); diff --git a/test/units/deploy/deploy.unit.test.ts b/test/units/deploy/deploy.unit.test.ts new file mode 100644 index 00000000..d62d83b8 --- /dev/null +++ b/test/units/deploy/deploy.unit.test.ts @@ -0,0 +1,49 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { runCommand } from '@oclif/test'; +import deploy from '../../../src/commands/deploy.ts'; +import nock from 'nock'; + +describe('deploy', () => { + const api = nock('https://api.iran.liara.ir'); + + let getConfigs: sinon.SinonStub; + beforeEach(() => { + getConfigs = sinon.stub(deploy.prototype, 'getMergedConfig'); + }); + afterEach(() => { + sinon.restore(); + }); + it('should thorw an error when project path is empty and image flag is specified', async () => { + getConfigs.returns({ path: 'test/fixtures/empty-project' }); + + const { error } = await runCommand(['deploy']); + + expect(error?.message).to.equal('Directory is empty!'); + }); + + it('should throw an error if healthcheck in specefied but healthcheck command is not', async () => { + getConfigs.returns({ healthCheck: {}, path: 'test/fixtures/nodejs-app' }); + + const { error } = await runCommand(['deploy']); + + expect(error?.message).to.equal( + '`command` field in healthCheck is required.', + ); + }); + + it('should throw an error if cache config is not a boolean', async () => { + getConfigs.returns({ + build: { + cache: 'string', + }, + path: 'test/fixtures/nodejs-app', + }); + + const { error } = await runCommand(['deploy']); + + expect(error?.message).to.equal( + '`cache` parameter field must be a boolean.', + ); + }); +}); From c193432e86f66cd230fb522dd66ca6f21693488f Mon Sep 17 00:00:00 2001 From: morteza Date: Sun, 16 Mar 2025 01:53:01 +0330 Subject: [PATCH 09/24] chore: add test for deploy command --- src/base.ts | 14 +++--- test/fixtures/apps/fixture.ts | 35 --------------- test/fixtures/nodejs-app/package.json | 4 +- test/fixtures/projects/fixture.ts | 63 +++++++++++++++++++++++++++ test/units/deploy/deploy.unit.test.ts | 20 +++++++++ 5 files changed, 91 insertions(+), 45 deletions(-) delete mode 100644 test/fixtures/apps/fixture.ts create mode 100644 test/fixtures/projects/fixture.ts diff --git a/src/base.ts b/src/base.ts index b264c527..755b7d0c 100644 --- a/src/base.ts +++ b/src/base.ts @@ -78,9 +78,9 @@ export interface IProject { created_at: string; isDeployed: boolean; network?: { - _id: string; + _id: string; name: string; - }; + }; } export interface IGetProjectsResponse { @@ -127,14 +127,14 @@ export interface IProjectDetails { bundlePlanID: string; fixedIPStatus: string; created_at: string; - node: { - _id: string; - IP: string; - }; hourlyPrice: number; isDeployed: boolean; reservedDiskSpace: number; - network: string; + readOnlyRootFilesystem: boolean; + network: { + _id: string; + name: string; + }; } export interface IProjectDetailsResponse { diff --git a/test/fixtures/apps/fixture.ts b/test/fixtures/apps/fixture.ts deleted file mode 100644 index 74ab55ea..00000000 --- a/test/fixtures/apps/fixture.ts +++ /dev/null @@ -1,35 +0,0 @@ -import {IGetProjectsResponse} from "../../../src/base" -export const projects: IGetProjectsResponse={ - projects: [ - { - _id: "64c7f1a2b3e8c91d0e5f7b2a", - project_id: "proj12345", - type: "web-application", - status: "ACTIVE", - scale: 3, - planID: "standard-plus-g2", - bundlePlanID: "basic", - created_at: "2023-10-05T12:34:56Z", - isDeployed: true, - network: { - _id: "64c7f1a2b3e8c91d0e5f7b2b", - name: "network-abc123" - } - }, - { - _id: "64c7f1a2b3e8c91d0e5f7b2c", - project_id: "proj67890", - type: "backend-service", - status: "INACTIVE", - scale: 1, - planID: "small-g2", - bundlePlanID: "standard", - created_at: "2023-10-01T09:15:30Z", - isDeployed: false, - network: { - _id: "64c7f1a2b3e8c91d0e5f7b2d", - name: "network-xyz789" - } - } - ] -}; \ No newline at end of file diff --git a/test/fixtures/nodejs-app/package.json b/test/fixtures/nodejs-app/package.json index c79771fb..c9c4e4e1 100644 --- a/test/fixtures/nodejs-app/package.json +++ b/test/fixtures/nodejs-app/package.json @@ -3,9 +3,7 @@ "version": "1.0.0", "description": "simple web server with expressjs", "main": "server.js", - "scripts": { - "start": "node server.js" - }, + "scripts": {}, "license": "ISC", "dependencies": { "express": "^4.17.1" diff --git a/test/fixtures/projects/fixture.ts b/test/fixtures/projects/fixture.ts new file mode 100644 index 00000000..efd817a2 --- /dev/null +++ b/test/fixtures/projects/fixture.ts @@ -0,0 +1,63 @@ +import { + IGetProjectsResponse, + IProjectDetailsResponse, +} from '../../../src/base'; +export const projects: IGetProjectsResponse = { + projects: [ + { + _id: '64c7f1a2b3e8c91d0e5f7b2a', + project_id: 'proj12345', + type: 'node', + status: 'ACTIVE', + scale: 1, + planID: 'standard-plus-g2', + bundlePlanID: 'basic', + created_at: '2023-10-05T12:34:56Z', + isDeployed: true, + network: { + _id: '64c7f1a2b3e8c91d0e5f7b2b', + name: 'network-abc123', + }, + }, + { + _id: '64c7f1a2b3e8c91d0e5f7b2c', + project_id: 'proj67890', + type: 'docker', + status: 'INACTIVE', + scale: 1, + planID: 'small-g2', + bundlePlanID: 'standard', + created_at: '2023-10-01T09:15:30Z', + isDeployed: false, + network: { + _id: '64c7f1a2b3e8c91d0e5f7b2d', + name: 'network-xyz789', + }, + }, + ], +}; + +export const getNodeProject: IProjectDetailsResponse = { + project: { + _id: '64c7f1a2b3e8c91d0e5f7b2a', + project_id: 'proj12345', + type: 'node', + status: 'ACTIVE', + readOnlyRootFilesystem: true, + defaultSubdomain: true, + zeroDowntime: true, + scale: 1, + envs: [], + planID: 'standard-plus-g2', + bundlePlanID: 'basic', + network: { + _id: '64c7f1a2b3e8c91d0e5f7b2b', + name: 'network-abc123', + }, + created_at: '2023-10-05T12:34:56Z', + fixedIPStatus: 'ACTIVE', + hourlyPrice: 137.5, + isDeployed: true, + reservedDiskSpace: 0, + }, +}; diff --git a/test/units/deploy/deploy.unit.test.ts b/test/units/deploy/deploy.unit.test.ts index d62d83b8..50514dc2 100644 --- a/test/units/deploy/deploy.unit.test.ts +++ b/test/units/deploy/deploy.unit.test.ts @@ -3,6 +3,7 @@ import sinon from 'sinon'; import { runCommand } from '@oclif/test'; import deploy from '../../../src/commands/deploy.ts'; import nock from 'nock'; +import { projects, getNodeProject } from '../../fixtures/projects/fixture.ts'; describe('deploy', () => { const api = nock('https://api.iran.liara.ir'); @@ -46,4 +47,23 @@ describe('deploy', () => { '`cache` parameter field must be a boolean.', ); }); + + it('should throw an error if platform is node and start command is not provided in package.json.', async () => { + getConfigs.returns({ + platform: 'node', + path: 'test/fixtures/nodejs-app', + app: getNodeProject.project.project_id, + }); + + api + .get(`/v1/projects/${getNodeProject.project.project_id}`) + .query({ teamID: '' }) + + .reply(200, getNodeProject); + + const { stdout } = await runCommand(['deploy', '--debug']); + expect(stdout).to.contains( + `Error: A NodeJS app must be runnable with 'npm start'`, + ); + }); }); From a63bddf2945f7da48b45663c354ba57987c1c221 Mon Sep 17 00:00:00 2001 From: Mortza81 Date: Mon, 17 Mar 2025 10:14:12 +0330 Subject: [PATCH 10/24] chore: image deployment test --- test/fixtures/projects/fixture.ts | 33 +++++++++++-- test/units/deploy/deploy.unit.test.ts | 69 ++++++++++++++++++++++++++- 2 files changed, 96 insertions(+), 6 deletions(-) diff --git a/test/fixtures/projects/fixture.ts b/test/fixtures/projects/fixture.ts index efd817a2..75ea4e8b 100644 --- a/test/fixtures/projects/fixture.ts +++ b/test/fixtures/projects/fixture.ts @@ -21,13 +21,13 @@ export const projects: IGetProjectsResponse = { }, { _id: '64c7f1a2b3e8c91d0e5f7b2c', - project_id: 'proj67890', + project_id: 'testdocker', type: 'docker', - status: 'INACTIVE', + status: 'ACTIVE', scale: 1, - planID: 'small-g2', + planID: 'medium-g2', bundlePlanID: 'standard', - created_at: '2023-10-01T09:15:30Z', + created_at: '2025-03-16T10:00:19.277Z', isDeployed: false, network: { _id: '64c7f1a2b3e8c91d0e5f7b2d', @@ -61,3 +61,28 @@ export const getNodeProject: IProjectDetailsResponse = { reservedDiskSpace: 0, }, }; + +export const getDockerProject: IProjectDetailsResponse = { + project: { + _id: '65435435432565kihvudoifjoip', + project_id: 'testdocker', + type: 'docker', + status: 'ACTIVE', + readOnlyRootFilesystem: false, + defaultSubdomain: true, + zeroDowntime: true, + scale: 1, + envs: [], + planID: 'medium-g2', + bundlePlanID: 'standard', + network: { + _id: '64c7f1a2b3e8c91d0e5f7b2d', + name: 'network-xyz789', + }, + created_at: '2025-03-16T10:00:19.277Z', + hourlyPrice: 206.9, + isDeployed: false, + fixedIPStatus: 'ACTIVE', + reservedDiskSpace: 2, + }, +}; diff --git a/test/units/deploy/deploy.unit.test.ts b/test/units/deploy/deploy.unit.test.ts index 50514dc2..c4b1af6b 100644 --- a/test/units/deploy/deploy.unit.test.ts +++ b/test/units/deploy/deploy.unit.test.ts @@ -3,7 +3,11 @@ import sinon from 'sinon'; import { runCommand } from '@oclif/test'; import deploy from '../../../src/commands/deploy.ts'; import nock from 'nock'; -import { projects, getNodeProject } from '../../fixtures/projects/fixture.ts'; +import { + projects, + getNodeProject, + getDockerProject, +} from '../../fixtures/projects/fixture.ts'; describe('deploy', () => { const api = nock('https://api.iran.liara.ir'); @@ -58,7 +62,6 @@ describe('deploy', () => { api .get(`/v1/projects/${getNodeProject.project.project_id}`) .query({ teamID: '' }) - .reply(200, getNodeProject); const { stdout } = await runCommand(['deploy', '--debug']); @@ -66,4 +69,66 @@ describe('deploy', () => { `Error: A NodeJS app must be runnable with 'npm start'`, ); }); + + it('should create a release if platform is docker.', async () => { + getConfigs.returns({ + path: 'test/fixtures/docker-platform/', + app: 'testdocker', + image: 'getmeili/meilisearch:v0.28', + port: 7700, + disks: [{ name: 'data', mountTo: '/meili_data' }], + platform: 'docker', + detach: true, + 'no-app-logs': false, + args: undefined, + }); + api + .get('/v1/projects/testdocker') + .query({ teamID: '' }) + .reply(200, getDockerProject); + + api + .post('/v2/projects/testdocker/releases', { + build: { + cache: true, + args: undefined, + dockerfile: undefined, + location: undefined, + }, + cron: undefined, + args: undefined, + port: 7700, + type: 'docker', + message: undefined, + disks: [ + { + name: 'data', + mountTo: '/meili_data', + }, + ], + image: 'getmeili/meilisearch:v0.28', + }) + .query({ teamID: '' }) + .reply(200, { releaseID: '6rqwrqwrqweBwd34124314' }); + + const { stdout } = await runCommand([ + 'deploy', + '--debug', + '--detach', + '--platform', + 'docker', + '--path', + 'test/fixtures/docker-platform', + ]); + + expect(stdout).to.match(/Disks:\s+data -> \/meili_data/); + expect(stdout).to.match( + /\[debug\] \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\] Using Build Cache: Enabled/, + ); + expect(stdout).to.contain('App: testdocker'); + expect(stdout).to.contain('Path: test/fixtures/docker-platform/'); + expect(stdout).to.contain('Platform: docker'); + expect(stdout).to.contain('Port: 7700'); + expect(stdout).to.contain('Deployment created successfully.'); + }); }); From 3b3f17ca50963756940076fa1a976ec02393d32f Mon Sep 17 00:00:00 2001 From: Mortza81 Date: Tue, 18 Mar 2025 16:36:15 +0330 Subject: [PATCH 11/24] WIP: create-archive tests --- test/{units => }/app/create.unit.test.ts | 2 +- .../{units => }/auth/account-use.unit.test.ts | 4 +-- test/{units => }/auth/login.unit.test.ts | 8 +++--- test/deploy/create-archive.test.ts | 25 +++++++++++++++++++ test/{units => }/deploy/deploy.unit.test.ts | 4 +-- 5 files changed, 34 insertions(+), 9 deletions(-) rename test/{units => }/app/create.unit.test.ts (98%) rename test/{units => }/auth/account-use.unit.test.ts (93%) rename test/{units => }/auth/login.unit.test.ts (93%) create mode 100644 test/deploy/create-archive.test.ts rename test/{units => }/deploy/deploy.unit.test.ts (97%) diff --git a/test/units/app/create.unit.test.ts b/test/app/create.unit.test.ts similarity index 98% rename from test/units/app/create.unit.test.ts rename to test/app/create.unit.test.ts index 3ffe890a..75919ade 100644 --- a/test/units/app/create.unit.test.ts +++ b/test/app/create.unit.test.ts @@ -1,7 +1,7 @@ import { runCommand } from '@oclif/test'; import nock from 'nock'; import { expect } from 'chai'; -import { networks } from '../../fixtures/networks/fixture.ts'; +import { networks } from '../fixtures/networks/fixture.ts'; describe('app:create', function () { const api = nock('https://api.iran.liara.ir'); diff --git a/test/units/auth/account-use.unit.test.ts b/test/auth/account-use.unit.test.ts similarity index 93% rename from test/units/auth/account-use.unit.test.ts rename to test/auth/account-use.unit.test.ts index cc3ef5e0..a3e15d9b 100644 --- a/test/units/auth/account-use.unit.test.ts +++ b/test/auth/account-use.unit.test.ts @@ -1,11 +1,11 @@ import { runCommand } from '@oclif/test'; import { expect } from 'chai'; import sinon from 'sinon'; -import { accounts, currentAccounts } from '../../fixtures/accounts/fixture.ts'; +import { accounts, currentAccounts } from '../fixtures/accounts/fixture.ts'; import fs from 'fs-extra'; import path from 'path'; import os from 'node:os'; -import AccountUse from '../../../src/commands/account/use.ts'; +import AccountUse from '../../src/commands/account/use.ts'; describe('account:use', async () => { let fsStub: sinon.SinonStub; diff --git a/test/units/auth/login.unit.test.ts b/test/auth/login.unit.test.ts similarity index 93% rename from test/units/auth/login.unit.test.ts rename to test/auth/login.unit.test.ts index f73b626e..9189e03c 100644 --- a/test/units/auth/login.unit.test.ts +++ b/test/auth/login.unit.test.ts @@ -1,13 +1,13 @@ import { runCommand } from '@oclif/test'; import { expect } from 'chai'; import sinon from 'sinon'; -import { accounts, currentAccounts } from '../../fixtures/accounts/fixture.ts'; -import Login from '../../../src/commands/login.ts'; +import { accounts, currentAccounts } from '../fixtures/accounts/fixture.ts'; +import Login from '../../src/commands/login.ts'; import fs from 'fs-extra'; import path from 'path'; import os from 'node:os'; -import AccountUse from '../../../src/commands/account/use.ts'; -import AccountAdd from '../../../src/commands/account/add.ts'; +import AccountUse from '../../src/commands/account/use.ts'; +import AccountAdd from '../../src/commands/account/add.ts'; describe('login', async () => { let fsStub: sinon.SinonStub; diff --git a/test/deploy/create-archive.test.ts b/test/deploy/create-archive.test.ts new file mode 100644 index 00000000..9c6d8643 --- /dev/null +++ b/test/deploy/create-archive.test.ts @@ -0,0 +1,25 @@ +import { expect } from 'chai'; +import createArchive from '../../src/utils/create-archive.js'; +import prepareTmpDirectory from '../../src/services/tmp-dir.js'; + +describe('create-archive', async () => { + const sourcePath = prepareTmpDirectory(); + + beforeAll(async () => {}); + it('should throw an error if all files are ignored', async () => { + try { + await createArchive( + sourcePath, + 'test/fixtures/archive/ignore-all', + 'node', + ); + } catch (error) { + expect(error.message).to + .equal(`Seems like you have ignored everything so we can't upload any of your files. Please double-check the content of your .gitignore, .dockerignore and .liaraignore files. + +> Read more: https://docs.liara.ir/app-features/ignore`); + } + }); + + it('should create an archive with the correct files', async () => {}); +}); diff --git a/test/units/deploy/deploy.unit.test.ts b/test/deploy/deploy.unit.test.ts similarity index 97% rename from test/units/deploy/deploy.unit.test.ts rename to test/deploy/deploy.unit.test.ts index c4b1af6b..b2b15722 100644 --- a/test/units/deploy/deploy.unit.test.ts +++ b/test/deploy/deploy.unit.test.ts @@ -1,13 +1,13 @@ import { expect } from 'chai'; import sinon from 'sinon'; import { runCommand } from '@oclif/test'; -import deploy from '../../../src/commands/deploy.ts'; +import deploy from '../../src/commands/deploy.ts'; import nock from 'nock'; import { projects, getNodeProject, getDockerProject, -} from '../../fixtures/projects/fixture.ts'; +} from '../fixtures/projects/fixture.ts'; describe('deploy', () => { const api = nock('https://api.iran.liara.ir'); From 15257c7a5dd579b160812773df969f66925f8f5d Mon Sep 17 00:00:00 2001 From: morteza Date: Tue, 25 Mar 2025 02:46:13 +0330 Subject: [PATCH 12/24] chore: add some tests for create archive function --- test/deploy/create-archive.test.ts | 67 ++++++++++++++++--- test/fixtures/all-ignored/.gitignore | 1 + .../ignore.txt => all-ignored/app.js} | 0 .../index.js => all-ignored/server.js} | 0 test/fixtures/default-ignores/.DS_Store | 0 test/fixtures/default-ignores/.dockerignore | 1 + test/fixtures/default-ignores/.idea/.gitkeep | 0 test/fixtures/default-ignores/.next/.gitkeep | 0 .../fixtures/default-ignores/.vscode/.gitkeep | 0 .../default-ignores/bower_components/.gitkeep | 0 test/fixtures/default-ignores/example.txt~ | 0 test/fixtures/default-ignores/file.txt | 1 + test/fixtures/default-ignores/liara.json | 1 + test/fixtures/simple-gitignore/.gitignore | 3 +- test/fixtures/simple-gitignore/index.html | 1 - test/helpers/getAllFiles.ts | 20 ++++++ 16 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 test/fixtures/all-ignored/.gitignore rename test/fixtures/{default-ignores/bower_components/ignore.txt => all-ignored/app.js} (100%) rename test/fixtures/{default-ignores/index.js => all-ignored/server.js} (100%) create mode 100644 test/fixtures/default-ignores/.DS_Store create mode 100644 test/fixtures/default-ignores/.dockerignore create mode 100644 test/fixtures/default-ignores/.idea/.gitkeep create mode 100644 test/fixtures/default-ignores/.next/.gitkeep create mode 100644 test/fixtures/default-ignores/.vscode/.gitkeep create mode 100644 test/fixtures/default-ignores/bower_components/.gitkeep create mode 100644 test/fixtures/default-ignores/example.txt~ create mode 100644 test/fixtures/default-ignores/file.txt create mode 100644 test/fixtures/default-ignores/liara.json delete mode 100644 test/fixtures/simple-gitignore/index.html create mode 100644 test/helpers/getAllFiles.ts diff --git a/test/deploy/create-archive.test.ts b/test/deploy/create-archive.test.ts index 9c6d8643..5ba33947 100644 --- a/test/deploy/create-archive.test.ts +++ b/test/deploy/create-archive.test.ts @@ -1,18 +1,15 @@ import { expect } from 'chai'; import createArchive from '../../src/utils/create-archive.js'; import prepareTmpDirectory from '../../src/services/tmp-dir.js'; +import getAllFiles from '../helpers/getAllFiles.js'; +import { extract } from 'tar'; +import fs from 'fs-extra'; describe('create-archive', async () => { - const sourcePath = prepareTmpDirectory(); - - beforeAll(async () => {}); it('should throw an error if all files are ignored', async () => { + const sourcePath = prepareTmpDirectory(); try { - await createArchive( - sourcePath, - 'test/fixtures/archive/ignore-all', - 'node', - ); + await createArchive(sourcePath, 'test/fixtures/all-ignored', 'node'); } catch (error) { expect(error.message).to .equal(`Seems like you have ignored everything so we can't upload any of your files. Please double-check the content of your .gitignore, .dockerignore and .liaraignore files. @@ -21,5 +18,57 @@ describe('create-archive', async () => { } }); - it('should create an archive with the correct files', async () => {}); + it('should create an archive and ignore files and directories listed in .gitignore', async () => { + const sourcePath = prepareTmpDirectory(); + + const originFiles = getAllFiles('test/fixtures/simple-gitignore'); + + await createArchive(sourcePath, 'test/fixtures/simple-gitignore', 'node'); + + fs.mkdirSync(`${sourcePath}-dir`); + + await extract({ + file: sourcePath, + cwd: `${sourcePath}-dir`, + }); + + expect(originFiles).to.deep.equal( + originFiles.filter((val) => val != 'node_modules'), + ); + }); + + it('should create an archive and ignore files and directories listed in .liaraignore', async () => { + const sourcePath = prepareTmpDirectory(); + + const originFiles = getAllFiles('test/fixtures/simple-gitignore'); + + await createArchive(sourcePath, 'test/fixtures/simple-gitignore', 'node'); + + fs.mkdirSync(`${sourcePath}-dir`); + + await extract({ + file: sourcePath, + cwd: `${sourcePath}-dir`, + }); + + expect(originFiles).to.deep.equal( + originFiles.filter((val) => val != 'node_modules'), + ); + }); + + it('should ignore some files and directories by default', async () => { + const sourcePath = prepareTmpDirectory(); + + await createArchive(sourcePath, 'test/fixtures/default-ignores', 'node'); + + fs.mkdirSync(`${sourcePath}-dir`); + + await extract({ + file: sourcePath, + cwd: `${sourcePath}-dir`, + }); + + const extractedFiles = getAllFiles(`${sourcePath}-dir`); + expect(extractedFiles).to.deep.equal(['file.txt']); + }); }); diff --git a/test/fixtures/all-ignored/.gitignore b/test/fixtures/all-ignored/.gitignore new file mode 100644 index 00000000..5fb03d00 --- /dev/null +++ b/test/fixtures/all-ignored/.gitignore @@ -0,0 +1 @@ +./* \ No newline at end of file diff --git a/test/fixtures/default-ignores/bower_components/ignore.txt b/test/fixtures/all-ignored/app.js similarity index 100% rename from test/fixtures/default-ignores/bower_components/ignore.txt rename to test/fixtures/all-ignored/app.js diff --git a/test/fixtures/default-ignores/index.js b/test/fixtures/all-ignored/server.js similarity index 100% rename from test/fixtures/default-ignores/index.js rename to test/fixtures/all-ignored/server.js diff --git a/test/fixtures/default-ignores/.DS_Store b/test/fixtures/default-ignores/.DS_Store new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/default-ignores/.dockerignore b/test/fixtures/default-ignores/.dockerignore new file mode 100644 index 00000000..51217588 --- /dev/null +++ b/test/fixtures/default-ignores/.dockerignore @@ -0,0 +1 @@ +# Ignored files and directories diff --git a/test/fixtures/default-ignores/.idea/.gitkeep b/test/fixtures/default-ignores/.idea/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/default-ignores/.next/.gitkeep b/test/fixtures/default-ignores/.next/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/default-ignores/.vscode/.gitkeep b/test/fixtures/default-ignores/.vscode/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/default-ignores/bower_components/.gitkeep b/test/fixtures/default-ignores/bower_components/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/default-ignores/example.txt~ b/test/fixtures/default-ignores/example.txt~ new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/default-ignores/file.txt b/test/fixtures/default-ignores/file.txt new file mode 100644 index 00000000..7f6809e0 --- /dev/null +++ b/test/fixtures/default-ignores/file.txt @@ -0,0 +1 @@ +This should be kept \ No newline at end of file diff --git a/test/fixtures/default-ignores/liara.json b/test/fixtures/default-ignores/liara.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/test/fixtures/default-ignores/liara.json @@ -0,0 +1 @@ +{} diff --git a/test/fixtures/simple-gitignore/.gitignore b/test/fixtures/simple-gitignore/.gitignore index dcaf7169..f91a63f2 100644 --- a/test/fixtures/simple-gitignore/.gitignore +++ b/test/fixtures/simple-gitignore/.gitignore @@ -1 +1,2 @@ -index.html +ignore_1.html +ignore_2.html \ No newline at end of file diff --git a/test/fixtures/simple-gitignore/index.html b/test/fixtures/simple-gitignore/index.html deleted file mode 100644 index ce013625..00000000 --- a/test/fixtures/simple-gitignore/index.html +++ /dev/null @@ -1 +0,0 @@ -hello diff --git a/test/helpers/getAllFiles.ts b/test/helpers/getAllFiles.ts new file mode 100644 index 00000000..82b1b6b6 --- /dev/null +++ b/test/helpers/getAllFiles.ts @@ -0,0 +1,20 @@ +import path from 'path'; +import fs from 'fs-extra'; + +export default function getAllFiles( + dir: string, + baseDir: string = dir, + filePaths: string[] = [], +): string[] { + const items = fs.readdirSync(dir); + items.forEach((item) => { + const itemPath = path.join(dir, item); + if (fs.statSync(itemPath).isDirectory()) { + getAllFiles(itemPath, baseDir, filePaths); + } else { + const relativePath = path.relative(baseDir, itemPath); + filePaths.push(relativePath); + } + }); + return filePaths; +} From 9bdf6fec78cdcd9be590f864b7a347e155c2a050 Mon Sep 17 00:00:00 2001 From: Mortza81 Date: Tue, 25 Mar 2025 14:45:39 +0330 Subject: [PATCH 13/24] chore: complete createArchive tests --- test/fixtures/dotnet-ignores/test.txt | 0 test/fixtures/dotnet-ignores/x64 | 0 test/fixtures/dotnet-ignores/x86 | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/fixtures/dotnet-ignores/test.txt create mode 100644 test/fixtures/dotnet-ignores/x64 create mode 100644 test/fixtures/dotnet-ignores/x86 diff --git a/test/fixtures/dotnet-ignores/test.txt b/test/fixtures/dotnet-ignores/test.txt new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/dotnet-ignores/x64 b/test/fixtures/dotnet-ignores/x64 new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/dotnet-ignores/x86 b/test/fixtures/dotnet-ignores/x86 new file mode 100644 index 00000000..e69de29b From 5008e881066dee157f25d6a91e2f8418c7532772 Mon Sep 17 00:00:00 2001 From: Mortza81 Date: Tue, 25 Mar 2025 14:47:18 +0330 Subject: [PATCH 14/24] chore: complete createArchive tests --- test/deploy/create-archive.test.ts | 188 +++++++++++++++++- test/fixtures/default-ignores/.dockerignore | 1 - .../.gitignore => dockerignore/.dockerignore} | 0 test/fixtures/dockerignore/ignore_1.html | 1 + test/fixtures/dockerignore/ignore_2.html | 1 + test/fixtures/dockerignore/index.php | 0 test/fixtures/liaraignore/.liaraignore | 2 + test/fixtures/liaraignore/ignore_1.html | 1 + test/fixtures/liaraignore/ignore_2.html | 1 + test/fixtures/liaraignore/index.php | 0 test/fixtures/multiple-ignores/.dockerignore | 1 + test/fixtures/multiple-ignores/.gitignore | 1 + test/fixtures/multiple-ignores/.liaraignore | 1 + test/fixtures/multiple-ignores/ignore_1.html | 1 + test/fixtures/multiple-ignores/ignore_3.html | 0 test/fixtures/multiple-ignores/index.php | 0 test/fixtures/php-ignores/index.php | 0 test/fixtures/php-ignores/vendor/.gitkeep | 0 test/fixtures/python-ignores/.cache | 0 test/fixtures/python-ignores/.env | 0 test/fixtures/python-ignores/.python-version | 0 test/fixtures/python-ignores/.venv | 0 test/fixtures/python-ignores/.webassets-cache | 0 test/fixtures/python-ignores/ENV | 0 .../python-ignores/celerybeat-schedule | 0 test/fixtures/python-ignores/example$py.class | 0 test/fixtures/python-ignores/example.pyd | 0 .../fixtures/python-ignores/instance/test.txt | 0 test/fixtures/python-ignores/lib/.gitkeep | 0 test/fixtures/python-ignores/lib64/.gitkeep | 0 .../fixtures/python-ignores/local_settings.py | 0 .../pip-delete-this-directory.txt | 0 test/fixtures/python-ignores/pip-log.txt | 0 .../python-ignores/staticfiles/test.txt | 0 test/fixtures/python-ignores/test.log | 0 test/fixtures/python-ignores/test.txt | 1 + test/fixtures/python-ignores/venv | 0 test/fixtures/simple-gitignore/ignore_1.html | 1 + test/fixtures/simple-gitignore/ignore_2.html | 1 + 39 files changed, 194 insertions(+), 8 deletions(-) rename test/fixtures/{simple-gitignore/.gitignore => dockerignore/.dockerignore} (100%) create mode 100644 test/fixtures/dockerignore/ignore_1.html create mode 100644 test/fixtures/dockerignore/ignore_2.html create mode 100644 test/fixtures/dockerignore/index.php create mode 100644 test/fixtures/liaraignore/.liaraignore create mode 100644 test/fixtures/liaraignore/ignore_1.html create mode 100644 test/fixtures/liaraignore/ignore_2.html create mode 100644 test/fixtures/liaraignore/index.php create mode 100644 test/fixtures/multiple-ignores/.dockerignore create mode 100644 test/fixtures/multiple-ignores/.gitignore create mode 100644 test/fixtures/multiple-ignores/.liaraignore create mode 100644 test/fixtures/multiple-ignores/ignore_1.html create mode 100644 test/fixtures/multiple-ignores/ignore_3.html create mode 100644 test/fixtures/multiple-ignores/index.php create mode 100644 test/fixtures/php-ignores/index.php create mode 100644 test/fixtures/php-ignores/vendor/.gitkeep create mode 100644 test/fixtures/python-ignores/.cache create mode 100644 test/fixtures/python-ignores/.env create mode 100644 test/fixtures/python-ignores/.python-version create mode 100644 test/fixtures/python-ignores/.venv create mode 100644 test/fixtures/python-ignores/.webassets-cache create mode 100644 test/fixtures/python-ignores/ENV create mode 100644 test/fixtures/python-ignores/celerybeat-schedule create mode 100644 test/fixtures/python-ignores/example$py.class create mode 100644 test/fixtures/python-ignores/example.pyd create mode 100644 test/fixtures/python-ignores/instance/test.txt create mode 100644 test/fixtures/python-ignores/lib/.gitkeep create mode 100644 test/fixtures/python-ignores/lib64/.gitkeep create mode 100644 test/fixtures/python-ignores/local_settings.py create mode 100644 test/fixtures/python-ignores/pip-delete-this-directory.txt create mode 100644 test/fixtures/python-ignores/pip-log.txt create mode 100644 test/fixtures/python-ignores/staticfiles/test.txt create mode 100644 test/fixtures/python-ignores/test.log create mode 100644 test/fixtures/python-ignores/test.txt create mode 100644 test/fixtures/python-ignores/venv create mode 100644 test/fixtures/simple-gitignore/ignore_1.html create mode 100644 test/fixtures/simple-gitignore/ignore_2.html diff --git a/test/deploy/create-archive.test.ts b/test/deploy/create-archive.test.ts index 5ba33947..1e146341 100644 --- a/test/deploy/create-archive.test.ts +++ b/test/deploy/create-archive.test.ts @@ -21,9 +21,12 @@ describe('create-archive', async () => { it('should create an archive and ignore files and directories listed in .gitignore', async () => { const sourcePath = prepareTmpDirectory(); + fs.writeFileSync('test/fixtures/simple-gitignore/ignore_1.html', 'test'); + fs.writeFileSync('test/fixtures/simple-gitignore/ignore_2.html', 'test'); + const originFiles = getAllFiles('test/fixtures/simple-gitignore'); - await createArchive(sourcePath, 'test/fixtures/simple-gitignore', 'node'); + await createArchive(sourcePath, 'test/fixtures/simple-gitignore', 'php'); fs.mkdirSync(`${sourcePath}-dir`); @@ -32,17 +35,21 @@ describe('create-archive', async () => { cwd: `${sourcePath}-dir`, }); - expect(originFiles).to.deep.equal( - originFiles.filter((val) => val != 'node_modules'), + const extractedFiles = getAllFiles(`${sourcePath}-dir`); + + expect(extractedFiles).to.deep.equal( + originFiles.filter( + (val) => val != 'ignore_1.html' && val != 'ignore_2.html', + ), ); }); it('should create an archive and ignore files and directories listed in .liaraignore', async () => { const sourcePath = prepareTmpDirectory(); - const originFiles = getAllFiles('test/fixtures/simple-gitignore'); + const originFiles = getAllFiles('test/fixtures/liaraignore'); - await createArchive(sourcePath, 'test/fixtures/simple-gitignore', 'node'); + await createArchive(sourcePath, 'test/fixtures/liaraignore', 'php'); fs.mkdirSync(`${sourcePath}-dir`); @@ -51,8 +58,38 @@ describe('create-archive', async () => { cwd: `${sourcePath}-dir`, }); - expect(originFiles).to.deep.equal( - originFiles.filter((val) => val != 'node_modules'), + const extractedFiles = getAllFiles(`${sourcePath}-dir`); + + expect(extractedFiles).to.deep.equal( + originFiles.filter( + (val) => val != 'ignore_1.html' && val != 'ignore_2.html', + ), + ); + }); + + it('should create an archive and ignore files and directories listed in .dockerignore', async () => { + const sourcePath = prepareTmpDirectory(); + + const originFiles = getAllFiles('test/fixtures/dockerignore'); + + await createArchive(sourcePath, 'test/fixtures/dockerignore', 'php'); + + fs.mkdirSync(`${sourcePath}-dir`); + + await extract({ + file: sourcePath, + cwd: `${sourcePath}-dir`, + }); + + const extractedFiles = getAllFiles(`${sourcePath}-dir`); + + expect(extractedFiles).to.deep.equal( + originFiles.filter( + (val) => + val != 'ignore_1.html' && + val != 'ignore_2.html' && + val != '.dockerignore', + ), ); }); @@ -71,4 +108,141 @@ describe('create-archive', async () => { const extractedFiles = getAllFiles(`${sourcePath}-dir`); expect(extractedFiles).to.deep.equal(['file.txt']); }); + + it('should ignore files listed in .liaraignore and disregard .gitignore and .dockerignore if present', async () => { + const sourcePath = prepareTmpDirectory(); + + fs.writeFileSync('test/fixtures/simple-gitignore/ignore_2.html', 'test'); + + const originFiles = getAllFiles('test/fixtures/multiple-ignores'); + + await createArchive(sourcePath, 'test/fixtures/multiple-ignores', 'php'); + + fs.mkdirSync(`${sourcePath}-dir`); + + await extract({ + file: sourcePath, + cwd: `${sourcePath}-dir`, + }); + + const extractedFiles = getAllFiles(`${sourcePath}-dir`); + + expect(extractedFiles).to.deep.equal( + originFiles.filter( + (val) => val != 'ignore_1.html' && val != '.dockerignore', + ), + ); + }); + + it('should respect a nested .gitignore file and apply its rules only to its directory and subdirectories', async () => { + const sourcePath = prepareTmpDirectory(); + + const originFiles = getAllFiles('test/fixtures/nested-ignore-files'); + + await createArchive(sourcePath, 'test/fixtures/nested-ignore-files', 'php'); + + fs.mkdirSync(`${sourcePath}-dir`); + + await extract({ + file: sourcePath, + cwd: `${sourcePath}-dir`, + }); + + const extractedFiles = getAllFiles(`${sourcePath}-dir`); + + expect(extractedFiles).to.deep.equal( + originFiles.filter( + (val) => val != 'ignore.me' && val != 'sub1/hello.html', + ), + ); + }); + + it('should ignore some files and directories in django platform', async () => { + const sourcePath = prepareTmpDirectory(); + + await createArchive(sourcePath, 'test/fixtures/python-ignores', 'django'); + + fs.mkdirSync(`${sourcePath}-dir`); + + await extract({ + file: sourcePath, + cwd: `${sourcePath}-dir`, + }); + const extractedFiles = getAllFiles(`${sourcePath}-dir`); + + expect(extractedFiles.sort()).to.deep.equal( + ['test.txt', '.webassets-cache', 'instance/test.txt'].sort(), + ); + }); + + it('should ignore some files and directories in dotnet platform', async () => { + const sourcePath = prepareTmpDirectory(); + + await createArchive(sourcePath, 'test/fixtures/dotnet-ignores', 'dotnet'); + + fs.mkdirSync(`${sourcePath}-dir`); + + await extract({ + file: sourcePath, + cwd: `${sourcePath}-dir`, + }); + + const extractedFiles = getAllFiles(`${sourcePath}-dir`); + expect(extractedFiles).to.deep.equal(['test.txt']); + }); + + it('should ignore /vendor directory in php platform', async () => { + const sourcePath = prepareTmpDirectory(); + + await createArchive(sourcePath, 'test/fixtures/php-ignores', 'php'); + + fs.mkdirSync(`${sourcePath}-dir`); + + await extract({ + file: sourcePath, + cwd: `${sourcePath}-dir`, + }); + + const extractedFiles = getAllFiles(`${sourcePath}-dir`); + expect(extractedFiles).to.deep.equal(['index.php']); + }); + + it('should ignore /vendor directory in laravel platform', async () => { + const sourcePath = prepareTmpDirectory(); + + await createArchive(sourcePath, 'test/fixtures/php-ignores', 'laravel'); + + fs.mkdirSync(`${sourcePath}-dir`); + + await extract({ + file: sourcePath, + cwd: `${sourcePath}-dir`, + }); + + const extractedFiles = getAllFiles(`${sourcePath}-dir`); + expect(extractedFiles).to.deep.equal(['index.php']); + }); + + it('should ignore some files and directories in flask platform', async () => { + const sourcePath = prepareTmpDirectory(); + + await createArchive(sourcePath, 'test/fixtures/python-ignores', 'flask'); + + fs.mkdirSync(`${sourcePath}-dir`); + + await extract({ + file: sourcePath, + cwd: `${sourcePath}-dir`, + }); + const extractedFiles = getAllFiles(`${sourcePath}-dir`); + + expect(extractedFiles.sort()).to.deep.equal( + [ + 'test.txt', + 'test.log', + 'local_settings.py', + 'staticfiles/test.txt', + ].sort(), + ); + }); }); diff --git a/test/fixtures/default-ignores/.dockerignore b/test/fixtures/default-ignores/.dockerignore index 51217588..e69de29b 100644 --- a/test/fixtures/default-ignores/.dockerignore +++ b/test/fixtures/default-ignores/.dockerignore @@ -1 +0,0 @@ -# Ignored files and directories diff --git a/test/fixtures/simple-gitignore/.gitignore b/test/fixtures/dockerignore/.dockerignore similarity index 100% rename from test/fixtures/simple-gitignore/.gitignore rename to test/fixtures/dockerignore/.dockerignore diff --git a/test/fixtures/dockerignore/ignore_1.html b/test/fixtures/dockerignore/ignore_1.html new file mode 100644 index 00000000..9daeafb9 --- /dev/null +++ b/test/fixtures/dockerignore/ignore_1.html @@ -0,0 +1 @@ +test diff --git a/test/fixtures/dockerignore/ignore_2.html b/test/fixtures/dockerignore/ignore_2.html new file mode 100644 index 00000000..9daeafb9 --- /dev/null +++ b/test/fixtures/dockerignore/ignore_2.html @@ -0,0 +1 @@ +test diff --git a/test/fixtures/dockerignore/index.php b/test/fixtures/dockerignore/index.php new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/liaraignore/.liaraignore b/test/fixtures/liaraignore/.liaraignore new file mode 100644 index 00000000..f91a63f2 --- /dev/null +++ b/test/fixtures/liaraignore/.liaraignore @@ -0,0 +1,2 @@ +ignore_1.html +ignore_2.html \ No newline at end of file diff --git a/test/fixtures/liaraignore/ignore_1.html b/test/fixtures/liaraignore/ignore_1.html new file mode 100644 index 00000000..9daeafb9 --- /dev/null +++ b/test/fixtures/liaraignore/ignore_1.html @@ -0,0 +1 @@ +test diff --git a/test/fixtures/liaraignore/ignore_2.html b/test/fixtures/liaraignore/ignore_2.html new file mode 100644 index 00000000..9daeafb9 --- /dev/null +++ b/test/fixtures/liaraignore/ignore_2.html @@ -0,0 +1 @@ +test diff --git a/test/fixtures/liaraignore/index.php b/test/fixtures/liaraignore/index.php new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/multiple-ignores/.dockerignore b/test/fixtures/multiple-ignores/.dockerignore new file mode 100644 index 00000000..603c9aa1 --- /dev/null +++ b/test/fixtures/multiple-ignores/.dockerignore @@ -0,0 +1 @@ +ignore_3.html diff --git a/test/fixtures/multiple-ignores/.gitignore b/test/fixtures/multiple-ignores/.gitignore new file mode 100644 index 00000000..29a83eae --- /dev/null +++ b/test/fixtures/multiple-ignores/.gitignore @@ -0,0 +1 @@ +ignore_2.html diff --git a/test/fixtures/multiple-ignores/.liaraignore b/test/fixtures/multiple-ignores/.liaraignore new file mode 100644 index 00000000..9dfe8fd2 --- /dev/null +++ b/test/fixtures/multiple-ignores/.liaraignore @@ -0,0 +1 @@ +ignore_1.html diff --git a/test/fixtures/multiple-ignores/ignore_1.html b/test/fixtures/multiple-ignores/ignore_1.html new file mode 100644 index 00000000..9daeafb9 --- /dev/null +++ b/test/fixtures/multiple-ignores/ignore_1.html @@ -0,0 +1 @@ +test diff --git a/test/fixtures/multiple-ignores/ignore_3.html b/test/fixtures/multiple-ignores/ignore_3.html new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/multiple-ignores/index.php b/test/fixtures/multiple-ignores/index.php new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/php-ignores/index.php b/test/fixtures/php-ignores/index.php new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/php-ignores/vendor/.gitkeep b/test/fixtures/php-ignores/vendor/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/python-ignores/.cache b/test/fixtures/python-ignores/.cache new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/python-ignores/.env b/test/fixtures/python-ignores/.env new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/python-ignores/.python-version b/test/fixtures/python-ignores/.python-version new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/python-ignores/.venv b/test/fixtures/python-ignores/.venv new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/python-ignores/.webassets-cache b/test/fixtures/python-ignores/.webassets-cache new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/python-ignores/ENV b/test/fixtures/python-ignores/ENV new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/python-ignores/celerybeat-schedule b/test/fixtures/python-ignores/celerybeat-schedule new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/python-ignores/example$py.class b/test/fixtures/python-ignores/example$py.class new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/python-ignores/example.pyd b/test/fixtures/python-ignores/example.pyd new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/python-ignores/instance/test.txt b/test/fixtures/python-ignores/instance/test.txt new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/python-ignores/lib/.gitkeep b/test/fixtures/python-ignores/lib/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/python-ignores/lib64/.gitkeep b/test/fixtures/python-ignores/lib64/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/python-ignores/local_settings.py b/test/fixtures/python-ignores/local_settings.py new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/python-ignores/pip-delete-this-directory.txt b/test/fixtures/python-ignores/pip-delete-this-directory.txt new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/python-ignores/pip-log.txt b/test/fixtures/python-ignores/pip-log.txt new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/python-ignores/staticfiles/test.txt b/test/fixtures/python-ignores/staticfiles/test.txt new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/python-ignores/test.log b/test/fixtures/python-ignores/test.log new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/python-ignores/test.txt b/test/fixtures/python-ignores/test.txt new file mode 100644 index 00000000..30d74d25 --- /dev/null +++ b/test/fixtures/python-ignores/test.txt @@ -0,0 +1 @@ +test \ No newline at end of file diff --git a/test/fixtures/python-ignores/venv b/test/fixtures/python-ignores/venv new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/simple-gitignore/ignore_1.html b/test/fixtures/simple-gitignore/ignore_1.html new file mode 100644 index 00000000..9daeafb9 --- /dev/null +++ b/test/fixtures/simple-gitignore/ignore_1.html @@ -0,0 +1 @@ +test diff --git a/test/fixtures/simple-gitignore/ignore_2.html b/test/fixtures/simple-gitignore/ignore_2.html new file mode 100644 index 00000000..9daeafb9 --- /dev/null +++ b/test/fixtures/simple-gitignore/ignore_2.html @@ -0,0 +1 @@ +test From 46600d0c3476f17153d25d99a24b523937bc8d3e Mon Sep 17 00:00:00 2001 From: Mortza81 Date: Tue, 25 Mar 2025 15:05:54 +0330 Subject: [PATCH 15/24] chore: change fixtures --- test/deploy/create-archive.test.ts | 53 +++++++++----------- test/fixtures/multiple-ignores/.gitignore | 1 - test/fixtures/multiple-ignores/ignore_2.html | 1 + 3 files changed, 26 insertions(+), 29 deletions(-) delete mode 100644 test/fixtures/multiple-ignores/.gitignore create mode 100644 test/fixtures/multiple-ignores/ignore_2.html diff --git a/test/deploy/create-archive.test.ts b/test/deploy/create-archive.test.ts index 1e146341..5a3b605d 100644 --- a/test/deploy/create-archive.test.ts +++ b/test/deploy/create-archive.test.ts @@ -21,9 +21,6 @@ describe('create-archive', async () => { it('should create an archive and ignore files and directories listed in .gitignore', async () => { const sourcePath = prepareTmpDirectory(); - fs.writeFileSync('test/fixtures/simple-gitignore/ignore_1.html', 'test'); - fs.writeFileSync('test/fixtures/simple-gitignore/ignore_2.html', 'test'); - const originFiles = getAllFiles('test/fixtures/simple-gitignore'); await createArchive(sourcePath, 'test/fixtures/simple-gitignore', 'php'); @@ -37,10 +34,10 @@ describe('create-archive', async () => { const extractedFiles = getAllFiles(`${sourcePath}-dir`); - expect(extractedFiles).to.deep.equal( - originFiles.filter( - (val) => val != 'ignore_1.html' && val != 'ignore_2.html', - ), + expect(extractedFiles.sort()).to.deep.equal( + originFiles + .filter((val) => val != 'ignore_1.html' && val != 'ignore_2.html') + .sort(), ); }); @@ -60,10 +57,10 @@ describe('create-archive', async () => { const extractedFiles = getAllFiles(`${sourcePath}-dir`); - expect(extractedFiles).to.deep.equal( - originFiles.filter( - (val) => val != 'ignore_1.html' && val != 'ignore_2.html', - ), + expect(extractedFiles.sort()).to.deep.equal( + originFiles + .filter((val) => val != 'ignore_1.html' && val != 'ignore_2.html') + .sort(), ); }); @@ -83,13 +80,15 @@ describe('create-archive', async () => { const extractedFiles = getAllFiles(`${sourcePath}-dir`); - expect(extractedFiles).to.deep.equal( - originFiles.filter( - (val) => - val != 'ignore_1.html' && - val != 'ignore_2.html' && - val != '.dockerignore', - ), + expect(extractedFiles.sort()).to.deep.equal( + originFiles + .filter( + (val) => + val != 'ignore_1.html' && + val != 'ignore_2.html' && + val != '.dockerignore', + ) + .sort(), ); }); @@ -112,8 +111,6 @@ describe('create-archive', async () => { it('should ignore files listed in .liaraignore and disregard .gitignore and .dockerignore if present', async () => { const sourcePath = prepareTmpDirectory(); - fs.writeFileSync('test/fixtures/simple-gitignore/ignore_2.html', 'test'); - const originFiles = getAllFiles('test/fixtures/multiple-ignores'); await createArchive(sourcePath, 'test/fixtures/multiple-ignores', 'php'); @@ -127,10 +124,10 @@ describe('create-archive', async () => { const extractedFiles = getAllFiles(`${sourcePath}-dir`); - expect(extractedFiles).to.deep.equal( - originFiles.filter( - (val) => val != 'ignore_1.html' && val != '.dockerignore', - ), + expect(extractedFiles.sort()).to.deep.equal( + originFiles + .filter((val) => val != 'ignore_1.html' && val != '.dockerignore') + .sort(), ); }); @@ -150,10 +147,10 @@ describe('create-archive', async () => { const extractedFiles = getAllFiles(`${sourcePath}-dir`); - expect(extractedFiles).to.deep.equal( - originFiles.filter( - (val) => val != 'ignore.me' && val != 'sub1/hello.html', - ), + expect(extractedFiles.sort()).to.deep.equal( + originFiles + .filter((val) => val != 'ignore.me' && val != 'sub1/hello.html') + .sort(), ); }); diff --git a/test/fixtures/multiple-ignores/.gitignore b/test/fixtures/multiple-ignores/.gitignore deleted file mode 100644 index 29a83eae..00000000 --- a/test/fixtures/multiple-ignores/.gitignore +++ /dev/null @@ -1 +0,0 @@ -ignore_2.html diff --git a/test/fixtures/multiple-ignores/ignore_2.html b/test/fixtures/multiple-ignores/ignore_2.html new file mode 100644 index 00000000..9daeafb9 --- /dev/null +++ b/test/fixtures/multiple-ignores/ignore_2.html @@ -0,0 +1 @@ +test From a0cada74757d9b19ce1eb6d623a95ef5cc7c93e8 Mon Sep 17 00:00:00 2001 From: Mortza81 Date: Tue, 25 Mar 2025 15:09:22 +0330 Subject: [PATCH 16/24] chore: add gitignore in fixtures --- test/fixtures/multiple-ignores/.gitignore | 1 + test/fixtures/simple-gitignore/.gitignore | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 test/fixtures/multiple-ignores/.gitignore create mode 100644 test/fixtures/simple-gitignore/.gitignore diff --git a/test/fixtures/multiple-ignores/.gitignore b/test/fixtures/multiple-ignores/.gitignore new file mode 100644 index 00000000..29a83eae --- /dev/null +++ b/test/fixtures/multiple-ignores/.gitignore @@ -0,0 +1 @@ +ignore_2.html diff --git a/test/fixtures/simple-gitignore/.gitignore b/test/fixtures/simple-gitignore/.gitignore new file mode 100644 index 00000000..15efd3a0 --- /dev/null +++ b/test/fixtures/simple-gitignore/.gitignore @@ -0,0 +1,2 @@ +ignore_2.html +ignore_1.html From 7a70426cef4be9f4afd237ff5b36dd8d7e4179d5 Mon Sep 17 00:00:00 2001 From: Mortza81 Date: Sat, 29 Mar 2025 12:39:26 +0330 Subject: [PATCH 17/24] delete unnecessary files in fixtures --- .../nodejs-app/public/css/BeautifulPeople.ttf | Bin 197192 -> 0 bytes test/fixtures/nodejs-app/public/css/app.css | 28 -- .../nodejs-app/public/images/favicon.ico | Bin 15406 -> 0 bytes test/fixtures/nodejs-app/public/js/app.js | 275 ------------------ 4 files changed, 303 deletions(-) delete mode 100644 test/fixtures/nodejs-app/public/css/BeautifulPeople.ttf delete mode 100644 test/fixtures/nodejs-app/public/css/app.css delete mode 100644 test/fixtures/nodejs-app/public/images/favicon.ico delete mode 100644 test/fixtures/nodejs-app/public/js/app.js diff --git a/test/fixtures/nodejs-app/public/css/BeautifulPeople.ttf b/test/fixtures/nodejs-app/public/css/BeautifulPeople.ttf deleted file mode 100644 index 8746c57477af2faa247d386a37406ed46c7f1f4b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 197192 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 1 ? 1 : p; - - this.complete = this._progress === 1; - }, - get progress() { - return this._progress; - }, - draw: function () { - ctx.fillStyle = '#fff'; - ctx.beginPath(); - ctx.arc( - this.x, - this.y, - this.r, - -HALF_PI, - TWO_PI * this._progress - HALF_PI, - ); - ctx.lineTo(this.x, this.y); - ctx.closePath(); - ctx.fill(); - }, -}; - -// pun intended -Exploader = function (x, y) { - this.x = x; - this.y = y; - - this.startRadius = 24; - - this.time = 0; - this.duration = 0.4; - this.progress = 0; - - this.complete = false; -}; - -Exploader.prototype = { - reset: function () { - this.time = 0; - this.progress = 0; - this.complete = false; - }, - update: function () { - this.time = Math.min(this.duration, this.time + timeStep); - this.progress = Ease.inBack(this.time, 0, 1, this.duration); - - this.complete = this.time === this.duration; - }, - draw: function () { - ctx.fillStyle = '#fff'; - ctx.beginPath(); - ctx.arc(this.x, this.y, this.startRadius * (1 - this.progress), 0, TWO_PI); - ctx.fill(); - }, -}; - -var particles = [], - loader, - exploader, - phase = 0; - -function initDrawingCanvas() { - drawingCanvas.width = viewWidth; - drawingCanvas.height = viewHeight; - ctx = drawingCanvas.getContext('2d'); - - createLoader(); - createExploader(); - createParticles(); -} - -function createLoader() { - loader = new Loader(viewWidth * 0.5, viewHeight * 0.5); -} - -function createExploader() { - exploader = new Exploader(viewWidth * 0.5, viewHeight * 0.5); -} - -function createParticles() { - for (var i = 0; i < 128; i++) { - var p0 = new Point(viewWidth * 0.5, viewHeight * 0.5); - var p1 = new Point(Math.random() * viewWidth, Math.random() * viewHeight); - var p2 = new Point(Math.random() * viewWidth, Math.random() * viewHeight); - var p3 = new Point(Math.random() * viewWidth, viewHeight + 64); - - particles.push(new Particle(p0, p1, p2, p3)); - } -} - -function update() { - switch (phase) { - case 0: - loader.progress += 1 / 45; - break; - case 1: - exploader.update(); - break; - case 2: - particles.forEach(function (p) { - p.update(); - }); - break; - } -} - -function draw() { - ctx.clearRect(0, 0, viewWidth, viewHeight); - - switch (phase) { - case 0: - loader.draw(); - break; - case 1: - exploader.draw(); - break; - case 2: - particles.forEach(function (p) { - p.draw(); - }); - break; - } -} - -window.onload = function () { - initDrawingCanvas(); - requestAnimationFrame(loop); -}; - -function loop() { - update(); - draw(); - - if (phase === 0 && loader.complete) { - phase = 1; - } else if (phase === 1 && exploader.complete) { - phase = 2; - } else if (phase === 2 && checkParticlesComplete()) { - // reset - phase = 2; - //loader.reset(); - exploader.reset(); - particles.length = 0; - createParticles(); - } - - requestAnimationFrame(loop); -} - -function checkParticlesComplete() { - for (var i = 0; i < particles.length; i++) { - if (particles[i].complete === false) return false; - } - return true; -} - -// math and stuff - -var Ease = { - inCubic: function (t, b, c, d) { - t /= d; - return c * t * t * t + b; - }, - outCubic: function (t, b, c, d) { - t /= d; - t--; - return c * (t * t * t + 1) + b; - }, - inOutCubic: function (t, b, c, d) { - t /= d / 2; - if (t < 1) return (c / 2) * t * t * t + b; - t -= 2; - return (c / 2) * (t * t * t + 2) + b; - }, - inBack: function (t, b, c, d, s) { - s = s || 1.70158; - return c * (t /= d) * t * ((s + 1) * t - s) + b; - }, -}; - -function cubeBezier(p0, c0, c1, p1, t) { - var p = new Point(); - var nt = 1 - t; - - p.x = - nt * nt * nt * p0.x + - 3 * nt * nt * t * c0.x + - 3 * nt * t * t * c1.x + - t * t * t * p1.x; - p.y = - nt * nt * nt * p0.y + - 3 * nt * nt * t * c0.y + - 3 * nt * t * t * c1.y + - t * t * t * p1.y; - - return p; -} From 855eaf8ad11b0a76fdccdc69c5a36d31d91c9810 Mon Sep 17 00:00:00 2001 From: Mortza81 Date: Thu, 31 Jul 2025 16:27:52 +0330 Subject: [PATCH 18/24] update: tests --- test/app/create.unit.test.ts | 2 +- test/deploy/deploy.unit.test.ts | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/test/app/create.unit.test.ts b/test/app/create.unit.test.ts index 75919ade..a69c64fc 100644 --- a/test/app/create.unit.test.ts +++ b/test/app/create.unit.test.ts @@ -4,7 +4,7 @@ import { expect } from 'chai'; import { networks } from '../fixtures/networks/fixture.ts'; describe('app:create', function () { - const api = nock('https://api.iran.liara.ir'); + const api = nock('https://api.liara.ir'); beforeEach(() => { api.get('/v1/networks').query({ teamID: '' }).reply(200, networks); diff --git a/test/deploy/deploy.unit.test.ts b/test/deploy/deploy.unit.test.ts index b2b15722..db600938 100644 --- a/test/deploy/deploy.unit.test.ts +++ b/test/deploy/deploy.unit.test.ts @@ -3,14 +3,14 @@ import sinon from 'sinon'; import { runCommand } from '@oclif/test'; import deploy from '../../src/commands/deploy.ts'; import nock from 'nock'; +import fs from 'fs-extra'; import { - projects, getNodeProject, getDockerProject, } from '../fixtures/projects/fixture.ts'; describe('deploy', () => { - const api = nock('https://api.iran.liara.ir'); + const api = nock('https://api.liara.ir'); let getConfigs: sinon.SinonStub; beforeEach(() => { @@ -20,10 +20,11 @@ describe('deploy', () => { sinon.restore(); }); it('should thorw an error when project path is empty and image flag is specified', async () => { + fs.mkdirSync('test/fixtures/empty-project'); getConfigs.returns({ path: 'test/fixtures/empty-project' }); const { error } = await runCommand(['deploy']); - + fs.rmdirSync('test/fixtures/empty-project'); expect(error?.message).to.equal('Directory is empty!'); }); From df4807cfed9134325b7173533a898abdaf05b48a Mon Sep 17 00:00:00 2001 From: Mortza81 Date: Fri, 8 Aug 2025 14:21:39 +0330 Subject: [PATCH 19/24] chore: refactor getAllFiles() --- test/helpers/getAllFiles.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/test/helpers/getAllFiles.ts b/test/helpers/getAllFiles.ts index 82b1b6b6..d7741c8d 100644 --- a/test/helpers/getAllFiles.ts +++ b/test/helpers/getAllFiles.ts @@ -7,14 +7,16 @@ export default function getAllFiles( filePaths: string[] = [], ): string[] { const items = fs.readdirSync(dir); - items.forEach((item) => { - const itemPath = path.join(dir, item); - if (fs.statSync(itemPath).isDirectory()) { - getAllFiles(itemPath, baseDir, filePaths); - } else { + if (items.length != 0) { + items.forEach((item) => { + const itemPath = path.join(dir, item); + if (fs.statSync(itemPath).isDirectory()) { + getAllFiles(itemPath, baseDir, filePaths); + return; + } const relativePath = path.relative(baseDir, itemPath); filePaths.push(relativePath); - } - }); + }); + } return filePaths; } From 30e5f247ae68fbe508fd58a710eeeb15be71afa3 Mon Sep 17 00:00:00 2001 From: Mortza81 Date: Fri, 8 Aug 2025 14:27:31 +0330 Subject: [PATCH 20/24] fix: return node param --- src/base.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/base.ts b/src/base.ts index 755b7d0c..77537c1e 100644 --- a/src/base.ts +++ b/src/base.ts @@ -128,6 +128,10 @@ export interface IProjectDetails { fixedIPStatus: string; created_at: string; hourlyPrice: number; + node: { + _id: string; + IP: string; + }; isDeployed: boolean; reservedDiskSpace: number; readOnlyRootFilesystem: boolean; From b75a678ddf9606acdf4816f2945f23bbf5f19a14 Mon Sep 17 00:00:00 2001 From: Mortza81 Date: Fri, 8 Aug 2025 14:33:35 +0330 Subject: [PATCH 21/24] chore: remove unnecessary param in network type --- src/types/network.ts | 2 -- test/fixtures/networks/fixture.ts | 23 +++++++++-------------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/types/network.ts b/src/types/network.ts index 9de02c7d..2dff0b6f 100644 --- a/src/types/network.ts +++ b/src/types/network.ts @@ -2,6 +2,4 @@ export default interface INetwork { _id: string; name: string; createdAt: string; - projectCount: number, - databaseCount: number } diff --git a/test/fixtures/networks/fixture.ts b/test/fixtures/networks/fixture.ts index 2fb04da1..bc8143d6 100644 --- a/test/fixtures/networks/fixture.ts +++ b/test/fixtures/networks/fixture.ts @@ -1,20 +1,15 @@ -import IGetNetworkResponse from "../../../src/types/get-network-response" +import IGetNetworkResponse from '../../../src/types/get-network-response'; export const networks: IGetNetworkResponse = { networks: [ { - _id: "64c7f1a2b3e8c91d0e5f7b2a", - name: "network-abc123", - createdAt: "2023-10-05T12:34:56Z", - projectCount: 1, - databaseCount: 0 + _id: '64c7f1a2b3e8c91d0e5f7b2a', + name: 'network-abc123', + createdAt: '2023-10-05T12:34:56Z', }, { - _id: "64c7f1a2b3e8c91d0e5f7b2b", - name: "network-xyz789", - createdAt: "2023-10-01T09:15:30Z", - projectCount: 1, - databaseCount: 0 - } - ] + _id: '64c7f1a2b3e8c91d0e5f7b2b', + name: 'network-xyz789', + createdAt: '2023-10-01T09:15:30Z', + }, + ], }; - From d401d5b0cc3281f96474f7eec8ff2330b5a47f4b Mon Sep 17 00:00:00 2001 From: Mortza81 Date: Fri, 8 Aug 2025 15:36:05 +0330 Subject: [PATCH 22/24] chore: add ci.yaml --- .github/workflows/ci.yaml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/ci.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..b5f21387 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,22 @@ +name: Run tests + +on: + push: + branches: + - master + - chore/add-tests + pull_request: + branches: + - master +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - run: npm i -g npm + - run: npm ci + - run: npm run test From a959c64f9035aebcc87986aa1180cb19949e78c7 Mon Sep 17 00:00:00 2001 From: Mortza81 Date: Fri, 8 Aug 2025 18:22:11 +0330 Subject: [PATCH 23/24] chore: remove region from tests --- test/auth/login.unit.test.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/test/auth/login.unit.test.ts b/test/auth/login.unit.test.ts index 9189e03c..fe5fe0ac 100644 --- a/test/auth/login.unit.test.ts +++ b/test/auth/login.unit.test.ts @@ -36,17 +36,15 @@ describe('login', async () => { const expectedData = JSON.stringify({ accounts: { - [`${accounts[0].email.split('@')[0]}_${accounts[0].region}`]: { + [`${accounts[0].email.split('@')[0]}`]: { email: accounts[0].email, - region: accounts[0].region, avatar: accounts[0].avatar, api_token: accounts[0].token, fullname: accounts[0].fullname, current: false, }, - [`${accounts[1].email.split('@')[0]}_${accounts[1].region}`]: { + [`${accounts[1].email.split('@')[0]}`]: { email: accounts[1].email, - region: accounts[1].region, avatar: accounts[1].avatar, api_token: accounts[1].token, fullname: accounts[1].fullname, @@ -72,17 +70,15 @@ describe('login', async () => { const expectedAccounts = { ...currentAccounts, - [`${accounts[0].email.split('@')[0]}_${accounts[0].region}`]: { + [`${accounts[0].email.split('@')[0]}`]: { email: accounts[0].email, - region: accounts[0].region, avatar: accounts[0].avatar, api_token: accounts[0].token, fullname: accounts[0].fullname, current: false, }, - [`${accounts[1].email.split('@')[0]}_${accounts[1].region}`]: { + [`${accounts[1].email.split('@')[0]}`]: { email: accounts[1].email, - region: accounts[1].region, avatar: accounts[1].avatar, api_token: accounts[1].token, fullname: accounts[1].fullname, From 26cbb929295cd120c54b60e634eae13bd743aaaf Mon Sep 17 00:00:00 2001 From: Mortza81 Date: Fri, 8 Aug 2025 18:24:07 +0330 Subject: [PATCH 24/24] fix: workflow --- .github/workflows/ci.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b5f21387..6956b7d8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -4,7 +4,6 @@ on: push: branches: - master - - chore/add-tests pull_request: branches: - master