From 38c132a6fe0ecaa3394550df15d36ae10dcce7d9 Mon Sep 17 00:00:00 2001
From: Tarun Gupta <tarun1995gupta@gmail.com>
Date: Sat, 8 Jul 2017 02:39:17 +0530
Subject: Add Channel Join and Leave Capability to MIX

Add UpdateSubscription feature for subscribing to additional
MIX nodes
Add Joining a channel with a preference form
Add requesting user preference form and updating the preferences

License:
This patch is BSD-licensed, see Documentation/Licenses/BSD-simplified.txt for details.

Test-Information:
Tests added for joining and leaving a channel as in XEP-0369,
which passes.
Tests also added for updating subscription, joining channel
with preference form and requesting user preference form as
in XEP-0369, which passes.
Tested on Ubuntu 16.04 LTS.

Change-Id: Ibc2737f6154eeee1a85e98cb5f80c8bdbad35dcd

diff --git a/Swiften/MIX/MIX.cpp b/Swiften/MIX/MIX.cpp
new file mode 100644
index 0000000..f3e3d69
--- /dev/null
+++ b/Swiften/MIX/MIX.cpp
@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) 2017 Tarun Gupta
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#include <Swiften/MIX/MIX.h>
+
+namespace Swift {
+
+MIX::~MIX() {
+}
+
+}
diff --git a/Swiften/MIX/MIX.h b/Swiften/MIX/MIX.h
new file mode 100644
index 0000000..1398a6e
--- /dev/null
+++ b/Swiften/MIX/MIX.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2017 Tarun Gupta
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#pragma once
+
+#include <memory>
+#include <string>
+#include <unordered_set>
+
+#include <boost/signals2.hpp>
+
+#include <Swiften/Base/API.h>
+#include <Swiften/JID/JID.h>
+#include <Swiften/Elements/Form.h>
+#include <Swiften/Elements/MIXJoin.h>
+#include <Swiften/Elements/MIXLeave.h>
+#include <Swiften/Elements/MIXUpdateSubscription.h>
+#include <Swiften/Elements/MIXUserPreference.h>
+#include <Swiften/Elements/ErrorPayload.h>
+
+namespace Swift {
+    class SWIFTEN_API MIX {
+        public:
+            using ref = std::shared_ptr<MIX>;
+
+        public:
+            virtual ~MIX();
+
+            /**
+            * Join a MIX channel and subscribe to nodes.
+            */
+            virtual void joinChannel(const std::unordered_set<std::string>& nodes) = 0;
+
+            /**
+            * Join Channel with a set of preferences.
+            */
+            virtual void joinChannelWithPreferences(const std::unordered_set<std::string>& nodes, Form::ref form) = 0;
+
+            /**
+            * Update subscription of nodes.
+            */
+            virtual void updateSubscription(const std::unordered_set<std::string>& nodes) = 0;
+
+            /**
+            * Leave a MIX channel and unsubcribe nodes.
+            */
+            virtual void leaveChannel() = 0;
+
+            /**
+            * Request a configuration form for updating preferences.
+            */
+            virtual void requestPreferencesForm() = 0;
+
+            /**
+            * Update preferences after requesting preference form.
+            */
+            virtual void updatePreferences(Form::ref form) = 0;
+
+        public:
+            boost::signals2::signal<void (MIXJoin::ref /* joinResponse */, ErrorPayload::ref /* joinError */)> onJoinResponse;
+            boost::signals2::signal<void (MIXLeave::ref /* leaveResponse */, ErrorPayload::ref /* leaveError */)> onLeaveResponse;
+            boost::signals2::signal<void (MIXUpdateSubscription::ref /* updateResponse */, ErrorPayload::ref /* updateError */)> onSubscriptionUpdateResponse;
+            boost::signals2::signal<void (Form::ref /* preferencesForm */, ErrorPayload::ref /* failedConfiguration */)> onPreferencesFormResponse;
+            boost::signals2::signal<void (MIXUserPreference::ref /* userPreferenceResponse */, ErrorPayload::ref /* failedUpdate */)> onPreferencesUpdateResponse;
+    };
+}
diff --git a/Swiften/MIX/MIXImpl.cpp b/Swiften/MIX/MIXImpl.cpp
new file mode 100644
index 0000000..cd3eb21
--- /dev/null
+++ b/Swiften/MIX/MIXImpl.cpp
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2017 Tarun Gupta
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#include <Swiften/MIX/MIXImpl.h>
+
+#include <Swiften/Client/StanzaChannel.h>
+#include <Swiften/Elements/IQ.h>
+#include <Swiften/Queries/GenericRequest.h>
+#include <Swiften/Queries/IQRouter.h>
+
+namespace Swift {
+
+MIXImpl::MIXImpl(const JID& ownJID, const JID& channelJID, IQRouter* iqRouter) : ownJID_(ownJID), channelJID_(channelJID), iqRouter_(iqRouter) {
+
+}
+
+MIXImpl::~MIXImpl() {
+
+}
+
+void MIXImpl::joinChannel(const std::unordered_set<std::string>& nodes) {
+    joinChannelWithPreferences(nodes, nullptr);
+}
+
+void MIXImpl::joinChannelWithPreferences(const std::unordered_set<std::string>& nodes, Form::ref form) {
+    auto joinPayload = std::make_shared<MIXJoin>();
+    joinPayload->setChannel(channelJID_);
+    for (auto node : nodes) {
+        joinPayload->addSubscription(node);
+    }
+    if (form) {
+        joinPayload->setForm(form);
+    }
+    auto request = std::make_shared<GenericRequest<MIXJoin>>(IQ::Set, getJID(), joinPayload, iqRouter_);
+    request->onResponse.connect(boost::bind(&MIXImpl::handleJoinResponse, this, _1, _2));
+    request->send();
+}
+
+void MIXImpl::handleJoinResponse(MIXJoin::ref payload, ErrorPayload::ref error) {
+    onJoinResponse(payload, error);
+}
+
+void MIXImpl::updateSubscription(const std::unordered_set<std::string>& nodes) {
+    auto updateSubscriptionPayload = std::make_shared<MIXUpdateSubscription>();
+    updateSubscriptionPayload->setSubscriptions(nodes);
+    auto request = std::make_shared<GenericRequest<MIXUpdateSubscription>>(IQ::Set, channelJID_, updateSubscriptionPayload, iqRouter_);
+    request->onResponse.connect(boost::bind(&MIXImpl::handleUpdateSubscriptionResponse, this, _1, _2));
+    request->send();
+}
+
+void MIXImpl::handleUpdateSubscriptionResponse(MIXUpdateSubscription::ref payload, ErrorPayload::ref error) {
+    onSubscriptionUpdateResponse(payload, error);
+}
+
+void MIXImpl::leaveChannel() {
+    auto leavePayload = std::make_shared<MIXLeave>();
+    leavePayload->setChannel(channelJID_);
+    auto request = std::make_shared<GenericRequest<MIXLeave>>(IQ::Set, getJID(), leavePayload, iqRouter_);
+    request->onResponse.connect(boost::bind(&MIXImpl::handleLeaveResponse, this, _1, _2));
+    request->send();
+}
+
+void MIXImpl::handleLeaveResponse(MIXLeave::ref payload, ErrorPayload::ref error) {
+    onLeaveResponse(payload, error);
+}
+
+void MIXImpl::requestPreferencesForm() {
+    auto prefPayload = std::make_shared<MIXUserPreference>();
+    auto request = std::make_shared<GenericRequest<MIXUserPreference>>(IQ::Get, channelJID_, prefPayload, iqRouter_);
+    request->onResponse.connect(boost::bind(&MIXImpl::handlePreferencesFormReceived, this, _1, _2));
+    request->send();
+}
+
+void MIXImpl::handlePreferencesFormReceived(MIXUserPreference::ref payload, ErrorPayload::ref error) {
+    Form::ref form = nullptr;
+    if (payload) {
+        form = payload->getData();
+    }
+    onPreferencesFormResponse(form, error);
+}
+
+void MIXImpl::handlePreferencesResultReceived(MIXUserPreference::ref payload, ErrorPayload::ref error) {
+    onPreferencesUpdateResponse(payload, error);
+}
+
+void MIXImpl::updatePreferences(Form::ref form) {
+    auto prefPayload = std::make_shared<MIXUserPreference>();
+    prefPayload->setData(form);
+    auto request = std::make_shared<GenericRequest<MIXUserPreference>>(IQ::Set, channelJID_, prefPayload, iqRouter_);
+    request->onResponse.connect(boost::bind(&MIXImpl::handlePreferencesResultReceived, this, _1, _2));
+    request->send();
+}
+
+}
diff --git a/Swiften/MIX/MIXImpl.h b/Swiften/MIX/MIXImpl.h
new file mode 100644
index 0000000..58b33f4
--- /dev/null
+++ b/Swiften/MIX/MIXImpl.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2017 Tarun Gupta
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#pragma once
+
+#include <Swiften/MIX/MIX.h>
+
+namespace Swift {
+    class StanzaChannel;
+    class IQRouter;
+
+    class SWIFTEN_API MIXImpl : public MIX {
+        public:
+            using ref = std::shared_ptr<MIXImpl>;
+
+        public:
+            MIXImpl(const JID& ownJID, const JID& channelJID, IQRouter* iqRouter);
+            virtual ~MIXImpl();
+
+            /**
+             * Returns the (bare) JID of the user.
+             */
+            virtual JID getJID() const {
+                return ownJID_.toBare();
+            }
+
+            /**
+             * Returns the JID of MIX channel.
+             */
+            virtual JID getChannelJID() const {
+                return channelJID_;
+            }
+
+            virtual void joinChannel(const std::unordered_set<std::string>& nodes) override;
+
+            virtual void joinChannelWithPreferences(const std::unordered_set<std::string>& nodes, Form::ref form) override;
+
+            virtual void updateSubscription(const std::unordered_set<std::string>& nodes) override;
+
+            virtual void leaveChannel() override;
+
+            virtual void requestPreferencesForm() override;
+
+            virtual void updatePreferences(Form::ref form) override;
+
+        private:
+            void handleJoinResponse(MIXJoin::ref, ErrorPayload::ref);
+            void handleLeaveResponse(MIXLeave::ref, ErrorPayload::ref);
+            void handleUpdateSubscriptionResponse(MIXUpdateSubscription::ref, ErrorPayload::ref);
+            void handlePreferencesFormReceived(MIXUserPreference::ref, ErrorPayload::ref);
+            void handlePreferencesResultReceived(MIXUserPreference::ref /*payload*/, ErrorPayload::ref error);
+
+        private:
+            JID ownJID_;
+            JID channelJID_;
+            IQRouter* iqRouter_;
+    };
+}
diff --git a/Swiften/MIX/UnitTest/MIXImplTest.cpp b/Swiften/MIX/UnitTest/MIXImplTest.cpp
new file mode 100644
index 0000000..05dde17
--- /dev/null
+++ b/Swiften/MIX/UnitTest/MIXImplTest.cpp
@@ -0,0 +1,312 @@
+/*
+ * Copyright (c) 2017 Tarun Gupta
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#include <gtest/gtest.h>
+
+#include <boost/bind.hpp>
+
+#include <Swiften/Elements/FormField.h>
+#include <Swiften/Client/DummyStanzaChannel.h>
+#include <Swiften/MIX/MIXImpl.h>
+#include <Swiften/Queries/IQRouter.h>
+
+using namespace Swift;
+
+class MIXImplTest : public ::testing::Test {
+
+    protected:
+        void SetUp() {
+            ownJID_ = JID("hag66@shakespeare.example/UUID-a1j/7533");
+            channelJID_ = JID("coven@mix.shakespeare.example");
+            channel_ = new DummyStanzaChannel();
+            router_ = new IQRouter(channel_);
+            successfulJoins_ = 0;
+        }
+
+        void TearDown() {
+           delete router_;
+           delete channel_;
+        }
+
+        MIX::ref createMIXClient() {
+            auto mix = std::make_shared<MIXImpl>(ownJID_, channelJID_, router_);
+            mix->onJoinResponse.connect(boost::bind(&MIXImplTest::handleJoin, this, _1, _2));
+            mix->onLeaveResponse.connect(boost::bind(&MIXImplTest::handleLeave, this, _1, _2));
+            mix->onSubscriptionUpdateResponse.connect(boost::bind(&MIXImplTest::handleSubscriptionUpdate, this, _1, _2));
+            mix->onPreferencesFormResponse.connect(boost::bind(&MIXImplTest::handlePreferencesForm, this, _1, _2));
+            return mix;
+        }
+
+        void handleJoin(MIXJoin::ref joinPayload, ErrorPayload::ref error) {
+            if (joinPayload) {
+                ASSERT_FALSE(error);
+                ASSERT_TRUE(joinPayload->getJID());
+                ASSERT_EQ(*joinPayload->getJID(), JID("123456#coven@mix.shakespeare.example"));
+                if (joinPayload->getForm()) {
+                    preferenceForm_ = joinPayload->getForm();
+                }
+                ++successfulJoins_;
+                subscribedNodes_ = joinPayload->getSubscriptions();
+            }
+        }
+
+        void handleLeave(MIXLeave::ref leavePayload, ErrorPayload::ref error) {
+            ASSERT_TRUE(leavePayload);
+            ASSERT_FALSE(error);
+            ASSERT_EQ(static_cast<int>(0), subscribedNodes_.size());
+        }
+
+        void handleSubscriptionUpdate(MIXUpdateSubscription::ref payload, ErrorPayload::ref error) {
+            ASSERT_TRUE(payload);
+            ASSERT_FALSE(error);
+            if (payload) {
+                for (auto node : payload->getSubscriptions()) {
+                    subscribedNodes_.insert(node);
+                }
+            }
+        }
+
+        void handlePreferencesForm(Form::ref form, ErrorPayload::ref error) {
+            ASSERT_FALSE(error);
+            if (form) {
+                preferenceForm_ = form;
+            }
+        }
+
+        IQ::ref createJoinResult(const std::unordered_set<std::string>& nodes, Form::ref form) {
+            auto joinResultPayload = std::make_shared<MIXJoin>();
+            for (auto node : nodes) {
+                joinResultPayload->addSubscription(node);
+            }
+            if (form) {
+                joinResultPayload->setForm(form);
+            }
+            joinResultPayload->setJID(JID("123456#coven@mix.shakespeare.example"));
+            return IQ::createResult(ownJID_, channel_->sentStanzas[0]->getTo(), channel_->sentStanzas[0]->getID(), joinResultPayload);
+        }
+
+        IQ::ref createLeaveResult() {
+            auto leaveResultPayload = std::make_shared<MIXLeave>();
+            return IQ::createResult(ownJID_, channel_->sentStanzas[0]->getTo(), channel_->sentStanzas[0]->getID(), leaveResultPayload);
+        }
+
+        IQ::ref createError() {
+            return IQ::createError(ownJID_, channel_->sentStanzas[0]->getTo(), channel_->sentStanzas[0]->getID());
+        }
+
+        bool hasSubscription(const std::string& value) {
+            return std::find(subscribedNodes_.begin(), subscribedNodes_.end(), value) != subscribedNodes_.end();
+        }
+
+        JID ownJID_;
+        JID channelJID_;
+        DummyStanzaChannel* channel_;
+        IQRouter* router_;
+        int successfulJoins_;
+        Form::ref preferenceForm_;
+        std::unordered_set<std::string> subscribedNodes_;
+};
+
+TEST_F(MIXImplTest, testJoinError) {
+    MIX::ref testling = createMIXClient();
+    testling->joinChannel(std::unordered_set<std::string>());
+
+    ASSERT_EQ(1, static_cast<int>(channel_->sentStanzas.size()));
+    ASSERT_TRUE(channel_->isRequestAtIndex<MIXJoin>(0, ownJID_.toBare(), IQ::Set));
+
+    auto iq = channel_->getStanzaAtIndex<IQ>(0);
+    ASSERT_TRUE(iq);
+    ASSERT_TRUE(iq->getPayload<MIXJoin>());
+    ASSERT_FALSE(iq->getPayload<MIXJoin>()->getForm());
+    ASSERT_EQ(static_cast<size_t>(0), iq->getPayload<MIXJoin>()->getSubscriptions().size());
+
+    channel_->onIQReceived(createError());
+    ASSERT_EQ(static_cast<int>(0), successfulJoins_);
+    ASSERT_EQ(static_cast<int>(0), subscribedNodes_.size());
+}
+
+TEST_F(MIXImplTest, testJoinWithAllSubscriptions) {
+    MIX::ref testling = createMIXClient();
+    std::unordered_set<std::string> nodes;
+    nodes.insert(std::string("urn:xmpp:mix:nodes:messages"));
+    nodes.insert(std::string("urn:xmpp:mix:nodes:presence"));
+    nodes.insert(std::string("urn:xmpp:mix:nodes:participants"));
+    nodes.insert(std::string("urn:xmpp:mix:nodes:config"));
+
+    testling->joinChannel(nodes);
+
+    ASSERT_EQ(1, static_cast<int>(channel_->sentStanzas.size()));
+    ASSERT_TRUE(channel_->isRequestAtIndex<MIXJoin>(0, ownJID_.toBare(), IQ::Set));
+
+    auto iq = channel_->getStanzaAtIndex<IQ>(0);
+    ASSERT_TRUE(iq);
+    ASSERT_TRUE(iq->getPayload<MIXJoin>());
+    ASSERT_FALSE(iq->getPayload<MIXJoin>()->getForm());
+    ASSERT_EQ(static_cast<size_t>(4), iq->getPayload<MIXJoin>()->getSubscriptions().size());
+
+    channel_->onIQReceived(createJoinResult(nodes, nullptr));
+    ASSERT_EQ(static_cast<int>(1), successfulJoins_);
+    ASSERT_EQ(static_cast<int>(4), subscribedNodes_.size());
+}
+
+TEST_F(MIXImplTest, testJoinWithSomeSubscriptions) {
+    MIX::ref testling = createMIXClient();
+    std::unordered_set<std::string> nodes;
+    nodes.insert(std::string("urn:xmpp:mix:nodes:messages"));
+    nodes.insert(std::string("urn:xmpp:mix:nodes:presence"));
+    nodes.insert(std::string("urn:xmpp:mix:nodes:participants"));
+    nodes.insert(std::string("urn:xmpp:mix:nodes:config"));
+
+    testling->joinChannel(nodes);
+
+    ASSERT_EQ(1, static_cast<int>(channel_->sentStanzas.size()));
+    ASSERT_TRUE(channel_->isRequestAtIndex<MIXJoin>(0, ownJID_.toBare(), IQ::Set));
+
+    auto iq = channel_->getStanzaAtIndex<IQ>(0);
+    ASSERT_TRUE(iq);
+    ASSERT_TRUE(iq->getPayload<MIXJoin>());
+    ASSERT_FALSE(iq->getPayload<MIXJoin>()->getForm());
+    ASSERT_EQ(static_cast<size_t>(4), iq->getPayload<MIXJoin>()->getSubscriptions().size());
+
+    std::unordered_set<std::string> subscribedTo;
+    subscribedTo.insert(std::string("urn:xmpp:mix:nodes:messages"));
+
+    channel_->onIQReceived(createJoinResult(subscribedTo, nullptr));
+    ASSERT_EQ(static_cast<int>(1), successfulJoins_);
+    ASSERT_EQ(static_cast<int>(1), subscribedNodes_.size());
+    ASSERT_TRUE(hasSubscription(std::string("urn:xmpp:mix:nodes:messages")));
+}
+
+TEST_F(MIXImplTest, testLeaveChannel) {
+    MIX::ref testling = createMIXClient();
+    testling->leaveChannel();
+    ASSERT_EQ(1, static_cast<int>(channel_->sentStanzas.size()));
+    ASSERT_TRUE(channel_->isRequestAtIndex<MIXLeave>(0, ownJID_.toBare(), IQ::Set));
+
+    auto iq = channel_->getStanzaAtIndex<IQ>(0);
+    ASSERT_TRUE(iq);
+    ASSERT_TRUE(iq->getPayload<MIXLeave>());
+    ASSERT_TRUE(iq->getPayload<MIXLeave>()->getChannel());
+
+    channel_->onIQReceived(createLeaveResult());
+}
+
+TEST_F(MIXImplTest, testUpdateSubscription) {
+    MIX::ref testling = createMIXClient();
+    std::unordered_set<std::string> nodes;
+    nodes.insert(std::string("urn:xmpp:mix:nodes:messages"));
+    nodes.insert(std::string("urn:xmpp:mix:nodes:presence"));
+
+    testling->joinChannel(nodes);
+
+    ASSERT_EQ(1, static_cast<int>(channel_->sentStanzas.size()));
+    ASSERT_TRUE(channel_->isRequestAtIndex<MIXJoin>(0, ownJID_.toBare(), IQ::Set));
+
+    channel_->onIQReceived(createJoinResult(nodes, nullptr));
+    ASSERT_EQ(static_cast<int>(1), successfulJoins_);
+    ASSERT_EQ(static_cast<int>(2), subscribedNodes_.size());
+
+    nodes.clear();
+    nodes.insert(std::string("urn:xmpp:mix:nodes:participants"));
+    nodes.insert(std::string("urn:xmpp:mix:nodes:config"));
+
+    testling->updateSubscription(nodes);
+
+    ASSERT_EQ(2, static_cast<int>(channel_->sentStanzas.size()));
+    ASSERT_TRUE(channel_->isRequestAtIndex<MIXUpdateSubscription>(1, channelJID_, IQ::Set));
+
+    // fake response
+    auto subscriptionUpdate = std::make_shared<MIXUpdateSubscription>();
+    subscriptionUpdate->setSubscriptions(nodes);
+    subscriptionUpdate->setJID(JID("hag66@shakespeare.example"));
+    auto response = IQ::createResult(ownJID_, channel_->sentStanzas[1]->getTo(), channel_->sentStanzas[1]->getID(), subscriptionUpdate);
+
+    channel_->onIQReceived(response);
+    ASSERT_EQ(static_cast<int>(4), subscribedNodes_.size());
+    ASSERT_TRUE(hasSubscription(std::string("urn:xmpp:mix:nodes:participants")));
+    ASSERT_TRUE(hasSubscription(std::string("urn:xmpp:mix:nodes:config")));
+    ASSERT_TRUE(hasSubscription(std::string("urn:xmpp:mix:nodes:messages")));
+}
+
+TEST_F(MIXImplTest, testJoinWithPreference) {
+    MIX::ref testling = createMIXClient();
+    std::unordered_set<std::string> nodes;
+    nodes.insert(std::string("urn:xmpp:mix:nodes:messages"));
+    nodes.insert(std::string("urn:xmpp:mix:nodes:presence"));
+
+    auto parameters = std::make_shared<Form>();
+    parameters->setType(Form::Type::SubmitType);
+
+    auto fieldType = std::make_shared<FormField>(FormField::HiddenType);
+    fieldType->setName("FORM_TYPE");
+    fieldType->addValue("urn:xmpp:mix:0");
+    parameters->addField(fieldType);
+
+    auto fieldJIDVisibility = std::make_shared<FormField>();
+    fieldJIDVisibility->setName("JID Visibility");
+    fieldJIDVisibility->addValue("never");
+    parameters->addField(fieldJIDVisibility);
+
+    testling->joinChannelWithPreferences(nodes, parameters);
+
+    ASSERT_EQ(1, static_cast<int>(channel_->sentStanzas.size()));
+    ASSERT_TRUE(channel_->isRequestAtIndex<MIXJoin>(0, ownJID_.toBare(), IQ::Set));
+
+    //fake response
+    auto responseForm = std::make_shared<Form>();
+    responseForm->setType(Form::Type::ResultType);
+
+    auto fieldTypeResponse = std::make_shared<FormField>(FormField::HiddenType);
+    fieldTypeResponse->setName("FORM_TYPE");
+    fieldTypeResponse->addValue("urn:xmpp:mix:0");
+    responseForm->addField(fieldTypeResponse);
+
+    auto fieldJIDVisibilityResponse = std::make_shared<FormField>();
+    fieldJIDVisibilityResponse->setName("JID Visibility");
+    fieldJIDVisibilityResponse->addValue("never");
+    responseForm->addField(fieldJIDVisibilityResponse);
+
+    auto fieldprivateMessagesResponse = std::make_shared<FormField>();
+    fieldprivateMessagesResponse->setName("Private Messages");
+    fieldprivateMessagesResponse->addValue("allow");
+    responseForm->addField(fieldprivateMessagesResponse);
+
+    auto vCardResponse = std::make_shared<FormField>();
+    vCardResponse->setName("vCard");
+    vCardResponse->addValue("block");
+    responseForm->addField(vCardResponse);
+
+    channel_->onIQReceived(createJoinResult(nodes, responseForm));
+    ASSERT_EQ(static_cast<int>(1), successfulJoins_);
+    ASSERT_EQ(static_cast<int>(2), subscribedNodes_.size());
+    ASSERT_TRUE(preferenceForm_);
+
+    ASSERT_TRUE(preferenceForm_->getField("JID Visibility"));
+    ASSERT_EQ(std::string("never"), preferenceForm_->getField("JID Visibility")->getTextSingleValue());
+}
+
+TEST_F(MIXImplTest, preferenceFormRequest) {
+    MIX::ref testling = createMIXClient();
+    testling->requestPreferencesForm();
+
+    ASSERT_EQ(1, static_cast<int>(channel_->sentStanzas.size()));
+    ASSERT_TRUE(channel_->isRequestAtIndex<MIXUserPreference>(0, channelJID_, IQ::Get));
+
+    //fake response
+    auto responseForm = std::make_shared<Form>();
+    responseForm->setType(Form::Type::FormType);
+
+    auto fieldTypeResponse = std::make_shared<FormField>(FormField::HiddenType);
+    fieldTypeResponse->setName("FORM_TYPE");
+    fieldTypeResponse->addValue("urn:xmpp:mix:0");
+    responseForm->addField(fieldTypeResponse);
+
+    auto preferenceResponse = std::make_shared<MIXUserPreference>();
+    preferenceResponse->setData(responseForm);
+
+    channel_->onIQReceived(IQ::createResult(ownJID_, channel_->sentStanzas[0]->getTo(), channel_->sentStanzas[0]->getID(), preferenceResponse));
+    ASSERT_TRUE(preferenceForm_);
+}
diff --git a/Swiften/SConscript b/Swiften/SConscript
index 7f2a92b..d028a0c 100644
--- a/Swiften/SConscript
+++ b/Swiften/SConscript
@@ -164,6 +164,8 @@ if env["SCONS_STAGE"] == "build" :
             "Elements/IsodeIQDelegation.cpp",
             "Entity/Entity.cpp",
             "Entity/PayloadPersister.cpp",
+            "MIX/MIX.cpp",
+            "MIX/MIXImpl.cpp",
             "MUC/MUC.cpp",
             "MUC/MUCImpl.cpp",
             "MUC/MUCManager.cpp",
@@ -415,6 +417,7 @@ if env["SCONS_STAGE"] == "build" :
             File("LinkLocal/UnitTest/LinkLocalServiceBrowserTest.cpp"),
             File("LinkLocal/UnitTest/LinkLocalServiceInfoTest.cpp"),
             File("LinkLocal/UnitTest/LinkLocalServiceTest.cpp"),
+            File("MIX/UnitTest/MIXImplTest.cpp"),
             File("MUC/UnitTest/MUCTest.cpp"),
             File("MUC/UnitTest/MockMUC.cpp"),
             File("Network/UnitTest/HostAddressTest.cpp"),
-- 
cgit v0.10.2-6-g49f6