diff --git a/package.json b/package.json index 0ec7aecd7..e12793b13 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "eslint-plugin-promise": "^4.0.1", "eslint-plugin-standard": "^4.0.0", "jasmine": "^3.3.1", + "mock-stdin": "^0.3.1", "nyc": "^14.1.1", "rewire": "^4.0.1" }, diff --git a/spec/telemetry.spec.js b/spec/telemetry.spec.js new file mode 100644 index 000000000..f70cbffb4 --- /dev/null +++ b/spec/telemetry.spec.js @@ -0,0 +1,278 @@ +/*! + 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 rewire = require('rewire'); +const Insight = require('insight'); +const mockStdin = require('mock-stdin'); + +describe('telemetry', () => { + let telemetry, insight; + + beforeEach(() => { + telemetry = rewire('../src/telemetry'); + insight = telemetry.__get__('insight'); + + // Prevent any settings from being persisted during testing + insight.config = { + get: jasmine.createSpy('insight.config.get'), + set: jasmine.createSpy('insight.config.set') + }; + + // Prevent tracking anything during testing + spyOn(Insight.prototype, '_save'); + + // Prevent prompts during testing + spyOn(Insight.prototype, 'askPermission'); + }); + + describe('hasUserOptedInOrOut', () => { + it('is false if insight.optOut is unset [T001]', () => { + expect(telemetry.hasUserOptedInOrOut()).toBe(false); + expect(insight.config.get).toHaveBeenCalledWith('optOut'); + }); + + it('is true if insight.optOut is set [T002]', () => { + insight.config.get.and.returnValues( + false, true, 0, 1, '', 'xxx', null + ); + for (let i = 0; i < 7; i++) { + expect(telemetry.hasUserOptedInOrOut()).toBe(true); + } + expect(insight.config.get).toHaveBeenCalledTimes(7); + }); + }); + + describe('isOptedIn', () => { + it('is the inverse of insight.optOut [T003]', () => { + insight.config.get.and.returnValues(false, true); + + expect(telemetry.isOptedIn()).toBe(true); + expect(telemetry.isOptedIn()).toBe(false); + expect(insight.config.get).toHaveBeenCalledTimes(2); + }); + + it('is true if user did not yet decide [T004]', () => { + expect(telemetry.isOptedIn()).toBe(true); + expect(insight.config.get).toHaveBeenCalledWith('optOut'); + }); + }); + + describe('clear', () => { + it('clears telemetry setting [T005]', () => { + telemetry.clear(); + expect(insight.config.set) + .toHaveBeenCalledWith('optOut', undefined); + }); + }); + + describe('turnOn', () => { + it('enables the telemetry setting [T006]', () => { + telemetry.turnOn(); + expect(insight.config.set) + .toHaveBeenCalledWith('optOut', false); + }); + }); + + describe('turnOff', () => { + it('disables the telemetry setting [T007]', () => { + telemetry.turnOff(); + expect(insight.config.set) + .toHaveBeenCalledWith('optOut', true); + }); + }); + + describe('track', () => { + beforeEach(() => { + spyOn(Insight.prototype, 'track'); + }); + + it('calls insight.track [T008]', () => { + telemetry.track(); + expect(insight.track).toHaveBeenCalled(); + }); + + it('passes its arguments to insight.track [T009]', () => { + const args = ['foo', 'bar', 42]; + telemetry.track(...args); + expect(insight.track).toHaveBeenCalledWith(...args); + }); + + // FIXME filtering is currently broken + xit('filters falsy and empty arguments [T010]', () => { + const args = [null, [23], [], 42, '']; + telemetry.track(...args); + expect(insight.track).toHaveBeenCalledWith([23], 42); + }); + }); + + describe('showPrompt', () => { + let response; + + beforeEach(() => { + spyOn(console, 'log'); + spyOn(telemetry, 'track'); + response = Symbol('response'); + insight.askPermission.and.callFake((_, cb) => cb(null, response)); + }); + + it('calls insight.askPermission [T011]', () => { + return telemetry.showPrompt().then(_ => { + expect(insight.askPermission).toHaveBeenCalled(); + }); + }); + + it('returns a promise resolved to the user response [T012]', () => { + return telemetry.showPrompt().then(result => { + expect(result).toBe(response); + }); + }); + + describe('when user opts in', () => { + beforeEach(() => { + response = true; + }); + + it('thanks the user [T013]', () => { + return telemetry.showPrompt().then(_ => { + expect(console.log).toHaveBeenCalledWith( + jasmine.stringMatching(/thanks/i) + ); + }); + }); + + it('tracks the user decision [T014]', () => { + return telemetry.showPrompt().then(_ => { + expect(telemetry.track).toHaveBeenCalledWith( + 'telemetry', 'on', 'via-cli-prompt-choice', 'successful' + ); + }); + }); + }); + + describe('when user declines', () => { + beforeEach(() => { + response = false; + }); + + it('returns a resolved promise if the user response was negative [T015]', () => { + return telemetry.showPrompt().then(result => { + expect(result).toBe(false); + }); + }); + + it('informs the user [T016]', () => { + return telemetry.showPrompt().then(_ => { + expect(console.log).toHaveBeenCalledWith( + jasmine.stringMatching(/opted out of telemetry.* cordova telemetry on/i) + ); + }); + }); + + it('tracks the user decision [T017]', () => { + return telemetry.showPrompt().then(_ => { + expect(telemetry.track).toHaveBeenCalledWith( + 'telemetry', 'off', 'via-cli-prompt-choice', 'successful' + ); + }); + }); + }); + + describe('gory details', () => { + let CI, stdin; + beforeEach(() => { + CI = process.env.CI; + delete process.env.CI; + stdin = mockStdin.stdin(); + insight.askPermission.and.callThrough(); + + // To silence the prompts by insight + spyOn(process.stdout, 'write'); + }); + afterEach(() => { + stdin.restore(); + if (CI !== undefined) { + process.env.CI = CI; + } + }); + + it('saves the user response [T018]', () => { + process.nextTick(_ => stdin.send('y\n')); + return telemetry.showPrompt().then(result => { + expect(result).toBe(true); + expect(insight.config.set) + .toHaveBeenCalledWith('optOut', false); + }); + }); + + it('is counted as a negative response if user does not decide [T019]', () => { + telemetry.timeoutInSecs = 0.01; + return telemetry.showPrompt().then(result => { + expect(result).toBe(false); + expect(insight.config.set) + .toHaveBeenCalledWith('optOut', true); + }); + }); + + it('does NOT show prompt when running on a CI [T020]', () => { + process.env.CI = 1; + return telemetry.showPrompt().then(result => { + expect(result).toBe(false); + expect(insight.config.set).not.toHaveBeenCalled(); + expect(process.stdout.write).not.toHaveBeenCalled(); + }); + }); + }); + }); + describe('insight.track', () => { + it('tracks without user choice [T021]', () => { + insight.track(); + expect(insight._save).toHaveBeenCalled(); + }); + + it('tracks with user consent [T022]', () => { + insight.config.get.and.returnValue(false); + insight.track(); + expect(insight._save).toHaveBeenCalled(); + }); + + it('does NOT track when user opted out [T023]', () => { + insight.config.get.and.returnValue(true); + insight.track(); + expect(insight._save).not.toHaveBeenCalled(); + }); + + describe('on CI', () => { + let CI; + beforeEach(() => { + CI = process.env.CI; + process.env.CI = 1; + }); + afterEach(() => { + if (CI !== undefined) { + process.env.CI = CI; + } + }); + + it('does still track [T024]', () => { + insight.track(); + expect(insight._save).toHaveBeenCalled(); + }); + }); + }); +});