From 50ca1aec1a9a38bf64e3867929261ce33ec66f2d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jannis=20Mo=C3=9Fhammer?= <jannis.mosshammer@netways.de>
Date: Wed, 31 Jul 2013 10:59:34 +0200
Subject: [PATCH] Refactor URL class and test

The constructor of the class is now private, instantiation happens
with Url::fromRequest and Url::fromPath. Also updated all occurences
of Url and fixed the Qlink helper to not overwrite the baseUrl with null
values

refs #4381
---
 application/views/helpers/QUrl.php            |   2 +-
 application/views/helpers/Qlink.php           |   7 +-
 library/Icinga/File/Pdf.php                   |   2 +-
 library/Icinga/Web/Url.php                    | 312 +++++++++++----
 library/Icinga/Web/View/helpers/url.php       |   4 +-
 library/Icinga/Web/Widget/Dashboard.php       |   6 +-
 .../Icinga/Web/Widget/Dashboard/Component.php |   4 +-
 library/Icinga/Web/Widget/Tabs.php            |  14 +-
 .../controllers/SoapController.php            |   2 +-
 test/php/library/Icinga/Web/UrlTest.php       | 364 ++++++++++++++++++
 10 files changed, 617 insertions(+), 100 deletions(-)
 create mode 100644 test/php/library/Icinga/Web/UrlTest.php

diff --git a/application/views/helpers/QUrl.php b/application/views/helpers/QUrl.php
index 56f1d704e..fbd66b5e3 100644
--- a/application/views/helpers/QUrl.php
+++ b/application/views/helpers/QUrl.php
@@ -38,7 +38,7 @@ class Zend_View_Helper_QUrl extends Zend_View_Helper_Abstract
         } else {
             $params = array();
         }
-        return Url::create($url, $params);
+        return Url::fromPath($url, $params);
         $params = array_map('rawurlencode', $params);
         return $this->view->baseUrl(vsprintf($url, $params));
     }
diff --git a/application/views/helpers/Qlink.php b/application/views/helpers/Qlink.php
index 6747db786..7b456f625 100755
--- a/application/views/helpers/Qlink.php
+++ b/application/views/helpers/Qlink.php
@@ -79,9 +79,12 @@ class Zend_View_Helper_Qlink extends Zend_View_Helper_Abstract
             $url = $urlFormat;
             $uriParams = $url->getParams() + $uriParams;
         } else {
-            $url = Url::create($urlFormat);
+            $url = Url::fromPath($urlFormat);
+        }
+        $url->setParams($uriParams);
+        if ($baseUrl) {
+            $url->setBaseUrl($baseUrl);
         }
-        $url->setParams($uriParams)->setBaseUrl($baseUrl);
         return sprintf(
             '<a href="%s"%s>%s</a>',
 //            $this->getFormattedUrl($urlFormat, $uriParams, $baseUrl),
diff --git a/library/Icinga/File/Pdf.php b/library/Icinga/File/Pdf.php
index 0c8aebed1..289dfb692 100644
--- a/library/Icinga/File/Pdf.php
+++ b/library/Icinga/File/Pdf.php
@@ -12,7 +12,7 @@ define('K_TCPDF_EXTERNAL_CONFIG', true);
 
 //define('K_PATH_URL', 'http://net-test-icinga-vm1.adm.netways.de/develop/'); // ???
 // define('K_PATH_URL', '/var/www/net-test-icinga-vm1.adm.netways.de/develop/public'); // ???
-define('K_PATH_URL', (string) Url::create('/') === '/' ? '' : (string) Url::create('/')); // ???'/'));
+define('K_PATH_URL', (string) Url::fromPath('/') === '/' ? '' : (string) Url::fromPath('/')); // ???'/'));
 define('K_PATH_MAIN', dirname(ICINGA_LIBDIR) . '/public');
 define('K_PATH_FONTS', ICINGA_LIBDIR . '/vendor/tcpdf/fonts/');
 define('K_PATH_CACHE', ICINGA_LIBDIR . '/vendor/tcpdf/cache/');
diff --git a/library/Icinga/Web/Url.php b/library/Icinga/Web/Url.php
index 1637c6b7b..b6c085c72 100644
--- a/library/Icinga/Web/Url.php
+++ b/library/Icinga/Web/Url.php
@@ -3,108 +3,221 @@
 namespace Icinga\Web;
 
 use Icinga\Application\Icinga;
-use Icinga\Exception\ProgrammingError;
 
+/**
+ *  Url class that provides convenient access to parameters, allows to modify query parameters and
+ *  returns Urls reflecting all changes made to the url and to the parameters.
+ *
+ *  Direct instantiation is prohibited and should be done either with @see Url::fromRequest() or
+ *  @see Url::fromUrlString()
+ *
+ *  Currently, protocol, host and port are ignored and will be implemented when required
+ *
+ */
 class Url
 {
-    protected $params = array();
-    protected $path;
-    protected $baseUrl;
-    protected $request;
+    /**
+     * An array of all parameters stored in this Url
+     *
+     * @var array
+     */
+    private $params = array();
 
-    public function __construct($url, $params = null, $request = null)
+    /**
+     * The relative path of this Url, without query parameters
+     *
+     * @var string
+     */
+    private $path = '';
+
+    /**
+     * The baseUrl that will be appended to @see Url::$path in order to
+     * create an absolute Url
+     *
+     * @var string
+     */
+    private $baseUrl = '/';
+
+
+
+    private function __construct()
+    {
+    }
+
+    /**
+     * Create a new Url class representing the current request
+     *
+     * If $params are given, those will be added to the request's parameters
+     * and overwrite any existing parameters
+     *
+     * @param string $url           The string representation of the Url to parse
+     * @param array $params         Parameters that should additionally be considered for the Url
+     * @param Zend_Request $request A request to use instead of the default one
+     *
+     * @return Url
+     */
+    public static function fromRequest(array $params = array(), $request = null)
     {
         if ($request === null) {
-            $this->request = Icinga::app()->getFrontController()->getRequest();
-        } else {
-            // Tests only
-            $this->request = $request;
-        }
-        if ($url === null) {
-            $this->path   = $this->request->getPathInfo();
-            $this->params = $this->request->getQuery();
-        } else {
-            if (($split = strpos($url, '?')) === false) {
-                $this->path = $url;
-            } else {
-                $this->path = substr($url, 0, $split);
-                // TODO: Use something better than parse_str
-                parse_str(substr($url, $split + 1), $urlParams);
-                $this->params = $urlParams;
-            }
-        }
-        if (! empty($params)) {
-            $this->setParams($params);
+            $request = Icinga::app()->getFrontController()->getRequest();
         }
+
+        $urlObject = new Url();
+        $urlObject->setPath($request->getPathInfo());
+        $urlObject->setParams(array_merge($request->getQuery(), $params));
+        $urlObject->setBaseUrl($request->getBaseUrl());
+        return $urlObject;
     }
 
-    public static function create($url, $params = null, $request = null)
+    /**
+     * Create a new Url class representing the given url
+     *
+     * If $params are given, those will be added to the urls parameters
+     * and overwrite any existing parameters
+     *
+     * @param string $url               The string representation of the Url to parse
+     * @param array $params             An array of parameters that should additionally be considered for the Url
+     * @param Zend_Request $request A   request to use instead of the default one
+     *
+     * @return Url
+     */
+    public static function fromPath($url, array $params = array(), $request = null)
     {
-        $u = new Url($url, $params, $request);
-        return $u;
+        $urlObject = new Url();
+        if ($request === null) {
+            $request = Icinga::app()->getFrontController()->getRequest();
+        }
+        $urlObject->setBaseUrl($request->getBaseUrl());
+
+        $urlParts = parse_url($url);
+        if (isset($urlParts["path"])) {
+            $urlObject->setPath($urlParts["path"]);
+        }
+        if (isset($urlParts["query"])) {
+            parse_str($urlParts["query"], $urlParams);
+            $params = array_merge($urlParams, $params);
+        }
+
+        $urlObject->setParams($params);
+        return $urlObject;
     }
 
-    // For tests
+
+    /**
+     * Overwrite the baseUrl.
+     *
+     * If an empty Url is given '/' is used as the base
+     *
+     * @param string $baseUrl      The url path to use as the Url Base
+     * @return $this
+     */
     public function setBaseUrl($baseUrl)
     {
+        if (trim($baseUrl) ==  '') {
+            $baseUrl = '/';
+        }
         $this->baseUrl = $baseUrl;
         return $this;
     }
 
-    public static function current($request = null)
+    /**
+     * Set the relative path of this url, without query parameters
+     *
+     * @param string $path         The path to set
+     */
+    public function setPath($path)
     {
-        $url = new Url(null, null, $request);
-        return $url;
+        $this->path = $path;
     }
 
+    /**
+     * Return the relative path of this Url, without query parameters
+     *
+     * If you want the relative path with query parameters use getRelativeUrl
+     *
+     * @return string
+     */
     public function getPath()
     {
         return $this->path;
     }
 
-    public function getRelative()
+    /**
+     * Return the relative url with query parameters as a string
+     *
+     * @return string
+     */
+    public function getRelativeUrl()
     {
-        $params = $args = array();
-        foreach ($this->params as $name => $value) {
-            if (is_int($name)) {
-                $params[] = rawurlencode($value);
-            } else {
-                $args[] = rawurlencode($name) . '=' . rawurlencode($value);
-            }
-        }
-        $url = vsprintf($this->path, $params);
-        if (! empty($args)) {
-            $url .= '?' . implode('&amp;', $args);
-        }
-        return $url;
+        if (empty($this->params))
+            return ltrim($this->path,'/');
+        return ltrim($this->path,'/').'?'.http_build_query($this->params);
     }
 
-    public function addParams($params)
+    /**
+     * Return the absolute url with query parameters as a string
+     *
+     * @return string
+     */
+    public function getAbsoluteUrl()
+    {
+        $url = $this->getRelativeUrl();
+        $baseUrl = '/'.ltrim($this->baseUrl, '/');
+        return $baseUrl.'/'.$url;
+    }
+
+    /**
+     * Add a set of parameters to the query part
+     *
+     * @param array $params  The parameters to add
+     * @return $this
+     */
+    public function addParams(array $params)
     {
         $this->params += $params;
         return $this;
     }
 
-    public function setParams($params)
+    /**
+     * Overwrite the parameters used in the query part
+     *
+     * @param array $params     The new parameters to use for the query part
+     * @return $this
+     */
+    public function setParams(array $params)
     {
-        if ($params === null) {
-            $this->params = array();
-        } else {
-            $this->params = $params;
-        }
+        $this->params = $params;
         return $this;
     }
 
+    /**
+     * Return all parameters that will be used in the query part
+     *
+     * @return array        An associative key => value array containing all parameters
+     */
     public function getParams()
     {
         return $this->params;
     }
 
+    /**
+     * Return true if the Urls' query parameter with $key exists, otherwise false
+     *
+     * @param $key      A key to check for existing
+     * @return bool
+     */
     public function hasParam($key)
     {
         return array_key_exists($key, $this->params);
     }
 
+    /**
+     * Return the Url's query parameter with the name $key if exists, otherwise $default
+     *
+     * @param $key              A query parameter name to return if existing
+     * @param mixed $default    A value to return when the parameter doesn't exist
+     * @return mixed
+     */
     public function getParam($key, $default = null)
     {
         if ($this->hasParam($key)) {
@@ -113,48 +226,85 @@ class Url
         return $default;
     }
 
+    /**
+     * Set a single parameter $key, overwriting existing ones with the same key
+     *
+     * @param string $key           A string representing the key of the parameter
+     * @param array|string $value   An array or string to set as the parameter value
+     * @return $this
+     */
     public function setParam($key, $value)
     {
         $this->params[$key] = $value;
         return $this;
     }
 
-    public function remove()
+    /**
+     * Remove provided key (if string) or keys (if array of string) from the query parameter array
+     *
+     * @param string|array  $keyOrArrayOfKeys       An array of strings or a string representing the key(s)
+     *                                              of the parameters to be removed
+     * @return $this
+     */
+    public function remove($keyOrArrayOfKeys)
     {
-        $args = func_get_args();
-        foreach ($args as $keys) {
-            if (! is_array($keys)) {
-                $keys = array($keys);
-            }
-            foreach ($keys as $key) {
-                if (array_key_exists($key, $this->params)) {
-                    unset($this->params[$key]);
-                }
-            }
+        if (is_array($keyOrArrayOfKeys)) {
+            $this->removeKeys($keyOrArrayOfKeys);
+        } else {
+            $this->removeKey($keyOrArrayOfKeys);
         }
         return $this;
     }
 
-    public function without()
+    /**
+     * Remove all parameters with the parameter names in the $keys array
+     *
+     * @param array $keys   An array of strings containing parameter names to remove
+     * @return $this
+     */
+    public function removeKeys(array $keys)
     {
-        $url = clone($this);
-        $args = func_get_args();
-        return call_user_func_array(array($url, 'remove'), $args);
+        foreach ($keys as $key) {
+            $this->removeKey($key);
+        }
+        return $this;
     }
 
-    public function __toString()
+    /**
+     * Remove a single parameter with the provided parameter name $key
+     *
+     * @param string $key   The key to remove from the Url
+     * @return $this
+     */
+    public function removeKey($key)
     {
-        $url = $this->getRelative();
-        $base = null === $this->baseUrl
-            ? $this->request->getBaseUrl()
-            : $this->baseUrl;
-        if ($base === '' && $url[0]!== '/') {
-            // Otherwise all URLs would be relative to wherever you are
-            $base = '/';
+        if (isset($this->params[$key])) {
+            unset($this->params[$key]);
         }
-        if (strlen($base) > 0 && strlen($url) > 0 && $url[0] !== '?') {
-            $base = rtrim($base, '/') . '/';
-        }
-        return $base . $url;
+        return $this;
+    }
+
+    /**
+     * Return a copy of this url without the parameter given
+     *
+     * The argument can either a single query parameter name or an array of parameter names to
+     * remove from the query list
+     *
+     * @param string|array $keyOrArrayOfKeys    A single string or an array containing parameter names
+     * @return Url
+     */
+    public function getUrlWithout($keyOrArrayOfKeys)
+    {
+        $url = clone($this);
+        $url->remove($keyOrArrayOfKeys);
+        return $url;
+    }
+
+    /**
+     * Alias for @see Url::getAbsoluteUrl()
+     * @return mixed
+     */
+    public function __toString() {
+        return $this->getAbsoluteUrl();
     }
 }
diff --git a/library/Icinga/Web/View/helpers/url.php b/library/Icinga/Web/View/helpers/url.php
index 41a8608c7..eb07ebf9f 100644
--- a/library/Icinga/Web/View/helpers/url.php
+++ b/library/Icinga/Web/View/helpers/url.php
@@ -7,12 +7,12 @@ use Icinga\Web\Url;
 function url($path = null, $params = null)
 {
     if ($path === null) {
-        $url = Url::current();
+        $url = Url::fromRequest();
         if ($params !== null) {
             $url->setParams($params);
         }
     } else {
-        $url = Url::create($path, $params);
+        $url = Url::fromPath($path, $params);
     }
     return $url;
 }
diff --git a/library/Icinga/Web/Widget/Dashboard.php b/library/Icinga/Web/Widget/Dashboard.php
index 964a71e35..fb8770f25 100644
--- a/library/Icinga/Web/Widget/Dashboard.php
+++ b/library/Icinga/Web/Widget/Dashboard.php
@@ -24,7 +24,7 @@ class Dashboard extends AbstractWidget
     protected function init()
     {
         if ($this->url === null) {
-            $this->url = Url::current()->without($this->tabParam);
+            $this->url = Url::fromRequest()->without($this->tabParam);
         }
     }
 
@@ -141,7 +141,7 @@ class Dashboard extends AbstractWidget
     {
         $active = $this->tabs()->getActiveName();
         if (! $active) {
-            if ($active = Url::current()->getParam($this->tabParam)) {
+            if ($active = Url::fromRequest()->getParam($this->tabParam)) {
                 $this->activate($active);
             } else {
                 reset($this->panes);
@@ -186,7 +186,7 @@ class Dashboard extends AbstractWidget
                 unset($item['base_url']);
                 $this->getPane($dashboard)->addComponent(
                     $title,
-                    Url::create($base_url, $item)
+                    Url::fromPath($base_url, $item)
                 );
             }
         }
diff --git a/library/Icinga/Web/Widget/Dashboard/Component.php b/library/Icinga/Web/Widget/Dashboard/Component.php
index 81fe64d62..48ddd78e0 100644
--- a/library/Icinga/Web/Widget/Dashboard/Component.php
+++ b/library/Icinga/Web/Widget/Dashboard/Component.php
@@ -22,7 +22,7 @@ class Component
         if ($url instanceof Url) {
             $this->url = $url;
         } else {
-            $this->url = Url::create($url);
+            $this->url = Url::fromPath($url);
         }
     }
 
@@ -57,7 +57,7 @@ class Component
         if ($url instanceof Url) {
             $this->url = $url;
         } else {
-            $this->url = Url::create($url);
+            $this->url = Url::fromPath($url);
         }
         return $this;
     }
diff --git a/library/Icinga/Web/Widget/Tabs.php b/library/Icinga/Web/Widget/Tabs.php
index db568a277..8e9c3483d 100644
--- a/library/Icinga/Web/Widget/Tabs.php
+++ b/library/Icinga/Web/Widget/Tabs.php
@@ -204,34 +204,34 @@ class Tabs extends AbstractWidget implements Countable
         $special = array();
         $special[] = $this->view()->qlink(
             $this->view()->img('img/classic/application-pdf.png') . ' PDF',
-            Url::current(),
+            Url::fromRequest(),
             array('filetype' => 'pdf'),
             array('target' => '_blank', 'quote' => false)
         );
         $special[] = $this->view()->qlink(
             $this->view()->img('img/classic/application-csv.png') . ' CSV',
-            Url::current(),
+            Url::fromRequest(),
             array('format' => 'csv'),
             array('target' => '_blank', 'quote' => false)
         );
         $special[] = $this->view()->qlink(
             $this->view()->img('img/classic/application-json.png') . ' JSON',
-            Url::current(),
+            Url::fromRequest(),
             array('format' => 'json', 'quote' => false),
             array('target' => '_blank', 'quote' => false)
         );
 
         $special[] = $this->view()->qlink(
             $this->view()->img('img/classic/basket.png') . ' URL Basket',
-            Url::create('basket/add'),
-            array('url' => Url::current()->getRelative()),
+            Url::fromPath('basket/add'),
+            array('url' => Url::fromRequest()->getRelativeUrl()),
             array('quote' => false)
         );
 
         $special[] = $this->view()->qlink(
             $this->view()->img('img/classic/dashboard.png') . ' Dashboard',
-            Url::create('dashboard/addurl'),
-            array('url' => Url::current()->getRelative()),
+            Url::fromPath('dashboard/addurl'),
+            array('url' => Url::fromRequest()->getRelativeUrl()),
             array('quote' => false)
         );
         // $auth = Auth::getInstance();
diff --git a/modules/monitoring/application/controllers/SoapController.php b/modules/monitoring/application/controllers/SoapController.php
index d6a7a1d80..f849200ec 100644
--- a/modules/monitoring/application/controllers/SoapController.php
+++ b/modules/monitoring/application/controllers/SoapController.php
@@ -39,7 +39,7 @@ class Monitoring_SoapController extends ModuleActionController
             $wsdl->handle();
         } else {
             $wsdl->dump('/tmp/test.wsdl');
-            $uri = 'http://itenos-devel.tom.local/' . Url::create('monitoring/soap');
+            $uri = 'http://itenos-devel.tom.local/' . Url::fromPath('monitoring/soap');
             $server = new Zend_Soap_Server('/tmp/test.wsdl');
             $server->setClass('Api');
             $server->handle();
diff --git a/test/php/library/Icinga/Web/UrlTest.php b/test/php/library/Icinga/Web/UrlTest.php
new file mode 100644
index 000000000..06a9038ef
--- /dev/null
+++ b/test/php/library/Icinga/Web/UrlTest.php
@@ -0,0 +1,364 @@
+<?php
+
+namespace Tests\Icinga\Web;
+require_once('../../library/Icinga/Web/Url.php');
+
+use Icinga\Web\Url;
+
+
+/**
+ * Request mock that implements all methods required by the
+ * Url class
+ *
+ */
+class RequestMock
+{
+    /**
+     * The path of the request
+     *
+     * @var string
+     */
+    public $path = "";
+
+    /**
+     * The baseUrl of the request
+     *
+     * @var string
+     */
+    public $baseUrl = "";
+
+    /**
+     * An array of query parameters that the request should resemble
+     *
+     * @var array
+     */
+    public $query = array();
+
+    /**
+     * Returns the path set for the request
+     *
+     * @return string
+     */
+    public function getPathInfo()
+    {
+        return $this->path;
+    }
+
+    /**
+     * Returns the baseUrl set for the request
+     *
+     * @return string
+     */
+    public function getBaseUrl()
+    {
+        return $this->baseUrl;
+    }
+
+    /**
+     * Returns the query set for the request
+     *
+     * @return array
+     */
+    public function getQuery()
+    {
+        return $this->query;
+    }
+}
+
+
+/**
+ *  Tests for the Icinga\Web\Url class that provides convenient access to Url manipulation method
+ *
+ *
+ */
+class UrlTest extends \PHPUnit_Framework_TestCase
+{
+
+    /**
+     * Tests whether a simple Url without query parameters and baseUrl is correctly parsed and returns correct Urls
+     *
+     */
+    function testFromStringWithoutQuery()
+    {
+        $url = Url::fromPath('http://myHost/my/test/url.html', array(), new RequestMock());
+        $this->assertEquals('/my/test/url.html', $url->getPath(), 'Assert the parsed url path to be equal to the input path');
+        $this->assertEquals($url->getPath(), '/'.$url->getRelativeUrl(), 'Assert the path of an url without query to be equal the relative path');
+    }
+
+    /**
+     * Tests whether a simple Url without query parameters and with baseUrl is correctly parsed and returns correct Urls
+     *
+     */
+    function testFromUrlWithBasePath()
+    {
+        $url = Url::fromPath('my/test/url.html', array(), new RequestMock());
+        $url->setBaseUrl('the/path/to');
+        $this->assertEquals('/the/path/to/my/test/url.html', $url->getAbsoluteUrl(), 'Assert the url path to be the base path with the relative path');
+    }
+
+    /**
+     *  Tests whether query parameters in Urls are correctly recognized and decoded
+     *
+     */
+    function testFromUrlWithKeyValueQuery()
+    {
+        $url = Url::fromPath('/my/test/url.html?param1=%25arg1&param2=arg2', array(), new RequestMock());
+        $this->assertEquals('/my/test/url.html', $url->getPath(), 'Assert the parsed url path to be equal to the input path');
+        $this->assertEquals(
+            array(
+                'param1' => '%arg1',
+                'param2' => 'arg2'
+            ),
+            $url->getParams(),
+            'Assert single key=value Url parameters to be correctly parsed and recognized'
+        );
+    }
+
+    /**
+     *  Tests whether unnamed query parameters in Urls are correctly recognized and decoded
+     *
+     */
+    function testFromUrlWithArrayInQuery()
+    {
+        $url = Url::fromPath('/my/test/url.html?param[]=%25val1&param[]=%40val2', array(), new RequestMock());
+        $this->assertEquals(
+            array(
+                'param' => array('%val1', '@val2')
+            ),
+            $url->getParams(),
+            'Assert arrays in param[] = value syntax to be correctly recognized and parsed as arrays'
+        );
+    }
+
+    /**
+     *  Tests whether named query parameters in Urls are correctly recognized and decoded
+     *
+     */
+    function testFromUrlWithAssociativeArrayInQuery()
+    {
+        $url = Url::fromPath('/my/test/url.html?param[value]=%25val1&param[value2]=%40val2', array(), new RequestMock());
+        $this->assertEquals(
+            array(
+                'param' => array(
+                    'value'     =>  '%val1',
+                    'value2'    =>  '@val2'
+                )
+            ),
+            $url->getParams(),
+            'Assert arrays in param[] = value syntax to be correctly recognized and parsed as arrays'
+        );
+    }
+
+    /**
+     *  Tests whether simple query parameters can be correctly added on an existing query and ends up in correct Urls
+     *
+     */
+    function testAddQueryParameterToUrlWithoutQuery()
+    {
+        $url = Url::fromPath(
+            '/my/test/url.html',
+            array(
+                'param1' => 'val1',
+                'param2' => 'val2'
+            ),
+            new RequestMock()
+        );
+        $url->setBaseUrl('path/to');
+        $this->assertEquals(
+            '/path/to/my/test/url.html?param1=val1&param2=val2',
+            $url->getAbsoluteUrl(),
+            'Assert additional parameters to be correctly added to the Url'
+        );
+    }
+
+    /**
+     * Test whether parameters are correctly added to existing query parameters and existing ones are correctly overwritten
+     * if they have the same key
+     *
+     */
+    function testOverwritePartialQuery()
+    {
+        $url = Url::fromPath(
+            '/my/test/url.html?param1=oldval1',
+            array(
+                'param1' => 'val1',
+                'param2' => 'val2'
+            ),
+            new RequestMock()
+        );
+        $url->setBaseUrl('path/to');
+        $this->assertEquals(
+            '/path/to/my/test/url.html?param1=val1&param2=val2',
+            $url->getAbsoluteUrl(),
+            'Assert additional parameters to be correctly added to the Url and overwriting existing parameters'
+        );
+    }
+
+    /**
+     * Test whether array parameters are correctly added to an existing Url and end up in correct Urls
+     *
+     */
+    function testSetQueryWithArrayParameter()
+    {
+        $url = Url::fromPath(
+            '/my/test/url.html',
+            array(
+                'flatarray' => array('val1', 'val2'),
+                'param' => array('value1'=>'val1', 'value2' => 'val2')
+            ),
+            new RequestMock()
+        );
+        $url->setBaseUrl('path/to');
+        $this->assertEquals(
+            '/path/to/my/test/url.html?flatarray'.urlencode('[0]').'=val1&'.
+                'flatarray'.urlencode('[1]').'=val2&'.
+                'param'.urlencode('[value1]').'=val1&'.
+                'param'.urlencode('[value2]').'=val2',
+            $url->getAbsoluteUrl(),
+            'Assert array parameters to be correctly encoded and added to the Url'
+        );
+    }
+
+    /**
+     * Test whether Urls from the request are correctly parsed when no query is given
+     *
+     */
+    function testUrlFromRequestWithoutQuery()
+    {
+        $request = new RequestMock();
+        $request->baseUrl = 'path/to';
+        $request->path = 'my/test/url.html';
+        $request->query = array();
+        $url = Url::fromRequest(array(), $request);
+        $this->assertEquals(
+            '/path/to/my/test/url.html',
+            $url->getAbsoluteUrl(),
+            'Asserting absolute path resembling the requests path appended by the baseUrl'
+        );
+    }
+
+    /**
+     * Test whether Urls from the request are correctly parsed when a query is given
+     *
+     */
+    function testUrlFromRequestWithQuery()
+    {
+        $request = new RequestMock();
+        $request->baseUrl = 'path/to';
+        $request->path = 'my/test/url.html';
+        $request->query = array(
+            'param1' => 'value1', 
+            'param2' => array('key1' => 'value1', 'key2' => 'value2')
+        );
+        $url = Url::fromRequest(array(), $request);
+        $this->assertEquals(
+            '/path/to/my/test/url.html?param1=value1&'.
+                'param2'.urlencode('[key1]').'=value1&'.
+                'param2'.urlencode('[key2]').'=value2',
+            $url->getAbsoluteUrl(),
+            'Asserting absolute path resembling the requests path appended by the baseUrl'
+        );
+    }
+
+    /**
+     * Test the @see Url::getParam($name, $default) function
+     *
+     */
+    function testGetParameterByName()
+    {
+        $url = Url::fromPath('/my/test/url.html?param=val&param2=val2', array(), new RequestMock());
+        $this->assertEquals(
+            "val",
+            $url->getParam("param", "wrongval"),
+            "Asserting a parameter can be fetched via getParam()"
+        );
+        $this->assertEquals(
+            "val2",
+            $url->getParam("param2", "wrongval2"),
+            "Asserting a parameter can be fetched via getParam()"
+        );
+        $this->assertEquals(
+            "nonexisting",
+            $url->getParam("param3", "nonexisting"),
+            "Asserting a non existing parameter returning the default value when fetched via getParam()"
+        );
+    }
+
+    /**
+     * Test the Url::remove function with a single key passed
+     *
+     */
+    function testRemoveSingleParameter()
+    {
+        $url = Url::fromPath('/my/test/url.html?param=val&param2=val2', array(), new RequestMock());
+        $url->remove("param");
+        $this->assertEquals(
+            "val2",
+            $url->getParam("param2", "wrongval2"),
+            "Asserting other parameters (param2) not being affected by remove"
+        );
+        $this->assertEquals(
+            "rightval",
+            $url->getParam("param", "rightval"),
+            "Asserting a parameter (param) can be removed via remove"
+        );
+    }
+
+    /**
+     * Test the Url::remove function with an array of keys passed
+     *
+     */
+    function testRemoveMultipleParameters()
+    {
+        $url = Url::fromPath('/my/test/url.html?param=val&param2=val2&param3=val3', array(), new RequestMock());
+        $url->remove(array("param", "param2"));
+        $this->assertEquals(
+            "val3",
+            $url->getParam("param3", "wrongval"),
+            "Asserting other parameters (param3) not being affected by remove"
+        );
+        $this->assertEquals(
+            "rightval",
+            $url->getParam("param", "rightval"),
+            "Asserting a parameter (param) can be removed via remove in a batch"
+        );
+        $this->assertEquals(
+            "rightval",
+            $url->getParam("param2", "rightval"),
+            "Asserting a parameter (param2) can be removed via remove in a batch"
+        );
+    }
+
+    /**
+     * Test the Url::without call and whether it returns a copy instead of working on the called object
+     *
+     */
+    function testWithoutCall()
+    {
+        $url = Url::fromPath('/my/test/url.html?param=val&param2=val2&param3=val3', array(), new RequestMock());
+        $url2 = $url->getUrlWithout(array("param"));
+        $this->assertNotEquals(
+            $url,
+            $url2,
+            "Asserting without creating a copy of the url"
+        );
+        $this->assertTrue(
+            $url->hasParam("param"),
+            "Asserting the original Url not being affected when calling 'without'"
+        );
+        $this->assertFalse(
+            $url2->hasParam("param"),
+            "Asserting the returned Url being without the passed parameter when calling 'without'"
+        );
+    }
+
+    /**
+     * Test whether toString is the same as getAbsoluteUrl
+     *
+     */
+    function testToString()
+    {
+        $url = Url::fromPath('/my/test/url.html?param=val&param2=val2&param3=val3', array(), new RequestMock());
+        $this->assertEquals($url->getAbsoluteUrl(), (string) $url, "Asserting whether toString returns the absolute Url");
+    }
+}
\ No newline at end of file