diff --git a/package-lock.json b/package-lock.json index f10b109b3..667e76559 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10500,6 +10500,18 @@ "resolved": "tools/file-walker", "link": true }, + "node_modules/fill-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", + "integrity": "sha512-tcgI872xXjwFF4xgQmLxi76GnwJG3g/3isB1l4/G5Z4zrbddGpBjqZCO9oEAcB5wX0Hj/5iQB3toxfO7in1hHA==", + "dependencies": { + "is-object": "~1.0.1", + "merge-descriptors": "~1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -11884,6 +11896,14 @@ "node": ">=0.12.0" } }, + "node_modules/is-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", + "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -13408,6 +13428,11 @@ "resolved": "tools/module-docgen", "link": true }, + "node_modules/module-not-found-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", + "integrity": "sha512-pEk4ECWQXV6z2zjhRZUongnLJNUeGQJ3w6OQ5ctGwD+i5o93qjRQUk2Rt6VdNeu3sEP0AB4LcfvdebpxBRVr4g==" + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -14730,6 +14755,16 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/proxyquire": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz", + "integrity": "sha512-BQWfCqYM+QINd+yawJz23tbBM40VIGXOdDw3X344KcclI/gtBbdWF6SlQ4nK/bYhF9d27KYug9WzljHC6B9Ysg==", + "dependencies": { + "fill-keys": "^1.0.2", + "module-not-found-error": "^1.0.1", + "resolve": "^1.11.1" + } + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -17712,6 +17747,7 @@ "openai": "^4.73.1", "otpauth": "9.2.4", "prompt-sync": "^4.2.0", + "proxyquire": "^2.1.3", "recursive-readdir": "^2.2.3", "response-time": "^2.3.2", "seedrandom": "^3.0.5", diff --git a/src/backend/package.json b/src/backend/package.json index 0fa91a399..ff5c0482f 100644 --- a/src/backend/package.json +++ b/src/backend/package.json @@ -4,7 +4,7 @@ "description": "Backend/Kernel for Puter", "main": "exports.js", "scripts": { - "test": "npx mocha" + "test": "npx mocha src/**/*.test.js && node ./tools/test.js" }, "dependencies": { "@anthropic-ai/sdk": "^0.26.1", @@ -61,6 +61,7 @@ "openai": "^4.73.1", "otpauth": "9.2.4", "prompt-sync": "^4.2.0", + "proxyquire": "^2.1.3", "recursive-readdir": "^2.2.3", "response-time": "^2.3.2", "seedrandom": "^3.0.5", diff --git a/src/backend/src/routers/get-launch-apps.js b/src/backend/src/routers/get-launch-apps.js index ab48519e7..bba49dafa 100644 --- a/src/backend/src/routers/get-launch-apps.js +++ b/src/backend/src/routers/get-launch-apps.js @@ -26,7 +26,7 @@ const { DB_READ } = require('../services/database/consts.js'); // -----------------------------------------------------------------------// // GET /get-launch-apps // -----------------------------------------------------------------------// -router.get('/get-launch-apps', auth, express.json(), async (req, res, next)=>{ +module.exports = async (req, res) => { let result = {}; // -----------------------------------------------------------------------// @@ -118,6 +118,4 @@ router.get('/get-launch-apps', auth, express.json(), async (req, res, next)=>{ } return res.send(result); -}) - -module.exports = router +}; diff --git a/src/backend/src/routers/get-launch-apps.test.js b/src/backend/src/routers/get-launch-apps.test.js new file mode 100644 index 000000000..b70f918de --- /dev/null +++ b/src/backend/src/routers/get-launch-apps.test.js @@ -0,0 +1,156 @@ +const sinon = require('sinon'); +const { expect } = require('chai'); +const proxyquire = require('proxyquire'); +const kvjs = require('@heyputer/kv.js'); +const uuid = require('uuid'); + +const TEST_UUID_NAMESPACE = '5568ab95-229d-4d87-b98c-0b12680a9524'; + +const apps_names_expected_to_exist = [ + 'app-center', + 'dev-center', + 'editor', +]; + +const data_mockapps = (() => { + const data_mockapps = []; + // List of app names that get-launch-apps expects to exist + for ( const name of apps_names_expected_to_exist ) { + data_mockapps.push({ + uid: 'app-' + uuid.v5(name, TEST_UUID_NAMESPACE), + name, + title: 'App Name', + icon: 'icon-goes-here', + godmode: false, + maximize_on_start: false, + index_url: 'index-url' + }); + } + + // An additional app that won't show up in taskbar + data_mockapps.push({ + uid: 'app-' + uuid.v5('hidden-app', TEST_UUID_NAMESPACE), + name: 'hidden-app', + title: 'Hidden App', + icon: 'icon-goes-here', + godmode: false, + maximize_on_start: false, + index_url: 'index-url' + }); + + // An additional app tha only shows up in recents + data_mockapps.push({ + uid: 'app-' + uuid.v5('recent-app', TEST_UUID_NAMESPACE), + name: 'recent-app', + title: 'Recent App', + icon: 'icon-goes-here', + godmode: false, + maximize_on_start: false, + index_url: 'index-url' + }); + + return data_mockapps; +})(); + +const data_appopens = [ + { + app_uid: 'app-' + uuid.v5('app-center', TEST_UUID_NAMESPACE), + }, + { + app_uid: 'app-' + uuid.v5('editor', TEST_UUID_NAMESPACE), + }, + { + app_uid: 'app-' + uuid.v5('recent-app', TEST_UUID_NAMESPACE), + }, +]; + +describe('GET /launch-apps', () => { + it('should return expected format', async () => { + globalThis.kv = new kvjs(); + const database_mock = { + read: async (query) => { + if ( query.includes('FROM app_opens') ) { + return data_appopens; + } + } + }; + const services_mock = { + get: (key) => { + if (key === 'database') { + return { + get: () => database_mock, + } + } + } + }; + + const req_mock = { + user: { + id: 1 + Math.floor(Math.random() * 1000**3), + }, + services: services_mock, + send: sinon.spy(), + }; + + const res_mock = { + send: sinon.spy(), + }; + + const get_launch_apps = proxyquire('./get-launch-apps', { + '../helpers.js': { + get_app: async ({ uid, name }) => { + if ( uid ) { + return data_mockapps.find(app => app.uid === uid); + } + if ( name ) { + return data_mockapps.find(app => app.name === name); + } + } + } + }); + + await get_launch_apps(req_mock, res_mock); + + expect(res_mock.send.calledOnce).to.equal(true, 'res.send should be called once'); + + const call = res_mock.send.firstCall; + response = call.args[0]; + console.log('response', response); + + expect(response).to.be.an('object'); + + expect(response).to.have.property('recommended'); + expect(response.recommended).to.be.an('array'); + expect(response.recommended).to.have.lengthOf(apps_names_expected_to_exist.length); + expect(response.recommended).to.deep.equal( + data_mockapps + .filter(app => apps_names_expected_to_exist.includes(app.name)) + .map(app => ({ + uuid: app.uid, + name: app.name, + title: app.title, + icon: app.icon, + godmode: app.godmode, + maximize_on_start: app.maximize_on_start, + index_url: app.index_url, + })) + ); + + expect(response).to.have.property('recent'); + expect(response.recent).to.be.an('array'); + expect(response.recent).to.have.lengthOf(data_appopens.length); + expect(response.recent).to.deep.equal( + data_mockapps + .filter(app => data_appopens.map(app_open => app_open.app_uid).includes(app.uid)) + .map(app => ({ + uuid: app.uid, + name: app.name, + title: app.title, + icon: app.icon, + godmode: app.godmode, + maximize_on_start: app.maximize_on_start, + index_url: app.index_url, + })) + ); + }) +}); \ No newline at end of file diff --git a/src/backend/src/services/PuterAPIService.js b/src/backend/src/services/PuterAPIService.js index 01cff4bb8..7134313c8 100644 --- a/src/backend/src/services/PuterAPIService.js +++ b/src/backend/src/services/PuterAPIService.js @@ -17,6 +17,8 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +const configurable_auth = require("../middleware/configurable_auth"); +const { Endpoint } = require("../util/expressutil"); const BaseService = require("./BaseService"); @@ -64,7 +66,7 @@ class PuterAPIService extends BaseService { app.use(require('../routers/kvstore/setItem')) app.use(require('../routers/kvstore/listItems')) app.use(require('../routers/kvstore/clearItems')) - app.use(require('../routers/get-launch-apps')) + // app.use(require('../routers/get-launch-apps')) app.use(require('../routers/itemMetadata')) app.use(require('../routers/login')) app.use(require('../routers/logout')) @@ -91,6 +93,14 @@ class PuterAPIService extends BaseService { app.use(require('../routers/update-taskbar-items')) require('../routers/whoami')(app); + + Endpoint({ + route: '/get-launch-apps', + methods: ['GET'], + mw: [configurable_auth()], + handler: require('../routers/get-launch-apps'), + }).attach(app); + } }