diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..94f480de9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf \ No newline at end of file diff --git a/js/background.js b/js/background.js index 1e48dc9b0..f086a83e5 100644 --- a/js/background.js +++ b/js/background.js @@ -448,7 +448,7 @@ try { await ConversationController.load(); - BlockedNumberController.load(); + BlockedNumberController.refresh(); } catch (error) { window.log.error( 'background.js: ConversationController failed to load:', diff --git a/js/blocked_number_controller.js b/js/blocked_number_controller.js index 7c48bf1b6..918a21749 100644 --- a/js/blocked_number_controller.js +++ b/js/blocked_number_controller.js @@ -13,24 +13,13 @@ window.getBlockedNumbers = () => blockedNumbers; window.BlockedNumberController = { - getAll() { - try { - this.load(); - } catch (e) { - window.log.warn(e); - } - return blockedNumbers; - }, reset() { + this.unblockAll(); blockedNumbers.reset([]); }, - load() { + refresh() { window.log.info('BlockedNumberController: starting initial fetch'); - if (blockedNumbers.length) { - throw new Error('BlockedNumberController: Already loaded!'); - } - if (!storage) { throw new Error( 'BlockedNumberController: Could not load blocked numbers' @@ -39,7 +28,7 @@ // Add the numbers to the collection const numbers = storage.getBlockedNumbers(); - blockedNumbers.add(numbers.map(number => ({ number }))); + blockedNumbers.reset(numbers.map(number => ({ number }))); }, block(number) { const ourNumber = textsecure.storage.user.getNumber(); @@ -53,25 +42,22 @@ storage.addBlockedNumber(number); // Make sure we don't add duplicates - if (blockedNumbers.getNumber(number)) return; + if (blockedNumbers.getModel(number)) return; blockedNumbers.add({ number }); }, unblock(number) { storage.removeBlockedNumber(number); - // Make sure we don't add duplicates - const model = blockedNumbers.getNumber(number); + // Remove the model from our collection + const model = blockedNumbers.getModel(number); if (model) { blockedNumbers.remove(model); } }, unblockAll() { - const all = blockedNumbers.models; - all.forEach(number => { - storage.removeBlockedNumber(number); - blockedNumbers.remove(number); - }); + const numbers = blockedNumbers.map(m => m.get('number')); + numbers.forEach(n => this.unblock(n)); }, isBlocked(number) { return storage.isBlocked(number); diff --git a/js/models/blockedNumbers.js b/js/models/blockedNumbers.js index fd07c37e6..b4604c2fc 100644 --- a/js/models/blockedNumbers.js +++ b/js/models/blockedNumbers.js @@ -85,7 +85,7 @@ comparator(m) { return m.get('number'); }, - getNumber(number) { + getModel(number) { return this.models.find(m => m.get('number') === number); }, }); diff --git a/js/models/profile.js b/js/models/profile.js index 096a5b281..6a92504d4 100644 --- a/js/models/profile.js +++ b/js/models/profile.js @@ -17,6 +17,10 @@ }; storage.setProfileName = async newName => { + if (typeof newName !== 'string' && newName !== null) { + throw Error('Name must be a string!'); + } + // Update our profiles accordingly' const trimmed = newName && newName.trim(); diff --git a/js/signal_protocol_store.js b/js/signal_protocol_store.js index d8a9c8d4e..be56da563 100644 --- a/js/signal_protocol_store.js +++ b/js/signal_protocol_store.js @@ -876,7 +876,7 @@ ConversationController.reset(); BlockedNumberController.reset(); await ConversationController.load(); - BlockedNumberController.load(); + BlockedNumberController.refresh(); }, async removeAllConfiguration() { await window.Signal.Data.removeAllConfiguration(); diff --git a/js/views/blocked_number_view.js b/js/views/blocked_number_view.js index 60911dd97..3b8696355 100644 --- a/js/views/blocked_number_view.js +++ b/js/views/blocked_number_view.js @@ -1,4 +1,5 @@ /* global BlockedNumberController: false */ +/* global getBlockedNumbers: false */ /* global Whisper: false */ /* global storage: false */ /* global i18n: false */ @@ -19,7 +20,8 @@ }, initialize() { storage.onready(() => { - this.collection = BlockedNumberController.getAll(); + BlockedNumberController.refresh(); + this.collection = getBlockedNumbers(); this.listView = new Whisper.BlockedNumberListView({ collection: this.collection, }); diff --git a/test/blocked_number_controller_test.js b/test/blocked_number_controller_test.js new file mode 100644 index 000000000..1c904065f --- /dev/null +++ b/test/blocked_number_controller_test.js @@ -0,0 +1,163 @@ +/* global textsecure, BlockedNumberController, storage */ + +'use strict'; + +describe('Blocked Number Controller', () => { + beforeEach(async () => { + // Purge everything manually + const numbers = storage.getBlockedNumbers(); + numbers.forEach(storage.removeBlockedNumber); + window.getBlockedNumbers().reset([]); + }); + + describe('reset', () => { + it('clears blocked numbers', () => { + BlockedNumberController.block('1'); + assert.isNotEmpty(storage.getBlockedNumbers()); + assert.isNotEmpty(window.getBlockedNumbers().models); + + BlockedNumberController.reset(); + assert.isEmpty(storage.getBlockedNumbers()); + assert.isEmpty(window.getBlockedNumbers().models); + }); + }); + + describe('refresh', () => { + it('loads blocked numbers from storage', () => { + BlockedNumberController.refresh(); + assert.isEmpty(window.getBlockedNumbers().models); + + storage.addBlockedNumber('1'); + storage.addBlockedNumber('2'); + BlockedNumberController.refresh(); + + const blocked = window.getBlockedNumbers().map(m => m.get('number')); + assert.lengthOf(blocked, 2); + assert.deepEqual(['1', '2'], blocked.sort()); + }); + + it('overrides old numbers if we refresh again', () => { + BlockedNumberController.refresh(); + assert.isEmpty(window.getBlockedNumbers().models); + + storage.addBlockedNumber('1'); + BlockedNumberController.refresh(); + assert.isNotEmpty( + window.getBlockedNumbers().find(m => m.get('number') === '1') + ); + + storage.removeBlockedNumber('1'); + storage.addBlockedNumber('2'); + BlockedNumberController.refresh(); + assert.isNotEmpty( + window.getBlockedNumbers().find(m => m.get('number') === '2') + ); + }); + + it('throws if storage is invalid', () => { + const _storage = window.storage; + window.storage = null; + assert.throws( + () => BlockedNumberController.refresh(), + 'BlockedNumberController: Could not load blocked numbers' + ); + window.storage = _storage; + }); + }); + + describe('block', () => { + beforeEach(() => { + BlockedNumberController.refresh(); + assert.isEmpty(storage.getBlockedNumbers()); + assert.isEmpty(window.getBlockedNumbers().models); + }); + + it('adds number to the blocked list', () => { + BlockedNumberController.block('1'); + + const numbers = window.getBlockedNumbers().models; + assert.lengthOf(numbers, 1); + assert.strictEqual('1', numbers[0].get('number')); + assert.deepEqual(['1'], storage.getBlockedNumbers()); + }); + + it('only blocks the same number once', () => { + BlockedNumberController.block('2'); + BlockedNumberController.block('2'); + assert.lengthOf(window.getBlockedNumbers().models, 1); + assert.deepEqual(['2'], storage.getBlockedNumbers()); + }); + + it('does not block our own number', () => { + BlockedNumberController.block(textsecure.storage.user.getNumber()); + assert.isEmpty(window.getBlockedNumbers().models); + assert.isEmpty(storage.getBlockedNumbers()); + }); + }); + + describe('unblock', () => { + beforeEach(() => { + BlockedNumberController.refresh(); + assert.isEmpty(storage.getBlockedNumbers()); + assert.isEmpty(window.getBlockedNumbers().models); + }); + + it('removes number from the blocked list', () => { + BlockedNumberController.block('1'); + BlockedNumberController.block('2'); + + assert.lengthOf(window.getBlockedNumbers().models, 2); + assert.lengthOf(storage.getBlockedNumbers(), 2); + + BlockedNumberController.unblock('1'); + + const numbers = window.getBlockedNumbers().models; + assert.lengthOf(numbers, 1); + assert.isEmpty(numbers.filter(n => n.get('number') === '1')); + assert.deepEqual(['2'], storage.getBlockedNumbers()); + }); + + it('removes number from the blocked list even if it is not present in the collection', () => { + BlockedNumberController.block('1'); + BlockedNumberController.block('2'); + window.getBlockedNumbers().reset([]); + + assert.isEmpty(window.getBlockedNumbers().models); + assert.lengthOf(storage.getBlockedNumbers(), 2); + + BlockedNumberController.unblock('1'); + assert.deepEqual(['2'], storage.getBlockedNumbers()); + }); + }); + + describe('unblockAll', () => { + it('removes all our blocked numbers', () => { + BlockedNumberController.refresh(); + + BlockedNumberController.block('1'); + BlockedNumberController.block('2'); + BlockedNumberController.block('3'); + + assert.lengthOf(window.getBlockedNumbers().models, 3); + assert.lengthOf(storage.getBlockedNumbers(), 3); + + BlockedNumberController.unblockAll(); + + assert.lengthOf(window.getBlockedNumbers().models, 0); + assert.lengthOf(storage.getBlockedNumbers(), 0); + }); + }); + + describe('isBlocked', () => { + it('returns whether a number is blocked', () => { + BlockedNumberController.refresh(); + + BlockedNumberController.block('1'); + assert.isOk(BlockedNumberController.isBlocked('1')); + assert.isNotOk(BlockedNumberController.isBlocked('2')); + + BlockedNumberController.unblock('1'); + assert.isNotOk(BlockedNumberController.isBlocked('1')); + }); + }); +}); diff --git a/test/index.html b/test/index.html index 192c6f556..80e223b1c 100644 --- a/test/index.html +++ b/test/index.html @@ -426,8 +426,10 @@ + + diff --git a/test/models/conversations_test.js b/test/models/conversations_test.js index bdec87d16..51bb11aab 100644 --- a/test/models/conversations_test.js +++ b/test/models/conversations_test.js @@ -229,4 +229,4 @@ describe('Conversation', () => { assert.isUndefined(collection.get(convo.id), 'got result for "+"'); }); }); -})(); +}); diff --git a/test/models/profile_test.js b/test/models/profile_test.js new file mode 100644 index 000000000..11112f62c --- /dev/null +++ b/test/models/profile_test.js @@ -0,0 +1,120 @@ +/* global storage */ + +/* eslint no-await-in-loop: 0 */ + +'use strict'; + +const PROFILE_ID = 'local-profile'; + +describe('Profile', () => { + beforeEach(async () => { + await clearDatabase(); + await storage.remove(PROFILE_ID); + }); + + describe('getLocalProfile', () => { + it('returns the local profile', async () => { + const values = [null, 'hello', { a: 'b' }]; + for (let i = 0; i < values.length; i += 1) { + await storage.put(PROFILE_ID, values[i]); + assert.strictEqual(values[i], storage.getLocalProfile()); + } + }); + }); + + describe('saveLocalProfile', () => { + it('saves a profile', async () => { + const values = [null, 'hello', { a: 'b' }]; + for (let i = 0; i < values.length; i += 1) { + await storage.saveLocalProfile(values[i]); + assert.strictEqual(values[i], storage.get(PROFILE_ID)); + } + }); + }); + + describe('removeLocalProfile', () => { + it('removes a profile', async () => { + await storage.saveLocalProfile('a'); + assert.strictEqual('a', storage.getLocalProfile()); + + await storage.removeLocalProfile(); + assert.strictEqual(null, storage.getLocalProfile()); + }); + }); + + describe('setProfileName', () => { + it('throws if a name is not a string', async () => { + const values = [0, { a: 'b' }, [1, 2]]; + for (let i = 0; i < values.length; i += 1) { + try { + await storage.setProfileName(values[i]); + assert.fail( + `setProfileName did not throw an error for ${typeof values[i]}` + ); + } catch (e) { + assert.throws(() => { + throw e; + }, 'Name must be a string!'); + } + } + }); + + it('does not throw if we pass a string or null', async () => { + const values = [null, '1']; + for (let i = 0; i < values.length; i += 1) { + try { + await storage.setProfileName(values[i]); + } catch (e) { + assert.fail('setProfileName threw an error'); + } + } + }); + + it('saves the display name', async () => { + await storage.setProfileName('hi there!'); + + const expected = { + displayName: 'hi there!', + }; + const profile = storage.getLocalProfile(); + assert.exists(profile.name); + assert.deepEqual(expected, profile.name); + }); + + it('saves the display name without overwriting the other profile properties', async () => { + const profile = { title: 'hello' }; + await storage.put(PROFILE_ID, profile); + await storage.setProfileName('hi there!'); + + const expected = { + ...profile, + name: { + displayName: 'hi there!', + }, + }; + assert.deepEqual(expected, storage.getLocalProfile()); + }); + + it('trims the display name', async () => { + await storage.setProfileName(' in middle '); + const profile = storage.getLocalProfile(); + const name = { + displayName: 'in middle', + }; + assert.deepEqual(name, profile.name); + }); + + it('unsets the name property if it is empty', async () => { + const profile = { + name: { + displayName: 'HI THERE!', + }, + }; + await storage.put(PROFILE_ID, profile); + assert.exists(storage.getLocalProfile().name); + + await storage.setProfileName(''); + assert.notExists(storage.getLocalProfile().name); + }); + }); +});