diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1bcec53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +xcuserdata/ +*.pbxuser +*.xcworkspace diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..7f8fff0 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,12 @@ +included: + - QuizTrain + - QuizTrainTests +disabled_rules: + - empty_enum_arguments + - file_length + - function_body_length + - identifier_name + - line_length + - nesting + - type_body_length + - type_name diff --git a/Entities.png b/Entities.png new file mode 100755 index 0000000..c5854ae Binary files /dev/null and b/Entities.png differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c24ae68 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2018 Venmo + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/QuizTrain.xcodeproj/project.pbxproj b/QuizTrain.xcodeproj/project.pbxproj new file mode 100644 index 0000000..d87fe29 --- /dev/null +++ b/QuizTrain.xcodeproj/project.pbxproj @@ -0,0 +1,2882 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 48; + objects = { + +/* Begin PBXBuildFile section */ + FE018DC71F85708C001A2FEF /* ModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE018DC61F85708C001A2FEF /* ModelTests.swift */; }; + FE0B15471FED689F0009B570 /* ObjectAPITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D4F1F75956C00DF1039 /* ObjectAPITests.swift */; }; + FE0B15481FED68A00009B570 /* ObjectAPITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D4F1F75956C00DF1039 /* ObjectAPITests.swift */; }; + FE0B15491FED68A00009B570 /* ObjectAPITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D4F1F75956C00DF1039 /* ObjectAPITests.swift */; }; + FE11181C1F6C3A4B00D24A5F /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE11181B1F6C3A4B00D24A5F /* Config.swift */; }; + FE1474D61FAA475A0049DA84 /* Equatable+OptionalArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474D51FAA475A0049DA84 /* Equatable+OptionalArrayTests.swift */; }; + FE1474DA1FAA698F0049DA84 /* ObjectAPI.RequestErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474D91FAA698F0049DA84 /* ObjectAPI.RequestErrorDebug.swift */; }; + FE1474DC1FAA69AA0049DA84 /* ObjectAPI.DataRequestErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474DB1FAA69AA0049DA84 /* ObjectAPI.DataRequestErrorDebug.swift */; }; + FE1474DE1FAA69B70049DA84 /* ObjectAPI.UpdateRequestErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474DD1FAA69B70049DA84 /* ObjectAPI.UpdateRequestErrorDebug.swift */; }; + FE1474E21FAA69F70049DA84 /* ObjectAPI.ClientErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474E11FAA69F70049DA84 /* ObjectAPI.ClientErrorDebug.swift */; }; + FE1474E51FAA6A0D0049DA84 /* ObjectAPI.ServerErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474E41FAA6A0D0049DA84 /* ObjectAPI.ServerErrorDebug.swift */; }; + FE1474E81FAA6A270049DA84 /* ObjectAPI.StatusCodeErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474E71FAA6A270049DA84 /* ObjectAPI.StatusCodeErrorDebug.swift */; }; + FE1474EA1FAA6A340049DA84 /* ObjectAPI.DataProcessingErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474E91FAA6A340049DA84 /* ObjectAPI.DataProcessingErrorDebug.swift */; }; + FE1474EC1FAA6A5B0049DA84 /* ObjectAPI.ObjectConversionErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474EB1FAA6A5B0049DA84 /* ObjectAPI.ObjectConversionErrorDebug.swift */; }; + FE1FB1041F9131B200383724 /* UpdateRequestJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1FB1031F9131B200383724 /* UpdateRequestJSON.swift */; }; + FE208DEB1FBB69B80065BE88 /* NewTestResults.Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DEA1FBB69B80065BE88 /* NewTestResults.Result.swift */; }; + FE208DED1FBB69C60065BE88 /* NewCaseResults.Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DEC1FBB69C60065BE88 /* NewCaseResults.Result.swift */; }; + FE208DEF1FBB6B920065BE88 /* NewTestResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DEE1FBB6B920065BE88 /* NewTestResults.swift */; }; + FE208DF11FBB6BA10065BE88 /* NewCaseResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DF01FBB6BA10065BE88 /* NewCaseResults.swift */; }; + FE208DF51FBB716B0065BE88 /* NewCaseResults.ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DF41FBB716B0065BE88 /* NewCaseResults.ResultTests.swift */; }; + FE208DFA1FBB95150065BE88 /* Validatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DF91FBB95150065BE88 /* Validatable.swift */; }; + FE20D03F1FD876820057A45C /* Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE20D03E1FD876820057A45C /* Identifiable.swift */; }; + FE2245851F75AC82009F2B2B /* CustomFieldsContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2245841F75AC82009F2B2B /* CustomFieldsContainer.swift */; }; + FE23A9261F59F3A0007E946D /* QuizTrain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FE23A91C1F59F3A0007E946D /* QuizTrain.framework */; }; + FE23A92D1F59F3A0007E946D /* QuizTrain.h in Headers */ = {isa = PBXBuildFile; fileRef = FE23A91F1F59F3A0007E946D /* QuizTrain.h */; settings = {ATTRIBUTES = (Public, ); }; }; + FE23A9671F59F4B3007E946D /* ResultField.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9661F59F4B3007E946D /* ResultField.swift */; }; + FE23A9691F59F4BA007E946D /* Run.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9681F59F4BA007E946D /* Run.swift */; }; + FE23A96B1F59F4C3007E946D /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A96A1F59F4C3007E946D /* Section.swift */; }; + FE23A96D1F59F4CE007E946D /* Suite.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A96C1F59F4CE007E946D /* Suite.swift */; }; + FE23A96F1F59F4D9007E946D /* Template.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A96E1F59F4D9007E946D /* Template.swift */; }; + FE23A9711F59F4E2007E946D /* Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9701F59F4E2007E946D /* Test.swift */; }; + FE23A9731F59F4EB007E946D /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9721F59F4EB007E946D /* User.swift */; }; + FE2877EC1FBB979C004503FB /* ValidatableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2877EB1FBB979C004503FB /* ValidatableTests.swift */; }; + FE2877EE1FBB9803004503FB /* AssertValidatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2877ED1FBB9803004503FB /* AssertValidatable.swift */; }; + FE2877F01FBB9A80004503FB /* ValidatableObjectProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2877EF1FBB9A80004503FB /* ValidatableObjectProvider.swift */; }; + FE2877F41FBBBC50004503FB /* NewCaseResultsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DF21FBB714B0065BE88 /* NewCaseResultsTests.swift */; }; + FE2877F61FBBBD37004503FB /* NewTestResultsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795BF1FA799A90030C395 /* NewTestResultsTests.swift */; }; + FE2877FA1FBBC47B004503FB /* FilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2877F91FBBC47B004503FB /* FilterTests.swift */; }; + FE2D018B1FBCE70B00473B84 /* NewPlan.Entry.Run.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2D018A1FBCE70B00473B84 /* NewPlan.Entry.Run.swift */; }; + FE2F1AD51F843AEE00FF9E0C /* AssertJSONDeserializing.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE58F54B1F7EBF24009A1B4E /* AssertJSONDeserializing.swift */; }; + FE2F1AD71F844FCD00FF9E0C /* JSONSerializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2F1AD61F844FCD00FF9E0C /* JSONSerializable.swift */; }; + FE30F11D1F6AE4A300AA7761 /* Milestone.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A95C1F59F450007E946D /* Milestone.swift */; }; + FE30F11E1F6AED8100AA7761 /* ConfigurationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9741F5A042D007E946D /* ConfigurationGroup.swift */; }; + FE30F11F1F6AEDF600AA7761 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A95A1F59F443007E946D /* Configuration.swift */; }; + FE331BDE1F6AF98F00F9A653 /* CaseType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9581F59F437007E946D /* CaseType.swift */; }; + FE331BDF1F6AFE5D00F9A653 /* CaseField.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9561F59F42B007E946D /* CaseField.swift */; }; + FE331C1A1F6B3C0200F9A653 /* Case.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9541F59F415007E946D /* Case.swift */; }; + FE331C1C1F6B406300F9A653 /* CustomFieldType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE331C1B1F6B406300F9A653 /* CustomFieldType.swift */; }; + FE3795AA1FA7915C0030C395 /* AssertUpdateRequestJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795A91FA7915C0030C395 /* AssertUpdateRequestJSON.swift */; }; + FE3795B21FA799270030C395 /* NewConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795B11FA799270030C395 /* NewConfigurationTests.swift */; }; + FE3795B41FA799330030C395 /* NewConfigurationGroupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795B31FA799330030C395 /* NewConfigurationGroupTests.swift */; }; + FE3795B61FA799400030C395 /* NewMilestoneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795B51FA799400030C395 /* NewMilestoneTests.swift */; }; + FE3795BC1FA7998F0030C395 /* NewProjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795BB1FA7998F0030C395 /* NewProjectTests.swift */; }; + FE3795BE1FA7999C0030C395 /* NewResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795BD1FA7999C0030C395 /* NewResultTests.swift */; }; + FE3795C21FA799B70030C395 /* NewTestResults.ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795C11FA799B70030C395 /* NewTestResults.ResultTests.swift */; }; + FE3795C61FA799E30030C395 /* NewSectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795C51FA799E30030C395 /* NewSectionTests.swift */; }; + FE3795C81FA799F50030C395 /* NewSuiteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795C71FA799F50030C395 /* NewSuiteTests.swift */; }; + FE3795CA1FA79A070030C395 /* UpdatePlanEntryRunsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795C91FA79A070030C395 /* UpdatePlanEntryRunsTests.swift */; }; + FE3795CC1FA7A3AD0030C395 /* ObjectProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795CB1FA7A3AD0030C395 /* ObjectProvider.swift */; }; + FE3795CE1FA7A4540030C395 /* AddModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795CD1FA7A4530030C395 /* AddModelTests.swift */; }; + FE3795D21FA7C7960030C395 /* InitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795D11FA7C7960030C395 /* InitTests.swift */; }; + FE3795D41FA7C7A50030C395 /* JSONDeserializingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795D31FA7C7A50030C395 /* JSONDeserializingTests.swift */; }; + FE3795D61FA7C7B60030C395 /* JSONSerializingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795D51FA7C7B60030C395 /* JSONSerializingTests.swift */; }; + FE3795D81FA7C7D00030C395 /* JSONTwoWaySerializationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795D71FA7C7D00030C395 /* JSONTwoWaySerializationTests.swift */; }; + FE3795DA1FA7C7E30030C395 /* VariablePropertyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795D91FA7C7E30030C395 /* VariablePropertyTests.swift */; }; + FE3795DC1FA7C7FA0030C395 /* UpdateRequestJSONTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795DB1FA7C7FA0030C395 /* UpdateRequestJSONTests.swift */; }; + FE3795E11FA7D8360030C395 /* UpdateModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795E01FA7D8360030C395 /* UpdateModelTests.swift */; }; + FE3795E41FA7E1770030C395 /* NewCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795AF1FA799150030C395 /* NewCaseTests.swift */; }; + FE3795E61FA7EA6C0030C395 /* AssertAddRequestJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795E51FA7EA6C0030C395 /* AssertAddRequestJSON.swift */; }; + FE3795E81FA7EB380030C395 /* AddRequestJSONTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795E71FA7EB380030C395 /* AddRequestJSONTests.swift */; }; + FE3899171FCF2BDE0032E265 /* GetTemplatesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3899161FCF2BDE0032E265 /* GetTemplatesOperation.swift */; }; + FE496B5F1F7D9BCF00AE9454 /* ResultFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D4A1F75914E00DF1039 /* ResultFieldTests.swift */; }; + FE4E11D71F7C4112004A315E /* Plan.Entry.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE4E11D61F7C4112004A315E /* Plan.Entry.swift */; }; + FE4F8F391F84361F00447F9E /* NewCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE4F8F381F84361F00447F9E /* NewCase.swift */; }; + FE4F8F3B1F8437DE00447F9E /* JSONDeserializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE4F8F3A1F8437DE00447F9E /* JSONDeserializable.swift */; }; + FE51EFA21F882454007012E0 /* JSONDictionaryContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE51EFA11F882454007012E0 /* JSONDictionaryContainerTests.swift */; }; + FE5259121F82D1AD00E0DDB7 /* JSONKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5259111F82D1AD00E0DDB7 /* JSONKey.swift */; }; + FE549C421FBE5147008CDFCE /* NewPlanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795B71FA799520030C395 /* NewPlanTests.swift */; }; + FE549C431FBE514A008CDFCE /* NewPlan.EntryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795B91FA7996F0030C395 /* NewPlan.EntryTests.swift */; }; + FE549C441FBE514C008CDFCE /* NewPlan.Entry.RunTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE64D1571FBD0C3700ABA133 /* NewPlan.Entry.RunTests.swift */; }; + FE549C461FBE60F3008CDFCE /* Array+ContentComparisonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE549C451FBE60F3008CDFCE /* Array+ContentComparisonTests.swift */; }; + FE5869D71F7478D600BE5C5C /* CustomFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5869D61F7478D600BE5C5C /* CustomFields.swift */; }; + FE58F5481F7EBF04009A1B4E /* AssertProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE58F5471F7EBF04009A1B4E /* AssertProperties.swift */; }; + FE58F54A1F7EBF12009A1B4E /* AssertCustomFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE58F5491F7EBF12009A1B4E /* AssertCustomFields.swift */; }; + FE58F5611F7EE91D009A1B4E /* JSONDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE58F5601F7EE91D009A1B4E /* JSONDataProvider.swift */; }; + FE58F5661F7EEA29009A1B4E /* CustomFieldsDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE58F5651F7EEA29009A1B4E /* CustomFieldsDataProvider.swift */; }; + FE5A24F21FE092D300198848 /* UniqueSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5A24F11FE092D300198848 /* UniqueSelection.swift */; }; + FE5A24F41FE099BD00198848 /* Array+Random.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECFE7FC1FD9E13500968EA3 /* Array+Random.swift */; }; + FE5E1A301F8804E3001E479B /* JSONDictionaryContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5E1A2F1F8804E3001E479B /* JSONDictionaryContainer.swift */; }; + FE63715F1FD61CA500192CED /* GetConfigurationGroupsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE63715E1FD61CA500192CED /* GetConfigurationGroupsOperation.swift */; }; + FE6989211FA39B99006CC783 /* UpdatePlanEntryRuns.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6989201FA39B99006CC783 /* UpdatePlanEntryRuns.swift */; }; + FE6A6D271F75908100DF1039 /* CaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D261F75908100DF1039 /* CaseTests.swift */; }; + FE6A6D291F75909600DF1039 /* CaseTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D281F75909600DF1039 /* CaseTypeTests.swift */; }; + FE6A6D2B1F7590A100DF1039 /* ConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D2A1F7590A100DF1039 /* ConfigurationTests.swift */; }; + FE6A6D2D1F7590B000DF1039 /* ConfigurationGroupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D2C1F7590B000DF1039 /* ConfigurationGroupTests.swift */; }; + FE6A6D2F1F7590BA00DF1039 /* MilestoneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D2E1F7590BA00DF1039 /* MilestoneTests.swift */; }; + FE6A6D311F7590C700DF1039 /* PlanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D301F7590C700DF1039 /* PlanTests.swift */; }; + FE6A6D331F7590D200DF1039 /* PriorityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D321F7590D200DF1039 /* PriorityTests.swift */; }; + FE6A6D351F7590DC00DF1039 /* ProjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D341F7590DC00DF1039 /* ProjectTests.swift */; }; + FE6A6D371F7590E500DF1039 /* ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D361F7590E500DF1039 /* ResultTests.swift */; }; + FE6A6D391F7590F000DF1039 /* RunTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D381F7590F000DF1039 /* RunTests.swift */; }; + FE6A6D3B1F7590F900DF1039 /* SectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D3A1F7590F900DF1039 /* SectionTests.swift */; }; + FE6A6D3D1F75910500DF1039 /* SuiteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D3C1F75910500DF1039 /* SuiteTests.swift */; }; + FE6A6D3F1F75910E00DF1039 /* TemplateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D3E1F75910E00DF1039 /* TemplateTests.swift */; }; + FE6A6D411F75911A00DF1039 /* TestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D401F75911A00DF1039 /* TestTests.swift */; }; + FE6A6D431F75912400DF1039 /* UserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D421F75912400DF1039 /* UserTests.swift */; }; + FE6A6D451F75913000DF1039 /* CaseFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D441F75913000DF1039 /* CaseFieldTests.swift */; }; + FE6A6D471F75913900DF1039 /* ConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D461F75913900DF1039 /* ConfigTests.swift */; }; + FE6A6D491F75914300DF1039 /* CustomFieldTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D481F75914300DF1039 /* CustomFieldTypeTests.swift */; }; + FE6C8AE21F8BEA2E00F45642 /* MutableCustomFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6C8AE11F8BEA2E00F45642 /* MutableCustomFields.swift */; }; + FE75B2E01F83078A00DF367A /* CustomFieldsContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE22458B1F75BECC009F2B2B /* CustomFieldsContainerTests.swift */; }; + FE77BB761FA9132800E23865 /* NewRunTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795C31FA799D10030C395 /* NewRunTests.swift */; }; + FE791A251F7D9FFA00D7E870 /* Project.SuiteModeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE791A241F7D9FFA00D7E870 /* Project.SuiteModeTests.swift */; }; + FE7B53931FBF5BB1003C26BD /* Array+ContentComparison.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE7B53921FBF5BB1003C26BD /* Array+ContentComparison.swift */; }; + FE7CD3E41F9ABDC300C6108E /* NewPlan.Entry.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE7CD3E31F9ABDC300C6108E /* NewPlan.Entry.swift */; }; + FE813A181F73126F00265569 /* JSONDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE813A171F73126F00265569 /* JSONDictionary.swift */; }; + FE8C2D5A1FBA391F005A4150 /* Date+Seconds.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE8C2D591FBA391F005A4150 /* Date+Seconds.swift */; }; + FE978CE91F9528320005D181 /* API.RequestResultDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE978CE81F9528320005D181 /* API.RequestResultDebug.swift */; }; + FE978CEB1F9528400005D181 /* API.RequestErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE978CEA1F9528400005D181 /* API.RequestErrorDebug.swift */; }; + FE978CED1F9532BA0005D181 /* URLRequestDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE978CEC1F9532BA0005D181 /* URLRequestDebug.swift */; }; + FE9F38A61F69E5AD003BBA36 /* Plan.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A95E1F59F45C007E946D /* Plan.swift */; }; + FEA348781F5A141300C1E37A /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEA348771F5A141300C1E37A /* API.swift */; }; + FEAE7F921F7C5ABE00906FE1 /* Config.Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEAE7F911F7C5ABE00906FE1 /* Config.Context.swift */; }; + FEAE7F961F7C5D0C00906FE1 /* Config.ContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEAE7F951F7C5D0C00906FE1 /* Config.ContextTests.swift */; }; + FEAE99591FEC38CE00B52CA9 /* QuizTrain.h in Headers */ = {isa = PBXBuildFile; fileRef = FE23A91F1F59F3A0007E946D /* QuizTrain.h */; settings = {ATTRIBUTES = (Public, ); }; }; + FEAE99681FEC3BCE00B52CA9 /* QuizTrain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FEAE995F1FEC3BCD00B52CA9 /* QuizTrain.framework */; }; + FEAE99841FEC3BEB00B52CA9 /* QuizTrain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FEAE997B1FEC3BEB00B52CA9 /* QuizTrain.framework */; }; + FEAE99921FEC42C600B52CA9 /* AddRequestJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEB546831F9135CB00AA6DA5 /* AddRequestJSON.swift */; }; + FEAE99931FEC42C600B52CA9 /* AddRequestJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEB546831F9135CB00AA6DA5 /* AddRequestJSON.swift */; }; + FEAE99941FEC42C600B52CA9 /* AddRequestJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEB546831F9135CB00AA6DA5 /* AddRequestJSON.swift */; }; + FEAE99951FEC42C900B52CA9 /* AddRequestJSONKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEB546851F9135D500AA6DA5 /* AddRequestJSONKeys.swift */; }; + FEAE99961FEC42CA00B52CA9 /* AddRequestJSONKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEB546851F9135D500AA6DA5 /* AddRequestJSONKeys.swift */; }; + FEAE99971FEC42CA00B52CA9 /* AddRequestJSONKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEB546851F9135D500AA6DA5 /* AddRequestJSONKeys.swift */; }; + FEAE99981FEC42CD00B52CA9 /* CustomFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5869D61F7478D600BE5C5C /* CustomFields.swift */; }; + FEAE99991FEC42CD00B52CA9 /* CustomFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5869D61F7478D600BE5C5C /* CustomFields.swift */; }; + FEAE999B1FEC42CE00B52CA9 /* CustomFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5869D61F7478D600BE5C5C /* CustomFields.swift */; }; + FEAE999C1FEC42D000B52CA9 /* MutableCustomFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6C8AE11F8BEA2E00F45642 /* MutableCustomFields.swift */; }; + FEAE999D1FEC42D100B52CA9 /* MutableCustomFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6C8AE11F8BEA2E00F45642 /* MutableCustomFields.swift */; }; + FEAE999E1FEC42D200B52CA9 /* MutableCustomFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6C8AE11F8BEA2E00F45642 /* MutableCustomFields.swift */; }; + FEAE999F1FEC42D400B52CA9 /* CustomFieldsContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2245841F75AC82009F2B2B /* CustomFieldsContainer.swift */; }; + FEAE99A01FEC42D500B52CA9 /* CustomFieldsContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2245841F75AC82009F2B2B /* CustomFieldsContainer.swift */; }; + FEAE99A11FEC42D500B52CA9 /* CustomFieldsContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2245841F75AC82009F2B2B /* CustomFieldsContainer.swift */; }; + FEAE99A21FEC42D800B52CA9 /* ErrorContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEBDA3C01FD1C7F400124430 /* ErrorContainer.swift */; }; + FEAE99A31FEC42D800B52CA9 /* ErrorContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEBDA3C01FD1C7F400124430 /* ErrorContainer.swift */; }; + FEAE99A41FEC42D900B52CA9 /* ErrorContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEBDA3C01FD1C7F400124430 /* ErrorContainer.swift */; }; + FEAE99A51FEC42DB00B52CA9 /* JSONDictionaryContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5E1A2F1F8804E3001E479B /* JSONDictionaryContainer.swift */; }; + FEAE99A61FEC42DB00B52CA9 /* JSONDictionaryContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5E1A2F1F8804E3001E479B /* JSONDictionaryContainer.swift */; }; + FEAE99A71FEC42DC00B52CA9 /* JSONDictionaryContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5E1A2F1F8804E3001E479B /* JSONDictionaryContainer.swift */; }; + FEAE99A81FEC42DE00B52CA9 /* DebugDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC214061FD741F700036B17 /* DebugDescription.swift */; }; + FEAE99A91FEC42DF00B52CA9 /* DebugDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC214061FD741F700036B17 /* DebugDescription.swift */; }; + FEAE99AB1FEC42E000B52CA9 /* DebugDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC214061FD741F700036B17 /* DebugDescription.swift */; }; + FEAE99AC1FEC42E400B52CA9 /* DebugDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC214081FD7420300036B17 /* DebugDetails.swift */; }; + FEAE99AD1FEC42E500B52CA9 /* DebugDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC214081FD7420300036B17 /* DebugDetails.swift */; }; + FEAE99AE1FEC42E500B52CA9 /* DebugDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC214081FD7420300036B17 /* DebugDetails.swift */; }; + FEAE99AF1FEC42E800B52CA9 /* SingleMatchError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDFF2741FE1B09000AEB3D6 /* SingleMatchError.swift */; }; + FEAE99B01FEC42E800B52CA9 /* SingleMatchError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDFF2741FE1B09000AEB3D6 /* SingleMatchError.swift */; }; + FEAE99B21FEC42E900B52CA9 /* SingleMatchError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDFF2741FE1B09000AEB3D6 /* SingleMatchError.swift */; }; + FEAE99B31FEC42EC00B52CA9 /* MultipleMatchError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDFF2761FE1B0A000AEB3D6 /* MultipleMatchError.swift */; }; + FEAE99B41FEC42ED00B52CA9 /* MultipleMatchError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDFF2761FE1B0A000AEB3D6 /* MultipleMatchError.swift */; }; + FEAE99B61FEC42EE00B52CA9 /* MultipleMatchError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDFF2761FE1B0A000AEB3D6 /* MultipleMatchError.swift */; }; + FEAE99B71FEC42F100B52CA9 /* Array+ContentComparison.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE7B53921FBF5BB1003C26BD /* Array+ContentComparison.swift */; }; + FEAE99B81FEC42F200B52CA9 /* Array+ContentComparison.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE7B53921FBF5BB1003C26BD /* Array+ContentComparison.swift */; }; + FEAE99BA1FEC42F300B52CA9 /* Array+ContentComparison.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE7B53921FBF5BB1003C26BD /* Array+ContentComparison.swift */; }; + FEAE99BB1FEC42F500B52CA9 /* Date+Seconds.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE8C2D591FBA391F005A4150 /* Date+Seconds.swift */; }; + FEAE99BC1FEC42F600B52CA9 /* Date+Seconds.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE8C2D591FBA391F005A4150 /* Date+Seconds.swift */; }; + FEAE99BD1FEC42F700B52CA9 /* Date+Seconds.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE8C2D591FBA391F005A4150 /* Date+Seconds.swift */; }; + FEAE99BE1FEC42FA00B52CA9 /* Equatable+OptionalArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2E774A1F85923F00EF5E54 /* Equatable+OptionalArray.swift */; }; + FEAE99BF1FEC42FA00B52CA9 /* Equatable+OptionalArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2E774A1F85923F00EF5E54 /* Equatable+OptionalArray.swift */; }; + FEAE99C01FEC42FB00B52CA9 /* Equatable+OptionalArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2E774A1F85923F00EF5E54 /* Equatable+OptionalArray.swift */; }; + FEAE99C11FEC42FC00B52CA9 /* Equatable+OptionalArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2E774A1F85923F00EF5E54 /* Equatable+OptionalArray.swift */; }; + FEAE99C21FEC42FE00B52CA9 /* Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE20D03E1FD876820057A45C /* Identifiable.swift */; }; + FEAE99C31FEC42FF00B52CA9 /* Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE20D03E1FD876820057A45C /* Identifiable.swift */; }; + FEAE99C41FEC430000B52CA9 /* Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE20D03E1FD876820057A45C /* Identifiable.swift */; }; + FEAE99C51FEC430200B52CA9 /* JSONKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5259111F82D1AD00E0DDB7 /* JSONKey.swift */; }; + FEAE99C61FEC430300B52CA9 /* JSONKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5259111F82D1AD00E0DDB7 /* JSONKey.swift */; }; + FEAE99C71FEC430300B52CA9 /* JSONKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5259111F82D1AD00E0DDB7 /* JSONKey.swift */; }; + FEAE99C81FEC430600B52CA9 /* JSONDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE813A171F73126F00265569 /* JSONDictionary.swift */; }; + FEAE99C91FEC430600B52CA9 /* JSONDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE813A171F73126F00265569 /* JSONDictionary.swift */; }; + FEAE99CA1FEC430800B52CA9 /* JSONDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE813A171F73126F00265569 /* JSONDictionary.swift */; }; + FEAE99CB1FEC430A00B52CA9 /* JSONDeserializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE4F8F3A1F8437DE00447F9E /* JSONDeserializable.swift */; }; + FEAE99CC1FEC430B00B52CA9 /* JSONDeserializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE4F8F3A1F8437DE00447F9E /* JSONDeserializable.swift */; }; + FEAE99CD1FEC430C00B52CA9 /* JSONDeserializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE4F8F3A1F8437DE00447F9E /* JSONDeserializable.swift */; }; + FEAE99CE1FEC430E00B52CA9 /* JSONSerializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2F1AD61F844FCD00FF9E0C /* JSONSerializable.swift */; }; + FEAE99CF1FEC430F00B52CA9 /* JSONSerializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2F1AD61F844FCD00FF9E0C /* JSONSerializable.swift */; }; + FEAE99D01FEC430F00B52CA9 /* JSONSerializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2F1AD61F844FCD00FF9E0C /* JSONSerializable.swift */; }; + FEAE99D11FEC431200B52CA9 /* AsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEBF50361FCF3E91005B86B7 /* AsyncOperation.swift */; }; + FEAE99D21FEC431300B52CA9 /* AsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEBF50361FCF3E91005B86B7 /* AsyncOperation.swift */; }; + FEAE99D31FEC431300B52CA9 /* AsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEBF50361FCF3E91005B86B7 /* AsyncOperation.swift */; }; + FEAE99D41FEC431500B52CA9 /* UpdateRequestJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1FB1031F9131B200383724 /* UpdateRequestJSON.swift */; }; + FEAE99D51FEC431600B52CA9 /* UpdateRequestJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1FB1031F9131B200383724 /* UpdateRequestJSON.swift */; }; + FEAE99D61FEC431700B52CA9 /* UpdateRequestJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1FB1031F9131B200383724 /* UpdateRequestJSON.swift */; }; + FEAE99D71FEC431900B52CA9 /* UpdateRequestJSONKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6838E11F8FE10500431C1C /* UpdateRequestJSONKeys.swift */; }; + FEAE99D81FEC431900B52CA9 /* UpdateRequestJSONKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6838E11F8FE10500431C1C /* UpdateRequestJSONKeys.swift */; }; + FEAE99D91FEC431A00B52CA9 /* UpdateRequestJSONKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6838E11F8FE10500431C1C /* UpdateRequestJSONKeys.swift */; }; + FEAE99DA1FEC431B00B52CA9 /* UpdateRequestJSONKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6838E11F8FE10500431C1C /* UpdateRequestJSONKeys.swift */; }; + FEAE99DB1FEC431E00B52CA9 /* Validatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DF91FBB95150065BE88 /* Validatable.swift */; }; + FEAE99DC1FEC431F00B52CA9 /* Validatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DF91FBB95150065BE88 /* Validatable.swift */; }; + FEAE99DD1FEC431F00B52CA9 /* Validatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DF91FBB95150065BE88 /* Validatable.swift */; }; + FEAE99DE1FEC432200B52CA9 /* Outcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEB546871F9170A900AA6DA5 /* Outcome.swift */; }; + FEAE99DF1FEC432300B52CA9 /* Outcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEB546871F9170A900AA6DA5 /* Outcome.swift */; }; + FEAE99E01FEC432300B52CA9 /* Outcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEB546871F9170A900AA6DA5 /* Outcome.swift */; }; + FEAE99E11FEC432700B52CA9 /* QueryItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEC443F1FB4D3270042BD5A /* QueryItemProvider.swift */; }; + FEAE99E21FEC432700B52CA9 /* QueryItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEC443F1FB4D3270042BD5A /* QueryItemProvider.swift */; }; + FEAE99E31FEC432800B52CA9 /* QueryItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEC443F1FB4D3270042BD5A /* QueryItemProvider.swift */; }; + FEAE99E41FEC432A00B52CA9 /* UniqueSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5A24F11FE092D300198848 /* UniqueSelection.swift */; }; + FEAE99E51FEC432A00B52CA9 /* UniqueSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5A24F11FE092D300198848 /* UniqueSelection.swift */; }; + FEAE99E61FEC432B00B52CA9 /* UniqueSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5A24F11FE092D300198848 /* UniqueSelection.swift */; }; + FEAE99E71FEC432E00B52CA9 /* CustomFieldType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE331C1B1F6B406300F9A653 /* CustomFieldType.swift */; }; + FEAE99E81FEC432E00B52CA9 /* CustomFieldType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE331C1B1F6B406300F9A653 /* CustomFieldType.swift */; }; + FEAE99E91FEC432F00B52CA9 /* CustomFieldType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE331C1B1F6B406300F9A653 /* CustomFieldType.swift */; }; + FEAE99EA1FEC433100B52CA9 /* Project.SuiteMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEAF0BFD1F7D9F15001D4F10 /* Project.SuiteMode.swift */; }; + FEAE99EB1FEC433200B52CA9 /* Project.SuiteMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEAF0BFD1F7D9F15001D4F10 /* Project.SuiteMode.swift */; }; + FEAE99EC1FEC433200B52CA9 /* Project.SuiteMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEAF0BFD1F7D9F15001D4F10 /* Project.SuiteMode.swift */; }; + FEAE99ED1FEC433500B52CA9 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE11181B1F6C3A4B00D24A5F /* Config.swift */; }; + FEAE99EE1FEC433600B52CA9 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE11181B1F6C3A4B00D24A5F /* Config.swift */; }; + FEAE99EF1FEC433600B52CA9 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE11181B1F6C3A4B00D24A5F /* Config.swift */; }; + FEAE99F01FEC433800B52CA9 /* Config.Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEAE7F911F7C5ABE00906FE1 /* Config.Context.swift */; }; + FEAE99F21FEC433B00B52CA9 /* Config.Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEAE7F911F7C5ABE00906FE1 /* Config.Context.swift */; }; + FEAE99F31FEC433B00B52CA9 /* Config.Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEAE7F911F7C5ABE00906FE1 /* Config.Context.swift */; }; + FEAE99F41FEC4EC100B52CA9 /* Case.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9541F59F415007E946D /* Case.swift */; }; + FEAE99F51FEC4EC200B52CA9 /* Case.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9541F59F415007E946D /* Case.swift */; }; + FEAE99F61FEC4EC200B52CA9 /* Case.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9541F59F415007E946D /* Case.swift */; }; + FEAE99F71FEC4EC500B52CA9 /* CaseField.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9561F59F42B007E946D /* CaseField.swift */; }; + FEAE99F81FEC4EC500B52CA9 /* CaseField.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9561F59F42B007E946D /* CaseField.swift */; }; + FEAE99F91FEC4EC600B52CA9 /* CaseField.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9561F59F42B007E946D /* CaseField.swift */; }; + FEAE99FA1FEC4EC800B52CA9 /* CaseType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9581F59F437007E946D /* CaseType.swift */; }; + FEAE99FB1FEC4EC800B52CA9 /* CaseType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9581F59F437007E946D /* CaseType.swift */; }; + FEAE99FC1FEC4EC900B52CA9 /* CaseType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9581F59F437007E946D /* CaseType.swift */; }; + FEAE99FD1FEC4ECB00B52CA9 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A95A1F59F443007E946D /* Configuration.swift */; }; + FEAE99FE1FEC4ECB00B52CA9 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A95A1F59F443007E946D /* Configuration.swift */; }; + FEAE99FF1FEC4ECD00B52CA9 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A95A1F59F443007E946D /* Configuration.swift */; }; + FEAE9A001FEC4ED000B52CA9 /* ConfigurationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9741F5A042D007E946D /* ConfigurationGroup.swift */; }; + FEAE9A011FEC4ED100B52CA9 /* ConfigurationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9741F5A042D007E946D /* ConfigurationGroup.swift */; }; + FEAE9A021FEC4ED100B52CA9 /* ConfigurationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9741F5A042D007E946D /* ConfigurationGroup.swift */; }; + FEAE9A031FEC4ED400B52CA9 /* Milestone.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A95C1F59F450007E946D /* Milestone.swift */; }; + FEAE9A041FEC4ED400B52CA9 /* Milestone.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A95C1F59F450007E946D /* Milestone.swift */; }; + FEAE9A051FEC4ED500B52CA9 /* Milestone.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A95C1F59F450007E946D /* Milestone.swift */; }; + FEAE9A061FEC4ED800B52CA9 /* Plan.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A95E1F59F45C007E946D /* Plan.swift */; }; + FEAE9A071FEC4ED900B52CA9 /* Plan.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A95E1F59F45C007E946D /* Plan.swift */; }; + FEAE9A081FEC4ED900B52CA9 /* Plan.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A95E1F59F45C007E946D /* Plan.swift */; }; + FEAE9A091FEC4EDC00B52CA9 /* Plan.Entry.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE4E11D61F7C4112004A315E /* Plan.Entry.swift */; }; + FEAE9A0A1FEC4EDD00B52CA9 /* Plan.Entry.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE4E11D61F7C4112004A315E /* Plan.Entry.swift */; }; + FEAE9A0B1FEC4EDD00B52CA9 /* Plan.Entry.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE4E11D61F7C4112004A315E /* Plan.Entry.swift */; }; + FEAE9A0C1FEC4EED00B52CA9 /* Priority.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9601F59F46C007E946D /* Priority.swift */; }; + FEAE9A0D1FEC4EED00B52CA9 /* Priority.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9601F59F46C007E946D /* Priority.swift */; }; + FEAE9A0E1FEC4EEE00B52CA9 /* Priority.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9601F59F46C007E946D /* Priority.swift */; }; + FEAE9A0F1FEC4EF100B52CA9 /* Project.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9621F59F478007E946D /* Project.swift */; }; + FEAE9A101FEC4EF200B52CA9 /* Project.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9621F59F478007E946D /* Project.swift */; }; + FEAE9A111FEC4EF200B52CA9 /* Project.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9621F59F478007E946D /* Project.swift */; }; + FEAE9A121FEC4EF600B52CA9 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9641F59F489007E946D /* Result.swift */; }; + FEAE9A131FEC4EF700B52CA9 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9641F59F489007E946D /* Result.swift */; }; + FEAE9A141FEC4EF700B52CA9 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9641F59F489007E946D /* Result.swift */; }; + FEAE9A151FEC4EFA00B52CA9 /* ResultField.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9661F59F4B3007E946D /* ResultField.swift */; }; + FEAE9A161FEC4EFB00B52CA9 /* ResultField.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9661F59F4B3007E946D /* ResultField.swift */; }; + FEAE9A171FEC4EFB00B52CA9 /* ResultField.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9661F59F4B3007E946D /* ResultField.swift */; }; + FEAE9A181FEC4EFF00B52CA9 /* Run.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9681F59F4BA007E946D /* Run.swift */; }; + FEAE9A191FEC4EFF00B52CA9 /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A96A1F59F4C3007E946D /* Section.swift */; }; + FEAE9A1A1FEC4EFF00B52CA9 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED295461FA10E9300746DAB /* Status.swift */; }; + FEAE9A1B1FEC4EFF00B52CA9 /* Suite.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A96C1F59F4CE007E946D /* Suite.swift */; }; + FEAE9A1C1FEC4EFF00B52CA9 /* Template.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A96E1F59F4D9007E946D /* Template.swift */; }; + FEAE9A1D1FEC4EFF00B52CA9 /* Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9701F59F4E2007E946D /* Test.swift */; }; + FEAE9A1E1FEC4EFF00B52CA9 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9721F59F4EB007E946D /* User.swift */; }; + FEAE9A1F1FEC4F0000B52CA9 /* Run.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9681F59F4BA007E946D /* Run.swift */; }; + FEAE9A201FEC4F0000B52CA9 /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A96A1F59F4C3007E946D /* Section.swift */; }; + FEAE9A211FEC4F0000B52CA9 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED295461FA10E9300746DAB /* Status.swift */; }; + FEAE9A221FEC4F0000B52CA9 /* Suite.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A96C1F59F4CE007E946D /* Suite.swift */; }; + FEAE9A231FEC4F0000B52CA9 /* Template.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A96E1F59F4D9007E946D /* Template.swift */; }; + FEAE9A241FEC4F0000B52CA9 /* Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9701F59F4E2007E946D /* Test.swift */; }; + FEAE9A251FEC4F0000B52CA9 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9721F59F4EB007E946D /* User.swift */; }; + FEAE9A261FEC4F0000B52CA9 /* Run.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9681F59F4BA007E946D /* Run.swift */; }; + FEAE9A271FEC4F0000B52CA9 /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A96A1F59F4C3007E946D /* Section.swift */; }; + FEAE9A281FEC4F0000B52CA9 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED295461FA10E9300746DAB /* Status.swift */; }; + FEAE9A291FEC4F0000B52CA9 /* Suite.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A96C1F59F4CE007E946D /* Suite.swift */; }; + FEAE9A2A1FEC4F0000B52CA9 /* Template.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A96E1F59F4D9007E946D /* Template.swift */; }; + FEAE9A2B1FEC4F0000B52CA9 /* Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9701F59F4E2007E946D /* Test.swift */; }; + FEAE9A2C1FEC4F0000B52CA9 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9721F59F4EB007E946D /* User.swift */; }; + FEAE9A2D1FEC4F0600B52CA9 /* API.RequestErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE978CEA1F9528400005D181 /* API.RequestErrorDebug.swift */; }; + FEAE9A2E1FEC4F0600B52CA9 /* API.RequestResultDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE978CE81F9528320005D181 /* API.RequestResultDebug.swift */; }; + FEAE9A2F1FEC4F0600B52CA9 /* API.RequestErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE978CEA1F9528400005D181 /* API.RequestErrorDebug.swift */; }; + FEAE9A301FEC4F0600B52CA9 /* API.RequestResultDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE978CE81F9528320005D181 /* API.RequestResultDebug.swift */; }; + FEAE9A311FEC4F0700B52CA9 /* API.RequestErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE978CEA1F9528400005D181 /* API.RequestErrorDebug.swift */; }; + FEAE9A321FEC4F0700B52CA9 /* API.RequestResultDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE978CE81F9528320005D181 /* API.RequestResultDebug.swift */; }; + FEAE9A331FEC4F0A00B52CA9 /* ObjectAPI.ClientErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474E11FAA69F70049DA84 /* ObjectAPI.ClientErrorDebug.swift */; }; + FEAE9A341FEC4F0A00B52CA9 /* ObjectAPI.DataProcessingErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474E91FAA6A340049DA84 /* ObjectAPI.DataProcessingErrorDebug.swift */; }; + FEAE9A351FEC4F0A00B52CA9 /* ObjectAPI.DataRequestErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474DB1FAA69AA0049DA84 /* ObjectAPI.DataRequestErrorDebug.swift */; }; + FEAE9A361FEC4F0A00B52CA9 /* ObjectAPI.MatchErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC213FF1FD7302900036B17 /* ObjectAPI.MatchErrorDebug.swift */; }; + FEAE9A371FEC4F0A00B52CA9 /* ObjectAPI.ObjectConversionErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474EB1FAA6A5B0049DA84 /* ObjectAPI.ObjectConversionErrorDebug.swift */; }; + FEAE9A381FEC4F0A00B52CA9 /* ObjectAPI.RequestErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474D91FAA698F0049DA84 /* ObjectAPI.RequestErrorDebug.swift */; }; + FEAE9A391FEC4F0A00B52CA9 /* ObjectAPI.ServerErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474E41FAA6A0D0049DA84 /* ObjectAPI.ServerErrorDebug.swift */; }; + FEAE9A3A1FEC4F0A00B52CA9 /* ObjectAPI.StatusCodeErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474E71FAA6A270049DA84 /* ObjectAPI.StatusCodeErrorDebug.swift */; }; + FEAE9A3B1FEC4F0A00B52CA9 /* ObjectAPI.UpdateRequestErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474DD1FAA69B70049DA84 /* ObjectAPI.UpdateRequestErrorDebug.swift */; }; + FEAE9A3C1FEC4F0A00B52CA9 /* URLRequestDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE978CEC1F9532BA0005D181 /* URLRequestDebug.swift */; }; + FEAE9A3D1FEC4F0B00B52CA9 /* ObjectAPI.ClientErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474E11FAA69F70049DA84 /* ObjectAPI.ClientErrorDebug.swift */; }; + FEAE9A3E1FEC4F0B00B52CA9 /* ObjectAPI.DataProcessingErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474E91FAA6A340049DA84 /* ObjectAPI.DataProcessingErrorDebug.swift */; }; + FEAE9A3F1FEC4F0B00B52CA9 /* ObjectAPI.DataRequestErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474DB1FAA69AA0049DA84 /* ObjectAPI.DataRequestErrorDebug.swift */; }; + FEAE9A401FEC4F0B00B52CA9 /* ObjectAPI.MatchErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC213FF1FD7302900036B17 /* ObjectAPI.MatchErrorDebug.swift */; }; + FEAE9A411FEC4F0B00B52CA9 /* ObjectAPI.ObjectConversionErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474EB1FAA6A5B0049DA84 /* ObjectAPI.ObjectConversionErrorDebug.swift */; }; + FEAE9A421FEC4F0B00B52CA9 /* ObjectAPI.RequestErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474D91FAA698F0049DA84 /* ObjectAPI.RequestErrorDebug.swift */; }; + FEAE9A431FEC4F0B00B52CA9 /* ObjectAPI.ServerErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474E41FAA6A0D0049DA84 /* ObjectAPI.ServerErrorDebug.swift */; }; + FEAE9A441FEC4F0B00B52CA9 /* ObjectAPI.StatusCodeErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474E71FAA6A270049DA84 /* ObjectAPI.StatusCodeErrorDebug.swift */; }; + FEAE9A451FEC4F0B00B52CA9 /* ObjectAPI.UpdateRequestErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474DD1FAA69B70049DA84 /* ObjectAPI.UpdateRequestErrorDebug.swift */; }; + FEAE9A461FEC4F0B00B52CA9 /* URLRequestDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE978CEC1F9532BA0005D181 /* URLRequestDebug.swift */; }; + FEAE9A471FEC4F0B00B52CA9 /* ObjectAPI.ClientErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474E11FAA69F70049DA84 /* ObjectAPI.ClientErrorDebug.swift */; }; + FEAE9A481FEC4F0B00B52CA9 /* ObjectAPI.DataProcessingErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474E91FAA6A340049DA84 /* ObjectAPI.DataProcessingErrorDebug.swift */; }; + FEAE9A491FEC4F0B00B52CA9 /* ObjectAPI.DataRequestErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474DB1FAA69AA0049DA84 /* ObjectAPI.DataRequestErrorDebug.swift */; }; + FEAE9A4A1FEC4F0B00B52CA9 /* ObjectAPI.MatchErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC213FF1FD7302900036B17 /* ObjectAPI.MatchErrorDebug.swift */; }; + FEAE9A4B1FEC4F0B00B52CA9 /* ObjectAPI.ObjectConversionErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474EB1FAA6A5B0049DA84 /* ObjectAPI.ObjectConversionErrorDebug.swift */; }; + FEAE9A4C1FEC4F0B00B52CA9 /* ObjectAPI.RequestErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474D91FAA698F0049DA84 /* ObjectAPI.RequestErrorDebug.swift */; }; + FEAE9A4D1FEC4F0B00B52CA9 /* ObjectAPI.ServerErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474E41FAA6A0D0049DA84 /* ObjectAPI.ServerErrorDebug.swift */; }; + FEAE9A4E1FEC4F0B00B52CA9 /* ObjectAPI.StatusCodeErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474E71FAA6A270049DA84 /* ObjectAPI.StatusCodeErrorDebug.swift */; }; + FEAE9A4F1FEC4F0B00B52CA9 /* ObjectAPI.UpdateRequestErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474DD1FAA69B70049DA84 /* ObjectAPI.UpdateRequestErrorDebug.swift */; }; + FEAE9A501FEC4F0B00B52CA9 /* URLRequestDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE978CEC1F9532BA0005D181 /* URLRequestDebug.swift */; }; + FEAE9A511FEC4F0F00B52CA9 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEC443A1FB4D1BC0042BD5A /* Filter.swift */; }; + FEAE9A521FEC4F0F00B52CA9 /* Filter.Value.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEC443D1FB4D23F0042BD5A /* Filter.Value.swift */; }; + FEAE9A531FEC4F1000B52CA9 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEC443A1FB4D1BC0042BD5A /* Filter.swift */; }; + FEAE9A541FEC4F1000B52CA9 /* Filter.Value.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEC443D1FB4D23F0042BD5A /* Filter.Value.swift */; }; + FEAE9A551FEC4F1000B52CA9 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEC443A1FB4D1BC0042BD5A /* Filter.swift */; }; + FEAE9A561FEC4F1000B52CA9 /* Filter.Value.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEC443D1FB4D23F0042BD5A /* Filter.Value.swift */; }; + FEAE9A571FEC4F1400B52CA9 /* NewCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE4F8F381F84361F00447F9E /* NewCase.swift */; }; + FEAE9A581FEC4F1400B52CA9 /* NewConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEEE1F96BEE70083AD46 /* NewConfiguration.swift */; }; + FEAE9A591FEC4F1400B52CA9 /* NewConfigurationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEF01F96BEF10083AD46 /* NewConfigurationGroup.swift */; }; + FEAE9A5A1FEC4F1400B52CA9 /* NewMilestone.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEF21F96BF020083AD46 /* NewMilestone.swift */; }; + FEAE9A5B1FEC4F1400B52CA9 /* NewPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEF41F96BF130083AD46 /* NewPlan.swift */; }; + FEAE9A5C1FEC4F1400B52CA9 /* NewPlan.Entry.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE7CD3E31F9ABDC300C6108E /* NewPlan.Entry.swift */; }; + FEAE9A5D1FEC4F1400B52CA9 /* NewPlan.Entry.Run.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2D018A1FBCE70B00473B84 /* NewPlan.Entry.Run.swift */; }; + FEAE9A5E1FEC4F1400B52CA9 /* NewProject.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEF61F96BF290083AD46 /* NewProject.swift */; }; + FEAE9A5F1FEC4F1400B52CA9 /* NewResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEF81F96BF3A0083AD46 /* NewResult.swift */; }; + FEAE9A601FEC4F1400B52CA9 /* NewCaseResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DF01FBB6BA10065BE88 /* NewCaseResults.swift */; }; + FEAE9A611FEC4F1400B52CA9 /* NewCaseResults.Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DEC1FBB69C60065BE88 /* NewCaseResults.Result.swift */; }; + FEAE9A621FEC4F1400B52CA9 /* NewTestResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DEE1FBB6B920065BE88 /* NewTestResults.swift */; }; + FEAE9A631FEC4F1400B52CA9 /* NewTestResults.Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DEA1FBB69B80065BE88 /* NewTestResults.Result.swift */; }; + FEAE9A641FEC4F1400B52CA9 /* NewRun.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEFA1F96BF540083AD46 /* NewRun.swift */; }; + FEAE9A651FEC4F1400B52CA9 /* NewSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEFC1F96BF610083AD46 /* NewSection.swift */; }; + FEAE9A661FEC4F1400B52CA9 /* NewSuite.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEFE1F96BF730083AD46 /* NewSuite.swift */; }; + FEAE9A671FEC4F1500B52CA9 /* NewCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE4F8F381F84361F00447F9E /* NewCase.swift */; }; + FEAE9A681FEC4F1500B52CA9 /* NewConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEEE1F96BEE70083AD46 /* NewConfiguration.swift */; }; + FEAE9A691FEC4F1500B52CA9 /* NewConfigurationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEF01F96BEF10083AD46 /* NewConfigurationGroup.swift */; }; + FEAE9A6A1FEC4F1500B52CA9 /* NewMilestone.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEF21F96BF020083AD46 /* NewMilestone.swift */; }; + FEAE9A6B1FEC4F1500B52CA9 /* NewPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEF41F96BF130083AD46 /* NewPlan.swift */; }; + FEAE9A6C1FEC4F1500B52CA9 /* NewPlan.Entry.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE7CD3E31F9ABDC300C6108E /* NewPlan.Entry.swift */; }; + FEAE9A6D1FEC4F1500B52CA9 /* NewPlan.Entry.Run.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2D018A1FBCE70B00473B84 /* NewPlan.Entry.Run.swift */; }; + FEAE9A6E1FEC4F1500B52CA9 /* NewProject.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEF61F96BF290083AD46 /* NewProject.swift */; }; + FEAE9A6F1FEC4F1500B52CA9 /* NewResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEF81F96BF3A0083AD46 /* NewResult.swift */; }; + FEAE9A701FEC4F1500B52CA9 /* NewCaseResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DF01FBB6BA10065BE88 /* NewCaseResults.swift */; }; + FEAE9A711FEC4F1500B52CA9 /* NewCaseResults.Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DEC1FBB69C60065BE88 /* NewCaseResults.Result.swift */; }; + FEAE9A721FEC4F1500B52CA9 /* NewTestResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DEE1FBB6B920065BE88 /* NewTestResults.swift */; }; + FEAE9A731FEC4F1500B52CA9 /* NewTestResults.Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DEA1FBB69B80065BE88 /* NewTestResults.Result.swift */; }; + FEAE9A741FEC4F1500B52CA9 /* NewRun.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEFA1F96BF540083AD46 /* NewRun.swift */; }; + FEAE9A751FEC4F1500B52CA9 /* NewSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEFC1F96BF610083AD46 /* NewSection.swift */; }; + FEAE9A761FEC4F1500B52CA9 /* NewSuite.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEFE1F96BF730083AD46 /* NewSuite.swift */; }; + FEAE9A771FEC4F1500B52CA9 /* NewCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE4F8F381F84361F00447F9E /* NewCase.swift */; }; + FEAE9A781FEC4F1500B52CA9 /* NewConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEEE1F96BEE70083AD46 /* NewConfiguration.swift */; }; + FEAE9A791FEC4F1500B52CA9 /* NewConfigurationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEF01F96BEF10083AD46 /* NewConfigurationGroup.swift */; }; + FEAE9A7A1FEC4F1500B52CA9 /* NewMilestone.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEF21F96BF020083AD46 /* NewMilestone.swift */; }; + FEAE9A7B1FEC4F1500B52CA9 /* NewPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEF41F96BF130083AD46 /* NewPlan.swift */; }; + FEAE9A7C1FEC4F1500B52CA9 /* NewPlan.Entry.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE7CD3E31F9ABDC300C6108E /* NewPlan.Entry.swift */; }; + FEAE9A7D1FEC4F1500B52CA9 /* NewPlan.Entry.Run.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2D018A1FBCE70B00473B84 /* NewPlan.Entry.Run.swift */; }; + FEAE9A7E1FEC4F1500B52CA9 /* NewProject.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEF61F96BF290083AD46 /* NewProject.swift */; }; + FEAE9A7F1FEC4F1500B52CA9 /* NewResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEF81F96BF3A0083AD46 /* NewResult.swift */; }; + FEAE9A801FEC4F1500B52CA9 /* NewCaseResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DF01FBB6BA10065BE88 /* NewCaseResults.swift */; }; + FEAE9A811FEC4F1500B52CA9 /* NewCaseResults.Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DEC1FBB69C60065BE88 /* NewCaseResults.Result.swift */; }; + FEAE9A821FEC4F1500B52CA9 /* NewTestResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DEE1FBB6B920065BE88 /* NewTestResults.swift */; }; + FEAE9A831FEC4F1500B52CA9 /* NewTestResults.Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DEA1FBB69B80065BE88 /* NewTestResults.Result.swift */; }; + FEAE9A841FEC4F1500B52CA9 /* NewRun.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEFA1F96BF540083AD46 /* NewRun.swift */; }; + FEAE9A851FEC4F1500B52CA9 /* NewSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEFC1F96BF610083AD46 /* NewSection.swift */; }; + FEAE9A861FEC4F1500B52CA9 /* NewSuite.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEFE1F96BF730083AD46 /* NewSuite.swift */; }; + FEAE9A871FEC4F1800B52CA9 /* UpdatePlanEntryRuns.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6989201FA39B99006CC783 /* UpdatePlanEntryRuns.swift */; }; + FEAE9A881FEC4F1900B52CA9 /* UpdatePlanEntryRuns.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6989201FA39B99006CC783 /* UpdatePlanEntryRuns.swift */; }; + FEAE9A891FEC4F1A00B52CA9 /* UpdatePlanEntryRuns.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6989201FA39B99006CC783 /* UpdatePlanEntryRuns.swift */; }; + FEAE9A8A1FEC4F1D00B52CA9 /* GetConfigurationGroupsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE63715E1FD61CA500192CED /* GetConfigurationGroupsOperation.swift */; }; + FEAE9A8B1FEC4F1D00B52CA9 /* GetProjectOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECF666F1FDF05DB00015CC4 /* GetProjectOperation.swift */; }; + FEAE9A8C1FEC4F1D00B52CA9 /* GetTemplatesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3899161FCF2BDE0032E265 /* GetTemplatesOperation.swift */; }; + FEAE9A8D1FEC4F1D00B52CA9 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEA348771F5A141300C1E37A /* API.swift */; }; + FEAE9A8E1FEC4F1D00B52CA9 /* ObjectAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE894C251F5A25CA0057E021 /* ObjectAPI.swift */; }; + FEAE9A8F1FEC4F1E00B52CA9 /* GetConfigurationGroupsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE63715E1FD61CA500192CED /* GetConfigurationGroupsOperation.swift */; }; + FEAE9A901FEC4F1E00B52CA9 /* GetProjectOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECF666F1FDF05DB00015CC4 /* GetProjectOperation.swift */; }; + FEAE9A911FEC4F1E00B52CA9 /* GetTemplatesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3899161FCF2BDE0032E265 /* GetTemplatesOperation.swift */; }; + FEAE9A921FEC4F1E00B52CA9 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEA348771F5A141300C1E37A /* API.swift */; }; + FEAE9A931FEC4F1E00B52CA9 /* ObjectAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE894C251F5A25CA0057E021 /* ObjectAPI.swift */; }; + FEAE9A941FEC4F1E00B52CA9 /* GetConfigurationGroupsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE63715E1FD61CA500192CED /* GetConfigurationGroupsOperation.swift */; }; + FEAE9A951FEC4F1E00B52CA9 /* GetProjectOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECF666F1FDF05DB00015CC4 /* GetProjectOperation.swift */; }; + FEAE9A961FEC4F1E00B52CA9 /* GetTemplatesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3899161FCF2BDE0032E265 /* GetTemplatesOperation.swift */; }; + FEAE9A971FEC4F1E00B52CA9 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEA348771F5A141300C1E37A /* API.swift */; }; + FEAE9A981FEC4F1E00B52CA9 /* ObjectAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE894C251F5A25CA0057E021 /* ObjectAPI.swift */; }; + FEAE9A991FEC4F2200B52CA9 /* QuizTrain.h in Headers */ = {isa = PBXBuildFile; fileRef = FE23A91F1F59F3A0007E946D /* QuizTrain.h */; settings = {ATTRIBUTES = (Public, ); }; }; + FEAE9A9A1FEC4F2200B52CA9 /* QuizTrain.h in Headers */ = {isa = PBXBuildFile; fileRef = FE23A91F1F59F3A0007E946D /* QuizTrain.h */; settings = {ATTRIBUTES = (Public, ); }; }; + FEAE9A9B1FEC4FF100B52CA9 /* Array+Random.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECFE7FC1FD9E13500968EA3 /* Array+Random.swift */; }; + FEAE9A9C1FEC4FF100B52CA9 /* TestCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEBFEEB81FE2EAAB00E7FE1B /* TestCredentials.swift */; }; + FEAE9A9D1FEC4FF100B52CA9 /* TestCredentials.json in Resources */ = {isa = PBXBuildFile; fileRef = FEBFEEBA1FE2EAB900E7FE1B /* TestCredentials.json */; }; + FEAE9A9E1FEC4FF200B52CA9 /* Array+Random.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECFE7FC1FD9E13500968EA3 /* Array+Random.swift */; }; + FEAE9A9F1FEC4FF200B52CA9 /* TestCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEBFEEB81FE2EAAB00E7FE1B /* TestCredentials.swift */; }; + FEAE9AA01FEC4FF200B52CA9 /* TestCredentials.json in Resources */ = {isa = PBXBuildFile; fileRef = FEBFEEBA1FE2EAB900E7FE1B /* TestCredentials.json */; }; + FEAE9AA11FEC4FF500B52CA9 /* AssertAddRequestJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795E51FA7EA6C0030C395 /* AssertAddRequestJSON.swift */; }; + FEAE9AA21FEC4FF500B52CA9 /* AssertCustomFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE58F5491F7EBF12009A1B4E /* AssertCustomFields.swift */; }; + FEAE9AA31FEC4FF500B52CA9 /* AssertEquatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEE774361FA8EA980016AACE /* AssertEquatable.swift */; }; + FEAE9AA41FEC4FF500B52CA9 /* AssertJSONDeserializing.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE58F54B1F7EBF24009A1B4E /* AssertJSONDeserializing.swift */; }; + FEAE9AA51FEC4FF500B52CA9 /* AssertJSONSerializing.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEF172621F857F53004FFFFF /* AssertJSONSerializing.swift */; }; + FEAE9AA61FEC4FF500B52CA9 /* AssertJSONTwoWaySerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEF172641F858230004FFFFF /* AssertJSONTwoWaySerialization.swift */; }; + FEAE9AA71FEC4FF500B52CA9 /* AssertProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE58F5471F7EBF04009A1B4E /* AssertProperties.swift */; }; + FEAE9AA81FEC4FF500B52CA9 /* AssertUpdateRequestJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795A91FA7915C0030C395 /* AssertUpdateRequestJSON.swift */; }; + FEAE9AA91FEC4FF500B52CA9 /* AssertValidatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2877ED1FBB9803004503FB /* AssertValidatable.swift */; }; + FEAE9AAA1FEC4FF500B52CA9 /* AssertAddRequestJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795E51FA7EA6C0030C395 /* AssertAddRequestJSON.swift */; }; + FEAE9AAB1FEC4FF500B52CA9 /* AssertCustomFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE58F5491F7EBF12009A1B4E /* AssertCustomFields.swift */; }; + FEAE9AAC1FEC4FF500B52CA9 /* AssertEquatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEE774361FA8EA980016AACE /* AssertEquatable.swift */; }; + FEAE9AAD1FEC4FF500B52CA9 /* AssertJSONDeserializing.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE58F54B1F7EBF24009A1B4E /* AssertJSONDeserializing.swift */; }; + FEAE9AAE1FEC4FF500B52CA9 /* AssertJSONSerializing.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEF172621F857F53004FFFFF /* AssertJSONSerializing.swift */; }; + FEAE9AAF1FEC4FF500B52CA9 /* AssertJSONTwoWaySerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEF172641F858230004FFFFF /* AssertJSONTwoWaySerialization.swift */; }; + FEAE9AB01FEC4FF500B52CA9 /* AssertProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE58F5471F7EBF04009A1B4E /* AssertProperties.swift */; }; + FEAE9AB11FEC4FF500B52CA9 /* AssertUpdateRequestJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795A91FA7915C0030C395 /* AssertUpdateRequestJSON.swift */; }; + FEAE9AB21FEC4FF500B52CA9 /* AssertValidatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2877ED1FBB9803004503FB /* AssertValidatable.swift */; }; + FEAE9AB31FEC4FF800B52CA9 /* CustomFieldsDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE58F5651F7EEA29009A1B4E /* CustomFieldsDataProvider.swift */; }; + FEAE9AB41FEC4FF800B52CA9 /* JSONDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE58F5601F7EE91D009A1B4E /* JSONDataProvider.swift */; }; + FEAE9AB51FEC4FF800B52CA9 /* ObjectProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795CB1FA7A3AD0030C395 /* ObjectProvider.swift */; }; + FEAE9AB61FEC4FF800B52CA9 /* ValidatableObjectProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2877EF1FBB9A80004503FB /* ValidatableObjectProvider.swift */; }; + FEAE9AB71FEC4FF900B52CA9 /* CustomFieldsDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE58F5651F7EEA29009A1B4E /* CustomFieldsDataProvider.swift */; }; + FEAE9AB81FEC4FF900B52CA9 /* JSONDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE58F5601F7EE91D009A1B4E /* JSONDataProvider.swift */; }; + FEAE9AB91FEC4FF900B52CA9 /* ObjectProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795CB1FA7A3AD0030C395 /* ObjectProvider.swift */; }; + FEAE9ABA1FEC4FF900B52CA9 /* ValidatableObjectProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2877EF1FBB9A80004503FB /* ValidatableObjectProvider.swift */; }; + FEAE9ABB1FEC4FFE00B52CA9 /* AddRequestJSONTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795E71FA7EB380030C395 /* AddRequestJSONTests.swift */; }; + FEAE9ABC1FEC4FFE00B52CA9 /* EquatableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEE774341FA8E2480016AACE /* EquatableTests.swift */; }; + FEAE9ABD1FEC4FFE00B52CA9 /* InitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795D11FA7C7960030C395 /* InitTests.swift */; }; + FEAE9ABE1FEC4FFE00B52CA9 /* JSONDeserializingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795D31FA7C7A50030C395 /* JSONDeserializingTests.swift */; }; + FEAE9ABF1FEC4FFE00B52CA9 /* JSONSerializingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795D51FA7C7B60030C395 /* JSONSerializingTests.swift */; }; + FEAE9AC01FEC4FFE00B52CA9 /* JSONTwoWaySerializationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795D71FA7C7D00030C395 /* JSONTwoWaySerializationTests.swift */; }; + FEAE9AC11FEC4FFE00B52CA9 /* UpdateRequestJSONTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795DB1FA7C7FA0030C395 /* UpdateRequestJSONTests.swift */; }; + FEAE9AC21FEC4FFE00B52CA9 /* ValidatableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2877EB1FBB979C004503FB /* ValidatableTests.swift */; }; + FEAE9AC31FEC4FFE00B52CA9 /* VariablePropertyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795D91FA7C7E30030C395 /* VariablePropertyTests.swift */; }; + FEAE9AC41FEC4FFF00B52CA9 /* AddRequestJSONTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795E71FA7EB380030C395 /* AddRequestJSONTests.swift */; }; + FEAE9AC51FEC4FFF00B52CA9 /* EquatableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEE774341FA8E2480016AACE /* EquatableTests.swift */; }; + FEAE9AC61FEC4FFF00B52CA9 /* InitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795D11FA7C7960030C395 /* InitTests.swift */; }; + FEAE9AC71FEC4FFF00B52CA9 /* JSONDeserializingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795D31FA7C7A50030C395 /* JSONDeserializingTests.swift */; }; + FEAE9AC81FEC4FFF00B52CA9 /* JSONSerializingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795D51FA7C7B60030C395 /* JSONSerializingTests.swift */; }; + FEAE9AC91FEC4FFF00B52CA9 /* JSONTwoWaySerializationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795D71FA7C7D00030C395 /* JSONTwoWaySerializationTests.swift */; }; + FEAE9ACA1FEC4FFF00B52CA9 /* UpdateRequestJSONTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795DB1FA7C7FA0030C395 /* UpdateRequestJSONTests.swift */; }; + FEAE9ACB1FEC4FFF00B52CA9 /* ValidatableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2877EB1FBB979C004503FB /* ValidatableTests.swift */; }; + FEAE9ACC1FEC4FFF00B52CA9 /* VariablePropertyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795D91FA7C7E30030C395 /* VariablePropertyTests.swift */; }; + FEAE9ACD1FEC500200B52CA9 /* CustomFieldsContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE22458B1F75BECC009F2B2B /* CustomFieldsContainerTests.swift */; }; + FEAE9ACE1FEC500200B52CA9 /* ErrorContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC2140A1FD7533000036B17 /* ErrorContainerTests.swift */; }; + FEAE9ACF1FEC500200B52CA9 /* JSONDictionaryContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE51EFA11F882454007012E0 /* JSONDictionaryContainerTests.swift */; }; + FEAE9AD01FEC500200B52CA9 /* CustomFieldsContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE22458B1F75BECC009F2B2B /* CustomFieldsContainerTests.swift */; }; + FEAE9AD11FEC500200B52CA9 /* ErrorContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC2140A1FD7533000036B17 /* ErrorContainerTests.swift */; }; + FEAE9AD21FEC500200B52CA9 /* JSONDictionaryContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE51EFA11F882454007012E0 /* JSONDictionaryContainerTests.swift */; }; + FEAE9AD31FEC500600B52CA9 /* Array+ContentComparisonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE549C451FBE60F3008CDFCE /* Array+ContentComparisonTests.swift */; }; + FEAE9AD41FEC500600B52CA9 /* Array+RandomTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECFE7FE1FD9E1AE00968EA3 /* Array+RandomTests.swift */; }; + FEAE9AD51FEC500600B52CA9 /* Equatable+OptionalArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474D51FAA475A0049DA84 /* Equatable+OptionalArrayTests.swift */; }; + FEAE9AD61FEC500700B52CA9 /* Array+ContentComparisonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE549C451FBE60F3008CDFCE /* Array+ContentComparisonTests.swift */; }; + FEAE9AD71FEC500700B52CA9 /* Array+RandomTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECFE7FE1FD9E1AE00968EA3 /* Array+RandomTests.swift */; }; + FEAE9AD81FEC500700B52CA9 /* Equatable+OptionalArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1474D51FAA475A0049DA84 /* Equatable+OptionalArrayTests.swift */; }; + FEAE9AD91FEC500900B52CA9 /* AsyncOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC2140D1FD7537800036B17 /* AsyncOperationTests.swift */; }; + FEAE9ADA1FEC500A00B52CA9 /* AsyncOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC2140D1FD7537800036B17 /* AsyncOperationTests.swift */; }; + FEAE9ADB1FEC500D00B52CA9 /* ModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE018DC61F85708C001A2FEF /* ModelTests.swift */; }; + FEAE9ADC1FEC500D00B52CA9 /* ModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE018DC61F85708C001A2FEF /* ModelTests.swift */; }; + FEAE9ADD1FEC501000B52CA9 /* CustomFieldTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D481F75914300DF1039 /* CustomFieldTypeTests.swift */; }; + FEAE9ADE1FEC501000B52CA9 /* Project.SuiteModeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE791A241F7D9FFA00D7E870 /* Project.SuiteModeTests.swift */; }; + FEAE9ADF1FEC501000B52CA9 /* UniqueSelectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECF66751FDF593600015CC4 /* UniqueSelectionTests.swift */; }; + FEAE9AE01FEC501100B52CA9 /* CustomFieldTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D481F75914300DF1039 /* CustomFieldTypeTests.swift */; }; + FEAE9AE11FEC501100B52CA9 /* Project.SuiteModeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE791A241F7D9FFA00D7E870 /* Project.SuiteModeTests.swift */; }; + FEAE9AE21FEC501100B52CA9 /* UniqueSelectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECF66751FDF593600015CC4 /* UniqueSelectionTests.swift */; }; + FEAE9AE31FEC501400B52CA9 /* ConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D461F75913900DF1039 /* ConfigTests.swift */; }; + FEAE9AE41FEC501400B52CA9 /* Config.ContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEAE7F951F7C5D0C00906FE1 /* Config.ContextTests.swift */; }; + FEAE9AE51FEC501400B52CA9 /* CaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D261F75908100DF1039 /* CaseTests.swift */; }; + FEAE9AE61FEC501400B52CA9 /* CaseFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D441F75913000DF1039 /* CaseFieldTests.swift */; }; + FEAE9AE71FEC501400B52CA9 /* CaseTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D281F75909600DF1039 /* CaseTypeTests.swift */; }; + FEAE9AE81FEC501400B52CA9 /* ConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D2A1F7590A100DF1039 /* ConfigurationTests.swift */; }; + FEAE9AE91FEC501400B52CA9 /* ConfigurationGroupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D2C1F7590B000DF1039 /* ConfigurationGroupTests.swift */; }; + FEAE9AEA1FEC501400B52CA9 /* MilestoneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D2E1F7590BA00DF1039 /* MilestoneTests.swift */; }; + FEAE9AEB1FEC501400B52CA9 /* PlanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D301F7590C700DF1039 /* PlanTests.swift */; }; + FEAE9AEC1FEC501400B52CA9 /* Plan.EntryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE30A6521F7C261400CE7D6B /* Plan.EntryTests.swift */; }; + FEAE9AED1FEC501400B52CA9 /* PriorityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D321F7590D200DF1039 /* PriorityTests.swift */; }; + FEAE9AEE1FEC501400B52CA9 /* ProjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D341F7590DC00DF1039 /* ProjectTests.swift */; }; + FEAE9AEF1FEC501400B52CA9 /* ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D361F7590E500DF1039 /* ResultTests.swift */; }; + FEAE9AF01FEC501400B52CA9 /* ResultFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D4A1F75914E00DF1039 /* ResultFieldTests.swift */; }; + FEAE9AF11FEC501400B52CA9 /* RunTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D381F7590F000DF1039 /* RunTests.swift */; }; + FEAE9AF21FEC501400B52CA9 /* SectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D3A1F7590F900DF1039 /* SectionTests.swift */; }; + FEAE9AF31FEC501400B52CA9 /* StatusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED295481FA1117F00746DAB /* StatusTests.swift */; }; + FEAE9AF41FEC501400B52CA9 /* SuiteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D3C1F75910500DF1039 /* SuiteTests.swift */; }; + FEAE9AF51FEC501400B52CA9 /* TemplateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D3E1F75910E00DF1039 /* TemplateTests.swift */; }; + FEAE9AF61FEC501400B52CA9 /* TestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D401F75911A00DF1039 /* TestTests.swift */; }; + FEAE9AF71FEC501400B52CA9 /* UserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D421F75912400DF1039 /* UserTests.swift */; }; + FEAE9AF81FEC501400B52CA9 /* ConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D461F75913900DF1039 /* ConfigTests.swift */; }; + FEAE9AF91FEC501400B52CA9 /* Config.ContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEAE7F951F7C5D0C00906FE1 /* Config.ContextTests.swift */; }; + FEAE9AFA1FEC501400B52CA9 /* CaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D261F75908100DF1039 /* CaseTests.swift */; }; + FEAE9AFB1FEC501400B52CA9 /* CaseFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D441F75913000DF1039 /* CaseFieldTests.swift */; }; + FEAE9AFC1FEC501400B52CA9 /* CaseTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D281F75909600DF1039 /* CaseTypeTests.swift */; }; + FEAE9AFD1FEC501400B52CA9 /* ConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D2A1F7590A100DF1039 /* ConfigurationTests.swift */; }; + FEAE9AFE1FEC501400B52CA9 /* ConfigurationGroupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D2C1F7590B000DF1039 /* ConfigurationGroupTests.swift */; }; + FEAE9AFF1FEC501400B52CA9 /* MilestoneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D2E1F7590BA00DF1039 /* MilestoneTests.swift */; }; + FEAE9B001FEC501400B52CA9 /* PlanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D301F7590C700DF1039 /* PlanTests.swift */; }; + FEAE9B011FEC501400B52CA9 /* Plan.EntryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE30A6521F7C261400CE7D6B /* Plan.EntryTests.swift */; }; + FEAE9B021FEC501400B52CA9 /* PriorityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D321F7590D200DF1039 /* PriorityTests.swift */; }; + FEAE9B031FEC501400B52CA9 /* ProjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D341F7590DC00DF1039 /* ProjectTests.swift */; }; + FEAE9B041FEC501400B52CA9 /* ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D361F7590E500DF1039 /* ResultTests.swift */; }; + FEAE9B051FEC501400B52CA9 /* ResultFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D4A1F75914E00DF1039 /* ResultFieldTests.swift */; }; + FEAE9B061FEC501400B52CA9 /* RunTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D381F7590F000DF1039 /* RunTests.swift */; }; + FEAE9B071FEC501400B52CA9 /* SectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D3A1F7590F900DF1039 /* SectionTests.swift */; }; + FEAE9B081FEC501400B52CA9 /* StatusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED295481FA1117F00746DAB /* StatusTests.swift */; }; + FEAE9B091FEC501400B52CA9 /* SuiteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D3C1F75910500DF1039 /* SuiteTests.swift */; }; + FEAE9B0A1FEC501400B52CA9 /* TemplateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D3E1F75910E00DF1039 /* TemplateTests.swift */; }; + FEAE9B0B1FEC501400B52CA9 /* TestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D401F75911A00DF1039 /* TestTests.swift */; }; + FEAE9B0C1FEC501400B52CA9 /* UserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6A6D421F75912400DF1039 /* UserTests.swift */; }; + FEAE9B0D1FEC501700B52CA9 /* FilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2877F91FBBC47B004503FB /* FilterTests.swift */; }; + FEAE9B0E1FEC501800B52CA9 /* FilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2877F91FBBC47B004503FB /* FilterTests.swift */; }; + FEAE9B0F1FEC501B00B52CA9 /* AddModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795CD1FA7A4530030C395 /* AddModelTests.swift */; }; + FEAE9B101FEC501B00B52CA9 /* UpdateModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795E01FA7D8360030C395 /* UpdateModelTests.swift */; }; + FEAE9B111FEC501C00B52CA9 /* AddModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795CD1FA7A4530030C395 /* AddModelTests.swift */; }; + FEAE9B121FEC501C00B52CA9 /* UpdateModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795E01FA7D8360030C395 /* UpdateModelTests.swift */; }; + FEAE9B131FEC501F00B52CA9 /* NewCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795AF1FA799150030C395 /* NewCaseTests.swift */; }; + FEAE9B141FEC501F00B52CA9 /* NewConfigurationGroupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795B31FA799330030C395 /* NewConfigurationGroupTests.swift */; }; + FEAE9B151FEC501F00B52CA9 /* NewConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795B11FA799270030C395 /* NewConfigurationTests.swift */; }; + FEAE9B161FEC501F00B52CA9 /* NewMilestoneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795B51FA799400030C395 /* NewMilestoneTests.swift */; }; + FEAE9B171FEC501F00B52CA9 /* NewPlanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795B71FA799520030C395 /* NewPlanTests.swift */; }; + FEAE9B181FEC501F00B52CA9 /* NewPlan.EntryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795B91FA7996F0030C395 /* NewPlan.EntryTests.swift */; }; + FEAE9B191FEC501F00B52CA9 /* NewPlan.Entry.RunTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE64D1571FBD0C3700ABA133 /* NewPlan.Entry.RunTests.swift */; }; + FEAE9B1A1FEC501F00B52CA9 /* NewProjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795BB1FA7998F0030C395 /* NewProjectTests.swift */; }; + FEAE9B1B1FEC501F00B52CA9 /* NewResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795BD1FA7999C0030C395 /* NewResultTests.swift */; }; + FEAE9B1C1FEC501F00B52CA9 /* NewCaseResultsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DF21FBB714B0065BE88 /* NewCaseResultsTests.swift */; }; + FEAE9B1D1FEC501F00B52CA9 /* NewCaseResults.ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DF41FBB716B0065BE88 /* NewCaseResults.ResultTests.swift */; }; + FEAE9B1E1FEC501F00B52CA9 /* NewTestResultsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795BF1FA799A90030C395 /* NewTestResultsTests.swift */; }; + FEAE9B1F1FEC501F00B52CA9 /* NewTestResults.ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795C11FA799B70030C395 /* NewTestResults.ResultTests.swift */; }; + FEAE9B201FEC501F00B52CA9 /* NewRunTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795C31FA799D10030C395 /* NewRunTests.swift */; }; + FEAE9B211FEC501F00B52CA9 /* NewSectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795C51FA799E30030C395 /* NewSectionTests.swift */; }; + FEAE9B221FEC501F00B52CA9 /* NewSuiteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795C71FA799F50030C395 /* NewSuiteTests.swift */; }; + FEAE9B231FEC502000B52CA9 /* NewCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795AF1FA799150030C395 /* NewCaseTests.swift */; }; + FEAE9B241FEC502000B52CA9 /* NewConfigurationGroupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795B31FA799330030C395 /* NewConfigurationGroupTests.swift */; }; + FEAE9B251FEC502000B52CA9 /* NewConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795B11FA799270030C395 /* NewConfigurationTests.swift */; }; + FEAE9B261FEC502000B52CA9 /* NewMilestoneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795B51FA799400030C395 /* NewMilestoneTests.swift */; }; + FEAE9B271FEC502000B52CA9 /* NewPlanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795B71FA799520030C395 /* NewPlanTests.swift */; }; + FEAE9B281FEC502000B52CA9 /* NewPlan.EntryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795B91FA7996F0030C395 /* NewPlan.EntryTests.swift */; }; + FEAE9B291FEC502000B52CA9 /* NewPlan.Entry.RunTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE64D1571FBD0C3700ABA133 /* NewPlan.Entry.RunTests.swift */; }; + FEAE9B2A1FEC502000B52CA9 /* NewProjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795BB1FA7998F0030C395 /* NewProjectTests.swift */; }; + FEAE9B2B1FEC502000B52CA9 /* NewResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795BD1FA7999C0030C395 /* NewResultTests.swift */; }; + FEAE9B2C1FEC502000B52CA9 /* NewCaseResultsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DF21FBB714B0065BE88 /* NewCaseResultsTests.swift */; }; + FEAE9B2D1FEC502000B52CA9 /* NewCaseResults.ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE208DF41FBB716B0065BE88 /* NewCaseResults.ResultTests.swift */; }; + FEAE9B2E1FEC502000B52CA9 /* NewTestResultsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795BF1FA799A90030C395 /* NewTestResultsTests.swift */; }; + FEAE9B2F1FEC502000B52CA9 /* NewTestResults.ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795C11FA799B70030C395 /* NewTestResults.ResultTests.swift */; }; + FEAE9B301FEC502000B52CA9 /* NewRunTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795C31FA799D10030C395 /* NewRunTests.swift */; }; + FEAE9B311FEC502000B52CA9 /* NewSectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795C51FA799E30030C395 /* NewSectionTests.swift */; }; + FEAE9B321FEC502000B52CA9 /* NewSuiteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795C71FA799F50030C395 /* NewSuiteTests.swift */; }; + FEAE9B331FEC502300B52CA9 /* UpdatePlanEntryRunsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795C91FA79A070030C395 /* UpdatePlanEntryRunsTests.swift */; }; + FEAE9B351FEC502300B52CA9 /* UpdatePlanEntryRunsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3795C91FA79A070030C395 /* UpdatePlanEntryRunsTests.swift */; }; + FEAF0BFE1F7D9F15001D4F10 /* Project.SuiteMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEAF0BFD1F7D9F15001D4F10 /* Project.SuiteMode.swift */; }; + FEB546841F9135CB00AA6DA5 /* AddRequestJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEB546831F9135CB00AA6DA5 /* AddRequestJSON.swift */; }; + FEB546861F9135D500AA6DA5 /* AddRequestJSONKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEB546851F9135D500AA6DA5 /* AddRequestJSONKeys.swift */; }; + FEB546881F9170A900AA6DA5 /* Outcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEB546871F9170A900AA6DA5 /* Outcome.swift */; }; + FEBDA3C11FD1C7F400124430 /* ErrorContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEBDA3C01FD1C7F400124430 /* ErrorContainer.swift */; }; + FEBDA3C21FD1C88300124430 /* ObjectAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE894C251F5A25CA0057E021 /* ObjectAPI.swift */; }; + FEBF50371FCF3E91005B86B7 /* AsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEBF50361FCF3E91005B86B7 /* AsyncOperation.swift */; }; + FEBFEEB91FE2EAAB00E7FE1B /* TestCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEBFEEB81FE2EAAB00E7FE1B /* TestCredentials.swift */; }; + FEBFEEBB1FE2EAB900E7FE1B /* TestCredentials.json in Resources */ = {isa = PBXBuildFile; fileRef = FEBFEEBA1FE2EAB900E7FE1B /* TestCredentials.json */; }; + FEC214001FD7302900036B17 /* ObjectAPI.MatchErrorDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC213FF1FD7302900036B17 /* ObjectAPI.MatchErrorDebug.swift */; }; + FEC214071FD741F700036B17 /* DebugDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC214061FD741F700036B17 /* DebugDescription.swift */; }; + FEC214091FD7420300036B17 /* DebugDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC214081FD7420300036B17 /* DebugDetails.swift */; }; + FEC2140B1FD7533000036B17 /* ErrorContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC2140A1FD7533000036B17 /* ErrorContainerTests.swift */; }; + FEC2140E1FD7537800036B17 /* AsyncOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC2140D1FD7537800036B17 /* AsyncOperationTests.swift */; }; + FECF66701FDF05DB00015CC4 /* GetProjectOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECF666F1FDF05DB00015CC4 /* GetProjectOperation.swift */; }; + FECF66761FDF593600015CC4 /* UniqueSelectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECF66751FDF593600015CC4 /* UniqueSelectionTests.swift */; }; + FECFE7FF1FD9E1AE00968EA3 /* Array+RandomTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECFE7FE1FD9E1AE00968EA3 /* Array+RandomTests.swift */; }; + FED295471FA10E9300746DAB /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED295461FA10E9300746DAB /* Status.swift */; }; + FED295491FA1117F00746DAB /* StatusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED295481FA1117F00746DAB /* StatusTests.swift */; }; + FEDCBEEF1F96BEE70083AD46 /* NewConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEEE1F96BEE70083AD46 /* NewConfiguration.swift */; }; + FEDCBEF11F96BEF10083AD46 /* NewConfigurationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEF01F96BEF10083AD46 /* NewConfigurationGroup.swift */; }; + FEDCBEF31F96BF020083AD46 /* NewMilestone.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEF21F96BF020083AD46 /* NewMilestone.swift */; }; + FEDCBEF51F96BF130083AD46 /* NewPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEF41F96BF130083AD46 /* NewPlan.swift */; }; + FEDCBEF71F96BF290083AD46 /* NewProject.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEF61F96BF290083AD46 /* NewProject.swift */; }; + FEDCBEF91F96BF3A0083AD46 /* NewResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEF81F96BF3A0083AD46 /* NewResult.swift */; }; + FEDCBEFB1F96BF540083AD46 /* NewRun.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEFA1F96BF540083AD46 /* NewRun.swift */; }; + FEDCBEFD1F96BF610083AD46 /* NewSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEFC1F96BF610083AD46 /* NewSection.swift */; }; + FEDCBEFF1F96BF730083AD46 /* NewSuite.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBEFE1F96BF730083AD46 /* NewSuite.swift */; }; + FEDD80781F69CA3C00D56EF9 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9641F59F489007E946D /* Result.swift */; }; + FEDD80791F69D7A700D56EF9 /* Project.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9621F59F478007E946D /* Project.swift */; }; + FEDD807A1F69DE1A00D56EF9 /* Priority.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE23A9601F59F46C007E946D /* Priority.swift */; }; + FEDFF2751FE1B09000AEB3D6 /* SingleMatchError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDFF2741FE1B09000AEB3D6 /* SingleMatchError.swift */; }; + FEDFF2771FE1B0A000AEB3D6 /* MultipleMatchError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDFF2761FE1B0A000AEB3D6 /* MultipleMatchError.swift */; }; + FEE774351FA8E2480016AACE /* EquatableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEE774341FA8E2480016AACE /* EquatableTests.swift */; }; + FEE774371FA8EA980016AACE /* AssertEquatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEE774361FA8EA980016AACE /* AssertEquatable.swift */; }; + FEEC443B1FB4D1BC0042BD5A /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEC443A1FB4D1BC0042BD5A /* Filter.swift */; }; + FEEC443E1FB4D23F0042BD5A /* Filter.Value.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEC443D1FB4D23F0042BD5A /* Filter.Value.swift */; }; + FEEC44401FB4D3270042BD5A /* QueryItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEC443F1FB4D3270042BD5A /* QueryItemProvider.swift */; }; + FEF172631F857F53004FFFFF /* AssertJSONSerializing.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEF172621F857F53004FFFFF /* AssertJSONSerializing.swift */; }; + FEF172661F858880004FFFFF /* AssertJSONTwoWaySerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEF172641F858230004FFFFF /* AssertJSONTwoWaySerialization.swift */; }; + FEF9CE7E1F7C538F0078CD4E /* Plan.EntryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE30A6521F7C261400CE7D6B /* Plan.EntryTests.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + FE23A9271F59F3A0007E946D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = FE23A9131F59F39F007E946D /* Project object */; + proxyType = 1; + remoteGlobalIDString = FE23A91B1F59F39F007E946D; + remoteInfo = QuizTrain; + }; + FEAE99691FEC3BCE00B52CA9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = FE23A9131F59F39F007E946D /* Project object */; + proxyType = 1; + remoteGlobalIDString = FEAE995E1FEC3BCD00B52CA9; + remoteInfo = "QuizTrain-tvOS"; + }; + FEAE99851FEC3BEB00B52CA9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = FE23A9131F59F39F007E946D /* Project object */; + proxyType = 1; + remoteGlobalIDString = FEAE997A1FEC3BEB00B52CA9; + remoteInfo = "QuizTrain-macOS"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + FE018DC61F85708C001A2FEF /* ModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelTests.swift; sourceTree = ""; }; + FE09FFB31FE320CA0009FB2F /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .swiftlint.yml; sourceTree = ""; }; + FE09FFCF1FE325CE0009FB2F /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; + FE11181B1F6C3A4B00D24A5F /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; + FE1474D51FAA475A0049DA84 /* Equatable+OptionalArrayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Equatable+OptionalArrayTests.swift"; sourceTree = ""; }; + FE1474D91FAA698F0049DA84 /* ObjectAPI.RequestErrorDebug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectAPI.RequestErrorDebug.swift; sourceTree = ""; }; + FE1474DB1FAA69AA0049DA84 /* ObjectAPI.DataRequestErrorDebug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectAPI.DataRequestErrorDebug.swift; sourceTree = ""; }; + FE1474DD1FAA69B70049DA84 /* ObjectAPI.UpdateRequestErrorDebug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectAPI.UpdateRequestErrorDebug.swift; sourceTree = ""; }; + FE1474E11FAA69F70049DA84 /* ObjectAPI.ClientErrorDebug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectAPI.ClientErrorDebug.swift; sourceTree = ""; }; + FE1474E41FAA6A0D0049DA84 /* ObjectAPI.ServerErrorDebug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectAPI.ServerErrorDebug.swift; sourceTree = ""; }; + FE1474E71FAA6A270049DA84 /* ObjectAPI.StatusCodeErrorDebug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectAPI.StatusCodeErrorDebug.swift; sourceTree = ""; }; + FE1474E91FAA6A340049DA84 /* ObjectAPI.DataProcessingErrorDebug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectAPI.DataProcessingErrorDebug.swift; sourceTree = ""; }; + FE1474EB1FAA6A5B0049DA84 /* ObjectAPI.ObjectConversionErrorDebug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectAPI.ObjectConversionErrorDebug.swift; sourceTree = ""; }; + FE1F301B1FEC0C890046F99E /* Entities.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Entities.png; sourceTree = ""; }; + FE1FB1031F9131B200383724 /* UpdateRequestJSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateRequestJSON.swift; sourceTree = ""; }; + FE208DEA1FBB69B80065BE88 /* NewTestResults.Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTestResults.Result.swift; sourceTree = ""; }; + FE208DEC1FBB69C60065BE88 /* NewCaseResults.Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewCaseResults.Result.swift; sourceTree = ""; }; + FE208DEE1FBB6B920065BE88 /* NewTestResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTestResults.swift; sourceTree = ""; }; + FE208DF01FBB6BA10065BE88 /* NewCaseResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewCaseResults.swift; sourceTree = ""; }; + FE208DF21FBB714B0065BE88 /* NewCaseResultsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewCaseResultsTests.swift; sourceTree = ""; }; + FE208DF41FBB716B0065BE88 /* NewCaseResults.ResultTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewCaseResults.ResultTests.swift; sourceTree = ""; }; + FE208DF91FBB95150065BE88 /* Validatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Validatable.swift; sourceTree = ""; }; + FE20D03E1FD876820057A45C /* Identifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Identifiable.swift; sourceTree = ""; }; + FE2245841F75AC82009F2B2B /* CustomFieldsContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomFieldsContainer.swift; sourceTree = ""; }; + FE22458B1F75BECC009F2B2B /* CustomFieldsContainerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomFieldsContainerTests.swift; sourceTree = ""; }; + FE23A91C1F59F3A0007E946D /* QuizTrain.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = QuizTrain.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FE23A91F1F59F3A0007E946D /* QuizTrain.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QuizTrain.h; sourceTree = ""; }; + FE23A9251F59F3A0007E946D /* QuizTrainTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = QuizTrainTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + FE23A92C1F59F3A0007E946D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + FE23A9541F59F415007E946D /* Case.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Case.swift; sourceTree = ""; }; + FE23A9561F59F42B007E946D /* CaseField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaseField.swift; sourceTree = ""; }; + FE23A9581F59F437007E946D /* CaseType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaseType.swift; sourceTree = ""; }; + FE23A95A1F59F443007E946D /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; + FE23A95C1F59F450007E946D /* Milestone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Milestone.swift; sourceTree = ""; }; + FE23A95E1F59F45C007E946D /* Plan.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Plan.swift; sourceTree = ""; }; + FE23A9601F59F46C007E946D /* Priority.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Priority.swift; sourceTree = ""; }; + FE23A9621F59F478007E946D /* Project.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Project.swift; sourceTree = ""; }; + FE23A9641F59F489007E946D /* Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = ""; }; + FE23A9661F59F4B3007E946D /* ResultField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultField.swift; sourceTree = ""; }; + FE23A9681F59F4BA007E946D /* Run.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Run.swift; sourceTree = ""; }; + FE23A96A1F59F4C3007E946D /* Section.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = ""; }; + FE23A96C1F59F4CE007E946D /* Suite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Suite.swift; sourceTree = ""; }; + FE23A96E1F59F4D9007E946D /* Template.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Template.swift; sourceTree = ""; }; + FE23A9701F59F4E2007E946D /* Test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test.swift; sourceTree = ""; }; + FE23A9721F59F4EB007E946D /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + FE23A9741F5A042D007E946D /* ConfigurationGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationGroup.swift; sourceTree = ""; }; + FE2877EB1FBB979C004503FB /* ValidatableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidatableTests.swift; sourceTree = ""; }; + FE2877ED1FBB9803004503FB /* AssertValidatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssertValidatable.swift; sourceTree = ""; }; + FE2877EF1FBB9A80004503FB /* ValidatableObjectProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidatableObjectProvider.swift; sourceTree = ""; }; + FE2877F91FBBC47B004503FB /* FilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterTests.swift; sourceTree = ""; }; + FE2D018A1FBCE70B00473B84 /* NewPlan.Entry.Run.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPlan.Entry.Run.swift; sourceTree = ""; }; + FE2E774A1F85923F00EF5E54 /* Equatable+OptionalArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Equatable+OptionalArray.swift"; sourceTree = ""; }; + FE2F1AD61F844FCD00FF9E0C /* JSONSerializable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONSerializable.swift; sourceTree = ""; }; + FE30A6521F7C261400CE7D6B /* Plan.EntryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Plan.EntryTests.swift; sourceTree = ""; }; + FE331C1B1F6B406300F9A653 /* CustomFieldType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomFieldType.swift; sourceTree = ""; }; + FE3795A91FA7915C0030C395 /* AssertUpdateRequestJSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssertUpdateRequestJSON.swift; sourceTree = ""; }; + FE3795AF1FA799150030C395 /* NewCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewCaseTests.swift; sourceTree = ""; }; + FE3795B11FA799270030C395 /* NewConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewConfigurationTests.swift; sourceTree = ""; }; + FE3795B31FA799330030C395 /* NewConfigurationGroupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewConfigurationGroupTests.swift; sourceTree = ""; }; + FE3795B51FA799400030C395 /* NewMilestoneTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewMilestoneTests.swift; sourceTree = ""; }; + FE3795B71FA799520030C395 /* NewPlanTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPlanTests.swift; sourceTree = ""; }; + FE3795B91FA7996F0030C395 /* NewPlan.EntryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPlan.EntryTests.swift; sourceTree = ""; }; + FE3795BB1FA7998F0030C395 /* NewProjectTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewProjectTests.swift; sourceTree = ""; }; + FE3795BD1FA7999C0030C395 /* NewResultTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewResultTests.swift; sourceTree = ""; }; + FE3795BF1FA799A90030C395 /* NewTestResultsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTestResultsTests.swift; sourceTree = ""; }; + FE3795C11FA799B70030C395 /* NewTestResults.ResultTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTestResults.ResultTests.swift; sourceTree = ""; }; + FE3795C31FA799D10030C395 /* NewRunTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewRunTests.swift; sourceTree = ""; }; + FE3795C51FA799E30030C395 /* NewSectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewSectionTests.swift; sourceTree = ""; }; + FE3795C71FA799F50030C395 /* NewSuiteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewSuiteTests.swift; sourceTree = ""; }; + FE3795C91FA79A070030C395 /* UpdatePlanEntryRunsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePlanEntryRunsTests.swift; sourceTree = ""; }; + FE3795CB1FA7A3AD0030C395 /* ObjectProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectProvider.swift; sourceTree = ""; }; + FE3795CD1FA7A4530030C395 /* AddModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddModelTests.swift; sourceTree = ""; }; + FE3795D11FA7C7960030C395 /* InitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitTests.swift; sourceTree = ""; }; + FE3795D31FA7C7A50030C395 /* JSONDeserializingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONDeserializingTests.swift; sourceTree = ""; }; + FE3795D51FA7C7B60030C395 /* JSONSerializingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONSerializingTests.swift; sourceTree = ""; }; + FE3795D71FA7C7D00030C395 /* JSONTwoWaySerializationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONTwoWaySerializationTests.swift; sourceTree = ""; }; + FE3795D91FA7C7E30030C395 /* VariablePropertyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VariablePropertyTests.swift; sourceTree = ""; }; + FE3795DB1FA7C7FA0030C395 /* UpdateRequestJSONTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateRequestJSONTests.swift; sourceTree = ""; }; + FE3795DD1FA7C90F0030C395 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + FE3795E01FA7D8360030C395 /* UpdateModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateModelTests.swift; sourceTree = ""; }; + FE3795E51FA7EA6C0030C395 /* AssertAddRequestJSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssertAddRequestJSON.swift; sourceTree = ""; }; + FE3795E71FA7EB380030C395 /* AddRequestJSONTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddRequestJSONTests.swift; sourceTree = ""; }; + FE3899161FCF2BDE0032E265 /* GetTemplatesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTemplatesOperation.swift; sourceTree = ""; }; + FE4E11D61F7C4112004A315E /* Plan.Entry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Plan.Entry.swift; sourceTree = ""; }; + FE4F8F381F84361F00447F9E /* NewCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewCase.swift; sourceTree = ""; }; + FE4F8F3A1F8437DE00447F9E /* JSONDeserializable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONDeserializable.swift; sourceTree = ""; }; + FE51EFA11F882454007012E0 /* JSONDictionaryContainerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONDictionaryContainerTests.swift; sourceTree = ""; }; + FE5259111F82D1AD00E0DDB7 /* JSONKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONKey.swift; sourceTree = ""; }; + FE549C451FBE60F3008CDFCE /* Array+ContentComparisonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+ContentComparisonTests.swift"; sourceTree = ""; }; + FE5869D61F7478D600BE5C5C /* CustomFields.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomFields.swift; sourceTree = ""; }; + FE58F5471F7EBF04009A1B4E /* AssertProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssertProperties.swift; sourceTree = ""; }; + FE58F5491F7EBF12009A1B4E /* AssertCustomFields.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssertCustomFields.swift; sourceTree = ""; }; + FE58F54B1F7EBF24009A1B4E /* AssertJSONDeserializing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssertJSONDeserializing.swift; sourceTree = ""; }; + FE58F5601F7EE91D009A1B4E /* JSONDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONDataProvider.swift; sourceTree = ""; }; + FE58F5651F7EEA29009A1B4E /* CustomFieldsDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomFieldsDataProvider.swift; sourceTree = ""; }; + FE5A24F11FE092D300198848 /* UniqueSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniqueSelection.swift; sourceTree = ""; }; + FE5E1A2F1F8804E3001E479B /* JSONDictionaryContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONDictionaryContainer.swift; sourceTree = ""; }; + FE63715E1FD61CA500192CED /* GetConfigurationGroupsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetConfigurationGroupsOperation.swift; sourceTree = ""; }; + FE64D1571FBD0C3700ABA133 /* NewPlan.Entry.RunTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPlan.Entry.RunTests.swift; sourceTree = ""; }; + FE6838E11F8FE10500431C1C /* UpdateRequestJSONKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateRequestJSONKeys.swift; sourceTree = ""; }; + FE6989201FA39B99006CC783 /* UpdatePlanEntryRuns.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePlanEntryRuns.swift; sourceTree = ""; }; + FE6A6D261F75908100DF1039 /* CaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaseTests.swift; sourceTree = ""; }; + FE6A6D281F75909600DF1039 /* CaseTypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaseTypeTests.swift; sourceTree = ""; }; + FE6A6D2A1F7590A100DF1039 /* ConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationTests.swift; sourceTree = ""; }; + FE6A6D2C1F7590B000DF1039 /* ConfigurationGroupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationGroupTests.swift; sourceTree = ""; }; + FE6A6D2E1F7590BA00DF1039 /* MilestoneTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MilestoneTests.swift; sourceTree = ""; }; + FE6A6D301F7590C700DF1039 /* PlanTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlanTests.swift; sourceTree = ""; }; + FE6A6D321F7590D200DF1039 /* PriorityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriorityTests.swift; sourceTree = ""; }; + FE6A6D341F7590DC00DF1039 /* ProjectTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectTests.swift; sourceTree = ""; }; + FE6A6D361F7590E500DF1039 /* ResultTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultTests.swift; sourceTree = ""; }; + FE6A6D381F7590F000DF1039 /* RunTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunTests.swift; sourceTree = ""; }; + FE6A6D3A1F7590F900DF1039 /* SectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionTests.swift; sourceTree = ""; }; + FE6A6D3C1F75910500DF1039 /* SuiteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuiteTests.swift; sourceTree = ""; }; + FE6A6D3E1F75910E00DF1039 /* TemplateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateTests.swift; sourceTree = ""; }; + FE6A6D401F75911A00DF1039 /* TestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTests.swift; sourceTree = ""; }; + FE6A6D421F75912400DF1039 /* UserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTests.swift; sourceTree = ""; }; + FE6A6D441F75913000DF1039 /* CaseFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaseFieldTests.swift; sourceTree = ""; }; + FE6A6D461F75913900DF1039 /* ConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigTests.swift; sourceTree = ""; }; + FE6A6D481F75914300DF1039 /* CustomFieldTypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomFieldTypeTests.swift; sourceTree = ""; }; + FE6A6D4A1F75914E00DF1039 /* ResultFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultFieldTests.swift; sourceTree = ""; }; + FE6A6D4F1F75956C00DF1039 /* ObjectAPITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectAPITests.swift; sourceTree = ""; }; + FE6C8AE11F8BEA2E00F45642 /* MutableCustomFields.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutableCustomFields.swift; sourceTree = ""; }; + FE791A241F7D9FFA00D7E870 /* Project.SuiteModeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Project.SuiteModeTests.swift; sourceTree = ""; }; + FE7B53921FBF5BB1003C26BD /* Array+ContentComparison.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+ContentComparison.swift"; sourceTree = ""; }; + FE7CD3E31F9ABDC300C6108E /* NewPlan.Entry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPlan.Entry.swift; sourceTree = ""; }; + FE813A171F73126F00265569 /* JSONDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONDictionary.swift; sourceTree = ""; }; + FE8464621FE307B1006CB58F /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + FE8464641FE307BB006CB58F /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + FE8464671FE319EA006CB58F /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + FE8464841FE31D4A006CB58F /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .swiftlint.yml; sourceTree = ""; }; + FE8464851FE31D58006CB58F /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .swiftlint.yml; sourceTree = ""; }; + FE894C251F5A25CA0057E021 /* ObjectAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectAPI.swift; sourceTree = ""; }; + FE8C2D591FBA391F005A4150 /* Date+Seconds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Seconds.swift"; sourceTree = ""; }; + FE978CE81F9528320005D181 /* API.RequestResultDebug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.RequestResultDebug.swift; sourceTree = ""; }; + FE978CEA1F9528400005D181 /* API.RequestErrorDebug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.RequestErrorDebug.swift; sourceTree = ""; }; + FE978CEC1F9532BA0005D181 /* URLRequestDebug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRequestDebug.swift; sourceTree = ""; }; + FEA348771F5A141300C1E37A /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; + FEAE7F911F7C5ABE00906FE1 /* Config.Context.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.Context.swift; sourceTree = ""; }; + FEAE7F951F7C5D0C00906FE1 /* Config.ContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.ContextTests.swift; sourceTree = ""; }; + FEAE99511FEC38BA00B52CA9 /* QuizTrain.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = QuizTrain.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FEAE995F1FEC3BCD00B52CA9 /* QuizTrain.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = QuizTrain.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FEAE99671FEC3BCE00B52CA9 /* QuizTrainTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = QuizTrainTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + FEAE997B1FEC3BEB00B52CA9 /* QuizTrain.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = QuizTrain.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FEAE99831FEC3BEB00B52CA9 /* QuizTrainTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = QuizTrainTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + FEAF0BFD1F7D9F15001D4F10 /* Project.SuiteMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Project.SuiteMode.swift; sourceTree = ""; }; + FEB546831F9135CB00AA6DA5 /* AddRequestJSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddRequestJSON.swift; sourceTree = ""; }; + FEB546851F9135D500AA6DA5 /* AddRequestJSONKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddRequestJSONKeys.swift; sourceTree = ""; }; + FEB546871F9170A900AA6DA5 /* Outcome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Outcome.swift; sourceTree = ""; }; + FEB67180200FF7EA006283A6 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; + FEBDA3C01FD1C7F400124430 /* ErrorContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorContainer.swift; sourceTree = ""; }; + FEBF50361FCF3E91005B86B7 /* AsyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncOperation.swift; sourceTree = ""; }; + FEBFEEB81FE2EAAB00E7FE1B /* TestCredentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCredentials.swift; sourceTree = ""; }; + FEBFEEBA1FE2EAB900E7FE1B /* TestCredentials.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = TestCredentials.json; sourceTree = ""; }; + FEC213FF1FD7302900036B17 /* ObjectAPI.MatchErrorDebug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectAPI.MatchErrorDebug.swift; sourceTree = ""; }; + FEC214061FD741F700036B17 /* DebugDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugDescription.swift; sourceTree = ""; }; + FEC214081FD7420300036B17 /* DebugDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugDetails.swift; sourceTree = ""; }; + FEC2140A1FD7533000036B17 /* ErrorContainerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorContainerTests.swift; sourceTree = ""; }; + FEC2140D1FD7537800036B17 /* AsyncOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncOperationTests.swift; sourceTree = ""; }; + FECF666F1FDF05DB00015CC4 /* GetProjectOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetProjectOperation.swift; sourceTree = ""; }; + FECF66751FDF593600015CC4 /* UniqueSelectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniqueSelectionTests.swift; sourceTree = ""; }; + FECFE7FC1FD9E13500968EA3 /* Array+Random.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Random.swift"; sourceTree = ""; }; + FECFE7FE1FD9E1AE00968EA3 /* Array+RandomTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+RandomTests.swift"; sourceTree = ""; }; + FED295461FA10E9300746DAB /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; }; + FED295481FA1117F00746DAB /* StatusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTests.swift; sourceTree = ""; }; + FEDCBEEE1F96BEE70083AD46 /* NewConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewConfiguration.swift; sourceTree = ""; }; + FEDCBEF01F96BEF10083AD46 /* NewConfigurationGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewConfigurationGroup.swift; sourceTree = ""; }; + FEDCBEF21F96BF020083AD46 /* NewMilestone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewMilestone.swift; sourceTree = ""; }; + FEDCBEF41F96BF130083AD46 /* NewPlan.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPlan.swift; sourceTree = ""; }; + FEDCBEF61F96BF290083AD46 /* NewProject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewProject.swift; sourceTree = ""; }; + FEDCBEF81F96BF3A0083AD46 /* NewResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewResult.swift; sourceTree = ""; }; + FEDCBEFA1F96BF540083AD46 /* NewRun.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewRun.swift; sourceTree = ""; }; + FEDCBEFC1F96BF610083AD46 /* NewSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewSection.swift; sourceTree = ""; }; + FEDCBEFE1F96BF730083AD46 /* NewSuite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewSuite.swift; sourceTree = ""; }; + FEDFF2741FE1B09000AEB3D6 /* SingleMatchError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleMatchError.swift; sourceTree = ""; }; + FEDFF2761FE1B0A000AEB3D6 /* MultipleMatchError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipleMatchError.swift; sourceTree = ""; }; + FEE774341FA8E2480016AACE /* EquatableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EquatableTests.swift; sourceTree = ""; }; + FEE774361FA8EA980016AACE /* AssertEquatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssertEquatable.swift; sourceTree = ""; }; + FEEC443A1FB4D1BC0042BD5A /* Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Filter.swift; path = QuizTrain/Network/Filters/Filter.swift; sourceTree = SOURCE_ROOT; }; + FEEC443D1FB4D23F0042BD5A /* Filter.Value.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filter.Value.swift; sourceTree = ""; }; + FEEC443F1FB4D3270042BD5A /* QueryItemProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryItemProvider.swift; sourceTree = ""; }; + FEF172621F857F53004FFFFF /* AssertJSONSerializing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssertJSONSerializing.swift; sourceTree = ""; }; + FEF172641F858230004FFFFF /* AssertJSONTwoWaySerialization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssertJSONTwoWaySerialization.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + FE23A9181F59F39F007E946D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FE23A9221F59F3A0007E946D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FE23A9261F59F3A0007E946D /* QuizTrain.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FEAE994D1FEC38BA00B52CA9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FEAE995B1FEC3BCD00B52CA9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FEAE99641FEC3BCE00B52CA9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FEAE99681FEC3BCE00B52CA9 /* QuizTrain.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FEAE99771FEC3BEB00B52CA9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FEAE99801FEC3BEB00B52CA9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FEAE99841FEC3BEB00B52CA9 /* QuizTrain.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + FE018DC91F85719A001A2FEF /* Tests */ = { + isa = PBXGroup; + children = ( + FE3795DD1FA7C90F0030C395 /* README.md */, + FE3795E71FA7EB380030C395 /* AddRequestJSONTests.swift */, + FEE774341FA8E2480016AACE /* EquatableTests.swift */, + FE3795D11FA7C7960030C395 /* InitTests.swift */, + FE3795D31FA7C7A50030C395 /* JSONDeserializingTests.swift */, + FE3795D51FA7C7B60030C395 /* JSONSerializingTests.swift */, + FE3795D71FA7C7D00030C395 /* JSONTwoWaySerializationTests.swift */, + FE3795DB1FA7C7FA0030C395 /* UpdateRequestJSONTests.swift */, + FE2877EB1FBB979C004503FB /* ValidatableTests.swift */, + FE3795D91FA7C7E30030C395 /* VariablePropertyTests.swift */, + ); + path = Tests; + sourceTree = ""; + }; + FE028C251F7037C1002582B3 /* Config */ = { + isa = PBXGroup; + children = ( + FE11181B1F6C3A4B00D24A5F /* Config.swift */, + FEAE7F911F7C5ABE00906FE1 /* Config.Context.swift */, + ); + path = Config; + sourceTree = ""; + }; + FE1474D71FAA484D0049DA84 /* Extensions */ = { + isa = PBXGroup; + children = ( + FE7B53921FBF5BB1003C26BD /* Array+ContentComparison.swift */, + FE8C2D591FBA391F005A4150 /* Date+Seconds.swift */, + FE2E774A1F85923F00EF5E54 /* Equatable+OptionalArray.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + FE1474D81FAA48900049DA84 /* Extensions */ = { + isa = PBXGroup; + children = ( + FE549C451FBE60F3008CDFCE /* Array+ContentComparisonTests.swift */, + FECFE7FE1FD9E1AE00968EA3 /* Array+RandomTests.swift */, + FE1474D51FAA475A0049DA84 /* Equatable+OptionalArrayTests.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + FE1474DF1FAA69D60049DA84 /* API */ = { + isa = PBXGroup; + children = ( + FE978CEA1F9528400005D181 /* API.RequestErrorDebug.swift */, + FE978CE81F9528320005D181 /* API.RequestResultDebug.swift */, + ); + path = API; + sourceTree = ""; + }; + FE1474E01FAA69DD0049DA84 /* ObjectAPI */ = { + isa = PBXGroup; + children = ( + FE1474E11FAA69F70049DA84 /* ObjectAPI.ClientErrorDebug.swift */, + FE1474E91FAA6A340049DA84 /* ObjectAPI.DataProcessingErrorDebug.swift */, + FE1474DB1FAA69AA0049DA84 /* ObjectAPI.DataRequestErrorDebug.swift */, + FEC213FF1FD7302900036B17 /* ObjectAPI.MatchErrorDebug.swift */, + FE1474EB1FAA6A5B0049DA84 /* ObjectAPI.ObjectConversionErrorDebug.swift */, + FE1474D91FAA698F0049DA84 /* ObjectAPI.RequestErrorDebug.swift */, + FE1474E41FAA6A0D0049DA84 /* ObjectAPI.ServerErrorDebug.swift */, + FE1474E71FAA6A270049DA84 /* ObjectAPI.StatusCodeErrorDebug.swift */, + FE1474DD1FAA69B70049DA84 /* ObjectAPI.UpdateRequestErrorDebug.swift */, + ); + path = ObjectAPI; + sourceTree = ""; + }; + FE20D03D1FD876710057A45C /* Identity */ = { + isa = PBXGroup; + children = ( + FE20D03E1FD876820057A45C /* Identifiable.swift */, + ); + path = Identity; + sourceTree = ""; + }; + FE2245861F75B66F009F2B2B /* Misc */ = { + isa = PBXGroup; + children = ( + FEB546821F91359100AA6DA5 /* Add */, + FE5E1A2C1F88049B001E479B /* Containment */, + FEC214051FD741ED00036B17 /* Debug */, + FEDFF2731FE1B07700AEB3D6 /* Errors */, + FE1474D71FAA484D0049DA84 /* Extensions */, + FE20D03D1FD876710057A45C /* Identity */, + FE5869D81F7485D100BE5C5C /* JSON */, + FEBF50351FCF3E85005B86B7 /* Operations */, + FEB546811F91358800AA6DA5 /* Update */, + FE6371601FD7108600192CED /* Validation */, + FEB546871F9170A900AA6DA5 /* Outcome.swift */, + FEEC443F1FB4D3270042BD5A /* QueryItemProvider.swift */, + FE5A24F11FE092D300198848 /* UniqueSelection.swift */, + ); + path = Misc; + sourceTree = ""; + }; + FE2245891F75BEB2009F2B2B /* Misc */ = { + isa = PBXGroup; + children = ( + FE22458A1F75BEB8009F2B2B /* Containment */, + FE1474D81FAA48900049DA84 /* Extensions */, + FEC2140C1FD7536700036B17 /* Operations */, + ); + path = Misc; + sourceTree = ""; + }; + FE22458A1F75BEB8009F2B2B /* Containment */ = { + isa = PBXGroup; + children = ( + FE6989271FA3BBC2006CC783 /* Containers */, + ); + path = Containment; + sourceTree = ""; + }; + FE23A9121F59F39F007E946D = { + isa = PBXGroup; + children = ( + FE23A91E1F59F3A0007E946D /* QuizTrain */, + FE23A9291F59F3A0007E946D /* QuizTrainTests */, + FE23A91D1F59F3A0007E946D /* Products */, + FE1F301B1FEC0C890046F99E /* Entities.png */, + FEB67180200FF7EA006283A6 /* LICENSE */, + FE8464621FE307B1006CB58F /* README.md */, + FE09FFCF1FE325CE0009FB2F /* .gitignore */, + FE09FFB31FE320CA0009FB2F /* .swiftlint.yml */, + ); + sourceTree = ""; + }; + FE23A91D1F59F3A0007E946D /* Products */ = { + isa = PBXGroup; + children = ( + FE23A91C1F59F3A0007E946D /* QuizTrain.framework */, + FE23A9251F59F3A0007E946D /* QuizTrainTests.xctest */, + FEAE99511FEC38BA00B52CA9 /* QuizTrain.framework */, + FEAE995F1FEC3BCD00B52CA9 /* QuizTrain.framework */, + FEAE99671FEC3BCE00B52CA9 /* QuizTrainTests.xctest */, + FEAE997B1FEC3BEB00B52CA9 /* QuizTrain.framework */, + FEAE99831FEC3BEB00B52CA9 /* QuizTrainTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + FE23A91E1F59F3A0007E946D /* QuizTrain */ = { + isa = PBXGroup; + children = ( + FE2245861F75B66F009F2B2B /* Misc */, + FE23A9361F59F3AF007E946D /* Models */, + FEA348591F5A13F700C1E37A /* Network */, + FE23A91F1F59F3A0007E946D /* QuizTrain.h */, + FE8464671FE319EA006CB58F /* Info.plist */, + FE8464841FE31D4A006CB58F /* .swiftlint.yml */, + ); + path = QuizTrain; + sourceTree = ""; + }; + FE23A9291F59F3A0007E946D /* QuizTrainTests */ = { + isa = PBXGroup; + children = ( + FE5A24F51FE099EB00198848 /* Testing Misc */, + FE58F5461F7EBEF8009A1B4E /* Testing Protocols */, + FE2245891F75BEB2009F2B2B /* Misc */, + FE6A6D241F75904B00DF1039 /* Models */, + FE6A6D4C1F75954D00DF1039 /* Network */, + FE23A92C1F59F3A0007E946D /* Info.plist */, + FE8464641FE307BB006CB58F /* README.md */, + FE8464851FE31D58006CB58F /* .swiftlint.yml */, + ); + path = QuizTrainTests; + sourceTree = ""; + }; + FE23A9361F59F3AF007E946D /* Models */ = { + isa = PBXGroup; + children = ( + FE52590D1F82A95100E0DDB7 /* Types */, + FE028C251F7037C1002582B3 /* Config */, + FE23A9541F59F415007E946D /* Case.swift */, + FE23A9561F59F42B007E946D /* CaseField.swift */, + FE23A9581F59F437007E946D /* CaseType.swift */, + FE23A95A1F59F443007E946D /* Configuration.swift */, + FE23A9741F5A042D007E946D /* ConfigurationGroup.swift */, + FE23A95C1F59F450007E946D /* Milestone.swift */, + FE23A95E1F59F45C007E946D /* Plan.swift */, + FE4E11D61F7C4112004A315E /* Plan.Entry.swift */, + FE23A9601F59F46C007E946D /* Priority.swift */, + FE23A9621F59F478007E946D /* Project.swift */, + FE23A9641F59F489007E946D /* Result.swift */, + FE23A9661F59F4B3007E946D /* ResultField.swift */, + FE23A9681F59F4BA007E946D /* Run.swift */, + FE23A96A1F59F4C3007E946D /* Section.swift */, + FED295461FA10E9300746DAB /* Status.swift */, + FE23A96C1F59F4CE007E946D /* Suite.swift */, + FE23A96E1F59F4D9007E946D /* Template.swift */, + FE23A9701F59F4E2007E946D /* Test.swift */, + FE23A9721F59F4EB007E946D /* User.swift */, + ); + path = Models; + sourceTree = ""; + }; + FE2877F81FBBC469004503FB /* Filters */ = { + isa = PBXGroup; + children = ( + FE2877F91FBBC47B004503FB /* FilterTests.swift */, + ); + path = Filters; + sourceTree = ""; + }; + FE3795E21FA7DB900030C395 /* Testing Protocols */ = { + isa = PBXGroup; + children = ( + FE018DC61F85708C001A2FEF /* ModelTests.swift */, + ); + path = "Testing Protocols"; + sourceTree = ""; + }; + FE3795E31FA7DB9E0030C395 /* Testing Protocols */ = { + isa = PBXGroup; + children = ( + FE3795CD1FA7A4530030C395 /* AddModelTests.swift */, + FE3795E01FA7D8360030C395 /* UpdateModelTests.swift */, + ); + path = "Testing Protocols"; + sourceTree = ""; + }; + FE47DC771FCE27B50048BACE /* Operations */ = { + isa = PBXGroup; + children = ( + FE63715E1FD61CA500192CED /* GetConfigurationGroupsOperation.swift */, + FECF666F1FDF05DB00015CC4 /* GetProjectOperation.swift */, + FE3899161FCF2BDE0032E265 /* GetTemplatesOperation.swift */, + ); + path = Operations; + sourceTree = ""; + }; + FE52590D1F82A95100E0DDB7 /* Types */ = { + isa = PBXGroup; + children = ( + FE331C1B1F6B406300F9A653 /* CustomFieldType.swift */, + FEAF0BFD1F7D9F15001D4F10 /* Project.SuiteMode.swift */, + ); + path = Types; + sourceTree = ""; + }; + FE52590E1F82A98200E0DDB7 /* Types */ = { + isa = PBXGroup; + children = ( + FE6A6D481F75914300DF1039 /* CustomFieldTypeTests.swift */, + FE791A241F7D9FFA00D7E870 /* Project.SuiteModeTests.swift */, + FECF66751FDF593600015CC4 /* UniqueSelectionTests.swift */, + ); + path = Types; + sourceTree = ""; + }; + FE5869D81F7485D100BE5C5C /* JSON */ = { + isa = PBXGroup; + children = ( + FE5259111F82D1AD00E0DDB7 /* JSONKey.swift */, + FE813A171F73126F00265569 /* JSONDictionary.swift */, + FE4F8F3A1F8437DE00447F9E /* JSONDeserializable.swift */, + FE2F1AD61F844FCD00FF9E0C /* JSONSerializable.swift */, + ); + path = JSON; + sourceTree = ""; + }; + FE58F5461F7EBEF8009A1B4E /* Testing Protocols */ = { + isa = PBXGroup; + children = ( + FE58F5621F7EE923009A1B4E /* Asserts */, + FE58F5641F7EE92B009A1B4E /* Providers */, + FE018DC91F85719A001A2FEF /* Tests */, + ); + path = "Testing Protocols"; + sourceTree = ""; + }; + FE58F5621F7EE923009A1B4E /* Asserts */ = { + isa = PBXGroup; + children = ( + FE3795E51FA7EA6C0030C395 /* AssertAddRequestJSON.swift */, + FE58F5491F7EBF12009A1B4E /* AssertCustomFields.swift */, + FEE774361FA8EA980016AACE /* AssertEquatable.swift */, + FE58F54B1F7EBF24009A1B4E /* AssertJSONDeserializing.swift */, + FEF172621F857F53004FFFFF /* AssertJSONSerializing.swift */, + FEF172641F858230004FFFFF /* AssertJSONTwoWaySerialization.swift */, + FE58F5471F7EBF04009A1B4E /* AssertProperties.swift */, + FE3795A91FA7915C0030C395 /* AssertUpdateRequestJSON.swift */, + FE2877ED1FBB9803004503FB /* AssertValidatable.swift */, + ); + path = Asserts; + sourceTree = ""; + }; + FE58F5641F7EE92B009A1B4E /* Providers */ = { + isa = PBXGroup; + children = ( + FE58F5651F7EEA29009A1B4E /* CustomFieldsDataProvider.swift */, + FE58F5601F7EE91D009A1B4E /* JSONDataProvider.swift */, + FE3795CB1FA7A3AD0030C395 /* ObjectProvider.swift */, + FE2877EF1FBB9A80004503FB /* ValidatableObjectProvider.swift */, + ); + path = Providers; + sourceTree = ""; + }; + FE5A24F51FE099EB00198848 /* Testing Misc */ = { + isa = PBXGroup; + children = ( + FECFE7FC1FD9E13500968EA3 /* Array+Random.swift */, + FEBFEEB81FE2EAAB00E7FE1B /* TestCredentials.swift */, + FEBFEEBA1FE2EAB900E7FE1B /* TestCredentials.json */, + ); + path = "Testing Misc"; + sourceTree = ""; + }; + FE5E1A2C1F88049B001E479B /* Containment */ = { + isa = PBXGroup; + children = ( + FE5E1A371F882003001E479B /* Protocols */, + FEDCBEED1F96BDC90083AD46 /* Containers */, + ); + path = Containment; + sourceTree = ""; + }; + FE5E1A371F882003001E479B /* Protocols */ = { + isa = PBXGroup; + children = ( + FE5869D61F7478D600BE5C5C /* CustomFields.swift */, + FE6C8AE11F8BEA2E00F45642 /* MutableCustomFields.swift */, + ); + path = Protocols; + sourceTree = ""; + }; + FE6371601FD7108600192CED /* Validation */ = { + isa = PBXGroup; + children = ( + FE208DF91FBB95150065BE88 /* Validatable.swift */, + ); + path = Validation; + sourceTree = ""; + }; + FE6989221FA3B12B006CC783 /* Add */ = { + isa = PBXGroup; + children = ( + FE4F8F381F84361F00447F9E /* NewCase.swift */, + FEDCBEEE1F96BEE70083AD46 /* NewConfiguration.swift */, + FEDCBEF01F96BEF10083AD46 /* NewConfigurationGroup.swift */, + FEDCBEF21F96BF020083AD46 /* NewMilestone.swift */, + FEDCBEF41F96BF130083AD46 /* NewPlan.swift */, + FE7CD3E31F9ABDC300C6108E /* NewPlan.Entry.swift */, + FE2D018A1FBCE70B00473B84 /* NewPlan.Entry.Run.swift */, + FEDCBEF61F96BF290083AD46 /* NewProject.swift */, + FEDCBEF81F96BF3A0083AD46 /* NewResult.swift */, + FE208DF01FBB6BA10065BE88 /* NewCaseResults.swift */, + FE208DEC1FBB69C60065BE88 /* NewCaseResults.Result.swift */, + FE208DEE1FBB6B920065BE88 /* NewTestResults.swift */, + FE208DEA1FBB69B80065BE88 /* NewTestResults.Result.swift */, + FEDCBEFA1F96BF540083AD46 /* NewRun.swift */, + FEDCBEFC1F96BF610083AD46 /* NewSection.swift */, + FEDCBEFE1F96BF730083AD46 /* NewSuite.swift */, + ); + path = Add; + sourceTree = ""; + }; + FE6989231FA3B130006CC783 /* Update */ = { + isa = PBXGroup; + children = ( + FE6989201FA39B99006CC783 /* UpdatePlanEntryRuns.swift */, + ); + path = Update; + sourceTree = ""; + }; + FE6989241FA3BB83006CC783 /* Models */ = { + isa = PBXGroup; + children = ( + FE3795E31FA7DB9E0030C395 /* Testing Protocols */, + FE6989251FA3BB90006CC783 /* Add */, + FE6989261FA3BB94006CC783 /* Update */, + ); + path = Models; + sourceTree = ""; + }; + FE6989251FA3BB90006CC783 /* Add */ = { + isa = PBXGroup; + children = ( + FE3795AF1FA799150030C395 /* NewCaseTests.swift */, + FE3795B31FA799330030C395 /* NewConfigurationGroupTests.swift */, + FE3795B11FA799270030C395 /* NewConfigurationTests.swift */, + FE3795B51FA799400030C395 /* NewMilestoneTests.swift */, + FE3795B71FA799520030C395 /* NewPlanTests.swift */, + FE3795B91FA7996F0030C395 /* NewPlan.EntryTests.swift */, + FE64D1571FBD0C3700ABA133 /* NewPlan.Entry.RunTests.swift */, + FE3795BB1FA7998F0030C395 /* NewProjectTests.swift */, + FE3795BD1FA7999C0030C395 /* NewResultTests.swift */, + FE208DF21FBB714B0065BE88 /* NewCaseResultsTests.swift */, + FE208DF41FBB716B0065BE88 /* NewCaseResults.ResultTests.swift */, + FE3795BF1FA799A90030C395 /* NewTestResultsTests.swift */, + FE3795C11FA799B70030C395 /* NewTestResults.ResultTests.swift */, + FE3795C31FA799D10030C395 /* NewRunTests.swift */, + FE3795C51FA799E30030C395 /* NewSectionTests.swift */, + FE3795C71FA799F50030C395 /* NewSuiteTests.swift */, + ); + path = Add; + sourceTree = ""; + }; + FE6989261FA3BB94006CC783 /* Update */ = { + isa = PBXGroup; + children = ( + FE3795C91FA79A070030C395 /* UpdatePlanEntryRunsTests.swift */, + ); + path = Update; + sourceTree = ""; + }; + FE6989271FA3BBC2006CC783 /* Containers */ = { + isa = PBXGroup; + children = ( + FE22458B1F75BECC009F2B2B /* CustomFieldsContainerTests.swift */, + FEC2140A1FD7533000036B17 /* ErrorContainerTests.swift */, + FE51EFA11F882454007012E0 /* JSONDictionaryContainerTests.swift */, + ); + path = Containers; + sourceTree = ""; + }; + FE6A6D241F75904B00DF1039 /* Models */ = { + isa = PBXGroup; + children = ( + FE3795E21FA7DB900030C395 /* Testing Protocols */, + FE52590E1F82A98200E0DDB7 /* Types */, + FE6A6D251F75905800DF1039 /* Custom Fields */, + FE6A6D261F75908100DF1039 /* CaseTests.swift */, + FE6A6D441F75913000DF1039 /* CaseFieldTests.swift */, + FE6A6D281F75909600DF1039 /* CaseTypeTests.swift */, + FE6A6D2A1F7590A100DF1039 /* ConfigurationTests.swift */, + FE6A6D2C1F7590B000DF1039 /* ConfigurationGroupTests.swift */, + FE6A6D2E1F7590BA00DF1039 /* MilestoneTests.swift */, + FE6A6D301F7590C700DF1039 /* PlanTests.swift */, + FE30A6521F7C261400CE7D6B /* Plan.EntryTests.swift */, + FE6A6D321F7590D200DF1039 /* PriorityTests.swift */, + FE6A6D341F7590DC00DF1039 /* ProjectTests.swift */, + FE6A6D361F7590E500DF1039 /* ResultTests.swift */, + FE6A6D4A1F75914E00DF1039 /* ResultFieldTests.swift */, + FE6A6D381F7590F000DF1039 /* RunTests.swift */, + FE6A6D3A1F7590F900DF1039 /* SectionTests.swift */, + FED295481FA1117F00746DAB /* StatusTests.swift */, + FE6A6D3C1F75910500DF1039 /* SuiteTests.swift */, + FE6A6D3E1F75910E00DF1039 /* TemplateTests.swift */, + FE6A6D401F75911A00DF1039 /* TestTests.swift */, + FE6A6D421F75912400DF1039 /* UserTests.swift */, + ); + path = Models; + sourceTree = ""; + }; + FE6A6D251F75905800DF1039 /* Custom Fields */ = { + isa = PBXGroup; + children = ( + FE6A6D461F75913900DF1039 /* ConfigTests.swift */, + FEAE7F951F7C5D0C00906FE1 /* Config.ContextTests.swift */, + ); + path = "Custom Fields"; + sourceTree = ""; + }; + FE6A6D4C1F75954D00DF1039 /* Network */ = { + isa = PBXGroup; + children = ( + FE2877F81FBBC469004503FB /* Filters */, + FE6989241FA3BB83006CC783 /* Models */, + FE6A6D4F1F75956C00DF1039 /* ObjectAPITests.swift */, + ); + path = Network; + sourceTree = ""; + }; + FE978CE71F95281E0005D181 /* Extensions */ = { + isa = PBXGroup; + children = ( + FE1474DF1FAA69D60049DA84 /* API */, + FE1474E01FAA69DD0049DA84 /* ObjectAPI */, + FE978CEC1F9532BA0005D181 /* URLRequestDebug.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + FE978CF11F9552120005D181 /* Models */ = { + isa = PBXGroup; + children = ( + FE6989221FA3B12B006CC783 /* Add */, + FE6989231FA3B130006CC783 /* Update */, + ); + path = Models; + sourceTree = ""; + }; + FEA348591F5A13F700C1E37A /* Network */ = { + isa = PBXGroup; + children = ( + FE978CE71F95281E0005D181 /* Extensions */, + FEEC443C1FB4D22B0042BD5A /* Filters */, + FE978CF11F9552120005D181 /* Models */, + FE47DC771FCE27B50048BACE /* Operations */, + FEA348771F5A141300C1E37A /* API.swift */, + FE894C251F5A25CA0057E021 /* ObjectAPI.swift */, + ); + path = Network; + sourceTree = ""; + }; + FEB546811F91358800AA6DA5 /* Update */ = { + isa = PBXGroup; + children = ( + FE1FB1031F9131B200383724 /* UpdateRequestJSON.swift */, + FE6838E11F8FE10500431C1C /* UpdateRequestJSONKeys.swift */, + ); + path = Update; + sourceTree = ""; + }; + FEB546821F91359100AA6DA5 /* Add */ = { + isa = PBXGroup; + children = ( + FEB546831F9135CB00AA6DA5 /* AddRequestJSON.swift */, + FEB546851F9135D500AA6DA5 /* AddRequestJSONKeys.swift */, + ); + path = Add; + sourceTree = ""; + }; + FEBF50351FCF3E85005B86B7 /* Operations */ = { + isa = PBXGroup; + children = ( + FEBF50361FCF3E91005B86B7 /* AsyncOperation.swift */, + ); + path = Operations; + sourceTree = ""; + }; + FEC214051FD741ED00036B17 /* Debug */ = { + isa = PBXGroup; + children = ( + FEC214061FD741F700036B17 /* DebugDescription.swift */, + FEC214081FD7420300036B17 /* DebugDetails.swift */, + ); + path = Debug; + sourceTree = ""; + }; + FEC2140C1FD7536700036B17 /* Operations */ = { + isa = PBXGroup; + children = ( + FEC2140D1FD7537800036B17 /* AsyncOperationTests.swift */, + ); + path = Operations; + sourceTree = ""; + }; + FEDCBEED1F96BDC90083AD46 /* Containers */ = { + isa = PBXGroup; + children = ( + FE2245841F75AC82009F2B2B /* CustomFieldsContainer.swift */, + FEBDA3C01FD1C7F400124430 /* ErrorContainer.swift */, + FE5E1A2F1F8804E3001E479B /* JSONDictionaryContainer.swift */, + ); + path = Containers; + sourceTree = ""; + }; + FEDFF2731FE1B07700AEB3D6 /* Errors */ = { + isa = PBXGroup; + children = ( + FEDFF2741FE1B09000AEB3D6 /* SingleMatchError.swift */, + FEDFF2761FE1B0A000AEB3D6 /* MultipleMatchError.swift */, + ); + path = Errors; + sourceTree = ""; + }; + FEEC443C1FB4D22B0042BD5A /* Filters */ = { + isa = PBXGroup; + children = ( + FEEC443A1FB4D1BC0042BD5A /* Filter.swift */, + FEEC443D1FB4D23F0042BD5A /* Filter.Value.swift */, + ); + path = Filters; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + FE23A9191F59F39F007E946D /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + FE23A92D1F59F3A0007E946D /* QuizTrain.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FEAE994E1FEC38BA00B52CA9 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + FEAE99591FEC38CE00B52CA9 /* QuizTrain.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FEAE995C1FEC3BCD00B52CA9 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + FEAE9A991FEC4F2200B52CA9 /* QuizTrain.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FEAE99781FEC3BEB00B52CA9 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + FEAE9A9A1FEC4F2200B52CA9 /* QuizTrain.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + FE23A91B1F59F39F007E946D /* QuizTrain-iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = FE23A9301F59F3A0007E946D /* Build configuration list for PBXNativeTarget "QuizTrain-iOS" */; + buildPhases = ( + FE23A9171F59F39F007E946D /* Sources */, + FE23A9181F59F39F007E946D /* Frameworks */, + FE23A9191F59F39F007E946D /* Headers */, + FE23A91A1F59F39F007E946D /* Resources */, + FE8464661FE31868006CB58F /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "QuizTrain-iOS"; + productName = QuizTrain; + productReference = FE23A91C1F59F3A0007E946D /* QuizTrain.framework */; + productType = "com.apple.product-type.framework"; + }; + FE23A9241F59F3A0007E946D /* QuizTrainTests-iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = FE23A9331F59F3A0007E946D /* Build configuration list for PBXNativeTarget "QuizTrainTests-iOS" */; + buildPhases = ( + FE23A9211F59F3A0007E946D /* Sources */, + FE23A9221F59F3A0007E946D /* Frameworks */, + FE23A9231F59F3A0007E946D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + FE23A9281F59F3A0007E946D /* PBXTargetDependency */, + ); + name = "QuizTrainTests-iOS"; + productName = QuizTrainTests; + productReference = FE23A9251F59F3A0007E946D /* QuizTrainTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + FEAE99501FEC38BA00B52CA9 /* QuizTrain-watchOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = FEAE99561FEC38BA00B52CA9 /* Build configuration list for PBXNativeTarget "QuizTrain-watchOS" */; + buildPhases = ( + FEAE994C1FEC38BA00B52CA9 /* Sources */, + FEAE994D1FEC38BA00B52CA9 /* Frameworks */, + FEAE994E1FEC38BA00B52CA9 /* Headers */, + FEAE994F1FEC38BA00B52CA9 /* Resources */, + FEDAE2CE1FED580100828842 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "QuizTrain-watchOS"; + productName = "QuizTrain-watchOS"; + productReference = FEAE99511FEC38BA00B52CA9 /* QuizTrain.framework */; + productType = "com.apple.product-type.framework"; + }; + FEAE995E1FEC3BCD00B52CA9 /* QuizTrain-tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = FEAE99701FEC3BCE00B52CA9 /* Build configuration list for PBXNativeTarget "QuizTrain-tvOS" */; + buildPhases = ( + FEAE995A1FEC3BCD00B52CA9 /* Sources */, + FEAE995B1FEC3BCD00B52CA9 /* Frameworks */, + FEAE995C1FEC3BCD00B52CA9 /* Headers */, + FEAE995D1FEC3BCD00B52CA9 /* Resources */, + FEDAE2CD1FED57FA00828842 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "QuizTrain-tvOS"; + productName = "QuizTrain-tvOS"; + productReference = FEAE995F1FEC3BCD00B52CA9 /* QuizTrain.framework */; + productType = "com.apple.product-type.framework"; + }; + FEAE99661FEC3BCE00B52CA9 /* QuizTrainTests-tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = FEAE99731FEC3BCE00B52CA9 /* Build configuration list for PBXNativeTarget "QuizTrainTests-tvOS" */; + buildPhases = ( + FEAE99631FEC3BCE00B52CA9 /* Sources */, + FEAE99641FEC3BCE00B52CA9 /* Frameworks */, + FEAE99651FEC3BCE00B52CA9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + FEAE996A1FEC3BCE00B52CA9 /* PBXTargetDependency */, + ); + name = "QuizTrainTests-tvOS"; + productName = "QuizTrain-tvOSTests"; + productReference = FEAE99671FEC3BCE00B52CA9 /* QuizTrainTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + FEAE997A1FEC3BEB00B52CA9 /* QuizTrain-macOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = FEAE998C1FEC3BEB00B52CA9 /* Build configuration list for PBXNativeTarget "QuizTrain-macOS" */; + buildPhases = ( + FEAE99761FEC3BEB00B52CA9 /* Sources */, + FEAE99771FEC3BEB00B52CA9 /* Frameworks */, + FEAE99781FEC3BEB00B52CA9 /* Headers */, + FEAE99791FEC3BEB00B52CA9 /* Resources */, + FEDAE2CC1FED57F000828842 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "QuizTrain-macOS"; + productName = "QuizTrain-macOS"; + productReference = FEAE997B1FEC3BEB00B52CA9 /* QuizTrain.framework */; + productType = "com.apple.product-type.framework"; + }; + FEAE99821FEC3BEB00B52CA9 /* QuizTrainTests-macOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = FEAE998F1FEC3BEB00B52CA9 /* Build configuration list for PBXNativeTarget "QuizTrainTests-macOS" */; + buildPhases = ( + FEAE997F1FEC3BEB00B52CA9 /* Sources */, + FEAE99801FEC3BEB00B52CA9 /* Frameworks */, + FEAE99811FEC3BEB00B52CA9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + FEAE99861FEC3BEB00B52CA9 /* PBXTargetDependency */, + ); + name = "QuizTrainTests-macOS"; + productName = "QuizTrain-macOSTests"; + productReference = FEAE99831FEC3BEB00B52CA9 /* QuizTrainTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + FE23A9131F59F39F007E946D /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 0900; + ORGANIZATIONNAME = "Gallagher, David"; + TargetAttributes = { + FE23A91B1F59F39F007E946D = { + CreatedOnToolsVersion = 9.0; + LastSwiftMigration = 0900; + ProvisioningStyle = Automatic; + }; + FE23A9241F59F3A0007E946D = { + CreatedOnToolsVersion = 9.0; + ProvisioningStyle = Automatic; + }; + FEAE99501FEC38BA00B52CA9 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Automatic; + }; + FEAE995E1FEC3BCD00B52CA9 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Automatic; + }; + FEAE99661FEC3BCE00B52CA9 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Automatic; + }; + FEAE997A1FEC3BEB00B52CA9 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Automatic; + }; + FEAE99821FEC3BEB00B52CA9 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = FE23A9161F59F39F007E946D /* Build configuration list for PBXProject "QuizTrain" */; + compatibilityVersion = "Xcode 8.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = FE23A9121F59F39F007E946D; + productRefGroup = FE23A91D1F59F3A0007E946D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + FE23A91B1F59F39F007E946D /* QuizTrain-iOS */, + FEAE997A1FEC3BEB00B52CA9 /* QuizTrain-macOS */, + FEAE995E1FEC3BCD00B52CA9 /* QuizTrain-tvOS */, + FEAE99501FEC38BA00B52CA9 /* QuizTrain-watchOS */, + FE23A9241F59F3A0007E946D /* QuizTrainTests-iOS */, + FEAE99821FEC3BEB00B52CA9 /* QuizTrainTests-macOS */, + FEAE99661FEC3BCE00B52CA9 /* QuizTrainTests-tvOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + FE23A91A1F59F39F007E946D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FE23A9231F59F3A0007E946D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FEBFEEBB1FE2EAB900E7FE1B /* TestCredentials.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FEAE994F1FEC38BA00B52CA9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FEAE995D1FEC3BCD00B52CA9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FEAE99651FEC3BCE00B52CA9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FEAE9AA01FEC4FF200B52CA9 /* TestCredentials.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FEAE99791FEC3BEB00B52CA9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FEAE99811FEC3BEB00B52CA9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FEAE9A9D1FEC4FF100B52CA9 /* TestCredentials.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + FE8464661FE31868006CB58F /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi"; + }; + FEDAE2CC1FED57F000828842 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi"; + }; + FEDAE2CD1FED57FA00828842 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi"; + }; + FEDAE2CE1FED580100828842 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + FE23A9171F59F39F007E946D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FE208DED1FBB69C60065BE88 /* NewCaseResults.Result.swift in Sources */, + FEEC44401FB4D3270042BD5A /* QueryItemProvider.swift in Sources */, + FEEC443E1FB4D23F0042BD5A /* Filter.Value.swift in Sources */, + FE2F1AD71F844FCD00FF9E0C /* JSONSerializable.swift in Sources */, + FEDCBEFB1F96BF540083AD46 /* NewRun.swift in Sources */, + FE813A181F73126F00265569 /* JSONDictionary.swift in Sources */, + FE9F38A61F69E5AD003BBA36 /* Plan.swift in Sources */, + FEBDA3C21FD1C88300124430 /* ObjectAPI.swift in Sources */, + FEC214001FD7302900036B17 /* ObjectAPI.MatchErrorDebug.swift in Sources */, + FE23A9671F59F4B3007E946D /* ResultField.swift in Sources */, + FE23A9711F59F4E2007E946D /* Test.swift in Sources */, + FEDFF2751FE1B09000AEB3D6 /* SingleMatchError.swift in Sources */, + FEDCBEF31F96BF020083AD46 /* NewMilestone.swift in Sources */, + FE30F11F1F6AEDF600AA7761 /* Configuration.swift in Sources */, + FE1474E51FAA6A0D0049DA84 /* ObjectAPI.ServerErrorDebug.swift in Sources */, + FE2245851F75AC82009F2B2B /* CustomFieldsContainer.swift in Sources */, + FE208DF11FBB6BA10065BE88 /* NewCaseResults.swift in Sources */, + FEC214071FD741F700036B17 /* DebugDescription.swift in Sources */, + FE63715F1FD61CA500192CED /* GetConfigurationGroupsOperation.swift in Sources */, + FE5869D71F7478D600BE5C5C /* CustomFields.swift in Sources */, + FE23A9731F59F4EB007E946D /* User.swift in Sources */, + FEDCBEF51F96BF130083AD46 /* NewPlan.swift in Sources */, + FEDCBEEF1F96BEE70083AD46 /* NewConfiguration.swift in Sources */, + FE4F8F3B1F8437DE00447F9E /* JSONDeserializable.swift in Sources */, + FEC214091FD7420300036B17 /* DebugDetails.swift in Sources */, + FEBF50371FCF3E91005B86B7 /* AsyncOperation.swift in Sources */, + FEB546841F9135CB00AA6DA5 /* AddRequestJSON.swift in Sources */, + FE1474EC1FAA6A5B0049DA84 /* ObjectAPI.ObjectConversionErrorDebug.swift in Sources */, + FEDCBEFD1F96BF610083AD46 /* NewSection.swift in Sources */, + FE1474DA1FAA698F0049DA84 /* ObjectAPI.RequestErrorDebug.swift in Sources */, + FE5E1A301F8804E3001E479B /* JSONDictionaryContainer.swift in Sources */, + FE30F11E1F6AED8100AA7761 /* ConfigurationGroup.swift in Sources */, + FE1FB1041F9131B200383724 /* UpdateRequestJSON.swift in Sources */, + FE23A96F1F59F4D9007E946D /* Template.swift in Sources */, + FE5A24F21FE092D300198848 /* UniqueSelection.swift in Sources */, + FE208DEF1FBB6B920065BE88 /* NewTestResults.swift in Sources */, + FE1474DE1FAA69B70049DA84 /* ObjectAPI.UpdateRequestErrorDebug.swift in Sources */, + FEDCBEF91F96BF3A0083AD46 /* NewResult.swift in Sources */, + FE20D03F1FD876820057A45C /* Identifiable.swift in Sources */, + FECF66701FDF05DB00015CC4 /* GetProjectOperation.swift in Sources */, + FEDCBEFF1F96BF730083AD46 /* NewSuite.swift in Sources */, + FE7B53931FBF5BB1003C26BD /* Array+ContentComparison.swift in Sources */, + FEAE7F921F7C5ABE00906FE1 /* Config.Context.swift in Sources */, + FED295471FA10E9300746DAB /* Status.swift in Sources */, + FE3899171FCF2BDE0032E265 /* GetTemplatesOperation.swift in Sources */, + FE4E11D71F7C4112004A315E /* Plan.Entry.swift in Sources */, + FEDD80781F69CA3C00D56EF9 /* Result.swift in Sources */, + FE6C8AE21F8BEA2E00F45642 /* MutableCustomFields.swift in Sources */, + FEAF0BFE1F7D9F15001D4F10 /* Project.SuiteMode.swift in Sources */, + FE30F11D1F6AE4A300AA7761 /* Milestone.swift in Sources */, + FEEC443B1FB4D1BC0042BD5A /* Filter.swift in Sources */, + FE5259121F82D1AD00E0DDB7 /* JSONKey.swift in Sources */, + FE978CEB1F9528400005D181 /* API.RequestErrorDebug.swift in Sources */, + FEA348781F5A141300C1E37A /* API.swift in Sources */, + FEB546861F9135D500AA6DA5 /* AddRequestJSONKeys.swift in Sources */, + FE331BDE1F6AF98F00F9A653 /* CaseType.swift in Sources */, + FEDCBEF71F96BF290083AD46 /* NewProject.swift in Sources */, + FE1474EA1FAA6A340049DA84 /* ObjectAPI.DataProcessingErrorDebug.swift in Sources */, + FE208DFA1FBB95150065BE88 /* Validatable.swift in Sources */, + FE7CD3E41F9ABDC300C6108E /* NewPlan.Entry.swift in Sources */, + FE2D018B1FBCE70B00473B84 /* NewPlan.Entry.Run.swift in Sources */, + FEDD80791F69D7A700D56EF9 /* Project.swift in Sources */, + FEBDA3C11FD1C7F400124430 /* ErrorContainer.swift in Sources */, + FE1474E21FAA69F70049DA84 /* ObjectAPI.ClientErrorDebug.swift in Sources */, + FEDD807A1F69DE1A00D56EF9 /* Priority.swift in Sources */, + FE1474DC1FAA69AA0049DA84 /* ObjectAPI.DataRequestErrorDebug.swift in Sources */, + FE331BDF1F6AFE5D00F9A653 /* CaseField.swift in Sources */, + FE11181C1F6C3A4B00D24A5F /* Config.swift in Sources */, + FEAE99D91FEC431A00B52CA9 /* UpdateRequestJSONKeys.swift in Sources */, + FE331C1C1F6B406300F9A653 /* CustomFieldType.swift in Sources */, + FE8C2D5A1FBA391F005A4150 /* Date+Seconds.swift in Sources */, + FE1474E81FAA6A270049DA84 /* ObjectAPI.StatusCodeErrorDebug.swift in Sources */, + FE6989211FA39B99006CC783 /* UpdatePlanEntryRuns.swift in Sources */, + FE23A96B1F59F4C3007E946D /* Section.swift in Sources */, + FE978CED1F9532BA0005D181 /* URLRequestDebug.swift in Sources */, + FEAE99C01FEC42FB00B52CA9 /* Equatable+OptionalArray.swift in Sources */, + FE331C1A1F6B3C0200F9A653 /* Case.swift in Sources */, + FEB546881F9170A900AA6DA5 /* Outcome.swift in Sources */, + FE208DEB1FBB69B80065BE88 /* NewTestResults.Result.swift in Sources */, + FE23A96D1F59F4CE007E946D /* Suite.swift in Sources */, + FEDFF2771FE1B0A000AEB3D6 /* MultipleMatchError.swift in Sources */, + FE978CE91F9528320005D181 /* API.RequestResultDebug.swift in Sources */, + FE23A9691F59F4BA007E946D /* Run.swift in Sources */, + FEDCBEF11F96BEF10083AD46 /* NewConfigurationGroup.swift in Sources */, + FE4F8F391F84361F00447F9E /* NewCase.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FE23A9211F59F3A0007E946D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FE6A6D311F7590C700DF1039 /* PlanTests.swift in Sources */, + FE58F5661F7EEA29009A1B4E /* CustomFieldsDataProvider.swift in Sources */, + FE3795B41FA799330030C395 /* NewConfigurationGroupTests.swift in Sources */, + FE58F5611F7EE91D009A1B4E /* JSONDataProvider.swift in Sources */, + FE6A6D2F1F7590BA00DF1039 /* MilestoneTests.swift in Sources */, + FE3795D41FA7C7A50030C395 /* JSONDeserializingTests.swift in Sources */, + FE3795CA1FA79A070030C395 /* UpdatePlanEntryRunsTests.swift in Sources */, + FE3795D21FA7C7960030C395 /* InitTests.swift in Sources */, + FE3795CC1FA7A3AD0030C395 /* ObjectProvider.swift in Sources */, + FE6A6D2D1F7590B000DF1039 /* ConfigurationGroupTests.swift in Sources */, + FE3795CE1FA7A4540030C395 /* AddModelTests.swift in Sources */, + FE6A6D331F7590D200DF1039 /* PriorityTests.swift in Sources */, + FE6A6D3B1F7590F900DF1039 /* SectionTests.swift in Sources */, + FE6A6D451F75913000DF1039 /* CaseFieldTests.swift in Sources */, + FE6A6D291F75909600DF1039 /* CaseTypeTests.swift in Sources */, + FE6A6D3D1F75910500DF1039 /* SuiteTests.swift in Sources */, + FE51EFA21F882454007012E0 /* JSONDictionaryContainerTests.swift in Sources */, + FECFE7FF1FD9E1AE00968EA3 /* Array+RandomTests.swift in Sources */, + FE3795B21FA799270030C395 /* NewConfigurationTests.swift in Sources */, + FE3795AA1FA7915C0030C395 /* AssertUpdateRequestJSON.swift in Sources */, + FE1474D61FAA475A0049DA84 /* Equatable+OptionalArrayTests.swift in Sources */, + FE3795C21FA799B70030C395 /* NewTestResults.ResultTests.swift in Sources */, + FE3795E41FA7E1770030C395 /* NewCaseTests.swift in Sources */, + FEF172631F857F53004FFFFF /* AssertJSONSerializing.swift in Sources */, + FE6A6D2B1F7590A100DF1039 /* ConfigurationTests.swift in Sources */, + FE6A6D491F75914300DF1039 /* CustomFieldTypeTests.swift in Sources */, + FE018DC71F85708C001A2FEF /* ModelTests.swift in Sources */, + FE6A6D391F7590F000DF1039 /* RunTests.swift in Sources */, + FE6A6D411F75911A00DF1039 /* TestTests.swift in Sources */, + FE3795B61FA799400030C395 /* NewMilestoneTests.swift in Sources */, + FEF9CE7E1F7C538F0078CD4E /* Plan.EntryTests.swift in Sources */, + FE3795DA1FA7C7E30030C395 /* VariablePropertyTests.swift in Sources */, + FE549C431FBE514A008CDFCE /* NewPlan.EntryTests.swift in Sources */, + FE3795D61FA7C7B60030C395 /* JSONSerializingTests.swift in Sources */, + FE6A6D271F75908100DF1039 /* CaseTests.swift in Sources */, + FE5A24F41FE099BD00198848 /* Array+Random.swift in Sources */, + FEC2140E1FD7537800036B17 /* AsyncOperationTests.swift in Sources */, + FE2877FA1FBBC47B004503FB /* FilterTests.swift in Sources */, + FE2F1AD51F843AEE00FF9E0C /* AssertJSONDeserializing.swift in Sources */, + FEC2140B1FD7533000036B17 /* ErrorContainerTests.swift in Sources */, + FE2877F01FBB9A80004503FB /* ValidatableObjectProvider.swift in Sources */, + FE58F54A1F7EBF12009A1B4E /* AssertCustomFields.swift in Sources */, + FE3795DC1FA7C7FA0030C395 /* UpdateRequestJSONTests.swift in Sources */, + FE2877EC1FBB979C004503FB /* ValidatableTests.swift in Sources */, + FE3795E81FA7EB380030C395 /* AddRequestJSONTests.swift in Sources */, + FEBFEEB91FE2EAAB00E7FE1B /* TestCredentials.swift in Sources */, + FE3795E11FA7D8360030C395 /* UpdateModelTests.swift in Sources */, + FEE774371FA8EA980016AACE /* AssertEquatable.swift in Sources */, + FE3795E61FA7EA6C0030C395 /* AssertAddRequestJSON.swift in Sources */, + FE6A6D471F75913900DF1039 /* ConfigTests.swift in Sources */, + FE3795C61FA799E30030C395 /* NewSectionTests.swift in Sources */, + FEE774351FA8E2480016AACE /* EquatableTests.swift in Sources */, + FE75B2E01F83078A00DF367A /* CustomFieldsContainerTests.swift in Sources */, + FE549C441FBE514C008CDFCE /* NewPlan.Entry.RunTests.swift in Sources */, + FEAE7F961F7C5D0C00906FE1 /* Config.ContextTests.swift in Sources */, + FE6A6D431F75912400DF1039 /* UserTests.swift in Sources */, + FE2877EE1FBB9803004503FB /* AssertValidatable.swift in Sources */, + FED295491FA1117F00746DAB /* StatusTests.swift in Sources */, + FE3795BE1FA7999C0030C395 /* NewResultTests.swift in Sources */, + FE549C421FBE5147008CDFCE /* NewPlanTests.swift in Sources */, + FE6A6D371F7590E500DF1039 /* ResultTests.swift in Sources */, + FE549C461FBE60F3008CDFCE /* Array+ContentComparisonTests.swift in Sources */, + FE77BB761FA9132800E23865 /* NewRunTests.swift in Sources */, + FE58F5481F7EBF04009A1B4E /* AssertProperties.swift in Sources */, + FE3795D81FA7C7D00030C395 /* JSONTwoWaySerializationTests.swift in Sources */, + FE6A6D351F7590DC00DF1039 /* ProjectTests.swift in Sources */, + FE2877F61FBBBD37004503FB /* NewTestResultsTests.swift in Sources */, + FE208DF51FBB716B0065BE88 /* NewCaseResults.ResultTests.swift in Sources */, + FE6A6D3F1F75910E00DF1039 /* TemplateTests.swift in Sources */, + FEF172661F858880004FFFFF /* AssertJSONTwoWaySerialization.swift in Sources */, + FE496B5F1F7D9BCF00AE9454 /* ResultFieldTests.swift in Sources */, + FE0B15471FED689F0009B570 /* ObjectAPITests.swift in Sources */, + FE3795C81FA799F50030C395 /* NewSuiteTests.swift in Sources */, + FE2877F41FBBBC50004503FB /* NewCaseResultsTests.swift in Sources */, + FECF66761FDF593600015CC4 /* UniqueSelectionTests.swift in Sources */, + FE3795BC1FA7998F0030C395 /* NewProjectTests.swift in Sources */, + FE791A251F7D9FFA00D7E870 /* Project.SuiteModeTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FEAE994C1FEC38BA00B52CA9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FEAE99A41FEC42D900B52CA9 /* ErrorContainer.swift in Sources */, + FEAE9A551FEC4F1000B52CA9 /* Filter.swift in Sources */, + FEAE9A661FEC4F1400B52CA9 /* NewSuite.swift in Sources */, + FEAE99A11FEC42D500B52CA9 /* CustomFieldsContainer.swift in Sources */, + FEAE999E1FEC42D200B52CA9 /* MutableCustomFields.swift in Sources */, + FEAE99D41FEC431500B52CA9 /* UpdateRequestJSON.swift in Sources */, + FEAE999B1FEC42CE00B52CA9 /* CustomFields.swift in Sources */, + FEAE9A8E1FEC4F1D00B52CA9 /* ObjectAPI.swift in Sources */, + FEAE9A341FEC4F0A00B52CA9 /* ObjectAPI.DataProcessingErrorDebug.swift in Sources */, + FEAE9A1D1FEC4EFF00B52CA9 /* Test.swift in Sources */, + FEAE9A651FEC4F1400B52CA9 /* NewSection.swift in Sources */, + FEAE9A5A1FEC4F1400B52CA9 /* NewMilestone.swift in Sources */, + FEAE9A151FEC4EFA00B52CA9 /* ResultField.swift in Sources */, + FEAE9A871FEC4F1800B52CA9 /* UpdatePlanEntryRuns.swift in Sources */, + FEAE99BA1FEC42F300B52CA9 /* Array+ContentComparison.swift in Sources */, + FEAE99D71FEC431900B52CA9 /* UpdateRequestJSONKeys.swift in Sources */, + FEAE9A3B1FEC4F0A00B52CA9 /* ObjectAPI.UpdateRequestErrorDebug.swift in Sources */, + FEAE9A8B1FEC4F1D00B52CA9 /* GetProjectOperation.swift in Sources */, + FEAE99E71FEC432E00B52CA9 /* CustomFieldType.swift in Sources */, + FEAE99DB1FEC431E00B52CA9 /* Validatable.swift in Sources */, + FEAE9A571FEC4F1400B52CA9 /* NewCase.swift in Sources */, + FEAE99CB1FEC430A00B52CA9 /* JSONDeserializable.swift in Sources */, + FEAE9A581FEC4F1400B52CA9 /* NewConfiguration.swift in Sources */, + FEAE99FF1FEC4ECD00B52CA9 /* Configuration.swift in Sources */, + FEAE9A5F1FEC4F1400B52CA9 /* NewResult.swift in Sources */, + FEAE99F91FEC4EC600B52CA9 /* CaseField.swift in Sources */, + FEAE9A361FEC4F0A00B52CA9 /* ObjectAPI.MatchErrorDebug.swift in Sources */, + FEAE9A351FEC4F0A00B52CA9 /* ObjectAPI.DataRequestErrorDebug.swift in Sources */, + FEAE99AB1FEC42E000B52CA9 /* DebugDescription.swift in Sources */, + FEAE9A371FEC4F0A00B52CA9 /* ObjectAPI.ObjectConversionErrorDebug.swift in Sources */, + FEAE99A71FEC42DC00B52CA9 /* JSONDictionaryContainer.swift in Sources */, + FEAE99FC1FEC4EC900B52CA9 /* CaseType.swift in Sources */, + FEAE99F61FEC4EC200B52CA9 /* Case.swift in Sources */, + FEAE9A121FEC4EF600B52CA9 /* Result.swift in Sources */, + FEAE99F01FEC433800B52CA9 /* Config.Context.swift in Sources */, + FEAE9A8D1FEC4F1D00B52CA9 /* API.swift in Sources */, + FEAE9A5B1FEC4F1400B52CA9 /* NewPlan.swift in Sources */, + FEAE99B21FEC42E900B52CA9 /* SingleMatchError.swift in Sources */, + FEAE9A5C1FEC4F1400B52CA9 /* NewPlan.Entry.swift in Sources */, + FEAE9A3A1FEC4F0A00B52CA9 /* ObjectAPI.StatusCodeErrorDebug.swift in Sources */, + FEAE9A1C1FEC4EFF00B52CA9 /* Template.swift in Sources */, + FEAE9A051FEC4ED500B52CA9 /* Milestone.swift in Sources */, + FEAE9A3C1FEC4F0A00B52CA9 /* URLRequestDebug.swift in Sources */, + FEAE99AE1FEC42E500B52CA9 /* DebugDetails.swift in Sources */, + FEAE9A0B1FEC4EDD00B52CA9 /* Plan.Entry.swift in Sources */, + FEAE9A611FEC4F1400B52CA9 /* NewCaseResults.Result.swift in Sources */, + FEAE99C81FEC430600B52CA9 /* JSONDictionary.swift in Sources */, + FEAE99B61FEC42EE00B52CA9 /* MultipleMatchError.swift in Sources */, + FEAE9A0F1FEC4EF100B52CA9 /* Project.swift in Sources */, + FEAE9A381FEC4F0A00B52CA9 /* ObjectAPI.RequestErrorDebug.swift in Sources */, + FEAE9A8C1FEC4F1D00B52CA9 /* GetTemplatesOperation.swift in Sources */, + FEAE9A331FEC4F0A00B52CA9 /* ObjectAPI.ClientErrorDebug.swift in Sources */, + FEAE9A391FEC4F0A00B52CA9 /* ObjectAPI.ServerErrorDebug.swift in Sources */, + FEAE9A0C1FEC4EED00B52CA9 /* Priority.swift in Sources */, + FEAE9A5E1FEC4F1400B52CA9 /* NewProject.swift in Sources */, + FEAE9A601FEC4F1400B52CA9 /* NewCaseResults.swift in Sources */, + FEAE9A181FEC4EFF00B52CA9 /* Run.swift in Sources */, + FEAE99BE1FEC42FA00B52CA9 /* Equatable+OptionalArray.swift in Sources */, + FEAE99CE1FEC430E00B52CA9 /* JSONSerializable.swift in Sources */, + FEAE99EA1FEC433100B52CA9 /* Project.SuiteMode.swift in Sources */, + FEAE99C51FEC430200B52CA9 /* JSONKey.swift in Sources */, + FEAE99941FEC42C600B52CA9 /* AddRequestJSON.swift in Sources */, + FEAE9A2E1FEC4F0600B52CA9 /* API.RequestResultDebug.swift in Sources */, + FEAE9A8A1FEC4F1D00B52CA9 /* GetConfigurationGroupsOperation.swift in Sources */, + FEAE9A5D1FEC4F1400B52CA9 /* NewPlan.Entry.Run.swift in Sources */, + FEAE99971FEC42CA00B52CA9 /* AddRequestJSONKeys.swift in Sources */, + FEAE9A641FEC4F1400B52CA9 /* NewRun.swift in Sources */, + FEAE99ED1FEC433500B52CA9 /* Config.swift in Sources */, + FEAE99E11FEC432700B52CA9 /* QueryItemProvider.swift in Sources */, + FEAE99DE1FEC432200B52CA9 /* Outcome.swift in Sources */, + FEAE9A191FEC4EFF00B52CA9 /* Section.swift in Sources */, + FEAE99C21FEC42FE00B52CA9 /* Identifiable.swift in Sources */, + FEAE9A631FEC4F1400B52CA9 /* NewTestResults.Result.swift in Sources */, + FEAE9A1E1FEC4EFF00B52CA9 /* User.swift in Sources */, + FEAE9A2D1FEC4F0600B52CA9 /* API.RequestErrorDebug.swift in Sources */, + FEAE9A021FEC4ED100B52CA9 /* ConfigurationGroup.swift in Sources */, + FEAE9A1B1FEC4EFF00B52CA9 /* Suite.swift in Sources */, + FEAE99D11FEC431200B52CA9 /* AsyncOperation.swift in Sources */, + FEAE9A1A1FEC4EFF00B52CA9 /* Status.swift in Sources */, + FEAE9A081FEC4ED900B52CA9 /* Plan.swift in Sources */, + FEAE9A621FEC4F1400B52CA9 /* NewTestResults.swift in Sources */, + FEAE9A561FEC4F1000B52CA9 /* Filter.Value.swift in Sources */, + FEAE99BB1FEC42F500B52CA9 /* Date+Seconds.swift in Sources */, + FEAE99E41FEC432A00B52CA9 /* UniqueSelection.swift in Sources */, + FEAE9A591FEC4F1400B52CA9 /* NewConfigurationGroup.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FEAE995A1FEC3BCD00B52CA9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FEAE99A31FEC42D800B52CA9 /* ErrorContainer.swift in Sources */, + FEAE9A531FEC4F1000B52CA9 /* Filter.swift in Sources */, + FEAE9A761FEC4F1500B52CA9 /* NewSuite.swift in Sources */, + FEAE99A01FEC42D500B52CA9 /* CustomFieldsContainer.swift in Sources */, + FEAE999D1FEC42D100B52CA9 /* MutableCustomFields.swift in Sources */, + FEAE99D51FEC431600B52CA9 /* UpdateRequestJSON.swift in Sources */, + FEAE99991FEC42CD00B52CA9 /* CustomFields.swift in Sources */, + FEAE9A931FEC4F1E00B52CA9 /* ObjectAPI.swift in Sources */, + FEAE9A3E1FEC4F0B00B52CA9 /* ObjectAPI.DataProcessingErrorDebug.swift in Sources */, + FEAE9A241FEC4F0000B52CA9 /* Test.swift in Sources */, + FEAE9A751FEC4F1500B52CA9 /* NewSection.swift in Sources */, + FEAE9A6A1FEC4F1500B52CA9 /* NewMilestone.swift in Sources */, + FEAE9A161FEC4EFB00B52CA9 /* ResultField.swift in Sources */, + FEAE9A881FEC4F1900B52CA9 /* UpdatePlanEntryRuns.swift in Sources */, + FEAE99B81FEC42F200B52CA9 /* Array+ContentComparison.swift in Sources */, + FEAE99D81FEC431900B52CA9 /* UpdateRequestJSONKeys.swift in Sources */, + FEAE9A451FEC4F0B00B52CA9 /* ObjectAPI.UpdateRequestErrorDebug.swift in Sources */, + FEAE9A901FEC4F1E00B52CA9 /* GetProjectOperation.swift in Sources */, + FEAE99E81FEC432E00B52CA9 /* CustomFieldType.swift in Sources */, + FEAE99DC1FEC431F00B52CA9 /* Validatable.swift in Sources */, + FEAE9A671FEC4F1500B52CA9 /* NewCase.swift in Sources */, + FEAE99CC1FEC430B00B52CA9 /* JSONDeserializable.swift in Sources */, + FEAE9A681FEC4F1500B52CA9 /* NewConfiguration.swift in Sources */, + FEAE99FE1FEC4ECB00B52CA9 /* Configuration.swift in Sources */, + FEAE9A6F1FEC4F1500B52CA9 /* NewResult.swift in Sources */, + FEAE99F81FEC4EC500B52CA9 /* CaseField.swift in Sources */, + FEAE9A401FEC4F0B00B52CA9 /* ObjectAPI.MatchErrorDebug.swift in Sources */, + FEAE9A3F1FEC4F0B00B52CA9 /* ObjectAPI.DataRequestErrorDebug.swift in Sources */, + FEAE99A91FEC42DF00B52CA9 /* DebugDescription.swift in Sources */, + FEAE9A411FEC4F0B00B52CA9 /* ObjectAPI.ObjectConversionErrorDebug.swift in Sources */, + FEAE99A61FEC42DB00B52CA9 /* JSONDictionaryContainer.swift in Sources */, + FEAE99FB1FEC4EC800B52CA9 /* CaseType.swift in Sources */, + FEAE99F51FEC4EC200B52CA9 /* Case.swift in Sources */, + FEAE9A131FEC4EF700B52CA9 /* Result.swift in Sources */, + FEAE99F31FEC433B00B52CA9 /* Config.Context.swift in Sources */, + FEAE9A921FEC4F1E00B52CA9 /* API.swift in Sources */, + FEAE9A6B1FEC4F1500B52CA9 /* NewPlan.swift in Sources */, + FEAE99B01FEC42E800B52CA9 /* SingleMatchError.swift in Sources */, + FEAE9A6C1FEC4F1500B52CA9 /* NewPlan.Entry.swift in Sources */, + FEAE9A441FEC4F0B00B52CA9 /* ObjectAPI.StatusCodeErrorDebug.swift in Sources */, + FEAE9A231FEC4F0000B52CA9 /* Template.swift in Sources */, + FEAE9A041FEC4ED400B52CA9 /* Milestone.swift in Sources */, + FEAE9A461FEC4F0B00B52CA9 /* URLRequestDebug.swift in Sources */, + FEAE99AD1FEC42E500B52CA9 /* DebugDetails.swift in Sources */, + FEAE9A0A1FEC4EDD00B52CA9 /* Plan.Entry.swift in Sources */, + FEAE9A711FEC4F1500B52CA9 /* NewCaseResults.Result.swift in Sources */, + FEAE99C91FEC430600B52CA9 /* JSONDictionary.swift in Sources */, + FEAE99B41FEC42ED00B52CA9 /* MultipleMatchError.swift in Sources */, + FEAE9A101FEC4EF200B52CA9 /* Project.swift in Sources */, + FEAE9A421FEC4F0B00B52CA9 /* ObjectAPI.RequestErrorDebug.swift in Sources */, + FEAE9A911FEC4F1E00B52CA9 /* GetTemplatesOperation.swift in Sources */, + FEAE9A3D1FEC4F0B00B52CA9 /* ObjectAPI.ClientErrorDebug.swift in Sources */, + FEAE9A431FEC4F0B00B52CA9 /* ObjectAPI.ServerErrorDebug.swift in Sources */, + FEAE9A0D1FEC4EED00B52CA9 /* Priority.swift in Sources */, + FEAE9A6E1FEC4F1500B52CA9 /* NewProject.swift in Sources */, + FEAE9A701FEC4F1500B52CA9 /* NewCaseResults.swift in Sources */, + FEAE9A1F1FEC4F0000B52CA9 /* Run.swift in Sources */, + FEAE99BF1FEC42FA00B52CA9 /* Equatable+OptionalArray.swift in Sources */, + FEAE99CF1FEC430F00B52CA9 /* JSONSerializable.swift in Sources */, + FEAE99EB1FEC433200B52CA9 /* Project.SuiteMode.swift in Sources */, + FEAE99C61FEC430300B52CA9 /* JSONKey.swift in Sources */, + FEAE99931FEC42C600B52CA9 /* AddRequestJSON.swift in Sources */, + FEAE9A301FEC4F0600B52CA9 /* API.RequestResultDebug.swift in Sources */, + FEAE9A8F1FEC4F1E00B52CA9 /* GetConfigurationGroupsOperation.swift in Sources */, + FEAE9A6D1FEC4F1500B52CA9 /* NewPlan.Entry.Run.swift in Sources */, + FEAE99961FEC42CA00B52CA9 /* AddRequestJSONKeys.swift in Sources */, + FEAE9A741FEC4F1500B52CA9 /* NewRun.swift in Sources */, + FEAE99EE1FEC433600B52CA9 /* Config.swift in Sources */, + FEAE99E21FEC432700B52CA9 /* QueryItemProvider.swift in Sources */, + FEAE99DF1FEC432300B52CA9 /* Outcome.swift in Sources */, + FEAE9A201FEC4F0000B52CA9 /* Section.swift in Sources */, + FEAE99C31FEC42FF00B52CA9 /* Identifiable.swift in Sources */, + FEAE9A731FEC4F1500B52CA9 /* NewTestResults.Result.swift in Sources */, + FEAE9A251FEC4F0000B52CA9 /* User.swift in Sources */, + FEAE9A2F1FEC4F0600B52CA9 /* API.RequestErrorDebug.swift in Sources */, + FEAE9A011FEC4ED100B52CA9 /* ConfigurationGroup.swift in Sources */, + FEAE9A221FEC4F0000B52CA9 /* Suite.swift in Sources */, + FEAE99D21FEC431300B52CA9 /* AsyncOperation.swift in Sources */, + FEAE9A211FEC4F0000B52CA9 /* Status.swift in Sources */, + FEAE9A071FEC4ED900B52CA9 /* Plan.swift in Sources */, + FEAE9A721FEC4F1500B52CA9 /* NewTestResults.swift in Sources */, + FEAE9A541FEC4F1000B52CA9 /* Filter.Value.swift in Sources */, + FEAE99BC1FEC42F600B52CA9 /* Date+Seconds.swift in Sources */, + FEAE99E51FEC432A00B52CA9 /* UniqueSelection.swift in Sources */, + FEAE9A691FEC4F1500B52CA9 /* NewConfigurationGroup.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FEAE99631FEC3BCE00B52CA9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FEAE9B1A1FEC501F00B52CA9 /* NewProjectTests.swift in Sources */, + FEAE9ADD1FEC501000B52CA9 /* CustomFieldTypeTests.swift in Sources */, + FEAE9B0D1FEC501700B52CA9 /* FilterTests.swift in Sources */, + FEAE9B161FEC501F00B52CA9 /* NewMilestoneTests.swift in Sources */, + FEAE9ADA1FEC500A00B52CA9 /* AsyncOperationTests.swift in Sources */, + FEAE9B151FEC501F00B52CA9 /* NewConfigurationTests.swift in Sources */, + FEAE9AD31FEC500600B52CA9 /* Array+ContentComparisonTests.swift in Sources */, + FEAE9AB81FEC4FF900B52CA9 /* JSONDataProvider.swift in Sources */, + FEAE9B1D1FEC501F00B52CA9 /* NewCaseResults.ResultTests.swift in Sources */, + FEAE9AEB1FEC501400B52CA9 /* PlanTests.swift in Sources */, + FEAE9AF21FEC501400B52CA9 /* SectionTests.swift in Sources */, + FEAE9AEE1FEC501400B52CA9 /* ProjectTests.swift in Sources */, + FEAE9ADE1FEC501000B52CA9 /* Project.SuiteModeTests.swift in Sources */, + FEAE9B1E1FEC501F00B52CA9 /* NewTestResultsTests.swift in Sources */, + FEAE9B1F1FEC501F00B52CA9 /* NewTestResults.ResultTests.swift in Sources */, + FEAE9B0F1FEC501B00B52CA9 /* AddModelTests.swift in Sources */, + FEAE9AAD1FEC4FF500B52CA9 /* AssertJSONDeserializing.swift in Sources */, + FEAE9AAA1FEC4FF500B52CA9 /* AssertAddRequestJSON.swift in Sources */, + FEAE9B1B1FEC501F00B52CA9 /* NewResultTests.swift in Sources */, + FEAE9ACF1FEC500200B52CA9 /* JSONDictionaryContainerTests.swift in Sources */, + FEAE9ABB1FEC4FFE00B52CA9 /* AddRequestJSONTests.swift in Sources */, + FEAE9ABE1FEC4FFE00B52CA9 /* JSONDeserializingTests.swift in Sources */, + FEAE9ABA1FEC4FF900B52CA9 /* ValidatableObjectProvider.swift in Sources */, + FEAE9B191FEC501F00B52CA9 /* NewPlan.Entry.RunTests.swift in Sources */, + FEAE9AF11FEC501400B52CA9 /* RunTests.swift in Sources */, + FEAE9AF61FEC501400B52CA9 /* TestTests.swift in Sources */, + FEAE9AD41FEC500600B52CA9 /* Array+RandomTests.swift in Sources */, + FEAE9AE41FEC501400B52CA9 /* Config.ContextTests.swift in Sources */, + FEAE9A9F1FEC4FF200B52CA9 /* TestCredentials.swift in Sources */, + FEAE9AE31FEC501400B52CA9 /* ConfigTests.swift in Sources */, + FEAE9AE51FEC501400B52CA9 /* CaseTests.swift in Sources */, + FEAE9ABF1FEC4FFE00B52CA9 /* JSONSerializingTests.swift in Sources */, + FEAE9ACE1FEC500200B52CA9 /* ErrorContainerTests.swift in Sources */, + FEAE9B181FEC501F00B52CA9 /* NewPlan.EntryTests.swift in Sources */, + FEAE9AB01FEC4FF500B52CA9 /* AssertProperties.swift in Sources */, + FEAE9B201FEC501F00B52CA9 /* NewRunTests.swift in Sources */, + FEAE9B131FEC501F00B52CA9 /* NewCaseTests.swift in Sources */, + FEAE9ABD1FEC4FFE00B52CA9 /* InitTests.swift in Sources */, + FEAE9ADC1FEC500D00B52CA9 /* ModelTests.swift in Sources */, + FEAE9ADF1FEC501000B52CA9 /* UniqueSelectionTests.swift in Sources */, + FEAE9AED1FEC501400B52CA9 /* PriorityTests.swift in Sources */, + FEAE9AF01FEC501400B52CA9 /* ResultFieldTests.swift in Sources */, + FEAE9AC11FEC4FFE00B52CA9 /* UpdateRequestJSONTests.swift in Sources */, + FEAE9AEC1FEC501400B52CA9 /* Plan.EntryTests.swift in Sources */, + FEAE9B211FEC501F00B52CA9 /* NewSectionTests.swift in Sources */, + FEAE9AB71FEC4FF900B52CA9 /* CustomFieldsDataProvider.swift in Sources */, + FEAE9AAF1FEC4FF500B52CA9 /* AssertJSONTwoWaySerialization.swift in Sources */, + FEAE9B141FEC501F00B52CA9 /* NewConfigurationGroupTests.swift in Sources */, + FEAE9AC01FEC4FFE00B52CA9 /* JSONTwoWaySerializationTests.swift in Sources */, + FEAE9B101FEC501B00B52CA9 /* UpdateModelTests.swift in Sources */, + FEAE9AEF1FEC501400B52CA9 /* ResultTests.swift in Sources */, + FEAE9ABC1FEC4FFE00B52CA9 /* EquatableTests.swift in Sources */, + FEAE9B221FEC501F00B52CA9 /* NewSuiteTests.swift in Sources */, + FEAE9AE61FEC501400B52CA9 /* CaseFieldTests.swift in Sources */, + FEAE9AAE1FEC4FF500B52CA9 /* AssertJSONSerializing.swift in Sources */, + FEAE9AB11FEC4FF500B52CA9 /* AssertUpdateRequestJSON.swift in Sources */, + FEAE9AAC1FEC4FF500B52CA9 /* AssertEquatable.swift in Sources */, + FEAE9AEA1FEC501400B52CA9 /* MilestoneTests.swift in Sources */, + FEAE9AE71FEC501400B52CA9 /* CaseTypeTests.swift in Sources */, + FEAE9AB21FEC4FF500B52CA9 /* AssertValidatable.swift in Sources */, + FEAE9A9E1FEC4FF200B52CA9 /* Array+Random.swift in Sources */, + FEAE9AF31FEC501400B52CA9 /* StatusTests.swift in Sources */, + FEAE9AE91FEC501400B52CA9 /* ConfigurationGroupTests.swift in Sources */, + FEAE9B171FEC501F00B52CA9 /* NewPlanTests.swift in Sources */, + FEAE9AD51FEC500600B52CA9 /* Equatable+OptionalArrayTests.swift in Sources */, + FEAE9AE81FEC501400B52CA9 /* ConfigurationTests.swift in Sources */, + FEAE9ACD1FEC500200B52CA9 /* CustomFieldsContainerTests.swift in Sources */, + FEAE9AC31FEC4FFE00B52CA9 /* VariablePropertyTests.swift in Sources */, + FEAE9B351FEC502300B52CA9 /* UpdatePlanEntryRunsTests.swift in Sources */, + FEAE9AF41FEC501400B52CA9 /* SuiteTests.swift in Sources */, + FEAE9AAB1FEC4FF500B52CA9 /* AssertCustomFields.swift in Sources */, + FE0B15491FED68A00009B570 /* ObjectAPITests.swift in Sources */, + FEAE9AF71FEC501400B52CA9 /* UserTests.swift in Sources */, + FEAE9AF51FEC501400B52CA9 /* TemplateTests.swift in Sources */, + FEAE9AB91FEC4FF900B52CA9 /* ObjectProvider.swift in Sources */, + FEAE9B1C1FEC501F00B52CA9 /* NewCaseResultsTests.swift in Sources */, + FEAE9AC21FEC4FFE00B52CA9 /* ValidatableTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FEAE99761FEC3BEB00B52CA9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FEAE99A21FEC42D800B52CA9 /* ErrorContainer.swift in Sources */, + FEAE9A511FEC4F0F00B52CA9 /* Filter.swift in Sources */, + FEAE9A861FEC4F1500B52CA9 /* NewSuite.swift in Sources */, + FEAE999F1FEC42D400B52CA9 /* CustomFieldsContainer.swift in Sources */, + FEAE999C1FEC42D000B52CA9 /* MutableCustomFields.swift in Sources */, + FEAE99D61FEC431700B52CA9 /* UpdateRequestJSON.swift in Sources */, + FEAE99981FEC42CD00B52CA9 /* CustomFields.swift in Sources */, + FEAE9A981FEC4F1E00B52CA9 /* ObjectAPI.swift in Sources */, + FEAE9A481FEC4F0B00B52CA9 /* ObjectAPI.DataProcessingErrorDebug.swift in Sources */, + FEAE9A2B1FEC4F0000B52CA9 /* Test.swift in Sources */, + FEAE9A851FEC4F1500B52CA9 /* NewSection.swift in Sources */, + FEAE9A7A1FEC4F1500B52CA9 /* NewMilestone.swift in Sources */, + FEAE9A171FEC4EFB00B52CA9 /* ResultField.swift in Sources */, + FEAE9A891FEC4F1A00B52CA9 /* UpdatePlanEntryRuns.swift in Sources */, + FEAE99B71FEC42F100B52CA9 /* Array+ContentComparison.swift in Sources */, + FEAE99DA1FEC431B00B52CA9 /* UpdateRequestJSONKeys.swift in Sources */, + FEAE9A4F1FEC4F0B00B52CA9 /* ObjectAPI.UpdateRequestErrorDebug.swift in Sources */, + FEAE9A951FEC4F1E00B52CA9 /* GetProjectOperation.swift in Sources */, + FEAE99E91FEC432F00B52CA9 /* CustomFieldType.swift in Sources */, + FEAE99DD1FEC431F00B52CA9 /* Validatable.swift in Sources */, + FEAE9A771FEC4F1500B52CA9 /* NewCase.swift in Sources */, + FEAE99CD1FEC430C00B52CA9 /* JSONDeserializable.swift in Sources */, + FEAE9A781FEC4F1500B52CA9 /* NewConfiguration.swift in Sources */, + FEAE99FD1FEC4ECB00B52CA9 /* Configuration.swift in Sources */, + FEAE9A7F1FEC4F1500B52CA9 /* NewResult.swift in Sources */, + FEAE99F71FEC4EC500B52CA9 /* CaseField.swift in Sources */, + FEAE9A4A1FEC4F0B00B52CA9 /* ObjectAPI.MatchErrorDebug.swift in Sources */, + FEAE9A491FEC4F0B00B52CA9 /* ObjectAPI.DataRequestErrorDebug.swift in Sources */, + FEAE99A81FEC42DE00B52CA9 /* DebugDescription.swift in Sources */, + FEAE9A4B1FEC4F0B00B52CA9 /* ObjectAPI.ObjectConversionErrorDebug.swift in Sources */, + FEAE99A51FEC42DB00B52CA9 /* JSONDictionaryContainer.swift in Sources */, + FEAE99FA1FEC4EC800B52CA9 /* CaseType.swift in Sources */, + FEAE99F41FEC4EC100B52CA9 /* Case.swift in Sources */, + FEAE9A141FEC4EF700B52CA9 /* Result.swift in Sources */, + FEAE99F21FEC433B00B52CA9 /* Config.Context.swift in Sources */, + FEAE9A971FEC4F1E00B52CA9 /* API.swift in Sources */, + FEAE9A7B1FEC4F1500B52CA9 /* NewPlan.swift in Sources */, + FEAE99AF1FEC42E800B52CA9 /* SingleMatchError.swift in Sources */, + FEAE9A7C1FEC4F1500B52CA9 /* NewPlan.Entry.swift in Sources */, + FEAE9A4E1FEC4F0B00B52CA9 /* ObjectAPI.StatusCodeErrorDebug.swift in Sources */, + FEAE9A2A1FEC4F0000B52CA9 /* Template.swift in Sources */, + FEAE9A031FEC4ED400B52CA9 /* Milestone.swift in Sources */, + FEAE9A501FEC4F0B00B52CA9 /* URLRequestDebug.swift in Sources */, + FEAE99AC1FEC42E400B52CA9 /* DebugDetails.swift in Sources */, + FEAE9A091FEC4EDC00B52CA9 /* Plan.Entry.swift in Sources */, + FEAE9A811FEC4F1500B52CA9 /* NewCaseResults.Result.swift in Sources */, + FEAE99CA1FEC430800B52CA9 /* JSONDictionary.swift in Sources */, + FEAE99B31FEC42EC00B52CA9 /* MultipleMatchError.swift in Sources */, + FEAE9A111FEC4EF200B52CA9 /* Project.swift in Sources */, + FEAE9A4C1FEC4F0B00B52CA9 /* ObjectAPI.RequestErrorDebug.swift in Sources */, + FEAE9A961FEC4F1E00B52CA9 /* GetTemplatesOperation.swift in Sources */, + FEAE9A471FEC4F0B00B52CA9 /* ObjectAPI.ClientErrorDebug.swift in Sources */, + FEAE9A4D1FEC4F0B00B52CA9 /* ObjectAPI.ServerErrorDebug.swift in Sources */, + FEAE9A0E1FEC4EEE00B52CA9 /* Priority.swift in Sources */, + FEAE9A7E1FEC4F1500B52CA9 /* NewProject.swift in Sources */, + FEAE9A801FEC4F1500B52CA9 /* NewCaseResults.swift in Sources */, + FEAE9A261FEC4F0000B52CA9 /* Run.swift in Sources */, + FEAE99C11FEC42FC00B52CA9 /* Equatable+OptionalArray.swift in Sources */, + FEAE99D01FEC430F00B52CA9 /* JSONSerializable.swift in Sources */, + FEAE99EC1FEC433200B52CA9 /* Project.SuiteMode.swift in Sources */, + FEAE99C71FEC430300B52CA9 /* JSONKey.swift in Sources */, + FEAE99921FEC42C600B52CA9 /* AddRequestJSON.swift in Sources */, + FEAE9A321FEC4F0700B52CA9 /* API.RequestResultDebug.swift in Sources */, + FEAE9A941FEC4F1E00B52CA9 /* GetConfigurationGroupsOperation.swift in Sources */, + FEAE9A7D1FEC4F1500B52CA9 /* NewPlan.Entry.Run.swift in Sources */, + FEAE99951FEC42C900B52CA9 /* AddRequestJSONKeys.swift in Sources */, + FEAE9A841FEC4F1500B52CA9 /* NewRun.swift in Sources */, + FEAE99EF1FEC433600B52CA9 /* Config.swift in Sources */, + FEAE99E31FEC432800B52CA9 /* QueryItemProvider.swift in Sources */, + FEAE99E01FEC432300B52CA9 /* Outcome.swift in Sources */, + FEAE9A271FEC4F0000B52CA9 /* Section.swift in Sources */, + FEAE99C41FEC430000B52CA9 /* Identifiable.swift in Sources */, + FEAE9A831FEC4F1500B52CA9 /* NewTestResults.Result.swift in Sources */, + FEAE9A2C1FEC4F0000B52CA9 /* User.swift in Sources */, + FEAE9A311FEC4F0700B52CA9 /* API.RequestErrorDebug.swift in Sources */, + FEAE9A001FEC4ED000B52CA9 /* ConfigurationGroup.swift in Sources */, + FEAE9A291FEC4F0000B52CA9 /* Suite.swift in Sources */, + FEAE99D31FEC431300B52CA9 /* AsyncOperation.swift in Sources */, + FEAE9A281FEC4F0000B52CA9 /* Status.swift in Sources */, + FEAE9A061FEC4ED800B52CA9 /* Plan.swift in Sources */, + FEAE9A821FEC4F1500B52CA9 /* NewTestResults.swift in Sources */, + FEAE9A521FEC4F0F00B52CA9 /* Filter.Value.swift in Sources */, + FEAE99BD1FEC42F700B52CA9 /* Date+Seconds.swift in Sources */, + FEAE99E61FEC432B00B52CA9 /* UniqueSelection.swift in Sources */, + FEAE9A791FEC4F1500B52CA9 /* NewConfigurationGroup.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FEAE997F1FEC3BEB00B52CA9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FEAE9B2A1FEC502000B52CA9 /* NewProjectTests.swift in Sources */, + FEAE9AE01FEC501100B52CA9 /* CustomFieldTypeTests.swift in Sources */, + FEAE9B0E1FEC501800B52CA9 /* FilterTests.swift in Sources */, + FEAE9B261FEC502000B52CA9 /* NewMilestoneTests.swift in Sources */, + FEAE9AD91FEC500900B52CA9 /* AsyncOperationTests.swift in Sources */, + FEAE9B251FEC502000B52CA9 /* NewConfigurationTests.swift in Sources */, + FEAE9AD61FEC500700B52CA9 /* Array+ContentComparisonTests.swift in Sources */, + FEAE9AB41FEC4FF800B52CA9 /* JSONDataProvider.swift in Sources */, + FEAE9B2D1FEC502000B52CA9 /* NewCaseResults.ResultTests.swift in Sources */, + FEAE9B001FEC501400B52CA9 /* PlanTests.swift in Sources */, + FEAE9B071FEC501400B52CA9 /* SectionTests.swift in Sources */, + FEAE9B031FEC501400B52CA9 /* ProjectTests.swift in Sources */, + FEAE9AE11FEC501100B52CA9 /* Project.SuiteModeTests.swift in Sources */, + FEAE9B2E1FEC502000B52CA9 /* NewTestResultsTests.swift in Sources */, + FEAE9B2F1FEC502000B52CA9 /* NewTestResults.ResultTests.swift in Sources */, + FEAE9B111FEC501C00B52CA9 /* AddModelTests.swift in Sources */, + FEAE9AA41FEC4FF500B52CA9 /* AssertJSONDeserializing.swift in Sources */, + FEAE9AA11FEC4FF500B52CA9 /* AssertAddRequestJSON.swift in Sources */, + FEAE9B2B1FEC502000B52CA9 /* NewResultTests.swift in Sources */, + FEAE9AD21FEC500200B52CA9 /* JSONDictionaryContainerTests.swift in Sources */, + FEAE9AC41FEC4FFF00B52CA9 /* AddRequestJSONTests.swift in Sources */, + FEAE9AC71FEC4FFF00B52CA9 /* JSONDeserializingTests.swift in Sources */, + FEAE9AB61FEC4FF800B52CA9 /* ValidatableObjectProvider.swift in Sources */, + FEAE9B291FEC502000B52CA9 /* NewPlan.Entry.RunTests.swift in Sources */, + FEAE9B061FEC501400B52CA9 /* RunTests.swift in Sources */, + FEAE9B0B1FEC501400B52CA9 /* TestTests.swift in Sources */, + FEAE9AD71FEC500700B52CA9 /* Array+RandomTests.swift in Sources */, + FEAE9AF91FEC501400B52CA9 /* Config.ContextTests.swift in Sources */, + FEAE9A9C1FEC4FF100B52CA9 /* TestCredentials.swift in Sources */, + FEAE9AF81FEC501400B52CA9 /* ConfigTests.swift in Sources */, + FEAE9AFA1FEC501400B52CA9 /* CaseTests.swift in Sources */, + FEAE9AC81FEC4FFF00B52CA9 /* JSONSerializingTests.swift in Sources */, + FEAE9AD11FEC500200B52CA9 /* ErrorContainerTests.swift in Sources */, + FEAE9B281FEC502000B52CA9 /* NewPlan.EntryTests.swift in Sources */, + FEAE9AA71FEC4FF500B52CA9 /* AssertProperties.swift in Sources */, + FEAE9B301FEC502000B52CA9 /* NewRunTests.swift in Sources */, + FEAE9B231FEC502000B52CA9 /* NewCaseTests.swift in Sources */, + FEAE9AC61FEC4FFF00B52CA9 /* InitTests.swift in Sources */, + FEAE9ADB1FEC500D00B52CA9 /* ModelTests.swift in Sources */, + FEAE9AE21FEC501100B52CA9 /* UniqueSelectionTests.swift in Sources */, + FEAE9B021FEC501400B52CA9 /* PriorityTests.swift in Sources */, + FEAE9B051FEC501400B52CA9 /* ResultFieldTests.swift in Sources */, + FEAE9ACA1FEC4FFF00B52CA9 /* UpdateRequestJSONTests.swift in Sources */, + FEAE9B011FEC501400B52CA9 /* Plan.EntryTests.swift in Sources */, + FEAE9B311FEC502000B52CA9 /* NewSectionTests.swift in Sources */, + FEAE9AB31FEC4FF800B52CA9 /* CustomFieldsDataProvider.swift in Sources */, + FEAE9AA61FEC4FF500B52CA9 /* AssertJSONTwoWaySerialization.swift in Sources */, + FEAE9B241FEC502000B52CA9 /* NewConfigurationGroupTests.swift in Sources */, + FEAE9AC91FEC4FFF00B52CA9 /* JSONTwoWaySerializationTests.swift in Sources */, + FEAE9B121FEC501C00B52CA9 /* UpdateModelTests.swift in Sources */, + FEAE9B041FEC501400B52CA9 /* ResultTests.swift in Sources */, + FEAE9AC51FEC4FFF00B52CA9 /* EquatableTests.swift in Sources */, + FEAE9B321FEC502000B52CA9 /* NewSuiteTests.swift in Sources */, + FEAE9AFB1FEC501400B52CA9 /* CaseFieldTests.swift in Sources */, + FEAE9AA51FEC4FF500B52CA9 /* AssertJSONSerializing.swift in Sources */, + FEAE9AA81FEC4FF500B52CA9 /* AssertUpdateRequestJSON.swift in Sources */, + FEAE9AA31FEC4FF500B52CA9 /* AssertEquatable.swift in Sources */, + FEAE9AFF1FEC501400B52CA9 /* MilestoneTests.swift in Sources */, + FEAE9AFC1FEC501400B52CA9 /* CaseTypeTests.swift in Sources */, + FEAE9AA91FEC4FF500B52CA9 /* AssertValidatable.swift in Sources */, + FEAE9A9B1FEC4FF100B52CA9 /* Array+Random.swift in Sources */, + FEAE9B081FEC501400B52CA9 /* StatusTests.swift in Sources */, + FEAE9AFE1FEC501400B52CA9 /* ConfigurationGroupTests.swift in Sources */, + FEAE9B271FEC502000B52CA9 /* NewPlanTests.swift in Sources */, + FEAE9AD81FEC500700B52CA9 /* Equatable+OptionalArrayTests.swift in Sources */, + FEAE9AFD1FEC501400B52CA9 /* ConfigurationTests.swift in Sources */, + FEAE9AD01FEC500200B52CA9 /* CustomFieldsContainerTests.swift in Sources */, + FEAE9ACC1FEC4FFF00B52CA9 /* VariablePropertyTests.swift in Sources */, + FEAE9B331FEC502300B52CA9 /* UpdatePlanEntryRunsTests.swift in Sources */, + FEAE9B091FEC501400B52CA9 /* SuiteTests.swift in Sources */, + FEAE9AA21FEC4FF500B52CA9 /* AssertCustomFields.swift in Sources */, + FE0B15481FED68A00009B570 /* ObjectAPITests.swift in Sources */, + FEAE9B0C1FEC501400B52CA9 /* UserTests.swift in Sources */, + FEAE9B0A1FEC501400B52CA9 /* TemplateTests.swift in Sources */, + FEAE9AB51FEC4FF800B52CA9 /* ObjectProvider.swift in Sources */, + FEAE9B2C1FEC502000B52CA9 /* NewCaseResultsTests.swift in Sources */, + FEAE9ACB1FEC4FFF00B52CA9 /* ValidatableTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + FE23A9281F59F3A0007E946D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = FE23A91B1F59F39F007E946D /* QuizTrain-iOS */; + targetProxy = FE23A9271F59F3A0007E946D /* PBXContainerItemProxy */; + }; + FEAE996A1FEC3BCE00B52CA9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = FEAE995E1FEC3BCD00B52CA9 /* QuizTrain-tvOS */; + targetProxy = FEAE99691FEC3BCE00B52CA9 /* PBXContainerItemProxy */; + }; + FEAE99861FEC3BEB00B52CA9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = FEAE997A1FEC3BEB00B52CA9 /* QuizTrain-macOS */; + targetProxy = FEAE99851FEC3BEB00B52CA9 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + FE23A92E1F59F3A0007E946D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MACOSX_DEPLOYMENT_TARGET = 10.12; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + FE23A92F1F59F3A0007E946D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MACOSX_DEPLOYMENT_TARGET = 10.12; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + FE23A9311F59F3A0007E946D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/QuizTrain/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = venmo.QuizTrain; + PRODUCT_NAME = QuizTrain; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + FE23A9321F59F3A0007E946D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/QuizTrain/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = venmo.QuizTrain; + PRODUCT_NAME = QuizTrain; + SKIP_INSTALL = YES; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + FE23A9341F59F3A0007E946D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = QuizTrainTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = venmo.QuizTrainTests; + PRODUCT_NAME = QuizTrainTests; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + FE23A9351F59F3A0007E946D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = QuizTrainTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = venmo.QuizTrainTests; + PRODUCT_NAME = QuizTrainTests; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + FEAE99571FEC38BA00B52CA9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/QuizTrain/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = venmo.QuizTrain; + PRODUCT_NAME = QuizTrain; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 3.0; + }; + name = Debug; + }; + FEAE99581FEC38BA00B52CA9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/QuizTrain/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = venmo.QuizTrain; + PRODUCT_NAME = QuizTrain; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 3.0; + }; + name = Release; + }; + FEAE99711FEC3BCE00B52CA9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/QuizTrain/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = venmo.QuizTrain; + PRODUCT_NAME = QuizTrain; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 10.0; + }; + name = Debug; + }; + FEAE99721FEC3BCE00B52CA9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/QuizTrain/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = venmo.QuizTrain; + PRODUCT_NAME = QuizTrain; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 10.0; + }; + name = Release; + }; + FEAE99741FEC3BCE00B52CA9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = QuizTrainTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = venmo.QuizTrainTests; + PRODUCT_NAME = QuizTrainTests; + SDKROOT = appletvos; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 11.2; + }; + name = Debug; + }; + FEAE99751FEC3BCE00B52CA9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = QuizTrainTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = venmo.QuizTrainTests; + PRODUCT_NAME = QuizTrainTests; + SDKROOT = appletvos; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 11.2; + }; + name = Release; + }; + FEAE998D1FEC3BEB00B52CA9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = "$(SRCROOT)/QuizTrain/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.12; + PRODUCT_BUNDLE_IDENTIFIER = venmo.QuizTrain; + PRODUCT_NAME = QuizTrain; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_VERSION = 4.0; + }; + name = Debug; + }; + FEAE998E1FEC3BEB00B52CA9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = "$(SRCROOT)/QuizTrain/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.12; + PRODUCT_BUNDLE_IDENTIFIER = venmo.QuizTrain; + PRODUCT_NAME = QuizTrain; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_VERSION = 4.0; + }; + name = Release; + }; + FEAE99901FEC3BEB00B52CA9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = QuizTrainTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.12; + PRODUCT_BUNDLE_IDENTIFIER = venmo.QuizTrainTests; + PRODUCT_NAME = QuizTrainTests; + SDKROOT = macosx; + SWIFT_VERSION = 4.0; + }; + name = Debug; + }; + FEAE99911FEC3BEB00B52CA9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = QuizTrainTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.12; + PRODUCT_BUNDLE_IDENTIFIER = venmo.QuizTrainTests; + PRODUCT_NAME = QuizTrainTests; + SDKROOT = macosx; + SWIFT_VERSION = 4.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + FE23A9161F59F39F007E946D /* Build configuration list for PBXProject "QuizTrain" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FE23A92E1F59F3A0007E946D /* Debug */, + FE23A92F1F59F3A0007E946D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + FE23A9301F59F3A0007E946D /* Build configuration list for PBXNativeTarget "QuizTrain-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FE23A9311F59F3A0007E946D /* Debug */, + FE23A9321F59F3A0007E946D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + FE23A9331F59F3A0007E946D /* Build configuration list for PBXNativeTarget "QuizTrainTests-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FE23A9341F59F3A0007E946D /* Debug */, + FE23A9351F59F3A0007E946D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + FEAE99561FEC38BA00B52CA9 /* Build configuration list for PBXNativeTarget "QuizTrain-watchOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FEAE99571FEC38BA00B52CA9 /* Debug */, + FEAE99581FEC38BA00B52CA9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + FEAE99701FEC3BCE00B52CA9 /* Build configuration list for PBXNativeTarget "QuizTrain-tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FEAE99711FEC3BCE00B52CA9 /* Debug */, + FEAE99721FEC3BCE00B52CA9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + FEAE99731FEC3BCE00B52CA9 /* Build configuration list for PBXNativeTarget "QuizTrainTests-tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FEAE99741FEC3BCE00B52CA9 /* Debug */, + FEAE99751FEC3BCE00B52CA9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + FEAE998C1FEC3BEB00B52CA9 /* Build configuration list for PBXNativeTarget "QuizTrain-macOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FEAE998D1FEC3BEB00B52CA9 /* Debug */, + FEAE998E1FEC3BEB00B52CA9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + FEAE998F1FEC3BEB00B52CA9 /* Build configuration list for PBXNativeTarget "QuizTrainTests-macOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FEAE99901FEC3BEB00B52CA9 /* Debug */, + FEAE99911FEC3BEB00B52CA9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = FE23A9131F59F39F007E946D /* Project object */; +} diff --git a/QuizTrain.xcodeproj/xcshareddata/xcschemes/QuizTrain-iOS.xcscheme b/QuizTrain.xcodeproj/xcshareddata/xcschemes/QuizTrain-iOS.xcscheme new file mode 100644 index 0000000..90f6f2e --- /dev/null +++ b/QuizTrain.xcodeproj/xcshareddata/xcschemes/QuizTrain-iOS.xcscheme @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QuizTrain.xcodeproj/xcshareddata/xcschemes/QuizTrain-macOS.xcscheme b/QuizTrain.xcodeproj/xcshareddata/xcschemes/QuizTrain-macOS.xcscheme new file mode 100644 index 0000000..b67ac23 --- /dev/null +++ b/QuizTrain.xcodeproj/xcshareddata/xcschemes/QuizTrain-macOS.xcscheme @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QuizTrain.xcodeproj/xcshareddata/xcschemes/QuizTrain-tvOS.xcscheme b/QuizTrain.xcodeproj/xcshareddata/xcschemes/QuizTrain-tvOS.xcscheme new file mode 100644 index 0000000..af89266 --- /dev/null +++ b/QuizTrain.xcodeproj/xcshareddata/xcschemes/QuizTrain-tvOS.xcscheme @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QuizTrain.xcodeproj/xcshareddata/xcschemes/QuizTrain-watchOS.xcscheme b/QuizTrain.xcodeproj/xcshareddata/xcschemes/QuizTrain-watchOS.xcscheme new file mode 100644 index 0000000..9f94c10 --- /dev/null +++ b/QuizTrain.xcodeproj/xcshareddata/xcschemes/QuizTrain-watchOS.xcscheme @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QuizTrain.xcodeproj/xcshareddata/xcschemes/QuizTrainTests-iOS.xcscheme b/QuizTrain.xcodeproj/xcshareddata/xcschemes/QuizTrainTests-iOS.xcscheme new file mode 100644 index 0000000..e1ee477 --- /dev/null +++ b/QuizTrain.xcodeproj/xcshareddata/xcschemes/QuizTrainTests-iOS.xcscheme @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QuizTrain.xcodeproj/xcshareddata/xcschemes/QuizTrainTests-macOS.xcscheme b/QuizTrain.xcodeproj/xcshareddata/xcschemes/QuizTrainTests-macOS.xcscheme new file mode 100644 index 0000000..5e4b8d9 --- /dev/null +++ b/QuizTrain.xcodeproj/xcshareddata/xcschemes/QuizTrainTests-macOS.xcscheme @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QuizTrain.xcodeproj/xcshareddata/xcschemes/QuizTrainTests-tvOS.xcscheme b/QuizTrain.xcodeproj/xcshareddata/xcschemes/QuizTrainTests-tvOS.xcscheme new file mode 100644 index 0000000..f6e6896 --- /dev/null +++ b/QuizTrain.xcodeproj/xcshareddata/xcschemes/QuizTrainTests-tvOS.xcscheme @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QuizTrain/.swiftlint.yml b/QuizTrain/.swiftlint.yml new file mode 100644 index 0000000..e69de29 diff --git a/QuizTrain/Info.plist b/QuizTrain/Info.plist new file mode 100644 index 0000000..4c0d218 --- /dev/null +++ b/QuizTrain/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/QuizTrain/Misc/Add/AddRequestJSON.swift b/QuizTrain/Misc/Add/AddRequestJSON.swift new file mode 100644 index 0000000..3365d6d --- /dev/null +++ b/QuizTrain/Misc/Add/AddRequestJSON.swift @@ -0,0 +1,16 @@ +/* + Returns JSON for an add request. + */ +protocol AddRequestJSON { + var addRequestJSON: JSONDictionary { get } +} + +extension AddRequestJSON where Self: JSONSerializable & AddRequestJSONKeys { + + var addRequestJSON: JSONDictionary { + var json = serialized() + json = json.filter { pair in addRequestJSONKeys.contains(pair.key) } + return json + } + +} diff --git a/QuizTrain/Misc/Add/AddRequestJSONKeys.swift b/QuizTrain/Misc/Add/AddRequestJSONKeys.swift new file mode 100644 index 0000000..865c340 --- /dev/null +++ b/QuizTrain/Misc/Add/AddRequestJSONKeys.swift @@ -0,0 +1,7 @@ +/* + Provides JSONKey's for all properties which can be submitted in an add request + to the API. + */ +protocol AddRequestJSONKeys { + var addRequestJSONKeys: [JSONKey] { get } +} diff --git a/QuizTrain/Misc/Containment/Containers/CustomFieldsContainer.swift b/QuizTrain/Misc/Containment/Containers/CustomFieldsContainer.swift new file mode 100644 index 0000000..1c9976c --- /dev/null +++ b/QuizTrain/Misc/Containment/Containers/CustomFieldsContainer.swift @@ -0,0 +1,67 @@ +/* + Provides a container for TestRail custom fields which can be added to some + objects. Enforces all custom fields to be prefixed with "custom_" (case + sensitive) in their key. Prevents adding any "custom_*" entries matching + omittedKeys. Any keys violating those rules will be silently omitted from being + added to customFields. + */ +struct CustomFieldsContainer: JSONDeserializable, JSONSerializable { + + // MARK: - Properties + + fileprivate var container: JSONDictionaryContainer + + public var customFields: JSONDictionary { + get { + return container.json + } + set { + container.json = CustomFieldsContainer.filter(newValue, requiringKeyPrefix: CustomFieldsContainer.requiredKeyPrefix, omittingKeys: self.omittedKeys) + } + } + + // MARK: - JSONDeserializable + + init(json: JSONDictionary) { + self.init(json: json, omittingKeys: []) + } + + init(json: JSONDictionary, omittingKeys omittedKeys: [JSONKey]) { + self.omittedKeys = omittedKeys + container = JSONDictionaryContainer(json: CustomFieldsContainer.filter(json, requiringKeyPrefix: CustomFieldsContainer.requiredKeyPrefix, omittingKeys: omittedKeys)) + } + + // MARK: - JSONSerializable + + func serialized() -> JSONDictionary { + return container.json + } + + // MARK: - Filtering + + public let omittedKeys: [JSONKey] // Keys which are omitted from customFields. This should contain any strongly-typed properties also prefixed with requiredKeyPrefix. + private static let requiredKeyPrefix: JSONKey = "custom_" // All TestRail custom fields are prefixed with: "custom_" + + private static func filter(_ json: JSONDictionary, requiringKeyPrefix requiredKeyPrefix: String, omittingKeys omittedKeys: [JSONKey]) -> JSONDictionary { + var jsonFiltered = json.filter { pair in pair.key.hasPrefix(requiredKeyPrefix) } + jsonFiltered = jsonFiltered.filter { pair in !omittedKeys.contains(pair.key) } + return jsonFiltered + } + +} + +extension CustomFieldsContainer: Equatable { + + static func==(lhs: CustomFieldsContainer, rhs: CustomFieldsContainer) -> Bool { + return lhs.container == rhs.container + } + +} + +extension CustomFieldsContainer { + + public static func empty() -> CustomFieldsContainer { + return CustomFieldsContainer(json: [:]) + } + +} diff --git a/QuizTrain/Misc/Containment/Containers/ErrorContainer.swift b/QuizTrain/Misc/Containment/Containers/ErrorContainer.swift new file mode 100644 index 0000000..d45fa96 --- /dev/null +++ b/QuizTrain/Misc/Containment/Containers/ErrorContainer.swift @@ -0,0 +1,44 @@ +/* + Provides a container conforming to Error which stores 1+ items also conforming + to Error. Useful in situations where multiple errors can occur. + */ +public struct ErrorContainer : Error { + + public let errors: [ErrorType] + + public init(_ error: ErrorType) { + self.errors = [error] + } + + public init?(_ errors: [ErrorType]) { + guard errors.count > 0 else { + return nil + } + self.errors = errors + } + +} + +extension ErrorContainer: DebugDescription { + + var debugDescription: String { + return "\(errors)" + } + +} + +extension ErrorContainer where ErrorType: DebugDescription { + + var debugDescription: String { + var description = "" + for error in errors { + if description.count == 0 { + description += error.debugDescription + } else { + description += "\n\n\(error.debugDescription)" + } + } + return description + } + +} diff --git a/QuizTrain/Misc/Containment/Containers/JSONDictionaryContainer.swift b/QuizTrain/Misc/Containment/Containers/JSONDictionaryContainer.swift new file mode 100644 index 0000000..9ee68f6 --- /dev/null +++ b/QuizTrain/Misc/Containment/Containers/JSONDictionaryContainer.swift @@ -0,0 +1,35 @@ +import Foundation + +/* + Provides a container to store arbitrary JSON dictionaries. + */ +struct JSONDictionaryContainer: JSONDeserializable, JSONSerializable { + + var json: JSONDictionary + + init(json: JSONDictionary) { + self.json = json + } + + func serialized() -> JSONDictionary { + return json + } + +} + +extension JSONDictionaryContainer: Equatable { + + static func==(lhs: JSONDictionaryContainer, rhs: JSONDictionaryContainer) -> Bool { + + let lhsSerialized = lhs.serialized() + let rhsSerialized = rhs.serialized() + + // Attempt to cast to NSDictionary to rely on its isEqual code. + if lhsSerialized as NSDictionary == rhsSerialized as NSDictionary { + return true + } + + return false + } + +} diff --git a/QuizTrain/Misc/Containment/Protocols/CustomFields.swift b/QuizTrain/Misc/Containment/Protocols/CustomFields.swift new file mode 100644 index 0000000..40a2c5d --- /dev/null +++ b/QuizTrain/Misc/Containment/Protocols/CustomFields.swift @@ -0,0 +1,13 @@ +/* + Provides read-only CustomField support. + */ +protocol CustomFields { + var customFields: JSONDictionary { get } + var customFieldsContainer: CustomFieldsContainer { get } +} + +extension CustomFields { + public var customFields: JSONDictionary { + return self.customFieldsContainer.customFields + } +} diff --git a/QuizTrain/Misc/Containment/Protocols/MutableCustomFields.swift b/QuizTrain/Misc/Containment/Protocols/MutableCustomFields.swift new file mode 100644 index 0000000..b338b1c --- /dev/null +++ b/QuizTrain/Misc/Containment/Protocols/MutableCustomFields.swift @@ -0,0 +1,18 @@ +/* + Provides read-write CustomField support. + */ +protocol MutableCustomFields: CustomFields { + var customFields: JSONDictionary { get set } + var customFieldsContainer: CustomFieldsContainer { get set } +} + +extension MutableCustomFields { + public var customFields: JSONDictionary { + get { + return self.customFieldsContainer.customFields + } + set { + customFieldsContainer.customFields = newValue + } + } +} diff --git a/QuizTrain/Misc/Debug/DebugDescription.swift b/QuizTrain/Misc/Debug/DebugDescription.swift new file mode 100644 index 0000000..40ad2ab --- /dev/null +++ b/QuizTrain/Misc/Debug/DebugDescription.swift @@ -0,0 +1,6 @@ +/* + Provides a property to return a description for use in debugging. + */ +protocol DebugDescription { + var debugDescription: String { get } +} diff --git a/QuizTrain/Misc/Debug/DebugDetails.swift b/QuizTrain/Misc/Debug/DebugDetails.swift new file mode 100644 index 0000000..724557e --- /dev/null +++ b/QuizTrain/Misc/Debug/DebugDetails.swift @@ -0,0 +1,6 @@ +/* + Provides a property to return a details string for use in debugging. + */ +protocol DebugDetails { + var debugDetails: String { get } +} diff --git a/QuizTrain/Misc/Errors/MultipleMatchError.swift b/QuizTrain/Misc/Errors/MultipleMatchError.swift new file mode 100644 index 0000000..3e120ce --- /dev/null +++ b/QuizTrain/Misc/Errors/MultipleMatchError.swift @@ -0,0 +1,34 @@ +public enum MultipleMatchError: Error { + case noMatchesFound(missing: Set) + case partialMatchesFound(matches: [MatchType], missing: Set) +} + +extension MultipleMatchError: DebugDescription { + + var debugDescription: String { + var description = "ObjectAPI.MultipleMatchError" + switch self { + case .noMatchesFound: + description += ".noMatchesFound:\n\n\(debugDetails)\n" + case .partialMatchesFound(_): + description += ".partialMatchesFound:\n\n\(debugDetails)\n" + } + return description + } + +} + +extension MultipleMatchError: DebugDetails { + + var debugDetails: String { + let details: String + switch self { + case .noMatchesFound: + details = "Zero matches were found." + case .partialMatchesFound(let matches): + details = "\(matches)" + } + return details + } + +} diff --git a/QuizTrain/Misc/Errors/SingleMatchError.swift b/QuizTrain/Misc/Errors/SingleMatchError.swift new file mode 100644 index 0000000..97728ad --- /dev/null +++ b/QuizTrain/Misc/Errors/SingleMatchError.swift @@ -0,0 +1,29 @@ +public enum SingleMatchError: Error { + case noMatchFound(missing: QueryType) +} + +extension SingleMatchError: DebugDescription { + + var debugDescription: String { + var description = "ObjectAPI.SingleMatchError" + switch self { + case .noMatchFound: + description += ".noMatchFound:\n\n\(debugDetails)\n" + } + return description + } + +} + +extension SingleMatchError: DebugDetails { + + var debugDetails: String { + let details: String + switch self { + case .noMatchFound: + details = "No match was found." + } + return details + } + +} diff --git a/QuizTrain/Misc/Extensions/Array+ContentComparison.swift b/QuizTrain/Misc/Extensions/Array+ContentComparison.swift new file mode 100644 index 0000000..8fd4ab8 --- /dev/null +++ b/QuizTrain/Misc/Extensions/Array+ContentComparison.swift @@ -0,0 +1,47 @@ +extension Array where Array.Element: Equatable { + + /* + For an array of Equatable elements, determines if both contain the same + contents regardless of their ordering. + + let a1 = [1, 2, 2] + let a2 = [2, 2, 1] + let a3 = [1, 1, 2] + + a1 == a2 // false + Array.contentsAreEqual(a1, a2) // true + Array.contentsAreEqual(a1, a3) // false + */ + static func contentsAreEqual(_ lhs: [Array.Element]?, _ rhs: [Array.Element]?) -> Bool { + switch (lhs, rhs) { + case (.some(let l), .some(let r)): + return Array.contentsAreEqual(l, r) + case (.none, .none): + return true + default: + return false + } + } + + private static func contentsAreEqual(_ lhs: [Array.Element], _ rhs: [Array.Element]) -> Bool { + + guard lhs.count == rhs.count else { + return false + } + + var rhsCopy = rhs + for item in lhs { + guard let index = rhsCopy.index(of: item) else { + return false + } + rhsCopy.remove(at: index) + } + + return true + } + + func contentsAreEqual(to array: [Array.Element]?) -> Bool { + return Array.contentsAreEqual(self, array) + } + +} diff --git a/QuizTrain/Misc/Extensions/Date+Seconds.swift b/QuizTrain/Misc/Extensions/Date+Seconds.swift new file mode 100644 index 0000000..02367e3 --- /dev/null +++ b/QuizTrain/Misc/Extensions/Date+Seconds.swift @@ -0,0 +1,17 @@ +import Foundation + +/* + Convinience methods for TestRail dates. TestRail dates are always a Unix + timestamp as a whole number. + */ +extension Date { + + init(secondsSince1970 seconds: Int) { + self.init(timeIntervalSince1970: TimeInterval(seconds)) + } + + var secondsSince1970: Int { + return Int(timeIntervalSince1970) + } + +} diff --git a/QuizTrain/Misc/Extensions/Equatable+OptionalArray.swift b/QuizTrain/Misc/Extensions/Equatable+OptionalArray.swift new file mode 100644 index 0000000..5d81b9a --- /dev/null +++ b/QuizTrain/Misc/Extensions/Equatable+OptionalArray.swift @@ -0,0 +1,13 @@ +/* + Adds == comparison to optional arrays containing an Equatable type. + */ +func ==(lhs: [Type]?, rhs: [Type]?) -> Bool { + switch (lhs, rhs) { + case (.some(let l), .some(let r)): + return l == r + case (.none, .none): + return true + default: + return false + } +} diff --git a/QuizTrain/Misc/Identity/Identifiable.swift b/QuizTrain/Misc/Identity/Identifiable.swift new file mode 100644 index 0000000..8bc4951 --- /dev/null +++ b/QuizTrain/Misc/Identity/Identifiable.swift @@ -0,0 +1,8 @@ +/* + Provides a way to identify an object. The Id type conforms to Hashable allowing + for comparison and use in sets. + */ +protocol Identifiable { + associatedtype Id: Hashable + var id: Id { get } +} diff --git a/QuizTrain/Misc/JSON/JSONDeserializable.swift b/QuizTrain/Misc/JSON/JSONDeserializable.swift new file mode 100644 index 0000000..8a7b5df --- /dev/null +++ b/QuizTrain/Misc/JSON/JSONDeserializable.swift @@ -0,0 +1,35 @@ +/* + Provides methods to deserialize JSON into an object or objects. + */ +protocol JSONDeserializable { + static func deserialized(_ json: [JSONDictionary]) -> [ObjectType]? + static func deserialized(_ json: JSONDictionary) -> ObjectType? + init?(json: JSONDictionary) +} + +extension JSONDeserializable { + + /* + Initializes and returns multiple objects of ObjectType which conform to + JSONDeserializable. + */ + static func deserialized(_ json: [JSONDictionary]) -> [ObjectType]? { + var objects = [ObjectType]() + for item in json { + guard let object: ObjectType = deserialized(item) else { + return nil + } + objects.append(object) + } + return objects + } + + /* + Initializes and returns a single object of ObjectType which conforms to + JSONDeserializable. + */ + static func deserialized(_ json: JSONDictionary) -> ObjectType? { + return ObjectType(json: json) + } + +} diff --git a/QuizTrain/Misc/JSON/JSONDictionary.swift b/QuizTrain/Misc/JSON/JSONDictionary.swift new file mode 100644 index 0000000..99e190d --- /dev/null +++ b/QuizTrain/Misc/JSON/JSONDictionary.swift @@ -0,0 +1 @@ +public typealias JSONDictionary = [JSONKey: Any] diff --git a/QuizTrain/Misc/JSON/JSONKey.swift b/QuizTrain/Misc/JSON/JSONKey.swift new file mode 100644 index 0000000..13a310f --- /dev/null +++ b/QuizTrain/Misc/JSON/JSONKey.swift @@ -0,0 +1 @@ +public typealias JSONKey = String diff --git a/QuizTrain/Misc/JSON/JSONSerializable.swift b/QuizTrain/Misc/JSON/JSONSerializable.swift new file mode 100644 index 0000000..a1404e9 --- /dev/null +++ b/QuizTrain/Misc/JSON/JSONSerializable.swift @@ -0,0 +1,30 @@ +/* + Provides methods to serialize an object or objects into JSON. + */ +protocol JSONSerializable { + static func serialized(_ objects: [ObjectType]) -> [JSONDictionary] + static func serialized(_ object: ObjectType) -> JSONDictionary + func serialized() -> JSONDictionary +} + +extension JSONSerializable { + + /* + Serializes an array of objects into JSONDictionary's. + */ + static func serialized(_ objects: [ObjectType]) -> [JSONDictionary] { + var jsonArray: [JSONDictionary] = [] + for object in objects { + jsonArray.append(ObjectType.serialized(object)) + } + return jsonArray + } + + /* + Serializes an object into a JSONDictionary. + */ + static func serialized(_ object: ObjectType) -> JSONDictionary { + return object.serialized() + } + +} diff --git a/QuizTrain/Misc/Operations/AsyncOperation.swift b/QuizTrain/Misc/Operations/AsyncOperation.swift new file mode 100644 index 0000000..b501e43 --- /dev/null +++ b/QuizTrain/Misc/Operations/AsyncOperation.swift @@ -0,0 +1,38 @@ +import Foundation + +/* + Asynchronous subclass of Operation providing state tracking with KVO. Subclass + this class to implement custom asynchronous operations. + */ +class AsyncOperation: Operation { + + enum State: String { + case ready = "isReady" + case executing = "isExecuting" + case finished = "isFinished" + } + + private var _state = State.ready + private let stateLock = NSLock() // atomic lock + var state: State { + get { + stateLock.lock() + let value = _state + stateLock.unlock() + return value + } + set { + willChangeValue(forKey: newValue.rawValue) + stateLock.lock() + _state = newValue + stateLock.unlock() + didChangeValue(forKey: newValue.rawValue) + } + } + + override var isAsynchronous: Bool { return true } + override var isReady: Bool { return state == .ready } + override var isExecuting: Bool { return state == .executing } + override var isFinished: Bool { return state == .finished } + +} diff --git a/QuizTrain/Misc/Outcome.swift b/QuizTrain/Misc/Outcome.swift new file mode 100644 index 0000000..36b0db8 --- /dev/null +++ b/QuizTrain/Misc/Outcome.swift @@ -0,0 +1,9 @@ +/* + Container for outcomes. For a .succeeded outcome which does not pass an object + ensure the Succeeded generic is an optional (e.g. Void?) and use + Outcome.succeeded(nil). + */ +public enum Outcome { + case succeeded(Succeeded) + case failed(Failed) +} diff --git a/QuizTrain/Misc/QueryItemProvider.swift b/QuizTrain/Misc/QueryItemProvider.swift new file mode 100644 index 0000000..2f2a907 --- /dev/null +++ b/QuizTrain/Misc/QueryItemProvider.swift @@ -0,0 +1,21 @@ +/* + Conforming objects can return a URLQueryItem representation of themselves. + */ +public protocol QueryItemProvider { + var queryItem: URLQueryItem { get } + static func queryItems(for providers: [QueryItemProvider]?) -> [URLQueryItem] +} + +extension QueryItemProvider { + + public static func queryItems(for providers: [QueryItemProvider]?) -> [URLQueryItem] { + var queryItems = [URLQueryItem]() + if let providers = providers { + for provider in providers { + queryItems.append(provider.queryItem) + } + } + return queryItems + } + +} diff --git a/QuizTrain/Misc/UniqueSelection.swift b/QuizTrain/Misc/UniqueSelection.swift new file mode 100644 index 0000000..1fd1aa4 --- /dev/null +++ b/QuizTrain/Misc/UniqueSelection.swift @@ -0,0 +1,36 @@ +/* + Provides unique selection state. + */ +enum UniqueSelection { + case all // Include everything. + case some(Set) // Include only what's specified. + case none // Include nothing. +} + +extension UniqueSelection: Equatable { + + public static func==(lhs: UniqueSelection, rhs: UniqueSelection) -> Bool { + switch lhs { + case .all: + switch rhs { + case .all: + return true + default: + return false + } + case .some(let lhsProjectIds): + guard case let .some(rhsProjectIds) = rhs else { + return false + } + return lhsProjectIds == rhsProjectIds + case .none: + switch rhs { + case .none: + return true + default: + return false + } + } + } + +} diff --git a/QuizTrain/Misc/Update/UpdateRequestJSON.swift b/QuizTrain/Misc/Update/UpdateRequestJSON.swift new file mode 100644 index 0000000..a1c056b --- /dev/null +++ b/QuizTrain/Misc/Update/UpdateRequestJSON.swift @@ -0,0 +1,16 @@ +/* + Returns JSON for an update request. + */ +protocol UpdateRequestJSON { + var updateRequestJSON: JSONDictionary { get } +} + +extension UpdateRequestJSON where Self: JSONSerializable & UpdateRequestJSONKeys { + + var updateRequestJSON: JSONDictionary { + var json = serialized() + json = json.filter { pair in updateRequestJSONKeys.contains(pair.key) } + return json + } + +} diff --git a/QuizTrain/Misc/Update/UpdateRequestJSONKeys.swift b/QuizTrain/Misc/Update/UpdateRequestJSONKeys.swift new file mode 100644 index 0000000..d8d19df --- /dev/null +++ b/QuizTrain/Misc/Update/UpdateRequestJSONKeys.swift @@ -0,0 +1,7 @@ +/* + Provides JSONKey's for all properties which can be submitted in an update + request to the API. + */ +protocol UpdateRequestJSONKeys { + var updateRequestJSONKeys: [JSONKey] { get } +} diff --git a/QuizTrain/Misc/Validation/Validatable.swift b/QuizTrain/Misc/Validation/Validatable.swift new file mode 100644 index 0000000..3bf9dc5 --- /dev/null +++ b/QuizTrain/Misc/Validation/Validatable.swift @@ -0,0 +1,7 @@ +/* + Provides a way to determine if something is in a valid state. It is up to the + conformer to determine what is valid or not. + */ +protocol Validatable { + var isValid: Bool { get } +} diff --git a/QuizTrain/Models/Case.swift b/QuizTrain/Models/Case.swift new file mode 100644 index 0000000..040f319 --- /dev/null +++ b/QuizTrain/Models/Case.swift @@ -0,0 +1,188 @@ +public struct Case: Identifiable, MutableCustomFields { + public typealias Id = Int + public let createdBy: User.Id + public let createdOn: Date + public var estimate: String? + public let estimateForecast: String? + public let id: Id + public var milestoneId: Milestone.Id? + public var priorityId: Priority.Id + public var refs: String? + public let sectionId: Section.Id? + public let suiteId: Suite.Id? + public var templateId: Template.Id + public var title: String + public var typeId: CaseType.Id + public let updatedBy: User.Id + public let updatedOn: Date + var customFieldsContainer: CustomFieldsContainer +} + +// MARK: - Foward Relationships (ObjectAPI) + +extension Case { + + public func createdBy(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.createdBy(self, completionHandler: completionHandler) + } + + public func milestone(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.milestone(self, completionHandler: completionHandler) + } + + public func priority(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome, ObjectAPI.GetError>>) -> Void) { + objectAPI.priority(self, completionHandler: completionHandler) + } + + public func section(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.section(self, completionHandler: completionHandler) + } + + public func suite(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.suite(self, completionHandler: completionHandler) + } + + public func template(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome, ErrorContainer>>) -> Void) { + objectAPI.template(self, completionHandler: completionHandler) + } + + public func type(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome, ObjectAPI.GetError>>) -> Void) { + objectAPI.type(self, completionHandler: completionHandler) + } + + public func updatedBy(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.updatedBy(self, completionHandler: completionHandler) + } + +} + +// MARK: - Equatable + +extension Case: Equatable { + + public static func==(lhs: Case, rhs: Case) -> Bool { + return (lhs.createdBy == rhs.createdBy && + lhs.createdOn.secondsSince1970 == rhs.createdOn.secondsSince1970 && + lhs.estimate == rhs.estimate && + lhs.estimateForecast == rhs.estimateForecast && + lhs.id == rhs.id && + lhs.milestoneId == rhs.milestoneId && + lhs.priorityId == rhs.priorityId && + lhs.refs == rhs.refs && + lhs.sectionId == rhs.sectionId && + lhs.suiteId == rhs.suiteId && + lhs.templateId == rhs.templateId && + lhs.title == rhs.title && + lhs.typeId == rhs.typeId && + lhs.updatedBy == rhs.updatedBy && + lhs.updatedOn.secondsSince1970 == rhs.updatedOn.secondsSince1970 && + lhs.customFieldsContainer == rhs.customFieldsContainer) + } + +} + +// MARK: - JSON Keys + +extension Case { + + enum JSONKeys: JSONKey { + case createdBy = "created_by" + case createdOn = "created_on" + case estimate + case estimateForecast = "estimate_forecast" + case id + case milestoneId = "milestone_id" + case priorityId = "priority_id" + case refs + case sectionId = "section_id" + case suiteId = "suite_id" + case templateId = "template_id" + case title + case typeId = "type_id" + case updatedBy = "updated_by" + case updatedOn = "updated_on" + } + +} + +extension Case: UpdateRequestJSONKeys { + + var updateRequestJSONKeys: [JSONKey] { + var keys = [ + JSONKeys.estimate.rawValue, + JSONKeys.milestoneId.rawValue, + JSONKeys.priorityId.rawValue, + JSONKeys.refs.rawValue, + JSONKeys.templateId.rawValue, + JSONKeys.title.rawValue, + JSONKeys.typeId.rawValue + ] + keys += self.customFieldsContainer.customFields.keys + return keys + } + +} + +// MARK: - Serialization + +extension Case: JSONDeserializable { + + init?(json: JSONDictionary) { + + guard let createdBy = json[JSONKeys.createdBy.rawValue] as? User.Id, + let createdOnSeconds = json[JSONKeys.createdOn.rawValue] as? Int, + let id = json[JSONKeys.id.rawValue] as? Id, + let priorityId = json[JSONKeys.priorityId.rawValue] as? Priority.Id, + let templateId = json[JSONKeys.templateId.rawValue] as? Template.Id, + let title = json[JSONKeys.title.rawValue] as? String, + let typeId = json[JSONKeys.typeId.rawValue] as? CaseType.Id, + let updatedBy = json[JSONKeys.updatedBy.rawValue] as? User.Id, + let updatedOnSeconds = json[JSONKeys.updatedOn.rawValue] as? Int else { + return nil + } + let createdOn = Date(secondsSince1970: createdOnSeconds) + let updatedOn = Date(secondsSince1970: updatedOnSeconds) + + let estimate = json[JSONKeys.estimate.rawValue] as? String ?? nil + let estimateForecast = json[JSONKeys.estimateForecast.rawValue] as? String ?? nil + let milestoneId = json[JSONKeys.milestoneId.rawValue] as? Milestone.Id ?? nil + let refs = json[JSONKeys.refs.rawValue] as? String ?? nil + let sectionId = json[JSONKeys.sectionId.rawValue] as? Section.Id ?? nil + let suiteId = json[JSONKeys.suiteId.rawValue] as? Suite.Id ?? nil + + let customFieldsContainer = CustomFieldsContainer(json: json) + + self.init(createdBy: createdBy, createdOn: createdOn, estimate: estimate, estimateForecast: estimateForecast, id: id, milestoneId: milestoneId, priorityId: priorityId, refs: refs, sectionId: sectionId, suiteId: suiteId, templateId: templateId, title: title, typeId: typeId, updatedBy: updatedBy, updatedOn: updatedOn, customFieldsContainer: customFieldsContainer) + } + +} + +extension Case: JSONSerializable { + + private var serializedProperties: JSONDictionary { + return [JSONKeys.createdBy.rawValue: createdBy, + JSONKeys.createdOn.rawValue: createdOn.secondsSince1970, + JSONKeys.estimate.rawValue: estimate as Any, + JSONKeys.estimateForecast.rawValue: estimateForecast as Any, + JSONKeys.id.rawValue: id, + JSONKeys.milestoneId.rawValue: milestoneId as Any, + JSONKeys.priorityId.rawValue: priorityId, + JSONKeys.refs.rawValue: refs as Any, + JSONKeys.templateId.rawValue: templateId, + JSONKeys.title.rawValue: title, + JSONKeys.typeId.rawValue: typeId, + JSONKeys.sectionId.rawValue: sectionId as Any, + JSONKeys.suiteId.rawValue: suiteId as Any, + JSONKeys.updatedBy.rawValue: updatedBy, + JSONKeys.updatedOn.rawValue: updatedOn.secondsSince1970] + } + + func serialized() -> JSONDictionary { + var json = serializedProperties + customFields.forEach { item in json[item.key] = item.value } + return json + } + +} + +extension Case: UpdateRequestJSON { } diff --git a/QuizTrain/Models/CaseField.swift b/QuizTrain/Models/CaseField.swift new file mode 100644 index 0000000..871e26c --- /dev/null +++ b/QuizTrain/Models/CaseField.swift @@ -0,0 +1,113 @@ +public struct CaseField: Identifiable { + public typealias Id = Int + public let configs: [Config] + public let description: String? + public let displayOrder: Int + public let id: Id + public let includeAll: Bool + public let isActive: Bool + public let label: String + public let name: String + public let systemName: String + public let templateIds: [Template.Id] + public let typeId: CustomFieldType +} + +// MARK: - Foward Relationships (ObjectAPI) + +extension CaseField { + + public func templates(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome<[Template], ObjectAPI.MatchError, ErrorContainer>>) -> Void) { + objectAPI.templates(self, completionHandler: completionHandler) + } + +} + +// MARK: - Equatable + +extension CaseField: Equatable { + + public static func==(lhs: CaseField, rhs: CaseField) -> Bool { + return (lhs.configs.contentsAreEqual(to: rhs.configs) && + lhs.description == rhs.description && + lhs.displayOrder == rhs.displayOrder && + lhs.id == rhs.id && + lhs.includeAll == rhs.includeAll && + lhs.isActive == rhs.isActive && + lhs.label == rhs.label && + lhs.name == rhs.name && + lhs.systemName == rhs.systemName && + lhs.templateIds.sorted() == rhs.templateIds.sorted() && + lhs.typeId == rhs.typeId) + } + +} + +// MARK: - JSON Keys + +extension CaseField { + + enum JSONKeys: JSONKey { + case configs + case description + case displayOrder = "display_order" + case id + case includeAll = "include_all" + case isActive = "is_active" + case label + case name + case systemName = "system_name" + case templateIds = "template_ids" + case typeId = "type_id" + } + +} + +// MARK: - Serialization + +extension CaseField: JSONDeserializable { + + init?(json: JSONDictionary) { + + guard let configsJson = json[JSONKeys.configs.rawValue] as? [JSONDictionary], + let configs: [Config] = CaseField.deserialized(configsJson), + let displayOrder = json[JSONKeys.displayOrder.rawValue] as? Int, + let id = json[JSONKeys.id.rawValue] as? Id, + let includeAll = json[JSONKeys.includeAll.rawValue] as? Bool, + let isActive = json[JSONKeys.isActive.rawValue] as? Bool, + let label = json[JSONKeys.label.rawValue] as? String, + let name = json[JSONKeys.name.rawValue] as? String, + let systemName = json[JSONKeys.systemName.rawValue] as? String, + let templateIds = json[JSONKeys.templateIds.rawValue] as? [Template.Id], + let typeIdInt = json[JSONKeys.typeId.rawValue] as? Int, + let typeId = CustomFieldType(rawValue: typeIdInt) else { + return nil + } + + let description = json[JSONKeys.description.rawValue] as? String ?? nil + + self.init(configs: configs, description: description, displayOrder: displayOrder, id: id, includeAll: includeAll, isActive: isActive, label: label, name: name, systemName: systemName, templateIds: templateIds, typeId: typeId) + } + +} + +extension CaseField: JSONSerializable { + + func serialized() -> JSONDictionary { + + let configsSerialized: [JSONDictionary] = Config.serialized(configs) + + return [JSONKeys.configs.rawValue: configsSerialized, + JSONKeys.description.rawValue: description as Any, + JSONKeys.displayOrder.rawValue: displayOrder, + JSONKeys.id.rawValue: id, + JSONKeys.includeAll.rawValue: includeAll, + JSONKeys.isActive.rawValue: isActive, + JSONKeys.label.rawValue: label, + JSONKeys.name.rawValue: name, + JSONKeys.systemName.rawValue: systemName, + JSONKeys.templateIds.rawValue: templateIds, + JSONKeys.typeId.rawValue: typeId.rawValue] + } + +} diff --git a/QuizTrain/Models/CaseType.swift b/QuizTrain/Models/CaseType.swift new file mode 100644 index 0000000..572080b --- /dev/null +++ b/QuizTrain/Models/CaseType.swift @@ -0,0 +1,57 @@ +public struct CaseType: Identifiable { + public typealias Id = Int + public let id: Id + public let isDefault: Bool + public let name: String +} + +// MARK: - Equatable + +extension CaseType: Equatable { + + public static func==(lhs: CaseType, rhs: CaseType) -> Bool { + return (lhs.id == rhs.id && + lhs.isDefault == rhs.isDefault && + lhs.name == rhs.name) + } + +} + +// MARK: - JSON Keys + +extension CaseType { + + enum JSONKeys: JSONKey { + case id + case isDefault = "is_default" + case name + } + +} + +// MARK: - Serialization + +extension CaseType: JSONDeserializable { + + init?(json: JSONDictionary) { + + guard let id = json[JSONKeys.id.rawValue] as? Id, + let isDefault = json[JSONKeys.isDefault.rawValue] as? Bool, + let name = json[JSONKeys.name.rawValue] as? String else { + return nil + } + + self.init(id: id, isDefault: isDefault, name: name) + } + +} + +extension CaseType: JSONSerializable { + + func serialized() -> JSONDictionary { + return [JSONKeys.id.rawValue: id, + JSONKeys.isDefault.rawValue: isDefault, + JSONKeys.name.rawValue: name] + } + +} diff --git a/QuizTrain/Models/Config/Config.Context.swift b/QuizTrain/Models/Config/Config.Context.swift new file mode 100644 index 0000000..89a7e3e --- /dev/null +++ b/QuizTrain/Models/Config/Config.Context.swift @@ -0,0 +1,56 @@ +extension Config { + + public struct Context { + public let isGlobal: Bool // True indicates all projects. + public let projectIds: [Project.Id]? // Applies only if isGlobal is false. Can include projectIds for projects you do not have at least Read-only access to. + } + +} + +// MARK: - Equatable + +extension Config.Context: Equatable { + + public static func==(lhs: Config.Context, rhs: Config.Context) -> Bool { + return (lhs.isGlobal == rhs.isGlobal && + lhs.projectIds?.sorted() == rhs.projectIds?.sorted()) + } + +} + +// MARK: - JSON Keys + +extension Config.Context { + + enum JSONKeys: JSONKey { + case isGlobal = "is_global" + case projectIds = "project_ids" + } + +} + +// MARK: - Serialization + +extension Config.Context: JSONDeserializable { + + init?(json: JSONDictionary) { + + guard let isGlobal = json[JSONKeys.isGlobal.rawValue] as? Bool else { + return nil + } + + let projectIds = json[JSONKeys.projectIds.rawValue] as? [Project.Id] ?? nil + + self.init(isGlobal: isGlobal, projectIds: projectIds) + } + +} + +extension Config.Context: JSONSerializable { + + func serialized() -> JSONDictionary { + return [JSONKeys.isGlobal.rawValue: isGlobal, + JSONKeys.projectIds.rawValue: projectIds as Any] + } + +} diff --git a/QuizTrain/Models/Config/Config.swift b/QuizTrain/Models/Config/Config.swift new file mode 100644 index 0000000..06f46d3 --- /dev/null +++ b/QuizTrain/Models/Config/Config.swift @@ -0,0 +1,91 @@ +public struct Config: Identifiable { + public typealias Id = String + typealias OptionsContainer = JSONDictionaryContainer + public let context: Config.Context + public let id: Id + public var options: [String: Any] { return optionsContainer.json } + let optionsContainer: OptionsContainer +} + +// MARK: - Foward Relationships (ObjectAPI) + +extension Config { + + public func accessibleProjects(_ objectAPI: ObjectAPI, completionHandler: @escaping(Outcome<[Project]?, ErrorContainer>) -> Void) { + objectAPI.accessibleProjects(self, completionHandler: completionHandler) + } + + public func projects(_ objectAPI: ObjectAPI, completionHandler: @escaping(Outcome<[Project]?, ObjectAPI.MatchError, ErrorContainer>>) -> Void) { + objectAPI.projects(self, completionHandler: completionHandler) + } + +} + +// MARK: - Equatable + +extension Config: Equatable { + + public static func==(lhs: Config, rhs: Config) -> Bool { + return (lhs.context == rhs.context && + lhs.id == rhs.id && + lhs.optionsContainer == rhs.optionsContainer) + } + +} + +// MARK: - JSON Keys + +extension Config { + + enum JSONKeys: JSONKey { + case context + case id + case options + } + +} + +// MARK: - Serialization + +extension Config: JSONDeserializable { + + init?(json: JSONDictionary) { + + guard let contextJson = json[JSONKeys.context.rawValue] as? JSONDictionary, + let context: Config.Context = Config.Context.deserialized(contextJson), + let id = json[JSONKeys.id.rawValue] as? Id, + let options = json[JSONKeys.options.rawValue] as? [String: Any] else { + return nil + } + + let optionsContainer = OptionsContainer(json: options) + + self.init(context: context, id: id, optionsContainer: optionsContainer) + } + +} + +extension Config: JSONSerializable { + + func serialized() -> JSONDictionary { + return [JSONKeys.context.rawValue: context.serialized(), + JSONKeys.id.rawValue: id, + JSONKeys.options.rawValue: options] + } + +} + +// MARK: - ProjectSelection + +extension Config { + + var projects: UniqueSelection { + if context.isGlobal == true { + return .all + } else if let projectIds = context.projectIds { + return .some(Set(projectIds)) + } + return .none + } + +} diff --git a/QuizTrain/Models/Configuration.swift b/QuizTrain/Models/Configuration.swift new file mode 100644 index 0000000..3a10505 --- /dev/null +++ b/QuizTrain/Models/Configuration.swift @@ -0,0 +1,79 @@ +public struct Configuration: Identifiable { + public typealias Id = Int + public let id: Id + public let groupId: ConfigurationGroup.Id + public var name: String +} + +// MARK: - Foward Relationships (ObjectAPI) + +extension Configuration { + + public func configurationGroup(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome, ErrorContainer>>) -> Void) { + objectAPI.configurationGroup(self, completionHandler: completionHandler) + } + +} + +// MARK: - Equatable + +extension Configuration: Equatable { + + public static func==(lhs: Configuration, rhs: Configuration) -> Bool { + return (lhs.id == rhs.id && + lhs.groupId == rhs.groupId && + lhs.name == rhs.name) + } + +} + +// MARK: - JSON Keys + +extension Configuration { + + enum JSONKeys: JSONKey { + case id + case groupId = "group_id" + case name + } + +} + +extension Configuration: UpdateRequestJSONKeys { + + var updateRequestJSONKeys: [JSONKey] { + return [ + JSONKeys.name.rawValue + ] + } + +} + +// MARK: - Serialization + +extension Configuration: JSONDeserializable { + + init?(json: JSONDictionary) { + + guard let id = json[JSONKeys.id.rawValue] as? Id, + let groupId = json[JSONKeys.groupId.rawValue] as? ConfigurationGroup.Id, + let name = json[JSONKeys.name.rawValue] as? String else { + return nil + } + + self.init(id: id, groupId: groupId, name: name) + } + +} + +extension Configuration: JSONSerializable { + + func serialized() -> JSONDictionary { + return [JSONKeys.id.rawValue: id, + JSONKeys.groupId.rawValue: groupId, + JSONKeys.name.rawValue: name] + } + +} + +extension Configuration: UpdateRequestJSON { } diff --git a/QuizTrain/Models/ConfigurationGroup.swift b/QuizTrain/Models/ConfigurationGroup.swift new file mode 100644 index 0000000..2e146fa --- /dev/null +++ b/QuizTrain/Models/ConfigurationGroup.swift @@ -0,0 +1,88 @@ +public struct ConfigurationGroup: Identifiable { + public typealias Id = Int + public let configs: [Configuration] + public let id: Id + public var name: String + public let projectId: Project.Id +} + +// MARK: - Foward Relationships (ObjectAPI) + +extension ConfigurationGroup { + + public func project(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.project(self, completionHandler: completionHandler) + } + +} + +// MARK: - Equatable + +extension ConfigurationGroup: Equatable { + + public static func==(lhs: ConfigurationGroup, rhs: ConfigurationGroup) -> Bool { + return (lhs.configs.contentsAreEqual(to: rhs.configs) && + lhs.id == rhs.id && + lhs.name == rhs.name && + lhs.projectId == rhs.projectId) + } + +} + +// MARK: - JSON Keys + +extension ConfigurationGroup { + + enum JSONKeys: JSONKey { + case configs + case id + case name + case projectId = "project_id" + } + +} + +extension ConfigurationGroup: UpdateRequestJSONKeys { + + var updateRequestJSONKeys: [JSONKey] { + return [ + JSONKeys.name.rawValue + ] + } + +} + +// MARK: - Serialization + +extension ConfigurationGroup: JSONDeserializable { + + init?(json: JSONDictionary) { + + guard let configsJson = json[JSONKeys.configs.rawValue] as? [JSONDictionary], + let configs: [Configuration] = ConfigurationGroup.deserialized(configsJson), + let id = json[JSONKeys.id.rawValue] as? Id, + let name = json[JSONKeys.name.rawValue] as? String, + let projectId = json[JSONKeys.projectId.rawValue] as? Project.Id else { + return nil + } + + self.init(configs: configs, id: id, name: name, projectId: projectId) + } + +} + +extension ConfigurationGroup: JSONSerializable { + + func serialized() -> JSONDictionary { + + let configsSerialized: [JSONDictionary] = Configuration.serialized(configs) + + return [JSONKeys.configs.rawValue: configsSerialized, + JSONKeys.id.rawValue: id, + JSONKeys.name.rawValue: name, + JSONKeys.projectId.rawValue: projectId] + } + +} + +extension ConfigurationGroup: UpdateRequestJSON { } diff --git a/QuizTrain/Models/Milestone.swift b/QuizTrain/Models/Milestone.swift new file mode 100644 index 0000000..f28b025 --- /dev/null +++ b/QuizTrain/Models/Milestone.swift @@ -0,0 +1,178 @@ +public struct Milestone: Identifiable { + public typealias Id = Int + public let completedOn: Date? + public var description: String? + public var dueOn: Date? + public let id: Id + public var isCompleted: Bool + public var isStarted: Bool + public let milestones: [Milestone]? // Certain API calls will always return nil for this value. See TestRail API documentation for details. + public var name: String + public var parentId: Id? + public let projectId: Project.Id + public var startOn: Date? + public let startedOn: Date? + public let url: URL +} + +// MARK: - Foward Relationships (ObjectAPI) + +extension Milestone { + + public func parent(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.parent(self, completionHandler: completionHandler) + } + + public func project(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.project(self, completionHandler: completionHandler) + } + +} + +// MARK: - Equatable + +extension Milestone: Equatable { + + public static func==(lhs: Milestone, rhs: Milestone) -> Bool { + return (lhs.completedOn?.secondsSince1970 == rhs.completedOn?.secondsSince1970 && + lhs.description == rhs.description && + lhs.dueOn?.secondsSince1970 == rhs.dueOn?.secondsSince1970 && + lhs.id == rhs.id && + lhs.isCompleted == rhs.isCompleted && + lhs.isStarted == rhs.isStarted && + Array.contentsAreEqual(lhs.milestones, rhs.milestones) && + lhs.name == rhs.name && + lhs.parentId == rhs.parentId && + lhs.projectId == rhs.projectId && + lhs.startOn?.secondsSince1970 == rhs.startOn?.secondsSince1970 && + lhs.startedOn?.secondsSince1970 == rhs.startedOn?.secondsSince1970 && + lhs.url == rhs.url) + } + +} + +// MARK: - JSON Keys + +extension Milestone { + + enum JSONKeys: JSONKey { + case completedOn = "completed_on" + case description + case dueOn = "due_on" + case id + case isCompleted = "is_completed" + case isStarted = "is_started" + case milestones + case name + case parentId = "parent_id" + case projectId = "project_id" + case startOn = "start_on" + case startedOn = "started_on" + case url + } + +} + +extension Milestone: UpdateRequestJSONKeys { + + var updateRequestJSONKeys: [JSONKey] { + return [ + JSONKeys.description.rawValue, + JSONKeys.dueOn.rawValue, + JSONKeys.isCompleted.rawValue, + JSONKeys.isStarted.rawValue, + JSONKeys.name.rawValue, + JSONKeys.parentId.rawValue, + JSONKeys.startOn.rawValue + ] + } + +} + +// MARK: - Serialization + +extension Milestone: JSONDeserializable { + + init?(json: JSONDictionary) { + + guard let id = json[JSONKeys.id.rawValue] as? Id, + let isCompleted = json[JSONKeys.isCompleted.rawValue] as? Bool, + let isStarted = json[JSONKeys.isStarted.rawValue] as? Bool, + let name = json[JSONKeys.name.rawValue] as? String, + let projectId = json[JSONKeys.projectId.rawValue] as? Project.Id, + let urlString = json[JSONKeys.url.rawValue] as? String, + let url = URL(string: urlString) else { + return nil + } + + let completedOn: Date? + if let seconds = json[JSONKeys.completedOn.rawValue] as? Int { + completedOn = Date(secondsSince1970: seconds) + } else { + completedOn = nil + } + + let dueOn: Date? + if let seconds = json[JSONKeys.dueOn.rawValue] as? Int { + dueOn = Date(secondsSince1970: seconds) + } else { + dueOn = nil + } + + let startOn: Date? + if let seconds = json[JSONKeys.startOn.rawValue] as? Int { + startOn = Date(secondsSince1970: seconds) + } else { + startOn = nil + } + + let startedOn: Date? + if let seconds = json[JSONKeys.startedOn.rawValue] as? Int { + startedOn = Date(secondsSince1970: seconds) + } else { + startedOn = nil + } + + let description = json[JSONKeys.description.rawValue] as? String ?? nil + let milestones: [Milestone]? + if let milestonesJson = json[JSONKeys.milestones.rawValue] as? [JSONDictionary] { + milestones = Milestone.deserialized(milestonesJson) + } else { + milestones = nil + } + let parentId = json[JSONKeys.parentId.rawValue] as? Id ?? nil + + self.init(completedOn: completedOn, description: description, dueOn: dueOn, id: id, isCompleted: isCompleted, isStarted: isStarted, milestones: milestones, name: name, parentId: parentId, projectId: projectId, startOn: startOn, startedOn: startedOn, url: url) + } + +} + +extension Milestone: JSONSerializable { + + func serialized() -> JSONDictionary { + + let milestonesSerialized: [JSONDictionary]? + if let milestones = self.milestones { + milestonesSerialized = Milestone.serialized(milestones) + } else { + milestonesSerialized = nil + } + + return [JSONKeys.completedOn.rawValue: completedOn?.secondsSince1970 as Any, + JSONKeys.description.rawValue: description as Any, + JSONKeys.dueOn.rawValue: dueOn?.secondsSince1970 as Any, + JSONKeys.id.rawValue: id, + JSONKeys.isCompleted.rawValue: isCompleted, + JSONKeys.isStarted.rawValue: isStarted, + JSONKeys.milestones.rawValue: milestonesSerialized as Any, + JSONKeys.name.rawValue: name, + JSONKeys.parentId.rawValue: parentId as Any, + JSONKeys.projectId.rawValue: projectId, + JSONKeys.startOn.rawValue: startOn?.secondsSince1970 as Any, + JSONKeys.startedOn.rawValue: startedOn?.secondsSince1970 as Any, + JSONKeys.url.rawValue: url.absoluteString] + } + +} + +extension Milestone: UpdateRequestJSON { } diff --git a/QuizTrain/Models/Plan.Entry.swift b/QuizTrain/Models/Plan.Entry.swift new file mode 100644 index 0000000..8c8a86c --- /dev/null +++ b/QuizTrain/Models/Plan.Entry.swift @@ -0,0 +1,90 @@ +extension Plan { + + public struct Entry: Identifiable { + public typealias Id = String + public let id: Id + public var name: String + public let runs: [Run] // NOTE: Runs can be updated using a PlanEntryRunsData. See ObjectAPI for details. + public let suiteId: Suite.Id + } + +} + +// MARK: - Foward Relationships (ObjectAPI) + +extension Plan.Entry { + + public func suite(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.suite(self, completionHandler: completionHandler) + } + +} + +// MARK: - Equatable + +extension Plan.Entry: Equatable { + + public static func==(lhs: Plan.Entry, rhs: Plan.Entry) -> Bool { + return (lhs.id == rhs.id && + lhs.name == rhs.name && + lhs.runs.contentsAreEqual(to: rhs.runs) && + lhs.suiteId == rhs.suiteId) + } + +} + +// MARK: - JSON Keys + +extension Plan.Entry { + + enum JSONKeys: JSONKey { + case id + case name + case runs + case suiteId = "suite_id" + } + +} + +extension Plan.Entry: UpdateRequestJSONKeys { + + var updateRequestJSONKeys: [JSONKey] { + return [JSONKeys.name.rawValue] + } + +} + +// MARK: - Serialization + +extension Plan.Entry: JSONDeserializable { + + init?(json: JSONDictionary) { + + guard let id = json[JSONKeys.id.rawValue] as? Id, + let name = json[JSONKeys.name.rawValue] as? String, + let runsJson = json[JSONKeys.runs.rawValue] as? [JSONDictionary], + let runs: [Run] = Run.deserialized(runsJson), + let suiteId = json[JSONKeys.suiteId.rawValue] as? Suite.Id else { + return nil + } + + self.init(id: id, name: name, runs: runs, suiteId: suiteId) + } + +} + +extension Plan.Entry: JSONSerializable { + + func serialized() -> JSONDictionary { + + let runsSerialized: [JSONDictionary] = Run.serialized(runs) + + return [JSONKeys.id.rawValue: id, + JSONKeys.name.rawValue: name, + JSONKeys.runs.rawValue: runsSerialized, + JSONKeys.suiteId.rawValue: suiteId] + } + +} + +extension Plan.Entry: UpdateRequestJSON { } diff --git a/QuizTrain/Models/Plan.swift b/QuizTrain/Models/Plan.swift new file mode 100644 index 0000000..7299a02 --- /dev/null +++ b/QuizTrain/Models/Plan.swift @@ -0,0 +1,216 @@ +public struct Plan: Identifiable { + public typealias Id = Int + public let assignedtoId: User.Id? + public let blockedCount: Int + public let completedOn: Date? + public let createdBy: User.Id + public let createdOn: Date + public let customStatus1Count: Int + public let customStatus2Count: Int + public let customStatus3Count: Int + public let customStatus4Count: Int + public let customStatus5Count: Int + public let customStatus6Count: Int + public let customStatus7Count: Int + public var description: String? + public let entries: [Plan.Entry]? // Certain API calls will always return nil for this value. Others will only return a single entry. See TestRail API documentation for details. + public let failedCount: Int + public let id: Id + public let isCompleted: Bool + public var milestoneId: Milestone.Id? + public var name: String + public let passedCount: Int + public let projectId: Project.Id + public let retestCount: Int + public let untestedCount: Int + public let url: URL +} + +// MARK: - Foward Relationships (ObjectAPI) + +extension Plan { + + public func assignedto(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.assignedto(self, completionHandler: completionHandler) + } + + public func createdBy(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.createdBy(self, completionHandler: completionHandler) + } + + public func milestone(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.milestone(self, completionHandler: completionHandler) + } + + public func project(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.project(self, completionHandler: completionHandler) + } + +} + +// MARK: - Equatable + +extension Plan: Equatable { + + public static func==(lhs: Plan, rhs: Plan) -> Bool { + return (lhs.assignedtoId == rhs.assignedtoId && + lhs.blockedCount == rhs.blockedCount && + lhs.completedOn?.secondsSince1970 == rhs.completedOn?.secondsSince1970 && + lhs.createdBy == rhs.createdBy && + lhs.createdOn.secondsSince1970 == rhs.createdOn.secondsSince1970 && + lhs.customStatus1Count == rhs.customStatus1Count && + lhs.customStatus2Count == rhs.customStatus2Count && + lhs.customStatus3Count == rhs.customStatus3Count && + lhs.customStatus4Count == rhs.customStatus4Count && + lhs.customStatus5Count == rhs.customStatus5Count && + lhs.customStatus6Count == rhs.customStatus6Count && + lhs.customStatus7Count == rhs.customStatus7Count && + lhs.description == rhs.description && + Array.contentsAreEqual(lhs.entries, rhs.entries) && + lhs.failedCount == rhs.failedCount && + lhs.id == rhs.id && + lhs.isCompleted == rhs.isCompleted && + lhs.milestoneId == rhs.milestoneId && + lhs.name == rhs.name && + lhs.passedCount == rhs.passedCount && + lhs.projectId == rhs.projectId && + lhs.retestCount == rhs.retestCount && + lhs.untestedCount == rhs.untestedCount && + lhs.url == rhs.url) + } + +} + +// MARK: - JSON Keys + +extension Plan { + + enum JSONKeys: JSONKey { + case assignedtoId = "assignedto_id" + case blockedCount = "blocked_count" + case completedOn = "completed_on" + case createdBy = "created_by" + case createdOn = "created_on" + case customStatus1Count = "custom_status1_count" + case customStatus2Count = "custom_status2_count" + case customStatus3Count = "custom_status3_count" + case customStatus4Count = "custom_status4_count" + case customStatus5Count = "custom_status5_count" + case customStatus6Count = "custom_status6_count" + case customStatus7Count = "custom_status7_count" + case description + case entries + case failedCount = "failed_count" + case id + case isCompleted = "is_completed" + case milestoneId = "milestone_id" + case name + case passedCount = "passed_count" + case projectId = "project_id" + case retestCount = "retest_count" + case untestedCount = "untested_count" + case url + } + +} + +extension Plan: UpdateRequestJSONKeys { + + var updateRequestJSONKeys: [JSONKey] { + return [ + JSONKeys.description.rawValue, + JSONKeys.milestoneId.rawValue, + JSONKeys.name.rawValue + ] + } + +} + +// MARK: - Serialization + +extension Plan: JSONDeserializable { + + init?(json: JSONDictionary) { + + guard let blockedCount = json[JSONKeys.blockedCount.rawValue] as? Int, + let createdBy = json[JSONKeys.createdBy.rawValue] as? User.Id, + let createdOnSeconds = json[JSONKeys.createdOn.rawValue] as? Int, + let customStatus1Count = json[JSONKeys.customStatus1Count.rawValue] as? Int, + let customStatus2Count = json[JSONKeys.customStatus2Count.rawValue] as? Int, + let customStatus3Count = json[JSONKeys.customStatus3Count.rawValue] as? Int, + let customStatus4Count = json[JSONKeys.customStatus4Count.rawValue] as? Int, + let customStatus5Count = json[JSONKeys.customStatus5Count.rawValue] as? Int, + let customStatus6Count = json[JSONKeys.customStatus6Count.rawValue] as? Int, + let customStatus7Count = json[JSONKeys.customStatus7Count.rawValue] as? Int, + let failedCount = json[JSONKeys.failedCount.rawValue] as? Int, + let id = json[JSONKeys.id.rawValue] as? Id, + let isCompleted = json[JSONKeys.isCompleted.rawValue] as? Bool, + let name = json[JSONKeys.name.rawValue] as? String, + let passedCount = json[JSONKeys.passedCount.rawValue] as? Int, + let projectId = json[JSONKeys.projectId.rawValue] as? Project.Id, + let retestCount = json[JSONKeys.retestCount.rawValue] as? Int, + let untestedCount = json[JSONKeys.untestedCount.rawValue] as? Int, + let urlString = json[JSONKeys.url.rawValue] as? String, + let url = URL(string: urlString) else { + return nil + } + let createdOn = Date(secondsSince1970: createdOnSeconds) + + let completedOn: Date? + if let seconds = json[JSONKeys.completedOn.rawValue] as? Int { + completedOn = Date(secondsSince1970: seconds) + } else { + completedOn = nil + } + + let assignedtoId = json[JSONKeys.assignedtoId.rawValue] as? User.Id ?? nil + let description = json[JSONKeys.description.rawValue] as? String ?? nil + let milestoneId = json[JSONKeys.milestoneId.rawValue] as? Milestone.Id ?? nil + let entriesJson = json[JSONKeys.entries.rawValue] as? [JSONDictionary] ?? nil + let entries: [Plan.Entry]? = entriesJson != nil ? Plan.Entry.deserialized(entriesJson!) : nil + + self.init(assignedtoId: assignedtoId, blockedCount: blockedCount, completedOn: completedOn, createdBy: createdBy, createdOn: createdOn, customStatus1Count: customStatus1Count, customStatus2Count: customStatus2Count, customStatus3Count: customStatus3Count, customStatus4Count: customStatus4Count, customStatus5Count: customStatus5Count, customStatus6Count: customStatus6Count, customStatus7Count: customStatus7Count, description: description, entries: entries, failedCount: failedCount, id: id, isCompleted: isCompleted, milestoneId: milestoneId, name: name, passedCount: passedCount, projectId: projectId, retestCount: retestCount, untestedCount: untestedCount, url: url) + } + +} + +extension Plan: JSONSerializable { + + func serialized() -> JSONDictionary { + + let entriesSerialized: [JSONDictionary]? + if let entries = self.entries { + entriesSerialized = Plan.Entry.serialized(entries) + } else { + entriesSerialized = nil + } + + return [JSONKeys.assignedtoId.rawValue: assignedtoId as Any, + JSONKeys.blockedCount.rawValue: blockedCount, + JSONKeys.completedOn.rawValue: completedOn?.secondsSince1970 as Any, + JSONKeys.createdBy.rawValue: createdBy, + JSONKeys.createdOn.rawValue: createdOn.secondsSince1970, + JSONKeys.customStatus1Count.rawValue: customStatus1Count, + JSONKeys.customStatus2Count.rawValue: customStatus2Count, + JSONKeys.customStatus3Count.rawValue: customStatus3Count, + JSONKeys.customStatus4Count.rawValue: customStatus4Count, + JSONKeys.customStatus5Count.rawValue: customStatus5Count, + JSONKeys.customStatus6Count.rawValue: customStatus6Count, + JSONKeys.customStatus7Count.rawValue: customStatus7Count, + JSONKeys.description.rawValue: description as Any, + JSONKeys.entries.rawValue: entriesSerialized as Any, + JSONKeys.failedCount.rawValue: failedCount, + JSONKeys.id.rawValue: id, + JSONKeys.isCompleted.rawValue: isCompleted, + JSONKeys.milestoneId.rawValue: milestoneId as Any, + JSONKeys.name.rawValue: name, + JSONKeys.passedCount.rawValue: passedCount, + JSONKeys.projectId.rawValue: projectId, + JSONKeys.retestCount.rawValue: retestCount, + JSONKeys.untestedCount.rawValue: untestedCount, + JSONKeys.url.rawValue: url.absoluteString] + } + +} + +extension Plan: UpdateRequestJSON { } diff --git a/QuizTrain/Models/Priority.swift b/QuizTrain/Models/Priority.swift new file mode 100644 index 0000000..a720740 --- /dev/null +++ b/QuizTrain/Models/Priority.swift @@ -0,0 +1,67 @@ +public struct Priority: Identifiable { + public typealias Id = Int + public let id: Id + public let isDefault: Bool + public let name: String + public let priority: Int + public let shortName: String +} + +// MARK: - Equatable + +extension Priority: Equatable { + + public static func==(lhs: Priority, rhs: Priority) -> Bool { + return (lhs.id == rhs.id && + lhs.isDefault == rhs.isDefault && + lhs.name == rhs.name && + lhs.priority == rhs.priority && + lhs.shortName == rhs.shortName) + } + +} + +// MARK: - JSON Keys + +extension Priority { + + enum JSONKeys: JSONKey { + case id + case isDefault = "is_default" + case name + case priority + case shortName = "short_name" + } + +} + +// MARK: - Serialization + +extension Priority: JSONDeserializable { + + init?(json: JSONDictionary) { + + guard let id = json[JSONKeys.id.rawValue] as? Id, + let isDefault = json[JSONKeys.isDefault.rawValue] as? Bool, + let name = json[JSONKeys.name.rawValue] as? String, + let priority = json[JSONKeys.priority.rawValue] as? Int, + let shortName = json[JSONKeys.shortName.rawValue] as? String else { + return nil + } + + self.init(id: id, isDefault: isDefault, name: name, priority: priority, shortName: shortName) + } + +} + +extension Priority: JSONSerializable { + + func serialized() -> JSONDictionary { + return [JSONKeys.id.rawValue: id, + JSONKeys.isDefault.rawValue: isDefault, + JSONKeys.name.rawValue: name, + JSONKeys.priority.rawValue: priority, + JSONKeys.shortName.rawValue: shortName] + } + +} diff --git a/QuizTrain/Models/Project.swift b/QuizTrain/Models/Project.swift new file mode 100644 index 0000000..039678d --- /dev/null +++ b/QuizTrain/Models/Project.swift @@ -0,0 +1,107 @@ +public struct Project: Identifiable { + public typealias Id = Int + public var announcement: String? + public let completedOn: Date? + public let id: Id + public var isCompleted: Bool + public var name: String + public var showAnnouncement: Bool + public var suiteMode: Project.SuiteMode + public let url: URL +} + +// MARK: - Equatable + +extension Project: Equatable { + + public static func==(lhs: Project, rhs: Project) -> Bool { + return (lhs.announcement == rhs.announcement && + lhs.completedOn?.secondsSince1970 == rhs.completedOn?.secondsSince1970 && + lhs.id == rhs.id && + lhs.isCompleted == rhs.isCompleted && + lhs.name == rhs.name && + lhs.showAnnouncement == rhs.showAnnouncement && + lhs.suiteMode == rhs.suiteMode && + lhs.url == rhs.url) + } + +} + +// MARK: - JSON Keys + +extension Project { + + enum JSONKeys: JSONKey { + case announcement + case completedOn = "completed_on" + case id + case isCompleted = "is_completed" + case name + case showAnnouncement = "show_announcement" + case suiteMode = "suite_mode" + case url + } + +} + +extension Project: UpdateRequestJSONKeys { + + var updateRequestJSONKeys: [JSONKey] { + return [ + JSONKeys.announcement.rawValue, + JSONKeys.isCompleted.rawValue, + JSONKeys.name.rawValue, + JSONKeys.showAnnouncement.rawValue, + JSONKeys.suiteMode.rawValue + ] + } + +} + +// MARK: - Serialization + +extension Project: JSONDeserializable { + + init?(json: JSONDictionary) { + + guard let id = json[JSONKeys.id.rawValue] as? Id, + let isCompleted = json[JSONKeys.isCompleted.rawValue] as? Bool, + let name = json[JSONKeys.name.rawValue] as? String, + let showAnnouncement = json[JSONKeys.showAnnouncement.rawValue] as? Bool, + let suiteModeInt = json[JSONKeys.suiteMode.rawValue] as? Int, + let suiteMode = Project.SuiteMode(rawValue: suiteModeInt), + let urlString = json[JSONKeys.url.rawValue] as? String, + let url = URL(string: urlString) else { + return nil + } + + let announcement = json[JSONKeys.announcement.rawValue] as? String ?? nil + + let completedOn: Date? + if let seconds = json[JSONKeys.completedOn.rawValue] as? Int { + completedOn = Date(secondsSince1970: seconds) + } else { + completedOn = nil + } + + self.init(announcement: announcement, completedOn: completedOn, id: id, isCompleted: isCompleted, name: name, showAnnouncement: showAnnouncement, suiteMode: suiteMode, url: url) + } + +} + +extension Project: JSONSerializable { + + func serialized() -> JSONDictionary { + return [JSONKeys.announcement.rawValue: announcement as Any, + JSONKeys.completedOn.rawValue: completedOn?.secondsSince1970 as Any, + JSONKeys.id.rawValue: id, + JSONKeys.isCompleted.rawValue: isCompleted, + JSONKeys.name.rawValue: name, + JSONKeys.showAnnouncement.rawValue: showAnnouncement, + JSONKeys.suiteMode.rawValue: suiteMode.rawValue, + JSONKeys.url.rawValue: url.absoluteString] + } + +} + +extension Project: UpdateRequestJSON { } diff --git a/QuizTrain/Models/Result.swift b/QuizTrain/Models/Result.swift new file mode 100644 index 0000000..c634835 --- /dev/null +++ b/QuizTrain/Models/Result.swift @@ -0,0 +1,126 @@ +public struct Result: CustomFields, Identifiable { + public typealias Id = Int + public let assignedtoId: User.Id? + public let comment: String? + public let createdBy: User.Id + public let createdOn: Date + public let defects: String? + public let elapsed: String? + public let id: Id + public let statusId: Status.Id? + public let testId: Test.Id + public let version: String? + let customFieldsContainer: CustomFieldsContainer +} + +// MARK: - Foward Relationships (ObjectAPI) + +extension Result { + + public func assignedto(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.assignedto(self, completionHandler: completionHandler) + } + + public func createdBy(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.createdBy(self, completionHandler: completionHandler) + } + + public func status(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome, ObjectAPI.GetError>>) -> Void) { + objectAPI.status(self, completionHandler: completionHandler) + } + + public func test(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.test(self, completionHandler: completionHandler) + } + +} + +// MARK: - Equatable + +extension Result: Equatable { + + public static func==(lhs: Result, rhs: Result) -> Bool { + return (lhs.assignedtoId == rhs.assignedtoId && + lhs.comment == rhs.comment && + lhs.createdBy == rhs.createdBy && + lhs.createdOn.secondsSince1970 == rhs.createdOn.secondsSince1970 && + lhs.defects == rhs.defects && + lhs.elapsed == rhs.elapsed && + lhs.id == rhs.id && + lhs.statusId == rhs.statusId && + lhs.testId == rhs.testId && + lhs.version == rhs.version && + lhs.customFieldsContainer == rhs.customFieldsContainer) + } + +} + +// MARK: - JSON Keys + +extension Result { + + enum JSONKeys: JSONKey { + case assignedtoId = "assignedto_id" + case comment + case createdBy = "created_by" + case createdOn = "created_on" + case defects + case elapsed + case id + case statusId = "status_id" + case testId = "test_id" + case version + } + +} + +// MARK: - Serialization + +extension Result: JSONDeserializable { + + init?(json: JSONDictionary) { + + guard let createdBy = json[JSONKeys.createdBy.rawValue] as? User.Id, + let createdOnSeconds = json[JSONKeys.createdOn.rawValue] as? Int, + let id = json[JSONKeys.id.rawValue] as? Id, + let testId = json[JSONKeys.testId.rawValue] as? Test.Id else { + return nil + } + let createdOn = Date(secondsSince1970: createdOnSeconds) + + let assignedtoId = json[JSONKeys.assignedtoId.rawValue] as? User.Id ?? nil + let comment = json[JSONKeys.comment.rawValue] as? String ?? nil + let defects = json[JSONKeys.defects.rawValue] as? String ?? nil + let elapsed = json[JSONKeys.elapsed.rawValue] as? String ?? nil + let statusId = json[JSONKeys.statusId.rawValue] as? Status.Id ?? nil + let version = json[JSONKeys.version.rawValue] as? String ?? nil + + let customFieldsContainer = CustomFieldsContainer(json: json) + + self.init(assignedtoId: assignedtoId, comment: comment, createdBy: createdBy, createdOn: createdOn, defects: defects, elapsed: elapsed, id: id, statusId: statusId, testId: testId, version: version, customFieldsContainer: customFieldsContainer) + } + +} + +extension Result: JSONSerializable { + + private var serializedProperties: JSONDictionary { + return [JSONKeys.assignedtoId.rawValue: assignedtoId as Any, + JSONKeys.comment.rawValue: comment as Any, + JSONKeys.createdBy.rawValue: createdBy, + JSONKeys.createdOn.rawValue: createdOn.secondsSince1970, + JSONKeys.defects.rawValue: defects as Any, + JSONKeys.elapsed.rawValue: elapsed as Any, + JSONKeys.id.rawValue: id, + JSONKeys.statusId.rawValue: statusId as Any, + JSONKeys.testId.rawValue: testId, + JSONKeys.version.rawValue: version as Any] + } + + func serialized() -> JSONDictionary { + var json = serializedProperties + customFields.forEach { item in json[item.key] = item.value } + return json + } + +} diff --git a/QuizTrain/Models/ResultField.swift b/QuizTrain/Models/ResultField.swift new file mode 100644 index 0000000..22b5e48 --- /dev/null +++ b/QuizTrain/Models/ResultField.swift @@ -0,0 +1,113 @@ +public struct ResultField: Identifiable { + public typealias Id = Int + public let configs: [Config] + public let description: String? + public let displayOrder: Int + public let id: Id + public let includeAll: Bool + public let isActive: Bool + public let label: String + public let name: String + public let systemName: String + public let templateIds: [Template.Id] + public let typeId: CustomFieldType +} + +// MARK: - Foward Relationships (ObjectAPI) + +extension ResultField { + + public func templates(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome<[Template], ObjectAPI.MatchError, ErrorContainer>>) -> Void) { + objectAPI.templates(self, completionHandler: completionHandler) + } + +} + +// MARK: - Equatable + +extension ResultField: Equatable { + + public static func==(lhs: ResultField, rhs: ResultField) -> Bool { + return (lhs.configs.contentsAreEqual(to: rhs.configs) && + lhs.description == rhs.description && + lhs.displayOrder == rhs.displayOrder && + lhs.id == rhs.id && + lhs.includeAll == rhs.includeAll && + lhs.isActive == rhs.isActive && + lhs.label == rhs.label && + lhs.name == rhs.name && + lhs.systemName == rhs.systemName && + lhs.templateIds.sorted() == rhs.templateIds.sorted() && + lhs.typeId == rhs.typeId) + } + +} + +// MARK: - JSON Keys + +extension ResultField { + + enum JSONKeys: JSONKey { + case configs + case description + case displayOrder = "display_order" + case id + case includeAll = "include_all" + case isActive = "is_active" + case label + case name + case systemName = "system_name" + case templateIds = "template_ids" + case typeId = "type_id" + } + +} + +// MARK: - Serialization + +extension ResultField: JSONDeserializable { + + init?(json: JSONDictionary) { + + guard let configsJson = json[JSONKeys.configs.rawValue] as? [JSONDictionary], + let configs: [Config] = ResultField.deserialized(configsJson), + let displayOrder = json[JSONKeys.displayOrder.rawValue] as? Int, + let id = json[JSONKeys.id.rawValue] as? Id, + let includeAll = json[JSONKeys.includeAll.rawValue] as? Bool, + let isActive = json[JSONKeys.isActive.rawValue] as? Bool, + let label = json[JSONKeys.label.rawValue] as? String, + let name = json[JSONKeys.name.rawValue] as? String, + let systemName = json[JSONKeys.systemName.rawValue] as? String, + let templateIds = json[JSONKeys.templateIds.rawValue] as? [Template.Id], + let typeIdInt = json[JSONKeys.typeId.rawValue] as? Int, + let typeId = CustomFieldType(rawValue: typeIdInt) else { + return nil + } + + let description = json[JSONKeys.description.rawValue] as? String ?? nil + + self.init(configs: configs, description: description, displayOrder: displayOrder, id: id, includeAll: includeAll, isActive: isActive, label: label, name: name, systemName: systemName, templateIds: templateIds, typeId: typeId) + } + +} + +extension ResultField: JSONSerializable { + + func serialized() -> JSONDictionary { + + let configsSerialized: [JSONDictionary] = Config.serialized(configs) + + return [JSONKeys.configs.rawValue: configsSerialized, + JSONKeys.description.rawValue: description as Any, + JSONKeys.displayOrder.rawValue: displayOrder, + JSONKeys.id.rawValue: id, + JSONKeys.includeAll.rawValue: includeAll, + JSONKeys.isActive.rawValue: isActive, + JSONKeys.label.rawValue: label, + JSONKeys.name.rawValue: name, + JSONKeys.systemName.rawValue: systemName, + JSONKeys.templateIds.rawValue: templateIds, + JSONKeys.typeId.rawValue: typeId.rawValue] + } + +} diff --git a/QuizTrain/Models/Run.swift b/QuizTrain/Models/Run.swift new file mode 100644 index 0000000..8c7e99b --- /dev/null +++ b/QuizTrain/Models/Run.swift @@ -0,0 +1,240 @@ +public struct Run: Identifiable { + public typealias Id = Int + public let assignedtoId: User.Id? + public let blockedCount: Int + public let completedOn: Date? + public let config: String? + public let configIds: [Configuration.Id]? + public let createdBy: User.Id + public let createdOn: Date + public let customStatus1Count: Int + public let customStatus2Count: Int + public let customStatus3Count: Int + public let customStatus4Count: Int + public let customStatus5Count: Int + public let customStatus6Count: Int + public let customStatus7Count: Int + public var description: String? + public let failedCount: Int + public let id: Id + public var includeAll: Bool + public let isCompleted: Bool + public var milestoneId: Milestone.Id? + public var name: String + public let planId: Plan.Id? + public let passedCount: Int + public let projectId: Project.Id + public let retestCount: Int + public let suiteId: Suite.Id? + public let untestedCount: Int + public let url: URL +} + +// MARK: - Foward Relationships (ObjectAPI) + +extension Run { + + public func assignedto(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.assignedto(self, completionHandler: completionHandler) + } + + public func configurations(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome<[Configuration]?, ObjectAPI.MatchError, ObjectAPI.GetError>>) -> Void) { + objectAPI.configurations(self, completionHandler: completionHandler) + } + + public func createdBy(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.createdBy(self, completionHandler: completionHandler) + } + + public func milestone(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.milestone(self, completionHandler: completionHandler) + } + + public func plan(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.plan(self, completionHandler: completionHandler) + } + + public func project(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.project(self, completionHandler: completionHandler) + } + + public func suite(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.suite(self, completionHandler: completionHandler) + } + +} + +// MARK: - Equatable + +extension Run: Equatable { + + public static func==(lhs: Run, rhs: Run) -> Bool { + return (lhs.assignedtoId == rhs.assignedtoId && + lhs.blockedCount == rhs.blockedCount && + lhs.completedOn?.secondsSince1970 == rhs.completedOn?.secondsSince1970 && + lhs.config == rhs.config && + lhs.configIds?.sorted() == rhs.configIds?.sorted() && + lhs.createdBy == rhs.createdBy && + lhs.createdOn.secondsSince1970 == rhs.createdOn.secondsSince1970 && + lhs.customStatus1Count == rhs.customStatus1Count && + lhs.customStatus2Count == rhs.customStatus2Count && + lhs.customStatus3Count == rhs.customStatus3Count && + lhs.customStatus4Count == rhs.customStatus4Count && + lhs.customStatus5Count == rhs.customStatus5Count && + lhs.customStatus6Count == rhs.customStatus6Count && + lhs.customStatus7Count == rhs.customStatus7Count && + lhs.description == rhs.description && + lhs.failedCount == rhs.failedCount && + lhs.id == rhs.id && + lhs.includeAll == rhs.includeAll && + lhs.isCompleted == rhs.isCompleted && + lhs.milestoneId == rhs.milestoneId && + lhs.name == rhs.name && + lhs.planId == rhs.planId && + lhs.passedCount == rhs.passedCount && + lhs.projectId == rhs.projectId && + lhs.retestCount == rhs.retestCount && + lhs.suiteId == rhs.suiteId && + lhs.untestedCount == rhs.untestedCount && + lhs.url == rhs.url) + } + +} + +// MARK: - JSON Keys + +extension Run { + + enum JSONKeys: JSONKey { + case assignedtoId = "assignedto_id" + case blockedCount = "blocked_count" + case completedOn = "completed_on" + case config + case configIds = "config_ids" + case createdBy = "created_by" + case createdOn = "created_on" + case customStatus1Count = "custom_status1_count" + case customStatus2Count = "custom_status2_count" + case customStatus3Count = "custom_status3_count" + case customStatus4Count = "custom_status4_count" + case customStatus5Count = "custom_status5_count" + case customStatus6Count = "custom_status6_count" + case customStatus7Count = "custom_status7_count" + case description + case failedCount = "failed_count" + case id + case includeAll = "include_all" + case isCompleted = "is_completed" + case milestoneId = "milestone_id" + case name + case planId = "plan_id" + case passedCount = "passed_count" + case projectId = "project_id" + case retestCount = "retest_count" + case suiteId = "suite_id" + case untestedCount = "untested_count" + case url + } + +} + +extension Run: UpdateRequestJSONKeys { + + var updateRequestJSONKeys: [JSONKey] { + return [ + JSONKeys.description.rawValue, + JSONKeys.includeAll.rawValue, + JSONKeys.milestoneId.rawValue, + JSONKeys.name.rawValue + ] + } + +} + +// MARK: - Serialization + +extension Run: JSONDeserializable { + + init?(json: JSONDictionary) { + + guard let blockedCount = json[JSONKeys.blockedCount.rawValue] as? Int, + let createdBy = json[JSONKeys.createdBy.rawValue] as? User.Id, + let createdOnSeconds = json[JSONKeys.createdOn.rawValue] as? Int, + let customStatus1Count = json[JSONKeys.customStatus1Count.rawValue] as? Int, + let customStatus2Count = json[JSONKeys.customStatus2Count.rawValue] as? Int, + let customStatus3Count = json[JSONKeys.customStatus3Count.rawValue] as? Int, + let customStatus4Count = json[JSONKeys.customStatus4Count.rawValue] as? Int, + let customStatus5Count = json[JSONKeys.customStatus5Count.rawValue] as? Int, + let customStatus6Count = json[JSONKeys.customStatus6Count.rawValue] as? Int, + let customStatus7Count = json[JSONKeys.customStatus7Count.rawValue] as? Int, + let failedCount = json[JSONKeys.failedCount.rawValue] as? Int, + let id = json[JSONKeys.id.rawValue] as? Id, + let includeAll = json[JSONKeys.includeAll.rawValue] as? Bool, + let isCompleted = json[JSONKeys.isCompleted.rawValue] as? Bool, + let name = json[JSONKeys.name.rawValue] as? String, + let passedCount = json[JSONKeys.passedCount.rawValue] as? Int, + let projectId = json[JSONKeys.projectId.rawValue] as? Project.Id, + let retestCount = json[JSONKeys.retestCount.rawValue] as? Int, + let untestedCount = json[JSONKeys.untestedCount.rawValue] as? Int, + let urlString = json[JSONKeys.url.rawValue] as? String, + let url = URL(string: urlString) else { + return nil + } + let createdOn = Date(secondsSince1970: createdOnSeconds) + + let completedOn: Date? + if let seconds = json[JSONKeys.completedOn.rawValue] as? Int { + completedOn = Date(secondsSince1970: seconds) + } else { + completedOn = nil + } + + let assignedtoId = json[JSONKeys.assignedtoId.rawValue] as? User.Id ?? nil + let config = json[JSONKeys.config.rawValue] as? String ?? nil + let configIds = json[JSONKeys.configIds.rawValue] as? [Configuration.Id] ?? nil + let description = json[JSONKeys.description.rawValue] as? String ?? nil + let milestoneId = json[JSONKeys.milestoneId.rawValue] as? Milestone.Id ?? nil + let planId = json[JSONKeys.planId.rawValue] as? Plan.Id ?? nil + let suiteId = json[JSONKeys.suiteId.rawValue] as? Suite.Id ?? nil + + self.init(assignedtoId: assignedtoId, blockedCount: blockedCount, completedOn: completedOn, config: config, configIds: configIds, createdBy: createdBy, createdOn: createdOn, customStatus1Count: customStatus1Count, customStatus2Count: customStatus2Count, customStatus3Count: customStatus3Count, customStatus4Count: customStatus4Count, customStatus5Count: customStatus5Count, customStatus6Count: customStatus6Count, customStatus7Count: customStatus7Count, description: description, failedCount: failedCount, id: id, includeAll: includeAll, isCompleted: isCompleted, milestoneId: milestoneId, name: name, planId: planId, passedCount: passedCount, projectId: projectId, retestCount: retestCount, suiteId: suiteId, untestedCount: untestedCount, url: url) + } + +} + +extension Run: JSONSerializable { + + func serialized() -> JSONDictionary { + return [JSONKeys.assignedtoId.rawValue: assignedtoId as Any, + JSONKeys.blockedCount.rawValue: blockedCount, + JSONKeys.completedOn.rawValue: completedOn?.secondsSince1970 as Any, + JSONKeys.config.rawValue: config as Any, + JSONKeys.configIds.rawValue: configIds as Any, + JSONKeys.createdBy.rawValue: createdBy, + JSONKeys.createdOn.rawValue: createdOn.secondsSince1970, + JSONKeys.customStatus1Count.rawValue: customStatus1Count, + JSONKeys.customStatus2Count.rawValue: customStatus2Count, + JSONKeys.customStatus3Count.rawValue: customStatus3Count, + JSONKeys.customStatus4Count.rawValue: customStatus4Count, + JSONKeys.customStatus5Count.rawValue: customStatus5Count, + JSONKeys.customStatus6Count.rawValue: customStatus6Count, + JSONKeys.customStatus7Count.rawValue: customStatus7Count, + JSONKeys.description.rawValue: description as Any, + JSONKeys.failedCount.rawValue: failedCount, + JSONKeys.id.rawValue: id, + JSONKeys.includeAll.rawValue: includeAll, + JSONKeys.isCompleted.rawValue: isCompleted, + JSONKeys.milestoneId.rawValue: milestoneId as Any, + JSONKeys.name.rawValue: name, + JSONKeys.planId.rawValue: planId as Any, + JSONKeys.passedCount.rawValue: passedCount, + JSONKeys.projectId.rawValue: projectId, + JSONKeys.retestCount.rawValue: retestCount, + JSONKeys.suiteId.rawValue: suiteId as Any, + JSONKeys.untestedCount.rawValue: untestedCount, + JSONKeys.url.rawValue: url.absoluteString] + } + +} + +extension Run: UpdateRequestJSON { } diff --git a/QuizTrain/Models/Section.swift b/QuizTrain/Models/Section.swift new file mode 100644 index 0000000..6f78abd --- /dev/null +++ b/QuizTrain/Models/Section.swift @@ -0,0 +1,105 @@ +public struct Section: Identifiable { + public typealias Id = Int + public let depth: Int + public var description: String? + public let displayOrder: Int + public let id: Id + public var name: String + public let parentId: Id? + public let suiteId: Suite.Id? +} + +// MARK: - Foward Relationships (ObjectAPI) + +extension Section { + + public func parent(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.parent(self, completionHandler: completionHandler) + } + + public func suite(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.suite(self, completionHandler: completionHandler) + } + +} + +// MARK: - Equatable + +extension Section: Equatable { + + public static func==(lhs: Section, rhs: Section) -> Bool { + return (lhs.depth == rhs.depth && + lhs.description == rhs.description && + lhs.displayOrder == rhs.displayOrder && + lhs.id == rhs.id && + lhs.name == rhs.name && + lhs.parentId == rhs.parentId && + lhs.suiteId == rhs.suiteId) + } + +} + +// MARK: - JSON Keys + +extension Section { + + enum JSONKeys: JSONKey { + case depth + case description + case displayOrder = "display_order" + case id + case name + case parentId = "parent_id" + case suiteId = "suite_id" + } + +} + +extension Section: UpdateRequestJSONKeys { + + var updateRequestJSONKeys: [JSONKey] { + return [ + JSONKeys.description.rawValue, + JSONKeys.name.rawValue + ] + } + +} + +// MARK: - Serialization + +extension Section: JSONDeserializable { + + init?(json: JSONDictionary) { + + guard let depth = json[JSONKeys.depth.rawValue] as? Int, + let displayOrder = json[JSONKeys.displayOrder.rawValue] as? Int, + let id = json[JSONKeys.id.rawValue] as? Id, + let name = json[JSONKeys.name.rawValue] as? String else { + return nil + } + + let description = json[JSONKeys.description.rawValue] as? String ?? nil + let parentId = json[JSONKeys.parentId.rawValue] as? Id ?? nil + let suiteId = json[JSONKeys.suiteId.rawValue] as? Suite.Id ?? nil + + self.init(depth: depth, description: description, displayOrder: displayOrder, id: id, name: name, parentId: parentId, suiteId: suiteId) + } + +} + +extension Section: JSONSerializable { + + func serialized() -> JSONDictionary { + return [JSONKeys.depth.rawValue: depth, + JSONKeys.description.rawValue: description as Any, + JSONKeys.displayOrder.rawValue: displayOrder, + JSONKeys.id.rawValue: id, + JSONKeys.name.rawValue: name, + JSONKeys.parentId.rawValue: parentId as Any, + JSONKeys.suiteId.rawValue: suiteId as Any] + } + +} + +extension Section: UpdateRequestJSON { } diff --git a/QuizTrain/Models/Status.swift b/QuizTrain/Models/Status.swift new file mode 100644 index 0000000..52a9f55 --- /dev/null +++ b/QuizTrain/Models/Status.swift @@ -0,0 +1,87 @@ +public struct Status: Identifiable { + public typealias Id = Int + public let colorBright: Int + public let colorDark: Int + public let colorMedium: Int + public let id: Id + public let isFinal: Bool + public let isSystem: Bool + public let isUntested: Bool + public let label: String + public let name: String +} + +// MARK: - Equatable + +extension Status: Equatable { + + public static func==(lhs: Status, rhs: Status) -> Bool { + return (lhs.colorBright == rhs.colorBright && + lhs.colorDark == rhs.colorDark && + lhs.colorMedium == rhs.colorMedium && + lhs.id == rhs.id && + lhs.isFinal == rhs.isFinal && + lhs.isSystem == rhs.isSystem && + lhs.isUntested == rhs.isUntested && + lhs.label == rhs.label && + lhs.name == rhs.name) + } + +} + +// MARK: - JSON Keys + +extension Status { + + enum JSONKeys: JSONKey { + case colorBright = "color_bright" + case colorDark = "color_dark" + case colorMedium = "color_medium" + case id + case isFinal = "is_final" + case isSystem = "is_system" + case isUntested = "is_untested" + case label + case name + } + +} + +// MARK: - Serialization + +extension Status: JSONDeserializable { + + init?(json: JSONDictionary) { + + guard let colorBright = json[JSONKeys.colorBright.rawValue] as? Int, + let colorDark = json[JSONKeys.colorDark.rawValue] as? Int, + let colorMedium = json[JSONKeys.colorMedium.rawValue] as? Int, + let id = json[JSONKeys.id.rawValue] as? Id, + let isFinal = json[JSONKeys.isFinal.rawValue] as? Bool, + let isSystem = json[JSONKeys.isSystem.rawValue] as? Bool, + let isUntested = json[JSONKeys.isUntested.rawValue] as? Bool, + let label = json[JSONKeys.label.rawValue] as? String, + let name = json[JSONKeys.name.rawValue] as? String else { + return nil + } + + self.init(colorBright: colorBright, colorDark: colorDark, colorMedium: colorMedium, id: id, isFinal: isFinal, isSystem: isSystem, isUntested: isUntested, label: label, name: name) + } + +} + +extension Status: JSONSerializable { + + func serialized() -> JSONDictionary { + return [JSONKeys.colorBright.rawValue: colorBright, + JSONKeys.colorDark.rawValue: colorDark, + JSONKeys.colorMedium.rawValue: colorMedium, + JSONKeys.id.rawValue: id, + JSONKeys.isFinal.rawValue: isFinal, + JSONKeys.isSystem.rawValue: isSystem, + JSONKeys.isUntested.rawValue: isUntested, + JSONKeys.label.rawValue: label, + JSONKeys.name.rawValue: name] + } + +} diff --git a/QuizTrain/Models/Suite.swift b/QuizTrain/Models/Suite.swift new file mode 100644 index 0000000..da28e31 --- /dev/null +++ b/QuizTrain/Models/Suite.swift @@ -0,0 +1,118 @@ +public struct Suite: Identifiable { + public typealias Id = Int + public let completedOn: Date? + public var description: String? + public let id: Id + public let isBaseline: Bool + public let isCompleted: Bool + public let isMaster: Bool + public var name: String + public let projectId: Project.Id + public let url: URL +} + +// MARK: - Foward Relationships (ObjectAPI) + +extension Suite { + + public func project(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.project(self, completionHandler: completionHandler) + } + +} + +// MARK: - Equatable + +extension Suite: Equatable { + + public static func==(lhs: Suite, rhs: Suite) -> Bool { + return (lhs.completedOn?.secondsSince1970 == rhs.completedOn?.secondsSince1970 && + lhs.description == rhs.description && + lhs.id == rhs.id && + lhs.isBaseline == rhs.isBaseline && + lhs.isCompleted == rhs.isCompleted && + lhs.isMaster == rhs.isMaster && + lhs.name == rhs.name && + lhs.projectId == rhs.projectId && + lhs.url == rhs.url) + } + +} + +// MARK: - JSON Keys + +extension Suite { + + enum JSONKeys: JSONKey { + case completedOn = "completed_on" + case description = "description" + case id + case isBaseline = "is_baseline" + case isCompleted = "is_completed" + case isMaster = "is_master" + case name + case projectId = "project_id" + case url + } + +} + +extension Suite: UpdateRequestJSONKeys { + + var updateRequestJSONKeys: [JSONKey] { + return [ + JSONKeys.description.rawValue, + JSONKeys.name.rawValue + ] + } + +} + +// MARK: - Serialization + +extension Suite: JSONDeserializable { + + init?(json: JSONDictionary) { + + guard let id = json[JSONKeys.id.rawValue] as? Id, + let isBaseline = json[JSONKeys.isBaseline.rawValue] as? Bool, + let isCompleted = json[JSONKeys.isCompleted.rawValue] as? Bool, + let isMaster = json[JSONKeys.isMaster.rawValue] as? Bool, + let name = json[JSONKeys.name.rawValue] as? String, + let projectId = json[JSONKeys.projectId.rawValue] as? Project.Id, + let urlString = json[JSONKeys.url.rawValue] as? String, + let url = URL(string: urlString) else { + return nil + } + + let completedOn: Date? + if let seconds = json[JSONKeys.completedOn.rawValue] as? Int { + completedOn = Date(secondsSince1970: seconds) + } else { + completedOn = nil + } + + let description = json[JSONKeys.description.rawValue] as? String ?? nil + + self.init(completedOn: completedOn, description: description, id: id, isBaseline: isBaseline, isCompleted: isCompleted, isMaster: isMaster, name: name, projectId: projectId, url: url) + } + +} + +extension Suite: JSONSerializable { + + func serialized() -> JSONDictionary { + return [JSONKeys.completedOn.rawValue: completedOn?.secondsSince1970 as Any, + JSONKeys.description.rawValue: description as Any, + JSONKeys.id.rawValue: id, + JSONKeys.isBaseline.rawValue: isBaseline, + JSONKeys.isCompleted.rawValue: isCompleted, + JSONKeys.isMaster.rawValue: isMaster, + JSONKeys.name.rawValue: name, + JSONKeys.projectId.rawValue: projectId, + JSONKeys.url.rawValue: url.absoluteString] + } + +} + +extension Suite: UpdateRequestJSON { } diff --git a/QuizTrain/Models/Template.swift b/QuizTrain/Models/Template.swift new file mode 100644 index 0000000..402b124 --- /dev/null +++ b/QuizTrain/Models/Template.swift @@ -0,0 +1,57 @@ +public struct Template: Identifiable { + public typealias Id = Int + public let isDefault: Bool + public let id: Id + public let name: String +} + +// MARK: - Equatable + +extension Template: Equatable { + + public static func==(lhs: Template, rhs: Template) -> Bool { + return (lhs.isDefault == rhs.isDefault && + lhs.id == rhs.id && + lhs.name == rhs.name) + } + +} + +// MARK: - JSON Keys + +extension Template { + + enum JSONKeys: JSONKey { + case isDefault = "is_default" + case id + case name + } + +} + +// MARK: - Serialization + +extension Template: JSONDeserializable { + + init?(json: JSONDictionary) { + + guard let isDefault = json[JSONKeys.isDefault.rawValue] as? Bool, + let id = json[JSONKeys.id.rawValue] as? Id, + let name = json[JSONKeys.name.rawValue] as? String else { + return nil + } + + self.init(isDefault: isDefault, id: id, name: name) + } + +} + +extension Template: JSONSerializable { + + func serialized() -> JSONDictionary { + return [JSONKeys.isDefault.rawValue: isDefault, + JSONKeys.id.rawValue: id, + JSONKeys.name.rawValue: name] + } + +} diff --git a/QuizTrain/Models/Test.swift b/QuizTrain/Models/Test.swift new file mode 100644 index 0000000..6922d77 --- /dev/null +++ b/QuizTrain/Models/Test.swift @@ -0,0 +1,156 @@ +public struct Test: CustomFields, Identifiable { + public typealias Id = Int + public let assignedtoId: User.Id? + public let caseId: Case.Id + public let estimate: String? + public let estimateForecast: String? + public let id: Id + public let milestoneId: Milestone.Id? + public let priorityId: Priority.Id + public let refs: String? + public let runId: Run.Id + public let statusId: Status.Id + public let templateId: Template.Id + public let title: String + public let typeId: CaseType.Id + let customFieldsContainer: CustomFieldsContainer +} + +// MARK: - Foward Relationships (ObjectAPI) + +extension Test { + + public func assignedto(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.assignedto(self, completionHandler: completionHandler) + } + + public func `case`(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.`case`(self, completionHandler: completionHandler) + } + + public func milestone(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.milestone(self, completionHandler: completionHandler) + } + + public func priority(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome, ObjectAPI.GetError>>) -> Void) { + objectAPI.priority(self, completionHandler: completionHandler) + } + + public func run(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome) -> Void) { + objectAPI.run(self, completionHandler: completionHandler) + } + + public func status(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome, ObjectAPI.GetError>>) -> Void) { + objectAPI.status(self, completionHandler: completionHandler) + } + + public func template(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome, ErrorContainer>>) -> Void) { + objectAPI.template(self, completionHandler: completionHandler) + } + + public func type(_ objectAPI: ObjectAPI, completionHandler: @escaping (Outcome, ObjectAPI.GetError>>) -> Void) { + objectAPI.type(self, completionHandler: completionHandler) + } + +} + +// MARK: - Equatable + +extension Test: Equatable { + + public static func==(lhs: Test, rhs: Test) -> Bool { + return (lhs.assignedtoId == rhs.assignedtoId && + lhs.caseId == rhs.caseId && + lhs.estimate == rhs.estimate && + lhs.estimateForecast == rhs.estimateForecast && + lhs.id == rhs.id && + lhs.milestoneId == rhs.milestoneId && + lhs.priorityId == rhs.priorityId && + lhs.refs == rhs.refs && + lhs.runId == rhs.runId && + lhs.statusId == rhs.statusId && + lhs.templateId == rhs.templateId && + lhs.title == rhs.title && + lhs.typeId == rhs.typeId && + lhs.customFieldsContainer == rhs.customFieldsContainer) + } + +} + +// MARK: - JSON Keys + +extension Test { + + enum JSONKeys: JSONKey { + case assignedtoId = "assignedto_id" + case caseId = "case_id" + case estimate + case estimateForecast = "estimate_forecast" + case id + case milestoneId = "milestone_id" + case priorityId = "priority_id" + case refs + case runId = "run_id" + case statusId = "status_id" + case templateId = "template_id" + case title + case typeId = "type_id" + } + +} + +// MARK: - Serialization + +extension Test: JSONDeserializable { + + init?(json: JSONDictionary) { + + guard let caseId = json[JSONKeys.caseId.rawValue] as? Case.Id, + let id = json[JSONKeys.id.rawValue] as? Id, + let priorityId = json[JSONKeys.priorityId.rawValue] as? Priority.Id, + let runId = json[JSONKeys.runId.rawValue] as? Run.Id, + let statusId = json[JSONKeys.statusId.rawValue] as? Status.Id, + let templateId = json[JSONKeys.templateId.rawValue] as? Template.Id, + let title = json[JSONKeys.title.rawValue] as? String, + let typeId = json[JSONKeys.typeId.rawValue] as? CaseType.Id else { + return nil + } + + let assignedtoId = json[JSONKeys.assignedtoId.rawValue] as? User.Id ?? nil + let estimate = json[JSONKeys.estimate.rawValue] as? String ?? nil + let estimateForecast = json[JSONKeys.estimateForecast.rawValue] as? String ?? nil + let milestoneId = json[JSONKeys.milestoneId.rawValue] as? Milestone.Id ?? nil + let refs = json[JSONKeys.refs.rawValue] as? String ?? nil + + let customFieldsContainer = CustomFieldsContainer(json: json) + + self.init(assignedtoId: assignedtoId, caseId: caseId, estimate: estimate, estimateForecast: estimateForecast, id: id, milestoneId: milestoneId, priorityId: priorityId, refs: refs, runId: runId, statusId: statusId, templateId: templateId, title: title, typeId: typeId, customFieldsContainer: customFieldsContainer) + } + +} + +extension Test: JSONSerializable { + + private var serializedProperties: JSONDictionary { + return [JSONKeys.assignedtoId.rawValue: assignedtoId as Any, + JSONKeys.caseId.rawValue: caseId, + JSONKeys.estimate.rawValue: estimate as Any, + JSONKeys.estimateForecast.rawValue: estimateForecast as Any, + JSONKeys.id.rawValue: id, + JSONKeys.milestoneId.rawValue: milestoneId as Any, + JSONKeys.priorityId.rawValue: priorityId, + JSONKeys.refs.rawValue: refs as Any, + JSONKeys.runId.rawValue: runId, + JSONKeys.statusId.rawValue: statusId, + JSONKeys.templateId.rawValue: templateId, + JSONKeys.title.rawValue: title, + JSONKeys.typeId.rawValue: typeId] + } + + func serialized() -> JSONDictionary { + var json = serializedProperties + customFields.forEach { item in json[item.key] = item.value } + return json + } + +} diff --git a/QuizTrain/Models/Types/CustomFieldType.swift b/QuizTrain/Models/Types/CustomFieldType.swift new file mode 100644 index 0000000..bd23d85 --- /dev/null +++ b/QuizTrain/Models/Types/CustomFieldType.swift @@ -0,0 +1,48 @@ +public enum CustomFieldType: Int { + case string = 1 + case integer = 2 + case text = 3 + case url = 4 + case checkbox = 5 + case dropdown = 6 + case user = 7 + case date = 8 + case milestone = 9 + case steps = 10 + case stepResults = 11 + case multiSelect = 12 +} + +extension CustomFieldType { + + // swiftlint:disable:next cyclomatic_complexity + public func description() -> String { + switch self { + case .string: + return "String" // String? + case .integer: + return "Integer" // Int? + case .text: + return "Text" // String? + case .url: + return "URL" // String? ("http://www.venmo.com/") + case .checkbox: + return "Checkbox" // Bool + case .dropdown: + return "Dropdown" // Int? + case .user: + return "User" // Int? + case .date: + return "Date" // String? ("10/17/2017") + case .milestone: + return "Milestone" // Int? + case .steps: + return "Steps" // String? + case .stepResults: + return "Step Results" // [[String: Any]] (unknown if this can be optional) + case .multiSelect: + return "Multi-Select" // [Int] + } + } + +} diff --git a/QuizTrain/Models/Types/Project.SuiteMode.swift b/QuizTrain/Models/Types/Project.SuiteMode.swift new file mode 100644 index 0000000..584b000 --- /dev/null +++ b/QuizTrain/Models/Types/Project.SuiteMode.swift @@ -0,0 +1,20 @@ +extension Project { + public enum SuiteMode: Int { + case singleSuite = 1 + case singleSuitePlusBaselines = 2 + case multipleSuites = 3 + } +} + +extension Project.SuiteMode { + public func description() -> String { + switch self { + case .singleSuite: + return "Single Suite" + case .singleSuitePlusBaselines: + return "Single Suite Plus Baselines" + case .multipleSuites: + return "Multiple Suites" + } + } +} diff --git a/QuizTrain/Models/User.swift b/QuizTrain/Models/User.swift new file mode 100644 index 0000000..1096a77 --- /dev/null +++ b/QuizTrain/Models/User.swift @@ -0,0 +1,62 @@ +public struct User: Identifiable { + public typealias Id = Int + public let email: String + public let id: Id + public let isActive: Bool + public let name: String +} + +// MARK: - Equatable + +extension User: Equatable { + + public static func==(lhs: User, rhs: User) -> Bool { + return (lhs.email == rhs.email && + lhs.id == rhs.id && + lhs.isActive == rhs.isActive && + lhs.name == rhs.name) + } + +} + +// MARK: - JSON Keys + +extension User { + + enum JSONKeys: JSONKey { + case email + case id + case isActive = "is_active" + case name + } + +} + +// MARK: - Serialization + +extension User: JSONDeserializable { + + init?(json: JSONDictionary) { + + guard let email = json[JSONKeys.email.rawValue] as? String, + let id = json[JSONKeys.id.rawValue] as? Id, + let isActive = json[JSONKeys.isActive.rawValue] as? Bool, + let name = json[JSONKeys.name.rawValue] as? String else { + return nil + } + + self.init(email: email, id: id, isActive: isActive, name: name) + } + +} + +extension User: JSONSerializable { + + func serialized() -> JSONDictionary { + return [JSONKeys.email.rawValue: email, + JSONKeys.id.rawValue: id, + JSONKeys.isActive.rawValue: isActive, + JSONKeys.name.rawValue: name] + } + +} diff --git a/QuizTrain/Network/API.swift b/QuizTrain/Network/API.swift new file mode 100644 index 0000000..c4a608e --- /dev/null +++ b/QuizTrain/Network/API.swift @@ -0,0 +1,676 @@ +import Foundation + +/* + Low-level interface to TestRail's API. Handles authentication, API endpoint + configuration, creating requests, and returning Outcome's to the consumer of + this API. This API is dumb and contains no error-handling logic, passing all + errors to its consumer. The consumer is responsible for handling all errors. + + Interfaces in this class accept basic types such as String, Int, Bool, and Data + to create and execute calls to the TestRail API. Naming conventions map to url + schemes rather than Swift naming conventions to make it easier to comprehend + API documentation. + + For a higher level of abstraction use ObjectAPI. + */ +final public class API { + + // MARK: - Properties + + public var username: String // your@email.com + public var secret: String // Password or API Key + public var hostname: String // yourinstance.testrail.net + public var port: Int // 443, 80, 8080, etc + public var scheme: String // "https" or "http" + private let session = URLSession(configuration: .`default`) + + // MARK: - Init + + public init(username: String, secret: String, hostname: String, port: Int = 443, scheme: String = "https") { + self.username = username + self.secret = secret + self.hostname = hostname + self.port = port + self.scheme = scheme + } + + // MARK: - Deinit + + deinit { + session.invalidateAndCancel() + } + + // MARK: - Request Options + + public enum HttpMethod: String { + case get = "GET" + case post = "POST" + } + + // MARK: - URL Components + + private var baseURLComponents: URLComponents { + var components = URLComponents() + components.scheme = scheme + components.host = hostname + components.path = "/index.php" + components.port = port + return components + } + + private func urlComponents(for uri: String, queryItems: [URLQueryItem]? = nil) -> URLComponents { + + var allQueryItems = [URLQueryItem]() + let firstQueryItem = URLQueryItem(name: "/api/v2/" + uri, value: nil) + allQueryItems.append(firstQueryItem) + + if let queryItems = queryItems { + allQueryItems += queryItems + } + + var urlComponents = baseURLComponents + urlComponents.queryItems = allQueryItems + + return urlComponents + } + + // MARK: - Header Fields + + private func authorization() -> String { + let usernamePassword = username + ":" + secret + let usernamePasswordBase64 = Data(usernamePassword.utf8).base64EncodedString() + return "Basic \(usernamePasswordBase64)" + } + + // MARK: - URLRequests + + private func testRailRequest(url: URL, cachePolicy: URLRequest.CachePolicy = .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: TimeInterval = 60.0) -> URLRequest { + var request = URLRequest(url: url, cachePolicy: cachePolicy, timeoutInterval: timeoutInterval) + request.setValue("application/json", forHTTPHeaderField: "Content-Type") // All requests require "application/json" Content-Type, including GETs. + request.setValue(authorization(), forHTTPHeaderField: "Authorization") // All requests require HTTP Basic Authentication. + return request + } + + // MARL: - Errors + + public enum RequestError: Error { + case error(request: URLRequest, error: Error) + case nilResponse(request: URLRequest) + case invalidResponse(request: URLRequest, response: URLResponse) + } + + // MARK: - Results + + public struct RequestResult { + public let request: URLRequest // Initial unaltered request. Note this will contain the HTTP Basic Authentication credentials. This info is useful for troubleshooting. + public let response: HTTPURLResponse // Response. + public let data: Data // Response data. This will be empty if none was returned. + } + + // MARK: - Outcomes + + public typealias RequestOutcome = Outcome + + // MARK: - Base Requests + + @discardableResult public func get(_ uri: String, queryItems: [URLQueryItem]? = nil, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return request(uri, queryItems: queryItems, httpMethod: .get, completionHandler: completionHandler) + } + + @discardableResult public func post(_ uri: String, queryItems: [URLQueryItem]? = nil, data: Data? = nil, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return request(uri, queryItems: queryItems, httpMethod: .post, data: data, completionHandler: completionHandler) + } + + @discardableResult public func request(_ uri: String, queryItems: [URLQueryItem]? = nil, httpMethod: HttpMethod, data: Data? = nil, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + let url = urlComponents(for: uri, queryItems: queryItems).url! + return request(url: url, httpMethod: httpMethod, data: data, completionHandler: completionHandler) + } + + private func request(url: URL, httpMethod: HttpMethod, data: Data? = nil, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + + var request = testRailRequest(url: url) + request.httpMethod = httpMethod.rawValue + request.httpBody = data + + let task = session.dataTask(with: request) { (data, response, error) in + + var outcome: RequestOutcome + defer { + completionHandler(outcome) + } + + guard error == nil else { + outcome = .failed(.error(request: request, error: error!)) + return + } + + guard let response = response else { + outcome = .failed(.nilResponse(request: request)) + return + } + + guard let urlResponse = response as? HTTPURLResponse else { + outcome = .failed(.invalidResponse(request: request, response: response)) + return + } + + let data = data ?? Data() + + outcome = .succeeded(RequestResult(request: request, response: urlResponse, data: data)) + } + + task.resume() + return task + } + + // MARK: - Cases + + /* + http://docs.gurock.com/testrail-api2/reference-cases#add_case + */ + @discardableResult public func addCase(sectionId: Int, data: Data, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("add_case/\(sectionId)", data: data, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-cases#delete_case + */ + @discardableResult public func deleteCase(caseId: Int, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("delete_case/\(caseId)", completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-cases#get_case + */ + @discardableResult public func getCase(caseId: Int, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_case/\(caseId)", completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-cases#get_cases + */ + @discardableResult public func getCases(projectId: Int, suiteId: Int? = nil, sectionId: Int? = nil, filters: [Filter]? = nil, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + + var queryItems = [URLQueryItem]() + + if let suiteId = suiteId { + queryItems.append(URLQueryItem(name: "suite_id", value: String(suiteId))) + } + + if let sectionId = sectionId { + queryItems.append(URLQueryItem(name: "section_id", value: String(sectionId))) + } + + if let filters = filters { + _ = filters.flatMap { queryItems.append($0.queryItem) } + } + + return get("get_cases/\(projectId)", queryItems: queryItems, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-cases#update_case + */ + @discardableResult public func updateCase(caseId: Int, data: Data, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("update_case/\(caseId)", data: data, completionHandler: completionHandler) + } + + // MARK: - Case Fields + + /* + http://docs.gurock.com/testrail-api2/reference-cases-fields#get_case_fields + */ + @discardableResult public func getCaseFields(completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_case_fields", completionHandler: completionHandler) + } + + // MARK: - Case Types + + /* + http://docs.gurock.com/testrail-api2/reference-cases-types#get_case_types + */ + @discardableResult public func getCaseTypes(completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_case_types", completionHandler: completionHandler) + } + + // MARK: - Configurations + + /* + http://docs.gurock.com/testrail-api2/reference-configs#add_config + */ + @discardableResult public func addConfiguration(configurationGroupId: Int, data: Data, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("add_config/\(configurationGroupId)", data: data, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-configs#delete_config + */ + @discardableResult public func deleteConfiguration(configurationId: Int, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("delete_config/\(configurationId)", completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-configs#get_configs + */ + @discardableResult public func getConfigurations(projectId: Int, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_configs/\(projectId)", completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-configs#update_config + */ + @discardableResult public func updateConfiguration(configurationId: Int, data: Data, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("update_config/\(configurationId)", data: data, completionHandler: completionHandler) + } + + // MARK: - Configuration Groups + + /* + http://docs.gurock.com/testrail-api2/reference-configs#add_config_group + */ + @discardableResult public func addConfigurationGroup(projectId: Int, data: Data, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("add_config_group/\(projectId)", data: data, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-configs#delete_config_group + */ + @discardableResult public func deleteConfigurationGroup(configurationGroupId: Int, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("delete_config_group/\(configurationGroupId)", completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-configs#update_config_group + */ + @discardableResult public func updateConfigurationGroup(configurationGroupId: Int, data: Data, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("update_config_group/\(configurationGroupId)", data: data, completionHandler: completionHandler) + } + + // MARK: - Milestones + + /* + http://docs.gurock.com/testrail-api2/reference-milestones#add_milestone + */ + @discardableResult public func addMilestone(projectId: Int, data: Data, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("add_milestone/\(projectId)", data: data, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-milestones#delete_milestone + */ + @discardableResult public func deleteMilestone(milestoneId: Int, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("delete_milestone/\(milestoneId)", completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-milestones#get_milestone + */ + @discardableResult public func getMilestone(milestoneId: Int, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_milestone/\(milestoneId)", completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-milestones#get_milestones + */ + @discardableResult public func getMilestones(projectId: Int, filters: [Filter]? = nil, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_milestones/\(projectId)", queryItems: Filter.queryItems(for: filters), completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-milestones#update_milestone + */ + @discardableResult public func updateMilestone(milestoneId: Int, data: Data, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("update_milestone/\(milestoneId)", data: data, completionHandler: completionHandler) + } + + // MARK: - Plans + + /* + http://docs.gurock.com/testrail-api2/reference-plans#add_plan + */ + @discardableResult public func addPlan(projectId: Int, data: Data, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("add_plan/\(projectId)", data: data, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-plans#close_plan + */ + @discardableResult public func closePlan(planId: Int, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("close_plan/\(planId)", completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-plans#delete_plan + */ + @discardableResult public func deletePlan(planId: Int, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("delete_plan/\(planId)", completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-plans#get_plan + */ + @discardableResult public func getPlan(planId: Int, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_plan/\(planId)", completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-plans#get_plans + */ + @discardableResult public func getPlans(projectId: Int, filters: [Filter]? = nil, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_plans/\(projectId)", queryItems: Filter.queryItems(for: filters), completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-plans#update_plan + */ + @discardableResult public func updatePlan(planId: Int, data: Data, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("update_plan/\(planId)", data: data, completionHandler: completionHandler) + } + + // MARK: - Plan Entries + + /* + http://docs.gurock.com/testrail-api2/reference-plans#add_plan_entry + */ + @discardableResult public func addPlanEntry(planId: Int, data: Data, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("add_plan_entry/\(planId)", data: data, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-plans#delete_plan_entry + */ + @discardableResult public func deletePlanEntry(planId: Int, planEntryId: String, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("delete_plan_entry/\(planId)/\(planEntryId)", completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-plans#update_plan_entry + */ + @discardableResult public func updatePlanEntry(planId: Int, planEntryId: String, data: Data, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("update_plan_entry/\(planId)/\(planEntryId)", data: data, completionHandler: completionHandler) + } + + // MARK: - Priorities + + /* + http://docs.gurock.com/testrail-api2/reference-priorities#get_priorities + */ + @discardableResult public func getPriorities(completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_priorities", completionHandler: completionHandler) + } + + // MARK: - Projects + + /* + http://docs.gurock.com/testrail-api2/reference-projects#add_project + */ + @discardableResult public func addProject(data: Data, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("add_project", data: data, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-projects#delete_project + */ + @discardableResult public func deleteProject(projectId: Int, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("delete_project/\(projectId)", completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-projects#get_project + */ + @discardableResult public func getProject(projectId: Int, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_project/\(projectId)", completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-projects#get_projects + */ + @discardableResult public func getProjects(filters: [Filter]? = nil, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_projects", queryItems: Filter.queryItems(for: filters), completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-projects#update_project + */ + @discardableResult public func updateProject(projectId: Int, data: Data, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("update_project/\(projectId)", data: data, completionHandler: completionHandler) + } + + // MARK: - Results + + /* + http://docs.gurock.com/testrail-api2/reference-results#add_result + */ + @discardableResult public func addResult(testId: Int, data: Data, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("add_result/\(testId)", data: data, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-results#add_result_for_case + */ + @discardableResult public func addResultForCase(runId: Int, caseId: Int, data: Data, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("add_result_for_case/\(runId)/\(caseId)", data: data, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-results#add_results + */ + @discardableResult public func addResults(runId: Int, data: Data, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("add_results/\(runId)", data: data, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-results#add_results_for_cases + */ + @discardableResult public func addResultsForCases(runId: Int, data: Data, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("add_results_for_cases/\(runId)", data: data, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-results#get_results + */ + @discardableResult public func getResults(testId: Int, filters: [Filter]? = nil, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_results/\(testId)", queryItems: Filter.queryItems(for: filters), completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-results#get_results_for_case + */ + @discardableResult public func getResultsForCase(runId: Int, caseId: Int, filters: [Filter]? = nil, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_results_for_case/\(runId)/\(caseId)", queryItems: Filter.queryItems(for: filters), completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-results#get_results_for_run + */ + @discardableResult public func getResultsForRun(runId: Int, filters: [Filter]? = nil, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_results_for_run/\(runId)", queryItems: Filter.queryItems(for: filters), completionHandler: completionHandler) + } + + // MARK: - Result Fields + + /* + http://docs.gurock.com/testrail-api2/reference-results-fields#get_result_fields + */ + @discardableResult public func getResultFields(completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_result_fields", completionHandler: completionHandler) + } + + // MARK: - Runs + + /* + http://docs.gurock.com/testrail-api2/reference-runs#add_run + */ + @discardableResult public func addRun(projectId: Int, data: Data, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("add_run/\(projectId)", data: data, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-runs#close_run + */ + @discardableResult public func closeRun(runId: Int, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("close_run/\(runId)", completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-runs#delete_run + */ + @discardableResult public func deleteRun(runId: Int, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("delete_run/\(runId)", completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-runs#get_run + */ + @discardableResult public func getRun(runId: Int, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_run/\(runId)", completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-runs#get_runs + */ + @discardableResult public func getRuns(projectId: Int, filters: [Filter]? = nil, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_runs/\(projectId)", queryItems: Filter.queryItems(for: filters), completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-runs#update_run + */ + @discardableResult public func updateRun(runId: Int, data: Data, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("update_run/\(runId)", data: data, completionHandler: completionHandler) + } + + // MARK: - Sections + + /* + http://docs.gurock.com/testrail-api2/reference-sections#add_section + */ + @discardableResult public func addSection(projectId: Int, data: Data, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("add_section/\(projectId)", data: data, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-sections#delete_section + */ + @discardableResult public func deleteSection(sectionId: Int, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("delete_section/\(sectionId)", completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-sections#get_section + */ + @discardableResult public func getSection(sectionId: Int, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_section/\(sectionId)", completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-sections#get_sections + */ + @discardableResult public func getSections(projectId: Int, suiteId: Int? = nil, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + let queryItems = suiteId != nil ? [URLQueryItem(name: "suite_id", value: String(suiteId!))] : nil + return get("get_sections/\(projectId)", queryItems: queryItems, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-sections#update_section + */ + @discardableResult public func updateSection(sectionId: Int, data: Data, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("update_section/\(sectionId)", data: data, completionHandler: completionHandler) + } + + // MARK: - Statuses + + /* + http://docs.gurock.com/testrail-api2/reference-statuses#get_statuses + */ + @discardableResult public func getStatuses(completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_statuses", completionHandler: completionHandler) + } + + // MARK: - Suites + + /* + http://docs.gurock.com/testrail-api2/reference-suites#add_suite + */ + @discardableResult public func addSuite(projectId: Int, data: Data, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("add_suite/\(projectId)", data: data, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-suites#delete_suite + */ + @discardableResult public func deleteSuite(suiteId: Int, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("delete_suite/\(suiteId)", completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-suites#get_suite + */ + @discardableResult public func getSuite(suiteId: Int, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_suite/\(suiteId)", completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-suites#get_suites + */ + @discardableResult public func getSuites(projectId: Int, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_suites/\(projectId)", completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-suites#update_suite + */ + @discardableResult public func updateSuite(suiteId: Int, data: Data, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return post("update_suite/\(suiteId)", data: data, completionHandler: completionHandler) + } + + // MARK: - Templates + + /* + http://docs.gurock.com/testrail-api2/reference-templates#get_templates + */ + @discardableResult public func getTemplates(projectId: Int, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_templates/\(projectId)", completionHandler: completionHandler) + } + + // MARK: - Tests + + /* + http://docs.gurock.com/testrail-api2/reference-tests#get_test + */ + @discardableResult public func getTest(testId: Int, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_test/\(testId)", completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-tests#get_tests + */ + @discardableResult public func getTests(runId: Int, filters: [Filter]? = nil, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_tests/\(runId)", queryItems: Filter.queryItems(for: filters), completionHandler: completionHandler) + } + + // MARK: - Users + + /* + http://docs.gurock.com/testrail-api2/reference-users#get_user + */ + @discardableResult public func getUser(userId: Int, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_user/\(userId)", completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-users#get_user_by_email + */ + @discardableResult public func getUserByEmail(_ email: String, completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + let queryItems = [URLQueryItem(name: "email", value: email)] + return get("get_user_by_email", queryItems: queryItems, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-users#get_users + */ + @discardableResult public func getUsers(completionHandler: @escaping (RequestOutcome) -> Void) -> URLSessionDataTask { + return get("get_users", completionHandler: completionHandler) + } + +} diff --git a/QuizTrain/Network/Extensions/API/API.RequestErrorDebug.swift b/QuizTrain/Network/Extensions/API/API.RequestErrorDebug.swift new file mode 100644 index 0000000..5b115d3 --- /dev/null +++ b/QuizTrain/Network/Extensions/API/API.RequestErrorDebug.swift @@ -0,0 +1,71 @@ +extension API.RequestError: DebugDescription { + + public var debugDescription: String { + + var description: String = "API.RequestError" + + switch self { + case .error(_, _): + description += ".error:\n\n\(debugDetails)\n" + case .invalidResponse(_, _): + description += ".invalidResponse:\n\n\(debugDetails)\n" + case .nilResponse(_): + description += ".nilResponse:\n\n\(debugDetails)\n" + } + + return description + } + +} + +extension API.RequestError: DebugDetails { + + public var debugDetails: String { + + let details: String + + switch self { + case .error(let request, let error): + details = """ + _____ERROR_____ + + \(error) + + _____REQUEST_____ + + \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "") + + \(request.httpHeaderFieldsAsMultiLineString(omittingHeaders: ["AUTHORIZATION"]) ?? "") + + \(request.httpBodyAsUTF8 ?? "") + """ + case .invalidResponse(let request, let response): + details = """ + _____REQUEST_____ + + \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "") + + \(request.httpHeaderFieldsAsMultiLineString(omittingHeaders: ["AUTHORIZATION"]) ?? "") + + \(request.httpBodyAsUTF8 ?? "") + + _____RESPONSE_____ + + \(response) + """ + case .nilResponse(let request): + details = """ + _____REQUEST_____ + + \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "") + + \(request.httpHeaderFieldsAsMultiLineString(omittingHeaders: ["AUTHORIZATION"]) ?? "") + + \(request.httpBodyAsUTF8 ?? "") + """ + } + + return details + } + +} diff --git a/QuizTrain/Network/Extensions/API/API.RequestResultDebug.swift b/QuizTrain/Network/Extensions/API/API.RequestResultDebug.swift new file mode 100644 index 0000000..c3388d1 --- /dev/null +++ b/QuizTrain/Network/Extensions/API/API.RequestResultDebug.swift @@ -0,0 +1,29 @@ +extension API.RequestResult: DebugDescription { + + public var debugDescription: String { + return "API.RequestResult:\n\n\(debugDetails)\n" + } + +} + +extension API.RequestResult: DebugDetails { + + public var debugDetails: String { + return """ + _____REQUEST_____ + + \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "") + + \(request.httpHeaderFieldsAsMultiLineString(omittingHeaders: ["AUTHORIZATION"]) ?? "") + + \(request.httpBodyAsUTF8 ?? "") + + _____RESPONSE_____ + + \(response) + + \(String(data: data, encoding: .utf8) ?? "") + """ + } + +} diff --git a/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.ClientErrorDebug.swift b/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.ClientErrorDebug.swift new file mode 100644 index 0000000..2f143bc --- /dev/null +++ b/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.ClientErrorDebug.swift @@ -0,0 +1,23 @@ +extension ObjectAPI.ClientError: DebugDescription { + + public var debugDescription: String { + return "ObjectAPI.ClientError:\n\n\(debugDetails)\n" + } + +} + +extension ObjectAPI.ClientError: DebugDetails { + + public var debugDetails: String { + return """ + _____DETAILS_____ + + CODE: \(statusCode) + + MESSAGE: \(message) + + \(requestResult.debugDetails) + """ + } + +} diff --git a/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.DataProcessingErrorDebug.swift b/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.DataProcessingErrorDebug.swift new file mode 100644 index 0000000..e6697ff --- /dev/null +++ b/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.DataProcessingErrorDebug.swift @@ -0,0 +1,56 @@ +extension ObjectAPI.DataProcessingError: DebugDescription { + + public var debugDescription: String { + var description = "ObjectAPI.DataProcessingError" + switch self { + case .couldNotConvertDataToJSON(_, _): + description += ".couldNotConvertDataToJSON:\n\n\(debugDetails)\n" + case .couldNotDeserializeFromJSON(_, _): + description += ".couldNotDeserializeFromJSON:\n\n\(debugDetails)\n" + case .invalidJSONFormat(_): + description += ".invalidJSONFormat:\n\n\(debugDetails)\n" + } + return description + } + +} + +extension ObjectAPI.DataProcessingError: DebugDetails { + + public var debugDetails: String { + + let details: String + + switch self { + case .couldNotConvertDataToJSON(let data, let error): + details = """ + _____DATA_____ + + \(data) + + _____ERROR_____ + + \(error) + """ + case .couldNotDeserializeFromJSON(let objectType, let json): + details = """ + _____DETAILS_____ + + Object Type: \(objectType) + + _____JSON_____ + + \(json) + """ + case .invalidJSONFormat(let json): + details = """ + _____JSON_____ + + \(json) + """ + } + + return details + } + +} diff --git a/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.DataRequestErrorDebug.swift b/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.DataRequestErrorDebug.swift new file mode 100644 index 0000000..5aaa19f --- /dev/null +++ b/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.DataRequestErrorDebug.swift @@ -0,0 +1,33 @@ +extension ObjectAPI.DataRequestError: DebugDescription { + + public var debugDescription: String { + var description = "ObjectAPI.DataRequestError" + switch self { + case .apiError(_): + description += ".apiError:\n\n\(debugDetails)\n" + case .dataProcessingError(_): + description += ".dataProcessingError:\n\n\(debugDetails)\n" + case .statusCodeError(_): + description += ".statusCodeError:\n\n\(debugDetails)\n" + } + return description + } + +} + +extension ObjectAPI.DataRequestError: DebugDetails { + + public var debugDetails: String { + let details: String + switch self { + case .apiError(let error): + details = error.debugDetails + case .dataProcessingError(let error): + details = error.debugDetails + case .statusCodeError(let error): + details = error.debugDetails + } + return details + } + +} diff --git a/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.MatchErrorDebug.swift b/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.MatchErrorDebug.swift new file mode 100644 index 0000000..d66e5c5 --- /dev/null +++ b/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.MatchErrorDebug.swift @@ -0,0 +1,59 @@ +extension ObjectAPI.MatchError: DebugDescription { + + var debugDescription: String { + var description = "ObjectAPI.MatchError" + switch self { + case .matchError(_): + description += ".matchError:\n\n\(debugDetails)\n" + case .otherError(_): + description += ".otherError:\n\n\(debugDetails)\n" + } + return description + } + +} + +extension ObjectAPI.MatchError where MatchErrorType: DebugDescription, OtherErrorType: DebugDescription { + + var debugDescription: String { + var description = "ObjectAPI.MatchError" + switch self { + case .matchError(let error): + description += ".matchError: \(error.debugDescription)\n" + case .otherError(let error): + description += ".otherError: \(error.debugDescription)\n" + } + return description + } + +} + +extension ObjectAPI.MatchError: DebugDetails { + + var debugDetails: String { + let details: String + switch self { + case .matchError(let error): + details = "\(error)" + case .otherError(let error): + details = "\(error)" + } + return details + } + +} + +extension ObjectAPI.MatchError where MatchErrorType: DebugDetails, OtherErrorType: DebugDetails { + + var debugDetails: String { + let details: String + switch self { + case .matchError(let error): + details = "\(error.debugDetails)" + case .otherError(let error): + details = "\(error.debugDetails)" + } + return details + } + +} diff --git a/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.ObjectConversionErrorDebug.swift b/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.ObjectConversionErrorDebug.swift new file mode 100644 index 0000000..28a4d8d --- /dev/null +++ b/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.ObjectConversionErrorDebug.swift @@ -0,0 +1,56 @@ +extension ObjectAPI.ObjectConversionError: DebugDescription { + + public var debugDescription: String { + var description = "ObjectAPI.ObjectConversionError" + switch self { + case .couldNotConvertObjectToData(_, _, _): + description += ".couldNotConvertObjectToData:\n\n\(debugDetails)\n" + case .couldNotConvertObjectsToData(_, _, _): + description += ".couldNotConvertObjectsToData:\n\n\(debugDetails)\n" + } + return description + } + +} + +extension ObjectAPI.ObjectConversionError: DebugDetails { + + public var debugDetails: String { + + let details: String + + switch self { + case .couldNotConvertObjectToData(let object, let json, let error): + details = """ + _____OBJECT_____ + + \(object) + + _____JSON_____ + + \(json) + + _____ERROR_____ + + \(error) + """ + case .couldNotConvertObjectsToData(let objects, let json, let error): + details = """ + _____OBJECTS_____ + + \(objects) + + _____JSON_____ + + \(json) + + _____ERROR_____ + + \(error) + """ + } + + return details + } + +} diff --git a/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.RequestErrorDebug.swift b/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.RequestErrorDebug.swift new file mode 100644 index 0000000..53fba09 --- /dev/null +++ b/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.RequestErrorDebug.swift @@ -0,0 +1,29 @@ +extension ObjectAPI.RequestError: DebugDescription { + + public var debugDescription: String { + var description = "ObjectAPI.RequestError" + switch self { + case .apiError(_): + description += ".apiError:\n\n\(debugDetails)\n" + case .statusCodeError(_): + description += ".statusCodeError:\n\n\(debugDetails)\n" + } + return description + } + +} + +extension ObjectAPI.RequestError: DebugDetails { + + public var debugDetails: String { + let details: String + switch self { + case .apiError(let error): + details = error.debugDetails + case .statusCodeError(let error): + details = error.debugDetails + } + return details + } + +} diff --git a/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.ServerErrorDebug.swift b/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.ServerErrorDebug.swift new file mode 100644 index 0000000..65a7d34 --- /dev/null +++ b/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.ServerErrorDebug.swift @@ -0,0 +1,23 @@ +extension ObjectAPI.ServerError: DebugDescription { + + public var debugDescription: String { + return "ObjectAPI.ServerError:\n\n\(debugDetails)\n" + } + +} + +extension ObjectAPI.ServerError: DebugDetails { + + public var debugDetails: String { + return """ + _____DETAILS_____ + + CODE: \(statusCode) + + MESSAGE: \(message) + + \(requestResult.debugDetails) + """ + } + +} diff --git a/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.StatusCodeErrorDebug.swift b/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.StatusCodeErrorDebug.swift new file mode 100644 index 0000000..8902ec7 --- /dev/null +++ b/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.StatusCodeErrorDebug.swift @@ -0,0 +1,33 @@ +extension ObjectAPI.StatusCodeError: DebugDescription { + + public var debugDescription: String { + var description = "ObjectAPI.StatusCodeError" + switch self { + case .clientError(_): + description += ".clientError:\n\n\(debugDetails)\n" + case .otherError(_): + description += ".otherError:\n\n\(debugDetails)\n" + case .serverError(_): + description += ".serverError:\n\n\(debugDetails)\n" + } + return description + } + +} + +extension ObjectAPI.StatusCodeError: DebugDetails { + + public var debugDetails: String { + let details: String + switch self { + case .clientError(let clientError): + details = clientError.debugDetails + case .otherError(let requestResult): + details = requestResult.debugDetails + case .serverError(let serverError): + details = serverError.debugDetails + } + return details + } + +} diff --git a/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.UpdateRequestErrorDebug.swift b/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.UpdateRequestErrorDebug.swift new file mode 100644 index 0000000..448b76f --- /dev/null +++ b/QuizTrain/Network/Extensions/ObjectAPI/ObjectAPI.UpdateRequestErrorDebug.swift @@ -0,0 +1,37 @@ +extension ObjectAPI.UpdateRequestError: DebugDescription { + + public var debugDescription: String { + var description = "ObjectAPI.UpdateRequestError" + switch self { + case .apiError(_): + description += ".apiError:\n\n\(debugDetails)\n" + case .dataProcessingError(_): + description += ".dataProcessingError:\n\n\(debugDetails)\n" + case .objectConversionError(_): + description += ".objectConversionError:\n\n\(debugDetails)\n" + case .statusCodeError(_): + description += ".statusCodeError:\n\n\(debugDetails)\n" + } + return description + } + +} + +extension ObjectAPI.UpdateRequestError: DebugDetails { + + public var debugDetails: String { + let details: String + switch self { + case .apiError(let error): + details = error.debugDetails + case .dataProcessingError(let error): + details = error.debugDetails + case .objectConversionError(error: let error): + details = error.debugDetails + case .statusCodeError(let error): + details = error.debugDetails + } + return details + } + +} diff --git a/QuizTrain/Network/Extensions/URLRequestDebug.swift b/QuizTrain/Network/Extensions/URLRequestDebug.swift new file mode 100644 index 0000000..2a2e10c --- /dev/null +++ b/QuizTrain/Network/Extensions/URLRequestDebug.swift @@ -0,0 +1,44 @@ +extension URLRequest { + + public var httpBodyAsUTF8: String? { + guard let httpBody = self.httpBody else { + return nil + } + return String(data: httpBody, encoding: .utf8) ?? nil + } + + public var httpHeaderFieldsAsMultiLineString: String? { + return httpHeaderFieldsAsMultiLineString(omittingHeaders: []) + } + + /* + Pass |omittedHeaders| to omit any headers. For example ["AUTHORIZATION"] + if you wish to display/print to logs without leaking sensitive credentials. + Comparison is performed lowercased() per RFC 7230. + */ + public func httpHeaderFieldsAsMultiLineString(omittingHeaders omittedHeaders: [String]) -> String? { + + guard let allHTTPHeaderFields = self.allHTTPHeaderFields else { + return nil + } + + var httpHeaderFields = "" + + for (field, value) in allHTTPHeaderFields { + + // Headers are case-insensitive per RFC 7230. + guard omittedHeaders.filter({ $0.lowercased() == field.lowercased() }).count == 0 else { + continue + } + + if httpHeaderFields.count > 0 { + httpHeaderFields += "\n" + } + + httpHeaderFields += "\(field): \(value)" + } + + return httpHeaderFields + } + +} diff --git a/QuizTrain/Network/Filters/Filter.Value.swift b/QuizTrain/Network/Filters/Filter.Value.swift new file mode 100644 index 0000000..77a2ae4 --- /dev/null +++ b/QuizTrain/Network/Filters/Filter.Value.swift @@ -0,0 +1,59 @@ +extension Filter { + + /* + Value types accepted by the TestRail API used in filters. + */ + public enum Value { + case bool(Bool) + case int(Int) + case intList([Int]) + case timestamp(Date) + } + +} + +extension Filter.Value: Equatable { + + public static func==(lhs: Filter.Value, rhs: Filter.Value) -> Bool { + switch lhs { + case .bool(let lhsBool): + guard case let .bool(rhsBool) = rhs else { + return false + } + return lhsBool == rhsBool + case .int(let lhsInt): + guard case let .int(rhsInt) = rhs else { + return false + } + return lhsInt == rhsInt + case .intList(let lhsIntList): + guard case let .intList(rhsIntList) = rhs else { + return false + } + return lhsIntList == rhsIntList + case .timestamp(let lhsDate): + guard case let .timestamp(rhsDate) = rhs else { + return false + } + return lhsDate.secondsSince1970 == rhsDate.secondsSince1970 + } + } + +} + +extension Filter.Value { + + public var string: String { + switch self { + case .bool(let bool): + return String(bool ? 1 : 0) + case .int(let int): + return String(int) + case .intList(let intList): + return intList.flatMap({String($0)}).joined(separator: ",") // [38, 208, 21, 324] ---> "38,208,21,324" + case .timestamp(let date): + return String(date.secondsSince1970) // Unix Timestamp as a whole number + } + } + +} diff --git a/QuizTrain/Network/Filters/Filter.swift b/QuizTrain/Network/Filters/Filter.swift new file mode 100644 index 0000000..ac37bb7 --- /dev/null +++ b/QuizTrain/Network/Filters/Filter.swift @@ -0,0 +1,54 @@ +/* + Filter results returned by the TestRail API. + + See TestRail's API documentation for allowed name/value pairs, chaining of + filters, and general usage: http://docs.gurock.com/testrail-api2/start + */ +public struct Filter { + + public var name: String + public var value: Filter.Value + + fileprivate init(name: String, value: Filter.Value) { + self.name = name + self.value = value + } + +} + +extension Filter { + + public init(named name: String, matching value: Bool) { + self.init(name: name, value: .bool(value)) + } + + public init(named name: String, matching value: Date) { + self.init(name: name, value: .timestamp(value)) + } + + public init(named name: String, matching value: Int) { + self.init(name: name, value: .int(value)) + } + + public init(named name: String, matching value: [Int]) { + self.init(name: name, value: .intList(value)) + } + +} + +extension Filter: Equatable { + + public static func==(lhs: Filter, rhs: Filter) -> Bool { + return (lhs.name == rhs.name && + lhs.value == rhs.value) + } + +} + +extension Filter: QueryItemProvider { + + public var queryItem: URLQueryItem { + return URLQueryItem(name: name, value: value.string) + } + +} diff --git a/QuizTrain/Network/Models/Add/NewCase.swift b/QuizTrain/Network/Models/Add/NewCase.swift new file mode 100644 index 0000000..24d1f1b --- /dev/null +++ b/QuizTrain/Network/Models/Add/NewCase.swift @@ -0,0 +1,102 @@ +public struct NewCase: MutableCustomFields { + + // MARK: Properties + + public var estimate: String? + public var milestoneId: Milestone.Id? + public var priorityId: Priority.Id? + public var refs: String? + public var templateId: Template.Id? + public var title: String + public var typeId: CaseType.Id? + var customFieldsContainer = CustomFieldsContainer.empty() + + // MARK: Init + + public init(estimate: String? = nil, milestoneId: Milestone.Id? = nil, priorityId: Priority.Id? = nil, refs: String? = nil, templateId: Template.Id? = nil, title: String, typeId: CaseType.Id? = nil, customFields: JSONDictionary? = nil) { + self.estimate = estimate + self.milestoneId = milestoneId + self.priorityId = priorityId + self.refs = refs + self.templateId = templateId + self.title = title + self.typeId = typeId + if let customFields = customFields { + customFieldsContainer.customFields = customFields + } + } + +} + +// MARK: - Equatable + +extension NewCase: Equatable { + + public static func==(lhs: NewCase, rhs: NewCase) -> Bool { + return (lhs.estimate == rhs.estimate && + lhs.milestoneId == rhs.milestoneId && + lhs.priorityId == rhs.priorityId && + lhs.refs == rhs.refs && + lhs.templateId == rhs.templateId && + lhs.title == rhs.title && + lhs.typeId == rhs.typeId && + lhs.customFieldsContainer == rhs.customFieldsContainer) + } + +} + +// MARK: - JSON Keys + +extension NewCase { + + enum JSONKeys: JSONKey { + case estimate + case milestoneId = "milestone_id" + case priorityId = "priority_id" + case refs + case templateId = "template_id" + case title + case typeId = "type_id" + } + +} + +extension NewCase: AddRequestJSONKeys { + + var addRequestJSONKeys: [JSONKey] { + var keys = [JSONKeys.estimate.rawValue, + JSONKeys.milestoneId.rawValue, + JSONKeys.priorityId.rawValue, + JSONKeys.refs.rawValue, + JSONKeys.templateId.rawValue, + JSONKeys.title.rawValue, + JSONKeys.typeId.rawValue] + customFields.forEach { item in keys.append(item.key) } + return keys + } + +} + +// MARK: - Serialization + +extension NewCase: JSONSerializable { + + private var serializedProperties: JSONDictionary { + return [JSONKeys.estimate.rawValue: estimate as Any, + JSONKeys.milestoneId.rawValue: milestoneId as Any, + JSONKeys.priorityId.rawValue: priorityId as Any, + JSONKeys.refs.rawValue: refs as Any, + JSONKeys.templateId.rawValue: templateId as Any, + JSONKeys.title.rawValue: title, + JSONKeys.typeId.rawValue: typeId as Any] + } + + func serialized() -> JSONDictionary { + var json = serializedProperties + customFields.forEach { item in json[item.key] = item.value } + return json + } + +} + +extension NewCase: AddRequestJSON { } diff --git a/QuizTrain/Network/Models/Add/NewCaseResults.Result.swift b/QuizTrain/Network/Models/Add/NewCaseResults.Result.swift new file mode 100644 index 0000000..566e9b0 --- /dev/null +++ b/QuizTrain/Network/Models/Add/NewCaseResults.Result.swift @@ -0,0 +1,120 @@ +extension NewCaseResults { + + public struct Result: MutableCustomFields { + + // MARK: Properties + + public var assignedtoId: User.Id? + public var caseId: Case.Id + public var comment: String? + public var defects: String? + public var elapsed: String? + public var statusId: Status.Id? + public var version: String? + var customFieldsContainer = CustomFieldsContainer.empty() + + // MARK: Init + + public init(assignedtoId: User.Id? = nil, caseId: Case.Id, comment: String? = nil, defects: String? = nil, elapsed: String? = nil, statusId: Status.Id? = nil, version: String? = nil, customFields: JSONDictionary? = nil) { + self.assignedtoId = assignedtoId + self.caseId = caseId + self.comment = comment + self.defects = defects + self.elapsed = elapsed + self.statusId = statusId + self.version = version + if let customFields = customFields { + customFieldsContainer.customFields = customFields + } + } + + } + +} + +// MARK: - Equatable + +extension NewCaseResults.Result: Equatable { + + public static func==(lhs: NewCaseResults.Result, rhs: NewCaseResults.Result) -> Bool { + return (lhs.assignedtoId == rhs.assignedtoId && + lhs.caseId == rhs.caseId && + lhs.comment == rhs.comment && + lhs.defects == rhs.defects && + lhs.elapsed == rhs.elapsed && + lhs.statusId == rhs.statusId && + lhs.version == rhs.version && + lhs.customFieldsContainer == rhs.customFieldsContainer) + } + +} + +// MARK: - Validatable + +extension NewCaseResults.Result: Validatable { + + /* + A result is valid if it's assigned and/or commented on and/or is given a + status. + */ + var isValid: Bool { + return (assignedtoId != nil || comment != nil || statusId != nil) + } + +} + +// MARK: - JSON Keys + +extension NewCaseResults.Result { + + enum JSONKeys: JSONKey { + case assignedtoId = "assignedto_id" + case caseId = "case_id" + case comment + case defects + case elapsed + case statusId = "status_id" + case version + } + +} + +extension NewCaseResults.Result: AddRequestJSONKeys { + + var addRequestJSONKeys: [JSONKey] { + var keys = [JSONKeys.assignedtoId.rawValue, + JSONKeys.caseId.rawValue, + JSONKeys.comment.rawValue, + JSONKeys.defects.rawValue, + JSONKeys.elapsed.rawValue, + JSONKeys.statusId.rawValue, + JSONKeys.version.rawValue] + customFields.forEach { item in keys.append(item.key) } + return keys + } + +} + +// MARK: - Serialization + +extension NewCaseResults.Result: JSONSerializable { + + private var serializedProperties: JSONDictionary { + return [JSONKeys.assignedtoId.rawValue: assignedtoId as Any, + JSONKeys.caseId.rawValue: caseId, + JSONKeys.comment.rawValue: comment as Any, + JSONKeys.defects.rawValue: defects as Any, + JSONKeys.elapsed.rawValue: elapsed as Any, + JSONKeys.statusId.rawValue: statusId as Any, + JSONKeys.version.rawValue: version as Any] + } + + func serialized() -> JSONDictionary { + var json = serializedProperties + customFields.forEach { item in json[item.key] = item.value } + return json + } + +} + +extension NewCaseResults.Result: AddRequestJSON { } diff --git a/QuizTrain/Network/Models/Add/NewCaseResults.swift b/QuizTrain/Network/Models/Add/NewCaseResults.swift new file mode 100644 index 0000000..d297949 --- /dev/null +++ b/QuizTrain/Network/Models/Add/NewCaseResults.swift @@ -0,0 +1,62 @@ +/* + Use to bulk-add multiple results associated with Cases. + */ +public struct NewCaseResults { + + public var results: [NewCaseResults.Result] + + public init(results: [NewCaseResults.Result]) { + self.results = results + } + +} + +// MARK: - Equatable + +extension NewCaseResults: Equatable { + + public static func==(lhs: NewCaseResults, rhs: NewCaseResults) -> Bool { + return (lhs.results.contentsAreEqual(to: rhs.results)) + } + +} + +// MARK: - Validatable + +extension NewCaseResults: Validatable { + + var isValid: Bool { + return (results.filter({ $0.isValid == false }).count == 0) + } + +} + +// MARK: - JSON Keys + +extension NewCaseResults { + + enum JSONKeys: JSONKey { + case results + } + +} + +extension NewCaseResults: AddRequestJSONKeys { + + var addRequestJSONKeys: [JSONKey] { + return [JSONKeys.results.rawValue] + } + +} + +// MARK: - Serialization + +extension NewCaseResults: JSONSerializable { + + func serialized() -> JSONDictionary { + return [JSONKeys.results.rawValue: NewCaseResults.Result.serialized(results)] + } + +} + +extension NewCaseResults: AddRequestJSON { } diff --git a/QuizTrain/Network/Models/Add/NewConfiguration.swift b/QuizTrain/Network/Models/Add/NewConfiguration.swift new file mode 100644 index 0000000..86b9c58 --- /dev/null +++ b/QuizTrain/Network/Models/Add/NewConfiguration.swift @@ -0,0 +1,49 @@ +public struct NewConfiguration { + + public var name: String + + public init(name: String) { + self.name = name + } + +} + +// MARK: - Equatable + +extension NewConfiguration: Equatable { + + public static func==(lhs: NewConfiguration, rhs: NewConfiguration) -> Bool { + return (lhs.name == rhs.name) + } + +} + +// MARK: - JSON Keys + +extension NewConfiguration { + + enum JSONKeys: JSONKey { + case name + } + +} + +extension NewConfiguration: AddRequestJSONKeys { + + var addRequestJSONKeys: [JSONKey] { + return [JSONKeys.name.rawValue] + } + +} + +// MARK: - Serialization + +extension NewConfiguration: JSONSerializable { + + func serialized() -> JSONDictionary { + return [JSONKeys.name.rawValue: name] + } + +} + +extension NewConfiguration: AddRequestJSON { } diff --git a/QuizTrain/Network/Models/Add/NewConfigurationGroup.swift b/QuizTrain/Network/Models/Add/NewConfigurationGroup.swift new file mode 100644 index 0000000..bfbd401 --- /dev/null +++ b/QuizTrain/Network/Models/Add/NewConfigurationGroup.swift @@ -0,0 +1,49 @@ +public struct NewConfigurationGroup { + + public var name: String + + public init(name: String) { + self.name = name + } + +} + +// MARK: - Equatable + +extension NewConfigurationGroup: Equatable { + + public static func==(lhs: NewConfigurationGroup, rhs: NewConfigurationGroup) -> Bool { + return (lhs.name == rhs.name) + } + +} + +// MARK: - JSON Keys + +extension NewConfigurationGroup { + + enum JSONKeys: JSONKey { + case name + } + +} + +extension NewConfigurationGroup: AddRequestJSONKeys { + + var addRequestJSONKeys: [JSONKey] { + return [JSONKeys.name.rawValue] + } + +} + +// MARK: - Serialization + +extension NewConfigurationGroup: JSONSerializable { + + func serialized() -> JSONDictionary { + return [JSONKeys.name.rawValue: name] + } + +} + +extension NewConfigurationGroup: AddRequestJSON { } diff --git a/QuizTrain/Network/Models/Add/NewMilestone.swift b/QuizTrain/Network/Models/Add/NewMilestone.swift new file mode 100644 index 0000000..0face03 --- /dev/null +++ b/QuizTrain/Network/Models/Add/NewMilestone.swift @@ -0,0 +1,73 @@ +public struct NewMilestone { + + public var description: String? + public var dueOn: Date? + public var name: String + public var parentId: Milestone.Id? + public var startOn: Date? + + public init(description: String? = nil, dueOn: Date? = nil, name: String, parentId: Milestone.Id? = nil, startOn: Date? = nil) { + self.description = description + self.dueOn = dueOn + self.name = name + self.parentId = parentId + self.startOn = startOn + } + +} + +// MARK: - Equatable + +extension NewMilestone: Equatable { + + public static func==(lhs: NewMilestone, rhs: NewMilestone) -> Bool { + return (lhs.description == rhs.description && + lhs.dueOn?.secondsSince1970 == rhs.dueOn?.secondsSince1970 && + lhs.name == rhs.name && + lhs.parentId == rhs.parentId && + lhs.startOn?.secondsSince1970 == rhs.startOn?.secondsSince1970) + } + +} + +// MARK: - JSON Keys + +extension NewMilestone { + + enum JSONKeys: JSONKey { + case description + case dueOn = "due_on" + case name + case parentId = "parent_id" + case startOn = "start_on" + } + +} + +extension NewMilestone: AddRequestJSONKeys { + + var addRequestJSONKeys: [JSONKey] { + return [JSONKeys.description.rawValue, + JSONKeys.dueOn.rawValue, + JSONKeys.name.rawValue, + JSONKeys.parentId.rawValue, + JSONKeys.startOn.rawValue] + } + +} + +// MARK: - Serialization + +extension NewMilestone: JSONSerializable { + + func serialized() -> JSONDictionary { + return [JSONKeys.description.rawValue: description as Any, + JSONKeys.dueOn.rawValue: dueOn?.secondsSince1970 as Any, + JSONKeys.name.rawValue: name, + JSONKeys.parentId.rawValue: parentId as Any, + JSONKeys.startOn.rawValue: startOn?.secondsSince1970 as Any] + } + +} + +extension NewMilestone: AddRequestJSON { } diff --git a/QuizTrain/Network/Models/Add/NewPlan.Entry.Run.swift b/QuizTrain/Network/Models/Add/NewPlan.Entry.Run.swift new file mode 100644 index 0000000..8802f3a --- /dev/null +++ b/QuizTrain/Network/Models/Add/NewPlan.Entry.Run.swift @@ -0,0 +1,120 @@ +/* + Each NewPlan.Entry.Run can be assigned to zero or more Configuration's so long + as there is no more than one Configuration per ConfigurationGroup. For example: + + - Group1 + - ConfigurationA.id = 100 + - ConfigurationB.id = 101 + - Group2 + - ConfigurationC.id = 200 + - ConfigurationD.id = 201 + + Valid NewPlan.Entry.Run.configIds values include: + + nil // No configurations will be used. + [] // No configurations will be used. + [100] // ConfigurationA will be used. + [100, 200] // ConfigurationA and ConfigurationC will be used. + [100, 201] // ConfigurationA and ConfigurationD will be used. + + Invalid values include: + + [100, 101] // Both are from Group1. + [200, 202] // Both are from Group2. + [100, 200, 201] // 200 and 201 are from Group2. + */ +extension NewPlan.Entry { + + public struct Run { + + public var assignedtoId: User.Id? // Overrides NewPlan.Entry.assignedtoId. + public var caseIds: [Case.Id]? // Overrides NewPlan.Entry.caseIds. + public var configIds: [Configuration.Id]? // Zero, one, or many Configuration.id's. Only one Configuration.id per ConfigurationGroup is allowed. + public var description: String? // Overrides NewPlan.Entry.caseIds.description. + public var includeAll: Bool? // Overrides NewPlan.Entry.includeAll. + public var milestoneId: Milestone.Id? // Milestone for the run. + public var name: String? // Overrides NewPlan.Entry.name + public var suiteId: Suite.Id? // Overrides NewPlan.Entry.suiteId + + public init(assignedtoId: User.Id? = nil, caseIds: [Case.Id]? = nil, configIds: [Configuration.Id]? = nil, description: String? = nil, includeAll: Bool? = nil, milestoneId: Milestone.Id? = nil, name: String? = nil, suiteId: Suite.Id? = nil) { + self.assignedtoId = assignedtoId + self.caseIds = caseIds + self.configIds = configIds + self.description = description + self.includeAll = includeAll + self.milestoneId = milestoneId + self.name = name + self.suiteId = suiteId + } + + } + +} + +// MARK: - Equatable + +extension NewPlan.Entry.Run: Equatable { + + public static func==(lhs: NewPlan.Entry.Run, rhs: NewPlan.Entry.Run) -> Bool { + return (lhs.assignedtoId == rhs.assignedtoId && + lhs.caseIds?.sorted() == rhs.caseIds?.sorted() && + lhs.configIds?.sorted() == rhs.configIds?.sorted() && + lhs.description == rhs.description && + lhs.includeAll == rhs.includeAll && + lhs.milestoneId == rhs.milestoneId && + lhs.name == rhs.name && + lhs.suiteId == rhs.suiteId) + } + +} + +// MARK: - JSON Keys + +extension NewPlan.Entry.Run { + + enum JSONKeys: JSONKey { + case assignedtoId = "assignedto_id" + case caseIds = "case_ids" + case configIds = "config_ids" + case description + case includeAll = "include_all" + case milestoneId = "milestone_id" + case name + case suiteId = "suite_id" + } + +} + +extension NewPlan.Entry.Run: AddRequestJSONKeys { + + var addRequestJSONKeys: [JSONKey] { + return [JSONKeys.assignedtoId.rawValue, + JSONKeys.caseIds.rawValue, + JSONKeys.configIds.rawValue, + JSONKeys.description.rawValue, + JSONKeys.includeAll.rawValue, + JSONKeys.milestoneId.rawValue, + JSONKeys.name.rawValue, + JSONKeys.suiteId.rawValue] + } + +} + +// MARK: - Serialization + +extension NewPlan.Entry.Run: JSONSerializable { + + func serialized() -> JSONDictionary { + return [JSONKeys.assignedtoId.rawValue: assignedtoId as Any, + JSONKeys.caseIds.rawValue: caseIds as Any, + JSONKeys.configIds.rawValue: configIds as Any, + JSONKeys.description.rawValue: description as Any, + JSONKeys.includeAll.rawValue: includeAll as Any, + JSONKeys.milestoneId.rawValue: milestoneId as Any, + JSONKeys.name.rawValue: name as Any, + JSONKeys.suiteId.rawValue: suiteId as Any] + } + +} + +extension NewPlan.Entry.Run: AddRequestJSON { } diff --git a/QuizTrain/Network/Models/Add/NewPlan.Entry.swift b/QuizTrain/Network/Models/Add/NewPlan.Entry.swift new file mode 100644 index 0000000..bc1def4 --- /dev/null +++ b/QuizTrain/Network/Models/Add/NewPlan.Entry.swift @@ -0,0 +1,131 @@ +extension NewPlan { + + public struct Entry { + + public var assignedtoId: User.Id? // Default for all runs with a nil assignedtoId. + public var caseIds: [Case.Id]? // Default for all runs with a nil or empty caseIds. + public var description: String? // Default for all runs with a nil description. + public var includeAll: Bool? // Default for all runs with a nil includeAll. + public var name: String? // Default for all runs with a nil name. + public var runs: [NewPlan.Entry.Run]? // If nil TestRail will return a single run including either all tests (includeAll), or specific tests (caseIds), from the suiteId without using configurations. + public var suiteId: Suite.Id // Default for all runs with a nil suiteId. + + public init(assignedtoId: User.Id? = nil, caseIds: [Case.Id]? = nil, description: String? = nil, includeAll: Bool? = nil, name: String? = nil, runs: [NewPlan.Entry.Run]? = nil, suiteId: Suite.Id) { + self.assignedtoId = assignedtoId + self.caseIds = caseIds + self.description = description + self.includeAll = includeAll + self.name = name + self.runs = runs + self.suiteId = suiteId + } + + /* + Contains every Configuration.id from all runs. + http://docs.gurock.com/testrail-api2/reference-plans#add_plan_entry + */ + public var configIds: [Int]? { + + guard let runs = self.runs else { + return nil + } + + var ids = [Int]() + var allRunConfigIdsAreNil = true + + for run in runs { + guard let runConfigIds = run.configIds else { + continue + } + allRunConfigIdsAreNil = false + ids.append(contentsOf: runConfigIds) + } + + guard allRunConfigIdsAreNil == false else { + return nil // If every run has a nil value for configIds nil is returned match the runs. + } + + ids = Array(Set(ids)) // Remove duplicates. + ids.sort() // Maintain a consistent order for Equatable. + + return ids + } + } + +} + +// MARK: - Equatable + +extension NewPlan.Entry: Equatable { + + public static func==(lhs: NewPlan.Entry, rhs: NewPlan.Entry) -> Bool { + return (lhs.assignedtoId == rhs.assignedtoId && + lhs.caseIds?.sorted() == rhs.caseIds?.sorted() && + lhs.configIds?.sorted() == rhs.configIds?.sorted() && + lhs.description == rhs.description && + lhs.includeAll == rhs.includeAll && + lhs.name == rhs.name && + Array.contentsAreEqual(lhs.runs, rhs.runs) && + lhs.suiteId == rhs.suiteId) + } + +} + +// MARK: - JSON Keys + +extension NewPlan.Entry { + + enum JSONKeys: JSONKey { + case assignedtoId = "assignedto_id" + case caseIds = "case_ids" + case configIds = "config_ids" + case description + case includeAll = "include_all" + case name + case runs + case suiteId = "suite_id" + } + +} + +extension NewPlan.Entry: AddRequestJSONKeys { + + var addRequestJSONKeys: [JSONKey] { + return [JSONKeys.assignedtoId.rawValue, + JSONKeys.caseIds.rawValue, + JSONKeys.configIds.rawValue, + JSONKeys.description.rawValue, + JSONKeys.includeAll.rawValue, + JSONKeys.name.rawValue, + JSONKeys.runs.rawValue, + JSONKeys.suiteId.rawValue] + } + +} + +// MARK: - Serialization + +extension NewPlan.Entry: JSONSerializable { + + func serialized() -> JSONDictionary { + + let runsSerialized: [JSONDictionary]? + if let runs = self.runs { + runsSerialized = NewRun.serialized(runs) + } else { + runsSerialized = nil + } + + return [JSONKeys.assignedtoId.rawValue: assignedtoId as Any, + JSONKeys.caseIds.rawValue: caseIds as Any, + JSONKeys.configIds.rawValue: configIds as Any, + JSONKeys.description.rawValue: description as Any, + JSONKeys.includeAll.rawValue: includeAll as Any, + JSONKeys.name.rawValue: name as Any, + JSONKeys.runs.rawValue: runsSerialized as Any, + JSONKeys.suiteId.rawValue: suiteId] + } + +} + +extension NewPlan.Entry: AddRequestJSON { } diff --git a/QuizTrain/Network/Models/Add/NewPlan.swift b/QuizTrain/Network/Models/Add/NewPlan.swift new file mode 100644 index 0000000..44fe95c --- /dev/null +++ b/QuizTrain/Network/Models/Add/NewPlan.swift @@ -0,0 +1,75 @@ +public struct NewPlan { + + public var description: String? + public var entries: [NewPlan.Entry]? + public var milestoneId: Milestone.Id? + public var name: String + + public init(description: String? = nil, entries: [NewPlan.Entry]? = nil, milestoneId: Milestone.Id? = nil, name: String) { + self.description = description + self.entries = entries + self.milestoneId = milestoneId + self.name = name + } + +} + +// MARK: - Equatable + +extension NewPlan: Equatable { + + public static func==(lhs: NewPlan, rhs: NewPlan) -> Bool { + return (lhs.description == rhs.description && + Array.contentsAreEqual(lhs.entries, rhs.entries) && + lhs.milestoneId == rhs.milestoneId && + lhs.name == rhs.name) + } + +} + +// MARK: - JSON Keys + +extension NewPlan { + + enum JSONKeys: JSONKey { + case description + case entries + case milestoneId = "milestone_id" + case name + } + +} + +extension NewPlan: AddRequestJSONKeys { + + var addRequestJSONKeys: [JSONKey] { + return [JSONKeys.description.rawValue, + JSONKeys.entries.rawValue, + JSONKeys.milestoneId.rawValue, + JSONKeys.name.rawValue] + } + +} + +// MARK: - Serialization + +extension NewPlan: JSONSerializable { + + func serialized() -> JSONDictionary { + + let entriesSerialized: [JSONDictionary]? + if let entries = entries { + entriesSerialized = NewPlan.Entry.serialized(entries) + } else { + entriesSerialized = nil + } + + return [JSONKeys.description.rawValue: description as Any, + JSONKeys.entries.rawValue: entriesSerialized as Any, + JSONKeys.milestoneId.rawValue: milestoneId as Any, + JSONKeys.name.rawValue: name] + } + +} + +extension NewPlan: AddRequestJSON { } diff --git a/QuizTrain/Network/Models/Add/NewProject.swift b/QuizTrain/Network/Models/Add/NewProject.swift new file mode 100644 index 0000000..8588ab3 --- /dev/null +++ b/QuizTrain/Network/Models/Add/NewProject.swift @@ -0,0 +1,67 @@ +public struct NewProject { + + public var announcement: String? + public var name: String + public var showAnnouncement: Bool + public var suiteMode: Project.SuiteMode + + public init(announcement: String? = nil, name: String, showAnnouncement: Bool, suiteMode: Project.SuiteMode) { + self.announcement = announcement + self.name = name + self.showAnnouncement = showAnnouncement + self.suiteMode = suiteMode + } + +} + +// MARK: - Equatable + +extension NewProject: Equatable { + + public static func==(lhs: NewProject, rhs: NewProject) -> Bool { + return (lhs.announcement == rhs.announcement && + lhs.name == rhs.name && + lhs.showAnnouncement == rhs.showAnnouncement && + lhs.suiteMode == rhs.suiteMode) + } + +} + +// MARK: - JSON Keys + +extension NewProject { + + enum JSONKeys: JSONKey { + case announcement + case name + case showAnnouncement = "show_announcement" + case suiteMode = "suite_mode" + } + +} + +extension NewProject: AddRequestJSONKeys { + + var addRequestJSONKeys: [JSONKey] { + return [JSONKeys.announcement.rawValue, + JSONKeys.name.rawValue, + JSONKeys.showAnnouncement.rawValue, + JSONKeys.suiteMode.rawValue] + } + +} + +// MARK: - Serialization + +extension NewProject: JSONSerializable { + + func serialized() -> JSONDictionary { + return [JSONKeys.announcement.rawValue: announcement as Any, + JSONKeys.name.rawValue: name, + JSONKeys.showAnnouncement.rawValue: showAnnouncement, + JSONKeys.suiteMode.rawValue: suiteMode.rawValue] + } + +} + +extension NewProject: AddRequestJSON { } diff --git a/QuizTrain/Network/Models/Add/NewResult.swift b/QuizTrain/Network/Models/Add/NewResult.swift new file mode 100644 index 0000000..24c741c --- /dev/null +++ b/QuizTrain/Network/Models/Add/NewResult.swift @@ -0,0 +1,113 @@ +/* + Used to add a single result. + */ +public struct NewResult: MutableCustomFields { + + // MARK: Properties + + public var assignedtoId: User.Id? + public var comment: String? + public var defects: String? + public var elapsed: String? + public var statusId: Status.Id? + public var version: String? + var customFieldsContainer = CustomFieldsContainer.empty() + + // MARK: Init + + public init(assignedtoId: User.Id? = nil, comment: String? = nil, defects: String? = nil, elapsed: String? = nil, statusId: Status.Id? = nil, version: String? = nil, customFields: JSONDictionary? = nil) { + self.assignedtoId = assignedtoId + self.comment = comment + self.defects = defects + self.elapsed = elapsed + self.statusId = statusId + self.version = version + if let customFields = customFields { + customFieldsContainer.customFields = customFields + } + } + +} + +// MARK: - Equatable + +extension NewResult: Equatable { + + public static func==(lhs: NewResult, rhs: NewResult) -> Bool { + return (lhs.assignedtoId == rhs.assignedtoId && + lhs.comment == rhs.comment && + lhs.defects == rhs.defects && + lhs.elapsed == rhs.elapsed && + lhs.statusId == rhs.statusId && + lhs.version == rhs.version && + lhs.customFieldsContainer == rhs.customFieldsContainer) + } + +} + +// MARK: - Validatable + +extension NewResult: Validatable { + + /* + A result is valid if it's assigned and/or commented on and/or is given a + status. + */ + var isValid: Bool { + return (assignedtoId != nil || comment != nil || statusId != nil) + } + +} + +// MARK: - JSON Keys + +extension NewResult { + + enum JSONKeys: JSONKey { + case assignedtoId = "assignedto_id" + case comment + case defects + case elapsed + case statusId = "status_id" + case version + } + +} + +extension NewResult: AddRequestJSONKeys { + + var addRequestJSONKeys: [JSONKey] { + var keys = [JSONKeys.assignedtoId.rawValue, + JSONKeys.comment.rawValue, + JSONKeys.defects.rawValue, + JSONKeys.elapsed.rawValue, + JSONKeys.statusId.rawValue, + JSONKeys.version.rawValue] + customFields.forEach { item in keys.append(item.key) } + return keys + } + +} + +// MARK: - Serialization + +extension NewResult: JSONSerializable { + + private var serializedProperties: JSONDictionary { + return [JSONKeys.assignedtoId.rawValue: assignedtoId as Any, + JSONKeys.comment.rawValue: comment as Any, + JSONKeys.defects.rawValue: defects as Any, + JSONKeys.elapsed.rawValue: elapsed as Any, + JSONKeys.statusId.rawValue: statusId as Any, + JSONKeys.version.rawValue: version as Any] + } + + func serialized() -> JSONDictionary { + var json = serializedProperties + customFields.forEach { item in json[item.key] = item.value } + return json + } + +} + +extension NewResult: AddRequestJSON { } diff --git a/QuizTrain/Network/Models/Add/NewRun.swift b/QuizTrain/Network/Models/Add/NewRun.swift new file mode 100644 index 0000000..ea08a23 --- /dev/null +++ b/QuizTrain/Network/Models/Add/NewRun.swift @@ -0,0 +1,85 @@ +public struct NewRun { + + public var assignedtoId: User.Id? + public var caseIds: [Case.Id]? + public var description: String? + public var includeAll: Bool? + public var milestoneId: Milestone.Id? + public var name: String + public var suiteId: Suite.Id? // Optional if project is running in single suite mode, otherwise required. + + public init(assignedtoId: User.Id? = nil, caseIds: [Case.Id]? = nil, description: String? = nil, includeAll: Bool? = nil, milestoneId: Milestone.Id? = nil, name: String, suiteId: Suite.Id? = nil) { + self.assignedtoId = assignedtoId + self.caseIds = caseIds + self.description = description + self.includeAll = includeAll + self.milestoneId = milestoneId + self.name = name + self.suiteId = suiteId + } + +} + +// MARK: - Equatable + +extension NewRun: Equatable { + + public static func==(lhs: NewRun, rhs: NewRun) -> Bool { + return (lhs.assignedtoId == rhs.assignedtoId && + lhs.caseIds?.sorted() == rhs.caseIds?.sorted() && + lhs.description == rhs.description && + lhs.includeAll == rhs.includeAll && + lhs.milestoneId == rhs.milestoneId && + lhs.name == rhs.name && + lhs.suiteId == rhs.suiteId) + } + +} + +// MARK: - JSON Keys + +extension NewRun { + + enum JSONKeys: JSONKey { + case assignedtoId = "assignedto_id" + case caseIds = "case_ids" + case description + case includeAll = "include_all" + case milestoneId = "milestone_id" + case name + case suiteId = "suite_id" + } + +} + +extension NewRun: AddRequestJSONKeys { + + var addRequestJSONKeys: [JSONKey] { + return [JSONKeys.assignedtoId.rawValue, + JSONKeys.caseIds.rawValue, + JSONKeys.description.rawValue, + JSONKeys.includeAll.rawValue, + JSONKeys.milestoneId.rawValue, + JSONKeys.name.rawValue, + JSONKeys.suiteId.rawValue] + } + +} + +// MARK: - Serialization + +extension NewRun: JSONSerializable { + + func serialized() -> JSONDictionary { + return [JSONKeys.assignedtoId.rawValue: assignedtoId as Any, + JSONKeys.caseIds.rawValue: caseIds as Any, + JSONKeys.description.rawValue: description as Any, + JSONKeys.includeAll.rawValue: includeAll as Any, + JSONKeys.milestoneId.rawValue: milestoneId as Any, + JSONKeys.name.rawValue: name, + JSONKeys.suiteId.rawValue: suiteId as Any] + } + +} + +extension NewRun: AddRequestJSON { } diff --git a/QuizTrain/Network/Models/Add/NewSection.swift b/QuizTrain/Network/Models/Add/NewSection.swift new file mode 100644 index 0000000..e094ed2 --- /dev/null +++ b/QuizTrain/Network/Models/Add/NewSection.swift @@ -0,0 +1,67 @@ +public struct NewSection { + + public var description: String? + public var name: String + public var parentId: Section.Id? + public var suiteId: Suite.Id? // Optional/ignored if project is running in single suite mode, otherwise required. + + public init(description: String? = nil, name: String, parentId: Section.Id? = nil, suiteId: Suite.Id? = nil) { + self.description = description + self.name = name + self.parentId = parentId + self.suiteId = suiteId + } + +} + +// MARK: - Equatable + +extension NewSection: Equatable { + + public static func==(lhs: NewSection, rhs: NewSection) -> Bool { + return (lhs.description == rhs.description && + lhs.name == rhs.name && + lhs.parentId == rhs.parentId && + lhs.suiteId == rhs.suiteId) + } + +} + +// MARK: - JSON Keys + +extension NewSection { + + enum JSONKeys: JSONKey { + case description + case name + case parentId = "parent_id" + case suiteId = "suite_id" + } + +} + +extension NewSection: AddRequestJSONKeys { + + var addRequestJSONKeys: [JSONKey] { + return [JSONKeys.description.rawValue, + JSONKeys.name.rawValue, + JSONKeys.parentId.rawValue, + JSONKeys.suiteId.rawValue] + } + +} + +// MARK: - Serialization + +extension NewSection: JSONSerializable { + + func serialized() -> JSONDictionary { + return [JSONKeys.description.rawValue: description as Any, + JSONKeys.name.rawValue: name, + JSONKeys.parentId.rawValue: parentId as Any, + JSONKeys.suiteId.rawValue: suiteId as Any] + } + +} + +extension NewSection: AddRequestJSON { } diff --git a/QuizTrain/Network/Models/Add/NewSuite.swift b/QuizTrain/Network/Models/Add/NewSuite.swift new file mode 100644 index 0000000..3d04d0b --- /dev/null +++ b/QuizTrain/Network/Models/Add/NewSuite.swift @@ -0,0 +1,55 @@ +public struct NewSuite { + + public var description: String? + public var name: String + + public init(description: String? = nil, name: String) { + self.description = description + self.name = name + } + +} + +// MARK: - Equatable + +extension NewSuite: Equatable { + + public static func==(lhs: NewSuite, rhs: NewSuite) -> Bool { + return (lhs.description == rhs.description && + lhs.name == rhs.name) + } + +} + +// MARK: - JSON Keys + +extension NewSuite { + + enum JSONKeys: JSONKey { + case description + case name + } + +} + +extension NewSuite: AddRequestJSONKeys { + + var addRequestJSONKeys: [JSONKey] { + return [JSONKeys.description.rawValue, + JSONKeys.name.rawValue] + } + +} + +// MARK: - Serialization + +extension NewSuite: JSONSerializable { + + func serialized() -> JSONDictionary { + return [JSONKeys.description.rawValue: description as Any, + JSONKeys.name.rawValue: name] + } + +} + +extension NewSuite: AddRequestJSON { } diff --git a/QuizTrain/Network/Models/Add/NewTestResults.Result.swift b/QuizTrain/Network/Models/Add/NewTestResults.Result.swift new file mode 100644 index 0000000..6f38e65 --- /dev/null +++ b/QuizTrain/Network/Models/Add/NewTestResults.Result.swift @@ -0,0 +1,120 @@ +extension NewTestResults { + + public struct Result: MutableCustomFields { + + // MARK: Properties + + public var assignedtoId: User.Id? + public var comment: String? + public var defects: String? + public var elapsed: String? + public var statusId: Status.Id? + public var testId: Test.Id + public var version: String? + var customFieldsContainer = CustomFieldsContainer.empty() + + // MARK: Init + + public init(assignedtoId: User.Id? = nil, comment: String? = nil, defects: String? = nil, elapsed: String? = nil, statusId: Status.Id? = nil, testId: Test.Id, version: String? = nil, customFields: JSONDictionary? = nil) { + self.assignedtoId = assignedtoId + self.comment = comment + self.defects = defects + self.elapsed = elapsed + self.statusId = statusId + self.testId = testId + self.version = version + if let customFields = customFields { + customFieldsContainer.customFields = customFields + } + } + + } + +} + +// MARK: - Equatable + +extension NewTestResults.Result: Equatable { + + public static func==(lhs: NewTestResults.Result, rhs: NewTestResults.Result) -> Bool { + return (lhs.assignedtoId == rhs.assignedtoId && + lhs.comment == rhs.comment && + lhs.defects == rhs.defects && + lhs.elapsed == rhs.elapsed && + lhs.statusId == rhs.statusId && + lhs.testId == rhs.testId && + lhs.version == rhs.version && + lhs.customFieldsContainer == rhs.customFieldsContainer) + } + +} + +// MARK: - Validatable + +extension NewTestResults.Result: Validatable { + + /* + A result is valid if it's assigned and/or commented on and/or is given a + status. + */ + var isValid: Bool { + return (assignedtoId != nil || comment != nil || statusId != nil) + } + +} + +// MARK: - JSON Keys + +extension NewTestResults.Result { + + enum JSONKeys: JSONKey { + case assignedtoId = "assignedto_id" + case comment + case defects + case elapsed + case statusId = "status_id" + case testId = "test_id" + case version + } + +} + +extension NewTestResults.Result: AddRequestJSONKeys { + + var addRequestJSONKeys: [JSONKey] { + var keys = [JSONKeys.assignedtoId.rawValue, + JSONKeys.comment.rawValue, + JSONKeys.defects.rawValue, + JSONKeys.elapsed.rawValue, + JSONKeys.statusId.rawValue, + JSONKeys.testId.rawValue, + JSONKeys.version.rawValue] + customFields.forEach { item in keys.append(item.key) } + return keys + } + +} + +// MARK: - Serialization + +extension NewTestResults.Result: JSONSerializable { + + private var serializedProperties: JSONDictionary { + return [JSONKeys.assignedtoId.rawValue: assignedtoId as Any, + JSONKeys.comment.rawValue: comment as Any, + JSONKeys.defects.rawValue: defects as Any, + JSONKeys.elapsed.rawValue: elapsed as Any, + JSONKeys.statusId.rawValue: statusId as Any, + JSONKeys.testId.rawValue: testId, + JSONKeys.version.rawValue: version as Any] + } + + func serialized() -> JSONDictionary { + var json = serializedProperties + customFields.forEach { item in json[item.key] = item.value } + return json + } + +} + +extension NewTestResults.Result: AddRequestJSON { } diff --git a/QuizTrain/Network/Models/Add/NewTestResults.swift b/QuizTrain/Network/Models/Add/NewTestResults.swift new file mode 100644 index 0000000..b38271a --- /dev/null +++ b/QuizTrain/Network/Models/Add/NewTestResults.swift @@ -0,0 +1,62 @@ +/* + Use to bulk-add multiple results associated with Tests. + */ +public struct NewTestResults { + + public var results: [NewTestResults.Result] + + public init(results: [NewTestResults.Result]) { + self.results = results + } + +} + +// MARK: - Equatable + +extension NewTestResults: Equatable { + + public static func==(lhs: NewTestResults, rhs: NewTestResults) -> Bool { + return (lhs.results.contentsAreEqual(to: rhs.results)) + } + +} + +// MARK: - Validatable + +extension NewTestResults: Validatable { + + var isValid: Bool { + return (results.filter({ $0.isValid == false }).count == 0) + } + +} + +// MARK: - JSON Keys + +extension NewTestResults { + + enum JSONKeys: JSONKey { + case results + } + +} + +extension NewTestResults: AddRequestJSONKeys { + + var addRequestJSONKeys: [JSONKey] { + return [JSONKeys.results.rawValue] + } + +} + +// MARK: - Serialization + +extension NewTestResults: JSONSerializable { + + func serialized() -> JSONDictionary { + return [JSONKeys.results.rawValue: NewTestResults.Result.serialized(results)] + } + +} + +extension NewTestResults: AddRequestJSON { } diff --git a/QuizTrain/Network/Models/Update/UpdatePlanEntryRuns.swift b/QuizTrain/Network/Models/Update/UpdatePlanEntryRuns.swift new file mode 100644 index 0000000..55e45d8 --- /dev/null +++ b/QuizTrain/Network/Models/Update/UpdatePlanEntryRuns.swift @@ -0,0 +1,70 @@ +/* + Applies to all Run's within Plan.Entry.runs. + */ +public struct UpdatePlanEntryRuns { + + public var assignedtoId: User.Id? + public var caseIds: [Case.Id]? + public var description: String? + public var includeAll: Bool? + + public init(assignedtoId: User.Id? = nil, caseIds: [Case.Id]? = nil, description: String? = nil, includeAll: Bool? = nil) { + self.assignedtoId = assignedtoId + self.caseIds = caseIds + self.description = description + self.includeAll = includeAll + } + +} + +// MARK: - Equatable + +extension UpdatePlanEntryRuns: Equatable { + + public static func==(lhs: UpdatePlanEntryRuns, rhs: UpdatePlanEntryRuns) -> Bool { + return (lhs.assignedtoId == rhs.assignedtoId && + lhs.caseIds == rhs.caseIds && + lhs.description == rhs.description && + lhs.includeAll == rhs.includeAll) + } + +} + +// MARK: - JSON Keys + +extension UpdatePlanEntryRuns { + + enum JSONKeys: JSONKey { + case assignedtoId = "assignedto_id" + case caseIds = "case_ids" + case description + case includeAll = "include_all" + } + +} + +extension UpdatePlanEntryRuns: UpdateRequestJSONKeys { + + var updateRequestJSONKeys: [JSONKey] { + return [JSONKeys.assignedtoId.rawValue, + JSONKeys.caseIds.rawValue, + JSONKeys.description.rawValue, + JSONKeys.includeAll.rawValue] + } + +} + +// MARK: - Serialization + +extension UpdatePlanEntryRuns: JSONSerializable { + + func serialized() -> JSONDictionary { + return [JSONKeys.assignedtoId.rawValue: assignedtoId as Any, + JSONKeys.caseIds.rawValue: caseIds as Any, + JSONKeys.description.rawValue: description as Any, + JSONKeys.includeAll.rawValue: includeAll as Any] + } + +} + +extension UpdatePlanEntryRuns: UpdateRequestJSON { } diff --git a/QuizTrain/Network/ObjectAPI.swift b/QuizTrain/Network/ObjectAPI.swift new file mode 100644 index 0000000..c26de74 --- /dev/null +++ b/QuizTrain/Network/ObjectAPI.swift @@ -0,0 +1,3032 @@ +import Foundation + +// MARK: - Properties & Init + +/* + Mid-level interface to TestRail's API. This class builds on top of API by + working with model objects and provides 429 Too Many Requests retry handling + when rate limits are reached when handle429TooManyRequestErrors is true. It + abstracts you away from having to deal with Data, JSON, and serialization & + deserialization of objects. In some cases it handles async API calls where a + single call is not possible. A completion handler is called either with a + succeeded or failed outcome on an undefined queue. + + Succeeded outcomes: + + - Add requests return a newly created single or multiple objects. + - Close requests return a newly created closed object. + - Delete requests return nil. + - Get requests return a single or multiple objects. They can be nil in some + cases. + - Update requests return a newly updated object. + + Failed outcomes: + + Failed outcomes return an error or ErrorContainer if multiple errors can occur. + See "Errors" extension for details. The consumer of this class is responsible + for handling all errors. However this class will handle 429 Too Many Request + errors automatically when handle429TooManyRequestErrors is true. + + NOTE: Certain API calls will return nil or partial-data for some object + properties. For example getPlans(...) will always return nil for Plan.entries, + but getPlan(...) will not if the plan has any entries. While this type of + behavior is documented here when known, the single source of truth will always + be in TestRail's official API docs: http://docs.gurock.com/testrail-api2/start + */ +final public class ObjectAPI { + + public let api: API + + fileprivate let asyncRequestQueue: OperationQueue + + public var asyncRequestQueueQoS: QualityOfService { + get { return asyncRequestQueue.qualityOfService } + set { asyncRequestQueue.qualityOfService = newValue } + } + + public var asyncRequestQueueMaxConcurrentOperationCount: NSInteger { + get { return asyncRequestQueue.maxConcurrentOperationCount } + set { asyncRequestQueue.maxConcurrentOperationCount = newValue } + } + + public var handle429TooManyRequestErrors: Bool = true + fileprivate let retryQueue: DispatchQueue + + public init(api: API, retryQueueQoS: DispatchQoS = .`default`) { + self.api = api + self.asyncRequestQueue = OperationQueue() + self.asyncRequestQueue.name = "ObjectAPI.asyncRequestQueue" + self.retryQueue = DispatchQueue(label: "ObjectAPI.retryQueue", qos: retryQueueQoS) + } + + public convenience init(username: String, secret: String, hostname: String, port: Int = 443, scheme: String = "https", retryQueueQoS: DispatchQoS = .`default`) { + let api = API(username: username, secret: secret, hostname: hostname, port: port, scheme: scheme) + self.init(api: api, retryQueueQoS: retryQueueQoS) + } + + deinit { + asyncRequestQueue.cancelAllOperations() + } + +} + +// MARK: - Errors + +extension ObjectAPI { + + // MARK: Status Code + + public struct ClientError { + public var statusCode: Int { return requestResult.response.statusCode } // 400...499 + public let message: String // Body or error message extracted from response. Could be empty. + public let requestResult: API.RequestResult + } + + public struct ServerError { + public var statusCode: Int { return requestResult.response.statusCode } // 500...599 + public let message: String // Body extracted from response. Could be empty. + public let requestResult: API.RequestResult + } + + // MARK: Base + + public enum StatusCodeError: Error { + case clientError(ClientError) // 400...499 + case serverError(ServerError) // 500...599 + case otherError(API.RequestResult) // Any other status code which is not any of the above or 200...299. + } + + public enum DataProcessingError: Error { + case couldNotConvertDataToJSON(data: Data, error: Error) // TestRail returned data which is not valid UTF8 encoded JSON. + case invalidJSONFormat(json: Any) // TestRail returned valid UTF8 encoded JSON but it could not be converted into an expected JSON format. + case couldNotDeserializeFromJSON(objectType: Any.Type, json: Any) // TestRail returned valid UTF8 encoded JSON but it could not be deserialized into an object or objects. This might mean a model needs to be updated. + } + + public enum ObjectConversionError: Error { + case couldNotConvertObjectToData(object: Any, json: JSONDictionary, error: Error) // An error occurred converting the object to Data from its JSON representation. + case couldNotConvertObjectsToData(objects: [Any], json: JSONDictionary, error: Error) // An error occurred converting the objects to Data from their JSON representations. + } + + // MARK: Low Level + + public enum RequestError: Error { + case apiError(API.RequestError) + case statusCodeError(StatusCodeError) + } + + public enum DataRequestError: Error { + case apiError(API.RequestError) + case statusCodeError(StatusCodeError) + case dataProcessingError(DataProcessingError) + } + + public enum UpdateRequestError: Error { + case objectConversionError(ObjectConversionError) + case apiError(API.RequestError) + case statusCodeError(StatusCodeError) + case dataProcessingError(DataProcessingError) + } + + // MARK: High Level + + public typealias AddError = UpdateRequestError + public typealias CloseError = DataRequestError + public typealias DeleteError = RequestError + public typealias GetError = DataRequestError + public typealias UpdateError = UpdateRequestError + + // MARK: Matching + + public enum MatchError: Error { + case matchError(MatchErrorType) + case otherError(OtherErrorType) + } + +} + +// MARK: - API.RequestOutcome Handling + +extension ObjectAPI { + + // MARK: RequestOutcome + + private typealias RequestOutcome = Outcome + + /* + Converts an API.RequestOutcome into a more strongly-defined RequestOutcome. + */ + private static func requestOutcome(from apiRequestOutcome: API.RequestOutcome) -> RequestOutcome { + + let requestResult: API.RequestResult + switch apiRequestOutcome { + case .succeeded(let aRequestResult): + requestResult = aRequestResult + case .failed(let error): + return .failed(.apiError(error)) + } + + switch requestResult.response.statusCode { + case 200...299: + return .succeeded(requestResult) + case 400...499: + let message: String + let json = try? JSONSerialization.jsonObject(with: requestResult.data, options: []) + if let json = json as? JSONDictionary, let jsonError = json["error"] as? String { // TestRail typically returns {"error": "Some error message."} as JSON. + message = jsonError + } else { + message = String(data: requestResult.data, encoding: .utf8) ?? "" // If no JSON error can be extracted attempt to decode as UTF8. + } + let clientError = ClientError(message: message, requestResult: requestResult) + return .failed(.statusCodeError(.clientError(clientError))) + case 500...599: + let message = String(data: requestResult.data, encoding: .utf8) ?? "" + let serverError = ServerError(message: message, requestResult: requestResult) + return .failed(.statusCodeError(.serverError(serverError))) + default: + return .failed(.statusCodeError(.otherError(requestResult))) + } + } + + // MARK: JSON + + private typealias JSONOutcome = Outcome + + /* + Attempts to extract JSON from `apiRequestOutcome` and return it. If + extraction fails a failed JSONOutcome is returned. + */ + private static func json(from apiRequestOutcome: API.RequestOutcome) -> JSONOutcome { + + let requestOutcome = ObjectAPI.requestOutcome(from: apiRequestOutcome) + + let data: Data + switch requestOutcome { + case .failed(let error): + switch error { + case .apiError(let apiError): + return .failed(.apiError(apiError)) + case .statusCodeError(let statusCodeError): + return .failed(.statusCodeError(statusCodeError)) + } + case .succeeded(let requestResult): + data = requestResult.data + } + + let json: Any + do { + json = try JSONSerialization.jsonObject(with: data) + } catch { + return .failed(.dataProcessingError(.couldNotConvertDataToJSON(data: data, error: error))) + } + + return .succeeded(json) + } + + // MARK: Object(s) + + private typealias ObjectOutcome = Outcome + private typealias ObjectsOutcome = Outcome<[ObjectType], DataRequestError> + + /* + Attempts to deserialize and return an object of ObjectType from + `apiRequestOutcome`. If deserializing fails a failed ObjectOutcome is + returned. + */ + private static func object(from apiRequestOutcome: API.RequestOutcome) -> ObjectOutcome { + + let jsonOutcome = ObjectAPI.json(from: apiRequestOutcome) + + let json: Any + switch jsonOutcome { + case .failed(let error): + return .failed(error) + case .succeeded(let aJson): + json = aJson + } + + guard let jsonDictionary = json as? JSONDictionary else { + return .failed(.dataProcessingError(.invalidJSONFormat(json: json))) + } + + guard let object: ObjectType = ObjectType.deserialized(jsonDictionary) else { + return .failed(.dataProcessingError(.couldNotDeserializeFromJSON(objectType: ObjectType.self, json: jsonDictionary))) + } + + return .succeeded(object) + } + + /* + Attempts to deserialize and return 0+ objects of ObjectType from + `apiRequestOutcome`. If deserializing fails a failed ObjectsOutcome is + returned. + */ + private static func object(from apiRequestOutcome: API.RequestOutcome) -> ObjectsOutcome { + + let jsonOutcome = ObjectAPI.json(from: apiRequestOutcome) + + let json: Any + switch jsonOutcome { + case .failed(let error): + return .failed(error) + case .succeeded(let aJson): + json = aJson + } + + guard let jsonArray = json as? [JSONDictionary] else { + return .failed(.dataProcessingError(.invalidJSONFormat(json: json))) + } + + guard let objects: [ObjectType] = ObjectType.deserialized(jsonArray) else { + return .failed(.dataProcessingError(.couldNotDeserializeFromJSON(objectType: ObjectType.self, json: jsonArray))) + } + + return .succeeded(objects) + } + + // MARK: Rate Limit + + /* + Helper for processing 429 Too Many Requests errors. + */ + private struct RateLimitReached { + + let retryAfter: UInt + + init?(clientError: ClientError) { + guard clientError.statusCode == 429 else { + return nil + } + self.retryAfter = clientError.requestResult.response.allHeaderFields["Retry-After"] as? UInt ?? 5 // Default to 5 seconds if Retry-After is missing. + } + + } + + // MARK: Processing + + /* + These methods handle processing an API.RequestOutcome calling one of the + handler closures when complete. Swift inference auto-detects which + process(...) method to call. + + The value passed to a Outcome.succeeded and .failed cases varies. See + method comments for details. + + If handle429TooManyRequestErrors is true and the API returned a 429 Too + Many Requests error, the `retryHandler` will be called after waiting the + API-specified wait time. The caller of a method should re-call itself in + this closure passing itself the same arguments it received for proper retry + behavior. + + In all other cases the `completionHandler` is called when complete. + */ + + /* + Process API.RequestOutcome's to delete an object. + + - Outcome.succeeded receives an API.DataResponse. + - Outcome.failed receives a RequestError. + */ + fileprivate func process(_ apiRequestOutcome: API.RequestOutcome, retryHandler: @escaping (() -> Void), completionHandler: @escaping (Outcome) -> Void) { + + let requestOutcome = ObjectAPI.requestOutcome(from: apiRequestOutcome) + + switch requestOutcome { + case .failed(let error): + switch error { + case .statusCodeError(.clientError(let clientError)): + if let rateLimitReached = RateLimitReached(clientError: clientError), handle429TooManyRequestErrors { + retryQueue.asyncAfter(deadline: DispatchTime.now() + Double(rateLimitReached.retryAfter)) { + retryHandler() + } + } else { + completionHandler(.failed(error)) + } + default: + completionHandler(.failed(error)) + } + case .succeeded(let success): + completionHandler(.succeeded(success)) + } + } + + /* + Process API.RequestOutcome's to get a single object. + + - Outcome.succeeded receives an object deserialized from + API.RequestOutcome. + - Outcome.failed receives a DataRequestError. + */ + fileprivate func process(_ apiRequestOutcome: API.RequestOutcome, retryHandler: @escaping (() -> Void), completionHandler: @escaping (Outcome) -> Void) { + + let objectOutcome: ObjectOutcome = ObjectAPI.object(from: apiRequestOutcome) + + switch objectOutcome { + case .failed(let error): + switch error { + case .statusCodeError(.clientError(let clientError)): + if let rateLimitReached = RateLimitReached(clientError: clientError), handle429TooManyRequestErrors { + retryQueue.asyncAfter(deadline: DispatchTime.now() + Double(rateLimitReached.retryAfter)) { + retryHandler() + } + } else { + completionHandler(.failed(error)) + } + default: + completionHandler(.failed(error)) + } + case .succeeded(let success): + completionHandler(.succeeded(success)) + } + } + + /* + Process API.RequestOutcome's to get multiple objects. + + - Outcome.succeeded receives 0+ object(s) deserialized from + API.RequestOutcome. + - Outcome.failed receives a DataRequestError. + */ + fileprivate func process(_ apiRequestOutcome: API.RequestOutcome, retryHandler: @escaping (() -> Void), completionHandler: @escaping (Outcome<[ObjectType], DataRequestError>) -> Void) { + + let objectOutcome: ObjectOutcome<[ObjectType]> = ObjectAPI.object(from: apiRequestOutcome) + + switch objectOutcome { + case .failed(let error): + switch error { + case .statusCodeError(.clientError(let clientError)): + if let rateLimitReached = RateLimitReached(clientError: clientError), handle429TooManyRequestErrors { + retryQueue.asyncAfter(deadline: DispatchTime.now() + Double(rateLimitReached.retryAfter)) { + retryHandler() + } + } else { + completionHandler(.failed(error)) + } + default: + completionHandler(.failed(error)) + } + case .succeeded(let success): + completionHandler(.succeeded(success)) + } + } + + /* + Process API.RequestOutcome's to add or update an object returning a new + added/updated object if successful. + + - Outcome.succeeded receives an object deserialized from + API.RequestOutcome. + - Outcome.failed receives an UpdateRequestError. + */ + fileprivate func process(_ apiRequestOutcome: API.RequestOutcome, retryHandler: @escaping (() -> Void), completionHandler: @escaping (Outcome) -> Void) { + + let objectOutcome: ObjectOutcome = ObjectAPI.object(from: apiRequestOutcome) + + switch objectOutcome { + case .failed(let error): + switch error { + case .apiError(let apiError): + completionHandler(.failed(.apiError(apiError))) + case .statusCodeError(.clientError(let clientError)): + if let rateLimitReached = RateLimitReached(clientError: clientError), handle429TooManyRequestErrors { + retryQueue.asyncAfter(deadline: DispatchTime.now() + Double(rateLimitReached.retryAfter)) { + retryHandler() + } + } else { + completionHandler(.failed(.statusCodeError(.clientError(clientError)))) + } + case .statusCodeError(let statusCodeError): + completionHandler(.failed(.statusCodeError(statusCodeError))) + case .dataProcessingError(let dataProcessingError): + completionHandler(.failed(.dataProcessingError(dataProcessingError))) + } + case .succeeded(let success): + completionHandler(.succeeded(success)) + } + } + + /* + Process API.RequestOutcome's to add or update multiple objects returning an + array of the added/updated objects if successful. + + - Outcome.succeeded receives an array of objects deserialized from + API.RequestOutcome. + - Outcome.failed receives an UpdateRequestError. + */ + fileprivate func process(_ apiRequestOutcome: API.RequestOutcome, retryHandler: @escaping (() -> Void), completionHandler: @escaping (Outcome<[ObjectType], UpdateRequestError>) -> Void) { + + let objectOutcome: ObjectOutcome<[ObjectType]> = ObjectAPI.object(from: apiRequestOutcome) + + switch objectOutcome { + case .failed(let error): + switch error { + case .apiError(let apiError): + completionHandler(.failed(.apiError(apiError))) + case .statusCodeError(.clientError(let clientError)): + if let rateLimitReached = RateLimitReached(clientError: clientError), handle429TooManyRequestErrors { + retryQueue.asyncAfter(deadline: DispatchTime.now() + Double(rateLimitReached.retryAfter)) { + retryHandler() + } + } else { + completionHandler(.failed(.statusCodeError(.clientError(clientError)))) + } + case .statusCodeError(let statusCodeError): + completionHandler(.failed(.statusCodeError(statusCodeError))) + case .dataProcessingError(let dataProcessingError): + completionHandler(.failed(.dataProcessingError(dataProcessingError))) + } + case .succeeded(let success): + completionHandler(.succeeded(success)) + } + } +} + +// MARK: - Object to JSON to Data + +extension ObjectAPI { + + fileprivate typealias DataOutcome = Outcome + + fileprivate func data(from object: AddRequestJSON) -> DataOutcome { + + let json = object.addRequestJSON + + let data: Data + do { + data = try JSONSerialization.data(withJSONObject: json, options: []) + } catch { + return .failed(.couldNotConvertObjectToData(object: object, json: json, error: error)) + } + + return .succeeded(data) + } + + fileprivate func data(from object: UpdateRequestJSON) -> DataOutcome { + + let json = object.updateRequestJSON + + let data: Data + do { + data = try JSONSerialization.data(withJSONObject: json, options: []) + } catch { + return .failed(.couldNotConvertObjectToData(object: object, json: json, error: error)) + } + + return .succeeded(data) + } + + /* + Combines JSON from all `objects` and returns Data from it. Any overlapping + JSON keys are overriden by the last-most object in `objects`. + */ + fileprivate func data(from objects: [UpdateRequestJSON]) -> DataOutcome { + + var json: JSONDictionary = [:] + for object in objects { + let objectJson = object.updateRequestJSON + objectJson.forEach { item in json[item.key] = item.value } + } + + let data: Data + do { + data = try JSONSerialization.data(withJSONObject: json, options: []) + } catch { + return .failed(.couldNotConvertObjectsToData(objects: objects, json: json, error: error)) + } + + return .succeeded(data) + } + +} + +// MARK: - Objects (Fileprivate Outcomes) + +extension ObjectAPI { + + // MARK: - ConfigurationGroup + + /* + Queries all Project's matching |projectIds| asynchronously to get their + ConfigurationGroups. Passes a dictionary of outcomes to the + completionHandler mapping projectIds to each outcome when complete: + + [projectId1: outcome1, projectId2: outcome2, ...] + + The totality of outcomes can be in three states: + + 1. All outcomes succeeded. + 2. All outcomes failed. + 3. Outcomes are a mixture of succeeded/failed states. + + It is up to the consumer of this method to determine how to handle #2/3. + */ + fileprivate func getConfigurationGroups(inProjectsWithIds projectIds: Set, completionHandler: @escaping ([Project.Id: Outcome<[ConfigurationGroup], GetError>]) -> Void) { + + // For every project create an operation to get all of its configuration groups. + var operations = [GetConfigurationGroupsOperation]() + for projectId in projectIds { + let operation = GetConfigurationGroupsOperation(api: self, projectId: projectId) + operations.append(operation) + } + + let completedOperation = BlockOperation { + + // Nothing should be cancelled. The asyncRequestQueue is fileprivate + // and will only be cancelled if the ObjectAPI is deinit'ed. + guard operations.filter({ $0.isCancelled == true }).count == 0 else { + return + } + + // Everything must have finished. + guard operations.filter({ $0.isFinished == true }).count == operations.count else { + return + } + + var allOutcomes = [Project.Id: Outcome<[ConfigurationGroup], GetError>]() + for operation in operations { + allOutcomes[operation.projectId] = operation.outcome + } + + completionHandler(allOutcomes) + } + + for operation in operations { + completedOperation.addDependency(operation) + } + + var allOperations: [Operation] = operations + allOperations.append(completedOperation) + + asyncRequestQueue.addOperations(allOperations, waitUntilFinished: false) + } + + // MARK: - Project + + /* + Asynchronously GETs each project in |projectIds|. Passes a dictionary of + outcomes to the completionHandler mapping projectIds to each outcome when + complete: + + [projectId1: outcome1, projectId2: outcome2, ...] + + The totality of outcomes can be in three states: + + 1. All outcomes succeeded. + 2. All outcomes failed. + 3. Outcomes are a mixture of succeeded/failed states. + + It is up to the consumer of this method to determine how to handle #2/3. + + 403 failure errors indicate an authorization issue (you do not have at + least "Read-only" access to that project). It is possible for a project to + have "No Access" set preventing anyone from accessing it. + */ + fileprivate func getProjects(_ projectIds: Set, completionHandler: @escaping ([Project.Id: Outcome]) -> Void) { + + // Create an operation to get each project. + var operations = [GetProjectOperation]() + for projectId in projectIds { + let operation = GetProjectOperation(api: self, projectId: projectId) + operations.append(operation) + } + + let completedOperation = BlockOperation { + + // Nothing should be cancelled. The asyncRequestQueue is fileprivate + // and will only be cancelled if the ObjectAPI is deinit'ed. + guard operations.filter({ $0.isCancelled == true }).count == 0 else { + return + } + + // Everything must have finished. + guard operations.filter({ $0.isFinished == true }).count == operations.count else { + return + } + + var allOutcomes = [Project.Id: Outcome]() + for operation in operations { + allOutcomes[operation.projectId] = operation.outcome + } + + completionHandler(allOutcomes) + } + + for operation in operations { + completedOperation.addDependency(operation) + } + + var allOperations: [Operation] = operations + allOperations.append(completedOperation) + + asyncRequestQueue.addOperations(allOperations, waitUntilFinished: false) + } + + // MARK: - Template + + /* + Queries all Project's matching |projectIds| asynchronously to get their + Templates. Passes a dictionary of outcomes to the completionHandler + mapping projectIds to each outcome when complete: + + [projectId1: outcome1, projectId2: outcome2, ...] + + The totality of outcomes can be in three states: + + 1. All outcomes succeeded. + 2. All outcomes failed. + 3. Outcomes are a mixture of succeeded/failed states. + + It is up to the consumer of this method to determine how to handle #2/3. + */ + fileprivate func getTemplates(inProjectsWithIds projectIds: Set, completionHandler: @escaping ([Project.Id: Outcome<[Template], GetError>]) -> Void) { + + // For every project create an operation to get all of its templates. + var operations = [GetTemplatesOperation]() + for projectId in projectIds { + let operation = GetTemplatesOperation(api: self, projectId: projectId) + operations.append(operation) + } + + let completedOperation = BlockOperation { + + // Nothing should be cancelled. The asyncRequestQueue is fileprivate + // and will only be cancelled if the ObjectAPI is deinit'ed. + guard operations.filter({ $0.isCancelled == true }).count == 0 else { + return + } + + // Everything must have finished. + guard operations.filter({ $0.isFinished == true }).count == operations.count else { + return + } + + var allOutcomes = [Project.Id: Outcome<[Template], GetError>]() + for operation in operations { + allOutcomes[operation.projectId] = operation.outcome + } + + completionHandler(allOutcomes) + } + + for operation in operations { + completedOperation.addDependency(operation) + } + + var allOperations: [Operation] = operations + allOperations.append(completedOperation) + + asyncRequestQueue.addOperations(allOperations, waitUntilFinished: false) + } + +} + +// MARK: - Objects + +extension ObjectAPI { + + // MARK: - Case + + /* + http://docs.gurock.com/testrail-api2/reference-cases#add_case + */ + public func addCase(_ newCase: NewCase, to section: Section, completionHandler: @escaping (Outcome) -> Void) { + addCase(newCase, toSectionWithId: section.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-cases#add_case + */ + public func addCase(_ newCase: NewCase, toSectionWithId sectionId: Section.Id, completionHandler: @escaping (Outcome) -> Void) { + + let dataOutcome = self.data(from: newCase) + let data: Data + switch dataOutcome { + case .failed(let error): + completionHandler(.failed(.objectConversionError(error))) + return + case .succeeded(let aData): + data = aData + } + + api.addCase(sectionId: sectionId, data: data) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.addCase(newCase, toSectionWithId: sectionId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-cases#delete_case + */ + public func deleteCase(_ case: Case, completionHandler: @escaping (Outcome) -> Void) { + deleteCase(`case`.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-cases#delete_case + */ + public func deleteCase(_ caseId: Case.Id, completionHandler: @escaping (Outcome) -> Void) { + api.deleteCase(caseId: caseId) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.deleteCase(caseId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + switch processedOutcome { + case .failed(let error): + completionHandler(.failed(error)) + case .succeeded(_): + completionHandler(.succeeded(nil)) + } + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-cases#get_case + */ + public func getCase(_ caseId: Case.Id, completionHandler: @escaping (Outcome) -> Void) { + api.getCase(caseId: caseId) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getCase(caseId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-cases#get_cases + + suite is required if the project is not running in single suite mode. + */ + public func getCases(in project: Project, in suite: Suite? = nil, in section: Section? = nil, filteredBy filters: [Filter]? = nil, completionHandler: @escaping (Outcome<[Case], GetError>) -> Void) { + getCases(inProjectWithId: project.id, inSuiteWithId: suite?.id, inSectionWithId: section?.id, filteredBy: filters, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-cases#get_cases + + suiteId is required if the project is not running in single suite mode. + */ + public func getCases(inProjectWithId projectId: Project.Id, inSuiteWithId suiteId: Suite.Id? = nil, inSectionWithId sectionId: Section.Id? = nil, filteredBy filters: [Filter]? = nil, completionHandler: @escaping (Outcome<[Case], GetError>) -> Void) { + api.getCases(projectId: projectId, suiteId: suiteId, sectionId: sectionId, filters: filters) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getCases(inProjectWithId: projectId, inSuiteWithId: suiteId, inSectionWithId: sectionId, filteredBy: filters, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-cases#update_case + */ + public func updateCase(_ case: Case, completionHandler: @escaping (Outcome) -> Void) { + + let dataOutcome = self.data(from: `case`) + let data: Data + switch dataOutcome { + case .failed(let error): + completionHandler(.failed(.objectConversionError(error))) + return + case .succeeded(let aData): + data = aData + } + + api.updateCase(caseId: `case`.id, data: data) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.updateCase(`case`, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + // MARK: - CaseField + + /* + http://docs.gurock.com/testrail-api2/reference-cases-fields#get_case_fields + */ + public func getCaseFields(completionHandler: @escaping (Outcome<[CaseField], GetError>) -> Void) { + api.getCaseFields { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getCaseFields(completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + // MARK: - CaseType + + /* + http://docs.gurock.com/testrail-api2/reference-cases-types#get_case_types + */ + public func getCaseTypes(completionHandler: @escaping (Outcome<[CaseType], GetError>) -> Void) { + api.getCaseTypes { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getCaseTypes(completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + // MARK: - Configuration + + /* + http://docs.gurock.com/testrail-api2/reference-configs#add_config + */ + public func addConfiguration(_ newConfiguration: NewConfiguration, to configurationGroup: ConfigurationGroup, completionHandler: @escaping (Outcome) -> Void) { + addConfiguration(newConfiguration, toConfigurationGroupWithId: configurationGroup.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-configs#add_config + */ + public func addConfiguration(_ newConfiguration: NewConfiguration, toConfigurationGroupWithId configurationGroupId: ConfigurationGroup.Id, completionHandler: @escaping (Outcome) -> Void) { + + let dataOutcome = self.data(from: newConfiguration) + let data: Data + switch dataOutcome { + case .failed(let error): + completionHandler(.failed(.objectConversionError(error))) + return + case .succeeded(let aData): + data = aData + } + + api.addConfiguration(configurationGroupId: configurationGroupId, data: data) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.addConfiguration(newConfiguration, toConfigurationGroupWithId: configurationGroupId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-configs#delete_config + */ + public func deleteConfiguration(_ configuration: Configuration, completionHandler: @escaping (Outcome) -> Void) { + deleteConfiguration(configuration.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-configs#delete_config + */ + public func deleteConfiguration(_ configurationId: Configuration.Id, completionHandler: @escaping (Outcome) -> Void) { + api.deleteConfiguration(configurationId: configurationId) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.deleteConfiguration(configurationId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + switch processedOutcome { + case .failed(let error): + completionHandler(.failed(error)) + case .succeeded(_): + completionHandler(.succeeded(nil)) + } + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-configs#update_config + */ + public func updateConfiguration(_ configuration: Configuration, completionHandler: @escaping (Outcome) -> Void) { + + let dataOutcome = self.data(from: configuration) + let data: Data + switch dataOutcome { + case .failed(let error): + completionHandler(.failed(.objectConversionError(error))) + return + case .succeeded(let aData): + data = aData + } + + api.updateConfiguration(configurationId: configuration.id, data: data) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.updateConfiguration(configuration, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + // MARK: - ConfigurationGroup + + /* + http://docs.gurock.com/testrail-api2/reference-configs#add_config_group + */ + public func addConfigurationGroup(_ newConfigurationGroup: NewConfigurationGroup, to project: Project, completionHandler: @escaping (Outcome) -> Void) { + addConfigurationGroup(newConfigurationGroup, toProjectWithId: project.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-configs#add_config_group + */ + public func addConfigurationGroup(_ newConfigurationGroup: NewConfigurationGroup, toProjectWithId projectId: Project.Id, completionHandler: @escaping (Outcome) -> Void) { + + let dataOutcome = self.data(from: newConfigurationGroup) + let data: Data + switch dataOutcome { + case .failed(let error): + completionHandler(.failed(.objectConversionError(error))) + return + case .succeeded(let aData): + data = aData + } + + api.addConfigurationGroup(projectId: projectId, data: data) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.addConfigurationGroup(newConfigurationGroup, toProjectWithId: projectId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-configs#delete_config_group + */ + public func deleteConfigurationGroup(_ configurationGroup: ConfigurationGroup, completionHandler: @escaping (Outcome) -> Void) { + deleteConfigurationGroup(configurationGroup.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-configs#delete_config_group + */ + public func deleteConfigurationGroup(_ configurationGroupId: ConfigurationGroup.Id, completionHandler: @escaping (Outcome) -> Void) { + api.deleteConfigurationGroup(configurationGroupId: configurationGroupId) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.deleteConfigurationGroup(configurationGroupId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + switch processedOutcome { + case .failed(let error): + completionHandler(.failed(error)) + case .succeeded(_): + completionHandler(.succeeded(nil)) + } + }) + } + } + + /* + Gets all ConfigurationGroups from all Projects. + + - Returns a de-duped array of 0+ ConfigurationGroups from all Projects upon + success. + - Returns an ErrorContainer of 1+ errors if any of the GET requests failed. + + This method makes multiple async API calls across all accessible projects + to gather all ConfigurationGroups. It may be expensive/slow to run if you + have many projects. + */ + public func getConfigurationGroups(completionHandler: @escaping (Outcome<[ConfigurationGroup], ErrorContainer>) -> Void) { + + getProjects { [weak self] (projectsOutcome) in + + switch projectsOutcome { + case .failed(let error): + completionHandler(.failed(ErrorContainer(error))) + case .succeeded(let projects): + + let projectIds = Set(projects.flatMap({ $0.id })) + self?.getConfigurationGroups(inProjectsWithIds: projectIds) { (configurationGroupsOutcomes) in + + let outcomes = configurationGroupsOutcomes.flatMap { $1 } // Discard projectId keys. + var allConfigurationGroups = [ConfigurationGroup]() + var allErrors = [GetError]() + + // Extract all ConfigurationGroups/errors from outcomes. + for outcome in outcomes { + switch outcome { + case .failed(let error): + allErrors.append(error) + case .succeeded(let configurationGroups): + for configurationGroup in configurationGroups { + guard allConfigurationGroups.filter({ $0.id == configurationGroup.id }).count == 0 else { // Skip duplicates. + continue + } + allConfigurationGroups.append(configurationGroup) + } + } + } + + if let errorContainer = ErrorContainer(allErrors) { + completionHandler(.failed(errorContainer)) // Fail if there are any errors. + } else { + completionHandler(.succeeded(allConfigurationGroups)) + } + } + } + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-configs#get_configs + */ + public func getConfigurationGroups(in project: Project, completionHandler: @escaping (Outcome<[ConfigurationGroup], GetError>) -> Void) { + getConfigurationGroups(inProjectWithId: project.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-configs#get_configs + */ + public func getConfigurationGroups(inProjectWithId projectId: Project.Id, completionHandler: @escaping (Outcome<[ConfigurationGroup], GetError>) -> Void) { + api.getConfigurations(projectId: projectId) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getConfigurationGroups(inProjectWithId: projectId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-configs#update_config_group + */ + public func updateConfigurationGroup(_ configurationGroup: ConfigurationGroup, completionHandler: @escaping (Outcome) -> Void) { + + let dataOutcome = self.data(from: configurationGroup) + let data: Data + switch dataOutcome { + case .failed(let error): + completionHandler(.failed(.objectConversionError(error))) + return + case .succeeded(let aData): + data = aData + } + + api.updateConfigurationGroup(configurationGroupId: configurationGroup.id, data: data) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.updateConfigurationGroup(configurationGroup, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + // MARK: - Milestone + + /* + http://docs.gurock.com/testrail-api2/reference-milestones#add_milestone + */ + public func addMilestone(_ newMilestone: NewMilestone, to project: Project, completionHandler: @escaping (Outcome) -> Void) { + addMilestone(newMilestone, toProjectWithId: project.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-milestones#add_milestone + */ + public func addMilestone(_ newMilestone: NewMilestone, toProjectWithId projectId: Project.Id, completionHandler: @escaping (Outcome) -> Void) { + + let dataOutcome = self.data(from: newMilestone) + let data: Data + switch dataOutcome { + case .failed(let error): + completionHandler(.failed(.objectConversionError(error))) + return + case .succeeded(let aData): + data = aData + } + + api.addMilestone(projectId: projectId, data: data) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.addMilestone(newMilestone, toProjectWithId: projectId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-milestones#delete_milestone + */ + public func deleteMilestone(_ milestone: Milestone, completionHandler: @escaping (Outcome) -> Void) { + deleteMilestone(milestone.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-milestones#delete_milestone + */ + public func deleteMilestone(_ milestoneId: Milestone.Id, completionHandler: @escaping (Outcome) -> Void) { + api.deleteMilestone(milestoneId: milestoneId) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.deleteMilestone(milestoneId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + switch processedOutcome { + case .failed(let error): + completionHandler(.failed(error)) + case .succeeded(_): + completionHandler(.succeeded(nil)) + } + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-milestones#get_milestone + */ + public func getMilestone(_ milestoneId: Milestone.Id, completionHandler: @escaping (Outcome) -> Void) { + api.getMilestone(milestoneId: milestoneId) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getMilestone(milestoneId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-milestones#get_milestones + + NOTE: This API call does not include .milestones for the Milestone's. For + that behavior use getMilestone(...). + */ + public func getMilestones(in project: Project, filteredBy filters: [Filter]? = nil, completionHandler: @escaping (Outcome<[Milestone], GetError>) -> Void) { + getMilestones(inProjectWithId: project.id, filteredBy: filters, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-milestones#get_milestones + + NOTE: This API call does not include .milestones for the Milestone's. For + that behavior use getMilestone(...). + */ + public func getMilestones(inProjectWithId projectId: Project.Id, filteredBy filters: [Filter]? = nil, completionHandler: @escaping (Outcome<[Milestone], GetError>) -> Void) { + api.getMilestones(projectId: projectId, filters: filters) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getMilestones(inProjectWithId: projectId, filteredBy: filters, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-milestones#update_milestone + */ + public func updateMilestone(_ milestone: Milestone, completionHandler: @escaping (Outcome) -> Void) { + + let dataOutcome = self.data(from: milestone) + let data: Data + switch dataOutcome { + case .failed(let error): + completionHandler(.failed(.objectConversionError(error))) + return + case .succeeded(let aData): + data = aData + } + + api.updateMilestone(milestoneId: milestone.id, data: data) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.updateMilestone(milestone, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + // MARK: - Plan + + /* + http://docs.gurock.com/testrail-api2/reference-plans#add_plan + */ + public func addPlan(_ newPlan: NewPlan, to project: Project, completionHandler: @escaping (Outcome) -> Void) { + addPlan(newPlan, toProjectWithId: project.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-plans#add_plan + */ + public func addPlan(_ newPlan: NewPlan, toProjectWithId projectId: Project.Id, completionHandler: @escaping (Outcome) -> Void) { + + let dataOutcome = self.data(from: newPlan) + let data: Data + switch dataOutcome { + case .failed(let error): + completionHandler(.failed(.objectConversionError(error))) + return + case .succeeded(let aData): + data = aData + } + + api.addPlan(projectId: projectId, data: data) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.addPlan(newPlan, toProjectWithId: projectId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-plans#close_plan + */ + public func closePlan(_ plan: Plan, completionHandler: @escaping (Outcome) -> Void) { + closePlan(plan.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-plans#close_plan + */ + public func closePlan(_ planId: Plan.Id, completionHandler: @escaping (Outcome) -> Void) { + api.closePlan(planId: planId) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.closePlan(planId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-plans#delete_plan + */ + public func deletePlan(_ plan: Plan, completionHandler: @escaping (Outcome) -> Void) { + deletePlan(plan.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-plans#delete_plan + */ + public func deletePlan(_ planId: Plan.Id, completionHandler: @escaping (Outcome) -> Void) { + api.deletePlan(planId: planId) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.deletePlan(planId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + switch processedOutcome { + case .failed(let error): + completionHandler(.failed(error)) + case .succeeded(_): + completionHandler(.succeeded(nil)) + } + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-plans#get_plan + */ + public func getPlan(_ planId: Plan.Id, completionHandler: @escaping (Outcome) -> Void) { + api.getPlan(planId: planId) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getPlan(planId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-plans#get_plans + + This API call does not include .entries for the Plan's. For that behavior + use getPlan(...). + */ + public func getPlans(in project: Project, filteredBy filters: [Filter]? = nil, completionHandler: @escaping (Outcome<[Plan], GetError>) -> Void) { + getPlans(inProjectWithId: project.id, filteredBy: filters, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-plans#get_plans + + This API call does not include .entries for the Plan's. For that behavior + use getPlan(...). + */ + public func getPlans(inProjectWithId projectId: Project.Id, filteredBy filters: [Filter]? = nil, completionHandler: @escaping (Outcome<[Plan], GetError>) -> Void) { + api.getPlans(projectId: projectId, filters: filters) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getPlans(inProjectWithId: projectId, filteredBy: filters, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-plans#update_plan + */ + public func updatePlan(_ plan: Plan, completionHandler: @escaping (Outcome) -> Void) { + + let dataOutcome = self.data(from: plan) + let data: Data + switch dataOutcome { + case .failed(let error): + completionHandler(.failed(.objectConversionError(error))) + return + case .succeeded(let aData): + data = aData + } + + api.updatePlan(planId: plan.id, data: data) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.updatePlan(plan, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + // MARK: - Plan.Entry + + /* + http://docs.gurock.com/testrail-api2/reference-plans#add_plan_entry + + Upon success only the newly added run(s) are included in Plan.Entry.runs. + Any other runs are not included. To get all runs call getPlan() after this + method completes successfully. + + Upon success `plan`.entries will be stale; use getPlan() to get a fresh + Plan. + */ + public func addPlanEntry(_ newPlanEntry: NewPlan.Entry, to plan: Plan, completionHandler: @escaping (Outcome) -> Void) { + addPlanEntry(newPlanEntry, toPlanWithId: plan.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-plans#add_plan_entry + + Upon success only the newly added run(s) are included in Plan.Entry.runs. + Any other runs are not included. To get all runs call getPlan() after this + method completes successfully. + + Upon success Plan.entries for `planId` will be stale; use getPlan() to get + a fresh Plan. + */ + public func addPlanEntry(_ newPlanEntry: NewPlan.Entry, toPlanWithId planId: Plan.Id, completionHandler: @escaping (Outcome) -> Void) { + + let dataOutcome = self.data(from: newPlanEntry) + let data: Data + switch dataOutcome { + case .failed(let error): + completionHandler(.failed(.objectConversionError(error))) + return + case .succeeded(let aData): + data = aData + } + + api.addPlanEntry(planId: planId, data: data) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.addPlanEntry(newPlanEntry, toPlanWithId: planId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-plans#delete_plan_entry + + Upon success `plan`.entries will be stale; use getPlan() to get a fresh + Plan. + */ + public func deletePlanEntry(_ planEntry: Plan.Entry, from plan: Plan, completionHandler: @escaping (Outcome) -> Void) { + deletePlanEntry(planEntry.id, fromPlanWithId: plan.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-plans#delete_plan_entry + + Upon success Plan.entries for `planId` will be stale; use getPlan() to get + a fresh Plan. + */ + public func deletePlanEntry(_ planEntryId: Plan.Entry.Id, fromPlanWithId planId: Plan.Id, completionHandler: @escaping (Outcome) -> Void) { + api.deletePlanEntry(planId: planId, planEntryId: planEntryId) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.deletePlanEntry(planEntryId, fromPlanWithId: planId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + switch processedOutcome { + case .failed(let error): + completionHandler(.failed(error)) + case .succeeded(_): + completionHandler(.succeeded(nil)) + } + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-plans#update_plan_entry + + Upon success this returns a new Plan.Entry including all of its test Run's. + `plan`.entries will be stale; use getPlan() to get a fresh Plan. + + Plan.Entry updates differ slightly from other API updates. You can: + + - Update variable properties affecting the Plan.Entry itself. + - Pass `planEntryRunsData` affecting all Plan.Entry.runs in bulk. + */ + public func updatePlanEntry(_ planEntry: Plan.Entry, in plan: Plan, with planEntryRuns: UpdatePlanEntryRuns? = nil, completionHandler: @escaping (Outcome) -> Void) { + updatePlanEntry(planEntry, inPlanWithId: plan.id, with: planEntryRuns, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-plans#update_plan_entry + + Upon success this returns a new Plan.Entry including all of its test Run's. + Plan.entries for `planId` will be stale; use getPlan() to get a fresh Plan. + + Plan.Entry updates differ slightly from other API updates. You can: + + - Update variable properties affecting the Plan.Entry itself. + - Pass `planEntryRunsData` affecting all Plan.Entry.runs in bulk. + */ + public func updatePlanEntry(_ planEntry: Plan.Entry, inPlanWithId planId: Plan.Id, with planEntryRuns: UpdatePlanEntryRuns? = nil, completionHandler: @escaping (Outcome) -> Void) { + + let dataOutcome: DataOutcome + if let planEntryRuns = planEntryRuns { + dataOutcome = self.data(from: [planEntry, planEntryRuns]) + } else { + dataOutcome = self.data(from: planEntry) + } + + let data: Data + switch dataOutcome { + case .failed(let error): + completionHandler(.failed(.objectConversionError(error))) + return + case .succeeded(let aData): + data = aData + } + + api.updatePlanEntry(planId: planId, planEntryId: planEntry.id, data: data) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.updatePlanEntry(planEntry, inPlanWithId: planId, with: planEntryRuns, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + // MARK: - Priority + + /* + http://docs.gurock.com/testrail-api2/reference-priorities#get_priorities + */ + public func getPriorities(completionHandler: @escaping (Outcome<[Priority], GetError>) -> Void) { + api.getPriorities { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getPriorities(completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + // MARK: - Project + + /* + http://docs.gurock.com/testrail-api2/reference-projects#add_project + */ + public func addProject(_ newProject: NewProject, completionHandler: @escaping (Outcome) -> Void) { + + let dataOutcome = self.data(from: newProject) + let data: Data + switch dataOutcome { + case .failed(let error): + completionHandler(.failed(.objectConversionError(error))) + return + case .succeeded(let aData): + data = aData + } + + api.addProject(data: data) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.addProject(newProject, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-projects#delete_project + */ + public func deleteProject(_ project: Project, completionHandler: @escaping (Outcome) -> Void) { + deleteProject(project.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-projects#delete_project + */ + public func deleteProject(_ projectId: Project.Id, completionHandler: @escaping (Outcome) -> Void) { + api.deleteProject(projectId: projectId) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.deleteProject(projectId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + switch processedOutcome { + case .failed(let error): + completionHandler(.failed(error)) + case .succeeded(_): + completionHandler(.succeeded(nil)) + } + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-projects#get_project + + You must have at least Read-only access to the project otherwise a 403 + error will be returned. + */ + public func getProject(_ projectId: Project.Id, completionHandler: @escaping (Outcome) -> Void) { + api.getProject(projectId: projectId) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getProject(projectId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-projects#get_projects + + Returns all projects which you have at least Read-only access to. All other + projects will be silently omitted. + + To determine if you have at least Read-only access to a project use + getProject() instead. It will return an explicit error if you do not have + access to a project. + */ + public func getProjects(filteredBy filters: [Filter]? = nil, completionHandler: @escaping (Outcome<[Project], GetError>) -> Void) { + api.getProjects(filters: filters) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getProjects(filteredBy: filters, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-projects#update_project + */ + public func updateProject(_ project: Project, completionHandler: @escaping (Outcome) -> Void) { + + let dataOutcome = self.data(from: project) + let data: Data + switch dataOutcome { + case .failed(let error): + completionHandler(.failed(.objectConversionError(error))) + return + case .succeeded(let aData): + data = aData + } + + api.updateProject(projectId: project.id, data: data) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.updateProject(project, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + // MARK: - Result + + /* + http://docs.gurock.com/testrail-api2/reference-results#add_result + */ + public func addResult(_ newResult: NewResult, to test: Test, completionHandler: @escaping (Outcome) -> Void) { + addResult(newResult, toTestWithId: test.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-results#add_result + */ + public func addResult(_ newResult: NewResult, toTestWithId testId: Test.Id, completionHandler: @escaping (Outcome) -> Void) { + + let dataOutcome = self.data(from: newResult) + let data: Data + switch dataOutcome { + case .failed(let error): + completionHandler(.failed(.objectConversionError(error))) + return + case .succeeded(let aData): + data = aData + } + + api.addResult(testId: testId, data: data) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.addResult(newResult, toTestWithId: testId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-results#add_result_for_case + */ + public func addResultForCase(_ newResult: NewResult, to run: Run, to case: Case, completionHandler: @escaping (Outcome) -> Void) { + addResultForCase(newResult, toRunWithId: run.id, toCaseWithId: `case`.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-results#add_result_for_case + */ + public func addResultForCase(_ newResult: NewResult, toRunWithId runId: Run.Id, toCaseWithId caseId: Case.Id, completionHandler: @escaping (Outcome) -> Void) { + + let dataOutcome = self.data(from: newResult) + let data: Data + switch dataOutcome { + case .failed(let error): + completionHandler(.failed(.objectConversionError(error))) + return + case .succeeded(let aData): + data = aData + } + + api.addResultForCase(runId: runId, caseId: caseId, data: data) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.addResultForCase(newResult, toRunWithId: runId, toCaseWithId: caseId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-results#add_results + */ + public func addResults(_ newTestResults: NewTestResults, to run: Run, completionHandler: @escaping (Outcome<[Result], AddError>) -> Void) { + addResults(newTestResults, toRunWithId: run.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-results#add_results + */ + public func addResults(_ newTestResults: NewTestResults, toRunWithId runId: Run.Id, completionHandler: @escaping (Outcome<[Result], AddError>) -> Void) { + + let dataOutcome = self.data(from: newTestResults) + let data: Data + switch dataOutcome { + case .failed(let error): + completionHandler(.failed(.objectConversionError(error))) + return + case .succeeded(let aData): + data = aData + } + + api.addResults(runId: runId, data: data) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.addResults(newTestResults, toRunWithId: runId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-results#add_results_for_cases + */ + public func addResultsForCases(_ newCaseResults: NewCaseResults, to run: Run, completionHandler: @escaping (Outcome<[Result], AddError>) -> Void) { + addResultsForCases(newCaseResults, toRunWithId: run.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-results#add_results_for_cases + */ + public func addResultsForCases(_ newCaseResults: NewCaseResults, toRunWithId runId: Run.Id, completionHandler: @escaping (Outcome<[Result], AddError>) -> Void) { + + let dataOutcome = self.data(from: newCaseResults) + let data: Data + switch dataOutcome { + case .failed(let error): + completionHandler(.failed(.objectConversionError(error))) + return + case .succeeded(let aData): + data = aData + } + + api.addResultsForCases(runId: runId, data: data) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.addResultsForCases(newCaseResults, toRunWithId: runId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-results#get_results + */ + public func getResultsForTest(_ test: Test, filteredBy filters: [Filter]? = nil, completionHandler: @escaping (Outcome<[Result], GetError>) -> Void) { + getResultsForTest(test.id, filteredBy: filters, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-results#get_results + */ + public func getResultsForTest(_ testId: Test.Id, filteredBy filters: [Filter]? = nil, completionHandler: @escaping (Outcome<[Result], GetError>) -> Void) { + api.getResults(testId: testId, filters: filters) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getResultsForTest(testId, filteredBy: filters, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-results#get_results_for_case + */ + public func getResultsForCase(_ case: Case, in run: Run, filteredBy filters: [Filter]? = nil, completionHandler: @escaping (Outcome<[Result], GetError>) -> Void) { + getResultsForCase(`case`.id, inRunWithId: run.id, filteredBy: filters, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-results#get_results_for_case + */ + public func getResultsForCase(_ caseId: Case.Id, inRunWithId runId: Run.Id, filteredBy filters: [Filter]? = nil, completionHandler: @escaping (Outcome<[Result], GetError>) -> Void) { + api.getResultsForCase(runId: runId, caseId: caseId, filters: filters) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getResultsForCase(caseId, inRunWithId: runId, filteredBy: filters, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-results#get_results_for_run + */ + public func getResultsForRun(_ run: Run, filteredBy filters: [Filter]? = nil, completionHandler: @escaping (Outcome<[Result], GetError>) -> Void) { + getResultsForRun(run.id, filteredBy: filters, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-results#get_results_for_run + */ + public func getResultsForRun(_ runId: Run.Id, filteredBy filters: [Filter]? = nil, completionHandler: @escaping (Outcome<[Result], GetError>) -> Void) { + api.getResultsForRun(runId: runId, filters: filters) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getResultsForRun(runId, filteredBy: filters, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + // MARK: - ResultField + + /* + http://docs.gurock.com/testrail-api2/reference-results-fields#get_result_fields + */ + public func getResultFields(completionHandler: @escaping (Outcome<[ResultField], GetError>) -> Void) { + api.getResultFields { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getResultFields(completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + // MARK: - Run + + /* + http://docs.gurock.com/testrail-api2/reference-runs#add_run + */ + public func addRun(_ newRun: NewRun, to project: Project, completionHandler: @escaping (Outcome) -> Void) { + addRun(newRun, toProjectWithId: project.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-runs#add_run + */ + public func addRun(_ newRun: NewRun, toProjectWithId projectId: Project.Id, completionHandler: @escaping (Outcome) -> Void) { + + let dataOutcome = self.data(from: newRun) + let data: Data + switch dataOutcome { + case .failed(let error): + completionHandler(.failed(.objectConversionError(error))) + return + case .succeeded(let aData): + data = aData + } + + api.addRun(projectId: projectId, data: data) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.addRun(newRun, toProjectWithId: projectId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-runs#close_run + */ + public func closeRun(_ run: Run, completionHandler: @escaping (Outcome) -> Void) { + closeRun(run.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-runs#close_run + */ + public func closeRun(_ runId: Run.Id, completionHandler: @escaping (Outcome) -> Void) { + api.closeRun(runId: runId) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.closeRun(runId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-runs#delete_run + */ + public func deleteRun(_ run: Run, completionHandler: @escaping (Outcome) -> Void) { + deleteRun(run.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-runs#delete_run + */ + public func deleteRun(_ runId: Run.Id, completionHandler: @escaping (Outcome) -> Void) { + api.deleteRun(runId: runId) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.deleteRun(runId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + switch processedOutcome { + case .failed(let error): + completionHandler(.failed(error)) + case .succeeded(_): + completionHandler(.succeeded(nil)) + } + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-runs#get_run + */ + public func getRun(_ runId: Run.Id, completionHandler: @escaping (Outcome) -> Void) { + api.getRun(runId: runId) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getRun(runId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-runs#get_runs + + This only returns Runs which are not part of a Plan. + */ + public func getRuns(in project: Project, filteredBy filters: [Filter]? = nil, completionHandler: @escaping (Outcome<[Run], GetError>) -> Void) { + getRuns(inProjectWithId: project.id, filteredBy: filters, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-runs#get_runs + + This only returns Runs which are not part of a Plan. + */ + public func getRuns(inProjectWithId projectId: Project.Id, filteredBy filters: [Filter]? = nil, completionHandler: @escaping (Outcome<[Run], GetError>) -> Void) { + api.getRuns(projectId: projectId, filters: filters) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getRuns(inProjectWithId: projectId, filteredBy: filters, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-runs#update_run + */ + public func updateRun(_ run: Run, completionHandler: @escaping (Outcome) -> Void) { + + let dataOutcome = self.data(from: run) + let data: Data + switch dataOutcome { + case .failed(let error): + completionHandler(.failed(.objectConversionError(error))) + return + case .succeeded(let aData): + data = aData + } + + api.updateRun(runId: run.id, data: data) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.updateRun(run, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + // MARK: - Section + + /* + http://docs.gurock.com/testrail-api2/reference-sections#add_section + */ + public func addSection(_ newSection: NewSection, to project: Project, completionHandler: @escaping (Outcome) -> Void) { + addSection(newSection, toProjectWithId: project.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-sections#add_section + */ + public func addSection(_ newSection: NewSection, toProjectWithId projectId: Project.Id, completionHandler: @escaping (Outcome) -> Void) { + + let dataOutcome = self.data(from: newSection) + let data: Data + switch dataOutcome { + case .failed(let error): + completionHandler(.failed(.objectConversionError(error))) + return + case .succeeded(let aData): + data = aData + } + + api.addSection(projectId: projectId, data: data) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.addSection(newSection, toProjectWithId: projectId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-sections#delete_section + */ + public func deleteSection(_ section: Section, completionHandler: @escaping (Outcome) -> Void) { + deleteSection(section.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-sections#delete_section + */ + public func deleteSection(_ sectionId: Section.Id, completionHandler: @escaping (Outcome) -> Void) { + api.deleteSection(sectionId: sectionId) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.deleteSection(sectionId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + switch processedOutcome { + case .failed(let error): + completionHandler(.failed(error)) + case .succeeded(_): + completionHandler(.succeeded(nil)) + } + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-sections#get_section + */ + public func getSection(_ sectionId: Section.Id, completionHandler: @escaping (Outcome) -> Void) { + api.getSection(sectionId: sectionId) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getSection(sectionId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-sections#get_sections + */ + public func getSections(in project: Project, in suite: Suite? = nil, completionHandler: @escaping (Outcome<[Section], GetError>) -> Void) { + getSections(inProjectWithId: project.id, inSuiteWithId: suite?.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-sections#get_sections + */ + public func getSections(inProjectWithId projectId: Project.Id, inSuiteWithId suiteId: Suite.Id? = nil, completionHandler: @escaping (Outcome<[Section], GetError>) -> Void) { + api.getSections(projectId: projectId, suiteId: suiteId) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getSections(inProjectWithId: projectId, inSuiteWithId: suiteId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-sections#update_section + */ + public func updateSection(_ section: Section, completionHandler: @escaping (Outcome) -> Void) { + + let dataOutcome = self.data(from: section) + let data: Data + switch dataOutcome { + case .failed(let error): + completionHandler(.failed(.objectConversionError(error))) + return + case .succeeded(let aData): + data = aData + } + + api.updateSection(sectionId: section.id, data: data) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.updateSection(section, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + // MARK: - Status + + /* + http://docs.gurock.com/testrail-api2/reference-statuses#get_statuses + */ + public func getStatuses(completionHandler: @escaping (Outcome<[Status], GetError>) -> Void) { + api.getStatuses { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getStatuses(completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + // MARK: - Suite + + /* + http://docs.gurock.com/testrail-api2/reference-suites#add_suite + */ + public func addSuite(_ newSuite: NewSuite, to project: Project, completionHandler: @escaping (Outcome) -> Void) { + addSuite(newSuite, toProjectWithId: project.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-suites#add_suite + */ + public func addSuite(_ newSuite: NewSuite, toProjectWithId projectId: Project.Id, completionHandler: @escaping (Outcome) -> Void) { + + let dataOutcome = self.data(from: newSuite) + let data: Data + switch dataOutcome { + case .failed(let error): + completionHandler(.failed(.objectConversionError(error))) + return + case .succeeded(let aData): + data = aData + } + + api.addSuite(projectId: projectId, data: data) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.addSuite(newSuite, toProjectWithId: projectId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-suites#delete_suite + */ + public func deleteSuite(_ suite: Suite, completionHandler: @escaping (Outcome) -> Void) { + deleteSuite(suite.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-suites#delete_suite + */ + public func deleteSuite(_ suiteId: Suite.Id, completionHandler: @escaping (Outcome) -> Void) { + api.deleteSuite(suiteId: suiteId) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.deleteSuite(suiteId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + switch processedOutcome { + case .failed(let error): + completionHandler(.failed(error)) + case .succeeded(_): + completionHandler(.succeeded(nil)) + } + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-suites#get_suite + */ + public func getSuite(_ suiteId: Suite.Id, completionHandler: @escaping (Outcome) -> Void) { + api.getSuite(suiteId: suiteId) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getSuite(suiteId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-suites#get_suites + */ + public func getSuites(in project: Project, completionHandler: @escaping (Outcome<[Suite], GetError>) -> Void) { + getSuites(inProjectWithId: project.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-suites#get_suites + */ + public func getSuites(inProjectWithId projectId: Project.Id, completionHandler: @escaping (Outcome<[Suite], GetError>) -> Void) { + api.getSuites(projectId: projectId) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getSuites(inProjectWithId: projectId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-suites#update_suite + */ + public func updateSuite(_ suite: Suite, completionHandler: @escaping (Outcome) -> Void) { + + let dataOutcome = self.data(from: suite) + let data: Data + switch dataOutcome { + case .failed(let error): + completionHandler(.failed(.objectConversionError(error))) + return + case .succeeded(let aData): + data = aData + } + + api.updateSuite(suiteId: suite.id, data: data) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.updateSuite(suite, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + // MARK: - Template + + /* + Gets all Templates from all Projects. A template may appear in some or all + projects. + + - Returns a de-duped array of 0+ Templates from all Projects upon success. + - Returns an ErrorContainer of 1+ errors if any of the GET requests failed. + + This method makes multiple async API calls across all accessible projects + to gather all templates. It may be expensive/slow to run if you have many + projects. + */ + public func getTemplates(completionHandler: @escaping (Outcome<[Template], ErrorContainer>) -> Void) { + + getProjects { [weak self] (projectsOutcome) in + + switch projectsOutcome { + case .failed(let error): + completionHandler(.failed(ErrorContainer(error))) + case .succeeded(let projects): + + let projectIds = Set(projects.flatMap({ $0.id })) + self?.getTemplates(inProjectsWithIds: projectIds) { (templatesOutcomes) in + + let outcomes = templatesOutcomes.flatMap { $1 } // Discard projectId keys. + var allTemplates = [Template]() + var allErrors = [GetError]() + + // Extract all templates/errors from outcomes. + for outcome in outcomes { + switch outcome { + case .failed(let error): + allErrors.append(error) + case .succeeded(let templates): + for template in templates { + guard allTemplates.filter({ $0.id == template.id }).count == 0 else { // Skip duplicates. + continue + } + allTemplates.append(template) + } + } + } + + if let errorContainer = ErrorContainer(allErrors) { + completionHandler(.failed(errorContainer)) // Fail if there are any errors. + } else { + completionHandler(.succeeded(allTemplates)) + } + } + } + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-templates#get_templates + */ + public func getTemplates(in project: Project, completionHandler: @escaping (Outcome<[Template], GetError>) -> Void) { + getTemplates(inProjectWithId: project.id, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-templates#get_templates + */ + public func getTemplates(inProjectWithId projectId: Project.Id, completionHandler: @escaping (Outcome<[Template], GetError>) -> Void) { + api.getTemplates(projectId: projectId) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getTemplates(inProjectWithId: projectId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + // MARK: - Test + + /* + http://docs.gurock.com/testrail-api2/reference-tests#get_test + */ + public func getTest(_ testId: Test.Id, completionHandler: @escaping (Outcome) -> Void) { + api.getTest(testId: testId) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getTest(testId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-tests#get_tests + */ + public func getTests(in run: Run, filteredBy filters: [Filter]? = nil, completionHandler: @escaping (Outcome<[Test], GetError>) -> Void) { + getTests(inRunWithId: run.id, filteredBy: filters, completionHandler: completionHandler) + } + + /* + http://docs.gurock.com/testrail-api2/reference-tests#get_tests + */ + public func getTests(inRunWithId runId: Run.Id, filteredBy filters: [Filter]? = nil, completionHandler: @escaping (Outcome<[Test], GetError>) -> Void) { + api.getTests(runId: runId, filters: filters) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getTests(inRunWithId: runId, filteredBy: filters, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + // MARK: - User + + /* + http://docs.gurock.com/testrail-api2/reference-users#get_user + */ + public func getUser(_ userId: User.Id, completionHandler: @escaping (Outcome) -> Void) { + api.getUser(userId: userId) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getUser(userId, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-users#get_user_by_email + */ + public func getUserByEmail(_ email: String, completionHandler: @escaping (Outcome) -> Void) { + api.getUserByEmail(email) { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getUserByEmail(email, completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + + /* + http://docs.gurock.com/testrail-api2/reference-users#get_users + */ + public func getUsers(completionHandler: @escaping (Outcome<[User], GetError>) -> Void) { + api.getUsers { [weak self] (apiRequestOutcome) in + self?.process(apiRequestOutcome, retryHandler: { + self?.getUsers(completionHandler: completionHandler) + }, completionHandler: { (processedOutcome) in + completionHandler(processedOutcome) + }) + } + } + +} + +// MARK: - Objects (Fileprivate Matching) + +extension ObjectAPI { + + /* + - Upon success returns an Outcome with items from |items| matching all + |ids|. Requires every id in |ids| to match an item. + - Upon failure returns an Outcome indicating if no matches were found, or + if only partial matches were found. + */ + fileprivate static func matches(from items: [Item], matchingIds ids: Set) -> Outcome<[Item], MultipleMatchError> { + + // Find all matches. + var matches = [Item]() + var missing = Set() + + for id in ids { + guard let item = items.filter({ $0.id == id }).first else { + missing.insert(id) + continue + } + guard matches.contains(where: { $0.id == item.id }) == false else { + continue + } + matches.append(item) + } + + // Were all matches found? + guard missing.count == 0 else { + if matches.count == 0 { + return .failed(.noMatchesFound(missing: missing)) + } else { + return .failed(.partialMatchesFound(matches: matches, missing: missing)) + } + } + + // All matches found. + return .succeeded(matches) + } + +} + +// MARK: - Objects (Matching) + +extension ObjectAPI { + + // MARK: - CaseType + + public func getCaseType(matching id: CaseType.Id, completionHandler: @escaping (Outcome, GetError>>) -> Void) { + getCaseTypes { (outcome) in + switch outcome { + case .failed(let error): + completionHandler(.failed(.otherError(error))) + case .succeeded(let caseTypes): + if let caseType = caseTypes.filter({ $0.id == id }).first { + completionHandler(.succeeded(caseType)) + } else { + completionHandler(.failed(.matchError(.noMatchFound(missing: id)))) + } + } + } + } + + // MARK: - ConfigurationGroup + + public func getConfigurationGroup(matching id: ConfigurationGroup.Id, completionHandler: @escaping (Outcome, ErrorContainer>>) -> Void) { + getConfigurationGroups { (outcome) in + switch outcome { + case .failed(let error): + completionHandler(.failed(.otherError(error))) + case .succeeded(let configurationGroups): + if let configurationGroup = configurationGroups.filter({ $0.id == id }).first { + completionHandler(.succeeded(configurationGroup)) + } else { + completionHandler(.failed(.matchError(.noMatchFound(missing: id)))) + } + } + } + } + + // MARK: - Priority + + public func getPriority(matching id: Priority.Id, completionHandler: @escaping (Outcome, GetError>>) -> Void) { + getPriorities { (outcome) in + switch outcome { + case .failed(let error): + completionHandler(.failed(.otherError(error))) + case .succeeded(let priorities): + if let priority = priorities.filter({ $0.id == id }).first { + completionHandler(.succeeded(priority)) + } else { + completionHandler(.failed(.matchError(.noMatchFound(missing: id)))) + } + } + } + } + + // MARK: - Status + + public func getStatus(matching id: Status.Id, completionHandler: @escaping (Outcome, GetError>>) -> Void) { + getStatuses { (outcome) in + switch outcome { + case .failed(let error): + completionHandler(.failed(.otherError(error))) + case .succeeded(let statuses): + if let status = statuses.filter({ $0.id == id }).first { + completionHandler(.succeeded(status)) + } else { + completionHandler(.failed(.matchError(.noMatchFound(missing: id)))) + } + } + } + } + + // MARK: - Template + + public func getTemplate(matching id: Template.Id, completionHandler: @escaping (Outcome, ErrorContainer>>) -> Void) { + getTemplates { (outcome) in + switch outcome { + case .failed(let errors): + completionHandler(.failed(.otherError(errors))) + case .succeeded(let templates): + if let template = templates.filter({ $0.id == id }).first { + completionHandler(.succeeded(template)) + } else { + completionHandler(.failed(.matchError(.noMatchFound(missing: id)))) + } + } + } + } + + public func getTemplates(matching ids: [Template.Id], completionHandler: @escaping (Outcome<[Template], MatchError, ErrorContainer>>) -> Void) { + getTemplates(matching: Set(ids), completionHandler: completionHandler) // De-dupe ids + } + + private func getTemplates(matching ids: Set, completionHandler: @escaping (Outcome<[Template], MatchError, ErrorContainer>>) -> Void) { + getTemplates { (outcome) in + switch outcome { + case .failed(let errors): + completionHandler(.failed(.otherError(errors))) + case .succeeded(let templates): + let matchesOutcome = ObjectAPI.matches(from: templates, matchingIds: ids) + switch matchesOutcome { + case .failed(let error): + completionHandler(.failed(.matchError(error))) + case .succeeded(let matches): + completionHandler(.succeeded(matches)) + } + } + } + } + +} + +// MARK: - Objects (Forward Relationships) + +extension ObjectAPI { + + // MARK: - Case + + public func createdBy(_ `case`: Case, completionHandler: @escaping (Outcome) -> Void) { + getUser(`case`.createdBy, completionHandler: completionHandler) + } + + public func milestone(_ `case`: Case, completionHandler: @escaping (Outcome) -> Void) { + milestone(`case`.milestoneId, completionHandler: completionHandler) + } + + public func priority(_ `case`: Case, completionHandler: @escaping (Outcome, GetError>>) -> Void) { + getPriority(matching: `case`.priorityId, completionHandler: completionHandler) + } + + public func section(_ `case`: Case, completionHandler: @escaping (Outcome) -> Void) { + section(`case`.sectionId, completionHandler: completionHandler) + } + + public func suite(_ `case`: Case, completionHandler: @escaping (Outcome) -> Void) { + suite(`case`.suiteId, completionHandler: completionHandler) + } + + public func template(_ `case`: Case, completionHandler: @escaping (Outcome, ErrorContainer>>) -> Void) { + getTemplate(matching: `case`.templateId, completionHandler: completionHandler) + } + + public func type(_ `case`: Case, completionHandler: @escaping (Outcome, GetError>>) -> Void) { + getCaseType(matching: `case`.typeId, completionHandler: completionHandler) + } + + public func updatedBy(_ `case`: Case, completionHandler: @escaping (Outcome) -> Void) { + getUser(`case`.updatedBy, completionHandler: completionHandler) + } + + // MARK: - CaseField + + public func templates(_ caseField: CaseField, completionHandler: @escaping (Outcome<[Template], MatchError, ErrorContainer>>) -> Void) { + getTemplates(matching: caseField.templateIds, completionHandler: completionHandler) + } + + // MARK: - Config + + /* + Calls the projects() method handling/stripping the MultipleMatchError. Upon + success this effectively returns all projects accessible to the current API + user while silently omitting any which are missing. Failure only occurs if + one or more GetError's occurred. + */ + public func accessibleProjects(_ config: Config, completionHandler: @escaping(Outcome<[Project]?, ErrorContainer>) -> Void) { + projects(config) { (outcome) in + switch outcome { + case .failed(let error): + switch error { + case .matchError(let matchError): + switch matchError { + case .noMatchesFound(_): + completionHandler(.succeeded([])) + case .partialMatchesFound(let matches, _): + completionHandler(.succeeded(matches)) + } + case .otherError(let errorContainer): + completionHandler(.failed(errorContainer)) + } + case .succeeded(let projects): + completionHandler(.succeeded(projects)) + } + } + } + + /* + Asynchronously gets and returns projects for a Config. The outcome passed + to the completionHandler varies based on: + + 1. The value of Config.projectIds. + 2. The authorization level of api.username for each project. + + api.username can read projects it has Read-only or higher access to. + Project access can be limited to some or all users. This limitation makes + it impossible for this method to guarentee all projects will be returned + for a Config. Possible scenarios for Config.projectIds values: + + - .none always passes .succeeded(nil) to the handler. + - .all passes .succeeded(projects) to the handler upon success. This + includes all projects the user has access to while silently omitting any + they do not. .failed(errorContainer) will be passed if there were any + errors. + - .some is the same as .all except it will fail if any specified projectIds + return a 403 error. + + --------------------------------------------------------------------------- + + If .some returns a MultipleMatchError you can potentially recover from it. + This indicates all projectIds were valid but some/all returned 403 errors. + To recover: + + 1. Change the user used by the API to one which has access to some/all of + the inaccessible projectIds. + 2. Re-call this command again. + 3. Merge and de-duplicate results. + 4. Repeat until all projects have been received for projectIds. + + If any project has "No Access" set to all users it will not be possible for + any user to read it without changing its access level. + */ + // swiftlint:disable:next cyclomatic_complexity + public func projects(_ config: Config, completionHandler: @escaping(Outcome<[Project]?, MatchError, ErrorContainer>>) -> Void) { + switch config.projects { + case .none: + completionHandler(.succeeded(nil)) + case .all: + getProjects { (outcome) in + switch outcome { + case .failed(let error): + completionHandler(.failed(.otherError(ErrorContainer(error)))) + case .succeeded(let projects): + completionHandler(.succeeded(projects)) + } + } + case .some(let projectIds): + getProjects(projectIds) { (outcomes) in + + var projects = [Project]() + var inaccessibleProjectIds = Set() + var non403Errors = [GetError]() + + for (projectId, outcome) in outcomes { + switch outcome { + case .failed(let getError): + if case let .statusCodeError(.clientError(clientError)) = getError, clientError.statusCode == 403 { + inaccessibleProjectIds.insert(projectId) // 403 errors. + } else { + non403Errors.append(getError) + } + case .succeeded(let project): + projects.append(project) + } + } + + // Fail if there are any non-403 errors. + if let errorContainer = ErrorContainer(non403Errors) { + completionHandler(.failed(.otherError(errorContainer))) + return + } + + // If some/all of the projectIds were 403 return any matches and + // all missing. + guard inaccessibleProjectIds.count == 0 else { + if projects.count == 0 { + completionHandler(.failed(.matchError(.noMatchesFound(missing: inaccessibleProjectIds)))) + } else { + completionHandler(.failed(.matchError(.partialMatchesFound(matches: projects, missing: inaccessibleProjectIds)))) + } + return + } + + // All projectIds were found. + completionHandler(.succeeded(projects)) + } + } + } + + // MARK: - Configuration + + public func configurationGroup(_ configuration: Configuration, completionHandler: @escaping (Outcome, ErrorContainer>>) -> Void) { + getConfigurationGroup(matching: configuration.groupId, completionHandler: completionHandler) + } + + // MARK: - ConfigurationGroup + + public func project(_ configurationGroup: ConfigurationGroup, completionHandler: @escaping (Outcome) -> Void) { + getProject(configurationGroup.projectId, completionHandler: completionHandler) + } + + // MARK: - Milestone + + public func parent(_ milestone: Milestone, completionHandler: @escaping (Outcome) -> Void) { + parent(milestone.parentId, completionHandler: completionHandler) + } + + public func project(_ milestone: Milestone, completionHandler: @escaping (Outcome) -> Void) { + getProject(milestone.projectId, completionHandler: completionHandler) + } + + // MARK: - Plan + + public func assignedto(_ plan: Plan, completionHandler: @escaping (Outcome) -> Void) { + assignedto(plan.assignedtoId, completionHandler: completionHandler) + } + + public func createdBy(_ plan: Plan, completionHandler: @escaping (Outcome) -> Void) { + getUser(plan.createdBy, completionHandler: completionHandler) + } + + public func milestone(_ plan: Plan, completionHandler: @escaping (Outcome) -> Void) { + milestone(plan.milestoneId, completionHandler: completionHandler) + } + + public func project(_ plan: Plan, completionHandler: @escaping (Outcome) -> Void) { + getProject(plan.projectId, completionHandler: completionHandler) + } + + // MARK: - Plan.Entry + + public func suite(_ planEntry: Plan.Entry, completionHandler: @escaping (Outcome) -> Void) { + getSuite(planEntry.suiteId, completionHandler: completionHandler) + } + + // MARK: - Result + + public func assignedto(_ result: Result, completionHandler: @escaping (Outcome) -> Void) { + assignedto(result.assignedtoId, completionHandler: completionHandler) + } + + public func createdBy(_ result: Result, completionHandler: @escaping (Outcome) -> Void) { + getUser(result.createdBy, completionHandler: completionHandler) + } + + public func status(_ result: Result, completionHandler: @escaping (Outcome, GetError>>) -> Void) { + status(matching: result.statusId, completionHandler: completionHandler) + } + + public func test(_ result: Result, completionHandler: @escaping (Outcome) -> Void) { + getTest(result.testId, completionHandler: completionHandler) + } + + // MARK: - ResultField + + public func templates(_ resultField: ResultField, completionHandler: @escaping (Outcome<[Template], MatchError, ErrorContainer>>) -> Void) { + getTemplates(matching: resultField.templateIds, completionHandler: completionHandler) + } + + // MARK: - Run + + public func assignedto(_ run: Run, completionHandler: @escaping (Outcome) -> Void) { + assignedto(run.assignedtoId, completionHandler: completionHandler) + } + + public func configurations(_ run: Run, completionHandler: @escaping (Outcome<[Configuration]?, MatchError, GetError>>) -> Void) { + configurations(inProjectWithId: run.projectId, matching: run.configIds, completionHandler: completionHandler) + } + + public func createdBy(_ run: Run, completionHandler: @escaping (Outcome) -> Void) { + getUser(run.createdBy, completionHandler: completionHandler) + } + + public func milestone(_ run: Run, completionHandler: @escaping (Outcome) -> Void) { + milestone(run.milestoneId, completionHandler: completionHandler) + } + + public func plan(_ run: Run, completionHandler: @escaping (Outcome) -> Void) { + plan(run.planId, completionHandler: completionHandler) + } + + public func project(_ run: Run, completionHandler: @escaping (Outcome) -> Void) { + getProject(run.projectId, completionHandler: completionHandler) + } + + public func suite(_ run: Run, completionHandler: @escaping (Outcome) -> Void) { + suite(run.suiteId, completionHandler: completionHandler) + } + + // MARK: - Section + + public func parent(_ section: Section, completionHandler: @escaping (Outcome) -> Void) { + parent(section.parentId, completionHandler: completionHandler) + } + + public func suite(_ section: Section, completionHandler: @escaping (Outcome) -> Void) { + suite(section.suiteId, completionHandler: completionHandler) + } + + // MARK: - Suite + + public func project(_ suite: Suite, completionHandler: @escaping (Outcome) -> Void) { + getProject(suite.projectId, completionHandler: completionHandler) + } + + // MARK: - Test + + public func assignedto(_ test: Test, completionHandler: @escaping (Outcome) -> Void) { + assignedto(test.assignedtoId, completionHandler: completionHandler) + } + + public func `case`(_ test: Test, completionHandler: @escaping (Outcome) -> Void) { + getCase(test.caseId, completionHandler: completionHandler) + } + + public func milestone(_ test: Test, completionHandler: @escaping (Outcome) -> Void) { + milestone(test.milestoneId, completionHandler: completionHandler) + } + + public func priority(_ test: Test, completionHandler: @escaping (Outcome, GetError>>) -> Void) { + getPriority(matching: test.priorityId, completionHandler: completionHandler) + } + + public func run(_ test: Test, completionHandler: @escaping (Outcome) -> Void) { + getRun(test.runId, completionHandler: completionHandler) + } + + public func status(_ test: Test, completionHandler: @escaping (Outcome, GetError>>) -> Void) { + getStatus(matching: test.statusId, completionHandler: completionHandler) + } + + public func template(_ test: Test, completionHandler: @escaping (Outcome, ErrorContainer>>) -> Void) { + getTemplate(matching: test.templateId, completionHandler: completionHandler) + } + + public func type(_ test: Test, completionHandler: @escaping (Outcome, GetError>>) -> Void) { + getCaseType(matching: test.typeId, completionHandler: completionHandler) + } + + // MARK: - Private Helpers + + // MARK: GET + + private func assignedto(_ userId: User.Id?, completionHandler: @escaping (Outcome) -> Void) { + if let userId = userId { + getUser(userId) { (outcome) in + switch outcome { + case .failed(let error): + completionHandler(.failed(error)) + case .succeeded(let user): + completionHandler(.succeeded(user)) + } + } + } else { + completionHandler(.succeeded(nil)) + } + } + + private func milestone(_ milestoneId: Milestone.Id?, completionHandler: @escaping (Outcome) -> Void) { + if let milestoneId = milestoneId { + getMilestone(milestoneId) { (outcome) in + switch outcome { + case .failed(let error): + completionHandler(.failed(error)) + case .succeeded(let milestone): + completionHandler(.succeeded(milestone)) + } + } + } else { + completionHandler(.succeeded(nil)) + } + } + + private func milestone(_ milestoneId: Milestone.Id, completionHandler: @escaping (Outcome) -> Void) { + getMilestone(milestoneId) { (outcome) in + switch outcome { + case .failed(let error): + completionHandler(.failed(error)) + case .succeeded(let milestone): + completionHandler(.succeeded(milestone)) + } + } + } + + private func parent(_ milestoneId: Milestone.Id?, completionHandler: @escaping (Outcome) -> Void) { + milestone(milestoneId, completionHandler: completionHandler) + } + + private func parent(_ milestoneId: Milestone.Id, completionHandler: @escaping (Outcome) -> Void) { + milestone(milestoneId, completionHandler: completionHandler) + } + + private func parent(_ sectionId: Section.Id?, completionHandler: @escaping (Outcome) -> Void) { + section(sectionId, completionHandler: completionHandler) + } + + private func plan(_ planId: Plan.Id?, completionHandler: @escaping (Outcome) -> Void) { + if let planId = planId { + getPlan(planId) { (outcome) in + switch outcome { + case .failed(let error): + completionHandler(.failed(error)) + case .succeeded(let plan): + completionHandler(.succeeded(plan)) + } + } + } else { + completionHandler(.succeeded(nil)) + } + } + + private func section(_ sectionId: Section.Id?, completionHandler: @escaping (Outcome) -> Void) { + if let sectionId = sectionId { + getSection(sectionId) { (outcome) in + switch outcome { + case .failed(let error): + completionHandler(.failed(error)) + case .succeeded(let section): + completionHandler(.succeeded(section)) + } + } + } else { + completionHandler(.succeeded(nil)) + } + } + + private func section(_ sectionId: Section.Id, completionHandler: @escaping (Outcome) -> Void) { + getSection(sectionId) { (outcome) in + switch outcome { + case .failed(let error): + completionHandler(.failed(error)) + case .succeeded(let section): + completionHandler(.succeeded(section)) + } + } + } + + private func suite(_ suiteId: Suite.Id?, completionHandler: @escaping (Outcome) -> Void) { + if let suiteId = suiteId { + getSuite(suiteId) { (outcome) in + switch outcome { + case .failed(let error): + completionHandler(.failed(error)) + case .succeeded(let suite): + completionHandler(.succeeded(suite)) + } + } + } else { + completionHandler(.succeeded(nil)) + } + } + + // MARK: Matching + + private func configurations(inProjectWithId projectId: Project.Id, matching configurationIds: [Configuration.Id]?, completionHandler: @escaping (Outcome<[Configuration]?, MatchError, GetError>>) -> Void) { + let uniqueConfigurationIds: Set? + if let configurationIds = configurationIds { + uniqueConfigurationIds = Set(configurationIds) + } else { + uniqueConfigurationIds = nil + } + configurations(inProjectWithId: projectId, matching: uniqueConfigurationIds, completionHandler: completionHandler) + } + + private func configurations(inProjectWithId projectId: Project.Id, matching configurationIds: [Configuration.Id], completionHandler: @escaping (Outcome<[Configuration], MatchError, GetError>>) -> Void) { + let uniqueIds = Set(configurationIds) + configurations(inProjectWithId: projectId, matching: uniqueIds, completionHandler: completionHandler) + } + + private func configurations(inProjectWithId projectId: Project.Id, matching configurationIds: Set?, completionHandler: @escaping (Outcome<[Configuration]?, MatchError, GetError>>) -> Void) { + if let ids = configurationIds { + getConfigurationGroups(inProjectWithId: projectId) { (outcome) in + switch outcome { + case .failed(let error): + completionHandler(.failed(.otherError(error))) + case .succeeded(let configurationGroups): + let configurations = configurationGroups.flatMap { $0.configs } + let matchesOutcome = ObjectAPI.matches(from: configurations, matchingIds: ids) + switch matchesOutcome { + case .failed(let error): + completionHandler(.failed(.matchError(error))) + case .succeeded(let matches): + completionHandler(.succeeded(matches)) + } + } + } + } else { + completionHandler(.succeeded(nil)) + } + } + + private func configurations(inProjectWithId projectId: Project.Id, matching configurationIds: Set, completionHandler: @escaping (Outcome<[Configuration], MatchError, GetError>>) -> Void) { + getConfigurationGroups(inProjectWithId: projectId) { (outcome) in + switch outcome { + case .failed(let error): + completionHandler(.failed(.otherError(error))) + case .succeeded(let configurationGroups): + let configurations = configurationGroups.flatMap { $0.configs } + let matchesOutcome = ObjectAPI.matches(from: configurations, matchingIds: configurationIds) + switch matchesOutcome { + case .failed(let error): + completionHandler(.failed(.matchError(error))) + case .succeeded(let matches): + completionHandler(.succeeded(matches)) + } + } + } + } + + private func status(matching id: Status.Id?, completionHandler: @escaping (Outcome, GetError>>) -> Void) { + if let id = id { + getStatus(matching: id) { (outcome) in + switch outcome { + case .failed(let error): + completionHandler(.failed(error)) + case .succeeded(let status): + completionHandler(.succeeded(status)) + } + } + } else { + completionHandler(.succeeded(nil)) + } + } + + private func status(matching id: Status.Id, completionHandler: @escaping (Outcome, GetError>>) -> Void) { + getStatus(matching: id) { (outcome) in + switch outcome { + case .failed(let error): + completionHandler(.failed(error)) + case .succeeded(let status): + completionHandler(.succeeded(status)) + } + } + } + +} diff --git a/QuizTrain/Network/Operations/GetConfigurationGroupsOperation.swift b/QuizTrain/Network/Operations/GetConfigurationGroupsOperation.swift new file mode 100644 index 0000000..318d186 --- /dev/null +++ b/QuizTrain/Network/Operations/GetConfigurationGroupsOperation.swift @@ -0,0 +1,46 @@ +import Foundation + +class GetConfigurationGroupsOperation: AsyncOperation { + + // MARK: Properties + + private weak var _api: ObjectAPI? + var api: ObjectAPI? { return _api } + let projectId: Project.Id + + // MARK: Init + + init(api: ObjectAPI, projectId: Project.Id) { + self._api = api + self.projectId = projectId + } + + // MARK: Execution + + override func start() { + + guard isCancelled == false else { + state = .finished + return + } + + guard let api = api else { + cancel() + state = .finished + return + } + + state = .executing + + api.getConfigurationGroups(inProjectWithId: projectId) { [weak self] (outcome) in + self?._outcome = outcome + self?.state = .finished + } + } + + // MARK: Completion + + private var _outcome: Outcome<[ConfigurationGroup], ObjectAPI.GetError>? + var outcome: Outcome<[ConfigurationGroup], ObjectAPI.GetError>? { return _outcome } + +} diff --git a/QuizTrain/Network/Operations/GetProjectOperation.swift b/QuizTrain/Network/Operations/GetProjectOperation.swift new file mode 100644 index 0000000..9efa811 --- /dev/null +++ b/QuizTrain/Network/Operations/GetProjectOperation.swift @@ -0,0 +1,46 @@ +import Foundation + +class GetProjectOperation: AsyncOperation { + + // MARK: Properties + + private weak var _api: ObjectAPI? + var api: ObjectAPI? { return _api } + let projectId: Project.Id + + // MARK: Init + + init(api: ObjectAPI, projectId: Project.Id) { + self._api = api + self.projectId = projectId + } + + // MARK: Execution + + override func start() { + + guard isCancelled == false else { + state = .finished + return + } + + guard let api = api else { + cancel() + state = .finished + return + } + + state = .executing + + api.getProject(projectId) { [weak self] (outcome) in + self?._outcome = outcome + self?.state = .finished + } + } + + // MARK: Completion + + private var _outcome: Outcome? + var outcome: Outcome? { return _outcome } + +} diff --git a/QuizTrain/Network/Operations/GetTemplatesOperation.swift b/QuizTrain/Network/Operations/GetTemplatesOperation.swift new file mode 100644 index 0000000..41ae3f2 --- /dev/null +++ b/QuizTrain/Network/Operations/GetTemplatesOperation.swift @@ -0,0 +1,46 @@ +import Foundation + +class GetTemplatesOperation: AsyncOperation { + + // MARK: Properties + + private weak var _api: ObjectAPI? + var api: ObjectAPI? { return _api } + let projectId: Project.Id + + // MARK: Init + + init(api: ObjectAPI, projectId: Project.Id) { + self._api = api + self.projectId = projectId + } + + // MARK: Execution + + override func start() { + + guard isCancelled == false else { + state = .finished + return + } + + guard let api = api else { + cancel() + state = .finished + return + } + + state = .executing + + api.getTemplates(inProjectWithId: projectId) { [weak self] (outcome) in + self?._outcome = outcome + self?.state = .finished + } + } + + // MARK: Completion + + private var _outcome: Outcome<[Template], ObjectAPI.GetError>? + var outcome: Outcome<[Template], ObjectAPI.GetError>? { return _outcome } + +} diff --git a/QuizTrain/QuizTrain.h b/QuizTrain/QuizTrain.h new file mode 100644 index 0000000..4868660 --- /dev/null +++ b/QuizTrain/QuizTrain.h @@ -0,0 +1,37 @@ +// +// QuizTrain.h +// QuizTrain +// +// Created by David Gallagher (david.gallagher@venmo.com). +// Copyright © 2018 Venmo. All rights reserved. +// +// _____LICENSE_____ +// +// Copyright 2018 Venmo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +#import + +//! Project version number for QuizTrain. +FOUNDATION_EXPORT double QuizTrainVersionNumber; + +//! Project version string for QuizTrain. +FOUNDATION_EXPORT const unsigned char QuizTrainVersionString[]; diff --git a/QuizTrainTests/.swiftlint.yml b/QuizTrainTests/.swiftlint.yml new file mode 100644 index 0000000..511bf9f --- /dev/null +++ b/QuizTrainTests/.swiftlint.yml @@ -0,0 +1,3 @@ +disabled_rules: + - force_cast + - type_name diff --git a/QuizTrainTests/Info.plist b/QuizTrainTests/Info.plist new file mode 100644 index 0000000..6c40a6c --- /dev/null +++ b/QuizTrainTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/QuizTrainTests/Misc/Containment/Containers/CustomFieldsContainerTests.swift b/QuizTrainTests/Misc/Containment/Containers/CustomFieldsContainerTests.swift new file mode 100644 index 0000000..557c7aa --- /dev/null +++ b/QuizTrainTests/Misc/Containment/Containers/CustomFieldsContainerTests.swift @@ -0,0 +1,228 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class CustomFieldsContainerTests: XCTestCase { + + typealias Object = CustomFieldsContainer + + func testInit() { + + XCTAssertEqual(Object(json: customFields).customFields.count, customFields.count) + + for (k, v) in customFields { + XCTAssertEqual(Object(json: [k: v]).customFields.count, 1) + } + } + + func testInitWithEmptyCustomFields() { + XCTAssertEqual(Object(json: emptyCustomFields).customFields.count, 0) + } + + func testInitWithInvalidCustomFields() { + + XCTAssertEqual(Object(json: invalidCustomFields).customFields.count, 0) + + for (k, v) in invalidCustomFields { + XCTAssertEqual(Object(json: [k: v]).customFields.count, 0) + } + } + + func testInitWithValidAndInvalidCustomFields() { + + let object = Object(json: validAndInvalidCustomFields) + XCTAssertEqual(object.customFields.count, customFields.count) + + for (k, _) in object.customFields { + XCTAssertTrue(customFieldsKeys.contains(k)) + XCTAssertFalse(invalidCustomFieldsKeys.contains(k)) + } + } + + func testInitOmittingCustomKeys() { + + XCTAssertEqual(Object(json: customFields, omittingKeys: []).customFields.count, customFields.count) + XCTAssertEqual(Object(json: customFields, omittingKeys: customFieldsKeys).customFields.count, 0) + + for key in customFieldsKeys { + let object = Object(json: customFields, omittingKeys: [key]) + XCTAssertEqual(object.customFields.count, customFields.count - 1) + XCTAssertNil(object.customFields[key]) + } + } + + func testInitOmittingCustomKeysWithEmptyCustomFields() { + XCTAssertEqual(Object(json: emptyCustomFields, omittingKeys: []).customFields.count, 0) + XCTAssertEqual(Object(json: emptyCustomFields, omittingKeys: customFieldsKeys).customFields.count, 0) + } + + func testInitOmittingCustomKeysWithInvalidCustomFields() { + XCTAssertEqual(Object(json: invalidCustomFields, omittingKeys: []).customFields.count, 0) + XCTAssertEqual(Object(json: invalidCustomFields, omittingKeys: customFieldsKeys).customFields.count, 0) + XCTAssertEqual(Object(json: invalidCustomFields, omittingKeys: invalidCustomFieldsKeys).customFields.count, 0) + } + + func testInitOmittingCustomKeysWithValidAndInvalidCustomFields() { + XCTAssertEqual(Object(json: validAndInvalidCustomFields, omittingKeys: []).customFields.count, customFields.count) + XCTAssertEqual(Object(json: validAndInvalidCustomFields, omittingKeys: invalidCustomFieldsKeys).customFields.count, customFields.count) + XCTAssertEqual(Object(json: validAndInvalidCustomFields, omittingKeys: customFieldsKeys).customFields.count, 0) + } + + func testJSONDeserializing() { + assertJSONDeserializing(type: Object.self, from: customFields) + assertJSONDeserializing(type: Object.self, from: [customFields, customFields, customFields]) + } + + func testJSONSerializing() { + + let objectA = Object(json: customFields) + + assertJSONSerializing(objectA) + assertJSONSerializing([objectA, objectA, objectA]) + + var objectB = Object(json: customFields) + objectB.customFields["custom_addingANewCustomField"] = "Howdy!" + + assertJSONSerializing(objectB) + assertJSONSerializing([objectB, objectB, objectB]) + } + + func testJSONTwoWaySerialization() { + + assertJSONTwoWaySerialization(customFields) + assertJSONTwoWaySerialization([customFields, customFields, customFields]) + + let objectA = Object(json: customFields) + + assertJSONTwoWaySerialization(objectA) + assertJSONTwoWaySerialization([objectA, objectA, objectA]) + + var objectB = Object(json: customFields) + objectB.customFields["custom_addingANewCustomField"] = "Howdy!" + + assertJSONTwoWaySerialization(objectB) + assertJSONTwoWaySerialization([objectB, objectB, objectB]) + } + + func testEquatable() { + + let objectA = Object(json: customFields) + var objectB = Object(json: customFields) + let objectC = Object(json: ["custom_field": "Hi"]) + + XCTAssertEqual(objectA, objectA) + XCTAssertEqual(objectA, objectB) + XCTAssertNotEqual(objectA, objectC) + + objectB.customFields["custom_addingANewCustomField"] = "Howdy!" + XCTAssertNotEqual(objectA, objectB) + objectB.customFields.removeValue(forKey: "custom_addingANewCustomField") + XCTAssertEqual(objectA, objectB) + + let key = customFieldsKeys.first! + objectB.customFields[key] = "New Value" + XCTAssertNotEqual(objectA, objectB) + objectB.customFields[key] = customFields[key] + XCTAssertEqual(objectA, objectB) + } + + func testAddingCustomFields() { + + // Valid + + var objectA = Object(json: customFields) + objectA.customFields["custom_validKey"] = 6000 + + XCTAssertEqual(objectA.customFields.count, customFields.count + 1) + XCTAssertNotNil(objectA.customFields["custom_validKey"]) + XCTAssertEqual(objectA.customFields["custom_validKey"] as! Int, 6000) + + // Invalid + + var objectB = Object(json: customFields) + objectB.customFields["This is not a valid custom_ key"] = "At least it better be!" + + XCTAssertEqual(objectB.customFields.count, customFields.count) + XCTAssertNil(objectA.customFields["This is not a valid custom_ key"]) + + // Overwrite + + var objectC = Object(json: customFields) + let key = customFieldsKeys.first! + objectC.customFields[key] = "New Value" + + XCTAssertEqual(objectC.customFields.count, customFields.count) + XCTAssertEqual(objectC.customFields[key] as! String, "New Value") + + objectC.customFields[key] = customFields[key] + XCTAssertEqual(objectC.customFields[key] as! Int, customFields[key] as! Int) + } + + func testAddingCustomFieldsWithOmittedKeys() { + + let omittedKeys: [JSONKey] = ["custom_hamsters", "custom_grubb"] + var object = Object(json: customFields, omittingKeys: omittedKeys) + + XCTAssertEqual(object.omittedKeys, omittedKeys) + XCTAssertEqual(object.customFields.count, customFields.count) + + object.customFields["custom_hamsters"] = "🐹🐹🐹" + XCTAssertNil(object.customFields["custom_hamsters"]) + XCTAssertEqual(object.customFields.count, customFields.count) + + object.customFields["custom_grubb"] = "🐛" + XCTAssertNil(object.customFields["custom_grubb"]) + XCTAssertEqual(object.customFields.count, customFields.count) + + object.customFields["custom_dolphin"] = "🐬" + XCTAssertNotNil(object.customFields["custom_dolphin"]) + XCTAssertEqual(object.customFields.count, customFields.count + 1) + } + + func testRemovingCustomFields() { + + var object = Object(json: customFields) + let key = customFieldsKeys.first! + + object.customFields.removeValue(forKey: key) + + XCTAssertNil(object.customFields[key]) + XCTAssertEqual(object.customFields.count, customFields.count - 1) + } + + func testRemovingAndAddingCustomFields() { + + var object = Object(json: customFields) + let key = customFieldsKeys.first! + let value = object.customFields[key]! + + object.customFields.removeValue(forKey: key) + + XCTAssertNil(object.customFields[key]) + XCTAssertNotEqual(object.customFields.count, customFields.count) + + object.customFields[key] = value + + XCTAssertNotNil(object.customFields[key]) + XCTAssertEqual(object.customFields.count, customFields.count) + } + + func testEmpty() { + let object = Object.empty() + XCTAssertEqual(object.customFields.count, 0) + } + +} + +// MARK: - Data + +extension CustomFieldsContainerTests: CustomFieldsDataProvider { } + +// MARK: - Assertions + +extension CustomFieldsContainerTests: AssertJSONDeserializing { } + +extension CustomFieldsContainerTests: AssertJSONSerializing { } + +extension CustomFieldsContainerTests: AssertJSONTwoWaySerialization { } diff --git a/QuizTrainTests/Misc/Containment/Containers/ErrorContainerTests.swift b/QuizTrainTests/Misc/Containment/Containers/ErrorContainerTests.swift new file mode 100644 index 0000000..304e2b9 --- /dev/null +++ b/QuizTrainTests/Misc/Containment/Containers/ErrorContainerTests.swift @@ -0,0 +1,84 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class ErrorContainerTests: XCTestCase { + + enum TestError: Error { + case errorCaseA + case errorCaseB + case errorCaseC + } + + enum TestErrorDebugDescription: String, Error, DebugDescription { + + case errorCaseA + case errorCaseB + case errorCaseC + + var debugDescription: String { + return self.rawValue + } + } + + func testSingleError() { + + let error = TestError.errorCaseB + let container = ErrorContainer(error) + + XCTAssertEqual(container.errors.count, 1) + XCTAssertTrue(container.errors.contains(error)) + } + + func testMultipleErrors() { + + let errors = [TestError.errorCaseC, TestError.errorCaseB] + + guard let container = ErrorContainer(errors) else { + XCTFail("ErrorContainer cannot be nil when initialized with 1+ errors: \(errors)") + return + } + + XCTAssertEqual(container.errors.count, errors.count) + for error in errors { + XCTAssertTrue(container.errors.contains(error)) + } + } + + func testNoErrors() { + + let errors = [TestError]() + let container = ErrorContainer(errors) + + XCTAssertNil(container) + } + + func testDebugDescription() { + + // Simple Errors + + let simpleErrors = [TestError.errorCaseC, TestError.errorCaseB] + guard let containerA = ErrorContainer(simpleErrors) else { + XCTFail("Container cannot be nil.") + return + } + + XCTAssertGreaterThan(containerA.debugDescription.count, 0) + + // Errors conforming to DebugDescription + + let debugDescriptionErrors = [TestErrorDebugDescription.errorCaseC, TestErrorDebugDescription.errorCaseB] + guard let containerB = ErrorContainer(debugDescriptionErrors) else { + XCTFail("Container cannot be nil.") + return + } + + XCTAssertGreaterThan(containerB.debugDescription.count, 0) + + for debugDescriptionError in debugDescriptionErrors { + XCTAssertNotNil(containerB.debugDescription.range(of: debugDescriptionError.rawValue)) + } + } + +} diff --git a/QuizTrainTests/Misc/Containment/Containers/JSONDictionaryContainerTests.swift b/QuizTrainTests/Misc/Containment/Containers/JSONDictionaryContainerTests.swift new file mode 100644 index 0000000..8080b8d --- /dev/null +++ b/QuizTrainTests/Misc/Containment/Containers/JSONDictionaryContainerTests.swift @@ -0,0 +1,141 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class JSONDictionaryContainerTests: XCTestCase { + + typealias Object = JSONDictionaryContainer + + func testInit() { + + XCTAssertEqual(Object(json: json).json.count, json.count) + + for (k, v) in json { + XCTAssertEqual(Object(json: [k: v]).json.count, 1) + } + } + + func testInitWithEmptyJSON() { + XCTAssertEqual(Object(json: [:]).json.count, 0) + } + + func testJSONDeserializing() { + assertJSONDeserializing(type: Object.self, from: json) + assertJSONDeserializing(type: Object.self, from: [json, json, json]) + } + + func testJSONSerializing() { + + let objectA = Object(json: json) + + assertJSONSerializing(objectA) + assertJSONSerializing([objectA, objectA, objectA]) + + var objectB = Object(json: json) + objectB.json["A new JSON Key"] = "Howdy!" + + assertJSONSerializing(objectB) + assertJSONSerializing([objectB, objectB, objectB]) + } + + func testJSONTwoWaySerialization() { + + assertJSONTwoWaySerialization(json) + assertJSONTwoWaySerialization([json, json, json]) + + let objectA = Object(json: json) + + assertJSONTwoWaySerialization(objectA) + assertJSONTwoWaySerialization([objectA, objectA, objectA]) + + var objectB = Object(json: json) + objectB.json["A new JSON Key"] = "Howdy!" + + assertJSONTwoWaySerialization(objectB) + assertJSONTwoWaySerialization([objectB, objectB, objectB]) + } + + func testEquatable() { + + let objectA = Object(json: json) + var objectB = Object(json: json) + let objectC = Object(json: ["Key": "Value!"]) + + XCTAssertEqual(objectA, objectA) + XCTAssertEqual(objectA, objectB) + XCTAssertNotEqual(objectA, objectC) + + objectB.json["A new JSON Key"] = "Howdy!" + XCTAssertNotEqual(objectA, objectB) + objectB.json.removeValue(forKey: "A new JSON Key") + XCTAssertEqual(objectA, objectB) + + let key = json.keys.first! + objectB.json[key] = "New Value" + XCTAssertNotEqual(objectA, objectB) + objectB.json[key] = json[key] + XCTAssertEqual(objectA, objectB) + } + + func testAddingJSON() { + + var object = Object(json: json) + let key: JSONKey = "A new JSON Key" + object.json[key] = 6000 + + XCTAssertEqual(object.json.count, json.count + 1) + XCTAssertNotNil(object.json[key]) + XCTAssertEqual(object.json[key] as! Int, 6000) + } + + func testRemovingJSON() { + + var object = Object(json: json) + let key: JSONKey = json.keys.first! + + object.json.removeValue(forKey: key) + + XCTAssertNil(object.json[key]) + XCTAssertEqual(object.json.count, json.count - 1) + } + + func testAddingAndRemovingJSON() { + + var object = Object(json: json) + let key = json.keys.first! + let value = object.json[key]! + + object.json.removeValue(forKey: key) + + XCTAssertNil(object.json[key]) + XCTAssertNotEqual(object.json.count, json.count) + + object.json[key] = value + + XCTAssertNotNil(object.json[key]) + XCTAssertEqual(object.json.count, json.count) + } + +} + +// MARK: - Data + +extension JSONDictionaryContainerTests { + + var json: JSONDictionary { + return ["Hello": "World!", + "Pie": 3.14, + "Array": [1, 2.0, "3", "4️⃣"], + "Dictionary": ["Three": "🐹🐹🐹", "Party like it's": 1999]] + } + +} + +// MARK: - Assertions + +extension JSONDictionaryContainerTests: AssertJSONDeserializing { } + +extension JSONDictionaryContainerTests: AssertJSONSerializing { } + +extension JSONDictionaryContainerTests: AssertJSONTwoWaySerialization { } diff --git a/QuizTrainTests/Misc/Extensions/Array+ContentComparisonTests.swift b/QuizTrainTests/Misc/Extensions/Array+ContentComparisonTests.swift new file mode 100644 index 0000000..ae38d20 --- /dev/null +++ b/QuizTrainTests/Misc/Extensions/Array+ContentComparisonTests.swift @@ -0,0 +1,42 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class Array_ContentComparisonTests: XCTestCase { + + func testContentsOfArrayAreEqualToArray() { + + let arrayA = [1, 2, 3] + + XCTAssertTrue(arrayA.contentsAreEqual(to: arrayA)) + + let arrayB = [1, 2, 3] + + XCTAssertTrue(arrayA.contentsAreEqual(to: arrayB)) + + let arrayC = [3, 2, 1] + + XCTAssertNotEqual(arrayA, arrayC) + XCTAssertTrue(arrayA.contentsAreEqual(to: arrayC)) + + let arrayD = [1, 2] + + XCTAssertFalse(arrayA.contentsAreEqual(to: arrayD)) + + let arrayE = [1, 2, 3, 4] + + XCTAssertFalse(arrayA.contentsAreEqual(to: arrayE)) + + let arrayF: [Int]? = nil + + XCTAssertFalse(arrayA.contentsAreEqual(to: arrayF)) + XCTAssertTrue(Array.contentsAreEqual(arrayF, arrayF)) + + let arrayG = [1, 1, 2] + let arrayH = [1, 2, 2] + + XCTAssertFalse(arrayG.contentsAreEqual(to: arrayH)) + } + +} diff --git a/QuizTrainTests/Misc/Extensions/Array+RandomTests.swift b/QuizTrainTests/Misc/Extensions/Array+RandomTests.swift new file mode 100644 index 0000000..efa757f --- /dev/null +++ b/QuizTrainTests/Misc/Extensions/Array+RandomTests.swift @@ -0,0 +1,40 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class Array_RandomTests: XCTestCase { + + func testRandomIndex() { + + let arrayA = [Int]() + XCTAssertNil(arrayA.randomIndex) + + let arrayB = [1] + XCTAssertEqual(arrayB.randomIndex, 0) + + let arrayC = [1, 2, 3] + let randomIndex = arrayC.randomIndex + XCTAssertNotNil(randomIndex) + if let randomIndex = randomIndex { + XCTAssertLessThan(randomIndex, arrayC.count) + } + } + + func testRandomElement() { + + let arrayA = [Int]() + XCTAssertNil(arrayA.randomElement) + + let arrayB = [1] + XCTAssertEqual(arrayB.randomElement, 1) + + let arrayC = [1, 2, 3] + let randomElement = arrayC.randomElement + XCTAssertNotNil(randomElement) + if let randomElement = randomElement { + XCTAssertTrue(arrayC.contains(randomElement)) + } + } + +} diff --git a/QuizTrainTests/Misc/Extensions/Equatable+OptionalArrayTests.swift b/QuizTrainTests/Misc/Extensions/Equatable+OptionalArrayTests.swift new file mode 100644 index 0000000..f42dea9 --- /dev/null +++ b/QuizTrainTests/Misc/Extensions/Equatable+OptionalArrayTests.swift @@ -0,0 +1,25 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class Equatable_OptionalArrayTests: XCTestCase { + + func testEquatableWithOptionalArrays() { + + let arrayA = [1, 2, 3] + let arrayB = [1, 2, 3] + let arrayC = [4, 5, 6] + let arrayD: [Int]? = nil + let arrayE: [Int]? = nil + let arrayF: [Int]? = [1, 2, 3] + + XCTAssertTrue(arrayA == arrayA) + XCTAssertTrue(arrayA == arrayB) + XCTAssertFalse(arrayA == arrayC) + XCTAssertFalse(arrayA == arrayD) + XCTAssertTrue(arrayD == arrayE) + XCTAssertTrue(arrayA == arrayF) + } + +} diff --git a/QuizTrainTests/Misc/Operations/AsyncOperationTests.swift b/QuizTrainTests/Misc/Operations/AsyncOperationTests.swift new file mode 100644 index 0000000..57b4a6e --- /dev/null +++ b/QuizTrainTests/Misc/Operations/AsyncOperationTests.swift @@ -0,0 +1,26 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class AsyncOperationTests: XCTestCase { + + func testIsAsynchronous() { + let operation = AsyncOperation() + XCTAssertTrue(operation.isAsynchronous) + } + + func testState() { + + let operation = AsyncOperation() + + XCTAssertTrue(operation.isReady) + + operation.state = .executing + XCTAssertTrue(operation.isExecuting) + + operation.state = .finished + XCTAssertTrue(operation.isFinished) + } + +} diff --git a/QuizTrainTests/Models/CaseFieldTests.swift b/QuizTrainTests/Models/CaseFieldTests.swift new file mode 100644 index 0000000..e864486 --- /dev/null +++ b/QuizTrainTests/Models/CaseFieldTests.swift @@ -0,0 +1,190 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class CaseFieldTests: XCTestCase, ModelTests { + + typealias Object = CaseField + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONDeserializing() { + _testJSONDeserializing() + } + + func testJSONDeserializingWithOptionalProperties() { + _testJSONDeserializingWithOptionalProperties() + } + + func testJSONDeserializingASingleObject() { + _testJSONDeserializingASingleObject() + } + + func testJSONDeserializingMultipleObjects() { + _testJSONDeserializingMultipleObjects() + } + + func testJSONDeserializingASingleObjectMissingRequiredProperties() { + _testJSONDeserializingASingleObjectMissingRequiredProperties() + } + + func testJSONDeserializingMultipleObjectsMissingRequiredProperties() { + _testJSONDeserializingMultipleObjectsMissingRequiredProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testJSONTwoWaySerializationForSingleItems() { + _testJSONTwoWaySerializationForSingleItems() + } + + func testJSONTwoWaySerializationForMultipleItems() { + _testJSONTwoWaySerializationForMultipleItems() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testUpdateRequestJSON() { + _testUpdateRequestJSON() + } + +} + +// MARK: - Data + +extension CaseFieldTests { + + struct Properties { + + struct Required { + static let configs = [ConfigTests.objectWithRequiredAndOptionalPropertiesFromJSON!, ConfigTests.objectWithRequiredPropertiesFromJSON!, ConfigTests.objectWithRequiredAndOptionalPropertiesFromJSON!] // This must match the order and datasources in: JSON.required["configs"] + static let displayOrder = 10 + static let id = 11 + static let includeAll = true + static let isActive = true + static let label = "Label" + static let name = "Name" + static let systemName = "System Name" + static let templateIds = [12, 13, 14, 15, 16] + static let typeId = CustomFieldType.text + } + + struct Optional { + static let description = "Description" + } + + } + +} + +extension CaseFieldTests: JSONDataProvider { + + static var requiredJSON: JSONDictionary { + return [Object.JSONKeys.configs.rawValue: [ConfigTests.requiredAndOptionalJSON, ConfigTests.requiredJSON, ConfigTests.requiredAndOptionalJSON], // This must match the order and datasources in: Properties.Required.configs + Object.JSONKeys.displayOrder.rawValue: Properties.Required.displayOrder, + Object.JSONKeys.id.rawValue: Properties.Required.id, + Object.JSONKeys.includeAll.rawValue: Properties.Required.includeAll, + Object.JSONKeys.isActive.rawValue: Properties.Required.isActive, + Object.JSONKeys.label.rawValue: Properties.Required.label, + Object.JSONKeys.name.rawValue: Properties.Required.name, + Object.JSONKeys.systemName.rawValue: Properties.Required.systemName, + Object.JSONKeys.templateIds.rawValue: Properties.Required.templateIds, + Object.JSONKeys.typeId.rawValue: Properties.Required.typeId.rawValue] + } + + static var optionalJSON: JSONDictionary { + return [Object.JSONKeys.description.rawValue: Properties.Optional.description] + } + +} + +// MARK: - Objects + +extension CaseFieldTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(configs: Properties.Required.configs, + description: nil, + displayOrder: Properties.Required.displayOrder, + id: Properties.Required.id, + includeAll: Properties.Required.includeAll, + isActive: Properties.Required.isActive, + label: Properties.Required.label, + name: Properties.Required.name, + systemName: Properties.Required.systemName, + templateIds: Properties.Required.templateIds, + typeId: Properties.Required.typeId) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(configs: Properties.Required.configs, + description: Properties.Optional.description, + displayOrder: Properties.Required.displayOrder, + id: Properties.Required.id, + includeAll: Properties.Required.includeAll, + isActive: Properties.Required.isActive, + label: Properties.Required.label, + name: Properties.Required.name, + systemName: Properties.Required.systemName, + templateIds: Properties.Required.templateIds, + typeId: Properties.Required.typeId) + } + +} + +// MARK: - Assertions + +extension CaseFieldTests: AssertEquatable { } + +extension CaseFieldTests: AssertJSONDeserializing { } + +extension CaseFieldTests: AssertJSONSerializing { } + +extension CaseFieldTests: AssertJSONTwoWaySerialization { } + +extension CaseFieldTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.configs.count, Properties.Required.configs.count) + XCTAssertEqual(object.displayOrder, Properties.Required.displayOrder) + XCTAssertEqual(object.id, Properties.Required.id) + XCTAssertEqual(object.includeAll, Properties.Required.includeAll) + XCTAssertEqual(object.isActive, Properties.Required.isActive) + XCTAssertEqual(object.label, Properties.Required.label) + XCTAssertEqual(object.name, Properties.Required.name) + XCTAssertEqual(object.systemName, Properties.Required.systemName) + XCTAssertEqual(object.templateIds, Properties.Required.templateIds) + XCTAssertEqual(object.typeId, Properties.Required.typeId) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { + if areNil { + XCTAssertNil(object.description) + } else { + XCTAssertNotNil(object.description) + XCTAssertEqual(object.description, Properties.Optional.description) + } + } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { /* none */ } + +} diff --git a/QuizTrainTests/Models/CaseTests.swift b/QuizTrainTests/Models/CaseTests.swift new file mode 100644 index 0000000..4383299 --- /dev/null +++ b/QuizTrainTests/Models/CaseTests.swift @@ -0,0 +1,278 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class CaseTests: XCTestCase, ModelTests { + + typealias Object = Case + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONDeserializing() { + _testJSONDeserializing() + } + + func testJSONDeserializingWithOptionalProperties() { + _testJSONDeserializingWithOptionalProperties() + } + + func testJSONDeserializingASingleObject() { + _testJSONDeserializingASingleObject() + } + + func testJSONDeserializingMultipleObjects() { + _testJSONDeserializingMultipleObjects() + } + + func testJSONDeserializingASingleObjectMissingRequiredProperties() { + _testJSONDeserializingASingleObjectMissingRequiredProperties() + } + + func testJSONDeserializingMultipleObjectsMissingRequiredProperties() { + _testJSONDeserializingMultipleObjectsMissingRequiredProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testJSONTwoWaySerializationForSingleItems() { + _testJSONTwoWaySerializationForSingleItems() + } + + func testJSONTwoWaySerializationForMultipleItems() { + _testJSONTwoWaySerializationForMultipleItems() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testUpdateRequestJSON() { + _testUpdateRequestJSON() + } + +} + +// MARK: - Data + +extension CaseTests { + + struct Properties { + + struct Required { + static let createdBy = 10 + static let createdOn = Date(secondsSince1970: 72973833) + static let id = 11 + static let priorityId = 12 + static let templateId = 13 + static let title = "Name" + static let typeId = 14 + static let updatedBy = 15 + static let updatedOn = Date(secondsSince1970: 72988400) + } + + struct Optional { + static let estimate = "2hr, 3min" + static let estimateForecast = "3hr" + static let milestoneId = 16 + static let refs = "1,2,3" + static let sectionId = 17 + static let suiteId = 18 + } + + } + +} + +extension CaseTests: CustomFieldsDataProvider { } + +extension CaseTests: JSONDataProvider { + + static var requiredJSON: JSONDictionary { + return [Object.JSONKeys.createdBy.rawValue: Properties.Required.createdBy, + Object.JSONKeys.createdOn.rawValue: Properties.Required.createdOn.secondsSince1970, + Object.JSONKeys.id.rawValue: Properties.Required.id, + Object.JSONKeys.priorityId.rawValue: Properties.Required.priorityId, + Object.JSONKeys.templateId.rawValue: Properties.Required.templateId, + Object.JSONKeys.title.rawValue: Properties.Required.title, + Object.JSONKeys.typeId.rawValue: Properties.Required.typeId, + Object.JSONKeys.updatedBy.rawValue: Properties.Required.updatedBy, + Object.JSONKeys.updatedOn.rawValue: Properties.Required.updatedOn.secondsSince1970] + } + + static var optionalJSON: JSONDictionary { + return [Object.JSONKeys.estimate.rawValue: Properties.Optional.estimate, + Object.JSONKeys.estimateForecast.rawValue: Properties.Optional.estimateForecast, + Object.JSONKeys.milestoneId.rawValue: Properties.Optional.milestoneId, + Object.JSONKeys.refs.rawValue: Properties.Optional.refs, + Object.JSONKeys.sectionId.rawValue: Properties.Optional.sectionId, + Object.JSONKeys.suiteId.rawValue: Properties.Optional.suiteId] + } + +} + +// MARK: - Objects + +extension CaseTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(createdBy: Properties.Required.createdBy, + createdOn: Properties.Required.createdOn, + estimate: nil, + estimateForecast: nil, + id: Properties.Required.id, + milestoneId: nil, + priorityId: Properties.Required.priorityId, + refs: nil, + sectionId: nil, + suiteId: nil, + templateId: Properties.Required.templateId, + title: Properties.Required.title, + typeId: Properties.Required.typeId, + updatedBy: Properties.Required.updatedBy, + updatedOn: Properties.Required.updatedOn, + customFieldsContainer: emptyCustomFieldsContainer) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(createdBy: Properties.Required.createdBy, + createdOn: Properties.Required.createdOn, + estimate: Properties.Optional.estimate, + estimateForecast: Properties.Optional.estimateForecast, + id: Properties.Required.id, + milestoneId: Properties.Optional.milestoneId, + priorityId: Properties.Required.priorityId, + refs: Properties.Optional.refs, + sectionId: Properties.Optional.sectionId, + suiteId: Properties.Optional.suiteId, + templateId: Properties.Required.templateId, + title: Properties.Required.title, + typeId: Properties.Required.typeId, + updatedBy: Properties.Required.updatedBy, + updatedOn: Properties.Required.updatedOn, + customFieldsContainer: customFieldsContainer) + } + +} + +// MARK: - Assertions + +extension CaseTests: AssertCustomFields { } + +extension CaseTests: AssertEquatable { } + +extension CaseTests: AssertJSONDeserializing { } + +extension CaseTests: AssertJSONSerializing { } + +extension CaseTests: AssertJSONTwoWaySerialization { } + +extension CaseTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.createdBy, Properties.Required.createdBy) + XCTAssertEqual(object.createdOn, Properties.Required.createdOn) + XCTAssertEqual(object.id, Properties.Required.id) + XCTAssertEqual(object.priorityId, Properties.Required.priorityId) + XCTAssertEqual(object.templateId, Properties.Required.templateId) + XCTAssertEqual(object.title, Properties.Required.title) + XCTAssertEqual(object.typeId, Properties.Required.typeId) + XCTAssertEqual(object.updatedBy, Properties.Required.updatedBy) + XCTAssertEqual(object.updatedOn, Properties.Required.updatedOn) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { + if areNil { + XCTAssertNil(object.estimate) + XCTAssertNil(object.estimateForecast) + XCTAssertNil(object.milestoneId) + XCTAssertNil(object.refs) + XCTAssertNil(object.sectionId) + XCTAssertNil(object.suiteId) + } else { + XCTAssertNotNil(object.estimate) + XCTAssertNotNil(object.estimateForecast) + XCTAssertNotNil(object.milestoneId) + XCTAssertNotNil(object.refs) + XCTAssertNotNil(object.sectionId) + XCTAssertNotNil(object.suiteId) + XCTAssertEqual(object.estimate, Properties.Optional.estimate) + XCTAssertEqual(object.estimateForecast, Properties.Optional.estimateForecast) + XCTAssertEqual(object.milestoneId, Properties.Optional.milestoneId) + XCTAssertEqual(object.refs, Properties.Optional.refs) + XCTAssertEqual(object.sectionId, Properties.Optional.sectionId) + XCTAssertEqual(object.suiteId, Properties.Optional.suiteId) + } + } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { + + // Properties + + object.estimate = "New Estimate" + object.milestoneId = 999 + object.priorityId = 9999 + object.refs = "9,9,9" + object.templateId = 99999 + object.title = "New Title" + object.typeId = 999999 + + XCTAssertNotEqual(object.estimate, Properties.Optional.estimate) + XCTAssertNotEqual(object.milestoneId, Properties.Optional.milestoneId) + XCTAssertNotEqual(object.priorityId, Properties.Required.priorityId) + XCTAssertNotEqual(object.refs, Properties.Optional.refs) + XCTAssertNotEqual(object.templateId, Properties.Required.templateId) + XCTAssertNotEqual(object.title, Properties.Required.title) + XCTAssertNotEqual(object.typeId, Properties.Required.typeId) + XCTAssertEqual(object.estimate, "New Estimate") + XCTAssertEqual(object.milestoneId, 999) + XCTAssertEqual(object.priorityId, 9999) + XCTAssertEqual(object.refs, "9,9,9") + XCTAssertEqual(object.templateId, 99999) + XCTAssertEqual(object.title, "New Title") + XCTAssertEqual(object.typeId, 999999) + + // Custom Fields + + let customFieldsCount = object.customFields.count + + object.customFields["custom_field_test01"] = "Custom Field Test 01" + object.customFields["custom_field_test02"] = 9000 + object.customFields["custom_field_test03"] = -8.0 + object.customFields["invalid_custom_field_test04"] = "This should not be added." + + XCTAssertNotNil(object.customFields["custom_field_test01"]) + XCTAssertNotNil(object.customFields["custom_field_test02"]) + XCTAssertNotNil(object.customFields["custom_field_test03"]) + XCTAssertNil(object.customFields["invalid_custom_field_test04"]) + XCTAssertEqual(object.customFields["custom_field_test01"] as! String, "Custom Field Test 01") + XCTAssertEqual(object.customFields["custom_field_test02"] as! Int, 9000) + XCTAssertEqual(object.customFields["custom_field_test03"] as! Double, -8.0) + + XCTAssertEqual(object.customFields.count, customFieldsCount + 3) + + object.customFields.removeValue(forKey: "custom_field_test01") + + XCTAssertNil(object.customFields["custom_field_test01"]) + XCTAssertEqual(object.customFields.count, customFieldsCount + 2) + } + +} + +extension CaseTests: AssertUpdateRequestJSON { } diff --git a/QuizTrainTests/Models/CaseTypeTests.swift b/QuizTrainTests/Models/CaseTypeTests.swift new file mode 100644 index 0000000..fe67d75 --- /dev/null +++ b/QuizTrainTests/Models/CaseTypeTests.swift @@ -0,0 +1,146 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class CaseTypeTests: XCTestCase, ModelTests { + + typealias Object = CaseType + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONDeserializing() { + _testJSONDeserializing() + } + + func testJSONDeserializingWithOptionalProperties() { + _testJSONDeserializingWithOptionalProperties() + } + + func testJSONDeserializingASingleObject() { + _testJSONDeserializingASingleObject() + } + + func testJSONDeserializingMultipleObjects() { + _testJSONDeserializingMultipleObjects() + } + + func testJSONDeserializingASingleObjectMissingRequiredProperties() { + _testJSONDeserializingASingleObjectMissingRequiredProperties() + } + + func testJSONDeserializingMultipleObjectsMissingRequiredProperties() { + _testJSONDeserializingMultipleObjectsMissingRequiredProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testJSONTwoWaySerializationForSingleItems() { + _testJSONTwoWaySerializationForSingleItems() + } + + func testJSONTwoWaySerializationForMultipleItems() { + _testJSONTwoWaySerializationForMultipleItems() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testUpdateRequestJSON() { + _testUpdateRequestJSON() + } + +} + +// MARK: - Data + +extension CaseTypeTests { + + struct Properties { + + struct Required { + static let id = 10 + static let isDefault = true + static let name = "Name" + } + + struct Optional { + // none + } + + } + +} + +extension CaseTypeTests: JSONDataProvider { + + static var requiredJSON: JSONDictionary { + return [Object.JSONKeys.id.rawValue: Properties.Required.id, + Object.JSONKeys.isDefault.rawValue: Properties.Required.isDefault, + Object.JSONKeys.name.rawValue: Properties.Required.name] + } + + static var optionalJSON: JSONDictionary { + return [:] // none + } + +} + +// MARK: - Objects + +extension CaseTypeTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(id: Properties.Required.id, + isDefault: Properties.Required.isDefault, + name: Properties.Required.name) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(id: Properties.Required.id, + isDefault: Properties.Required.isDefault, + name: Properties.Required.name) + } + +} + +// MARK: - Assertions + +extension CaseTypeTests: AssertEquatable { } + +extension CaseTypeTests: AssertJSONDeserializing { } + +extension CaseTypeTests: AssertJSONSerializing { } + +extension CaseTypeTests: AssertJSONTwoWaySerialization { } + +extension CaseTypeTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.id, Properties.Required.id) + XCTAssertEqual(object.isDefault, Properties.Required.isDefault) + XCTAssertEqual(object.name, Properties.Required.name) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { /* none */ } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { /* none */ } + +} diff --git a/QuizTrainTests/Models/ConfigurationGroupTests.swift b/QuizTrainTests/Models/ConfigurationGroupTests.swift new file mode 100644 index 0000000..7ea7b8b --- /dev/null +++ b/QuizTrainTests/Models/ConfigurationGroupTests.swift @@ -0,0 +1,157 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class ConfigurationGroupTests: XCTestCase, ModelTests { + + typealias Object = ConfigurationGroup + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONDeserializing() { + _testJSONDeserializing() + } + + func testJSONDeserializingWithOptionalProperties() { + _testJSONDeserializingWithOptionalProperties() + } + + func testJSONDeserializingASingleObject() { + _testJSONDeserializingASingleObject() + } + + func testJSONDeserializingMultipleObjects() { + _testJSONDeserializingMultipleObjects() + } + + func testJSONDeserializingASingleObjectMissingRequiredProperties() { + _testJSONDeserializingASingleObjectMissingRequiredProperties() + } + + func testJSONDeserializingMultipleObjectsMissingRequiredProperties() { + _testJSONDeserializingMultipleObjectsMissingRequiredProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testJSONTwoWaySerializationForSingleItems() { + _testJSONTwoWaySerializationForSingleItems() + } + + func testJSONTwoWaySerializationForMultipleItems() { + _testJSONTwoWaySerializationForMultipleItems() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testUpdateRequestJSON() { + _testUpdateRequestJSON() + } + +} + +// MARK: - Data + +extension ConfigurationGroupTests { + + struct Properties { + + struct Required { + static let configs = [ConfigurationTests.objectWithRequiredAndOptionalPropertiesFromJSON!, ConfigurationTests.objectWithRequiredPropertiesFromJSON!, ConfigurationTests.objectWithRequiredAndOptionalPropertiesFromJSON!] // This must match the order and datasources in: JSON.required["configs"] + static let id = 10 + static let name = "Name" + static let projectId = 11 + } + + struct Optional { + // none + } + + } + +} + +extension ConfigurationGroupTests: JSONDataProvider { + + static var requiredJSON: JSONDictionary { + return [Object.JSONKeys.configs.rawValue: [ConfigurationTests.requiredAndOptionalJSON, ConfigurationTests.requiredJSON, ConfigurationTests.requiredAndOptionalJSON], // This must match the order and datasources in: Properties.Required.configs + Object.JSONKeys.id.rawValue: Properties.Required.id, + Object.JSONKeys.name.rawValue: Properties.Required.name, + Object.JSONKeys.projectId.rawValue: Properties.Required.projectId] + } + + static var optionalJSON: JSONDictionary { + return [:] // none + } + +} + +// MARK: - Objects + +extension ConfigurationGroupTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(configs: Properties.Required.configs, + id: Properties.Required.id, + name: Properties.Required.name, + projectId: Properties.Required.projectId) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(configs: Properties.Required.configs, + id: Properties.Required.id, + name: Properties.Required.name, + projectId: Properties.Required.projectId) + } + +} + +// MARK: - Assertions + +extension ConfigurationGroupTests: AssertEquatable { } + +extension ConfigurationGroupTests: AssertJSONDeserializing { } + +extension ConfigurationGroupTests: AssertJSONSerializing { } + +extension ConfigurationGroupTests: AssertJSONTwoWaySerialization { } + +extension ConfigurationGroupTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.configs.count, Properties.Required.configs.count) + XCTAssertEqual(object.id, Properties.Required.id) + XCTAssertEqual(object.name, Properties.Required.name) + XCTAssertEqual(object.projectId, Properties.Required.projectId) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { /* none */ } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { + object.name = "New Name" + XCTAssertNotEqual(object.name, Properties.Required.name) + XCTAssertEqual(object.name, "New Name") + } + +} + +extension ConfigurationGroupTests: AssertUpdateRequestJSON { } diff --git a/QuizTrainTests/Models/ConfigurationTests.swift b/QuizTrainTests/Models/ConfigurationTests.swift new file mode 100644 index 0000000..19aa987 --- /dev/null +++ b/QuizTrainTests/Models/ConfigurationTests.swift @@ -0,0 +1,152 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class ConfigurationTests: XCTestCase, ModelTests { + + typealias Object = Configuration + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONDeserializing() { + _testJSONDeserializing() + } + + func testJSONDeserializingWithOptionalProperties() { + _testJSONDeserializingWithOptionalProperties() + } + + func testJSONDeserializingASingleObject() { + _testJSONDeserializingASingleObject() + } + + func testJSONDeserializingMultipleObjects() { + _testJSONDeserializingMultipleObjects() + } + + func testJSONDeserializingASingleObjectMissingRequiredProperties() { + _testJSONDeserializingASingleObjectMissingRequiredProperties() + } + + func testJSONDeserializingMultipleObjectsMissingRequiredProperties() { + _testJSONDeserializingMultipleObjectsMissingRequiredProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testJSONTwoWaySerializationForSingleItems() { + _testJSONTwoWaySerializationForSingleItems() + } + + func testJSONTwoWaySerializationForMultipleItems() { + _testJSONTwoWaySerializationForMultipleItems() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testUpdateRequestJSON() { + _testUpdateRequestJSON() + } + +} + +// MARK: - Data + +extension ConfigurationTests { + + struct Properties { + + struct Required { + static let id = 10 + static let groupId = 11 + static let name = "Name" + } + + struct Optional { + // none + } + + } + +} + +extension ConfigurationTests: JSONDataProvider { + + static var requiredJSON: JSONDictionary { + return [Object.JSONKeys.id.rawValue: Properties.Required.id, + Object.JSONKeys.groupId.rawValue: Properties.Required.groupId, + Object.JSONKeys.name.rawValue: Properties.Required.name] + } + + static var optionalJSON: JSONDictionary { + return [:] // none + } + +} + +// MARK: - Objects + +extension ConfigurationTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(id: Properties.Required.id, + groupId: Properties.Required.groupId, + name: Properties.Required.name) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(id: Properties.Required.id, + groupId: Properties.Required.groupId, + name: Properties.Required.name) + } + +} + +// MARK: - Assertions + +extension ConfigurationTests: AssertEquatable { } + +extension ConfigurationTests: AssertJSONDeserializing { } + +extension ConfigurationTests: AssertJSONSerializing { } + +extension ConfigurationTests: AssertJSONTwoWaySerialization { } + +extension ConfigurationTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.id, Properties.Required.id) + XCTAssertEqual(object.groupId, Properties.Required.groupId) + XCTAssertEqual(object.name, Properties.Required.name) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { /* none */ } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { + object.name = "New Name" + XCTAssertNotEqual(object.name, Properties.Required.name) + XCTAssertEqual(object.name, "New Name") + } + +} + +extension ConfigurationTests: AssertUpdateRequestJSON { } diff --git a/QuizTrainTests/Models/Custom Fields/Config.ContextTests.swift b/QuizTrainTests/Models/Custom Fields/Config.ContextTests.swift new file mode 100644 index 0000000..ef97da8 --- /dev/null +++ b/QuizTrainTests/Models/Custom Fields/Config.ContextTests.swift @@ -0,0 +1,145 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class Config_ContextTests: XCTestCase, ModelTests { + + typealias Object = Config.Context + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONDeserializing() { + _testJSONDeserializing() + } + + func testJSONDeserializingWithOptionalProperties() { + _testJSONDeserializingWithOptionalProperties() + } + + func testJSONDeserializingASingleObject() { + _testJSONDeserializingASingleObject() + } + + func testJSONDeserializingMultipleObjects() { + _testJSONDeserializingMultipleObjects() + } + + func testJSONDeserializingASingleObjectMissingRequiredProperties() { + _testJSONDeserializingASingleObjectMissingRequiredProperties() + } + + func testJSONDeserializingMultipleObjectsMissingRequiredProperties() { + _testJSONDeserializingMultipleObjectsMissingRequiredProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testJSONTwoWaySerializationForSingleItems() { + _testJSONTwoWaySerializationForSingleItems() + } + + func testJSONTwoWaySerializationForMultipleItems() { + _testJSONTwoWaySerializationForMultipleItems() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testUpdateRequestJSON() { + _testUpdateRequestJSON() + } + +} + +// MARK: - Data + +extension Config_ContextTests { + + struct Properties { + + struct Required { + static let isGlobal = true + } + + struct Optional { + static let projectIds = [1, 2, 3, 4, 5] + } + + } + +} + +extension Config_ContextTests: JSONDataProvider { + + static var requiredJSON: JSONDictionary { + return [Object.JSONKeys.isGlobal.rawValue: Properties.Required.isGlobal] + } + + static var optionalJSON: JSONDictionary { + return [Object.JSONKeys.projectIds.rawValue: Properties.Optional.projectIds] + } + +} + +// MARK: - Objects + +extension Config_ContextTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(isGlobal: Properties.Required.isGlobal, + projectIds: nil) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(isGlobal: Properties.Required.isGlobal, + projectIds: Properties.Optional.projectIds) + } + +} + +// MARK: - Assertions + +extension Config_ContextTests: AssertEquatable { } + +extension Config_ContextTests: AssertJSONDeserializing { } + +extension Config_ContextTests: AssertJSONSerializing { } + +extension Config_ContextTests: AssertJSONTwoWaySerialization { } + +extension Config_ContextTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.isGlobal, Properties.Required.isGlobal) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { + if areNil { + XCTAssertNil(object.projectIds) + } else { + XCTAssertNotNil(object.projectIds) + XCTAssertEqual(object.projectIds!, Properties.Optional.projectIds) + } + } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { /* none */ } + +} diff --git a/QuizTrainTests/Models/Custom Fields/ConfigTests.swift b/QuizTrainTests/Models/Custom Fields/ConfigTests.swift new file mode 100644 index 0000000..b158988 --- /dev/null +++ b/QuizTrainTests/Models/Custom Fields/ConfigTests.swift @@ -0,0 +1,182 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class ConfigTests: XCTestCase, ModelTests { + + typealias Object = Config + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONDeserializing() { + _testJSONDeserializing() + } + + func testJSONDeserializingWithOptionalProperties() { + _testJSONDeserializingWithOptionalProperties() + } + + func testJSONDeserializingASingleObject() { + _testJSONDeserializingASingleObject() + } + + func testJSONDeserializingMultipleObjects() { + _testJSONDeserializingMultipleObjects() + } + + func testJSONDeserializingASingleObjectMissingRequiredProperties() { + _testJSONDeserializingASingleObjectMissingRequiredProperties() + } + + func testJSONDeserializingMultipleObjectsMissingRequiredProperties() { + _testJSONDeserializingMultipleObjectsMissingRequiredProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testJSONTwoWaySerializationForSingleItems() { + _testJSONTwoWaySerializationForSingleItems() + } + + func testJSONTwoWaySerializationForMultipleItems() { + _testJSONTwoWaySerializationForMultipleItems() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testUpdateRequestJSON() { + _testUpdateRequestJSON() + } + + func testProjects() { + + let contextA = Object.Context(isGlobal: true, projectIds: nil) + let objectA = Object(context: contextA, id: "id", optionsContainer: Config.OptionsContainer(json: ["test": "test"])) + assertProjects(in: objectA) + + let contextB = Object.Context(isGlobal: false, projectIds: nil) + let objectB = Object(context: contextB, id: "id", optionsContainer: Config.OptionsContainer(json: ["test": "test"])) + assertProjects(in: objectB) + + let contextC = Object.Context(isGlobal: false, projectIds: [1, 2, 3]) + let objectC = Object(context: contextC, id: "id", optionsContainer: Config.OptionsContainer(json: ["test": "test"])) + assertProjects(in: objectC) + } + +} + +// MARK: - Data + +extension ConfigTests { + + struct Properties { + + struct Required { + static let context = Config_ContextTests.objectWithRequiredAndOptionalPropertiesFromJSON! // This must match the order and datasources in: JSON.required["context"] + static let id = "id" + static let options: [String: Any] = ["optionA": true, "optionB": "Hello", "optionC": [1, 2.0, -3]] + } + + struct Optional { + // none + } + + } + +} + +extension ConfigTests: JSONDataProvider { + + static var requiredJSON: JSONDictionary { + return [Object.JSONKeys.context.rawValue: Config_ContextTests.requiredAndOptionalJSON, // This must match the order and datasources in: Properties.Required.context + Object.JSONKeys.id.rawValue: Properties.Required.id, + Object.JSONKeys.options.rawValue: Properties.Required.options] + } + + static var optionalJSON: JSONDictionary { + return [:] // none + } + +} + +// MARK: - Objects + +extension ConfigTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(context: Properties.Required.context, + id: Properties.Required.id, + optionsContainer: Config.OptionsContainer(json: Properties.Required.options)) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(context: Properties.Required.context, + id: Properties.Required.id, + optionsContainer: Config.OptionsContainer(json: Properties.Required.options)) + } + +} + +// MARK: - Assertions + +extension ConfigTests { + + func assertProjects(in object: Object) { + if object.context.isGlobal { + XCTAssertEqual(object.projects, UniqueSelection.all) + } else if object.context.projectIds == nil { + XCTAssertEqual(object.projects, UniqueSelection.none) + } else { + XCTAssertNotNil(object.context.projectIds) + if let projectIds = object.context.projectIds { + XCTAssertEqual(object.projects, UniqueSelection.some(Set(projectIds))) + } + } + } + +} + +extension ConfigTests: AssertEquatable { } + +extension ConfigTests: AssertJSONDeserializing { } + +extension ConfigTests: AssertJSONSerializing { } + +extension ConfigTests: AssertJSONTwoWaySerialization { } + +extension ConfigTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertNotNil(object.context) + XCTAssertEqual(object.context.isGlobal, Properties.Required.context.isGlobal) + XCTAssertNotNil(object.context.projectIds) + XCTAssertNotNil(Properties.Required.context.projectIds) + XCTAssertEqual(object.context.projectIds!, Properties.Required.context.projectIds!) + XCTAssertEqual(object.id, Properties.Required.id) + XCTAssertEqual(object.options.count, Properties.Required.options.count) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { /* none */ } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { /* none */ } + +} diff --git a/QuizTrainTests/Models/MilestoneTests.swift b/QuizTrainTests/Models/MilestoneTests.swift new file mode 100644 index 0000000..48d12d1 --- /dev/null +++ b/QuizTrainTests/Models/MilestoneTests.swift @@ -0,0 +1,239 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class MilestoneTests: XCTestCase, ModelTests { + + typealias Object = Milestone + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONDeserializing() { + _testJSONDeserializing() + } + + func testJSONDeserializingWithOptionalProperties() { + _testJSONDeserializingWithOptionalProperties() + } + + func testJSONDeserializingASingleObject() { + _testJSONDeserializingASingleObject() + } + + func testJSONDeserializingMultipleObjects() { + _testJSONDeserializingMultipleObjects() + } + + func testJSONDeserializingASingleObjectMissingRequiredProperties() { + _testJSONDeserializingASingleObjectMissingRequiredProperties() + } + + func testJSONDeserializingMultipleObjectsMissingRequiredProperties() { + _testJSONDeserializingMultipleObjectsMissingRequiredProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testJSONTwoWaySerializationForSingleItems() { + _testJSONTwoWaySerializationForSingleItems() + } + + func testJSONTwoWaySerializationForMultipleItems() { + _testJSONTwoWaySerializationForMultipleItems() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testUpdateRequestJSON() { + _testUpdateRequestJSON() + } + +} + +// MARK: - Data + +extension MilestoneTests { + + struct Properties { + + struct Required { + static let id = 10 + static let isCompleted = true + static let isStarted = true + static let name = "Name" + static let projectId = 11 + static let url = URL(string: "https://www.testrail.com/")! + } + + struct Optional { + static let completedOn = Date(secondsSince1970: 72988302) + static let description = "Description" + static let dueOn = Date(secondsSince1970: 72988400) + static let milestones = [MilestoneTests.objectWithRequiredPropertiesFromJSON!, MilestoneTests.objectWithRequiredPropertiesFromJSON!, MilestoneTests.objectWithRequiredPropertiesFromJSON!] // This must match the order and datasources in: JSON.optionals["milestones"] + static let parentId = 12 + static let startOn = Date(secondsSince1970: 72977000) + static let startedOn = Date(secondsSince1970: 72977321) + } + + } + +} + +extension MilestoneTests: JSONDataProvider { + + static var requiredJSON: JSONDictionary { + return [Object.JSONKeys.id.rawValue: Properties.Required.id, + Object.JSONKeys.isCompleted.rawValue: Properties.Required.isCompleted, + Object.JSONKeys.isStarted.rawValue: Properties.Required.isStarted, + Object.JSONKeys.name.rawValue: Properties.Required.name, + Object.JSONKeys.projectId.rawValue: Properties.Required.projectId, + Object.JSONKeys.url.rawValue: Properties.Required.url.absoluteString] + } + + static var optionalJSON: JSONDictionary { + return [Object.JSONKeys.completedOn.rawValue: Properties.Optional.completedOn.secondsSince1970, + Object.JSONKeys.description.rawValue: Properties.Optional.description, + Object.JSONKeys.dueOn.rawValue: Properties.Optional.dueOn.secondsSince1970, + Object.JSONKeys.milestones.rawValue: [MilestoneTests.requiredJSON, MilestoneTests.requiredJSON, MilestoneTests.requiredJSON], // This must match the order and datasources in: Properties.Optional.milestones + Object.JSONKeys.parentId.rawValue: Properties.Optional.parentId, + Object.JSONKeys.startOn.rawValue: Properties.Optional.startOn.secondsSince1970, + Object.JSONKeys.startedOn.rawValue: Properties.Optional.startedOn.secondsSince1970] + } + +} + +// MARK: - Objects + +extension MilestoneTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(completedOn: nil, + description: nil, + dueOn: nil, + id: Properties.Required.id, + isCompleted: Properties.Required.isCompleted, + isStarted: Properties.Required.isStarted, + milestones: nil, + name: Properties.Required.name, + parentId: nil, + projectId: Properties.Required.projectId, + startOn: nil, + startedOn: nil, + url: Properties.Required.url) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(completedOn: Properties.Optional.completedOn, + description: Properties.Optional.description, + dueOn: Properties.Optional.dueOn, + id: Properties.Required.id, + isCompleted: Properties.Required.isCompleted, + isStarted: Properties.Required.isStarted, + milestones: Properties.Optional.milestones, + name: Properties.Required.name, + parentId: Properties.Optional.parentId, + projectId: Properties.Required.projectId, + startOn: Properties.Optional.startOn, + startedOn: Properties.Optional.startedOn, + url: Properties.Required.url) + } + +} + +// MARK: - Assertions + +extension MilestoneTests: AssertEquatable { } + +extension MilestoneTests: AssertJSONDeserializing { } + +extension MilestoneTests: AssertJSONSerializing { } + +extension MilestoneTests: AssertJSONTwoWaySerialization { } + +extension MilestoneTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.id, Properties.Required.id) + XCTAssertEqual(object.isCompleted, Properties.Required.isCompleted) + XCTAssertEqual(object.isStarted, Properties.Required.isStarted) + XCTAssertEqual(object.name, Properties.Required.name) + XCTAssertEqual(object.projectId, Properties.Required.projectId) + XCTAssertEqual(object.url, Properties.Required.url) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { + if areNil { + XCTAssertNil(object.completedOn) + XCTAssertNil(object.description) + XCTAssertNil(object.dueOn) + XCTAssertNil(object.milestones) + XCTAssertNil(object.parentId) + XCTAssertNil(object.startOn) + XCTAssertNil(object.startedOn) + } else { + XCTAssertNotNil(object.completedOn) + XCTAssertNotNil(object.description) + XCTAssertNotNil(object.dueOn) + XCTAssertNotNil(object.milestones) + XCTAssertNotNil(object.parentId) + XCTAssertNotNil(object.startOn) + XCTAssertNotNil(object.startedOn) + XCTAssertEqual(object.completedOn, Properties.Optional.completedOn) + XCTAssertEqual(object.description, Properties.Optional.description) + XCTAssertEqual(object.dueOn, Properties.Optional.dueOn) + XCTAssertEqual(object.milestones!, Properties.Optional.milestones) + XCTAssertEqual(object.parentId, Properties.Optional.parentId) + XCTAssertEqual(object.startOn, Properties.Optional.startOn) + XCTAssertEqual(object.startedOn, Properties.Optional.startedOn) + } + } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { + + object.description = "New Description" + object.dueOn = Date(secondsSince1970: 90000000) + object.isCompleted = false + object.isStarted = false + object.name = "New Name" + object.parentId = 9999 + object.startOn = Date(secondsSince1970: 80000000) + + XCTAssertNotEqual(object.description, Properties.Optional.description) + XCTAssertNotEqual(object.dueOn, Properties.Optional.dueOn) + XCTAssertNotEqual(object.isCompleted, Properties.Required.isCompleted) + XCTAssertNotEqual(object.isStarted, Properties.Required.isStarted) + XCTAssertNotEqual(object.name, Properties.Required.name) + XCTAssertNotEqual(object.parentId, Properties.Optional.parentId) + XCTAssertNotEqual(object.startOn, Properties.Optional.startOn) + + XCTAssertEqual(object.description, "New Description") + XCTAssertEqual(object.dueOn, Date(secondsSince1970: 90000000)) + XCTAssertEqual(object.isCompleted, false) + XCTAssertEqual(object.isStarted, false) + XCTAssertEqual(object.name, "New Name") + XCTAssertEqual(object.parentId, 9999) + XCTAssertEqual(object.startOn, Date(secondsSince1970: 80000000)) + } + +} + +extension MilestoneTests: AssertUpdateRequestJSON { } diff --git a/QuizTrainTests/Models/Plan.EntryTests.swift b/QuizTrainTests/Models/Plan.EntryTests.swift new file mode 100644 index 0000000..6ae824d --- /dev/null +++ b/QuizTrainTests/Models/Plan.EntryTests.swift @@ -0,0 +1,157 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class Plan_EntryTests: XCTestCase, ModelTests { + + typealias Object = Plan.Entry + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONDeserializing() { + _testJSONDeserializing() + } + + func testJSONDeserializingWithOptionalProperties() { + _testJSONDeserializingWithOptionalProperties() + } + + func testJSONDeserializingASingleObject() { + _testJSONDeserializingASingleObject() + } + + func testJSONDeserializingMultipleObjects() { + _testJSONDeserializingMultipleObjects() + } + + func testJSONDeserializingASingleObjectMissingRequiredProperties() { + _testJSONDeserializingASingleObjectMissingRequiredProperties() + } + + func testJSONDeserializingMultipleObjectsMissingRequiredProperties() { + _testJSONDeserializingMultipleObjectsMissingRequiredProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testJSONTwoWaySerializationForSingleItems() { + _testJSONTwoWaySerializationForSingleItems() + } + + func testJSONTwoWaySerializationForMultipleItems() { + _testJSONTwoWaySerializationForMultipleItems() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testUpdateRequestJSON() { + _testUpdateRequestJSON() + } + +} + +// MARK: - Data + +extension Plan_EntryTests { + + struct Properties { + + struct Required { + static let id = "Id" + static let name = "Name" + static let runs = [RunTests.objectWithRequiredAndOptionalPropertiesFromJSON!, RunTests.objectWithRequiredPropertiesFromJSON!, RunTests.objectWithRequiredAndOptionalPropertiesFromJSON!] // This must match the order and datasources in: JSON.required["runs"] + static let suiteId = 10 + } + + struct Optional { + // none + } + + } + +} + +extension Plan_EntryTests: JSONDataProvider { + + static var requiredJSON: JSONDictionary { + return [Object.JSONKeys.id.rawValue: Properties.Required.id, + Object.JSONKeys.name.rawValue: Properties.Required.name, + Object.JSONKeys.runs.rawValue: [RunTests.requiredAndOptionalJSON, RunTests.requiredJSON, RunTests.requiredAndOptionalJSON], // This must match the order and datasources in: Properties.Required.runs + Object.JSONKeys.suiteId.rawValue: Properties.Required.suiteId] + } + + static var optionalJSON: JSONDictionary { + return [:] // none + } + +} + +// MARK: - Objects + +extension Plan_EntryTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(id: Properties.Required.id, + name: Properties.Required.name, + runs: Properties.Required.runs, + suiteId: Properties.Required.suiteId) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(id: Properties.Required.id, + name: Properties.Required.name, + runs: Properties.Required.runs, + suiteId: Properties.Required.suiteId) + } + +} + +// MARK: - Assertions + +extension Plan_EntryTests: AssertEquatable { } + +extension Plan_EntryTests: AssertJSONDeserializing { } + +extension Plan_EntryTests: AssertJSONSerializing { } + +extension Plan_EntryTests: AssertJSONTwoWaySerialization { } + +extension Plan_EntryTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.id, Properties.Required.id) + XCTAssertEqual(object.name, Properties.Required.name) + XCTAssertEqual(object.runs.count, Properties.Required.runs.count) + XCTAssertEqual(object.suiteId, Properties.Required.suiteId) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { /* none */ } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { + object.name = "New Name" + XCTAssertNotEqual(object.name, Properties.Required.name) + XCTAssertEqual(object.name, "New Name") + } + +} + +extension Plan_EntryTests: AssertUpdateRequestJSON { } diff --git a/QuizTrainTests/Models/PlanTests.swift b/QuizTrainTests/Models/PlanTests.swift new file mode 100644 index 0000000..e111eb8 --- /dev/null +++ b/QuizTrainTests/Models/PlanTests.swift @@ -0,0 +1,278 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class PlanTests: XCTestCase, ModelTests { + + typealias Object = Plan + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONDeserializing() { + _testJSONDeserializing() + } + + func testJSONDeserializingWithOptionalProperties() { + _testJSONDeserializingWithOptionalProperties() + } + + func testJSONDeserializingASingleObject() { + _testJSONDeserializingASingleObject() + } + + func testJSONDeserializingMultipleObjects() { + _testJSONDeserializingMultipleObjects() + } + + func testJSONDeserializingASingleObjectMissingRequiredProperties() { + _testJSONDeserializingASingleObjectMissingRequiredProperties() + } + + func testJSONDeserializingMultipleObjectsMissingRequiredProperties() { + _testJSONDeserializingMultipleObjectsMissingRequiredProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testJSONTwoWaySerializationForSingleItems() { + _testJSONTwoWaySerializationForSingleItems() + } + + func testJSONTwoWaySerializationForMultipleItems() { + _testJSONTwoWaySerializationForMultipleItems() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testUpdateRequestJSON() { + _testUpdateRequestJSON() + } + +} + +// MARK: - Data + +extension PlanTests { + + struct Properties { + + struct Required { + static let blockedCount = 9 + static let createdBy = 10 + static let createdOn = Date(secondsSince1970: 72973833) + static let customStatus1Count = 11 + static let customStatus2Count = 12 + static let customStatus3Count = 13 + static let customStatus4Count = 14 + static let customStatus5Count = 15 + static let customStatus6Count = 16 + static let customStatus7Count = 17 + static let failedCount = 18 + static let id = 19 + static let isCompleted = true + static let name = "Name" + static let passedCount = 20 + static let projectId = 21 + static let retestCount = 22 + static let untestedCount = 23 + static let url = URL(string: "https://www.testrail.com/")! + } + + struct Optional { + static let assignedtoId = 24 + static let completedOn = Date(secondsSince1970: 72988302) + static let description = "Description" + static let entries = [Plan_EntryTests.objectWithRequiredAndOptionalPropertiesFromJSON!, Plan_EntryTests.objectWithRequiredPropertiesFromJSON!, Plan_EntryTests.objectWithRequiredAndOptionalPropertiesFromJSON!] // This must match the order and datasources in: JSON.optionals["entries"] + static let milestoneId = 25 + } + + } + +} + +extension PlanTests: JSONDataProvider { + + static var requiredJSON: JSONDictionary { + return [Object.JSONKeys.blockedCount.rawValue: Properties.Required.blockedCount, + Object.JSONKeys.createdBy.rawValue: Properties.Required.createdBy, + Object.JSONKeys.createdOn.rawValue: Properties.Required.createdOn.secondsSince1970, + Object.JSONKeys.customStatus1Count.rawValue: Properties.Required.customStatus1Count, + Object.JSONKeys.customStatus2Count.rawValue: Properties.Required.customStatus2Count, + Object.JSONKeys.customStatus3Count.rawValue: Properties.Required.customStatus3Count, + Object.JSONKeys.customStatus4Count.rawValue: Properties.Required.customStatus4Count, + Object.JSONKeys.customStatus5Count.rawValue: Properties.Required.customStatus5Count, + Object.JSONKeys.customStatus6Count.rawValue: Properties.Required.customStatus6Count, + Object.JSONKeys.customStatus7Count.rawValue: Properties.Required.customStatus7Count, + Object.JSONKeys.failedCount.rawValue: Properties.Required.failedCount, + Object.JSONKeys.id.rawValue: Properties.Required.id, + Object.JSONKeys.isCompleted.rawValue: Properties.Required.isCompleted, + Object.JSONKeys.name.rawValue: Properties.Required.name, + Object.JSONKeys.passedCount.rawValue: Properties.Required.passedCount, + Object.JSONKeys.projectId.rawValue: Properties.Required.projectId, + Object.JSONKeys.retestCount.rawValue: Properties.Required.retestCount, + Object.JSONKeys.untestedCount.rawValue: Properties.Required.untestedCount, + Object.JSONKeys.url.rawValue: Properties.Required.url.absoluteString] + } + + static var optionalJSON: JSONDictionary { + return [Object.JSONKeys.assignedtoId.rawValue: Properties.Optional.assignedtoId, + Object.JSONKeys.completedOn.rawValue: Properties.Optional.completedOn.secondsSince1970, + Object.JSONKeys.description.rawValue: Properties.Optional.description, + Object.JSONKeys.entries.rawValue: [Plan_EntryTests.requiredAndOptionalJSON, Plan_EntryTests.requiredJSON, Plan_EntryTests.requiredAndOptionalJSON], // This must match the order and datasources in: Properties.Optional.entries + Object.JSONKeys.milestoneId.rawValue: Properties.Optional.milestoneId] + } + +} + +// MARK: - Objects + +extension PlanTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(assignedtoId: nil, + blockedCount: Properties.Required.blockedCount, + completedOn: nil, + createdBy: Properties.Required.createdBy, + createdOn: Properties.Required.createdOn, + customStatus1Count: Properties.Required.customStatus1Count, + customStatus2Count: Properties.Required.customStatus2Count, + customStatus3Count: Properties.Required.customStatus3Count, + customStatus4Count: Properties.Required.customStatus4Count, + customStatus5Count: Properties.Required.customStatus5Count, + customStatus6Count: Properties.Required.customStatus6Count, + customStatus7Count: Properties.Required.customStatus7Count, + description: nil, + entries: nil, + failedCount: Properties.Required.failedCount, + id: Properties.Required.id, + isCompleted: Properties.Required.isCompleted, + milestoneId: nil, + name: Properties.Required.name, + passedCount: Properties.Required.passedCount, + projectId: Properties.Required.projectId, + retestCount: Properties.Required.retestCount, + untestedCount: Properties.Required.untestedCount, + url: Properties.Required.url) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(assignedtoId: Properties.Optional.assignedtoId, + blockedCount: Properties.Required.blockedCount, + completedOn: Properties.Optional.completedOn, + createdBy: Properties.Required.createdBy, + createdOn: Properties.Required.createdOn, + customStatus1Count: Properties.Required.customStatus1Count, + customStatus2Count: Properties.Required.customStatus2Count, + customStatus3Count: Properties.Required.customStatus3Count, + customStatus4Count: Properties.Required.customStatus4Count, + customStatus5Count: Properties.Required.customStatus5Count, + customStatus6Count: Properties.Required.customStatus6Count, + customStatus7Count: Properties.Required.customStatus7Count, + description: Properties.Optional.description, + entries: Properties.Optional.entries, + failedCount: Properties.Required.failedCount, + id: Properties.Required.id, + isCompleted: Properties.Required.isCompleted, + milestoneId: Properties.Optional.milestoneId, + name: Properties.Required.name, + passedCount: Properties.Required.passedCount, + projectId: Properties.Required.projectId, + retestCount: Properties.Required.retestCount, + untestedCount: Properties.Required.untestedCount, + url: Properties.Required.url) + } + +} + +// MARK: - Assertions + +extension PlanTests: AssertEquatable { } + +extension PlanTests: AssertJSONDeserializing { } + +extension PlanTests: AssertJSONSerializing { } + +extension PlanTests: AssertJSONTwoWaySerialization { } + +extension PlanTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.blockedCount, Properties.Required.blockedCount) + XCTAssertEqual(object.createdBy, Properties.Required.createdBy) + XCTAssertEqual(object.createdOn, Properties.Required.createdOn) + XCTAssertEqual(object.customStatus1Count, Properties.Required.customStatus1Count) + XCTAssertEqual(object.customStatus2Count, Properties.Required.customStatus2Count) + XCTAssertEqual(object.customStatus3Count, Properties.Required.customStatus3Count) + XCTAssertEqual(object.customStatus4Count, Properties.Required.customStatus4Count) + XCTAssertEqual(object.customStatus5Count, Properties.Required.customStatus5Count) + XCTAssertEqual(object.customStatus6Count, Properties.Required.customStatus6Count) + XCTAssertEqual(object.customStatus7Count, Properties.Required.customStatus7Count) + XCTAssertEqual(object.failedCount, Properties.Required.failedCount) + XCTAssertEqual(object.id, Properties.Required.id) + XCTAssertEqual(object.isCompleted, Properties.Required.isCompleted) + XCTAssertEqual(object.name, Properties.Required.name) + XCTAssertEqual(object.passedCount, Properties.Required.passedCount) + XCTAssertEqual(object.projectId, Properties.Required.projectId) + XCTAssertEqual(object.retestCount, Properties.Required.retestCount) + XCTAssertEqual(object.untestedCount, Properties.Required.untestedCount) + XCTAssertEqual(object.url, Properties.Required.url) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { + if areNil { + XCTAssertNil(object.assignedtoId) + XCTAssertNil(object.completedOn) + XCTAssertNil(object.description) + XCTAssertNil(object.entries) + XCTAssertNil(object.milestoneId) + } else { + XCTAssertNotNil(object.assignedtoId) + XCTAssertNotNil(object.completedOn) + XCTAssertNotNil(object.description) + XCTAssertNotNil(object.entries) + XCTAssertNotNil(object.milestoneId) + XCTAssertEqual(object.assignedtoId, Properties.Optional.assignedtoId) + XCTAssertEqual(object.completedOn, Properties.Optional.completedOn) + XCTAssertEqual(object.description, Properties.Optional.description) + XCTAssertEqual(object.entries!, Properties.Optional.entries) + XCTAssertEqual(object.milestoneId, Properties.Optional.milestoneId) + } + } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { + + object.description = "New Description" + object.milestoneId = 9999 + object.name = "New Name" + + XCTAssertNotEqual(object.description, Properties.Optional.description) + XCTAssertNotEqual(object.milestoneId, Properties.Optional.milestoneId) + XCTAssertNotEqual(object.name, Properties.Required.name) + + XCTAssertEqual(object.description, "New Description") + XCTAssertEqual(object.milestoneId, 9999) + XCTAssertEqual(object.name, "New Name") + } + +} + +extension PlanTests: AssertUpdateRequestJSON { } diff --git a/QuizTrainTests/Models/PriorityTests.swift b/QuizTrainTests/Models/PriorityTests.swift new file mode 100644 index 0000000..a04dee5 --- /dev/null +++ b/QuizTrainTests/Models/PriorityTests.swift @@ -0,0 +1,156 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class PriorityTests: XCTestCase, ModelTests { + + typealias Object = Priority + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONDeserializing() { + _testJSONDeserializing() + } + + func testJSONDeserializingWithOptionalProperties() { + _testJSONDeserializingWithOptionalProperties() + } + + func testJSONDeserializingASingleObject() { + _testJSONDeserializingASingleObject() + } + + func testJSONDeserializingMultipleObjects() { + _testJSONDeserializingMultipleObjects() + } + + func testJSONDeserializingASingleObjectMissingRequiredProperties() { + _testJSONDeserializingASingleObjectMissingRequiredProperties() + } + + func testJSONDeserializingMultipleObjectsMissingRequiredProperties() { + _testJSONDeserializingMultipleObjectsMissingRequiredProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testJSONTwoWaySerializationForSingleItems() { + _testJSONTwoWaySerializationForSingleItems() + } + + func testJSONTwoWaySerializationForMultipleItems() { + _testJSONTwoWaySerializationForMultipleItems() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testUpdateRequestJSON() { + _testUpdateRequestJSON() + } + +} + +// MARK: - Data + +extension PriorityTests { + + struct Properties { + + struct Required { + static let id = 472 + static let isDefault = true + static let name = "Name" + static let priority = 3 + static let shortName = "Short Name" + } + + struct Optional { + // none + } + + } + +} + +extension PriorityTests: JSONDataProvider { + + static var requiredJSON: JSONDictionary { + return [Object.JSONKeys.id.rawValue: Properties.Required.id, + Object.JSONKeys.isDefault.rawValue: Properties.Required.isDefault, + Object.JSONKeys.name.rawValue: Properties.Required.name, + Object.JSONKeys.priority.rawValue: Properties.Required.priority, + Object.JSONKeys.shortName.rawValue: Properties.Required.shortName] + } + + static var optionalJSON: JSONDictionary { + return [:] // none + } + +} + +// MARK: - Objects + +extension PriorityTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(id: Properties.Required.id, + isDefault: Properties.Required.isDefault, + name: Properties.Required.name, + priority: Properties.Required.priority, + shortName: Properties.Required.shortName) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(id: Properties.Required.id, + isDefault: Properties.Required.isDefault, + name: Properties.Required.name, + priority: Properties.Required.priority, + shortName: Properties.Required.shortName) + } + +} + +// MARK: - Assertions + +extension PriorityTests: AssertEquatable { } + +extension PriorityTests: AssertJSONDeserializing { } + +extension PriorityTests: AssertJSONSerializing { } + +extension PriorityTests: AssertJSONTwoWaySerialization { } + +extension PriorityTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.id, Properties.Required.id) + XCTAssertEqual(object.isDefault, Properties.Required.isDefault) + XCTAssertEqual(object.name, Properties.Required.name) + XCTAssertEqual(object.priority, Properties.Required.priority) + XCTAssertEqual(object.shortName, Properties.Required.shortName) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { /* none */ } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { /* none */ } + +} diff --git a/QuizTrainTests/Models/ProjectTests.swift b/QuizTrainTests/Models/ProjectTests.swift new file mode 100644 index 0000000..6b97e2b --- /dev/null +++ b/QuizTrainTests/Models/ProjectTests.swift @@ -0,0 +1,198 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class ProjectTests: XCTestCase, ModelTests { + + typealias Object = Project + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONDeserializing() { + _testJSONDeserializing() + } + + func testJSONDeserializingWithOptionalProperties() { + _testJSONDeserializingWithOptionalProperties() + } + + func testJSONDeserializingASingleObject() { + _testJSONDeserializingASingleObject() + } + + func testJSONDeserializingMultipleObjects() { + _testJSONDeserializingMultipleObjects() + } + + func testJSONDeserializingASingleObjectMissingRequiredProperties() { + _testJSONDeserializingASingleObjectMissingRequiredProperties() + } + + func testJSONDeserializingMultipleObjectsMissingRequiredProperties() { + _testJSONDeserializingMultipleObjectsMissingRequiredProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testJSONTwoWaySerializationForSingleItems() { + _testJSONTwoWaySerializationForSingleItems() + } + + func testJSONTwoWaySerializationForMultipleItems() { + _testJSONTwoWaySerializationForMultipleItems() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testUpdateRequestJSON() { + _testUpdateRequestJSON() + } + +} + +// MARK: - Data + +extension ProjectTests { + + struct Properties { + + struct Required { + static let id = 472 + static let isCompleted = true + static let name = "Name" + static let showAnnouncement = true + static let suiteMode = Project.SuiteMode.multipleSuites + static let url = URL(string: "https://www.testrail.com/")! + } + + struct Optional { + static let announcement = "Announcement" + static let completedOn = Date(secondsSince1970: 72988302) + } + + } + +} + +extension ProjectTests: JSONDataProvider { + + static var requiredJSON: JSONDictionary { + return [Object.JSONKeys.id.rawValue: Properties.Required.id, + Object.JSONKeys.isCompleted.rawValue: Properties.Required.isCompleted, + Object.JSONKeys.name.rawValue: Properties.Required.name, + Object.JSONKeys.showAnnouncement.rawValue: Properties.Required.showAnnouncement, + Object.JSONKeys.suiteMode.rawValue: Properties.Required.suiteMode.rawValue, + Object.JSONKeys.url.rawValue: Properties.Required.url.absoluteString] + } + + static var optionalJSON: JSONDictionary { + return [Object.JSONKeys.announcement.rawValue: Properties.Optional.announcement, + Object.JSONKeys.completedOn.rawValue: Properties.Optional.completedOn.secondsSince1970] + } + +} + +// MARK: - Objects + +extension ProjectTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(announcement: nil, + completedOn: nil, + id: Properties.Required.id, + isCompleted: Properties.Required.isCompleted, + name: Properties.Required.name, + showAnnouncement: Properties.Required.showAnnouncement, + suiteMode: Properties.Required.suiteMode, + url: Properties.Required.url) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(announcement: Properties.Optional.announcement, + completedOn: Properties.Optional.completedOn, + id: Properties.Required.id, + isCompleted: Properties.Required.isCompleted, + name: Properties.Required.name, + showAnnouncement: Properties.Required.showAnnouncement, + suiteMode: Properties.Required.suiteMode, + url: Properties.Required.url) + } + +} + +// MARK: - Assertions + +extension ProjectTests: AssertEquatable { } + +extension ProjectTests: AssertJSONDeserializing { } + +extension ProjectTests: AssertJSONSerializing { } + +extension ProjectTests: AssertJSONTwoWaySerialization { } + +extension ProjectTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.id, Properties.Required.id) + XCTAssertEqual(object.isCompleted, Properties.Required.isCompleted) + XCTAssertEqual(object.name, Properties.Required.name) + XCTAssertEqual(object.showAnnouncement, Properties.Required.showAnnouncement) + XCTAssertEqual(object.suiteMode, Properties.Required.suiteMode) + XCTAssertEqual(object.url, Properties.Required.url) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { + if areNil { + XCTAssertNil(object.announcement) + XCTAssertNil(object.completedOn) + } else { + XCTAssertNotNil(object.announcement) + XCTAssertNotNil(object.completedOn) + XCTAssertEqual(object.announcement, Properties.Optional.announcement) + XCTAssertEqual(object.completedOn, Properties.Optional.completedOn) + } + } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { + + object.announcement = "New Annoucement" + object.isCompleted = false + object.name = "New Name" + object.showAnnouncement = false + object.suiteMode = .singleSuitePlusBaselines + + XCTAssertNotEqual(object.announcement, Properties.Optional.announcement) + XCTAssertNotEqual(object.isCompleted, Properties.Required.isCompleted) + XCTAssertNotEqual(object.name, Properties.Required.name) + XCTAssertNotEqual(object.showAnnouncement, Properties.Required.showAnnouncement) + XCTAssertNotEqual(object.suiteMode, Properties.Required.suiteMode) + + XCTAssertEqual(object.announcement, "New Annoucement") + XCTAssertEqual(object.isCompleted, false) + XCTAssertEqual(object.name, "New Name") + XCTAssertEqual(object.showAnnouncement, false) + XCTAssertEqual(object.suiteMode, .singleSuitePlusBaselines) + } + +} + +extension ProjectTests: AssertUpdateRequestJSON { } diff --git a/QuizTrainTests/Models/ResultFieldTests.swift b/QuizTrainTests/Models/ResultFieldTests.swift new file mode 100644 index 0000000..5900b8b --- /dev/null +++ b/QuizTrainTests/Models/ResultFieldTests.swift @@ -0,0 +1,190 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class ResultFieldTests: XCTestCase, ModelTests { + + typealias Object = ResultField + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONDeserializing() { + _testJSONDeserializing() + } + + func testJSONDeserializingWithOptionalProperties() { + _testJSONDeserializingWithOptionalProperties() + } + + func testJSONDeserializingASingleObject() { + _testJSONDeserializingASingleObject() + } + + func testJSONDeserializingMultipleObjects() { + _testJSONDeserializingMultipleObjects() + } + + func testJSONDeserializingASingleObjectMissingRequiredProperties() { + _testJSONDeserializingASingleObjectMissingRequiredProperties() + } + + func testJSONDeserializingMultipleObjectsMissingRequiredProperties() { + _testJSONDeserializingMultipleObjectsMissingRequiredProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testJSONTwoWaySerializationForSingleItems() { + _testJSONTwoWaySerializationForSingleItems() + } + + func testJSONTwoWaySerializationForMultipleItems() { + _testJSONTwoWaySerializationForMultipleItems() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testUpdateRequestJSON() { + _testUpdateRequestJSON() + } + +} + +// MARK: - Data + +extension ResultFieldTests { + + struct Properties { + + struct Required { + static let configs = [ConfigTests.objectWithRequiredAndOptionalPropertiesFromJSON!, ConfigTests.objectWithRequiredPropertiesFromJSON!, ConfigTests.objectWithRequiredAndOptionalPropertiesFromJSON!] // This must match the order and datasources in: JSON.required["configs"] + static let displayOrder = 10 + static let id = 11 + static let includeAll = true + static let isActive = true + static let label = "Label" + static let name = "Name" + static let systemName = "System Name" + static let templateIds = [12, 13, 14, 15, 16] + static let typeId = CustomFieldType.text + } + + struct Optional { + static let description = "Description" + } + + } + +} + +extension ResultFieldTests: JSONDataProvider { + + static var requiredJSON: JSONDictionary { + return [Object.JSONKeys.configs.rawValue: [ConfigTests.requiredAndOptionalJSON, ConfigTests.requiredJSON, ConfigTests.requiredAndOptionalJSON], // This must match the order and datasources in: Properties.Required.configs + Object.JSONKeys.displayOrder.rawValue: Properties.Required.displayOrder, + Object.JSONKeys.id.rawValue: Properties.Required.id, + Object.JSONKeys.includeAll.rawValue: Properties.Required.includeAll, + Object.JSONKeys.isActive.rawValue: Properties.Required.isActive, + Object.JSONKeys.label.rawValue: Properties.Required.label, + Object.JSONKeys.name.rawValue: Properties.Required.name, + Object.JSONKeys.systemName.rawValue: Properties.Required.systemName, + Object.JSONKeys.templateIds.rawValue: Properties.Required.templateIds, + Object.JSONKeys.typeId.rawValue: Properties.Required.typeId.rawValue] + } + + static var optionalJSON: JSONDictionary { + return [Object.JSONKeys.description.rawValue: Properties.Optional.description] + } + +} + +// MARK: - Objects + +extension ResultFieldTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(configs: Properties.Required.configs, + description: nil, + displayOrder: Properties.Required.displayOrder, + id: Properties.Required.id, + includeAll: Properties.Required.includeAll, + isActive: Properties.Required.isActive, + label: Properties.Required.label, + name: Properties.Required.name, + systemName: Properties.Required.systemName, + templateIds: Properties.Required.templateIds, + typeId: Properties.Required.typeId) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(configs: Properties.Required.configs, + description: Properties.Optional.description, + displayOrder: Properties.Required.displayOrder, + id: Properties.Required.id, + includeAll: Properties.Required.includeAll, + isActive: Properties.Required.isActive, + label: Properties.Required.label, + name: Properties.Required.name, + systemName: Properties.Required.systemName, + templateIds: Properties.Required.templateIds, + typeId: Properties.Required.typeId) + } + +} + +// MARK: - Assertions + +extension ResultFieldTests: AssertEquatable { } + +extension ResultFieldTests: AssertJSONDeserializing { } + +extension ResultFieldTests: AssertJSONSerializing { } + +extension ResultFieldTests: AssertJSONTwoWaySerialization { } + +extension ResultFieldTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.configs.count, Properties.Required.configs.count) + XCTAssertEqual(object.displayOrder, Properties.Required.displayOrder) + XCTAssertEqual(object.id, Properties.Required.id) + XCTAssertEqual(object.includeAll, Properties.Required.includeAll) + XCTAssertEqual(object.isActive, Properties.Required.isActive) + XCTAssertEqual(object.label, Properties.Required.label) + XCTAssertEqual(object.name, Properties.Required.name) + XCTAssertEqual(object.systemName, Properties.Required.systemName) + XCTAssertEqual(object.templateIds, Properties.Required.templateIds) + XCTAssertEqual(object.typeId, Properties.Required.typeId) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { + if areNil { + XCTAssertNil(object.description) + } else { + XCTAssertNotNil(object.description) + XCTAssertEqual(object.description, Properties.Optional.description) + } + } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { /* none */ } + +} diff --git a/QuizTrainTests/Models/ResultTests.swift b/QuizTrainTests/Models/ResultTests.swift new file mode 100644 index 0000000..24beb8a --- /dev/null +++ b/QuizTrainTests/Models/ResultTests.swift @@ -0,0 +1,201 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class ResultTests: XCTestCase, ModelTests { + + typealias Object = Result + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONDeserializing() { + _testJSONDeserializing() + } + + func testJSONDeserializingWithOptionalProperties() { + _testJSONDeserializingWithOptionalProperties() + } + + func testJSONDeserializingASingleObject() { + _testJSONDeserializingASingleObject() + } + + func testJSONDeserializingMultipleObjects() { + _testJSONDeserializingMultipleObjects() + } + + func testJSONDeserializingASingleObjectMissingRequiredProperties() { + _testJSONDeserializingASingleObjectMissingRequiredProperties() + } + + func testJSONDeserializingMultipleObjectsMissingRequiredProperties() { + _testJSONDeserializingMultipleObjectsMissingRequiredProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testJSONTwoWaySerializationForSingleItems() { + _testJSONTwoWaySerializationForSingleItems() + } + + func testJSONTwoWaySerializationForMultipleItems() { + _testJSONTwoWaySerializationForMultipleItems() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testUpdateRequestJSON() { + _testUpdateRequestJSON() + } + +} + +// MARK: - Data + +extension ResultTests { + + struct Properties { + + struct Required { + static let createdBy = 67 + static let createdOn = Date(secondsSince1970: 72973833) + static let id = 972 + static let testId = 7 + } + + struct Optional { + static let assignedtoId = 47 + static let comment = "Comment" + static let defects = "Defects" + static let elapsed = "4hr, 31min" + static let statusId = 3 + static let version = "1.2.3" + } + + } + +} + +extension ResultTests: CustomFieldsDataProvider { } + +extension ResultTests: JSONDataProvider { + + static var requiredJSON: JSONDictionary { + return [Object.JSONKeys.createdBy.rawValue: Properties.Required.createdBy, + Object.JSONKeys.createdOn.rawValue: Properties.Required.createdOn.secondsSince1970, + Object.JSONKeys.id.rawValue: Properties.Required.id, + Object.JSONKeys.testId.rawValue: Properties.Required.testId] + } + + static var optionalJSON: JSONDictionary { + return [Object.JSONKeys.assignedtoId.rawValue: Properties.Optional.assignedtoId, + Object.JSONKeys.comment.rawValue: Properties.Optional.comment, + Object.JSONKeys.defects.rawValue: Properties.Optional.defects, + Object.JSONKeys.elapsed.rawValue: Properties.Optional.elapsed, + Object.JSONKeys.statusId.rawValue: Properties.Optional.statusId, + Object.JSONKeys.version.rawValue: Properties.Optional.version] + } + +} + +// MARK: - Objects + +extension ResultTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(assignedtoId: nil, + comment: nil, + createdBy: Properties.Required.createdBy, + createdOn: Properties.Required.createdOn, + defects: nil, + elapsed: nil, + id: Properties.Required.id, + statusId: nil, + testId: Properties.Required.testId, + version: nil, + customFieldsContainer: emptyCustomFieldsContainer) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(assignedtoId: Properties.Optional.assignedtoId, + comment: Properties.Optional.comment, + createdBy: Properties.Required.createdBy, + createdOn: Properties.Required.createdOn, + defects: Properties.Optional.defects, + elapsed: Properties.Optional.elapsed, + id: Properties.Required.id, + statusId: Properties.Optional.statusId, + testId: Properties.Required.testId, + version: Properties.Optional.version, + customFieldsContainer: customFieldsContainer) + } + +} + +// MARK: - Assertions + +extension ResultTests: AssertCustomFields { } + +extension ResultTests: AssertEquatable { } + +extension ResultTests: AssertJSONDeserializing { } + +extension ResultTests: AssertJSONSerializing { } + +extension ResultTests: AssertJSONTwoWaySerialization { } + +extension ResultTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.createdBy, Properties.Required.createdBy) + XCTAssertEqual(object.createdOn, Properties.Required.createdOn) + XCTAssertEqual(object.id, Properties.Required.id) + XCTAssertEqual(object.testId, Properties.Required.testId) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { + if areNil { + XCTAssertNil(object.assignedtoId) + XCTAssertNil(object.comment) + XCTAssertNil(object.defects) + XCTAssertNil(object.elapsed) + XCTAssertNil(object.statusId) + XCTAssertNil(object.version) + } else { + XCTAssertNotNil(object.assignedtoId) + XCTAssertNotNil(object.comment) + XCTAssertNotNil(object.defects) + XCTAssertNotNil(object.elapsed) + XCTAssertNotNil(object.statusId) + XCTAssertNotNil(object.version) + XCTAssertEqual(object.assignedtoId, Properties.Optional.assignedtoId) + XCTAssertEqual(object.comment, Properties.Optional.comment) + XCTAssertEqual(object.defects, Properties.Optional.defects) + XCTAssertEqual(object.elapsed, Properties.Optional.elapsed) + XCTAssertEqual(object.statusId, Properties.Optional.statusId) + XCTAssertEqual(object.version, Properties.Optional.version) + } + } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { /* none */ } + +} diff --git a/QuizTrainTests/Models/RunTests.swift b/QuizTrainTests/Models/RunTests.swift new file mode 100644 index 0000000..9bada4a --- /dev/null +++ b/QuizTrainTests/Models/RunTests.swift @@ -0,0 +1,307 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class RunTests: XCTestCase, ModelTests { + + typealias Object = Run + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONDeserializing() { + _testJSONDeserializing() + } + + func testJSONDeserializingWithOptionalProperties() { + _testJSONDeserializingWithOptionalProperties() + } + + func testJSONDeserializingASingleObject() { + _testJSONDeserializingASingleObject() + } + + func testJSONDeserializingMultipleObjects() { + _testJSONDeserializingMultipleObjects() + } + + func testJSONDeserializingASingleObjectMissingRequiredProperties() { + _testJSONDeserializingASingleObjectMissingRequiredProperties() + } + + func testJSONDeserializingMultipleObjectsMissingRequiredProperties() { + _testJSONDeserializingMultipleObjectsMissingRequiredProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testJSONTwoWaySerializationForSingleItems() { + _testJSONTwoWaySerializationForSingleItems() + } + + func testJSONTwoWaySerializationForMultipleItems() { + _testJSONTwoWaySerializationForMultipleItems() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testUpdateRequestJSON() { + _testUpdateRequestJSON() + } + +} + +// MARK: - Data + +extension RunTests { + + struct Properties { + + struct Required { + static let blockedCount = 2 + static let createdBy = 4 + static let createdOn = Date(secondsSince1970: 72973833) + static let customStatus1Count = 3 + static let customStatus2Count = 45 + static let customStatus3Count = 78 + static let customStatus4Count = 73 + static let customStatus5Count = 820 + static let customStatus6Count = 1023 + static let customStatus7Count = 567 + static let failedCount = 80 + static let id = 20 + static let includeAll = true + static let isCompleted = true + static let name = "Name" + static let passedCount = 834 + static let projectId = 8 + static let retestCount = 73 + static let untestedCount = 53 + static let url = URL(string: "https://www.testrail.com/")! + } + + struct Optional { + static let assignedtoId = 355 + static let completedOn = Date(secondsSince1970: 72988302) + static let config = "Config" + static let configIds = [3, 4, 23, 328] + static let description = "Description" + static let milestoneId = 33 + static let planId = 2034 + static let suiteId = 36 + } + + } + +} + +extension RunTests: JSONDataProvider { + + static var requiredJSON: JSONDictionary { + return [Object.JSONKeys.blockedCount.rawValue: Properties.Required.blockedCount, + Object.JSONKeys.createdBy.rawValue: Properties.Required.createdBy, + Object.JSONKeys.createdOn.rawValue: Properties.Required.createdOn.secondsSince1970, + Object.JSONKeys.customStatus1Count.rawValue: Properties.Required.customStatus1Count, + Object.JSONKeys.customStatus2Count.rawValue: Properties.Required.customStatus2Count, + Object.JSONKeys.customStatus3Count.rawValue: Properties.Required.customStatus3Count, + Object.JSONKeys.customStatus4Count.rawValue: Properties.Required.customStatus4Count, + Object.JSONKeys.customStatus5Count.rawValue: Properties.Required.customStatus5Count, + Object.JSONKeys.customStatus6Count.rawValue: Properties.Required.customStatus6Count, + Object.JSONKeys.customStatus7Count.rawValue: Properties.Required.customStatus7Count, + Object.JSONKeys.failedCount.rawValue: Properties.Required.failedCount, + Object.JSONKeys.id.rawValue: Properties.Required.id, + Object.JSONKeys.includeAll.rawValue: Properties.Required.includeAll, + Object.JSONKeys.isCompleted.rawValue: Properties.Required.isCompleted, + Object.JSONKeys.name.rawValue: Properties.Required.name, + Object.JSONKeys.passedCount.rawValue: Properties.Required.passedCount, + Object.JSONKeys.projectId.rawValue: Properties.Required.projectId, + Object.JSONKeys.retestCount.rawValue: Properties.Required.retestCount, + Object.JSONKeys.untestedCount.rawValue: Properties.Required.untestedCount, + Object.JSONKeys.url.rawValue: Properties.Required.url.absoluteString] + } + + static var optionalJSON: JSONDictionary { + return [Object.JSONKeys.assignedtoId.rawValue: Properties.Optional.assignedtoId, + Object.JSONKeys.completedOn.rawValue: Properties.Optional.completedOn.secondsSince1970, + Object.JSONKeys.config.rawValue: Properties.Optional.config, + Object.JSONKeys.configIds.rawValue: Properties.Optional.configIds, + Object.JSONKeys.description.rawValue: Properties.Optional.description, + Object.JSONKeys.milestoneId.rawValue: Properties.Optional.milestoneId, + Object.JSONKeys.planId.rawValue: Properties.Optional.planId, + Object.JSONKeys.suiteId.rawValue: Properties.Optional.suiteId] + } + +} + +// MARK: - Objects + +extension RunTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(assignedtoId: nil, + blockedCount: Properties.Required.blockedCount, + completedOn: nil, + config: nil, + configIds: nil, + createdBy: Properties.Required.createdBy, + createdOn: Properties.Required.createdOn, + customStatus1Count: Properties.Required.customStatus1Count, + customStatus2Count: Properties.Required.customStatus2Count, + customStatus3Count: Properties.Required.customStatus3Count, + customStatus4Count: Properties.Required.customStatus4Count, + customStatus5Count: Properties.Required.customStatus5Count, + customStatus6Count: Properties.Required.customStatus6Count, + customStatus7Count: Properties.Required.customStatus7Count, + description: nil, + failedCount: Properties.Required.failedCount, + id: Properties.Required.id, + includeAll: Properties.Required.includeAll, + isCompleted: Properties.Required.isCompleted, + milestoneId: nil, + name: Properties.Required.name, + planId: nil, + passedCount: Properties.Required.passedCount, + projectId: Properties.Required.projectId, + retestCount: Properties.Required.retestCount, + suiteId: nil, + untestedCount: Properties.Required.untestedCount, + url: Properties.Required.url) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(assignedtoId: Properties.Optional.assignedtoId, + blockedCount: Properties.Required.blockedCount, + completedOn: Properties.Optional.completedOn, + config: Properties.Optional.config, + configIds: Properties.Optional.configIds, + createdBy: Properties.Required.createdBy, + createdOn: Properties.Required.createdOn, + customStatus1Count: Properties.Required.customStatus1Count, + customStatus2Count: Properties.Required.customStatus2Count, + customStatus3Count: Properties.Required.customStatus3Count, + customStatus4Count: Properties.Required.customStatus4Count, + customStatus5Count: Properties.Required.customStatus5Count, + customStatus6Count: Properties.Required.customStatus6Count, + customStatus7Count: Properties.Required.customStatus7Count, + description: Properties.Optional.description, + failedCount: Properties.Required.failedCount, + id: Properties.Required.id, + includeAll: Properties.Required.includeAll, + isCompleted: Properties.Required.isCompleted, + milestoneId: Properties.Optional.milestoneId, + name: Properties.Required.name, + planId: Properties.Optional.planId, + passedCount: Properties.Required.passedCount, + projectId: Properties.Required.projectId, + retestCount: Properties.Required.retestCount, + suiteId: Properties.Optional.suiteId, + untestedCount: Properties.Required.untestedCount, + url: Properties.Required.url) + } + +} + +// MARK: - Assertions + +extension RunTests: AssertEquatable { } + +extension RunTests: AssertJSONDeserializing { } + +extension RunTests: AssertJSONSerializing { } + +extension RunTests: AssertJSONTwoWaySerialization { } + +extension RunTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.blockedCount, Properties.Required.blockedCount) + XCTAssertEqual(object.createdBy, Properties.Required.createdBy) + XCTAssertEqual(object.createdOn, Properties.Required.createdOn) + XCTAssertEqual(object.customStatus1Count, Properties.Required.customStatus1Count) + XCTAssertEqual(object.customStatus2Count, Properties.Required.customStatus2Count) + XCTAssertEqual(object.customStatus3Count, Properties.Required.customStatus3Count) + XCTAssertEqual(object.customStatus4Count, Properties.Required.customStatus4Count) + XCTAssertEqual(object.customStatus5Count, Properties.Required.customStatus5Count) + XCTAssertEqual(object.customStatus6Count, Properties.Required.customStatus6Count) + XCTAssertEqual(object.customStatus7Count, Properties.Required.customStatus7Count) + XCTAssertEqual(object.failedCount, Properties.Required.failedCount) + XCTAssertEqual(object.id, Properties.Required.id) + XCTAssertEqual(object.includeAll, Properties.Required.includeAll) + XCTAssertEqual(object.isCompleted, Properties.Required.isCompleted) + XCTAssertEqual(object.name, Properties.Required.name) + XCTAssertEqual(object.passedCount, Properties.Required.passedCount) + XCTAssertEqual(object.projectId, Properties.Required.projectId) + XCTAssertEqual(object.retestCount, Properties.Required.retestCount) + XCTAssertEqual(object.untestedCount, Properties.Required.untestedCount) + XCTAssertEqual(object.url, Properties.Required.url) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { + if areNil { + XCTAssertNil(object.assignedtoId) + XCTAssertNil(object.completedOn) + XCTAssertNil(object.config) + XCTAssertNil(object.configIds) + XCTAssertNil(object.description) + XCTAssertNil(object.milestoneId) + XCTAssertNil(object.planId) + XCTAssertNil(object.suiteId) + } else { + XCTAssertNotNil(object.assignedtoId) + XCTAssertNotNil(object.completedOn) + XCTAssertNotNil(object.config) + XCTAssertNotNil(object.configIds) + XCTAssertNotNil(object.description) + XCTAssertNotNil(object.milestoneId) + XCTAssertNotNil(object.planId) + XCTAssertNotNil(object.suiteId) + XCTAssertEqual(object.assignedtoId, Properties.Optional.assignedtoId) + XCTAssertEqual(object.completedOn, Properties.Optional.completedOn) + XCTAssertEqual(object.config, Properties.Optional.config) + XCTAssertEqual(object.configIds!, Properties.Optional.configIds) + XCTAssertEqual(object.description, Properties.Optional.description) + XCTAssertEqual(object.milestoneId, Properties.Optional.milestoneId) + XCTAssertEqual(object.planId, Properties.Optional.planId) + XCTAssertEqual(object.suiteId, Properties.Optional.suiteId) + } + } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { + + object.description = "New Description" + object.includeAll = false + object.milestoneId = 99999 + object.name = "New Name" + + XCTAssertNotEqual(object.description, Properties.Optional.description) + XCTAssertNotEqual(object.includeAll, Properties.Required.includeAll) + XCTAssertNotEqual(object.milestoneId, Properties.Optional.milestoneId) + XCTAssertNotEqual(object.name, Properties.Required.name) + + XCTAssertEqual(object.description, "New Description") + XCTAssertEqual(object.includeAll, false) + XCTAssertEqual(object.milestoneId, 99999) + XCTAssertEqual(object.name, "New Name") + } + +} + +extension RunTests: AssertUpdateRequestJSON { } diff --git a/QuizTrainTests/Models/SectionTests.swift b/QuizTrainTests/Models/SectionTests.swift new file mode 100644 index 0000000..bf643c4 --- /dev/null +++ b/QuizTrainTests/Models/SectionTests.swift @@ -0,0 +1,186 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class SectionTests: XCTestCase, ModelTests { + + typealias Object = Section + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONDeserializing() { + _testJSONDeserializing() + } + + func testJSONDeserializingWithOptionalProperties() { + _testJSONDeserializingWithOptionalProperties() + } + + func testJSONDeserializingASingleObject() { + _testJSONDeserializingASingleObject() + } + + func testJSONDeserializingMultipleObjects() { + _testJSONDeserializingMultipleObjects() + } + + func testJSONDeserializingASingleObjectMissingRequiredProperties() { + _testJSONDeserializingASingleObjectMissingRequiredProperties() + } + + func testJSONDeserializingMultipleObjectsMissingRequiredProperties() { + _testJSONDeserializingMultipleObjectsMissingRequiredProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testJSONTwoWaySerializationForSingleItems() { + _testJSONTwoWaySerializationForSingleItems() + } + + func testJSONTwoWaySerializationForMultipleItems() { + _testJSONTwoWaySerializationForMultipleItems() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testUpdateRequestJSON() { + _testUpdateRequestJSON() + } + +} + +// MARK: - Data + +extension SectionTests { + + struct Properties { + + struct Required { + static let depth = 2 + static let displayOrder = 1 + static let id = 2733 + static let name = "Name" + } + + struct Optional { + static let description = "Description" + static let parentId = 382 + static let suiteId = 33 + } + + } + +} + +extension SectionTests: JSONDataProvider { + + static var requiredJSON: JSONDictionary { + return [Object.JSONKeys.depth.rawValue: Properties.Required.depth, + Object.JSONKeys.displayOrder.rawValue: Properties.Required.displayOrder, + Object.JSONKeys.id.rawValue: Properties.Required.id, + Object.JSONKeys.name.rawValue: Properties.Required.name] + } + + static var optionalJSON: JSONDictionary { + return [Object.JSONKeys.description.rawValue: Properties.Optional.description, + Object.JSONKeys.parentId.rawValue: Properties.Optional.parentId, + Object.JSONKeys.suiteId.rawValue: Properties.Optional.suiteId] + } + +} + +// MARK: - Objects + +extension SectionTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(depth: Properties.Required.depth, + description: nil, + displayOrder: Properties.Required.displayOrder, + id: Properties.Required.id, + name: Properties.Required.name, + parentId: nil, + suiteId: nil) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(depth: Properties.Required.depth, + description: Properties.Optional.description, + displayOrder: Properties.Required.displayOrder, + id: Properties.Required.id, + name: Properties.Required.name, + parentId: Properties.Optional.parentId, + suiteId: Properties.Optional.suiteId) + } + +} + +// MARK: - Assertions + +extension SectionTests: AssertEquatable { } + +extension SectionTests: AssertJSONDeserializing { } + +extension SectionTests: AssertJSONSerializing { } + +extension SectionTests: AssertJSONTwoWaySerialization { } + +extension SectionTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.depth, Properties.Required.depth) + XCTAssertEqual(object.displayOrder, Properties.Required.displayOrder) + XCTAssertEqual(object.id, Properties.Required.id) + XCTAssertEqual(object.name, Properties.Required.name) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { + if areNil { + XCTAssertNil(object.description) + XCTAssertNil(object.parentId) + XCTAssertNil(object.suiteId) + } else { + XCTAssertNotNil(object.description) + XCTAssertNotNil(object.parentId) + XCTAssertNotNil(object.suiteId) + XCTAssertEqual(object.description, Properties.Optional.description) + XCTAssertEqual(object.parentId, Properties.Optional.parentId) + XCTAssertEqual(object.suiteId, Properties.Optional.suiteId) + } + } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { + + object.description = "New Description" + object.name = "New Name" + + XCTAssertNotEqual(object.description, Properties.Optional.description) + XCTAssertNotEqual(object.name, Properties.Required.name) + + XCTAssertEqual(object.description, "New Description") + XCTAssertEqual(object.name, "New Name") + } + +} + +extension SectionTests: AssertUpdateRequestJSON { } diff --git a/QuizTrainTests/Models/StatusTests.swift b/QuizTrainTests/Models/StatusTests.swift new file mode 100644 index 0000000..995233e --- /dev/null +++ b/QuizTrainTests/Models/StatusTests.swift @@ -0,0 +1,174 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class StatusTests: XCTestCase, ModelTests { + + typealias Object = Status + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONDeserializing() { + _testJSONDeserializing() + } + + func testJSONDeserializingWithOptionalProperties() { + _testJSONDeserializingWithOptionalProperties() + } + + func testJSONDeserializingASingleObject() { + _testJSONDeserializingASingleObject() + } + + func testJSONDeserializingMultipleObjects() { + _testJSONDeserializingMultipleObjects() + } + + func testJSONDeserializingASingleObjectMissingRequiredProperties() { + _testJSONDeserializingASingleObjectMissingRequiredProperties() + } + + func testJSONDeserializingMultipleObjectsMissingRequiredProperties() { + _testJSONDeserializingMultipleObjectsMissingRequiredProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testJSONTwoWaySerializationForSingleItems() { + _testJSONTwoWaySerializationForSingleItems() + } + + func testJSONTwoWaySerializationForMultipleItems() { + _testJSONTwoWaySerializationForMultipleItems() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testUpdateRequestJSON() { + _testUpdateRequestJSON() + } + +} + +// MARK: - Data + +extension StatusTests { + + struct Properties { + + struct Required { + static let colorBright = 1000 + static let colorDark = 1001 + static let colorMedium = 1002 + static let id = 1 + static let isFinal = true + static let isSystem = true + static let isUntested = true + static let label = "Label" + static let name = "Name" + } + + struct Optional { /* none */ } + + } + +} + +extension StatusTests: JSONDataProvider { + + static var requiredJSON: JSONDictionary { + return [Object.JSONKeys.colorBright.rawValue: Properties.Required.colorBright, + Object.JSONKeys.colorDark.rawValue: Properties.Required.colorDark, + Object.JSONKeys.colorMedium.rawValue: Properties.Required.colorMedium, + Object.JSONKeys.id.rawValue: Properties.Required.id, + Object.JSONKeys.isFinal.rawValue: Properties.Required.isFinal, + Object.JSONKeys.isSystem.rawValue: Properties.Required.isSystem, + Object.JSONKeys.isUntested.rawValue: Properties.Required.isUntested, + Object.JSONKeys.label.rawValue: Properties.Required.label, + Object.JSONKeys.name.rawValue: Properties.Required.name] + } + + static var optionalJSON: JSONDictionary { + return [:] // none + } + +} + +// MARK: - Objects + +extension StatusTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(colorBright: Properties.Required.colorBright, + colorDark: Properties.Required.colorDark, + colorMedium: Properties.Required.colorMedium, + id: Properties.Required.id, + isFinal: Properties.Required.isFinal, + isSystem: Properties.Required.isSystem, + isUntested: Properties.Required.isUntested, + label: Properties.Required.label, + name: Properties.Required.name) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(colorBright: Properties.Required.colorBright, + colorDark: Properties.Required.colorDark, + colorMedium: Properties.Required.colorMedium, + id: Properties.Required.id, + isFinal: Properties.Required.isFinal, + isSystem: Properties.Required.isSystem, + isUntested: Properties.Required.isUntested, + label: Properties.Required.label, + name: Properties.Required.name) + } + +} + +// MARK: - Assertions + +extension StatusTests: AssertEquatable { } + +extension StatusTests: AssertJSONDeserializing { } + +extension StatusTests: AssertJSONSerializing { } + +extension StatusTests: AssertJSONTwoWaySerialization { } + +extension StatusTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.colorBright, Properties.Required.colorBright) + XCTAssertEqual(object.colorDark, Properties.Required.colorDark) + XCTAssertEqual(object.colorMedium, Properties.Required.colorMedium) + XCTAssertEqual(object.id, Properties.Required.id) + XCTAssertEqual(object.isFinal, Properties.Required.isFinal) + XCTAssertEqual(object.isSystem, Properties.Required.isSystem) + XCTAssertEqual(object.isUntested, Properties.Required.isUntested) + XCTAssertEqual(object.label, Properties.Required.label) + XCTAssertEqual(object.name, Properties.Required.name) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { /* none */ } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { /* none */ } + +} diff --git a/QuizTrainTests/Models/SuiteTests.swift b/QuizTrainTests/Models/SuiteTests.swift new file mode 100644 index 0000000..8af52fc --- /dev/null +++ b/QuizTrainTests/Models/SuiteTests.swift @@ -0,0 +1,194 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class SuiteTests: XCTestCase, ModelTests { + + typealias Object = Suite + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONDeserializing() { + _testJSONDeserializing() + } + + func testJSONDeserializingWithOptionalProperties() { + _testJSONDeserializingWithOptionalProperties() + } + + func testJSONDeserializingASingleObject() { + _testJSONDeserializingASingleObject() + } + + func testJSONDeserializingMultipleObjects() { + _testJSONDeserializingMultipleObjects() + } + + func testJSONDeserializingASingleObjectMissingRequiredProperties() { + _testJSONDeserializingASingleObjectMissingRequiredProperties() + } + + func testJSONDeserializingMultipleObjectsMissingRequiredProperties() { + _testJSONDeserializingMultipleObjectsMissingRequiredProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testJSONTwoWaySerializationForSingleItems() { + _testJSONTwoWaySerializationForSingleItems() + } + + func testJSONTwoWaySerializationForMultipleItems() { + _testJSONTwoWaySerializationForMultipleItems() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testUpdateRequestJSON() { + _testUpdateRequestJSON() + } + +} + +// MARK: - Data + +extension SuiteTests { + + struct Properties { + + struct Required { + static let id = 27 + static let isBaseline = true + static let isCompleted = true + static let isMaster = true + static let name = "Name" + static let projectId = 3 + static let url = URL(string: "https://www.testrail.com/")! + } + + struct Optional { + static let completedOn = Date(secondsSince1970: 72973833) + static let description = "Description" + } + + } + +} + +extension SuiteTests: JSONDataProvider { + + static var requiredJSON: JSONDictionary { + return [Object.JSONKeys.id.rawValue: Properties.Required.id, + Object.JSONKeys.isBaseline.rawValue: Properties.Required.isBaseline, + Object.JSONKeys.isCompleted.rawValue: Properties.Required.isCompleted, + Object.JSONKeys.isMaster.rawValue: Properties.Required.isMaster, + Object.JSONKeys.name.rawValue: Properties.Required.name, + Object.JSONKeys.projectId.rawValue: Properties.Required.projectId, + Object.JSONKeys.url.rawValue: Properties.Required.url.absoluteString] + } + + static var optionalJSON: JSONDictionary { + return [Object.JSONKeys.completedOn.rawValue: Properties.Optional.completedOn.secondsSince1970, + Object.JSONKeys.description.rawValue: Properties.Optional.description] + } + +} + +// MARK: - Objects + +extension SuiteTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(completedOn: nil, + description: nil, + id: Properties.Required.id, + isBaseline: Properties.Required.isBaseline, + isCompleted: Properties.Required.isCompleted, + isMaster: Properties.Required.isMaster, + name: Properties.Required.name, + projectId: Properties.Required.projectId, + url: Properties.Required.url) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(completedOn: Properties.Optional.completedOn, + description: Properties.Optional.description, + id: Properties.Required.id, + isBaseline: Properties.Required.isBaseline, + isCompleted: Properties.Required.isCompleted, + isMaster: Properties.Required.isMaster, + name: Properties.Required.name, + projectId: Properties.Required.projectId, + url: Properties.Required.url) + } + +} + +// MARK: - Assertions + +extension SuiteTests: AssertEquatable { } + +extension SuiteTests: AssertJSONDeserializing { } + +extension SuiteTests: AssertJSONSerializing { } + +extension SuiteTests: AssertJSONTwoWaySerialization { } + +extension SuiteTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.id, Properties.Required.id) + XCTAssertEqual(object.isBaseline, Properties.Required.isBaseline) + XCTAssertEqual(object.isCompleted, Properties.Required.isCompleted) + XCTAssertEqual(object.isMaster, Properties.Required.isMaster) + XCTAssertEqual(object.name, Properties.Required.name) + XCTAssertEqual(object.projectId, Properties.Required.projectId) + XCTAssertEqual(object.url, Properties.Required.url) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { + if areNil { + XCTAssertNil(object.completedOn) + XCTAssertNil(object.description) + } else { + XCTAssertNotNil(object.completedOn) + XCTAssertNotNil(object.description) + XCTAssertEqual(object.completedOn, Properties.Optional.completedOn) + XCTAssertEqual(object.description, Properties.Optional.description) + } + } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { + + object.description = "New Description" + object.name = "New Name" + + XCTAssertNotEqual(object.description, Properties.Optional.description) + XCTAssertNotEqual(object.name, Properties.Required.name) + + XCTAssertEqual(object.description, "New Description") + XCTAssertEqual(object.name, "New Name") + } + +} + +extension SuiteTests: AssertUpdateRequestJSON { } diff --git a/QuizTrainTests/Models/TemplateTests.swift b/QuizTrainTests/Models/TemplateTests.swift new file mode 100644 index 0000000..426e895 --- /dev/null +++ b/QuizTrainTests/Models/TemplateTests.swift @@ -0,0 +1,146 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class TemplateTests: XCTestCase, ModelTests { + + typealias Object = Template + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONDeserializing() { + _testJSONDeserializing() + } + + func testJSONDeserializingWithOptionalProperties() { + _testJSONDeserializingWithOptionalProperties() + } + + func testJSONDeserializingASingleObject() { + _testJSONDeserializingASingleObject() + } + + func testJSONDeserializingMultipleObjects() { + _testJSONDeserializingMultipleObjects() + } + + func testJSONDeserializingASingleObjectMissingRequiredProperties() { + _testJSONDeserializingASingleObjectMissingRequiredProperties() + } + + func testJSONDeserializingMultipleObjectsMissingRequiredProperties() { + _testJSONDeserializingMultipleObjectsMissingRequiredProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testJSONTwoWaySerializationForSingleItems() { + _testJSONTwoWaySerializationForSingleItems() + } + + func testJSONTwoWaySerializationForMultipleItems() { + _testJSONTwoWaySerializationForMultipleItems() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testUpdateRequestJSON() { + _testUpdateRequestJSON() + } + +} + +// MARK: - Data + +extension TemplateTests { + + struct Properties { + + struct Required { + static let isDefault = true + static let id = 4 + static let name = "Name" + } + + struct Optional { + // none + } + + } + +} + +extension TemplateTests: JSONDataProvider { + + static var requiredJSON: JSONDictionary { + return [Object.JSONKeys.isDefault.rawValue: Properties.Required.isDefault, + Object.JSONKeys.id.rawValue: Properties.Required.id, + Object.JSONKeys.name.rawValue: Properties.Required.name] + } + + static var optionalJSON: JSONDictionary { + return [:] // none + } + +} + +// MARK: - Objects + +extension TemplateTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(isDefault: Properties.Required.isDefault, + id: Properties.Required.id, + name: Properties.Required.name) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(isDefault: Properties.Required.isDefault, + id: Properties.Required.id, + name: Properties.Required.name) + } + +} + +// MARK: - Assertions + +extension TemplateTests: AssertEquatable { } + +extension TemplateTests: AssertJSONDeserializing { } + +extension TemplateTests: AssertJSONSerializing { } + +extension TemplateTests: AssertJSONTwoWaySerialization { } + +extension TemplateTests: AssertProperties { + + func assertRequiredProperties(in template: Object) { + XCTAssertEqual(template.isDefault, Properties.Required.isDefault) + XCTAssertEqual(template.id, Properties.Required.id) + XCTAssertEqual(template.name, Properties.Required.name) + } + + func assertOptionalProperties(in template: Object, areNil: Bool) { /* none */ } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { /* none */ } + +} diff --git a/QuizTrainTests/Models/TestTests.swift b/QuizTrainTests/Models/TestTests.swift new file mode 100644 index 0000000..f9f8d40 --- /dev/null +++ b/QuizTrainTests/Models/TestTests.swift @@ -0,0 +1,215 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class TestTests: XCTestCase, ModelTests { + + typealias Object = Test + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONDeserializing() { + _testJSONDeserializing() + } + + func testJSONDeserializingWithOptionalProperties() { + _testJSONDeserializingWithOptionalProperties() + } + + func testJSONDeserializingASingleObject() { + _testJSONDeserializingASingleObject() + } + + func testJSONDeserializingMultipleObjects() { + _testJSONDeserializingMultipleObjects() + } + + func testJSONDeserializingASingleObjectMissingRequiredProperties() { + _testJSONDeserializingASingleObjectMissingRequiredProperties() + } + + func testJSONDeserializingMultipleObjectsMissingRequiredProperties() { + _testJSONDeserializingMultipleObjectsMissingRequiredProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testJSONTwoWaySerializationForSingleItems() { + _testJSONTwoWaySerializationForSingleItems() + } + + func testJSONTwoWaySerializationForMultipleItems() { + _testJSONTwoWaySerializationForMultipleItems() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testUpdateRequestJSON() { + _testUpdateRequestJSON() + } + +} + +// MARK: - Data + +extension TestTests { + + struct Properties { + + struct Required { + static let caseId = 3405 + static let id = 4 + static let priorityId = 23 + static let runId = 1 + static let statusId = 93 + static let templateId = 8834 + static let title = "Title" + static let typeId = 738 + } + + struct Optional { + static let assignedtoId = 345 + static let estimate = "1hr 2min" + static let estimateForecast = "2hr" + static let milestoneId = 513 + static let refs = "1,2,3" + } + + } + +} + +extension TestTests: CustomFieldsDataProvider { } + +extension TestTests: JSONDataProvider { + + static var requiredJSON: JSONDictionary { + return [Object.JSONKeys.caseId.rawValue: Properties.Required.caseId, + Object.JSONKeys.id.rawValue: Properties.Required.id, + Object.JSONKeys.priorityId.rawValue: Properties.Required.priorityId, + Object.JSONKeys.runId.rawValue: Properties.Required.runId, + Object.JSONKeys.statusId.rawValue: Properties.Required.statusId, + Object.JSONKeys.templateId.rawValue: Properties.Required.templateId, + Object.JSONKeys.title.rawValue: Properties.Required.title, + Object.JSONKeys.typeId.rawValue: Properties.Required.typeId] + } + + static var optionalJSON: JSONDictionary { + return [Object.JSONKeys.assignedtoId.rawValue: Properties.Optional.assignedtoId, + Object.JSONKeys.estimate.rawValue: Properties.Optional.estimate, + Object.JSONKeys.estimateForecast.rawValue: Properties.Optional.estimateForecast, + Object.JSONKeys.milestoneId.rawValue: Properties.Optional.milestoneId, + Object.JSONKeys.refs.rawValue: Properties.Optional.refs] + } + +} + +// MARK: - Objects + +extension TestTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(assignedtoId: nil, + caseId: Properties.Required.caseId, + estimate: nil, + estimateForecast: nil, + id: Properties.Required.id, + milestoneId: nil, + priorityId: Properties.Required.priorityId, + refs: nil, + runId: Properties.Required.runId, + statusId: Properties.Required.statusId, + templateId: Properties.Required.templateId, + title: Properties.Required.title, + typeId: Properties.Required.typeId, + customFieldsContainer: emptyCustomFieldsContainer) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(assignedtoId: Properties.Optional.assignedtoId, + caseId: Properties.Required.caseId, + estimate: Properties.Optional.estimate, + estimateForecast: Properties.Optional.estimateForecast, + id: Properties.Required.id, + milestoneId: Properties.Optional.milestoneId, + priorityId: Properties.Required.priorityId, + refs: Properties.Optional.refs, + runId: Properties.Required.runId, + statusId: Properties.Required.statusId, + templateId: Properties.Required.templateId, + title: Properties.Required.title, + typeId: Properties.Required.typeId, + customFieldsContainer: customFieldsContainer) + } + +} + +// MARK: - Assertions + +extension TestTests: AssertCustomFields { } + +extension TestTests: AssertEquatable { } + +extension TestTests: AssertJSONDeserializing { } + +extension TestTests: AssertJSONSerializing { } + +extension TestTests: AssertJSONTwoWaySerialization { } + +extension TestTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.caseId, Properties.Required.caseId) + XCTAssertEqual(object.id, Properties.Required.id) + XCTAssertEqual(object.priorityId, Properties.Required.priorityId) + XCTAssertEqual(object.runId, Properties.Required.runId) + XCTAssertEqual(object.statusId, Properties.Required.statusId) + XCTAssertEqual(object.templateId, Properties.Required.templateId) + XCTAssertEqual(object.title, Properties.Required.title) + XCTAssertEqual(object.typeId, Properties.Required.typeId) + } + + func assertOptionalProperties(in object: Test, areNil: Bool) { + if areNil { + XCTAssertNil(object.assignedtoId) + XCTAssertNil(object.estimate) + XCTAssertNil(object.estimateForecast) + XCTAssertNil(object.milestoneId) + XCTAssertNil(object.refs) + } else { + XCTAssertNotNil(object.assignedtoId) + XCTAssertNotNil(object.estimate) + XCTAssertNotNil(object.estimateForecast) + XCTAssertNotNil(object.milestoneId) + XCTAssertNotNil(object.refs) + XCTAssertNotNil(object.typeId) + XCTAssertEqual(object.assignedtoId, Properties.Optional.assignedtoId) + XCTAssertEqual(object.estimate, Properties.Optional.estimate) + XCTAssertEqual(object.estimateForecast, Properties.Optional.estimateForecast) + XCTAssertEqual(object.milestoneId, Properties.Optional.milestoneId) + XCTAssertEqual(object.refs, Properties.Optional.refs) + } + } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { /* none */ } + +} diff --git a/QuizTrainTests/Models/Testing Protocols/ModelTests.swift b/QuizTrainTests/Models/Testing Protocols/ModelTests.swift new file mode 100644 index 0000000..2f7a41e --- /dev/null +++ b/QuizTrainTests/Models/Testing Protocols/ModelTests.swift @@ -0,0 +1 @@ +protocol ModelTests: EquatableTests, InitTests, JSONDeserializingTests, JSONSerializingTests, JSONTwoWaySerializationTests, UpdateRequestJSONTests, VariablePropertyTests { } diff --git a/QuizTrainTests/Models/Types/CustomFieldTypeTests.swift b/QuizTrainTests/Models/Types/CustomFieldTypeTests.swift new file mode 100644 index 0000000..3f3043b --- /dev/null +++ b/QuizTrainTests/Models/Types/CustomFieldTypeTests.swift @@ -0,0 +1,48 @@ +import XCTest +@testable import QuizTrain + +class CustomFieldTypeTests: XCTestCase { + + func testRawValueInit() { + for i in -1000...0 { + XCTAssertNil(CustomFieldType(rawValue: i)) + } + for i in 1...12 { + XCTAssertNotNil(CustomFieldType(rawValue: i)) + } + for i in 13...1000 { + XCTAssertNil(CustomFieldType(rawValue: i)) + } + } + + func testCaseRawValues() { + XCTAssertEqual(CustomFieldType.string.rawValue, 1) + XCTAssertEqual(CustomFieldType.integer.rawValue, 2) + XCTAssertEqual(CustomFieldType.text.rawValue, 3) + XCTAssertEqual(CustomFieldType.url.rawValue, 4) + XCTAssertEqual(CustomFieldType.checkbox.rawValue, 5) + XCTAssertEqual(CustomFieldType.dropdown.rawValue, 6) + XCTAssertEqual(CustomFieldType.user.rawValue, 7) + XCTAssertEqual(CustomFieldType.date.rawValue, 8) + XCTAssertEqual(CustomFieldType.milestone.rawValue, 9) + XCTAssertEqual(CustomFieldType.steps.rawValue, 10) + XCTAssertEqual(CustomFieldType.stepResults.rawValue, 11) + XCTAssertEqual(CustomFieldType.multiSelect.rawValue, 12) + } + + func testDescription() { + XCTAssertEqual(CustomFieldType.string.description(), "String") + XCTAssertEqual(CustomFieldType.integer.description(), "Integer") + XCTAssertEqual(CustomFieldType.text.description(), "Text") + XCTAssertEqual(CustomFieldType.url.description(), "URL") + XCTAssertEqual(CustomFieldType.checkbox.description(), "Checkbox") + XCTAssertEqual(CustomFieldType.dropdown.description(), "Dropdown") + XCTAssertEqual(CustomFieldType.user.description(), "User") + XCTAssertEqual(CustomFieldType.date.description(), "Date") + XCTAssertEqual(CustomFieldType.milestone.description(), "Milestone") + XCTAssertEqual(CustomFieldType.steps.description(), "Steps") + XCTAssertEqual(CustomFieldType.stepResults.description(), "Step Results") + XCTAssertEqual(CustomFieldType.multiSelect.description(), "Multi-Select") + } + +} diff --git a/QuizTrainTests/Models/Types/Project.SuiteModeTests.swift b/QuizTrainTests/Models/Types/Project.SuiteModeTests.swift new file mode 100644 index 0000000..0b5f2ca --- /dev/null +++ b/QuizTrainTests/Models/Types/Project.SuiteModeTests.swift @@ -0,0 +1,30 @@ +import XCTest +@testable import QuizTrain + +class Project_SuiteModeTests: XCTestCase { + + func testRawValueInit() { + for i in -1000...0 { + XCTAssertNil(Project.SuiteMode(rawValue: i)) + } + for i in 1...3 { + XCTAssertNotNil(Project.SuiteMode(rawValue: i)) + } + for i in 4...1000 { + XCTAssertNil(Project.SuiteMode(rawValue: i)) + } + } + + func testCaseRawValues() { + XCTAssertEqual(Project.SuiteMode.singleSuite.rawValue, 1) + XCTAssertEqual(Project.SuiteMode.singleSuitePlusBaselines.rawValue, 2) + XCTAssertEqual(Project.SuiteMode.multipleSuites.rawValue, 3) + } + + func testDescription() { + XCTAssertEqual(Project.SuiteMode.singleSuite.description(), "Single Suite") + XCTAssertEqual(Project.SuiteMode.singleSuitePlusBaselines.description(), "Single Suite Plus Baselines") + XCTAssertEqual(Project.SuiteMode.multipleSuites.description(), "Multiple Suites") + } + +} diff --git a/QuizTrainTests/Models/Types/UniqueSelectionTests.swift b/QuizTrainTests/Models/Types/UniqueSelectionTests.swift new file mode 100644 index 0000000..ecacac7 --- /dev/null +++ b/QuizTrainTests/Models/Types/UniqueSelectionTests.swift @@ -0,0 +1,34 @@ +import XCTest +@testable import QuizTrain + +class UniqueSelectionTests: XCTestCase { + + func testEquatable() { + + let a: UniqueSelection = UniqueSelection.all + let b: UniqueSelection = UniqueSelection.none + let c = UniqueSelection.some([1, 2, 3]) + let d = UniqueSelection.some([1, 2, 3, 4]) + + XCTAssertEqual(a, UniqueSelection.all) + XCTAssertEqual(b, UniqueSelection.none) + XCTAssertEqual(c, UniqueSelection.some([1, 2, 3])) + + XCTAssertNotEqual(a, b) + XCTAssertNotEqual(a, c) + XCTAssertNotEqual(a, d) + + XCTAssertNotEqual(b, a) + XCTAssertNotEqual(b, c) + XCTAssertNotEqual(b, d) + + XCTAssertNotEqual(c, a) + XCTAssertNotEqual(c, b) + XCTAssertNotEqual(c, d) + + XCTAssertNotEqual(d, a) + XCTAssertNotEqual(d, b) + XCTAssertNotEqual(d, c) + } + +} diff --git a/QuizTrainTests/Models/UserTests.swift b/QuizTrainTests/Models/UserTests.swift new file mode 100644 index 0000000..0725b89 --- /dev/null +++ b/QuizTrainTests/Models/UserTests.swift @@ -0,0 +1,149 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class UserTests: XCTestCase, ModelTests { + + typealias Object = User + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONDeserializing() { + _testJSONDeserializing() + } + + func testJSONDeserializingWithOptionalProperties() { + _testJSONDeserializingWithOptionalProperties() + } + + func testJSONDeserializingASingleObject() { + _testJSONDeserializingASingleObject() + } + + func testJSONDeserializingMultipleObjects() { + _testJSONDeserializingMultipleObjects() + } + + func testJSONDeserializingASingleObjectMissingRequiredProperties() { + _testJSONDeserializingASingleObjectMissingRequiredProperties() + } + + func testJSONDeserializingMultipleObjectsMissingRequiredProperties() { + _testJSONDeserializingMultipleObjectsMissingRequiredProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testJSONTwoWaySerializationForSingleItems() { + _testJSONTwoWaySerializationForSingleItems() + } + + func testJSONTwoWaySerializationForMultipleItems() { + _testJSONTwoWaySerializationForMultipleItems() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testUpdateRequestJSON() { + _testUpdateRequestJSON() + } + +} + +// MARK: - Data + +extension UserTests { + + struct Properties { + + struct Required { + static let email = "hello@email.com" + static let id = 108 + static let isActive = true + static let name = "Name" + } + + struct Optional { /* none */ } + + } + +} + +extension UserTests: JSONDataProvider { + + static var requiredJSON: JSONDictionary { + return [Object.JSONKeys.email.rawValue: Properties.Required.email, + Object.JSONKeys.id.rawValue: Properties.Required.id, + Object.JSONKeys.isActive.rawValue: Properties.Required.isActive, + Object.JSONKeys.name.rawValue: Properties.Required.name] + } + + static var optionalJSON: JSONDictionary { + return [:] // none + } + +} + +// MARK: - Objects + +extension UserTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(email: Properties.Required.email, + id: Properties.Required.id, + isActive: Properties.Required.isActive, + name: Properties.Required.name) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(email: Properties.Required.email, + id: Properties.Required.id, + isActive: Properties.Required.isActive, + name: Properties.Required.name) + } + +} + +// MARK: - Assertions + +extension UserTests: AssertEquatable { } + +extension UserTests: AssertJSONDeserializing { } + +extension UserTests: AssertJSONSerializing { } + +extension UserTests: AssertJSONTwoWaySerialization { } + +extension UserTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.email, Properties.Required.email) + XCTAssertEqual(object.id, Properties.Required.id) + XCTAssertEqual(object.isActive, Properties.Required.isActive) + XCTAssertEqual(object.name, Properties.Required.name) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { /* none */ } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { /* none */ } + +} diff --git a/QuizTrainTests/Network/Filters/FilterTests.swift b/QuizTrainTests/Network/Filters/FilterTests.swift new file mode 100644 index 0000000..8dfd67f --- /dev/null +++ b/QuizTrainTests/Network/Filters/FilterTests.swift @@ -0,0 +1,168 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class FilterTests: XCTestCase { + + func testInitWithBool() { + let filter = Filter(named: "Bool", matching: true) + XCTAssertEqual(filter.name, "Bool") + XCTAssertEqual(filter.value, .bool(true)) + } + + func testInitWithDate() { + let date = Date() + let filter = Filter(named: "Date", matching: date) + XCTAssertEqual(filter.name, "Date") + XCTAssertEqual(filter.value, .timestamp(date)) + } + + func testInitWithInt() { + let filter = Filter(named: "Int", matching: 100) + XCTAssertEqual(filter.name, "Int") + XCTAssertEqual(filter.value, .int(100)) + } + + func testInitWithIntList() { + let filter = Filter(named: "IntList", matching: [1, 2, 3]) + XCTAssertEqual(filter.name, "IntList") + XCTAssertEqual(filter.value, .intList([1, 2, 3])) + } + + func testEquatable() { + + let objectA = Filter(named: "Hello", matching: true) + let objectB = Filter(named: "Hello", matching: 1) + let objectC = Filter(named: "Hello", matching: [100, 200, 300]) + let objectD = Filter(named: "Hello", matching: Date()) + + XCTAssertNotEqual(objectA, objectB) + XCTAssertNotEqual(objectA, objectC) + XCTAssertNotEqual(objectA, objectD) + + XCTAssertNotEqual(objectB, objectA) + XCTAssertNotEqual(objectB, objectC) + XCTAssertNotEqual(objectB, objectD) + + XCTAssertNotEqual(objectC, objectA) + XCTAssertNotEqual(objectC, objectB) + XCTAssertNotEqual(objectC, objectD) + + XCTAssertNotEqual(objectD, objectA) + XCTAssertNotEqual(objectD, objectB) + XCTAssertNotEqual(objectD, objectC) + } + + func testEquatableBool() { + + let objectA = Filter(named: "Hello", matching: true) + var objectB = Filter(named: "Hello", matching: true) + let objectC = Filter(named: "Hello", matching: false) + + XCTAssertEqual(objectA, objectA) + XCTAssertEqual(objectA, objectB) + XCTAssertNotEqual(objectA, objectC) + + objectB.value = .bool(false) + + XCTAssertNotEqual(objectA, objectB) + XCTAssertEqual(objectB, objectC) + + objectB.name = "Goodbye" + + XCTAssertNotEqual(objectA, objectC) + } + + func testEquatableInt() { + + let objectA = Filter(named: "Hello", matching: 1) + var objectB = Filter(named: "Hello", matching: 1) + let objectC = Filter(named: "Hello", matching: 2) + + XCTAssertEqual(objectA, objectA) + XCTAssertEqual(objectA, objectB) + XCTAssertNotEqual(objectA, objectC) + + objectB.value = .int(2) + + XCTAssertNotEqual(objectA, objectB) + XCTAssertEqual(objectB, objectC) + + objectB.name = "Goodbye" + + XCTAssertNotEqual(objectA, objectC) + } + + func testEquatableIntList() { + + let objectA = Filter(named: "Hello", matching: [100, 200, 300]) + var objectB = Filter(named: "Hello", matching: [100, 200, 300]) + let objectC = Filter(named: "Hello", matching: [100, 200]) + + XCTAssertEqual(objectA, objectA) + XCTAssertEqual(objectA, objectB) + XCTAssertNotEqual(objectA, objectC) + + objectB.value = .intList([100, 200]) + + XCTAssertNotEqual(objectA, objectB) + XCTAssertEqual(objectB, objectC) + + objectB.name = "Goodbye" + + XCTAssertNotEqual(objectA, objectC) + } + + func testEquatableTimestamp() { + + let dateA = Date() + let dateB = Date(timeInterval: 100, since: dateA) + + let objectA = Filter(named: "Hello", matching: dateA) + var objectB = Filter(named: "Hello", matching: dateA) + let objectC = Filter(named: "Hello", matching: dateB) + + XCTAssertEqual(objectA, objectA) + XCTAssertEqual(objectA, objectB) + XCTAssertNotEqual(objectA, objectC) + + objectB.value = .timestamp(dateB) + + XCTAssertNotEqual(objectA, objectB) + XCTAssertEqual(objectB, objectC) + + objectB.name = "Goodbye" + + XCTAssertNotEqual(objectA, objectC) + } + + func testQueryItemProvider() { + + // Bool + + var filter = Filter(named: "Bool", matching: true) + var queryItem = URLQueryItem(name: "Bool", value: "1") + XCTAssertEqual(filter.queryItem, queryItem) + + // Int + + filter = Filter(named: "Int", matching: 1) + queryItem = URLQueryItem(name: "Int", value: "1") + XCTAssertEqual(filter.queryItem, queryItem) + + // Int List + + filter = Filter(named: "IntList", matching: [1, 2, 3]) + queryItem = URLQueryItem(name: "IntList", value: "1,2,3") + XCTAssertEqual(filter.queryItem, queryItem) + + // Timestamp + + let date = Date() + filter = Filter(named: "Timestamp", matching: date) + queryItem = URLQueryItem(name: "Timestamp", value: String(date.secondsSince1970)) + XCTAssertEqual(filter.queryItem, queryItem) + } + +} diff --git a/QuizTrainTests/Network/Models/Add/NewCaseResults.ResultTests.swift b/QuizTrainTests/Network/Models/Add/NewCaseResults.ResultTests.swift new file mode 100644 index 0000000..9dd7811 --- /dev/null +++ b/QuizTrainTests/Network/Models/Add/NewCaseResults.ResultTests.swift @@ -0,0 +1,220 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class NewCaseResults_ResultTests: XCTestCase, AddModelTests, ValidatableTests { + + typealias Object = NewCaseResults.Result + + func testAddRequestJSON() { + _testAddRequestJSON() + } + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testIsValid() { + _testIsValid() + } + + func testIsInvalid() { + _testIsInvalid() + } + +} + +// MARK: - Data + +extension NewCaseResults_ResultTests { + + struct Properties { + + struct Required { + static let caseId = 10 + } + + struct Optional { + static let assignedtoId = 11 + static let comment = "Comment" + static let defects = "Defects" + static let elapsed = "4hr, 31min" + static let statusId = 12 + static let version = "1.2.3" + static let customFields = NewCaseResults_ResultTests.customFields + } + + } + +} + +extension NewCaseResults_ResultTests: CustomFieldsDataProvider { } + +// MARK: - Objects + +extension NewCaseResults_ResultTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(assignedtoId: nil, + caseId: Properties.Required.caseId, + comment: nil, + defects: nil, + elapsed: nil, + statusId: nil, + version: nil, + customFields: nil) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(assignedtoId: Properties.Optional.assignedtoId, + caseId: Properties.Required.caseId, + comment: Properties.Optional.comment, + defects: Properties.Optional.defects, + elapsed: Properties.Optional.elapsed, + statusId: Properties.Optional.statusId, + version: Properties.Optional.version, + customFields: Properties.Optional.customFields) + } + +} + +extension NewCaseResults_ResultTests: ValidatableObjectProvider { + + var validObject: Validatable { + return objectWithRequiredAndOptionalProperties + } + + var invalidObject: Validatable { + var object = objectWithRequiredAndOptionalProperties + object.assignedtoId = nil + object.comment = nil + object.statusId = nil + return object + } + +} + +// MARK: - Assertions + +extension NewCaseResults_ResultTests: AssertAddRequestJSON { } + +extension NewCaseResults_ResultTests: AssertCustomFields { } + +extension NewCaseResults_ResultTests: AssertEquatable { } + +extension NewCaseResults_ResultTests: AssertJSONSerializing { } + +extension NewCaseResults_ResultTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.caseId, Properties.Required.caseId) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { + if areNil { + XCTAssertNil(object.assignedtoId) + XCTAssertNil(object.comment) + XCTAssertNil(object.defects) + XCTAssertNil(object.elapsed) + XCTAssertNil(object.statusId) + XCTAssertNil(object.version) + XCTAssertTrue(object.customFields.isEmpty) + } else { + XCTAssertNotNil(object.assignedtoId) + XCTAssertNotNil(object.comment) + XCTAssertNotNil(object.defects) + XCTAssertNotNil(object.elapsed) + XCTAssertNotNil(object.statusId) + XCTAssertNotNil(object.version) + XCTAssertNotNil(object.customFields) + XCTAssertEqual(object.assignedtoId, Properties.Optional.assignedtoId) + XCTAssertEqual(object.comment, Properties.Optional.comment) + XCTAssertEqual(object.defects, Properties.Optional.defects) + XCTAssertEqual(object.elapsed, Properties.Optional.elapsed) + XCTAssertEqual(object.statusId, Properties.Optional.statusId) + XCTAssertEqual(object.version, Properties.Optional.version) + XCTAssertEqual(object.customFields.count, Properties.Optional.customFields.count) + } + } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { + + // Properties + + object.assignedtoId = 1000 + object.caseId = 1001 + object.comment = "New Comment" + object.defects = "New Defects" + object.elapsed = "99hr, 99min" + object.version = "4.5.6" + object.statusId = 1002 + object.customFields = NewCaseResults_ResultTests.emptyCustomFields + + XCTAssertNotEqual(object.assignedtoId, Properties.Optional.assignedtoId) + XCTAssertNotEqual(object.caseId, Properties.Required.caseId) + XCTAssertNotEqual(object.comment, Properties.Optional.comment) + XCTAssertNotEqual(object.defects, Properties.Optional.defects) + XCTAssertNotEqual(object.elapsed, Properties.Optional.elapsed) + XCTAssertNotEqual(object.version, Properties.Optional.version) + XCTAssertNotEqual(object.statusId, Properties.Optional.statusId) + XCTAssertNotEqual(object.customFields.count, Properties.Optional.customFields.count) + + XCTAssertEqual(object.assignedtoId, 1000) + XCTAssertEqual(object.caseId, 1001) + XCTAssertEqual(object.comment, "New Comment") + XCTAssertEqual(object.defects, "New Defects") + XCTAssertEqual(object.elapsed, "99hr, 99min") + XCTAssertEqual(object.version, "4.5.6") + XCTAssertEqual(object.statusId, 1002) + XCTAssertEqual(object.customFields.count, NewCaseResults_ResultTests.emptyCustomFields.count) + + // Custom Fields + + let customFieldsCount = object.customFields.count + + object.customFields["custom_field_test01"] = "Custom Field Test 01" + object.customFields["custom_field_test02"] = 9000 + object.customFields["custom_field_test03"] = -8.0 + object.customFields["invalid_custom_field_test04"] = "This should not be added." + + XCTAssertNotNil(object.customFields["custom_field_test01"]) + XCTAssertNotNil(object.customFields["custom_field_test02"]) + XCTAssertNotNil(object.customFields["custom_field_test03"]) + XCTAssertNil(object.customFields["invalid_custom_field_test04"]) + + XCTAssertEqual(object.customFields["custom_field_test01"] as! String, "Custom Field Test 01") + XCTAssertEqual(object.customFields["custom_field_test02"] as! Int, 9000) + XCTAssertEqual(object.customFields["custom_field_test03"] as! Double, -8.0) + + XCTAssertEqual(object.customFields.count, customFieldsCount + 3) + + object.customFields.removeValue(forKey: "custom_field_test01") + + XCTAssertNil(object.customFields["custom_field_test01"]) + XCTAssertEqual(object.customFields.count, customFieldsCount + 2) + } + +} + +extension NewCaseResults_ResultTests: AssertValidatable { } diff --git a/QuizTrainTests/Network/Models/Add/NewCaseResultsTests.swift b/QuizTrainTests/Network/Models/Add/NewCaseResultsTests.swift new file mode 100644 index 0000000..51c413d --- /dev/null +++ b/QuizTrainTests/Network/Models/Add/NewCaseResultsTests.swift @@ -0,0 +1,117 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class NewCaseResultsTests: XCTestCase, AddModelTests, ValidatableTests { + + typealias Object = NewCaseResults + + func testAddRequestJSON() { + _testAddRequestJSON() + } + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testIsValid() { + _testIsValid() + } + + func testIsInvalid() { + _testIsInvalid() + } + +} + +// MARK: - Data + +extension NewCaseResultsTests { + + struct Properties { + + struct Required { + static let results = [NewCaseResults_ResultTests.objectWithRequiredAndOptionalProperties, NewCaseResults_ResultTests.objectWithRequiredAndOptionalProperties, NewCaseResults_ResultTests.objectWithRequiredAndOptionalProperties] + } + + struct Optional { /* none */ } + + } + +} + +extension NewCaseResultsTests: CustomFieldsDataProvider { } + +// MARK: - Objects + +extension NewCaseResultsTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(results: Properties.Required.results) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(results: Properties.Required.results) + } + +} + +extension NewCaseResultsTests: ValidatableObjectProvider { + + var validObject: Validatable { + return objectWithRequiredAndOptionalProperties + } + + var invalidObject: Validatable { + return Object(results: [NewCaseResults_ResultTests.objectWithRequiredProperties, NewCaseResults_ResultTests.objectWithRequiredProperties, NewCaseResults_ResultTests.objectWithRequiredProperties]) + } + +} + +// MARK: - Assertions + +extension NewCaseResultsTests: AssertAddRequestJSON { } + +extension NewCaseResultsTests: AssertEquatable { } + +extension NewCaseResultsTests: AssertJSONSerializing { } + +extension NewCaseResultsTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.results, Properties.Required.results) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { /* none */ } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { + object.results.append(NewCaseResults_ResultTests.objectWithRequiredProperties) + XCTAssertNotEqual(object.results, Properties.Required.results) + object.results.remove(at: object.results.count - 1) + XCTAssertEqual(object.results, Properties.Required.results) + } + +} + +extension NewCaseResultsTests: AssertValidatable { } diff --git a/QuizTrainTests/Network/Models/Add/NewCaseTests.swift b/QuizTrainTests/Network/Models/Add/NewCaseTests.swift new file mode 100644 index 0000000..a62630b --- /dev/null +++ b/QuizTrainTests/Network/Models/Add/NewCaseTests.swift @@ -0,0 +1,193 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class NewCaseTests: XCTestCase, AddModelTests { + + typealias Object = NewCase + + func testAddRequestJSON() { + _testAddRequestJSON() + } + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testVariableProperties() { + _testVariableProperties() + } + +} + +// MARK: - Data + +extension NewCaseTests { + + struct Properties { + + struct Required { + static let title = "Title" + } + + struct Optional { + static let estimate = "2hr, 3min" + static let milestoneId = 10 + static let priorityId = 11 + static let refs = "1,2,3" + static let templateId = 12 + static let typeId = 13 + static let customFields = NewCaseTests.customFields + } + + } + +} + +extension NewCaseTests: CustomFieldsDataProvider { } + +// MARK: - Objects + +extension NewCaseTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(estimate: nil, + milestoneId: nil, + priorityId: nil, + refs: nil, + templateId: nil, + title: Properties.Required.title, + typeId: nil, + customFields: nil) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(estimate: Properties.Optional.estimate, + milestoneId: Properties.Optional.milestoneId, + priorityId: Properties.Optional.priorityId, + refs: Properties.Optional.refs, + templateId: Properties.Optional.templateId, + title: Properties.Required.title, + typeId: Properties.Optional.typeId, + customFields: Properties.Optional.customFields) + } + +} + +// MARK: - Assertions + +extension NewCaseTests: AssertAddRequestJSON { } + +extension NewCaseTests: AssertCustomFields { } + +extension NewCaseTests: AssertEquatable { } + +extension NewCaseTests: AssertJSONSerializing { } + +extension NewCaseTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.title, Properties.Required.title) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { + if areNil { + XCTAssertNil(object.estimate) + XCTAssertNil(object.milestoneId) + XCTAssertNil(object.priorityId) + XCTAssertNil(object.refs) + XCTAssertNil(object.templateId) + XCTAssertNil(object.typeId) + XCTAssertTrue(object.customFields.isEmpty) + } else { + XCTAssertNotNil(object.estimate) + XCTAssertNotNil(object.milestoneId) + XCTAssertNotNil(object.priorityId) + XCTAssertNotNil(object.refs) + XCTAssertNotNil(object.templateId) + XCTAssertNotNil(object.typeId) + XCTAssertNotNil(object.customFields) + XCTAssertEqual(object.estimate, Properties.Optional.estimate) + XCTAssertEqual(object.milestoneId, Properties.Optional.milestoneId) + XCTAssertEqual(object.priorityId, Properties.Optional.priorityId) + XCTAssertEqual(object.refs, Properties.Optional.refs) + XCTAssertEqual(object.templateId, Properties.Optional.templateId) + XCTAssertEqual(object.typeId, Properties.Optional.typeId) + XCTAssertEqual(object.customFields.count, Properties.Optional.customFields.count) + } + } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { + + // Properties + + object.estimate = "New Estimate" + object.milestoneId = 1000 + object.priorityId = 1001 + object.refs = "4,5,6" + object.title = "New Title" + object.templateId = 1002 + object.typeId = 1003 + object.customFields = NewCaseTests.emptyCustomFields + + XCTAssertNotEqual(object.estimate, Properties.Optional.estimate) + XCTAssertNotEqual(object.milestoneId, Properties.Optional.milestoneId) + XCTAssertNotEqual(object.priorityId, Properties.Optional.priorityId) + XCTAssertNotEqual(object.refs, Properties.Optional.refs) + XCTAssertNotEqual(object.title, Properties.Required.title) + XCTAssertNotEqual(object.templateId, Properties.Optional.templateId) + XCTAssertNotEqual(object.typeId, Properties.Optional.typeId) + XCTAssertNotEqual(object.customFields.count, Properties.Optional.customFields.count) + + XCTAssertEqual(object.estimate, "New Estimate") + XCTAssertEqual(object.milestoneId, 1000) + XCTAssertEqual(object.priorityId, 1001) + XCTAssertEqual(object.refs, "4,5,6") + XCTAssertEqual(object.title, "New Title") + XCTAssertEqual(object.templateId, 1002) + XCTAssertEqual(object.typeId, 1003) + + // Custom Fields + + let customFieldsCount = object.customFields.count + + object.customFields["custom_field_test01"] = "Custom Field Test 01" + object.customFields["custom_field_test02"] = 9000 + object.customFields["custom_field_test03"] = -8.0 + object.customFields["invalid_custom_field_test04"] = "This should not be added." + + XCTAssertNotNil(object.customFields["custom_field_test01"]) + XCTAssertNotNil(object.customFields["custom_field_test02"]) + XCTAssertNotNil(object.customFields["custom_field_test03"]) + XCTAssertNil(object.customFields["invalid_custom_field_test04"]) + + XCTAssertEqual(object.customFields["custom_field_test01"] as! String, "Custom Field Test 01") + XCTAssertEqual(object.customFields["custom_field_test02"] as! Int, 9000) + XCTAssertEqual(object.customFields["custom_field_test03"] as! Double, -8.0) + + XCTAssertEqual(object.customFields.count, customFieldsCount + 3) + + object.customFields.removeValue(forKey: "custom_field_test01") + + XCTAssertNil(object.customFields["custom_field_test01"]) + XCTAssertEqual(object.customFields.count, customFieldsCount + 2) + } + +} diff --git a/QuizTrainTests/Network/Models/Add/NewConfigurationGroupTests.swift b/QuizTrainTests/Network/Models/Add/NewConfigurationGroupTests.swift new file mode 100644 index 0000000..daab0e6 --- /dev/null +++ b/QuizTrainTests/Network/Models/Add/NewConfigurationGroupTests.swift @@ -0,0 +1,88 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class NewConfigurationGroupTests: XCTestCase, AddModelTests { + + typealias Object = NewConfigurationGroup + + func testAddRequestJSON() { + _testAddRequestJSON() + } + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testVariableProperties() { + _testVariableProperties() + } + +} + +// MARK: - Data + +extension NewConfigurationGroupTests { + + struct Properties { + + struct Required { + static let name = "Name" + } + + struct Optional { /* none */ } + + } + +} + +// MARK: - Objects + +extension NewConfigurationGroupTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(name: Properties.Required.name) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(name: Properties.Required.name) + } + +} + +// MARK: - Assertions + +extension NewConfigurationGroupTests: AssertAddRequestJSON { } + +extension NewConfigurationGroupTests: AssertEquatable { } + +extension NewConfigurationGroupTests: AssertJSONSerializing { } + +extension NewConfigurationGroupTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.name, Properties.Required.name) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { /* none */ } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { /* none */ } + +} diff --git a/QuizTrainTests/Network/Models/Add/NewConfigurationTests.swift b/QuizTrainTests/Network/Models/Add/NewConfigurationTests.swift new file mode 100644 index 0000000..ae696d2 --- /dev/null +++ b/QuizTrainTests/Network/Models/Add/NewConfigurationTests.swift @@ -0,0 +1,88 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class NewConfigurationTests: XCTestCase, AddModelTests { + + typealias Object = NewConfiguration + + func testAddRequestJSON() { + _testAddRequestJSON() + } + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testVariableProperties() { + _testVariableProperties() + } + +} + +// MARK: - Data + +extension NewConfigurationTests { + + struct Properties { + + struct Required { + static let name = "Name" + } + + struct Optional { /* none */ } + + } + +} + +// MARK: - Objects + +extension NewConfigurationTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(name: Properties.Required.name) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(name: Properties.Required.name) + } + +} + +// MARK: - Assertions + +extension NewConfigurationTests: AssertAddRequestJSON { } + +extension NewConfigurationTests: AssertEquatable { } + +extension NewConfigurationTests: AssertJSONSerializing { } + +extension NewConfigurationTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.name, Properties.Required.name) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { /* none */ } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { /* none */ } + +} diff --git a/QuizTrainTests/Network/Models/Add/NewMilestoneTests.swift b/QuizTrainTests/Network/Models/Add/NewMilestoneTests.swift new file mode 100644 index 0000000..c2f46b5 --- /dev/null +++ b/QuizTrainTests/Network/Models/Add/NewMilestoneTests.swift @@ -0,0 +1,136 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class NewMilestoneTests: XCTestCase, AddModelTests { + + typealias Object = NewMilestone + + func testAddRequestJSON() { + _testAddRequestJSON() + } + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testVariableProperties() { + _testVariableProperties() + } + +} + +// MARK: - Data + +extension NewMilestoneTests { + + struct Properties { + + struct Required { + static let name = "Name" + } + + struct Optional { + static let description = "Description" + static let dueOn = Date(secondsSince1970: 2000000) + static let parentId = 10 + static let startOn = Date(secondsSince1970: 1000000) + } + + } + +} + +// MARK: - Objects + +extension NewMilestoneTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(description: nil, + dueOn: nil, + name: Properties.Required.name, + parentId: nil, + startOn: nil) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(description: Properties.Optional.description, + dueOn: Properties.Optional.dueOn, + name: Properties.Required.name, + parentId: Properties.Optional.parentId, + startOn: Properties.Optional.startOn) + } + +} + +// MARK: - Assertions + +extension NewMilestoneTests: AssertAddRequestJSON { } + +extension NewMilestoneTests: AssertEquatable { } + +extension NewMilestoneTests: AssertJSONSerializing { } + +extension NewMilestoneTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.name, Properties.Required.name) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { + if areNil { + XCTAssertNil(object.description) + XCTAssertNil(object.dueOn) + XCTAssertNil(object.parentId) + XCTAssertNil(object.startOn) + } else { + XCTAssertNotNil(object.description) + XCTAssertNotNil(object.dueOn) + XCTAssertNotNil(object.parentId) + XCTAssertNotNil(object.startOn) + XCTAssertEqual(object.description, Properties.Optional.description) + XCTAssertEqual(object.dueOn, Properties.Optional.dueOn) + XCTAssertEqual(object.parentId, Properties.Optional.parentId) + XCTAssertEqual(object.startOn, Properties.Optional.startOn) + } + } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { + + object.description = "New Description" + object.dueOn = Date(secondsSince1970: 2999999) + object.name = "New Name" + object.parentId = 1000 + object.startOn = Date(secondsSince1970: 3999999) + + XCTAssertNotEqual(object.description, Properties.Optional.description) + XCTAssertNotEqual(object.dueOn, Properties.Optional.dueOn) + XCTAssertNotEqual(object.name, Properties.Required.name) + XCTAssertNotEqual(object.parentId, Properties.Optional.parentId) + XCTAssertNotEqual(object.startOn, Properties.Optional.startOn) + + XCTAssertEqual(object.description, "New Description") + XCTAssertEqual(object.dueOn, Date(secondsSince1970: 2999999)) + XCTAssertEqual(object.name, "New Name") + XCTAssertEqual(object.parentId, 1000) + XCTAssertEqual(object.startOn, Date(secondsSince1970: 3999999)) + } + +} diff --git a/QuizTrainTests/Network/Models/Add/NewPlan.Entry.RunTests.swift b/QuizTrainTests/Network/Models/Add/NewPlan.Entry.RunTests.swift new file mode 100644 index 0000000..de3b681 --- /dev/null +++ b/QuizTrainTests/Network/Models/Add/NewPlan.Entry.RunTests.swift @@ -0,0 +1,161 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class NewPlan_Entry_RunTests: XCTestCase, AddModelTests { + + typealias Object = NewPlan.Entry.Run + + func testAddRequestJSON() { + _testAddRequestJSON() + } + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testVariableProperties() { + _testVariableProperties() + } + +} + +// MARK: - Data + +extension NewPlan_Entry_RunTests { + + struct Properties { + + struct Required { /* none */ } + + struct Optional { + static let assignedtoId: Int = 1 + static let caseIds: [Int] = [2, 3, 4] + static let configIds: [Int] = [5, 6] + static let description: String = "Description" + static let includeAll: Bool = false + static let milestoneId: Int = 7 + static let name: String = "Name" + static let suiteId: Int = 8 + } + + } + +} + +// MARK: - Objects + +extension NewPlan_Entry_RunTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(assignedtoId: nil, + caseIds: nil, + configIds: nil, + description: nil, + includeAll: nil, + milestoneId: nil, + name: nil, + suiteId: nil) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(assignedtoId: Properties.Optional.assignedtoId, + caseIds: Properties.Optional.caseIds, + configIds: Properties.Optional.configIds, + description: Properties.Optional.description, + includeAll: Properties.Optional.includeAll, + milestoneId: Properties.Optional.milestoneId, + name: Properties.Optional.name, + suiteId: Properties.Optional.suiteId) + } + +} + +// MARK: - Assertions + +extension NewPlan_Entry_RunTests: AssertAddRequestJSON { } + +extension NewPlan_Entry_RunTests: AssertEquatable { } + +extension NewPlan_Entry_RunTests: AssertJSONSerializing { } + +extension NewPlan_Entry_RunTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { /* none */ } + + func assertOptionalProperties(in object: Object, areNil: Bool) { + if areNil { + XCTAssertNil(object.assignedtoId) + XCTAssertNil(object.caseIds) + XCTAssertNil(object.configIds) + XCTAssertNil(object.description) + XCTAssertNil(object.includeAll) + XCTAssertNil(object.milestoneId) + XCTAssertNil(object.name) + XCTAssertNil(object.suiteId) + } else { + XCTAssertNotNil(object.assignedtoId) + XCTAssertNotNil(object.caseIds) + XCTAssertNotNil(object.configIds) + XCTAssertNotNil(object.description) + XCTAssertNotNil(object.includeAll) + XCTAssertNotNil(object.milestoneId) + XCTAssertNotNil(object.name) + XCTAssertNotNil(object.suiteId) + XCTAssertEqual(object.assignedtoId, Properties.Optional.assignedtoId) + if let caseIds = object.caseIds { XCTAssertEqual(caseIds, Properties.Optional.caseIds) } + if let configIds = object.configIds { XCTAssertEqual(configIds, Properties.Optional.configIds) } + XCTAssertEqual(object.description, Properties.Optional.description) + XCTAssertEqual(object.includeAll, Properties.Optional.includeAll) + XCTAssertEqual(object.milestoneId, Properties.Optional.milestoneId) + XCTAssertEqual(object.name, Properties.Optional.name) + XCTAssertEqual(object.suiteId, Properties.Optional.suiteId) + } + } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { + + object.assignedtoId = 1000 + object.caseIds = [1001, 1002, 1003] + object.configIds = [1004, 1005] + object.description = "New Description" + object.includeAll = true + object.milestoneId = 1006 + object.name = "New Name" + object.suiteId = 1007 + + XCTAssertNotEqual(object.assignedtoId, Properties.Optional.assignedtoId) + if let caseIds = object.caseIds { XCTAssertNotEqual(caseIds, Properties.Optional.caseIds) } + if let configIds = object.configIds { XCTAssertNotEqual(configIds, Properties.Optional.configIds) } + XCTAssertNotEqual(object.description, Properties.Optional.description) + XCTAssertNotEqual(object.includeAll, Properties.Optional.includeAll) + XCTAssertNotEqual(object.name, Properties.Optional.name) + XCTAssertNotEqual(object.suiteId, Properties.Optional.suiteId) + + XCTAssertEqual(object.assignedtoId, 1000) + if let caseIds = object.caseIds { XCTAssertEqual(caseIds, [1001, 1002, 1003]) } + if let configIds = object.configIds { XCTAssertEqual(configIds, [1004, 1005]) } + XCTAssertEqual(object.description, "New Description") + XCTAssertEqual(object.includeAll, true) + XCTAssertEqual(object.name, "New Name") + XCTAssertEqual(object.suiteId, 1007) + } + +} diff --git a/QuizTrainTests/Network/Models/Add/NewPlan.EntryTests.swift b/QuizTrainTests/Network/Models/Add/NewPlan.EntryTests.swift new file mode 100644 index 0000000..da16924 --- /dev/null +++ b/QuizTrainTests/Network/Models/Add/NewPlan.EntryTests.swift @@ -0,0 +1,202 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class NewPlan_EntryTests: XCTestCase, AddModelTests { + + typealias Object = NewPlan.Entry + + func testAddRequestJSON() { + _testAddRequestJSON() + } + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testVariableProperties() { + _testVariableProperties() + } + +} + +// MARK: - Data + +extension NewPlan_EntryTests { + + struct Properties { + + struct Required { + static let suiteId = 10 + } + + struct Optional { + static let assignedtoId = 11 + static let caseIds = [12, 13, 14] + static let description = "Description" + static let includeAll = true + static let name = "Name" + static let runs = [NewPlan_Entry_RunTests.objectWithRequiredAndOptionalProperties, NewPlan_Entry_RunTests.objectWithRequiredAndOptionalProperties, NewPlan_Entry_RunTests.objectWithRequiredAndOptionalProperties] + } + + } + +} + +// MARK: - Objects + +extension NewPlan_EntryTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(assignedtoId: nil, + caseIds: nil, + description: nil, + includeAll: nil, + name: nil, + runs: nil, + suiteId: Properties.Required.suiteId) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(assignedtoId: Properties.Optional.assignedtoId, + caseIds: Properties.Optional.caseIds, + description: Properties.Optional.description, + includeAll: Properties.Optional.includeAll, + name: Properties.Optional.name, + runs: Properties.Optional.runs, + suiteId: Properties.Required.suiteId) + } + +} + +// MARK: - Assertions + +extension NewPlan_EntryTests: AssertAddRequestJSON { } + +extension NewPlan_EntryTests: AssertEquatable { } + +extension NewPlan_EntryTests: AssertJSONSerializing { } + +extension NewPlan_EntryTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.suiteId, Properties.Required.suiteId) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { + if areNil { + XCTAssertNil(object.assignedtoId) + XCTAssertNil(object.caseIds) + XCTAssertNil(object.configIds) + XCTAssertNil(object.description) + XCTAssertNil(object.includeAll) + XCTAssertNil(object.name) + XCTAssertNil(object.runs) + } else { + XCTAssertNotNil(object.assignedtoId) + XCTAssertNotNil(object.caseIds) + XCTAssertNotNil(object.configIds) + XCTAssertNotNil(object.description) + XCTAssertNotNil(object.includeAll) + XCTAssertNotNil(object.name) + XCTAssertNotNil(object.runs) + XCTAssertEqual(object.assignedtoId, Properties.Optional.assignedtoId) + if let caseIds = object.caseIds { XCTAssertEqual(caseIds, Properties.Optional.caseIds) } + if let configIds = object.configIds { + if let configIdsInRuns = self.configIds(in: Properties.Optional.runs) { + XCTAssertEqual(configIds, configIdsInRuns) + } + } + XCTAssertEqual(object.description, Properties.Optional.description) + XCTAssertEqual(object.includeAll, Properties.Optional.includeAll) + XCTAssertEqual(object.name, Properties.Optional.name) + if let runs = object.runs { XCTAssertEqual(runs, Properties.Optional.runs) } + } + } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { + + let initialConfigIds = object.configIds + + object.assignedtoId = 1000 + object.caseIds = [1001, 1002, 1003] + object.description = "New Description" + object.includeAll = false + object.name = "New Name" + object.runs = [NewPlan_Entry_RunTests.objectWithRequiredProperties, NewPlan_Entry_RunTests.objectWithRequiredProperties] + object.suiteId = 1004 + + XCTAssertNotEqual(object.assignedtoId, Properties.Optional.assignedtoId) + if let caseIds = object.caseIds { XCTAssertNotEqual(caseIds, Properties.Optional.caseIds) } + if let configIds = object.configIds, let initialConfigIds = initialConfigIds { XCTAssertNotEqual(configIds, initialConfigIds) } + XCTAssertNotEqual(object.description, Properties.Optional.description) + XCTAssertNotEqual(object.includeAll, Properties.Optional.includeAll) + XCTAssertNotEqual(object.name, Properties.Optional.name) + XCTAssertNotEqual(object.runs!, Properties.Optional.runs) + if let runs = object.runs { XCTAssertNotEqual(runs, Properties.Optional.runs) } + XCTAssertNotEqual(object.suiteId, Properties.Required.suiteId) + + XCTAssertEqual(object.assignedtoId, 1000) + if let caseIds = object.caseIds { XCTAssertEqual(caseIds, [1001, 1002, 1003]) } + if let configIds = object.configIds { + if let allRunConfigIds = self.configIds(in: [NewPlan_Entry_RunTests.objectWithRequiredAndOptionalProperties, NewPlan_Entry_RunTests.objectWithRequiredAndOptionalProperties]) { + XCTAssertEqual(configIds, allRunConfigIds) + } + } + XCTAssertEqual(object.description, "New Description") + XCTAssertEqual(object.includeAll, false) + XCTAssertEqual(object.name, "New Name") + if let runs = object.runs { XCTAssertEqual(runs, [NewPlan_Entry_RunTests.objectWithRequiredProperties, NewPlan_Entry_RunTests.objectWithRequiredProperties]) } + XCTAssertEqual(object.suiteId, 1004) + } + +} + +extension NewPlan_EntryTests { + + /* + Helper to return all configIds from an array of NewPlan.Entry.Run's. + */ + func configIds(in runs: [NewPlan.Entry.Run]) -> [Int]? { + + var allRunConfigIds = [Int]() + var allRunConfigIdsAreNil = true + + for run in runs { + + guard let runConfigIds = run.configIds else { + continue + } + + allRunConfigIdsAreNil = false + allRunConfigIds.append(contentsOf: runConfigIds) + } + + if allRunConfigIdsAreNil { + return nil + } + + allRunConfigIds = Array(Set(allRunConfigIds)) // Remove duplicates + allRunConfigIds.sort() // configIds is sorted by default + + return allRunConfigIds + } + +} diff --git a/QuizTrainTests/Network/Models/Add/NewPlanTests.swift b/QuizTrainTests/Network/Models/Add/NewPlanTests.swift new file mode 100644 index 0000000..43d5f2e --- /dev/null +++ b/QuizTrainTests/Network/Models/Add/NewPlanTests.swift @@ -0,0 +1,127 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class NewPlanTests: XCTestCase, AddModelTests { + + typealias Object = NewPlan + + func testAddRequestJSON() { + _testAddRequestJSON() + } + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testVariableProperties() { + _testVariableProperties() + } + +} + +// MARK: - Data + +extension NewPlanTests { + + struct Properties { + + struct Required { + static let name = "Name" + } + + struct Optional { + static let description = "Description" + static let entries = [NewPlan_EntryTests.objectWithRequiredAndOptionalProperties, NewPlan_EntryTests.objectWithRequiredAndOptionalProperties, NewPlan_EntryTests.objectWithRequiredAndOptionalProperties] + static let milestoneId = 10 + } + + } + +} + +// MARK: - Objects + +extension NewPlanTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(description: nil, + entries: nil, + milestoneId: nil, + name: Properties.Required.name) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(description: Properties.Optional.description, + entries: Properties.Optional.entries, + milestoneId: Properties.Optional.milestoneId, + name: Properties.Required.name) + } + +} + +// MARK: - Assertions + +extension NewPlanTests: AssertAddRequestJSON { } + +extension NewPlanTests: AssertEquatable { } + +extension NewPlanTests: AssertJSONSerializing { } + +extension NewPlanTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.name, Properties.Required.name) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { + if areNil { + XCTAssertNil(object.description) + XCTAssertNil(object.entries) + XCTAssertNil(object.milestoneId) + } else { + XCTAssertNotNil(object.description) + XCTAssertNotNil(object.entries) + XCTAssertNotNil(object.milestoneId) + XCTAssertEqual(object.description, Properties.Optional.description) + if let entries = object.entries { XCTAssertEqual(entries, Properties.Optional.entries) } + XCTAssertEqual(object.milestoneId, Properties.Optional.milestoneId) + } + } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { + + object.description = "New Description" + object.entries = [NewPlan_EntryTests.objectWithRequiredAndOptionalProperties, NewPlan_EntryTests.objectWithRequiredAndOptionalProperties] + object.milestoneId = 1000 + object.name = "New Name" + + XCTAssertNotEqual(object.description, Properties.Optional.description) + if let entries = object.entries { XCTAssertNotEqual(entries, Properties.Optional.entries) } + XCTAssertNotEqual(object.milestoneId, Properties.Optional.milestoneId) + XCTAssertNotEqual(object.name, Properties.Required.name) + + XCTAssertEqual(object.description, "New Description") + if let entries = object.entries { XCTAssertEqual(entries, [NewPlan_EntryTests.objectWithRequiredAndOptionalProperties, NewPlan_EntryTests.objectWithRequiredAndOptionalProperties]) } + XCTAssertEqual(object.milestoneId, 1000) + XCTAssertEqual(object.name, "New Name") + } + +} diff --git a/QuizTrainTests/Network/Models/Add/NewProjectTests.swift b/QuizTrainTests/Network/Models/Add/NewProjectTests.swift new file mode 100644 index 0000000..a7fd1cf --- /dev/null +++ b/QuizTrainTests/Network/Models/Add/NewProjectTests.swift @@ -0,0 +1,123 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class NewProjectTests: XCTestCase, AddModelTests { + + typealias Object = NewProject + + func testAddRequestJSON() { + _testAddRequestJSON() + } + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testVariableProperties() { + _testVariableProperties() + } + +} + +// MARK: - Data + +extension NewProjectTests { + + struct Properties { + + struct Required { + static let name = "Name" + static let showAnnouncement = true + static let suiteMode = Project.SuiteMode.multipleSuites + } + + struct Optional { + static let announcement = "Announcement" + } + + } + +} + +// MARK: - Objects + +extension NewProjectTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(announcement: nil, + name: Properties.Required.name, + showAnnouncement: Properties.Required.showAnnouncement, + suiteMode: Properties.Required.suiteMode) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(announcement: Properties.Optional.announcement, + name: Properties.Required.name, + showAnnouncement: Properties.Required.showAnnouncement, + suiteMode: Properties.Required.suiteMode) + } + +} + +// MARK: - Assertions + +extension NewProjectTests: AssertAddRequestJSON { } + +extension NewProjectTests: AssertEquatable { } + +extension NewProjectTests: AssertJSONSerializing { } + +extension NewProjectTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.name, Properties.Required.name) + XCTAssertEqual(object.showAnnouncement, Properties.Required.showAnnouncement) + XCTAssertEqual(object.suiteMode, Properties.Required.suiteMode) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { + if areNil { + XCTAssertNil(object.announcement) + } else { + XCTAssertNotNil(object.announcement) + XCTAssertEqual(object.announcement, Properties.Optional.announcement) + } + } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { + + object.announcement = "New Announcement" + object.name = "New Name" + object.showAnnouncement = false + object.suiteMode = .singleSuite + + XCTAssertNotEqual(object.announcement, Properties.Optional.announcement) + XCTAssertNotEqual(object.name, Properties.Required.name) + XCTAssertNotEqual(object.showAnnouncement, Properties.Required.showAnnouncement) + XCTAssertNotEqual(object.suiteMode, Properties.Required.suiteMode) + + XCTAssertEqual(object.announcement, "New Announcement") + XCTAssertEqual(object.name, "New Name") + XCTAssertEqual(object.showAnnouncement, false) + XCTAssertEqual(object.suiteMode, .singleSuite) + } + +} diff --git a/QuizTrainTests/Network/Models/Add/NewResultTests.swift b/QuizTrainTests/Network/Models/Add/NewResultTests.swift new file mode 100644 index 0000000..f38ee2b --- /dev/null +++ b/QuizTrainTests/Network/Models/Add/NewResultTests.swift @@ -0,0 +1,211 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class NewResultTests: XCTestCase, AddModelTests, ValidatableTests { + + typealias Object = NewResult + + func testAddRequestJSON() { + _testAddRequestJSON() + } + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testIsValid() { + _testIsValid() + } + + func testIsInvalid() { + _testIsInvalid() + } + +} + +// MARK: - Data + +extension NewResultTests { + + struct Properties { + + struct Required { /* none */ } + + struct Optional { + static let assignedtoId = 11 + static let comment = "Comment" + static let defects = "Defects" + static let elapsed = "4hr, 31min" + static let statusId = 10 + static let version = "1.2.3" + static let customFields = NewResultTests.customFields + } + + } + +} + +extension NewResultTests: CustomFieldsDataProvider { } + +// MARK: - Objects + +extension NewResultTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(assignedtoId: nil, + comment: nil, + defects: nil, + elapsed: nil, + statusId: nil, + version: nil, + customFields: nil) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(assignedtoId: Properties.Optional.assignedtoId, + comment: Properties.Optional.comment, + defects: Properties.Optional.defects, + elapsed: Properties.Optional.elapsed, + statusId: Properties.Optional.statusId, + version: Properties.Optional.version, + customFields: Properties.Optional.customFields) + } + +} + +extension NewResultTests: ValidatableObjectProvider { + + var validObject: Validatable { + return objectWithRequiredAndOptionalProperties + } + + var invalidObject: Validatable { + var object = objectWithRequiredAndOptionalProperties + object.assignedtoId = nil + object.comment = nil + object.statusId = nil + return object + } + +} + +// MARK: - Assertions + +extension NewResultTests: AssertAddRequestJSON { } + +extension NewResultTests: AssertCustomFields { } + +extension NewResultTests: AssertEquatable { } + +extension NewResultTests: AssertJSONSerializing { } + +extension NewResultTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { /* none */ } + + func assertOptionalProperties(in object: Object, areNil: Bool) { + if areNil { + XCTAssertNil(object.assignedtoId) + XCTAssertNil(object.comment) + XCTAssertNil(object.defects) + XCTAssertNil(object.elapsed) + XCTAssertNil(object.statusId) + XCTAssertNil(object.version) + XCTAssertTrue(object.customFields.isEmpty) + } else { + XCTAssertNotNil(object.assignedtoId) + XCTAssertNotNil(object.comment) + XCTAssertNotNil(object.defects) + XCTAssertNotNil(object.elapsed) + XCTAssertNotNil(object.statusId) + XCTAssertNotNil(object.version) + XCTAssertNotNil(object.customFields) + XCTAssertEqual(object.assignedtoId, Properties.Optional.assignedtoId) + XCTAssertEqual(object.comment, Properties.Optional.comment) + XCTAssertEqual(object.defects, Properties.Optional.defects) + XCTAssertEqual(object.elapsed, Properties.Optional.elapsed) + XCTAssertEqual(object.statusId, Properties.Optional.statusId) + XCTAssertEqual(object.version, Properties.Optional.version) + XCTAssertEqual(object.customFields.count, Properties.Optional.customFields.count) + } + } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { + + // Properties + + object.assignedtoId = 1000 + object.comment = "New Comment" + object.defects = "New Defects" + object.elapsed = "99hr, 99min" + object.version = "4.5.6" + object.statusId = 1001 + object.customFields = NewResultTests.emptyCustomFields + + XCTAssertNotEqual(object.assignedtoId, Properties.Optional.assignedtoId) + XCTAssertNotEqual(object.comment, Properties.Optional.comment) + XCTAssertNotEqual(object.defects, Properties.Optional.defects) + XCTAssertNotEqual(object.elapsed, Properties.Optional.elapsed) + XCTAssertNotEqual(object.version, Properties.Optional.version) + XCTAssertNotEqual(object.statusId, Properties.Optional.statusId) + XCTAssertNotEqual(object.customFields.count, Properties.Optional.customFields.count) + + XCTAssertEqual(object.assignedtoId, 1000) + XCTAssertEqual(object.comment, "New Comment") + XCTAssertEqual(object.defects, "New Defects") + XCTAssertEqual(object.elapsed, "99hr, 99min") + XCTAssertEqual(object.version, "4.5.6") + XCTAssertEqual(object.statusId, 1001) + XCTAssertEqual(object.customFields.count, NewResultTests.emptyCustomFields.count) + + // Custom Fields + + let customFieldsCount = object.customFields.count + + object.customFields["custom_field_test01"] = "Custom Field Test 01" + object.customFields["custom_field_test02"] = 9000 + object.customFields["custom_field_test03"] = -8.0 + object.customFields["invalid_custom_field_test04"] = "This should not be added." + + XCTAssertNotNil(object.customFields["custom_field_test01"]) + XCTAssertNotNil(object.customFields["custom_field_test02"]) + XCTAssertNotNil(object.customFields["custom_field_test03"]) + XCTAssertNil(object.customFields["invalid_custom_field_test04"]) + + XCTAssertEqual(object.customFields["custom_field_test01"] as! String, "Custom Field Test 01") + XCTAssertEqual(object.customFields["custom_field_test02"] as! Int, 9000) + XCTAssertEqual(object.customFields["custom_field_test03"] as! Double, -8.0) + + XCTAssertEqual(object.customFields.count, customFieldsCount + 3) + + object.customFields.removeValue(forKey: "custom_field_test01") + + XCTAssertNil(object.customFields["custom_field_test01"]) + XCTAssertEqual(object.customFields.count, customFieldsCount + 2) + } + +} + +extension NewResultTests: AssertValidatable { } diff --git a/QuizTrainTests/Network/Models/Add/NewRunTests.swift b/QuizTrainTests/Network/Models/Add/NewRunTests.swift new file mode 100644 index 0000000..17f407c --- /dev/null +++ b/QuizTrainTests/Network/Models/Add/NewRunTests.swift @@ -0,0 +1,154 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class NewRunTests: XCTestCase, AddModelTests { + + typealias Object = NewRun + + func testAddRequestJSON() { + _testAddRequestJSON() + } + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testVariableProperties() { + _testVariableProperties() + } + +} + +// MARK: - Data + +extension NewRunTests { + + struct Properties { + + struct Required { + static let name = "Name" + } + + struct Optional { + static let assignedtoId = 13 + static let caseIds = [10, 11, 12] + static let description = "Description" + static let includeAll = true + static let milestoneId = 14 + static let suiteId = 15 + } + + } + +} + +// MARK: - Objects + +extension NewRunTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(assignedtoId: nil, + caseIds: nil, + description: nil, + includeAll: nil, + milestoneId: nil, + name: Properties.Required.name, + suiteId: nil) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(assignedtoId: Properties.Optional.assignedtoId, + caseIds: Properties.Optional.caseIds, + description: Properties.Optional.description, + includeAll: Properties.Optional.includeAll, + milestoneId: Properties.Optional.milestoneId, + name: Properties.Required.name, + suiteId: Properties.Optional.suiteId) + } + +} + +// MARK: - Assertions + +extension NewRunTests: AssertAddRequestJSON { } + +extension NewRunTests: AssertEquatable { } + +extension NewRunTests: AssertJSONSerializing { } + +extension NewRunTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.name, Properties.Required.name) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { + if areNil { + XCTAssertNil(object.assignedtoId) + XCTAssertNil(object.caseIds) + XCTAssertNil(object.description) + XCTAssertNil(object.includeAll) + XCTAssertNil(object.milestoneId) + XCTAssertNil(object.suiteId) + } else { + XCTAssertNotNil(object.assignedtoId) + XCTAssertNotNil(object.caseIds) + XCTAssertNotNil(object.description) + XCTAssertNotNil(object.includeAll) + XCTAssertNotNil(object.milestoneId) + XCTAssertNotNil(object.suiteId) + XCTAssertEqual(object.assignedtoId, Properties.Optional.assignedtoId) + if let objectCaseIds = object.caseIds { XCTAssertEqual(objectCaseIds, Properties.Optional.caseIds) } + XCTAssertEqual(object.description, Properties.Optional.description) + XCTAssertEqual(object.includeAll, Properties.Optional.includeAll) + XCTAssertEqual(object.milestoneId, Properties.Optional.milestoneId) + XCTAssertEqual(object.suiteId, Properties.Optional.suiteId) + } + } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { + + object.assignedtoId = 1000 + object.caseIds = [1001, 1002, 1003] + object.description = "New Description" + object.includeAll = false + object.milestoneId = 1004 + object.name = "New Name" + object.suiteId = 1005 + + XCTAssertNotEqual(object.assignedtoId, Properties.Optional.assignedtoId) + if let objectCaseIds = object.caseIds { XCTAssertNotEqual(objectCaseIds, Properties.Optional.caseIds) } + XCTAssertNotEqual(object.description, Properties.Optional.description) + XCTAssertNotEqual(object.includeAll, Properties.Optional.includeAll) + XCTAssertNotEqual(object.milestoneId, Properties.Optional.milestoneId) + XCTAssertNotEqual(object.name, Properties.Required.name) + XCTAssertNotEqual(object.suiteId, Properties.Optional.suiteId) + + XCTAssertEqual(object.assignedtoId, 1000) + if let objectCaseIds = object.caseIds { XCTAssertEqual(objectCaseIds, [1001, 1002, 1003]) } + XCTAssertEqual(object.description, "New Description") + XCTAssertEqual(object.includeAll, false) + XCTAssertEqual(object.milestoneId, 1004) + XCTAssertEqual(object.name, "New Name") + XCTAssertEqual(object.suiteId, 1005) + } + +} diff --git a/QuizTrainTests/Network/Models/Add/NewSectionTests.swift b/QuizTrainTests/Network/Models/Add/NewSectionTests.swift new file mode 100644 index 0000000..3dd0408 --- /dev/null +++ b/QuizTrainTests/Network/Models/Add/NewSectionTests.swift @@ -0,0 +1,127 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class NewSectionTests: XCTestCase, AddModelTests { + + typealias Object = NewSection + + func testAddRequestJSON() { + _testAddRequestJSON() + } + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testVariableProperties() { + _testVariableProperties() + } + +} + +// MARK: - Data + +extension NewSectionTests { + + struct Properties { + + struct Required { + static let name = "Name" + } + + struct Optional { + static let description = "Description" + static let parentId = 10 + static let suiteId = 11 + } + + } + +} + +// MARK: - Objects + +extension NewSectionTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(description: nil, + name: Properties.Required.name, + parentId: nil, + suiteId: nil) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(description: Properties.Optional.description, + name: Properties.Required.name, + parentId: Properties.Optional.parentId, + suiteId: Properties.Optional.suiteId) + } + +} + +// MARK: - Assertions + +extension NewSectionTests: AssertAddRequestJSON { } + +extension NewSectionTests: AssertEquatable { } + +extension NewSectionTests: AssertJSONSerializing { } + +extension NewSectionTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.name, Properties.Required.name) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { + if areNil { + XCTAssertNil(object.description) + XCTAssertNil(object.parentId) + XCTAssertNil(object.suiteId) + } else { + XCTAssertNotNil(object.description) + XCTAssertNotNil(object.parentId) + XCTAssertNotNil(object.suiteId) + XCTAssertEqual(object.description, Properties.Optional.description) + XCTAssertEqual(object.parentId, Properties.Optional.parentId) + XCTAssertEqual(object.suiteId, Properties.Optional.suiteId) + } + } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { + + object.description = "New Description" + object.name = "New Name" + object.parentId = 1000 + object.suiteId = 1001 + + XCTAssertNotEqual(object.description, Properties.Optional.description) + XCTAssertNotEqual(object.name, Properties.Required.name) + XCTAssertNotEqual(object.parentId, Properties.Optional.parentId) + XCTAssertNotEqual(object.suiteId, Properties.Optional.suiteId) + + XCTAssertEqual(object.description, "New Description") + XCTAssertEqual(object.name, "New Name") + XCTAssertEqual(object.parentId, 1000) + XCTAssertEqual(object.suiteId, 1001) + } + +} diff --git a/QuizTrainTests/Network/Models/Add/NewSuiteTests.swift b/QuizTrainTests/Network/Models/Add/NewSuiteTests.swift new file mode 100644 index 0000000..8cd58b8 --- /dev/null +++ b/QuizTrainTests/Network/Models/Add/NewSuiteTests.swift @@ -0,0 +1,109 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class NewSuiteTests: XCTestCase, AddModelTests { + + typealias Object = NewSuite + + func testAddRequestJSON() { + _testAddRequestJSON() + } + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testVariableProperties() { + _testVariableProperties() + } + +} + +// MARK: - Data + +extension NewSuiteTests { + + struct Properties { + + struct Required { + static let name = "Name" + } + + struct Optional { + static let description = "Description" + } + + } + +} + +// MARK: - Objects + +extension NewSuiteTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(description: nil, + name: Properties.Required.name) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(description: Properties.Optional.description, + name: Properties.Required.name) + } + +} + +// MARK: - Assertions + +extension NewSuiteTests: AssertAddRequestJSON { } + +extension NewSuiteTests: AssertEquatable { } + +extension NewSuiteTests: AssertJSONSerializing { } + +extension NewSuiteTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.name, Properties.Required.name) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { + if areNil { + XCTAssertNil(object.description) + } else { + XCTAssertNotNil(object.description) + XCTAssertEqual(object.description, Properties.Optional.description) + } + } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { + + object.description = "New Description" + object.name = "New Name" + + XCTAssertNotEqual(object.description, Properties.Optional.description) + XCTAssertNotEqual(object.name, Properties.Required.name) + + XCTAssertEqual(object.description, "New Description") + XCTAssertEqual(object.name, "New Name") + } + +} diff --git a/QuizTrainTests/Network/Models/Add/NewTestResults.ResultTests.swift b/QuizTrainTests/Network/Models/Add/NewTestResults.ResultTests.swift new file mode 100644 index 0000000..9de9723 --- /dev/null +++ b/QuizTrainTests/Network/Models/Add/NewTestResults.ResultTests.swift @@ -0,0 +1,220 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class NewTestResults_ResultTests: XCTestCase, AddModelTests, ValidatableTests { + + typealias Object = NewTestResults.Result + + func testAddRequestJSON() { + _testAddRequestJSON() + } + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testIsValid() { + _testIsValid() + } + + func testIsInvalid() { + _testIsInvalid() + } + +} + +// MARK: - Data + +extension NewTestResults_ResultTests { + + struct Properties { + + struct Required { + static let testId = 10 + } + + struct Optional { + static let assignedtoId = 11 + static let comment = "Comment" + static let defects = "Defects" + static let elapsed = "4hr, 31min" + static let statusId = 12 + static let version = "1.2.3" + static let customFields = NewTestResults_ResultTests.customFields + } + + } + +} + +extension NewTestResults_ResultTests: CustomFieldsDataProvider { } + +// MARK: - Objects + +extension NewTestResults_ResultTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(assignedtoId: nil, + comment: nil, + defects: nil, + elapsed: nil, + statusId: nil, + testId: Properties.Required.testId, + version: nil, + customFields: nil) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(assignedtoId: Properties.Optional.assignedtoId, + comment: Properties.Optional.comment, + defects: Properties.Optional.defects, + elapsed: Properties.Optional.elapsed, + statusId: Properties.Optional.statusId, + testId: Properties.Required.testId, + version: Properties.Optional.version, + customFields: Properties.Optional.customFields) + } + +} + +extension NewTestResults_ResultTests: ValidatableObjectProvider { + + var validObject: Validatable { + return objectWithRequiredAndOptionalProperties + } + + var invalidObject: Validatable { + var object = objectWithRequiredAndOptionalProperties + object.assignedtoId = nil + object.comment = nil + object.statusId = nil + return object + } + +} + +// MARK: - Assertions + +extension NewTestResults_ResultTests: AssertAddRequestJSON { } + +extension NewTestResults_ResultTests: AssertCustomFields { } + +extension NewTestResults_ResultTests: AssertEquatable { } + +extension NewTestResults_ResultTests: AssertJSONSerializing { } + +extension NewTestResults_ResultTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.testId, Properties.Required.testId) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { + if areNil { + XCTAssertNil(object.assignedtoId) + XCTAssertNil(object.comment) + XCTAssertNil(object.defects) + XCTAssertNil(object.elapsed) + XCTAssertNil(object.statusId) + XCTAssertNil(object.version) + XCTAssertTrue(object.customFields.isEmpty) + } else { + XCTAssertNotNil(object.assignedtoId) + XCTAssertNotNil(object.comment) + XCTAssertNotNil(object.defects) + XCTAssertNotNil(object.elapsed) + XCTAssertNotNil(object.statusId) + XCTAssertNotNil(object.version) + XCTAssertNotNil(object.customFields) + XCTAssertEqual(object.assignedtoId, Properties.Optional.assignedtoId) + XCTAssertEqual(object.comment, Properties.Optional.comment) + XCTAssertEqual(object.defects, Properties.Optional.defects) + XCTAssertEqual(object.elapsed, Properties.Optional.elapsed) + XCTAssertEqual(object.statusId, Properties.Optional.statusId) + XCTAssertEqual(object.version, Properties.Optional.version) + XCTAssertEqual(object.customFields.count, Properties.Optional.customFields.count) + } + } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { + + // Properties + + object.assignedtoId = 1000 + object.comment = "New Comment" + object.defects = "New Defects" + object.elapsed = "99hr, 99min" + object.version = "4.5.6" + object.statusId = 1001 + object.testId = 1002 + object.customFields = NewTestResults_ResultTests.emptyCustomFields + + XCTAssertNotEqual(object.assignedtoId, Properties.Optional.assignedtoId) + XCTAssertNotEqual(object.comment, Properties.Optional.comment) + XCTAssertNotEqual(object.defects, Properties.Optional.defects) + XCTAssertNotEqual(object.elapsed, Properties.Optional.elapsed) + XCTAssertNotEqual(object.version, Properties.Optional.version) + XCTAssertNotEqual(object.statusId, Properties.Optional.statusId) + XCTAssertNotEqual(object.testId, Properties.Required.testId) + XCTAssertNotEqual(object.customFields.count, Properties.Optional.customFields.count) + + XCTAssertEqual(object.assignedtoId, 1000) + XCTAssertEqual(object.comment, "New Comment") + XCTAssertEqual(object.defects, "New Defects") + XCTAssertEqual(object.elapsed, "99hr, 99min") + XCTAssertEqual(object.version, "4.5.6") + XCTAssertEqual(object.statusId, 1001) + XCTAssertEqual(object.testId, 1002) + XCTAssertEqual(object.customFields.count, NewTestResults_ResultTests.emptyCustomFields.count) + + // Custom Fields + + let customFieldsCount = object.customFields.count + + object.customFields["custom_field_test01"] = "Custom Field Test 01" + object.customFields["custom_field_test02"] = 9000 + object.customFields["custom_field_test03"] = -8.0 + object.customFields["invalid_custom_field_test04"] = "This should not be added." + + XCTAssertNotNil(object.customFields["custom_field_test01"]) + XCTAssertNotNil(object.customFields["custom_field_test02"]) + XCTAssertNotNil(object.customFields["custom_field_test03"]) + XCTAssertNil(object.customFields["invalid_custom_field_test04"]) + + XCTAssertEqual(object.customFields["custom_field_test01"] as! String, "Custom Field Test 01") + XCTAssertEqual(object.customFields["custom_field_test02"] as! Int, 9000) + XCTAssertEqual(object.customFields["custom_field_test03"] as! Double, -8.0) + + XCTAssertEqual(object.customFields.count, customFieldsCount + 3) + + object.customFields.removeValue(forKey: "custom_field_test01") + + XCTAssertNil(object.customFields["custom_field_test01"]) + XCTAssertEqual(object.customFields.count, customFieldsCount + 2) + } + +} + +extension NewTestResults_ResultTests: AssertValidatable { } diff --git a/QuizTrainTests/Network/Models/Add/NewTestResultsTests.swift b/QuizTrainTests/Network/Models/Add/NewTestResultsTests.swift new file mode 100644 index 0000000..9123e43 --- /dev/null +++ b/QuizTrainTests/Network/Models/Add/NewTestResultsTests.swift @@ -0,0 +1,117 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class NewTestResultsTests: XCTestCase, AddModelTests, ValidatableTests { + + typealias Object = NewTestResults + + func testAddRequestJSON() { + _testAddRequestJSON() + } + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testVariableProperties() { + _testVariableProperties() + } + + func testIsValid() { + _testIsValid() + } + + func testIsInvalid() { + _testIsInvalid() + } + +} + +// MARK: - Data + +extension NewTestResultsTests { + + struct Properties { + + struct Required { + static let results = [NewTestResults_ResultTests.objectWithRequiredAndOptionalProperties, NewTestResults_ResultTests.objectWithRequiredAndOptionalProperties, NewTestResults_ResultTests.objectWithRequiredAndOptionalProperties] + } + + struct Optional { /* none */ } + + } + +} + +extension NewTestResultsTests: CustomFieldsDataProvider { } + +// MARK: - Objects + +extension NewTestResultsTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(results: Properties.Required.results) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(results: Properties.Required.results) + } + +} + +extension NewTestResultsTests: ValidatableObjectProvider { + + var validObject: Validatable { + return objectWithRequiredAndOptionalProperties + } + + var invalidObject: Validatable { + return Object(results: [NewTestResults_ResultTests.objectWithRequiredProperties, NewTestResults_ResultTests.objectWithRequiredProperties, NewTestResults_ResultTests.objectWithRequiredProperties]) + } + +} + +// MARK: - Assertions + +extension NewTestResultsTests: AssertAddRequestJSON { } + +extension NewTestResultsTests: AssertEquatable { } + +extension NewTestResultsTests: AssertJSONSerializing { } + +extension NewTestResultsTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { + XCTAssertEqual(object.results, Properties.Required.results) + } + + func assertOptionalProperties(in object: Object, areNil: Bool) { /* none */ } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { + object.results.append(NewTestResults_ResultTests.objectWithRequiredProperties) + XCTAssertNotEqual(object.results, Properties.Required.results) + object.results.remove(at: object.results.count - 1) + XCTAssertEqual(object.results, Properties.Required.results) + } + +} + +extension NewTestResultsTests: AssertValidatable { } diff --git a/QuizTrainTests/Network/Models/Testing Protocols/AddModelTests.swift b/QuizTrainTests/Network/Models/Testing Protocols/AddModelTests.swift new file mode 100644 index 0000000..0af3236 --- /dev/null +++ b/QuizTrainTests/Network/Models/Testing Protocols/AddModelTests.swift @@ -0,0 +1 @@ +protocol AddModelTests: AddRequestJSONTests, EquatableTests, InitTests, JSONSerializingTests, VariablePropertyTests { } diff --git a/QuizTrainTests/Network/Models/Testing Protocols/UpdateModelTests.swift b/QuizTrainTests/Network/Models/Testing Protocols/UpdateModelTests.swift new file mode 100644 index 0000000..c0bc9e7 --- /dev/null +++ b/QuizTrainTests/Network/Models/Testing Protocols/UpdateModelTests.swift @@ -0,0 +1 @@ +protocol UpdateModelTests: UpdateRequestJSONTests, EquatableTests, InitTests, JSONSerializingTests, VariablePropertyTests { } diff --git a/QuizTrainTests/Network/Models/Update/UpdatePlanEntryRunsTests.swift b/QuizTrainTests/Network/Models/Update/UpdatePlanEntryRunsTests.swift new file mode 100644 index 0000000..d3fb75c --- /dev/null +++ b/QuizTrainTests/Network/Models/Update/UpdatePlanEntryRunsTests.swift @@ -0,0 +1,127 @@ +import XCTest +@testable import QuizTrain + +// MARK: - Tests + +class UpdatePlanEntryRunsTests: XCTestCase, UpdateModelTests { + + typealias Object = UpdatePlanEntryRuns + + func testUpdateRequestJSON() { + _testUpdateRequestJSON() + } + + func testEquatable() { + _testEquatable() + } + + func testInit() { + _testInit() + } + + func testInitWithOptionalProperties() { + _testInitWithOptionalProperties() + } + + func testJSONSerializingSingleObjects() { + _testJSONSerializingSingleObjects() + } + + func testJSONSerializingMultipleObjects() { + _testJSONSerializingMultipleObjects() + } + + func testVariableProperties() { + _testVariableProperties() + } + +} + +// MARK: - Data + +extension UpdatePlanEntryRunsTests { + + struct Properties { + + struct Required { /* none */ } + + struct Optional { + static let assignedtoId = 10 + static let caseIds = [11, 12, 13] + static let description = "Description" + static let includeAll = true + } + + } + +} + +// MARK: - Objects + +extension UpdatePlanEntryRunsTests: ObjectProvider { + + static var objectWithRequiredProperties: Object { + return Object(assignedtoId: nil, + caseIds: nil, + description: nil, + includeAll: nil) + } + + static var objectWithRequiredAndOptionalProperties: Object { + return Object(assignedtoId: Properties.Optional.assignedtoId, + caseIds: Properties.Optional.caseIds, + description: Properties.Optional.description, + includeAll: Properties.Optional.includeAll) + } + +} + +// MARK: - Assertions + +extension UpdatePlanEntryRunsTests: AssertUpdateRequestJSON { } + +extension UpdatePlanEntryRunsTests: AssertEquatable { } + +extension UpdatePlanEntryRunsTests: AssertJSONSerializing { } + +extension UpdatePlanEntryRunsTests: AssertProperties { + + func assertRequiredProperties(in object: Object) { /* none */ } + + func assertOptionalProperties(in object: Object, areNil: Bool) { + if areNil { + XCTAssertNil(object.assignedtoId) + XCTAssertNil(object.caseIds) + XCTAssertNil(object.description) + XCTAssertNil(object.includeAll) + } else { + XCTAssertNotNil(object.assignedtoId) + XCTAssertNotNil(object.caseIds) + XCTAssertNotNil(object.description) + XCTAssertNotNil(object.includeAll) + XCTAssertEqual(object.assignedtoId, Properties.Optional.assignedtoId) + XCTAssertEqual(object.caseIds!, Properties.Optional.caseIds) + XCTAssertEqual(object.description, Properties.Optional.description) + XCTAssertEqual(object.includeAll, Properties.Optional.includeAll) + } + } + + func assertVariablePropertiesCanBeChanged(in object: inout Object) { + + object.assignedtoId = 1000 + object.caseIds = [1001, 1002, 1003] + object.description = "New Description" + object.includeAll = false + + XCTAssertNotEqual(object.assignedtoId, Properties.Optional.assignedtoId) + XCTAssertNotEqual(object.caseIds!, Properties.Optional.caseIds) + XCTAssertNotEqual(object.description, Properties.Optional.description) + XCTAssertNotEqual(object.includeAll, Properties.Optional.includeAll) + + XCTAssertEqual(object.assignedtoId, 1000) + XCTAssertEqual(object.caseIds!, [1001, 1002, 1003]) + XCTAssertEqual(object.description, "New Description") + XCTAssertEqual(object.includeAll, false) + } + +} diff --git a/QuizTrainTests/Network/ObjectAPITests.swift b/QuizTrainTests/Network/ObjectAPITests.swift new file mode 100644 index 0000000..f5f1504 --- /dev/null +++ b/QuizTrainTests/Network/ObjectAPITests.swift @@ -0,0 +1,5274 @@ +import XCTest +@testable import QuizTrain + +/* + ObjectAPI systems tests. These tests run against a real-world TestRail + instance. Sections: + + - Initialization, deinit, stored properties. + - Shared Setup/Teardown logic. This created/destroys a TestProject to be used + during testing. + - Data Provider logic. This provides data for objects used during tests. + - Tests separated into related sections (Arrange). + - Assertions separated into related sections used by tests. These perform API + requests and assert their results (Act, Assert). In some cases tests may + perform extra assertions based on the context of a specific test. + + Tests will create and delete several projects and their contents during + testing. They will not delete anything else unless you or someone modifies them + to do so. Generally it is safe to run tests against production so long as you + are OK with rate limits potentially being triggered while tests run and with + tests reading some production data in your instance. Even so it is advised that + you backup your production instance fully before running tests and verify that + the backup is valid. + + If tests crash rouge test projects may be abandoned in your instance. Their + names will be prefixed with "QuizTrainTests". It is safe to delete them so long + as you have no production projects starting with the same name. + + For tests to run you must populate TestCredentials.json with all properties + required by TestCredentials.swift. + + The user must have permissions to create/read/update/delete projects and all + objects inside of them. Furthermore any required custom fields you have created + must either have default values set, or be temporarily unmarked as required, + for tests to run. Alternatively you can add your required custom field data to + appropriate objects in the "Data Provider" section. + */ +class ObjectAPITests: XCTestCase { + + let timeout = 60.0 + let objectCount = 2 // Quantity of each object in TestProject to create. This should be 2 or higher for tests to work well. Values higher than 3 might slow down setUp and tests considerably due to rate limiting. + + static var testCredentials: TestCredentials! + var testCredentials: TestCredentials! { get { return ObjectAPITests.testCredentials } set { ObjectAPITests.testCredentials = newValue } } + + static var objectAPI: ObjectAPI! + var objectAPI: ObjectAPI! { get { return ObjectAPITests.objectAPI } set { ObjectAPITests.objectAPI = newValue } } + + static var testProject: TestProject! + var testProject: TestProject! { get { return ObjectAPITests.testProject } set { ObjectAPITests.testProject = newValue } } + + override func setUp() { + super.setUp() + continueAfterFailure = false + setUpTestCredentials() + setUpObjectAPI() + setUpTestProject() + continueAfterFailure = true + } + + override static func tearDown() { + super.tearDown() + tearDownTestProject() + } + +} + +// MARK: - TestProject + +extension ObjectAPITests { + + struct TestProject { + var cases: [Case] + var caseFields: [CaseField] + var configurationGroups: [ConfigurationGroup] + var configurations: [Configuration] + var milestones: [Milestone] + var plans: [Plan] + var project: Project + var resultFields: [ResultField] + var runs: [Run] + var sections: [Section] + var suites: [Suite] + var templates: [Template] + var tests: [Test] + var user: User + } + +} + +// MARK: - Setup/Teardown + +extension ObjectAPITests { + + func setUpTestCredentials() { + + guard testCredentials == nil else { + return + } + + do { + let bundle = Bundle(for: type(of: self)) + testCredentials = try TestCredentials.load(from: bundle) + } catch { + XCTFail("FAILED: \(#file):\(#line):\(#function): \(error)") + } + } + + func setUpObjectAPI() { + + guard objectAPI == nil else { + return + } + + guard testCredentials != nil else { + XCTFail("FAILED: \(#file):\(#line):\(#function)") + return + } + + objectAPI = ObjectAPI(username: testCredentials.username, secret: testCredentials.secret, hostname: testCredentials.hostname, port: testCredentials.port, scheme: testCredentials.scheme) + } + + // swiftlint:disable:next cyclomatic_complexity + func setUpTestProject() { + + guard testProject == nil else { + return + } + + continueAfterFailure = false + + // Project + + let newProject = NewProject(announcement: "Project Announcement", name: "QuizTrainTests - Test Project", showAnnouncement: true, suiteMode: .multipleSuites) + guard let project = assertAddProject(newProject) else { + XCTFail("FAILED: \(#file):\(#line):\(#function)") + return + } + + defer { + // If setup fails delete the test project from TestRail. This will + // also delete any other objects created below in the project. + if testProject == nil { + continueAfterFailure = true + assertDeleteProject(project) + XCTFail("FAILED: \(#file):\(#line):\(#function)") + } + } + + // Suites + + var suites = [Suite]() + + for i in 0.. NewCase { + + precondition(testProject != nil, "The Test Project must be setup before invoking \(#function)") + + var newCase = NewCase(estimate: nil, + milestoneId: nil, + priorityId: nil, + refs: nil, + templateId: nil, + title: "Test Add: Case Title", + typeId: nil, + customFields: nil) + + switch properties { + case .requiredProperties: + break + case .requiredAndOptionalProperties: + newCase.estimate = "3m" + newCase.milestoneId = nil // Setting the milestoneId does not appear to work. + newCase.priorityId = 3 + newCase.refs = "RF-1, RF-2" + newCase.templateId = testProject.templates[0].id + newCase.typeId = 1 + // data.customFields can be set by caller if necessary. + } + + return newCase + } + + func newConfiguration() -> NewConfiguration { + precondition(testProject != nil, "The Test Project must be setup before invoking \(#function)") + return NewConfiguration(name: "Test Add: Configuration Name") + } + + func newConfigurationGroup() -> NewConfigurationGroup { + precondition(testProject != nil, "The Test Project must be setup before invoking \(#function)") + return NewConfigurationGroup(name: "Test Add: Configuration Group Name") + } + + func newMilestone(with properties: Properties) -> NewMilestone { + + precondition(testProject != nil, "The Test Project must be setup before invoking \(#function)") + + var newMilestone = NewMilestone(description: nil, + dueOn: nil, + name: "Test Add: Milestone Name", + parentId: nil, + startOn: nil) + + switch properties { + case .requiredProperties: + break + case .requiredAndOptionalProperties: + let now = Date() + newMilestone.description = "Test Add: Milestone Description" + newMilestone.dueOn = Date(timeInterval: 86400, since: now) + newMilestone.parentId = nil // Caller can set if necessary. + newMilestone.startOn = now + } + + return newMilestone + } + + func newPlan(with properties: Properties) -> NewPlan { + + precondition(testProject != nil, "The Test Project must be setup before invoking \(#function)") + + var newPlan = NewPlan(description: nil, + entries: nil, + milestoneId: nil, + name: "Test Add: Plan Name") + + switch properties { + case .requiredProperties: + break + case .requiredAndOptionalProperties: + newPlan.description = "Test Add: Plan Description" + newPlan.entries = [] + for _ in 0..<3 { + let newPlanEntry = self.newPlanEntry(with: .requiredAndOptionalProperties) + newPlan.entries?.append(newPlanEntry) + } + newPlan.milestoneId = testProject.milestones[0].id + } + + return newPlan + } + + func newPlanEntry(with properties: Properties) -> NewPlan.Entry { + + precondition(testProject != nil, "The Test Project must be setup before invoking \(#function)") + + let suite = testProject.suites[0] + + var newPlanEntry = NewPlan.Entry(assignedtoId: nil, + caseIds: nil, + description: nil, + includeAll: nil, + name: nil, + runs: nil, + suiteId: suite.id) + + switch properties { + case .requiredProperties: + break + case .requiredAndOptionalProperties: + + let suiteCaseIds = testProject.cases.filter { $0.suiteId == suite.id } + + newPlanEntry.assignedtoId = testProject.user.id + newPlanEntry.caseIds = suiteCaseIds.flatMap { $0.id } + newPlanEntry.description = "Test Add: Plan Entry Description" + newPlanEntry.includeAll = false + newPlanEntry.name = "Test Add: Plan Entry Name" + newPlanEntry.runs = [] + + var groupedConfigIds = [Int: [Int]]() + for i in 0.. NewPlan.Entry.Run { + + precondition(testProject != nil, "The Test Project must be setup before invoking \(#function)") + + return NewPlan.Entry.Run(assignedtoId: nil, + caseIds: nil, + configIds: nil, + description: nil, + includeAll: nil, + milestoneId: nil, + name: nil, + suiteId: nil) + } + + func newProject(with properties: Properties) -> NewProject { + + precondition(testProject != nil, "The Test Project must be setup before invoking \(#function)") + + var newProject = NewProject(announcement: nil, + name: "QuizTrainTests: Test Add: Project Name", + showAnnouncement: false, + suiteMode: .multipleSuites) + + switch properties { + case .requiredProperties: + break + case .requiredAndOptionalProperties: + newProject.announcement = "QuizTrainTests: Test Add: Project Annoucement" + newProject.showAnnouncement = true + } + + return newProject + } + + func newResult(with properties: Properties) -> NewResult { + + precondition(testProject != nil, "The Test Project must be setup before invoking \(#function)") + + var newResult = NewResult(assignedtoId: nil, + comment: nil, + defects: nil, + elapsed: nil, + statusId: 1, + version: nil, + customFields: nil) + + switch properties { + case .requiredProperties: + break + case .requiredAndOptionalProperties: + newResult.assignedtoId = testProject.user.id + newResult.comment = "Test Add: Comment" + newResult.defects = "Test Add: Defects" + newResult.elapsed = "1m 30s" + newResult.version = "INVALID" + // data.customFields can be set by caller if necessary. + } + + return newResult + } + + func newCaseResults(with properties: Properties) -> NewCaseResults { + + precondition(testProject != nil, "The Test Project must be setup before invoking \(#function)") + + var newCaseResults = NewCaseResults(results: []) + + for _ in 0..<3 { + newCaseResults.results.append(newCaseResultsResult(with: properties)) + } + + return newCaseResults + } + + func newCaseResultsResult(with properties: Properties) -> NewCaseResults.Result { + + precondition(testProject != nil, "The Test Project must be setup before invoking \(#function)") + + var newCaseResultsResult = NewCaseResults.Result(assignedtoId: nil, + caseId: testProject.cases[0].id, + comment: nil, + defects: nil, + elapsed: nil, + statusId: nil, + version: nil, + customFields: nil) + + switch properties { + case .requiredProperties: + break + case .requiredAndOptionalProperties: + newCaseResultsResult.assignedtoId = testProject.user.id + newCaseResultsResult.comment = "Test Add: Comment" + newCaseResultsResult.defects = "Test Add: Defects" + newCaseResultsResult.elapsed = "1m 30s" + newCaseResultsResult.statusId = 1 + newCaseResultsResult.version = "INVALID" + // customFields can be set by caller if necessary. + } + + return newCaseResultsResult + } + + func newTestResults(with properties: Properties) -> NewTestResults { + + precondition(testProject != nil, "The Test Project must be setup before invoking \(#function)") + + var newTestResults = NewTestResults(results: []) + + for _ in 0..<3 { + newTestResults.results.append(newTestResultsResult(with: properties)) + } + + return newTestResults + } + + func newTestResultsResult(with properties: Properties) -> NewTestResults.Result { + + precondition(testProject != nil, "The Test Project must be setup before invoking \(#function)") + + var newTestResultsResult = NewTestResults.Result(assignedtoId: nil, + comment: nil, + defects: nil, + elapsed: nil, + statusId: nil, + testId: testProject.tests[0].id, + version: nil, + customFields: nil) + + switch properties { + case .requiredProperties: + break + case .requiredAndOptionalProperties: + newTestResultsResult.assignedtoId = testProject.user.id + newTestResultsResult.comment = "Test Add: Comment" + newTestResultsResult.defects = "Test Add: Defects" + newTestResultsResult.elapsed = "1m 30s" + newTestResultsResult.statusId = 1 + newTestResultsResult.version = "INVALID" + // customFields can be set by caller if necessary. + } + + return newTestResultsResult + } + + func newRun(with properties: Properties) -> NewRun { + + precondition(testProject != nil, "The Test Project must be setup before invoking \(#function)") + + var newRun = NewRun(assignedtoId: nil, + caseIds: testProject.cases.flatMap { $0.id }, + description: nil, + includeAll: false, + milestoneId: nil, + name: "Test Add: Run Name", + suiteId: nil) + + if testProject.project.suiteMode != .singleSuite { + newRun.suiteId = testProject.suites[0].id + } + + switch properties { + case .requiredProperties: + break + case .requiredAndOptionalProperties: + newRun.assignedtoId = testProject.user.id + newRun.description = "Test Add: Run Description" + newRun.milestoneId = testProject.milestones[0].id + } + + return newRun + } + + func newSection(with properties: Properties) -> NewSection { + + precondition(testProject != nil, "The Test Project must be setup before invoking \(#function)") + + var newSection = NewSection(description: nil, + name: "Test Add: Section Name", + parentId: nil, + suiteId: nil) + + if testProject.project.suiteMode != .singleSuite { + newSection.suiteId = testProject.suites[0].id + } + + switch properties { + case .requiredProperties: + break + case .requiredAndOptionalProperties: + newSection.description = "Test Add: Section Description" + newSection.parentId = nil // Caller can set if necessary. + } + + return newSection + } + + func newSuite(with properties: Properties) -> NewSuite { + + precondition(testProject != nil, "The Test Project must be setup before invoking \(#function)") + + var newSuite = NewSuite(description: nil, + name: "Test Add: Suite Name") + + switch properties { + case .requiredProperties: + break + case .requiredAndOptionalProperties: + newSuite.description = "Test Add: Suite Description" + } + + return newSuite + } + + func updatePlanEntryRuns() -> UpdatePlanEntryRuns { + return UpdatePlanEntryRuns(assignedtoId: testProject.user.id, + caseIds: [testProject.cases[objectCount - 1].id], + description: "Test Update: Plan.Entry Run Description", + includeAll: false) + } + +} + +// MARK: - Object Tests + +extension ObjectAPITests { + + // MARK: Case + + func testAddCase() { + + var newCase1 = newCase(with: .requiredProperties) + newCase1.title += ": \(#function)" + assertAddCase(newCase1, to: testProject.sections[0]) + + var newCase2 = newCase(with: .requiredProperties) + newCase2.title += ": \(#function)" + assertAddCase(newCase(with: .requiredAndOptionalProperties), to: testProject.sections[0]) + } + + func testDeleteCase() { + + continueAfterFailure = false + var newCase = self.newCase(with: .requiredAndOptionalProperties) + newCase.title += ": \(#function)" + guard let `case` = assertAddCase(newCase, to: testProject.sections[0]) else { return } + continueAfterFailure = true + + assertDeleteCase(`case`) + } + + func testGetCase() { + + continueAfterFailure = false + var newCase = self.newCase(with: .requiredAndOptionalProperties) + newCase.title += ": \(#function)" + guard let `case` = assertAddCase(newCase, to: testProject.sections[0]) else { return } + continueAfterFailure = true + + assertGetCase(`case`.id) + } + + func testGetCases() { + + continueAfterFailure = false + var newCase1 = newCase(with: .requiredProperties) + var newCase2 = newCase(with: .requiredAndOptionalProperties) + newCase1.title += ": \(#function)" + newCase2.title += ": \(#function)" + guard let case1 = assertAddCase(newCase1, to: testProject.sections[0]) else { return } + guard let case2 = assertAddCase(newCase2, to: testProject.sections[0]) else { return } + let addedCases = [case1, case2] + continueAfterFailure = true + + // Unfiltered + + if let cases = assertGetCases(in: testProject.project, in: testProject.suites[0], in: testProject.sections[0], filteredBy: nil) { + XCTAssertGreaterThanOrEqual(cases.count, addedCases.count) + for addedCase in addedCases { + XCTAssertEqual(cases.filter({ $0.id == addedCase.id }).count, 1, "Added Case \(addedCase.id) was not returned when getting all cases: \(cases)") + } + } + + // Filtered + + let priorityIds = [1, 2] + let filters = [Filter(named: "priority_id", matching: priorityIds)] + + if let cases = assertGetCases(in: testProject.project, in: testProject.suites[0], in: testProject.sections[0], filteredBy: filters) { + for `case` in cases { + XCTAssertEqual(priorityIds.filter({ $0 == `case`.priorityId }).count, 1, "Case \(`case`.id) did not match filter for priorityIds: \(priorityIds)") + } + } + } + + func testUpdateCase() { + + continueAfterFailure = false + var newCase = self.newCase(with: .requiredAndOptionalProperties) + newCase.title += ": \(#function)" + guard var `case` = assertAddCase(newCase, to: testProject.sections[0]) else { return } + continueAfterFailure = true + + `case`.estimate = "10m" + `case`.milestoneId = nil // Marked as inactive for the project so unable to update. + `case`.priorityId = 2 + `case`.refs = "RF-1001, RF-1002" + `case`.templateId = testProject.templates[1].id + `case`.title = "Test Update: Case Title: \(#function)" + `case`.typeId = 2 + + assertUpdateCase(`case`) + } + + // MARK: CaseField + + func testGetCaseFields() { + assertGetCaseFields() + } + + // MARK: CaseType + + func testGetCaseTypes() { + assertGetCaseTypes() + } + + // MARK: Configuration + + func testAddConfiguration() { + var newConfiguration = self.newConfiguration() + newConfiguration.name += ": \(#function)" + assertAddConfiguration(newConfiguration, to: testProject.configurationGroups[0]) + } + + func testDeleteConfiguration() { + + continueAfterFailure = false + var newConfiguration = self.newConfiguration() + newConfiguration.name += ": \(#function)" + guard let configuration = assertAddConfiguration(newConfiguration, to: testProject.configurationGroups[0]) else { return } + continueAfterFailure = true + + assertDeleteConfiguration(configuration) + } + + func testUpdateConfiguration() { + + continueAfterFailure = false + var newConfiguration = self.newConfiguration() + newConfiguration.name += ": \(#function)" + guard var configuration = assertAddConfiguration(newConfiguration, to: testProject.configurationGroups[0]) else { return } + continueAfterFailure = true + + configuration.name = "Test Update: Configuration Name: \(#function)" + + assertUpdateConfiguration(configuration) + } + + // MARK: ConfigurationGroup + + func testAddConfigurationGroup() { + var newConfigurationGroup = self.newConfigurationGroup() + newConfigurationGroup.name += ": \(#function)" + assertAddConfigurationGroup(newConfigurationGroup, to: testProject.project) + } + + func testDeleteConfigurationGroup() { + + continueAfterFailure = false + var newConfigurationGroup = self.newConfigurationGroup() + newConfigurationGroup.name += ": \(#function)" + guard let configurationGroup = assertAddConfigurationGroup(newConfigurationGroup, to: testProject.project) else { return } + continueAfterFailure = true + + assertDeleteConfigurationGroup(configurationGroup) + } + + func testGetConfigurationGroups() { + if let configurationGroups = assertGetConfigurationGroups() { + for configurationGroup in testProject.configurationGroups { + XCTAssertEqual(configurationGroups.filter({ $0.id == configurationGroup.id }).count, 1, "ConfigurationGroup \(configurationGroup.id) was not returned when getting all configuration group's in all projects: \(configurationGroups)") + } + } + } + + func testGetConfigurationGroupsInProject() { + if let configurationGroups = assertGetConfigurationGroups(in: testProject.project) { + for configurationGroup in testProject.configurationGroups { + XCTAssertEqual(configurationGroups.filter({ $0.id == configurationGroup.id }).count, 1, "ConfigurationGroup \(configurationGroup.id) was not returned when getting all configuration group's in project \(testProject.project.id): \(configurationGroups)") + } + } + } + + func testUpdateConfigurationGroup() { + + continueAfterFailure = false + var newConfigurationGroup = self.newConfigurationGroup() + newConfigurationGroup.name += ": \(#function)" + guard var configurationGroup = assertAddConfigurationGroup(newConfigurationGroup, to: testProject.project) else { return } + continueAfterFailure = true + + configurationGroup.name = "Test Update: ConfigurationGroup Name" + + assertUpdateConfigurationGroup(configurationGroup) + } + + // MARK: Milestone + + func testAddMilestone() { + var newMilestone1 = newMilestone(with: .requiredProperties) + var newMilestone2 = newMilestone(with: .requiredAndOptionalProperties) + newMilestone1.name += ": \(#function)" + newMilestone2.name += ": \(#function)" + assertAddMilestone(newMilestone1, to: testProject.project) + assertAddMilestone(newMilestone2, to: testProject.project) + } + + func testDeleteMilestone() { + + continueAfterFailure = false + var newMilestone = self.newMilestone(with: .requiredAndOptionalProperties) + newMilestone.name += ": \(#function)" + guard let milestone = assertAddMilestone(newMilestone, to: testProject.project) else { return } + continueAfterFailure = true + + assertDeleteMilestone(milestone) + } + + func testGetMilestone() { + assertGetMilestone(testProject.milestones[0].id) + } + + func testGetMilestones() { + + // Unfiltered + + if let milestones = assertGetMilestones(in: testProject.project, filteredBy: nil) { + XCTAssertGreaterThanOrEqual(milestones.count, testProject.milestones.count) + for milestone in testProject.milestones { + XCTAssertEqual(milestones.filter({ $0.id == milestone.id }).count, 1, "Milestone \(milestone.id) was not returned when getting all milestones: \(milestones)") + } + } + + // Filtered + + let filters = [Filter(named: "is_completed", matching: false)] + + if let milestones = assertGetMilestones(in: testProject.project, filteredBy: filters) { + for milestone in milestones { + XCTAssertEqual(milestone.isCompleted, false) + } + } + } + + func testUpdateMilestone() { + + continueAfterFailure = false + var newMilestone1 = newMilestone(with: .requiredProperties) + var newMilestone2 = newMilestone(with: .requiredProperties) + var newMilestone3 = newMilestone(with: .requiredProperties) + newMilestone1.name += ": \(#function)" + newMilestone2.name += ": \(#function)" + newMilestone3.name += ": \(#function)" + guard var milestone1 = assertAddMilestone(newMilestone1, to: testProject.project) else { return } + guard var milestone2 = assertAddMilestone(newMilestone2, to: testProject.project) else { return } + guard var milestone3 = assertAddMilestone(newMilestone3, to: testProject.project) else { return } + continueAfterFailure = true + + /* + - A Milestone that isStarted cannot have a startOn date. + - A Milestone with a startOn data cannot have isStarted set to true. + + If those rules are not followed TestRail will return a 400 error: + + "Milestone start date given but not marked as scheduled/upcoming." + */ + + // Not Started + + let date1 = Date() + milestone1.description = "Test Update: Milestone Description 1" + milestone1.dueOn = Date(timeInterval: 1000, since: date1) + milestone1.isCompleted = false + milestone1.isStarted = false // Must be false since startOn is not nil. + milestone1.name = "Test Update: Milestone Name 1: \(#function)" + milestone1.parentId = testProject.milestones[0].id + milestone1.startOn = date1 + + assertUpdateMilestone(milestone1) + + // Started + + let date2 = Date() + milestone2.description = "Test Update: Milestone Description 2" + milestone2.dueOn = Date(timeInterval: 1000, since: date2) + milestone2.isCompleted = false + milestone2.isStarted = true + milestone2.name = "Test Update: Milestone Name 2: \(#function)" + milestone2.parentId = testProject.milestones[0].id + milestone2.startOn = nil // Must be nil since isStarted is true. + + assertUpdateMilestone(milestone2) + + // Completed + + let date3 = Date() + milestone3.description = "Test Update: Milestone Description 3" + milestone3.dueOn = Date(timeInterval: 1000, since: date3) + milestone3.isCompleted = true + milestone3.isStarted = true + milestone3.name = "Test Update: Milestone Name 3: \(#function)" + milestone3.parentId = testProject.milestones[0].id + milestone3.startOn = nil + + assertUpdateMilestone(milestone3) + } + + // MARK: Plan + + func testAddPlan() { + + var newPlan1 = self.newPlan(with: .requiredProperties) + newPlan1.name += ": \(#function)" + assertAddPlan(newPlan1, to: testProject.project) + + var newPlan2 = self.newPlan(with: .requiredAndOptionalProperties) + newPlan2.name += ": \(#function)" + assertAddPlan(newPlan2, to: testProject.project) + } + + func testClosePlan() { + + continueAfterFailure = false + var newPlan = self.newPlan(with: .requiredAndOptionalProperties) + newPlan.name += ": \(#function)" + guard let plan = assertAddPlan(newPlan, to: testProject.project) else { return } + continueAfterFailure = true + + assertClosePlan(plan) + } + + func testDeletePlan() { + + continueAfterFailure = false + var newPlan = self.newPlan(with: .requiredAndOptionalProperties) + newPlan.name += ": \(#function)" + guard let plan = assertAddPlan(newPlan, to: testProject.project) else { return } + continueAfterFailure = true + + assertDeletePlan(plan) + } + + func testGetPlan() { + assertGetPlan(testProject.plans[0].id) + } + + func testGetPlans() { + + // Unfiltered + + if let plans = assertGetPlans(in: testProject.project, filteredBy: nil) { + for plan in testProject.plans { + XCTAssertEqual(plans.filter({ $0.id == plan.id }).count, 1, "Plan \(plan.id) was not returned when getting all plans: \(plans)") + } + } + + // Filtered + + // The limit/offset filters can be combined to paginate a request. + let limit = 1 + let filters = [Filter(named: "limit", matching: limit), + Filter(named: "offset", matching: 1)] + + if let plans = assertGetPlans(in: testProject.project, filteredBy: filters) { + XCTAssertLessThanOrEqual(plans.count, limit) + } + } + + func testUpdatePlan() { + + continueAfterFailure = false + var newPlan = NewPlan(description: "Test Add: Plan Description", entries: nil, milestoneId: nil, name: "Test Add: Plan Name") + newPlan.name += ": \(#function)" + guard var plan = assertAddPlan(newPlan, to: testProject.project) else { return } + continueAfterFailure = true + + plan.description = "Test Update: Plan Description" + plan.milestoneId = testProject.milestones[0].id + plan.name = "Test Update: Plan Name: \(#function)" + + assertUpdatePlan(plan) + } + + // MARK: Plan.Entry + + func testAddPlanEntry() { + var newPlanEntry1 = newPlanEntry(with: .requiredProperties) + var newPlanEntry2 = newPlanEntry(with: .requiredAndOptionalProperties) + newPlanEntry1.name? += ": \(#function)" + newPlanEntry2.name? += ": \(#function)" + assertAddPlanEntry(newPlanEntry1, to: testProject.plans[0]) + assertAddPlanEntry(newPlanEntry2, to: testProject.plans[1]) + } + + func testDeletePlanEntry() { + + continueAfterFailure = false + let plan = testProject.plans[0] + var newPlanEntry = self.newPlanEntry(with: .requiredAndOptionalProperties) + newPlanEntry.name? += ": \(#function)" + guard let planEntry = assertAddPlanEntry(newPlanEntry, to: plan) else { return } + continueAfterFailure = true + + assertDeletePlanEntry(planEntry, from: plan) + } + + func testUpdatePlanEntry() { + + continueAfterFailure = false + let plan = testProject.plans[0] + var newPlanEntry1 = newPlanEntry(with: .requiredAndOptionalProperties) + var newPlanEntry2 = newPlanEntry(with: .requiredAndOptionalProperties) + newPlanEntry1.name? += ": \(#function)" + newPlanEntry2.name? += ": \(#function)" + guard let planEntry1 = assertAddPlanEntry(newPlanEntry1, to: plan) else { return } + guard let planEntry2 = assertAddPlanEntry(newPlanEntry2, to: plan) else { return } + continueAfterFailure = true + + // Plan.Entry Only + + assertUpdatePlanEntry(planEntry1, in: plan) + + // Plan.Entry and Runs + + // NOTE: This does not appear to change anything on TestRail even though + // the API call succeeds. + + var updatedRuns = updatePlanEntryRuns() + updatedRuns.description? += ": \(#function)" + + assertUpdatePlanEntry(planEntry2, in: plan, with: updatedRuns) + } + + // MARK: Priority + + func testGetPriorities() { + assertGetPriorities() + } + + // MARK: Project + + func testAddProject() { + + var newProject1 = newProject(with: .requiredProperties) + newProject1.name += ": \(#function)" + if let project = assertAddProject(newProject1) { + assertDeleteProject(project) + } + + var newProject2 = newProject(with: .requiredAndOptionalProperties) + newProject2.name += ": \(#function)" + if let project = assertAddProject(newProject2) { + assertDeleteProject(project) + } + } + + func testDeleteProject() { + + continueAfterFailure = false + var newProject = self.newProject(with: .requiredAndOptionalProperties) + newProject.name += ": \(#function)" + guard let project = assertAddProject(newProject) else { return } + continueAfterFailure = true + + assertDeleteProject(project) + } + + func testGetProject() { + assertGetProject(testProject.project.id) + } + + func testGetProjects() { + assertGetProjects() + } + + func testUpdateProject() { + + continueAfterFailure = false + var newProject = self.newProject(with: .requiredAndOptionalProperties) + newProject.name += ": \(#function)" + guard var project = assertAddProject(newProject) else { return } + continueAfterFailure = true + + defer { + continueAfterFailure = true + assertDeleteProject(project) // Cleanup when complete. + } + + project.announcement = "QuizTrainTests: Test Update: Project Annoucement" + project.isCompleted = true + project.name = "QuizTrainTests: Test Update: Project Name" + project.showAnnouncement = true + // Updating project.suiteMode does not appear to work, so that is ommitted from this test. + + assertUpdateProject(project) + } + + // MARK: Result + + func testAddResult() { + let newResult1 = newResult(with: .requiredProperties) + let newResult2 = newResult(with: .requiredAndOptionalProperties) + assertAddResult(newResult1, to: testProject.tests[0]) + assertAddResult(newResult2, to: testProject.tests[0]) + } + + func testAddResultForCase() { + let newResult1 = newResult(with: .requiredProperties) + let newResult2 = newResult(with: .requiredAndOptionalProperties) + assertAddResultForCase(newResult1, to: testProject.runs[0], to: testProject.cases[0]) + assertAddResultForCase(newResult2, to: testProject.runs[0], to: testProject.cases[0]) + } + + func testAddResults() { + + // AssignedtoId + + var newTestResults = self.newTestResults(with: .requiredProperties) + + newTestResults.results = newTestResults.results.map { + var result = $0 + result.assignedtoId = testProject.user.id + return result + } + + XCTAssertTrue(newTestResults.isValid) + assertAddResults(newTestResults, to: testProject.runs[0]) + + // Comment + + newTestResults = self.newTestResults(with: .requiredProperties) + + newTestResults.results = newTestResults.results.map { + var result = $0 + result.comment = "Test Add: Test Comment" + return result + } + + XCTAssertTrue(newTestResults.isValid) + assertAddResults(newTestResults, to: testProject.runs[0]) + + // StatusId + + newTestResults = self.newTestResults(with: .requiredProperties) + + newTestResults.results = newTestResults.results.map { + var result = $0 + result.statusId = 1 + return result + } + + XCTAssertTrue(newTestResults.isValid) + assertAddResults(newTestResults, to: testProject.runs[0]) + + // All of the above. + + newTestResults = self.newTestResults(with: .requiredAndOptionalProperties) + + XCTAssertTrue(newTestResults.isValid) + assertAddResults(newTestResults, to: testProject.runs[0]) + } + + func testAddResultsForCases() { + + // AssignedtoId + + var newCaseResults = self.newCaseResults(with: .requiredProperties) + + newCaseResults.results = newCaseResults.results.map { + var result = $0 + result.assignedtoId = testProject.user.id + return result + } + + XCTAssertTrue(newCaseResults.isValid) + assertAddResultsForCases(newCaseResults, to: testProject.runs[0]) + + // Comment + + newCaseResults = self.newCaseResults(with: .requiredProperties) + + newCaseResults.results = newCaseResults.results.map { + var result = $0 + result.comment = "Test Add: Test Comment" + return result + } + + XCTAssertTrue(newCaseResults.isValid) + assertAddResultsForCases(newCaseResults, to: testProject.runs[0]) + + // StatusId + + newCaseResults = self.newCaseResults(with: .requiredProperties) + + newCaseResults.results = newCaseResults.results.map { + var result = $0 + result.statusId = 1 + return result + } + + XCTAssertTrue(newCaseResults.isValid) + assertAddResultsForCases(newCaseResults, to: testProject.runs[0]) + + // All of the above. + + newCaseResults = self.newCaseResults(with: .requiredAndOptionalProperties) + + XCTAssertTrue(newCaseResults.isValid) + assertAddResultsForCases(newCaseResults, to: testProject.runs[0]) + } + + func testGetResultsForTest() { + assertGetResultsForTest(testProject.tests[0]) + } + + func testGetResultsForCase() { + + // Unfiltered + + assertGetResultsForCase(testProject.cases[0], in: testProject.runs[0], filteredBy: nil) + + // Filtered + + let statusId = 1 + let filters = [Filter(named: "status_id", matching: statusId)] + + if let results = assertGetResultsForCase(testProject.cases[0], in: testProject.runs[0], filteredBy: filters) { + for result in results { + XCTAssertEqual(result.statusId, statusId, "Result \(result.id) did not match filter for status_id: \(statusId)") + } + } + } + + func testGetResultsForRun() { + + // Unfiltered + + assertGetResultsForRun(testProject.runs[0], filteredBy: nil) + + // Filtered + + let statusId = 1 + let filters = [Filter(named: "status_id", matching: statusId)] + + if let results = assertGetResultsForRun(testProject.runs[0], filteredBy: filters) { + for result in results { + XCTAssertEqual(result.statusId, statusId, "Result \(result.id) did not match filter for status_id: \(statusId)") + } + } + } + + // MARK: ResultField + + func testGetResultFields() { + assertGetResultFields() + } + + // MARK: Run + + func testAddRun() { + var newRun1 = newRun(with: .requiredProperties) + var newRun2 = newRun(with: .requiredAndOptionalProperties) + newRun1.name += ": \(#function)" + newRun2.name += ": \(#function)" + assertAddRun(newRun1, to: testProject.project) + assertAddRun(newRun2, to: testProject.project) + } + + func testCloseRun() { + + continueAfterFailure = false + var newRun = self.newRun(with: .requiredAndOptionalProperties) + newRun.name += ": \(#function)" + guard let run = assertAddRun(newRun, to: testProject.project) else { return } + continueAfterFailure = true + + assertCloseRun(run) + } + + func testDeleteRun() { + + continueAfterFailure = false + var newRun = self.newRun(with: .requiredAndOptionalProperties) + newRun.name += ": \(#function)" + guard let run = assertAddRun(newRun, to: testProject.project) else { return } + continueAfterFailure = true + + assertDeleteRun(run) + } + + func testGetRun() { + assertGetRun(testProject.runs[0].id) + } + + func testGetRuns() { + + // Unfiltered + + assertGetRuns(in: testProject.project, filteredBy: nil) + + // Filtered + + let isCompleted = false + let filters = [Filter(named: "is_completed", matching: isCompleted)] + + if let runs = assertGetRuns(in: testProject.project, filteredBy: filters) { + for run in runs { + XCTAssertEqual(run.isCompleted, isCompleted) + } + } + } + + func testUpdateRun() { + + continueAfterFailure = false + var newRun = self.newRun(with: .requiredAndOptionalProperties) + newRun.name += ": \(#function)" + guard var run = assertAddRun(newRun, to: testProject.project) else { return } + continueAfterFailure = true + + run.description = "Test Update: Run Description" + run.includeAll = true + run.milestoneId = testProject.milestones[1].id + run.name = "Test Update: Run Name: \(#function)" + + assertUpdateRun(run) + } + + // MARK: Section + + func testAddSection() { + var newSection1 = self.newSection(with: .requiredProperties) + var newSection2 = self.newSection(with: .requiredAndOptionalProperties) + newSection1.name += ": \(#function)" + newSection2.name += ": \(#function)" + assertAddSection(newSection1, to: testProject.project) + assertAddSection(newSection2, to: testProject.project) + } + + func testDeleteSection() { + + continueAfterFailure = false + var newSection = self.newSection(with: .requiredAndOptionalProperties) + newSection.name += ": \(#function)" + guard let section = assertAddSection(newSection, to: testProject.project) else { return } + continueAfterFailure = true + + assertDeleteSection(section) + } + + func testGetSection() { + assertGetSection(testProject.sections[0].id) + } + + func testGetSections() { + if let sections = assertGetSections(in: testProject.project, in: testProject.suites[0]) { + for section in sections { + XCTAssertEqual(section.suiteId, testProject.suites[0].id) + } + } + } + + func testUpdateSection() { + + continueAfterFailure = false + var newSection = self.newSection(with: .requiredAndOptionalProperties) + newSection.name += ": \(#function)" + guard var section = assertAddSection(newSection, to: testProject.project) else { return } + continueAfterFailure = true + + section.description = "Section Description - Updated" + section.name = "Section Name - Updated: \(#function)" + + assertUpdateSection(section) + } + + // MARK: Status + + func testGetStatuses() { + assertGetStatuses() + } + + // MARK: Suite + + func testAddSuite() { + var newSuite1 = newSuite(with: .requiredProperties) + var newSuite2 = newSuite(with: .requiredAndOptionalProperties) + newSuite1.name += ": \(#function)" + newSuite2.name += ": \(#function)" + assertAddSuite(newSuite1, to: testProject.project) + assertAddSuite(newSuite2, to: testProject.project) + } + + func testDeleteSuite() { + + continueAfterFailure = false + var newSuite = self.newSuite(with: .requiredAndOptionalProperties) + newSuite.name += ": \(#function)" + guard let suite = assertAddSuite(newSuite, to: testProject.project) else { return } + continueAfterFailure = true + + assertDeleteSuite(suite) + } + + func testGetSuite() { + assertGetSuite(testProject.suites[0].id) + } + + func testGetSuites() { + if let suites = assertGetSuites(in: testProject.project) { + for suite in testProject.suites { + XCTAssertEqual(suites.filter({ $0.id == suite.id }).count, 1, "Suite \(suite.id) was not returned when getting all suite's: \(suites)") + } + } + } + + func testUpdateSuite() { + + continueAfterFailure = false + var newSuite = self.newSuite(with: .requiredAndOptionalProperties) + newSuite.name += ": \(#function)" + guard var suite = assertAddSuite(newSuite, to: testProject.project) else { return } + continueAfterFailure = true + + suite.description = "Test Update: Suite Description" + suite.name = "Test Update: Suite Name" + + assertUpdateSuite(suite) + } + + // MARK: Template + + func testGetTemplates() { + assertGetTemplates() + } + + func testGetTemplatesInProject() { + assertGetTemplates(in: testProject.project) + } + + // MARK: Test + + func testGetTest() { + assertGetTest(testProject.tests[0].id) + } + + func testGetTests() { + + // Unfiltered + + assertGetTests(in: testProject.runs[0], filteredBy: nil) + + // Filtered + + let statusId = 1 + let filters = [Filter(named: "status_id", matching: statusId)] + + if let tests = assertGetTests(in: testProject.runs[0], filteredBy: filters) { + for test in tests { + XCTAssertEqual(test.statusId, statusId) + } + } + } + + // MARK: User + + func testGetUser() { + + continueAfterFailure = false + guard let user = assertGetUsers()?.first else { return } + continueAfterFailure = true + + assertGetUser(user.id) + } + + func testGetUserByEmail() { + + continueAfterFailure = false + guard let user = assertGetUsers()?.first else { return } + continueAfterFailure = true + + assertGetUserByEmail(user.email) + } + + func testGetUsers() { + if let users = assertGetUsers() { + XCTAssertEqual(users.filter({ $0.email == objectAPI.api.username }).count, 1, "User \(objectAPI.api.username) was not returned when getting all users: \(users)") + } + } + +} + +// MARK: - Object Matching Tests + +extension ObjectAPITests { + + // MARK: Case + + func testGetCaseTypeMatchingId() { + + continueAfterFailure = false + guard let caseTypes = assertGetCaseTypes() else { return } + XCTAssertGreaterThan(caseTypes.count, 0, "This test cannot continue because there are no CaseType's.") + continueAfterFailure = true + + guard let randomCaseType = caseTypes.randomElement else { return } + + assertGetCaseTypeMatchingId(randomCaseType.id) + } + + // MARK: ConfigurationGroup + + func testGetConfigurationGroupMatchingId() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.configurationGroups.count, 0, "This test cannot continue because there are no ConfigurationGroup's.") + continueAfterFailure = true + + guard let randomConfigurationGroup = testProject.configurationGroups.randomElement else { return } + + assertGetConfigurationGroupMatchingId(randomConfigurationGroup.id) + } + + // MARK: Priority + + func testGetPriorityMatchingId() { + + continueAfterFailure = false + guard let priorities = assertGetPriorities() else { return } + XCTAssertGreaterThan(priorities.count, 0, "This test cannot continue because there are no Priorities.") + continueAfterFailure = true + + guard let randomPriority = priorities.randomElement else { return } + + assertGetPriorityMatchingId(randomPriority.id) + } + + // MARK: Status + + func testGetStatusMatchingId() { + + continueAfterFailure = false + guard let statuses = assertGetStatuses() else { return } + XCTAssertGreaterThan(statuses.count, 0, "This test cannot continue because there are no Statuses.") + continueAfterFailure = true + + guard let randomStatus = statuses.randomElement else { return } + + assertGetStatusMatchingId(randomStatus.id) + } + + // MARK: Template + + func testGetTemplateMatchingId() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.templates.count, 0, "This test cannot continue because there are no Templates.") + continueAfterFailure = true + + guard let randomTemplate = testProject.templates.randomElement else { return } + + assertGetTemplateMatchingId(randomTemplate.id) + } + + func testGetTemplatesMatchingIds() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.templates.count, 0, "This test cannot continue because there are no Templates.") + continueAfterFailure = true + + // Pick up to 3 template ids randomly. + + var allTemplates = testProject.templates + var randomTemplates = [Template]() + + if allTemplates.count > 2 { + for _ in 0..<3 { + let randomIndex = Int(arc4random_uniform(UInt32(allTemplates.count))) + randomTemplates.append(allTemplates[randomIndex]) + allTemplates.remove(at: randomIndex) + } + } else { + randomTemplates.append(contentsOf: allTemplates) + } + + let randomTemplateIds = randomTemplates.flatMap({ $0.id }) + + assertGetTemplatesMatchingIds(randomTemplateIds) + } + +} + +// MARK: - Object Forward Relationship Tests + +extension ObjectAPITests { + + // MARK: Case + + func testGetCaseToCreatedByRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.cases.count, 0, "This test cannot continue because there are no Cases.") + continueAfterFailure = true + + for `case` in testProject.cases { + assertGetCaseToCreatedByRelationship(`case`) + assertGetCaseToCreatedByRelationship(`case`, usingObjectToRelationshipMethod: true) + } + } + + func testGetCaseToMilestoneRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.cases.count, 0, "This test cannot continue because there are no Cases.") + continueAfterFailure = true + + for `case` in testProject.cases { + assertGetCaseToMilestoneRelationship(`case`) + assertGetCaseToMilestoneRelationship(`case`, usingObjectToRelationshipMethod: true) + } + } + + func testGetCaseToPriorityRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.cases.count, 0, "This test cannot continue because there are no Cases.") + continueAfterFailure = true + + for `case` in testProject.cases { + assertGetCaseToPriorityRelationship(`case`) + assertGetCaseToPriorityRelationship(`case`, usingObjectToRelationshipMethod: true) + } + } + + func testGetCaseToSectionRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.cases.count, 0, "This test cannot continue because there are no Cases.") + continueAfterFailure = true + + for `case` in testProject.cases { + assertGetCaseToSectionRelationship(`case`) + assertGetCaseToSectionRelationship(`case`, usingObjectToRelationshipMethod: true) + } + } + + func testGetCaseToSuiteRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.cases.count, 0, "This test cannot continue because there are no Cases.") + continueAfterFailure = true + + for `case` in testProject.cases { + assertGetCaseToSuiteRelationship(`case`) + assertGetCaseToSuiteRelationship(`case`, usingObjectToRelationshipMethod: true) + } + } + + func testGetCaseToTemplateRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.cases.count, 0, "This test cannot continue because there are no Cases.") + continueAfterFailure = true + + for `case` in testProject.cases { + assertGetCaseToTemplateRelationship(`case`) + assertGetCaseToTemplateRelationship(`case`, usingObjectToRelationshipMethod: true) + } + } + + func testGetCaseToTypeRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.cases.count, 0, "This test cannot continue because there are no Cases.") + continueAfterFailure = true + + for `case` in testProject.cases { + assertGetCaseToTypeRelationship(`case`) + assertGetCaseToTypeRelationship(`case`, usingObjectToRelationshipMethod: true) + } + } + + func testGetCaseToUpdatedByRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.cases.count, 0, "This test cannot continue because there are no Cases.") + continueAfterFailure = true + + for `case` in testProject.cases { + assertGetCaseToUpdatedByRelationship(`case`) + assertGetCaseToUpdatedByRelationship(`case`, usingObjectToRelationshipMethod: true) + } + } + + // MARK: CaseField + + func testGetCaseFieldToTemplatesRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.caseFields.count, 0, "This test cannot continue because there are no CaseFields.") + continueAfterFailure = true + + for caseField in testProject.caseFields { + assertGetCaseFieldToTemplatesRelationship(caseField) + assertGetCaseFieldToTemplatesRelationship(caseField, usingObjectToRelationshipMethod: true) + } + } + + // MARK: CaseField.Config + + func testGetCaseFieldConfigToAccessibleProjectsRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.caseFields.count, 0, "This test cannot continue because there are no CaseFields.") + continueAfterFailure = true + + for caseField in testProject.caseFields { + for config in caseField.configs { + assertGetConfigToAccessibleProjectsRelationship(config) + assertGetConfigToAccessibleProjectsRelationship(config, usingObjectToRelationshipMethod: true) + } + } + } + + func testGetCaseFieldConfigToProjectsRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.caseFields.count, 0, "This test cannot continue because there are no CaseFields.") + continueAfterFailure = true + + for caseField in testProject.caseFields { + for config in caseField.configs { + assertGetConfigToProjectsRelationship(config) + assertGetConfigToProjectsRelationship(config, usingObjectToRelationshipMethod: true) + } + } + } + + // MARK: Configuration + + func testGetConfigurationToConfigurationGroupRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.configurations.count, 0, "This test cannot continue because there are no Configurations.") + continueAfterFailure = true + + for configuration in testProject.configurations { + assertGetConfigurationToConfigurationGroupRelationship(configuration) + assertGetConfigurationToConfigurationGroupRelationship(configuration, usingObjectToRelationshipMethod: true) + } + } + + // MARK: ConfigurationGroup + + func testGetConfigurationGroupToProjectRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.configurationGroups.count, 0, "This test cannot continue because there are no ConfigurationGroups.") + continueAfterFailure = true + + for configurationGroup in testProject.configurationGroups { + assertGetConfigurationGroupToProjectRelationship(configurationGroup) + assertGetConfigurationGroupToProjectRelationship(configurationGroup, usingObjectToRelationshipMethod: true) + } + } + + // MARK: Milestone + + func testGetMilestoneToParentRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.milestones.count, 0, "This test cannot continue because there are no Milestones.") + continueAfterFailure = true + + for milestone in testProject.milestones { + assertGetMilestoneToParentRelationship(milestone) + assertGetMilestoneToParentRelationship(milestone, usingObjectToRelationshipMethod: true) + } + } + + func testGetMilestoneToProjectRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.milestones.count, 0, "This test cannot continue because there are no Milestones.") + continueAfterFailure = true + + for milestone in testProject.milestones { + assertGetMilestoneToProjectRelationship(milestone) + assertGetMilestoneToProjectRelationship(milestone, usingObjectToRelationshipMethod: true) + } + } + + // MARK: Plan + + func testGetPlanToAssignedtoRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.plans.count, 0, "This test cannot continue because there are no Plans.") + continueAfterFailure = true + + for plan in testProject.plans { + assertGetPlanToAssignedtoRelationship(plan) + assertGetPlanToAssignedtoRelationship(plan, usingObjectToRelationshipMethod: true) + } + } + + func testGetPlanToCreatedByRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.plans.count, 0, "This test cannot continue because there are no Plans.") + continueAfterFailure = true + + for plan in testProject.plans { + assertGetPlanToCreatedByRelationship(plan) + assertGetPlanToCreatedByRelationship(plan, usingObjectToRelationshipMethod: true) + } + } + + func testGetPlanToMilestoneRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.plans.count, 0, "This test cannot continue because there are no Plans.") + continueAfterFailure = true + + for plan in testProject.plans { + assertGetPlanToMilestoneRelationship(plan) + assertGetPlanToMilestoneRelationship(plan, usingObjectToRelationshipMethod: true) + } + } + + func testGetPlanToProjectRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.plans.count, 0, "This test cannot continue because there are no Plans.") + continueAfterFailure = true + + for plan in testProject.plans { + assertGetPlanToProjectRelationship(plan) + assertGetPlanToProjectRelationship(plan, usingObjectToRelationshipMethod: true) + } + } + + // MARK: Plan.Entry + + func testGetPlanEntryToSuiteRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.plans.count, 0, "This test cannot continue because there are no Plans.") + continueAfterFailure = true + + for plan in testProject.plans { + if let planEntries = plan.entries { + for planEntry in planEntries { + assertGetPlanEntryToSuiteRelationship(planEntry) + assertGetPlanEntryToSuiteRelationship(planEntry, usingObjectToRelationshipMethod: true) + } + } + } + } + + // MARK: Result + + func testGetResultToAssignedtoRelationship() { + + continueAfterFailure = false + let newTestResults = self.newTestResults(with: .requiredAndOptionalProperties) + guard let results = assertAddResults(newTestResults, to: testProject.runs[0]) else { return } + continueAfterFailure = true + + for result in results { + assertGetResultToAssignedtoRelationship(result) + assertGetResultToAssignedtoRelationship(result, usingObjectToRelationshipMethod: true) + } + } + + func testGetResultToCreatedByRelationship() { + + continueAfterFailure = false + let newTestResults = self.newTestResults(with: .requiredAndOptionalProperties) + guard let results = assertAddResults(newTestResults, to: testProject.runs[0]) else { return } + continueAfterFailure = true + + for result in results { + assertGetResultToCreatedByRelationship(result) + assertGetResultToCreatedByRelationship(result, usingObjectToRelationshipMethod: true) + } + } + + func testGetResultToStatusRelationship() { + + continueAfterFailure = false + let newTestResults = self.newTestResults(with: .requiredAndOptionalProperties) + guard let results = assertAddResults(newTestResults, to: testProject.runs[0]) else { return } + continueAfterFailure = true + + for result in results { + assertGetResultToStatusRelationship(result) + assertGetResultToStatusRelationship(result, usingObjectToRelationshipMethod: true) + } + } + + func testGetResultToTestRelationship() { + + continueAfterFailure = false + let newTestResults = self.newTestResults(with: .requiredAndOptionalProperties) + guard let results = assertAddResults(newTestResults, to: testProject.runs[0]) else { return } + continueAfterFailure = true + + for result in results { + assertGetResultToTestRelationship(result) + assertGetResultToTestRelationship(result, usingObjectToRelationshipMethod: true) + } + } + + // MARK: ResultField + + func testGetResultFieldToTemplatesRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.resultFields.count, 0, "This test cannot continue because there are no ResultFields.") + continueAfterFailure = true + + for resultField in testProject.resultFields { + assertGetResultFieldToTemplatesRelationship(resultField) + assertGetResultFieldToTemplatesRelationship(resultField, usingObjectToRelationshipMethod: true) + } + } + + // MARK: ResultField.Config + + func testGetResultFieldConfigToAccessibleProjectsRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.resultFields.count, 0, "This test cannot continue because there are no CaseFields.") + continueAfterFailure = true + + for resultField in testProject.resultFields { + for config in resultField.configs { + assertGetConfigToAccessibleProjectsRelationship(config) + assertGetConfigToAccessibleProjectsRelationship(config, usingObjectToRelationshipMethod: true) + } + } + } + + func testGetResultFieldConfigToProjectsRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.resultFields.count, 0, "This test cannot continue because there are no ResultFields.") + continueAfterFailure = true + + for resultField in testProject.resultFields { + for config in resultField.configs { + assertGetConfigToProjectsRelationship(config) + assertGetConfigToProjectsRelationship(config, usingObjectToRelationshipMethod: true) + } + } + } + + // MARK: Run + + func testGetRunToAssignedtoRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.runs.count, 0, "This test cannot continue because there are no Runs.") + continueAfterFailure = true + + for run in testProject.runs { + assertGetRunToAssignedtoRelationship(run) + assertGetRunToAssignedtoRelationship(run, usingObjectToRelationshipMethod: true) + } + } + + func testGetRunToConfigurationsRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.runs.count, 0, "This test cannot continue because there are no Runs.") + continueAfterFailure = true + + for run in testProject.runs { + assertGetRunToConfigurationsRelationship(run) + assertGetRunToConfigurationsRelationship(run, usingObjectToRelationshipMethod: true) + } + } + + func testGetRunToCreatedByRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.runs.count, 0, "This test cannot continue because there are no Runs.") + continueAfterFailure = true + + for run in testProject.runs { + assertGetRunToCreatedByRelationship(run) + assertGetRunToCreatedByRelationship(run, usingObjectToRelationshipMethod: true) + } + } + + func testGetRunToMilestoneRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.runs.count, 0, "This test cannot continue because there are no Runs.") + continueAfterFailure = true + + for run in testProject.runs { + assertGetRunToMilestoneRelationship(run) + assertGetRunToMilestoneRelationship(run, usingObjectToRelationshipMethod: true) + } + } + + func testGetRunToPlanRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.runs.count, 0, "This test cannot continue because there are no Runs.") + continueAfterFailure = true + + for run in testProject.runs { + assertGetRunToPlanRelationship(run) + assertGetRunToPlanRelationship(run, usingObjectToRelationshipMethod: true) + } + } + + func testGetRunToProjectRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.runs.count, 0, "This test cannot continue because there are no Runs.") + continueAfterFailure = true + + for run in testProject.runs { + assertGetRunToProjectRelationship(run) + assertGetRunToProjectRelationship(run, usingObjectToRelationshipMethod: true) + } + } + + func testGetRunToSuiteRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.runs.count, 0, "This test cannot continue because there are no Runs.") + continueAfterFailure = true + + for run in testProject.runs { + assertGetRunToSuiteRelationship(run) + assertGetRunToSuiteRelationship(run, usingObjectToRelationshipMethod: true) + } + } + + // MARK: Section + + func testGetSectionToParentRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.sections.count, 0, "This test cannot continue because there are no Sections.") + continueAfterFailure = true + + for section in testProject.sections { + assertGetSectionToParentRelationship(section) + assertGetSectionToParentRelationship(section, usingObjectToRelationshipMethod: true) + } + } + + func testGetSectionToSuiteRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.sections.count, 0, "This test cannot continue because there are no Sections.") + continueAfterFailure = true + + for section in testProject.sections { + assertGetSectionToSuiteRelationship(section) + assertGetSectionToSuiteRelationship(section, usingObjectToRelationshipMethod: true) + } + } + + // MARK: Suite + + func testGetSuiteToProjectRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.suites.count, 0, "This test cannot continue because there are no Suites.") + continueAfterFailure = true + + for suite in testProject.suites { + assertGetSuiteToProjectRelationship(suite) + assertGetSuiteToProjectRelationship(suite, usingObjectToRelationshipMethod: true) + } + } + + // MARK: Test + + func testGetTestToAssignedtoRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.tests.count, 0, "This test cannot continue because there are no Tests.") + continueAfterFailure = true + + for test in testProject.tests { + assertGetTestToAssignedtoRelationship(test) + assertGetTestToAssignedtoRelationship(test, usingObjectToRelationshipMethod: true) + } + } + + func testGetTestToCaseRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.tests.count, 0, "This test cannot continue because there are no Tests.") + continueAfterFailure = true + + for test in testProject.tests { + assertGetTestToCaseRelationship(test) + assertGetTestToCaseRelationship(test, usingObjectToRelationshipMethod: true) + } + } + + func testGetTestToMilestoneRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.tests.count, 0, "This test cannot continue because there are no Tests.") + continueAfterFailure = true + + for test in testProject.tests { + assertGetTestToMilestoneRelationship(test) + assertGetTestToMilestoneRelationship(test, usingObjectToRelationshipMethod: true) + } + } + + func testGetTestToPriorityRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.tests.count, 0, "This test cannot continue because there are no Tests.") + continueAfterFailure = true + + for test in testProject.tests { + assertGetTestToPriorityRelationship(test) + assertGetTestToPriorityRelationship(test, usingObjectToRelationshipMethod: true) + } + } + + func testGetTestToRunRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.tests.count, 0, "This test cannot continue because there are no Tests.") + continueAfterFailure = true + + for test in testProject.tests { + assertGetTestToRunRelationship(test) + assertGetTestToRunRelationship(test, usingObjectToRelationshipMethod: true) + } + } + + func testGetTestToStatusRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.tests.count, 0, "This test cannot continue because there are no Tests.") + continueAfterFailure = true + + for test in testProject.tests { + assertGetTestToStatusRelationship(test) + assertGetTestToStatusRelationship(test, usingObjectToRelationshipMethod: true) + } + } + + func testGetTestToTemplateRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.tests.count, 0, "This test cannot continue because there are no Tests.") + continueAfterFailure = true + + for test in testProject.tests { + assertGetTestToTemplateRelationship(test) + assertGetTestToTemplateRelationship(test, usingObjectToRelationshipMethod: true) + } + } + + func testGetTestToTypeRelationship() { + + continueAfterFailure = false + XCTAssertGreaterThan(testProject.tests.count, 0, "This test cannot continue because there are no Tests.") + continueAfterFailure = true + + for test in testProject.tests { + assertGetTestToTypeRelationship(test) + assertGetTestToTypeRelationship(test, usingObjectToRelationshipMethod: true) + } + } + +} + +// MARK: - Assertions: Helpers + +extension ObjectAPITests { + + // MARK: Outcome + + func assertOutcomeSucceeded(_ outcome: Outcome) -> ObjectType? { + let object: ObjectType? + switch outcome { + case .failed(let error): + XCTFail(error.debugDescription) + object = nil + case .succeeded(let _object): + object = _object + } + return object + } + + func assertOutcomeSucceeded(_ outcome: Outcome) -> ObjectType? { + let object: ObjectType? + switch outcome { + case .failed(let error): + XCTFail(error.debugDescription) + object = nil + case .succeeded(let _object): + object = _object + } + return object + } + + // MARK: CustomFields + + /* + TestRail may add any omitted custom fields when creating a new object. Use + this to methods to assert only provided custom fields during an add + request. + */ + func assertCustomFieldKeyValuePairs(in lhs: CustomFields, existIn rhs: CustomFields) { + for (key, _) in lhs.customFields { + XCTAssertNotNil(rhs.customFields[key]) + } + } + +} + +// MARK: - Assertions: Objects + +extension ObjectAPITests { + + // MARK: Case + + @discardableResult func assertAddCase(_ newCase: NewCase, to section: Section) -> Case? { + + let expectation = XCTestExpectation(description: "Add Case") + + var `case`: Case? = nil + objectAPI.addCase(newCase, to: section) { (outcome) in + `case` = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(`case`) + + if let `case` = `case` { + + XCTAssertEqual(`case`.sectionId, section.id) + XCTAssertEqual(`case`.title, newCase.title) + + // TestRail may assign default values so only assert if nil was not passed in data. + if let value = newCase.estimate { XCTAssertEqual(value, `case`.estimate) } + if let value = newCase.milestoneId { XCTAssertEqual(value, `case`.milestoneId) } + if let value = newCase.priorityId { XCTAssertEqual(value, `case`.priorityId) } + if let value = newCase.refs { XCTAssertEqual(value, `case`.refs) } + if let value = newCase.templateId { XCTAssertEqual(value, `case`.templateId) } + if let value = newCase.typeId { XCTAssertEqual(value, `case`.typeId) } + + assertCustomFieldKeyValuePairs(in: newCase, existIn: `case`) + } + + return `case` + } + + func assertDeleteCase(_ case: Case) { + + let expectation = XCTestExpectation(description: "Delete Case") + + objectAPI.deleteCase(`case`) { (outcome) in + self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + } + + @discardableResult func assertGetCase(_ caseId: Int) -> Case? { + + let expectation = XCTestExpectation(description: "Get Case") + + var `case`: Case? = nil + objectAPI.getCase(caseId) { (outcome) in + `case` = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(`case`) + + if let `case` = `case` { + XCTAssertEqual(`case`.id, caseId) + } + + return `case` + } + + @discardableResult func assertGetCases(in project: Project, in suite: Suite? = nil, in section: Section? = nil, filteredBy filters: [Filter]? = nil) -> [Case]? { + + let expectation = XCTestExpectation(description: "Get Cases") + + var cases: [Case]? = nil + objectAPI.getCases(in: project, in: suite, in: section, filteredBy: filters) { (outcome) in + cases = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(cases) + + if let cases = cases { + for `case` in cases { + XCTAssertEqual(`case`.suiteId, suite?.id) + XCTAssertEqual(`case`.sectionId, section?.id) + } + } + + return cases + } + + @discardableResult func assertUpdateCase(_ case: Case) -> Case? { + + let expectation = XCTestExpectation(description: "Update Case") + + var updatedCase: Case? = nil + objectAPI.updateCase(`case`) { (outcome) in + updatedCase = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(updatedCase) + + if let updatedCase = updatedCase { + // Identity + XCTAssertEqual(updatedCase.id, `case`.id) + // Updates + XCTAssertEqual(updatedCase.estimate, `case`.estimate) + XCTAssertEqual(updatedCase.milestoneId, `case`.milestoneId) + XCTAssertEqual(updatedCase.priorityId, `case`.priorityId) + XCTAssertEqual(updatedCase.refs, `case`.refs) + XCTAssertEqual(updatedCase.templateId, `case`.templateId) + XCTAssertEqual(updatedCase.title, `case`.title) + XCTAssertEqual(updatedCase.typeId, `case`.typeId) + XCTAssertEqual(updatedCase.customFieldsContainer, `case`.customFieldsContainer) + } + + return updatedCase + } + + // MARK: CaseField + + @discardableResult func assertGetCaseFields() -> [CaseField]? { + + let expectation = XCTestExpectation(description: "Get Case Fields") + + var caseFields: [CaseField]? = nil + objectAPI.getCaseFields { (outcome) in + caseFields = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(caseFields) + + return caseFields + } + + // MARK: CaseType + + @discardableResult func assertGetCaseTypes() -> [CaseType]? { + + let expectation = XCTestExpectation(description: "Get Case Types") + + var caseTypes: [CaseType]? = nil + objectAPI.getCaseTypes { (outcome) in + caseTypes = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(caseTypes) + + return caseTypes + } + + // MARK: Configuration + + @discardableResult func assertAddConfiguration(_ newConfiguration: NewConfiguration, to configurationGroup: ConfigurationGroup) -> Configuration? { + + let expectation = XCTestExpectation(description: "Add Configuration") + + var configuration: Configuration? = nil + objectAPI.addConfiguration(newConfiguration, to: configurationGroup) { (outcome) in + configuration = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(configuration) + + if let configuration = configuration { + XCTAssertEqual(configuration.name, newConfiguration.name) + XCTAssertEqual(configuration.groupId, configurationGroup.id) + } + + return configuration + } + + func assertDeleteConfiguration(_ configuration: Configuration) { + + let expectation = XCTestExpectation(description: "Delete Configuration") + + objectAPI.deleteConfiguration(configuration) { (outcome) in + self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + } + + @discardableResult func assertUpdateConfiguration(_ configuration: Configuration) -> Configuration? { + + let expectation = XCTestExpectation(description: "Update Configuration") + + var updatedConfiguration: Configuration? = nil + objectAPI.updateConfiguration(configuration) { (outcome) in + updatedConfiguration = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(updatedConfiguration) + + if let updatedConfiguration = updatedConfiguration { + // Identity + XCTAssertEqual(updatedConfiguration.id, configuration.id) + // Updates + XCTAssertEqual(updatedConfiguration.name, configuration.name) + } + + return updatedConfiguration + } + + // MARK: ConfigurationGroup + + @discardableResult func assertAddConfigurationGroup(_ newConfigurationGroup: NewConfigurationGroup, to project: Project) -> ConfigurationGroup? { + + let expectation = XCTestExpectation(description: "Add Configuration Group") + + var configurationGroup: ConfigurationGroup? = nil + objectAPI.addConfigurationGroup(newConfigurationGroup, to: project) { (outcome) in + configurationGroup = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(configurationGroup) + + if let configurationGroup = configurationGroup { + XCTAssertEqual(configurationGroup.name, newConfigurationGroup.name) + XCTAssertEqual(configurationGroup.projectId, project.id) + } + + return configurationGroup + } + + func assertDeleteConfigurationGroup(_ configurationGroup: ConfigurationGroup) { + + let expectation = XCTestExpectation(description: "Delete Configuration Group") + + objectAPI.deleteConfigurationGroup(configurationGroup) { (outcome) in + self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + } + + @discardableResult func assertGetConfigurationGroups() -> [ConfigurationGroup]? { + + let expectation = XCTestExpectation(description: "Get Configuration Groups") + + var configurationGroups: [ConfigurationGroup]? = nil + objectAPI.getConfigurationGroups { (outcome) in + configurationGroups = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(configurationGroups) + + return configurationGroups + } + + @discardableResult func assertGetConfigurationGroups(in project: Project) -> [ConfigurationGroup]? { + + let expectation = XCTestExpectation(description: "Get Configuration Groups In Project") + + var configurationGroups: [ConfigurationGroup]? = nil + objectAPI.getConfigurationGroups(in: project) { (outcome) in + configurationGroups = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(configurationGroups) + + if let configurationGroups = configurationGroups { + for configurationGroup in configurationGroups { + XCTAssertEqual(configurationGroup.projectId, project.id) + } + } + + return configurationGroups + } + + @discardableResult func assertUpdateConfigurationGroup(_ configurationGroup: ConfigurationGroup) -> ConfigurationGroup? { + + let expectation = XCTestExpectation(description: "Update Configuration Group") + + var updatedConfigurationGroup: ConfigurationGroup? = nil + objectAPI.updateConfigurationGroup(configurationGroup) { (outcome) in + updatedConfigurationGroup = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(updatedConfigurationGroup) + + if let updatedConfigurationGroup = updatedConfigurationGroup { + // Identity + XCTAssertEqual(updatedConfigurationGroup.id, configurationGroup.id) + // Updates + XCTAssertEqual(updatedConfigurationGroup.name, configurationGroup.name) + } + + return updatedConfigurationGroup + } + + // MARK: Milestone + + @discardableResult func assertAddMilestone(_ newMilestone: NewMilestone, to project: Project) -> Milestone? { + + let expectation = XCTestExpectation(description: "Add Milestone") + + var milestone: Milestone? = nil + objectAPI.addMilestone(newMilestone, to: project) { (outcome) in + milestone = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(milestone) + + if let milestone = milestone { + + XCTAssertEqual(milestone.name, newMilestone.name) + XCTAssertEqual(milestone.projectId, project.id) + + // TestRail may assign default values so only assert if nil was not passed in data. + if let value = newMilestone.description { XCTAssertEqual(value, milestone.description) } + if let value = newMilestone.dueOn?.secondsSince1970 { XCTAssertEqual(value, milestone.dueOn?.secondsSince1970) } + if let value = newMilestone.parentId { XCTAssertEqual(value, milestone.parentId) } + if let value = newMilestone.startOn?.secondsSince1970 { XCTAssertEqual(value, milestone.startOn?.secondsSince1970) } + } + + return milestone + } + + func assertDeleteMilestone(_ milestone: Milestone) { + + let expectation = XCTestExpectation(description: "Delete Milestone") + + objectAPI.deleteMilestone(milestone) { (outcome) in + self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + } + + @discardableResult func assertGetMilestone(_ milestoneId: Int) -> Milestone? { + + let expectation = XCTestExpectation(description: "Get Milestone") + + var milestone: Milestone? = nil + objectAPI.getMilestone(milestoneId) { (outcome) in + milestone = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(milestone) + + if let milestone = milestone { + XCTAssertEqual(milestone.id, milestoneId) + } + + return milestone + } + + @discardableResult func assertGetMilestones(in project: Project, filteredBy filters: [Filter]? = nil) -> [Milestone]? { + + let expectation = XCTestExpectation(description: "Get Milestones") + + var milestones: [Milestone]? = nil + objectAPI.getMilestones(in: project, filteredBy: filters) { (outcome) in + milestones = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(milestones) + + if let milestones = milestones { + for milestone in milestones { + XCTAssertEqual(milestone.projectId, project.id) + } + } + + return milestones + } + + @discardableResult func assertUpdateMilestone(_ milestone: Milestone) -> Milestone? { + + let expectation = XCTestExpectation(description: "Update Milestone") + + var updatedMilestone: Milestone? = nil + objectAPI.updateMilestone(milestone) { (outcome) in + updatedMilestone = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(updatedMilestone) + + if let updatedMilestone = updatedMilestone { + // Identity + XCTAssertEqual(updatedMilestone.id, milestone.id) + // Updates + XCTAssertEqual(updatedMilestone.description, milestone.description) + XCTAssertEqual(updatedMilestone.dueOn?.secondsSince1970, milestone.dueOn?.secondsSince1970) + XCTAssertEqual(updatedMilestone.isCompleted, milestone.isCompleted) + XCTAssertEqual(updatedMilestone.isStarted, milestone.isStarted) + XCTAssertEqual(updatedMilestone.name, milestone.name) + XCTAssertEqual(updatedMilestone.parentId, milestone.parentId) + XCTAssertEqual(updatedMilestone.startOn?.secondsSince1970, milestone.startOn?.secondsSince1970) + } + + return updatedMilestone + } + + // MARK: Plan + + @discardableResult func assertAddPlan(_ newPlan: NewPlan, to project: Project) -> Plan? { + + let expectation = XCTestExpectation(description: "Add Plan") + + var plan: Plan? = nil + objectAPI.addPlan(newPlan, to: project) { (outcome) in + plan = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(plan) + + if let plan = plan { + + XCTAssertEqual(plan.name, newPlan.name) + XCTAssertEqual(plan.projectId, project.id) + + // TestRail may assign default values so only assert if nil was not passed in data. + if let value = newPlan.description { XCTAssertEqual(value, plan.description) } + if let value = newPlan.milestoneId { XCTAssertEqual(value, plan.milestoneId) } + if let value = newPlan.entries { XCTAssertEqual(value.count, plan.entries?.count) } + } + + return plan + } + + @discardableResult func assertClosePlan(_ plan: Plan) -> Plan? { + + let expectation = XCTestExpectation(description: "Close Plan") + + var closedPlan: Plan? = nil + objectAPI.closePlan(plan) { (outcome) in + closedPlan = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(closedPlan) + + if let closedPlan = closedPlan { + XCTAssertNotNil(closedPlan.completedOn) + XCTAssertEqual(closedPlan.isCompleted, true) + } + + return closedPlan + } + + func assertDeletePlan(_ plan: Plan) { + + let expectation = XCTestExpectation(description: "Delete Plan") + + objectAPI.deletePlan(plan) { (outcome) in + self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + } + + @discardableResult func assertGetPlan(_ planId: Int) -> Plan? { + + let expectation = XCTestExpectation(description: "Get Plan") + + var plan: Plan? = nil + objectAPI.getPlan(planId) { (outcome) in + plan = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(plan) + + if let plan = plan { + XCTAssertEqual(plan.id, planId) + } + + return plan + } + + @discardableResult func assertGetPlans(in project: Project, filteredBy filters: [Filter]? = nil) -> [Plan]? { + + let expectation = XCTestExpectation(description: "Get Plans") + + var plans: [Plan]? = nil + objectAPI.getPlans(in: project, filteredBy: filters) { (outcome) in + plans = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(plans) + + if let plans = plans { + for plan in plans { + XCTAssertEqual(plan.projectId, project.id) + } + } + + return plans + } + + @discardableResult func assertUpdatePlan(_ plan: Plan) -> Plan? { + + let expectation = XCTestExpectation(description: "Update Plan") + + var updatedPlan: Plan? = nil + objectAPI.updatePlan(plan) { (outcome) in + updatedPlan = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(updatedPlan) + + if let updatedPlan = updatedPlan { + // Identity + XCTAssertEqual(updatedPlan.id, plan.id) + // Updates + XCTAssertEqual(updatedPlan.description, plan.description) + XCTAssertEqual(updatedPlan.milestoneId, plan.milestoneId) + XCTAssertEqual(updatedPlan.name, plan.name) + } + + return updatedPlan + } + + // MARK: Plan.Entry + + @discardableResult func assertAddPlanEntry(_ newPlanEntry: NewPlan.Entry, to plan: Plan) -> Plan.Entry? { + + let expectation = XCTestExpectation(description: "Add Plan Entry") + + var planEntry: Plan.Entry? = nil + objectAPI.addPlanEntry(newPlanEntry, to: plan) { (outcome) in + planEntry = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(planEntry) + + if let planEntry = planEntry { + + XCTAssertEqual(planEntry.suiteId, newPlanEntry.suiteId) + + // TestRail may assign default values so only assert if nil was not passed in data. + if let value = newPlanEntry.name { XCTAssertEqual(value, planEntry.name) } + + if let newPlanEntryRuns = newPlanEntry.runs, newPlanEntryRuns.count > 0 { + XCTAssertEqual(planEntry.runs.count, newPlanEntryRuns.count) + } else { + // A default Run will be returned if newPlanEntry.runs was nil + // or empty. This Run will include all tests for the Suite + // matching the Suite for newPlanEntry.suiteId. + XCTAssertEqual(planEntry.runs.count, 1) + } + } + + return planEntry + } + + func assertDeletePlanEntry(_ planEntry: Plan.Entry, from plan: Plan) { + + let expectation = XCTestExpectation(description: "Delete Plan Entry") + + objectAPI.deletePlanEntry(planEntry, from: plan) { (outcome) in + self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + } + + @discardableResult func assertUpdatePlanEntry(_ planEntry: Plan.Entry, in plan: Plan, with planEntryRuns: UpdatePlanEntryRuns? = nil) -> Plan.Entry? { + + let expectation = XCTestExpectation(description: "Update Plan Entry") + + var updatedPlanEntry: Plan.Entry? = nil + objectAPI.updatePlanEntry(planEntry, in: plan, with: planEntryRuns) { (outcome) in + updatedPlanEntry = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(updatedPlanEntry) + + if let updatedPlanEntry = updatedPlanEntry { + + // Identity + XCTAssertEqual(updatedPlanEntry.id, planEntry.id) + + // Updates + XCTAssertEqual(updatedPlanEntry.name, planEntry.name) + } + + return updatedPlanEntry + } + + // MARK: Priority + + @discardableResult func assertGetPriorities() -> [Priority]? { + + let expectation = XCTestExpectation(description: "Get Priorities") + + var priorities: [Priority]? = nil + objectAPI.getPriorities { (outcome) in + priorities = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(priorities) + + return priorities + } + + // MARK: Project + + @discardableResult func assertAddProject(_ newProject: NewProject) -> Project? { + + let expectation = XCTestExpectation(description: "Add Project") + + var project: Project? = nil + objectAPI.addProject(newProject) { (outcome) in + project = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(project) + + if let project = project { + + XCTAssertEqual(project.name, newProject.name) + XCTAssertEqual(project.showAnnouncement, newProject.showAnnouncement) + XCTAssertEqual(newProject.suiteMode, newProject.suiteMode) + + // TestRail may assign default values so only assert if nil was not passed in data. + if let value = newProject.announcement { XCTAssertEqual(value, project.announcement) } + } + + return project + } + + func assertDeleteProject(_ project: Project) { + + let expectation = XCTestExpectation(description: "Test Delete Project") + + objectAPI.deleteProject(project) { (outcome) in + self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + } + + @discardableResult func assertGetProject(_ projectId: Int) -> Project? { + + let expectation = XCTestExpectation(description: "Get Project") + + var project: Project? = nil + objectAPI.getProject(projectId) { (outcome) in + project = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(project) + + if let project = project { + XCTAssertEqual(project.id, projectId) + } + + return project + } + + @discardableResult func assertGetProjects() -> [Project]? { + + let expectation = XCTestExpectation(description: "Get Projects") + + var projects: [Project]? = nil + objectAPI.getProjects { (outcome) in + projects = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(projects) + + return projects + } + + @discardableResult func assertUpdateProject(_ project: Project) -> Project? { + + let expectation = XCTestExpectation(description: "Update Projects") + + var updatedProject: Project? = nil + objectAPI.updateProject(project) { (outcome) in + updatedProject = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(updatedProject) + + if let updatedProject = updatedProject { + // Identity + XCTAssertEqual(updatedProject.id, project.id) + // Updates + XCTAssertEqual(updatedProject.announcement, project.announcement) + XCTAssertEqual(updatedProject.isCompleted, project.isCompleted) + XCTAssertEqual(updatedProject.name, project.name) + XCTAssertEqual(updatedProject.showAnnouncement, project.showAnnouncement) + XCTAssertEqual(updatedProject.suiteMode, project.suiteMode) + } + + return updatedProject + } + + // MARK: Result + + @discardableResult func assertAddResult(_ newResult: NewResult, to test: Test) -> Result? { + + let expectation = XCTestExpectation(description: "Add Result") + + var result: Result? = nil + objectAPI.addResult(newResult, to: test) { (outcome) in + result = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(result) + + if let result = result { + + XCTAssertEqual(result.statusId, newResult.statusId) + XCTAssertEqual(result.testId, test.id) + + // TestRail may assign default values so only assert if nil was not passed in data. + if let value = newResult.assignedtoId { XCTAssertEqual(value, result.assignedtoId) } + if let value = newResult.comment { XCTAssertEqual(value, result.comment) } + if let value = newResult.defects { XCTAssertEqual(value, result.defects) } + if let value = newResult.elapsed { XCTAssertEqual(value, result.elapsed) } + if let value = newResult.version { XCTAssertEqual(value, result.version) } + + assertCustomFieldKeyValuePairs(in: newResult, existIn: result) + } + + return result + } + + @discardableResult func assertAddResultForCase(_ newResult: NewResult, to run: Run, to case: Case) -> Result? { + + let expectation = XCTestExpectation(description: "Add Result For Case") + + var result: Result? = nil + objectAPI.addResultForCase(newResult, to: run, to: `case`) { (outcome) in + result = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(result) + + if let result = result { + + XCTAssertEqual(result.statusId, newResult.statusId) + + // TestRail may assign default values so only assert if nil was not passed in data. + if let value = newResult.assignedtoId { XCTAssertEqual(value, result.assignedtoId) } + if let value = newResult.comment { XCTAssertEqual(value, result.comment) } + if let value = newResult.defects { XCTAssertEqual(value, result.defects) } + if let value = newResult.elapsed { XCTAssertEqual(value, result.elapsed) } + if let value = newResult.version { XCTAssertEqual(value, result.version) } + + assertCustomFieldKeyValuePairs(in: newResult, existIn: result) + } + + return result + } + + @discardableResult func assertAddResults(_ newTestResults: NewTestResults, to run: Run) -> [Result]? { + + let expectation = XCTestExpectation(description: "Add Results") + + var results: [Result]? = nil + objectAPI.addResults(newTestResults, to: run) { (outcome) in + results = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(results) + + if let results = results { + XCTAssertEqual(results.count, newTestResults.results.count) + } + + return results + } + + @discardableResult func assertAddResultsForCases(_ newCaseResults: NewCaseResults, to run: Run) -> [Result]? { + + let expectation = XCTestExpectation(description: "Add Results For Cases") + + var results: [Result]? = nil + objectAPI.addResultsForCases(newCaseResults, to: run) { (outcome) in + results = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(results) + + if let results = results { + XCTAssertEqual(results.count, newCaseResults.results.count) + } + + return results + } + + @discardableResult func assertGetResultsForTest(_ test: Test, filteredBy filters: [Filter]? = nil) -> [Result]? { + + let expectation = XCTestExpectation(description: "Get Results For Test") + + var results: [Result]? = nil + objectAPI.getResultsForTest(test, filteredBy: filters) { (outcome) in + results = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(results) + + return results + } + + @discardableResult func assertGetResultsForCase(_ case: Case, in run: Run, filteredBy filters: [Filter]? = nil) -> [Result]? { + + let expectation = XCTestExpectation(description: "Get Results For Case") + + var results: [Result]? = nil + objectAPI.getResultsForCase(`case`, in: run, filteredBy: filters) { (outcome) in + results = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(results) + + return results + } + + @discardableResult func assertGetResultsForRun(_ run: Run, filteredBy filters: [Filter]? = nil) -> [Result]? { + + let expectation = XCTestExpectation(description: "Get Results For Run") + + var results: [Result]? = nil + objectAPI.getResultsForRun(run, filteredBy: filters) { (outcome) in + results = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(results) + + return results + } + + // MARK: ResultField + + @discardableResult func assertGetResultFields() -> [ResultField]? { + + let expectation = XCTestExpectation(description: "Get ResultFields") + + var resultFields: [ResultField]? = nil + objectAPI.getResultFields { (outcome) in + resultFields = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(resultFields) + + return resultFields + } + + // MARK: Run + + @discardableResult func assertAddRun(_ newRun: NewRun, to project: Project) -> Run? { + + let expectation = XCTestExpectation(description: "Add Run") + + var run: Run? = nil + objectAPI.addRun(newRun, to: project) { (outcome) in + run = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(run) + + if let run = run { + + XCTAssertEqual(run.includeAll, newRun.includeAll) + XCTAssertEqual(run.name, newRun.name) + XCTAssertEqual(run.projectId, project.id) + + // TestRail may assign default values so only assert if nil was not passed in data. + if let value = newRun.assignedtoId { XCTAssertEqual(value, run.assignedtoId) } + if let value = newRun.description { XCTAssertEqual(value, run.description) } + if let value = newRun.milestoneId { XCTAssertEqual(value, run.milestoneId) } + if let value = newRun.suiteId { XCTAssertEqual(value, run.suiteId) } + } + + return run + } + + @discardableResult func assertCloseRun(_ run: Run) -> Run? { + + let expectation = XCTestExpectation(description: "Close Run") + + var closedRun: Run? = nil + objectAPI.closeRun(run) { (outcome) in + closedRun = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(closedRun) + + if let closedRun = closedRun { + XCTAssertNotNil(closedRun.completedOn) + XCTAssertTrue(closedRun.isCompleted) + XCTAssertEqual(closedRun.id, run.id) + } + + return closedRun + } + + func assertDeleteRun(_ run: Run) { + + let expectation = XCTestExpectation(description: "Delete Run") + + objectAPI.deleteRun(run) { (outcome) in + self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + } + + @discardableResult func assertGetRun(_ runId: Int) -> Run? { + + let expectation = XCTestExpectation(description: "Get Run") + + var run: Run? = nil + objectAPI.getRun(runId) { (outcome) in + run = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(run) + + if let run = run { + XCTAssertEqual(run.id, runId) + } + + return run + } + + @discardableResult func assertGetRuns(in project: Project, filteredBy filters: [Filter]? = nil) -> [Run]? { + + let expectation = XCTestExpectation(description: "Get Runs") + + var runs: [Run]? = nil + objectAPI.getRuns(in: project, filteredBy: filters) { (outcome) in + runs = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(runs) + + if let runs = runs { + for run in runs { + XCTAssertEqual(run.projectId, project.id) + } + } + + return runs + } + + @discardableResult func assertUpdateRun(_ run: Run) -> Run? { + + let expectation = XCTestExpectation(description: "Update Run") + + var updatedRun: Run? = nil + objectAPI.updateRun(run) { (outcome) in + updatedRun = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(updatedRun) + + if let updatedRun = updatedRun { + // Identity + XCTAssertEqual(updatedRun.id, run.id) + // Updates + XCTAssertEqual(updatedRun.description, run.description) + XCTAssertEqual(updatedRun.includeAll, run.includeAll) + XCTAssertEqual(updatedRun.milestoneId, run.milestoneId) + XCTAssertEqual(updatedRun.name, run.name) + } + + return updatedRun + } + + // MARK: Section + + @discardableResult func assertAddSection(_ newSection: NewSection, to project: Project) -> Section? { + + let expectation = XCTestExpectation(description: "Add Section") + + var section: Section? = nil + objectAPI.addSection(newSection, to: project) { (outcome) in + section = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(section) + + if let section = section { + + XCTAssertEqual(section.name, newSection.name) + + // TestRail may assign default values so only assert if nil was not passed in data. + if let value = newSection.description { XCTAssertEqual(value, section.description) } + if let value = newSection.parentId { XCTAssertEqual(value, section.parentId) } + + if project.suiteMode == .singleSuite { + XCTAssertNil(section.suiteId) // Optional/ignored if project is running in single suite mode, otherwise required. + } else { + XCTAssertNotNil(section.suiteId) + if let value = newSection.suiteId { XCTAssertEqual(value, section.suiteId) } + } + } + + return section + } + + func assertDeleteSection(_ section: Section) { + + let expectation = XCTestExpectation(description: "Delete Section") + + objectAPI.deleteSection(section) { (outcome) in + self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + } + + @discardableResult func assertGetSection(_ sectionId: Int) -> Section? { + + let expectation = XCTestExpectation(description: "Get Section") + + var section: Section? = nil + objectAPI.getSection(sectionId) { (outcome) in + section = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(section) + + if let section = section { + XCTAssertEqual(section.id, sectionId) + } + + return section + } + + @discardableResult func assertGetSections(in project: Project, in suite: Suite? = nil) -> [Section]? { + + let expectation = XCTestExpectation(description: "Get Sections") + + var sections: [Section]? = nil + objectAPI.getSections(in: project, in: suite) { (outcome) in + sections = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(sections) + + if let sections = sections, let suite = suite { + for section in sections { + XCTAssertEqual(section.suiteId, suite.id) + } + } + + return sections + } + + @discardableResult func assertUpdateSection(_ section: Section) -> Section? { + + let expectation = XCTestExpectation(description: "Update Section") + + var updatedSection: Section? = nil + objectAPI.updateSection(section) { (outcome) in + updatedSection = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(updatedSection) + + if let updatedSection = updatedSection { + // Identity + XCTAssertEqual(updatedSection.id, section.id) + // Updates + XCTAssertEqual(updatedSection.description, section.description) + XCTAssertEqual(updatedSection.name, section.name) + } + + return updatedSection + } + + // MARK: Status + + @discardableResult func assertGetStatuses() -> [Status]? { + + let expectation = XCTestExpectation(description: "Get Statuses") + + var statuses: [Status]? = nil + objectAPI.getStatuses { (outcome) in + statuses = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(statuses) + + return statuses + } + + // MARK: Suite + + @discardableResult func assertAddSuite(_ newSuite: NewSuite, to project: Project) -> Suite? { + + let expectation = XCTestExpectation(description: "Add Suite") + + var suite: Suite? = nil + objectAPI.addSuite(newSuite, to: project) { (outcome) in + suite = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(suite) + + if let suite = suite { + + XCTAssertEqual(suite.name, newSuite.name) + XCTAssertEqual(suite.projectId, project.id) + + // TestRail may assign default values so only assert if nil was not passed in data. + if let value = newSuite.description { XCTAssertEqual(value, suite.description) } + } + + return suite + } + + func assertDeleteSuite(_ suite: Suite) { + + let expectation = XCTestExpectation(description: "Delete Suite") + + objectAPI.deleteSuite(suite) { (outcome) in + self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + } + + @discardableResult func assertGetSuite(_ suiteId: Int) -> Suite? { + + let expectation = XCTestExpectation(description: "Get Suite") + + var suite: Suite? = nil + objectAPI.getSuite(suiteId) { (outcome) in + suite = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(suite) + + if let suite = suite { + XCTAssertEqual(suite.id, suiteId) + } + + return suite + } + + @discardableResult func assertGetSuites(in project: Project) -> [Suite]? { + + let expectation = XCTestExpectation(description: "Get Suites") + + var suites: [Suite]? = nil + objectAPI.getSuites(in: project) { (outcome) in + suites = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(suites) + + if let suites = suites { + for suite in suites { + XCTAssertEqual(suite.projectId, project.id) + } + } + + return suites + } + + @discardableResult func assertUpdateSuite(_ suite: Suite) -> Suite? { + + let expectation = XCTestExpectation(description: "Update Suite") + + var updatedSuite: Suite? = nil + objectAPI.updateSuite(suite) { (outcome) in + updatedSuite = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(updatedSuite) + + if let updatedSuite = updatedSuite { + // Identity + XCTAssertEqual(updatedSuite.id, suite.id) + // Updates + XCTAssertEqual(updatedSuite.description, suite.description) + XCTAssertEqual(updatedSuite.name, suite.name) + } + + return updatedSuite + } + + // MARK: Template + + @discardableResult func assertGetTemplates() -> [Template]? { + + let expectation = XCTestExpectation(description: "Get Templates") + + var templates: [Template]? = nil + objectAPI.getTemplates { (outcome) in + templates = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(templates) + + return templates + } + + @discardableResult func assertGetTemplates(in project: Project) -> [Template]? { + + let expectation = XCTestExpectation(description: "Get Templates In Project") + + var templates: [Template]? = nil + objectAPI.getTemplates(in: project) { (outcome) in + templates = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(templates) + + return templates + } + + // MARK: Test + + @discardableResult func assertGetTest(_ testId: Int) -> Test? { + + let expectation = XCTestExpectation(description: "Get Test") + + var test: Test? = nil + objectAPI.getTest(testId) { (outcome) in + test = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(test) + + if let test = test { + XCTAssertEqual(test.id, testId) + } + + return test + } + + @discardableResult func assertGetTests(in run: Run, filteredBy filters: [Filter]? = nil) -> [Test]? { + + let expectation = XCTestExpectation(description: "Get Tests") + + var tests: [Test]? = nil + objectAPI.getTests(in: run, filteredBy: filters) { (outcome) in + tests = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(tests) + + if let tests = tests { + for test in tests { + XCTAssertEqual(test.runId, run.id) + } + } + + return tests + } + + // MARK: User + + @discardableResult func assertGetUser(_ userId: Int) -> User? { + + let expectation = XCTestExpectation(description: "Get User") + + var user: User? = nil + objectAPI.getUser(userId) { (outcome) in + user = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(user) + + if let user = user { + XCTAssertEqual(user.id, userId) + } + + return user + } + + @discardableResult func assertGetUserByEmail(_ email: String) -> User? { + + let expectation = XCTestExpectation(description: "Get User by Email") + + var user: User? = nil + objectAPI.getUserByEmail(email) { (outcome) in + user = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(user) + + if let user = user { + XCTAssertEqual(user.email, email) + } + + return user + } + + @discardableResult func assertGetUsers() -> [User]? { + + let expectation = XCTestExpectation(description: "Get Users") + + var users: [User]? = nil + objectAPI.getUsers { (outcome) in + users = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(users) + + if let users = users { + XCTAssertNotEqual(users.count, 0) + } + + return users + } + +} + +// MARK: - Assertions: Object Matching + +extension ObjectAPITests { + + // MARK: Case + + @discardableResult func assertGetCaseTypeMatchingId(_ id: Int) -> CaseType? { + + let expectation = XCTestExpectation(description: "Get CaseType Matching ID") + + var caseType: CaseType? + objectAPI.getCaseType(matching: id) { (outcome) in + caseType = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(caseType) + + if let caseType = caseType { + XCTAssertEqual(caseType.id, id) + } + + return caseType + } + + // MARK: ConfigurationGroup + + @discardableResult func assertGetConfigurationGroupMatchingId(_ id: Int) -> ConfigurationGroup? { + + let expectation = XCTestExpectation(description: "Get ConfigurationGroup Matching ID") + + var configurationGroup: ConfigurationGroup? + objectAPI.getConfigurationGroup(matching: id) { (outcome) in + configurationGroup = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(configurationGroup) + + if let configurationGroup = configurationGroup { + XCTAssertEqual(configurationGroup.id, id) + } + + return configurationGroup + } + + // MARK: Priority + + @discardableResult func assertGetPriorityMatchingId(_ id: Int) -> Priority? { + + let expectation = XCTestExpectation(description: "Get Priority Matching ID") + + var priority: Priority? + objectAPI.getPriority(matching: id) { (outcome) in + priority = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(priority) + + if let priority = priority { + XCTAssertEqual(priority.id, id) + } + + return priority + } + + // MARK: Status + + @discardableResult func assertGetStatusMatchingId(_ id: Int) -> Status? { + + let expectation = XCTestExpectation(description: "Get Status Matching ID") + + var status: Status? + objectAPI.getStatus(matching: id) { (outcome) in + status = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(status) + + if let status = status { + XCTAssertEqual(status.id, id) + } + + return status + } + + // MARK: Template + + @discardableResult func assertGetTemplateMatchingId(_ id: Int) -> Template? { + + let expectation = XCTestExpectation(description: "Get Template Matching ID") + + var template: Template? + objectAPI.getTemplate(matching: id) { (outcome) in + template = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(template) + + if let template = template { + XCTAssertEqual(template.id, id) + } + + return template + } + + @discardableResult func assertGetTemplatesMatchingIds(_ ids: [Int]) -> [Template]? { + + let expectation = XCTestExpectation(description: "Get Templates Matching IDs") + + var templates: [Template]? + objectAPI.getTemplates(matching: ids) { (outcome) in + templates = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(templates) + + if let templates = templates { + let uniqueIds = Set(ids) + XCTAssertEqual(uniqueIds.count, templates.count) + for id in uniqueIds { + XCTAssertEqual(templates.filter({ $0.id == id }).count, 1) + } + } + + return templates + } + +} + +// MARK: - Assertions: Object Forward Relationships + +extension ObjectAPITests { + + // MARK: Case + + @discardableResult func assertGetCaseToCreatedByRelationship(_ `case`: Case, usingObjectToRelationshipMethod: Bool = false) -> User? { + + let expectation = XCTestExpectation(description: "Get Case to CreatedBy (User) Relationship") + + var createdBy: User? + if usingObjectToRelationshipMethod { + `case`.createdBy(objectAPI) { (outcome) in + createdBy = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.createdBy(`case`) { (outcome) in + createdBy = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(createdBy) + + if let createdBy = createdBy { + XCTAssertEqual(createdBy.id, `case`.createdBy) + } + + return createdBy + } + + @discardableResult func assertGetCaseToMilestoneRelationship(_ `case`: Case, usingObjectToRelationshipMethod: Bool = false) -> Milestone? { + + let expectation = XCTestExpectation(description: "Get Case to Milestone Relationship") + + var milestone: Milestone? + if usingObjectToRelationshipMethod { + `case`.milestone(objectAPI) { (outcome) in + milestone = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.milestone(`case`) { (outcome) in + milestone = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + + } + + wait(for: [expectation], timeout: timeout) + + if let milestoneId = `case`.milestoneId { + XCTAssertNotNil(milestone) + if let milestone = milestone { + XCTAssertEqual(milestone.id, milestoneId) + } + } else { + XCTAssertNil(milestone) + } + + return milestone + } + + @discardableResult func assertGetCaseToPriorityRelationship(_ `case`: Case, usingObjectToRelationshipMethod: Bool = false) -> Priority? { + + let expectation = XCTestExpectation(description: "Get Case to Priority Relationship") + + var priority: Priority? + if usingObjectToRelationshipMethod { + `case`.priority(objectAPI) { (outcome) in + priority = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.priority(`case`) { (outcome) in + priority = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(priority) + + if let priority = priority { + XCTAssertEqual(priority.id, `case`.priorityId) + } + + return priority + } + + @discardableResult func assertGetCaseToSectionRelationship(_ `case`: Case, usingObjectToRelationshipMethod: Bool = false) -> Section? { + + let expectation = XCTestExpectation(description: "Get Case to Section Relationship") + + var section: Section? + if usingObjectToRelationshipMethod { + `case`.section(objectAPI) { (outcome) in + section = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.section(`case`) { (outcome) in + section = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + if let sectionId = `case`.sectionId { + XCTAssertNotNil(section) + if let section = section { + XCTAssertEqual(section.id, sectionId) + } + } else { + XCTAssertNil(section) + } + + return section + } + + @discardableResult func assertGetCaseToSuiteRelationship(_ `case`: Case, usingObjectToRelationshipMethod: Bool = false) -> Suite? { + + let expectation = XCTestExpectation(description: "Get Case to Suite Relationship") + + var suite: Suite? + if usingObjectToRelationshipMethod { + `case`.suite(objectAPI) { (outcome) in + suite = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.suite(`case`) { (outcome) in + suite = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + if let suiteId = `case`.suiteId { + XCTAssertNotNil(suite) + if let suite = suite { + XCTAssertEqual(suite.id, suiteId) + } + } else { + XCTAssertNil(suite) + } + + return suite + } + + @discardableResult func assertGetCaseToTemplateRelationship(_ `case`: Case, usingObjectToRelationshipMethod: Bool = false) -> Template? { + + let expectation = XCTestExpectation(description: "Get Case to Template Relationship") + + var template: Template? + if usingObjectToRelationshipMethod { + `case`.template(objectAPI) { (outcome) in + template = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.template(`case`) { (outcome) in + template = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(template) + + if let template = template { + XCTAssertEqual(template.id, `case`.templateId) + } + + return template + } + + @discardableResult func assertGetCaseToTypeRelationship(_ `case`: Case, usingObjectToRelationshipMethod: Bool = false) -> CaseType? { + + let expectation = XCTestExpectation(description: "Get Case to Type (CaseType) Relationship") + + var type: CaseType? + if usingObjectToRelationshipMethod { + `case`.type(objectAPI) { (outcome) in + type = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.type(`case`) { (outcome) in + type = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(type) + + if let type = type { + XCTAssertEqual(type.id, `case`.typeId) + } + + return type + } + + @discardableResult func assertGetCaseToUpdatedByRelationship(_ `case`: Case, usingObjectToRelationshipMethod: Bool = false) -> User? { + + let expectation = XCTestExpectation(description: "Get Case to UpdatedBy (User) Relationship") + + var updatedBy: User? + if usingObjectToRelationshipMethod { + `case`.updatedBy(objectAPI) { (outcome) in + updatedBy = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.updatedBy(`case`) { (outcome) in + updatedBy = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(updatedBy) + + if let updatedBy = updatedBy { + XCTAssertEqual(updatedBy.id, `case`.updatedBy) + } + + return updatedBy + } + + // MARK: Config + + @discardableResult func assertGetConfigToAccessibleProjectsRelationship(_ config: Config, usingObjectToRelationshipMethod: Bool = false) -> [Project]? { + + let expectation = XCTestExpectation(description: "Get Config to Accessible Projects Relationship") + + var projects: [Project]? + if usingObjectToRelationshipMethod { + config.accessibleProjects(objectAPI) { (outcome) in + projects = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.accessibleProjects(config) { (outcome) in + projects = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(projects) + + return projects + } + + /* + failOnMatchError true will cause a failure if any project returns a 403 + not authorized error. failOnMatchError false will not fail if 403's are + returned as long as there are no other non-403 errors. + + 403 errors might be unavoidable for some projects. For details see comments + in the ObjectAPI.projects(...) method called here. + */ + @discardableResult func assertGetConfigToProjectsRelationship(_ config: Config, usingObjectToRelationshipMethod: Bool = false, failOnMatchError: Bool = false) -> [Project]? { + + let expectation = XCTestExpectation(description: "Get Config to Projects Relationship") + + var _outcome: Outcome<[Project]?, ObjectAPI.MatchError, ErrorContainer>>? + if usingObjectToRelationshipMethod { + config.projects(objectAPI) { (outcome) in + _outcome = outcome + expectation.fulfill() + } + } else { + objectAPI.projects(config) { (outcome) in + _outcome = outcome + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + // Sanity: should never fail. + guard let outcome = _outcome else { + XCTAssertNotNil(_outcome) + return nil + } + + // Unpack outcome. + var projects: [Project]? + switch outcome { + case .failed(let error): + switch error { + case .matchError(let matchError): + if failOnMatchError { + XCTFail(error.debugDescription) + } else { + print("\(#file):\(#line):\(#function) - WARNING: failOnMatchError is disabled. Partial matches will be returned: \(error.debugDescription)") + switch matchError { + case .noMatchesFound(_): + projects = [] + case .partialMatchesFound(let matches, _): + projects = matches + } + } + default: + XCTFail(error.debugDescription) + } + case .succeeded(let _projects): + projects = _projects + } + + XCTAssertNotNil(projects) + + return projects + } + + // MARK: CaseField + + @discardableResult func assertGetCaseFieldToTemplatesRelationship(_ caseField: CaseField, usingObjectToRelationshipMethod: Bool = false) -> [Template]? { + + let expectation = XCTestExpectation(description: "Get CaseField to Templates Relationship") + + var templates: [Template]? + if usingObjectToRelationshipMethod { + caseField.templates(objectAPI) { (outcome) in + templates = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.templates(caseField) { (outcome) in + templates = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(templates) + + if let templates = templates { + XCTAssertEqual(templates.count, caseField.templateIds.count) + for id in caseField.templateIds { + XCTAssertEqual(templates.filter({ $0.id == id }).count, 1) + } + } + + return templates + } + + // MARK: Configuration + + @discardableResult func assertGetConfigurationToConfigurationGroupRelationship(_ configuration: Configuration, usingObjectToRelationshipMethod: Bool = false) -> ConfigurationGroup? { + + let expectation = XCTestExpectation(description: "Get Configuration to ConfigurationGroup Relationship") + + var configurationGroup: ConfigurationGroup? + if usingObjectToRelationshipMethod { + configuration.configurationGroup(objectAPI) { (outcome) in + configurationGroup = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.configurationGroup(configuration) { (outcome) in + configurationGroup = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(configurationGroup) + + if let configurationGroup = configurationGroup { + XCTAssertEqual(configurationGroup.id, configuration.groupId) + } + + return configurationGroup + } + + // MARK: ConfigurationGroup + + @discardableResult func assertGetConfigurationGroupToProjectRelationship(_ configurationGroup: ConfigurationGroup, usingObjectToRelationshipMethod: Bool = false) -> Project? { + + let expectation = XCTestExpectation(description: "Get ConfigurationGroup to Project Relationship") + + var project: Project? + if usingObjectToRelationshipMethod { + configurationGroup.project(objectAPI) { (outcome) in + project = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.project(configurationGroup) { (outcome) in + project = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(project) + + if let project = project { + XCTAssertEqual(project.id, configurationGroup.projectId) + } + + return project + } + + // MARK: Milestone + + @discardableResult func assertGetMilestoneToParentRelationship(_ milestone: Milestone, usingObjectToRelationshipMethod: Bool = false) -> Milestone? { + + let expectation = XCTestExpectation(description: "Get Milestone to Parent (Milestone) Relationship") + + var parent: Milestone? + if usingObjectToRelationshipMethod { + milestone.parent(objectAPI) { (outcome) in + parent = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.parent(milestone) { (outcome) in + parent = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + if let parentId = milestone.parentId { + XCTAssertNotNil(parent) + if let parent = parent { + XCTAssertEqual(parent.id, parentId) + } + } else { + XCTAssertNil(parent) + } + + return parent + } + + @discardableResult func assertGetMilestoneToProjectRelationship(_ milestone: Milestone, usingObjectToRelationshipMethod: Bool = false) -> Project? { + + let expectation = XCTestExpectation(description: "Get Milestone to Project Relationship") + + var project: Project? + if usingObjectToRelationshipMethod { + milestone.project(objectAPI) { (outcome) in + project = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.project(milestone) { (outcome) in + project = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(project) + + if let project = project { + XCTAssertEqual(project.id, milestone.projectId) + } + + return project + } + + // MARK: Plan + + @discardableResult func assertGetPlanToAssignedtoRelationship(_ plan: Plan, usingObjectToRelationshipMethod: Bool = false) -> User? { + + let expectation = XCTestExpectation(description: "Get Plan to Assignedto (User) Relationship") + + var assignedto: User? + if usingObjectToRelationshipMethod { + plan.assignedto(objectAPI) { (outcome) in + assignedto = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.assignedto(plan) { (outcome) in + assignedto = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + if let assignedtoId = plan.assignedtoId { + XCTAssertNotNil(assignedto) + if let assignedto = assignedto { + XCTAssertEqual(assignedto.id, assignedtoId) + } + } else { + XCTAssertNil(assignedto) + } + + return assignedto + } + + @discardableResult func assertGetPlanToCreatedByRelationship(_ plan: Plan, usingObjectToRelationshipMethod: Bool = false) -> User? { + + let expectation = XCTestExpectation(description: "Get Plan to CreatedBy (User) Relationship") + + var createdBy: User? + if usingObjectToRelationshipMethod { + plan.createdBy(objectAPI) { (outcome) in + createdBy = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.createdBy(plan) { (outcome) in + createdBy = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(createdBy) + + if let createdBy = createdBy { + XCTAssertEqual(createdBy.id, plan.createdBy) + } + + return createdBy + } + + @discardableResult func assertGetPlanToMilestoneRelationship(_ plan: Plan, usingObjectToRelationshipMethod: Bool = false) -> Milestone? { + + let expectation = XCTestExpectation(description: "Get Plan to Milestone Relationship") + + var milestone: Milestone? + if usingObjectToRelationshipMethod { + plan.milestone(objectAPI) { (outcome) in + milestone = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.milestone(plan) { (outcome) in + milestone = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + if let milestoneId = plan.milestoneId { + XCTAssertNotNil(milestone) + if let milestone = milestone { + XCTAssertEqual(milestone.id, milestoneId) + } + } else { + XCTAssertNil(milestone) + } + + return milestone + } + + @discardableResult func assertGetPlanToProjectRelationship(_ plan: Plan, usingObjectToRelationshipMethod: Bool = false) -> Project? { + + let expectation = XCTestExpectation(description: "Get Plan to Project Relationship") + + var project: Project? + if usingObjectToRelationshipMethod { + plan.project(objectAPI) { (outcome) in + project = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.project(plan) { (outcome) in + project = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(project) + + if let project = project { + XCTAssertEqual(project.id, plan.projectId) + } + + return project + } + + // MARK: Plan.Entry + + @discardableResult func assertGetPlanEntryToSuiteRelationship(_ planEntry: Plan.Entry, usingObjectToRelationshipMethod: Bool = false) -> Suite? { + + let expectation = XCTestExpectation(description: "Get Plan.Entry to Suite Relationship") + + var suite: Suite? + if usingObjectToRelationshipMethod { + planEntry.suite(objectAPI) { (outcome) in + suite = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.suite(planEntry) { (outcome) in + suite = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(suite) + if let suite = suite { + XCTAssertEqual(suite.id, planEntry.suiteId) + } + + return suite + } + + // MARK: Result + + @discardableResult func assertGetResultToAssignedtoRelationship(_ result: Result, usingObjectToRelationshipMethod: Bool = false) -> User? { + + let expectation = XCTestExpectation(description: "Get Result to Assignedto (User) Relationship") + + var assignedto: User? + if usingObjectToRelationshipMethod { + result.assignedto(objectAPI) { (outcome) in + assignedto = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.assignedto(result) { (outcome) in + assignedto = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + if let assignedtoId = result.assignedtoId { + XCTAssertNotNil(assignedto) + if let assignedto = assignedto { + XCTAssertEqual(assignedto.id, assignedtoId) + } + } else { + XCTAssertNil(assignedto) + } + + return assignedto + } + + @discardableResult func assertGetResultToCreatedByRelationship(_ result: Result, usingObjectToRelationshipMethod: Bool = false) -> User? { + + let expectation = XCTestExpectation(description: "Get Result to CreatedBy (User) Relationship") + + var createdBy: User? + if usingObjectToRelationshipMethod { + result.createdBy(objectAPI) { (outcome) in + createdBy = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.createdBy(result) { (outcome) in + createdBy = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(createdBy) + + if let createdBy = createdBy { + XCTAssertEqual(createdBy.id, result.createdBy) + } + + return createdBy + } + + @discardableResult func assertGetResultToStatusRelationship(_ result: Result, usingObjectToRelationshipMethod: Bool = false) -> Status? { + + let expectation = XCTestExpectation(description: "Get Result to Status Relationship") + + var status: Status? + if usingObjectToRelationshipMethod { + result.status(objectAPI) { (outcome) in + status = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.status(result) { (outcome) in + status = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + if let statusId = result.statusId { + XCTAssertNotNil(status) + if let status = status { + XCTAssertEqual(status.id, statusId) + } + } else { + XCTAssertNil(status) + } + + return status + } + + @discardableResult func assertGetResultToTestRelationship(_ result: Result, usingObjectToRelationshipMethod: Bool = false) -> Test? { + + let expectation = XCTestExpectation(description: "Get Result to Test Relationship") + + var test: Test? + if usingObjectToRelationshipMethod { + result.test(objectAPI) { (outcome) in + test = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.test(result) { (outcome) in + test = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(test) + + if let test = test { + XCTAssertEqual(test.id, result.testId) + } + + return test + } + + // MARK: ResultField + + @discardableResult func assertGetResultFieldToTemplatesRelationship(_ resultField: ResultField, usingObjectToRelationshipMethod: Bool = false) -> [Template]? { + + let expectation = XCTestExpectation(description: "Get ResultField to Templates Relationship") + + var templates: [Template]? + if usingObjectToRelationshipMethod { + resultField.templates(objectAPI) { (outcome) in + templates = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.templates(resultField) { (outcome) in + templates = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(templates) + + if let templates = templates { + XCTAssertEqual(templates.count, resultField.templateIds.count) + for templateId in resultField.templateIds { + XCTAssertEqual(templates.filter({ $0.id == templateId }).count, 1) + } + } + + return templates + } + + // MARK: Run + + @discardableResult func assertGetRunToAssignedtoRelationship(_ run: Run, usingObjectToRelationshipMethod: Bool = false) -> User? { + + let expectation = XCTestExpectation(description: "Get Run to Assignedto (User) Relationship") + + var assignedto: User? + if usingObjectToRelationshipMethod { + run.assignedto(objectAPI) { (outcome) in + assignedto = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.assignedto(run) { (outcome) in + assignedto = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + if let assignedtoId = run.assignedtoId { + XCTAssertNotNil(assignedto) + if let assignedto = assignedto { + XCTAssertEqual(assignedto.id, assignedtoId) + } + } else { + XCTAssertNil(assignedto) + } + + return assignedto + } + + @discardableResult func assertGetRunToConfigurationsRelationship(_ run: Run, usingObjectToRelationshipMethod: Bool = false) -> [Configuration]? { + + let expectation = XCTestExpectation(description: "Get Run to Configurations Relationship") + + var configurations: [Configuration]? + if usingObjectToRelationshipMethod { + run.configurations(objectAPI) { (outcome) in + configurations = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.configurations(run) { (outcome) in + configurations = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + if let runConfigIds = run.configIds { + XCTAssertNotNil(configurations) + if let configurations = configurations { + XCTAssertEqual(configurations.count, runConfigIds.count) + for id in runConfigIds { + XCTAssertEqual(configurations.filter({ $0.id == id }).count, 1) + } + } + } else { + XCTAssertNil(configurations) + } + + return configurations + } + + @discardableResult func assertGetRunToCreatedByRelationship(_ run: Run, usingObjectToRelationshipMethod: Bool = false) -> User? { + + let expectation = XCTestExpectation(description: "Get Run to CreatedBy (User) Relationship") + + var createdBy: User? + if usingObjectToRelationshipMethod { + run.createdBy(objectAPI) { (outcome) in + createdBy = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.createdBy(run) { (outcome) in + createdBy = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(createdBy) + + if let createdBy = createdBy { + XCTAssertEqual(createdBy.id, run.createdBy) + } + + return createdBy + } + + @discardableResult func assertGetRunToMilestoneRelationship(_ run: Run, usingObjectToRelationshipMethod: Bool = false) -> Milestone? { + + let expectation = XCTestExpectation(description: "Get Run to Milestone Relationship") + + var milestone: Milestone? + if usingObjectToRelationshipMethod { + run.milestone(objectAPI) { (outcome) in + milestone = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.milestone(run) { (outcome) in + milestone = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + if let milestoneId = run.milestoneId { + XCTAssertNotNil(milestone) + if let milestone = milestone { + XCTAssertEqual(milestone.id, milestoneId) + } + } else { + XCTAssertNil(milestone) + } + + return milestone + } + + @discardableResult func assertGetRunToPlanRelationship(_ run: Run, usingObjectToRelationshipMethod: Bool = false) -> Plan? { + + let expectation = XCTestExpectation(description: "Get Run to Plan Relationship") + + var plan: Plan? + if usingObjectToRelationshipMethod { + run.plan(objectAPI) { (outcome) in + plan = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.plan(run) { (outcome) in + plan = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + if let planId = run.planId { + XCTAssertNotNil(plan) + if let plan = plan { + XCTAssertEqual(plan.id, planId) + } + } else { + XCTAssertNil(plan) + } + + return plan + } + + @discardableResult func assertGetRunToProjectRelationship(_ run: Run, usingObjectToRelationshipMethod: Bool = false) -> Project? { + + let expectation = XCTestExpectation(description: "Get Run to Project Relationship") + + var project: Project? + if usingObjectToRelationshipMethod { + run.project(objectAPI) { (outcome) in + project = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.project(run) { (outcome) in + project = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(project) + + if let project = project { + XCTAssertEqual(project.id, run.projectId) + } + + return project + } + + @discardableResult func assertGetRunToSuiteRelationship(_ run: Run, usingObjectToRelationshipMethod: Bool = false) -> Suite? { + + let expectation = XCTestExpectation(description: "Get Run to Suite Relationship") + + var suite: Suite? + if usingObjectToRelationshipMethod { + run.suite(objectAPI) { (outcome) in + suite = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.suite(run) { (outcome) in + suite = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + if let suiteId = run.suiteId { + XCTAssertNotNil(suite) + if let suite = suite { + XCTAssertEqual(suite.id, suiteId) + } + } else { + XCTAssertNil(suite) + } + + return suite + } + + // MARK: Section + + @discardableResult func assertGetSectionToParentRelationship(_ section: Section, usingObjectToRelationshipMethod: Bool = false) -> Section? { + + let expectation = XCTestExpectation(description: "Get Section to Parent (Section) Relationship") + + var parent: Section? + if usingObjectToRelationshipMethod { + section.parent(objectAPI) { (outcome) in + parent = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.parent(section) { (outcome) in + parent = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + if let parentId = section.parentId { + XCTAssertNotNil(parent) + if let parent = parent { + XCTAssertEqual(parent.id, parentId) + } + } else { + XCTAssertNil(parent) + } + + return parent + } + + @discardableResult func assertGetSectionToSuiteRelationship(_ section: Section, usingObjectToRelationshipMethod: Bool = false) -> Suite? { + + let expectation = XCTestExpectation(description: "Get Section to Suite Relationship") + + var suite: Suite? + if usingObjectToRelationshipMethod { + section.suite(objectAPI) { (outcome) in + suite = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.suite(section) { (outcome) in + suite = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + if let suiteId = section.suiteId { + XCTAssertNotNil(suite) + if let suite = suite { + XCTAssertEqual(suite.id, suiteId) + } + } else { + XCTAssertNil(suite) + } + + return suite + } + + // MARK: Suite + + @discardableResult func assertGetSuiteToProjectRelationship(_ suite: Suite, usingObjectToRelationshipMethod: Bool = false) -> Project? { + + let expectation = XCTestExpectation(description: "Get Suite to Project Relationship") + + var project: Project? + if usingObjectToRelationshipMethod { + suite.project(objectAPI) { (outcome) in + project = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.project(suite) { (outcome) in + project = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(project) + + if let project = project { + XCTAssertEqual(project.id, suite.projectId) + } + + return project + } + + // MARK: Test + + @discardableResult func assertGetTestToAssignedtoRelationship(_ test: Test, usingObjectToRelationshipMethod: Bool = false) -> User? { + + let expectation = XCTestExpectation(description: "Get Test to Assignedto (User) Relationship") + + var assignedto: User? + if usingObjectToRelationshipMethod { + test.assignedto(objectAPI) { (outcome) in + assignedto = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.assignedto(test) { (outcome) in + assignedto = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + if let assignedtoId = test.assignedtoId { + XCTAssertNotNil(assignedto) + if let assignedto = assignedto { + XCTAssertEqual(assignedto.id, assignedtoId) + } + } else { + XCTAssertNil(assignedto) + } + + return assignedto + } + + @discardableResult func assertGetTestToCaseRelationship(_ test: Test, usingObjectToRelationshipMethod: Bool = false) -> Case? { + + let expectation = XCTestExpectation(description: "Get Test to Case Relationship") + + var `case`: Case? + if usingObjectToRelationshipMethod { + test.`case`(objectAPI) { (outcome) in + `case` = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.`case`(test) { (outcome) in + `case` = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(`case`) + + if let `case` = `case` { + XCTAssertEqual(`case`.id, test.caseId) + } + + return `case` + } + + @discardableResult func assertGetTestToMilestoneRelationship(_ test: Test, usingObjectToRelationshipMethod: Bool = false) -> Milestone? { + + let expectation = XCTestExpectation(description: "Get Test to Milestone Relationship") + + var milestone: Milestone? + if usingObjectToRelationshipMethod { + test.milestone(objectAPI) { (outcome) in + milestone = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.milestone(test) { (outcome) in + milestone = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + if let milestoneId = test.milestoneId { + XCTAssertNotNil(milestone) + if let milestone = milestone { + XCTAssertEqual(milestone.id, milestoneId) + } + } else { + XCTAssertNil(milestone) + } + + return milestone + } + + @discardableResult func assertGetTestToPriorityRelationship(_ test: Test, usingObjectToRelationshipMethod: Bool = false) -> Priority? { + + let expectation = XCTestExpectation(description: "Get Test to Priority Relationship") + + var priority: Priority? + if usingObjectToRelationshipMethod { + test.priority(objectAPI) { (outcome) in + priority = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.priority(test) { (outcome) in + priority = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(priority) + + if let priority = priority { + XCTAssertEqual(priority.id, test.priorityId) + } + + return priority + } + + @discardableResult func assertGetTestToRunRelationship(_ test: Test, usingObjectToRelationshipMethod: Bool = false) -> Run? { + + let expectation = XCTestExpectation(description: "Get Test to Run Relationship") + + var run: Run? + if usingObjectToRelationshipMethod { + test.run(objectAPI) { (outcome) in + run = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.run(test) { (outcome) in + run = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(run) + + if let run = run { + XCTAssertEqual(run.id, test.runId) + } + + return run + } + + @discardableResult func assertGetTestToStatusRelationship(_ test: Test, usingObjectToRelationshipMethod: Bool = false) -> Status? { + + let expectation = XCTestExpectation(description: "Get Test to Status Relationship") + + var status: Status? + if usingObjectToRelationshipMethod { + test.status(objectAPI) { (outcome) in + status = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.status(test) { (outcome) in + status = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(status) + if let status = status { + XCTAssertEqual(status.id, test.statusId) + } + + return status + } + + @discardableResult func assertGetTestToTemplateRelationship(_ test: Test, usingObjectToRelationshipMethod: Bool = false) -> Template? { + + let expectation = XCTestExpectation(description: "Get Test to Template Relationship") + + var template: Template? + if usingObjectToRelationshipMethod { + test.template(objectAPI) { (outcome) in + template = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.template(test) { (outcome) in + template = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(template) + + if let template = template { + XCTAssertEqual(template.id, test.templateId) + } + + return template + } + + @discardableResult func assertGetTestToTypeRelationship(_ test: Test, usingObjectToRelationshipMethod: Bool = false) -> CaseType? { + + let expectation = XCTestExpectation(description: "Get Test to Type (CaseType) Relationship") + + var type: CaseType? + if usingObjectToRelationshipMethod { + test.type(objectAPI) { (outcome) in + type = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } else { + objectAPI.type(test) { (outcome) in + type = self.assertOutcomeSucceeded(outcome) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + + XCTAssertNotNil(type) + + if let type = type { + XCTAssertEqual(type.id, test.typeId) + } + + return type + } + +} diff --git a/QuizTrainTests/README.md b/QuizTrainTests/README.md new file mode 100644 index 0000000..3a96581 --- /dev/null +++ b/QuizTrainTests/README.md @@ -0,0 +1,15 @@ +# QuizTrainTests 📝🚆✅ + +QuizTrainTests provides unit tests and systems tests against a real TestRail instance. It is advised that you backup your instance fully before running tests and verify that the backup is valid. For more details see comments and code in [ObjectAPITests.swift](Network/ObjectAPITests.swift). + +## Running Tests + +1. Update [`TestCredentials.json`](Testing%20Misc/TestCredentials.json) accordingly. + - *The `username` user must have permissions to create and delete projects.* +2. Select a scheme. + - *Apple does not support unit testing on watchOS.* +3. Select a target to run tests on. + - *It must have full network connectivity to your TestRail instance.* +4. In the Xcode menu: `Product -> Test` + +For [ObjectAPITests.swift](Network/ObjectAPITests.swift) to run you must set any custom **Case Fields** and **Result Fields** in your TestRail instance either as not-required, or required with a default value, for the duration of testing. Otherwise tests might not be able to setup necessary test projects since they do not know of required customizations specific to your instance. diff --git a/QuizTrainTests/Testing Misc/Array+Random.swift b/QuizTrainTests/Testing Misc/Array+Random.swift new file mode 100644 index 0000000..3bf7b5a --- /dev/null +++ b/QuizTrainTests/Testing Misc/Array+Random.swift @@ -0,0 +1,19 @@ +import Foundation + +extension Array { + + public var randomElement: Element? { + guard let index = randomIndex else { + return nil + } + return self[index] + } + + public var randomIndex: Int? { + guard count > 0 else { + return nil + } + return Int(arc4random_uniform(UInt32(count))) + } + +} diff --git a/QuizTrainTests/Testing Misc/TestCredentials.json b/QuizTrainTests/Testing Misc/TestCredentials.json new file mode 100644 index 0000000..1ed738d --- /dev/null +++ b/QuizTrainTests/Testing Misc/TestCredentials.json @@ -0,0 +1,7 @@ +{ + "hostname": "yourInstance.testrail.net", + "port": 443, + "scheme": "https", + "secret": "your_api_key_or_password", + "username": "your@testRailAccount.email" +} diff --git a/QuizTrainTests/Testing Misc/TestCredentials.swift b/QuizTrainTests/Testing Misc/TestCredentials.swift new file mode 100644 index 0000000..4e80e34 --- /dev/null +++ b/QuizTrainTests/Testing Misc/TestCredentials.swift @@ -0,0 +1,59 @@ +import Foundation + +/* + Represents credentials used in tests. Use the load() method to load data from + TestCredentials.json. + */ +final class TestCredentials: Codable { + let hostname: String // "yourinstance.testrail.net" + let port: Int // 443, 80, 8080, etc + let scheme: String // "https", "http" + let secret: String // Your TestRail API Key or Password + let username: String // "your@testrailAccount.email" +} + +extension TestCredentials { + + enum LoadError: Error { + case couldNotFindURLForResourceInBundle(resource: String, extension: String, bundle: Bundle) + case couldNotLoadDataFromURL(url: URL, error: Error) + case couldNotDecodeObjectFromJSONData(data: Data, error: Error) + } + + /* + Note the Bundle used for tests is different than Bundle.main. Main will not + be able to see anything inside the test target (e.g. TestCredentials.json). + Inside a test case load it like so: + + let testCredentials: TestCredentials + do { + let bundle = Bundle(for: type(of: self)) + testCredentials = try TestCredentials.load(from: bundle) + } catch { + // Handle TestCredentials.LoadError "error" + } + */ + static func load(from bundle: Bundle, resource: String = "TestCredentials", withExtension `extension`: String = "json") throws -> TestCredentials { + + guard let url = bundle.url(forResource: resource, withExtension: `extension`) else { + throw LoadError.couldNotFindURLForResourceInBundle(resource: resource, extension: `extension`, bundle: bundle) + } + + let data: Data + do { + data = try Data(contentsOf: url) + } catch { + throw LoadError.couldNotLoadDataFromURL(url: url, error: error) + } + + let testCredentials: TestCredentials + do { + testCredentials = try JSONDecoder().decode(TestCredentials.self, from: data) + } catch { + throw LoadError.couldNotDecodeObjectFromJSONData(data: data, error: error) + } + + return testCredentials + } + +} diff --git a/QuizTrainTests/Testing Protocols/Asserts/AssertAddRequestJSON.swift b/QuizTrainTests/Testing Protocols/Asserts/AssertAddRequestJSON.swift new file mode 100644 index 0000000..5a48fcb --- /dev/null +++ b/QuizTrainTests/Testing Protocols/Asserts/AssertAddRequestJSON.swift @@ -0,0 +1,18 @@ +import XCTest +@testable import QuizTrain + +protocol AssertAddRequestJSON { + func assertAddRequestJSON(_ object: Object) +} + +extension AssertAddRequestJSON { + + func assertAddRequestJSON(_ object: Object) { + let addRequestJSON = object.addRequestJSON + XCTAssertEqual(addRequestJSON.count, object.addRequestJSONKeys.count) + for key in object.addRequestJSONKeys { + XCTAssertNotNil(addRequestJSON[key]) + } + } + +} diff --git a/QuizTrainTests/Testing Protocols/Asserts/AssertCustomFields.swift b/QuizTrainTests/Testing Protocols/Asserts/AssertCustomFields.swift new file mode 100644 index 0000000..b4a0041 --- /dev/null +++ b/QuizTrainTests/Testing Protocols/Asserts/AssertCustomFields.swift @@ -0,0 +1,21 @@ +import XCTest +@testable import QuizTrain + +protocol AssertCustomFields { + func assertCustomFields(in object: Object, areEmpty: Bool) +} + +extension AssertCustomFields { + + func assertCustomFields(in object: Object, areEmpty: Bool) { + if areEmpty { + XCTAssertEqual(object.customFields.count, 0) + } else { + XCTAssertGreaterThan(object.customFields.count, 0) + for (key, _) in object.customFields { + XCTAssertTrue(key.hasPrefix("custom_")) + } + } + } + +} diff --git a/QuizTrainTests/Testing Protocols/Asserts/AssertEquatable.swift b/QuizTrainTests/Testing Protocols/Asserts/AssertEquatable.swift new file mode 100644 index 0000000..a51e78b --- /dev/null +++ b/QuizTrainTests/Testing Protocols/Asserts/AssertEquatable.swift @@ -0,0 +1,18 @@ +import XCTest + +protocol AssertEquatable { + func assertEqual(_ lhs: Object?, _ rhs: Object?) + func assertNotEqual(_ lhs: Object?, _ rhs: Object?) +} + +extension AssertEquatable { + + func assertEqual(_ lhs: Object?, _ rhs: Object?) { + XCTAssertEqual(lhs, rhs) + } + + func assertNotEqual(_ lhs: Object?, _ rhs: Object?) { + XCTAssertNotEqual(lhs, rhs) + } + +} diff --git a/QuizTrainTests/Testing Protocols/Asserts/AssertJSONDeserializing.swift b/QuizTrainTests/Testing Protocols/Asserts/AssertJSONDeserializing.swift new file mode 100644 index 0000000..406c989 --- /dev/null +++ b/QuizTrainTests/Testing Protocols/Asserts/AssertJSONDeserializing.swift @@ -0,0 +1,47 @@ +import XCTest +@testable import QuizTrain + +protocol AssertJSONDeserializing { + func assertJSONDeserializing(type: Object.Type, from json: JSONDictionary) + func assertJSONDeserializing(type: Object.Type, from json: [JSONDictionary]) + func assertJSONDeserializing(type: Object.Type, failsByOmittingKeysFrom json: JSONDictionary) + func assertJSONDeserializing(type: Object.Type, failsByOmittingKeysFrom json: [JSONDictionary]) +} + +extension AssertJSONDeserializing { + + func assertJSONDeserializing(type: Object.Type, from json: JSONDictionary) { + let object: Object? = Object.deserialized(json) + XCTAssertNotNil(object) + } + + func assertJSONDeserializing(type: Object.Type, from json: [JSONDictionary]) { + let objects: [Object]? = Object.deserialized(json) + XCTAssertNotNil(objects) + XCTAssertEqual(objects!.count, json.count) + } + + func assertJSONDeserializing(type: Object.Type, failsByOmittingKeysFrom json: JSONDictionary) { + for (k, _) in json { + var incompleteJson = json + incompleteJson.removeValue(forKey: k) + let object: Object? = Object.deserialized(incompleteJson) + XCTAssertNil(object) + } + } + + func assertJSONDeserializing(type: Object.Type, failsByOmittingKeysFrom json: [JSONDictionary]) { + for index in 0..(_ object: Object) + func assertJSONSerializing(_ objects: [Object]) +} + +extension AssertJSONSerializing { + + func assertJSONSerializing(_ object: Object) { + let serializedA: JSONDictionary = Object.serialized(object) + XCTAssertNotNil(serializedA) // This will always pass. + let serializedB = object.serialized() + XCTAssertNotNil(serializedB) // This will always pass. + } + + func assertJSONSerializing(_ objects: [Object]) { + let serialized: [JSONDictionary] = Object.serialized(objects) + XCTAssertNotNil(serialized) // This will always pass. + XCTAssertEqual(serialized.count, objects.count) + } + +} diff --git a/QuizTrainTests/Testing Protocols/Asserts/AssertJSONTwoWaySerialization.swift b/QuizTrainTests/Testing Protocols/Asserts/AssertJSONTwoWaySerialization.swift new file mode 100644 index 0000000..af3828f --- /dev/null +++ b/QuizTrainTests/Testing Protocols/Asserts/AssertJSONTwoWaySerialization.swift @@ -0,0 +1,87 @@ +import XCTest +@testable import QuizTrain + +protocol AssertJSONTwoWaySerialization { + associatedtype Object: Equatable, JSONDeserializable, JSONSerializable + func assertJSONTwoWaySerialization(_ json: JSONDictionary) + func assertJSONTwoWaySerialization(_ json: [JSONDictionary]) + func assertJSONTwoWaySerialization(_ object: Object) + func assertJSONTwoWaySerialization(_ objects: [Object]) +} + +extension AssertJSONTwoWaySerialization { + + // MARK: JSON to Object(s) to JSON + + func assertJSONTwoWaySerialization(_ json: JSONDictionary) { + + let object: Object? = Object.deserialized(json) + XCTAssertNotNil(object) + + // Instance Method + let serializedA: JSONDictionary = object!.serialized() + + for (k, _) in json { + XCTAssertNotNil(serializedA[k]) + } + + let deserializedA: Object? = Object.deserialized(serializedA) + XCTAssertNotNil(deserializedA) + XCTAssertEqual(deserializedA!, object!) + + // Class Method + let serializedB: JSONDictionary = Object.serialized(object!) + + for (k, _) in json { + XCTAssertNotNil(serializedB[k]) + } + + let deserializedB: Object? = Object.deserialized(serializedB) + XCTAssertNotNil(deserializedB) + XCTAssertEqual(deserializedB!, object!) + } + + func assertJSONTwoWaySerialization(_ json: [JSONDictionary]) { + + let objects: [Object]? = Object.deserialized(json) + XCTAssertNotNil(objects) + XCTAssertEqual(objects!.count, json.count) + + let serialized: [JSONDictionary] = Object.serialized(objects!) + XCTAssertEqual(serialized.count, json.count) + + let deserialized: [Object]? = Object.deserialized(serialized) + XCTAssertNotNil(deserialized) + XCTAssertEqual(deserialized!, objects!) + } + + // MARK: Object(s) to JSON to Object(s) + + func assertJSONTwoWaySerialization(_ object: Object) { + + // Instance Method + let serializedA: JSONDictionary = object.serialized() + let deserializedA: Object? = Object.deserialized(serializedA) + + XCTAssertNotNil(deserializedA) + XCTAssertEqual(deserializedA!, object) + + // Class Method + let serializedB: JSONDictionary = Object.serialized(object) + let deserializedB: Object? = Object.deserialized(serializedB) + + XCTAssertNotNil(deserializedB) + XCTAssertEqual(deserializedB!, object) + } + + func assertJSONTwoWaySerialization(_ objects: [Object]) { + + let serialized: [JSONDictionary] = Object.serialized(objects) + XCTAssertEqual(serialized.count, objects.count) + + let deserialized: [Object]? = Object.deserialized(serialized) + XCTAssertNotNil(deserialized) + XCTAssertEqual(deserialized!, objects) + } + +} diff --git a/QuizTrainTests/Testing Protocols/Asserts/AssertProperties.swift b/QuizTrainTests/Testing Protocols/Asserts/AssertProperties.swift new file mode 100644 index 0000000..f6c1bed --- /dev/null +++ b/QuizTrainTests/Testing Protocols/Asserts/AssertProperties.swift @@ -0,0 +1,8 @@ +import XCTest + +protocol AssertProperties { + associatedtype Object + func assertRequiredProperties(in object: Object) + func assertOptionalProperties(in object: Object, areNil: Bool) + func assertVariablePropertiesCanBeChanged(in object: inout Object) +} diff --git a/QuizTrainTests/Testing Protocols/Asserts/AssertUpdateRequestJSON.swift b/QuizTrainTests/Testing Protocols/Asserts/AssertUpdateRequestJSON.swift new file mode 100644 index 0000000..5251153 --- /dev/null +++ b/QuizTrainTests/Testing Protocols/Asserts/AssertUpdateRequestJSON.swift @@ -0,0 +1,19 @@ +import XCTest +@testable import QuizTrain + +protocol AssertUpdateRequestJSON { + associatedtype Object: UpdateRequestJSON, UpdateRequestJSONKeys + func assertUpdateRequestJSON(_ object: Object) +} + +extension AssertUpdateRequestJSON { + + func assertUpdateRequestJSON(_ object: Object) { + let updateRequestJSON = object.updateRequestJSON + XCTAssertEqual(updateRequestJSON.count, object.updateRequestJSONKeys.count) + for key in object.updateRequestJSONKeys { + XCTAssertNotNil(updateRequestJSON[key]) + } + } + +} diff --git a/QuizTrainTests/Testing Protocols/Asserts/AssertValidatable.swift b/QuizTrainTests/Testing Protocols/Asserts/AssertValidatable.swift new file mode 100644 index 0000000..8e0e9b1 --- /dev/null +++ b/QuizTrainTests/Testing Protocols/Asserts/AssertValidatable.swift @@ -0,0 +1,19 @@ +import XCTest +@testable import QuizTrain + +protocol AssertValidatable { + func assertValid(_ object: Validatable) + func assertInvalid(_ object: Validatable) +} + +extension AssertValidatable { + + func assertValid(_ object: Validatable) { + XCTAssertTrue(object.isValid) + } + + func assertInvalid(_ object: Validatable) { + XCTAssertFalse(object.isValid) + } + +} diff --git a/QuizTrainTests/Testing Protocols/Providers/CustomFieldsDataProvider.swift b/QuizTrainTests/Testing Protocols/Providers/CustomFieldsDataProvider.swift new file mode 100644 index 0000000..83ea760 --- /dev/null +++ b/QuizTrainTests/Testing Protocols/Providers/CustomFieldsDataProvider.swift @@ -0,0 +1,141 @@ +@testable import QuizTrain + +protocol CustomFieldsDataProvider { + + // Valid + + var customFields: JSONDictionary { get } + var customFieldsContainer: CustomFieldsContainer { get } + var customFieldsKeys: [JSONKey] { get } + static var customFields: JSONDictionary { get } + static var customFieldsContainer: CustomFieldsContainer { get } + static var customFieldsKeys: [JSONKey] { get } + + // Empty + + var emptyCustomFields: JSONDictionary { get } + var emptyCustomFieldsContainer: CustomFieldsContainer { get } + static var emptyCustomFields: JSONDictionary { get } + static var emptyCustomFieldsContainer: CustomFieldsContainer { get } + + // Invalid + + var invalidCustomFields: JSONDictionary { get } + var invalidCustomFieldsKeys: [JSONKey] { get } + static var invalidCustomFields: JSONDictionary { get } + static var invalidCustomFieldsKeys: [JSONKey] { get } + + // Valid/Invalid + + var validAndInvalidCustomFields: JSONDictionary { get } + var validAndInvalidCustomFieldsKeys: [JSONKey] { get } + static var validAndInvalidCustomFields: JSONDictionary { get } + static var validAndInvalidCustomFieldsKeys: [JSONKey] { get } +} + +// MARK: - Valid + +extension CustomFieldsDataProvider { + + var customFields: JSONDictionary { + return Self.customFields + } + + var customFieldsContainer: CustomFieldsContainer { + return Self.customFieldsContainer + } + + var customFieldsKeys: [JSONKey] { + return Self.customFieldsKeys + } + + static var customFields: JSONDictionary { + return [ + "custom_field_0": -587, + "custom_field_1": "What is the meaning of life?", + "custom_field_2": 3.14159, + "custom_field_3": ["Hello": ["🐹🐹🐹", 4.32, true, 789]] + ] + } + + static var customFieldsContainer: CustomFieldsContainer { + return CustomFieldsContainer(json: customFields) + } + + static var customFieldsKeys: [JSONKey] { + return Self.customFields.map { $0.key } + } + +} + +// MARK: - Empty + +extension CustomFieldsDataProvider { + + var emptyCustomFields: JSONDictionary { + return Self.emptyCustomFields + } + + var emptyCustomFieldsContainer: CustomFieldsContainer { + return Self.emptyCustomFieldsContainer + } + + static var emptyCustomFields: JSONDictionary { + return [:] + } + + static var emptyCustomFieldsContainer: CustomFieldsContainer { + return CustomFieldsContainer(json: [:]) + } + +} + +// MARK: - Invalid + +extension CustomFieldsDataProvider { + + var invalidCustomFields: JSONDictionary { + return Self.invalidCustomFields + } + + var invalidCustomFieldsKeys: [JSONKey] { + return Self.invalidCustomFieldsKeys + } + + static var invalidCustomFields: JSONDictionary { + return ["_custom_field": "Invalid", + "customField": "Invalid", + "this_custom_field": "is also invalid", + "Custom_field": "Invalid", + " custom_field": ["😀😀😀": 3.14]] + } + + static var invalidCustomFieldsKeys: [JSONKey] { + return Self.invalidCustomFields.map { $0.key } + } + +} + +// MARK: - Valid/Invalid + +extension CustomFieldsDataProvider { + + var validAndInvalidCustomFields: JSONDictionary { + return Self.validAndInvalidCustomFields + } + + var validAndInvalidCustomFieldsKeys: [JSONKey] { + return Self.validAndInvalidCustomFieldsKeys + } + + static var validAndInvalidCustomFields: JSONDictionary { + var dict = CustomFieldsContainerTests.customFields + CustomFieldsContainerTests.invalidCustomFields.forEach { item in dict[item.key] = item.value } + return dict + } + + static var validAndInvalidCustomFieldsKeys: [JSONKey] { + return Self.validAndInvalidCustomFields.map { $0.key } + } + +} diff --git a/QuizTrainTests/Testing Protocols/Providers/JSONDataProvider.swift b/QuizTrainTests/Testing Protocols/Providers/JSONDataProvider.swift new file mode 100644 index 0000000..8a4c5d7 --- /dev/null +++ b/QuizTrainTests/Testing Protocols/Providers/JSONDataProvider.swift @@ -0,0 +1,46 @@ +import XCTest +@testable import QuizTrain + +protocol JSONDataProvider { + + var requiredJSON: JSONDictionary { get } + var optionalJSON: JSONDictionary { get } + var requiredAndOptionalJSON: JSONDictionary { get } + + static var requiredJSON: JSONDictionary { get } + static var optionalJSON: JSONDictionary { get } + static var requiredAndOptionalJSON: JSONDictionary { get } +} + +extension JSONDataProvider { + + var requiredJSON: JSONDictionary { + return Self.requiredJSON + } + + var optionalJSON: JSONDictionary { + return Self.optionalJSON + } + + var requiredAndOptionalJSON: JSONDictionary { + return Self.requiredAndOptionalJSON + } + + static var requiredAndOptionalJSON: JSONDictionary { + var dict = Self.requiredJSON + Self.optionalJSON.forEach { item in dict[item.key] = item.value } + return dict + } + +} + +extension JSONDataProvider where Self: CustomFieldsDataProvider { + + static var requiredAndOptionalJSON: JSONDictionary { + var dict = Self.requiredJSON + Self.optionalJSON.forEach { item in dict[item.key] = item.value } + customFields.forEach { item in dict[item.key] = item.value } + return dict + } + +} diff --git a/QuizTrainTests/Testing Protocols/Providers/ObjectProvider.swift b/QuizTrainTests/Testing Protocols/Providers/ObjectProvider.swift new file mode 100644 index 0000000..3da1eed --- /dev/null +++ b/QuizTrainTests/Testing Protocols/Providers/ObjectProvider.swift @@ -0,0 +1,45 @@ +import XCTest +@testable import QuizTrain + +protocol ObjectProvider { + + associatedtype Object + + var objectWithRequiredProperties: Object { get } + var objectWithRequiredAndOptionalProperties: Object { get } + + static var objectWithRequiredProperties: Object { get } + static var objectWithRequiredAndOptionalProperties: Object { get } +} + +extension ObjectProvider { + + var objectWithRequiredProperties: Object { + return Self.objectWithRequiredProperties + } + + var objectWithRequiredAndOptionalProperties: Object { + return Self.objectWithRequiredAndOptionalProperties + } + +} + +extension ObjectProvider where Self: JSONDataProvider, Object: JSONDeserializable { + + var objectWithRequiredPropertiesFromJSON: Object? { + return Self.objectWithRequiredPropertiesFromJSON + } + + var objectWithRequiredAndOptionalPropertiesFromJSON: Object? { + return Self.objectWithRequiredAndOptionalPropertiesFromJSON + } + + static var objectWithRequiredPropertiesFromJSON: Object? { + return Object(json: requiredJSON) + } + + static var objectWithRequiredAndOptionalPropertiesFromJSON: Object? { + return Object(json: requiredAndOptionalJSON) + } + +} diff --git a/QuizTrainTests/Testing Protocols/Providers/ValidatableObjectProvider.swift b/QuizTrainTests/Testing Protocols/Providers/ValidatableObjectProvider.swift new file mode 100644 index 0000000..df6f501 --- /dev/null +++ b/QuizTrainTests/Testing Protocols/Providers/ValidatableObjectProvider.swift @@ -0,0 +1,7 @@ +import XCTest +@testable import QuizTrain + +protocol ValidatableObjectProvider { + var validObject: Validatable { get } + var invalidObject: Validatable { get } +} diff --git a/QuizTrainTests/Testing Protocols/Tests/AddRequestJSONTests.swift b/QuizTrainTests/Testing Protocols/Tests/AddRequestJSONTests.swift new file mode 100644 index 0000000..133078b --- /dev/null +++ b/QuizTrainTests/Testing Protocols/Tests/AddRequestJSONTests.swift @@ -0,0 +1,18 @@ +import XCTest +@testable import QuizTrain + +protocol AddRequestJSONTests { + + func testAddRequestJSON() + func _testAddRequestJSON() + +} + +extension AddRequestJSONTests where Self: AssertAddRequestJSON & ObjectProvider, Self.Object: AddRequestJSON & AddRequestJSONKeys { + + func _testAddRequestJSON() { + let object = objectWithRequiredAndOptionalProperties + assertAddRequestJSON(object) + } + +} diff --git a/QuizTrainTests/Testing Protocols/Tests/EquatableTests.swift b/QuizTrainTests/Testing Protocols/Tests/EquatableTests.swift new file mode 100644 index 0000000..ee5d4e0 --- /dev/null +++ b/QuizTrainTests/Testing Protocols/Tests/EquatableTests.swift @@ -0,0 +1,19 @@ +import XCTest + +protocol EquatableTests { + + func testEquatable() + func _testEquatable() + +} + +extension EquatableTests where Self: AssertEquatable & ObjectProvider, Self.Object: Equatable { + + func _testEquatable() { + let objectA = objectWithRequiredAndOptionalProperties + let objectB = objectWithRequiredAndOptionalProperties + assertEqual(objectA, objectB) + assertNotEqual(objectA, nil) + } + +} diff --git a/QuizTrainTests/Testing Protocols/Tests/InitTests.swift b/QuizTrainTests/Testing Protocols/Tests/InitTests.swift new file mode 100644 index 0000000..8c60b7d --- /dev/null +++ b/QuizTrainTests/Testing Protocols/Tests/InitTests.swift @@ -0,0 +1,46 @@ +import XCTest +@testable import QuizTrain + +protocol InitTests { + + func testInit() + func testInitWithOptionalProperties() + + func _testInit() + func _testInitWithOptionalProperties() + +} + +extension InitTests where Self: AssertProperties & ObjectProvider { + + func _testInit() { + let object = objectWithRequiredProperties + assertRequiredProperties(in: object) + assertOptionalProperties(in: object, areNil: true) + } + + func _testInitWithOptionalProperties() { + let object = objectWithRequiredAndOptionalProperties + assertRequiredProperties(in: object) + assertOptionalProperties(in: object, areNil: false) + } + +} + +extension InitTests where Self: AssertCustomFields & AssertProperties & ObjectProvider, Self.Object: CustomFields { + + func _testInit() { + let object = objectWithRequiredProperties + assertRequiredProperties(in: object) + assertOptionalProperties(in: object, areNil: true) + assertCustomFields(in: object, areEmpty: true) + } + + func _testInitWithOptionalProperties() { + let object = objectWithRequiredAndOptionalProperties + assertRequiredProperties(in: object) + assertOptionalProperties(in: object, areNil: false) + assertCustomFields(in: object, areEmpty: false) + } + +} diff --git a/QuizTrainTests/Testing Protocols/Tests/JSONDeserializingTests.swift b/QuizTrainTests/Testing Protocols/Tests/JSONDeserializingTests.swift new file mode 100644 index 0000000..ca11c87 --- /dev/null +++ b/QuizTrainTests/Testing Protocols/Tests/JSONDeserializingTests.swift @@ -0,0 +1,82 @@ +import XCTest +@testable import QuizTrain + +protocol JSONDeserializingTests { + + func testJSONDeserializing() + func testJSONDeserializingWithOptionalProperties() + func testJSONDeserializingASingleObject() + func testJSONDeserializingMultipleObjects() + func testJSONDeserializingASingleObjectMissingRequiredProperties() + func testJSONDeserializingMultipleObjectsMissingRequiredProperties() + + func _testJSONDeserializing() + func _testJSONDeserializingWithOptionalProperties() + func _testJSONDeserializingASingleObject() + func _testJSONDeserializingMultipleObjects() + func _testJSONDeserializingASingleObjectMissingRequiredProperties() + func _testJSONDeserializingMultipleObjectsMissingRequiredProperties() + +} + +extension JSONDeserializingTests where Self: AssertJSONDeserializing & AssertProperties & JSONDataProvider & ObjectProvider, Self.Object: JSONDeserializable { + + func _testJSONDeserializing() { + guard let object = objectWithRequiredPropertiesFromJSON else { + XCTFail("nil object returned.") + return + } + assertRequiredProperties(in: object) + assertOptionalProperties(in: object, areNil: true) + } + + func _testJSONDeserializingWithOptionalProperties() { + guard let object = objectWithRequiredAndOptionalPropertiesFromJSON else { + XCTFail("nil object returned.") + return + } + assertRequiredProperties(in: object) + assertOptionalProperties(in: object, areNil: false) + } + + func _testJSONDeserializingASingleObject() { + assertJSONDeserializing(type: Object.self, from: requiredAndOptionalJSON) + } + + func _testJSONDeserializingMultipleObjects() { + assertJSONDeserializing(type: Object.self, from: [requiredAndOptionalJSON, requiredAndOptionalJSON, requiredAndOptionalJSON]) + } + + func _testJSONDeserializingASingleObjectMissingRequiredProperties() { + assertJSONDeserializing(type: Object.self, failsByOmittingKeysFrom: requiredJSON) + } + + func _testJSONDeserializingMultipleObjectsMissingRequiredProperties() { + assertJSONDeserializing(type: Object.self, failsByOmittingKeysFrom: [requiredJSON, requiredJSON, requiredJSON]) + } + +} + +extension JSONDeserializingTests where Self: AssertCustomFields & AssertJSONDeserializing & AssertProperties & JSONDataProvider & ObjectProvider, Self.Object: CustomFields & JSONDeserializable { + + func _testJSONDeserializing() { + guard let object = objectWithRequiredPropertiesFromJSON else { + XCTFail("nil object returned.") + return + } + assertRequiredProperties(in: object) + assertOptionalProperties(in: object, areNil: true) + assertCustomFields(in: object, areEmpty: true) + } + + func _testJSONDeserializingWithOptionalProperties() { + guard let object = objectWithRequiredAndOptionalPropertiesFromJSON else { + XCTFail("nil object returned.") + return + } + assertRequiredProperties(in: object) + assertOptionalProperties(in: object, areNil: false) + assertCustomFields(in: object, areEmpty: false) + } + +} diff --git a/QuizTrainTests/Testing Protocols/Tests/JSONSerializingTests.swift b/QuizTrainTests/Testing Protocols/Tests/JSONSerializingTests.swift new file mode 100644 index 0000000..e66d7aa --- /dev/null +++ b/QuizTrainTests/Testing Protocols/Tests/JSONSerializingTests.swift @@ -0,0 +1,26 @@ +import XCTest +@testable import QuizTrain + +protocol JSONSerializingTests { + + func testJSONSerializingSingleObjects() + func testJSONSerializingMultipleObjects() + + func _testJSONSerializingSingleObjects() + func _testJSONSerializingMultipleObjects() + +} + +extension JSONSerializingTests where Self: AssertJSONSerializing & ObjectProvider, Self.Object: JSONSerializable { + + func _testJSONSerializingSingleObjects() { + assertJSONSerializing(objectWithRequiredProperties) + assertJSONSerializing(objectWithRequiredAndOptionalProperties) + } + + func _testJSONSerializingMultipleObjects() { + assertJSONSerializing([objectWithRequiredProperties, objectWithRequiredProperties, objectWithRequiredProperties]) + assertJSONSerializing([objectWithRequiredAndOptionalProperties, objectWithRequiredAndOptionalProperties, objectWithRequiredAndOptionalProperties]) + } + +} diff --git a/QuizTrainTests/Testing Protocols/Tests/JSONTwoWaySerializationTests.swift b/QuizTrainTests/Testing Protocols/Tests/JSONTwoWaySerializationTests.swift new file mode 100644 index 0000000..227dda5 --- /dev/null +++ b/QuizTrainTests/Testing Protocols/Tests/JSONTwoWaySerializationTests.swift @@ -0,0 +1,33 @@ +import XCTest + +protocol JSONTwoWaySerializationTests { + + func testJSONTwoWaySerializationForSingleItems() + func testJSONTwoWaySerializationForMultipleItems() + + func _testJSONTwoWaySerializationForSingleItems() + func _testJSONTwoWaySerializationForMultipleItems() + +} + +extension JSONTwoWaySerializationTests where Self: AssertJSONTwoWaySerialization & JSONDataProvider & ObjectProvider { + + func _testJSONTwoWaySerializationForSingleItems() { + // Object -> JSON -> Object + assertJSONTwoWaySerialization(objectWithRequiredProperties) + assertJSONTwoWaySerialization(objectWithRequiredAndOptionalProperties) + // JSON -> Object -> JSON + assertJSONTwoWaySerialization(requiredJSON) + assertJSONTwoWaySerialization(requiredAndOptionalJSON) + } + + func _testJSONTwoWaySerializationForMultipleItems() { + // Object -> JSON -> Object + assertJSONTwoWaySerialization([objectWithRequiredProperties, objectWithRequiredProperties, objectWithRequiredProperties]) + assertJSONTwoWaySerialization([objectWithRequiredAndOptionalProperties, objectWithRequiredAndOptionalProperties, objectWithRequiredAndOptionalProperties]) + // JSON -> Object -> JSON + assertJSONTwoWaySerialization([requiredJSON, requiredJSON, requiredJSON]) + assertJSONTwoWaySerialization([requiredAndOptionalJSON, requiredAndOptionalJSON, requiredAndOptionalJSON]) + } + +} diff --git a/QuizTrainTests/Testing Protocols/Tests/README.md b/QuizTrainTests/Testing Protocols/Tests/README.md new file mode 100644 index 0000000..43a83c6 --- /dev/null +++ b/QuizTrainTests/Testing Protocols/Tests/README.md @@ -0,0 +1,20 @@ +## XCTestCase Protocol Extensions + +Apple's testing framework is unable to identify `test*()` methods defined in protocol extensions applied to `XCTestCase` appearing in a different file than the `XCTestCase`. Because of this methods are defined in protocols appearing in the `../Tests/` group as `_test()` and implemented in a protocol extension. Objects conforming to this protocol must call the corresponding `_test*()` method inside of their `test*()` implementation. For example: + + // SomeProtocol + + func testSomething() + func _testSomething() + + // SomeProtocol Extension + + func _testSomething() { + ...test code... + } + + // Some XCTestCase conforming to SomeProtocol + + func testSomething() { + _testSomething() + } diff --git a/QuizTrainTests/Testing Protocols/Tests/UpdateRequestJSONTests.swift b/QuizTrainTests/Testing Protocols/Tests/UpdateRequestJSONTests.swift new file mode 100644 index 0000000..553f058 --- /dev/null +++ b/QuizTrainTests/Testing Protocols/Tests/UpdateRequestJSONTests.swift @@ -0,0 +1,23 @@ +import XCTest + +protocol UpdateRequestJSONTests { + + func testUpdateRequestJSON() + func _testUpdateRequestJSON() + +} + +extension UpdateRequestJSONTests { + + func _testUpdateRequestJSON() { /* Applies only to tests conforming to AssertUpdateRequestJSON/ObjectProvider. */ } + +} + +extension UpdateRequestJSONTests where Self: AssertUpdateRequestJSON & ObjectProvider { + + func _testUpdateRequestJSON() { + let object = objectWithRequiredAndOptionalProperties + assertUpdateRequestJSON(object) + } + +} diff --git a/QuizTrainTests/Testing Protocols/Tests/ValidatableTests.swift b/QuizTrainTests/Testing Protocols/Tests/ValidatableTests.swift new file mode 100644 index 0000000..4d11ced --- /dev/null +++ b/QuizTrainTests/Testing Protocols/Tests/ValidatableTests.swift @@ -0,0 +1,22 @@ +import XCTest + +protocol ValidatableTests { + + func testIsValid() + func testIsInvalid() + func _testIsValid() + func _testIsInvalid() + +} + +extension ValidatableTests where Self: AssertValidatable & ValidatableObjectProvider { + + func _testIsValid() { + assertValid(validObject) + } + + func _testIsInvalid() { + assertInvalid(invalidObject) + } + +} diff --git a/QuizTrainTests/Testing Protocols/Tests/VariablePropertyTests.swift b/QuizTrainTests/Testing Protocols/Tests/VariablePropertyTests.swift new file mode 100644 index 0000000..ca031b8 --- /dev/null +++ b/QuizTrainTests/Testing Protocols/Tests/VariablePropertyTests.swift @@ -0,0 +1,17 @@ +import XCTest + +protocol VariablePropertyTests { + + func testVariableProperties() + func _testVariableProperties() + +} + +extension VariablePropertyTests where Self: AssertProperties & ObjectProvider { + + func _testVariableProperties() { + var object = objectWithRequiredAndOptionalProperties + assertVariablePropertiesCanBeChanged(in: &object) + } + +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..542adaf --- /dev/null +++ b/README.md @@ -0,0 +1,256 @@ +# QuizTrain 📝🚆 + +QuizTrain is a framework created at Venmo allowing you to interact with [TestRail's API](http://docs.gurock.com/testrail-api2/start) using Swift. It supports iOS, macOS, tvOS, and watchOS. + +To use QuizTrain you must have a valid [TestRail](http://www.gurock.com/testrail/) license and instance to access. + +## Licensing + +QuizTrain is open source software released under the MIT License. See the [LICENSE](LICENSE) file for details. + +## Installation + +[Carthage](https://github.com/Carthage/Carthage) is the recommended way to install QuizTrain. Add the following to your `Cartfile` or `Cartfile.private` file: + + github "venmo/QuizTrain" ~> 1.0.0 + +See [Adding frameworks to an application](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application) for further instructions. Once complete `import QuizTrain` in any Swift files you wish to use QuizTrain in. + +## Usage + +Create an `ObjectAPI` to get, add, update, delete, and close items on your TestRail instance. + + let objectAPI = ObjectAPI(username: "your@testRailAccount.email", secret: "your_api_key_or_password", hostname: "yourInstance.testrail.net", port: 443, scheme: "https") + +Alternatively you can use `API` directly if you would rather work with basic Swift types. Generally it is better to use `ObjectAPI` as `API` is a lower level of abstraction. For differences see comments in [API.swift](QuizTrain/Network/API.swift) and [ObjectAPI.swift](QuizTrain/Network/ObjectAPI.swift). + +## Examples + +Below shows a limited number of examples. For all examples see [ObjectAPITests.swift](QuizTrainTests/Network/ObjectAPITests.swift). + +#### Get all Cases in a Project + + objectAPI.getCases(inProjectWithId: 5) { (outcome) in + switch outcome { + case .failed(let error): + print(error.debugDescription) + case .succeeded(let cases): + print(cases) // Do something with cases. + } + } + +#### Add a Case + + let section: Section = ... + let newCase = NewCase(estimate: nil, milestoneId: nil, priorityId: nil, refs: nil, templateId: nil, title: "New Case Title", typeId: nil, customFields: nil) + + objectAPI.addCase(newCase, to: section) { (outcome) in + switch outcome { + case .failed(let error): + print(error.debugDescription) + case .succeeded(let `case`): + print(`case`.title) // Do something with the newly created `case`. + } + } + +#### Update a Suite + + var suite: Suite = ... + suite.description = "Updated description for this suite." + suite.name = "Updated name of this suite." + + objectAPI.updateSuite(suite) { (outcome) in + switch outcome { + case .failed(let error): + print(error.debugDescription) + case .succeeded(let updatedSuite): + print(updatedSuite.description) // "Updated description for this suite." + print(updatedSuite.name) // "Updated name of this suite." + } + } + +#### Delete a Section + + let section: Section = ... + + objectAPI.deleteSection(section) { (outcome) in + switch outcome { + case .failed(let error): + print(error.debugDescription) + case .succeeded(_): // nil on successful deletes + print("The section has been successfully deleted.") + } + } + +#### Close a Plan + + let plan: Plan = ... + + objectAPI.closePlan(plan) { (outcome) in + switch outcome { + case .failed(let error): + print(error.debugDescription) + case .succeeded(let closedPlan): + print(closedPlan.isCompleted) // true + print(closedPlan.completedOn) // timestamp + } + } + +#### Get a Relationship + + let milestone: Milestone = ... + + milestone.parent(objectAPI) { (outcome) in + switch outcome { + case .failed(let error): + print(error.debugDescription) + case .succeeded(let optionalParent): + if let parent = optionalParent { + print("Milestone \(milestone.id) has a parent with an id of \(parent.id).") + } else { + print("Milestone \(milestone.id) does not have a parent.") + } + } + } + +#### Get Completed Runs in a Project using a single Filter + + let filters = [Filter(named: "is_completed", matching: true)] + + objectAPI.getRuns(inProjectWithId: 3, filteredBy: filters) { (outcome) in + switch outcome { + case .failed(let error): + print(error.debugDescription) + case .succeeded(let completedRuns): + for completedRun in completedRuns { + print(completedRun.isCompleted) // true + } + } + } + +#### Get Plans in a Project using multiple Filters + + let project: Project = ... + let filters = [Filter(named: "offset", matching: 3), + Filter(named: "limit", matching: 5)] + + objectAPI.getPlans(in: project, filteredBy: filters) { (outcome) in + switch outcome { + case .failed(let error): + print(error.debugDescription) + case .succeeded(let plans): // There will be 5 or less plans. + for plan in plans { + print(plan.name) + } + } + } + +## Error Handling + +It is up to the consumer of QuizTrain to handle all errors. However `ObjectAPI` will handle 429 Too Many Request (rate limit reached) errors automatically unless you set `.handle429TooManyRequestErrors` to `false`. + +Errors are defined here: + +- [API.swift](QuizTrain/Network/API.swift) in the *Errors* section. +- [ObjectAPI.swift](QuizTrain/Network/ObjectAPI.swift) in the *Errors* section. + +You can handle errors two ways: + +1. Simply by using `error.debugDescription` to print a rich description of an error. + - *Provided by errors conforming to `DebugDescription`.* +2. Advanced by `switch`'ing on an error. + +Simple is best for debugging and logging. Advanced is best for everything else. + +All API and ObjectAPI errors conform to [`DebugDescription`](QuizTrain/Misc/Debug/DebugDescription.swift). If these errors print a `URLRequest` through this protocol then its `AUTHORIZATION` header will be stripped to avoid exposing your TestRail credentials. + +### Example + +This shows both simple and advanced error handling when adding a new Case to a Section. + +#### Setup + + let newCase = NewCase(estimate: nil, milestoneId: nil, priorityId: nil, refs: nil, templateId: nil, title: "New Case Title", typeId: nil, customFields: nil) + +#### Simple + + objectAPI.addCase(newCase, toSectionWithId: 5) { (outcome) in + switch outcome { + case .failed(let error): + print(error.debugDescription) + case .succeeded(let `case`): + print(`case`.title) // Do something with the newly created `case`. + } + } + +#### Advanced + + objectAPI.addCase(newCase, toSectionWithId: 5) { (outcome) in + switch outcome { + case .failed(let error): + switch error { + case .apiError(let apiError): // API.RequestError + switch apiError { + case .error(let request, let error): + print(request) + print(error) + case .invalidResponse(let request, let response): + print(request) + print(response) + case .nilResponse(let request): + print(request) + } + case .dataProcessingError(let dataProcessingError): // ObjectAPI.DataProcessingError + switch dataProcessingError { + case .couldNotConvertDataToJSON(let data, let error): + print(data) + print(error) + case .couldNotDeserializeFromJSON(let objectType, let json): + print(objectType) + print(json) + case .invalidJSONFormat(let json): + print(json) + } + case .objectConversionError(let objectConversionError): // ObjectAPI.ObjectConversionError + switch objectConversionError { + case .couldNotConvertObjectsToData(let objects, let json, let error): + print(objects) + print(json) + print(error) + case .couldNotConvertObjectToData(let object, let json, let error): + print(object) + print(json) + print(error) + } + case .statusCodeError(let statusCodeError): // ObjectAPI.StatusCodeError + switch statusCodeError { + case .clientError(let clientError): // ObjectAPI.ClientError + print(clientError.message) + print(clientError.statusCode) + print(clientError.requestResult.request) + print(clientError.requestResult.response) + print(clientError.requestResult.data) + case .otherError(let otherError): // API.RequestResult + print(otherError.request) + print(otherError.response) + print(otherError.data) + case .serverError(let serverError): // ObjectAPI.ServerError + print(serverError.message) + print(serverError.statusCode) + print(serverError.requestResult.request) + print(serverError.requestResult.response) + print(serverError.requestResult.data) + } + } + case .succeeded(let `case`): + print(`case`.title) // Do something with the newly created `case`. + } + } + +## Testing + +See the [QuizTrainTests Readme](QuizTrainTests/README.md). + +## Entities + +![Image of Entities](Entities.png)