| /** |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, |
| software distributed under the License is distributed on an |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| KIND, either express or implied. See the License for the |
| specific language governing permissions and limitations |
| under the License. |
| */ |
| |
| const fs = require('node:fs'); |
| const fsp = require('node:fs/promises'); |
| const path = require('node:path'); |
| const rewire = require('rewire'); |
| const cordovaServe = require('cordova-serve'); |
| const { tmpDir: getTmpDir, omniStub } = require('../helpers'); |
| const cordovaUtil = require('../../src/cordova/util'); |
| const platforms = require('../../src/platforms/platforms'); |
| |
| describe('cordova/serve', () => { |
| let serve; |
| |
| beforeEach(() => { |
| serve = rewire('../../src/cordova/serve'); |
| }); |
| |
| describe('main export', () => { |
| const PROJECT_ROOT = '/root'; |
| |
| class HooksRunnerMock { |
| constructor (dir) { expect(dir).toBe(PROJECT_ROOT); } |
| |
| fire (hook, fireOpts) { return Promise.resolve(); } |
| } |
| |
| beforeEach(() => { |
| spyOn(cordovaUtil, 'cdProjectRoot').and.returnValue(PROJECT_ROOT); |
| serve.__set__({ |
| HooksRunner: HooksRunnerMock, |
| serve: jasmine.createSpy('serve', _ => Promise.resolve()) |
| }); |
| }); |
| |
| it('should call hooks and `serve` in proper order', () => { |
| const PORT = 1234567; |
| const OPTS = {}; |
| |
| // Records order of some spy calls since jasmine has no support for |
| // checking interleaved calls. |
| const calls = []; |
| const pushAndResolve = tag => { |
| calls.push(tag); |
| return Promise.resolve(tag); |
| }; |
| serve.__get__('serve').and.callFake(port => { |
| expect(port).toBe(PORT); |
| return pushAndResolve('serve'); |
| }); |
| spyOn(HooksRunnerMock.prototype, 'fire').and.callFake((hook, opts) => { |
| expect(opts).toBe(OPTS); |
| return pushAndResolve(hook); |
| }); |
| |
| return serve(PORT, OPTS).then(result => { |
| expect(result).toBe('serve'); |
| expect(calls).toEqual([ |
| 'before_serve', 'serve', 'after_serve' |
| ]); |
| }); |
| }); |
| |
| it('should fail if run outside of a Cordova project', async () => { |
| const fakeMessage = 'CAN I HAZ CORDOVA PLZ?'; |
| cordovaUtil.cdProjectRoot.and.throwError(fakeMessage); |
| |
| await expectAsync( |
| serve(1234567, {}) |
| ).toBeRejectedWithError(fakeMessage); |
| |
| expect(cordovaUtil.cdProjectRoot).toHaveBeenCalled(); |
| }); |
| |
| it('should fail if serve fails', () => { |
| const fakeError = new Error('FAKE'); |
| serve.__get__('serve').and.returnValue(Promise.reject(fakeError)); |
| |
| return expectAsync( |
| serve(1234567, {}) |
| ).toBeRejectedWith(fakeError); |
| }, 100); |
| }); |
| |
| describe('serve', () => { |
| let privateServe, serverSpy; |
| |
| beforeEach(() => { |
| serverSpy = jasmine.createSpyObj('server', ['launchServer']); |
| serverSpy.app = Symbol('server.app'); |
| serverSpy.server = Symbol('server.server'); |
| |
| serve.__set__({ |
| cordovaPrepare: jasmine.createSpy('cordovaPrepare').and.returnValue(Promise.resolve()), |
| cordovaServe: jasmine.createSpy('cordova-serve').and.returnValue(serverSpy), |
| registerRoutes: jasmine.createSpy('registerRoutes') |
| }); |
| privateServe = serve.__get__('serve'); |
| }); |
| |
| it('should launch a server after preparing the project', () => { |
| const PORT = 1234567; |
| const registerRoutes = serve.__get__('registerRoutes'); |
| const prepareSpy = serve.__get__('cordovaPrepare'); |
| |
| return privateServe(PORT).then(result => { |
| expect(result).toBe(serverSpy.server); |
| expect(registerRoutes).toHaveBeenCalledWith(serverSpy.app); |
| expect(serverSpy.launchServer).toHaveBeenCalledWith( |
| jasmine.objectContaining({ port: PORT }) |
| ); |
| expect(prepareSpy).toHaveBeenCalledBefore(serverSpy.launchServer); |
| }); |
| }); |
| |
| it('should work without arguments', () => { |
| return privateServe().then(result => { |
| expect(serverSpy.launchServer).toHaveBeenCalledWith( |
| jasmine.objectContaining({ port: 8000 }) |
| ); |
| }); |
| }); |
| |
| it('should fail if prepare fails', () => { |
| const fakeError = new Error('FAKE'); |
| const prepareSpy = serve.__get__('cordovaPrepare'); |
| prepareSpy.and.returnValue(Promise.reject(fakeError)); |
| |
| return expectAsync( |
| privateServe(1234567, {}) |
| ).toBeRejectedWith(fakeError); |
| }, 100); |
| |
| it('should fail if launching the server fails', () => { |
| const fakeError = new Error('FAKE'); |
| serverSpy.launchServer.and.returnValue(Promise.reject(fakeError)); |
| |
| return expectAsync( |
| privateServe(1234567, {}) |
| ).toBeRejectedWith(fakeError); |
| }, 100); |
| }); |
| |
| describe('registerRoutes', () => { |
| let registerRoutes, app; |
| |
| beforeEach(() => { |
| spyOn(cordovaUtil, 'listPlatforms').and.returnValue(['foo']); |
| serve.__set__({ |
| handleRoot: jasmine.createSpy('handleRoot'), |
| absolutePathHandler: jasmine.createSpy('absolutePathHandler'), |
| platformRouter: jasmine.createSpy('platformRouter') |
| .and.returnValue(_ => _) |
| }); |
| registerRoutes = serve.__get__('registerRoutes'); |
| app = new cordovaServe.Router(); |
| }); |
| |
| it('should register a route for absolute paths', () => { |
| const absolutePathHandler = serve.__get__('absolutePathHandler'); |
| |
| registerRoutes(app); |
| app({ method: 'GET', url: '/config.xml' }, null); |
| expect(absolutePathHandler).toHaveBeenCalled(); |
| }); |
| |
| it('should register a fallback root route', () => { |
| const absolutePathHandler = serve.__get__('absolutePathHandler'); |
| absolutePathHandler.and.callFake((req, res, next) => next()); |
| const handleRoot = serve.__get__('handleRoot'); |
| |
| registerRoutes(app); |
| app({ method: 'GET', url: '/' }, null); |
| expect(handleRoot).toHaveBeenCalled(); |
| }); |
| |
| it('should register platform routes', () => { |
| cordovaUtil.listPlatforms.and.returnValue(['foo', 'bar']); |
| const platformRouter = serve.__get__('platformRouter'); |
| platformRouter |
| .withArgs('foo').and.returnValue(jasmine.createSpy('fooHandler')) |
| .withArgs('bar').and.returnValue(jasmine.createSpy('barHandler')); |
| |
| registerRoutes(app); |
| app({ method: 'GET', url: '/foo/index.html' }, null); |
| expect(platformRouter('foo')).toHaveBeenCalled(); |
| expect(platformRouter('bar')).not.toHaveBeenCalled(); |
| |
| platformRouter('foo').calls.reset(); |
| app({ method: 'GET', url: '/bar/index.html' }, null); |
| expect(platformRouter('bar')).toHaveBeenCalled(); |
| expect(platformRouter('foo')).not.toHaveBeenCalled(); |
| }); |
| }); |
| |
| describe('handleRoot', () => { |
| let handleRoot, response; |
| |
| beforeEach(() => { |
| handleRoot = serve.__get__('handleRoot'); |
| serve.__set__({ |
| platforms: { foo: 0, bar: 0 }, |
| installedPlatforms: ['foo'], |
| projectRoot: '', |
| ConfigParser: function () { |
| return omniStub({ src: undefined }); |
| } |
| }); |
| |
| spyOn(cordovaUtil, 'projectConfig'); |
| spyOn(cordovaUtil, 'findPlugins').and.returnValue([ |
| 'cordova-plugin-beer' |
| ]); |
| |
| response = jasmine.createSpyObj('response', ['send']); |
| }); |
| |
| it('should return an index of available platforms and plugins', () => { |
| handleRoot(null, response); |
| expect(response.send).toHaveBeenCalledTimes(1); |
| |
| const [document] = response.send.calls.argsFor(0); |
| expect(document).toContain('cordova-plugin-beer'); |
| expect(document).toContain('foo'); |
| expect(document).toContain('bar'); |
| |
| // Contains links to installed platforms only |
| expect(document).toContain('"foo/www/index.html"'); |
| expect(document).not.toContain('"bar/www/index.html"'); |
| }); |
| }); |
| |
| describe('absolutePathHandler', () => { |
| let absolutePathHandler, next; |
| |
| beforeEach(() => { |
| absolutePathHandler = serve.__get__('absolutePathHandler'); |
| serve.__set__({ installedPlatforms: ['foo'] }); |
| |
| next = jasmine.createSpy('next'); |
| }); |
| |
| it('should do nothing if `referer` is not set', () => { |
| const request = { headers: {} }; |
| |
| absolutePathHandler(request, null, next); |
| expect(next).toHaveBeenCalled(); |
| }); |
| |
| it('should do nothing if `referer` is not a platform URL', () => { |
| const request = { headers: { referer: '/www/index.html' } }; |
| |
| absolutePathHandler(request, null, next); |
| expect(next).toHaveBeenCalled(); |
| }); |
| |
| it('should do nothing if requested URL is a platform URL', () => { |
| const request = { |
| headers: { referer: '/foo/www/index.html' }, |
| originalUrl: '/foo/www/style.css' |
| }; |
| |
| absolutePathHandler(request, null, next); |
| expect(next).toHaveBeenCalled(); |
| }); |
| |
| it('should redirect all other requests relative to /platform/www', () => { |
| const request = { |
| headers: { referer: '/foo/index.html' }, |
| originalUrl: '/style.css' |
| }; |
| const response = jasmine.createSpyObj('response', ['redirect']); |
| |
| absolutePathHandler(request, response, next); |
| expect(next).not.toHaveBeenCalled(); |
| expect(response.redirect).toHaveBeenCalledWith('/foo/www/style.css'); |
| }); |
| }); |
| |
| describe('platformRouter', () => { |
| const PLATFORM = 'atari'; |
| let platformRouter; |
| |
| beforeEach(() => { |
| platformRouter = serve.__get__('platformRouter'); |
| spyOn(platforms, 'getPlatformApi').and.returnValue({ |
| getPlatformInfo: _ => ({ |
| locations: { configXml: 'CONFIG', www: 'WWW_DIR' } |
| }) |
| }); |
| }); |
| |
| it('should serve static from www', () => { |
| const staticSpy = jasmine.createSpy('static'); |
| spyOn(cordovaServe, 'static').and.returnValue(staticSpy); |
| |
| const router = platformRouter(PLATFORM); |
| expect(cordovaServe.static).toHaveBeenCalledWith('WWW_DIR'); |
| |
| router({ method: 'GET', url: '/www/foo' }, null); |
| expect(staticSpy).toHaveBeenCalled(); |
| }); |
| |
| it('should serve the platform config.xml', () => { |
| const router = platformRouter(PLATFORM); |
| const sendFile = jasmine.createSpy('sendFile'); |
| |
| router({ method: 'GET', url: '/config.xml' }, { sendFile }); |
| expect(sendFile).toHaveBeenCalledWith('CONFIG'); |
| }); |
| |
| it('should serve generated information under /project.json', () => { |
| serve.__set__({ generateWwwFileList: x => x }); |
| const router = platformRouter(PLATFORM); |
| const send = jasmine.createSpy('send'); |
| |
| router({ method: 'GET', url: '/project.json' }, { send }); |
| expect(send).toHaveBeenCalledWith({ |
| configPath: `/${PLATFORM}/config.xml`, |
| wwwPath: `/${PLATFORM}/www`, |
| wwwFileList: 'WWW_DIR' |
| }); |
| }); |
| }); |
| |
| describe('generateWwwFileList', () => { |
| const emptyStringMd5 = 'd41d8cd98f00b204e9800998ecf8427e'; |
| let generateWwwFileList, tmpDir; |
| |
| beforeEach(() => { |
| generateWwwFileList = serve.__get__('generateWwwFileList'); |
| tmpDir = getTmpDir('serve-test'); |
| }); |
| |
| afterEach(() => { |
| return fsp.rm(tmpDir, { recursive: true, force: true }); |
| }); |
| |
| it('should generate a list of files with MD5 sums', () => { |
| for (const f of ['a', path.join('b', 'c')]) { |
| const fullpath = path.join(tmpDir, f); |
| fs.mkdirSync(path.dirname(fullpath), { recursive: true }); |
| fs.writeFileSync(fullpath, '', 'utf8'); |
| } |
| expect(generateWwwFileList(tmpDir)).toEqual([ |
| { path: 'a', etag: emptyStringMd5 }, |
| { path: 'b/c', etag: emptyStringMd5 } |
| ]); |
| }); |
| |
| it('should not include hidden files or directories', () => { |
| for (const f of ['a', path.join('.b', 'c'), '.d']) { |
| const fullpath = path.join(tmpDir, f); |
| fs.mkdirSync(path.dirname(fullpath), { recursive: true }); |
| fs.writeFileSync(fullpath, '', 'utf8'); |
| } |
| expect(generateWwwFileList(tmpDir)).toEqual([ |
| { path: 'a', etag: emptyStringMd5 } |
| ]); |
| }); |
| }); |
| }); |