From f5aa730d9d140ea3cdbca16ef1911dfd2b7f4f48 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sat, 4 Jan 2025 22:09:44 +0200 Subject: [PATCH] Add tests --- karma.conf.js | 1 + package-lock.json | 6 +- src/headless/package.json | 2 +- src/headless/plugins/blocking/collection.js | 22 ++-- src/headless/plugins/blocking/plugin.js | 57 +++++++-- .../plugins/blocking/tests/blocking.js | 95 ++++++++++++++ src/headless/plugins/disco/api.js | 8 -- src/headless/plugins/disco/entity.js | 2 +- src/headless/plugins/disco/tests/disco.js | 116 +++++------------- src/headless/plugins/disco/utils.js | 3 + src/headless/plugins/muc/tests/messages.js | 11 +- src/headless/shared/_converse.js | 2 +- .../types/plugins/blocking/collection.d.ts | 21 ++++ .../types/plugins/blocking/index.d.ts | 2 + .../types/plugins/blocking/model.d.ts | 6 + .../types/plugins/blocking/plugin.d.ts | 2 + src/headless/types/plugins/disco/api.d.ts | 5 - src/headless/types/plugins/roster/utils.d.ts | 2 +- src/shared/tests/mock.js | 25 ++-- 19 files changed, 252 insertions(+), 136 deletions(-) create mode 100644 src/headless/plugins/blocking/tests/blocking.js create mode 100644 src/headless/types/plugins/blocking/collection.d.ts create mode 100644 src/headless/types/plugins/blocking/index.d.ts create mode 100644 src/headless/types/plugins/blocking/model.d.ts create mode 100644 src/headless/types/plugins/blocking/plugin.d.ts diff --git a/karma.conf.js b/karma.conf.js index 1ae20e72e4..8b5b2adda3 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -24,6 +24,7 @@ module.exports = function(config) { }, { pattern: "src/shared/tests/mock.js", type: 'module' }, + { pattern: "src/headless/plugins/blocking/tests/blocking.js", type: 'module' }, { pattern: "src/headless/plugins/bookmarks/tests/bookmarks.js", type: 'module' }, { pattern: "src/headless/plugins/bookmarks/tests/deprecated.js", type: 'module' }, { pattern: "src/headless/plugins/caps/tests/caps.js", type: 'module' }, diff --git a/package-lock.json b/package-lock.json index b0bcaf86f5..c3fa29cd84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9868,8 +9868,8 @@ }, "node_modules/strophe.js": { "version": "3.1.0", - "resolved": "git+ssh://git@github.com/strophe/strophejs.git#9b643a43b5cd79f0d8a2e0015e9d2c0d0fec8578", - "integrity": "sha512-/T9ptJEZPy98XaGaFoNfvrzcRQDafRDjSY/Kq3Gk3ilMEmD2oDGslyIdFFYxjAgU6ON6Z1e0Z9CnzgXggqHtFQ==", + "resolved": "git+ssh://git@github.com/strophe/strophejs.git#b4f3369ba07dee4f04750de6709ea8ebe8adf9a5", + "integrity": "sha512-M/T9Pio3eG7GUzVmQUSNg+XzjFwQ6qhzI+Z3uSwUIItxxpRIB8lQB2Afb0L7lbQiRYB7/9tS03GxksdqjfrS5g==", "license": "MIT", "optionalDependencies": { "@types/jsdom": "^21.1.7", @@ -11121,7 +11121,7 @@ "pluggable.js": "3.0.1", "sizzle": "^2.3.5", "sprintf-js": "^1.1.2", - "strophe.js": "strophe/strophejs#9b643a43b5cd79f0d8a2e0015e9d2c0d0fec8578", + "strophe.js": "strophe/strophejs#b4f3369ba07dee4f04750de6709ea8ebe8adf9a5", "urijs": "^1.19.10" }, "devDependencies": {} diff --git a/src/headless/package.json b/src/headless/package.json index 3c61030d40..1a5f7de063 100644 --- a/src/headless/package.json +++ b/src/headless/package.json @@ -42,7 +42,7 @@ "pluggable.js": "3.0.1", "sizzle": "^2.3.5", "sprintf-js": "^1.1.2", - "strophe.js": "strophe/strophejs#9b643a43b5cd79f0d8a2e0015e9d2c0d0fec8578", + "strophe.js": "strophe/strophejs#b4f3369ba07dee4f04750de6709ea8ebe8adf9a5", "urijs": "^1.19.10" }, "devDependencies": {} diff --git a/src/headless/plugins/blocking/collection.js b/src/headless/plugins/blocking/collection.js index a690f3fefe..71ff417862 100644 --- a/src/headless/plugins/blocking/collection.js +++ b/src/headless/plugins/blocking/collection.js @@ -1,9 +1,10 @@ import { getOpenPromise } from '@converse/openpromise'; +import { Collection } from '@converse/skeletor'; import log from '../../log.js'; import _converse from '../../shared/_converse.js'; +import { initStorage } from '../../utils/storage.js'; import api from '../../shared/api/index.js'; import converse from '../../shared/api/public.js'; -import { Collection } from '@converse/skeletor'; import BlockedEntity from './model.js'; const { stx, u } = converse.env; @@ -19,11 +20,13 @@ class Blocklist extends Collection { } async initialize() { - await this.fetchBlocklist(); - const { session } = _converse; const cache_key = `converse.blocklist-${session.get('bare_jid')}`; - this.fetched_flag = cache_key + 'fetched'; + this.fetched_flag = `${cache_key}-fetched`; + initStorage(this, cache_key); + + await this.fetchBlocklist(); + /** * Triggered once the {@link Blocklist} collection * has been created and cached blocklist have been fetched. @@ -69,12 +72,11 @@ class Blocklist extends Collection { * @param {Element} iq */ async onBlocklistReceived(deferred, iq) { - Array.from(iq.querySelectorAll('blocklist item')) - .forEach((item) => { - const jid = item.getAttribute('jid'); - const blocked = this.get(jid); - blocked ? blocked.save({ jid }) : this.create({ jid }); - }); + Array.from(iq.querySelectorAll('blocklist item')).forEach((item) => { + const jid = item.getAttribute('jid'); + const blocked = this.get(jid); + blocked ? blocked.save({ jid }) : this.create({ jid }); + }); window.sessionStorage.setItem(this.fetched_flag, 'true'); if (deferred !== undefined) { diff --git a/src/headless/plugins/blocking/plugin.js b/src/headless/plugins/blocking/plugin.js index 517290b80d..4b17d54bcc 100644 --- a/src/headless/plugins/blocking/plugin.js +++ b/src/headless/plugins/blocking/plugin.js @@ -6,22 +6,63 @@ import _converse from '../../shared/_converse.js'; import api from '../../shared/api/index.js'; import converse from '../../shared/api/public.js'; +import log from '../../log.js'; import Blocklist from './collection.js'; import BlockedEntity from './model.js'; -const { Strophe } = converse.env; +const { Strophe, sizzle } = converse.env; -Strophe.addNamespace('BLOCKING', "urn:xmpp:blocking"); +Strophe.addNamespace('BLOCKING', 'urn:xmpp:blocking'); converse.plugins.add('converse-blocking', { - dependencies: ["converse-disco"], + dependencies: ['converse-disco'], - initialize () { - const exports = { Blocklist, BlockedEntity }; + initialize() { + const exports = { Blocklist, BlockedEntity }; Object.assign(_converse.exports, exports); - api.listen.on('discoInitialized', () => { - _converse.state.blocklist = new _converse.exports.Blocklist(); + api.promises.add(['blocklistInitialized']); + + api.listen.on('connected', () => { + const connection = api.connection.get(); + connection.addHandler( + /** @param {Element} stanza */ (stanza) => { + const bare_jid = _converse.session.get('bare_jid'); + const from = stanza.getAttribute('from'); + if (Strophe.getBareJidFromJid(from ?? bare_jid) != bare_jid) { + log.warn(`Received a blocklist push stanza from a suspicious JID ${from}`); + return true; + } + + sizzle(`block[xmlns="${Strophe.NS.BLOCKING}"] item`, stanza).forEach( + /** @param {Element} item */ (item) => { + const jid = item.getAttribute('jid'); + const { state } = _converse; + state.blocklist?.create({ jid }); + } + ); + return true; + }, + Strophe.NS.BLOCKING, + 'iq', + 'set' + ); + }); + + api.listen.on('clearSession', () => { + const { state } = _converse; + if (state.blocklist) { + state.blocklist.clearStore({ 'silent': true }); + window.sessionStorage.removeItem(state.blocklist.fetched_flag); + delete state.blocklist; + } + }); + + api.listen.on('discoInitialized', async () => { + const domain = _converse.session.get('domain'); + if (await api.disco.supports(Strophe.NS.BLOCKING, domain)) { + _converse.state.blocklist = new _converse.exports.Blocklist(); + } }); - } + }, }); diff --git a/src/headless/plugins/blocking/tests/blocking.js b/src/headless/plugins/blocking/tests/blocking.js new file mode 100644 index 0000000000..01df0ce26a --- /dev/null +++ b/src/headless/plugins/blocking/tests/blocking.js @@ -0,0 +1,95 @@ +/*global mock, converse */ +const { u, stx } = converse.env; + +describe('A block list', function () { + beforeEach(() => { + jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }); + window.sessionStorage.removeItem('converse.blocklist-romeo@montague.lit-fetched'); + }); + + it( + 'is automatically fetched from the server once the user logs in', + mock.initConverse(['discoInitialized'], {}, async function (_converse) { + const { api } = _converse; + await mock.waitUntilDiscoConfirmed( + _converse, + _converse.domain, + [{ 'category': 'server', 'type': 'IM' }], + ['urn:xmpp:blocking'] + ); + await mock.waitForRoster(_converse, 'current', 0); + + const IQ_stanzas = _converse.api.connection.get().IQ_stanzas; + const sent_stanza = await u.waitUntil(() => IQ_stanzas.find((s) => s.querySelector('iq blocklist'))); + + expect(sent_stanza).toEqualStanza(stx` + + + `); + + const stanza = stx` + + + + + + `; + _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); + + const blocklist = await api.waitUntil('blocklistInitialized'); + expect(blocklist.length).toBe(2); + expect(blocklist.models.map((m) => m.get('jid'))).toEqual(['iago@shakespeare.lit', 'juliet@capulet.lit']); + }) + ); + + it( + 'is updated when the server sends IQ stanzas', + mock.initConverse(['discoInitialized'], {}, async function (_converse) { + const { api } = _converse; + await mock.waitUntilDiscoConfirmed( + _converse, + _converse.domain, + [{ 'category': 'server', 'type': 'IM' }], + ['urn:xmpp:blocking'] + ); + await mock.waitForRoster(_converse, 'current', 0); + + const IQ_stanzas = _converse.api.connection.get().IQ_stanzas; + const sent_stanza = await u.waitUntil(() => IQ_stanzas.find((s) => s.querySelector('iq blocklist'))); + + const stanza = stx` + + + + + `; + _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); + + const blocklist = await api.waitUntil('blocklistInitialized'); + expect(blocklist.length).toBe(1); + + // The server sends a push IQ stanza + _converse.api.connection.get()._dataRecv( + mock.createRequest( + stx` + + + + + ` + ) + ); + await u.waitUntil(() => blocklist.length === 2); + expect(blocklist.models.map((m) => m.get('jid'))).toEqual(['iago@shakespeare.lit', 'juliet@capulet.lit']); + }) + ); +}); diff --git a/src/headless/plugins/disco/api.js b/src/headless/plugins/disco/api.js index 797972319f..5254e30191 100644 --- a/src/headless/plugins/disco/api.js +++ b/src/headless/plugins/disco/api.js @@ -382,14 +382,6 @@ export default { return entity.waitUntilFeaturesDiscovered; }, - /** - * @deprecated Use {@link api.disco.refresh} instead. - * @method api.disco.refreshFeatures - */ - refreshFeatures (jid) { - return api.refresh(jid); - }, - /** * Return all the features associated with a disco entity * diff --git a/src/headless/plugins/disco/entity.js b/src/headless/plugins/disco/entity.js index 22f80eff88..a4ed6cd43d 100644 --- a/src/headless/plugins/disco/entity.js +++ b/src/headless/plugins/disco/entity.js @@ -70,7 +70,7 @@ class DiscoEntity extends Model { */ async getFeature (feature) { await this.waitUntilFeaturesDiscovered; - if (this.features.findWhere({ 'var': feature })) { + if (this.features.findWhere({ var: feature })) { return this; } } diff --git a/src/headless/plugins/disco/tests/disco.js b/src/headless/plugins/disco/tests/disco.js index 29c71b7b1a..6e4a39ead7 100644 --- a/src/headless/plugins/disco/tests/disco.js +++ b/src/headless/plugins/disco/tests/disco.js @@ -1,5 +1,7 @@ /*global mock, converse */ +const { u, $iq, stx } = converse.env; + describe("Service Discovery", function () { describe("Whenever a server is queried for its features", function () { @@ -9,7 +11,6 @@ describe("Service Discovery", function () { ['discoInitialized'], {}, async function (_converse) { - const { u, $iq } = converse.env; const IQ_stanzas = _converse.api.connection.get().IQ_stanzas; const IQ_ids = _converse.api.connection.get().IQ_ids; await u.waitUntil(function () { @@ -17,63 +18,27 @@ describe("Service Discovery", function () { return iq.querySelector('iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]'); }).length > 0; }); - /* - * - * - * - * - * - * - * - * - * - * - * - * - * - */ let stanza = IQ_stanzas.find(function (iq) { return iq.querySelector('iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]'); }); const info_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)]; - stanza = $iq({ - 'type': 'result', - 'from': 'montague.lit', - 'to': 'romeo@montague.lit/orchard', - 'id': info_IQ_id - }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'}) - .c('identity', { - 'category': 'server', - 'type': 'im'}).up() - .c('identity', { - 'category': 'conference', - 'type': 'text', - 'name': 'Play-Specific Chatrooms'}).up() - .c('identity', { - 'category': 'directory', - 'type': 'chatroom', - 'name': 'Play-Specific Chatrooms'}).up() - .c('feature', { - 'var': 'http://jabber.org/protocol/disco#info'}).up() - .c('feature', { - 'var': 'http://jabber.org/protocol/disco#items'}).up() - .c('feature', { - 'var': 'jabber:iq:register'}).up() - .c('feature', { - 'var': 'jabber:iq:time'}).up() - .c('feature', { - 'var': 'jabber:iq:version'}); + stanza = stx` + + + + + + + + + + + + `; _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); await u.waitUntil(function () { @@ -111,35 +76,22 @@ describe("Service Discovery", function () { stanza = IQ_stanzas.find(iq => iq.querySelector('iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#items"]')); const items_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)]; - stanza = $iq({ - 'type': 'result', - 'from': 'montague.lit', - 'to': 'romeo@montague.lit/orchard', - 'id': items_IQ_id - }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#items'}) - .c('item', { - 'jid': 'people.shakespeare.lit', - 'name': 'Directory of Characters'}).up() - .c('item', { - 'jid': 'plays.shakespeare.lit', - 'name': 'Play-Specific Chatrooms'}).up() - .c('item', { - 'jid': 'words.shakespeare.lit', - 'name': 'Gateway to Marlowe IM'}).up() - .c('item', { - 'jid': 'montague.lit', - 'node': 'books', - 'name': 'Books by and about Shakespeare'}).up() - .c('item', { - 'node': 'montague.lit', - 'name': 'Wear your literary taste with pride'}).up() - .c('item', { - 'jid': 'montague.lit', - 'node': 'music', - 'name': 'Music from the time of Shakespeare' - }); - _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); + _converse.api.connection.get()._dataRecv(mock.createRequest(stx` + + + + + + + + + + `)); const entities = await _converse.api.disco.entities.get() expect(entities.length).toBe(5); // We have an extra entity, which is the user's JID diff --git a/src/headless/plugins/disco/utils.js b/src/headless/plugins/disco/utils.js index 60813edafd..0c9ebad3a2 100644 --- a/src/headless/plugins/disco/utils.js +++ b/src/headless/plugins/disco/utils.js @@ -7,6 +7,9 @@ import { createStore } from '../../utils/storage.js'; const { Strophe, $iq } = converse.env; +/** + * @param {Element} stanza + */ function onDiscoInfoRequest (stanza) { const node = stanza.getElementsByTagName('query')[0].getAttribute('node'); const attrs = {xmlns: Strophe.NS.DISCO_INFO}; diff --git a/src/headless/plugins/muc/tests/messages.js b/src/headless/plugins/muc/tests/messages.js index 35727d14dd..8edbe2e190 100644 --- a/src/headless/plugins/muc/tests/messages.js +++ b/src/headless/plugins/muc/tests/messages.js @@ -96,8 +96,12 @@ describe("A MUC message", function () { const muc_jid = 'lounge@montague.lit'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const impersonated_jid = `${muc_jid}/alice`; - const received_stanza = u.toStanza(` - + const received_stanza = stx` + Yet I should kill thee with much cherishing. - - `); + `; spyOn(converse.env.log, 'error').and.callThrough(); _converse.api.connection.get()._dataRecv(mock.createRequest(received_stanza)); await u.waitUntil(() => converse.env.log.error.calls.count() === 1); diff --git a/src/headless/shared/_converse.js b/src/headless/shared/_converse.js index 3758f01334..f0620a0fe5 100644 --- a/src/headless/shared/_converse.js +++ b/src/headless/shared/_converse.js @@ -89,7 +89,7 @@ class ConversePrivateGlobal extends EventEmitter(Object) { this.storage = /** @type {Record} */{}; this.promises = { - 'initialized': getOpenPromise(), + initialized: getOpenPromise(), }; this.NUM_PREKEYS = 100; // DEPRECATED. Set here so that tests can override diff --git a/src/headless/types/plugins/blocking/collection.d.ts b/src/headless/types/plugins/blocking/collection.d.ts new file mode 100644 index 0000000000..d710d7f72b --- /dev/null +++ b/src/headless/types/plugins/blocking/collection.d.ts @@ -0,0 +1,21 @@ +export default Blocklist; +declare class Blocklist extends Collection { + constructor(); + get idAttribute(): string; + model: typeof BlockedEntity; + initialize(): Promise; + fetched_flag: string; + fetchBlocklist(): any; + /** + * @param {Object} deferred + */ + fetchBlocklistFromServer(deferred: any): Promise; + /** + * @param {Object} deferred + * @param {Element} iq + */ + onBlocklistReceived(deferred: any, iq: Element): Promise; +} +import { Collection } from '@converse/skeletor'; +import BlockedEntity from './model.js'; +//# sourceMappingURL=collection.d.ts.map \ No newline at end of file diff --git a/src/headless/types/plugins/blocking/index.d.ts b/src/headless/types/plugins/blocking/index.d.ts new file mode 100644 index 0000000000..e26a57a8ca --- /dev/null +++ b/src/headless/types/plugins/blocking/index.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/src/headless/types/plugins/blocking/model.d.ts b/src/headless/types/plugins/blocking/model.d.ts new file mode 100644 index 0000000000..05b0828531 --- /dev/null +++ b/src/headless/types/plugins/blocking/model.d.ts @@ -0,0 +1,6 @@ +export default BlockedEntity; +declare class BlockedEntity extends Model { + getDisplayName(): any; +} +import { Model } from '@converse/skeletor'; +//# sourceMappingURL=model.d.ts.map \ No newline at end of file diff --git a/src/headless/types/plugins/blocking/plugin.d.ts b/src/headless/types/plugins/blocking/plugin.d.ts new file mode 100644 index 0000000000..6d717ab1c9 --- /dev/null +++ b/src/headless/types/plugins/blocking/plugin.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=plugin.d.ts.map \ No newline at end of file diff --git a/src/headless/types/plugins/disco/api.d.ts b/src/headless/types/plugins/disco/api.d.ts index e09947a532..91ec0a0101 100644 --- a/src/headless/types/plugins/disco/api.d.ts +++ b/src/headless/types/plugins/disco/api.d.ts @@ -193,11 +193,6 @@ declare namespace _default { * await api.disco.refresh('room@conference.example.org'); */ export function refresh(jid: string): Promise; - /** - * @deprecated Use {@link api.disco.refresh} instead. - * @method api.disco.refreshFeatures - */ - export function refreshFeatures(jid: any): any; /** * Return all the features associated with a disco entity * diff --git a/src/headless/types/plugins/roster/utils.d.ts b/src/headless/types/plugins/roster/utils.d.ts index 5622581a22..1ccb97557a 100644 --- a/src/headless/types/plugins/roster/utils.d.ts +++ b/src/headless/types/plugins/roster/utils.d.ts @@ -5,7 +5,7 @@ export function unregisterPresenceHandler(): void; export function onClearSession(): Promise; /** * Roster specific event handler for the presencesInitialized event - * @param { Boolean } reconnecting + * @param {Boolean} reconnecting */ export function onPresencesInitialized(reconnecting: boolean): void; /** diff --git a/src/shared/tests/mock.js b/src/shared/tests/mock.js index 34a69ef0f4..94938a53c3 100644 --- a/src/shared/tests/mock.js +++ b/src/shared/tests/mock.js @@ -38,7 +38,6 @@ function initConverse (promise_names=[], settings=null, func) { } document.title = "Converse Tests"; - await _initConverse(settings); await Promise.all((promise_names || []).map(_converse.api.waitUntil)); @@ -57,17 +56,19 @@ function initConverse (promise_names=[], settings=null, func) { async function waitUntilDiscoConfirmed (_converse, entity_jid, identities, features=[], items=[], type='info') { const sel = `iq[to="${entity_jid}"] query[xmlns="http://jabber.org/protocol/disco#${type}"]`; - const iq = await u.waitUntil(() => _converse.api.connection.get().IQ_stanzas.filter(iq => sizzle(sel, iq).length).pop()); - const stanza = $iq({ - 'type': 'result', - 'from': entity_jid, - 'to': 'romeo@montague.lit/orchard', - 'id': iq.getAttribute('id'), - }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#'+type}); - - identities?.forEach(identity => stanza.c('identity', {'category': identity.category, 'type': identity.type}).up()); - features?.forEach(feature => stanza.c('feature', {'var': feature}).up()); - items?.forEach(item => stanza.c('item', {'jid': item}).up()); + const iq = await u.waitUntil(() => _converse.api.connection.get().IQ_stanzas.find(iq => sizzle(sel, iq).length)); + const stanza = stx` + + + ${identities?.map(identity => stx``)} + ${features?.map(feature => stx``)} + ${items?.map(item => stx``)} + + `; _converse.api.connection.get()._dataRecv(createRequest(stanza)); }