diff --git a/crm_core/contact/src/Tests/EventProcessingTest.php b/crm_core/contact/src/Tests/EventProcessingTest.php
new file mode 100644
index 0000000..0755935
--- /dev/null
+++ b/crm_core/contact/src/Tests/EventProcessingTest.php
@@ -0,0 +1,296 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\collect_crm_core_contact\Tests\EventProcessingTest.
+ */
+
+namespace Drupal\collect_crm_core_contact\Tests;
+
+use Drupal\collect\CollectContainerInterface;
+use Drupal\collect\Entity\Container;
+use Drupal\collect\Event\CollectEvent;
+use Drupal\crm_core_activity\Entity\Activity;
+use Drupal\crm_core_contact\Entity\Contact;
+use Drupal\crm_core_default_matching_engine\Entity\MatchingRule;
+use Drupal\simpletest\KernelTestBase;
+
+/**
+ * Tests the processing of contact form submission on integration level.
+ *
+ * @group collect
+ */
+class EventProcessingTest extends KernelTestBase {
+
+  /**
+   * Disabled config schema checking temporarily until all errors are resolved.
+   */
+  protected $strictConfigSchema = FALSE;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = array(
+    'user',
+    'serialization',
+    'rest',
+    'hal',
+    'collect',
+    'field',
+    'text',
+    'datetime',
+    'filter',
+    'entity_reference',
+    'crm_core_contact',
+    'crm_core_activity',
+    'crm_core_match',
+    'crm_core_default_matching_engine',
+    'collect_crm_core_contact',
+  );
+
+  /**
+   * Test json data.
+   *
+   * @var string
+   */
+  protected $json;
+
+  /**
+   * The submission to process.
+   *
+   * @var \Drupal\collect\CollectContainerInterface
+   */
+  protected $submission;
+
+  /**
+   * An existing contact.
+   *
+   * @var \Drupal\crm_core_contact\Entity\Contact
+   */
+  protected $contact;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installEntitySchema('collect_container');
+    $this->installEntitySchema('crm_core_contact');
+    $this->installEntitySchema('crm_core_activity');
+    $this->installEntitySchema('user');
+
+    $this->installConfig(array(
+      'crm_core_contact',
+      'crm_core_activity',
+      'collect_crm_core_contact',
+      'collect',
+    ));
+
+    \Drupal::configFactory()->getEditable('crm_core_match.engines')->set('default', array('status' => TRUE))->save();
+    /* @var \Drupal\crm_core_default_matching_engine\Entity\MatchingRule $rule */
+    $rule = MatchingRule::load('individual');
+    $rule->setStatus(TRUE);
+    $rule->threshold = 8;
+    $rule->rules = array(
+      'contact_remote_id' => array(
+        'value' => array(
+          'status' => '1',
+          'operator' => '=',
+          'options' => '',
+          'score' => '10',
+          'weight' => '0',
+        ),
+      ),
+      'contact_mail' => array(
+        'value' => array(
+          'status' => '1',
+          'operator' => '=',
+          'options' => '',
+          'score' => '9',
+          'weight' => '0',
+        ),
+      ),
+    );
+    $rule->save();
+
+    $this->json = file_get_contents(__DIR__ . '/../../tests/src/Unit/fixture.json');
+    $this->submission = Container::create(array(
+      'origin_uri' => 'http://localhost/entity/message/feedback/2494b3ba-158b-4066-9833-510bd72c82eb',
+      'date' => REQUEST_TIME,
+      'type' => 'application/json',
+      'schema_uri' => 'https://drupal.org/project/collect_client/contact',
+      'data' => $this->json,
+    ));
+    $this->submission->save();
+
+    $this->contact = Contact::create(array(
+      'type' => 'individual',
+      'name' => 'Aenean Risus',
+      'contact_mail' => 'anean@example.com',
+    ));
+    $this->contact->save();
+  }
+
+  /**
+   * Tests submission containing a user that does not match an existing contact.
+   */
+  public function testWithUserWithoutMatch() {
+    $this->triggerProcessing($this->submission);
+
+    $contact = Contact::load(2);
+    $this->assertNewUserContact($contact);
+
+    $activity = Activity::load(1);
+    $this->assertActivity($activity, $contact);
+  }
+
+  /**
+   * Tests submission containing a user that does match an existing contact.
+   */
+  public function testWithUserWithMatch() {
+    $this->contact->set('contact_remote_id', 'http://example.com/user/1');
+    $this->contact->save();
+
+    $this->triggerProcessing($this->submission);
+
+    $contact = Contact::load(2);
+    $this->assertNull($contact, 'No new contact was created.');
+
+    $contact = Contact::load(1);
+    $this->assertMatchContact($contact);
+    $activity = Activity::load(1);
+    $this->assertActivity($activity, $contact);
+  }
+
+  /**
+   * Tests submission not containing a user not matching an existing contact.
+   */
+  public function testWithoutUserWithoutMatch() {
+    $this->unsetUser();
+
+    $this->triggerProcessing($this->submission);
+
+    $contact = Contact::load(2);
+    $this->assertNewMessageContact($contact);
+    $activity = Activity::load(1);
+    $this->assertActivity($activity, $contact);
+  }
+
+  /**
+   * Tests submission not containing a user that does match an existing contact.
+   */
+  public function testWithoutUserWithMatch() {
+    $this->unsetUser();
+    $data = json_decode($this->json, TRUE);
+    $data['values']['mail'][0]['value'] = 'anean@example.com';
+    $this->json = json_encode($data);
+    $this->submission->setData($this->json);
+
+    $this->triggerProcessing($this->submission);
+
+    $contact = Contact::load(2);
+    $this->assertNull($contact, 'No new contact was created.');
+
+    $contact = Contact::load(1);
+    $this->assertMatchContact($contact);
+    $activity = Activity::load(1);
+    $this->assertActivity($activity, $contact);
+  }
+
+  /**
+   * Asserts an activity.
+   *
+   * An activity was created with values from the submission and linked to the
+   * container and contact
+   *
+   * @param Activity $activity
+   *   The activity to assert.
+   * @param Contact $contact
+   *   The contact expected to be linked with the activity.
+   */
+  protected function assertActivity(Activity $activity = NULL, Contact $contact = NULL) {
+    $this->assertNotNull($activity, 'New activity was created.');
+    if ($activity) {
+      $this->assertEqual('Aenean lacinia bibendum nulla sed consectetur', $activity->get('title')->value, 'Found expected activity title');
+      $data = json_decode($this->json, TRUE);
+      $this->assertEqual($data['values']['message'][0]['value'], $activity->get('activity_notes')->value, 'Found expected activity title');
+      if ($contact) {
+        $this->assertEqual($contact->id(), $activity->get('activity_participants')->target_id, 'Activity was assigned to the expected contact');
+      }
+      $this->assertEqual($this->submission->id(), $activity->get('activity_submission')->target_id, 'Activity was linked to the container record');
+      $this->assertNotNull($activity->get('activity_date')->value, 'Activity has date set');
+    }
+  }
+
+  /**
+   * Asserts a new user contact.
+   *
+   * A new contact was created with values from the user section of the
+   * submission.
+   *
+   * @param Contact $contact
+   *   The contact to assert.
+   */
+  protected function assertNewUserContact(Contact $contact = NULL) {
+    $this->assertNotNull($contact, 'New contact was created.');
+    if ($contact) {
+      $this->assertEqual('dapibus', $contact->get('name')->value, 'Contact is named \'dapibus\'');
+      $this->assertEqual('dapibus@example.com', $contact->get('contact_mail')->value, 'Contact mail is \'dapibus@example.com\'');
+      $this->assertEqual('http://example.com/user/1', $contact->get('contact_remote_id')->value, 'Remote identifier is \'http://example.com/user/1\'');
+    }
+  }
+
+  /**
+   * Asserts a new message contact.
+   *
+   * A new contact was created with values from the message part of the
+   * submission.
+   *
+   * @param Contact $contact
+   *   The contact to assert.
+   */
+  protected function assertNewMessageContact(Contact $contact = NULL) {
+    $this->assertNotNull($contact, 'New contact was created.');
+    if ($contact) {
+      $this->assertEqual('Ullamcorper Fermentum', $contact->get('name')->value, 'Contact is named \'Ullamcorper Fermentum\'');
+      $this->assertEqual('ullamcorper@example.com', $contact->get('contact_mail')->value, 'Contact mail is \'ullamcorper@example.com\'');
+      $this->assertNull($contact->get('contact_remote_id')->value, 'Remote identifier is undefined');
+    }
+  }
+
+  /**
+   * Asserts the value of a contact identified as a match.
+   *
+   * The existing contact was not overwritten.
+   *
+   * @param Contact $contact
+   *   The contact to assert.
+   */
+  protected function assertMatchContact(Contact $contact) {
+    $this->assertEqual('Aenean Risus', $contact->get('name')->value, 'Contact is named \'Aenean Risus\'');
+    $this->assertEqual('anean@example.com', $contact->get('contact_mail')->value, 'Contact mail is \'anean@example.com\'');
+  }
+
+  /**
+   * Triggers processing.
+   *
+   * @param \Drupal\collect\CollectContainerInterface $submission
+   *   The submission to be processed.
+   */
+  protected function triggerProcessing(CollectContainerInterface $submission) {
+    $event = new CollectEvent($submission);
+    /* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher */
+    $dispatcher = $this->container->get('event_dispatcher');
+    $dispatcher->dispatch(CollectEvent::NAME, $event);
+  }
+
+  /**
+   * Sets the user to null in the submission data.
+   */
+  protected function unsetUser() {
+    $data = json_decode($this->json, TRUE);
+    $data['user'] = NULL;
+    $this->json = json_encode($data);
+    $this->submission->setData($this->json);
+  }
+}
diff --git a/crm_core/contact/src/Tests/ProcessContactTest.php b/crm_core/contact/src/Tests/ProcessContactTest.php
new file mode 100644
index 0000000..a4496fb
--- /dev/null
+++ b/crm_core/contact/src/Tests/ProcessContactTest.php
@@ -0,0 +1,244 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\collect_crm_core_contact\Tests\ProcessContactTest\ProcessContactTest.
+ */
+
+namespace Drupal\collect_crm_core_contact\Tests;
+
+use Drupal\collect\Entity\Container;
+use Drupal\crm_core_activity\Entity\ActivityType;
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\node\Entity\Node;
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests a processing workflow for creating CRM Contacts from a container.
+ *
+ * @group collect
+ */
+class ProcessContactTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'collect_crm_core_contact',
+    'crm_core_activity_ui',
+    'entity_test',
+    // @todo Remove node dependency after https://www.drupal.org/node/2308745
+    'node',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    // Make Contact mail field visible.
+    entity_get_display('crm_core_contact', 'individual', 'default')
+      ->setComponent('contact_mail', [
+        'type' => 'email_mailto',
+      ])
+      ->save();
+
+    // Add fields to the test entity type: one for some kind of originator, one
+    // for some kind of recipient. One is a name and one is an email, to test
+    // different aspects of matching.
+    $this->createField('entity_test', 'gift', 'donor', 'email', 'Donor email');
+    $this->createField('entity_test', 'gift', 'recipient', 'string', 'Recipient name');
+
+    // Add activity type.
+    ActivityType::create([
+      'name' => t('Gift'),
+      'type' => 'gift',
+    ])->save();
+  }
+
+  /**
+   * Tests setting up contact matching through the processing configuration UI.
+   *
+   * An entity is created for a dummy entity type with a name and an email
+   * field. A CollectJSON schema is created to handle it, and its processing is
+   * set up to match CRM Contacts from containers and create a CRM Activity
+   * record. The post-processing is then triggered, and the resulting Contacts
+   * and the Activity are asserted.
+   */
+  public function testContactProcessing() {
+    // Create an entity.
+    $entity = EntityTest::create([
+      'type' => 'gift',
+      'name' => 'Cookie',
+      'donor' => 'jlennon@example.com',
+      'recipient' => 'Yoko',
+    ]);
+    $entity->save();
+
+    // Log in as Collect administrator.
+    $admin_user = $this->drupalCreateUser([
+      'administer collect',
+      'view any crm_core_contact entity',
+      'view any crm_core_activity entity',
+      'view test entity',
+      'administer default matching engine']);
+    $this->drupalLogin($admin_user);
+
+    // Enable CRM matching rule.
+    $this->drupalPostForm('admin/config/crm-core/match/default/edit/individual', [
+      'status' => TRUE,
+      'rules[name:value][status]' => TRUE,
+      'rules[name:value][operator]' => 'CONTAINS',
+      'rules[contact_mail:value][status]' => TRUE,
+      'rules[contact_mail:value][operator]' => '=',
+    ], t('Save'));
+
+    // Capture the created entity.
+    $this->drupalPostForm('admin/content/collect/capture', ['entity_type' => 'entity_test'], t('Select entity type'));
+    $this->drupalPostForm(NULL, ['operation' => 'single', 'entity' => 'Cookie (' . $entity->id() . ')'], t('Capture'));
+    $this->assertText('The Test entity entity has been captured');
+
+    // Create suggested schema.
+    $this->clickLink('Set up a Collect JSON schema');
+    $this->drupalPostForm(NULL, ['label' => 'User entity', 'id' => 'user_entity'], t('Save'));
+
+    // Edit schema processing workflow.
+    $this->drupalGet('admin/structure/collect-schemas/user_entity/processing');
+    $this->drupalPostForm(NULL, ['processor_add_select' => 'contact_matcher'], t('Add'));
+    $this->drupalPostForm(NULL, ['processor_add_select' => 'contact_matcher'], t('Add'));
+    $this->drupalPostForm(NULL, ['processor_add_select' => 'activity_creator'], t('Add'));
+    $this->assertText('Matches or creates a CRM Core Contact entity.');
+    $this->assertText('Creates a CRM Core Activity entity, including matched contacts.');
+    // Form submission is divided because field list is populated after
+    // selecting contact_type.
+    $this->drupalPostForm(NULL, [
+      'processors[0][settings][relation]' => 'from',
+      'processors[0][settings][contact_type]' => 'individual',
+      'processors[1][settings][relation]' => 'to',
+      'processors[1][settings][contact_type]' => 'individual',
+      'processors[2][settings][title_property]' => 'name',
+    ], t('Save'));
+    $this->drupalPostForm(NULL, [
+      // The donor is identified by email.
+      'processors[0][settings][fields][contact_mail][schema_property]' => 'donor',
+      // The recipient is identified by name.
+      'processors[1][settings][fields][name][schema_property]' => 'recipient',
+    ], t('Save'));
+
+    // Execute processing on the entity container.
+    $containers = Container::loadMultiple();
+    $user_container = end($containers);
+    \Drupal::service('collect.postprocessor')->process($user_container);
+
+    $contact_ids = \Drupal::entityQuery('crm_core_contact')->execute();
+
+    // Assert new CRM Contact was created.
+    $this->drupalGet('crm-core/contact');
+    // Find recipient's name.
+    $this->assertLink('Yoko');
+    // Click the nameless donor and find its email address.
+    $this->clickLink(t('Nameless #@id', ['@id' => current($contact_ids)]));
+    $this->assertText('jlennon@example.com');
+
+    // Assert new CRM Activity was created.
+    $this->drupalGet('crm-core/activity');
+    $this->clickLink('Cookie');
+    $this->assertLink(t('Nameless #@id', ['@id' => current($contact_ids)]));
+    $this->assertLink('Yoko');
+
+    // The next processing should match the existing contacts, and not create
+    // new ones.
+    \Drupal::service('collect.postprocessor')->process($user_container);
+    $this->drupalGet('crm-core/contact');
+    $this->assertEqual(2, count($this->xpath('//tbody/tr')));
+    // A new activity should be created.
+    $this->drupalGet('crm-core/activity');
+    $this->assertEqual(2, count($this->xpath('//tbody/tr')));
+  }
+
+  /**
+   * Creates a field on a given entity type.
+   */
+  protected function createField($entity_type, $bundle, $name, $type, $label) {
+    FieldStorageConfig::create([
+      'field_name' => $name,
+      'type' => $type,
+      'entity_type' => $entity_type,
+    ])->save();
+    FieldConfig::create([
+      'field_name' => $name,
+      'field_type' => $type,
+      'entity_type' => $entity_type,
+      'bundle' => $bundle,
+      'label' => $label,
+    ])->save();
+  }
+
+  /**
+   * Tests matching contacts through user URI.
+   */
+  public function testUserUriContactMatching() {
+    // Add a content type.
+    $this->drupalCreateContentType(['type' => 'article']);
+
+    // Log in as Collect administrator.
+    $admin_user = $this->drupalCreateUser([
+      'create article content',
+      'edit any article content',
+      'administer collect',
+      'view any crm_core_contact entity',
+      'view any crm_core_activity entity',
+      'view test entity',
+      'administer default matching engine'
+    ]);
+    $this->drupalLogin($admin_user);
+
+    // Create a node.
+    $entity = Node::create(['title' => 'Foo', 'type' => 'article']);
+    $entity->save();
+
+    // Capture the created entity.
+    $this->drupalPostForm('admin/content/collect/capture', ['entity_type' => 'node'], t('Select entity type'));
+    $this->drupalPostForm(NULL, ['operation' => 'single', 'entity' => 'Foo (' . $entity->id() . ')'], t('Capture'));
+
+    // Create suggested schema.
+    $this->clickLink('Set up a Collect JSON schema');
+    $this->drupalPostForm(NULL, [
+      'label' => 'Content entity',
+      'id' => 'collect_json_node_article'
+    ], t('Save'));
+
+    // Add a new contact matcher processor.
+    $this->drupalGet('admin/structure/collect-schemas/collect_json_node_article/processing');
+    $this->drupalPostForm(NULL, ['processor_add_select' => 'contact_matcher'], t('Add'));
+    $this->drupalPostForm(NULL, [
+      'processors[0][settings][relation]' => 'relation',
+      'processors[0][settings][contact_type]' => 'individual',
+    ], t('Save'));
+
+    // Edit existing entity and capture it in order to create a new CRM contact.
+    $entity->setTitle('Foo Bar');
+    $entity->save();
+    $this->drupalPostForm('admin/content/collect/capture', ['entity_type' => 'node'], t('Select entity type'));
+    $this->drupalPostForm(NULL, ['operation' => 'single', 'entity' => 'Foo Bar (' . $entity->id() . ')'], t('Capture'));
+
+    // Go to contacts and assert there is a new contact created.
+    $this->drupalGet('crm-core/contact');
+    $this->assertText('Nameless');
+    $this->assertText('Individual');
+    $this->assertEqual(count($this->xpath('//tbody/tr')), 1);
+
+    // Create a new node with the same user, and assert that new contact is not created.
+    $entity = Node::create(['title' => 'Pa ra pa pa', 'type' => 'article']);
+    $entity->save();
+    $this->drupalPostForm('admin/content/collect/capture', ['entity_type' => 'node'], t('Select entity type'));
+    $this->drupalPostForm(NULL, ['operation' => 'single', 'entity' => 'Pa ra pa pa (' . $entity->id() . ')'], t('Capture'));
+    $this->drupalGet('crm-core/contact');
+    $this->assertEqual(count($this->xpath('//tbody/tr')), 1);
+  }
+
+}
diff --git a/crm_core/contact/tests/src/Unit/EventSubscriberTest.php b/crm_core/contact/tests/src/Unit/EventSubscriberTest.php
new file mode 100644
index 0000000..fa235ed
--- /dev/null
+++ b/crm_core/contact/tests/src/Unit/EventSubscriberTest.php
@@ -0,0 +1,404 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\Tests\collect_crm_core_contact\Unit\EventSubscriberTest.
+ */
+
+namespace Drupal\Tests\collect_crm_core_contact\Unit;
+
+use Drupal\collect\Event\CollectEvent;
+use Drupal\collect_crm_core_contact\EventSubscriber;
+use Drupal\Tests\UnitTestCase;
+
+if (!defined('DATETIME_DATETIME_STORAGE_FORMAT') && !defined('DATETIME_STORAGE_TIMEZONE')) {
+  define('DATETIME_STORAGE_TIMEZONE', 'UTC');
+  define('DATETIME_DATETIME_STORAGE_FORMAT', 'Y-m-d\TH:i:s');
+}
+/**
+ * Tests the processing of contact form submissions.
+ *
+ * @group collect
+ */
+class EventSubscriberTest extends UnitTestCase {
+
+  /**
+   * The tested event subscriber.
+   *
+   * @var \Drupal\collect_crm_core_contact\EventSubscriber
+   */
+  protected $subscriber;
+
+  /**
+   * The entity manager service.
+   *
+   * @var \Drupal\Core\Entity\EntityManagerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $entityManager;
+
+  /**
+   * The mocked contact storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $contactStorage;
+
+  /**
+   * The mocked activity storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $activityStorage;
+
+  /**
+   * The mocked logger channel.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $logger;
+
+  /**
+   * The mocked contact matcher.
+   *
+   * @var \Drupal\crm_core_match\MatcherInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $matcher;
+
+  /**
+   * The mocked data container.
+   *
+   * @var \Drupal\collect\CollectContainerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $submission;
+
+  /**
+   * Test json data.
+   *
+   * @var string
+   */
+  protected $json;
+
+  /**
+   * The decoded json test data.
+   *
+   * @var array
+   */
+  protected $data;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->json = file_get_contents(__DIR__ . '/fixture.json');
+    $this->data = json_decode($this->json, TRUE);
+
+    $this->entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface');
+    $this->contactStorage = $this->getMock('Drupal\Core\Entity\EntityStorageInterface');
+    $this->activityStorage = $this->getMock('Drupal\Core\Entity\EntityStorageInterface');
+    $this->logger = $this->getMock('Drupal\Core\Logger\LoggerChannelInterface');
+    $this->matcher = $this->getMock('Drupal\crm_core_match\MatcherInterface');
+    $this->submission = $this->getMock('Drupal\collect\CollectContainerInterface');
+
+    $this->subscriber = new EventSubscriber($this->entityManager, $this->logger, $this->matcher);
+
+    $this->entityManager->expects($this->any())
+      ->method('getStorage')
+      ->will($this->returnValueMap(array(
+        array('crm_core_contact', $this->contactStorage),
+        array('crm_core_activity', $this->activityStorage),
+      )));
+
+    $this->submission->expects($this->any())
+      ->method('getSchemaUri')
+      ->will($this->returnValue('https://drupal.org/project/collect_client/contact'));
+  }
+
+  /**
+   * Tests the announced event subscription..
+   */
+  public function testGetSubscribedEvents() {
+    $expected_subscription = array(
+      'collect.process' => 'process',
+    );
+    $subscription = EventSubscriber::getSubscribedEvents();
+
+    $this->assertArrayEquals($expected_subscription, $subscription, 'Got subscription to event collect.process to method process.');
+  }
+
+  /**
+   * Tests the processing of an unknown mime type.
+   */
+  public function testProcessUnknownType() {
+    $event = new CollectEvent($this->submission);
+
+    $this->submission->expects($this->any())
+      ->method('getType')
+      ->will($this->returnValue('application/pdf'));
+
+    $this->logger->expects($this->once())
+      ->method('notice')
+      ->with('Unsupported MIME type {type} when processing submission with scheme {schema}', array(
+        'type' => 'application/pdf',
+        'schema' => 'https://drupal.org/project/collect_client/contact',
+      ));
+    $this->subscriber->process($event);
+  }
+
+  /**
+   * Tests the processing with a user present in the received data.
+   */
+  public function testProcessWithUser() {
+    $event = new CollectEvent($this->submission);
+    $contact = $this->getMockBuilder('Drupal\crm_core_contact\Entity\Contact')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $activity = $this->getMockBuilder('Drupal\crm_core_activity\Entity\Activity')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $this->submission->expects($this->any())
+      ->method('getType')
+      ->will($this->returnValue('application/json'));
+    $this->submission->expects($this->any())
+      ->method('getData')
+      ->will($this->returnValue($this->json));
+
+    $this->contactStorage->expects($this->once())
+      ->method('create')
+      ->with(array(
+        'type' => 'individual',
+        'contact_remote_id' => 'http://example.com/user/1',
+        'name' => 'dapibus',
+        'contact_mail' => array(
+          'dapibus@example.com',
+          'dapibus@example.com',
+        ),
+      ))
+      ->will($this->returnValue($contact));
+
+    $contact->expects($this->once())
+      ->method('save');
+
+    $this->activityStorage->expects($this->once())
+      ->method('create')
+      ->with(array(
+        'type' => 'contact',
+        'title' => $this->data['values']['subject'][0]['value'],
+        'activity_notes' => $this->data['values']['message'][0]['value'],
+        'activity_participants' => $contact,
+        'activity_submission' => $this->submission,
+        'activity_date' => '1970-01-01T00:00:00',
+      ))
+      ->will($this->returnValue($activity));
+
+    $activity->expects($this->once())
+      ->method('save');
+
+    $this->subscriber->process($event);
+  }
+
+  /**
+   * Tests the processing with no user present in the received data.
+   */
+  public function testProcessWithoutUser() {
+    $data = $this->data;
+    $data['user'] = NULL;
+
+    $event = new CollectEvent($this->submission);
+    $contact = $this->getMockBuilder('Drupal\crm_core_contact\Entity\Contact')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $activity = $this->getMockBuilder('Drupal\crm_core_activity\Entity\Activity')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $this->submission->expects($this->any())
+      ->method('getType')
+      ->will($this->returnValue('application/json'));
+    $this->submission->expects($this->any())
+      ->method('getData')
+      ->will($this->returnValue(json_encode($data)));
+
+    $this->contactStorage->expects($this->once())
+      ->method('create')
+      ->with(array(
+        'type' => 'individual',
+        'name' => 'Ullamcorper Fermentum',
+        'contact_mail' => array(
+          'ullamcorper@example.com',
+        ),
+      ))
+      ->will($this->returnValue($contact));
+
+    $contact->expects($this->once())
+      ->method('save');
+
+    $this->activityStorage->expects($this->once())
+      ->method('create')
+      ->will($this->returnValue($activity));
+
+    $activity->expects($this->once())
+      ->method('save');
+
+    $this->subscriber->process($event);
+  }
+
+  /**
+   * Tests the processing with a user present in the received data and a match.
+   */
+  public function testProcessWithUserAndUserMatch() {
+    $event = new CollectEvent($this->submission);
+    $contact = $this->getMockBuilder('Drupal\crm_core_contact\Entity\Contact')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $contact_match = $this->getMockBuilder('Drupal\crm_core_contact\Entity\Contact')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $activity = $this->getMockBuilder('Drupal\crm_core_activity\Entity\Activity')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $this->submission->expects($this->any())
+      ->method('getType')
+      ->will($this->returnValue('application/json'));
+    $this->submission->expects($this->any())
+      ->method('getData')
+      ->will($this->returnValue($this->json));
+
+    $this->contactStorage->expects($this->once())
+      ->method('create')
+      ->with(array(
+        'type' => 'individual',
+        'contact_remote_id' => 'http://example.com/user/1',
+        'name' => 'dapibus',
+        'contact_mail' => array(
+          'dapibus@example.com',
+          'dapibus@example.com',
+        ),
+      ))
+      ->will($this->returnValue($contact));
+
+    $this->matcher->expects($this->once())
+      ->method('match')
+      ->with($contact)
+      ->will($this->returnValue(array(42)));
+
+    $this->contactStorage->expects($this->once())
+      ->method('load')
+      ->with(42)
+      ->will($this->returnValue($contact_match));
+
+    $contact->expects($this->any())
+      ->method('get')
+      ->will($this->returnValueMap(array(
+        array('contact_remote_id', (object) array('value' => 'http://example.com/user/42')),
+        array('name', (object) array('value' => 'Amet Dolor')),
+        array('contact_mail', (object) array('value' => 'amet@example.com')),
+      )));
+    $contact_match->expects($this->never())
+      ->method('set');
+
+    $contact_match->expects($this->never())
+      ->method('save');
+
+    $this->activityStorage->expects($this->once())
+      ->method('create')
+      ->with(array(
+        'type' => 'contact',
+        'title' => $this->data['values']['subject'][0]['value'],
+        'activity_notes' => $this->data['values']['message'][0]['value'],
+        'activity_participants' => $contact_match,
+        'activity_submission' => $this->submission,
+        'activity_date' => '1970-01-01T00:00:00',
+      ))
+      ->will($this->returnValue($activity));
+
+    $activity->expects($this->once())
+      ->method('save');
+
+    $this->subscriber->process($event);
+  }
+
+  /**
+   * Tests the processing with no user present in the received data and a match.
+   */
+  public function testProcessWithoutUserAndUserMatch() {
+    $data = $this->data;
+    $data['user'] = NULL;
+
+    $event = new CollectEvent($this->submission);
+    $contact = $this->getMockBuilder('Drupal\crm_core_contact\Entity\Contact')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $contact_match = $this->getMockBuilder('Drupal\crm_core_contact\Entity\Contact')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $activity = $this->getMockBuilder('Drupal\crm_core_activity\Entity\Activity')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $this->submission->expects($this->any())
+      ->method('getType')
+      ->will($this->returnValue('application/json'));
+    $this->submission->expects($this->any())
+      ->method('getType')
+      ->will($this->returnValue('application/json'));
+
+    $this->submission->expects($this->any())
+      ->method('getData')
+      ->will($this->returnValue(json_encode($data)));
+
+    $this->contactStorage->expects($this->once())
+      ->method('create')
+      ->with(array(
+        'type' => 'individual',
+        'name' => 'Ullamcorper Fermentum',
+        'contact_mail' => array(
+          'ullamcorper@example.com',
+        ),
+      ))
+      ->will($this->returnValue($contact));
+
+    $this->matcher->expects($this->once())
+      ->method('match')
+      ->with($contact)
+      ->will($this->returnValue(array(42)));
+
+    $this->contactStorage->expects($this->once())
+      ->method('load')
+      ->with(42)
+      ->will($this->returnValue($contact_match));
+
+    $contact->expects($this->any())
+      ->method('get')
+      ->will($this->returnValueMap(array(
+        array('contact_remote_id', (object) array('value' => NULL)),
+        array('name', (object) array('value' => 'Amet Dolor')),
+        array('contact_mail', (object) array('value' => 'amet@example.com')),
+      )));
+
+    $contact_match->expects($this->never())
+      ->method('set');
+
+    $contact_match->expects($this->never())
+      ->method('save');
+
+    $this->activityStorage->expects($this->once())
+      ->method('create')
+      ->with(array(
+        'type' => 'contact',
+        'title' => $this->data['values']['subject'][0]['value'],
+        'activity_notes' => $this->data['values']['message'][0]['value'],
+        'activity_participants' => $contact_match,
+        'activity_submission' => $this->submission,
+        'activity_date' => '1970-01-01T00:00:00',
+      ))
+      ->will($this->returnValue($activity));
+
+    $activity->expects($this->once())
+      ->method('save');
+
+    $this->subscriber->process($event);
+  }
+}
diff --git a/crm_core/contact/tests/src/Unit/fixture.json b/crm_core/contact/tests/src/Unit/fixture.json
new file mode 100644
index 0000000..52d389d
--- /dev/null
+++ b/crm_core/contact/tests/src/Unit/fixture.json
@@ -0,0 +1,223 @@
+{
+  "user": {
+    "_links": {
+      "self": {
+        "href": "http://example.com/user/1"
+      },
+      "type": {
+        "href": "http://example.com/rest/type/user/user"
+      }
+    },
+    "uuid": [
+      {
+        "value": "6d8e652c-77ce-4c1a-9500-e1d59b4c80fc"
+      }
+    ],
+    "langcode": [
+      {
+        "value": "en"
+      }
+    ],
+    "preferred_langcode": [
+      {
+        "value": "und"
+      }
+    ],
+    "preferred_admin_langcode": [
+      {
+        "value": "und"
+      }
+    ],
+    "name": [
+      {
+        "value": "dapibus"
+      }
+    ],
+    "mail": [
+      {
+        "value": "dapibus@example.com"
+      }
+    ],
+    "timezone": [
+      {
+        "value": "Europe/Berlin"
+      }
+    ],
+    "status": [
+      {
+        "value": 1
+      }
+    ],
+    "created": [
+      {
+        "value": 1403211798
+      }
+    ],
+    "access": [
+      {
+        "value": 1403693295
+      }
+    ],
+    "login": [
+      {
+        "value": 1403212044
+      }
+    ],
+    "init": [
+      {
+        "value": "dapibus@example.com"
+      }
+    ],
+    "roles": [
+      {
+        "value": "authenticated"
+      },
+      {
+        "value": "administrator"
+      }
+    ]
+  },
+  "fields": {
+    "category": {
+      "type": "entity_reference",
+      "label": "Category ID",
+      "description": "The ID of the associated category.",
+      "required": true,
+      "properties": {
+        "target_id": {
+          "type": "string",
+          "label": "Entity ID",
+          "description": null
+        },
+        "entity": {
+          "type": "entity_reference",
+          "label": "Entity",
+          "description": "The referenced entity"
+        }
+      }
+    },
+    "name": {
+      "type": "string",
+      "label": "The sender's name",
+      "description": "The name of the person that is sending the contact message.",
+      "required": false,
+      "properties": {
+        "value": {
+          "type": "string",
+          "label": "Text value",
+          "description": null
+        }
+      }
+    },
+    "mail": {
+      "type": "email",
+      "label": "The sender's email",
+      "description": "The email of the person that is sending the contact message.",
+      "required": false,
+      "properties": {
+        "value": {
+          "type": "email",
+          "label": "Email value",
+          "description": null
+        }
+      }
+    },
+    "subject": {
+      "type": "string",
+      "label": "The message subject",
+      "description": "The subject of the contact message.",
+      "required": false,
+      "properties": {
+        "value": {
+          "type": "string",
+          "label": "Text value",
+          "description": null
+        }
+      }
+    },
+    "message": {
+      "type": "string",
+      "label": "The message text",
+      "description": "The text of the contact message.",
+      "required": false,
+      "properties": {
+        "value": {
+          "type": "string",
+          "label": "Text value",
+          "description": null
+        }
+      }
+    },
+    "copy": {
+      "type": "boolean",
+      "label": "Copy",
+      "description": "Whether to send a copy of the message to the sender.",
+      "required": false,
+      "properties": {
+        "value": {
+          "type": "boolean",
+          "label": "Boolean value",
+          "description": null
+        }
+      }
+    },
+    "recipient": {
+      "type": "entity_reference",
+      "label": "Recipient ID",
+      "description": "The ID of the recipient user for personal contact messages.",
+      "required": false,
+      "properties": {
+        "target_id": {
+          "type": "integer",
+          "label": "Entity ID",
+          "description": null
+        },
+        "entity": {
+          "type": "entity_reference",
+          "label": "Entity",
+          "description": "The referenced entity"
+        }
+      }
+    }
+  },
+  "values": {
+    "_links": {
+      "self": {
+        "href": ""
+      },
+      "type": {
+        "href": "http://client/rest/type/contact_message/feedback"
+      }
+    },
+    "category": [
+      {
+        "target_id": "feedback"
+      }
+    ],
+    "name": [
+      {
+        "value": "Ullamcorper Fermentum"
+      }
+    ],
+    "mail": [
+      {
+        "value": "ullamcorper@example.com"
+      }
+    ],
+    "subject": [
+      {
+        "value": "Aenean lacinia bibendum nulla sed consectetur"
+      }
+    ],
+    "message": [
+      {
+        "value": "Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Maecenas faucibus mollis interdum. Donec id elit non mi porta gravida at eget metus. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Integer posuere erat a ante venenatis dapibus posuere velit aliquet.\r\n\r\nVivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Nulla vitae elit libero, a pharetra augue. Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sed odio dui. Lorem ipsum dolor sit amet, consectetur adipiscing elit."
+      }
+    ],
+    "copy": [
+      {
+        "value": 0
+      }
+    ]
+  }
+}
