Path

ez components / resources / articles and publications / webdav locking and unconventional test cases


WebDAV locking and unconventional test cases

Author: Tobias Schlitt
Revision: $Rev$
Date: 2009-01-06 09:15 CET
Status: Draft

Version 2008.2 of the eZ Components package brings a new release of the Webdav component 1.1. This version introduces a lot of new features, making it easier for you to integrate the component into your environment and offering advanced built-in WebDAV capabilities. This article introduces the new features and gives you some insight on the advanced testing techniques we used in developing this new release.

Architecture

The goal of the Webdav component is to provide a generic WebDAV server that can be easily integrated into any PHP-driven web application. The component can be integrated into legacy applications and provides an interface to support any WebDAV client.

WebDAV is quite a complex protocol and it is time-consuming to read the RFCs to understand and integrate WebDAV facilities. In fact, many clients are problematic because they do not stick to the RFC definitions. As a result, it is quite a challenge to create a WebDAV server that satisfies the nuances of every client, but that is also maintainable and flexible.

The eZ Components developer team has thus come up with a modular 3 layer architecture.

The Webdav components architecture.

The transport layer is responsible for the abstraction of the request / response handling and therefore compensates for client nuances. The RFC compliant transport class is capable of parsing requests and producing responses that comply with the WebDAV RFCs. To react to client-specific issues, this class can be extended and replaced. The Webdav component automatically detects the client it is communicating with and uses the appropriate transport class.

A transport always creates a unified object structure, representing a certain request and the data submitted from the client. On the other side, it receives a unified object structure that represents a response and the data to be sent to the client. It then formats it for the client to understand.

The second layer represents the central control unit of the component: the server itself. It is responsible for the configuration of the entire component and facilitates the communication between the other layers and modules. Each request that is received by the server layer from the transport layer is given to the back-end layer and/or the plugin API. One of these recipients processes the request and returns a response object, which is given back to the transport layer to be sent to the client.

The plugin API is a new feature of the Webdav component in version 1.1. You can read more about this a bit later.

The back-end layer is responsible for accessing the underlying data store and processing the instructions encapsulated in the abstract request object. It then generates a response object and returns it to the server layer for further processing. A back-end can operate on an arbitrary data store, such as a file system, a database, memory, or whatever you'd like to be accessible through the WebDAV protocol.

Since the operations supported by the back-end are quite complex, the Webdav component offers an abstract base class ezcWebdavSimpleBackend, on which you can build a custom back-end. This base class implements the methods to process all request types required by the WebDAV RFC. It defines a simpler set of methods to be extended by a back-end class.

In addition, the simple back-end takes care of authorization and the processing of certain HTTP headers. This is described in more detail next.

New in the Webdav component 1.1

The 1.1 release of the Webdav component essentially adds 4 features to the Webdav component:

  • support for the If-[None-]Match HTTP header
  • support for authentication and authorization
  • the plugin API
  • a plugin that supports locking for WebDAV

The HTTP headers If-Match and If-None-Match are used by WebDAV clients to determine whether or not to perform an operation based on the states of the affected resources. You might be familiar with ETags (entity tags), which are used to represent the state of a resource. For example, the client receives the ETag of a resource when it is downloaded. If the user manipulates it and then uploads it again, the client can send the If-Match header to ensure that the desired file was not changed by someone else. The ezcWebdavSimpleBackend class now supports state-based restrictions according to ETags. This also means that all back-ends that extend this base class automatically support this feature.

Of perhaps more interest is the integration of authentication and authorization capabilities. Before version 1.1, it was hardly possible to secure a WebDAV installation and to restrict its overall usage to certain users. The only possibility was to restrict access to WebDAV-enabled URLs through the underlying web server. Also, fine-grained authorization definitions were not possible at all.

This has been addressed with the introduction of a central API on the server layer. A set of interfaces enables you to implement a gateway to the authentication and authorization mechanisms of your application, although it is still possible to run a WebDAV server without these controls. Authentication is handled centrally on the server layer, while the simple back-end base class transparently handles the authorization aspect.

The new plugin API enables you to integrate new functionality into the Webdav component, without modifying or dealing with the internals of the component. Plugins can process requests before they reach the transport layer or after they have been parsed. In addition, you can parse requests that are not yet recognized by the Webdav component. The same applies for the response communication, which can be manipulated and completely handled by a plugin.

The included lock plugin makes use of the newly integrated plugin API and is easy to activate. This plugin provides additional requests and behavior to the WebDAV RFC, which are described by WebDAV compliance class 2. To avoid the lost update problem, where two or more clients save conflicting edits, it supports clients that can perform locking.

One of the advantages of supporting locks via a plugin is that you do not need to account for it in any custom Webdav component extensions. For example, if you have written a custom back-end, you can still use the lock plugin regardless of whether or not you extended ezcWebdavSimpleBackend. The plugin works almost out of the box with every installation and only requires slight adjustments to your infrastructure.

For further details about the usage of these new features, please refer to the tutorial for the Webdav component and to the API documentation.

The testing dilemma

The eZ Components team sticks to the TDD (test-driven development) approach as much as possible. As the basis for that, we use PHPUnit, the de-facto standard for unit tests in the PHP world. While we differ a bit from pure TDD, with an extensive design and discussion process before starting to make feature implementations, we still try to write tests before the implementation step and have our code fulfill the tests.

However, there are edge cases in software development where pure unit testing does not work. The Webdav component is an example of such an edge case. Most of the component's classes are unit tested and many of them are tested in a traditional way. During the development of version 1.0 of the component, we already noticed that pure unit testing was not enough.

Having different clients that do not conform to the RFC introduced the need to check the behavior of the server for regressions. How do you ensure that a client still works if you change something internally on the server? We initially introduced the idea of somewhat automated client tests while developing version 1.0. For version 1.1, we combined the concept of the client regression tests with real unit testing, to satisfy the testing needs regarding the lock plugin.

Client regression tests

The idea of client regression tests is simple. First, we set up a typical WebDAV server environment where request and response data is captured. This is the so-called "client test generator". It logs each request/response combination that occurs while typical WebDAV operations are performed using each client. A special test case is then integrated into the Webdav component's test suite, which replays the collected requests and asserts that the generated response still matches the requirements of the client.

The client test generator is basically an instance of the WebDAV server, with some small extensions to grab the request and response information. The server uses the ezcWebdavMemoryBackend class, a non-public extension of ezcWebdavSimpleBackend, which was implemented for testing purposes. The memory back-end simply stores the necessary information in RAM and can be serialized to a file for storage between requests. This ensures that tests always run under the same circumstances, avoiding the need for complex file setups.

The desired client is then tested by a human tester. A test recipe - a step by step guide - defines which actions are to be performed in order to generate a good client test run. The steps include the creation of directories; uploading and downloading of files; actions for copying, moving, and deleting; and more.

If this test recipe is successfully run using the desired client, the test generator has logged quite a bit of information about each request and response:

  • all relevant keys of the $_SERVER array
  • the request body
  • any generated response headers
  • the response status code
  • the body that has been sent to the client

To integrate this kind of test into the Webdav component's test suite, we created a dedicated test case class, which further extends the PHPUnit base class for a test case. It takes care of scanning the directory where a specific client's test log resides. For each request / response data combination, a test case is registered, while maintaining the original order of requests.

When the test suite is run, it performs each of the requests against the very same client test generator setup and checks that the generated response is still correct. The state of the back-end is maintained between requests, since the manipulations that took place in the last request are preconditions for the next one.

If all test cases run successfully, we are sure that the desired client still works with the Webdav component. However, if such a test run fails, this does not necessarily mean that the client no longer works. This is only an indication for us to re-perform the client test procedure.

Testing the lock plugin

The purpose of client regression tests since version 1.0 of the Webdav component was for us to indicate that a client needs re-testing. For version 1.1, we determined that parts of the lock plugin are almost not unit testable. While we could unit test all classes that have no (or only a few) dependencies to other objects, testing the core of the lock plugin using the existing approaches did not seem possible.

The core of the lock plugin is the handling of the LOCK and UNLOCK WebDAV requests and the investigation of other requests that are affected by locking. The responsible classes interact with the authorization mechanism and therefore the server layer. They also send requests directly to the back-end and process the received responses. The transport layer is involved when lock properties need to be stored or read in the back-end. Setting up the environment for a single test case inside a PHPUnit test case class would be more than unmaintainable.

Inspired by the client regression tests, we introduced another custom test case, which works in a similar way. First, it involves the instantiation of a typical WebDAV server setup. For each scenario that needs to be tested, we store a dedicated memory back-end setup to mirror the preconditions of a test. This setup is stored in a dedicated file. The same applies for the request and response information, which is stored in a similar way as for the client tests.

In addition to the normal client regression tests, a dedicated file with assertions is stored. The test case not only performs the typical assertions - that the generated response information was as expected - but it also reads a dedicated set of assertions for each scenario from a file.

This assertion file contains a dedicated assertions class, which can have one or more assertions in the form of methods. Each method receives the back-end state after the request was performed, and checks certain conditions.

To give you better insight into such a test scenario, consider the test for an UNLOCK request. First, the client sends an UNLOCK request to release a certain lock, which is identified by a lock token. This request does not need to be sent to the URI to which the lock was originally issued; it can be sent to any URI affected by the lock. If the lock still exists (and was not purged), and the user issuing the request is authorized to manipulate it, it must be removed from all affected resources. All other locks should remain untouched, especially those that affect the same resources that the current lock in question affects (this is what is called a "shared lock" with WebDAV).

One scenario creates a back-end with 2 shared locks whose affected resources overlap. The UNLOCK request is sent to a resource other than the one where the lock was originally issued. The UNLOCK operation must then succeed in removing the lock from all affected resources, while leaving the second lock untouched.

The response assertion takes place in the same manner as in the client regression tests. If this assertion succeeds, the test case reads the dedicated assertion class and checks the following assertions:

  • the lock to be released is no longer available in the back-end
  • the other lock is still intact on all affected resources

Using this testing technique, we created tests for all important scenarios that might occur when interacting with locks. The tests include those that are supposed to succeed and those that are supposed to fail.

Conclusion

The Webdav component is an easy to use module that can be implemented on complex server applications. It presented us with some testing challenges that were beyond the traditional realm of unit testing. By introducing unconventional test cases on top of the PHPUnit test framework, we managed to create automated test suites for functional and black box testing.

The test suites ensure the correct behavior of the component against multiple clients and warn us if we have behavior regressions against certain clients. If we identify a behavior regression to be correct, we can re-test the client manually to ensure that it still works. The test suites also enable us to test highly complex classes that are not unit testable.