(function (root, factory) { define(["jasmine", "mock", "test-utils"], factory); } (this, function (jasmine, mock, test_utils) { "use strict"; const _ = converse.env._; const Backbone = converse.env.Backbone; const Strophe = converse.env.Strophe; const $iq = converse.env.$iq; const $msg = converse.env.$msg; const moment = converse.env.moment; const u = converse.env.utils; // See: https://xmpp.org/rfcs/rfc3921.html describe("Message Archive Management", function () { // Implement the protocol defined in https://xmpp.org/extensions/xep-0313.html#config describe("Archived Messages", function () { it("aren't shown as duplicates by comparing their stanza id and archive id", mock.initConverse( null, ['discoInitialized'], {}, async function (done, _converse) { await test_utils.openAndEnterChatRoom(_converse, 'trek-radio', 'conference.lightwitch.org', 'jcbrand'); const view = _converse.chatboxviews.get('trek-radio@conference.lightwitch.org'); let stanza = u.toStanza( ` negan `); _converse.connection._dataRecv(test_utils.createRequest(stanza)); await test_utils.waitUntil(() => view.content.querySelectorAll('.chat-msg').length); // Not sure whether such a race-condition might pose a problem // in "real-world" situations. stanza = u.toStanza( ` negan `); spyOn(view.model, 'hasDuplicateArchiveID').and.callThrough(); view.model.onMessage(stanza); await test_utils.waitUntil(() => view.model.hasDuplicateArchiveID.calls.count()); expect(view.model.hasDuplicateArchiveID.calls.count()).toBe(1); const result = await view.model.hasDuplicateArchiveID.calls.all()[0].returnValue expect(result).toBe(true); expect(view.content.querySelectorAll('.chat-msg').length).toBe(1); done(); })); it("aren't shown as duplicates by comparing only their archive id", mock.initConverse( null, ['discoInitialized'], {}, async function (done, _converse) { await test_utils.openAndEnterChatRoom(_converse, 'discuss', 'conference.conversejs.org', 'dummy'); const view = _converse.chatboxviews.get('discuss@conference.conversejs.org'); let stanza = u.toStanza( ` looks like omemo fails completely with "bundle is undefined" when there is a device in the devicelist that has no keys published `); view.model.onMessage(stanza); await test_utils.waitUntil(() => view.content.querySelectorAll('.chat-msg').length); expect(view.content.querySelectorAll('.chat-msg').length).toBe(1); stanza = u.toStanza( ` looks like omemo fails completely with "bundle is undefined" when there is a device in the devicelist that has no keys published `); spyOn(view.model, 'hasDuplicateArchiveID').and.callThrough(); view.model.onMessage(stanza); await test_utils.waitUntil(() => view.model.hasDuplicateArchiveID.calls.count()); expect(view.model.hasDuplicateArchiveID.calls.count()).toBe(1); const result = await view.model.hasDuplicateArchiveID.calls.all()[0].returnValue expect(result).toBe(true); expect(view.content.querySelectorAll('.chat-msg').length).toBe(1); done(); })) }); describe("The archive.query API", function () { it("can be used to query for all archived messages", mock.initConverse( null, ['discoInitialized'], {}, function (done, _converse) { let sent_stanza, IQ_id; const sendIQ = _converse.connection.sendIQ; spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { sent_stanza = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); if (!_converse.disco_entities.get(_converse.domain).features.findWhere({'var': Strophe.NS.MAM})) { _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); } _converse.api.archive.query(); const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); expect(sent_stanza.toString()).toBe( ``); done(); })); it("can be used to query for all messages to/from a particular JID", mock.initConverse( null, [], {}, async function (done, _converse) { const entity = await _converse.api.disco.entities.get(_converse.domain); if (!entity.features.findWhere({'var': Strophe.NS.MAM})) { _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); } let sent_stanza, IQ_id; const sendIQ = _converse.connection.sendIQ; spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { sent_stanza = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); _converse.api.archive.query({'with':'juliet@capulet.lit'}); const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); expect(sent_stanza.toString()).toBe( ``+ ``+ ``+ ``+ `urn:xmpp:mam:2`+ ``+ ``+ `juliet@capulet.lit`+ ``+ ``+ ``+ ``); done(); })); it("can be used to query for archived messages from a chat room", mock.initConverse( null, [], {}, async function (done, _converse) { const entity = await _converse.api.disco.entities.get(_converse.domain); if (!entity.features.findWhere({'var': Strophe.NS.MAM})) { _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); } let sent_stanza, IQ_id; const sendIQ = _converse.connection.sendIQ; spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { sent_stanza = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); const callback = jasmine.createSpy('callback'); _converse.api.archive.query({'with': 'coven@chat.shakespeare.lit', 'groupchat': true}, callback); const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); expect(sent_stanza.toString()).toBe( ``+ ``+ ``+ ``+ `urn:xmpp:mam:2`+ ``+ ``+ ``+ ``); done(); })); it("checks whether returned MAM messages from a MUC room are from the right JID", mock.initConverse( null, [], {}, async function (done, _converse) { const entity = await _converse.api.disco.entities.get(_converse.domain); if (!entity.features.findWhere({'var': Strophe.NS.MAM})) { _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); } let sent_stanza, IQ_id; const sendIQ = _converse.connection.sendIQ; spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { sent_stanza = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); const callback = jasmine.createSpy('callback'); _converse.api.archive.query({'with': 'coven@chat.shakespear.lit', 'groupchat': true, 'max':'10'}, callback); const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); /* * * * * * Thrice the brinded cat hath mew'd. * * * * * * * */ const msg1 = $msg({'id':'iasd207', 'from': 'other@chat.shakespear.lit', 'to': 'dummy@localhost'}) .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'34482-21985-73620'}) .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() .c('message', { 'xmlns':'jabber:client', 'to':'dummy@localhost', 'id':'162BEBB1-F6DB-4D9A-9BD8-CFDCC801A0B2', 'from':'coven@chat.shakespeare.lit/firstwitch', 'type':'groupchat' }) .c('body').t("Thrice the brinded cat hath mew'd."); _converse.connection._dataRecv(test_utils.createRequest(msg1)); /* Send an stanza to indicate the end of the result set. * * * * * 28482-98726-73623 * 09af3-cc343-b409f * 20 * * */ const stanza = $iq({'type': 'result', 'id': IQ_id}) .c('fin', {'xmlns': 'urn:xmpp:mam:2'}) .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) .c('first', {'index': '0'}).t('23452-4534-1').up() .c('last').t('09af3-cc343-b409f').up() .c('count').t('16'); _converse.connection._dataRecv(test_utils.createRequest(stanza)); await test_utils.waitUntil(() => callback.calls.count()); expect(callback).toHaveBeenCalled(); const args = callback.calls.argsFor(0); expect(args[0].length).toBe(0); done(); })); it("can be used to query for all messages in a certain timespan", mock.initConverse( null, [], {}, async function (done, _converse) { let sent_stanza, IQ_id; const sendIQ = _converse.connection.sendIQ; spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { sent_stanza = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); const entities = await _converse.api.disco.entities.get(); if (!entities.get(_converse.domain).features.findWhere({'var': Strophe.NS.MAM})) { _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); } const start = '2010-06-07T00:00:00Z'; const end = '2010-07-07T13:23:54Z'; _converse.api.archive.query({ 'start': start, 'end': end }); const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); expect(sent_stanza.toString()).toBe( ``+ ``+ ``+ ``+ `urn:xmpp:mam:2`+ ``+ ``+ `${moment(start).format()}`+ ``+ ``+ `${moment(end).format()}`+ ``+ ``+ ``+ `` ); done(); })); it("throws a TypeError if an invalid date is provided", mock.initConverse( null, [], {}, async function (done, _converse) { const entity = await _converse.api.disco.entities.get(_converse.domain); if (!entity.features.findWhere({'var': Strophe.NS.MAM})) { _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); } expect(_.partial(_converse.api.archive.query, {'start': 'not a real date'})).toThrow( new TypeError('archive.query: invalid date provided for: start') ); done(); })); it("can be used to query for all messages after a certain time", mock.initConverse( null, [], {}, async function (done, _converse) { const entity = await _converse.api.disco.entities.get(_converse.domain); if (!entity.features.findWhere({'var': Strophe.NS.MAM})) { _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); } let sent_stanza, IQ_id; const sendIQ = _converse.connection.sendIQ; spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { sent_stanza = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); if (!_converse.disco_entities.get(_converse.domain).features.findWhere({'var': Strophe.NS.MAM})) { _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); } const start = '2010-06-07T00:00:00Z'; _converse.api.archive.query({'start': start}); const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); expect(sent_stanza.toString()).toBe( ``+ ``+ ``+ ``+ `urn:xmpp:mam:2`+ ``+ ``+ `${moment(start).format()}`+ ``+ ``+ ``+ `` ); done(); })); it("can be used to query for a limited set of results", mock.initConverse( null, [], {}, async function (done, _converse) { const entity = await _converse.api.disco.entities.get(_converse.domain); if (!entity.features.findWhere({'var': Strophe.NS.MAM})) { _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); } let sent_stanza, IQ_id; const sendIQ = _converse.connection.sendIQ; spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { sent_stanza = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); const start = '2010-06-07T00:00:00Z'; _converse.api.archive.query({'start': start, 'max':10}); const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); expect(sent_stanza.toString()).toBe( ``+ ``+ ``+ ``+ `urn:xmpp:mam:2`+ ``+ ``+ `${moment(start).format()}`+ ``+ ``+ ``+ `10`+ ``+ ``+ `` ); done(); })); it("can be used to page through results", mock.initConverse( null, [], {}, async function (done, _converse) { const entity = await _converse.api.disco.entities.get(_converse.domain); if (!entity.features.findWhere({'var': Strophe.NS.MAM})) { _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); } let sent_stanza, IQ_id; const sendIQ = _converse.connection.sendIQ; spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { sent_stanza = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); const start = '2010-06-07T00:00:00Z'; _converse.api.archive.query({ 'start': start, 'after': '09af3-cc343-b409f', 'max':10 }); const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); expect(sent_stanza.toString()).toBe( ``+ ``+ ``+ ``+ `urn:xmpp:mam:2`+ ``+ ``+ `${moment(start).format()}`+ ``+ ``+ ``+ `10`+ `09af3-cc343-b409f`+ ``+ ``+ ``); done(); })); it("accepts \"before\" with an empty string as value to reverse the order", mock.initConverse( null, [], {}, async function (done, _converse) { const entity = await _converse.api.disco.entities.get(_converse.domain); if (!entity.features.findWhere({'var': Strophe.NS.MAM})) { _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); } let sent_stanza, IQ_id; const sendIQ = _converse.connection.sendIQ; spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { sent_stanza = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); _converse.api.archive.query({'before': '', 'max':10}); const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); expect(sent_stanza.toString()).toBe( ``+ ``+ ``+ ``+ `urn:xmpp:mam:2`+ ``+ ``+ ``+ `10`+ ``+ ``+ ``+ ``); done(); })); it("accepts a Strophe.RSM object for the query options", mock.initConverse( null, [], {}, async function (done, _converse) { const entity = await _converse.api.disco.entities.get(_converse.domain); if (!entity.features.findWhere({'var': Strophe.NS.MAM})) { _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); } let sent_stanza, IQ_id; const sendIQ = _converse.connection.sendIQ; spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { sent_stanza = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); // Normally the user wouldn't manually make a Strophe.RSM object // and pass it in. However, in the callback method an RSM object is // returned which can be reused for easy paging. This test is // more for that usecase. const rsm = new Strophe.RSM({'max': '10'}); rsm['with'] = 'romeo@montague.lit'; // eslint-disable-line dot-notation rsm.start = '2010-06-07T00:00:00Z'; _converse.api.archive.query(rsm); const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); expect(sent_stanza.toString()).toBe( ``+ ``+ ``+ ``+ `urn:xmpp:mam:2`+ ``+ ``+ `romeo@montague.lit`+ ``+ ``+ `${moment(rsm.start).format()}`+ ``+ ``+ ``+ `10`+ ``+ ``+ ``); done(); })); it("accepts a callback function, which it passes the messages and a Strophe.RSM object", mock.initConverse( null, [], {}, async function (done, _converse) { const entity = await _converse.api.disco.entities.get(_converse.domain); if (!entity.features.findWhere({'var': Strophe.NS.MAM})) { _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); } let sent_stanza, IQ_id; const sendIQ = _converse.connection.sendIQ; spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { sent_stanza = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); const callback = jasmine.createSpy('callback'); _converse.api.archive.query({'with': 'romeo@capulet.lit', 'max':'10'}, callback); const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); /* * * * * * Call me but love, and I'll be new baptized; Henceforth I never will be Romeo. * * * * */ const msg1 = $msg({'id':'aeb213', 'to':'juliet@capulet.lit/chamber'}) .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'28482-98726-73623'}) .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() .c('message', { 'xmlns':'jabber:client', 'to':'juliet@capulet.lit/balcony', 'from':'romeo@montague.lit/orchard', 'type':'chat' }) .c('body').t("Call me but love, and I'll be new baptized;"); _converse.connection._dataRecv(test_utils.createRequest(msg1)); const msg2 = $msg({'id':'aeb213', 'to':'juliet@capulet.lit/chamber'}) .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'28482-98726-73624'}) .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() .c('message', { 'xmlns':'jabber:client', 'to':'juliet@capulet.lit/balcony', 'from':'romeo@montague.lit/orchard', 'type':'chat' }) .c('body').t("Henceforth I never will be Romeo."); _converse.connection._dataRecv(test_utils.createRequest(msg2)); /* Send an stanza to indicate the end of the result set. * * * * * 28482-98726-73623 * 09af3-cc343-b409f * 20 * * */ const stanza = $iq({'type': 'result', 'id': IQ_id}) .c('fin', {'xmlns': 'urn:xmpp:mam:2'}) .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) .c('first', {'index': '0'}).t('23452-4534-1').up() .c('last').t('09af3-cc343-b409f').up() .c('count').t('16'); _converse.connection._dataRecv(test_utils.createRequest(stanza)); await test_utils.waitUntil(() => callback.calls.count()); expect(callback).toHaveBeenCalled(); const args = callback.calls.argsFor(0); expect(args[0].length).toBe(2); expect(args[0][0].outerHTML).toBe(msg1.nodeTree.outerHTML); expect(args[0][1].outerHTML).toBe(msg2.nodeTree.outerHTML); expect(args[1]['with']).toBe('romeo@capulet.lit'); // eslint-disable-line dot-notation expect(args[1].max).toBe('10'); expect(args[1].count).toBe('16'); expect(args[1].first).toBe('23452-4534-1'); expect(args[1].last).toBe('09af3-cc343-b409f'); done() })); }); describe("The default preference", function () { it("is set once server support for MAM has been confirmed", mock.initConverse( null, [], {}, async function (done, _converse) { const entity = await _converse.api.disco.entities.get(_converse.domain); let sent_stanza, IQ_id; const sendIQ = _converse.connection.sendIQ; spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { sent_stanza = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); spyOn(_converse, 'onMAMPreferences').and.callThrough(); _converse.message_archiving = 'never'; const feature = new Backbone.Model({ 'var': Strophe.NS.MAM }); spyOn(feature, 'save').and.callFake(feature.set); // Save will complain about a url not being set entity.onFeatureAdded(feature); expect(_converse.connection.sendIQ).toHaveBeenCalled(); expect(sent_stanza.toLocaleString()).toBe( ``+ ``+ ``); /* Example 20. Server responds with current preferences * * * * * * * */ let stanza = $iq({'type': 'result', 'id': IQ_id}) .c('prefs', {'xmlns': Strophe.NS.MAM, 'default':'roster'}) .c('always').c('jid').t('romeo@montague.lit').up().up() .c('never').c('jid').t('montague@montague.lit'); _converse.connection._dataRecv(test_utils.createRequest(stanza)); await test_utils.waitUntil(() => _converse.onMAMPreferences.calls.count()); expect(_converse.onMAMPreferences).toHaveBeenCalled(); expect(sent_stanza.toString()).toBe( ``+ ``+ `romeo@montague.lit`+ `montague@montague.lit`+ ``+ `` ); expect(feature.get('preference')).toBe(undefined); /* * * * romeo@montague.lit * * * montague@montague.lit * * * */ stanza = $iq({'type': 'result', 'id': IQ_id}) .c('prefs', {'xmlns': Strophe.NS.MAM, 'default':'always'}) .c('always').up() .c('never'); _converse.connection._dataRecv(test_utils.createRequest(stanza)); await test_utils.waitUntil(() => feature.save.calls.count()); expect(feature.save).toHaveBeenCalled(); expect(feature.get('preferences')['default']).toBe('never'); // eslint-disable-line dot-notation done(); })); }); }); }));