Bladeren bron

Fix disco hierarchy

Previously we kept all entities and their items (which are also
instances of _converse.DiscoEntity) in a flat array.

Instead, we should have a tree-like structure where items are stored
on the relevant entity (and recursively on other items).
JC Brand 7 jaren geleden
bovenliggende
commit
a9d2881888
11 gewijzigde bestanden met toevoegingen van 919 en 738 verwijderingen
  1. 1 1
      .eslintrc.json
  2. 93 85
      spec/disco.js
  3. 155 118
      spec/http-file-upload.js
  4. 535 449
      spec/mam.js
  5. 1 1
      src/converse-bookmarks.js
  6. 1 2
      src/converse-chatview.js
  7. 78 60
      src/converse-disco.js
  8. 34 2
      src/converse-http-file-upload.js
  9. 3 3
      src/converse-mam.js
  10. 1 1
      src/converse-vcard.js
  11. 17 16
      src/converse.js

+ 1 - 1
.eslintrc.json

@@ -30,7 +30,7 @@
         "accessor-pairs": "error",
         "array-bracket-spacing": "off",
         "array-callback-return": "error",
-        "arrow-body-style": "error",
+        "arrow-body-style": "off",
         "arrow-parens": "error",
         "arrow-spacing": "error",
         "block-scoped-var": "off",

+ 93 - 85
spec/disco.js

@@ -20,7 +20,7 @@
                 var IQ_ids =  _converse.connection.IQ_ids;
                 test_utils.waitUntil(function () {
                     return _.filter(IQ_stanzas, function (iq) {
-                        return iq.nodeTree.querySelector('query[xmlns="http://jabber.org/protocol/disco#info"]');
+                        return iq.nodeTree.querySelector('iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
                     }).length > 0;
                 }, 300).then(function () {
                     /* <iq type='result'
@@ -49,8 +49,11 @@
                      *  </query>
                      *  </iq>
                      */
-                    var info_IQ_id = IQ_ids[0];
-                    var stanza = $iq({
+                    var stanza = _.filter(IQ_stanzas, function (iq) {
+                        return iq.nodeTree.querySelector('iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
+                    })[0];
+                    var info_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
+                    stanza = $iq({
                         'type': 'result',
                         'from': 'localhost',
                         'to': 'dummy@localhost/resource',
@@ -79,92 +82,97 @@
                             'var': 'jabber:iq:version'});
                     _converse.connection._dataRecv(test_utils.createRequest(stanza));
 
-                    var entities = _converse.disco_entities;
-                    expect(entities.length).toBe(2); // We have an extra entity, which is the user's JID
-                    expect(entities.get(_converse.domain).features.length).toBe(5);
-                    expect(entities.get(_converse.domain).identities.length).toBe(3);
-                    expect(entities.get('localhost').features.where({'var': 'jabber:iq:version'}).length).toBe(1);
-                    expect(entities.get('localhost').features.where({'var': 'jabber:iq:time'}).length).toBe(1);
-                    expect(entities.get('localhost').features.where({'var': 'jabber:iq:register'}).length).toBe(1);
-                    expect(entities.get('localhost').features.where(
-                        {'var': 'http://jabber.org/protocol/disco#items'}).length).toBe(1);
-                    expect(entities.get('localhost').features.where(
-                        {'var': 'http://jabber.org/protocol/disco#info'}).length).toBe(1);
+                    _converse.api.disco.entities.get().then(function (entities) {
+                        expect(entities.length).toBe(2); // We have an extra entity, which is the user's JID
+                        expect(entities.get(_converse.domain).features.length).toBe(5);
+                        expect(entities.get(_converse.domain).identities.length).toBe(3);
+                        expect(entities.get('localhost').features.where({'var': 'jabber:iq:version'}).length).toBe(1);
+                        expect(entities.get('localhost').features.where({'var': 'jabber:iq:time'}).length).toBe(1);
+                        expect(entities.get('localhost').features.where({'var': 'jabber:iq:register'}).length).toBe(1);
+                        expect(entities.get('localhost').features.where(
+                            {'var': 'http://jabber.org/protocol/disco#items'}).length).toBe(1);
+                        expect(entities.get('localhost').features.where(
+                            {'var': 'http://jabber.org/protocol/disco#info'}).length).toBe(1);
 
 
-                test_utils.waitUntil(function () {
-                    // Converse.js sees that the entity has a disco#items feature,
-                    // so it will make a query for it.
-                    return _.filter(IQ_stanzas, function (iq) {
-                        return iq.nodeTree.querySelector('query[xmlns="http://jabber.org/protocol/disco#items"]');
-                    }).length > 0;
-                }, 300).then(function () {
-                    /* <iq type='result'
-                     *     from='catalog.shakespeare.lit'
-                     *     to='romeo@montague.net/orchard'
-                     *     id='items2'>
-                     * <query xmlns='http://jabber.org/protocol/disco#items'>
-                     *     <item jid='people.shakespeare.lit'
-                     *         name='Directory of Characters'/>
-                     *     <item jid='plays.shakespeare.lit'
-                     *         name='Play-Specific Chatrooms'/>
-                     *     <item jid='mim.shakespeare.lit'
-                     *         name='Gateway to Marlowe IM'/>
-                     *     <item jid='words.shakespeare.lit'
-                     *         name='Shakespearean Lexicon'/>
-                     *
-                     *     <item jid='catalog.shakespeare.lit'
-                     *         node='books'
-                     *         name='Books by and about Shakespeare'/>
-                     *     <item jid='catalog.shakespeare.lit'
-                     *         node='clothing'
-                     *         name='Wear your literary taste with pride'/>
-                     *     <item jid='catalog.shakespeare.lit'
-                     *         node='music'
-                     *         name='Music from the time of Shakespeare'/>
-                     * </query>
-                     * </iq>
-                     */
-                   var items_IQ_id = IQ_ids.pop();
-                   stanza = $iq({
-                       'type': 'result',
-                       'from': 'localhost',
-                       'to': 'dummy@localhost/resource',
-                       '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': 'localhost',
-                           'name': 'Shakespearean Lexicon'}).up()
+                        test_utils.waitUntil(function () {
+                            // Converse.js sees that the entity has a disco#items feature,
+                            // so it will make a query for it.
+                            return _.filter(IQ_stanzas, function (iq) {
+                                return iq.nodeTree.querySelector('query[xmlns="http://jabber.org/protocol/disco#items"]');
+                            }).length > 0;
+                        }, 300).then(function () {
+                            /* <iq type='result'
+                             *     from='catalog.shakespeare.lit'
+                             *     to='romeo@montague.net/orchard'
+                             *     id='items2'>
+                             * <query xmlns='http://jabber.org/protocol/disco#items'>
+                             *     <item jid='people.shakespeare.lit'
+                             *         name='Directory of Characters'/>
+                             *     <item jid='plays.shakespeare.lit'
+                             *         name='Play-Specific Chatrooms'/>
+                             *     <item jid='mim.shakespeare.lit'
+                             *         name='Gateway to Marlowe IM'/>
+                             *     <item jid='words.shakespeare.lit'
+                             *         name='Shakespearean Lexicon'/>
+                             *
+                             *     <item jid='catalog.shakespeare.lit'
+                             *         node='books'
+                             *         name='Books by and about Shakespeare'/>
+                             *     <item jid='catalog.shakespeare.lit'
+                             *         node='clothing'
+                             *         name='Wear your literary taste with pride'/>
+                             *     <item jid='catalog.shakespeare.lit'
+                             *         node='music'
+                             *         name='Music from the time of Shakespeare'/>
+                             * </query>
+                             * </iq>
+                             */
+                            var stanza = _.filter(IQ_stanzas, function (iq) {
+                                return iq.nodeTree.querySelector('iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#items"]');
+                            })[0];
+                            var items_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
+                            stanza = $iq({
+                                'type': 'result',
+                                'from': 'localhost',
+                                'to': 'dummy@localhost/resource',
+                                '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': 'localhost',
-                           'node': 'books',
-                           'name': 'Books by and about Shakespeare'}).up()
-                       .c('item', {
-                           'node': 'localhost',
-                           'name': 'Wear your literary taste with pride'}).up()
-                       .c('item', {
-                           'jid': 'localhost',
-                           'node': 'music',
-                           'name': 'Music from the time of Shakespeare'
-                       });
-                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                                .c('item', {
+                                    'jid': 'localhost',
+                                    'node': 'books',
+                                    'name': 'Books by and about Shakespeare'}).up()
+                                .c('item', {
+                                    'node': 'localhost',
+                                    'name': 'Wear your literary taste with pride'}).up()
+                                .c('item', {
+                                    'jid': 'localhost',
+                                    'node': 'music',
+                                    'name': 'Music from the time of Shakespeare'
+                                });
+                            _converse.connection._dataRecv(test_utils.createRequest(stanza));
 
-                    entities = _converse.disco_entities;
-                    expect(entities.length).toBe(5); // We have an extra entity, which is the user's JID
-                    expect(entities.get(_converse.domain).identities.where({'category': 'conference'}).length).toBe(1);
-                    expect(entities.get(_converse.domain).identities.where({'category': 'directory'}).length).toBe(1);
-                    done();
-                });
+                            entities = _converse.disco_entities;
+                            expect(entities.length).toBe(2); // We have an extra entity, which is the user's JID
+                            expect(entities.get(_converse.domain).items.length).toBe(3);
+                            expect(_.includes(entities.get(_converse.domain).items.pluck('jid'), 'people.shakespeare.lit')).toBeTruthy();
+                            expect(_.includes(entities.get(_converse.domain).items.pluck('jid'), 'plays.shakespeare.lit')).toBeTruthy();
+                            expect(_.includes(entities.get(_converse.domain).items.pluck('jid'), 'words.shakespeare.lit')).toBeTruthy();
+                            expect(entities.get(_converse.domain).identities.where({'category': 'conference'}).length).toBe(1);
+                            expect(entities.get(_converse.domain).identities.where({'category': 'directory'}).length).toBe(1);
+                            done();
+                        });
+                    });
                 });
             }));
         });

+ 155 - 118
spec/http-file-upload.js

@@ -10,6 +10,7 @@
     var Strophe = converse.env.Strophe;
     var $iq = converse.env.$iq;
     var _ = converse.env._;
+    var f = converse.env.f;
 
     describe("XEP-0363: HTTP File Upload", function () {
 
@@ -18,135 +19,171 @@
             it("is done automatically", mock.initConverseWithAsync(function (done, _converse) {
                 var IQ_stanzas = _converse.connection.IQ_stanzas;
                 var IQ_ids =  _converse.connection.IQ_ids;
-                test_utils.waitUntil(function () {
-                    return _.filter(IQ_stanzas, function (iq) {
-                        return iq.nodeTree.querySelector('query[xmlns="http://jabber.org/protocol/disco#info"]');
-                    }).length > 0;
-                }, 300).then(function () {
-                    /* <iq type='result'
-                     *      from='plays.shakespeare.lit'
-                     *      to='romeo@montague.net/orchard'
-                     *      id='info1'>
-                     *  <query xmlns='http://jabber.org/protocol/disco#info'>
-                     *      <identity
-                     *          category='server'
-                     *          type='im'/>
-                     *      <feature var='http://jabber.org/protocol/disco#info'/>
-                     *      <feature var='http://jabber.org/protocol/disco#items'/>
-                     *  </query>
-                     *  </iq>
-                     */
-                    var info_IQ_id = IQ_ids[0];
-                    var stanza = $iq({
-                        'type': 'result',
-                        'from': 'localhost',
-                        'to': 'dummy@localhost/resource',
-                        'id': info_IQ_id
-                    }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'})
-                        .c('identity', {
-                            'category': 'server',
-                            'type': 'im'}).up()
-                        .c('feature', {
-                            'var': 'http://jabber.org/protocol/disco#info'}).up()
-                        .c('feature', {
-                            'var': 'http://jabber.org/protocol/disco#items'});
-                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
 
-                    var entities = _converse.disco_entities;
-                    expect(entities.length).toBe(2); // We have an extra entity, which is the user's JID
-                    expect(entities.get(_converse.domain).features.length).toBe(2);
-                    expect(entities.get(_converse.domain).identities.length).toBe(1);
-
-                    return test_utils.waitUntil(function () {
-                        // Converse.js sees that the entity has a disco#items feature,
-                        // so it will make a query for it.
+                test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], []).then(function () {
+                    test_utils.waitUntil(function () {
                         return _.filter(IQ_stanzas, function (iq) {
-                            return iq.nodeTree.querySelector('query[xmlns="http://jabber.org/protocol/disco#items"]');
+                            return iq.nodeTree.querySelector('iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
                         }).length > 0;
-                    }, 300);
-                }).then(function () {
-                    /* <iq from='montague.tld'
-                     *      id='step_01'
-                     *      to='romeo@montague.tld/garden'
-                     *      type='result'>
-                     *  <query xmlns='http://jabber.org/protocol/disco#items'>
-                     *      <item jid='upload.montague.tld' name='HTTP File Upload' />
-                     *      <item jid='conference.montague.tld' name='Chatroom Service' />
-                     *  </query>
-                     *  </iq>
-                     */
-                   var items_IQ_id = IQ_ids[IQ_ids.length-1];
-                   var stanza = $iq({
-                       'type': 'result',
-                       'from': 'localhost',
-                       'to': 'dummy@localhost/resource',
-                       'id': items_IQ_id
-                   }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#items'})
-                       .c('item', {
-                           'jid': 'upload.localhost',
-                           'name': 'HTTP File Upload'}).up()
-                       .c('item', {
-                           'jid': 'conference.localhost',
-                           'name': 'Chatrooms Service'});
-                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                    }, 300).then(function () {
+                        /* <iq type='result'
+                         *      from='plays.shakespeare.lit'
+                         *      to='romeo@montague.net/orchard'
+                         *      id='info1'>
+                         *  <query xmlns='http://jabber.org/protocol/disco#info'>
+                         *      <identity
+                         *          category='server'
+                         *          type='im'/>
+                         *      <feature var='http://jabber.org/protocol/disco#info'/>
+                         *      <feature var='http://jabber.org/protocol/disco#items'/>
+                         *  </query>
+                         *  </iq>
+                         */
+                        var stanza = _.filter(IQ_stanzas, function (iq) {
+                            return iq.nodeTree.querySelector('iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
+                        })[0];
+                        var info_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
 
-                    var entities = _converse.disco_entities;
-                    expect(entities.length).toBe(4); // We have an extra entity, which is the user's JID
+                        stanza = $iq({
+                            'type': 'result',
+                            'from': 'localhost',
+                            'to': 'dummy@localhost/resource',
+                            'id': info_IQ_id
+                        }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'})
+                            .c('identity', {
+                                'category': 'server',
+                                'type': 'im'}).up()
+                            .c('feature', {
+                                'var': 'http://jabber.org/protocol/disco#info'}).up()
+                            .c('feature', {
+                                'var': 'http://jabber.org/protocol/disco#items'});
+                        _converse.connection._dataRecv(test_utils.createRequest(stanza));
 
-                    return test_utils.waitUntil(function () {
-                        // Converse.js sees that the entity has a disco#items feature,
-                        // so it will make a query for it.
-                        return _.filter(IQ_stanzas, function (iq) {
-                            return iq.nodeTree.querySelector('iq[to="upload.localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
-                        }).length > 0;
-                    }, 300);
-                }).then(function () {
+                        _converse.api.disco.entities.get().then(function(entities) {
+                            expect(entities.length).toBe(2);
+                            expect(_.includes(entities.pluck('jid'), 'localhost')).toBe(true);
+                            expect(_.includes(entities.pluck('jid'), 'dummy@localhost')).toBe(true);
+
+                            expect(entities.get(_converse.domain).features.length).toBe(2);
+                            expect(entities.get(_converse.domain).identities.length).toBe(1);
+
+                            return test_utils.waitUntil(function () {
+                                // Converse.js sees that the entity has a disco#items feature,
+                                // so it will make a query for it.
+                                return _.filter(IQ_stanzas, function (iq) {
+                                    return iq.nodeTree.querySelector('iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#items"]');
+                                }).length > 0;
+                            }, 300);
+                        });
+                    }).then(function () {
+                        /* <iq from='montague.tld'
+                         *      id='step_01'
+                         *      to='romeo@montague.tld/garden'
+                         *      type='result'>
+                         *  <query xmlns='http://jabber.org/protocol/disco#items'>
+                         *      <item jid='upload.montague.tld' name='HTTP File Upload' />
+                         *      <item jid='conference.montague.tld' name='Chatroom Service' />
+                         *  </query>
+                         *  </iq>
+                         */
                     var stanza = _.filter(IQ_stanzas, function (iq) {
-                        return iq.nodeTree.querySelector('iq[to="upload.localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
+                        return iq.nodeTree.querySelector('iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#items"]');
                     })[0];
-                    var IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
-                    expect(stanza.toLocaleString()).toBe(
-                        "<iq from='dummy@localhost/resource' to='upload.localhost' type='get' xmlns='jabber:client' id='"+IQ_id+"'>"+
-                            "<query xmlns='http://jabber.org/protocol/disco#info'/>"+
-                        "</iq>");
+                    var items_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
+                    stanza = $iq({
+                        'type': 'result',
+                        'from': 'localhost',
+                        'to': 'dummy@localhost/resource',
+                        'id': items_IQ_id
+                    }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#items'})
+                        .c('item', {
+                            'jid': 'upload.localhost',
+                            'name': 'HTTP File Upload'});
+                        _converse.connection._dataRecv(test_utils.createRequest(stanza));
 
-                    // Upload service responds and reports a maximum file size of 5MiB
-                    /* <iq from='upload.montague.tld'
-                     *     id='step_02'
-                     *     to='romeo@montague.tld/garden'
-                     *     type='result'>
-                     * <query xmlns='http://jabber.org/protocol/disco#info'>
-                     *     <identity category='store'
-                     *             type='file'
-                     *             name='HTTP File Upload' />
-                     *     <feature var='urn:xmpp:http:upload:0' />
-                     *     <x type='result' xmlns='jabber:x:data'>
-                     *     <field var='FORM_TYPE' type='hidden'>
-                     *         <value>urn:xmpp:http:upload:0</value>
-                     *     </field>
-                     *     <field var='max-file-size'>
-                     *         <value>5242880</value>
-                     *     </field>
-                     *     </x>
-                     * </query>
-                     * </iq>
-                     */
-                    stanza = $iq({'type': 'result', 'to': 'dummy@localhost/resource', 'id': IQ_id, 'from': 'upload.localhost'})
-                        .c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'})
-                            .c('identity', {'category':'store', 'type':'file', 'name':'HTTP File Upload'}).up()
-                            .c('feature', {'var':'urn:xmpp:http:upload:0'}).up()
-                            .c('x', {'type':'result', 'xmlns':'jabber:x:data'})
-                                .c('field', {'var':'FORM_TYPE', 'type':'hidden'})
-                                    .c('value').t('urn:xmpp:http:upload:0').up().up()
-                                .c('field', {'var':'max-file-size'})
-                                    .c('value').t('5242880');
-                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                        _converse.api.disco.entities.get().then(function (entities) {
+                            expect(entities.length).toBe(2);
+                            expect(entities.get('localhost').items.length).toBe(1);
+                            return test_utils.waitUntil(function () {
+                                // Converse.js sees that the entity has a disco#info feature,
+                                // so it will make a query for it.
+                                return _.filter(IQ_stanzas, function (iq) {
+                                    return iq.nodeTree.querySelector('iq[to="upload.localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
+                                }).length > 0;
+                            }, 300);
+                        });
+                    }).then(function () {
+                        var stanza = _.filter(IQ_stanzas, function (iq) {
+                            return iq.nodeTree.querySelector('iq[to="upload.localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
+                        })[0];
+                        var IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
+                        expect(stanza.toLocaleString()).toBe(
+                            "<iq from='dummy@localhost/resource' to='upload.localhost' type='get' xmlns='jabber:client' id='"+IQ_id+"'>"+
+                                "<query xmlns='http://jabber.org/protocol/disco#info'/>"+
+                            "</iq>");
 
-                    var entities = _converse.disco_entities;
-                    expect(entities.get('upload.localhost').identities.where({'category': 'store'}).length).toBe(1);
-                    done();
+                        // Upload service responds and reports a maximum file size of 5MiB
+                        /* <iq from='upload.montague.tld'
+                         *     id='step_02'
+                         *     to='romeo@montague.tld/garden'
+                         *     type='result'>
+                         * <query xmlns='http://jabber.org/protocol/disco#info'>
+                         *     <identity category='store'
+                         *             type='file'
+                         *             name='HTTP File Upload' />
+                         *     <feature var='urn:xmpp:http:upload:0' />
+                         *     <x type='result' xmlns='jabber:x:data'>
+                         *     <field var='FORM_TYPE' type='hidden'>
+                         *         <value>urn:xmpp:http:upload:0</value>
+                         *     </field>
+                         *     <field var='max-file-size'>
+                         *         <value>5242880</value>
+                         *     </field>
+                         *     </x>
+                         * </query>
+                         * </iq>
+                         */
+                        stanza = $iq({'type': 'result', 'to': 'dummy@localhost/resource', 'id': IQ_id, 'from': 'upload.localhost'})
+                            .c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'})
+                                .c('identity', {'category':'store', 'type':'file', 'name':'HTTP File Upload'}).up()
+                                .c('feature', {'var':'urn:xmpp:http:upload:0'}).up()
+                                .c('x', {'type':'result', 'xmlns':'jabber:x:data'})
+                                    .c('field', {'var':'FORM_TYPE', 'type':'hidden'})
+                                        .c('value').t('urn:xmpp:http:upload:0').up().up()
+                                    .c('field', {'var':'max-file-size'})
+                                        .c('value').t('5242880');
+                        _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+                        _converse.api.disco.entities.get().then(function (entities) {
+                            expect(entities.get('localhost').items.get('upload.localhost').identities.where({'category': 'store'}).length).toBe(1);
+                            _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain).then(
+                                function (result) {
+                                    expect(result.length).toBe(1);
+                                    expect(result[0].get('jid')).toBe('upload.localhost');
+                                    done();
+                                }
+                            );
+                        }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+                    })
                 })
             }));
         });
+
+        describe("When supported", function () {
+
+            describe("A file upload toolbar button", function () {
+
+                it("appears in private chats", mock.initConverseWithAsync(function (done, _converse) {
+                    test_utils.createContacts(_converse, 'current');
+                    var contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost';
+                    test_utils.openChatBoxFor(_converse, contact_jid);
+                    done();
+                }));
+
+                it("appears in MUC chats", mock.initConverseWithAsync(function (done, _converse) {
+                    done();
+                }));
+            });
+        });
     });
 }));

+ 535 - 449
spec/mam.js

@@ -69,507 +69,593 @@
                 done();
             }));
 
-           it("can be used to query for all messages to/from a particular JID", mock.initConverse(function (_converse) {
-                var sent_stanza, IQ_id;
-                var 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({'with':'juliet@capulet.lit'});
-                var queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
-                expect(sent_stanza.toString()).toBe(
-                    "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
-                        "<query xmlns='urn:xmpp:mam:2' queryid='"+queryid+"'>"+
-                            "<x xmlns='jabber:x:data' type='submit'>"+
-                            "<field var='FORM_TYPE' type='hidden'>"+
-                                "<value>urn:xmpp:mam:2</value>"+
-                            "</field>"+
-                            "<field var='with'>"+
-                                "<value>juliet@capulet.lit</value>"+
-                            "</field>"+
-                            "</x>"+
-                        "</query>"+
-                    "</iq>"
-                );
-            }));
-
-           it("can be used to query for archived messages from a chat room", mock.initConverse(function (_converse) {
-                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});
-                }
-                var sent_stanza, IQ_id;
-                var 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);
-                });
-                var callback = jasmine.createSpy('callback');
-
-                _converse.api.archive.query({'with': 'coven@chat.shakespeare.lit', 'groupchat': true}, callback);
-                var queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
-
-                expect(sent_stanza.toString()).toBe(
-                    "<iq type='set' to='coven@chat.shakespeare.lit' xmlns='jabber:client' id='"+IQ_id+"'>"+
-                        "<query xmlns='urn:xmpp:mam:2' queryid='"+queryid+"'>"+
-                            "<x xmlns='jabber:x:data' type='submit'>"+
+           it("can be used to query for all messages to/from a particular JID",
+                    mock.initConverseWithPromises(
+                        null, [], {},
+                        function (done, _converse) {
+
+                _converse.api.disco.entities.get(_converse.domain).then(function (entity) {
+                    if (!entity.features.findWhere({'var': Strophe.NS.MAM})) {
+                        _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
+                    }
+                    var sent_stanza, IQ_id;
+                    var 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'});
+                    var queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
+                    expect(sent_stanza.toString()).toBe(
+                        "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
+                            "<query xmlns='urn:xmpp:mam:2' queryid='"+queryid+"'>"+
+                                "<x xmlns='jabber:x:data' type='submit'>"+
                                 "<field var='FORM_TYPE' type='hidden'>"+
                                     "<value>urn:xmpp:mam:2</value>"+
                                 "</field>"+
-                            "</x>"+
-                        "</query>"+
-                    "</iq>");
-           }));
-
-           it("checks whether returned MAM messages from a MUC room are from the right JID", mock.initConverse(function (_converse) {
-                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});
-                }
-                var sent_stanza, IQ_id;
-                var 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);
-                });
-                var callback = jasmine.createSpy('callback');
-
-                _converse.api.archive.query({'with': 'coven@chat.shakespear.lit', 'groupchat': true, 'max':'10'}, callback);
-                var queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
-
-                /* <message id='iasd207' from='coven@chat.shakespeare.lit' to='hag66@shakespeare.lit/pda'>
-                 *     <result xmlns='urn:xmpp:mam:2' queryid='g27' id='34482-21985-73620'>
-                 *         <forwarded xmlns='urn:xmpp:forward:0'>
-                 *         <delay xmlns='urn:xmpp:delay' stamp='2002-10-13T23:58:37Z'/>
-                 *         <message xmlns="jabber:client"
-                 *             from='coven@chat.shakespeare.lit/firstwitch'
-                 *             id='162BEBB1-F6DB-4D9A-9BD8-CFDCC801A0B2'
-                 *             type='groupchat'>
-                 *             <body>Thrice the brinded cat hath mew'd.</body>
-                 *             <x xmlns='http://jabber.org/protocol/muc#user'>
-                 *             <item affiliation='none'
-                 *                     jid='witch1@shakespeare.lit'
-                 *                     role='participant' />
-                 *             </x>
-                 *         </message>
-                 *         </forwarded>
-                 *     </result>
-                 * </message>
-                 */
-                var 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 <iq> stanza to indicate the end of the result set.
-                 *
-                 * <iq type='result' id='juliet1'>
-                 *     <fin xmlns='urn:xmpp:mam:2'>
-                 *     <set xmlns='http://jabber.org/protocol/rsm'>
-                 *         <first index='0'>28482-98726-73623</first>
-                 *         <last>09af3-cc343-b409f</last>
-                 *         <count>20</count>
-                 *     </set>
-                 * </iq>
-                 */
-                var 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));
-
-                expect(callback).toHaveBeenCalled();
-                var args = callback.calls.argsFor(0);
-                expect(args[0].length).toBe(0);
-           }));
-
-           it("can be used to query for all messages in a certain timespan", mock.initConverse(function (_converse) {
-                var sent_stanza, IQ_id;
-                var 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);
+                                "<field var='with'>"+
+                                    "<value>juliet@capulet.lit</value>"+
+                                "</field>"+
+                                "</x>"+
+                            "</query>"+
+                        "</iq>"
+                    );
+                    done();
                 });
-                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});
-                }
-                var start = '2010-06-07T00:00:00Z';
-                var end = '2010-07-07T13:23:54Z';
-                _converse.api.archive.query({
-                    'start': start,
-                    'end': end
+            }));
 
+           it("can be used to query for archived messages from a chat room",
+                    mock.initConverseWithPromises(
+                        null, [], {},
+                        function (done, _converse) {
+
+                _converse.api.disco.entities.get(_converse.domain).then(function (entity) {
+                    if (!entity.features.findWhere({'var': Strophe.NS.MAM})) {
+                        _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
+                    }
+
+                    var sent_stanza, IQ_id;
+                    var 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);
+                    });
+                    var callback = jasmine.createSpy('callback');
+
+                    _converse.api.archive.query({'with': 'coven@chat.shakespeare.lit', 'groupchat': true}, callback);
+                    var queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
+
+                    expect(sent_stanza.toString()).toBe(
+                        "<iq type='set' to='coven@chat.shakespeare.lit' xmlns='jabber:client' id='"+IQ_id+"'>"+
+                            "<query xmlns='urn:xmpp:mam:2' queryid='"+queryid+"'>"+
+                                "<x xmlns='jabber:x:data' type='submit'>"+
+                                    "<field var='FORM_TYPE' type='hidden'>"+
+                                        "<value>urn:xmpp:mam:2</value>"+
+                                    "</field>"+
+                                "</x>"+
+                            "</query>"+
+                        "</iq>");
+                    done();
                 });
-                var queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
-                expect(sent_stanza.toString()).toBe(
-                    "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
-                        "<query xmlns='urn:xmpp:mam:2' queryid='"+queryid+"'>"+
-                            "<x xmlns='jabber:x:data' type='submit'>"+
-                            "<field var='FORM_TYPE' type='hidden'>"+
-                                "<value>urn:xmpp:mam:2</value>"+
-                            "</field>"+
-                            "<field var='start'>"+
-                                "<value>"+moment(start).format()+"</value>"+
-                            "</field>"+
-                            "<field var='end'>"+
-                                "<value>"+moment(end).format()+"</value>"+
-                            "</field>"+
-                            "</x>"+
-                        "</query>"+
-                    "</iq>"
-                );
            }));
 
-           it("throws a TypeError if an invalid date is provided", mock.initConverse(function (_converse) {
-                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});
-                }
-                expect(_.partial(_converse.api.archive.query, {'start': 'not a real date'})).toThrow(
-                    new TypeError('archive.query: invalid date provided for: start')
-                );
-           }));
+            it("checks whether returned MAM messages from a MUC room are from the right JID",
+                    mock.initConverseWithPromises(
+                        null, [], {},
+                        function (done, _converse) {
+
+                _converse.api.disco.entities.get(_converse.domain).then(function (entity) {
+                    if (!entity.features.findWhere({'var': Strophe.NS.MAM})) {
+                        _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
+                    }
+                    var sent_stanza, IQ_id;
+                    var 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);
+                    });
+                    var callback = jasmine.createSpy('callback');
+
+                    _converse.api.archive.query({'with': 'coven@chat.shakespear.lit', 'groupchat': true, 'max':'10'}, callback);
+                    var queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
+
+                    /* <message id='iasd207' from='coven@chat.shakespeare.lit' to='hag66@shakespeare.lit/pda'>
+                     *     <result xmlns='urn:xmpp:mam:2' queryid='g27' id='34482-21985-73620'>
+                     *         <forwarded xmlns='urn:xmpp:forward:0'>
+                     *         <delay xmlns='urn:xmpp:delay' stamp='2002-10-13T23:58:37Z'/>
+                     *         <message xmlns="jabber:client"
+                     *             from='coven@chat.shakespeare.lit/firstwitch'
+                     *             id='162BEBB1-F6DB-4D9A-9BD8-CFDCC801A0B2'
+                     *             type='groupchat'>
+                     *             <body>Thrice the brinded cat hath mew'd.</body>
+                     *             <x xmlns='http://jabber.org/protocol/muc#user'>
+                     *             <item affiliation='none'
+                     *                     jid='witch1@shakespeare.lit'
+                     *                     role='participant' />
+                     *             </x>
+                     *         </message>
+                     *         </forwarded>
+                     *     </result>
+                     * </message>
+                     */
+                    var 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 <iq> stanza to indicate the end of the result set.
+                     *
+                     * <iq type='result' id='juliet1'>
+                     *     <fin xmlns='urn:xmpp:mam:2'>
+                     *     <set xmlns='http://jabber.org/protocol/rsm'>
+                     *         <first index='0'>28482-98726-73623</first>
+                     *         <last>09af3-cc343-b409f</last>
+                     *         <count>20</count>
+                     *     </set>
+                     * </iq>
+                     */
+                    var 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));
 
-           it("can be used to query for all messages after a certain time", mock.initConverse(function (_converse) {
-                var sent_stanza, IQ_id;
-                var 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);
+                    expect(callback).toHaveBeenCalled();
+                    var args = callback.calls.argsFor(0);
+                    expect(args[0].length).toBe(0);
+                    done();
                 });
-                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});
-                }
-                var start = '2010-06-07T00:00:00Z';
-                _converse.api.archive.query({'start': start});
-                var queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
-                expect(sent_stanza.toString()).toBe(
-                    "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
-                        "<query xmlns='urn:xmpp:mam:2' queryid='"+queryid+"'>"+
-                            "<x xmlns='jabber:x:data' type='submit'>"+
-                            "<field var='FORM_TYPE' type='hidden'>"+
-                                "<value>urn:xmpp:mam:2</value>"+
-                            "</field>"+
-                            "<field var='start'>"+
-                                "<value>"+moment(start).format()+"</value>"+
-                            "</field>"+
-                            "</x>"+
-                        "</query>"+
-                    "</iq>"
-                );
            }));
 
-           it("can be used to query for a limited set of results", mock.initConverse(function (_converse) {
+           it("can be used to query for all messages in a certain timespan",
+                    mock.initConverseWithPromises(
+                        null, [], {},
+                        function (done, _converse) {
+
                 var sent_stanza, IQ_id;
                 var 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});
-                }
-                var start = '2010-06-07T00:00:00Z';
-                _converse.api.archive.query({'start': start, 'max':10});
-                var queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
-                expect(sent_stanza.toString()).toBe(
-                    "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
-                        "<query xmlns='urn:xmpp:mam:2' queryid='"+queryid+"'>"+
-                            "<x xmlns='jabber:x:data' type='submit'>"+
+                _converse.api.disco.entities.get().then(function (entities) {
+                    if (!entities.get(_converse.domain).features.findWhere({'var': Strophe.NS.MAM})) {
+                        _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
+                    }
+                    var start = '2010-06-07T00:00:00Z';
+                    var end = '2010-07-07T13:23:54Z';
+                    _converse.api.archive.query({
+                        'start': start,
+                        'end': end
+
+                    });
+                    var queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
+                    expect(sent_stanza.toString()).toBe(
+                        "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
+                            "<query xmlns='urn:xmpp:mam:2' queryid='"+queryid+"'>"+
+                                "<x xmlns='jabber:x:data' type='submit'>"+
                                 "<field var='FORM_TYPE' type='hidden'>"+
                                     "<value>urn:xmpp:mam:2</value>"+
                                 "</field>"+
                                 "<field var='start'>"+
                                     "<value>"+moment(start).format()+"</value>"+
                                 "</field>"+
-                            "</x>"+
-                            "<set xmlns='http://jabber.org/protocol/rsm'>"+
-                                "<max>10</max>"+
-                            "</set>"+
-                        "</query>"+
-                    "</iq>"
-                );
+                                "<field var='end'>"+
+                                    "<value>"+moment(end).format()+"</value>"+
+                                "</field>"+
+                                "</x>"+
+                            "</query>"+
+                        "</iq>"
+                    );
+                    done();
+                });
            }));
 
-           it("can be used to page through results", mock.initConverse(function (_converse) {
-                var sent_stanza, IQ_id;
-                var 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});
-                }
-                var start = '2010-06-07T00:00:00Z';
-                _converse.api.archive.query({
-                    'start': start,
-                    'after': '09af3-cc343-b409f',
-                    'max':10
+           it("throws a TypeError if an invalid date is provided",
+                    mock.initConverseWithPromises(
+                        null, [], {},
+                        function (done, _converse) {
+
+                _converse.api.disco.entities.get(_converse.domain).then(function (entity) {
+                    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();
                 });
-                var queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
-                expect(sent_stanza.toString()).toBe(
-                    "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
-                        "<query xmlns='urn:xmpp:mam:2' queryid='"+queryid+"'>"+
-                            "<x xmlns='jabber:x:data' type='submit'>"+
+           }));
+
+           it("can be used to query for all messages after a certain time",
+                    mock.initConverseWithPromises(
+                        null, [], {},
+                        function (done, _converse) {
+
+                _converse.api.disco.entities.get(_converse.domain).then(function (entity) {
+                    if (!entity.features.findWhere({'var': Strophe.NS.MAM})) {
+                        _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
+                    }
+                    var sent_stanza, IQ_id;
+                    var 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});
+                    }
+                    var start = '2010-06-07T00:00:00Z';
+                    _converse.api.archive.query({'start': start});
+                    var queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
+                    expect(sent_stanza.toString()).toBe(
+                        "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
+                            "<query xmlns='urn:xmpp:mam:2' queryid='"+queryid+"'>"+
+                                "<x xmlns='jabber:x:data' type='submit'>"+
                                 "<field var='FORM_TYPE' type='hidden'>"+
                                     "<value>urn:xmpp:mam:2</value>"+
                                 "</field>"+
                                 "<field var='start'>"+
                                     "<value>"+moment(start).format()+"</value>"+
                                 "</field>"+
-                            "</x>"+
-                            "<set xmlns='http://jabber.org/protocol/rsm'>"+
-                                "<max>10</max>"+
-                                "<after>09af3-cc343-b409f</after>"+
-                            "</set>"+
-                        "</query>"+
-                    "</iq>"
-                );
+                                "</x>"+
+                            "</query>"+
+                        "</iq>"
+                    );
+                    done();
+                });
            }));
 
-           it("accepts \"before\" with an empty string as value to reverse the order", mock.initConverse(function (_converse) {
-                var sent_stanza, IQ_id;
-                var 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);
+           it("can be used to query for a limited set of results",
+                    mock.initConverseWithPromises(
+                        null, [], {},
+                        function (done, _converse) {
+
+                _converse.api.disco.entities.get(_converse.domain).then(function (entity) {
+                    if (!entity.features.findWhere({'var': Strophe.NS.MAM})) {
+                        _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
+                    }
+
+                    var sent_stanza, IQ_id;
+                    var 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);
+                    });
+                    var start = '2010-06-07T00:00:00Z';
+                    _converse.api.archive.query({'start': start, 'max':10});
+                    var queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
+                    expect(sent_stanza.toString()).toBe(
+                        "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
+                            "<query xmlns='urn:xmpp:mam:2' queryid='"+queryid+"'>"+
+                                "<x xmlns='jabber:x:data' type='submit'>"+
+                                    "<field var='FORM_TYPE' type='hidden'>"+
+                                        "<value>urn:xmpp:mam:2</value>"+
+                                    "</field>"+
+                                    "<field var='start'>"+
+                                        "<value>"+moment(start).format()+"</value>"+
+                                    "</field>"+
+                                "</x>"+
+                                "<set xmlns='http://jabber.org/protocol/rsm'>"+
+                                    "<max>10</max>"+
+                                "</set>"+
+                            "</query>"+
+                        "</iq>"
+                    );
+                    done();
                 });
-                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({'before': '', 'max':10});
-                var queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
-                expect(sent_stanza.toString()).toBe(
-                    "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
-                        "<query xmlns='urn:xmpp:mam:2' queryid='"+queryid+"'>"+
-                            "<x xmlns='jabber:x:data' type='submit'>"+
-                                "<field var='FORM_TYPE' type='hidden'>"+
-                                    "<value>urn:xmpp:mam:2</value>"+
-                                "</field>"+
-                            "</x>"+
-                            "<set xmlns='http://jabber.org/protocol/rsm'>"+
-                                "<max>10</max>"+
-                                "<before></before>"+
-                            "</set>"+
-                        "</query>"+
-                    "</iq>"
-                );
            }));
 
-           it("accepts a Strophe.RSM object for the query options", mock.initConverse(function (_converse) {
-                // 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.
-                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});
-                }
-                var sent_stanza, IQ_id;
-                var 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);
+           it("can be used to page through results",
+                    mock.initConverseWithPromises(
+                        null, [], {},
+                        function (done, _converse) {
+
+                _converse.api.disco.entities.get(_converse.domain).then(function (entity) {
+                    if (!entity.features.findWhere({'var': Strophe.NS.MAM})) {
+                        _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
+                    }
+                    var sent_stanza, IQ_id;
+                    var 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);
+                    });
+                    var start = '2010-06-07T00:00:00Z';
+                    _converse.api.archive.query({
+                        'start': start,
+                        'after': '09af3-cc343-b409f',
+                        'max':10
+                    });
+                    var queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
+                    expect(sent_stanza.toString()).toBe(
+                        "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
+                            "<query xmlns='urn:xmpp:mam:2' queryid='"+queryid+"'>"+
+                                "<x xmlns='jabber:x:data' type='submit'>"+
+                                    "<field var='FORM_TYPE' type='hidden'>"+
+                                        "<value>urn:xmpp:mam:2</value>"+
+                                    "</field>"+
+                                    "<field var='start'>"+
+                                        "<value>"+moment(start).format()+"</value>"+
+                                    "</field>"+
+                                "</x>"+
+                                "<set xmlns='http://jabber.org/protocol/rsm'>"+
+                                    "<max>10</max>"+
+                                    "<after>09af3-cc343-b409f</after>"+
+                                "</set>"+
+                            "</query>"+
+                        "</iq>"
+                    );
+                    done();
                 });
-                var 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);
+           }));
 
-                var queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
-                expect(sent_stanza.toString()).toBe(
-                    "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
-                        "<query xmlns='urn:xmpp:mam:2' queryid='"+queryid+"'>"+
-                            "<x xmlns='jabber:x:data' type='submit'>"+
-                                "<field var='FORM_TYPE' type='hidden'>"+
-                                    "<value>urn:xmpp:mam:2</value>"+
-                                "</field>"+
-                                "<field var='with'>"+
-                                    "<value>romeo@montague.lit</value>"+
-                                "</field>"+
-                                "<field var='start'>"+
-                                    "<value>"+moment(rsm.start).format()+"</value>"+
-                                "</field>"+
-                            "</x>"+
-                            "<set xmlns='http://jabber.org/protocol/rsm'>"+
-                                "<max>10</max>"+
-                            "</set>"+
-                        "</query>"+
-                    "</iq>"
-                );
+           it("accepts \"before\" with an empty string as value to reverse the order",
+                    mock.initConverseWithPromises(
+                        null, [], {},
+                        function (done, _converse) {
+
+                _converse.api.disco.entities.get(_converse.domain).then(function (entity) {
+                    if (!entity.features.findWhere({'var': Strophe.NS.MAM})) {
+                        _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
+                    }
+                    var sent_stanza, IQ_id;
+                    var 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});
+                    var queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
+                    expect(sent_stanza.toString()).toBe(
+                        "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
+                            "<query xmlns='urn:xmpp:mam:2' queryid='"+queryid+"'>"+
+                                "<x xmlns='jabber:x:data' type='submit'>"+
+                                    "<field var='FORM_TYPE' type='hidden'>"+
+                                        "<value>urn:xmpp:mam:2</value>"+
+                                    "</field>"+
+                                "</x>"+
+                                "<set xmlns='http://jabber.org/protocol/rsm'>"+
+                                    "<max>10</max>"+
+                                    "<before></before>"+
+                                "</set>"+
+                            "</query>"+
+                        "</iq>"
+                    );
+                    done();
+                });
            }));
 
-           it("accepts a callback function, which it passes the messages and a Strophe.RSM object", mock.initConverse(function (_converse) {
-                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});
-                }
-                var sent_stanza, IQ_id;
-                var 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);
+           it("accepts a Strophe.RSM object for the query options",
+                    mock.initConverseWithPromises(
+                        null, [], {},
+                        function (done, _converse) {
+
+                _converse.api.disco.entities.get(_converse.domain).then(function (entity) {
+                    if (!entity.features.findWhere({'var': Strophe.NS.MAM})) {
+                        _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
+                    }
+                    var sent_stanza, IQ_id;
+                    var 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.
+                    var 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);
+
+                    var queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
+                    expect(sent_stanza.toString()).toBe(
+                        "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
+                            "<query xmlns='urn:xmpp:mam:2' queryid='"+queryid+"'>"+
+                                "<x xmlns='jabber:x:data' type='submit'>"+
+                                    "<field var='FORM_TYPE' type='hidden'>"+
+                                        "<value>urn:xmpp:mam:2</value>"+
+                                    "</field>"+
+                                    "<field var='with'>"+
+                                        "<value>romeo@montague.lit</value>"+
+                                    "</field>"+
+                                    "<field var='start'>"+
+                                        "<value>"+moment(rsm.start).format()+"</value>"+
+                                    "</field>"+
+                                "</x>"+
+                                "<set xmlns='http://jabber.org/protocol/rsm'>"+
+                                    "<max>10</max>"+
+                                "</set>"+
+                            "</query>"+
+                        "</iq>"
+                    );
+                    done();
                 });
-                var callback = jasmine.createSpy('callback');
+           }));
 
-                _converse.api.archive.query({'with': 'romeo@capulet.lit', 'max':'10'}, callback);
-                var queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
+           it("accepts a callback function, which it passes the messages and a Strophe.RSM object",
+                    mock.initConverseWithPromises(
+                        null, [], {},
+                        function (done, _converse) {
+
+                _converse.api.disco.entities.get(_converse.domain).then(function (entity) {
+                    if (!entity.features.findWhere({'var': Strophe.NS.MAM})) {
+                        _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
+                    }
+                    var sent_stanza, IQ_id;
+                    var 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);
+                    });
+                    var callback = jasmine.createSpy('callback');
+
+                    _converse.api.archive.query({'with': 'romeo@capulet.lit', 'max':'10'}, callback);
+                    var queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
+
+                    /*  <message id='aeb213' to='juliet@capulet.lit/chamber'>
+                     *  <result xmlns='urn:xmpp:mam:2' queryid='f27' id='28482-98726-73623'>
+                     *      <forwarded xmlns='urn:xmpp:forward:0'>
+                     *      <delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>
+                     *      <message xmlns='jabber:client'
+                     *          to='juliet@capulet.lit/balcony'
+                     *          from='romeo@montague.lit/orchard'
+                     *          type='chat'>
+                     *          <body>Call me but love, and I'll be new baptized; Henceforth I never will be Romeo.</body>
+                     *      </message>
+                     *      </forwarded>
+                     *  </result>
+                     *  </message>
+                     */
+                    var 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));
+
+                    var 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 <iq> stanza to indicate the end of the result set.
+                     *
+                     * <iq type='result' id='juliet1'>
+                     *     <fin xmlns='urn:xmpp:mam:2'>
+                     *     <set xmlns='http://jabber.org/protocol/rsm'>
+                     *         <first index='0'>28482-98726-73623</first>
+                     *         <last>09af3-cc343-b409f</last>
+                     *         <count>20</count>
+                     *     </set>
+                     * </iq>
+                     */
+                    var 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));
 
-                /*  <message id='aeb213' to='juliet@capulet.lit/chamber'>
-                 *  <result xmlns='urn:xmpp:mam:2' queryid='f27' id='28482-98726-73623'>
-                 *      <forwarded xmlns='urn:xmpp:forward:0'>
-                 *      <delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>
-                 *      <message xmlns='jabber:client'
-                 *          to='juliet@capulet.lit/balcony'
-                 *          from='romeo@montague.lit/orchard'
-                 *          type='chat'>
-                 *          <body>Call me but love, and I'll be new baptized; Henceforth I never will be Romeo.</body>
-                 *      </message>
-                 *      </forwarded>
-                 *  </result>
-                 *  </message>
-                 */
-                var 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));
-
-                var 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 <iq> stanza to indicate the end of the result set.
-                 *
-                 * <iq type='result' id='juliet1'>
-                 *     <fin xmlns='urn:xmpp:mam:2'>
-                 *     <set xmlns='http://jabber.org/protocol/rsm'>
-                 *         <first index='0'>28482-98726-73623</first>
-                 *         <last>09af3-cc343-b409f</last>
-                 *         <count>20</count>
-                 *     </set>
-                 * </iq>
-                 */
-                var 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));
-
-                expect(callback).toHaveBeenCalled();
-                var 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');
+                    expect(callback).toHaveBeenCalled();
+                    var 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(function (_converse) {
-                var sent_stanza, IQ_id;
-                var 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';
+            it("is set once server support for MAM has been confirmed",
+                    mock.initConverseWithPromises(
+                        null, [], {},
+                        function (done, _converse) {
+
+                _converse.api.disco.entities.get(_converse.domain).then(function (entity) {
+                    var sent_stanza, IQ_id;
+                    var 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';
+
+                    var 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(
+                        "<iq type='get' xmlns='jabber:client' id='"+IQ_id+"'>"+
+                            "<prefs xmlns='urn:xmpp:mam:2'/>"+
+                        "</iq>"
+                    );
+
+                    /* Example 20. Server responds with current preferences
+                     *
+                     * <iq type='result' id='juliet2'>
+                     *   <prefs xmlns='urn:xmpp:mam:0' default='roster'>
+                     *     <always/>
+                     *     <never/>
+                     *   </prefs>
+                     * </iq>
+                     */
+                    var 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));
 
-                var feature = new Backbone.Model({
-                    'var': Strophe.NS.MAM
+                    expect(_converse.onMAMPreferences).toHaveBeenCalled();
+                    expect(_converse.connection.sendIQ.calls.count()).toBe(2);
+
+                    expect(sent_stanza.toString()).toBe(
+                        "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
+                            "<prefs xmlns='urn:xmpp:mam:2' default='never'>"+
+                                "<always><jid>romeo@montague.lit</jid></always>"+
+                                "<never><jid>montague@montague.lit</jid></never>"+
+                            "</prefs>"+
+                        "</iq>"
+                    );
+
+                    expect(feature.get('preference')).toBe(undefined);
+                    /* <iq type='result' id='juliet3'>
+                     *   <prefs xmlns='urn:xmpp:mam:0' default='always'>
+                     *       <always>
+                     *          <jid>romeo@montague.lit</jid>
+                     *       </always>
+                     *       <never>
+                     *          <jid>montague@montague.lit</jid>
+                     *       </never>
+                     *   </prefs>
+                     * </iq>
+                     */
+                    stanza = $iq({'type': 'result', 'id': IQ_id})
+                        .c('prefs', {'xmlns': Strophe.NS.MAM, 'default':'always'})
+                        .c('always').up()
+                        .c('never').up();
+                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                    expect(feature.save).toHaveBeenCalled();
+                    expect(feature.get('preferences')['default']).toBe('never'); // eslint-disable-line dot-notation
+                    done();
                 });
-                spyOn(feature, 'save').and.callFake(feature.set); // Save will complain about a url not being set
-                _converse.disco_entities.get(_converse.domain).onFeatureAdded(feature);
-
-                expect(_converse.connection.sendIQ).toHaveBeenCalled();
-                expect(sent_stanza.toLocaleString()).toBe(
-                    "<iq type='get' xmlns='jabber:client' id='"+IQ_id+"'>"+
-                        "<prefs xmlns='urn:xmpp:mam:2'/>"+
-                    "</iq>"
-                );
-
-                /* Example 20. Server responds with current preferences
-                 *
-                 * <iq type='result' id='juliet2'>
-                 *   <prefs xmlns='urn:xmpp:mam:0' default='roster'>
-                 *     <always/>
-                 *     <never/>
-                 *   </prefs>
-                 * </iq>
-                 */
-                var 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));
-
-                expect(_converse.onMAMPreferences).toHaveBeenCalled();
-
-                expect(_converse.connection.sendIQ.calls.count()).toBe(2);
-                expect(sent_stanza.toString()).toBe(
-                    "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
-                        "<prefs xmlns='urn:xmpp:mam:2' default='never'>"+
-                            "<always><jid>romeo@montague.lit</jid></always>"+
-                            "<never><jid>montague@montague.lit</jid></never>"+
-                        "</prefs>"+
-                    "</iq>"
-                );
-
-                expect(feature.get('preference')).toBe(undefined);
-                /* <iq type='result' id='juliet3'>
-                 *   <prefs xmlns='urn:xmpp:mam:0' default='always'>
-                 *       <always>
-                 *          <jid>romeo@montague.lit</jid>
-                 *       </always>
-                 *       <never>
-                 *          <jid>montague@montague.lit</jid>
-                 *       </never>
-                 *   </prefs>
-                 * </iq>
-                 */
-                stanza = $iq({'type': 'result', 'id': IQ_id})
-                    .c('prefs', {'xmlns': Strophe.NS.MAM, 'default':'always'})
-                    .c('always').up()
-                    .c('never').up();
-                _converse.connection._dataRecv(test_utils.createRequest(stanza));
-                expect(feature.save).toHaveBeenCalled();
-                expect(feature.get('preferences')['default']).toBe('never'); // eslint-disable-line dot-notation
-
-                // Restore
-                _converse.message_archiving = 'never';
             }));
         });
     });

+ 1 - 1
src/converse-bookmarks.js

@@ -550,7 +550,7 @@
                         _converse.api.disco.getIdentity('pubsub', 'pep', _converse.bare_jid),
                         _converse.api.disco.supports(Strophe.NS.PUBSUB+'#publish-options', _converse.bare_jid)
                     ]).then((args) => {
-                        resolve(args[0] && (args[1].supported || _converse.allow_public_bookmarks));
+                        resolve(args[0] && (args[1].length || _converse.allow_public_bookmarks));
                     }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
                 }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
             }

+ 1 - 2
src/converse-chatview.js

@@ -341,8 +341,7 @@
                     Promise.all(_.map(_.keys(resources), (resource) =>
                         _converse.api.disco.supports(Strophe.NS.SPOILER, `${contact_jid}/${resource}`)
                     )).then((results) => {
-                        const supported = _.every(f.map(f.get('supported'))(results));
-                        if (supported) {
+                        if (results.length) {
                             const html = tpl_spoiler_button(this.model.toJSON());
                             if (_converse.visible_toolbar_buttons.emoji) {
                                 this.el.querySelector('.toggle-smiley').insertAdjacentHTML('afterEnd', html);

+ 78 - 60
src/converse-disco.js

@@ -12,7 +12,7 @@
     define(["converse-core", "sizzle", "strophe.disco"], factory);
 }(this, function (converse, sizzle) {
 
-    const { Backbone, Promise, Strophe, b64_sha1, utils, _ } = converse.env;
+    const { Backbone, Promise, Strophe, b64_sha1, utils, _, f } = converse.env;
 
     converse.plugins.add('converse-disco', {
 
@@ -22,24 +22,10 @@
              */
             const { _converse } = this;
 
-            function onDiscoItems (stanza) {
-                _.each(stanza.querySelectorAll('query item'), (item) => {
-                    if (item.getAttribute("node")) {
-                        // XXX: ignore nodes for now.
-                        // See: https://xmpp.org/extensions/xep-0030.html#items-nodes
-                        return;
-                    }
-                    const jid = item.getAttribute('jid');
-                    const entities = _converse.disco_entities;
-                    if (_.isUndefined(entities.get(jid))) {
-                        entities.create({'jid': jid});
-                    }
-                });
-            }
-
             // Promises exposed by this plugin
             _converse.api.promises.add('discoInitialized');
 
+
             _converse.DiscoEntity = Backbone.Model.extend({
                 /* A Disco Entity is a JID addressable entity that can be queried
                  * for features.
@@ -63,6 +49,10 @@
                     );
                     this.fetchFeatures();
 
+                    this.items = new _converse.DiscoEntities();
+                    this.items.browserStorage = new Backbone.BrowserStorage[_converse.storage](
+                        b64_sha1(`converse.disco-items-${this.get('jid')}`)
+                    );
                 },
 
                 getIdentity (category, type) {
@@ -98,17 +88,10 @@
                     const entity = this;
                     return new Promise((resolve, reject) => {
                         function fulfillPromise () {
-                            const model = entity.features.findWhere({'var': feature });
-                            if (model) {
-                                resolve({
-                                    'supported': true,
-                                    'feature': model
-                                });
+                            if (entity.features.findWhere({'var': feature})) {
+                                resolve(entity);
                             } else {
-                                resolve({
-                                    'supported': false,
-                                    'feature': null
-                                });
+                                resolve();
                             }
                         }
                         entity.waitUntilFeaturesDiscovered
@@ -141,13 +124,27 @@
                     _converse.connection.disco.info(this.get('jid'), null, this.onInfo.bind(this));
                 },
 
+                onDiscoItems (stanza) {
+                    _.each(stanza.querySelectorAll('query item'), (item) => {
+                        if (item.getAttribute("node")) {
+                            // XXX: ignore nodes for now.
+                            // See: https://xmpp.org/extensions/xep-0030.html#items-nodes
+                            return;
+                        }
+                        const jid = item.getAttribute('jid');
+                        if (_.isUndefined(this.items.get(jid))) {
+                            this.items.create({'jid': jid});
+                        }
+                    });
+                },
+
                 queryForItems () {
                     if (_.isEmpty(this.identities.where({'category': 'server'}))) {
                         // Don't fetch features and items if this is not a
                         // server or a conference component.
                         return;
                     }
-                    _converse.connection.disco.items(this.get('jid'), null, onDiscoItems);
+                    _converse.connection.disco.items(this.get('jid'), null, this.onDiscoItems.bind(this));
                 },
 
                 onInfo (stanza) {
@@ -175,26 +172,11 @@
             _converse.DiscoEntities = Backbone.Collection.extend({
                 model: _converse.DiscoEntity,
 
-                initialize () {
-                    this.browserStorage = new Backbone.BrowserStorage[_converse.storage](
-                        b64_sha1(`converse.disco-entities-${_converse.bare_jid}`)
-                    );
-                    this.fetchEntities().then(
-                        _.partial(_converse.emit, 'discoInitialized'),
-                        _.partial(_converse.emit, 'discoInitialized')
-                    ).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
-                },
-
                 fetchEntities () {
                     return new Promise((resolve, reject) => {
                         this.fetch({
                             add: true,
-                            success: function (collection) {
-                                if (collection.length === 0 || !collection.get(_converse.domain)) {
-                                    this.create({'jid': _converse.domain});
-                                }
-                                resolve();
-                            }.bind(this),
+                            success: resolve,
                             error () {
                                 reject (new Error("Could not fetch disco entities"));
                             }
@@ -227,6 +209,18 @@
             function initializeDisco () {
                 addClientFeatures();
                 _converse.disco_entities = new _converse.DiscoEntities();
+                _converse.disco_entities.browserStorage = new Backbone.BrowserStorage[_converse.storage](
+                    b64_sha1(`converse.disco-entities-${_converse.bare_jid}`)
+                );
+
+                _converse.disco_entities.fetchEntities().then((collection) => {
+                    if (collection.length === 0 || !collection.get(_converse.domain)) {
+                        // If we don't have an entity for our own XMPP server,
+                        // create one.
+                        _converse.disco_entities.create({'jid': _converse.domain});
+                    }
+                    _converse.emit('discoInitialized');
+                }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
             }
             _converse.api.listen.on('reconnected', initializeDisco);
             _converse.api.listen.on('connected', initializeDisco);
@@ -247,28 +241,49 @@
                 'disco': {
                     'entities': {
                         'get' (entity_jid, create=false) {
-                            const entity = _converse.disco_entities.get(entity_jid);
-                            if (entity || !create) {
-                                return entity;
-                            }
-                            return _converse.disco_entities.create({'jid': entity_jid});
+                            return _converse.api.waitUntil('discoInitialized').then(() => {
+                                if (_.isNil(entity_jid)) {
+                                    return _converse.disco_entities;
+                                }
+                                const entity = _converse.disco_entities.get(entity_jid);
+                                if (entity || !create) {
+                                    return entity;
+                                }
+                                return _converse.disco_entities.create({'jid': entity_jid});
+                            });
                         }
                     },
 
                     'supports' (feature, entity_jid) {
-                        /* Returns a Promise which resolves with a map indicating
-                         * whether a given feature is supported.
+                        /* Returns a Promise which resolves with a list containing
+                         * _converse.Entity instances representing the entity
+                         * itself or those items associated with the entity if
+                         * they support the given feature.
                          *
                          * Parameters:
                          *    (String) feature - The feature that might be
-                         *          supported. In the XML stanza, this is the `var`
-                         *          attribute of the `<feature>` element. For
-                         *          example: 'http://jabber.org/protocol/muc'
-                         *    (String) entity_jid - The JID of the entity which might support the feature.
+                         *         supported. In the XML stanza, this is the `var`
+                         *         attribute of the `<feature>` element. For
+                         *         example: 'http://jabber.org/protocol/muc'
+                         *    (String) entity_jid - The JID of the entity
+                         *         (and its associated items) which should be queried
                          */
-                        return _converse.api.waitUntil('discoInitialized').then(() => {
-                            const entity = _converse.api.disco.entities.get(entity_jid, true);
-                            return entity.hasFeature(feature);
+                        if (_.isNil(entity_jid)) {
+                            throw new TypeError('disco.supports: You need to provide an entity JID');
+                        }
+                        return _converse.api.waitUntil('discoInitialized').then((entity) => {
+                            return new Promise((resolve, reject) => {
+                                _converse.api.disco.entities.get(entity_jid, true).then((entity) => {
+                                    Promise.all(
+                                        _.concat(
+                                            entity.items.map((item) => item.hasFeature(feature)),
+                                            entity.hasFeature(feature)
+                                        )
+                                    ).then((result) => {
+                                        resolve(f.filter(f.isObject, result));
+                                    }).catch(reject);
+                                })
+                            });
                         }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
                     },
 
@@ -286,10 +301,13 @@
                          *          In the XML stanza, this is the `type`
                          *          attribute of the `<identity>` element.
                          *          For example: 'pep'
+                         *    (String) entity_jid - The JID of the entity which might have the identity
                          */
-                        return _converse.api.waitUntil('discoInitialized').then(() => {
-                            const entity = _converse.api.disco.entities.get(entity_jid, true);
-                            return entity.getIdentity(category, type);
+                        return new Promise((resolve, reject) => {
+                            _converse.api.waitUntil('discoInitialized').then(() => {
+                                _converse.api.disco.entities.get(entity_jid, true)
+                                    .then((entity) => resolve(entity.getIdentity(category, type)));
+                            })
                         }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
                     }
                 }

+ 34 - 2
src/converse-http-file-upload.js

@@ -1,16 +1,48 @@
 (function (root, factory) {
-    define(["converse-http-file-upload"], factory);
+    define(["converse-core"], factory);
 }(this, function (converse) {
     "use strict";
 
+    const { Promise, Strophe, _ } = converse.env;
+    const u = converse.env.utils;
+
+    Strophe.addNamespace('HTTPUPLOAD', 'urn:xmpp:http:upload:0');
+
     converse.plugins.add('converse-http-file-upload', {
+        /* Plugin dependencies are other plugins which might be
+         * overridden or relied upon, and therefore need to be loaded before
+         * this plugin.
+         *
+         * If the setting "strict_plugin_dependencies" is set to true,
+         * an error will be raised if the plugin is not found. By default it's
+         * false, which means these plugins are only loaded opportunistically.
+         *
+         * NB: These plugins need to have already been loaded via require.js.
+         */
+        dependencies: ["converse-chatview"],
+
+        overrides: {
+
+            ChatBoxView:  {
+                addFileUploadButton (options) {
+                },
+
+                renderToolbar (toolbar, options) {
+                    const { _converse } = this.__super__;
+                    const result = this.__super__.renderToolbar.apply(this, arguments);
+                    // TODO: check results.length
+                    _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain)
+                        .then(this.addFileUploadButton.bind(this));
+                    return result;
+                }
+            }
+        },
 
         initialize () {
             /* The initialize function gets called as soon as the plugin is
              * loaded by converse.js's plugin machinery.
              */
             const { _converse } = this;
-
         }
     });
 }));

+ 3 - 3
src/converse-mam.js

@@ -154,8 +154,8 @@
                     if (this.disable_mam) { return; }
                     const { _converse } = this.__super__;
                     _converse.api.disco.supports(Strophe.NS.MAM, _converse.bare_jid).then(
-                        (result) => { // Success
-                            if (result.supported) {
+                        (results) => { // Success
+                            if (result.length) {
                                 const most_recent_msg = utils.getMostRecentMessage(this.model);
                                 if (_.isNil(most_recent_msg)) {
                                     this.fetchArchivedMessages();
@@ -193,7 +193,7 @@
                     const { _converse } = this.__super__;
                     _converse.api.disco.supports(Strophe.NS.MAM, _converse.bare_jid).then(
                         (result) => { // Success
-                            if (result.supported) {
+                            if (result.length) {
                                 this.fetchArchivedMessages();
                             }
                             this.model.save({'mam_initialized': true});

+ 1 - 1
src/converse-vcard.js

@@ -186,7 +186,7 @@
                 if (_.isNil(_converse.xmppstatus.get('vcard_updated'))) {
                     _converse.api.disco.supports(Strophe.NS.VCARD, _converse.domain)
                         .then((result) => {
-                            if (result.supported) {
+                            if (result.length) {
                                 _converse.api.vcard.get(_converse.bare_jid)
                                     .then((vcard) => _converse.xmppstatus.save(vcard));
                             }})

+ 17 - 16
src/converse.js

@@ -7,24 +7,25 @@ if (typeof define !== 'undefined') {
          * --------------------
          * Any of the following components may be removed if they're not needed.
          */
-        "converse-chatview",    // Renders standalone chat boxes for single user chat
-        "converse-controlbox",  // The control box
-        "converse-bookmarks",   // XEP-0048 Bookmarks
-        "converse-roomslist",   // Show currently open chat rooms
-        "converse-mam",         // XEP-0313 Message Archive Management
-        "converse-muc",         // XEP-0045 Multi-user chat
-        "converse-muc-views",   // Views related to MUC
+        "converse-bookmarks",       // XEP-0048 Bookmarks
+        "converse-chatview",        // Renders standalone chat boxes for single user chat
+        "converse-controlbox",      // The control box
+        "converse-dragresize",      // Allows chat boxes to be resized by dragging them
+        "converse-fullscreen",
+        "converse-headline",        // Support for headline messages
+        "converse-http-file-upload",
+        "converse-mam",             // XEP-0313 Message Archive Management
+        "converse-minimize",        // Allows chat boxes to be minimized
+        "converse-muc",             // XEP-0045 Multi-user chat
         "converse-muc-embedded",
         "converse-muc-views",
-        "converse-vcard",       // XEP-0054 VCard-temp
-        "converse-otr",         // Off-the-record encryption for one-on-one messages
-        "converse-register",    // XEP-0077 In-band registration
-        "converse-ping",        // XEP-0199 XMPP Ping
-        "converse-notification",// HTML5 Notifications
-        "converse-minimize",    // Allows chat boxes to be minimized
-        "converse-dragresize",  // Allows chat boxes to be resized by dragging them
-        "converse-headline",    // Support for headline messages
-        "converse-fullscreen"
+        "converse-muc-views",       // Views related to MUC
+        "converse-notification",    // HTML5 Notifications
+        "converse-otr",             // Off-the-record encryption for one-on-one messages
+        "converse-ping",            // XEP-0199 XMPP Ping
+        "converse-register",        // XEP-0077 In-band registration
+        "converse-roomslist",       // Show currently open chat rooms
+        "converse-vcard",           // XEP-0054 VCard-temp
         /* END: Removable components */
     ], function (converse) {
         return converse;