C# Client Library
A C# Client Library for the AnalyzeRe REST API
Loading...
Searching...
No Matches
BaseResourceTestSuite.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Diagnostics;
4using System.Linq;
5using System.Linq.Expressions;
6using System.Net;
7using System.Reflection;
8
9using AnalyzeRe;
14
15#if MSTEST
16using Microsoft.VisualStudio.TestTools.UnitTesting;
17#elif NUNIT
18using NUnit.Framework;
19using TestClass = NUnit.Framework.TestFixtureAttribute;
20using TestMethod = NUnit.Framework.TestAttribute;
21using TestCategory = NUnit.Framework.CategoryAttribute;
22#endif
23
25{
26 [TestClass]
28 {
29 #region Properties
30 private const string TypeName = "BaseResourceTest";
31
35
44
45 // Deriving class should override the below if the default behaviour isn't desired.
46 protected virtual bool DELETE_Allowed => false;
47
52
55 public virtual void AdditionalValidResourceTests(T posted) { }
56 #endregion Properties
57
58 #region Inherited Test Methods
59 #region GET
60 [TestMethod, TestCategory(TypeName)]
61 public virtual void Test_Resource_GET_Existing()
62 {
63 GenericTest.GET_ExistingResource_Succeeds(TestResource_Existing);
64 }
65
66 // Retrieve the resource as a typed object, and then as a JSON object, and
67 // verify that there are no properties in the JSON object that we weren't expecting.
68 [TestMethod, TestCategory(TypeName)]
70 {
71 GenericTest.GET_AllPropertiesRecognized(TestResource_Existing);
72 }
73 #endregion GET
74
75 #region Collection GET Methods
77 [TestMethod, TestCategory(TypeName)]
78 public virtual void Test_Resource_GetCollectionName()
79 {
81 Assert.IsNotNull(typeCollectionName);
82 Console.WriteLine($"{typeof(T).NiceTypeName()} Collection Name: {typeCollectionName}");
83 // Also check that we can get the collection name from the instance.
84 Assert.AreEqual(typeCollectionName, TestResource.collection_name,
85 "The instance returned a different collection name than what it's type resolves.");
86 }
87
90 [TestMethod, TestCategory(TypeName)]
91 public virtual void Test_Resource_GET_FromBatch()
92 {
94 // Assert that the response when retrieving a resource in a batch request
95 // is equivalent to what was posted.
96 ICollectionResponse<T> searchResult = API.BatchGet<T>(new[] { posted.id });
97 Assert.AreEqual(1, searchResult.meta.total_count, $"Expected to find exactly one " +
98 $"resource with the id {posted.id}. Found {searchResult.meta.total_count}");
99 T getResult = searchResult.items.Single();
100 AssertApi.AllPropertiesEqual(posted, getResult);
101 }
102 #endregion Collection GET Methods
103
104 #region POST
105 [TestMethod, TestCategory(TypeName)]
106 public virtual void Test_Resource_POST_Valid() =>
108
111 [TestMethod, TestCategory(TypeName)]
113 {
115 Assert.Inconclusive("RUN_OFFLINE = true");
118 Assert.AreEqual(1, response.Count, "Expected only one key in the response (the id).");
119 Assert.IsTrue(response.ContainsKey("id"), "Expected the id in the response.");
120 AssertApi.IsValidUUID(response["id"] as string);
121 }
122
123 #region Id
126 [TestMethod, TestCategory(TypeName)]
127 public virtual void Test_Resource_POST_Id_Ignored()
128 {
130 POST_Attribute_Null(r => r.id);
132 POST_WithValue(r => r.id, "MALFORMED", true);
133 }
134
141 [TestMethod, TestCategory(TypeName)]
142 public virtual void Test_Resource_POST_ExistingId()
143 {
144 // Create a resource to POST that has the id of an existing resource.
146 POST_ThenDoAction(TestResource.Change(r => r.id, existingId), copy =>
147 {
148 // If this is a persisted resource, the copy should have a new, different id.
149 // TODO: test with two different objects and ensure the original object is unmodified.
150 // e.g. only PUT/PATCH can modify resources, POST always creates a new one
151 if (copy is IStoredAPIResource)
152 Assert.AreNotEqual(existingId, copy.id, "The copied resource belongs to a " +
153 $"collection of persisted resources ({copy.collection_name}) - expected " +
154 "POST to return a copy with a new id, but the id returned is identical.");
155 // If this is a transient resource, the id will be identical due to hashing.
156 else
157 Assert.AreEqual(existingId, copy.id, "The copied resource belongs to a " +
158 $"collection of transient resources ({copy.collection_name}) - expected " +
159 "POST to return the existing resource, but the id returned is different.");
160 });
161 }
162 #endregion Id
163
164 #region POST Test Helpers
171 GenericTest.POST_ThenDoAction(validToPost, toExecute);
172
188
197 {
198 if (!Equals(default(TProperty), null))
199 throw new ArgumentException("Can only be used on properties whose default type is null.");
200 if (!shouldSucceed.HasValue)
203 }
204
210 {
211 if (!shouldSucceed.HasValue)
214 // TODO: Do a message check.
215 // Output.MsgMissingAttribute(((MemberExpression)PropertyExpression.Body).Member.Name, validResource._type));
216 }
217
218 #region List Attribute Test Helpers
236
239 {
240 if (!shouldSucceed.HasValue)
244 // TODO: Do a message check.
245 // Output.MsgMissingAttribute(((MemberExpression)PropertyExpression.Body).Member.Name, validResource._type));
246 }
251 {
255 // TODO: Do a message check.
256 // Output.MsgMissingAttribute(((MemberExpression)PropertyExpression.Body).Member.Name, validResource._type));
257 }
258 #endregion List Attribute Test Helpers
259
260 #region Reference Attribute Test Helpers
290
295
298 {
299 // Create a new list of the same runtime type to populate with an invalid reference
303 }
304 #endregion Reference Attribute Test Helpers
305 #endregion POST Test Method Helpers
306 #endregion POST
307
308 #region DELETE
309 [TestMethod, TestCategory(TypeName)]
310 public virtual void Test_Resource_DELETE()
311 {
313 Assert.Inconclusive("RUN_OFFLINE = true");
316 }
317
318 [TestMethod, TestCategory(TypeName)]
320 {
322 Assert.Inconclusive("RUN_OFFLINE = true");
324 DELETE_Allowed ? HttpStatusCode.NotFound : HttpStatusCode.MethodNotAllowed;
325 AssertApi.ExceptionThrown(
326 () => GenericTest.Mock<T>(Samples.Valid_NonExistant_UUID).Delete(),
328 }
329
331 {
332 AssertApi.MethodIsAllowed(() => toDelete.Delete(), "DELETE", isDeleteAllowed);
333 // If delete is allowed, assert that it worked and the resource is gone
334 if (isDeleteAllowed)
335 {
336 AssertApi.ExceptionThrown(() => toDelete.Get(), (APIRequestException e) =>
337 Assert.AreEqual(HttpStatusCode.NotFound, e.RestResponse.StatusCode,
338 "Subsequent GET after a successful DELETE of the " +
339 $"{typeof(T).NiceTypeName()} returned the wrong type of error:\n{e}"));
340 }
341 }
342 #endregion DELETE
343
344 #region Utilities and Extension Methods
347 [TestMethod, TestCategory(TypeName)]
348 public virtual void Test_Resource_ShallowCopy()
349 {
350 // Test copying an un-posted resource
351 TestCopyHelper(TestResource, r => r.ShallowCopy());
352 // Repeat the test for an already-posted resource instance.
353 TestCopyHelper(TestResource_Existing, r => r.ShallowCopy());
354 }
355
359 [TestMethod, TestCategory(TypeName)]
360 public virtual void Test_Resource_DeepCopy()
361 {
362 // DeepCopy works by Serializing then Deserializing an IAPIType,
363 // resulting in a new instance with its own copies of just the serializable attributes.
364 // To help debug this method, it is useful to output a serialized version of the current resource.
365 Console.WriteLine($"The following resource will be deep copied:\n{TestResource.Serialize(true)}");
366
367 // Test copying an un-posted resource
368 TestCopyHelper(TestResource, r => r.DeepCopy());
369 // Repeat the test for an already-posted resource instance.
370 TestCopyHelper(TestResource_Existing, r => r.DeepCopy());
371 }
372
374 [TestMethod, TestCategory(TypeName)]
376 {
377 // Test the ability to reflect on properties of the type
378 Assert.AreEqual("id", ReflectionUtilities.GetPropertyName<T>(resource => resource.id));
380 Assert.IsNotNull(idProperty);
381 Assert.IsTrue(idProperty.IsAttributeDefinedFast<AnalyzeRe.Attributes.ServerGenerated>());
382 // Test the ability to change (via reflection) a runtime property defined by an interface
384 instance.Change(i => i.id, "Test");
385 Assert.AreEqual("Test", instance.id);
386 // Local helper function for testing type reflection.
387 void TestTypeReflection(Type type)
388 {
389 PropertyInfo[] properties = type.GetUserFacingProperties();
390 Console.WriteLine($"{type.NiceTypeName()} User-Facing Properties:\n " +
391 String.Join("\n ", properties.Select(p =>
392 $"{p.PropertyType.NiceTypeName()} {p.DeclaringType.NiceTypeName()}.{p.Name} " +
393 (p.CanRead ? "{get;} " : "") + (p.CanWrite ? "{set;} " : ""))));
394 // Assert that no two reflected user-facing properties returned have the same name.
395 HashSet<string> uniqueNames = new HashSet<string>(properties.Select(p => p.Name));
396 Assert.AreEqual(properties.Length, uniqueNames.Count,
397 "One or more property names are duplicated in the list of user-facing properties.");
398 }
399 // Test the ability to get all user-facing properties from the runtime type.
400 TestTypeReflection(instance.GetType());
401 // If the base/interface type is different from the runtime type, test that separately
402 if (typeof(T) != instance.GetType())
404 }
405
408 [TestMethod, TestCategory(TypeName), TestCategory("Randomized")]
410 {
411 // Test the ability to randomly generate a resource of this type.
412 Reflection offlineReflection = new Reflection(Samples) { Validation_Enabled = false };
413 T generated = offlineReflection.CreateRandomizedInstance<T>();
414 Debug.WriteLine(generated.Serialize(true));
415 // Test the ability to randomize the value of every writable property of an instance
417 foreach (PropertyInfo pi in instance.GetType().GetUserFacingProperties(true, true))
418 offlineReflection.ChangePropertyValueRandomly(instance, pi);
419 }
420
426 public bool TryPost_RandomTestResources { get; set; } = true;
427
432
435 [TestMethod, TestCategory(TypeName), TestCategory("Randomized")]
437 {
438 // Test the ability to randomly generate a resource of this type.
440 // Verify that the resource is valid to be posted (if applicable)
441 if (TryPost_RandomTestResources && !typeof(T).IsAttributeDefinedFast<NotSaveableAttribute>())
442 {
443 fullyRandom = fullyRandom.Post();
444 fullyRandom = Reflection.FinalizePostedResource(fullyRandom);
445 }
448 }
449
452 [TestMethod, TestCategory(TypeName), TestCategory("Randomized")]
454 {
455 // Test the ability to randomize the value of every writable property of an instance
457 foreach (PropertyInfo pi in resourceTemplate.GetType().GetUserFacingProperties(true, true))
458 {
459 try
460 {
461 T instance = resourceTemplate.DeepCopy();
462 object original = pi.GetValue(instance);
464 Assert.AreNotEqual(original, newValue,
465 "ChangePropertyValueRandomly succeeded, but the value was not changed.");
466 // Now attempt to POST the resource and verify that it is accepted by the server
468 {
469 instance = instance.Post();
470 instance = Reflection.FinalizePostedResource(instance);
471 }
474 }
475 // We allow the method to fail (some properties cannot be changed in isolation),
476 // so long as the appropriate exception is raised.
478 {
479 Console.WriteLine(ex.Message);
480 // Any reason for failure other than this one is acceptable.
481 if (ex.Message.Contains("new value generated is equal to the original value"))
482 throw;
483 }
484 }
485 }
486
487 #region Helper Methods
490 {
491 // Check that all sub-resources (which contain a reference to their "owner" resource)
492 // do, in fact, reference this resource.
493 TestSubResourceEndpoints(original);
494
495 // Make a copy of the resource.
497 // Check that the resource and its copy are different objects
499 // Assert that the object properties are identical when compared in depth
500 AssertApi.AllPropertiesEqual(original, copy);
501 // Ensure that any sub-endpoints reference the new object, not the copy
502 TestSubResourceEndpoints(copy);
503
504 // Double-check that the original object's sub-resources are intact.
505 TestSubResourceEndpoints(original);
506 }
507
511 private static void TestSubResourceEndpoints(object owner)
512 {
513 foreach (PropertyInfo subResourceProperty in owner.GetType().GetPublicProperties_Fast()
514 .Where(p => typeof(SubResourceEndpoint).IsAssignableFrom(p.PropertyType)))
515 {
517 Assert.IsTrue(ReferenceEquals(owner, endpoint.Owner),
518 $"The {endpoint.GetType().NiceTypeName()} endpoint \"{subResourceProperty.Name}\" " +
519 "is still referencing the old copied resource instead of the new one.");
520 // If this SubResourceEndpoint contains its own SubResources, test them as well.
521 TestSubResourceEndpoints(endpoint);
522 }
523 }
524 #endregion Helper Methods
525 #endregion Utilities and Extension Methods
526 #endregion Inherited Test Methods
527 }
528}
static Reflection Reflection
Shared instance of a class for generating random resources.
void POST_StringAttribute_Empty(Expression< Func< T, string > > PropertyExpression, bool? shouldSucceed=null)
Post a resource with the specified string set to an empty string.
virtual void Test_Resource_POST_FieldsIdOnly()
Test that a user can post a resource and get back only the id using the omit query parameter.
virtual void Test_Resource_TypeReflectionUtilities()
Test reflection utilities meant to get extract information about runtime types.
virtual T TestResource_Existing
The resource used for all unit tests that require a prepared and already posted resource....
IInjectableResource< T > TestInjectableResource
Deriving class must override the TestResourceFactory to return a disposable POSTable test resource th...
virtual void Test_Resource_GetCollectionName()
Verify that all runtime types expose their collection name.
bool TryPost_RandomTestResources
Allows deriving classes to disable attempting to POST and validate random resources generated by the ...
void POST_ListAttribute_Duplicates< TItem, TValue >(Expression< Func< T, ICollection< TItem > > > PropertyExpression, TValue valueToDuplicate, bool shouldSucceed)
virtual void POST_WithValue< TProperty >(Expression< Func< T, TProperty > > PropertyExpression, TProperty newValue, bool shouldSucceed)
Modify a property of the test resource, and then test it.
virtual void Test_Resource_OfflineRandomGeneration()
Test reflection utilities meant to randomly change property values (used for testing comprarer covera...
bool TryRunAdditionalValidResourceTests_RandomTestResources
Similar to TryPost_RandomTestResources - but controls whether these resources have the AdditionalVali...
virtual void Test_Reflection_ChangePropertyValueRandomly()
Test reflection utilities meant to randomly change property values in a way that would pass server va...
static void TestCopyHelper< TCopy >(TCopy original, Func< TCopy, TCopy > copyAction)
virtual void Test_Resource_ShallowCopy()
Test that any resource can be "Shallow Copied", which is a quick copy that does a member-wise clone o...
virtual void Test_Resource_GET_FromBatch()
Test that resources of this type can be retrieved using the collection batch-get feature.
void POST_Reference_NonExistantId< TRefType >(Expression< Func< T, IReference< TRefType > > > PropertyExpression)
Posts a reference with a non-existent Id and verifies the correct error is returned.
virtual void Test_Resource_DeepCopy()
Test that any resource can be "Deep Copied", a slower copy method that creates a new duplicate instan...
void POST_ListAttribute_Empty< TItem >(Expression< Func< T, ICollection< TItem > > > PropertyExpression, bool? shouldSucceed=null)
void POST_Attribute_Null< TProperty >(Expression< Func< T, TProperty > > PropertyExpression, bool? shouldSucceed=null)
Post a resource with the specified property set to null.
virtual void Test_Resource_POST_ExistingId()
Verifies that it is okay to post an object with an ID that already exists on the server,...
virtual void Test_Resource_POST_Id_Ignored()
No matter what string we set the Id property to, POST should succeed because the Id of the posted res...
void POST_Reference_NullId< TRefType >(Expression< Func< T, IReference< TRefType > > > PropertyExpression)
Posts a reference with a null Id and verifies the correct error is returned.
virtual void AdditionalValidResourceTests(T posted)
Deriving classes can optionally override this function to perform additional validation on every succ...
void POST_ReferenceList_NonExistantId< TRefType >(Expression< Func< T, ICollection< IReference< TRefType > > > > PropertyExpression)
Posts a reference list with a non-existent Id and verifies the correct error is returned.
static ICollection< TCollectionItem > NewCollection< TTarget, TCollectionItem >(Expression< Func< TTarget, ICollection< TCollectionItem > > > propertyExpression, IEnumerable< TCollectionItem > initialItems=null)
Helper method to create a new collection instance based on the type of the referenced property expres...
virtual T TestResource
The resource used for all unit tests that require a valid prepared but unPOSTed resource.
virtual void POST_ThenDoAction(T validToPost, Action< T > toExecute)
Post a valid resource under the assumption that it will succeed, then perform an action on the result...
void TestResourceDeleteHelper(T toDelete, bool isDeleteAllowed)
virtual void Test_Resource_OnlineRandomGeneration()
Test reflection utilities meant to randomly change property values in a way that would pass server va...
void POST_Reference_EmptyStringId< TRefType >(Expression< Func< T, IReference< TRefType > > > PropertyExpression)
Posts a reference with a empty string Id and verifies the correct error is returned.
Create a test class that takes care of setting the server URL and cleaning up after each unit test.
Exposes sample resource objects, with built-in methods for injecting dependencies.
Definition Samples.cs:14
static string Valid_NonExistant_UUID
Definition Samples.cs:41
static void MethodIsAllowed(Action request, string methodName, bool methodAllowed=true)
Wrap a request in a tryGet with some formatting for testing purposes.
Definition AssertApi.cs:98
static void IsValidUUID(string toCheck)
Assert that the string is a valid UUID.
Definition AssertApi.cs:644
static Action< APIRequestException > ApiExceptionTest(HttpStatusCode expectedStatusCode)
Generate a function that will test a REST request exception in a standard way.
Definition AssertApi.cs:539
Retrieve settings from environment variables if they exist, or the project settings file otherwise.
static bool RUN_OFFLINE
Controls whether tests that normally require a connection to the server should be allowed to try to r...
Generic Unit test implementations that will test REST methods on arbitrary resources.
An exception throw when a random generation routine fails to produce a POSTable resource.
A collection of filthy hacks to populate some fields of APIResources objects of any type.
Definition Reflection.cs:41
object ChangePropertyValueRandomly(IAPIType obj, PropertyInfo prop, int max_attempts=Max_ReRandomize_Attempts, RecursionContext parentRecursionInfo=null)
Randomly change the specified property to some different value.
object CreateRandomizedInstance(Type desiredType, RecursionContext parentRecursionInfo=null)
Generates a new randomized object of the specified type.
A custom exception class that includes the RestSharp.IRestResponse that generated the exception,...
Describes a collection of resources which can be listed.
Describes an additional sub-resource available off a resource which is not actually a property of tha...
Parameters that can be added to your REST requests to access additional API features.
static RequestParameters Fields(IEnumerable< string > fieldNames)
Can be added to your GET requests to return only the specified fields in the response body....
API methods / requests made available to the user.
static string GetCollectionName(Type requestType)
Gets the collection name for the given APIResource type, whether it's instantiable or not.
static ICollectionResponse< IAPIResource > BatchGet(Type resourceType, IEnumerable< string > ids, IEnumerable< Parameter > requestParameters=null, string collectionNameOverride=null, int? timeout=null)
Get a collection of resources from the server that match the set of resource ids specified.
Specifies that the enumerable property with this attribute cannot be empty when POSTing the resource ...
Specifies that the property with this attribute cannot be null when POSTing the resource to the serve...
Indicates that while the current APIResource-derived class can be constructed and potentially inlined...
Specifies that a property is generated by the server and should not be specified on the client side d...
Utilities that reflect on a type or property expression.
T Unposted
The unPOSTed resource definition.
T Posted
The posted resource, ready to be referenced.
Interface for Base class used by all resources.
string id
The resource's unique identifier. It will be used in the request URL when requesting the resource fro...