diff --git a/src/FillPdfLinkManipulatorInterface.php b/src/FillPdfLinkManipulatorInterface.php
index 9a849c043091c3080bb68ea8ceb964d0bc190bf4..cedd9442bc25b0aa12af35479ac4e6dc49f19726 100644
--- a/src/FillPdfLinkManipulatorInterface.php
+++ b/src/FillPdfLinkManipulatorInterface.php
@@ -62,8 +62,11 @@ interface FillPdfLinkManipulatorInterface {
    *       always be downloaded. TRUE if yes, otherwise NULL.
    *     flatten: false|null Flag indicating if the populated file should
    *       be flattened. FALSE if not, otherwise NULL.
+   *
+   * @throws \InvalidArgumentException
+   *   If $link contains no query string or doesn't specify a valid FillPdfForm.
    */
-  public static function parseLink(Url $link);
+  public function parseLink(Url $link);
 
   /**
    * Generates a FillPdf Url from the given parameters.
diff --git a/src/Service/FillPdfLinkManipulator.php b/src/Service/FillPdfLinkManipulator.php
index 80ac7395c9c712c07d6bafd51a11a5ee5c72a428..ec001a8f208e762c06fbf4a18cde8e289056641a 100644
--- a/src/Service/FillPdfLinkManipulator.php
+++ b/src/Service/FillPdfLinkManipulator.php
@@ -24,7 +24,15 @@ class FillPdfLinkManipulator implements FillPdfLinkManipulatorInterface {
     $path = $request->getUri();
     $request_url = $this->createUrlFromString($path);
 
-    return static::parseLink($request_url);
+    return $this->parseLink($request_url);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function parseUrlString($url) {
+    $link = $this->createUrlFromString($url);
+    return $this->parseLink($link);
   }
 
   /**
@@ -50,105 +58,124 @@ class FillPdfLinkManipulator implements FillPdfLinkManipulatorInterface {
   /**
    * {@inheritdoc}
    */
-  public static function parseLink(Url $link) {
+  public function parseLink(Url $link) {
     $query = $link->getOption('query');
 
     if (!$query) {
       throw new \InvalidArgumentException("This link doesn't specify a query string, so failing.");
     }
 
-    $request_context = [
-      'entity_ids' => [],
-      'fid' => NULL,
-      'sample' => FALSE,
-      'force_download' => FALSE,
-      'flatten' => TRUE,
-    ];
+    if (empty($query['fid'])) {
+      throw new \InvalidArgumentException('No FillPdfForm was specified in the query string, so failing.');
+    }
 
-    if (!empty($query['fid'])) {
-      $request_context['fid'] = $query['fid'];
+    $fillpdf_form = FillPdfForm::load($query['fid']);
+    if (!$fillpdf_form) {
+      throw new \InvalidArgumentException("The requested FillPdfForm doesn't exist, so failing.");
     }
-    else {
-      throw new \InvalidArgumentException('No FillPdfForm was specified in the query string, so failing.');
+
+    // Set the fid, merging in evaluated boolean flags.
+    $context = [
+      'fid' => $query['fid'],
+    ] + static::parseBooleanFlags($query);
+
+    // Early return if PDF is just to be populated with sample data.
+    if ($context['sample'] === TRUE) {
+      $context['entity_ids'] = [];
+      return $context;
     }
 
+    // No sample and no entities given, so try enriching with defaults.
+    if (empty($query['entity_id']) && empty($query['entity_ids'])) {
+      $default_entity_id = $fillpdf_form->default_entity_id->value;
+      if ($default_entity_id) {
+        $default_entity_type = $fillpdf_form->default_entity_type->value;
+        if (empty($default_entity_type)) {
+          $default_entity_type = 'node';
+        }
+        $query['entity_ids'] = [
+          $default_entity_type => [$default_entity_id => $default_entity_id],
+        ];
+      }
+    }
+
+    // Merge in parsed entities.
+    $context += static::parseEntityIds($query);
+
+    return $context;
+  }
+
+  /**
+   * Helper method parsing boolean flags.
+   *
+   * @param array $query
+   *   Array of query parameters.
+   *
+   * @return array
+   *   An associative array representing the request context.
+   *
+   * @internal
+   */
+  public static function parseBooleanFlags(array $query) {
+    $context = [
+      'force_download' => FALSE,
+      'flatten' => TRUE,
+      'sample' => FALSE,
+    ];
+
     if (isset($query['download']) && filter_var($query['download'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) === TRUE) {
-      $request_context['force_download'] = TRUE;
+      $context['force_download'] = TRUE;
     }
 
     if (isset($query['flatten']) && $query['flatten'] !== '' && filter_var($query['flatten'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) === FALSE) {
-      $request_context['flatten'] = FALSE;
+      $context['flatten'] = FALSE;
     }
 
-    // Early return if PDF is just to be populated with sample data.
     if (isset($query['sample']) && filter_var($query['sample'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) === TRUE) {
-      $request_context['sample'] = TRUE;
-      return $request_context;
+      $context['sample'] = TRUE;
     }
 
-    if (!empty($query['entity_type'])) {
-      $request_context['entity_type'] = $query['entity_type'];
+    return $context;
+  }
+
+  /**
+   * Helper method parsing entities.
+   *
+   * @param array $query
+   *   Array of query parameters.
+   *
+   * @return array
+   *   An associative array representing the request context.
+   *
+   * @internal
+   */
+  public static function parseEntityIds(array $query) {
+    if (!empty($query['entity_id'])) {
+      $query['entity_ids'] = [$query['entity_id']];
     }
 
-    if (!empty($query['entity_id']) || !empty($query['entity_ids'])) {
-      $entity_ids = (!empty($query['entity_id']) ? [$query['entity_id']] : $query['entity_ids']);
+    $context = [
+      'entity_ids' => [],
+    ];
+    if (!empty($query['entity_ids'])) {
+      $common_entity_type = !empty($query['entity_type']) ? $query['entity_type'] : 'node';
 
       // Re-key entity IDs so they can be loaded easily with loadMultiple().
-      // If we have type information, add it to the types array, and remove it
-      // in order to make sure we only store the ID in the entity_ids key.
-      foreach ($entity_ids as $entity_id) {
+      foreach ($query['entity_ids'] as $entity_id) {
         $entity_id_parts = explode(':', $entity_id);
 
         if (count($entity_id_parts) == 2) {
           $entity_type = $entity_id_parts[0];
           $entity_id = $entity_id_parts[1];
         }
-        elseif (!empty($request_context['entity_type'])) {
-          $entity_type = $request_context['entity_type'];
-        }
         else {
-          $entity_type = 'node';
-        }
-        $request_context['entity_ids'] += [
-          $entity_type => [],
-        ];
-
-        $request_context['entity_ids'][$entity_type][$entity_id] = $entity_id;
-      }
-    }
-    else {
-      // Populate defaults.
-      $fillpdf_form = FillPdfForm::load($request_context['fid']);
-
-      if (!$fillpdf_form) {
-        throw new \InvalidArgumentException("The requested FillPdfForm doesn't exist, so failing.");
-      }
-
-      $default_entity_id = $fillpdf_form->default_entity_id->value;
-      if ($default_entity_id) {
-        $default_entity_type = $fillpdf_form->default_entity_type->value;
-        if (empty($default_entity_type)) {
-          $default_entity_type = 'node';
+          $entity_type = $common_entity_type;
         }
-
-        $request_context['entity_ids'] = [
-          $default_entity_type => [$default_entity_id => $default_entity_id],
-        ];
+        $context['entity_ids'][$entity_type][$entity_id] = $entity_id;
       }
     }
 
-    // We've processed the shorthand forms, so unset them.
-    unset($request_context['entity_id'], $request_context['entity_type']);
-
-    return $request_context;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function parseUrlString($url) {
-    $link = $this->createUrlFromString($url);
-    return static::parseLink($link);
+    return $context;
   }
 
   /**
diff --git a/tests/src/Functional/LinkManipulatorTest.php b/tests/src/Functional/LinkManipulatorTest.php
index 9bb148da1cf8d60a183b48c437962aa110af33be..b7ee8aff8274be7dcb80224dea58752b3103f755 100644
--- a/tests/src/Functional/LinkManipulatorTest.php
+++ b/tests/src/Functional/LinkManipulatorTest.php
@@ -3,6 +3,8 @@
 namespace Drupal\Tests\fillpdf\Functional;
 
 use Drupal\Core\Url;
+use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\fillpdf\Traits\TestFillPdfTrait;
 
 /**
  * @coversDefaultClass \Drupal\fillpdf\Service\FillPdfLinkManipulator
@@ -11,7 +13,22 @@ use Drupal\Core\Url;
  *
  * @todo Convert into a unit test.
  */
-class LinkManipulatorTest extends FillPdfUploadTestBase {
+class LinkManipulatorTest extends BrowserTestBase {
+
+  use TestFillPdfTrait;
+
+  static public $modules = ['fillpdf_test'];
+  protected $profile = 'minimal';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->configureFillPdf();
+    $this->initializeUser();
+  }
 
   /**
    * Tests handling of a non-existing FillPdfForm ID.
@@ -47,4 +64,33 @@ class LinkManipulatorTest extends FillPdfUploadTestBase {
     $this->assertSession()->pageTextContains("The requested FillPdfForm doesn't exist, so failing.");
   }
 
+  /**
+   * Tests parsing a sample link.
+   */
+  public function testSampleLink() {
+    $this->uploadTestPdf('fillpdf_test_v3.pdf');
+    $form_id = $this->getLatestFillPdfForm();
+
+    // Prepare a query with the sample flag and all kinds of (redundant)
+    // entity parameters set.
+    $query = [
+      'fid' => $form_id,
+      'entity_type' => 'user',
+      'entity_id' => 3,
+      'entity_ids' => ['node:1', 'node:2'],
+      'sample' => TRUE,
+    ];
+    $url = Url::fromRoute('fillpdf.populate_pdf', [], ['query' => $query]);
+    $context = \Drupal::service('fillpdf.link_manipulator')->parseLink($url);
+
+    // Test 'fid' and 'sample' parameters are correctly set.
+    $this->assertEquals($form_id, $context['fid']);
+    $this->assertEquals(TRUE, $context['sample']);
+
+    // Make sure 'entity_ids' is empty and all other entity parameters stripped.
+    $this->assertEmpty($context['entity_ids']);
+    $this->assertArrayNotHasKey('entity_type', $context);
+    $this->assertArrayNotHasKey('entity_id', $context);
+  }
+
 }
diff --git a/tests/src/Unit/LinkManipulator/ParseLinkBooleansTest.php b/tests/src/Unit/LinkManipulator/ParseBooleanFlagsTest.php
similarity index 60%
rename from tests/src/Unit/LinkManipulator/ParseLinkBooleansTest.php
rename to tests/src/Unit/LinkManipulator/ParseBooleanFlagsTest.php
index 1476d7199acb7206b90fcb2b2df4c0f6c3701888..8050f0cee727ad05a1bd50709d56b2cb5ea702db 100644
--- a/tests/src/Unit/LinkManipulator/ParseLinkBooleansTest.php
+++ b/tests/src/Unit/LinkManipulator/ParseBooleanFlagsTest.php
@@ -3,51 +3,50 @@
 namespace Drupal\Tests\fillpdf\Unit\LinkManipulator;
 
 use Drupal\fillpdf\Service\FillPdfLinkManipulator;
-use Drupal\Core\Url;
 use Drupal\Tests\UnitTestCase;
 
 /**
- * @covers \Drupal\fillpdf\Service\FillPdfLinkManipulator::parseLink
+ * @coversDefaultClass \Drupal\fillpdf\Service\FillPdfLinkManipulator
  *
  * @group fillpdf
  */
-class ParseLinkBooleansTest extends UnitTestCase {
+class ParseBooleanFlagsTest extends UnitTestCase {
 
   /**
    * Tests &sample=, &download= and &flatten= query parameters.
    *
-   * @dataProvider dataProvider
+   * @covers ::parseBooleanFlags
+   *
+   * @dataProvider providerTestBooleanFlags
    */
-  public function testBooleans($input, $expected) {
-    $request_context = FillPdfLinkManipulator::parseLink($this->link($input));
+  public function testBooleanFlags($input, $expected) {
+    $context = FillPdfLinkManipulator::parseBooleanFlags($this->buildQuery($input));
 
-    $this->assertEquals(is_null($expected) ? FALSE : $expected, $request_context['sample']);
+    $this->assertEquals(is_null($expected) ? FALSE : $expected, $context['sample']);
 
-    $this->assertEquals(is_null($expected) ? FALSE : $expected, $request_context['force_download']);
+    $this->assertEquals(is_null($expected) ? FALSE : $expected, $context['force_download']);
 
-    $this->assertEquals(is_null($expected) ? TRUE : $expected, $request_context['flatten']);
+    $this->assertEquals(is_null($expected) ? TRUE : $expected, $context['flatten']);
   }
 
   /**
-   * Input helper for testBooleans().
+   * Input helper for testBooleanFlags().
    */
-  public function link($input) {
-    return Url::fromRoute('fillpdf.populate_pdf', [], [
-      'query' => [
-        'fid' => 1,
-        'entity_type' => 'node',
-        'entity_id' => 1,
-        'sample' => $input,
-        'download' => $input,
-        'flatten' => $input,
-      ],
-    ]);
+  public function buildQuery($input) {
+    return [
+      'fid' => 1,
+      'entity_type' => 'node',
+      'entity_id' => 1,
+      'sample' => $input,
+      'download' => $input,
+      'flatten' => $input,
+    ];
   }
 
   /**
-   * Data provider for testBooleans().
+   * Data provider for testBooleanFlags().
    */
-  public function dataProvider() {
+  public function providerTestBooleanFlags() {
     return [
       ['1', TRUE],
       ['true', TRUE],
diff --git a/tests/src/Unit/LinkManipulator/ParseEntityIdsTest.php b/tests/src/Unit/LinkManipulator/ParseEntityIdsTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..010d1b96846072994eb4c180b893ced7ee6f8f48
--- /dev/null
+++ b/tests/src/Unit/LinkManipulator/ParseEntityIdsTest.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace Drupal\Tests\fillpdf\Unit\LinkManipulator;
+
+use Drupal\fillpdf\Service\FillPdfLinkManipulator;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\fillpdf\Service\FillPdfLinkManipulator
+ *
+ * @group fillpdf
+ */
+class ParseEntityIdsTest extends UnitTestCase {
+
+  /**
+   * Tests parsing entities.
+   *
+   * @covers ::parseEntityIds
+   *
+   * @dataProvider providerTestEntityIds
+   */
+  public function testEntityIds($entity_ids, $entity_type, $entity_id, $expected) {
+    $context = FillPdfLinkManipulator::parseEntityIds($this->buildQuery($entity_ids, $entity_type, $entity_id));
+
+    $this->assertEquals($expected, $context['entity_ids']);
+  }
+
+  /**
+   * Input helper for testEntityIds().
+   */
+  public function buildQuery($entity_ids, $entity_type, $entity_id) {
+    $query = [
+      'fid' => 1,
+    ];
+    if (!empty($entity_ids)) {
+      $query['entity_ids'] = $entity_ids;
+    }
+    if (!empty($entity_type)) {
+      $query['entity_type'] = $entity_type;
+    }
+    if (!empty($entity_id)) {
+      $query['entity_id'] = $entity_id;
+    }
+    return $query;
+  }
+
+  /**
+   * Data provider for testEntityIds().
+   */
+  public function providerTestEntityIds() {
+    $cases = [];
+    $cases[0] = [
+      [], NULL, NULL, [],
+    ];
+    $cases[1] = [
+      ['node:1'], NULL, NULL, ['node' => [1 => '1']],
+    ];
+    $cases[2] = [
+      ['term:5'], NULL, NULL, ['term' => [5 => '5']],
+    ];
+    $cases[3] = [
+      ['node:1', 'node:2'], NULL, NULL, ['node' => [1 => '1', 2 => '2']],
+    ];
+    $cases[4] = [
+      ['node:1', 'node:1'], NULL, NULL, ['node' => [1 => '1']],
+    ];
+    $cases[5] = [
+      ['user:3', 'term:5'], NULL, NULL, ['user' => [3 => '3'], 'term' => [5 => '5']],
+    ];
+    $cases[6] = [
+      NULL, NULL, 1, ['node' => [1 => '1']],
+    ];
+    $cases[7] = [
+      NULL, 'term', 5, ['term' => [5 => '5']],
+    ];
+    $cases[8] = [
+      ['1'], 'node', NULL, ['node' => [1 => '1']],
+    ];
+    $cases[9] = [
+      ['1', '2'], 'node', NULL, ['node' => [1 => '1', 2 => '2']],
+    ];
+    $cases[10] = [
+      ['3', '4'], 'user', NULL, ['user' => [3 => '3', 4 => '4']],
+    ];
+    $cases[11] = [
+      ['3', '4'], 'user', 5, ['user' => [5 => '5']],
+    ];
+    return $cases;
+  }
+
+}
diff --git a/tests/src/Unit/LinkManipulator/ParseLinkSampleTest.php b/tests/src/Unit/LinkManipulator/ParseLinkSampleTest.php
deleted file mode 100644
index 39d12666ad994d5685684180428943fe47463e63..0000000000000000000000000000000000000000
--- a/tests/src/Unit/LinkManipulator/ParseLinkSampleTest.php
+++ /dev/null
@@ -1,80 +0,0 @@
-<?php
-
-namespace Drupal\Tests\fillpdf\Unit\LinkManipulator;
-
-use Drupal\fillpdf\Service\FillPdfLinkManipulator;
-use Drupal\Core\Url;
-use Drupal\Tests\UnitTestCase;
-
-/**
- * @covers \Drupal\fillpdf\Service\FillPdfLinkManipulator::parseLink
- *
- * @group fillpdf
- */
-class ParseLinkSampleTest extends UnitTestCase {
-
-  /**
-   * Tests boolean query parameters.
-   *
-   * @dataProvider dataProvider
-   */
-  public function testSample($sample, $entity_ids, $entity_type = NULL, $entity_id = NULL) {
-    $request_context = FillPdfLinkManipulator::parseLink($this->link($sample, $entity_ids, $entity_type, $entity_id));
-
-    // Test '&fid=' is set.
-    $this->assertEquals(1, $request_context['fid']);
-
-    // Test '&entity_ids=' is only set if '&sample=' isn't.
-    if ($request_context['sample']) {
-      $this->assertEmpty($request_context['entity_ids']);
-    }
-    else {
-      $expected = [
-        'node' => ['1' => '1'],
-      ];
-      if (is_array($entity_ids) && count($entity_ids) == 2) {
-        $expected['node']['2'] = '2';
-      }
-      $this->assertEquals($expected, $request_context['entity_ids']);
-    }
-  }
-
-  /**
-   * Input helper for testBooleanFlags().
-   */
-  public function link($sample, $entity_ids, $entity_type = NULL, $entity_id = NULL) {
-    $query = [
-      'fid' => 1,
-      'sample' => $sample,
-    ];
-
-    if (!empty($entity_ids)) {
-      $query['entity_ids'] = $entity_ids;
-    }
-    if (!empty($entity_type)) {
-      $query['entity_type'] = $entity_type;
-    }
-    if (!empty($entity_id)) {
-      $query['entity_id'] = $entity_id;
-    }
-
-    return Url::fromRoute('fillpdf.populate_pdf', [], ['query' => $query]);
-  }
-
-  /**
-   * Data provider for testSample().
-   *
-   * @todo Mock FillPdfForm::load() so we can also test default entities.
-   */
-  public function dataProvider() {
-    return [
-      ['true',  ['node:1']],
-      ['false', ['node:1']],
-      ['true',  ['node:1', 'node:2']],
-      ['false', ['node:1', 'node:2']],
-      ['true',  NULL, 'node', '1'],
-      ['false', NULL, 'node', '1'],
-    ];
-  }
-
-}