diff --git a/includes/mollom.class.inc b/includes/mollom.class.inc
index 661d1d2..93565c2 100644
--- a/includes/mollom.class.inc
+++ b/includes/mollom.class.inc
@@ -511,7 +511,7 @@ abstract class Mollom {
    * @see http://tools.ietf.org/html/rfc5849
    * @see Mollom::oAuthStrategy
    */
-  public function addAuthentication($method, $server, $path, &$data, &$headers) {
+  public function addAuthentication($method, $server, $path, array &$data, array &$headers) {
     $oauth['oauth_consumer_key'] = $this->publicKey;
     $oauth['oauth_version'] = '1.0';
     // Random string; must be unique across all requests with the same
diff --git a/mollom.module b/mollom.module
index 00c89dd..0de6074 100644
--- a/mollom.module
+++ b/mollom.module
@@ -2353,6 +2353,9 @@ function mollom_moderate_validate_oauth() {
   $publicKey = variable_get('mollom_public_key', '');
   $privateKey = variable_get('mollom_private_key', '');
   if ($publicKey === '' || $privateKey === '') {
+    _mollom_watchdog(array(
+      'Missing module configuration' => array(),
+    ), WATCHDOG_WARNING);
     return FALSE;
   }
 
@@ -2361,6 +2364,11 @@ function mollom_moderate_validate_oauth() {
 
   // Validate protocol parameters.
   if (!isset($header['oauth_consumer_key'], $header['oauth_nonce'], $header['oauth_timestamp'], $header['oauth_signature_method'], $header['oauth_signature'])) {
+    _mollom_watchdog(array(
+      'Missing protocol parameters' => array(),
+      '<p>Request: @request</p>' => array('@request' => $_SERVER['REQUEST_METHOD'] . ' ' . $_GET['q']),
+      'Headers:<pre>@headers</pre>' => array('@headers' => $header),
+    ), WATCHDOG_WARNING);
     return FALSE;
   }
 
@@ -2398,6 +2406,11 @@ function mollom_moderate_validate_oauth() {
 
   // Validate nonce.
   if (empty($header['oauth_nonce'])) {
+    _mollom_watchdog(array(
+      'Missing authentication nonce' => array(),
+      '<p>Request: @request</p>' => array('@request' => $_SERVER['REQUEST_METHOD'] . ' ' . $_GET['q']),
+      'Headers:<pre>@headers</pre>' => array('@headers' => $header),
+    ), WATCHDOG_WARNING);
     return FALSE;
   }
   if ($cached = cache_get('mollom_moderation_nonces', 'cache')) {
@@ -2431,7 +2444,18 @@ function mollom_moderate_validate_oauth() {
   $key = Mollom::rawurlencode($privateKey) . '&' . '';
   $signature = rawurlencode(base64_encode(hash_hmac('sha1', $base_string, $key, TRUE)));
 
-  return $signature === $sent_signature;
+  $valid = ($signature === $sent_signature);
+  if (!$valid) {
+    _mollom_watchdog(array(
+      'Invalid authentication signature' => array(),
+      '<p>Request: @request</p>' => array('@request' => $_SERVER['REQUEST_METHOD'] . ' ' . $_GET['q']),
+      'Headers:<pre>@headers</pre>' => array('@headers' => $header + array('oauth_signature' => $sent_signature)),
+      'Base string:<pre>@base_string</pre>' => array('@base_string' => $base_string),
+      'Expected signature:<pre>@signature</pre>' => array('@signature' => $signature),
+      //'Expected key:<pre>@key</pre>' => array('@key' => $key),
+    ), WATCHDOG_WARNING);
+  }
+  return $valid;
 }
 
 /**
@@ -2449,6 +2473,7 @@ function mollom_moderate_validate_oauth() {
  */
 function mollom_moderate($data, $action) {
   $result = FALSE;
+  $messages = array();
 
   // Verify that action is supported or respond with a 404.
   if (!in_array($action, array('approve', 'spam', 'delete'))) {
@@ -2461,23 +2486,29 @@ function mollom_moderate($data, $action) {
   if ($form_info) {
     // Programmatically invoke publish action.
     if ($action == 'approve') {
+      $messages[] = "Attempt to load $form_info[entity] entity via entity_load().";
       // @todo Abstract this. Possibly requires Entity module as dependency.
       $entity = entity_load($data->entity, array($data->id));
       if (isset($entity[$data->id])) {
         $entity = $entity[$data->id];
         $entity->status = 1;
         $function = $data->entity . '_save';
+        $messages[] = "Attempt to save $form_info[entity] entity via {$data->entity}_save().";
         if (function_exists($function)) {
           $function($entity);
+          $messages[] = "Updated status of $form_info[entity] entity via {$data->entity}_save().";
           $result = TRUE;
         }
       }
     }
     // Invoke entity delete multiple callback.
     elseif (!empty($form_info['entity delete multiple callback'])) {
-      if (function_exists($form_info['entity delete multiple callback'])) {
-        $function = $form_info['entity delete multiple callback'];
-        $function(array($data->id));
+      $delete_callback = $form_info['entity delete multiple callback'];
+      $messages[] = "Attempt to delete $form_info[entity] entity via $delete_callback().";
+      if (function_exists($delete_callback)) {
+        $delete_callback(array($data->id));
+        $messages[] = "Deleted $form_info[entity] entity via $delete_callback().";
+
         // Entity delete callbacks do not return success or failure, so we can
         // only assume success.
         $result = TRUE;
@@ -2485,6 +2516,7 @@ function mollom_moderate($data, $action) {
     }
     // Programmatically invoke delete confirmation form.
     elseif (isset($form_info['delete form'])) {
+      $messages[] = "Attempt to delete $form_info[entity] entity via {$form_info['delete form']} form.";
       if (isset($form_info['delete form file'])) {
         $file = $form_info['delete form file'] + array(
           'type' => 'inc',
@@ -2492,6 +2524,7 @@ function mollom_moderate($data, $action) {
           'name' => NULL,
         );
         module_load_include($file['type'], $file['module'], $file['name']);
+        $messages[] = "Loaded {$file['name']}.{$file['type']} in {$file['module']}.";
       }
       // Programmatic form submissions are not able to automatically use the
       // primary form submit button/action, so we need to resemble
@@ -2500,12 +2533,13 @@ function mollom_moderate($data, $action) {
       $form_state = form_state_defaults();
       // We assume that all delete confirmation forms take the fully loaded
       // entity as (only) argument.
+      $messages[] = "Attempt to load $form_info[entity] entity via entity_load().";
       $entities = entity_load($data->entity, array($data->id));
-      $form_state['build_info']['args'][] = reset($entities);
+      $form_state['build_info']['args'][] = $entities[$data->id];
       $form = drupal_retrieve_form($form_id, $form_state);
 
       $form_state['values'] = array();
-      $form_state['values']['mollom']['feedback'] = 0;
+      $form_state['values']['mollom']['feedback'] = '';
       // Take over the primary submit button of confirm_form().
       $form_state['values']['op'] = $form['actions']['submit']['#value'];
 
@@ -2525,6 +2559,8 @@ function mollom_moderate($data, $action) {
     }
   }
 
+  $messages = implode("\n", $messages);
+
   if ($result) {
     _mollom_watchdog(array(
       '%action moderation success for @entity @id' => array(
@@ -2532,26 +2568,28 @@ function mollom_moderate($data, $action) {
         '@entity' => $data->entity,
         '@id' => $data->id,
       ),
-      'Mollom data:<pre>@data</pre>' => array('@data' => (array) $data),
-      'Messages:<pre>@messages</pre>' => array('@messages' => drupal_get_messages()),
     ));
   }
   else {
+    // 400 is not really appropriate, but comes closest.
+    drupal_add_http_header('Status', '400 Unable to process moderation request');
+
     _mollom_watchdog(array(
       '%action moderation failure for @entity @id' => array(
         '%action' => $action,
         '@entity' => $data->entity,
         '@id' => $data->id,
       ),
+      'Form ID:<pre>@form_id</pre>' => array('@form_id' => $data->form_id),
+      'Actions:<pre>@actions</pre>' => array('@actions' => $messages),
       'Mollom data:<pre>@data</pre>' => array('@data' => (array) $data),
       'Mollom form:<pre>@form</pre>' => array('@form' => $form_info),
-      'Messages:<pre>@messages</pre>' => array('@messages' => drupal_get_messages()),
+      'System messages:<pre>@messages</pre>' => array('@messages' => drupal_get_messages()),
     ), WATCHDOG_ERROR);
   }
 
   // Report back result as success status.
   echo (int) $result;
-  drupal_exit();
 }
 
 /**
diff --git a/tests/mollom.test b/tests/mollom.test
index facfbe8..75de9fe 100644
--- a/tests/mollom.test
+++ b/tests/mollom.test
@@ -89,6 +89,12 @@ class MollomWebTestCase extends DrupalWebTestCase {
    */
   protected $createKeys = TRUE;
 
+  function __construct($test_id = NULL) {
+    parent::__construct($test_id);
+    // Hide this base class in assertions.
+    $this->skipClasses[__CLASS__] = TRUE;
+  }
+
   /**
    * Set up an administrative user account and testing keys.
    */
@@ -187,7 +193,6 @@ class MollomWebTestCase extends DrupalWebTestCase {
     $this->messages = array();
     $query = db_select('watchdog', 'w')
       ->fields('w')
-      ->condition('w.type', 'mollom')
       ->orderBy('w.timestamp', 'ASC');
 
     // The comparison logic applied in this function is a bit confusing, since
@@ -209,11 +214,11 @@ class MollomWebTestCase extends DrupalWebTestCase {
           $this->error($output, 'User notice');
         }
         else {
-          $this->pass($output, t('Watchdog'));
+          $this->pass($output, t('Watchdog: @type', array('@type' => $row->type)));
         }
       }
       else {
-        $this->fail($output, t('Watchdog'));
+        $this->fail($output, t('Watchdog: @type', array('@type' => $row->type)));
       }
       // In case a severe message is expected, non-severe messages always pass,
       // since we would trigger a false positive test failure otherwise.
@@ -1544,7 +1549,7 @@ class MollomAccessTestCase extends MollomWebTestCase {
     // settings page.
     $this->web_user = $this->drupalCreateUser(array_diff(module_invoke_all('perm'), array('administer mollom')));
     $this->drupalLogin($this->web_user);
-    $this->drupalGet('admin/config/content/mollom');
+    $this->drupalGet('admin/config/content/mollom', array('watchdog' => WATCHDOG_WARNING));
     $this->assertResponse(403);
   }
 }
@@ -3485,8 +3490,10 @@ class MollomAnalysisTestCase extends MollomWebTestCase {
     $this->drupalLogin($this->admin_user);
     // Verify that mollom_basic_test_form cannot be configured to put posts into
     // moderation queue.
+    $this->setProtection('mollom_basic_elements_test_form');
     $this->drupalGet('admin/config/content/mollom/manage/mollom_basic_elements_test_form');
     $this->assertNoFieldByName('mollom[discard]');
+
     // Configure mollom_test_form to accept bad posts.
     $this->setProtection('mollom_test_form', MOLLOM_MODE_ANALYSIS, NULL, array(
       'mollom[checks][profanity]' => TRUE,
@@ -3942,12 +3949,41 @@ class MollomModerationIntegrationTestCase extends MollomWebTestCase {
     $headers = array();
     $this->mollom->addAuthentication($method, $endpoint, $path, $data, $headers);
 
-    // Apply any OAuth header hacks.
-    foreach ($oauth_hacks as $name => $value) {
-      $headers['Authorization'] = preg_replace('@(' . $name . ')="[^"]*"@', '$1="' . $value . '"', $headers['Authorization']);
+    if (!empty($oauth_hacks)) {
+      // Extract OAuth header.
+      $input = $headers['Authorization'];
+      preg_match_all('@([^, =]+)="([^"]*)"@', $input, $header);
+      $oauth = array_combine($header[1], $header[2]);
+
+      // Apply any OAuth header hacks.
+      foreach ($oauth_hacks as $name => $value) {
+        $oauth[$name] = $value;
+      }
+      // PHPWTF: Can't access static methods on class in a property.
+      $class = $this->mollom;
+
+      // Regenerate signature based on hacked protocol parameters.
+      // Tests do not send additional GET/POST data at this point, so we do not
+      // account for that possibility.
+      // @see Mollom::addAuthentication()
+      if (!isset($oauth_hacks['oauth_signature'])) {
+        $oauth_query = $class::httpBuildQuery($oauth);
+        $base_string = implode('&', array(
+          $method,
+          $class::rawurlencode($endpoint . '/' . $path),
+          $class::rawurlencode($oauth_query),
+        ));
+        $key = $class::rawurlencode($this->mollom->privateKey) . '&' . '';
+        $oauth['oauth_signature'] = base64_encode(hash_hmac('sha1', $base_string, $key, TRUE));
+      }
+
+      foreach ($oauth as $key => $value) {
+        $oauth[$key] = $key . '="' . $class::rawurlencode($value) . '"';
+      }
+      $headers['Authorization'] = 'OAuth ' . implode(', ', $oauth);
     }
 
-    // curlexec() expects HTTP headers as "name: value" strings.
+    // curlExec() expects HTTP headers as "name: value" strings.
     $curl_headers = array();
     foreach ($headers as $name => $value) {
       $curl_headers[] = $name . ': ' . $value;
@@ -4013,8 +4049,8 @@ class MollomModerationIntegrationTestCase extends MollomWebTestCase {
     ));
 
     $this->mollomRequest('POST', $path);
+    $this->assertMollomWatchdogMessages(WATCHDOG_WARNING);
     $this->assertResponse(403);
-    $this->assertMollomWatchdogMessages();
     $record = mollom_test_load($data->id);
     $this->assertTrue(!empty($record) && $record['status'] == 0, 'Test post still exists.');
 
@@ -4024,14 +4060,20 @@ class MollomModerationIntegrationTestCase extends MollomWebTestCase {
     $this->drupalLogout();
 
     $replay_attack_nonce = md5(microtime() . mt_rand());
+
+    // Prime the nonce cache in order to test the replay attack.
+    $nonces = array();
+    $nonces[$replay_attack_nonce] = REQUEST_TIME;
+    cache_set('mollom_moderation_nonces', $nonces, 'cache');
+
     $tests = array(
       // Test entirely missing OAuth header throws no notices or exceptions.
       array('class' => array('oAuthStrategy' => '')),
       // Test OAuth query parameters are not supported.
       array('class' => array('oAuthStrategy' => 'query')),
       // Test missing or invalid public key.
-      array('class' => array('publicKey' => ''), 'watchdog' => WATCHDOG_WARNING),
-      array('class' => array('publicKey' => md5('foo')), 'watchdog' => WATCHDOG_WARNING),
+      array('class' => array('publicKey' => '')),
+      array('class' => array('publicKey' => md5('foo'))),
       // Test missing or invalid private key.
       // Only impacts the resulting oauth_signature and there is no Mollom log
       // message on a signature mismatch, just a regular 403 response.
@@ -4039,10 +4081,10 @@ class MollomModerationIntegrationTestCase extends MollomWebTestCase {
       array('class' => array('privateKey' => md5('foo'))),
       // Test nonce replay attack, more than once.
       array('hacks' => array('oauth_nonce' => $replay_attack_nonce)),
-      array('hacks' => array('oauth_nonce' => $replay_attack_nonce), 'watchdog' => WATCHDOG_WARNING),
-      array('hacks' => array('oauth_nonce' => $replay_attack_nonce), 'watchdog' => WATCHDOG_WARNING),
-      // Test invalid/oudated timestamp.
-      array('hacks' => array('oauth_timestamp' => REQUEST_TIME - 1000), 'watchdog' => WATCHDOG_WARNING),
+      array('hacks' => array('oauth_nonce' => $replay_attack_nonce)),
+      // Test invalid/outdated timestamp.
+      array('hacks' => array('oauth_timestamp' => REQUEST_TIME - 1000)),
+      array('hacks' => array('oauth_timestamp' => REQUEST_TIME + 1000)),
       // Test invalid signature.
       array('hacks' => array('oauth_signature' => base64_encode(hash_hmac('sha1', 'foo', 'bar', TRUE)))),
       // Test unsupported signature hashing algo.
@@ -4054,9 +4096,7 @@ class MollomModerationIntegrationTestCase extends MollomWebTestCase {
       $test += array(
         'class' => array(),
         'hacks' => array(),
-        // Watchdog log message assertion only accounts for Mollom messages,
-        // not for generic 403s.
-        'watchdog' => WATCHDOG_NOTICE,
+        'watchdog' => WATCHDOG_WARNING,
       );
       foreach ($test['class'] as $key => $value) {
         $this->mollomBackup->$key = $this->mollom->$key;
@@ -4064,8 +4104,8 @@ class MollomModerationIntegrationTestCase extends MollomWebTestCase {
       }
       // Execute test.
       $this->mollomRequest('POST', $path, $test['hacks']);
-      $this->assertResponse(403);
       $this->assertMollomWatchdogMessages($test['watchdog']);
+      $this->assertResponse(403);
       $record = mollom_test_load($data->id);
       $this->assertTrue(!empty($record) && $record['status'] == 0, 'Test post still exists.');
 
@@ -4090,13 +4130,13 @@ class MollomModerationIntegrationTestCase extends MollomWebTestCase {
       // Send a moderation request for the post.
       $path = 'mollom/moderate/' . $data->contentId . '/' . $action;
       $this->mollomRequest('POST', $path);
-      $this->assertMollomWatchdogMessages();
 
       $record = mollom_test_load($data->id);
       $new_data = mollom_data_load('mollom_test', $data->id);
 
       // Verify that the post was published.
       if ($action == 'approve') {
+        $this->assertMollomWatchdogMessages();
         $this->assertResponse(200);
         $this->assertSame('Publishing status', $record['status'], 1);
         $this->assertTrue($new_data, 'Mollom data for approved post still exists.');
@@ -4104,12 +4144,14 @@ class MollomModerationIntegrationTestCase extends MollomWebTestCase {
       }
       // Verify that the post was deleted.
       elseif ($action == 'spam' || $action == 'delete') {
+        $this->assertMollomWatchdogMessages();
         $this->assertResponse(200);
         $this->assertFalse($record, 'Test post not found.');
         $this->assertFalse($new_data, 'Moderated/deleted record no longer exists.');
       }
       // Verify that post still exists after invalid/unsupported action.
       else {
+        $this->assertMollomWatchdogMessages(WATCHDOG_WARNING);
         $this->assertResponse(404);
         $this->assertTrue(!empty($record) && $record['status'] == 0, 'Test post still exists.');
         $this->assertTrue($new_data->moderate, 'Post is not flagged as moderated.');
diff --git a/tests/mollom_test.module b/tests/mollom_test.module
index b43e221..2b206e9 100644
--- a/tests/mollom_test.module
+++ b/tests/mollom_test.module
@@ -40,6 +40,12 @@ function mollom_test_menu() {
     'page arguments' => array('mollom_test_form'),
     'access callback' => TRUE,
   );
+  $items['mollom-test/form/%mollom_test/delete'] = array(
+    'title' => 'Delete Mollom test item',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('mollom_test_delete_form', 2),
+    'access callback' => TRUE,
+  );
   return $items;
 }
 
@@ -60,8 +66,8 @@ function mollom_test_mollom_form_list() {
   $forms['mollom_test_form'] = array(
     'title' => 'Mollom test form',
     'entity' => 'mollom_test',
-    'entity delete multiple callback' => 'mollom_test_delete_multiple',
     'moderation callback' => 'mollom_test_mollom_form_moderation',
+    'delete form' => 'mollom_test_delete_form',
   );
   // The basic test form is identical to the mollom_test_form, but only
   // registers minimal information (e.g., no entity or moderation callback) to
@@ -73,6 +79,13 @@ function mollom_test_mollom_form_list() {
   $forms['mollom_basic_elements_test_form'] = array(
     'title' => 'Mollom basic elements test form',
   );
+  // Same as mollom_test_form, but supports an entity delete callback.
+  // @todo Test mollom/moderate endpoint with entity delete callback.
+  $forms['mollom_entity_test_form'] = array(
+    'title' => 'Mollom entity test form',
+    'entity' => 'mollom_test',
+    'entity delete multiple callback' => 'mollom_test_delete_multiple',
+  );
   return $forms;
 }
 
@@ -303,3 +316,30 @@ function mollom_test_delete_multiple(array $mids) {
     ->condition('mid', $mids)
     ->execute();
 }
+
+/**
+ * Form constructor for deleting a Mollom test item.
+ */
+function mollom_test_delete_form($form, &$form_state, $record) {
+  // @todo Mollom Test module inconsistently uses an array for $record, but
+  //   entity_load() always returns an object.
+  if (!is_object($record)) {
+    $record = (object) $record;
+  }
+  $form['mid'] = array('#type' => 'value', '#value' => $record->mid);
+  return confirm_form($form,
+    t('Are you sure you want to delete the %title?', array('%title' => $record->title)),
+    'mollom-test/form/' . $record->mid,
+    NULL,
+    t('Delete')
+  );
+}
+
+/**
+ * Form submission handler for mollom_test_delete_form().
+ */
+function mollom_test_delete_form_submit($form, &$form_state) {
+  mollom_test_delete_multiple(array($form_state['values']['mid']));
+  drupal_set_message('The record has been deleted.');
+  $form_state['redirect'] = 'mollom-test/form';
+}
