PageRenderTime 37ms CodeModel.GetById 9ms app.highlight 21ms RepoModel.GetById 1ms app.codeStats 0ms

/src/Server/DeviceHive.Test/ResourceTest.cs

https://github.com/oryol/devicehive-.net
C# | 515 lines | 342 code | 46 blank | 127 comment | 6 complexity | 4abfb3ae0180e0082cc66e076b67512b MD5 | raw file
  1using System;
  2using System.Collections;
  3using System.Collections.Generic;
  4using System.Configuration;
  5using System.Linq;
  6using System.Reflection;
  7using System.Text;
  8using System.Text.RegularExpressions;
  9using Newtonsoft.Json.Linq;
 10using NUnit.Framework;
 11using NUnit.Framework.Constraints;
 12
 13namespace DeviceHive.Test
 14{
 15    /// <summary>
 16    /// Represents base class for resource-based API tests
 17    /// </summary>
 18    public abstract class ResourceTest : AssertionHelper
 19    {
 20        private Stack<string> _resources;
 21
 22        #region Protected Properties
 23
 24        /// <summary>
 25        /// Gets resource relative URI
 26        /// </summary>
 27        protected string ResourceUri { get; set; }
 28
 29        /// <summary>
 30        /// Gets JsonClient object
 31        /// </summary>
 32        protected internal JsonClient Client { get; private set; }
 33
 34        /// <summary>
 35        /// Gets or sets expected status code for create POST call
 36        /// </summary>
 37        protected int ExpectedCreatedStatus { get; set; }
 38
 39        /// <summary>
 40        /// Gets or sets expected status code for DELETE call
 41        /// </summary>
 42        protected int ExpectedDeletedStatus { get; set; }
 43
 44        /// <summary>
 45        /// Gets or sets unexisting resource ID to test for 404 calls
 46        /// </summary>
 47        protected int UnexistingResourceID { get; set; }
 48
 49        /// <summary>
 50        /// Gets or sets username of existing administrator account
 51        /// </summary>
 52        protected string ExistingAdminUsername { get; set; }
 53
 54        /// <summary>
 55        /// Gets or sets password of existing administrator account
 56        /// </summary>
 57        protected string ExistingAdminPassword { get; set; }
 58
 59        #endregion
 60
 61        #region Constructor
 62
 63        /// <summary>
 64        /// Default constructor
 65        /// </summary>
 66        /// <param name="resourceUri">Resource relative URI</param>
 67        public ResourceTest(string resourceUri)
 68        {
 69            if (string.IsNullOrEmpty(resourceUri))
 70                throw new ArgumentException("ResourceUri is null or empty!", "resourceUri");
 71
 72            ResourceUri = resourceUri;
 73            Client = new JsonClient(ConfigurationManager.AppSettings["ApiUrl"]);
 74
 75            ExpectedCreatedStatus = 201;
 76            ExpectedDeletedStatus = 204;
 77            UnexistingResourceID = 999999;
 78            ExistingAdminUsername = "ut";
 79            ExistingAdminPassword = "ut_165GxCl$$boTTc2";
 80        }
 81        #endregion
 82
 83        #region Protected Methods
 84
 85        /// <summary>
 86        /// Test set up method
 87        /// </summary>
 88        [SetUp]
 89        protected virtual void SetUp()
 90        {
 91            _resources = new Stack<string>();
 92            OnCreateDependencies();
 93        }
 94
 95        /// <summary>
 96        /// Test tear down method
 97        /// Default implementation deletes all created resources
 98        /// </summary>
 99        [TearDown]
100        protected virtual void TearDown()
101        {
102            foreach (var resource in _resources)
103            {
104                Client.Delete(resource, auth: Admin);
105            }
106            _resources = null;
107        }
108
109        /// <summary>
110        /// Creates dependencies required to test current resource
111        /// </summary>
112        protected virtual void OnCreateDependencies()
113        {
114        }
115
116        /// <summary>
117        /// Gets list of resources from the server
118        /// </summary>
119        /// <param name="auth">Authorization info</param>
120        /// <returns>JArray that represents server response</returns>
121        protected virtual JArray Get(Authorization auth = null)
122        {
123            // invoke get
124            var response = Client.Get(ResourceUri, auth: auth);
125
126            // verify response object
127            ExpectResponseStatus(response, 200);
128            Expect(response.Json, Is.InstanceOf<JArray>());
129            return (JArray)response.Json;
130        }
131
132        /// <summary>
133        /// Gets list of resources from the server
134        /// </summary>
135        /// <param name="query">Optional query parameters</param>
136        /// <param name="auth">Authorization info</param>
137        /// <returns>JArray that represents server response</returns>
138        protected virtual JArray Get(Dictionary<string, string> query, Authorization auth = null)
139        {
140            // invoke get
141            var response = Client.Get(ResourceUri + "?" +
142                string.Join("&", query.Select(q => string.Format("{0}={1}", q.Key, Uri.EscapeUriString(q.Value)))), auth: auth);
143
144            // verify response object
145            ExpectResponseStatus(response, 200);
146            Expect(response.Json, Is.InstanceOf<JArray>());
147            return (JArray)response.Json;
148        }
149
150        /// <summary>
151        /// Gets resource from the server
152        /// </summary>
153        /// <param name="resource">Resource to get</param>
154        /// <param name="auth">Authorization info</param>
155        /// <returns>JObject that represents server response</returns>
156        protected virtual JObject Get(object resource, Authorization auth = null)
157        {
158            // invoke get
159            var resourceId = GetResourceId(resource);
160            var response = Client.Get(ResourceUri + "/" + resourceId, auth: auth);
161
162            // verify response object
163            ExpectResponseStatus(response, 200);
164            Expect(response.Json, Is.InstanceOf<JObject>());
165            Expect(GetResourceId((JObject)response.Json), Is.EqualTo(resourceId.ToString()));
166            return (JObject)response.Json;
167        }
168
169        /// <summary>
170        /// Creates resource on the server
171        /// </summary>
172        /// <param name="resourceObject">Resource object to create</param>
173        /// <param name="auth">Authorization info</param>
174        /// <returns>JObject that represents server response</returns>
175        protected virtual JObject Create(object resourceObject, Authorization auth = null)
176        {
177            // invoke create
178            var response = Client.Post(ResourceUri, resourceObject, auth: auth);
179
180            // verify response object
181            ExpectResponseStatus(response, ExpectedCreatedStatus);
182            Expect(response.Json, Is.InstanceOf<JObject>());
183            RegisterForDeletion(ResourceUri + "/" + GetResourceId((JObject)response.Json));
184            return (JObject)response.Json;
185        }
186
187
188        /// <summary>
189        /// Updates resource on the server
190        /// </summary>
191        /// <param name="resource">Resource to update</param>
192        /// <param name="resourceObject">Resource object to send as update</param>
193        /// <param name="auth">Authorization info</param>
194        /// <returns>JObject that represents server response</returns>
195        protected virtual JObject Update(object resource, object resourceObject, Authorization auth = null)
196        {
197            // invoke update
198            var resourceId = GetResourceId(resource);
199            var response = Client.Put(ResourceUri + "/" + resourceId, resourceObject, auth: auth);
200
201            // verify response object
202            ExpectResponseStatus(response, 200);
203            Expect(response.Json, Is.InstanceOf<JObject>());
204            Expect(GetResourceId((JObject)response.Json), Is.EqualTo(resourceId.ToString()));
205            return (JObject)response.Json;
206        }
207
208        /// <summary>
209        /// Deletes resource on the server
210        /// </summary>
211        /// <param name="resource">Resource to delete</param>
212        /// <param name="auth">Authorization info</param>
213        protected virtual void Delete(object resource, Authorization auth = null)
214        {
215            // invoke delete
216            var resourceId = GetResourceId(resource);
217            var response = Client.Delete(ResourceUri + "/" + resourceId, auth: auth);
218
219            // verify response object
220            ExpectResponseStatus(response, ExpectedDeletedStatus);
221        }
222
223        /// <summary>
224        /// Gets resource identifier
225        /// </summary>
226        /// <param name="resource">Resource identifier or JObject representing the resource</param>
227        /// <returns>Resource identifier</returns>
228        protected virtual string GetResourceId(object resource)
229        {
230            if (resource is JObject)
231            {
232                var id = (JValue)((JObject)resource)["id"];
233                if (id == null)
234                    throw new ArgumentException("Resource is JObject but does not include id property!", "resourceId");
235
236                return id.Value.ToString();
237            }
238            return resource.ToString();
239        }
240
241        /// <summary>
242        /// Creates a user and returns corresponding Authorization object
243        /// </summary>
244        /// <param name="role">User role</param>
245        /// <param name="networks">List of network resources to associate user with</param>
246        /// <returns>Coresponding Authorization object</returns>
247        protected Authorization CreateUser(int role, params object[] networks)
248        {
249            // assign login and password
250            var login = "_ut_" + Guid.NewGuid().ToString();
251            var password = "pwd";
252
253            // create user
254            var userResource = Client.Post("/user", new { login = login, password = password, role = role, status = 0 }, auth: Admin);
255            Expect(userResource.Status, Is.EqualTo(ExpectedCreatedStatus));
256            var userId = GetResourceId(userResource.Json);
257            RegisterForDeletion("/user/" + userId);
258
259            // create user/network association
260            foreach (var network in networks ?? new object[] { })
261            {
262                var networkId = GetResourceId(network);
263                var userNetworkResponse = Client.Put("/user/" + userId + "/network/" + networkId, new { }, auth: Admin);
264                Expect(userNetworkResponse.Status, Is.EqualTo(200));
265                RegisterForDeletion("/user/" + userId + "/network/" + networkId);
266            }
267
268            // return user authorization object
269            return User(login, password);
270        }
271
272        /// <summary>
273        /// Registers resource for deletion when the test ends
274        /// </summary>
275        /// <param name="resource">Full resource url</param>
276        protected void RegisterForDeletion(string resource)
277        {
278            _resources.Push(resource);
279        }
280
281        /// <summary>
282        /// Gets default authorization for admin user
283        /// </summary>
284        protected Authorization Admin
285        {
286            get { return new Authorization("User", ExistingAdminUsername, ExistingAdminPassword); }
287        }
288
289        /// <summary>
290        /// Gets authorization for a user
291        /// </summary>
292        /// <param name="username">User login</param>
293        /// <param name="password">User password</param>
294        protected Authorization User(string login, string password)
295        {
296            return new Authorization("User", login, password);
297        }
298
299        /// <summary>
300        /// Gets authorization for a device
301        /// </summary>
302        /// <param name="id">Device identifier</param>
303        /// <param name="key">Device key</param>
304        protected Authorization Device(string id, string key)
305        {
306            return new Authorization("Device", id, key);
307        }
308
309        /// <summary>
310        /// Gets FailsWith constraint
311        /// </summary>
312        /// <param name="status">Expected status</param>
313        /// <returns>ResponseFailsWithContraint object</returns>
314        protected ResponseFailsWithContraint FailsWith(int status)
315        {
316            return new ResponseFailsWithContraint(status);
317        }
318
319        /// <summary>
320        /// Gets Matches constraint
321        /// </summary>
322        /// <param name="expected">Expected object</param>
323        /// <returns>ResponseMatchesContraint object</returns>
324        protected ResponseMatchesContraint Matches(object expected)
325        {
326            return new ResponseMatchesContraint(expected);
327        }
328        #endregion
329
330        #region Private Methods
331
332        private void ExpectResponseStatus(JsonResponse response, int expected)
333        {
334            if (response.Status != expected)
335            {
336                throw new ResourceException(response.Status,
337                    string.Format("Invalid server response! Expected: {0}, Actual: {1}", expected, response.Status));
338            }
339        }
340        #endregion
341    }
342
343    public class ResponseMatchesContraint : Constraint
344    {
345        private readonly Regex _timestampRegex = new Regex(@"\""\d{4}\-\d{2}\-\d{2}T\d{2}\:\d{2}\:\d{2}\.\d{1,6}\""");
346
347        #region Public Properties
348
349        public object Expected { get; private set; }
350
351        public static object Timestamp
352        {
353            get { return new TimestampValue(); }
354        }
355        #endregion
356
357        #region Constructor
358
359        public ResponseMatchesContraint(object expected)
360        {
361            Expected = expected;
362        }
363        #endregion
364
365        #region Constraint Members
366
367        public override bool Matches(object actual)
368        {
369            this.actual = actual;
370
371            var actualJson = (JToken)actual;
372            if (actualJson == null)
373                return false;
374
375            return IsMatches(actualJson, Expected);
376        }
377
378        public override void WriteDescriptionTo(MessageWriter writer)
379        {
380            writer.Write(JToken.FromObject(Expected));
381        }
382
383        public override void WriteActualValueTo(MessageWriter writer)
384        {
385            writer.Write(actual);
386        }
387        #endregion
388
389        #region Private Methods
390
391        private bool IsMatches(JToken actual, object expected)
392        {
393            foreach (var property in expected.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))
394            {
395                var expectedProperty = property.GetValue(expected, null);
396                var actualProperty = actual[property.Name];
397
398                if (expectedProperty is TimestampValue)
399                {
400                    // a timestamp is expected
401                    var jvalue = actualProperty as JValue;
402                    if (jvalue == null || !(jvalue.Value is DateTime))
403                        return false;
404                    if (!_timestampRegex.IsMatch(jvalue.Parent.ToString()))
405                        return false;
406                    continue;
407                }
408
409                if (actualProperty is JValue)
410                {
411                    if (expectedProperty is int)
412                        expectedProperty = (long)(int)expectedProperty;
413
414                    if (!object.Equals(((JValue)actualProperty).Value, expectedProperty))
415                        return false;
416                }
417                else if (actualProperty is JObject)
418                {
419                    if (expectedProperty == null || expectedProperty.GetType().IsPrimitive)
420                        return false;
421                    if (!IsMatches(actualProperty, expectedProperty))
422                        return false;
423                }
424                else if (actualProperty is JArray)
425                {
426                    if (expectedProperty == null || !(expectedProperty is IEnumerable))
427                        return false;
428                    foreach (var expectedItem in ((IEnumerable)expectedProperty))
429                    {
430                        if (!actualProperty.Any(ap => IsMatches(ap, expectedItem)))
431                            return false;
432                    }
433                }
434                else
435                {
436                    // unexpected type
437                    return false;
438                }
439            }
440            return true;
441        }
442
443        #endregion
444
445        #region TimestampValue struct
446
447        private struct TimestampValue
448        {
449        }
450        #endregion
451    }
452
453    public class ResponseFailsWithContraint : Constraint
454    {
455        #region Public Properties
456
457        public int Status { get; private set; }
458
459        #endregion
460
461        #region Constructor
462
463        public ResponseFailsWithContraint(int status)
464        {
465            Status = status;
466        }
467        #endregion
468
469        #region Constraint Members
470
471        public override bool Matches(ActualValueDelegate del)
472        {
473            try
474            {
475                var response = del();
476                actual = "SUCCESS";
477                return false; // the delegate must throw a ResourceException to indicate failure
478            }
479            catch (ResourceException ex)
480            {
481                actual = ex.Status;
482                return ex.Status == Status;
483            }
484        }
485
486        public override bool Matches(object actual)
487        {
488            return false;
489        }
490
491        public override void WriteDescriptionTo(MessageWriter writer)
492        {
493            writer.Write(Status);
494        }
495        #endregion
496    }
497
498    public class ResourceException : Exception
499    {
500        #region Public Properties
501
502        public int Status { get; private set; }
503
504        #endregion
505
506        #region Constructor
507
508        public ResourceException(int status, string message)
509            : base(message)
510        {
511            Status = status;
512        }
513        #endregion
514    }
515}