From 178900de6e814021c1dc02be4b7fdb94fff9eab5 Mon Sep 17 00:00:00 2001 From: Timon Borter Date: Tue, 26 Sep 2023 21:04:17 +0200 Subject: [PATCH] feat: test results on landing page displaying test results on the landing page mimics the behaviour of the old ui. --- .github/workflows/frontend.yml | 10 +- DEVELOPMENT.md | 2 + pom.xml | 36 +- .../src/main/asciidoc/images/dashboard.png | Bin 85679 -> 312225 bytes simulator-samples/pom.xml | 6 +- simulator-samples/sample-bank-service/pom.xml | 2 +- simulator-samples/sample-combined/pom.xml | 2 +- simulator-samples/sample-jms-fax/pom.xml | 2 +- simulator-samples/sample-jms/pom.xml | 2 +- simulator-samples/sample-mail/pom.xml | 2 +- simulator-samples/sample-rest/pom.xml | 8 +- simulator-samples/sample-swagger/pom.xml | 2 +- .../simulator/SimulatorSwaggerIT.java | 3 +- simulator-samples/sample-ws-client/pom.xml | 2 +- simulator-samples/sample-ws/pom.xml | 2 +- simulator-samples/sample-wsdl/pom.xml | 2 +- .../simulator/sample/Simulator.java | 2 +- .../src/main/resources/application.properties | 4 + ...ava => SimulatorWebServiceWithWsdlIT.java} | 4 +- simulator-starter/pom.xml | 78 +- .../simulator/SimulatorAutoConfiguration.java | 4 +- .../controller/ActivityController.java | 75 -- .../controller/MessageController.java | 49 -- .../controller/SummaryController.java | 66 -- .../http/SimulatorRestAutoConfiguration.java | 1 + .../listener/SimulatorStatusListener.java | 52 +- .../model/AbstractAuditingEntity.java | 24 +- .../simulator/model/MessageFilter.java | 67 +- .../simulator/model/TestParameter.java | 41 +- .../simulator/model/TestResult.java | 72 +- .../repository/MessageRepositoryImpl.java | 4 +- .../repository/TestParameterRepository.java | 13 + .../repository/TestResultRepository.java | 30 +- .../simulator/service/ActivityService.java | 2 +- .../simulator/service/QueryService.java | 526 ++++++++++++ .../service/TestParameterQueryService.java | 110 +++ .../service/TestParameterService.java | 36 + .../service/TestResultQueryService.java | 125 +++ .../simulator/service/TestResultService.java | 85 +- .../simulator/service/criteria/Criteria.java | 5 + .../criteria/TestParameterCriteria.java | 176 ++++ .../service/criteria/TestResultCriteria.java | 289 +++++++ .../service/dto/TestResultByStatus.java | 10 + .../simulator/service/filter/Filter.java | 200 +++++ .../service/filter/InstantFilter.java | 101 +++ .../service/filter/IntegerFilter.java | 35 + .../simulator/service/filter/LongFilter.java | 35 + .../simulator/service/filter/RangeFilter.java | 182 ++++ .../service/filter/StringFilter.java | 125 +++ .../impl/TestParameterServiceImpl.java | 53 ++ .../service/impl/TestResultServiceImpl.java | 63 ++ .../web/rest/TestParameterResource.java | 85 ++ .../web/rest/TestResultResource.java | 97 +++ .../simulator/web/util/PaginationUtil.java | 62 ++ .../simulator/web/util/ResponseUtil.java | 41 + .../simulator/IntegrationTest.java | 20 + .../repository/TestResultRepositoryIT.java | 56 ++ .../service/MessageRepositoryTest.java | 197 ----- .../simulator/service/MessageServiceTest.java | 7 - .../ScenarioExecutionRepositoryTest.java | 265 ------ .../criteria/TestParameterCriteriaTest.java | 92 +++ .../criteria/TestResultCriteriaTest.java | 162 ++++ .../service/dto/TestResultByStatusTest.java | 37 + .../simulator/service/filter/FilterTest.java | 96 +++ .../service/filter/InstantFilterTest.java | 75 ++ .../service/filter/IntegerFilterTest.java | 54 ++ .../service/filter/LongFilterTest.java | 54 ++ .../service/filter/RangeFilterTest.java | 111 +++ .../service/filter/StringFilterTest.java | 101 +++ .../impl/TestParameterServiceImplTest.java | 102 +++ .../impl/TestResultServiceImplTest.java | 90 ++ .../simulator/test/TestApplication.java | 17 + .../web/rest/TestParameterResourceIT.java | 510 ++++++++++++ .../web/rest/TestResultResourceIT.java | 781 ++++++++++++++++++ .../simulator/web/rest/TestUtil.java | 76 ++ .../simulator/web/util/ResponseUtilTest.java | 77 ++ simulator-ui/.eslintrc.json | 12 +- simulator-ui/.jhipster/TestParameter.json | 34 + simulator-ui/.jhipster/TestResult.json | 59 ++ simulator-ui/.yo-rc.json | 2 +- simulator-ui/README.md | 2 +- simulator-ui/pom.xml | 74 +- .../ui/config/SecurityConfiguration.java | 86 +- .../simulator/ui/config/ServletUtils.java | 22 + .../simulator/ui/filter/CookieCsrfFilter.java | 28 - .../simulator/ui/filter/SpaWebFilter.java | 19 +- .../webapp/app/config/font-awesome-icons.ts | 2 + .../config/application-config.service.spec.ts | 8 - .../core/config/application-config.service.ts | 14 +- .../app/core/util/alert.service.spec.ts | 16 +- .../app/entities/entity-routing.module.ts | 10 + .../test-parameter-detail.component.html | 42 + .../test-parameter-detail.component.spec.ts | 38 + .../detail/test-parameter-detail.component.ts | 22 + .../list/test-parameter.component.html | 104 +++ .../list/test-parameter.component.spec.ts | 125 +++ .../list/test-parameter.component.ts | 154 ++++ ...-parameter-routing-resolve.service.spec.ts | 101 +++ .../test-parameter-routing-resolve.service.ts | 31 + .../service/test-parameter.service.spec.ts | 165 ++++ .../service/test-parameter.service.ts | 128 +++ .../test-parameter/test-parameter.model.ts | 12 + .../test-parameter/test-parameter.routes.ts | 25 + .../test-parameter.test-samples.ts | 48 ++ .../detail/test-result-detail.component.html | 56 ++ .../test-result-detail.component.spec.ts | 38 + .../detail/test-result-detail.component.ts | 22 + .../list/test-result.component.html | 147 ++++ .../list/test-result.component.spec.ts | 125 +++ .../test-result/list/test-result.component.ts | 154 ++++ ...est-result-routing-resolve.service.spec.ts | 99 +++ .../test-result-routing-resolve.service.ts | 29 + .../service/test-result.service.spec.ts | 174 ++++ .../service/test-result.service.ts | 111 +++ .../entities/test-result/test-result.model.ts | 15 + .../test-result/test-result.routes.ts | 25 + .../test-result/test-result.test-samples.ts | 49 ++ .../main/webapp/app/home/home.component.html | 33 +- .../webapp/app/home/home.component.spec.ts | 56 ++ .../main/webapp/app/home/home.component.ts | 26 +- .../home/test-result-summary.component.html | 45 + .../test-result-summary.component.spec.ts | 77 ++ .../app/home/test-result-summary.component.ts | 39 + .../app/layouts/navbar/navbar.component.html | 24 + .../layouts/profiles/profile-info.model.ts | 8 + .../src/main/webapp/i18n/en/global.json | 2 + .../src/main/webapp/i18n/en/home.json | 13 +- .../main/webapp/i18n/en/testParameter.json | 20 + .../src/main/webapp/i18n/en/testResult.json | 24 + .../simulator/ui/IntegrationTest.java | 4 +- .../simulator/ui/SimulatorStarter.java | 12 - .../config/InfoEndpointConfigurationTest.java | 34 + .../simulator/ui/config/ServletUtilsTest.java | 27 + .../SimulatorUiAutoconfigurationIT.java | 19 + .../simulator/ui/filter/SpaWebFilterIT.java | 36 +- .../simulator/ui/filter/SpaWebFilterTest.java | 104 +++ .../simulator/ui/test/TestApplication.java | 26 + .../src/test/resources/application.properties | 5 + 138 files changed, 8183 insertions(+), 970 deletions(-) rename simulator-samples/sample-wsdl/src/test/java/org/citrusframework/simulator/{SimulatorWebServiceIT.java => SimulatorWebServiceWithWsdlIT.java} (97%) delete mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/controller/ActivityController.java delete mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/controller/MessageController.java delete mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/controller/SummaryController.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/repository/TestParameterRepository.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/service/QueryService.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/service/TestParameterQueryService.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/service/TestParameterService.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/service/TestResultQueryService.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/Criteria.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/TestParameterCriteria.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/TestResultCriteria.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/service/dto/TestResultByStatus.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/Filter.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/InstantFilter.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/IntegerFilter.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/LongFilter.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/RangeFilter.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/StringFilter.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/TestParameterServiceImpl.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/TestResultServiceImpl.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/TestParameterResource.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/TestResultResource.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/web/util/PaginationUtil.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/web/util/ResponseUtil.java create mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/IntegrationTest.java create mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/repository/TestResultRepositoryIT.java delete mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/service/MessageRepositoryTest.java delete mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/service/ScenarioExecutionRepositoryTest.java create mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/service/criteria/TestParameterCriteriaTest.java create mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/service/criteria/TestResultCriteriaTest.java create mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/service/dto/TestResultByStatusTest.java create mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/service/filter/FilterTest.java create mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/service/filter/InstantFilterTest.java create mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/service/filter/IntegerFilterTest.java create mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/service/filter/LongFilterTest.java create mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/service/filter/RangeFilterTest.java create mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/service/filter/StringFilterTest.java create mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/service/impl/TestParameterServiceImplTest.java create mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/service/impl/TestResultServiceImplTest.java create mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/test/TestApplication.java create mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/TestParameterResourceIT.java create mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/TestResultResourceIT.java create mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/TestUtil.java create mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/web/util/ResponseUtilTest.java create mode 100644 simulator-ui/.jhipster/TestParameter.json create mode 100644 simulator-ui/.jhipster/TestResult.json create mode 100644 simulator-ui/src/main/java/org/citrusframework/simulator/ui/config/ServletUtils.java delete mode 100644 simulator-ui/src/main/java/org/citrusframework/simulator/ui/filter/CookieCsrfFilter.java create mode 100644 simulator-ui/src/main/webapp/app/entities/test-parameter/detail/test-parameter-detail.component.html create mode 100644 simulator-ui/src/main/webapp/app/entities/test-parameter/detail/test-parameter-detail.component.spec.ts create mode 100644 simulator-ui/src/main/webapp/app/entities/test-parameter/detail/test-parameter-detail.component.ts create mode 100644 simulator-ui/src/main/webapp/app/entities/test-parameter/list/test-parameter.component.html create mode 100644 simulator-ui/src/main/webapp/app/entities/test-parameter/list/test-parameter.component.spec.ts create mode 100644 simulator-ui/src/main/webapp/app/entities/test-parameter/list/test-parameter.component.ts create mode 100644 simulator-ui/src/main/webapp/app/entities/test-parameter/route/test-parameter-routing-resolve.service.spec.ts create mode 100644 simulator-ui/src/main/webapp/app/entities/test-parameter/route/test-parameter-routing-resolve.service.ts create mode 100644 simulator-ui/src/main/webapp/app/entities/test-parameter/service/test-parameter.service.spec.ts create mode 100644 simulator-ui/src/main/webapp/app/entities/test-parameter/service/test-parameter.service.ts create mode 100644 simulator-ui/src/main/webapp/app/entities/test-parameter/test-parameter.model.ts create mode 100644 simulator-ui/src/main/webapp/app/entities/test-parameter/test-parameter.routes.ts create mode 100644 simulator-ui/src/main/webapp/app/entities/test-parameter/test-parameter.test-samples.ts create mode 100644 simulator-ui/src/main/webapp/app/entities/test-result/detail/test-result-detail.component.html create mode 100644 simulator-ui/src/main/webapp/app/entities/test-result/detail/test-result-detail.component.spec.ts create mode 100644 simulator-ui/src/main/webapp/app/entities/test-result/detail/test-result-detail.component.ts create mode 100644 simulator-ui/src/main/webapp/app/entities/test-result/list/test-result.component.html create mode 100644 simulator-ui/src/main/webapp/app/entities/test-result/list/test-result.component.spec.ts create mode 100644 simulator-ui/src/main/webapp/app/entities/test-result/list/test-result.component.ts create mode 100644 simulator-ui/src/main/webapp/app/entities/test-result/route/test-result-routing-resolve.service.spec.ts create mode 100644 simulator-ui/src/main/webapp/app/entities/test-result/route/test-result-routing-resolve.service.ts create mode 100644 simulator-ui/src/main/webapp/app/entities/test-result/service/test-result.service.spec.ts create mode 100644 simulator-ui/src/main/webapp/app/entities/test-result/service/test-result.service.ts create mode 100644 simulator-ui/src/main/webapp/app/entities/test-result/test-result.model.ts create mode 100644 simulator-ui/src/main/webapp/app/entities/test-result/test-result.routes.ts create mode 100644 simulator-ui/src/main/webapp/app/entities/test-result/test-result.test-samples.ts create mode 100644 simulator-ui/src/main/webapp/app/home/home.component.spec.ts create mode 100644 simulator-ui/src/main/webapp/app/home/test-result-summary.component.html create mode 100644 simulator-ui/src/main/webapp/app/home/test-result-summary.component.spec.ts create mode 100644 simulator-ui/src/main/webapp/app/home/test-result-summary.component.ts create mode 100644 simulator-ui/src/main/webapp/i18n/en/testParameter.json create mode 100644 simulator-ui/src/main/webapp/i18n/en/testResult.json delete mode 100644 simulator-ui/src/test/java/org/citrusframework/simulator/ui/SimulatorStarter.java create mode 100644 simulator-ui/src/test/java/org/citrusframework/simulator/ui/config/InfoEndpointConfigurationTest.java create mode 100644 simulator-ui/src/test/java/org/citrusframework/simulator/ui/config/ServletUtilsTest.java create mode 100644 simulator-ui/src/test/java/org/citrusframework/simulator/ui/config/SimulatorUiAutoconfigurationIT.java create mode 100644 simulator-ui/src/test/java/org/citrusframework/simulator/ui/filter/SpaWebFilterTest.java create mode 100644 simulator-ui/src/test/java/org/citrusframework/simulator/ui/test/TestApplication.java create mode 100644 simulator-ui/src/test/resources/application.properties diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 18a73d4ce..4d4cb830f 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -36,7 +36,7 @@ on: - 'LICENSE' - 'NOTICE' jobs: - build: + simulator-ui: strategy: fail-fast: true matrix: @@ -51,11 +51,15 @@ jobs: uses: actions/checkout@v4 - name: Info run: | - node -version - npm -version + node --version + npm --version - name: Change to Citrus-Simulator UI run: cd simulator-ui - name: Install dependencies run: npm ci --cache .npm + - name: Lint + run: npm run lint + - name: Prettier + run: npm run prettier:check - name: Frontend Tests run: npm run ci:frontend:test diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 99b8cf854..6546ecb35 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -9,6 +9,8 @@ After forking/cloning the source code repository from [https://github.com/citrus mvn clean install ``` +Add the `-DskipFrontend=false` parameter to include the `simulator-ui` into the build. + ## Lombok This will compile all classes and generate constructors as well as getters and setters using [Project Lombok](https://projectlombok.org/). diff --git a/pom.xml b/pom.xml index 5073eeeec..0dd55d2e1 100644 --- a/pom.xml +++ b/pom.xml @@ -20,9 +20,11 @@ none + 3.11.0 + 1.18.20 4.0.0-M2 - 3.1.2 + 3.1.2 6.0.9 3.1.3 7.5.1 @@ -164,7 +166,7 @@ org.springframework.boot spring-boot-dependencies - ${spring.boot.version} + ${spring-boot.version} pom import @@ -252,12 +254,6 @@ ${testng.version} test - - org.mockito - mockito-core - 2.15.0 - test - @@ -292,13 +288,6 @@ ${project.build.sourceEncoding} ${java.version} ${java.version} - - - org.projectlombok - lombok - ${lombok.version} - - @@ -377,21 +366,32 @@ org.apache.maven.plugins maven-surefire-plugin - 2.22.2 + 3.1.2 + + **/*IT.java + + false 1 ${reuseForks} - false org.apache.maven.plugins maven-failsafe-plugin - 2.22.2 + 3.1.2 false + + + + integration-test + verify + + + diff --git a/simulator-docs/src/main/asciidoc/images/dashboard.png b/simulator-docs/src/main/asciidoc/images/dashboard.png index d4a57fdba2e3d4a5ee998a0c0e7b72746a2d8bac..145bff80adbe6a823d3088185cb4b27ac56c345d 100644 GIT binary patch literal 312225 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

Ixs;%1MMrlz9-Hr zxCE;K>Q)8rDyKatusYe*y2#+$gP-**^8CQ_Ngx#1kWWQ{LpS&u(fFhH1sh`LpJC*I zHp?xAnKqvDxVR57mtVa)6`3=E_@_u%h@~FQNxtmL)j((3%igy6a5-7qO?oh325)C*&y=RnoTB0;o3F{*&q=ZhHu4Zjj`1WE-By3RXLSssHA-V2l7I24#Ox zH6UPGOvIM^Ulsy=spaIOhZm?OCXbH|9VLZY@fkymQ`!_kYSDD9N&&2*CZ+&n=?DO! zH-y6=HY7)CxC3+lt5NkDk_#OLHl0*|aC+=I^_4_|SUndolCLoJ6D$KnFn`;5!@$x4 z9;juNF!j41R$WjRqVjqXOp_v5vovH|^*Hpgt*YBc6dL_y~) z8$czR4pqAd4EmNw=J~bh$Q;xmRhj327bVe8PB&$myT4ax=g9R6)c7cIVCV?C_iO$r zpUI^6(`V%UnfR9-VYLsVy^scv-XxP|=ZA+g6(2np@U5BA3jQoC_p8`AnqB&IaV`gC zu2O#Q`(;DYP1DVhe|8oH+~g)*;I~SMDX&Vkdn+dH0yoNT>>*8Go%P+8yS(JL+f%yL|vkN_G@&1JehS zAuG5PZgW%QQRgMuGxEoSut#chkmysX*;q{_s*kpLE`$d9AB{$ymVkN{@?%Qrl8|x} zXwZ_W2o<#Ih`d!U@H%7)(f zc$11Ho+{SQz;+e`YQyV|GNreT+A@)-2f#1RX8@3BJXmTT!^^S4taTQ(iQQHq1E>8J zkap%Tx8}N~Ty|G1U%12H-S#|jv%Fa~bQ!S{d>w*#ghARHN5yy-%ceaK>ghdzMM_P( zE6KE9te>QpcNx+=)htER)o2=8NIs`UR)fyO-#o)XQ=0D=EHu|7bW71}!W^dghb5uP z2?+#ja<^_r3A=3FC5|X%_e%r>#@s}7QDROZ#QkH+s-XP>Vkfv`(ukkzno`5e_(f1Q zNBCgl3D&L-Ttxu)0L}KUiAm;;KGrPkET#u8dq>TS7m4PS@REX@Y`k7whe3R#-cy&l z3exl%&7-A%@0D{3fY~O|HlaceSU3ak7l-B3NnAnf_k~dI z{ZqAotJ+M98K?GHJzVtR|9Cx^9K69#GU&IbHJD2py|b7iHI2p;&@Qx(XW59h#IJQ7 z*wK$Z>*nEQMS-w`o!))x)kw8t=ggKirGp2k$)@1X54p@01m%6p8NhCv6lLgRur45oia$JdA(S zQIW_9P>Zn^%St3B8RtU=!P}B2^SX+ct#r6;QJ=Xk`jB@Vy|ziFQ*I>NUvh+Og&s&t8I`-dXGnWl z`V8)=G$rhQgXbxB za_a4zhj?6InPtE-(*|Uhi)^Mz$`;MUp<}|3ASxn9^7B-4a;!WowMlvsNZ+nS)cC~D zpwA4*HhfyaA6*yzO?wGRxNR8BZPL}y<#uTc@;uU-ZtIj^!xAMCmSO1iSOvl`T204U zdRGIi)Zsk>sH(M-x1f6%wflIM7{AQ%j_f25RF~e75*d5>4t=DU4Og-T{CpbJDHr*( z?LlTKMda1sG-8h0t>AiUDyy&se7%aGjHaVP%PgIsh!8cRI;z=|Afu$8$mas3Cd0bB#Z+NtanCc78FjM!NAbSq3rrTBszo= zpZjwBfxHTth4HXf27v(OgBo=-n5?F$-jRrw{KON)NG&eV0H<-$Jb9kuDu-Yl_($*a z>A}XAG{J~9XW)WX<aRl99ALj^+~RC`MDsXT&y~W z!kOgpX(-k}Q;Pd;Ji@ndelr1c9kg%OH4p6=p@h<6Nh++olD#rq(WE-!K%^6$p#Hx0 z=K0Un4P0`wg%MqxmZKKO#&-_{b0^*Dwf^C$e&u2QyVO_^(H?Yt?2Pg!hVVdNi)hXs z<`NK?@X}&;Z&QqVj(&96S^DT7-EDYLn+8UKkA7AOAB7xOuF;ADu0fwF$1&5MC{hi^ zTgm<2$AM#|N1PS!Hxwa;K@BXZ zs1!W9sZvr>?t78xR;Xq|l+|~z@K^{woE+)iN2%k4&;#WU;nW(#_1z_B9vt%J`hk)R(Z@pu@}6^Mm7Ku2h$pKvn&n z(7ng#&Cw0^7mgcbR#o~||E#6N^_Z~mL;%oY_A(vv?4TH>BH!8@rVL+^v_&;mqr*c) zPmk=hTS4MIw0+@hw?NNb9C_`R8iZgCuIMT4c~wLhszD#l{srw2?@73T!GS#EKA#qo zC^QWq*feMpo}U~!4_4Y`#EZHgRJKI3Yir=r7`46ae#J&5+FZB!9LCQnz)g9DbtMTP zM97alXRbMmT!+u;_|iv!t9#O0?cwW-vPw3hsJb_)BRlYr?aEUR*&YAg0w|^?fe;v` z>|D#gSw~Jxoa|?jTfOrP9T0L4Dm`|};DC-b5WhZzwTR7$6kM~2i~8&Dbis-P9(JkW zMP1KhW&qavPpC&fz;-zC)wG5GoOVL4kN{|79ubcr%hs&3!SK(TkdWKj->)WmvZv3S z6S~r{{dZE!s&IX$HdxkbT z&12ZD?sshiV|=Sw(CmfPXGF;<%)Go(HXIXlxquUHsL!pg(l5T*%lSOZP50Kzu7JaB zh7lPS*3&_DG8!T-uwqx*wM>iQ{6EUO|7Cp_fcoyzQ97XjQ!@+Jd!NMbV#f=2r)tIG z-!cc`mmWoCG7rYxQHai%x57k_2p1mt+_sW>lpcQ2dV}6`-fDQt}u1wZ|+Vdc;)R~*)KnOGbuL|u>SIVQ#U37q;pFuC?r zlIo(ovu6{ynzvU*HHka6Aa#^g-U4(qp?4a(d%r`@4CXkf$h+Q>yn%5jhj9m;q>KA9 zn+JA5(}9ANlu8}$IY6>e^#?GY7Ib>aVN%JYu6H8Q1xz zYJRS{^f`JW9AotBiD8>fbYRd5e-bXuU=Qh6-wRRxftO$g5I>COB3xiX&?E^v-)^2y zHRqr!@!HkJ3NHK!Y8^Dj{zm^NWFo4UnngY3ZS%!3Np5wq@K@rHYsc#TFRUQzo{o=SxU^NjqNnpMQtl)w=5v2fZOlKT`P~J~odhwG zGFpBcy_$k#JAJTaTc9NO2pNTC&!^}u@zvdcrQ!L+XWYP;FNFXyJQB>&a9l(>K_x-2 zHzg+BlCWcBA&Eo755Qml27CYzDi-!c$iBJ7BVN7fb>1s?Sv9Qj?ojfSJFwCfLKZcz z#N&|+MCLnfNN0`4ID5{d_r2T#^uKl;nb>^vRu58+arX`vOJnj;Fs7pYrO;e4p`4Lm=zQ{(-aE+ zjl0@ktMghd{J*e{{|5p2|N7g`3y}X*oJZj(hjIQAF=Fq88Z4^44PQS9_!W+Qa2_39 z&heF!Wa4LfG^pBqRhzjr0|L-$s+Xe<{}HZzj`4B_XC{OsE}(L$Ia*Uw4S@$_`n*92 z>ecIElw8dz+d%zY9iDO!TTqTR#-~RD#{A3)tQhW1^wx(lHV_8 zy;@6Jd0jxIM_(hQL^LiWE5t5TT$bJJ@`+~%wCG6i$y{-QXa+;5zyWt{YQZ&^75YU4 z33O(RmXseAh?Vkk`%UNK#jV)$zph=e7m1v+dY_s)Z3@yu8JX*t*d8_4>nMwv9ug$-{ymC|zga0n7^wHl z(z`RkqtaD7O!7FmM;xhw=Uoo-Jj+3=Z+&>Yn5Pc>f-HbtN5SBPsJbWWym1EzlPs+` zU}+@~giqxvK0vO<^VtqIv`!rMomTMSn`X+Hzi;80$9nyV2lTj+^4Iu*Hore>toG*E zoOaZ}mOf5RR0`@m_3nF`X=jIDqY3X+_f-?)hM8Cb8)16uPna*|fYMGZ$=B1lY#P2+`ctIP% z?#?=#(dnABC=|EMRV_5#(YER~cDxiIvKv3^)~UU9>~^&3dj=E1;Ps#W!X)nnzu4U1 zB@C0qo0ff#AxhK6YswW6$oyDsxb)i<71a+@>$x8LjU zuOmVU!>1N^+K3B__^zLq=cL)-^K}GC5(~+2+8`a{q;Oe9A}iVXdz_adl{l|Sbayrf zp2V(q4-^bqJmV7$Ipq@FvfJ|<{$dnvU2FSAZ#@%h>-ZY$4GPy`2q`#Fp6pnDDAK}& zMJ#>>0u~FKS_=r{7mKOBI?$bH+8~JIkJ9 z9bv5FU`}wYG+DdRa!b~%)7AUCm%3FCg#OCt9VZTKvU1~ZJ8i#fiQ%4fAELc>sdXcG z149*;o0LTU;JJx6pC?{wAIP5lh4_#mzTdigZ?JVYeRlkUZPq1h*SPCEmu??ynU(9L zs#rJRpPH@hmUq(z*jdZra%WlaVb*n=5*-LG*ZX_S9}OvYPYJ6%o+dwRIQyYr zq&QOLO!ehIdCpz;;W_7GP1#}*7le6PFDtXwZC!e$*B~gJro~K^W$E5BhN2TP8F?z+ zRF84xC|j3W+tVFm`G9(|E5CcXKV{T?yJ?;*rgv_|9LtLmMTVekTrJu-Kfk>c6Ip9V zZ%%&i@qc~6{yK=q`n!td8`{_|0TW9t1C6U+4q^)2{G zl*N-LLRr$b2IEwcmCa)=-4>h}y4h7&K6qb-MXkQOub+PHW&d8zxZ?5()yr{i!963x z%8-bBIT}LOo6!cLs^^_TvzONws0t}*V<`XV%lQy_rg7|16`Ps#8Siu0M{%A}Y@U?t z6Wa_w?t8x>*x|KVQTjSJMa?OAKsD-i>_KpJz-)lrvurOIUXIlFb1NIk&qzFl^ z^>X9uo_BRHmO}nhE&Qj1isNA+cFN9#$`Xq=XkdQnk7uY$sxMG|$f^WEc=Ylkng|NrN3&L1{~Y+i%!D9XE48AL^x*|^g18PWxi*s^#9PlGC!8j_et8{ zXMzW_PV}d>#7{lWft)y=vvrj+4SX-Xtc!nX2vqhi=fBK8t;^N&Zz1`EDoK1d1kT83 z{$e~##Ak_Qi|+}_C*P%Pp9o0B;8Qm0Xl@Fj(z8{+B6eItBl24oxvKWhy%$X%wTkhn z(XtgxcqrENIU0}OVd$}-*qZkMjZ?YSE0#a`c`h)E7w|poS%SuU>muTfMO6DN(ZzLF z{VJ=g9OGPWMr|y6D zc-Y|a^!eR{%K43WV14bPGSBR^fki@@t663Y!fa}>PcMIX@|MuP3>DQv-%2fe%#EH$ zbw8;vjH8hrCVBkTTPE$-g3ASu*|-X=DQ_HtOa zryd`>er4)LO1dlUtbwtdj+H_&%J}&vR?*I-tF+dMV>N+qL;vXWec#S=E10%$ss;GW>kMMMX3w1>+)rp7qd8ZO43J%RqOiRfQ z)yS$<5nKD!m;$yz#2=MP6F3GkH@*nNoLALQ7N)`L# zaZOFCI=;5@e5xJvbJe#@ES>f4Sx-0p>$v_&aWW}|6QD|31P7T(vZ4G)ll5fr?H!A# zLDz`I(AMKXCZ;HvivG#%KFuT>Ypt(o$5D#+I|?5D@yRnufzKuXw0;RbS5@zjb~fm6 zb1-Q1%_#AULUH|3SGsnCr59)3dqLLDQ^w1#D+G^|H)~;$pXjhxtgQ|fb_AD%7k=uKzPjaM+%i0{50XySUp7Rk48Gy9Dc?H`>p zM5B%lCX9DA4Pl2@<@%2L*>sJo$V|8hFWir5da_4z@v+sqlW^a~Aq!qp%_j^F)uW#t zXQ-O)lqIy~)t1HA{J0)(_9wr2k18xmLpp3YQVb$km|v(3Ht7jDBh&B5lRBiDS^7Po zT&Gw&a8uTKs9yW3X`HjEHe>*oEb)0IS-ji5C2|AiKYF|Wk4C&sam{tzfz@-nd(Zka zgl2Nf%9rsmT1#_YdQ-IV92d(IFW=a>l1VI_u(F3NP0m!i& zy-)tiPR%^?Zg)BY{-D@LT!!IQW$OJkQtt<3nQqouZ-gBTuhNZT8mAjt-)2wAd9Umy znlxa^YucaQF!#I1lQK< z*YAjxv)z0!`TFXPiGPOHRcsdBcLVZ+1bNoCvqvXAAdRTnx4~~Jj5v&94ONX;th$qT=n^WT#o|KP7 zmKxrF=uE13(bdDdn_GY>DqrEXlDcU+&FJ%4{BvhzcFjnto1Rt51tDH0s1XyWPXl?)pM_*e8QEr&d2KAw;Gie_EF~DO#N)RH4fdYEBo|Ui$8?? z6e2qx+-}tA6!-*#Ok_QlUvQNsd-})?pAGLn6`>E&&~1NIab!s|ph596^+le6#6D~!jvkMldBr*9o3pN$$cxBJ0j4!91cge{Z+!mKFZ~aUh6^9fH6AT+1(Wr1!DQVxWuDV*vX8vS-z{=9 zt@l1O5-g@Y--(-dv{88T3Ugn-;}!4h$(1Xe9VvfxTxD53P@aMA_kuIrsyh`SMQB=N z6W($Kork`$@jO<@F%SNI(`}sXeYe|i?bXMeoHu|1-+%fv#n3EU6K*@zM90Z)K=i>k zupqTrua@*G+(G#A;OCo%O$(qnF$^7BKb*)^LTLh@rD1$YoGAs4m0?EKv6JocV~ z+We8Pom5Ih4X~3NKTl88>W+6b(OPsYUy02n2w=h%0#d#AdGyJl?r+U7Ky>;=c$i^kxt+~Oe zckm_$d+ry#U1@!WV3KMW%PU6i_xFDrcqj7rH1$H`7X%u<@jTf~GPm5S=!2oOwdk=$5A<<`14qKr}R8RZC+e%kkIPdHR(pG`cN6+78WMTZQ) zy&QT&a3O=xv*SV?j+&GJagNHV3!9$k`<0Cn(4p#JUjNEoy>uBuDk8v6CY_T%gGKOl z%%+JTT>|7jAHbxo{P;055b1or6M|Ni)8qnhAhPN4aMZSm(CHTCqYj#>uDmP*u7id# zvE$aXM524i5V@J&ZQs;52H)#oN!R$ciy#bvjDz zJ1BpRPGZlM1HXS6|i=whOqZ5A3g9F zWYHDru096Au4#XFO^xtA1bi^Fuw-=i>ew`5Z@@j>U*W1b3kTNHAv9~(zEY%`!)B4z zC{M7iW7p1ORIVD=ZsZ}ih!X2nS_a~igQIp!`B6=wj-qeb(7tL5SEsPPLqi`4r^mgX z+TF1mU?Hv8uQL%B7)nVQ1`n0Sl0!A+n8^BP()Ro&eTZo=Qd6%jf0q#5#Hi-Z*C#8q z9#b|!dim$VN_XB#H5F2+qK0@lv&E># zSLz|>C#L>*+1f~ULYB_?&JNgY7G!z0vR%>o;^**>{Tf`Vjbp*V=LJ?OCfmZghG_RT zS~>WtFZUyXQmUk!LI1&a@yc_~V7NzRN)G_pY~Et^n~w(ZL=%?d($&BP;;)yydyK6j zg85FPZq1esN!Yc>6U(;D3A*n0=Umv0-_3ErT98N{#9U@~ee#?r=yKQCQk3)G?ZSHi zH{Q0H(nzf{y~@vQSCK$s;RzU~2h-kkyJ0N5WIY{QT3T`;GIx4h&1lZo4@Q_z_?j|Q zg*vNYLMst)&G>hGpObk-&#cl~hYXQ%Khn0m+@W=cPLbV^M~VB#;Oo+`sy}wvR9{x5 zSw;kEfelfk5^fVi2mNg93|q(18ZU;K_|{V0MRgP%+;ZKq*q?f>h`yh+w5kuKRsB&f zzoWOh$E)nz)ami6XY12IS|d>ph-ru&S6dEn@43e!?P?J8xIpb}3;b;F@u|4K;@|1V zf6&i1la-wpiIz8l#n&pJlA0RJHMLIW>hDnjdboi7M^~H+hUvK7lV1r_(*yf5E@sD= zJJamimAKMk*D;#)e66t@a6XWr^h6}Hf2^GQpEP=QULWO{nqXjXCg=paz+NCLF(Pt5Bau8V+-L;9@$5cU(H zM6=8%LNCl(eu27O#bM0ZxSL^KXthS)A-`|ca&#dG1;DY$q_a~VyD1AB?vn3;=^3jhcQUF|GAh;Vp-t$Pm|sUZOSybaJc)c=H9=Y z>XGBH+^DtA)VJRAmSeB{yi}6IP8HdI%XZ?<{=s6tw5Qc4?>!Sjx|e#Zp!%BfI!nmu z=>ZBS=CkMUO`e+{t9L1(uj-Mm)-k_Sks?vR_m0|t@b^j_iiBanJfS-I0@iKShWZ2J z!RPbcl&&Wq=tia^k4P0s;z)W-P-`w9CQ8+q9z7n+X@Mslg{5`88>LWQZ+OPh@#4kXaQ|la+9DkLA-#+N^j&4(3u6rzJ;)595hy}YL5GMNoG%7-WD$sMVmr+n z)WpzNlK?mU@nQo3jw@Jh-Oz);2qc3-_>s_jcWU69=;(La>OB0}PxT2V7j_AS*Th+` zU5kKr^|rAMt|WNNXh3i!P*%Tjtf=TVm-rD>FSY(mT02kBP^g2zXli!06{4$50;tWy zYzowxlHfyXDBXU9RXIC58||qscBRa!B|X6>Fd(O(h$}@A+$lFWp2^8jfP2=;$u3@u zU6qiMs@_%raU!AM978dw1@{#i8uW#ChT!2fYMC&R;h)MHx8f1o8PIS4wS&G&Gs%7D zW9c34M)Yka&G^27<{Yf7x1`Ds>O8i(WhZR-7oskH)Xx`99^Xbs@-N+YgJ6|7bHAUX z>5@$Zl%L@WWXMkALgb3Q1X0~{?jO%L+xc3HJ4MYCrOmBPV7Lg5y!U%c*Ap2imxg&h zlEh!Nyf@V7-W0Jq8nTODP)v0Hl`^sL6B(1){2k9?gyHdO+x=(T7NO#cSWC23_hT~Z zO-3&Z|BWPzV`6>MmOWh$_xuXc96IJLP{6;TYf`{75M7%DST#vi51N&$5S$a+_nZ<0Hmzx?Yu-8BPT|cqjmG=S!LC!UWbcDqsg1pwYJq@g`3^e2oKo1y zCO5)FzfouTW_H3|o(Q*oZO2%Nv9&JZb;B6#t5jQk?3F{Ye8Qg^bzl;j9j@v$!}+Oc zRrv+~mk0EoXXV41_9{;)L|Qc4gf#qJ_-c!XA6NYx-skSq$&fDf6=VnKtX8jz78BCK zZ{(P4bywIe?=JiZQj47d=X%T?)KO=)?y)2oIJDPJ1m9>=XyRG4Y-1yD95c_WY{uW> z&5$ah%9K`ls^?SNU9;RK@mtGEgCML3xpowfe1Iw@J6+~qXVZRDIIDVV9p<(1K>F{MvxzrAu>1;LjZs`fX}gK8 zE3Z@(Mf&iIP&AMuOiXaHunAh}-13-n&q}Lv&*8viZR{aN!{3*0uSjqiMXT$+9W{82 zgj%-IB}F7{9}wVE3&s{6p>?yCeo&u&#%JlLb>S76R5wmw2J5N&c6k$gNtlbp{{H@@ zbbVdv0pa}+;P=<%19cDk2D_NF<(k^jUQ-bIJON=?-$dzFV{}f2w$FALgq_&z_t&5F z{W|d$Z>W(i?INt`CS101gZB3g!oAyxw;@oq?N_z~nU#f~R36oLw0+GvT}46Q%M#6T zhS>2v?gCo6S3b4q2Gei6{N2A@RQ_KrKsMu2d_2u&H<%RP*_*EIt+&wUtKC;=)Dh{0 z>6zsqkJljG+kk+yden;RYtC!}XhOGOdY*{wtAHU>eq-STwEMZcbtWuJV9VpqevRBS zR<1V6z9;3YXI)|~wE27JP(i?%Mji8!vyT}f3Nab;HF*<_kRb%;Zlf4jargu)r{&*X z+Zm}*wbm7IjmF}$FxeL5y>f?m!%3r-5RajY#yd~NgIu%YRPZeCtoqE~O+Mlp27!gy z+CyI-s-<2pT_=-_VRSnA5@r^yNR!m*gZgrC*M5g;MRju>3LGzINRhc zO>ouR3;kw}HA^N5DoUj+pi>{ON@cxTS@*`Fi-22xy0(5q*qY1Ap}@wG7;o1hVB|Q_ zLg$*(eSsU9#W)Poah~i4%Oly^j(zvf)(LV?WigI|Z}G=MUCz&J>ZLxhUcyeQ9n|hN zOscO%b_SZXSd%{=T(CDNlqRELQR`<#^jJ zi|0srUQKkGz%_-OArT^i+Qr?H;?lZaL1g_)j=Bm|r}AFoT5Q}|O+JJuB#d$$<#;=l z1{;~|>8xt|uD&GaIl?OhfaJkAd(s{>#PH;B! z)x^lOr*{PE$AyiGG+@6kWMx^j-@dFqIKlnt>^9an%0M>#7u3EJKPAsGG}< zqzmpG9D8v6%A&R29_>n54GpMSB;4*@a~Mq-f3o~8`{^2tbiMfVS)<@kR3E*kmSlQu zAlvi*VeLDk;au0SH$jjni55W+5qSId2=w6rw*B7W- z>knsMw;R=Ijxys*2#`VqC>l7-yOMWZU0r(|YL6PCSR{~_TPD`Aw@Z{}i-2?GyW+e| zLl*Zs%2CN(-~n(CG@Z6CJ*Dc?W+kEJLSKtk24fus7x7f~Ixj>5#=f~`fA0MEv5MoNrkt{27{S*D@LSA2_5ZFFiy*T= zz1Z^u?NGOVJB-;yDZUbH8~=&XhH=!ZYo+-&zgr(GYrZUJGbc2)v>(n9O-(_L17ag& zI=HMt*Cc@|yxg$FW8K~kFObsrv$w7g21KM^V2k2p^;%FK*(MR<6OpHo~liAnWdNb2r+^gSu zJkZw&nw6g7+W7&!Qr+Mm6Q}yK* z-{Vp#elZMO*ax+id9%wiDm{nS^+wAfIy%YhOWD!lu$j+>G#CWfa?{iArUeTBBRpSw zAY&W!zJ_>-!rV5XmMkN^>Hii28`y$pg4Byi044}Up#8b1s^q&Bi#Rp{=*rCw79lme zE44b6UzumsWx{`vE>&5+-1K}?bA*kL6tQ~RzIxUs%a%@91!rD68vN$v&@}O9#%nCmED2x)UsdqLZeWa2iu57j`Z%BMU1?$qH7RhUOu?#%eb<(L?Oy* z9d-=5h-CJmE$cgDP=E=fG?`Jnw{fZ{D+7*g??%x=7hu5fYq`SfA{0*@2DMSz8cZnk zLhExmSJJVh`Ai<1Y$H~%xxG39B`iGrl zWkz^k{D&3wy-a%r(wmM`WD1^xm>T=_Lzm2hhV!acw1mfuyi3fDTbbXk794CoyK-++ z;oSr8`&n{pJgftMkWr38w3v|%yeuwzyhyhW zD=er3*JH<*kBsn$SA8Fu z9BSBamlXA_#Dh>7KC~u?qm0Q7LWnCimrzOV#a?E7_$1wTM|m9d6=-7Uv_m!H@JOFe zE?6@!thWoc&GLIryC*TyH-MxKTWR4PE3C}V4E+FHkPdyU98^m^0fc9H(Vn8&cJ({( zQBa-H(%1jY?EU-0H=w8=;GYs|-VtKu9Akh zS#piy*-fJN=hQNrH8d?Vx%N|@u6^ekCtZeP#3S<`g9QB1NcyQ#6CSHZMx4($n=7#k z<#pcmlJJIL^W~?`F8?|tg`{xPMiWY@Jq>}&`JhUaw0WZ?7fulc;zJh@5%zdrkB+j$ zha-X~6wWqLKya(G57-t^VD7EE%M1$2hmJFnyROrjk1Qz!Gh8~kUBGGwod5}wrWq$u zoo4t^rAK>|B@rQkvnca^Zkr1NUw>fOpukNdW%`qCw8kAyu5_=g@sR^i7z;FKmtC9n z-HrP0P-24k4cf(glZTq6x)OJ7^Jak^OeaLw_!UVt+5?jYH2g;1O9@$luP6vagsU8; zZ9?trg;RV%_c2(t?szVvS`Tg#oAb0MKRpYyasz+dfZ)wCbZ7sxKe~;Ag5n&OMuMXf zo%>^;xG#z#2vO!WsSj9#f8(tLwvd6Kl`15`{zjH15GFSC@>g@x;#Eh2MVU8!EMf-C z#-5QlwGp%~LdKA=vH10Xzqdu(JAhdp+Bq$f6r7k<4UCMBW>sGIYbT|y!;WdSSSHOy zuV0R8??VY0%3}hK?vX* z4xjSf?RjY3&nCdkL>a5#b3tgUfPI;_xaKD&m-$ASi6hWIWSQHqXBAGz3SnNKNt9pt zW8}vsfL-o_O6DT~cp5)EEj~NLFJAy5R7e7C`Rdh(&jt|o3Y~(q#|Lz!MfmD;WHtue z{D`Ca&>(l?^-e>PF(~NMy8xv0D%23&km62tAIO}B4gge>RNgDzb>g8Cgs{nJGF}fV zoOh+T6c!emJ)#y|ZBTM{vI=FoQAUonVVD7T*+lSt9f;^DG8f$YN~S+-eqXsE@T_Ql z2XEH{0In5<%>*;Y;Sx!jzsIU5eG4iP6{8FCY_XpSU)`JLT^icx>Tc%R8WwAgxoTB1 zLpJrClXgpG1XImfSW>zVP)uOXo*_UEs$2-9Yqp8@mD@=+U9U{HBq3C@0+SNpkT zsP6^3rMA(ft8kd0G!tvv%BPF(Eqrd$+ZM~N9M95N#SrxPy+8aj?{Pw8CH!o*G{Qn@ zgytrO`<>P~VkBRvjoVjGcT?wuGKtuYvEf}#7>`E2IC(vx!K48hP$~#vc-8ng!2ZAH z=PSWruZET^*6GrWMDzoJ^IyD|eZ z!_MB2dP>a5W$YQiwI2eH%{5TKFq#xGmfGig_{sE&F1|NN`(k(%0}-LvL-pgH^-blf zG64onmQcdSk0A7a#MjqgVxnF6Q4i$e=M*F7po>bxqsvor0hWR_h3wYuRFk$v06DWR z%E$@z1x?3sn*i0{3K9giq|}(oHYp8aLPqY=#mug+HXD2IC81fAX?}A}3n-cU&nHJ_ zeyx1y6tQAtajq7!4SknkS<^A_4 z==K5%^q@IF@hQKz3uceqhu1qtsMmq*SGU7={3q-gw%w&+0$lh8qH8A*)md4OU0htc z%@#7lZrG5hgQ<{aVR0MCn%rh5b;Dq`)J;XPiyGfM+%mX+2A6ojg7E_xf9dC?d+)6a zXawKt3tTRmwyUf|Ju6|e&Pg!tCz@38ZU#!=uF@)5Z0!8teC@gWM7c+$ zcGy1|f%5}=YiQ}V*hNC_JEey)7nRL+CureQN4M%oHn_*-s45gP{WG-vw~C2G;4!u7 zsT?s^tYVJR#@z@gEf_moDA!F#t_Jzve?&EQO21oKlv;-yfH&x18_BTtSyFEk8Z`I>gaCM0TCtBj7aT?NgQP zWK*e*`Pd+>K3Zq-6r2&<5{kI0*Qn-Os2dqDU2;=lE9rx1Ah~Yfp7)w!*|=TA zNVJT>Jwo)JWK*Ndcdw+AyG>E(Z2#NP)xa#4=%V{UhQbk!>jq?SL*Aw$KJ;U|5ZAAd zDVofbJgz<3O84xJ4a!u^d)zVnXOZ*2o#WWs_#^!LrWgrf!RxdtZ1Y+T?RhP2Xk`U;uJUI(^)$*T?T4X3WlioU(|fQ%rIwGUaXJB|?_O!@h4u9Qaaf*mdBr56zBJ&uPbJuf4If@%j6Y5!q21 zj-z8mpXU;SBs`QxjyIL^>)}mlu8L9bIu5~Mc!yhI?WU}uAdWB&4pYL$o?i7mT6WBv z4nOURD%}BF$$5C~&j=;W$!n$^3f!RGB@(9_E^96u6z%`x{rmI3#ZrS=Dw(+@4H0!d z@0`A%Zq>B!?fZOmO2HbA`vXxM1(40Y#2?G5`+`Pwo9yEknTIuVNHQ*6Gj=?+)m1T@ zt~E&WH4Y|G!8XWQ)aptyT(X8;AzH_jBH^mSwk` zC4I4f!j61{XJs0orSnMw-(PVGI4ibmT%39@=J<@t=B>}JLi82r^Q2GwDVSE@S^lei zqozon=0sDrLmKrc!`JK_bBub8I=+p?33H+QFE+>YT|!yzRteiLR9V3pC=-&H2xd21 zs`sp}G|C{m@|Jv-mgp9W%tuZ`S8{shf93suee`>uZcT1B+_&guDi5yx#<{7-F+5qO z)ty)|__%4IM{|GPh%6yVCn-V1qkc?+?$+1txoDQFcV{@URqL%YVh^Bg7&DU+d}L{0 zZA{RBK)MrEe}!~*cD6E*YP0eMWXunuH$(=v^Ha(G`>6hnWB%g>h#`C)>>q<=b4hDt zGVI+jQunl?nNnOWtyGE!3PuMqE$ooxC?5X|FX>}#-^RNSc-<(kS$hn;dU9{hu=Yv) zyT(KfdG9@{SN+{_gAd7wf-BXgJUel5BbACI+)mnm<5~WDJ{9;9TshTHap=uAZ@d}} z*+5zG_DKby{&<|ovS@a^U7dA?(xG?Yeq$u`Op=T-4RT3MLSU&c{{4jmU49@?e8*X1 z0ex6ijY+?ilcP{7qFvaa7SEr^A5b&?O-Kun?EmMhCF_HKvKZ)ZTGFqnZ#I)!HGP-H zJ{C%F6EkJbOg*<~9cYlVs}d1kAo0Y+n!U;iALV0@Q4DHRY`5@T$A%x&FK2|*H~P_+ zY|lG|_bfe}Sh0+#Nqy1PK@Ux-TCg5%I%j%4Y^O@F` z%ei&+K3q=N&z5l!MOP!@af_BW%tng2j=3|e#}K;UdkNNT+z`{hC4 zk0ZKohgr=8k(_g#aVO6gTacNrGH2!zdlX9DQi!S?Ce@pnbgyhJIrtfMlrQCz7F2@&Q|(Cy4hwvHo;_$ z0=MM#Tf}->ow>-*k*c4hfu6+c;TvzQWpaIv(^E z`7)vB>C-Mw>dVM`zevAOdAWZVC7(3R(MJ8}1O2_aO-TS`wbve*%ohW-uZ}rA#%c42 zWLDO`SF6Tnu+{X;$cNb~n3#q0R|RjUHr^tU$vvs3BudyXO)q%uhLT^!9od=fKXne+ ztsJ=GZK#R1?p?+@NGK;>x$3!L^VZk){TqN(N#Uf(5)uWizJJV^v@RVL)vxaGR1PBx zP&yQF`Qsl%H3D0})2v)ZXwVTdvX`#mpem&=Z52{0#lk8)`QxU*g|Vz2h2GVMuy2H9 zJT)8XT)zAI`}c7LC|BmrG<16UBVQRTS9eiw9Ew9q>2vNvdEyrd7?F@L0yWy7aPu}4glSk&RMG%n$#`(_FmZR(YOkmA^y zK#HTe-0MNgwj~p|jyqAF$~a+&yH4nB5lsVK;jeu5c!P&S`>5a`%FNLylvf4AA`zJ_ zr&w0)cn>SQ-fdRK_luOl>XlHRz4;~`$zdJ=c(*Ovserdi5*#871XL5 zv#Fl34hzj>LHN8YRJ>Qt>i}0!3T~LJ#$$O4d{?Jx*mYD?uxHIJmA}5A_61wu=F4t3 z5+DxFP|2tp4{TSAb2cxz(WtL@zA66)h7le3czxF!3nteEzAN6`-a7`ImkB5;m!lc) z38$nmZuHR06+u3RG>gB(}FEpYY>RHAZIP>JPz;9ck7YLbiw#>-La6^!fZUvk~ zD1^;n!d+>Eni>}voC5?!VC#Y{DI<<=uArucV24HNzj5yZPD`%pl`Ht#>O^=kQ57hZ z>u$^|MrM0vpb8o?{O{L3xfi}X@G*4}>y3x=)7K=A`0EV zYUp4mzM~+i$U#Gh9*x*c%E@{(o~wY>$acm2ICqb>U-K{XDSokhnaq9`x$06iCFzV^ zcO>#TepmG6zRqNob=r^Rs0GvSKTk-%r%=L$YXY0uy%*K)i;Gqlo>h5nk1iPRLEfRt zj^s=-)H`{TH-e_uD!RUQd5ajF-WmQ!AP&BI)x!L|!w~3pb?dzLS=Gr>yPLS?yWTjV zmogpKCBGueK{6=#bf&qB6sVo9eINT;=X|x=7VA*Yp z77=G)xRn)`k9F$H;5}Q~Z*Gq^LEN&fK3f>efoh{UeKgIc4Yv7C^-l_Qg=}r1)oGlL zzdsrDf!g?qugv)v++EZB+w7{Bk->}`*K6GY{=vc+=6&?bv;z;+-gT z)5do%LUNWz;C*xfXgaRm1^(^bI*|xDE@!t*^8{44I6j%2u3aR%ZVa_QJX{d8aoe!D z#zcVncrAf$!LaIyuPsu^37we#v^T>m*&De<$|#cp*m`kuxlzxhGI-+mj3!l{awJZ! z6sU9N#kkihDc=1=_P(fZxKVUSiMSXj;P=w z%~I!b*@l>JBpDUew)X)14kSXJ&TebTWgg7+jD!<>hohB1%7=DL^F3`KVeMrK$QqmU z%-=_tL2|SkgpFI!_H6)kfPwknfv`|yFTzCpe1ja#8cP08uAe60Iy!b5(S_WMO(&Gt zNVyByHC69xmKTSZq|>IJ)R9<)z<64Adv1~?V5n0(!h3q?6hW~sNQkKVDf~fa`IuCY zn;O7dCrn7?{BN3%ArQ+^JKb4-1;rQH!5$Zy8+~$WRvI#NU^%ar7O04lCABtwWPU?8 zuYosR`-JizRPbNF09=u_Uht^`TN+WPso#@MlX{M{Xxe@KZ`f(mZzSt1`@Xkt#y$&# zd0Hx)Erz~Kq#i0hb5&td-#Tn{88 zgVCpqT4kh|AEI|^1Wths_8IZxSGP1DG_OvikYrcDjZOk(TjJeLcUcCvTV&)@e-Ztog7+qv zc}H=!;DCG924Q;Z?yE_ehk3#F8~J4mtAl(S#ddFG>xp=*5d2LGeA@=n=4c&loycG4 zcQvN-kFs-q@!k9X`viEB<}(zpu;Eno(=!saq~GTocNgKGoyLBkggL)L)9I$(G_WCFwTc0;<^squc-W3IDwx`|(4b87jOd!yBs}&2is>`2{!e zcoUWSOHLaV<-eH)#AhZ;r3syZw~VCb_TZ(PnHom82;72yhUrsYo~()7KAPQId7E?j z%MbqvX!S>68#taj4gh)>B(Cc7(eaQx((UNzQ1;>5=4Z3r^t%PV77!F@4r)&V+!U^* z(FZbxi&!CH=lE2Q_$mnFt2OqGal01aukHHz?f(Y8|Cyj=EkFU{Edp-p8X&6rUCigBsOQYBnaK(tKgD0^FZyh){D+y@GYkoDG2POuRo=5zKoi>)GnZAb z-Hfg>mTy#}b39LmETr$6pb6sYA1l8uAPV{*K~>G_my2<0ZB{N$T>nQI${1uQ*eu&V zM=S|pW2)zu1D&%w>l0PV!9#+6Z-a*V=$&itda6-;tgWS@w;DZLSRLkt9SP1c-7$GJ zHNA2bK6Pb8;MtL??1dqlb-`|-ex{!L2oe4(vlFd}Ta?6vnh*Ag0a%c{!zYnfU;nt? zKcMBv<%lv-TD?0)H0B=_1^-Vd;Gur_I8Y`U!+DilIVDKa+2H=(bzU|@hxFg-N5=5P zcOBx_@AsfQKh^kPreXVnb78}`;LRCNM#*Yx~CzVDSdD0(JrGvAQ zyEy+**t-r=P!}}t4?<R;wB}F_pkm@d!5%#fFxAw&+ zJQ6U77@AY&m`rvpoQPw1s4wHqi5wBD^LY7Y#P+pZ9K%rDAVP$eRH=2C;mP~n$pZ1m z3e{Wdw{o5*H0~zEJgu`(kFntg84r&ED}9s&G%`uR`NJ=_;O65?0Hld7u(E3j5Pa0M z$((0b0B0ouP-~3n`3yM0cHh@7jgUf}4q){98}MloPEJm0M%zdVxcc+dS-k;63%}&2 zi2n@ir%eQF725GK4FvtAVFuB?ez~PubaA{zv=~YHb;2^=up&aH%Op#9c2+_x)W0z73(i7`;i(vKH0@w<~HY+vS9B`3p^VBo=@vC})@FpU(6l@=~o*61NZ+;0h84sQ2WXrL@?z4;0DwQ2D zPVq!T&UfM**r-=`2A?*~0D64~&=Bzg`tE1Dsha1^lwpUSf$6%zXV;^P7~eatuKcpO z2MWJXN6y4eCzVaGw09e?85cXol!Ng?(aIi!Hhz864zBTh`tr0c1QId3nFN-2wZ8PD zkBIr>91|JCTiB+^cI~RS{kWy~tF_yQE?N=rd$@2B>Nw$X%JEA3^vh_ZPV}ZBry_Qh zt?K%%_W`F&9iCz@J3lR-`cBTX2eXAG4Rg z+o4gHWLIQ%e(S9WcBu(SXxMD|?4w5TnqZ|zj{>J$y7(;D@qBy6UVlv4ua|)IUMSrM zG`%*V0_x&xU)>NhZ!-$TGL;Z26s>^UdrX0RonW?!=flYU29oi+cu@sd#ARG>Qern# zum7+8gn(+bI?DkF$%^}8*N+aB4_-YPyAPDQZpUIay%NkN5^2EFpAjRq^ik`@4sHam zI2M_9(=s4wyLl=#ILwmfz5Ll_(!N?Bo8&Ht?JM>|c)}-3e*iYzExa;0xbY8?@>!xp zyTXsk<@8;NPcu>xD{7Wro=HK>9j@cASB#f~MJoK^4!dj=)d#dLLxBeAjXj&l8i_kDChaeMTN@)gifko%OR| z+!YkX@on)bCUT;$FFIXswykKhI8+<*Os9u0S-EfW`ehvREJ*&lcB8fp1YD%+lq2_N zf#mTznvbv21jrCWBP$-4;v#vA|3z4v4?|!l3L%Nt8-{-{0WYfuNS_+9%yzIo=pD&5 zuxMe{i~t-9n<${bx-X#{!z}jDLxWzA`~P_{w!>wtNLm>Bv*QPi3*$; zx{|o~Du=I)ipe~72V3(yfIV&?JeDG;EnIwSYr?kV^ZAT3U^uc5z4;MlTixYc!WZXy zas966jTv<#Z1Sm>p&D{!EoLb=9@i6YR>HF;15NiodcB*3y#GIZ)U45VTALkM`^oSg z`jqo_Sjel^rLw8A#nyp>JN&jHmHAR7X}?+g1ILWS$Yv$#%|{r9S{mn9zOHAqN3qz~ zCC_v)C+wzwlM+}9(cwe?W_ZiO@pleb zIZV2M%|D$$@neC=$w8{*m~8^f1lC=(8LveOz|FHNWD(K^E5V-tAJfE>S;8JNc^i$V z&5=5kST#UIOAk2n269u+7x^!va$eA3RI4+xm+PCiYdi2Fg=laM4a|2E#T_TBdQON* zzz)wc;Iz$Wa*NA;S}7n#Q0J4!^o(ntR8RC|9VRZ-^lFlzps+*Y54p>!J9Xm6{rHk00$XhMZLB=$7&$A=S6fCk7EH)8sCeQC)qbnPdm(L+1B{a00 zv^(rsxgFg6a7k_a^Y}E*`$qc7`Ftw0*LzyHQPOwA^T{By81-Li$AOokiybP9ofDR*tY*q;%(tB?F|W7T^oP9r%LlyJ-15o;RJX(${ZN3% zo=*L!e(rsDaYNT08?71<$V`;XnVI{h>eH1(n&i5ioK_u(cCC2Z?U=s%>YFWc;>EYK zY5sh$a~f+F4CKw?h_0y+gg|?XYw~=XOo&*SOO^aoP3Qh9Rlq^8*0jjIdwY)S%gdH4 ztZ3f$Yepf?V2O(d*rzuM*ykXF@}^|KNa3bj?GgLPa-hS0+4JBg1Y=zGBII;gq_GT$PdWi{ucC4pB6Z@?r+Y{KIp&E>|JT%<>!as4nH`aJ)Z$`(wh8Z@{N9<>44+OS@%>8ZZFyG##0XnmJ@DKz7iEafuGwp#sr1vm>VACHbG3oq^B@Mj+ zBz5JgBGZDHiAoY<4w;j!U?E}Q;_tr%Wfw278$d9A3lUtH5C#*c`U^U>^helrTj0gt zDBx&rZr=VK?^nMv;{#~g_Q1zp$H2g5(ylye?aaw=T|Xi7RZeBg;Nxi-@ixHlevMbH zAPyGZd4O-kNDPvuAW;Sca_gHQuR9ZBYGp2wlOrW1kYL+KWn!jPOuG5}&jA$NY=^g` z{so|%?-^J$6GOoZcD3^JN$*e{t>ZzUr27#JHKtQDih;^Y`KWxb)b1NFpg>R8Rj-4Z z(gU!z!|G?IrUJ@R<7H=`6wmA7zM##9^PdZY#p*+0WP4MGz+ELZ$F5>PA-W*_bCFC# zt}`HO+rfJS{ZKa$hs!BxuK6%Tpy%f^lhJo47x<94exMC;Op)~dwtLcln&Z9PlVWw0 zKO{72w20yD-}ChWf~K^UsC0E72H4WdN{ROwI=QQ(Ltg+a18Z=GeM2)Ac&fzQkV@rA zteu7Kqn7qukD1BbquP!7rjwJCD~*PAM-j`H1lN^uy&$@*OqCp+qoqfn3&26Zb7NL- z0wjA3uqSX&C$6R6myqKV5%KIPf7VBWxMq1de_6 zb0Ejbck@ObF=PmL`p})?Jh6#X{x2CY_xsx%2SiXlzZxG5k)QN^lEB^gmRk~S>eD5G zZSLUs85(9)oL^ORBl$&xEAHFWm!~a?4G*VmRM!*WF|NJ?(8`ijltrW>m4V5UoeAom&rez3d_3JeGsh^(GOs3Lzn`V({@<%)eD ziBLVmRS??f8acNb(9wh6R{10(E>|rLMYKOmTUEC=sA3N`pm@v$UKG!~8ghjWSqEOl zI=sC18<6dp7v*qWg_FICeQ#Fs*n<3fKF@EUudhN1o&&XLsz&b5EOLLI`mcYWC`6{_ z-oB)j9SmS7;>HwwndQpxy(-+!xN&Wsu!CHg`!_J`7>kt?%J}I?ZkCXgBmvc4RbV$F zj;Pr1>I(~U2IXO8dET_gWDl8J;kV_J!`-wueRwvvp?xJpke=&Fp{+_Fb5SKQL{U>x zo~Qo2rtYMqN*~7J@rmZ)@V+~+I1wYr&C0sTRgrr?4O=+(@n?CH>TzP=vM7*Mv0Mm| z&I95a@|FxBURJZ#c{HWeH1h)(2iK!ug*SCDkS`8kd$`9>Bk~Kp2U*l#e=5Od;MC=W zqfMEzET+fW2ll{A$vw!4yoIN|=u6_yaB`47A~!Pc0v{8O~kUxr8<56sZ*;+mzpXCt#WOsb35FZQ|zF`^0*bC zUjWfdHQxH@v+0T+fr`|un=wlAVzo8y!x740R4B)Sw_it7bAd>W&JFTLpFlv0H7F(J z_#TSPZiM}Vt!TQ`s<_4$aKBpXeHtyce2t5<=uIJEh>tX|wVKENk~woOND(QeRh+isjQ(Al16pH2PSHA^Y_M0*_S*bf%lh6;Y>OY?X0?8E1Q3D~y?D z7U*1CgH3tFJ@*(?@P(uBH%C0r;ETk%V^LUUGS8@0<~yq;7NLb>g{ODCObiXtaK%h@ zsQ}tuWVy@uw^wRT($vDy9#XK{()15UKf->_9Nh_&_R7TN2IWrFZA8i1o^Hf7fegDj zB`}{%IDfPnn9dQH2*)|q&U)5T(lWD3`Bd!s5AQ0Q>xGQby%}|FQLWS>e&Ig5=9fzq zw`!ahTTNIG+@Bg;r{-pfebm={4nc;!5hs=IzBcnAFS>}WY{r#*7?phy#8mGJ74G8M z7;-GiXY}HQJq+ZULA1{X?`p9pY{nI}vg|pII#NJ;A49Qwj|?Z-nfBk;E<3RpT!fCO ze@a$O`oCO&UmHH?x+`+kYm`{Z?TcP3Cz|V2o0jAHXOXQb9=zW|Z=Z*m(x1EtIO(gd z>U(p;W3yqP@6`|aJ`TTOxlPmSf2Fn}I}rTIn6DD?H;lY0nJ*heIH=Z(2wgoIJ zM4x1zXsDWtLhj%-3u%oyW{Qvah>so)6m%hdo5c2K+TN331k5DAvyPmJ z$M>3%wOrZCF0R*?4(tyLze_0 z9$nmq?|YJSa4Qu)O@Y~chMSdaP(k>SsYSsG$yyLey6aO`8GcB(B2?p!Cc(1CNLrNfU+5h zZIkBhjYlib1MQ46GI{a~ALbFxkRQTatiEuY0$e!NlJOP2@>F|T?40J47&I4%_J*Qv0c^@m-f#Zc_Ohm5>D z?9gS+)KFQ5ipI}+l24;NV?QC}Rguj|^KHDGf87v|7KV3OdX z3?>N^M2RxKX{RT1rgFWe21XUVGILKSAICo^YaG<=aOS$bs8k$t#j$cG5=9Q(T^z-H zuD(&tEg)?qFuNRFU6EFO!<_4eZ}R?TWgyy}$P?2Jfi4yyc{c;peSY3n^LA(1JoS+N z(YE2PJvBoyt6q8ZnPyR3w~TV9C(UACOFGs-^z`xy(mgLtWfxnNr!=Tji%@__B~yu4Ce0LvtlY4Tg3 zK)y}kNH;6_=|P^#En85U{8NwIq>9qBfIRJSPE%&roV(=FYPQ;lXvlJwP>31C9kRlU zOjIh>%`5|^ZlZPxb0C!xGQ+Y&U4FOgRWH^yMwfgUW$~OUvwZer0W|$4Tt@V(U)j=o z^LSiHl3~g`TiN74$WKFU$iLnxg*kVSV|i9{Wvy#at>ghbZiUC({j@K&h|P&R<%EcL zfI-*>d_;A`ggD$i9r9UwwBm8|q(hy3%PuuPKJsh8ZNIw@CDZAhV9NU*>s-`n)q{{i zzC83QspX=rr5L4s`Y}ZMD)kiY-FU;Anet#ntvJxn_0gdKIId& zJyTt4nvKwcWSX$PaNadUvu5;G*X7=;a8zfW4}D;l0q};|qT;eCI3|C0R77Yp!m!BE zb3^K->a2}ve8S9*S_|xlfUfqW4wdSI+3!$&2M=U&8NO;Y!Cp)W5V!xBu(A@GdY*-A ztn5pIOnjYnU~u1@)>up~{(eagp!ZXh1hZ zee8*34BbVDzw+eGc%iaJa_5M~7mhrJ1NmBa^XfgHX*JAKC7C#n-l|$%3Ayqs&8z*j z$02v7-1xxlXRqkSt!~ZMza;t733_I13cZ&jyh1HouAMGoB4Ma}TkcsjDcVzcl_%`^#i4&+FHzz+I?Tu( zEEYD`OU4B^4m<-6gYG}S1EWi9G6coC#rr86l?v(!a~Ns~n*n5(kf74~IDCJTZZ%cZdBFfprB@HUCu~Cmf@6woV#qmp z-q2eE&!ex?z$Q%i_2Adiu72;~{nT!=lHv!(a!m(Z6(s&al_u?$)AXehy_O^GpJZ z&N{XIep!`QttLI+K`kxz$eDQP0-xrb+u6yXnwJl7z`0V$!W)~9Rt(&c)mr7BI9>?cDqA@eFyi#IE)089M%1`xOtJW4D7QCF}(v2RY`M}(JI zl7BA(yT&9QZ2;-6OV4a!-1o=t$h$&q$rI3NEZ!@2*b1@KhL2P|OFVQjw|JyLyb`FR$2 zUe=kIn`j0j8i$WCMWmV2o(#nm7t)UhVZ9U0E+^-8?iIo|!*^Cbmv`Jwd4leo{c~XR zUsJ^Y{_%|pU_SF_t^(Wj4hB7^@d2IvN2}!W<7WXH!#VYL594#U!*4CBtBqsG_jtyb zRG<^4nYfb_|4pCvUc+@8_4D;tqo)^G&{l}o6uZ*V-bS~n+SkiJ(``6;JySX9G7hLX zHm&d4|1^8Vi<|M9u`fQpD30{ok2|FIrQ5CXA{nmVg9{z=Vk<|hU|E9&D24($s$Jcp z@7p8TT6yHZ{M!1}hykDvv3{H>5pp{? z@rA`K;vF{`?$rV(mzrGJk|3=nNKuUF0%@MjD}R6gF=vB9o^}*-%LMHgt$ejWc?cz> zU5$Nqjviz%HvO8XKgnZx1>ro%%;a=F@_k}mFFRnVxUFk77OKtp+EWA>+Zr~0+3D$2d?@DT?$ki<_w#n);$6E%NE2!GT{|a zBVxdG-u8@lD}asbGaa<x)DVl$l=o6qy$<+2M31odi_jB1_UO#hH8@sEMspNE9BD@J0AF>5>`SCe!sTJ$e*oKQc&AG-j^!G{# zR|Sl18jALy8hi)#C$6sPoww#Kr=%xSJKscK7D1wm%8tHh$maWdp9+7AVx=^d%H>10 zKKK)pQL-$puC1N$n0U>_^{E;lh7vX8>G|ZNco)grf7HEbCC@Xhk$pY~U-bf-cy}Hz z5?E&|6P(O5cM-HpUSFu=VOW^U3Lb^&ou_lqVI{_92@!iVLtaw6AMw`QSL0N_e1!3o%FD|?uP!i~0mh6D;-Nf!(ZCrB;0EPwnsg>~ zK9}e)Y0`eRbPNn(sT|s+IQ4*|c$zz!pY9rI%|S+AKjZ}++l{sBcaWK+WO}A9!11>fw z!Kb0(EL?tT2DpB#6Ws>julpoOpQ961j~B_m@vxN~Yp4bqBh+b0DEG_4=Pu-Tdb7P9 z2i2Yw4q~rA@|8mfv9g!m2&{EEutU3Atf(3=I|o}FowUI0qDrS6P*E|=-*~bk6sCrSil%K!*gu^QU`zvYf3<{{#B5SGV#-a89q~@(g^>sX zw(pGqQo{lh=Si?9I}Pm)>`l-{0RusZZMX{1`ddsW-sSsn4CZ!TGX?WV+@@pVc!y(e z6{PXpv3*8$a&!EiUG2M!7ZaBm~R$Y zpwXxv^m*982Lqi)AMDR21}lo=JN|V|#4}_c1|tTwtQG05)7{k4np21D1{K;Nk;n<) zr?95Iq3y(Pq@|~1t{SpH_=roSDH(v3O$EcSFX|Qa>ZP7rb4|H6@5x;riJU20zn-L+(YPkYX)J}uR4x=p-!2Aa6gS0x1nWkppJJnKy+#F}7I zV=4h=<5MO#$@b%;cqU20dIh-0p!g|9f|KFJP{WH|U?{X(FSe5EnrhPqQVnk{Zkhz@ ze%i56SaLJqi&8=)XlnuU(Kl-p4@h7_d6`3yAi|o!(GGZpL`cc>3U2UArIj zhTY#gE^r_E#RJTk`acCBnto}xTS`stITJ&&DYf6~WB^FjYn09r+|*;?@g@_ zrKH-#4>=y%r-N~G3WG-HD-lRPI1{eYLYw6I31GHp0TWHyh(wU*c0Q;{e)Wvu+50qL zRHh6Cw!~>rwth_z|DDuXX@J1rNb8O&!Lc`reDpzk&)1=qK*XO!4ZG0?$Jg3`nB2i_gBV4@J0 zIb-4Dnd*ZosOsXcgS$$t){3Ly<*BeG2SY=EwJZ94lFK|$vIk2GdZA~(wS;YC0=gMdS8)T92kGAjODEjYy8_V?UPT@LRC&98p6ivP z;+p~xeyWk%YN@OBmsWaGR6B9Bf!g?6tmT4@S60^KxLO;^_}7+ZHUc@~k`3@$uORCd`%)q_ zs-u|N*kW2H(r5a;nxkUDndUApmk;sVml&bMFR#&uwBo1h@)su-G8n#_bp9OGsof}y zu4I+MP`RI98~XHY9sU#n8)I@z5poQjJ!TsTXSo`j zI{`%LRUip9G^wEpB=qDvQSZ#1nR~zScO9R{f2_&-p0mr^Yp?ZHe(}r*IdqC)_=F3YX-9a*dO&`LHmYZSl2!kks>vj!&jV^cnB5K7-5cKPmRfj6mzXd_8#AnemW6A_*OJ{?4|r6ryk{zp5_#`>f4$8bbTqT^jGWqvzDYeG zj&*~@{r3hOv}C`J53FHNQD9i?EJJ?{h-30yxpJg}HK-PPTS^z?fk)B|V9dPsrYRCm zL`wjDLz$jkK&-ND#%nO;td>!YSB5`ZzAd(4bcI_0%xZAb7lr!I0NA&oHJUFu;ygvt z60h;}^@j8}pkhZ?xGft58wiH+;=DU3vSt*aBD_94veSjq0TcwbX4BC)l#d7FUG;#r z6_YP!$GulsK7=I}C~s!m33J9N0uGr|U)#fdKJoaBqd>{*^&mtoe*?7a&bS~!3@YPq zDA?yRdjMd8)1N)60XxmPBo>8aM+q?=1(jtUF8NcJ#vXAK6o?BT*XlHZ!w)d6u#K9q zjpWmFa!^mV_QOVlJ_@!DRqJ7-eYe0BPiW-gtJz{t6f$8Q%GI0gUi>e^Sd=g+EM9Yr z?B_j$lgG^Kk@7qiTK1Qf?j0l?@_OCN-BBQ!%};k~!Ow;yP`bW8($1vOBDQ%p8zs`D ziQP$>?ESrot2CIn_D*)@9y&Y58ds8CJCRA|bWyNQ8D`@nXg!@jgxFlk;D=vchY;^i zlM1D%H4Di>wCrk7U)37xB)plF--u%A8s>ujM&8c+r8WM|RyVTJQrwVP8uL=2{eyEr zbNJ;lvxm{V_g$`oIe5rcvu}|_1x_)`E%#$?Ar~1k9MMOkPp*!(t+mMFPbU1}BD)$> zBK>VQz-UdVKL_wtjm($!h(sz$ST6kKlM4#r`ZLSSa_VR~>g%P0)^L%4ytAn=GVk-U zfya3NBa^@g2QV>r+UKHR#tzELVM3|Ab2ME!E3s6U=}rJ^l^9y1+hBa|KARpQan1^g z#(%xwdhEixl6qyt{uaPBt8@u3ZhX+8oPP~Oe|&c}%=Hw)blEC9`SI4));Ydj7%Uyh zPOZnhOILjNW`S%ffRX!iPK-#5<VijyEYt{|>6!`1?9McKc-%Bf;@2CI(Rh(QRto#g&=L+uyzv46k0o~X4O`{y6 zaAFBObxS=LlHsQZezWqN`@5x2Ee@QR^Fmppd9&vWQp&t(c*@!5GYX9-Gdb&RYMb*$ifcUN!zc5Y}AwJF_e)GT7ip@)_;_ zToh1vkT_IN&sJ^tWeK!H5_ik=MoV^xBlYwn zrmZ*}ZI~yrdHTXt_%f$`5pt)Bp~hm6L+6JXi0lrNybF}{fB1ftvJY%cRvwo3s}_q`bidG7fII2@k*CWcDO+WskO zo)y0p2Ovu(F7=b%&2P@xbPl|~{VM5xI}CK>D#ebuRKs+gJS!fX;~@aZ9Z70!p~ze^ z-I1LCn6Ut4E|oz-!DD;@$r3tI~~po1T_HpMmmd zK!w2eU3ADZJ#Z%z*1jrmy2)@j0!>oMXfEa@y=u<_rVr2?dT1#$w{C+WyW)%+5Pnwa zHwaWp9;GPvZ)pdTBs(%(nAsM>+&SmU0jneku;KLFF*+9FT?eJl0Dp&B{TbK`*jUo* zXMm!hx=#xFJWO*$!f_bzsXU2q0PuZg=0Vq2V2ePAnE8?O(lU`e(uw<((?z7s2$6r2 zmIMXpT1m_vm{>_*<5HHrkvWA3m;^pWy}=QMKk@o#v1M1}-DQ@bn{E-a5 zqP>i+TB_t*(2UyRP{Hn~xNj#HgKs0boIqi6Y&pJ6;qxxFBExhSxccE6asHCCW%2%y z*^!LVn#B^XzjFZ$DcplKdrXmv$((HOn#LX>@qVq#dTqM(5@5ZHW8I~G-TcI6Ns%7E z54BZEYC|tu9G2&rju1FYc%D>#RL|V{Waj4uTk{<~xlex5|IRVMi6$E37v8CNjjWcCl zMmr<lpZcWVALdmjLe{GGvF~f3g=FyFVu6{F1<`p-iy&`7cGko{U{hSkfv^Q+WQ}^O=xww6il0v=}e>dg0oCQk^f-Ixc`+9lV`8gDR*FgGijgKMYZ z`CuP~rtXGFi=wKggLkG#57BmAR?2x0E_WkvO!LZ&-Z{P?uM>Y$XU0vn#Y%q41BO^< zK%cQ#+^eFp`8Xp?)S3Pa58EXL>Y@{eG@G@@rKvsph7TM@3t-kB0$PkV1t+I)>}RU^ zuE6pWNNtDrv%!R{YlIwaQ ztF0Mr@8&Rn3i$+;qKc3oq3h&bK+Gi8ms(fOxQ_5R=2-YT_6o@iyIkyXOW)Z7J`~O{ z4gBCTHZOIp{SfjBV&@(fk+OgLY!D9O-8rw5y>qimR!n&#tNs&=Bkw_W%587btS4wo zRqC`iB9Za3`2rx)E^F>-0viNL4BdecUDZO|=Q-ceIWaT=YqWfz8hXhP`vuTFmwd0u zE;~pcUdO${tPjnB-}JyG0xspwP0q&dBK_JCT_Yoj6q#>nGar$GfPgl$fdMq}871bf zN>ETYx{PvdtAtHg_6-@U`l7yY+8Wp&=Ed8Fmf@Utv7TM;rDnP?_~?F_{oeiip806s zv?nHf4mwQK+(}qS@|Yf68|4?uBa@|@IV+Pl6Blg(CTL*`?zlwfOdIcFE0}P-OVu0# zl%;^?8(p=Hgay^l2Zy#vXA5Q;?`isUrr<%-LxV#EXjLsmaW3TK#SYZNB>1|zN=&2` zlX+`)=odJ`Y&%BrhFD3x+M9%-=f7tU+yd;N64?~O0mgovu2{HlU}4X@@^thh!@hU1 z(UtRwEy;B58(OL-uU)>Rc!k|XM{ce9`V>@hi+6+ho{+d@!m;PCKD8Vo;&$rU-5W+? zE8{wB_fqy2CJ$y;&7YZl*ZLBbP_+F$q@A2cFw-^5a>YPiB9S>^t_e8|%18nI1od1J9PlX10ajcFhfZN0Aky6^a6M6q`V8Ucd#` zxffY0_b%H&etLN|V0z$D(Yu9nk;cpQ=7$$nUeOoch6uQpVElrY9X1SzaqyZzl^ptn z#^Ummk``WcxsS+)&DaPw8#-%``^Q;1A4TKtYc-v$nu&Dp-72Fzns3RjuSBr9;Z{oL zlWn+bOqCU5E;_#`4znX_S?C%-*_}vl7dMLcVIirdB7(oXZd;B$2>Jw^HYJ#GPx;7~ zCi~fb$cWb^&41Z`QV-?>vl{%_=HYb)9wX+!V#^7(bjKPdh-j?=wTaU7m71fPf_8(# zloBP0t{@T#HR#Pyh!BNdA$ksOkndn=Ub6{4J5Xrv$@fgNS0<*%nS@(3OV4Heoeews zi@?!(f3i~PQ&}ITxDp_Xdx`a%S1vFP1i zv{9L&@)&jsVX{`U4E=FS-uMERzh~Y0@gU#|b7Sk^%nMrDUCQc!*V^;|=-XMp-=IuK z`M(Dg&jSj!vSp1D=WeIuaz8k0cD>oEWx2Vz`E7onyiLY$^{Q^wh6wE?70Ub;$O$rP zJCNN-+?sj8e7@U++LV?^m#*j)Z?3R;FgTum>QrIS)N--n{h0XLshSuuPj0M>^d4r? zoupjrF#_qno?;EetC^>hU0vE;%vYb(vq$6)gRo{e5f))+AtvpQ!jl5S{}b1=**331 z`))o^GGkm^NR%9!&-RU*HR%!ckB0OQF`5t;-m9HiZJj7aM1=ktb7|D!34;_yA4Mg7 z5l|CCIl{mHys^c6fw6|(Eu_`h+WrssqH+PJ_Z+syaa>LkP@@)?1ISftoeRy2pnom= zacW;HygsxyURqfxaYoEOiBi&DMXmRQw|~O@7BKK*K{XU$!tbsL(NZ0H&KT$NAi#!r z2l@Jx6tX<6ZuRgm^mfOs@5B4{BaAmt>V?I_GKo6l^dU{c*b1>E6S6-@E@r_;2J2I{ zhWIoM)t0?mN;=6smULFk7TavyCEuRhpmHan*GxAr8tlN%Nz<0>H4+9P`kMaGs zIr7=hV3zcFlh)Mhkn5vBhw$NHUxH=~{P=M@KUzyYA(N z#89DB0d@Ma-#C5M#L{fE)(Jt>_v#@tdV=*D03&U~`23YZ2vDdxvem3n{~4M@?E=(oorna z_%97uqkaMHFX{ln$Am&I_mbim0zA+KiVanCOzG$-CRF>{FAtd1E(?k)DhkYah>K;T zx&Ucm-h^Dn<{3e_XMMgLSLoN&vqR^k0abUqrbmshPnCKTtlK&|N(j4Ty%;v|3zNJ4 zX}_@S7oh_nd|8p1mu^qD5WWa=sY_TeGwXdHSID*%rygRn zO(Dsfr8tZiQfEC`7IKg_fr>=iglVO1e2`Zuqm1I#oJ7(pvg)g&?89wrvlw^>37&0p zGH0jFTXz~2@`^&$4fWcpia)j!N4$zBQ4PW0_sk};jHdNELKqsVvu+(M78Xs|_^8mWe*&4*KdIz|Kdvy=72BY?meL`O2vND_^SxFPQ#FDQ+4!ep;}u@Cxw%9 zhB|QoJU$gxCub;Qs5kNPm{GV!bP_gS6&m{*G8l}`+n&xNu zHlz2@Wt$2#cxz%R} zrE1m2qt99Ocy$D$Q2?&=Bad|%!pD+64A)LRkGNq;S>tKn$Y&@6qI4ssb~gN7Zpgjg z4#)pQN>YL{wF`H-IzUUcsA4>I*kkCe-qjagBRV=dm^6y?&?s{ zr)zvCMY%FvN>kd)923xfTc*0$H?Wp9QFik!5@gPn;KHrX?1evWB8+JF5GScB{l43OaB|8-s2 z)%AJ>))3R5LwW=~RzBcnQVL^QTZ{89q)=->efEH1-V6D)Z3o*kkaaJq5Q`(fLzey* z#7|jnK#>^=Rh7|GI6FdBa|tpi)?~|$6)Y?&LR1%j7=bQ!unYIo-B~<^O<`hx7oKnR zDh~(3%aTUAVRH5r)j;N@35JcBjr;4x4Z<=}a(%dLj*^(c!8lARPck1IhA7~1ZovJj z3Y2sN@&DwF|MqFI4rbXj7r(Jev=^=aq|TgWuP=cG%%bZVyce_}o~o7o zOew(NqBTMH{Xriw|INc~__BxfYfgWnXd2iFoCeF*l5E^x%HE;7Qibm-oybX?G}P0% z7dkaP{Y6@aegCV78;yl@uX?T%62x70Y#&p9rZ2ss>feKX_!H{w_GfOU6h3x>ymL7I z8gHA;#%1Z{$=4J1IS7$u^@zuP3By;jer@p1%gknBb1i2NY5C3lk$le+3i+QM>19`|gb;w{J6Ex#b@(FMZG9h?DfZz<0932bS$7Vx;2b&^Cw4nIfLFjL|a|pF&|y zWA)Wd_)Fl)uxeQM&^Ly)(#?5&_CystU9))A&fl6O;rVS6pXQ&G$@O;lFB2J4kg3~| zThr~q6K^3Yiz+-3ugh@hNt3qNar zE9;VqCBMNg)Te0Z9vb)l8GXsZk~HI>z_a7cw*s?*))2%+F(D_bhuFUUe#>Yy?&BU6 zJDuo2qiD5DA)e&=Cuvod@|M6W136V4&AT%lmyXd#Nh}dge4EjEXks!vsV;skVDr$L zt?0#QbwO4tp5q_Mk6vy)?ow*eZ%O%(Rf?IPAJL-USYNP6loFR5XdCnV7isYC=i>hx z?%65Q+XrPTY>x&Kx$@`HAq&}OI+ZL37Bj6KmhL2Ke|qtdb}{H{r!J&D7~8N|uy=E*ec_`_ z?L~s2ZpRyyh*jBW@kw~v4(~iV3p_8n*LzLjerqL4`nr80;D^p_s9T>uLeiErsMf}N z`K1@z{@>2Q|0GgKWP^&W(+f8sLUoGo0iJ>2AFyrjlqbN~24t-J3X2LSW$wUeV!(8` zeexdKwf&iF;^97&Sj*{v-Smzbt5>wv`1#yYq|CIu!}dBhW+=m|*V@^EQ))7C)l?H;`qS z_pgemla+3|Mw zJaE8ebBfv6+V*-4&W{-ed@RFpE`?>=tzfeeuABbfkC^{i8447(hG;>VekV=(P_eMc zV0Dyx!S6KeQ%-XK2jvc7M-XIpD$xKG59TW6ULoUYj(20za;#W1d&PDo6&b2Epw zPg3pQHgJxJUUs`RJbCI*{CT=7KzZSgT+JQIks__uX|wY{=yJ>^e?B|)+8Wuy$5uq7L=$_AJh+qO_smUl^Bb|;4fA`X zu74h(-q3(oW+iKJAGFQ_`3txIc`}-f}z+Q{ME-7%5U-SOInl8A|VsVjY_9eV@qBUN)10?jHU>>W@hR= zm{IduGrPUR4yuFSZ~T_7(!cm3vQ3n>y`W2=N}vhlz=1hROg*5mmrea@TxKGD|N5BaDbMzq!=#FCNqr(BH-$2CJfj znX;dIwnUe|BqJ(H2MODXiZ45&67iJ*ypq%FOJ+ek>~XC;@rUalQY{;%X6f={Q_@0=t{}0fYzfn=dI^DqiSkz2g zg$NAQ*RoA|?*(mbX=-baRR-Lb+=f46;grM`ItvPUCaE3fYP%fBlG{)v!@+$S_7zn(>_|mB@$ltpAhQE=SP^J!L@+t)~7JHu;)93y~sK2{~QcSHLezJI> z8%G^8>|2BI{m!8^wDRqnwv|;Vyk>4zqkMT^OmDV>MAl=K1NC%$DhZa#uULxp(&w%7 zM80TSmFK6`&CkzY@y^BO?l^Sj<7%srBis3ozd7LgJAC|)rDtFt;FBmL#DK+w>|1Kx z(|)4M#Sb;fd+i?;I#;zC7Ih6-N{lKczBv>ndj;DkBIn{+1KmJ`C4cc1$|})|PZ-pN z!yR6D?OkfPt+|MB5<%{aTj(tJBHjPAwFESm(_kp)F3_FtTGJuvWMP9ugjdF{_5g^u zC45kToxN~pz2LncgHlspduW{mRn2m3(e}ouU727B>4AeCTRQFvIUF<)h{9I9^)jB_ zqkzB`0G{)ob??7gDc3`Rt>o%Qhsn;ieW-Wad`{j{DIfQGdd_h-y^KgUhFwvTKbuUY z3=t?A#2Sg(SlycuoJgrEMdCn>K5;sArlq91S_;eTjaLq2Ssd_znBf$sIRV=`IYmuktWW9yw$vw~aJ8n92O=Pb!?dDA3gOB??`jI#iqv zBHA785s%)_oo|-&zUb&{^au0ZUFhDDjJVd7CC?~%DC77G^|S_~^fDrv72(yedL!aZ zqls=6F9E~Na`!49zo^l(u7D&Ot;D&2h73(&fSEoa^OtN?X=!Q8h$G; z`O6n(-|TdBv=kKfynS8LY?znL%Qqw(+`Z9=UtN_zt&M#uNwRyhk}xTf3w*H zzH`Gfx4gWWmzNKbQ_&FpIoLy`^r>>G)Bn-!r02OZd%3o*wxVb_pFXEU6!+tV zY%C3Or}s~6j?!EXgW7C(CUxWjnBZ$PxR5yMMVDEZ5hYCvxmWONDW5(WlC57wNMQ=; z=p+{^Ow8iByQ?1+pp|_2*H~CgZikUt>(Qk9sE(_A_{sr&Y@u53I4=w~?CP;?OTyf5 zYi)5GNe%xuPuh>amP{#%W@N|<9D(%w-5LfaYbxwkU|f(pkPX4w0A{d|_vr0|9Hs-( zn6_Dw!t(G-4|cZsFvYg_ss`S5VJ)E`vd~=#uk`m zq^Z5w)@9N8^nLCJWDU;RdVaUo_-)m+Q$pD~a8uiYK=F|*XFTQNG5A(b zz?GOV$LzCKHuP0W@95%*nIPpXt+dRV&T3Vux47Hoai|8)<_%2W_Wm*%R@5ZEr=+^Q zu&~h9%{)^?YzIO$_TG)W7?0Umx6xn!@XOWv-#m3>X7J&nDIe~R@%K@cvX`L|I@qgI zr%*H1cBN(2=|hixJg{RcenQxJh&LodtapKdfbq1+P5)}ZJmM)j`5n|JPN9(Qp#=t_ zW`)eH)KMb`O6T%_gpW!@*X^#+=T~!PTdb!5^yceoX_l$OozxHXh2@oEMrDnuEanUL zCyEg;qHYu!GE~USJgs-rKX7Z8IS{^k&a}*~jv%1jLi8zLRDt|KCA9Cr$Dd$evSMAC zXA0tmOWA9jD~Tu>Z_$Pj@(#O#+<$0u9_P5e;_X9Ew_bl{&1!0%0oK?*oI8n4BL9nWPIJmmH`%lsJ{}{q0-%s-&46vPI zRsb*T@P>4q^Y(fNB~}py#u7z7&qMZZOp5mO^h`YWPD~i4wq+WTz3HC<=Gcbr9;I!L8hX-}{rT^KbU!X;HwLWbqMq(&G8<0cvaV+EYZ$T>ITAPrzuSx>z?hwGYNHv+5zau?eRWbKh4E5>}RK#XozK zmn5)zb3YL;ur84VR0W8DX*RHp&vO8Z;Vv;18#o`y04g$B$v_hSZ;XM`q(QxH7hw@5 za^Z>WFcnT~xvX`E#7Kb(ubc{S=FKt*SGoFNt@SXb+SKa~PoLOkyPkSfp@_M;xej)f z96XDk$0M>zI|;*nWXD9ek$|NCm<^m&rxqP{{G6|--tPgHI7I=8laFumvrxP@y$>ZLD(q-0g`*vqifhHYp7RM6<@qG0T8Qby`9lv|a80em;P~$NBMkuP0 zzL}Ys?qFqN=s7JDqYjnXzk}%434s2z4ICs8PGGgkwr=X9&Hd6dM@Jf0O9y0rRK4M{ z1T%*Ct!<$tcrs8^=EojBjN=9wg}1=Y|6tpa?@<$l%RV}VYO|EP~XC>no#e;X`$ zcg0hoC?eEg6?g!MPcB4j8S2>F11l_4!A_$YS%!kjD$8wGcat6qBK!jR0fwyO$_8!ck3i zy+r5XU#H0H*F@9sbMS_*CG{Emss9*HS=#(yw%XThJ%+lL=llpza97<+B;EQAAt?10zt*uV!4B#zqImwPl>#WmDbQg!`6R3^iq}w z(Kw3SBS9Fb*;&g4`HTjT{lNB`i*!IPJ5YZxiQ-l*1&$*9KqC4PA9z~2WQChAU%t!* z>sX?e-DiOwb%$-NfqMbFd;h1^)zx~H>tL0l3aEtGQuaIKS|Ya_D?7uxYo%#?z^;>J z6L3%b3a39 z`T^6Ug!z*#Kq%6aqCUv@ zCJBUgM~-Ct*y8)&+Sq?Q~>QL>t{a0WgtnX(Ob0r?4KsY+AtkTbjz3yy79cO0l6 z>Yh2|iO)5MmOz^4i*n!lw(+jSF;DN1pXbeDXyDBs<-Jq=T6JN=S}?m;4O%`Z%$SYs zD|rQ}$5u`SUQ!h*oOzHwy=8`tN;^M+a&%?5OsAd4_Rp8q{g~?ebMKNku&3+R4qj(F z5IRgLMp4An+#63>Y(nb38Ux{}0${5#c<1#=lSDGaXJa1+G@9hJoBTHAp4{0JHxC?t zc*5tVLDdsF_Y-ZEr6m)l0#(Q=Wd5edV&zL4oqUhn7upXVK74mx^ufFH()-l7dWXm< zuT1t|jF%MJP`Ot#QB*XOuB%z%m0d1AO_eIv;h+w zb6mBTpy`)G6+nJlci}d*Ec964*KN<^IF}BOLxO7r5CSyq^J5@GQf=14>NV9(zuV+gmoh^YPHjc@7Ys z8|#OPS~F29wha=}*K9`96$ZO-^L4U!O%>snDm@XmSn?vZ-JabND{o-2y&$!%*Q^!f zXLl*+y!ma@Wi_tSmyD4rnS*%-rg($st*ZFpK!-s+#;^6);&s1TYRsP=wu0c0)Ly); zZKBC~T1En2x1N(X&7J;(M@ ziY6vHB5WgrB=xaYkN%9P5`$gQ|MHanM-U9OrSS6?9bS-*$=00+j3Y9ju&;ZvWN!CB z*oAS_&VITMwrCpK-AU?#Wt>8i!s1mTds(Q5d2bCe#4{FsS7S%KFjx1{6iu(SgHAK1 z>Q%qD#!HmNOqhugA;ac&r^?<@Ur~9vSN_HAe1;~eqXIGkXK|vvwT9Yn1VAz4ZNPle z*{3?V>oSMylXa9s!uX!;Pf|u5T_7``=B&Ea$kMl(o59RXK4@ufrMR;|Zc2HpRe8MU zoP6-C2(5LNl6ARk)-x1Eh+ip1N_^ZnZ~SoKUj{sy8Nu<|`p(>u#v6Bp0hwL8$K3`$=ejC-Hvbv`@z(kwz|`-`4XOhWk?g95|% zZP(#oVb8*Xy#$62s;M$O=*jUf2L#y{*hQ@0wWmlY8atTgg^m1UH2YUW zuYV|`-aG>-&}Aj_t00hKx;mCfdf&IVlkkPJc!iePIu~0X<~8DTx9p+C^saDW|AYvm z=Pdj2GKZ)HghzD0$0C2Y9d82$C1*QQOk1|q$pSSKAuz(RS=siM?<|uQOz~$fX6;F6 zBKE4YF!f?4H|J8KC;HPvZqV5C5v8|v9cAY)eO6?BMb2PQ6L@zk=mm>qo`)FM_ea(d zHe${Sw{7UNb6WAIl6{$`Q`}q~)_*WWOjhb~VlB3mh;7?J~KE!vY zgdTtBqih&(Q{*aK7YCt0Ky&S#^>@b!s`w*BQBl#>hYIYM0RIGddIz{k0gJM~^t{Bm zK!$8GJh^!OiL5gfA%X<0R!_aVWb!J-dfoHlNjf}Q@!;asXobh4x5l66)qLU)q+On6 za?>-oy=CgcJO?qw)iqwrBf9t)P>ZGVmKquqE_r|5miS(?t#)U7b!%^8Et{Oo%F1%r z#3bE+=8b3$7NNW#Qc1V7$h*gpY~?Ds^YiBC-#Ux7Lcw^z*MQNMk21+D)hQ8k$H1Q0aGWra0VDCIOk$AY5NPq;?fU|k0Pi!M6H}@S7 zpE6LnF@E5bgsbJS!%BuX#pUPQ%uL%;>cfogry%~-6c;4t0rS`v5o|H3BOL4?BDD+T zG*0>e9vJ|qTzA0>?4JCv$nhBU<5rBh5*Zih zQ2u_`XMMIH#}Mc{EC7$S?Eo^U0w-Kcca@))+R9RgW-G;?2fIvIW$5y_qc+e?c?9;E zu&C%J_aQrr=BgY9f_OQQd=ZmY*V4)kz-Y`G>{WwD5z@pr$1mJz9uMK< z*bwTjrY#%d2lX;DVL-RsF&z|TD!wwQCzzSLGEa^eVvfKIn_ii2KcpX zZxtXX`9Nzg7jyF<`$Yz+l_wm{wsz)v`C36Hoc^08@j@1zSuxN!tsp2`?BPQbO=eSj zUEu9e!O@R%<{5RGY_!|9WB#Do2o%cYVEhPB`8Y%sYt-Lrlmw)R?d02|vk=K{5FT(s%zu8)ok>va-B^+8B^HjZUCB4P8JQ6DUK z{NwGvzFW%}y%Ih4f#J54zxw=?Mr6HIOReKa5NDIoc00HLh4Xwd*(D0hrVmreu6a^+ z-ih$yN4*6lgrmV(^>YlCpOW_p@H1MNV_nNSyg*#z?Dus3I~M>1I$Op#pbGr>T=OHV z+#|&BQ$lx;Gkfn}5@fm~6WlF1|58~cx)##pmk!<1i>uR8@;GL!w6uLuPghR5a zdJqV(1bELL2s$YTnxS`|?AtFogd`zRwgZCkA~rEHVcL6MTI$h>H5JM|0XMvk{l&6X z2>{79yxa|dxO|3M?o~~Un;2WH2*Rs4fQ`e_PVFNxVy`OMyYhtVa-_&!R^JQA<@6kv zORFkGB}J7v85)k#jb5(D2ErP)%;dw7r#-J+(0jTzbKen1=0apxLM6HhaU+`Y<_9^she(t0#T)H`Kl7Y0>x1#SjnM#z`8!*BBCIXh4WqMN zjJptiLyokTN{%~yS4p@ZF89uQ0=B&c9=I*qAILR(%W5bmLesIpe?gA{3NwO_+1Myj zhjf8xy|~PEPI}C%{d!YzxyTSWJA=7hGf}`=r*TgTc5Kmzw94vtdI?rLV?$3Hd&hW&$XR9p6v^+vSN;0I})GRAlut&5NWhfqkT-hqvTgZ!ZRr zcSc#dT1t%_^k4If9sy~;@4zEBEs>V;5_2Fk;fL(2(opw7uaAIx(%&r-b4BO;kECyG zr#FtzGR~tr8XFsnPag-WX(QdIlv5Q&5E~!@yrWtSV6d+M#yz~0&@JKPGRYspVmRwR&3S8Y zI7M?k(VQ`1x8jQ?wlv!&6rmUNZEyTq!SWlus2H=aA3amzFEWOHSoG_w8X0u@TAZ9s z&wG{kP5o23z4jOG7KEAZ*;f2lEqC9?HD4DOo-D#&9%8n3Wp&uUS8lt@C z(s6~4N0lxNRC4;R1Ol@n=539BAcEW)cin8=F_TU?s}Phj63Hrlt+tt3rSYZg{kN|Qa5q%vH6M`LGa4&>7hs*6yj@C~rCjR*&^ z{5^*=1mGOz0I|;5y*{S%7YmSPDoQi7!e|MoNx94)YZi<{b9Jm2&(XlD&PX%Tok?X( zz*f9+m0mpJRD7+VrOrtQb!oLBg3;fl^b1Dhkz1-$B7Wg*^*NsxlPd<3_8pj`H5*y? zA5Fb1F3y%)EFsMK%?#!ng4R;~ZsW|xt1ixKzN1UA;jx|BhW4J2Ge4o~e7P~Ou>Pi~ zQ=;u%j~sHb-txDVV8um_s@Y7ZY4pj|3B7Lq|@&C#hjjs~Klg-}*dF}JFs z6wRf9iU6h3Jc3HhQl_+L@8D0TDfKg{rc}QJh@Dck^RxSd(sd3s0-VVBNjd>Qobg+N z4KkAA9GrTj<1bDy0*3)zE^%l%VhNbRhs)*`s9<(Y9yBIvym>%P$7HZIQmKfI%=nTL z8#|jJUI1CGU|HE)(F^5=w}kp9CKNLaYbMFNvl3j8dEFbMcgNs5Ruy+{9WKS%?Avff zp65Slq*3gon@7ij6%)QIOzO7vtK}CkqERx;Rqnj1XwjAw^rO|O)1+d*-aqtw1vnM9 zfPMFGpecyDd+|M)2m&uW#1}ZVUYr{$%Nti!djH9g`GZEFVg?>GoG}!GWDsx8QJ&)0 zruFWRbg4kmGK7L(AX3^0#>-$so)@Udh95aCd{;xZq{JMW@3H!=#d*XH+fo;}f2u=l zRCw1g!9imW?(6B$aNId+!tLtA*AmN((ArJ>$q+9Yetzj5#)>*prjs} za31lrCp0*U$vU{>FEwi*rc{8Td|RkExaWdA$KrO*ombokq(=n(sBF z`?`bc2cJ%$xso$opzmTsN3y~e(J_!?-JN0_`OB`uTZJ9ajjSSr&Yx!_?RH=|+kzdH zPOU$RQhKa{kysmW6%!MuD9t;M2eHmG=}aQP;RXq|>NT)aH5^bjf&h(+Tgs<3_bH1c zV`i=C4!}vETPj1FfkSP#y}Ja2e!MULc+MLXnF6W3_1^7^`Xa~0;zfV~a>ZJwq&Y~d zt1F)w9H8&LK>ghf{`H;b;SWpDr$9Aa|Ct{FLb*OAh=W~cK>(tHKJH_`|BjEnTu8yG zzgXf3Qn2I6yeEJVRCj;h#+rxXJWB{Sb6P(tLCR?KaI^5lM=Axa5eW}Z%QmW_7Kx=m~@uPNaKq*MIp z6Oe6}0ock*F+Lvm5vKM*Kko-^@5qWv1yE)Qk}CvC5f7Qn(KF4H-(O(y(U$8VfDYEE_QAw7# zHHjooOW9q*{|{@B2Q{OZr{{0`%1GQ#&6;E&k!56z&asT4sad`|n4u zL^Z3zFP0V$o@gq4+3&3t>Q^T^(ap6w=nXA}w^SoWB(s#?A$`VAwH!`Z6Sr%_B7%nH z$6rh>7tB3*DHz+o=)MQpubbANLH#NWx%yc((ojrQxH8%A=VjLBHGZ7$0fvAvAdtHk zIp)z_N&!Bn-m<-~r<^Ok0Te?tLnBe-=^!}(%-+CsN}-P(#41TIaBV(-X0>4kq+%`! zuQHzg9bg~al6Ldikx>ZiGt_($MRRR7&;T%HXOZ!A#1x>Cz6!$7Y$_A;Q{~4UN#g1Z z)E;<@a$jB~M;l3zRS(}jwe!A@zIUM^ zCDvj+(!E4gc`X`R!kB}M>OZ}xfYM47#70ylnE zp9qz02E9@H^VIBZ&>(LcoG0`=U+gP*ao=j!{bb@y`qsKAAH$XAuBf-8a!0qn*_Mfl zR}FkC_q5pX**(m&AbAZZS`+@`w19(>5JJ;&;~M)o4k&sxyw*I5e!ye`omES&0D_rw zy}qCqQdk2;vp|#$vLXwr&U_>pphLwXIXBqo+cv*XPK9!(bmJ+IHbc}BIQ!dqVDd#U z&gq>ScI^bOnk!)QGpC?vfd?>w_O>(#G68~O*0Vw-Q(VkPH1~~zF_?1T;IvNy`PAO` zCQh-c+Q|LDqfHd82SACB$F_j5>^WfA(i2ei;lnKzsqeSM0nnETV9`6_kN@h_=Mh-) zbD-%K2P#257mn$mZow2d-==)ll`tPQDI7vA*~c?Xd08UQadq) zRCBt)w(am-?wk_<#8rVe)8&CBVoUHw(N8ct(De{-Pv;2c&Nw~S)atnGzq80qfiWUQ zm4-5&hEd`WiM*kAILp1yc)8qZ^3lYXP=K)I9Q@(iE`3)Q=BFN4VAj+Mv{|@IZv=$2 zyXJcU*H}RxqHP6$*v!8*N8aT8EWjH&NQFc|8WP1!s+0A*Ob3+ z`FiJT2aN!6Uz!$t+UN=19M%r`zm|PJ+yhTFAx)eHJXOGM`3~zFk>uJtXv59I=@S0V z=!Oc6EU9UoQuBG8V^zW<5~*8`eW-8hZAa&RBrpMMczm?yQsPxTUHywd9yZ26CQG>u zA>#Ga&89;lnV!c+G*DZ)Q{|Da!>A7~W@043BAzzU;h57UiHR^o_3Lm76f3FR-X#G;`F+KZLs}i?{ zyh~~ft;8F7%VLzuTh?XR%%^&Q>R*G-ydX-HdMHm;nC)#si-4jry0znKd>*y%eZ|~F zYc_g?59tNh`SX!)8TlP2`nuEtgYV>ZtyBa_F7fPf;JoP)$Ep54lsO*=+B%)o8Y^Pq z`C+)+8-OkgSE0)Z9A4j!`|-jPhT%`c^pN@h%>|$+aWo)6`azZBczrp&p8X8~BT-ao zcnpZYP5E#dfAmgi|E{v3LL8r;>Z10OsO( zP-U&^f#is%uEWyIICPb_m%98avCRt~E}IxCZ~u-RLFSYDqB8!8Se(k|sMq}59)|bs z^uVg`)v@KO4i9Emzc6K2t$rZt!gB@RSM>s^5+aVUD!k%|%udQi1(FEO5c{%>tvo`D z{LQ9H{8-pMn-XjxU5ok#<@T{=r6H)Uri-KmJy1~5GYyIxB2li!MJw|PE4F?L987ks zbooL>Ea%J0;Cu@6RN(Z}Kyp{@K-=~dV{&zg)Qd1BY*qa*Fi^Rj~ zh+z{y0{xTy@HinAA3z9huL?xLk(bs%A8z{N!xgu%3igh_azg(`+Q{8q5EU@-OlJhq zYYi>r+#%dD{-}gq=D`Fy9oKOrX-D4iPkH70%g!N&jok^?Y0^89;azuNA3D1$ub60} ze|OjHtyvHC&gm<4D(VcS{?gG}HZ?&XJh7&HJo{4OhC-K54<4rHVDzU!tGL$^|{(UKoTA=DblJ*I>JE4-{ME=2ckM6IF>NR7&jKO+MG$r3&&z zd9nesvXREgwa=iv+&u(zJ5ac2ZUJ=ZOuY_WEZTF^v>VXhn;P!z{c<;vabvclFY`uj zc5|EJBl3u&L-?^vdOj-dbsqn!Ryj7(@jVAbW7rn~5(|lWnzb4z_zb3}rzYw zR>81zsyZ#{QRoWK0MOk8J?4XQnwl(L*cW>f6M>3ZENmcD<$mZYG*XGqfMRe|PK!0j2IJEb zUCKzYt5R=KPv7evS7M~KB``mH^;{fk=W};Pi~VxBgQto zU{!%q05S>+2J-&-y#WH$5A;4?QU(p;hk?Ux1%@M=!8xM55TLbwwKJH6l7gOuiaBYc zV*g*joOk45HYT)Z10cQ3taA$>CBXtBy|83TxsDur_=Lpb4dA7K=2$H2AUye*cYjeg ztL8g@KHj5RpxvE5?}B0>R=Fx8Sy?SLWSWzS(Y2*ixK}>Tmg~{(>^d05s{t}wX=Cvx z?(n~-A5fw(04{NHKu59SsodRt(T|i@M3$DwVwCQ?X0MAMN%{FCkl5=^F3znlNMwpK zUU^r=5kgSvi9W|}6+WeCD=~F{tJK;fm_0dfZ+e!dv;XRLh8@!-)tBZHmzpyr7LZ1= z(0sL~CPA6^wU!fwQWuE#iE)VSd8Z%D@$MbxnFbA>nBMo)7GU)Am&5Hbh**B4UmEdp zJgc^6mW2VWLYTO|emW&HGd=xaPJkK+?b;VC_bZg!v|cy{9-tFW`KykEbpWO_t}pU{ zi7+T!9SQ8>GeD`!+iX71ZaoGzWRtEMlp(#NSmXho{8X0|UqtQU5+uddOz#>&_YoK) zgev!V$-}eOco`K;Zx?ag1)ceA8r-+G`%4oZ4hoz2s1Pk8Ko`>~fSyU&e3cJl{=0(? zVBw=$es!#`(^M+{*l9DO;9@(gbHeO`l-@uItj)Qk00qjiO{m3cV^d#uT+d~6g z?O2{{Jt|q-g1+b9xwWeeHjZ+eS-}Qc7m&VuhUV`PjzRR{Y8o@0nMk3Om)U(8NOJY9 zPP~D&-Ib=7ix2j2e&<*}XMJ29Z_ppFe;tVoeo?-il^Pg&d@##Lhb#dz=jTAT*Qw*J zfG6*+Y?zFFodn$lLqNgKi$%LCZjhnRf8%fcxIfpk0#w74P_iP{K2c29Ul=-~6#_nZ zKFYa6g==}#_P1+~d8V~iKvfeRs=s(zb+y{4!z966Pp)DvOa$@E^Sd~oGb_o1>@Kex zMa8Ttz&O@Z2D(1+4VB+)@F$>n((_y^?(pZrW>Z_VY?Ma9NU`wO3P zRD%g9DkF&^HerOh{8EZfVb;d28kuiMpd_*%irCJ=-x6tE>xK-S0o&&lo-NV|&+H7a}+NP=xoXOU6*~xZMkB$ z1z-hzFpp{PBFXCFkmCONC+bz^rBbg+YWJ79Py?8zKx{R_cA?j|Yc3oB6-6+>{JjG7 z^RyPMvKxfU^gyU}?+}&hssL4ZHmMosOy%}xXy=R9fI?~ofy{fLt?wSJCwv5z5d|2? zg*P!P8-cPlN0O)>lb;K<8`v-C;@ki2JAX*cR3IGSo*Y&+j74qH7%awNM+MABj zL|Mf>UXVexm+K1MC^?Sr85AoCVp?dtu(b^A*xqgq*>nY{5AchT9C2>axvvcRtW)~A z9;`JZrB?*W)bDT%TRazYV|Kjy4%L`#G%^TH6|)j(li~w)3wJs ztwUGoU+C?+?#M)H*ry1%1o(0r7(#%nn{HUe`)2DywhOL<+uqAw#G!#qlvV$ticHQgmYOkfBFFtu#V;35dA$wxuw zvorM$cGigT&m*fM!^3~dI?A7eS5`WOji9VLfT3lSALY9=dZq@z6rdLc6*Dmk0g}AC zdeOk~}eNR9M-fg4LR-yP>vPVyqq+LE2jGf%GQDf6r$OrRU zg)0G<85Alm#ex}MYA(p0y~A$knB7poSe_xk#ZEuALAmZX6SjOkbU6KD9xX<$YmZD~ zeohWO4^_VBm-&aioBQ(xZU>V$kv|+%8KQY(`uxcjA>o{5o zWLL8w8L5$b){hOoUPZadLyNI2|FAs|zX1pw8GMSDCF1|6ahFIjWNq%tuIR5s=W22O|bS&PU}UBF7>VFB3G`u0dJ3$EzPPIHFPb zWY1gkAt}-=zawzs!*dHN#VYMrYUa$poHv#iQF{Hh$7Wp6pLEV%`Z7db zq!NTLtfoSQf2ER+%R(YdZ%V)?LCmoR6@3SIi4nvJdyh@Cjumd{fq82srfh9WSc^R+ z3!o$&!amUd`9}Z$)(OAjt##UIEx`W<_2ayKCFuh8-TgoJn zY}SroZ2Lz~Ef~9!L~O5WM%iEzHU)d)YG3aSwOWc>2#8DXR}-0&5>Stl3t5+@fcnms zdI&epzLa_GvSa>yP~|@hIsDq#R4A}SYEG(~V1oM_%oOB;-jd@S?uS9Kmh^quv1{`T zqZuFlH1+1>n?)QBV;od3A35#T?2GR{#lCxR{o+(0e?#?Sne8>7j7N)wLum(Pg?V&X zaB2mKMW$Z#&#zT34a{zv@S(pMf=^G2A7|D?Ru>@f`5J^yJd+RdNk*Q_zMF{PLLu)!?zc+;>bn*N!IObK9Q3JiB?}9*^SVd22 z;1e-a75-e0Kg2WS0WH=)3~QjWt%jh0E-DE0D=6Xe_|~zZKZ3m7X$9+)g~!c$taO0Lwv{hRsX0VJg%>dKy05t1Z9CHBGwt^g9y(@t=E>{?eR=v|-h#wJ zTWd$ydHd8xo%;>>q^4zIn^4n?{_Bzr9fiMRYHGyNXk&^Rj)KJTKXPd|39mg6Nc<+A z#1S>hiTH<#5O@s#%3#m<+9GT@60{5&(I& zuk|Xsu!77u+AUA?uKf(HJxsY^+lH!@x34^Q(JXBJxie+e(rGf(Kj%s;B4as=`U*0T zkr8uYP7UKC&s@xp6Q$C!j+b$H8VpX@$m*q5lOM@CssIucw|eS zU2Z%Mt6Bpf-Hc3B{tqOGN-H1a%}iXfK`i*%B^;(bmEI8xH5JKao5R~OoI6*=+x=cY z|FFI#o_^#-CIMSn>=vy|aE{@GXPf2?Uw$h{EwjcmvOO~RtDMTjp=UKMoiB>y742L<9XzWO-$o~Mps83=MF2~ z5cDR>SlObH%;YB=qzI5x$g)Igy_VG1*C`$%b~avEN|sg{rj%;24Q2WU9p0FD@@;Q! z@XJ=S^?xEor(v`k&p{x(*`Z^Y7}d<@;S3HgOi6DS;ZOKjl)j*PY~4EM<`30Jnvv@E z)dJ+k5`1Mg-ge%tkWJ>W#h)bH?y9bvFBAgE zR_uZ|5%$I{_DTPKG~<8TPByBWiG1)Z5FrLEO@s2?Ux&z9JJeA9O`tmn8!;|?)9{GzaEwoOkx!ZE(;y-Ji0(}hNA zB4xfMv$dm5T?L{FF|j)=9nn@&%R)ik^)s$QL&iB9M!ISHG?OyRu_wUde`3CW=h=H2 z{=z;+(Rr}aUflA;T2*q*>gK(g>ymR@JJ*Y%W&%oVPcWW z3FH&SD_b%%R`)YwzC?hTHiP^^tOS3sVCd(MmtL4eHC@B40mJl64qv!Rr^P}ShSuS7 z7sP`=BSC+ z#kF180^KzJo)uVF!R(l+9s~3*8TlhE|L!M|SO7LlURr}*I(pBv8q8+Jf8=g6@(m8; z*2<0WjjUV0yM77D*0QeJ5%EcRg?u348)aVtR5bH>kjT$#>}5$n$r6w=E z#rjF3yh@CfTe5}Ha6m%%LL-A#D@!;U%kdq;4CJTOiG_bTtbfS5MyOIM1B*bQ05!p}IqBo(e5$&x#NSd?nSm{;Ik>nPwU)n^Jz zqzZMaVb)Y2E~c8arAg*{?4KQZQ`M@zNye^SlZo$c2!?Uyi%|J(HM$fmmo~MLc5cb_ z7U)|<($}vaZe%Dh5{F8UJXkya@M7Mez10_LPtS5WRzlEORuxG=K(@YSDsYALki^-PE^ns^h8U{E%bIIG9K;D+Ty;M5 zWjx+rp_wR<*!-@rZO>=z-AK)ASy}0yTH<45%k?bTu<&KQtvFk4@D8{)GEgGjMFI@B3=?yRl@jp|UmRAZS76SyU_xenC(?b=%3!4UV z%Nm!BQQybqP%uGT=ouv8kjyiSt5V|jkDO|ZP;Y7qjY{0(+S+rTxQNMAwXtT5pI*#Z2<3szhVg2x6785Y?=@E5uB@OX~a*q8J=oa|vnU{r+6PfpxJvFdeR<$FhmbF&5U!oBWKV5n0dxZ-j}U4iiT&t4Z=s zWWm>z0#!&lE1wj&kRZFvM(!&T4_%OUXn74s$I#dnjEm3tdQXtxzv6jD!1E@!41xfz zsbc9QIAv2>dkdK4GMN-Dda9=D?KnpEaO7H~X?rmqH$R?;-nM*3b3>)Pc!BP@3K*@d z!yc=?sL#`+JWP{3xTJ6WXe!KNiP%gGlGMh~<5t6eP+QRAYcf0gIK3q8&a{SZ(fjUR zjx@Dbt>}Un+hI$`hc$DybpK6vTX*+p06W^c(@24$@#g=r&!X4RmZ{QfGVXCZtqXpc zF&APyZYYaf*9`ITdZ+zmIS*t*dS9b+e0aY|W#{OdOhvztO-)oGUb-aN)) z5C&bRSoK2Jne7S9gq`lG7gwhr-R%|NaBeE+1S(l57W;K!Taj7YcZUp83}nr^TCx&G;ko zI#KH-$XBLqh43gkUzja%6nF~Lka64@#l?hjM9L;>RFN**M#?GMzJAvo@CHML$|+x< zdf_r}?NRYcW9_rpd2ra&DGKfS`_}@ZI|1${40G=pY ze7GlAIs^LnJjsXUd2Qm2Pl+X?$JzG4TZ1}nC{+&X&_T!!GRzT2v_I z32zlBNeok`|6#&Va79@AE@1XAkwpiO*`hh-$C3_NA@$Y`WEYysO!bFH{V!Z-TG(zL zc{TlpJCk&giH)HjM8N{ya}<;st3={9bsT*I7>ta^=5knI;Ahn!>ycI1ei!s+}%wWw*(-45X^_*9sZe zaiy+#{Yq(v5qf3IA;Ad{0g3H}dY5nYo8(4zZ;o0V;`yGZkTB|7{z|6$w1RZq*0(MB zwet-}o#MwYvPWb>lo+Xtf)#b1CIUd`d!8Jg=oJ~CiC6WCg?NRf_7hL)hZMEp07~4~ z4VK#~@Z>BiO;D%#f(~L=$l(@S(|J6%`-ks7h-&T4e|6OjGX0Q5cE*YxQ<#B2$HkVP z`X|JEn3DCUm;F8W47`za8wY=7zbdcNU^RcXIOk@}pL6=m<8eTgIjhlS^DjNRGeARW zw$~xlfU1<3^UF7G*Ia|eSVe@6&x#IUmBq*~#QJsDu!(x|NkmQDurq{%OqiEPtYU`deg#zyb= zpI4iufR;u4(%ih7!w3bld(8CV_2||a_xgJJ#BFXPj7*~D2r_z7fCGincLUU*VnlTm z)8$P0RWN`j_G35V|5R<8GJ$xw6xNxB1d}I0>!{Jud)t5Z&i}C-gkvWa76m{{SLRD2 zg+jeYmYTBW%1_p%CZ23K$r|gc5_Jd%iIiwd44D}vYmh9<-_lfAKtIx7NIUZ12{Vu{ zO0rgf7D-mrj6!Y^E*{~&8#4_3a@kJ772x0 zgoRUIW}8@KqA<&lnk~%c$+9zusGVHh^mqY+U2SQ+ug)k@0SLiSHg~2V=7%Y}jYKmy3%XF;=`K_$A*vDWGnBe`zOOQUE0twZBb%;2m)!}Wv4;KJGV#9k^O`QYbdb^r z-^4~AoEmTR92X*NU#N*++*vFWD*b@{3%Yp3p***TFh8MFR`JL~B_VBL-f#Y_PX@Rd zvFMaRe^NDi>0rTm&PRO{Z7je$dC%R{VYaJ00Tlm7zaLapQ=)qP`nBc$qYud^iD|9%K$fF> zj_OWVjl6hHPI<~3{L66;5|7RTga5-?g44iDcg7v?&+r+Q30R!LQf~6!X~0fYHwm-u z>i}$2P+!`B>a|64(Ttd{g=C8F{^SaE<&C}hF7H^rI#J?5ep4XjYQP@Itb7u-OlF!^M?B@I*yda1oCMfT52n0cVtph*Oy zEdXknAZ|~|bR;z4*E#lu^E5w~C6-%p-=r%)YiH^J}>CIgfV$#JBA* zB>jcUihP3jC0;OLbBO|j_8XLHj2=a(bF{{$RF#`EZh zK#|R28Xwz+?9{%GksfOjPGFu+T@m>itu{2gQ-?vk(QS}s#)jYk+V~Fn*U!5OH&iy} zn+GH6okZj%M$IB;M{M+rKt6MJ8a@ben2@fn^UrI*ZGS5>6&MrJ2oWM8P_44VnU;Yp zwW+&f=~mV9sc{`*61zYdF4-IxuO~=(azmP`3M|N=Z$F9Rlvq?zC`RlYN>Mz0a?Zm6 zaO))Qn(dz_YA9810(s|ps@wtjSv5iIWj8|(g=MIut=qm6YplCG~dl3&yre`e>%2R$X>mj?%m5#a?OE>@!mJuS<+I+5$}4$Jd-i( zdjYnNE|cUMQ)fG)pS1z^vGlz=u5SF#R0{jn(=(mTI(NJIM7PC{jdm@s|JEOWj`%mV zw&;MibE#1@KeH${a=^0RK(<*!PIr`ah-yGuyAm1uS9bv|Ik2IZbX%YYr4RARtu8k5 z8tb*fG4b7|vTLJ>{uH{SPmG+8j~0!1NI`+qGC@_YwVfZNyq#%b?9f=Zr8GeQ4iL4A zzeMe4m|H+WhC5_Kcu|GFwEAoMv|emLJ)4U35Ltjw69D>mzf}opI*#mr4(xCZ<1aJ) zts$W(g|bqW{P1+KR70pI`xGgE8vA+(0IJcZG-;f^gNlCv5?z4!QvOAvLItoRc#WL! zIgjgC-uA+x;Im8G<<_cCLu}$#qx%_h@cF|0DQ>g*H|_6sDGxX?ORfbNb(xxs#?ci? za~2qZjJcS&*u=6dMJVp}{A>9S+hx}wqLv|GWE#a2NUH=Wk0(bQjp5PCQ&0Qg?GuF7 zY`-9a8VR<*@BO9@Eo$j9iN&Xj`upzmX&i(d$WrTF^fQf;gaG+qGYBMn|BrN01q0+f z+Suj*UK_07|Luw6kA3(ZB73tvJOAlhD6H{#se92}Gj@Uk*(aX$68ycTZwp(l;PbWr zZuOk!$qhm}>j?6}Ty{Zt2!c{7mHj@4@lJF`fUyYVE|8`EMPD8Yf4`y0uwZsJ&Jl5} zL@!m$G<9r(s8QWSaeIci-@U!EfrLm*r=7vGZ^?ijc3FLG`ez%p19t?=Mrw7K&thvf zoG3P2jTlHIT+WO2jCA2IwIL-?Rgj3s#nex6 zp*b`YxqYDpb$Q0B5)1hW+_#ex=owl9R)UNs?cv_$A{VOm4qcgkP~1PttLdU`62y{G zlGgfJ5g_a7LJSgp(MstwO~g2uWq2gp)kn9oIrp}s4SgS;ESVLAuVHlW{&=)cP|$~P zcd!EFJWmMm%^Xm(bV#ukv_JVRY=poODr=~5Vm`TS_)F`3qy_tQz6##)V8Z-u##pC1 zcW<(Y93^28p!PHxEhVNm(ivAR#MpmNd&S8m24*Ph@(^(#t{{3Cb%EG8^{YI!q)%7> z!&=Zii9Pp6K3>iF5`IhS)&hwoc{EKnqOx?{8qmtGy2hQIiKDn~4lhcayCy#qYu0vw ziHmnzom$a=U#uzY~lQPStA!Mw~LZ!?Gd787gST*R!6Uv7y2$hML0qar{uTGuWna`!>4b(E4jfNj&>Z#8TfimiK)qe~ zn!l~5;ZF6YPdT&t@;?81>B#-%nytcHq^ly6B~^nB^6XX5GoICa!Gy*W`h-`A(d0-> z5*T;cZ(LY!GB1W7Na_qi%GPO+{P(Vq){e2sM9Lp_%&g152$1<(NV*7kBl zYA8%1hl!cl+|~-hNFFBgh*~nUSmH)v2a7AyRIK=#MiqNVd0sb-osW90s!84G#K=XI zj%HiydP;Y4jGd^)%PzffXWUf60cf$b6kv7p^qiA)Q6)tED~UsA9`({oaPCm&+~E_< z^lRrHjbMwkGfn2dJc9&f ztkR(N5F%RYGsx4j&F^;Q(bpA97KR9rv zyZ5>8&5k*Ky?KR>igfw+g@|BwB2F>|OYP(5vqiMZg}v zcVmdsx355-l;5I}#Dg+6QjoB!KcaCDrN6*z+OsdAMu#S%zM^%(gFv?<@?&+7if#5z z*NBYGPWFwKe9OTRV%fUhz#8hOU?wBWcX!nkFwcxlCplP7Dj}qMLl6r~Mo#WtvX;oO z$|Xf6HbiMwp~%JM(voM%h2=by-%CY3NLQH{zmipf#{U}m1ElE`fBV6%eD)FB32w`9`!D(8>EF!0_E80_jKn^B zy|r(+Sr!a=(B;hWBJp-|Or^J_9B0|H*uh-d6(M$cT%q z|G;THV$2`X&TwPCT~u8mc7}q0-T1PL8~8AEHOb6uH!3f5kA>`xtz1{P2C}euGQyo5 zt=uU4M=sv>j7i-{WTYmR9fqqJ^~xt$jyuGIy{>Q0!W^!C5yNDBypVLWEmA&bIkFZx zLJs$Bd}{V!Ei%_(+beB_N47ZKW$%Q;sBQs%!?paY5-}+nrL777=!zxO4Q!}W>@c`U z{F^WNQA*$Xj~W7>K=BVgii=foFb!&1C_W|jrHGIJ&`s+?2HD&z{%GHIr`$oor?U83 zRKRN&r*9lNo;4mqAxxe%_nF?Q@;C~~2(u*)=Hi)NTb8fQlv-h;@1$Kf=N(XRVIueS z13Vc_I&7xmNa({}S0)|To{v$(Yt{*4xg?UK`0yq@^nvc|FO3G-ax+4Pj_|>48&bMUL3)8mX$v~znXd5sIqkw z>h(bG4So~WJ?1MUPto+h5Wg&a`#}F*mb-hBXN$}gWj`twsMn%I^lS8V{<#*zuI)Hg zJc+PxcO(YyxU?TPZkFG8vOD|8m-s4BZlXgeHl5`I1o>uKSFT_@? zB_`IrAJgcchBz$X);IjC@Eze7pK;-DS)p|;(U`{(=&IS&3>J&C$5y1kZe6=nH&%W_ zIGJbVeSXW>#pGMJ?4yk7h8Z-wF){g5aou4-?=WK7y|OG~!{fZW->oAi^arHQ8)7uF z+)RWfYYlMRrxTUiL&(ne(TMckmG}Kxv!zzc zxYudGxE0L(PF4;!F{O%j9=fsg-FouZmr&dKXo3ZHIhZ>A>0fqP40g#bTXY-iUT`92 z0n+h$c2viEv&Yqa0+>991~thd;@@wN!`5~UYAY&VzN}M(G86YjGTHBZs7t9gcgI;a zcw{oJp&2qKpPP^EZ2JU}WtYo_yVLSm-$CiT*I|>U{%4Lh!z)jf(eN7owV}=Qy?8_nsEKD+WO5kh*RAacT{jn>7 zaTFPVJSUXWVEU*9Q@qK-A$)ck>*?T&yX*ZGMrWi1@4m-j?Qf;<1PA1f;UKxKxaI6OBqxY}B~W&omLx;Y;z_)Spov5nC+YXSm-H?@g;7MY>u9~6A_ z;@c;j1J658>(LVxAj#yd<>}qhGBW7V#)h*-82YZ0lG}|*!_*QL7P&-o*@o;x0-sBl zY_R$`KYn{cn&3VZ^^mx$duQAFXpN%FaPedd-N~L%KluutO{+gW+tj@uX5foXL%vpg zXI}(-TBr#qzu(3Q_VpPXeJ_=Zh^sn(K=MMs5y8Alz`6(j9x2@C=*{@zvUBlRb$=G~ zJaqxH^5SZ`emMvCBg`6VhghQJo;l>ec?g>^iJUP!tA*iNopKBuuZEDNv(Cenx`*QW zl{8Gh{8BEDnV}RFh#$`NS7!KIPu_ND|60foQn0Sdru!B|XY2ZS8?~X&x|1n?Z72_Q z{;FPQ3@ZI`2=o6gbiq=X1vRo4Kb0@gRxv#?!0l$En7NEbHJE-M_V9^GY6IDd zXOPHnpew>dLiQ7*rr*@V5OyTI`wRE*!tG}TbRr@zJl7;wdj_W-a8o1^x~pKifIkwI z)A}F#G%+KddwQ*CW-BgB@KeahTw?J=M5?T5B&JFajS>!7QJt_D6${u*o4AWc?Ag*C zN&$lThc|cJy`!9L?1d`%vlxMO!L26bIL~ z$5y4EaWMEU4UknwO_bg#q_)^^4iQni-#pcGAXn6P^`=)N@}Yy99+48_7`-&1+S6MP zL&HpoNFq0ib&e=8uh~CLfuXj{wb7q_H+?j)VOrDmS&YUjSvUXVi|PjkN`|Zldea(j zvtT%g3}4MJDmO|}JYh@{Z;X2ABa}aLaA4FtNXx^;;ZNG3DNDYSreAe=0#S7lNV5i+ z#2Y95@;(^`K1UMI1@>b7{l(|MC6f!RcVt0il;R0G8H=!PPxZhuoVDQ1*#RkC(khH9 z!vaiPx-{YuaeNfeHA`)|uw{mcbw0%*B*h^|)Vg}>AeZ@8EIWkuXvgGQMnJ;57OXtEyH_&Pp87>+L`@|{mH&mX*Z<>e(@QQx8DDL7(nYpJUt)a7^`3hgMYT5!t+aX%|q` ztxSVnaqFo^N`L+e!mlhv*Xn};rnRluXy0fC@wf&bpC-{N^7Z%kc!;_mCGHUSEMi6s zM2#3++k{Ehcl3o@EHyoHl#~RT4_7%Zrrd+~xgY-4LlsQHs&%90UOtn~-RajMFe5Kv z)|5n(8vMk`rpMao=b`cLpXpAG<7YO&h<*2 zV_ir#&Hj)jyMgv7ukRdt;$x037YRixBl0OBX<<382mDw zLpLDBxUjR4uhn+nZ|y-|9AkY;WK53IH92*MJ3)5MtuiNY%!(OByXacRMtDDk0-5ug zRSEGyzD71R9vQtMT(hku;#;Y0ZXD!*_u+7$)W;8O#}ff$D$ny3DmGP=@4hPJP^@sg zf6Crg_W)|3`_W?SHn}gjamk)b(ph1~%PPGkiC+LMs|jP4I4qP1K-?k@&}`luDR2Sb z3Fby9<NX}?DUN{`r>P?ikkP-?;h8nYhy|92^$plkqT}lpBji-lz|wNjNAVg z15p8fYc&IkGxcs;EFnILAnabw(c~gqauB1Vn|Ilt-F0<@ssal^%QzPFb$s}p*78)@ zO+f^n;4<;&u@3U?2yIQM7s4umiMpSmp)qpsBW?QX$8iENhn{N^LfX@0S1>RH1&0|) zoTm>yOyo;uDtgwmSLkfzMw)cK{bs3^^vP*9PkZzQPndu$3E#^R4*stp@84s_D^;bF z%4Ooxzu4v4$11`;QMw-`u&fyRV@I}TS=0j^#UphLS127)EK?UNt2U8`!hzb~%6)ex ztBi1P3GL?KA0*qgAO%(%I;Xm5O4_|{6za}*wDr(c$eZ~2^p<=Xr?V)p!m;_eALziDq;O#0Sg)o@?1iY1Y`^e}d)%Y& zSo=+!3Lo^xh6lT6GaOs`Uk<9Vpp-rD>YkF`I+4m!BSV=HR&l?7fwZ|%@YDhLVSs`* z10EKSPuGU}$!81{vOX6dm39t`Tbydw&EL{w@SOhld2#}2!^M>ZI2yJ>?COeK8qJ7} zR@y6qFIfp(J57~_@@07+c7DeC8uHo3XAP=e*Bv#~vVGo5zZIxidhz&rd6wJ{0$e%m zw_@e4YhbwU%Z=6aBw*sKXX6jId8^;@0UUZ=5n!`J2lMItn zD4`i@<&)_7T(zE^P2zrjh_362jddXa8saqnHpm`~-~eMh%T)_Gswg5sD$IN5$*E!9 zeF=W?fy4ap!_z0ILw@ngu%Te@$MYU0Jmk07W{929pG!L*-KP+8Fpuh{APX+hFPJs| z7#QKk3Sl6O3li*|uU@Lr@XxjUqQqNB@(iPg^wn4L#fal-J}>*>?QO6HKj`;Ql^8I={|Q4;wcdT>nY`yz~JW)%SD3&mA@_CL(=F z&N!kc!hmdELag^0!*fJ(-Rky+&r`Yi05X()k@2b#6v!>R9z1^OHRxOx``R1C-Si@X{539+N(2lZ$bg4-}i}I-*_zXi? zhQ6@bn$ssJ%U~e|NIXh_I$+aFzb!-VCgT%{gG-ruf)qlI22tJR4BwUyZ+!Ar>KJyv zk=u^51weI;=ni{oD5Q5Rs>fOU3XVbnt9^szOa1LT&(J#=##&e*MXUUG{Ay_p2@JHy z^IAb-S+PbzSZ-%&B!x65{H=7Qh9>;FD09{zl#f#R`?kinJ}J3RJq#qyU5l*nZu#kz zOm8Qb#)HV1@9+H{>BjWj3?vsM(KNGqP~wE)`3UWs;iHyEYiqcyX&EZ&>OELKN%R`b zKDo0NCx^{H3UsB3`SIKoq|jxB7YOBY=~Tq&zE>{+G3~lIKg|q#{_5N0O5)LGaM0&T^jBTS`cV&6ah5#jjj{Wb`^3WCgf(Jhr?&j>x3+k&0*CV!bJ7Z~A?h|!KSY!Jo z72sb{H8!Ge=sc|j5K1M&O`!+=+iDA5Lvr#hv9j>+E5ni^C6&(dMGvwnSKo4smJVP3 zbuLr4(IB9uW`zaOvFWgy0^C=;3G~%C3rE*26p4!5XHvqLH_#$47 zI#V|JU1?$&JNOZ<>$IIp`PMg}$*)4(Gy$>@$?7qUGJT%X2B%fn2I+sm{v_;f9^9)frzLa#rNG;-U1+YAvVqBPj8 z#RhPr(M|lo|Hi)E#Nun~iHCI#Z)eZZ)291H@kdxitXjnTg!5#}8|(9dUq__d>vz0W z?)me5?gZMKC;s~f&RJPgY8;4A>)|ENlDS^OfF_ks_}4?09_!oua&c%8?a?SjJF zW77M_?;ck9AudE6`XwGKy`4=k^x%z$4mzvb0b_<@?I{i*H4;+8(RCUK_G`yxquyRz zrQ-+Lm*4s$&=8XTKZ2#h3)HS5WBGhPF(r7_s^}w2{e!ArAF-4PjeL^${%R4k*j03` zZ0*RuBSqox%}+i{tI{$^oI?k~vMG_X#2ULmt@>QNQmUF%bc_7A)$CWRhW-jI`>Lu# z-+8JEtw+e?p4i}6PRy>ZCwF1_T@gY$U7Y;FZuT*#lK+)Dioq1@3)QAQ--uhAeGihzEVyKUIJ7Lc@r0C>aZ)iFVrNp=N97 zhN&#jJ5_Lc#Dic42DAl%Bt>=!e)k<>4JWM8JSK#W#e>EU`CHFFCEu#E&pk3w%;!iE zgocwp=(@dBZunc_y>aKjgid>L8ytP$$DL_W$PmB{XHu8(zJwm(V;{#2mBhSles2Yv zcYYMBjhtJ@4r6iH?UJxmyn?Hn7tXGbzm~;NZ+?+RC_gP2^-u;euG^4N*rF*d_!tHB5;;t=@6uaJ-ktBzyl?gW)} z;~Wr^DfpuzzLw+4ImS+wfc zp|b-YB#lb))P-;lS_)SdDchJ5Z2Occ!Yx)#y_u~qI9oN;^gCN-NUBm)0HJnS=o8D{XxzKt%)slTCB#t;*1$On@5VK6G^~Vi7ip$ z2+qwECm&Pg=ZJNGA5oox{ZllzKJ0Y?1)5On7x>uag`k^g-D9R>H}km5f_=I5|~Ecf18Q z$&ct%_`YA4Vl!jR@g#XfQEc1c_I`EAo?1HC(@2+>O;z`k`=TczwI(5uscI`prOEI8 zlf5w0zE6Z_&d0|Hz#)hl4{5o zl<>eOv`IYWS+teGN=N3`1bWJ^8)aEPhXks9R=}-_hh+>Y=*!^)U4{D}DN9^Gknqlb z?=ZdLMD$|$aS04(MJO5SW4|iXN+!srzJa~**=KKww@@+L^I+bZCHm`S8cK(LGYb|> zBTv@CS!{(476m!Z(GSpAndpJRYkY?WCUUuQSY6G|`pBLC!`fFzRrv+&{s<}QZUjXT zq*J;@6zPUTcXyX`qkteC(%lFgQo2D}y1TpXdlb}ft-IE@?ppUhJskAivuDpd`E{-2#C*f@0xQ|uWxGnG`yXB(f@gE~x zU*Aa5N8>vZ@xLSXftjvKzr~VD>j;#=wWKn9% zr2k&_Z7#!o?v}I0mI&2i&H!KlX!!U^kHv^E5E{G{cenxr5Ka#L`Bi_yJe(D0fN`)S zT|658vztIRvcdua=FIYw8f4(UV+&h^=1!n(iQ-+W_;f&}B1$pdCkK?TYKDS9L7^YPllP*qV;p02lb^GkF zLPYFONSm(Ss15S|9n9u+prp`FZ_lU6l+V|YSaB4b_;;tVJQQi1-?Znf)8aE4Kx2Nw zJh=I8z4-RRk8X2{XgemVbHAj3)uFL!JH?afCi{Xy{wFMqi(TaeTb)+1TVNNHj7p7! z-ys4@7oXks;&i~sFNPF2sF4E@RM-f0%Uw?fBZem0r9;WMG7_{X$nbg2aLT2w25_ex zsq#|2QL-G3C8$tBMriGH)p)RZKEfkUX5(ntBqg{nv@!Wcd)ViOt*Y83?3K&k-!weh6GKy9 zMripyo*Uj(zvg1SYbi&zv`ly{84EA`9>M;iRI{>I3bcypQj#{?u3{FFxaJLd*+LT< z@kJ$~nd|Hmff1W72NYuUSGe85@MUhS7eq3da#GjXe6ah_9dpKIUx+orHC_?_Q=E!d)|W}ogXbhp#lRgi+Z0O zpqMRcl7*6YeS=Qnp|xd81^bNlu3Or2%(;A*?KhZWsYZ0-y}zoDJmPsZLG&~a*KU^| zUptLFj>VYxgJ@gH_LPCljzdL`%av0GoCNxWtnD*^%(7qEVvfF29cVzng9^ToYpmdW z2Dl~NxHj3Jn_;9H5T0kFlFO~=aj(DO`ypWqL{!%oykEc(CBkgxVf6Q)}?mD>T6$_Gd4c7&X;eaWuPW^pVZz;gVg zp{;ceTD?+_U-}2IXv+JC1vog7)aubSTplr;holG_T8w8f=!##BOP3fig?`~6GdW<{ z+@7id%~-8Y$Yf3zR1)>&;Tduy7e5?Q0Vr4ClvXN*K5+5B9%tSiFqzBT7n|3B*U=*w zRlf|#%3YBt`ex;PR^y9cwMpUFWM#rKEPK3Ln#G+TNj|e-in^2nBZDxMvv1;qwEnZc zc9hKk^`!e2toEbjQ<5S27oapKlcc109DZQP*tZ&UWX@|XiikCyFa2FRp|=?n zhR}Z*EMfabwXpT*n@(zh`x*muO3Pfk0Deit+&;#nN}wQ@%ZzRomI`E~hiMi5ew$LB0^l zoxPgw%|$?EzAK9XUQcA>?JgGD($!h6eB1i2^9-ay1XSNio^s1T4jvH9=AZw9f#37) z3fl`()}D~RkOBUM>yd$l+~<8eMe|$1T6^aknp%BX_V`nV=*h;XzQT@!b6ewD!@Vz@ zcDp{~4=%w3taj#%ao4N`7)@RiB&@Cjm{UaKTPXk9OGVQ4*r8PhjnF;3zP9tUjG@%s zr|f+(l_ROvWV!?1cl&2jo)k?jSGKOt^-IP{=}zwhS4}pAfLi-B-8?JIUA4581~Zta z;aagXG`;=2%eaK&RDu<*aS~TjwQIAs)w~uF&y9uPPVJJL?{bm$hf_^MSKNwiW5=O0 zf2ziwhg0p&7JFayO0hy5Evf7ahy^~D{w5v(g7p=)RtZOd7xDXlKqV9dBCu#Wc@KQF;38xSnY@S9DRo%=@Mf)ZVAuLbt{5t>Z=X zi*ZD###$^jJ3%T?KEo@T3slH!8Y@(Qr;FzeGC)b7pk}bKbhf9Fl5nmhT+pb$>s?4S zF%bXhApcbt*_SR~WltFq?jwDDLzP_i&2u#z?|stuoZY?o(lLhgC9h@!+hx{?y4!&{ zrn{}Kb#4cWO;!t?5yHFa3MgR6EfZ+{92Mk}o!=Grz=dv%XHrXl&5=Bssr4X%0lB`} z0Ju^~y={?uGYOyq(Wj66Nx5ycyjwVuEede431#2P?mz%=@Z7b z27k+8Z2M_am>__}rCpfC2)~xu;P}+}FON^mS zx`eB8K(Yfow{%m|BObO1TG~CnyZ2}-H$B&u4J9E?Gfi>VL=!%aGF57+MqX7Vnz9eW-$&dej6*OJN%uW}y!s zhAuY@hj#s{5YQf1b4^S(&jJPgA$Kb>*4#U4F*N*zeT}01K_x2w*N5_Cs1Q8&aff= zt2P3*U=ITbPOkl*xFH}avQQv6=TkfdG?+k=E-&};@=~9>KNYRgSahrlnp;8q(#(vC#+T><5Luhwf%V0(A23{xgr6W+6<>Dyza3qlmmAE5M0VKyG zxMtcEWRlcB6cQGG)Jf442PYO_hGOW8M0<{f1Xm-zc~hh=6u$a7#znt8v#bR9IB(R% zh&2o92wNtG^OeeDAB0EKQ_6+oiZAY&GAWGxA6k~%eQHkSQl2t51#r(%iS+Q!CPQS_ znU=9Ikc>i`!L?t)>=jFt8d4<2Oh3lk?19@WoR38P%6I{**z0V+dk8{xkowwm*Sa0o zwdJ(f^}!jlNHV`q$snWKL?Hty7LFSon<2a_huK`HjAmHt)ST`GOxn_vXy|!;|z%5TfBx{Ns#(t2~8x8&?mTqm6s!{ zKK~s&qDkS_mdQR0BHQ0!*!H=dxQomhnE7uLAQ|JTH=aQXU>{CFfv`BWnCs0zg>F<_HNGQPL=)b#+!5!u=Hcdf%sV{~{8~K$isZw3{Jiht~;q z`gFJLpp&J>=_1&v@#6{j0Q|O82Z%#|k5~<))^$n}>Napb&|VqG?)dq6#fsF=IG%f+h2&ihN z5-{_##-VTXW8cel7Zfq!u8WOOwylWi?`Ov2q<^s3`* z#~Y9{u}ql868={GKww}FV1)2)YvKp~01~C^Ujt>ly;pB{eF6XV%Jn+`5LMt355yX| zC~oW@E_9LD=VI{>E*>4?j-}U2E=rl4J<|g zN(vo_HG3{wR#h8Cw@F z+{!4N8Y6xETo8}4m&e6R*H+JQK~Lty{HVxWxz3s7mb*d|aeb^norIUmq0ML1M4v<5 znRiv}>Ac&PIq_PsxLC_3R`EoWu z1}yed8)G0mxniCQmV(K+=#XqC1~DOArJMM9QqOD+sZUBn+|3CVVK&{ugP_&>tw`7d>>{Z3F3Ao`~ zy1}~!y1cU@AM#Dke8DfK^vw3?fFgUJWnpFuC<+EI=~ZE{ns_$_yn`+y`dvN z<)GTPQiS6%cxFF|WsxsFU~O`83OkB^nz#1)8B8dMK|vJtNhW8Nwz^)|f|f3EN+>a# zAS1Ea6gXj|1N-Z(D3Z@@o&iKr{a(p(ac11{0Vj|8 z^fCE|GRz&SD6xe?cFXx0ha?hJI~`dHkW)4tP(BFC&1mQyBDOajZC=J$B8hKJ791Kc z((1~~R4zSb_!Gar(k~4$x1AGv1FhC&g2q)57wod-UGh61FP%!khLH<(+MIS1UcpW!~{T& z=ObBJWN{4buI>11ZDp_LjCB_8J$c!7kt*O7o1L~kjjTE7wS)$PV?zA29U@k{v9r@H z)2_B9sc}gIMQ@U=^`^`^&T~W`fiTt;6mrnW_F0dJzW5G&8E=tP<2B**-SmK# zWtHqU1W!Y5;$Bpclzjf8aPpj?-Sdjl#eUPfY5;7;EE5a0w=i`AXmlKd`ta~M%qebX z>SuLI&en66)>-QLlPykR{Z9z=juKV4`L0giQv4+JZ)5Rra0*wn1XI)C1gsgMx-xk1 z96p1KAPQez*1P6(#+ATfFb2p+*Bioj@vk0bzK#BlrrBSHUsU;Gf|gI$eB=bBj+U%( z=*V2=%{dn!4^1J4|MisrJ=(?s;KUEw?B>}!cFXV8PyN$OcDV2vooU=|u6JFZtVty` zt?%yDzMQxkRd|A~`=A9#jj(b$Lk+#sFO_BDq>?NiNmt6xmd=vZcjGcl){t{Q-h3Qb zXYJDrs{1kV<+phSpk6}`jePjhuS>@RUBL?>a0XOtr~m?o4_93BjW_a1H@Y8PpQgOb z3D6o$U8(Q({tBO-%UhppQ+R#j&>R>UmTN-l^u8#8{;~|^pm!{vuY+LTqZv_ zMsQ_PZGi}LVFH%;p?yFX;tmqSf=kbJ?mp_qaXV8;;L_@pA8tQtT&z~`Tv}@2JpdR! zgC#SRQ&0_YSP@LXqyk&b8P!wk&E8jVuI7An4^?r<6$=tD?AY%SEO#+Dta>_&--#ik z3w<}JQ%u0tx6i$=ma`oOte<5k^C#Z4ouyru3=#a&+lLjqc;D z@LNoP8e~mq`K(n8;$f8YPatzCp>M#y{U+HXE-oREK)(~6*9xRWEP@9zi5M4fj%a$o zIaD&lw9YQ=&!u_vN>`t*rnDOH=J-rkFC=KgXy9&ZoVJ=3oIBIkq7r$F4OG~j^L62$ zTX#6rkdlO1-d*B(EY@LEReZ0RUOzi5ml#(uU8tJ%TFy`ze^acg9Um_#-otZ~K6%k*!o;$#8bKX>X=5|_5oYQMV~aI z+jU(7+X>9=7@y?5YZ@fq*YiLfzS@|CsLB$PeE%4o@n6=hNgD!AHf#vDZ|WU6fUsDFhGQz#LzaJ{{zzs0GO76Q^W$O9Cd0!i(v%D z;HZY>=Av5i2w+hMH>#oqaq*PGz>FMs4W|607HV)6F;>ceVOcg4cPk^k@tpD(m&sJB zQX-TWSnJ3qXC18iZ}+CC2-Zb3PDvy8Bf>GkLU@Fzvn zaeJO`wz-0H6zQXC$@Pzll77cIK*S)03L*Q_-GTnCAtOf1Ls-~HCS|-k_G(qyEuoWu zl=Ug{&P=P$-5ZuQt)11vI5rPfn&?tc&~|5+9H6%$PBy!y_CMZD-l@bZRQ4morIWe5 zVb#YB#$!V>uII3Wr4N>9%3tm4ci{WuMqbd)kVYn5DlK$0neB5y4)r<@l%wq1z(vvE zFYj3QsxRWIm+_D{PB2t?Ztbe@zWgpSC~p|jP1HFY-R)mgyvd;y0?UckjDN%hl*=Sn z_C8`fg9ove4X>S)Yi35&L%{y;b@m7kCJ%lx7`|c+n8Cw=IVJrW;H?{wraO(gejTry za&l8*MDL+ei?11uAK_BD8AsPTrnMLF6khjk$VZvcyl8ULFiD;-ZrxpG<-9W}qx&r& z_q4erzHdw@(Ev76Em0riu(h}BE#xZ5#% zg?wAX7_t4y%}rf$GKI6kRdd9+E~HqlSF+nOicdc)1eqI0U7j@u-CRW%0ggn`&iBWW z@8cXuWbl4FHHt$oFe4X^;lz#QQeEbYlh49h61PJ^w_y@6rwy)mDM#dq4 z$=g1@b@_qs@4~wYylUzfO0bfKu(&oojYZc)5XI;76SDRWch@ZbobM9cc=m7HzIaj` zyO1;x@x!$RD;IfG>5ZDWRckT1IBt8zs;Z*Nc4}GI&EBA6W54`Ri-nO>dV71e{Ka;0 zYDuQlOr=wxB%>Kr_&xyZqoGW$x{5C>{41wTVzGOK^53Jxr+~-Ygy$}L2S*vaYmS%P z+P5pGVk{VVQvQZwAVsiR5G>PRr_xKzk7E*Z5&%QW=2DhEIX? z$I>P{gdqM|I%z`9QMYB<=7Ty%uur-OkkISX&IpJ|Ohh`SC1;Kc92%V65`ASlM>kU~ zD~&-C&7P@%o@c^#o<9Jv+)W3CRH}rZ1nzk9%Y_iEsqG*=y4(l|$D(9L_bYowbv}$N0oW$%0L4E!=2` zG}FJgcZ6+!@wbP2|3u$E*0RhyA0E#s&eFb`b%zKp*GD$YqhEr4_`Yv&oUAou$hzIf zT5N-n@IQ4%i(o6Tj5!;Q$zSKP!bkEASlas73M3uDe0QpWTc~^vGqD>kv_&8Kq`u3| zHrMS+BA|r5Pf>p$WO^X_QZv(}IHP%1MpLTevxcj5p^oS~lAAXcKPl?r{zU2$NN>sh zT-uFtmBAxI{+|f;CZkoV-UQ_5$67e2T#r_LJ(%qK6&8acIbg+9aq%M;n)YHzspA@iOw77t#VSlOqnsV!*J1lg zjN?wA*&7eYVo@{R*7dIsuA+n(8km+SP1z9*!^^~GL`PN@JV`v^i;M|dyDP<~9lr@1 zf_bgA=2=&|IM+ywdg{UMjLg3CGk7y5e_^^*^I*>8A+#w&sATNt0A$OTkx3WYo$eOS zIy{qB30;@*+Mk!Q>Bt8#QgQg3#Rx(w?O!v%(hxyZ_{3)J0- zaNq_ZQY^FdvY%@F&N+ot{JnTnxn+Z&;dG;4lh#N;)4IKCfyN>2=Yb@j{zcoh@{Wi_ z&$ph{_}ejnToTZ}e4nX-XkSvzBS6Wlmk#{ zIwg1&bSF@FFh29-T;9e_MXWd%eQBsk_4+6Xy?kL2YCXJAIG&#AwcJX(78(C2SfF_7 z5bt(dR}(F!MXgM~-1YnYFj@)_ftQW0f3$q>wLX;MZ5_y2T5{!ZMY>AwP0r|TV2D)H z!HA(13q7jeV{C!OqNJe`iygDQz!c9rEdIOFcW%v~rwz-RMB0n)!{$umx#eq2+Us6* zqaIO}Ty8HLo;jRPuMX`WbWj8oMe!zFt;pcQNgQHd$-4zLz6%;gc>Gv6^*mIb ziZs-bcK102L!uwDU0$1Od#q}Mmm|pasv*V%D+*Io+L6a$cN&IYlOLnAAXv8Ttq|YD z^HZ^x;VG|jd&d`6#t-_5l1kR*J@I)g&fNind>k52XR_FQcEEQMSGJ0z-<|fZYj_b{ zu3B$7x_B7#meX@1{jkC10moKHy3NmV*cxh`T(WhWiay2>ZL?Xyz$AXj$G|$Dms8=O zxT~~%Lx=IfUCCFwoLxoGX?4JM|4P%7h5-kG!7C33q6a0;T`U*75J-L;%P`+vncuy| zz;i=<>9}Re+&6twORcRx`Bk#5Yy|@YH*K3zCE?{MX4up0@E4{o^037Ljr`)TqC%f{ zIAAC^>90>|@_a2zETLxj&`VBE-ttpPeoYd64?IAt5WccyS^oo9!?&^$a$>{z8CGwW zLxutY`d9#C*CSOz5}QU@yny@7LDYifx3Ok0pGY~JWHeGo+3-R9BiiUnJ#&JX=c@kFdBQ>#l{S!YhHQVVNu2x?w5WlclP9foreSn5To=!SRdUb2H8&jo=R{jr=s@F8=}Q zX}<_3DQDVKE($a<+_@gc`LJ2{1NnsU{k605(!RZK}BsTJAbm6=JX#R*2K9Z@6K`e`u(2*>iI z$u_&g7BU=)Oc!taIM1}V>RjpZF_Lf3R9e7%6z!(i2hqpM#m*xe$AiICDA~a#PRX-w zKCRN`3)<)YB2JfzH6HiGj3o3dFl}&ReZv+{M4`NJ!toqIM+Ao%jKC%0>zDB^7t`ok znwMv$AP6(Cva)cxOj5pc%F8yEUa*)WRodH1zEE0Fes$-oFD`R@0Ybe2P*$5L#WJK9 zAs2LeAxV;ot1ya`VG#z%_`gue)QVVVUWL2EGcJY{17Vw9wF>j-PTl)6fRHx7TTa&E z{3AN2haiR|HZfSH>b7 z`sJjbuCE=I!s(Q&?Rnp)V4>xIvxo7Z_@SaKpIxX{&JMzb(kAGQ**V|9T0c2}pM-N| zDJr#{vjD2N7_ZxE;wJDZuk#QGsc{1I1COb2Q(Ixtgb z>r+g?3^u*Ac0v`M7bk*m6|wnLNp!&PtYy!+DJS{cBtlkA^gzhfnoH?&*Dj}?Uk2Mc zKY5bd$o}$%eWYKU`VhdWzgij%!7H`cu;i>Y#Ipc37P%>ri+EV5Wg>0z-##PcJ3+qw zekDHHe!TwS(;|zqk|N+kD?14sB`bRhoMo<5kfz)3=T0jND})EPm4QxDibbkyJQMpe zE3wGiv&EUgXIX0F*J18z>#U=^RJ*bnAc`w5H@`OPjmQvnHacJiqEA?ZWd5-U7lDKjh2YEXeG z0h&mi3|?JSU0H#HfV^dWesh(UrZI06s;D7!WKC+MUD(l$7a8xA+ub7IIkx7oTx=uO zI!X;5v9%FBtxqUowbgc8nWHAkV-!UizD}|%gPR3Cu^5o)c&s-r*lD;7`P{{(;i0|Y zOK)Px?_@YX2EB{9ZEyOFKKyU0Zb}DO-+HF|M~6Id{Ttw-WyvM^kV7Gh5`gqqT-|em zzgGz)tk^RMN#d=JD~=-wB3NZ3l+ysjr^8j(Z6kI!HzO%ITS0Uur))-O1s@Jp+v-&s zaz3Qd-y!@^h;O2L@_ogW^H6VM#?GCDw39#cv#NbSV}lwu>`UD( zt#|cbxYa&-&)T46!@IDheY2Icj_I@rOAF&EP#+lYfX7(a5S;V%g*Ad^Ggts!#LI$} zzV&^-3uO8n&omnN3}b(kNY=)jp}z**2iz~cO#I7B=HEbg)?s7o!aq*a1coqA#Ygid z532zdX7e2It^RAy_aJ)GywPo+dD9{UNGkO`@kO2hlWj1Xo13MBJlRc|C-a}V z?bYj0&eN_9ZQ3z@brNf;9;D@!sUPr7OtWGAy+2Jpz=NNpwl2y(G??7aLok4ES%GZl z{YEZ}LwKbFYsNp@n(9RaIOLXpKnvxAt`6dqPhx?*+^25^0{na5&AVn9JF%7mrap+G zmsYqQvBvH#T1Jdc{nE6&(9STqMX5EA%+v~6zl21lfo{4a2UBJq$q-JylDoPT(h!4n z$?q1htg&6S)B1c)M{BJua#i;f20(hNlKidb=dk!9IP3FG0`4w5d04JC}W92(g)yRI_DZbn`p_h5N+m)l7 z7(TSvYfoZfEVnZ?HUC-vz-biPAy6?;e5>a!z8~|< zNOJO7eJrJYmyS4%zcMZ0hJ%&I^r`|&(9eSun;M99ETB@b&m!<1WX(Y{m$`%nT;ra)@RS23BiF!(}#-_J7lA9 zyrVuvMaI8gan(nCMe!1wuc2Y1M=lR7|4IV)AO(}Uf!MmgM9_sKq)pvn@h8I{f7|zfB#pb!Z^HlP2t7zv zcMx&|Xykoms0?y~_H!a}HY4rm6^R>tFAj?78@KbV7sQS6(dPVW+^l)%{ul#FC6#p} zSLXcAD0kd;1n~WH-&YbQ_h`jo-Dz#70)_)w3DjQ@akI~3el$j-f_YK8g zPVM7!p&7>nAo99A;T{sQzn(`$r`y(Re!Hh8XV~F_-`fiM+Nh- zJpFAz^;7g~Pb9s{0_t|&my9=x!T6%yO_~)|Oe@VNe1?ue*tqh?Dcw?i(1KYhL2%58 zUJd;pu_BEiD*v#fM+LLzgMUHV`|{Qs{`q{nAZLk2T*n1 zg3diCWi-P2h>LDh4P6Wl!LK}g>5vFE-($?bp~W53no1Z=WKW^PA_=SY<9A4GZP!(b zERd?y7q4>vAUa=d;n$L*(NJk?%A(Q(FlZ^Ig!+s8U{PgTdH0jW9u*8IEMJa4JT{bVrcdJO!(F~k3toByk zmM)(5ZFGz?M{*#J_pGuRw>!)0@t4tVqVhXCdhD+u^QUgEW9G(9D#bBJ?aSFKyU7H- zz4vhxJj&U9$REAwdL!N^8?Kb0n5lH#$1J9}Cy6<3tsmtm?PP=i)UbOQo?03YkuTP1 zNHmRq{*3F1;vw!=t3@DFIc=k$qqh3ce(`V1%02$vM}-{ICE{<7|JYK%+Yb25nd%?# z;FJJZeAXfTC)&SGw++cf9xUO2|NRtb%6a>N?*JDaf8luax^|6l)gr=Ss=f?!vkX*8 zPMcF(E2(0S-?+!Qx7nkd$S;!7@-!x7u~e1vz;0(42womDnFJ?q_7&h)x@{apX1~t` z6louI5xob`mS=mk*nT{cT#b$~7X}J99aOv(3DMEWg|{G_+1{j>k|N}DRRql~krj|i zGlwZKvdq^M=7&kn_lZ#J7wIOJpK?$WKW!#T461zm0K->En@BeKjSEOxwSlYZG- zIVxm$ZHs(VooKyTYs)+th4xu!>vi{%HOBGDsRJD-q7hZSO_O5KRpS~=s~SlTPeJcY(um4>dXEfb(Q%V)Hk+33%57NrR(U%4__$)a{qESwSYm_2aI%J^UiXJKuIWuvPksI2F_$h;;{D9Nd3i`6>h9|L;}!x9z!i%_lZ1ogZ<(YX z1X!q;U&rEJl>_(+uvlO=$IM6D)_<+R2q;%#Ie^h9^HtF8*f7XJ*5Yz=k6*5~>lDw9 zEhRa3B)a4_)AcMKEh~$H=>-AY!T1#$zf8LlW&U|^ zWN~rv-b@hzlx7krK>jVI+m4DIySq8k2@R9JjE!V?^@DEl$19fS+@8D`Bn`9k zpT23lGTsnscDns|==4oS6dScKp}fk_lS3rXaj5^TvhzGm1zZNNaMJAAjRuVLivI;~ z9;SkwFc49Id81Gog^D4-_@J!dmW>#vn>59%ki7acnhTS*^DBk@zAiZZmHEi|c~?2T zdb)!*Xa}?AXEtC2qARNF?(xnp{lW;K(gh{j-THdx_Ux+7YTh z{u5F6UqU>8tG-6{pHO@SAb=ijh?MpIKK{Rklz9rMi$o~*4>^Nd#STL_W+|tYDghsJ z8a*@RrVU|0d2+D4q_oBZ=m}T-(^iD#<4Gmo8td$jaiID;Ok$eyBNT-)^4dhHN5P6t z8(FxIMN%Di7Vy8;)Yudja-%Un)7q~}rjB##GYf3YsU^b1e3rL&IpZ$rzoK8GWLN@j zDWP7@_jOYMbhZ~)E#n<%F5p>XD9soI3{zIZ^T7YC^HPwYjCIVJf6xwSA|4pjVti=z z`aa$5;r2lYgUL0bP#*Q5lcuA97AaqL0K30Mp-Gh{RXX6cXTxYTi)dMm z+BH_Yba*y9$A<3xBFLcQ+0#<*`2Q$56TZE|-0$N7QGRG(w#?`F%IRj>`NYvm>xsO{ z=*%l*bSNDHps|=V|XyNkCQ1YI|9+gSXqA~{t8eg<-{|M^!V{eQU#0lua6Yl06j7w?JG zqfoJ@$8XXxvo&XxHu{j6+%5>N9rN75GVVyODc!LKBb7--fKc3nhm1|_1A_G8 z4s)k6_f?m=bJWtTis-r`fc-*HtF|j<1D+Hd>qhr-UlfWv5Z{S6aH{@Px3EtD7*l8Cv zLW=d560j-)#p2!HJ|0GvJRA=hz$r*#mN@^OAt*<{1I9_D{7nBw7wpa;NL(6P`mIs; zb-29m${oq(C1c!)n)a$DX9~))sh{)=_)!Z>e@sK5q0d5~cJ2BMPTK|tAU7*vJd$bZ z!xlsr6lDMWSx9YNoF(3E0I^EC_WjhezM2911H>H`&jD`+%c!gC7xN^*R=IS%vcbdN z7l(@CLo}8}d_T=_R!ecF-ab2WfT|{6^WGapgUn&VX#*zdk^^cccH8#6W|viKR5s1% z>=?%A*65K@bh=4C7Cm_A!+=Mfzf^?C!e_p_QKO`i3Z4MBlOH;)4fS;64qu=fWW&#r z8*QG;xQ`pLP6;*i2cMhB)2Kbt=D$m%u?hKNzSVtg>D{d5HJRw&BtTNI=n5tJ_Nmdl z5UhMoa&|}tk#d*|?7F28jtpB57YQ$3$GxQ?EwMeW|Dc;Ejli;2mBMfS95&!M0RziG zjyyO;_uV596qNb|_#UoEg!?~0ocdpzP($U9LJNP&8P2yVrU9Q(c(`#A5}~zm_Ks>U z<20_T5J3%F5<9t<-9fKGchk%nfA*n0sOtp+_NdFK30r{#8?;aeh28F1xUy?$RA9Y@n4bO=_^XXW>E?pEEGyZZ-i!UX;2JlQ*2hc)W^iJ z-2s2K_OaulR_Io*HPFmd+I*t0DLIrqb?NU1J594Jp_@loB{+XIvaJzrJ2h7iE2%QB zgzC_5z)ABvmE}WHnG3Tz-k(%vm1^5(SbwL1Kf6na?|pHJ7t2XBu6X8uIRCP zR_k%etjbBjqM}XJgoNWS>0Br0l0)0NQy8AI4^h`e)NvFLIzqdnd9Fw5n~2)0SE_!I z=~F_wAQZvT%m7+baT^D=y7MK9nIuEAkp!(&WvhhWq|Gkh34vwp7l1?Zgtjw=_BwFmu!^oKfZwtfxUk2 z(1|I3Qkitq5afHR(M$h)PrB&{LhaVGECs)qw}zI@8IN>(j9_I}scI;c7&4kMu*!m( zq+?WmXmqS~OVevm9cg;-!pVK?3{@i|8}(G6OWMvt6@}pk9lWQ^%*pNByurF75oC6< zV4+^~J!GMQxzXOV4ys3XnGy{WD>IA@U3y}92(uEermq;@XynTXXAJ3`#OB=vYn2FI zbI=uF2S25puFU$n{*gxL`!Hn~mBQ8u7EXL7;tjMgdewEH@Y#A`bHSyWG-9?_lTb~w zpm)`ieQ>P2yHbjYm#FVmCarWn#;Dh*ihq^zaAb7!r(B8>dfsQ(nw22Q^6)_iV&j1_ z^|3J%cA<}i@cIYc^a)YFbF!Bugpnp%>(%(n-xxwZLRJWW^Y_q8D+7Cv`fo5%U5*7l<;Z<@~t?P_6p3jyUp=PVMiJ?`@~z-Wz@IWn#!cDbChTY+Ni zYn0)QRQVU>i6TIIE#0;2-W+SCY*3Rxk`Zl#$>Y;Ya=U)eIUd7S1J#R`9uGvNRC-%#>(!5K{v*v~m3wf;Qjgt1EpLcEoTd0h}2@;g4AL6}eQ zKtTRq=5rSqR$0uQf;#?Bg+Z$rSm$YQ>A!=|>mW??F4gyw|8c+}A(3k?jzzj@4fZ8& z2*P=$%XLG5hXbnT^;V5Z@hF8|+-OmrusU)UD-VZuEZtcrtt*td5fjGt=sB#^Xbsl7 z>$!03oG7t9a}x$?~s3CFW!E1+*TGIKbCJihA|cXRHM``pkG@u)Q_tG_FADqj%_N< zi}?2(F*9gDEbDR5NBUCTaqYyHS1IDua$$6pVPeG3{2!2_{shS4j1FTW*LFrJGgCcT zbWJcg`4ck{B!6rK2uvZMTXFikuhaJIC1N<)M8HH2^?`uFxnAsEuSOk6mvkm#&^Kc zJ)|?jKTWYRK<`ZVU+N#y=I}U?(;btEqo@aN?r6l3)Bcpf3{-*Y%{Awwm1;)J{uc!J zK}Nu?!qm8(BCjHoP`A`8vKq}It&rv*@gFpp_Kwm{-0h49MufG&>r$nUMM>ctc3Hdp zW7|Xsumb;IXB%1tdeQwzi2jedo~r5eVy{&B);3z$Z5UqV7qmhmbtA3WMMq5mFAP>X zPir+`Zznb86MU8U*4!-kC7g+k4d+EJTsCPTT^tc1oZrcNKdmT)gZOs_YH{NfkL+x} zecfzP4VyZBZhNU;aW1%GDJtQG+6ZPsW2iQp|8c_YYGrz@kn=RJNiz|R;r9c3fg#Q~ zRaeDSzPcc$&V&1&Sl3di>R+DB8xl{*i6q79NAp>dP4{2&tq=A!^qc*2qn(FvwtJXV za}9n>8r3KO;pRY}~?BcenLs9Ais;^r5eheKE}#(rS4wgTHyPCD0|DWD!X8RSW-Hak`^AirMp3qPNhQu z=>~yKNHi|L}g(+kIWGSu<;Te#0KSJHl4L%_62% z;s0{&$D3bT2M8T@3AgJBNqh=@0*tyI&J7*CJIy`Y-_g)pT-$5x3=DeQZcb@g!7aA5 zGxMs^9pOg6gBW$9AjYw@t#hu8ThGH!g+`uS6h1+135F`8Cq~?dI_yB=I?J9Uog7q3 zH2I%U{AqhE)0yKf3_%#8sz(w1wBx#0pmIiuj`VEUD_PQWY$0NP>KQJ6p{QB{#57S3Bby7sN-PA8Jsh#a~_t$hfAimhk4KDl-gcm~;|z zJFYA>zN)mK6chIuc3ZzZvAjthKd?nUJ6g8nal0U+ZS&bQ6V!Zc32{Lt)EQ6VmzfJE zxgi0j<`BhcFg_^To|=G0P8snS@CG%*DI{AD)Z-=&>#6^QCV&eD-A~}W=c5qE+~zzZ z)XQ0xnD8aEnBq#SSd+7kh5oc=)KRO>bIy9#u5P+YNOR0npm)UM>iRz?k}LGJ^ukts zuCJ!gSxMg)J)0K1M3eCin68fz9?)yq1fMJz9y$6VJWOj>iC8M{;JuY~9$w=&U`=3K z2#YK2i;0Z(ZLp6`f7!dXzfD=bt&7TkALn>OagJ85|A+2_cE>=`M`??^=s1zzjo0FG zqY9)ZqS^5AA^21zJFc7gb1_6nVfy2n_JtI`FcseGgbiCnO)CE?`Bl|~Y;`~Lnp?OQ zgK5ChOZ8ektJkBk(we5lqL{~F#^W|D)=du5ry`@~rzVqu)I!UywlTJ)^3jZy9F!kl z^pLF>;(p{>!KgXTxH;sAKdoFacb>R3#`Tq!+6#@T&@xkU!9wT zCh#E;%8fU*2cM&2>6X#zf-Vaz0-|B@OKB9Os(-Ew7aE;h?hiGJX>;AhUrkxS-Ntxc zDZ8A^iSDHvR-+KG>-%1XGn<~b?&=jecb*dP8IXUFKw7+csd;%if8}dkxhI+P3>ygx zJa|a7%^4DK(of7zq-HMQWy~UM?H1T)Lgoq6_`uU)XO6CSR_dVXGaaSz!q^Eo)p%~( zbJvk!kt-8~6U5C|@6Q*(cDC?A%U|U;tLvc1=={=&)THYI`{i*5U8Cqj@1O62b)2oV zBP{b`DSvv-00Z5tT&Er19JNxp3D_3vofl|GkwAOpOTE^`TSIJRmRC4tYK30<2RF_~ z{Vwj+w}D6(x~+Z5I*OT9SEmuTVf8`YhZ>9Zak@?H<$23y+vt6hXS|xhBDPy5$z6ta z2byU9%&!C5mpsXEu)UYI3yn+$jCw8`c5O8F~KXp9%Svjr8;E-C*D;KnbY_?}4KQh(xWL*f|m+<+{MCqQ;=zU?8#Q*|RO zvYXK?LjB@v-b2p4peLr6>Uppa7fH$^K7><(x}GgK;iyvO;W^O$1`Z&5WYej?SMW`u zZqx*8y3#U?ThDAmJ&Vn}MIxG655wB2hmemo2(#{mnib~NS=7#Dvw=apu%%`)u<^c( z1Hf^^=?UDd({c5PT=6j;R#J**rk`t=s#b~ZsYlpId~D&aP27P&8d}V5LSEN3hgsB* z)4WWxIU(J;mSDW;dID}NGHezL{VXIW0527{{9dQp1)IGitkVWNlvyn^R4a+$-n$pM z0W5ROp40xML1;W5A}aNOR9NnmFuBx?X}J04==ji}*32of;s^mH{@y0!gUVuq72~$~ zK%fIGN{eWPP#n&FY{>EyZNKN(#|bu zZ_G2UO?v;vLqyl`MBiKUNgm;Cj6F&}Lz?_^_?H1kVh2T$vdg#Rx`P9k4i7UuhmbL& znPT4Vo-et1>Q?zmQaEg}K#1b(na(T%3SRG3ID5u|j`r;1>)-N^hXZxCZem8#Vh6#^ z>0G!q?O_c<>!oR?iiet*?`y-pye&e%&!KbyFGm+b@Q*TfQj*(Ft(ybVk-sSx6Zwm-M#On;l4Hs;B!mW{)I#yk>m2B>I3lzD@HR3VHSQ z7@z6|uPSi^)-gkXYyXa!9J{DN75dv@JKa(mtGl=Jg<&DQ8FHKg@-EN=g`po(U=v4| z8^+s9+L7wyw$-|fGlnLy!L`f0@rVo`)^C!B5;o zMSzA3kg}|4n5}u->3Io9NsZb4h$pb-ksktk?AMD{%%$>h%dcZ*Ma!i+RW@9=%@Db~ z%HV)z)8UIS_0I%&{o2&@KZO`cDeSptBeCMVa?*{z-gfEI($OQVIlT}@{leium{4IF ziYRA~`MfGhc;e4SIxjaMY<61{XqZ0{wut9PJ#4%{oNh`Lz|3*(#UCo$F*hA8cT2X^ z8P?zbFh+~7gm$g#N!RO8Z^9yeoq)Nxvqto`7RP5eFg$8CZO^=5-=Sc$BzDGf8rH$H z`Uxr6<8-li1n$X9*k|u9T^+^Z?{gC+mp43NWBezE2MecPRcY-=ZtY|lUHdhI&*5s) z4Z9zejJHVrxgo^`XamoCg?p3zD`5c?O-O-c`A;VOD#yp_YA0k?gQuxhi;n-B_gO6?p z=QsEX&{N#eKzz=$5})%`SUwAO;?3?rz}6L87t5#fN(ILKp_p&FfibycP9o9f=Jg?_ zHTpN)*0W9P-aZy%R=u$-DQupi7puOt%ZG#5j~&tXPBuU1bbo|nS!_Po{xbdu3fe3j7b^u z1c~mG3*JzlzMitgW8C2jPZb$^g+-KOl>kjV(g>z$;jb!}CSi$NXl_t&*w zM+Ch!-wl*%q_f)G(B~766H&Fa+G%&p7U)sD0C#qU@BGxxJxaY8Z>2F>Y$B!ANPCre zvN!n3mzgPHsXKtI1f=77ecjwgwLZPcE1*`YnQZhD-JeA(!md3e%_I@8WV#glUa?g{ zXHX%J?*;8w?)#5oLykwMW7YaOHkQtZK5K{P+5U~kqW@POWOsK|e}`m}TFoNk){F>9f08PiOo&-&+B4^V*A=9}+l1=VD!5?gO*1~m1W?J8^ z9V!zf0HG=ivP^OR&^8nF`a=gzIBEM@qHuHkhOE`x&04j*?o0B7?|YQk zidI)w_NLQy)r`_ye^M?@AgD9v%oVjN*9skFKd6gA+Q*EX=td}zPvQx~Jsj5`xhhU0 z1lI+a<8e&#n=iR@xKs#{dR`36@?W`+#x1RZq{_CAa(A}YB-IvT4krwML_Zxu=+T<# zpG=G=&pp8)p2J^Zh;{TP&-zsc1K#g%vJ-=`{cW<@_=ZBAfbgZLdSRpEK#%v#W8@Hg zPs{Hd*m%2?EiGR^Prrm&t_VvOYpro6jXvy#=(SBV8!Dfkybn)==vTVf^!Jd|7TX?) z%!t#xznADg$f!I2G~evG%A*$573?typg2dm?X+oZnV9F(_5h{feTyC>G?KbvHcW9EhGhdJ7cH zXH)BhIaYd>1x~iG^R0iW zmx62l3Matbnvqa$hionRxakO;k*l!h81C&R@w2+Oz|Tww&lSN`K6*5i(|LKe+PlN! zy3YD7*NSs%92@4xL@FMj=60v6%y7S$Y2!%)#0*8uJ{o?cczFUJi=Cq%TH(PVgi#( zWA&#mNh~o7Z_X0QY&lO5DI^H0bfQcYE6r9cCLET>Ege1!V2%o{hZ4HGgt1$bg#S*W z>}vw%uKvWdn(Y@)e?NsTAhoH%g-Zk^@Hb%n+Y*cDe+UN6kB-IO;P|nGck)>fQ78lf znK-;LvTqleCwh8Uu20x2ZVf!8$+o|uvM}d%x_X|u*JMxTU4sq54t=6a;tubY;GC4- zLKYU9^3@F5iQ<<+o;dra!!?9AjM3-wp-D=-m&VjuUw++>n0>{Fgw%6IZTFQW^f%gm z+RMx&#?>UcIg+Rg8gdNAX-sVSDzzU*1IEky^*Zxm=~Cf#=CJ=tu}kbe1G zlrlf~PJ)45e5$m=8`rzPcQdQ!h%A;{_-w};7p0EFS!g0mp-nww4adT!08be7!sUCG$C$uu(n+& zmsdeS$A&z2;rOwh=AZoG25)f2mA-WXgbr#PRqUkmN8(eWp;v*B( zdlxR9Cm1=b?)t!xzc}_ueNC!e4w8fw1Tjf7OoMQ|wdtH;xZ$;tz{wEY+15`rMo2|3 z9+yY9_GgPVSL;pKviw_u>A;3R`BR|OSq~g^nxW(5Ut_p01eL@+*=kA@9yRj*H@-?& zKMxO`HwAW|oyAg46t6aM1~8jAW^D!{DXp0Ieb&42whUld%L9WQNBnSo#4-*pEKh&A z_`Ck~KrsLbt5G>j%2btJshqhMt3g$R+rCOba3PPIV74c}&5IYwV^^+HGb$a=NbU?j z;S*+{r@|rrk@I&x9*kc$y9(^ZkKiW!g$K9WRS-|^6Irrj%y!!6k4VUv;+KUwZx!eO zX}d`&4I~!ER>AL+wt4=%(8#mS5aCnQr{Qz}_0H^0=5#c#0F1?0Sy+69di~#|+J)&4^i>0NCiVB-q(}SaPn?O?)x?a_|0llW9 zVk0)&0Y+c^MY!j0<`pe|1N_>E16O7*9Q`92r*60uPE%k8O^|=?Jx@rjw({FzgLtr#kT=I1e}i9+k(YU@4cJnK;G{( zyzR(CupRD7m4L@SJU`KB<;Ovl!wFgk_~w*I}_`vaFH;=IXwW^^JMb#VSM#FqH)jf zWG02WtyN=*MGiI!`PUb2+0iiT!x!zL6^W-ep^YrJA#6G&o{g@voBc?b7-I7iKZGM` z1|>^j{+w0_3XU;I%*p+C5FLmvi2;{mr60(iPf|rDV8Lbk>})P~hdHMNcW&2*qmRg2 zsvwm2BCdpFtOZ~5*wP8NG&|CCeif`TjumNx+oSwCMP?_uo9Axw0H|=*!n+OX zRt4&&cNnZglf0iVtk!7N#tPIbnKc`SZV5^@!QnwpWx{`Nm zFy*iIO4yl7CZ>xYYoRAyUPiVf+hchM)Kr_Tl$WrbgG#u~Nhnvbusb>^6qL!70DuP* z)2dBz5{9ln9sk)*qqn#k##1+wM>)b&>c@bH!a(1 zqm!N6e{#cZ8;>+-3_=Q>)!#clgLlAHvs*jb38sH5vr_%ZUTK)1;ti8jnI}nvz^f;0 zw#XD+wNK@YJZPH?;Bksnc=2)~8|PW8ZpumX z+HxYd3JKH9JeP0%+TW8t?Bh&tJmn_H?SZc>w~Jj>=DbsAp;u^1?q87;Vo9@Pb7q*v zkmYo)-9K0;9%4`Qte?T}J+P?zkz`eyMX^Zk5)sa~Ud?wic>6Im#sviCSTSxYM>&yZ zmw2lyS*W5b1tuu=$nr@u39dw&sWxW}WAaG>KEF!YUM472FN>UeS~7TSFIe&u+1Y0> zS#T?Mvti(d&(7EOwL|bq;wXS_auQrBdI(16`*8=`&$E+ zFTR={q{RyvW9XWW!8?+Hliq<)O&BGUI?a|po22xV>^90LH-CD~z-l!0!-Cl-MMI|E5=#ctscOBXN zq!p%bYrg-azOqAVX?Yk9n=reVV4PyQ@(C{jDx!mQ^`rI@*-EPk=d&#)b&hqbfM@1vU6v_Ty5rJrbh0ch<_X+yacMb!_jN z3)=pBCE=xUp&9A&SUE@4*fDaX)}!J!YQxPKd;2-|m|KR8je)M3lLY?ik$lR`-YQjg zue$Svz?VD6Nt%ga1>LE9F?6-?WbI(Sy0dm0XKFWP&NiV z=%SrTnP7hHzi%jc@Y5V@O!zav+IdE-S=r_Erk7rq`(*d~lRZpg{k#u-zek}(U1CZ`nLq{B=Nl%5@ev6<9HcP`Wmw<5*LC}RsdS8aW;vxmEMOONaa-y9HXFp!3m zkn65(qD$2Y8?jwfx^=5}pC)A2%z!Q^ED{A~Pp;j-l^y+U>jTzlu73HLb5m89(!{*i zn8raUE&o3pJjtdn?x@DM7O>v(pZh_GoC!=WZx6^YbQHJv+K1B(-qm(q9jClPd*wE{ z>(MZ>*sT1N;6lbyK$VZ9H&#Us&)xNi@5Y$}579Og;;xvGGRbp+vwU7{GCVt8g>_JTjwXBQ6?`1x%xsRo)@_Z{V?fUXr}l+gCm#79S3T% z`%pik-+Fl>o>yUc;IJ%J9WI!!VO@r`vx|<8{|&qyb~T}h@+g9g53N4HJrE^^eyX9y zxLa9Cu7E(-mGjWNoy>W!Imk$Q*rY3xadiFI6>axG=puq3Ja;U2suu0O9^MoN@Vx(= z6+#B-JTbWTd%RXP95kmRQ-@XRy^q z59L~Bbmp6UI7z$8>c&FGzgnC>b7@c;W$YC?zog6m(uh~8F6)<~xB3vRXO8l|opYh~ zmtMOBfacnM=+3dd*%cFb#ee$nuwA?Q!$%UJWMf3LG%PiUx7kmr)~QpsV{So`0&0f< zIS|9c2Pqy}KcvlgHHfIOMSEl=2fPDyoESaHL)^R2t zjaNIi9|^`b(7$)BmY?^4ot9@ChvR3{eP(l}{sIoX0h%d#6aQ(&1ysauBmwf5B4lLf z!g7K66_D}u;|!XQ7P9t}6?b`AXCVvg!Q+kQm+_^`-vo8sGc5=4^OkqA7}l!iiPFsW zazRRGi?U+LmVyA}U#pK2C$sqj!T)#^RmTrS)EQyvcP@E`H(b zJ<4LQsVPM}iAz0F6H2X;Np8E5lOn2>*T9g;Qpanh>zkQ>sTN2lfHiOjg#Ssz;GtWT z%8}{3GgdOFhV;T|W4$8anPne=YToO|1$v%3Cxk1AZW?`hs0n5|niO!v`jgU9?;bgBqZrHK+;&{niy>6K8aB63^0@#6g+wN9h zs=iTWD~PA8=-T!zh*qWWOMS1t+n)%37CIBZjk&zuJqnu2n_Tb_%I{RkWZyg_AMs(f$U=5nDxp zray1Wo);Hwm7;ZKYu2D)OM=4w)Hee~dLZnuqr2d99n|%YX?0DPaCsGO9yVJiR~MJ$ zZ?ID;bf&qBn7+I;Y(0R|2drs(iO*oT<;2g6uos%*7a@%)VVcTpL>tl`T#=rsuiI!v zOQs~yi&H8*g;75oh)tp!#V>d-^CI48(Ck0^|DFBEDf%}1VR-N~_c*il2y(!#+j)!q6J3hk6M3m>CqK?N!)s<{vKwnXLGT5V>(bY z`kcEh6Z8IM9D|QB%iF>}e%A%~gQCxsC8|GTxQanr7+-|#MMKB5PLe184vslb%*9FYIThcx{UwHF2p|8pttJj66iSp_R?VJgIe~Xh@^vi@r$oS_Ar0a87|2{-Z9zPSexxn z+ywm>Vkn4bx+E_tLY*S{aR)AWQ8xarEF@OSQaB<#?$HWo>~O9`5sEpVM%?f-`=|&m zvi4I?-BLi5oM=f3TQ6Bu#iJ6=G2;URwwCf9a~JJ4aej+uBpu`cDQKDYcfUUQx_>GS zOan6bxn0Lm;4vzxtAi#^M=0hj=(6iEO(6Vqd zFqz!iab?96@6%^xsP#)8o=HCVp(rk);n1e^{Ps@*WSS1(oJOEXKy zXRDr!*MfVK2Zoy={~g%3b+=}vyWJLMzgn@X64e~?Cc`wC$9%}8 zJIG#2Jg4U(b%^RC1C}lanze&5C;L=DkN+R;LamV7sWI!i3TuNKK0IPT>U{Kbcz?+G zl-|YLX9PtSEd*cAmwBXEbo|n&4sVld(yf{~4h~nHjEqw?67fubn(kNx2p+A?$2V-) z%kB&#tM$IsIXntW5zpP`Z}fCN7&6t#rmD3ybDU9 zaR87DiOlnogx^NHcyo6C@GM{TM{@6^8z02rG7e&Nk!E5EKY4Dfa++~zvuXCBb$l@g z4m6a{2YH`a(i%&aV%U+ZmsWIY-I-L}if7U(GN^lnW_hcbV2Me`nuq$`SoP`fmZYTa zfxnegTw_W~K`>aRpwHAA%Q6F{VF2Sy#yZ)IgK(wmjREM?5Np)=Y^xbE`jLy&kWk4O z>+LMw9Crbj-q%Z3khc7wD8+B4Yyvae2olls$o1JS2d60j*nOc+_CzX-u6fF_j}F2P zL+5^_&%=Y2tlQS-N=&bEx-{7gS6cj@k8K^C)aqDZx}F~`qd$M~LTdgp_bhXeQ$h8b z>1i%|<5W)6myN0W$ch^}#r|yuAgqcbSayLs}QmNh6EF_`5mL2 zUZd;TS~qGOn|n83KG77};?v#hMwXule-WN}p!mO8kL~|$!9=O52L#m0Q$m{@0Y#$; z=aglnY|*baP%Qa+xpjEbg|_2s-rexeHtJqI-ywh7>B7p@tu8hHxnZV0Bh{w%}&V2&yW||86y6!KUrgw;udx zq+k51_)U*XjdTb(J(EmI*C z0VqOq>vRGvV)avM3YUjx@A5UPFdiF6RFtpW!|Pn@qGj)!EabG8deC&cF@rLNB9uirY8oNtlkpI`7_5xK!- z1miXHvv9PGmX~<$ujG=r*(m#ZnjCLz%J==9C?*9kMZphA@Z$c>?!wSOhc*6th4B)` z8vA+ud-Nec*IuWemPOKfg?EAGerIVwN4)RgMK4HUBJeK}(Kq->92FwGhNkdqlZ zO>`a)GyR~IN~dH~(6im>xPkV0jJx1Y?%i}%{8mX!@B{eGGN8oeoNJ@S^o}*5kK%cv zv6~S`R&L#hRwBxljaQBY!GS-+zYJJ*mOyF{ zExYNhYK55Z8uqmr6yuw9Cw9vgNBS*Vmlpj!0hK2@$sjDku_Te4Jm&N%VB9mJJ(iN~Ip+&bBf+)bGZB$%A2{i8%a&Rl zDzdQW4 z5;Z2{Q@4UHxD{!DcH|8@F+3$TdWb37Nji6!pUU8=I{)t7Bk*+xCXs)~pkM$1cYQg~ z@dt3l3m-boavpHmSu6~o2_lAm5r8+4Y!Pr0AHMC-zlfh&7cQyX-fShUea-yk&rnDf zRPH!5<1p%gzE0b#l#uutaH30<{)w8sPED%2{K;{1`XrDyF z5SOYrxhYt`sx-t#17&da;G#2rFZeGox858>yR|ZF3B{4*L>{tJ$(Bvxrl`*SR^&mn z3R>=yU z@RymFSSbA6*`N!c(}A6d*FS8$#Kt2HKAD3YO`N@7QCqb%UB-Zow z?Z;`YKh*#2`j1oqqP|1qPAi;!1eoi7+OrLT%12)51GL?d{!YE}d}cf|JOx`g(h>d{ zJA|Y7^e0ljX2W1fbdG$b5oRJ2?uKO3w#-(xvkr0C20oQX{=2siB#$O`(hl%NrypRK zp<@~7<$MB*VLvG4MR)aSF0}o@r5-XANI{EcX3n*^cuRjmpDXOWptQAuv4OjjPiH%X zLua3r@a(QARpl-q};FP3UBW1U3p%@P95#csTK#R_wXjhRI zFerE^wJOxtD`#|!vlWjIFP+7UhZM8=7W+ikEat)|eI;bqFDj%pNSrdUp@XWwZczjp z?)>~R8T?1)p=W?VC&ai@m`|m=VUb>i=CEeIMq&Nn>}XlP4lnyIoWVvHtc)a8nfnG` z*=Lx4p@4Iov*$sX^(zExVHE^=zeXMALJQ(Ji;h*w;j_&)ISu(laCL4+lE{23*=ioR zl>ddTnx#G~N00WvG=Ky8sgZ({G8tY%zwstBRPNLiYEEN=ipWOLzHKsy_eMgQ5mc?r<>mVNCyg# z4(C^2?&H=y=sa}oKveBN1A?g{9?;}o1mUG((?j@BZsz7kuVx=Q(OZWkO?>fJEgw_T zC`TeOF3&TYu<57v@Ok4iRVb7^bat+panKwxrbN+Z{{f#nO74{>IicH4u+N!$!i?^Y zLSIvqlH05qh%g4ltN;VXCav^kN<9PMU$9wjYB41iV(Mx9Jqc8s@#p@clm1*k^Qk;Ec@9$gqUJFPmlX+j{Fs`?W zZ_c=1$g8}zl)0OKdu=&NR8M$LV)*FD>DYFyBZl(B!D4T@VxD^yKMVUrxlTB@K455x zYiwqiP8Dg4g(3W015D$YXhPF+F!j;SdHz1vJ- z)!pXpA}AyefAi$MHU@AW#G!3?Xn!IksH8&u1@0v3(?1=mfLN0Q&*%SvSsE$XWLVr3 z`3b4<0hQy;3QSF_vqTrNCAV8$f;>iA}+dedME1GP@UE5 zv+ntZORMMk+km!C^a$&?vERkt2L`QPf1Q5ets5o2n!64DJqU zb8ku)NK3N-FFT(a9^Tbze-7kW{R}Fz$L%OPLFU`d}w#=5{!`wdWzZ{|uCPLug5c!oHxJLK`JNlOIEoW!F#CAF-A z(ms_0=+gTML-O^WkPxbQtG|`*8udAG<81a)@Ca)@_3Uh=z)f=W$t(I$e?t*ZlszVv z^Xb&^g$btq@OwP*lUF`DWXU5bNG!CzNms7*}iGbpr4}!IC-??IiB>6EqlR z0N&O`&kOsMdor82q4q4yewF#YKhTQ>s`_7M_1FK=R8R+x+zp!8GyyL0H9XqzZ4;qp z!n0GPZDCK;$IjXg+1)&gqCJIRIH?7$wte(R0V z{V6571vfalO#Wsy4PD9OVS-B|8Vz}5KcvH7*(8p{epZ4|)_B>WY}FyN?@tW$HZG|T zj=%lDnbn%*VfqFid`YD5vA~8yR@t0k?p)h#B`y9~X=*379pP@MdJ7=PF^SXUPbVnw zKL0l(-L-(bnHTchP5!I1(xE!bC3p6IS2*;NeyuUcd3h5{%!dFlf?(Ft z!C25%Rf4|5RLO5+RAV9ZE={DMgDVARJtlYmB4o3cPzQ^nhaV_xzRVrTn;^3Ao_7O3 zki$}~5iw=T8N$4V&j9udZA{NuJ~K~1Hp31)(Rep!`A+)|WUdNG&I8j^_Jn`g?gy~l zb@8j+%fD<70NNjp7hBiH|FS&}YQHib_FDe!9R*rip*VLtO@=BcXTi`KuL#KxhMAc1 z!T1>|hu3bJ>RQ!dniF*4r06?&p;w0+1ZeXv61zCemNb4Y{2Y(GAv6pWimF>HG4)Nu z3@;{~w)w)fWgPgF4okO&mb4dD>yIDTmLlSc#y38>xfw~Z!#I+ezZ#F3=uxz=6$U0Q=S%hjw^+o=5|3A!um*jHtV@Sdax2tNRt7&L-+{&Cg> zB!T-;;H4{!x@SEg)B{jk^uB`lGb|~cKHC!mjzmGbodIo!g!bU^5P6ZSP6Lg4nWqGE zxu52ikM%%U(gX8lwJD{1(R698TgUWYSJK~pRbvaJn>cq(!=3>Gk4n!D(cJZ|(7YLI zBSrp`RCfOXM_B+(r9cY(){7s;Ix4%*oFkz`bv>pb3&#=`4vxL0Q5e_UHv{viC3cq+ z`}W;Z8WSh7v>~| zMXhB^Yr&NK9QoMZ8p+v#G`TqTKZ=C7)pL=KJ>2L?g_FWsbIj>5=58m+_|i=6?Vx=>&PexU0B#=eYWu;&m;b!bDyZ7pb)nb(M{S9S0#AV{ zko59j1>VFDeIMcWKbUD3SYgDEgp2%R0o)`A$Iuy=d11kC24m(^BNB(qV*7&)?(-^- za|T4CN@DTc?(-P8Ufi+8unV=JtJ@xt0;H}^v#EAnZ=7_5%Yhe9=Us?vKlyUJ9M7$+ z4tveH3x7d+!pYAU-rr$eK^RuRa$)7@oylp=Et!C*M8HzCKF8Z-dSc@Zy#rj1N zL)=D%{p)@y(+^?`Yn_|zR&LL=e}NCvK-H0iHpbs!?K*8JvxW7o{sJbP#Y36xv@pwj ze@pu?0JA|0k=2s_GW!PrV9H{R4cM_LFQHX|D=`^mDkAR;VadgiSbH~-H><-H1c z0VVk7Wc9myf`P0z@ZOIP?Ck%2?~zc<5JqaQG_O6Rb?X~cVG{8#Q%U!|u@JP6U=7LR zPc^U+)o*Vl-R%|x0M8nMFi&cVRX{0&AURtTwwz&Kh!!gol~1qst%PhEHZq;@?0Cpf z;Zt<=JY2w$BjqcF6D7l}ED*~b&l{dao|I{xZu{{jt57~yv;fy$CXT6(0k$2lxbTw2 zTwm!*^W=R#_&e2TWsiSeaXyGMt8h395%YO9%-i9g$z^W}p@|lt?gjO)eaLzLf^zSR zLP_2q`V)IWeVcYD$p?#e=JzBwL3fHC5BscD{9_SlZJ;Q)bpR5Dw=C?%4+)@lp$JIA zT^8?cz;vkDYiwXvkI$lSPhPjxyAiy6)QDU~kQlvIg&B<|T0ZhMTO|i6*@G7Lspi{c z&}0)k{E|?z_nF+sigtf^1hKD*Zy0CwS;=3gXs#1|gl(D^`2Ivs-IZZfGgoq*HBVU13@+suIgucbHj5o>;>-G71+FW3{n2$(lMaRWfc5g5BG^^Z*2 z$G>3ktX5#_j3+y-pH~*D5DOBT}tM(j0&GjN!Yn*S9uNl`=+ai(mKuf5{g)AaW$|-3I?0a8?XBU?mSC^M`xu zquAsPu#FEZO8g%kt=J?EW!#X@atYl68jOk<$H@1m0|Y<=JIC+7rMZ{Cy$kWqANn|v z@Vn?Izl8}keIiTDniYp$GnfKPJmr$n2pv7w-{33+7sXL-m!q1qAI%c$*3v19eNhv7 zv?>|kOd2ojN&gBG2r5YPpY5do-am9#!STxI8sz?EcsWD$ZPHqg&i^<#DM0nku%Q-; zOR?9Op*I^FhI$6Z{^6x{mO2Z>_NLt)z(A8qv`lv6i*`6R)xfH|AJNetnYQ>1) zsJogmDsV{0r(9WoJ^5cLzBm%v%D*9*JJcwUHNsSZ{u2DZ{avWHb05k>==GmH`Dn~# zQYuI{yT#(c_Jd*ZcRPmTq@^OoD?iSpJbnuS3<&^kVW6Dx-DUbS@&^ErN%a!YU!_z% z@ygVTCU~Otr*!Aks`c-&VN-M4&Y%o-G{u}b+jqZ>F%J~fk7mj4FNo{H3X~V;F!pYL zdC>`d3&n72PJelU35{9d0{WQ#HQ?l!059H$056IQ!1sOdQga~2v_PE!K){x)o2Z0q zNUuog9DW&uQEPrB2{Uj9b3>$yV^h==1Ci6Ov+3b7d-WXa>Kd+Ofc(a4hFJ++{ertm zpX2G6K5ca421xg+qau2OlquYKi+$2<&j8_-tRkX6-x;h-6xkR=F{XIK$ z%Fc1Vbg}23SMeL(`UH5=f46OFaYgJf)n}(RZaAGzf0& z4|_8_QI#c+b?cGNYJMi1wxNAtQYK1Dq4#r&h_PAuZY3O-!i&4PKw;G=&wQ6nBW3_p zr;#8p;r%)Hsep5WHp;3s?$5aZ%}2T3KCut@OZ5blW95(6JnlILkQ%85Py@jQhEwFo zIn`K7YJTP&$S_^Evz!ao!Cm~RMkWTi*VE_5edl$M#gKw|(d2UTU*XaF0sp6x&aGlT zlB{|ciEe|A3hj`~v2Q317pib*)KM=*oYmhy)YI4r)E$hK>`jo=d6v~F`^ngsnzqj< zw%6~Mka!sZhjI+1l>VDcYk-;tmSn!uy9P4o`RjnGGTl8vt=&5h6Uv{nTtM$Oxc;04 zFz|uDLKQaf!(Bid{DR~zLjJa?vK@!zEt2C3X7O{o8)t~ysqD$<{2lZE(pk^(tG=(W z#e?7ndYH!r(YlRfQPu^;|*(~(edKlJWkzQ1><-~%s!^7j#;r*(nC0Ye-TX8*zg zr~dCmBR~|u8aRM8SVh75;Oxo7`IkLN7td=>yD}s~As#}F%2Ov>E^{4DA1dkg=Tpwg((1;9>6aOmPtayo@5w0)(=EvoCf%nsV>> z%3yh?r*?jmm)~7!Q|&w&nhWg?t02Dtnyj^sHGtr>ToZ)94Y2_qscAK7Xy2;smuoUU zf7GlWHN`vTc3F3Z-qEQ2Bti{o^(llVRW%nY6$>Ekm&x!Ngla8z>He_4S}On=vDqkN z{GK<0sszPA;6dS02xa|~e6tA#*eGwXlKKy?arS9}E4bT-%Ol-n_`+vcpZ08_LS#X3 zMG#deZ6+-8&2#bEw=^U*g@|gctXJ(Q7$i|XN(;K)ZGwrmASch6xbHGluyBs7fsKdV z7yRBHKn+Db%bQ}MacKQ|%ykd-R0edE28O`gTAhV>vNLWhhuCK5?|tV|Q>@KLB0uc; z4~u+2I!9UQZl9w&G_gX)XYs52ccRN17dkC| zs-W#VtCj72=XO~M%qj;z{K=3}q{&jy}$ zfx3yG?7h&UT62slmg4kr;u7z&YXjfcq% z^6|_j-fXBdyj!RcU|5)u7psqoQW@TMpx7zVk{4?l-69I3LU0>pZZ4?Q94OOc`p7Wm)fc{M2`z`MoKn~>ap zAV)zu!B{Hbd3^uH4j%YPx(U;t4-dT4#YKuv*Q=xzMD<_8_Wu_@VO1Hcw!F~(Zr8%I zbWj!*Q>1un&x3>XVi?jV|4AW#xB6n6_S-Eh(vebX@z&zmVmZF-UZm3+$&;xG+{!77 zc1TdrYnZufn0c8DoO5>rSKaAi=Unv&*PjLtdY`=8IewANo#N|u8o|M#{Sp!~qQ5u5 zFc4}a9o##$l&zEKv@JNKbe;{8!IXkBQbY{=WM%#J%Re7P z7S;iI73A9;R>67|D^kxD<1n$qoY(RHvG$!|O|4tETSQS&P*6~*Dhg6X6ok+=D2VhT zy$RAwqy`d-5fPQ9C<0QICLN@gP((nagd)-tr3V5L0tqe2T|sbrzVAHu9J%+OPmGUm z-Zk48bIiF+LwjFWmU!W`#mSqsnPU`k0n&C|uiV_-*jeU|1yL_ZzHVPnCqmOau-mf& z!gr9saDES(l&Y^vQ>J2oT0s73~yrED$+BOAJR0UKB3WY7_$1>;>K!)N@F_fA6Iq1MG=VA~G>HHY}v2^RXN$ z*kn8@2kGq_a_rMIUOT^r{p0={)RQWg16I`SUzZf|Gca&>X#!UrFq^HubJc}Cz^Md; zT;+ZSzWaZ{1LzMB2quR9J0I+!2@>HR0IfnM9@4H0cM&bYLq}HmW=CM?d>J=9LhjSD zdA>AQ*K~P3JnpMrSiJ&`SR(>6qEmEqQs?5KR-%G17mSxTb;$i09|Z9Mr+ZvJruOTTwqw9oB9cmfeLx$*0Q~IKaZZ_i@bmPZFS=Y1;-wQS zPHx}N$t8u&$!?I;xJV!RUBYruB^P^NKRq^c<6E2w(v*$DSSLV+L>J$bCq_A_Hyv0$`(v{+^5a6a>%7hE z+o_jP3pM8&74hDfQH#R9TMrO0-Z5tI>b=e7u=0-_I2mhgSYBysy8HP52J{-z+1t9rd|Ez(KKs zx6l4cgiBPPBQqrIoLrvU1o(=x%JVdS-t6T~I zJNUEr4l&QW3fJ}+Tf_(AdV{d`#`?Cs~ zJ@ekgU~&{L$Jf-OpJ>(ZQQFRVCONcXdbT+A8M%?W@Bxin)%Qeg*bLcb-cXI45fTz| zcAlrYYsPa(^y+OOp}}d8TrlHJ%|yG5m`zd<_-SNvfchr(qO}_yBrc+7+firEaoQZe z|1Tii6|&G0=wSUHilIt7Cxep@)T~5XlP=bL%qGebl3!KyT;BP+Jv7Ha<|H}B< z$(---*ADMw?D|<|K1wg7pjHtiy7L)uKXC^R5NtoZI{@z!=(7Kk+Ur_?-JA9XH8RUk zh+1xH86&rtw||)zL6u*PurTPP#!eO{pUtH}raZfVimsBz+9dLMu3}f`24TdgtDRJr ziPIH^FQ-p&gx@N{=3Q%m=SE(Zvb}a1~P@=5M>^x53p1c7w>t7g!xzqI0tU!gT z5*fm892z07WR4|)<*Gm3#%tECMD<$D9jv-%`!svt0Zrt>wL1-xcy+!LheXB2rNH4? zS;k*xR|e3Tpk!JdW#vbyhBu`)FARQZ{cvv+2VNbI6^v+JGm-e{#hOr)E0!1BM0M;Z zh+nCA$1DCh)c@s^!YZJ+FzEM}6oR+!b1cnzToSbKG}05J!4klL(W7QeSK#rUy564vJVq&#`;X$2vVx3Xd%?b9ZmRK_F*>gZpk^3M)1A>)Nu)a#f`u}J} zxHV35b?7bp%u_sFu@|$g@Q2zD6Y|;AKU6mH3{*qFS4`$_w5Goqy;aFxX(~J$-h9R^ zP&GJB757Qi3~G_R`KhYExubwIsz|8BvYMZYNy58#LA2?NtY73PHacOS$O!+*X?LQR zQ zv#A#;;H#J}6OPlby~3vX<(RpK2so?v*Px#kJ@XexkTM;P*9^LB|M3oYPPOBRTQ;6U zEuL3KqF*q5GEGUOU40!6o$~5eXh5uZn?hR}<78)daGbHOC=LE#`w6W2G_XQAHdkmK zaV9+Om1;VsK!<5|o+q&Z-^$2ats?PlNpvwe0_Nn0i3TT&eTiQnv4JHodMB(LV(kx- zuQYw)*K=g>zjC$z<%Nba)ci1=pYT$CiUt5?#vQi6X>T>Cx&~gmaC*L-lei7b+Gj6b zw>$%H1OS%TxJp0DmWgc9ellC{ZIs_+FAq*NHLj-R3R}lJ#IiZQ8ce>s={fVT@fCt> zpb;UFTzf}!IDfFR$?D=$mHR{gA5Pj~du}LCf9s@93+v`sX#T6g@eHF+uzXq2Au)yL zaVHEO*R#`z1tHEU_F=RZ$lddvW5K}7V1|QCPmgl|FoR*aY&g+g+@zJ5c5aou;afGb zg1^>#wkaeJx*cd_u#UH6Egmn0fzJI3wg2n>{%^nK(kgJbL4nE|H&ehg;Yu=6q1aD@ z<&C)?K1h&P@>ju;k#^9fM1zD=EfL4JP*dZ2Xgeo!*ZqQ~c7URO`cjH(0>n#Dt zVjz&NalmU2eQ_$Cxo}6?+JUA#VDic8?78ZgveipDt}r5|$YxwCWLMt?Ez{3 z-VLiIxQHg@t{`T>6}db1cH%rg*@S;X$uW9NGiW53?((I zIq@r5%EA{5{G^iBX6Q0AUq;=Z85o#3(_5XW5r#{AtiL1^y7;X*IWn?t5#4SyyHj&m z#b4DjBAJ39p4`-+w6k+2M;QNEe-!V~5E#Yx$o=v$`FM#u<<88rAT z4`x*mdMU9O6(ndrcC~9*qjkF!3z$~6BAS(JXbMcY7X^^2ViOXcDxBepKJ})1@)0a& z(!FGOo-amqF`PNzB6;j;Kf8O0h@Yj&%h0vp2<*x!n!Y!8IRDE#{2y+ga~-hHXw#S0 zOrMMa1ahOe`%@M6b#{3&c&impI{=`xChG?2;0as$4yJ z4z2lNPoY61ilPcD8lZjBAHnb{maDNmm!xH)BED-LPG;wVj@DjUBy00ei~;!De5S%& z?q-S($5YcWr(X#0kN$AD7BJUu=;zE#GokW1ePQ~=gE2#GLRC*+6Pkm$V3%IcV~r1U zN$5v_M24$O73K~K8e47(BuCiEt~X%3(yh)kD=DY;0q+q+J;7WUa<~Mst%o>fv*@ls zksD!$Z(A&o<7r_pcpNu#T*^O=pDU2nvQ9I7j5PPsM_yGQ;nfm0IBUW8?SEY4e|i0e z`=}=TEsFiQyc}(hE5f1Q+}f8WW_0bQv{;ha^tfM?qXy^|OHbfQT7AQBi)~2Bu2RS~ zOpi&PernP~2yfI;99dNCTm1gO-uvL<(IjG8an(v^>{|Lbnlo^1wvBP~ZsR>s*_AoF z{2#gnPDRjb zntlx1&IGJQAM|Jj542;4;`Q0H^{n_XB_K)36QvW4f7wP{ftp{8Nj1HAL)Kk8N8)W# zdt6T7N-l5#&ts$rWXsi;tMkQj?kQLmHh#Aj81mE9ov&Kaezng=6m5>k6e*woqx`k~ zI>euNqkNwrO`r-b|6Y z(*E$@7FhquMl4SNI4=Kzs{3_-iw4*)LaOtltG!)rr&-ytZX5l{Phqtr=ZugVx0b9T zfBZVtB~uLGnP8}en(U8B^Gqa2ghX$i{1A#)sepX-$+8Bo7srSZfFZ#zPUO&M3SBLCqO|G-GD83Oz>juw8E|I#XO%Wk5{ln}e|akLMA zZ=<;{Dx+d%g>9=gFyBY;`yS!5FkaU?x9mH8#6UnhPoFW-Bi0g!OuEb>QD^OO0aj3Y zcG7*a>#r4K#UC~Q>7M@GDdD%d0W$HBaO<&ycZIs`vIHhbM#fX-t0$&~-TSbZ%89^8BMFvDKXR9pSk+2~2zl}%E>r~J-m+YSFL!-B zCw$G<2v*S7wDz&eG%gjiWV~;2WLG!12Vf03xtUPa`<^@tEpsjl}KP=eTRu0dt|Ph2XtW= z>373)G?52^Y`cNzq+-=KFAdqc6Uq|bx`+~%exG?vN>{=fD*QovvAE_pDiFaNo@ewpsrEvM1 zD1b@5=QvJhH$a*9Ac{?$G^6y;@=&=E>R!n!Isi~Gf&%rtUsIG+u zYuk5_KxMS6Fsn%IHO|Bd#LPNgj`c-)_ujUAO7x5Ij!f{(Wb72X5;gZc`hUGoIa+H{B>e$5KgjL7q!%9d%DK!L@#G9E!~gUfvSD z>~0_H37phc8T}JcEpB$XEq<4vOy(NX%!j^B_Ha)(4mTC9+f?A+-Bm8%00>`^+^0+@ zP2n1u9N|#^dyEx_GzsPCQ4TKBEOT7a=I;Q&@aDHHHsPmPA+?oVB}N4?PlHdg`B%=l zu3O3YM#d#%H<9_shD@76=l6Ugy+L_)qLVi}S4W)WF6Dex#|^MgCtNOjM^aA`(&CRv z)#9@{vzr>^=t(W02-$-;*>bi^1n!-4qQf@TVsb-Y;>dvvGdVgp>D38T#dmz9Ewlu$ z0lFZ&T&8=O+J{`D&ETvONvirjcK*zf+x((e^b*ZM(X`M^TR$K9)0-TpIG;XA(*+mz z$!bxk-3q@ApnxJ&|71r)yeVcqZ@$jFRVa#m_^!=q86zXUo4{cCgX(0Ih=gbI88mse zVgU1>akU>>#3gGHnj^b{P&>8Lo}gxgYYEiiNo}*K?g?w{(%?H@+_6xOzBE;Am)3dA zClk2amk*mc8@p;e3U~}^o%Ml@B{zK!=|CkeT;+9!H%RtB|iQtlj` zDXynCNMassUEEu}3>w*ZBH?$f)_;?rnB+Kp!mhdKT$8Nae78ldPL`4|=B4xQ{OWKL z6;X{oVLmpK1F#hQS!kqrg1-#8o~3RoAJ>#W?HHG&`H5PbOdT7*$Acp~b*pVV?8RS; z%1REhmj6W4N{m^gU`FRqcWrL3%9(T>$wN2xU?!z@c}XF_CCTaa@c@^!h4ilsINcSx zH*SG@N+GPaacCC53mmT2j1*!EdC!-=AMU#^X|0@MGQqiNe4M_xF-2FmG&Z|mtPd72 zQ?dFZp5os5nzA_5Z&TH8ETn&Pp-4}-ps$6vyE+LD0DX>`Ik#&xgNN8T8c?PWF8gqV zfJvX37yFdG?Vxug7Oq__1DhvP<7E5!AE|7pQ+YT)fvte#K$%GQwCfX zm8Z!YinGy&DyfYpv0*zQ*3Xmf+F9#b_e(cH-*!_Z!5gSm+(@+q#GTE7@(oWHxX$H-D>AoP={#*o$%5DNB) z{`1Wg(<~*~WaPWomw!vWf8Y&9Ky%Udd=>+MBf1>L0eT?+!3$x}kEWOJNOpe+o)sal zY4A_|3-e62Ql4+| z0YWPUKZeTv*PhAjz{K$fmznsYKWp=`h-GK|YVG~eoyzM2G=h;zwjux-ubf^1&_vF+ zv>e>yNLkQ{{(yBZF`3ohxq6R-(jIelj~DAU+Lu*Za@o|S5G=Nua~F=GMrti_REpUu z`xUJpTPTi(oaNHrMB*hc1lh5ch@GnWC)x#$csxuF{OYS@-StC zm^n_T(p3}`B#apUuEs^#=IXatCSmwAB!I^o?KQWox+N;O&6p_80cgaeLEna*%dD*2 zFH2$OWFmIR#lONT8G2f{&oMZKn6g(04O84!A1Go%|l-@E<{cc z;^q^Z>Fkv0#RL$!3e?WLeHn~VLb$|8*7qlIkC8h}<9|GMa|->jSHeOTp}0OQE-ua# z{M`C2{;7KRDi@856z6$gA}$)XCL&6zzmH&k(TWQMb2Q9Z^M1r_jwFA)>$Y64 zOWMroZ^CW%8W0yR_M2m7yM7G8>^0SPlWI=hqt+gE+%4yxMaxSZ9Pe||HHy}?V}Q3U z`0*s20^fBk#y=GsA9!v>^|*V5)clRDTE0pbknjlBU%(b_T6=O_r1oSUPld_FXe*zJ z)0L(H=o%6S^hIakM$2`kg*=IPJPj*;&9K~6ki7Jg*c4+9tlS*gDP>GG9??wRx_a<# zou|dAueYE6k&*9w*%AYok7{{wRDCwnj5kQ7u)Hk0vu20^MjDN5R+Jlx#}xfu?O%$i z@0CD^T;N(=g2(jywJ>1Vqb+f8Nkc^0TSG}XYf!CuW zf~yGms#SYf>6=y?eq3A}whNr74*<*IPq@vSXu32&~*KU^7s zT?y}8;5}FOJq_-ghNLtWFD`3bu-pk&i#b%e_^%6j<(v5Q_Jdj84I~@-79Gb7i z6-TjoLMWHq+009$OggLk{i_KdJBVcNUVK8Gm!c`8p>lZSzuY^b2iJOUvOWscvmeCfle*3MqR^u3}vXkiS}pfAM>RDp_r}#MMbt zCRWq?T@49Iz>ZYD3udoEo0wSstjymj?e_r6HI8SY_YTvLSg0f^Ec8SH+U4sxgY4YZ z+K}NNzf@no5o`i2i7_r6KgELf{zSMB85tnaOxd863%(0hKsYSMHK67KCntRHmOyZOfy>us+mUCGFS7Ov~{cRCp`j%aGI;X}-NmZ!iXo;R+i{|FjTR{sjPhr;5|&R-kHXHpBkVsU$|{DsJy2 zQaoKi${f2!NW63{qyFW>PEY$R8f0_P3t}I*$!IG$9x5D9fkXOSU zp{OxlKzXn}29!teeneP zCzte3)0T~+z4Slvsp7lBerxlY&>L~QAEb&d9<2Eu6(+&dcIiQfX&n|!S0`!+JU=(W=clvyadFL6Y2O72ZzcDlK zT>uo_7g7foNF03706J^!)0VscDKI1Nd&HJgzLVB=ir-K11Ypv!Dt$4wjIPOX#< z)(F?;Olf!a^tV*62xxvZ;2KQCaQ+Yd%vT84<3;qw^s)3?*NbaKB$N01d5ArzZAH$8 zE2(MpWsKL+hmZD!FM=p-or)Qx9Z_+~$XwfTzT=_V{LwK+P42sic7MSqfp90Rj5Ma% zEBtIfwAZqr=snFp)KCTvI^JAiwUM!f#LvL9C_i9}SXNw^sWrcnvXXgH0QW>SPT7Ho zT0}5+->OAQC_Tzv1c5j0r6r|6A7LnmmX9GQ@6QS?i|d#7xv%-@@%P#27bWb;>gRFvRmAVf&B=cAt!ff-fpd>479PCVVsC?KtC(S9gOs_dlN4atN}s#r zqmU4W>a;=U=GBiAcPumk>Cz+XuPqiyOSP*#0+9CCM1q*bvHseXwBU&GmBWDktnrkd z_AYs?EGj&WE{>?@KMJVByWjO2G^j-8pF@>9X^!~e7%%(3bK#9>7Gec&$$g;5Vxcp& zKC_QPGc)yXrJp<%2Rdo2iE_G-;pp+IecD}{gEQRmA{X+NL^E9=LDROQ`iB?Wc(#O4 z!8tEkjMA`CiuFAaB&-CPrI+)(Z8DP3vh25;!Uhh6LoCNYj@u=gHrAi*-d7ekgKr#^ zUdQU0ZeRjUzq}5CSA6IvhX>SOnXvnsI4J4@^Fn>UWfL!0)((z(tSPB$UC z6Rn9>Ocfj)9N~Vln89InTb?s$0puDUkX&=dZlh8WkpUAmg_gWDr$W&>`HPquQ@$EM zNX_oDk-k1q_}C!kl`L=UGgf(59{ms&9(`iK1l*Dikxsy5q$>zhTdY)526Pf}W-wxb z%HHa99<^ch4^=Blt|RTK(%O=JBr=G8}Si>TMvhOvo^a%cHB?MYs)B5sWw_U57tcw2^NDOXS@0LB@4| z>M<*sMV?e!c(uZfDNCC##$(K@-%JmAsOK^qQ5M7KpuVR2{oV5a<$&T>E6;8WOlZM( zh8EQvTu0m-ozXS2TN6vSEtld0nKjXVzM8$g3xfa?8fq#l2P5B+BA8PIcw6E%EzAqc z0S)znR?;I__}aeD(T)?0wQTmgaj-o!0%w7m_iVfPnZhL+hfGw7b%EPm+5n1bWQ#;C zMIj^J;$LMlI}N|2jL5^cQ+2Y~rHH^d@wScvUq2^-u=Yss_-v?II-T?~bXJQAJ71Ej zT_BR$Ib>+eADyZtm>(Xp@^Uw4h3}#E%L?h;*lCOo0_ch}7ifyXL`+3e( zGI^U>`b-7X&=V&qpEJkfA_MLI= zUe5QuN1*k$G61atU-a|=b7I+u;O>%`4K<1FMe5K533j=bz8!QvymU-V#zdG^<()`x zR2x5GXJl||nspjHdbHo{o;wwg0kcUv>|jlA=$aoexTz9VwszNii7t+Or<1)kl9NmF zJh^X>D{f`nL_|`kEa}q7oHj#T44`J%)Cu`E^kpJyE;l;3a>AUy?@j4a#n$Z0A7A<< zEx@t%dT}r)a)aRdOtUWMzoSYo_oNsP<#}i||Kfheok4C_Tq}K4x6U3~|3#75q$^=6 zjr&rfi<|jzFAAfsh-ke@;bc5SSm1q+(xwTiov;FIGZzEsX1==L^+VcVb{ye;_6EHKlGN*JdrcDzv>GFk5JO5>> z)+gn>uaC2UiatWqrCNAFZZU>2Yh|l}D_VCKSz-cS=X@4XqU$?UG!kpUo$P5O93v3B z%S64?QL#Kv9hg$^{Vs#T+o7L~z8HKIzRlfteTa|$oBJHxT#ArklKvgFoYRM4_nrn2 z*||lMwH+j}^DVR2#e?ryqEZ~CzK(66bqcDauGD-<|7RW?fER)M8o@hSfd3v!3Q|Xt zpWJ(O?Brc|=|Iut`GG-a@%^*L`IU39qPa@r47;k|3HdW{Gl1s;TJpgeLaXaS!*VQ| z`NNl;P&X@&CL?BZ_w{#5yrH%uDef(3AY8nk&5LdV#t3?BB+K$i zh12wqj~-l<#I0lVqJDf|B>;_%j^RtDVVb1%IQslVumS+uXtG5BKBzPc(}`%_q!9yg z6#tqP{oRZHqstkd0t%jJw=#P82|$B!y4$r}-xGNbVKh`!+~=q?l;@+>>=H=4yV(H= zc#UyGtu%+bH4%QQGev$)Uz%rddUp~f5;y{vJ%EtBzO4^7)E^B3A;sBNCS>m2L(#ul zx#w?c3Jku+dV(Z&X`>pz1yAPAU$<3yD4dVV0zo8Oh!BIvSrz+B6s z0pli5@A&VGyu1i?q;GLzh4sdG5sq+~ROqoFJD$Bdl*P*BL)wAFMpuF9vEr|pXDB5> z2k}*qNFC87M z_opKL4m@a{#r)1LncJN;6YxF9XS}Pm3(^}~@d}+2o5K)^pQ)f)d~LI|ccDtW6L)Lhy>R10U`xru&(^His|iLJ2YD`VRbp?!}3CNSXkRWs>vOTjhB8wZ&>a(xE< z7>2TfZU*LDNARzMcr!+Ad-IFwB1=4c*OM%P^v}nVs>g4=OV2(7c=m}0Wk=xAO!imf zyB)6@I99_4>;enHT@Mf!JI|NPu``EzXK4JSkrunStux~Jlq?zGnG|H3)e>36#t~06 zAbV}DgJL*YIwzL^HWm6Q?sO|1yJr-m)TJrbPm|1bpWg(G?9Q3jfz1-bTn4ufvWvn? zPujNeHk2%>Hk1It_*ikW(>M{giCdgla0cccG8X}eDMt9Yt?|LM{^)m zY{bTlh#70$W&_&v^0)RnO5BaFVm71|MNp~!vGTX^R?mN`lkn}18d$O zfig6=^GlKAZ%y;nxYEtzZza`%4z#?MZCruKon|E2gEOYr!pjqmgF2x%sD)tRvwFq? zNx9RJ{&rtWu6%*DR~6l?u3ZSYqbWEk0^a;0(L6_)Kn0h{VdvKTmWUREM53HW$uQ8s zJ*}Ew{fZ@n1J(<=^3RF> zGw-&&U8Op+5}~nPnhcUf!4?i8G$bPH-|yR6umXsOrPDOh^`1U|?mueY0?fA+BTDKJ zLPWApt6jn=Y_d_>Fy=bK+{Asfw4_jvU-^xpscIzFXjhUBWnk8s__%vGR-OO=v7r*{ zd1JOFo@e@M4;(NzL7o|jQgk-h&99N;1)QrY87-2(b4zQqNwpY%(Y5**MzvG%4EU+H z@egN+kTwo9FePN7N>6ZI5307dA10hP`_OJlY1sF5jE+NapPOuB`j_I}g{N;nfbbLm zI>3J8pVOu}*%ukU4!~}0A@Z<1u&|?=>c#&K5$;`sFMLcI@WMDT)FgOgI5;_S0N*^N^c&{_}vYzt`b(K)SCqmF4f_zpy)4 zb^_Q)W47n6$x*|LKc%kL>1AxO{ykb2rx#RPQcjKRP-3N0uDR?G^RlZy`Q`dm16dBF z-*Ql|NaiV&_1Ewl!pUd}ew0O z^XHS6>#kw2Hqk<;q8V_h4h-_vavm}x?=&sVnl>CDee5p3MT zY&9j_99f!c31$-4s3vq?-Jcf(Dd1GLG9Ox5y%(W8CQabkjr^HC54c_BxejRl)r(s3 zuWu!R)Q9r&w6pX9EW+h80|eyyze+%(Tg=yzP_NszrgKY7P)W-<>h@td5?^V`UK}u5 zdvRkgf^HwoVgPUT^2L37DaUXnWJ=pX!mC?AN6YSExE=QOwi$n*ap=dFY*g+}uGY1) z$_1zd0pUYWmiQ4uK0-T-;d}$i-(P{Q`}=zk*Au&D7^5V?J!o6(iEh*LI3Q^BYb#bk z0S|oi`Sb7orBTHAiO7X#5s2||*o*vv4TZ9;Nkd_Q0TYb}2(jt|TqSed`$(|;k~GY{ z#I9w;mp?8Dx3K$60nm{P^uNsu07R!sF&nR$^uq6|L#0=~CG2OUyK5RqOkM1XB-lb< z?{I~8sj!@mUQNc+0)XaCy}&zss7j*srI(nxBA$_I5nVZ%*WtEI?>m%b)@rHqTBg3b zxq;IgF`j#XyEtFjQEERG!gE(&@67Jvg%LH*ojJgMo@U-#;9#H4@ConIE_QYRR67Bh z#QjsNb(Hj$aW-_|nO^+n5R!unCqsO=)h@Gy1Imr(JYP&yJ|K#l$(;x~p@i4jLi_{O z7{-N&H!j1#5rNGf2bozJs>`0&aHVniSqchjX$a!+hQg)|yR5r|0rbksv?K+;eHIXj zzU_0&i|V!YX*U(l?t5djk8MlHb6CR2JZopRBgSANrSEt83D~9z$YH9wpYySV%ACo& zTL^h`$x+M~>|D@9Y)*1@B&2PL0UQnr2<8poA0@e1@D(HO0(Fu;$zD5-FHcpT7w8`2*9Lp4Fqk1QRqc ztnvY{msZR;^uP&1?QoARS&mWxa$Q?uRyFa9z5P3*L`Uz*OZb?tY9U4%Z=!`!iwKWn zX2mhA*gRlc-sT+<5mJ57au8z}irU~os9)J#BiB*|IHsyq@#A{SgK2OeLsYXU*J?d> zue?0hJv^VLXlH#-%2W2&9EZ$r83NGmwTQ^^8Y87orB8reoSjQ})7Bm!D$_o+M{@Jk zt(Pul%aN;-W;?H5oWBIJD1->r$qDbfI)6XOTVaq+;L0`DLb@ICo; z4qav9=X@+i33#_LF26UKRUYcbd;{_MS-_gMO0uy@Y;AG|E@yX^ARh>L*2Z`?Kb0x~ zP8~9|{&Yc-FaAVmI^U~X#Svom98$yoa9kDhqnRzQ!WFlexds{}Zu2I#cJ3>ND>D4B zGA5<2JZKssHShDi2@xXy>ZwW#x*O=q0X*G``;P!@UQDxFKdC$FyMRgy3ZVaS>rkd; z+y!``^&Pl^=vv{!9xS7(p-=QKH$EVMokj$H8&3EhmY>jwZuKDvWPEy7y%9kd`S)Dc zT9Vv`1~$pr=8)UZeH|#NceYhUR!+BmB@a1|tY;5BT9l^!$-rKGk%h!-cRNJ^{w`UB z2Y0*J0{sI}2p8c<9p~fqvsDTuBmjj>oaX`8@?z44$r7B-@6PEcjmDn;508Rp6bb^u zLFu(ylkUe4H7RoX1Ng3gWCby>GSX&ttGFWPS1!@Mb%X|gNIGCm5-2lTF_Ig5t3ph) zasc&*cy-XKRyvcS;>L%!gLFWQu>`xK@R?kG&jS8>&>atG*(%DP*zQvMWDKx2-FI1z zo|OKnQ(XAQ;k-{n517$U_B0KMM6em&_$5%mf-aUIt(56NyNc4 zAP9CMc6q-9Aes!uXWs;xmOa4ABrCI=)CAhDB@f!JE!rs&ey4ICmlRgF)~P8bgWTR# zmGEY375TE+Z`!U3{d7IrK5(E0Qp>Ftf`ReDP*L+ee#k*1GT~cfsTaOY{=5T+` z0$KX$d9S2tqBL@Pbee>#4osB!5Ue`y(P*aGG+m4L?6FROE@xNFCbC6Qw=KeH1E5X7 z7!%IVYoR4NSCuf^S9f@Bp>nEnIUD_7z+}g%Ej+z#Ek`eLE+| zjRe!UwN;NX#nuu#eU|p({4gy7nX}qC^BiQXH)wH_9z@_T0z=TO<^!DbGF1*DN`(?~ zTBt#3+=vrE=zKzDt;Jk8xx2@U0UmEJ)hM<10i!tpRd8d}= z5pE*RD+56G$07(~Ng8!v0C=Zr4bG)#wg}g$-a5Wq;MX)OXkg*zpaP=rtt+&iHa~Cf z`zafkn-?LC)^hl>3Jic8&1#fMFT7JAbMKOe*LFogqzVJG)w7zw*2MO6UCvj|;-Vz8 zQe{i6>I`t6QbxSv&7TT4S1aQxR+~ejlBY95#hbH-L=<1NN@zW+=cKV9e{ga8U`PIu zUYGS!wFugm9YglFwY}iofhEBpGXfTiJxC(!rhk(pg`c5r^)oyXb|gh(J%)Rk-ZZbT zSe_uFI1GpxlK&(9{qL%HOA6rpvZrWYRmxl2O4-`k#jFYK2L-&*dMy2=ysjG%`VGdO z%KdRvf8O46Y|{y+YQ8oLNj%CHF;MsSQpfQGANbsY-{sllHS~BGZyh13X*OEmtxi@u zX=8a=P0h|iym0#3-IdRLF`qpG`#uGfiNzOBze`?sa$8m=LoJ8RgAl1K1g_N&p(TY0 zGQI9=P1BBl!u|a7xY7UyTHP*-34Vk1(_VycAw);{jaK^u#0tqg@VHB7^;GS^pxUt6 zS`ZlKQsYJv0ln3jwQE4IDu0_T06(c-sFf3@Y!I9EVACTR!PsO2_V@gA3&v09LEhJM z5JbDpXSa)s`q=yDI$#BBQnwzJnc25;xzWY_-ndg77KjuA-^AAmB(R`;N*?9Ay)C0Nk}a*z0!BH_?egQ`~q`= zkZmCGvjO{M2{5$ZQ%%BxY(rbH_KDSc_KO4q4_XuGA3I-%B!63!kJtG0*0Gdjjg@b= zp%N%ndyTTMTXyez9UcdCI6`Z2@0<;V4c;br?Um^|c{Q1B0m?5+Ow4?-l=!b{2o>0S zZt+`hwfXv@3D=tDA}Ai5z>0m=Cy;6?cmqV=R|~|?AzIWe9;W-SeMe*fCfQ%N*^M-j;~lM<2o;c{Oowq zwbsYFhdP-4tp0|C^huMltAzE%9pg!{55{H|T)XQv@prdU>M>9zk%5j?pPgwA9N@#f z8t?#7Y!rf5`idEuF-wsxeM@Y$-POSlv^2A--LsyA*Ob?hexex~{?=%vQmBj*ttr-bGz}t!J2lJ7ym3&2 zXfo`#8;Yi$2S76#W4e&(qN68EyJmG#^qp2J8B*v@Cu)F-$oQKa@gcsU>l5uV$-hUMHKAih5odUw|%MN8@vE1%lQkhwp320yTP^&Kt1 zuQ0_T2*@ZbfiXP6aQU3U$DOhqA~91us^xQji9E`AIc}LCw&{SeB*FIBuh{M6CD6li zJG?%Cb_og2DW)*~SVWF8QwaHTlJ89Nbe=>D7_@gdln)pAw{#AewRu>aQ~XxH=PS|U_GQ(J1WgpIE8eAQ8aW0O4c=Xn(YYIQpQCk+%I9^i zoDLt5l)?9GS{GCcd{)k%Q!PMSf`O%K8Z7+cP-ecnX5CIk5YOFF>)hc0BY9f+_5s1{ zdcOVotS7#)#&`4=NhFJlA)GEoc-PB_P{yVzh`gYvB+Pb>ilzn}!U-Q&JCL{;g!1T(m1WWb%e8k$Y%}P8n(!h)mgI_3`e(1<@w6mH*kOH{pD{OUG zrjfZSm82L5wgW`mc-rPH-~6h%u;$W*K-DEz-U2v*5c91s~(c0n`kIx%u}Pk714;+ifT~%2w~xG0^@B+* zUoRWrqGBxVO#Z~3f4OioH5Y6qWwEK>m#^*%^RAY`sZT#BAC+zb;lAuskELJloPQn? z!peKyysSbRpukvVm#Yia0FGOZ5P2%Zb@kVyzz@kbYy=Dn66pMlX z%9FGfBO{@v4py)l6(w#Q8Y#9>_WSm#L#Ckyqihidk@Zp6^dKgBw)a_t&aR{6kkPr; z;4EYWg3dpL!E<)e{EJh_@}mAd&~Ea8{Ar-!eZg2h#rSy;~K4G-Jk z?}J4*?5=V6;khk1XX3#n?-!aAIjh)^2E2X9jf2XB;%RkOm$2KVI&!M*=euI+M{vtw zy5mQB^m90dAq&`3?{N``K^S)+*Rt4BVIb$a1eEm?C~Z2i=MT5rY~y)&FihUogJ^t?%r&`A!+5wY}zy{MP889kZ4A&{ipW2%k?jh%~S zN(x3VK-|f-9q8)a(OvZ)s6CS@yQ=_FJXy+663Xx+^a$(--V0=Ov}!Gsds&CWJK0}k ztH?0`VI0V->Q3V>Y>cW@D~BE0aaZg?8>A)9Ai~cnf!s9>Gd7S}n7B4&ZGm%GSWpR~ z)xa~1h0B1FiKXLrHH2T?($4#O0l(z>>kS@%U>rwpQs<72>RzM?J=67BAv;kpcin1Z zxW)#z#1@PlDW-Qun`GIVNvTfyipUMmE2WBx;cA)^wku?#Au+Ri&CiQ4%c(5*(}dTT z;PF5*$ftxP6Fle8WQ@zA2p7d!x^`?}RZe50xME!ZjByy9l}P3)sKScCURT3zTI%&eNNmQbhkw^e7)dzBJ0w(^hFY~RIVUh)3_l8i%t~$ntfx$FLMQt|oP5w(y z&-A6SVp9!Esq9&kuR++`!9EK#lSM6M&=-k9KHNz~+kxdO8hW{QISBOtvBB~Sro@of zn{`D(>2ahP^WH-O`Yr46T$nkvwTYhv8O{sXYc&^uT7Zr6h`iG;7SZGDpadU!;XJXh zn|lZLcG1G7nU_3h&hd10RH=%`iygD>FOBKR6?yV6L>QW2BYdX^{6DOHX*`te8+M5(6;kvN>M4=6QYm{WDqAA^R@UsY3=cbNLIgE}9io78{3#M=WV*V0lp~-BlfjniK;fZ6o)cI-q+2+al zeXF#E$DCu$4fFzURr)|Qtvw<5NB+N5-`k4yv`eV3G8r}=ccNvL6I$tX0`a7{#(^SC z0@T(4rb=FIp`|~UK4cVs|1%D*n$5=)5}O2P6pGYMf-M9$lFLVvNQ;kcjl~Lbj=(LxQRS8QbEzl~#B;iWy5#P{YU^Tr2HmOj7Tv$TYLy>G zuz98?#x()7nH`*eCYd?32|Bb3+F2NXjPtpo{Nj5L-yVaJ%4K2$4!9BZrCz0(<&c6l zb@`sTN1BKS9zEK)!*X+rC@c;SW8L8KZzplp$_;l1mshu9ET6_Tr1rJ*tSTb$`*2Xc z#vSy4AFz`exJYCoWTLfV*}-f0s_Nw`7q5IpPuiQ{b?B-Z`z$&9ARqPad8RT!L>c&K zo~nZBMIX&=92^2MNBh0_o7AC{$89&#Z-LHib|?y~dsgtFuYk}i;)M0cVKU85vunvQ zrR0Q+qKhSi`WcY+{GLIKc0&zaSCdG;95jRG8>!67aGOKlWLNDVQ1Y0GyvgiBdF37(|4VZ(wAI@Q7RMv-=}!Pcn|z z2%Q~Bgxym+M(=ubG+K#exRv(Ty0|N@9C@<^|AzfwR`2X!Nl*%t&L@V22y=Im*W_bp0WkYB>jd=DOES3|Ml2@*{^!JPo=@k3~Y~^aj z*hnO731@exz|1mo4)1-dDDa`*bD_YGko>D2d^48rShwZ;F9FDv9ZK&u!w)XWnwSDl4NI}w$H!O+F*UmW zpG8#VdHL~cH_wFji=Y(pl^lbM0s%ic7iE4p(+bu5%uf-^FBYNy0V2|#yTfZW6}ufD z>T_Lq`jY9tix2+`bU(xk&U`xHdH!h}ORGkoeoV*JdBoZcdC>p~2-b@A!gzx*^O z`?3_@5~>Ccnwl)_HFfGb^8D5jUv6rDl;Uxf6;y+$;@xvlu>+JKv3Xo&_dQ3U&cw#H zGi{+u1hh91H{X7#%H~dz+sM}9xBqg!BON*511q>S$zyUEoYZ>MF`y_@Psbj6ak(l1 z9c@(1liW7_2%(4_yg=q9Us=`c^7=Esu%`?ijK^+XJM$E;M2lO@zy!M%1@=1k)uDQ< z`H9%Xb_Y0JgKtw$t{!Pk#Hkd6Vh~}G|Nf2vQ`0q0T>=n`cjcp{@A~ctGvuIXTuThP zH0vOc$jlk}T`O=USRVE}MfmMTe~UO~+dZQh*`qAvu+BL`U0Yr2$csLK`f9FsQA#FY zdB~0=0YtsCP9No=8D||n#3tL{+C5>Ne=|oyy+gtV_*{BG!z6KR&g)N#gv-e)n{f?eQJN{%POs5@secMbc&exQX59i9 zQAHm$j()1@Z(CvGQGP27IM9APG;=auQ5seV!=rD49^hMf`{vz`GVvtUZ1yvF&&4d% zauILBYh3Akn^Mb;spN~x`;w^Y8^U?m+_h0=)vW1?)^3dh{8`P|7#{X_Nm7 zJx{56FweP@uVkUfItlY+nOyk)d_MRk7DuhXZ&wGDh<%BWg$2imH)Frab2U}t>3f8O zQ@PRdhfx(7=mN53EIlIyP<6w8=<{AR`T znY0$13pTM>HuhpFHko>iIKZ8eXBn4k)h91MYd~eAlChP831RNY#2?zR`Vv<&1JAr4 ziDBk@_yHCm7w7?qf8>-iyhs12q_Kp)jB%h!oA^%hFWt}>HKn+6W^oJGGfe6$$35%t zN__Z``OuWf)iX1QDIpOtq0+SYEamnrJ?UJloWTg)3?#m8FrhWA77fc%YmYa1%p3kw zz_s_kugO#eECsY6VprL4oSHQY0KU21xwO-=FguG@Sz;F4v-e>vl2qvdJ&JV+ra|I(ET+EOaMSalsE%oK~OCIC5#FV zpCfbfo#1P#W6J7*0ev+j{Uaauuxb=`&u4y&lZ+FOmsA$Yz&c+=VQ23V?&?%*%$bii zlT{OaQ2*042JAR3d}8CTZuNv*rcs9d{rt`g$N~Q8?5?^cS);EDOw!Ug zkk+8yXTqyvc1PUJ7!+k2v|X-Lcf@5C#{)?o%u!xF1XMop`nf+xccHQ5O*pK&v~72uqrfZ0^XFc z@`7-t$O=BjLh75Bz&faZs>S`@SUz$gS6hiuRWKIJ5jSH|5sTD5}fj_DaHzLtI~@>Y4}+lt6~ z?G`^2CGUl1;IdOeAyVxmr-5t(c&I;r7WMuJ#nr0qOVET=k31f940gIk7=0uY z8juZ7+=G3p5M=6d4&L{&>xDF=ZhbI6(q@#kjasYRIs1ObNHI66YvP7z(?ruhDUpO} z>hi>7Ig<-dJN}nO;J=SGSZS2n1pFhFsgv`r@&a;l$9z|Q7X(S0+Qv!6Dmc&iS>Omr z(uU*9H`p6xsvNFfbl?mF<|w{yX+J2TE_VJxw8>*%|%;Y07Wx+Ppz?qXSs8paNXU-Z@vzWwt!yv z3{98%i0BMkEC=kUN_p>nM%pP8_ zvqUE%dV8Ul_{|>G^_odT$*=hCh?)PX{{3G|h4t#7UQ|j!m`9+hId@Ks%x>HKsl@Vf zyV5-!3%|Y!+_#p$J-Olk7J-N@zMk>y4(<;9ir>!p7p{~#*PCIqg890esW;6E?6$f; zY9OQu(dUpCE$WW-aJHo=bifquVWhrKQ)oPW)dh}3NmO@&<; zoOrT6sLgXGct+?2#+|b(o-~id4MCPoM5jc{(gI@M=YSj;o-d0T+B1FjhJrs(0uuJZp9jIX*Z6 zTV6A+Po=FEHqI`XX2$1Zm)%A{-%%{)SkQ|vL585d10BOHitVh-Jh^RfYsUKJv)m>p zei!&>E-rKlc~{xs#dp3hKrJ_}eSsiPTs8k!b}no@P)1o?SbS$L0^p#Wo*8d^a;c0d zTA8zzOu;vQ$a7fDU-0i=y?eX4>e{>K!SDWIGWekYSS1=c>^HduSL#W80*!pg7+VR* zQ1+y={gK6TX35e2;(S=iA86W?3-TlCQ z;bZldj&FD6cM&%RaQ<-d&2+QyTlapPLXlmG(dh{njpaO; zeCQKJ>z}^_gU-64yuNFe_RD=UW#h|SVyfN00klGZC7YPmw@;Lm$Y_|YR*@EFs^3~@ z3iJIo!^}mrbP&;sf?W40XR)-_YlBX->*x?`vdYcr zT+oDGJUE@x_fZV;2i*TN?uvPCwH*|NZ@I5t<>FF80ZBbBgrW!`)~u$<`@%0`Zi;K< zRncYjOj^7Lm}*7)co@{^@nZ(wlVJxt^oz)vu(xf=0jCtWu0;=Rk@bH2Res}UjfL#C z;uhBYtuI-TG~r>&kMaHDsc>B+*PQ%WrX%5X)Zk!k`52Ti|W#MIk8!SD+$rIZWG+QRksrbkKK zD>Vzhn^n@j=wRy*xbhF$c(c`ZaiPKL+KqDBkHDaLtk{XXtby@eK`SnLwF_LXT7HYs zmA2|hpZsnY1ycBC#lWapzoLD($al&1hdH_SrwD7ZLzq&W_WWEEm*mU(>Y9y%f}3%_ zXhnURtNBlHW}&n{^D&}ey)Sk#UBp#q_7D0xR&2||9aIz!Td5oydygFqV)Rk?5`G!D$DR#55%`aoLf$9J--?k8Ku)v7fVudaaEztq(IO@7BGhdiv*D7 z%WZOZJm3rjHo95G_L9x(T_xtNQW1St4jEPzW3QSg#Er9|LT^vYDc`qL zz-*CxPbz*<(;s5`Z+qTn{q4n`Q3!=il69XQ;~Jzu&ZIjquExx_JFA0(?q?kwZn-P+ zlrB~K2q!UVbl(Y73Zap<8!TEtQuG0X>ZBTR&A#1WzT5et{D$IVeWbM%X~oeUNLO9m zpt_55V|yF&b*+p)H}d%7t(6Z%)1)jpRv)Wg5Zz~KDlA|l%DEI@q(+C=&(FJ#C$}%F zM$%X%dRso7t#s_i-)mv6GS>qkd18GeDKL$V@j-xpHqe_@cS;-(JXrS({2BFR`U+?3 z8ns>Jc~G=gy9K{vYUK$=?$G>}CMX;sJ=_)iy82iBCbMs^C(N>fJd%rbwzq6IS09EY zaS|z_yT*cyAxw#Ad%zWhm=w+KKa>VqX3UU{DnXo~m&f5HSVWw!62UGxTJzr7VXQwz zjHnaxp0n-3ne)jZh78OeRUMjIrTS^XS*+aichl1G(tU>SO(d?Cnhg`9QKlJ$O7ntF zOmz@^h9+so6jn*l1`n?>h~SQHVFMfJ`LuO-OUx$^D2rgsax@Fq?#6c*NiMrYr{I5i zq3`Wz*it9kbx-UcFg`Ldb^jMn>6Q64^zv!WrG7H%iR{%wNmfYQ=Y?FRzT5ee;Ja8> zv-xZH&*lo36`7BY{Ot@Qn^6wAF8u6rFpP9lX`{FVdu0n0hx_apjzCnGr^18E7?lmBmHisx>W60_a*FJv!TyaE9ynvM{jyVmW0I^vw)O;C7OIw=; zE}&~#&j!-aSZVHoD~0sbAA~KN1W^k4V}xC(-$`o&C_UhIENtZYZDUTH|FWev8WGh( zR~KXy84j!$=g+FHXhn4Fg!(CoytBQyo2j#z0yHes+%Va}dRJy3kv%99#|FAy!TjQB zAQLx?X%IR2?@eyys`se8+*hNM;qpH-1&7ja3;bXxzob)@$47ZC(&Tq51Pts=;x5l% z>Si5zt^u@-NQ4B#wA<_Iu)P-uaHtXamL1N;#aO5F8nOe>E#Pr~G*M>gP>yptD6Sr^e}&+VgV}w~`chSEuSI%~&^vhcyWPw(0wbWrnB76ea2ITY+e;kD zJwNcUS#gywyzZ-_f7H+#O2j=JQ$`INZ>adi)Xg!7ZW@bfi2Dhds>OHUSk%Itu_o_6 zu^h4b8QKamT8DAJmTr0o6Ix;I zC@i6aEZD!dDelJ0<~T2O3Y4k3@D*d%`t<>~8%H8RC_6EC?PN(`fx|g2U|FKd2ySd{ zu_i&Oe+1bE>u*~L!msmxCD)FF5EwG zaZiBEwkk5qwBG~pu{$tZ1GJz1biK=kS7fw4mh{A(BiFS#h|Qb`?-e~a^X{PFF1UwE zrF8zrEN^4ml57a+>Doj2suQN@9S7eegQk)B4&+~Rdx64KzY!v%`G3v0Y>~j=*_lXy zfSw+Ml!ddh6q^R-ot8^}+jacaR|A;ohDUAv?>?fKlvz?kew1XpX)16N#2)qh?K2P^ zwD*nH)wtMmxq`GW&e)5Iy4>aqcot%tJHBl+TfYTOYslD7HFHbGGYdOIbJHD#qlDAF z*7@Zd(5izkR<8X?96YX;#^VK2E5;h_;)DMu!WlSSU;%Pe-=azL*!St|8%#VD?FvvH zy@#=kzy^P=6g-#MOcR#*kyfpVS-mnstN!%i7++ zhI9}457*bk^cskx)qrMMXX(}pTcV8E2}dn`p4xK)%~0TOa5_^=Jfn);$q@lp0|*-c z;yv!8yOBXKm!R1wu~>Br@9$w=E$>K-DC_}usUq10&9Kk>Gr8rxK8U{%A*+tXxFZB;%5NE*Hf92wI5Xf0FUH}W6l})Ro<`aSzE7#cc9W& zL1ulkrcwT1RTIS8O13SPThFgCIt3DxgYC zKE)9crY1Zu6qjFYd%5Cx|L-m$*b-Xy!r0dVpACIRCc?3ZRFXQ^aAU)Cq^&)Kd z<;iAR;q*UQWYxPO1p|;vPb-H%$rN*5k_O8bnkz_5>W4K9@LyFZ#3A(zG^%V|{pzOw zskreutHK}tj^EB}(1bFu#%V#TYxN@P`5i}1JBAS7Us!rRTTOaqx~(+E_Z4VtT4$^m za6ZqT@Ay5OrPr)451DCR(7uD22WE2`yf%LIL19+DOE$l}XjcdB1tGr{|5F^cqx7K@ z`gccJ&9eekKVOT0wv9_swymt3mGlpqqn|S%qP%6_xpc$SjbE{68~@}XD_z}agytGF zHuoU_)oNLWZ#cCCnaD;!2n`408&Mb9{EmJU2V&ujRDuFaW6#y{g&*>b48XG@k~8HWsYh&Pjm8wubO8dwR$sy6?6763+$lepNg&Wo^Uc z`XDLBLf=u-;jrosHU~#kXF}uU%=&7UfUnYAsD+D+6uaz6{5$e@h%%2Q`gLCww7s48 zvNHdlv47KUBb($;%VY-0LXv9*m|{|O{!q634=h>5^2-|O5NqN2 z+Zfqm?$RDLxU8#E{iw3rj{(~}BB@|YPN}%P&tvYZ-OA8Ma&!GynpJt+SE5Y^m z9#Va0uy|@kvhLa9kKaBvY!SQT!+*wGCAK#U?v>P7@(I+)(7DnwzwWtxX0pUr$#j(u z0=OrX_?DXP(l^gx+3l|tnE6QHA>ae1be@T?KEs{Rs$dAj+~#lZwP3ZCiXNqAkxJuz zb0Aunigh>?bhE#K{GQ*zRkCUmE~D&inH0SAhLNW9{`Eo1Ab)dT1dP=??%o^co~chC z8E#}*48@~D1v8mJp5OpuBrs!7a9!y^*qXuD~=i0rl!I+n`gmHsLb*0l%qR!mSvT` zqTJfs+D@R_?@JrX#24p(?VSh=nqrOzfdYVGPv@|FP~FU?u#lVM>QZKDAD;s~J=aFL z`}89joEGH*Op8k26eBuaA!7@vmGS{ITbTUUfg9ReYwP4yUe4d&3Y+NJiS6A=P8Ela zftrw5=ic`ja$obPGil?ddQYs&+$2*k|6kR;X#dw8t{_C(`%c~wIZxdA1~MZZ*A>dK z{M<%3O}PF7@eSp!qnz6|FWBd%J4u$Ui`@R-vMjv@TceMoS_*_9*Mg=Y(TO?nbQPoQ z(ytF!!<8TZ)nq-7T0vR6TS>%iSURXMlU^REG5n2>5^nGu5`{BtjbQ}s_2vxVIuGeg zM?PQSY4zRKnuG$w6)OS?$<$u;#p!m3m_AFGzp~0Iffw|axCXHi28)iVcs5Vk@o%<{ zS^ovw7;bP73Y7FdN=ZMwAoU!d@-ct1PO8Ja^&cijF;#alO8z_ycGn)ABV38+1@Aq4 zr>;2PvD?w|(y|PC!pAa@4C!$=#zp?HkPqDo=kL$_5zDJsAgWc$Dn4_FMwzvC`b^Gf zG;VX~#mu2Z*y1JOmeiN}IeM+WH<)A(UojTk+@mh&+GH~F;8#?W?kITQ0g$H4g%?)l zrs@NpDx{OPIEisb%uHHjVYbl?Kp-yiu1(eqk7gIBycoZW*%&geg6Mp%VDu1m1aGJHZ~V-NS@VongQzXXnXI3-Py`vU05W%MMS^HQ0(FWO|9 zfvY>5RfGQ5;)eNhjz8s4!(lVMUq}2!6$Ib;jJsdRO`k>f)dkSUT<#h|>n1TM;5x1_ zWygCR2E3TFFJ^gc%@rHQMEo&H0j9j>QYypfPCqw_W7t-514P$~K*Dlbkiy)ci5 z$h0daFbT&H0FEgtw12tIvFGZM+Z{gQPqYkudcL>>JXhc{l(lokDG0>}(Y}Yi93T$; zNfM-1`$AOyvR)(fRmptHHhQMjrT`oj6Hlx9BD=;9U@!>J)JiRnQX}gnS;a$aoQ_O+ zFC$_A)Dc(S>v;6kaX7H%0bX%m)<)N>JVO_KiAXEs4&998P$)Zbv;4QW<04^87-l3u z)vY=Wbd$&nqZL%vFo#776yATDKV5%x1T-7(BFRwn(m0s`Ib)!cR541gH<@Gp7#Q_| zzzQhYDLi{C>xam-|6DpMF6=?j2J)=8$KIf-<@|D4zw?Sa3h?+Z){2DZYaK*khDmws zq$jB#SsPD;`Qe&{Zq3$0L&nZ|wf)s_^)v;2I3BnQ(gsaTrWVqRfIQCVs&|G`n6gK4 z90v4Mf5XG-M;u`Okw9W1U-3S4fTuJUIcnqULv%njm%E3DDrbzSD_ghTmK`&s86MB*{y^l4Jn=x&1rUaUBame76tyq&$pLBVwuMPpS zRaC8@=HD(G|J%Iis>D|H0|}}9-W^?+C7BwoenXbN`6^oB^elZmN!&1_p7$F+@(uNB z8N~1PBd7~0%YHGP-In^!>mn1R&V%Yp009LnacDoSwwP+HZz)2%;gXTJ@~+|{@0Uj^ zaSt?AgiN--OI*G&59YIVSa-9ZK07AN9qT zp<*Wo7N38K@JxcYcNEaKzG*mFvE$2@zz3pHv-w*}Ejf)EO+U9gd9srKb>J8#V(EKB ztD;BJiIz}kF=RT^k02e_EL$bUBp`pm2VVC~!4B!${-fu)$RHbO-UD-!z_eh5&v8fQMgm^| zeP{*IJK?){LDvQ_<7?9T#tzM5BXKULJhx0CFeCmXFFRKSY9VL|4STVY%|{?Kb4C)n0*+uZ1hioFtcl1$W~CU`&jVYb&hOb zbGgou^~+b>dbg+6c`FVV&QwrhRdbLj_f0I8CQBK#%`8iCXYeRSlOjTQIiSGFMULO6 zc;0BUo*222{=~1!FX!NbpNkU-dNK(z(~4r2vuV}@$#`8!QcU!@_7N_`#JduGfIsfY zWWJ(bBp}eF!UWptUi}N>z54zWo%%{E3F5hpEXefPk={SLb{1V4g!YSUV8DG?9-k4HTZ0j#9W&v|RdWL{JWovM&e>JvW!;zj9JvY4nkQ`(FB?c2{eE zNUyTI$rkFyUV6vFH~k5*91Rn}HW1tPy}ocX<#ukekAq<}J(6eVAwp+`X(kekC?uR< z&rE@BzliNKr=r}pe}{9 zZ{h{!MrBh?L}Yy=8VM!8yZCZbE+bB!qHOA=dDG)<{rZ>H6>S!uAG4iMMfp3C;?=)b zQUY*n!KXFBF#r3aSj9W1+udcEdbIBu4#N$xYO0u#O;5!tpH@1U-^EK&5>TyFeq-v` zOO2e-JxyyarmK%EjTANpx&Q3jivyIm(<4>^Ma1$CIzKi;*yN@9#Ah5R9P-%&CZo2DoQUNlX4{2acbcSj1g*0=mKCshKZ0Fd}9JI*u*QuFB> zucwvBV=06mUt4C^TI9s)si?k{voKh!%2L!`uc4vaI|`7VC9Dn^FyRG}5rIPLOr5I9 zR;28$!$Yt5S>)!UV`*$0$Ck>3m@?3PJOV0KwnbBIn|02!#rbHg==@M3EH5?> zxxAcOpYIWpP2}n(`yJ+!-<)3?NMZ}72>rIJ{jl-GP+qGl!3lEFSEi~kk<)uHw)1dz z{ok?kFPv!nrHMO`g`coA?iKveF(y&kR`}!u2{6~i!?y7NkSPmIEQ+X_cg9{5slFwj zM&y3z6N0|qUK~tK=GHrx5t#4w`7h1-58ctT0aang#GkVTC~}PmV^eX+@h6f(ey+J| zk$CeZZ>fpxj|WSU7Fl+dt&&;+%21{R3XbDoe~}Jr)*kqTREKNJ738qxisZ|mlas)t zL~*x)6#m?d!n{rVZv}7s4bHT^Af2ZqSdVwIeF>M-1Mgl6Ci- zAV)3zwfXJg=Mvm!A8#&ZFv9~ihIeim_C(>xM`!qTodq{ty^K4hF?w@YGHF_Ak$M~d zJx4HU_B>w(Ib>{Hlvp<9hB{yGL7mTkk?evf<(*d9D4kYat{xJ)Fn+yNStK%RC8tGC zmAZXb-U_>D#QgP|%2uE3@|(o3SKOHDs!d?@;nx~p0`;D-QDB{R1Rg+CV zJ19KyzEM=bNw@u_%C>=IRo-Q5N^+qkDd<+D>ctS9Akz4Rhk`BEP@2}QS{35zIP71- zg67|Mb?ICDv5JLGccoFRg9k;s=MzzQF@Jr?-H^9K(4aN5*LmI9s+5^xWjl)&gKoNR zw;jI3;nS6cZWPd7$m_c+_MgK|Du=5U3o0#}gz{65N*d>v5<5`1{3rHYW-PDjky z1{-*K&hvk4+7ZZ3h!_vUSWd{eEC*6Yl}|6z^a@(<{H1JvDh&EVeg96p@x~rn(BaLC zGt8kyaUdOdED{VgS&gJlaio>XZXH+;prvwVt^$1%ma?nG=rY55f5 zbX-W>y6Ftp^-QCjdzoPR^^NqFpg^(z)ad>DhYx`9^vI6>oQo`y+}AQzWg@(s3+|z3 zN^oZ{U=PBnV6N7jPc-41sS(839*uGis*~82s8vq)`M2QwGfALNG4RtO=g*bbl zaZf+j;?Af(yq>bqciD#wlMH-9!-U4$qS!@C2A^Nf5j%f!DGmd7gVzpv)^rTO(fK~#)Q`k-%q_jDRF?N#-b zs(_KFc+j0sW*ZC^hlyqwf4!DUdbdlk>tvgYwA2+0-n=OP^Lw0gX~nn#=O0Fp|A1?k zfDbj1uGq63UdREmGU#MHHauK{^M3iT+GlBV?1j>gi5G)WcM(l&@mHvi^{#*(hP#ST zDTBAFkTZKpG*f|9;gBzLXQEApC*quQ!RqzDdMD?HQW6WhV|GVrP!L|v>|F7lfs4GrH=e>_9hS8APko%!AX+Pr!E`u=1;g*D}TgbhWeMmP&Bk#gUuQCxipo84zg)FlC71sdW*KJDh&0(9^uyZzdu^be{1f{}+d zJ9aRc{sO?sn!WoKL_fO1jUQ$XX<(^ZZQl1rzRDQb82e(+fHKkJ7x9H2eEmmB{WIh@xw_i8t3-!aF%%)G_737}}I zt2DOcIrO^vNRJ{1Ie#Uqx9c9-U?Qb&m`LW|8ylYzHcr5poSb-rF&_M+=q$f!VVF%i zr;6Icz8XacAAeWCSJ6AWk$0NHi{Xj$+3#bT`k79Dcn@2kav~v~6#LeLsX%g(G1wAx zA-y^X~@118{*W zEz#u)v%3`wp0gfs2KmnuWN{ai)sh8C`298h6Hk-uQX@fUH+_Cm%yGUbk*CM^-shXhvM3Ro1$SA0$aGm{Lsmry3su2P zPU&t|n<@9f=8I=)r(TD}r)^{A@`BUHf?0{l@2iS;jo)O<=tg!2G&D)Z-8h@Uw#GMU z71bt0*Q?#kI*eNxj<-N<9Mi|H(=L-rj9QO#!OjJ0uIBZ6y;jHwSXemiOL}tVcI;i| z(g*;q0BDmocwmS~lF(-N&hBr{=#X>ubybFO|Lm$D_g1#PikMc`j*U1yU>tv=2g zczyqZ-<)*x`8@LB<}WWA5Z+{K-O<3l$(F3aOxU;nw3ra`90(dZP7EFv%+~J zIQ41$1X0=vGQ?DK4EK?bCE$T{X@It$AABre;&_ZZSqEcKAfJYFdO0L@uf1WjA11$n z^?N5I!A}g^fQICEC`T2Q(_RF32CwBHM$SN>xBJ|}d{x)=Y=7+7ro>DV0D-y*=s4!} z=KXSTo?Rt;lL$N!1!Yhu^;vc)i3tuOEzkoa=Pbj&vdixp%8<+LRC|w^YKdFZK4i z8DCwfPqqG>H~~$E{K&r+vW8wdM|&KVBPbg+7)=Yzf^AcqQDxo6)EyNC1ibX(giCU@ zI4dx~0I*Y+_u);qex8wLp_>eO8e-

Vn~=x+}($3QDFoG((O_wE9X0qt6ma`|ydn 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 diff --git a/simulator-samples/pom.xml b/simulator-samples/pom.xml index 2e2dad316..ac53a6c0c 100644 --- a/simulator-samples/pom.xml +++ b/simulator-samples/pom.xml @@ -1,7 +1,7 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 @@ -41,7 +41,7 @@ org.springframework.boot spring-boot-maven-plugin - ${spring.boot.version} + ${spring-boot.version} diff --git a/simulator-samples/sample-bank-service/pom.xml b/simulator-samples/sample-bank-service/pom.xml index 6bdfdf2da..2cabaf918 100644 --- a/simulator-samples/sample-bank-service/pom.xml +++ b/simulator-samples/sample-bank-service/pom.xml @@ -19,7 +19,7 @@ org.springframework.boot spring-boot-dependencies - ${spring.boot.version} + ${spring-boot.version} pom import diff --git a/simulator-samples/sample-combined/pom.xml b/simulator-samples/sample-combined/pom.xml index da53ad3b0..0efcb2556 100644 --- a/simulator-samples/sample-combined/pom.xml +++ b/simulator-samples/sample-combined/pom.xml @@ -23,7 +23,7 @@ org.springframework.boot spring-boot-dependencies - ${spring.boot.version} + ${spring-boot.version} pom import diff --git a/simulator-samples/sample-jms-fax/pom.xml b/simulator-samples/sample-jms-fax/pom.xml index ce9fb4b17..d4ce74947 100644 --- a/simulator-samples/sample-jms-fax/pom.xml +++ b/simulator-samples/sample-jms-fax/pom.xml @@ -27,7 +27,7 @@ org.springframework.boot spring-boot-dependencies - ${spring.boot.version} + ${spring-boot.version} pom import diff --git a/simulator-samples/sample-jms/pom.xml b/simulator-samples/sample-jms/pom.xml index 51e27bf78..b06350c44 100644 --- a/simulator-samples/sample-jms/pom.xml +++ b/simulator-samples/sample-jms/pom.xml @@ -23,7 +23,7 @@ org.springframework.boot spring-boot-dependencies - ${spring.boot.version} + ${spring-boot.version} pom import diff --git a/simulator-samples/sample-mail/pom.xml b/simulator-samples/sample-mail/pom.xml index d8eff0d73..ba094d6a0 100644 --- a/simulator-samples/sample-mail/pom.xml +++ b/simulator-samples/sample-mail/pom.xml @@ -19,7 +19,7 @@ org.springframework.boot spring-boot-dependencies - ${spring.boot.version} + ${spring-boot.version} pom import diff --git a/simulator-samples/sample-rest/pom.xml b/simulator-samples/sample-rest/pom.xml index 0ce4643ab..b0249f2fe 100644 --- a/simulator-samples/sample-rest/pom.xml +++ b/simulator-samples/sample-rest/pom.xml @@ -19,7 +19,7 @@ org.springframework.boot spring-boot-dependencies - ${spring.boot.version} + ${spring-boot.version} pom import @@ -49,12 +49,6 @@ testng test - - - - org.springframework.boot - spring-boot-devtools - diff --git a/simulator-samples/sample-swagger/pom.xml b/simulator-samples/sample-swagger/pom.xml index 5ed14d304..1751205c0 100644 --- a/simulator-samples/sample-swagger/pom.xml +++ b/simulator-samples/sample-swagger/pom.xml @@ -19,7 +19,7 @@ org.springframework.boot spring-boot-dependencies - ${spring.boot.version} + ${spring-boot.version} pom import diff --git a/simulator-samples/sample-swagger/src/test/java/org/citrusframework/simulator/SimulatorSwaggerIT.java b/simulator-samples/sample-swagger/src/test/java/org/citrusframework/simulator/SimulatorSwaggerIT.java index b5db72f08..fcbbeb190 100644 --- a/simulator-samples/sample-swagger/src/test/java/org/citrusframework/simulator/SimulatorSwaggerIT.java +++ b/simulator-samples/sample-swagger/src/test/java/org/citrusframework/simulator/SimulatorSwaggerIT.java @@ -74,7 +74,8 @@ public void testUiInfo() { "{" + "\"name\":\"REST Petstore Simulator\"," + "\"version\":\"@ignore@\"" + - "}" + + "}," + + "\"activeProfiles\": []" + "}")); } diff --git a/simulator-samples/sample-ws-client/pom.xml b/simulator-samples/sample-ws-client/pom.xml index 57af62783..96efab4d8 100644 --- a/simulator-samples/sample-ws-client/pom.xml +++ b/simulator-samples/sample-ws-client/pom.xml @@ -19,7 +19,7 @@ org.springframework.boot spring-boot-dependencies - ${spring.boot.version} + ${spring-boot.version} pom import diff --git a/simulator-samples/sample-ws/pom.xml b/simulator-samples/sample-ws/pom.xml index 999230bda..b752eac5a 100644 --- a/simulator-samples/sample-ws/pom.xml +++ b/simulator-samples/sample-ws/pom.xml @@ -19,7 +19,7 @@ org.springframework.boot spring-boot-dependencies - ${spring.boot.version} + ${spring-boot.version} pom import diff --git a/simulator-samples/sample-wsdl/pom.xml b/simulator-samples/sample-wsdl/pom.xml index 297e0cd1d..6e139633c 100644 --- a/simulator-samples/sample-wsdl/pom.xml +++ b/simulator-samples/sample-wsdl/pom.xml @@ -19,7 +19,7 @@ org.springframework.boot spring-boot-dependencies - ${spring.boot.version} + ${spring-boot.version} pom import diff --git a/simulator-samples/sample-wsdl/src/main/java/org/citrusframework/simulator/sample/Simulator.java b/simulator-samples/sample-wsdl/src/main/java/org/citrusframework/simulator/sample/Simulator.java index 38ab7992c..05fe874da 100644 --- a/simulator-samples/sample-wsdl/src/main/java/org/citrusframework/simulator/sample/Simulator.java +++ b/simulator-samples/sample-wsdl/src/main/java/org/citrusframework/simulator/sample/Simulator.java @@ -40,7 +40,7 @@ public static void main(String[] args) { @Override public String servletMapping(SimulatorWebServiceConfigurationProperties simulatorWebServiceConfiguration) { - return "/services/ws/HelloService/v1/*"; + return "/services/ws/HelloService/*"; } @Override diff --git a/simulator-samples/sample-wsdl/src/main/resources/application.properties b/simulator-samples/sample-wsdl/src/main/resources/application.properties index 83a11b72f..d1aaf978d 100644 --- a/simulator-samples/sample-wsdl/src/main/resources/application.properties +++ b/simulator-samples/sample-wsdl/src/main/resources/application.properties @@ -21,3 +21,7 @@ citrus.simulator.defaultScenario=Default # Should Citrus validate incoming messages on syntax and semantics citrus.simulator.templateValidation=true + + +logging.level.root=DEBUG +logging.level.web=TRACE diff --git a/simulator-samples/sample-wsdl/src/test/java/org/citrusframework/simulator/SimulatorWebServiceIT.java b/simulator-samples/sample-wsdl/src/test/java/org/citrusframework/simulator/SimulatorWebServiceWithWsdlIT.java similarity index 97% rename from simulator-samples/sample-wsdl/src/test/java/org/citrusframework/simulator/SimulatorWebServiceIT.java rename to simulator-samples/sample-wsdl/src/test/java/org/citrusframework/simulator/SimulatorWebServiceWithWsdlIT.java index 1240a4981..23eecf134 100644 --- a/simulator-samples/sample-wsdl/src/test/java/org/citrusframework/simulator/SimulatorWebServiceIT.java +++ b/simulator-samples/sample-wsdl/src/test/java/org/citrusframework/simulator/SimulatorWebServiceWithWsdlIT.java @@ -40,8 +40,8 @@ * @author Christoph Deppisch */ @Test -@ContextConfiguration(classes = SimulatorWebServiceIT.EndpointConfig.class) -public class SimulatorWebServiceIT extends TestNGCitrusSpringSupport { +@ContextConfiguration(classes = SimulatorWebServiceWithWsdlIT.EndpointConfig.class) +public class SimulatorWebServiceWithWsdlIT extends TestNGCitrusSpringSupport { @Autowired private WebServiceClient soapClient; diff --git a/simulator-starter/pom.xml b/simulator-starter/pom.xml index 4477337c9..a0e38c6d6 100644 --- a/simulator-starter/pom.xml +++ b/simulator-starter/pom.xml @@ -14,6 +14,11 @@ citrus-simulator-starter ${project.artifactId} + + + 6.2.7.Final + + @@ -36,10 +41,17 @@ org.springframework.boot spring-boot-starter-validation + + org.springframework.boot spring-boot-configuration-processor - true + provided + + + org.hibernate.orm + hibernate-jpamodelgen + provided @@ -48,6 +60,7 @@ h2 + jakarta.interceptor jakarta.interceptor-api @@ -57,6 +70,13 @@ jakarta.transaction-api + + + org.springdoc + springdoc-openapi-starter-common + 2.2.0 + + wsdl4j wsdl4j @@ -144,5 +164,61 @@ true + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${java.version} + ${java.version} + + + org.springframework.boot + spring-boot-configuration-processor + ${spring-boot.version} + + + org.hibernate.orm + hibernate-jpamodelgen + ${hibernate.version} + + + org.projectlombok + lombok + ${lombok.version} + + + + + + + + + + IDE + + + org.hibernate.orm + hibernate-jpamodelgen + + + + diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/SimulatorAutoConfiguration.java b/simulator-starter/src/main/java/org/citrusframework/simulator/SimulatorAutoConfiguration.java index e17918533..668f01190 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/SimulatorAutoConfiguration.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/SimulatorAutoConfiguration.java @@ -55,7 +55,9 @@ */ @Configuration @ComponentScan(basePackages = { + // TODO: Remove when scenario controller has been migrated "org.citrusframework.simulator.controller", + "org.citrusframework.simulator.web.rest", "org.citrusframework.simulator.listener", "org.citrusframework.simulator.service", "org.citrusframework.simulator.endpoint", @@ -163,7 +165,7 @@ public JsonPathMappingDataDictionary outboundJsonDataDictionary() { return outboundJsonDataDictionary; } - + @Bean public QueryFilterAdapterFactory queryFilterAdapterFactory(SimulatorConfigurationProperties cfg) { return new QueryFilterAdapterFactory(cfg); diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/controller/ActivityController.java b/simulator-starter/src/main/java/org/citrusframework/simulator/controller/ActivityController.java deleted file mode 100644 index 681244d72..000000000 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/controller/ActivityController.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2006-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.citrusframework.simulator.controller; - -import java.time.Instant; -import java.util.Collection; -import org.citrusframework.simulator.model.ScenarioExecution; -import org.citrusframework.simulator.model.ScenarioExecution.Status; -import org.citrusframework.simulator.model.ScenarioExecutionFilter; -import org.citrusframework.simulator.service.ActivityService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("api/activity") -public class ActivityController { - - @Autowired - private ActivityService activityService; - - @RequestMapping(method = RequestMethod.GET) - public Collection getScenarioExecutions( - @RequestParam(value = "fromDate", required = false) Instant fromDate, - @RequestParam(value = "toDate", required = false) Instant toDate, - @RequestParam(value = "page", required = false) Integer page, - @RequestParam(value = "size", required = false) Integer size - ) { - return activityService.getScenarioExecutionsByStartDate(fromDate, toDate, page, size); - } - - @RequestMapping(method = RequestMethod.DELETE) - public void clearExecutions() { - activityService.clearScenarioExecutions(); - } - - @RequestMapping(method = RequestMethod.GET, value = "/scenario/{name}") - public Collection getScenarioExecutionsByName(@PathVariable("name") String name) { - return activityService.getScenarioExecutionsByName(name); - } - - @RequestMapping(method = RequestMethod.GET, value = "/status/{status}") - public Collection getScenarioExecutionsByStatus(@PathVariable("status") String status) { - return activityService.getScenarioExecutionsByStatus(Status.valueOf(status)); - } - - @RequestMapping(method = RequestMethod.GET, value = "/{id}") - public ScenarioExecution getScenarioExecution(@PathVariable("id") Long id) { - return activityService.getScenarioExecutionById(id); - } - - @RequestMapping(method = RequestMethod.POST) - public Collection getScenarioExecutions(@RequestBody ScenarioExecutionFilter filter) { - return activityService.getScenarioExecutions(filter); - } - -} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/controller/MessageController.java b/simulator-starter/src/main/java/org/citrusframework/simulator/controller/MessageController.java deleted file mode 100644 index 8ea1b03cb..000000000 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/controller/MessageController.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2006-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.citrusframework.simulator.controller; - -import org.citrusframework.simulator.model.Message; -import org.citrusframework.simulator.model.MessageFilter; -import org.citrusframework.simulator.service.MessageService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; - -import java.util.Collection; - -@RestController -@RequestMapping("api/message") -public class MessageController { - - @Autowired - private MessageService messageService; - - @RequestMapping(method = RequestMethod.POST) - public Collection getMessages(@RequestBody MessageFilter filter) { - return messageService.getMessagesMatchingFilter(filter); - } - - @RequestMapping(method = RequestMethod.DELETE) - public void clearMessages() { - messageService.clearMessages(); - } - - @RequestMapping(method = RequestMethod.GET, value = "/{id}") - public Message getMessageById(@PathVariable("id") Long id) { - return messageService.getMessageById(id); - } - -} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/controller/SummaryController.java b/simulator-starter/src/main/java/org/citrusframework/simulator/controller/SummaryController.java deleted file mode 100644 index 930c7422b..000000000 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/controller/SummaryController.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2006-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.citrusframework.simulator.controller; - -import org.citrusframework.report.TestResults; -import org.citrusframework.simulator.listener.SimulatorStatusListener; -import org.citrusframework.simulator.model.TestResult; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("api/summary") -public class SummaryController { - - @Autowired - private SimulatorStatusListener statusListener; - - /** - * Get a summary of all tests results - * - * @return - */ - @RequestMapping(method = RequestMethod.GET, value = "/results") - public List getSummaryTestResults() { - return statusListener.getTestResultService(); - } - - /** - * Get a summary of all tests results - * - * @return - */ - @RequestMapping(method = RequestMethod.DELETE, value = "/results") - public TestResults clearSummaryTestResults() { - statusListener.clearResults(); - return new TestResults(); - } - - /** - * Get count of active scenarios - * - * @return - */ - @RequestMapping(method = RequestMethod.GET, value = "/active") - public Integer getSummaryActive() { - return statusListener.getCountActiveScenarios(); - } - -} - diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/http/SimulatorRestAutoConfiguration.java b/simulator-starter/src/main/java/org/citrusframework/simulator/http/SimulatorRestAutoConfiguration.java index b953e5018..e25c6d434 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/http/SimulatorRestAutoConfiguration.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/http/SimulatorRestAutoConfiguration.java @@ -96,6 +96,7 @@ public FilterRegistrationBean requestCachingFilter( urlMapping = urlMapping.substring(0, urlMapping.length() - 1); } filterRegistrationBean.setUrlPatterns(Collections.singleton(urlMapping)); + return filterRegistrationBean; } diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/listener/SimulatorStatusListener.java b/simulator-starter/src/main/java/org/citrusframework/simulator/listener/SimulatorStatusListener.java index cc4293805..1e55885f4 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/listener/SimulatorStatusListener.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/listener/SimulatorStatusListener.java @@ -16,7 +16,6 @@ package org.citrusframework.simulator.listener; -import org.apache.commons.lang.NotImplementedException; import org.citrusframework.DefaultTestCase; import org.citrusframework.TestAction; import org.citrusframework.TestCase; @@ -49,7 +48,9 @@ public class SimulatorStatusListener extends AbstractTestListener implements Tes private static final Logger logger = LoggerFactory.getLogger(SimulatorStatusListener.class); /** - * Currently running test + * Currently running test. + * + * TODO: Replace with metric. */ private Map runningTests = new ConcurrentHashMap<>(); @@ -85,7 +86,7 @@ public void onTestSuccess(TestCase test) { result = TestResult.success(test.getName(), test.getTestClass().getSimpleName()); } - testResultService.save(result); + testResultService.transformAndSave(result); executionService.completeScenarioExecutionSuccess(test); logger.info(result.toString()); @@ -100,7 +101,7 @@ public void onTestFailure(TestCase test, Throwable cause) { result = TestResult.failed(test.getName(), test.getTestClass().getSimpleName(), cause); } - testResultService.save(result); + testResultService.transformAndSave(result); executionService.completeScenarioExecutionFailure(test, cause); logger.info(result.toString()); @@ -128,17 +129,13 @@ public void onTestActionFinish(TestCase testCase, TestAction testAction) { } } - private boolean ignoreTestAction(TestAction testAction) { - return testAction.getClass().equals(SleepAction.class); - } - @Override public void onTestActionSkipped(TestCase testCase, TestAction testAction) { } private String[] getParameters(TestCase test) { - List parameterStrings = new ArrayList(); + List parameterStrings = new ArrayList<>(); if (test instanceof DefaultTestCase) { for (Map.Entry param : ((DefaultTestCase) test).getParameters().entrySet()) { @@ -146,41 +143,10 @@ private String[] getParameters(TestCase test) { } } - return parameterStrings.toArray(new String[parameterStrings.size()]); - } - - /** - * Gets the value of the testResults property. - * - * @return the testResults - */ - public List getTestResultService() { - return testResultService.findAll(); - } - - /** - * Gets the value of the runningTests property. - * - * @return the runningTests - */ - public Map getRunningTests() { - return runningTests; - } - - /** - * Deletes all test results. Shall be removed, but will be kept for now to ensure backward compatibility. - * - * @deprecated will be removed wihin the next breaking release! - */ - @Deprecated(forRemoval = true) - public void clearResults() { - throw new NotImplementedException("This method will be removed within the next breaking release!"); + return parameterStrings.toArray(new String[0]); } - /** - * Get the count of active scenarios - */ - public int getCountActiveScenarios() { - return runningTests.size(); + private boolean ignoreTestAction(TestAction testAction) { + return testAction.getClass().equals(SleepAction.class); } } diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/model/AbstractAuditingEntity.java b/simulator-starter/src/main/java/org/citrusframework/simulator/model/AbstractAuditingEntity.java index 96e4f8720..d92c300cc 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/model/AbstractAuditingEntity.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/model/AbstractAuditingEntity.java @@ -10,6 +10,7 @@ import java.io.Serializable; import java.time.Instant; +import java.time.ZonedDateTime; /** * Base abstract class for entities which will hold definitions for created and last modified by attributes. @@ -37,11 +38,6 @@ public void setCreatedDate(Instant createdDate) { this.createdDate = createdDate; } - public T createdDate(Instant createdDate) { - setCreatedDate(createdDate); - return (T) this; - } - public Instant getLastModifiedDate() { return lastModifiedDate; } @@ -50,8 +46,20 @@ public void setLastModifiedDate(Instant lastModifiedDate) { this.lastModifiedDate = lastModifiedDate; } - public T lastModifiedDate(Instant lastModifiedDate) { - setLastModifiedDate(lastModifiedDate); - return (T) this; + public static abstract class AuditingEntityBuilder, E extends AbstractAuditingEntity, A> { + + @SuppressWarnings("unchecked") + public B createdDate(ZonedDateTime createdDate) { + getEntity().setCreatedDate(createdDate.toInstant()); + return (B) this; + } + + @SuppressWarnings("unchecked") + public B lastModifiedDate(ZonedDateTime lastModifiedDate) { + getEntity().setLastModifiedDate(lastModifiedDate.toInstant()); + return (B) this; + } + + protected abstract E getEntity(); } } diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/model/MessageFilter.java b/simulator-starter/src/main/java/org/citrusframework/simulator/model/MessageFilter.java index cdeea8b97..6124c8127 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/model/MessageFilter.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/model/MessageFilter.java @@ -16,14 +16,11 @@ package org.citrusframework.simulator.model; -import lombok.Data; - import java.time.Instant; /** * Filter for filtering {@link Message}s */ -@Data public class MessageFilter { private Instant fromDate; private Instant toDate; @@ -43,4 +40,68 @@ public class MessageFilter { * filter. */ private String headerFilter; + + public Instant getFromDate() { + return fromDate; + } + + public void setFromDate(Instant fromDate) { + this.fromDate = fromDate; + } + + public Instant getToDate() { + return toDate; + } + + public void setToDate(Instant toDate) { + this.toDate = toDate; + } + + public Integer getPageNumber() { + return pageNumber; + } + + public void setPageNumber(Integer pageNumber) { + this.pageNumber = pageNumber; + } + + public Integer getPageSize() { + return pageSize; + } + + public void setPageSize(Integer pageSize) { + this.pageSize = pageSize; + } + + public Boolean getDirectionInbound() { + return directionInbound; + } + + public void setDirectionInbound(Boolean directionInbound) { + this.directionInbound = directionInbound; + } + + public Boolean getDirectionOutbound() { + return directionOutbound; + } + + public void setDirectionOutbound(Boolean directionOutbound) { + this.directionOutbound = directionOutbound; + } + + public String getContainingText() { + return containingText; + } + + public void setContainingText(String containingText) { + this.containingText = containingText; + } + + public String getHeaderFilter() { + return headerFilter; + } + + public void setHeaderFilter(String headerFilter) { + this.headerFilter = headerFilter; + } } diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/model/TestParameter.java b/simulator-starter/src/main/java/org/citrusframework/simulator/model/TestParameter.java index aa99c5e07..b61d977d2 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/model/TestParameter.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/model/TestParameter.java @@ -29,6 +29,7 @@ import java.io.Serial; import java.io.Serializable; +import java.util.Objects; /** * Represents a parameter of a test result, holding a key-value pair of parameter details. It is linked to @@ -64,7 +65,7 @@ public class TestParameter extends AbstractAuditingEntity { + + private final TestParameter testParameter = new TestParameter(); + + public TestParameter build() { + return testParameter; + } + + public TestParameterBuilder key(String key) { + if (Objects.isNull(testParameter.testParameterId)) { + testParameter.testParameterId = new TestParameterId(); + } + + testParameter.testParameterId.key = key; + return this; + } + + public TestParameterBuilder value(String value) { + testParameter.value = value; + return this; + } + + @Override + protected TestParameter getEntity() { + return testParameter; + } + } } diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/model/TestResult.java b/simulator-starter/src/main/java/org/citrusframework/simulator/model/TestResult.java index 2f818eb37..ea23d5d7d 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/model/TestResult.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/model/TestResult.java @@ -24,6 +24,7 @@ import jakarta.persistence.Id; import jakarta.persistence.OneToMany; import jakarta.validation.constraints.NotEmpty; + import java.io.Serial; import java.io.Serializable; import java.util.Arrays; @@ -123,10 +124,19 @@ public TestResult(org.citrusframework.TestResult testResult) { failureType = testResult.getFailureType(); } + public static TestResultBuilder builder() { + return new TestResultBuilder(); + } + public Long getId() { return id; } + public TestResult id(Long id) { + this.id = id; + return this; + } + public Status getStatus() { return Status.fromId(status); } @@ -143,6 +153,10 @@ public Set getTestParameters() { return testParameters; } + public void addTestParameter(TestParameter testParameter) { + testParameters.add(testParameter); + } + public String getErrorMessage() { return errorMessage; } @@ -157,10 +171,10 @@ public String getFailureType() { private int convertToStatus(String resultName) { return Arrays.stream(Status.values()) - .filter(result -> result.name().equals(resultName)) - .findFirst() - .orElse(Status.UNKNOWN) - .id; + .filter(result -> result.name().equals(resultName)) + .findFirst() + .orElse(Status.UNKNOWN) + .id; } @Override @@ -171,9 +185,9 @@ public String toString() { ", status='" + getStatus() + "'" + ", testName='" + getTestName() + "'" + ", className='" + getClassName() + "'" + - ", errorMessage='" +getErrorMessage() + "'" + - ", failureStack='" +getFailureStack() + "'" + - ", failureType='" +getFailureType() + "'" + + ", errorMessage='" + getErrorMessage() + "'" + + ", failureStack='" + getFailureStack() + "'" + + ", failureType='" + getFailureType() + "'" + "}"; } @@ -198,4 +212,48 @@ public static Status fromId(int id) { .orElse(Status.UNKNOWN); } } + + public static class TestResultBuilder extends AuditingEntityBuilder { + + private final TestResult testResult = new TestResult(); + + public TestResult build() { + return testResult; + } + + public TestResultBuilder status(Integer status) { + testResult.status = status; + return this; + } + + public TestResultBuilder testName(String testName) { + testResult.testName = testName; + return this; + } + + public TestResultBuilder className(String className) { + testResult.className = className; + return this; + } + + public TestResultBuilder errorMessage(String errorMessage) { + testResult.errorMessage = errorMessage; + return this; + } + + public TestResultBuilder failureStack(String failureStack) { + testResult.failureStack = failureStack; + return this; + } + + public TestResultBuilder failureType(String failureType) { + testResult.failureType = failureType; + return this; + } + + @Override + protected TestResult getEntity() { + return testResult; + } + } } diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/repository/MessageRepositoryImpl.java b/simulator-starter/src/main/java/org/citrusframework/simulator/repository/MessageRepositoryImpl.java index bb880b8f0..e8feeb204 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/repository/MessageRepositoryImpl.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/repository/MessageRepositoryImpl.java @@ -68,12 +68,12 @@ private void addPayloadPredicate(MessageFilter filter, CriteriaBuilder criteriaB private void addDatePredicates(MessageFilter filter, CriteriaBuilder criteriaBuilder, Root message, List predicates) { if (filter.getFromDate() != null) { - predicates.add(criteriaBuilder.greaterThanOrEqualTo(message.get("date"), + predicates.add(criteriaBuilder.greaterThanOrEqualTo(message.get("createdDate"), filter.getFromDate())); } if (filter.getToDate() != null) { - predicates.add(criteriaBuilder.lessThanOrEqualTo(message.get("date"), + predicates.add(criteriaBuilder.lessThanOrEqualTo(message.get("createdDate"), filter.getToDate())); } } diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/repository/TestParameterRepository.java b/simulator-starter/src/main/java/org/citrusframework/simulator/repository/TestParameterRepository.java new file mode 100644 index 000000000..282b5a3a4 --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/repository/TestParameterRepository.java @@ -0,0 +1,13 @@ +package org.citrusframework.simulator.repository; + +import org.citrusframework.simulator.model.TestParameter; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Repository; + +/** + * Spring Data JPA repository for the TestParameter entity. + */ +@SuppressWarnings("unused") +@Repository +public interface TestParameterRepository extends JpaRepository, JpaSpecificationExecutor {} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/repository/TestResultRepository.java b/simulator-starter/src/main/java/org/citrusframework/simulator/repository/TestResultRepository.java index 79c27270e..c544657ce 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/repository/TestResultRepository.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/repository/TestResultRepository.java @@ -1,26 +1,22 @@ -/* - * Copyright 2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package org.citrusframework.simulator.repository; import org.citrusframework.simulator.model.TestResult; +import org.citrusframework.simulator.service.dto.TestResultByStatus; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; +/** + * Spring Data JPA repository for the TestResult entity. + */ +@SuppressWarnings("unused") @Repository -public interface TestResultRepository extends JpaRepository { +public interface TestResultRepository extends JpaRepository, JpaSpecificationExecutor { + @Query("select new org.citrusframework.simulator.service.dto.TestResultByStatus(" + + "sum(case when t.status = 1 then 1 else 0 end), " + + "sum(case when t.status = 2 then 1 else 0 end)) " + + "from TestResult t") + TestResultByStatus countByStatus(); } diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/ActivityService.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/ActivityService.java index 293f5e74d..eabd240e6 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/ActivityService.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/ActivityService.java @@ -239,6 +239,6 @@ private long lookupScenarioExecutionId(TestCase testCase) { } private Instant getTimeNow() { - return LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant(); + return LocalDateTime.now().toInstant(ZoneOffset.UTC); } } diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/QueryService.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/QueryService.java new file mode 100644 index 000000000..d48c2ad93 --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/QueryService.java @@ -0,0 +1,526 @@ +package org.citrusframework.simulator.service; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.Root; +import jakarta.persistence.criteria.SetJoin; +import jakarta.persistence.metamodel.SetAttribute; +import jakarta.persistence.metamodel.SingularAttribute; +import org.citrusframework.simulator.service.filter.Filter; +import org.citrusframework.simulator.service.filter.RangeFilter; +import org.citrusframework.simulator.service.filter.StringFilter; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collection; +import java.util.Set; +import java.util.function.Function; + +/** + * Base service for constructing and executing complex queries. + * + * @param the type of the entity which is queried. + */ +@Transactional(readOnly = true) +public abstract class QueryService { + + /** + * Helper function to return a specification for filtering on a single field, where equality, and null/non-null + * conditions are supported. + * + * @param filter the individual attribute filter coming from the frontend. + * @param field the JPA static metamodel representing the field. + * @param The type of the attribute which is filtered. + * @return a Specification + */ + protected Specification buildSpecification(Filter filter, SingularAttribute + field) { + return buildSpecification(filter, root -> root.get(field)); + } + + /** + * Helper function to return a specification for filtering on a single field, where equality, and null/non-null + * conditions are supported. + * + * @param filter the individual attribute filter coming from the frontend. + * @param metaclassFunction the function, which navigates from the current entity to a column, for which the filter applies. + * @param The type of the attribute which is filtered. + * @return a Specification + */ + protected Specification buildSpecification(Filter filter, Function, Expression> metaclassFunction) { + if (filter.getEquals() != null) { + return equalsSpecification(metaclassFunction, filter.getEquals()); + } else if (filter.getIn() != null) { + return valueIn(metaclassFunction, filter.getIn()); + } else if (filter.getNotIn() != null) { + return valueNotIn(metaclassFunction, filter.getNotIn()); + } else if (filter.getNotEquals() != null) { + return notEqualsSpecification(metaclassFunction, filter.getNotEquals()); + } else if (filter.getSpecified() != null) { + return byFieldSpecified(metaclassFunction, filter.getSpecified()); + } + return null; + } + + /** + * Helper function to return a specification for filtering on a {@link java.lang.String} field, where equality, containment, + * and null/non-null conditions are supported. + * + * @param filter the individual attribute filter coming from the frontend. + * @param field the JPA static metamodel representing the field. + * @return a Specification + */ + protected Specification buildStringSpecification(StringFilter filter, SingularAttribute field) { + return buildSpecification(filter, root -> root.get(field)); + } + + /** + * Helper function to return a specification for filtering on a {@link java.lang.String} field, where equality, containment, + * and null/non-null conditions are supported. + * + * @param filter the individual attribute filter coming from the frontend. + * @param metaclassFunction lambda, which based on a Root<ENTITY> returns Expression - basicaly picks a column + * @return a Specification + */ + protected Specification buildSpecification(StringFilter filter, Function, Expression> metaclassFunction) { + if (filter.getEquals() != null) { + return equalsSpecification(metaclassFunction, filter.getEquals()); + } else if (filter.getIn() != null) { + return valueIn(metaclassFunction, filter.getIn()); + } else if (filter.getNotIn() != null) { + return valueNotIn(metaclassFunction, filter.getNotIn()); + } else if (filter.getContains() != null) { + return likeUpperSpecification(metaclassFunction, filter.getContains()); + } else if (filter.getDoesNotContain() != null) { + return doesNotContainSpecification(metaclassFunction, filter.getDoesNotContain()); + } else if (filter.getNotEquals() != null) { + return notEqualsSpecification(metaclassFunction, filter.getNotEquals()); + } else if (filter.getSpecified() != null) { + return byFieldSpecified(metaclassFunction, filter.getSpecified()); + } + return null; + } + + /** + * Helper function to return a specification for filtering on a single {@link java.lang.Comparable}, where equality, less + * than, greater than and less-than-or-equal-to and greater-than-or-equal-to and null/non-null conditions are + * supported. + * + * @param The type of the attribute which is filtered. + * @param filter the individual attribute filter coming from the frontend. + * @param field the JPA static metamodel representing the field. + * @return a Specification + */ + protected > Specification buildRangeSpecification(RangeFilter filter, + SingularAttribute field) { + return buildSpecification(filter, root -> root.get(field)); + } + + /** + * Helper function to return a specification for filtering on a single {@link java.lang.Comparable}, where equality, less + * than, greater than and less-than-or-equal-to and greater-than-or-equal-to and null/non-null conditions are + * supported. + * + * @param The type of the attribute which is filtered. + * @param filter the individual attribute filter coming from the frontend. + * @param metaclassFunction lambda, which based on a Root<ENTITY> returns Expression - basicaly picks a column + * @return a Specification + */ + protected > Specification buildSpecification(RangeFilter filter, + Function, Expression> metaclassFunction) { + if (filter.getEquals() != null) { + return equalsSpecification(metaclassFunction, filter.getEquals()); + } else if (filter.getIn() != null) { + return valueIn(metaclassFunction, filter.getIn()); + } + + Specification result = Specification.where(null); + if (filter.getSpecified() != null) { + result = result.and(byFieldSpecified(metaclassFunction, filter.getSpecified())); + } + if (filter.getNotEquals() != null) { + result = result.and(notEqualsSpecification(metaclassFunction, filter.getNotEquals())); + } + if (filter.getNotIn() != null) { + result = result.and(valueNotIn(metaclassFunction, filter.getNotIn())); + } + if (filter.getGreaterThan() != null) { + result = result.and(greaterThan(metaclassFunction, filter.getGreaterThan())); + } + if (filter.getGreaterThanOrEqual() != null) { + result = result.and(greaterThanOrEqualTo(metaclassFunction, filter.getGreaterThanOrEqual())); + } + if (filter.getLessThan() != null) { + result = result.and(lessThan(metaclassFunction, filter.getLessThan())); + } + if (filter.getLessThanOrEqual() != null) { + result = result.and(lessThanOrEqualTo(metaclassFunction, filter.getLessThanOrEqual())); + } + return result; + } + + /** + * Helper function to return a specification for filtering on one-to-one or many-to-one reference. Usage: + *

+     *   Specification<Employee> specByProjectId = buildReferringEntitySpecification(criteria.getProjectId(),
+     * Employee_.project, Project_.id);
+     *   Specification<Employee> specByProjectName = buildReferringEntitySpecification(criteria.getProjectName(),
+     * Employee_.project, Project_.name);
+     * 
+ * + * @param filter the filter object which contains a value, which needs to match or a flag if nullness is + * checked. + * @param reference the attribute of the static metamodel for the referring entity. + * @param valueField the attribute of the static metamodel of the referred entity, where the equality should be + * checked. + * @param The type of the referenced entity. + * @param The type of the attribute which is filtered. + * @return a Specification + */ + protected Specification buildReferringEntitySpecification(Filter filter, + SingularAttribute reference, + SingularAttribute valueField) { + return buildSpecification(filter, root -> root.get(reference).get(valueField)); + } + + /** + * Helper function to return a specification for filtering on one-to-many or many-to-many reference. Usage: + *
+     *   Specification<Employee> specByEmployeeId = buildReferringEntitySpecification(criteria.getEmployeId(),
+     * Project_.employees, Employee_.id);
+     *   Specification<Employee> specByEmployeeName = buildReferringEntitySpecification(criteria.getEmployeName(),
+     * Project_.project, Project_.name);
+     * 
+ * + * @param filter the filter object which contains a value, which needs to match or a flag if emptiness is + * checked. + * @param reference the attribute of the static metamodel for the referring entity. + * @param valueField the attribute of the static metamodel of the referred entity, where the equality should be + * checked. + * @param The type of the referenced entity. + * @param The type of the attribute which is filtered. + * @return a Specification + */ + protected Specification buildReferringEntitySpecification(Filter filter, + SetAttribute reference, + SingularAttribute valueField) { + return buildReferringEntitySpecification(filter, root -> root.join(reference), entity -> entity.get(valueField)); + } + + /** + * Helper function to return a specification for filtering on one-to-many or many-to-many reference.Usage:
+     *   Specification<Employee> specByEmployeeId = buildReferringEntitySpecification(
+     *          criteria.getEmployeId(),
+     *          root -> root.get(Project_.company).join(Company_.employees),
+     *          entity -> entity.get(Employee_.id));
+     *   Specification<Employee> specByProjectName = buildReferringEntitySpecification(
+     *          criteria.getProjectName(),
+     *          root -> root.get(Project_.project)
+     *          entity -> entity.get(Project_.name));
+     * 
+ * + * @param filter the filter object which contains a value, which needs to match or a flag if emptiness is + * checked. + * @param functionToEntity the function, which joins he current entity to the entity set, on which the filtering is applied. + * @param entityToColumn the function, which of the static metamodel of the referred entity, where the equality should be + * checked. + * @param The type of the referenced entity. + * @param The type of the entity which is the last before the OTHER in the chain. + * @param The type of the attribute which is filtered. + * @return a Specification + */ + protected Specification buildReferringEntitySpecification(Filter filter, + Function, SetJoin> functionToEntity, + Function, Expression> entityToColumn) { + if (filter.getEquals() != null) { + return equalsSpecification(functionToEntity.andThen(entityToColumn), filter.getEquals()); + } else if (filter.getSpecified() != null) { + // Interestingly, 'functionToEntity' doesn't work, we need the longer lambda formula + return byFieldSpecified(functionToEntity::apply, filter.getSpecified()); + } + return null; + } + + /** + * Helper function to return a specification for filtering on one-to-many or many-to-many reference.Where equality, less + * than, greater than and less-than-or-equal-to and greater-than-or-equal-to and null/non-null conditions are + * supported. Usage: + *
+     *   Specification<Employee> specByEmployeeId = buildReferringEntitySpecification(criteria.getEmployeId(),
+     * Project_.employees, Employee_.id);
+     *   Specification<Employee> specByEmployeeName = buildReferringEntitySpecification(criteria.getEmployeName(),
+     * Project_.project, Project_.name);
+     * 
+ * + * @param The type of the attribute which is filtered. + * @param filter the filter object which contains a value, which needs to match or a flag if emptiness is + * checked. + * @param reference the attribute of the static metamodel for the referring entity. + * @param valueField the attribute of the static metamodel of the referred entity, where the equality should be + * checked. + * @param The type of the referenced entity. + * @return a Specification + */ + protected > Specification buildReferringEntitySpecification( + RangeFilter filter, + SetAttribute reference, + SingularAttribute valueField) { + return buildReferringEntitySpecification(filter, root -> root.join(reference), entity -> entity.get(valueField)); + } + + /** + * Helper function to return a specification for filtering on one-to-many or many-to-many reference.Where equality, less + * than, greater than and less-than-or-equal-to and greater-than-or-equal-to and null/non-null conditions are + * supported. Usage: + *

+     *   Specification<Employee> specByEmployeeId = buildReferringEntitySpecification(
+     *          criteria.getEmployeId(),
+     *          root -> root.get(Project_.company).join(Company_.employees),
+     *          entity -> entity.get(Employee_.id));
+     *   Specification<Employee> specByProjectName = buildReferringEntitySpecification(
+     *          criteria.getProjectName(),
+     *          root -> root.get(Project_.project)
+     *          entity -> entity.get(Project_.name));
+     * 
+     * 
+ * + * @param The type of the attribute which is filtered. + * @param filter the filter object which contains a value, which needs to match or a flag if emptiness is + * checked. + * @param functionToEntity the function, which joins he current entity to the entity set, on which the filtering is applied. + * @param entityToColumn the function, which of the static metamodel of the referred entity, where the equality should be + * checked. + * @param The type of the referenced entity. + * @param The type of the entity which is the last before the OTHER in the chain. + * @return a Specification + */ + protected > Specification buildReferringEntitySpecification( + RangeFilter filter, + Function, SetJoin> functionToEntity, + Function, Expression> entityToColumn) { + + Function, Expression> fused = functionToEntity.andThen(entityToColumn); + if (filter.getEquals() != null) { + return equalsSpecification(fused, filter.getEquals()); + } else if (filter.getIn() != null) { + return valueIn(fused, filter.getIn()); + } + Specification result = Specification.where(null); + if (filter.getSpecified() != null) { + // Interestingly, 'functionToEntity' doesn't work, we need the longer lambda formula + result = result.and(byFieldSpecified(functionToEntity::apply, filter.getSpecified())); + } + if (filter.getNotEquals() != null) { + result = result.and(notEqualsSpecification(fused, filter.getNotEquals())); + } + if (filter.getNotIn() != null) { + result = result.and(valueNotIn(fused, filter.getNotIn())); + } + if (filter.getGreaterThan() != null) { + result = result.and(greaterThan(fused, filter.getGreaterThan())); + } + if (filter.getGreaterThanOrEqual() != null) { + result = result.and(greaterThanOrEqualTo(fused, filter.getGreaterThanOrEqual())); + } + if (filter.getLessThan() != null) { + result = result.and(lessThan(fused, filter.getLessThan())); + } + if (filter.getLessThanOrEqual() != null) { + result = result.and(lessThanOrEqualTo(fused, filter.getLessThanOrEqual())); + } + return result; + } + + /** + * Generic method, which based on a Root<ENTITY> returns an Expression which type is the same as the given 'value' type. + * + * @param metaclassFunction function which returns the column which is used for filtering. + * @param value the actual value to filter for. + * @param The type of the attribute which is filtered. + * @return a Specification. + */ + protected Specification equalsSpecification(Function, Expression> metaclassFunction, X value) { + return (root, query, builder) -> builder.equal(metaclassFunction.apply(root), value); + } + + /** + * Generic method, which based on a Root<ENTITY> returns an Expression which type is the same as the given 'value' type. + * + * @param metaclassFunction function which returns the column which is used for filtering. + * @param value the actual value to exclude for. + * @param The type of the attribute which is filtered. + * @return a Specification. + */ + protected Specification notEqualsSpecification(Function, Expression> metaclassFunction, X value) { + return (root, query, builder) -> builder.not(builder.equal(metaclassFunction.apply(root), value)); + } + + /** + *

likeUpperSpecification.

+ * + * @param metaclassFunction a {@link java.util.function.Function} object. + * @param value a {@link java.lang.String} object. + * @return a {@link org.springframework.data.jpa.domain.Specification} object. + */ + protected Specification likeUpperSpecification(Function, Expression> metaclassFunction, + String value) { + return (root, query, builder) -> builder.like(builder.upper(metaclassFunction.apply(root)), wrapLikeQuery(value)); + } + + /** + *

doesNotContainSpecification.

+ * + * @param metaclassFunction a {@link java.util.function.Function} object. + * @param value a {@link java.lang.String} object. + * @return a {@link org.springframework.data.jpa.domain.Specification} object. + */ + protected Specification doesNotContainSpecification(Function, Expression> metaclassFunction, + String value) { + return (root, query, builder) -> builder.not(builder.like(builder.upper(metaclassFunction.apply(root)), wrapLikeQuery(value))); + } + + /** + *

byFieldSpecified.

+ * + * @param metaclassFunction a {@link java.util.function.Function} object. + * @param specified a boolean. + * @param a X object. + * @return a {@link org.springframework.data.jpa.domain.Specification} object. + */ + protected Specification byFieldSpecified(Function, Expression> metaclassFunction, + boolean specified) { + return specified ? + (root, query, builder) -> builder.isNotNull(metaclassFunction.apply(root)) : + (root, query, builder) -> builder.isNull(metaclassFunction.apply(root)); + } + + /** + *

byFieldEmptiness.

+ * + * @param metaclassFunction a {@link java.util.function.Function} object. + * @param specified a boolean. + * @param a X object. + * @return a {@link org.springframework.data.jpa.domain.Specification} object. + */ + protected Specification byFieldEmptiness(Function, Expression>> metaclassFunction, + boolean specified) { + return specified ? + (root, query, builder) -> builder.isNotEmpty(metaclassFunction.apply(root)) : + (root, query, builder) -> builder.isEmpty(metaclassFunction.apply(root)); + } + + /** + *

valueIn.

+ * + * @param metaclassFunction a {@link java.util.function.Function} object. + * @param values a {@link java.util.Collection} object. + * @param a X object. + * @return a {@link org.springframework.data.jpa.domain.Specification} object. + */ + protected Specification valueIn(Function, Expression> metaclassFunction, + Collection values) { + return (root, query, builder) -> { + CriteriaBuilder.In in = builder.in(metaclassFunction.apply(root)); + for (X value : values) { + in = in.value(value); + } + return in; + }; + } + + /** + *

valueNotIn.

+ * + * @param metaclassFunction a {@link java.util.function.Function} object. + * @param values a {@link java.util.Collection} object. + * @param a X object. + * @return a {@link org.springframework.data.jpa.domain.Specification} object. + */ + protected Specification valueNotIn(Function, Expression> metaclassFunction, + Collection values) { + return (root, query, builder) -> { + CriteriaBuilder.In in = builder.in(metaclassFunction.apply(root)); + for (X value : values) { + in = in.value(value); + } + return builder.not(in); + }; + } + + /** + *

greaterThanOrEqualTo.

+ * + * @param metaclassFunction a {@link java.util.function.Function} object. + * @param value a X object. + * @param a X object. + * @return a {@link org.springframework.data.jpa.domain.Specification} object. + */ + protected > Specification greaterThanOrEqualTo(Function, Expression> metaclassFunction, + X value) { + return (root, query, builder) -> builder.greaterThanOrEqualTo(metaclassFunction.apply(root), value); + } + + /** + *

greaterThan.

+ * + * @param metaclassFunction a {@link java.util.function.Function} object. + * @param value a X object. + * @param a X object. + * @return a {@link org.springframework.data.jpa.domain.Specification} object. + */ + protected > Specification greaterThan(Function, Expression> metaclassFunction, + X value) { + return (root, query, builder) -> builder.greaterThan(metaclassFunction.apply(root), value); + } + + /** + *

lessThanOrEqualTo.

+ * + * @param metaclassFunction a {@link java.util.function.Function} object. + * @param value a X object. + * @param a X object. + * @return a {@link org.springframework.data.jpa.domain.Specification} object. + */ + protected > Specification lessThanOrEqualTo(Function, Expression> metaclassFunction, + X value) { + return (root, query, builder) -> builder.lessThanOrEqualTo(metaclassFunction.apply(root), value); + } + + /** + *

lessThan.

+ * + * @param metaclassFunction a {@link java.util.function.Function} object. + * @param value a X object. + * @param a X object. + * @return a {@link org.springframework.data.jpa.domain.Specification} object. + */ + protected > Specification lessThan(Function, Expression> metaclassFunction, + X value) { + return (root, query, builder) -> builder.lessThan(metaclassFunction.apply(root), value); + } + + /** + *

wrapLikeQuery.

+ * + * @param txt a {@link java.lang.String} object. + * @return a {@link java.lang.String} object. + */ + protected String wrapLikeQuery(String txt) { + return "%" + txt.toUpperCase() + '%'; + } + + /** + *

distinct.

+ * + * @param distinct a boolean. + * @return a {@link org.springframework.data.jpa.domain.Specification} object. + */ + protected Specification distinct(boolean distinct) { + return (root, query, cb) -> { + query.distinct(distinct); + return null; + }; + } + +} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestParameterQueryService.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestParameterQueryService.java new file mode 100644 index 000000000..d2b8e9374 --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestParameterQueryService.java @@ -0,0 +1,110 @@ +package org.citrusframework.simulator.service; + +import jakarta.persistence.criteria.JoinType; +import org.citrusframework.simulator.model.TestParameter; +import org.citrusframework.simulator.model.TestParameter_; +import org.citrusframework.simulator.model.TestResult_; +import org.citrusframework.simulator.repository.TestParameterRepository; +import org.citrusframework.simulator.service.criteria.TestParameterCriteria; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * Service for executing complex queries for {@link TestParameter} entities in the database. + * The main input is a {@link TestParameterCriteria} which gets converted to {@link Specification}, + * in a way that all the filters must apply. + * It returns a {@link List} of {@link TestParameter} or a {@link Page} of {@link TestParameter} which fulfills the criteria. + */ +@Service +@Transactional(readOnly = true) +public class TestParameterQueryService extends QueryService { + + private final Logger log = LoggerFactory.getLogger(TestParameterQueryService.class); + + private final TestParameterRepository testParameterRepository; + + public TestParameterQueryService(TestParameterRepository testParameterRepository) { + this.testParameterRepository = testParameterRepository; + } + + /** + * Return a {@link List} of {@link TestParameter} which matches the criteria from the database. + * @param criteria The object which holds all the filters, which the entities should match. + * @return the matching entities. + */ + @Transactional(readOnly = true) + public List findByCriteria(TestParameterCriteria criteria) { + log.debug("find by criteria : {}", criteria); + final Specification specification = createSpecification(criteria); + return testParameterRepository.findAll(specification); + } + + /** + * Return a {@link Page} of {@link TestParameter} which matches the criteria from the database. + * @param criteria The object which holds all the filters, which the entities should match. + * @param page The page, which should be returned. + * @return the matching entities. + */ + @Transactional(readOnly = true) + public Page findByCriteria(TestParameterCriteria criteria, Pageable page) { + log.debug("find by criteria : {}, page: {}", criteria, page); + final Specification specification = createSpecification(criteria); + return testParameterRepository.findAll(specification, page); + } + + /** + * Return the number of matching entities in the database. + * @param criteria The object which holds all the filters, which the entities should match. + * @return the number of matching entities. + */ + @Transactional(readOnly = true) + public long countByCriteria(TestParameterCriteria criteria) { + log.debug("count by criteria : {}", criteria); + final Specification specification = createSpecification(criteria); + return testParameterRepository.count(specification); + } + + /** + * Function to convert {@link TestParameterCriteria} to a {@link Specification} + * @param criteria The object which holds all the filters, which the entities should match. + * @return the matching {@link Specification} of the entity. + */ + protected Specification createSpecification(TestParameterCriteria criteria) { + Specification specification = Specification.where(null); + if (criteria != null) { + // This has to be called first, because the distinct method returns null + if (criteria.getDistinct() != null) { + specification = specification.and(distinct(criteria.getDistinct())); + } + if (criteria.getKey() != null) { + specification = specification.and(buildSpecification(criteria.getKey(), root -> root.get(TestParameter_.testParameterId).get("key"))); + } + if (criteria.getValue() != null) { + specification = specification.and(buildStringSpecification(criteria.getValue(), TestParameter_.value)); + } + if (criteria.getCreatedDate() != null) { + specification = specification.and(buildRangeSpecification(criteria.getCreatedDate(), TestParameter_.createdDate)); + } + if (criteria.getLastModifiedDate() != null) { + specification = specification.and(buildRangeSpecification(criteria.getLastModifiedDate(), TestParameter_.lastModifiedDate)); + } + if (criteria.getTestResultId() != null) { + specification = + specification.and( + buildSpecification( + criteria.getTestResultId(), + root -> root.join(TestParameter_.testResult, JoinType.LEFT).get(TestResult_.id) + ) + ); + } + } + return specification; + } +} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestParameterService.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestParameterService.java new file mode 100644 index 000000000..71ed7efe1 --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestParameterService.java @@ -0,0 +1,36 @@ +package org.citrusframework.simulator.service; + +import org.citrusframework.simulator.model.TestParameter; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.Optional; + +/** + * Service Interface for managing {@link org.citrusframework.simulator.model.TestParameter}. + */ +public interface TestParameterService { + /** + * Save a testParameter. + * + * @param testParameter the entity to save. + * @return the persisted entity. + */ + TestParameter save(TestParameter testParameter); + + /** + * Get all the testParameters. + * + * @param pageable the pagination information. + * @return the list of entities. + */ + Page findAll(Pageable pageable); + + /** + * Get the "id" testParameter. + * + * @param id the id of the entity. + * @return the entity. + */ + Optional findOne(Long testResultId, String key); +} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestResultQueryService.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestResultQueryService.java new file mode 100644 index 000000000..edbb0aaa4 --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestResultQueryService.java @@ -0,0 +1,125 @@ +package org.citrusframework.simulator.service; + +import jakarta.persistence.criteria.JoinType; +import org.citrusframework.simulator.model.TestParameter_; +import org.citrusframework.simulator.model.TestResult; +import org.citrusframework.simulator.model.TestResult_; +import org.citrusframework.simulator.repository.TestResultRepository; +import org.citrusframework.simulator.service.criteria.TestResultCriteria; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * Service for executing complex queries for {@link TestResult} entities in the database. + * The main input is a {@link TestResultCriteria} which gets converted to {@link Specification}, + * in a way that all the filters must apply. + * It returns a {@link List} of {@link TestResult} or a {@link Page} of {@link TestResult} which fulfills the criteria. + */ +@Service +@Transactional(readOnly = true) +public class TestResultQueryService extends QueryService { + + private final Logger logger = LoggerFactory.getLogger(TestResultQueryService.class); + + private final TestResultRepository testResultRepository; + + public TestResultQueryService(TestResultRepository testResultRepository) { + this.testResultRepository = testResultRepository; + } + + /** + * Return a {@link List} of {@link TestResult} which matches the criteria from the database. + * @param criteria The object which holds all the filters, which the entities should match. + * @return the matching entities. + */ + @Transactional(readOnly = true) + public List findByCriteria(TestResultCriteria criteria) { + logger.debug("find by criteria : {}", criteria); + final Specification specification = createSpecification(criteria); + return testResultRepository.findAll(specification); + } + + /** + * Return a {@link Page} of {@link TestResult} which matches the criteria from the database. + * @param criteria The object which holds all the filters, which the entities should match. + * @param page The page, which should be returned. + * @return the matching entities. + */ + @Transactional(readOnly = true) + public Page findByCriteria(TestResultCriteria criteria, Pageable page) { + logger.debug("find by criteria : {}, page: {}", criteria, page); + final Specification specification = createSpecification(criteria); + return testResultRepository.findAll(specification, page); + } + + /** + * Return the number of matching entities in the database. + * @param criteria The object which holds all the filters, which the entities should match. + * @return the number of matching entities. + */ + @Transactional(readOnly = true) + public long countByCriteria(TestResultCriteria criteria) { + logger.debug("count by criteria : {}", criteria); + final Specification specification = createSpecification(criteria); + return testResultRepository.count(specification); + } + + /** + * Function to convert {@link TestResultCriteria} to a {@link Specification} + * @param criteria The object which holds all the filters, which the entities should match. + * @return the matching {@link Specification} of the entity. + */ + protected Specification createSpecification(TestResultCriteria criteria) { + Specification specification = Specification.where(null); + if (criteria != null) { + // This has to be called first, because the distinct method returns null + if (criteria.getDistinct() != null) { + specification = specification.and(distinct(criteria.getDistinct())); + } + if (criteria.getId() != null) { + specification = specification.and(buildRangeSpecification(criteria.getId(), TestResult_.id)); + } + if (criteria.getStatus() != null) { + specification = specification.and(buildRangeSpecification(criteria.getStatus(), TestResult_.status)); + } + if (criteria.getTestName() != null) { + specification = specification.and(buildStringSpecification(criteria.getTestName(), TestResult_.testName)); + } + if (criteria.getClassName() != null) { + specification = specification.and(buildStringSpecification(criteria.getClassName(), TestResult_.className)); + } + if (criteria.getErrorMessage() != null) { + specification = specification.and(buildStringSpecification(criteria.getErrorMessage(), TestResult_.errorMessage)); + } + if (criteria.getFailureStack() != null) { + specification = specification.and(buildStringSpecification(criteria.getFailureStack(), TestResult_.failureStack)); + } + if (criteria.getFailureType() != null) { + specification = specification.and(buildStringSpecification(criteria.getFailureType(), TestResult_.failureType)); + } + if (criteria.getCreatedDate() != null) { + specification = specification.and(buildRangeSpecification(criteria.getCreatedDate(), TestResult_.createdDate)); + } + if (criteria.getLastModifiedDate() != null) { + specification = specification.and(buildRangeSpecification(criteria.getLastModifiedDate(), TestResult_.lastModifiedDate)); + } + if (criteria.getTestParameterKey() != null) { + specification = + specification.and( + buildSpecification( + criteria.getTestParameterKey(), + root -> root.join(TestResult_.testParameters, JoinType.LEFT).get(TestParameter_.testParameterId).get("key") + ) + ); + } + } + return specification; + } +} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestResultService.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestResultService.java index bdd6853f2..1566895c8 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestResultService.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestResultService.java @@ -1,41 +1,54 @@ -/* - * Copyright 2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package org.citrusframework.simulator.service; -import org.citrusframework.TestResult; -import org.citrusframework.simulator.repository.TestResultRepository; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -public class TestResultService { +import org.citrusframework.simulator.model.TestResult; +import org.citrusframework.simulator.service.dto.TestResultByStatus; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; - private final TestResultRepository testResultRepository; +import java.util.Optional; - public TestResultService(TestResultRepository testResultRepository) { - this.testResultRepository = testResultRepository; - } - - public org.citrusframework.simulator.model.TestResult save(TestResult result) { - return testResultRepository.save(new org.citrusframework.simulator.model.TestResult(result)); - } - - public List findAll() { - return testResultRepository.findAll(); - } +/** + * Service Interface for managing {@link org.citrusframework.simulator.model.TestResult}. + */ +public interface TestResultService { + + /** + * Save a citrus testResult. + * + * @param testResult the entity to save. + * @return the persisted entity. + * @see org.citrusframework.TestResult + */ + TestResult transformAndSave(org.citrusframework.TestResult testResult); + + /** + * Save a testResult. + * + * @param testResult the entity to save. + * @return the persisted entity. + */ + TestResult save(TestResult testResult); + + /** + * Get all the testResults. + * + * @param pageable the pagination information. + * @return the list of entities. + */ + Page findAll(Pageable pageable); + + /** + * Get the "id" testResult. + * + * @param id the id of the entity. + * @return the entity. + */ + Optional findOne(Long id); + + /** + * Count the total {@link TestResult} by their status. + * + * @return the TestResult count. + */ + TestResultByStatus countByStatus(); } diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/Criteria.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/Criteria.java new file mode 100644 index 000000000..7a2e6f25b --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/Criteria.java @@ -0,0 +1,5 @@ +package org.citrusframework.simulator.service.criteria; + +public interface Criteria { + Criteria copy(); +} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/TestParameterCriteria.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/TestParameterCriteria.java new file mode 100644 index 000000000..54dd4639a --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/TestParameterCriteria.java @@ -0,0 +1,176 @@ +package org.citrusframework.simulator.service.criteria; + +import org.citrusframework.simulator.service.filter.InstantFilter; +import org.citrusframework.simulator.service.filter.LongFilter; +import org.citrusframework.simulator.service.filter.StringFilter; +import org.springdoc.core.annotations.ParameterObject; + +import java.io.Serializable; +import java.util.Objects; + +/** + * Criteria class for the {@link org.citrusframework.simulator.model.TestParameter} entity. This class is used + * in {@link org.citrusframework.simulator.web.rest.TestParameterResource} to receive all the possible filtering options + * from the Http GET request parameters. + *

+ * For example the following could be a valid request: + * {@code /test-parameters?id.greaterThan=5&attr1.contains=something&attr2.specified=false} + *

+ * As Spring is unable to properly convert the types, unless + * specific {@link org.citrusframework.simulator.service.filter.Filter} class are used, we need to use fix type specific + * filters. + */ +@ParameterObject +@SuppressWarnings("common-java:DuplicatedBlocks") +public class TestParameterCriteria implements Serializable, Criteria { + + private static final long serialVersionUID = 1L; + + private StringFilter key; + + private StringFilter value; + + private InstantFilter createdDate; + + private InstantFilter lastModifiedDate; + + private LongFilter testResultId; + + private Boolean distinct; + + public TestParameterCriteria() {} + + public TestParameterCriteria(TestParameterCriteria other) { + this.key = other.key == null ? null : other.key.copy(); + this.value = other.value == null ? null : other.value.copy(); + this.createdDate = other.createdDate == null ? null : other.createdDate.copy(); + this.lastModifiedDate = other.lastModifiedDate == null ? null : other.lastModifiedDate.copy(); + this.testResultId = other.testResultId == null ? null : other.testResultId.copy(); + this.distinct = other.distinct; + } + + @Override + public TestParameterCriteria copy() { + return new TestParameterCriteria(this); + } + + public StringFilter getKey() { + return key; + } + + public StringFilter key() { + if (key == null) { + key = new StringFilter(); + } + return key; + } + + public void setKey(StringFilter key) { + this.key = key; + } + + public StringFilter getValue() { + return value; + } + + public StringFilter value() { + if (value == null) { + value = new StringFilter(); + } + return value; + } + + public void setValue(StringFilter value) { + this.value = value; + } + + public InstantFilter getCreatedDate() { + return createdDate; + } + + public InstantFilter createdDate() { + if (createdDate == null) { + createdDate = new InstantFilter(); + } + return createdDate; + } + + public void setCreatedDate(InstantFilter createdDate) { + this.createdDate = createdDate; + } + + public InstantFilter getLastModifiedDate() { + return lastModifiedDate; + } + + public InstantFilter lastModifiedDate() { + if (lastModifiedDate == null) { + lastModifiedDate = new InstantFilter(); + } + return lastModifiedDate; + } + + public void setLastModifiedDate(InstantFilter lastModifiedDate) { + this.lastModifiedDate = lastModifiedDate; + } + + public LongFilter getTestResultId() { + return testResultId; + } + + public LongFilter testResultId() { + if (testResultId == null) { + testResultId = new LongFilter(); + } + return testResultId; + } + + public void setTestResultId(LongFilter testResultId) { + this.testResultId = testResultId; + } + + public Boolean getDistinct() { + return distinct; + } + + public void setDistinct(Boolean distinct) { + this.distinct = distinct; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final TestParameterCriteria that = (TestParameterCriteria) o; + return ( + Objects.equals(key, that.key) && + Objects.equals(value, that.value) && + Objects.equals(createdDate, that.createdDate) && + Objects.equals(lastModifiedDate, that.lastModifiedDate) && + Objects.equals(testResultId, that.testResultId) && + Objects.equals(distinct, that.distinct) + ); + } + + @Override + public int hashCode() { + return Objects.hash(key, value, createdDate, lastModifiedDate, testResultId, distinct); + } + + // prettier-ignore + @Override + public String toString() { + return "TestParameterCriteria{" + + (key != null ? "key=" + key + ", " : "") + + (value != null ? "value=" + value + ", " : "") + + (createdDate != null ? "createdDate=" + createdDate + ", " : "") + + (lastModifiedDate != null ? "lastModifiedDate=" + lastModifiedDate + ", " : "") + + (testResultId != null ? "testResultId=" + testResultId + ", " : "") + + (distinct != null ? "distinct=" + distinct + ", " : "") + + "}"; + } +} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/TestResultCriteria.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/TestResultCriteria.java new file mode 100644 index 000000000..847f2e4c1 --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/TestResultCriteria.java @@ -0,0 +1,289 @@ +package org.citrusframework.simulator.service.criteria; + +import org.citrusframework.simulator.service.filter.InstantFilter; +import org.citrusframework.simulator.service.filter.IntegerFilter; +import org.citrusframework.simulator.service.filter.LongFilter; +import org.citrusframework.simulator.service.filter.StringFilter; +import org.springdoc.core.annotations.ParameterObject; + +import java.io.Serializable; +import java.util.Objects; + +/** + * Criteria class for the {@link org.citrusframework.simulator.model.TestResult} entity. This class is used + * in {@link org.citrusframework.simulator.web.rest.TestResultResource} to receive all the possible filtering options + * from the Http GET request parameters. + *

+ * For example the following could be a valid request: + * {@code /test-results?id.greaterThan=5&attr1.contains=something&attr2.specified=false} + *

+ * As Spring is unable to properly convert the types, unless + * specific {@link org.citrusframework.simulator.service.filter.Filter} class are used, we need to use fix type specific + * filters. + */ +@ParameterObject +@SuppressWarnings("common-java:DuplicatedBlocks") +public class TestResultCriteria implements Serializable, Criteria { + + private static final long serialVersionUID = 1L; + + private LongFilter id; + + private IntegerFilter status; + + private StringFilter testName; + + private StringFilter className; + + private StringFilter errorMessage; + + private StringFilter failureStack; + + private StringFilter failureType; + + private InstantFilter createdDate; + + private InstantFilter lastModifiedDate; + + private StringFilter testParameterKey; + + private Boolean distinct; + + public TestResultCriteria() {} + + public TestResultCriteria(TestResultCriteria other) { + this.id = other.id == null ? null : other.id.copy(); + this.status = other.status == null ? null : other.status.copy(); + this.testName = other.testName == null ? null : other.testName.copy(); + this.className = other.className == null ? null : other.className.copy(); + this.errorMessage = other.errorMessage == null ? null : other.errorMessage.copy(); + this.failureStack = other.failureStack == null ? null : other.failureStack.copy(); + this.failureType = other.failureType == null ? null : other.failureType.copy(); + this.createdDate = other.createdDate == null ? null : other.createdDate.copy(); + this.lastModifiedDate = other.lastModifiedDate == null ? null : other.lastModifiedDate.copy(); + this.testParameterKey = other.testParameterKey == null ? null : other.testParameterKey.copy(); + this.distinct = other.distinct; + } + + @Override + public TestResultCriteria copy() { + return new TestResultCriteria(this); + } + + public LongFilter getId() { + return id; + } + + public LongFilter id() { + if (id == null) { + id = new LongFilter(); + } + return id; + } + + public void setId(LongFilter id) { + this.id = id; + } + + public IntegerFilter getStatus() { + return status; + } + + public IntegerFilter status() { + if (status == null) { + status = new IntegerFilter(); + } + return status; + } + + public void setStatus(IntegerFilter status) { + this.status = status; + } + + public StringFilter getTestName() { + return testName; + } + + public StringFilter testName() { + if (testName == null) { + testName = new StringFilter(); + } + return testName; + } + + public void setTestName(StringFilter testName) { + this.testName = testName; + } + + public StringFilter getClassName() { + return className; + } + + public StringFilter className() { + if (className == null) { + className = new StringFilter(); + } + return className; + } + + public void setClassName(StringFilter className) { + this.className = className; + } + + public StringFilter getErrorMessage() { + return errorMessage; + } + + public StringFilter errorMessage() { + if (errorMessage == null) { + errorMessage = new StringFilter(); + } + return errorMessage; + } + + public void setErrorMessage(StringFilter errorMessage) { + this.errorMessage = errorMessage; + } + + public StringFilter getFailureStack() { + return failureStack; + } + + public StringFilter failureStack() { + if (failureStack == null) { + failureStack = new StringFilter(); + } + return failureStack; + } + + public void setFailureStack(StringFilter failureStack) { + this.failureStack = failureStack; + } + + public StringFilter getFailureType() { + return failureType; + } + + public StringFilter failureType() { + if (failureType == null) { + failureType = new StringFilter(); + } + return failureType; + } + + public void setFailureType(StringFilter failureType) { + this.failureType = failureType; + } + + public InstantFilter getCreatedDate() { + return createdDate; + } + + public InstantFilter createdDate() { + if (createdDate == null) { + createdDate = new InstantFilter(); + } + return createdDate; + } + + public void setCreatedDate(InstantFilter createdDate) { + this.createdDate = createdDate; + } + + public InstantFilter getLastModifiedDate() { + return lastModifiedDate; + } + + public InstantFilter lastModifiedDate() { + if (lastModifiedDate == null) { + lastModifiedDate = new InstantFilter(); + } + return lastModifiedDate; + } + + public void setLastModifiedDate(InstantFilter lastModifiedDate) { + this.lastModifiedDate = lastModifiedDate; + } + + public StringFilter getTestParameterKey() { + return testParameterKey; + } + + public StringFilter testParameterId() { + if (testParameterKey == null) { + testParameterKey = new StringFilter(); + } + return testParameterKey; + } + + public void setTestParameterKey(StringFilter testParameterKey) { + this.testParameterKey = testParameterKey; + } + + public Boolean getDistinct() { + return distinct; + } + + public void setDistinct(Boolean distinct) { + this.distinct = distinct; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final TestResultCriteria that = (TestResultCriteria) o; + return ( + Objects.equals(id, that.id) && + Objects.equals(status, that.status) && + Objects.equals(testName, that.testName) && + Objects.equals(className, that.className) && + Objects.equals(errorMessage, that.errorMessage) && + Objects.equals(failureStack, that.failureStack) && + Objects.equals(failureType, that.failureType) && + Objects.equals(createdDate, that.createdDate) && + Objects.equals(lastModifiedDate, that.lastModifiedDate) && + Objects.equals(testParameterKey, that.testParameterKey) && + Objects.equals(distinct, that.distinct) + ); + } + + @Override + public int hashCode() { + return Objects.hash( + id, + status, + testName, + className, + errorMessage, + failureStack, + failureType, + createdDate, + lastModifiedDate, + testParameterKey, + distinct + ); + } + + // prettier-ignore + @Override + public String toString() { + return "TestResultCriteria{" + + (id != null ? "id=" + id + ", " : "") + + (status != null ? "status=" + status + ", " : "") + + (testName != null ? "testName=" + testName + ", " : "") + + (className != null ? "className=" + className + ", " : "") + + (errorMessage != null ? "errorMessage=" + errorMessage + ", " : "") + + (failureStack != null ? "failureStack=" + failureStack + ", " : "") + + (failureType != null ? "failureType=" + failureType + ", " : "") + + (createdDate != null ? "createdDate=" + createdDate + ", " : "") + + (lastModifiedDate != null ? "lastModifiedDate=" + lastModifiedDate + ", " : "") + + (testParameterKey != null ? "testParameterId=" + testParameterKey + ", " : "") + + (distinct != null ? "distinct=" + distinct + ", " : "") + + "}"; + } +} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/dto/TestResultByStatus.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/dto/TestResultByStatus.java new file mode 100644 index 000000000..73e6794af --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/dto/TestResultByStatus.java @@ -0,0 +1,10 @@ +package org.citrusframework.simulator.service.dto; + +import java.util.Objects; + +public record TestResultByStatus(Long successful, Long failed, Long total) { + + public TestResultByStatus(Long successful, Long failed) { + this(Objects.isNull(successful) ? 0 : successful, Objects.isNull(failed)?0: failed, Objects.isNull(successful) || Objects.isNull(failed) ? 0: successful + failed); + } +} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/Filter.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/Filter.java new file mode 100644 index 000000000..92c81f36d --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/Filter.java @@ -0,0 +1,200 @@ +package org.citrusframework.simulator.service.filter; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Base class for the various attribute filters. It can be added to a criteria class as a member, to support the + * following query parameters: + *

+ *      fieldName.equals='something'
+ *      fieldName.notEquals='somethingElse'
+ *      fieldName.specified=true
+ *      fieldName.specified=false
+ *      fieldName.in='something','other'
+ *      fieldName.notIn='something','other'
+ * 
+ */ +public class Filter implements Serializable { + + private static final long serialVersionUID = 1L; + private FIELD_TYPE equals; + private FIELD_TYPE notEquals; + private Boolean specified; + private List in; + private List notIn; + + /** + *

Constructor for Filter.

+ */ + public Filter() { + } + + /** + *

Constructor for Filter.

+ * + * @param filter a {@link Filter} object. + */ + public Filter(Filter filter) { + equals = filter.equals; + notEquals = filter.notEquals; + specified = filter.specified; + in = filter.in == null ? null : new ArrayList<>(filter.in); + notIn = filter.notIn == null ? null : new ArrayList<>(filter.notIn); + } + + /** + *

copy.

+ * + * @return a {@link Filter} object. + */ + public Filter copy() { + return new Filter<>(this); + } + + /** + *

Getter for the field equals.

+ * + * @return a FIELD_TYPE object. + */ + public FIELD_TYPE getEquals() { + return equals; + } + + /** + *

Setter for the field equals.

+ * + * @param equals a FIELD_TYPE object. + * @return a {@link Filter} object. + */ + public Filter setEquals(FIELD_TYPE equals) { + this.equals = equals; + return this; + } + + /** + *

Getter for the field notEquals.

+ * + * @return a FIELD_TYPE object. + */ + public FIELD_TYPE getNotEquals() { + return notEquals; + } + + /** + *

Setter for the field notEquals.

+ * + * @param notEquals a FIELD_TYPE object. + * @return a {@link Filter} object. + */ + public Filter setNotEquals(FIELD_TYPE notEquals) { + this.notEquals = notEquals; + return this; + } + + /** + *

Getter for the field specified.

+ * + * @return a {@link java.lang.Boolean} object. + */ + public Boolean getSpecified() { + return specified; + } + + /** + *

Setter for the field specified.

+ * + * @param specified a {@link java.lang.Boolean} object. + * @return a {@link Filter} object. + */ + public Filter setSpecified(Boolean specified) { + this.specified = specified; + return this; + } + + /** + *

Getter for the field in.

+ * + * @return a {@link java.util.List} object. + */ + public List getIn() { + return in; + } + + /** + *

Setter for the field in.

+ * + * @param in a {@link java.util.List} object. + * @return a {@link Filter} object. + */ + public Filter setIn(List in) { + this.in = in; + return this; + } + + /** + *

Getter for the field notIn.

+ * + * @return a {@link java.util.List} object. + */ + public List getNotIn() { + return notIn; + } + + /** + *

Setter for the field notIn.

+ * + * @param notIn a {@link java.util.List} object. + * @return a {@link Filter} object. + */ + public Filter setNotIn(List notIn) { + this.notIn = notIn; + return this; + } + + /** {@inheritDoc} */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Filter filter = (Filter) o; + return Objects.equals(equals, filter.equals) && + Objects.equals(notEquals, filter.notEquals) && + Objects.equals(specified, filter.specified) && + Objects.equals(in, filter.in) && + Objects.equals(notIn, filter.notIn); + } + + /** {@inheritDoc} */ + @Override + public int hashCode() { + return Objects.hash(equals, notEquals, specified, in, notIn); + } + + /** {@inheritDoc} */ + @Override + public String toString() { + return getFilterName() + " [" + + (getEquals() != null ? "equals=" + getEquals() + ", " : "") + + (getNotEquals() != null ? "notEquals=" + getNotEquals() + ", " : "") + + (getSpecified() != null ? "specified=" + getSpecified() + ", " : "") + + (getIn() != null ? "in=" + getIn() + ", " : "") + + (getNotIn() != null ? "notIn=" + getNotIn() : "") + + "]"; + } + + /** + *

getFilterName.

+ * + * @return a {@link java.lang.String} object. + */ + protected String getFilterName() { + return getClass().getSimpleName(); + } +} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/InstantFilter.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/InstantFilter.java new file mode 100644 index 000000000..bb5b0f3f9 --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/InstantFilter.java @@ -0,0 +1,101 @@ +package org.citrusframework.simulator.service.filter; + +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.Instant; +import java.util.List; + +/** + * Filter class for {@link java.time.Instant} type attributes. + * + * @see RangeFilter + */ +public class InstantFilter extends RangeFilter { + + private static final long serialVersionUID = 1L; + + /** + *

Constructor for InstantFilter.

+ */ + public InstantFilter() { + } + + /** + *

Constructor for InstantFilter.

+ * + * @param filter a {@link InstantFilter} object. + */ + public InstantFilter(InstantFilter filter) { + super(filter); + } + + /** {@inheritDoc} */ + @Override + public InstantFilter copy() { + return new InstantFilter(this); + } + + /** {@inheritDoc} */ + @Override + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + public InstantFilter setEquals(Instant equals) { + super.setEquals(equals); + return this; + } + + /** {@inheritDoc} */ + @Override + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + public InstantFilter setNotEquals(Instant equals) { + super.setNotEquals(equals); + return this; + } + + /** {@inheritDoc} */ + @Override + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + public InstantFilter setIn(List in) { + super.setIn(in); + return this; + } + + /** {@inheritDoc} */ + @Override + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + public InstantFilter setNotIn(List notIn) { + super.setNotIn(notIn); + return this; + } + + /** {@inheritDoc} */ + @Override + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + public InstantFilter setGreaterThan(Instant equals) { + super.setGreaterThan(equals); + return this; + } + + /** {@inheritDoc} */ + @Override + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + public InstantFilter setLessThan(Instant equals) { + super.setLessThan(equals); + return this; + } + + /** {@inheritDoc} */ + @Override + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + public InstantFilter setGreaterThanOrEqual(Instant equals) { + super.setGreaterThanOrEqual(equals); + return this; + } + + /** {@inheritDoc} */ + @Override + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + public InstantFilter setLessThanOrEqual(Instant equals) { + super.setLessThanOrEqual(equals); + return this; + } +} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/IntegerFilter.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/IntegerFilter.java new file mode 100644 index 000000000..2d0e1374b --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/IntegerFilter.java @@ -0,0 +1,35 @@ +package org.citrusframework.simulator.service.filter; + +/** + * Filter class for {@link java.lang.Integer} type attributes. + * + * @see RangeFilter + */ +public class IntegerFilter extends RangeFilter { + + private static final long serialVersionUID = 1L; + + /** + *

Constructor for IntegerFilter.

+ */ + public IntegerFilter() { + } + + /** + *

Constructor for IntegerFilter.

+ * + * @param filter a {@link IntegerFilter} object. + */ + public IntegerFilter(IntegerFilter filter) { + super(filter); + } + + /** + *

copy.

+ * + * @return a {@link IntegerFilter} object. + */ + public IntegerFilter copy() { + return new IntegerFilter(this); + } +} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/LongFilter.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/LongFilter.java new file mode 100644 index 000000000..579398c33 --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/LongFilter.java @@ -0,0 +1,35 @@ +package org.citrusframework.simulator.service.filter; + +/** + * Filter class for {@link java.lang.Long} type attributes. + * + * @see RangeFilter + */ +public class LongFilter extends RangeFilter { + + private static final long serialVersionUID = 1L; + + /** + *

Constructor for LongFilter.

+ */ + public LongFilter() { + } + + /** + *

Constructor for LongFilter.

+ * + * @param filter a {@link LongFilter} object. + */ + public LongFilter(LongFilter filter) { + super(filter); + } + + /** + *

copy.

+ * + * @return a {@link LongFilter} object. + */ + public LongFilter copy() { + return new LongFilter(this); + } +} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/RangeFilter.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/RangeFilter.java new file mode 100644 index 000000000..7c2da9665 --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/RangeFilter.java @@ -0,0 +1,182 @@ +package org.citrusframework.simulator.service.filter; + +import java.util.Objects; + +/** + * Filter class for Comparable types, where less than / greater than / etc relations could be interpreted. It can be + * added to a criteria class as a member, to support the following query parameters: + *
+ *      fieldName.equals=42
+ *      fieldName.notEquals=42
+ *      fieldName.specified=true
+ *      fieldName.specified=false
+ *      fieldName.in=43,42
+ *      fieldName.notIn=43,42
+ *      fieldName.greaterThan=41
+ *      fieldName.lessThan=44
+ *      fieldName.greaterThanOrEqual=42
+ *      fieldName.lessThanOrEqual=44
+ * 
+ * Due to problems with the type conversions, the descendant classes should be used, where the generic type parameter + * is materialized. + * + * @param the type of filter. + * @see IntegerFilter + * @see LongFilter + * @see InstantFilter + */ +public class RangeFilter> extends Filter { + + private static final long serialVersionUID = 1L; + + private FIELD_TYPE greaterThan; + private FIELD_TYPE lessThan; + private FIELD_TYPE greaterThanOrEqual; + private FIELD_TYPE lessThanOrEqual; + + /** + *

Constructor for RangeFilter.

+ */ + public RangeFilter() { + } + + /** + *

Constructor for RangeFilter.

+ * + * @param filter a {@link RangeFilter} object. + */ + public RangeFilter(RangeFilter filter) { + super(filter); + greaterThan = filter.greaterThan; + lessThan = filter.lessThan; + greaterThanOrEqual = filter.greaterThanOrEqual; + lessThanOrEqual = filter.lessThanOrEqual; + } + + /** {@inheritDoc} */ + @Override + public RangeFilter copy() { + return new RangeFilter<>(this); + } + + /** + *

Getter for the field greaterThan.

+ * + * @return a FIELD_TYPE object. + */ + public FIELD_TYPE getGreaterThan() { + return greaterThan; + } + + /** + *

Setter for the field greaterThan.

+ * + * @param greaterThan a FIELD_TYPE object. + * @return a {@link RangeFilter} object. + */ + public RangeFilter setGreaterThan(FIELD_TYPE greaterThan) { + this.greaterThan = greaterThan; + return this; + } + + /** + *

Getter for the field lessThan.

+ * + * @return a FIELD_TYPE object. + */ + public FIELD_TYPE getLessThan() { + return lessThan; + } + + /** + *

Setter for the field lessThan.

+ * + * @param lessThan a FIELD_TYPE object. + * @return a {@link RangeFilter} object. + */ + public RangeFilter setLessThan(FIELD_TYPE lessThan) { + this.lessThan = lessThan; + return this; + } + + /** + *

Getter for the field greaterThanOrEqual.

+ * + * @return a FIELD_TYPE object. + */ + public FIELD_TYPE getGreaterThanOrEqual() { + return greaterThanOrEqual; + } + + /** + *

Setter for the field greaterThanOrEqual.

+ * + * @param greaterThanOrEqual a FIELD_TYPE object. + * @return a {@link RangeFilter} object. + */ + public RangeFilter setGreaterThanOrEqual(FIELD_TYPE greaterThanOrEqual) { + this.greaterThanOrEqual = greaterThanOrEqual; + return this; + } + + /** + *

Getter for the field lessThanOrEqual.

+ * + * @return a FIELD_TYPE object. + */ + public FIELD_TYPE getLessThanOrEqual() { + return lessThanOrEqual; + } + + /** + *

Setter for the field lessThanOrEqual.

+ * + * @param lessThanOrEqual a FIELD_TYPE object. + * @return a {@link RangeFilter} object. + */ + public RangeFilter setLessThanOrEqual(FIELD_TYPE lessThanOrEqual) { + this.lessThanOrEqual = lessThanOrEqual; + return this; + } + + /** {@inheritDoc} */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + RangeFilter that = (RangeFilter) o; + return Objects.equals(greaterThan, that.greaterThan) && + Objects.equals(lessThan, that.lessThan) && + Objects.equals(greaterThanOrEqual, that.greaterThanOrEqual) && + Objects.equals(lessThanOrEqual, that.lessThanOrEqual); + } + + /** {@inheritDoc} */ + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), greaterThan, lessThan, greaterThanOrEqual, lessThanOrEqual); + } + + /** {@inheritDoc} */ + @Override + public String toString() { + return getFilterName() + " [" + + (getEquals() != null ? "equals=" + getEquals() + ", " : "") + + (getNotEquals() != null ? "notEquals=" + getNotEquals() + ", " : "") + + (getSpecified() != null ? "specified=" + getSpecified() + ", " : "") + + (getIn() != null ? "in=" + getIn() + ", " : "") + + (getNotIn() != null ? "notIn=" + getNotIn() + ", " : "") + + (getGreaterThan() != null ? "greaterThan=" + getGreaterThan() + ", " : "") + + (getLessThan() != null ? "lessThan=" + getLessThan() + ", " : "") + + (getGreaterThanOrEqual() != null ? "greaterThanOrEqual=" + getGreaterThanOrEqual() + ", " : "") + + (getLessThanOrEqual() != null ? "lessThanOrEqual=" + getLessThanOrEqual() : "") + + "]"; + } +} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/StringFilter.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/StringFilter.java new file mode 100644 index 000000000..02393a4f4 --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/StringFilter.java @@ -0,0 +1,125 @@ +package org.citrusframework.simulator.service.filter; + +import java.util.Objects; + +/** + * Class for filtering attributes with {@link java.lang.String} type. + * It can be added to a criteria class as a member, to support the following query parameters: + * + * fieldName.equals='something' + * fieldName.notEquals='something' + * fieldName.specified=true + * fieldName.specified=false + * fieldName.in='something','other' + * fieldName.notIn='something','other' + * fieldName.contains='thing' + * fieldName.doesNotContain='thing' + * + */ +public class StringFilter extends Filter { + + private static final long serialVersionUID = 1L; + + private String contains; + private String doesNotContain; + + /** + *

Constructor for StringFilter.

+ */ + public StringFilter() { + } + + /** + *

Constructor for StringFilter.

+ * + * @param filter a {@link StringFilter} object. + */ + public StringFilter(StringFilter filter) { + super(filter); + contains = filter.contains; + doesNotContain = filter.doesNotContain; + } + + /** {@inheritDoc} */ + @Override + public StringFilter copy() { + return new StringFilter(this); + } + + /** + *

Getter for the field contains.

+ * + * @return a {@link java.lang.String} object. + */ + public String getContains() { + return contains; + } + + /** + *

Setter for the field contains.

+ * + * @param contains a {@link java.lang.String} object. + * @return a {@link StringFilter} object. + */ + public StringFilter setContains(String contains) { + this.contains = contains; + return this; + } + + /** + *

Getter for the field doesNotContain.

+ * + * @return a {@link java.lang.String} object. + */ + public String getDoesNotContain() { + return doesNotContain; + } + + /** + *

Setter for the field doesNotContain.

+ * + * @param doesNotContain a {@link java.lang.String} object. + * @return a {@link StringFilter} object. + */ + public StringFilter setDoesNotContain(String doesNotContain) { + this.doesNotContain = doesNotContain; + return this; + } + + /** {@inheritDoc} */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + StringFilter that = (StringFilter) o; + return Objects.equals(contains, that.contains) && + Objects.equals(doesNotContain, that.doesNotContain); + } + + /** {@inheritDoc} */ + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), contains, doesNotContain); + } + + /** {@inheritDoc} */ + @Override + public String toString() { + return getFilterName() + " [" + + (getEquals() != null ? "equals=" + getEquals() + ", " : "") + + (getNotEquals() != null ? "notEquals=" + getNotEquals() + ", " : "") + + (getSpecified() != null ? "specified=" + getSpecified() + ", " : "") + + (getIn() != null ? "in=" + getIn() + ", " : "") + + (getNotIn() != null ? "notIn=" + getNotIn() + ", " : "") + + (getContains() != null ? "contains=" + getContains() + ", " : "") + + (getDoesNotContain() != null ? "doesNotContain=" + getDoesNotContain() : "") + + "]"; + } +} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/TestParameterServiceImpl.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/TestParameterServiceImpl.java new file mode 100644 index 000000000..83c41591e --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/TestParameterServiceImpl.java @@ -0,0 +1,53 @@ +package org.citrusframework.simulator.service.impl; + +import org.citrusframework.simulator.model.TestParameter; +import org.citrusframework.simulator.repository.TestParameterRepository; +import org.citrusframework.simulator.service.TestParameterService; +import org.citrusframework.simulator.service.TestResultService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +/** + * Service Implementation for managing {@link org.citrusframework.simulator.model.TestParameter}. + */ +@Service +@Transactional +public class TestParameterServiceImpl implements TestParameterService { + + private final Logger logger = LoggerFactory.getLogger(TestParameterServiceImpl.class); + + private final TestResultService testResultService; + private final TestParameterRepository testParameterRepository; + + public TestParameterServiceImpl(TestResultService testResultService, TestParameterRepository testParameterRepository) { + this.testResultService = testResultService; + this.testParameterRepository = testParameterRepository; + } + + @Override + public TestParameter save(TestParameter testParameter) { + logger.debug("Request to save TestParameter : {}", testParameter); + return testParameterRepository.save(testParameter); + } + + @Override + @Transactional(readOnly = true) + public Page findAll(Pageable pageable) { + logger.debug("Request to get all TestParameters"); + return testParameterRepository.findAll(pageable); + } + + @Override + @Transactional(readOnly = true) + public Optional findOne(Long testResultId, String key) { + logger.debug("Request to get TestParameter '{}' of TestResult : {}", key, testResultId); +return testResultService.findOne(testResultId) + .flatMap(testResult -> testParameterRepository.findById(new TestParameter.TestParameterId(key, testResult))); + } +} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/TestResultServiceImpl.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/TestResultServiceImpl.java new file mode 100644 index 000000000..7c84544f6 --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/TestResultServiceImpl.java @@ -0,0 +1,63 @@ +package org.citrusframework.simulator.service.impl; + +import org.citrusframework.simulator.model.TestResult; +import org.citrusframework.simulator.repository.TestResultRepository; +import org.citrusframework.simulator.service.TestResultService; +import org.citrusframework.simulator.service.dto.TestResultByStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +/** + * Service Implementation for managing {@link org.citrusframework.simulator.model.TestResult}. + */ +@Service +@Transactional +public class TestResultServiceImpl implements TestResultService { + + private final Logger logger = LoggerFactory.getLogger(TestResultServiceImpl.class); + + private final TestResultRepository testResultRepository; + + public TestResultServiceImpl(TestResultRepository testResultRepository) { + this.testResultRepository = testResultRepository; + } + + @Override + public TestResult transformAndSave(org.citrusframework.TestResult testResult) { + logger.debug("Request to save citrus TestResult : {}", testResult); + return save(new TestResult(testResult)); + } + + @Override + public TestResult save(TestResult testResult) { + logger.debug("Request to save TestResult : {}", testResult); + return testResultRepository.save(testResult); + } + + @Override + @Transactional(readOnly = true) + public Page findAll(Pageable pageable) { + logger.debug("Request to get all TestResults"); + return testResultRepository.findAll(pageable); + } + + @Override + @Transactional(readOnly = true) + public Optional findOne(Long id) { + logger.debug("Request to get TestResult : {}", id); + return testResultRepository.findById(id); + } + + @Override + @Transactional(readOnly = true) + public TestResultByStatus countByStatus() { + logger.debug("count total by status"); + return testResultRepository.countByStatus(); + } +} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/TestParameterResource.java b/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/TestParameterResource.java new file mode 100644 index 000000000..5e4913e4f --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/TestParameterResource.java @@ -0,0 +1,85 @@ +package org.citrusframework.simulator.web.rest; + +import org.citrusframework.simulator.model.TestParameter; +import org.citrusframework.simulator.service.TestParameterQueryService; +import org.citrusframework.simulator.service.TestParameterService; +import org.citrusframework.simulator.service.criteria.TestParameterCriteria; +import org.citrusframework.simulator.web.util.PaginationUtil; +import org.citrusframework.simulator.web.util.ResponseUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.util.List; +import java.util.Optional; + +/** + * REST controller for managing {@link org.citrusframework.simulator.model.TestParameter}. + */ +@RestController +@RequestMapping("/api") +public class TestParameterResource { + + private final Logger logger = LoggerFactory.getLogger(TestParameterResource.class); + + private final TestParameterService testParameterService; + + private final TestParameterQueryService testParameterQueryService; + + public TestParameterResource(TestParameterService testParameterService, TestParameterQueryService testParameterQueryService) { + this.testParameterService = testParameterService; + this.testParameterQueryService = testParameterQueryService; + } + + /** + * {@code GET /test-parameters} : get all the testParameters. + * + * @param pageable the pagination information. + * @param criteria the criteria which the requested entities should match. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of testParameters in body. + */ + @GetMapping("/test-parameters") + public ResponseEntity> getAllTestParameters( + TestParameterCriteria criteria, + @org.springdoc.core.annotations.ParameterObject Pageable pageable + ) { + logger.debug("REST request to get TestParameters by criteria: {}", criteria); + + Page page = testParameterQueryService.findByCriteria(criteria, pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); + return ResponseEntity.ok().headers(headers).body(page.getContent()); + } + + /** + * {@code GET /test-parameters/count} : count all the testParameters. + * + * @param criteria the criteria which the requested entities should match. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the count in body. + */ + @GetMapping("/test-parameters/count") + public ResponseEntity countTestParameters(TestParameterCriteria criteria) { + logger.debug("REST request to count TestParameters by criteria: {}", criteria); + return ResponseEntity.ok().body(testParameterQueryService.countByCriteria(criteria)); + } + + /** + * {@code GET /test-parameters/:id} : get the "id" testParameter. + * + * @param id the id of the testParameter to retrieve. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the testParameter, or with status {@code 404 (Not Found)}. + */ + @GetMapping("/test-parameters/{testResultId}/{key}") + public ResponseEntity getTestParameter(@PathVariable Long testResultId, @PathVariable String key) { + logger.debug("REST request to get TestParameter '{}' of TestResult: {}", key, testResultId); + Optional testParameter = testParameterService.findOne(testResultId, key); + return ResponseUtil.wrapOrNotFound(testParameter); + } +} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/TestResultResource.java b/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/TestResultResource.java new file mode 100644 index 000000000..aab7f553b --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/TestResultResource.java @@ -0,0 +1,97 @@ +package org.citrusframework.simulator.web.rest; + +import org.citrusframework.simulator.model.TestResult; +import org.citrusframework.simulator.service.TestResultQueryService; +import org.citrusframework.simulator.service.TestResultService; +import org.citrusframework.simulator.service.criteria.TestResultCriteria; +import org.citrusframework.simulator.service.dto.TestResultByStatus; +import org.citrusframework.simulator.web.util.PaginationUtil; +import org.citrusframework.simulator.web.util.ResponseUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.util.List; +import java.util.Optional; + +/** + * REST controller for managing {@link org.citrusframework.simulator.model.TestResult}. + */ +@RestController +@RequestMapping("/api") +public class TestResultResource { + + private final Logger logger = LoggerFactory.getLogger(TestResultResource.class); + + private final TestResultService testResultService; + + private final TestResultQueryService testResultQueryService; + + public TestResultResource(TestResultService testResultService, TestResultQueryService testResultQueryService) { + this.testResultService = testResultService; + this.testResultQueryService = testResultQueryService; + } + + /** + * {@code GET /test-results} : get all the testResults. + * + * @param pageable the pagination information. + * @param criteria the criteria which the requested entities should match. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of testResults in body. + */ + @GetMapping("/test-results") + public ResponseEntity> getAllTestResults( + TestResultCriteria criteria, + @org.springdoc.core.annotations.ParameterObject Pageable pageable + ) { + logger.debug("REST request to get TestResults by criteria: {}", criteria); + + Page page = testResultQueryService.findByCriteria(criteria, pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); + return ResponseEntity.ok().headers(headers).body(page.getContent()); + } + + /** + * {@code GET /test-results/count} : count all the testResults. + * + * @param criteria the criteria which the requested entities should match. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the count in body. + */ + @GetMapping("/test-results/count") + public ResponseEntity countTestResults(TestResultCriteria criteria) { + logger.debug("REST request to count TestResults by criteria: {}", criteria); + return ResponseEntity.ok().body(testResultQueryService.countByCriteria(criteria)); + } + + /** + * {@code GET /test-results/count-by-status} : count all the testResults by their status. + * + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the count in body. + */ + @GetMapping("/test-results/count-by-status") + public ResponseEntity countTestResultsByStatus() { + logger.debug("REST request to count total TestResults by status"); + return ResponseEntity.ok().body(testResultService.countByStatus()); + } + + /** + * {@code GET /test-results/:id} : get the "id" testResult. + * + * @param id the id of the testResult to retrieve. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the testResult, or with status {@code 404 (Not Found)}. + */ + @GetMapping("/test-results/{id}") + public ResponseEntity getTestResult(@PathVariable Long id) { + logger.debug("REST request to get TestResult : {}", id); + Optional testResult = testResultService.findOne(id); + return ResponseUtil.wrapOrNotFound(testResult); + } +} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/web/util/PaginationUtil.java b/simulator-starter/src/main/java/org/citrusframework/simulator/web/util/PaginationUtil.java new file mode 100644 index 000000000..a12ec24e9 --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/web/util/PaginationUtil.java @@ -0,0 +1,62 @@ +package org.citrusframework.simulator.web.util; + +import org.springframework.data.domain.Page; +import org.springframework.http.HttpHeaders; +import org.springframework.web.util.UriComponentsBuilder; + +import java.text.MessageFormat; + + +/** + * Utility class for handling pagination. + * + *
+
+
+

+ Test Parameter +

+ +
+ + + + + +
+
Key
+
+ {{ testParameter.key }} +
+
Value
+
+ {{ testParameter.value }} +
+
Created Date
+
+ {{ testParameter.createdDate | formatMediumDatetime }} +
+
Last Modified Date
+
+ {{ testParameter.lastModifiedDate | formatMediumDatetime }} +
+
Test Result
+
+ {{ testParameter.testResult.id }} +
+
+ + +
+
+
diff --git a/simulator-ui/src/main/webapp/app/entities/test-parameter/detail/test-parameter-detail.component.spec.ts b/simulator-ui/src/main/webapp/app/entities/test-parameter/detail/test-parameter-detail.component.spec.ts new file mode 100644 index 000000000..3e4057e8b --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-parameter/detail/test-parameter-detail.component.spec.ts @@ -0,0 +1,38 @@ +import { TestBed } from '@angular/core/testing'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness, RouterTestingModule } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { TestParameterDetailComponent } from './test-parameter-detail.component'; + +describe('TestParameter Management Detail Component', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TestParameterDetailComponent, RouterTestingModule.withRoutes([], { bindToComponentInputs: true })], + providers: [ + provideRouter( + [ + { + path: '**', + component: TestParameterDetailComponent, + resolve: { testParameter: () => of({ id: 123 }) }, + }, + ], + withComponentInputBinding(), + ), + ], + }) + .overrideTemplate(TestParameterDetailComponent, '') + .compileComponents(); + }); + + describe('OnInit', () => { + it('Should load testParameter on init', async () => { + const harness = await RouterTestingHarness.create(); + const instance = await harness.navigateByUrl('/', TestParameterDetailComponent); + + // THEN + expect(instance.testParameter).toEqual(expect.objectContaining({ id: 123 })); + }); + }); +}); diff --git a/simulator-ui/src/main/webapp/app/entities/test-parameter/detail/test-parameter-detail.component.ts b/simulator-ui/src/main/webapp/app/entities/test-parameter/detail/test-parameter-detail.component.ts new file mode 100644 index 000000000..20f86d5bb --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-parameter/detail/test-parameter-detail.component.ts @@ -0,0 +1,22 @@ +import { Component, Input } from '@angular/core'; +import { ActivatedRoute, RouterModule } from '@angular/router'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { ITestParameter } from '../test-parameter.model'; + +@Component({ + standalone: true, + selector: 'jhi-test-parameter-detail', + templateUrl: './test-parameter-detail.component.html', + imports: [SharedModule, RouterModule, DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe], +}) +export class TestParameterDetailComponent { + @Input() testParameter: ITestParameter | null = null; + + constructor(protected activatedRoute: ActivatedRoute) {} + + previousState(): void { + window.history.back(); + } +} diff --git a/simulator-ui/src/main/webapp/app/entities/test-parameter/list/test-parameter.component.html b/simulator-ui/src/main/webapp/app/entities/test-parameter/list/test-parameter.component.html new file mode 100644 index 000000000..96648b2e1 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-parameter/list/test-parameter.component.html @@ -0,0 +1,104 @@ +
+

+ Test Parameters + +
+ +
+

+ + + + + + + +
+ No Test Parameters found +
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+
+ Key + +
+
+
+ Value + +
+
+
+ Created Date + +
+
+
+ Last Modified Date + +
+
+
+ Test Result + +
+
{{ testParameter.key }}{{ testParameter.value }}{{ testParameter.createdDate | formatMediumDatetime }}{{ testParameter.lastModifiedDate | formatMediumDatetime }} + {{ testParameter.testResult.id }} + +
+ +
+
+
+ +
+
+ +
+ +
+ +
+
+
diff --git a/simulator-ui/src/main/webapp/app/entities/test-parameter/list/test-parameter.component.spec.ts b/simulator-ui/src/main/webapp/app/entities/test-parameter/list/test-parameter.component.spec.ts new file mode 100644 index 000000000..b0fb437ef --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-parameter/list/test-parameter.component.spec.ts @@ -0,0 +1,125 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRoute } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { TestParameterService } from '../service/test-parameter.service'; + +import { TestParameterComponent } from './test-parameter.component'; +import SpyInstance = jest.SpyInstance; + +describe('TestParameter Management Component', () => { + let comp: TestParameterComponent; + let fixture: ComponentFixture; + let service: TestParameterService; + let routerNavigateSpy: SpyInstance>; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule.withRoutes([{ path: 'test-parameter', component: TestParameterComponent }]), + HttpClientTestingModule, + TestParameterComponent, + ], + providers: [ + { + provide: ActivatedRoute, + useValue: { + data: of({ + defaultSort: 'createdDate,desc', + }), + queryParamMap: of( + jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'createdDate,desc', + 'filter[someKey.in]': 'dc4279ea-cfb9-11ec-9d64-0242ac120002', + }), + ), + snapshot: { queryParams: {} }, + }, + }, + ], + }) + .overrideTemplate(TestParameterComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(TestParameterComponent); + comp = fixture.componentInstance; + service = TestBed.inject(TestParameterService); + routerNavigateSpy = jest.spyOn(comp.router, 'navigate'); + + const headers = new HttpHeaders(); + jest.spyOn(service, 'query').mockReturnValue( + of( + new HttpResponse({ + body: [{ key: 'key', testResult: { id: 123 } }], + headers, + }), + ), + ); + }); + + it('Should call load all on init', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenCalled(); + expect(comp.testParameters?.[0]).toEqual(expect.objectContaining({ key: 'key', testResult: { id: 123 } })); + }); + + describe('trackId', () => { + it('Should forward to testParameterService', () => { + const entity = { key: 'key', testResult: { id: 123 } }; + jest.spyOn(service, 'getTestParameterIdentifier'); + const id = comp.trackId(0, entity); + expect(service.getTestParameterIdentifier).toHaveBeenCalledWith(entity); + expect(id).toBe(2018011204); // This is a hash of the composite primary key + }); + }); + + it('should load a page', () => { + // WHEN + comp.navigateToPage(1); + + // THEN + expect(routerNavigateSpy).toHaveBeenCalled(); + }); + + it('should calculate the sort attribute for an id', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenLastCalledWith(expect.objectContaining({ sort: ['createdDate,desc'] })); + }); + + it('should calculate the sort attribute for a non-id attribute', () => { + // GIVEN + comp.predicate = 'name'; + + // WHEN + comp.navigateToWithComponentValues(); + + // THEN + expect(routerNavigateSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + queryParams: expect.objectContaining({ + sort: ['name,asc'], + }), + }), + ); + }); + + it('should calculate the filter attribute', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenLastCalledWith(expect.objectContaining({ 'someKey.in': ['dc4279ea-cfb9-11ec-9d64-0242ac120002'] })); + }); +}); diff --git a/simulator-ui/src/main/webapp/app/entities/test-parameter/list/test-parameter.component.ts b/simulator-ui/src/main/webapp/app/entities/test-parameter/list/test-parameter.component.ts new file mode 100644 index 000000000..06dc18098 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-parameter/list/test-parameter.component.ts @@ -0,0 +1,154 @@ +import { Component, NgZone, OnInit } from '@angular/core'; +import { HttpHeaders } from '@angular/common/http'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, Observable, switchMap, tap } from 'rxjs'; + +import SharedModule from 'app/shared/shared.module'; +import { SortDirective, SortByDirective } from 'app/shared/sort'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { ItemCountComponent } from 'app/shared/pagination'; +import { FormsModule } from '@angular/forms'; + +import { ITEMS_PER_PAGE, PAGE_HEADER, TOTAL_COUNT_RESPONSE_HEADER } from 'app/config/pagination.constants'; +import { ASC, DESC, SORT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { FilterComponent, FilterOptions, IFilterOptions, IFilterOption } from 'app/shared/filter'; +import { EntityArrayResponseType, TestParameterService } from '../service/test-parameter.service'; +import { ITestParameter } from '../test-parameter.model'; + +@Component({ + standalone: true, + selector: 'jhi-test-parameter', + templateUrl: './test-parameter.component.html', + imports: [ + RouterModule, + FormsModule, + SharedModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + FilterComponent, + ItemCountComponent, + ], +}) +export class TestParameterComponent implements OnInit { + testParameters?: ITestParameter[]; + isLoading = false; + + predicate = 'createdDate'; + ascending = true; + filters: IFilterOptions = new FilterOptions(); + + itemsPerPage = ITEMS_PER_PAGE; + totalItems = 0; + page = 1; + + constructor( + protected testParameterService: TestParameterService, + protected activatedRoute: ActivatedRoute, + private ngZone: NgZone, + public router: Router, + ) {} + + trackId = (_index: number, item: ITestParameter): number => this.testParameterService.getTestParameterIdentifier(item); + + ngOnInit(): void { + this.load(); + + this.filters.filterChanges.subscribe(filterOptions => this.handleNavigation(1, this.predicate, this.ascending, filterOptions)); + } + + load(): void { + this.loadFromBackendWithRouteInformations().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(): void { + this.handleNavigation(this.page, this.predicate, this.ascending, this.filters.filterOptions); + } + + navigateToPage(page = this.page): void { + this.handleNavigation(page, this.predicate, this.ascending, this.filters.filterOptions); + } + + protected loadFromBackendWithRouteInformations(): Observable { + return combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.data]).pipe( + tap(([params, data]) => this.fillComponentAttributeFromRoute(params, data)), + switchMap(() => this.queryBackend(this.page, this.predicate, this.ascending, this.filters.filterOptions)), + ); + } + + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + const page = params.get(PAGE_HEADER); + this.page = +(page ?? 1); + const sort = (params.get(SORT) ?? data[DEFAULT_SORT_DATA]).split(','); + this.predicate = sort[0]; + this.ascending = sort[1] === ASC; + this.filters.initializeFromParams(params); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + this.fillComponentAttributesFromResponseHeader(response.headers); + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.testParameters = dataFromBody; + } + + protected fillComponentAttributesFromResponseBody(data: ITestParameter[] | null): ITestParameter[] { + return data ?? []; + } + + protected fillComponentAttributesFromResponseHeader(headers: HttpHeaders): void { + this.totalItems = Number(headers.get(TOTAL_COUNT_RESPONSE_HEADER)); + } + + protected queryBackend( + page?: number, + predicate?: string, + ascending?: boolean, + filterOptions?: IFilterOption[], + ): Observable { + this.isLoading = true; + const pageToLoad: number = page ?? 1; + const queryObject: any = { + page: pageToLoad - 1, + size: this.itemsPerPage, + sort: this.getSortQueryParam(predicate, ascending), + }; + filterOptions?.forEach(filterOption => { + queryObject[filterOption.name] = filterOption.values; + }); + return this.testParameterService.query(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected handleNavigation(page = this.page, predicate?: string, ascending?: boolean, filterOptions?: IFilterOption[]): void { + const queryParamsObj: any = { + page, + size: this.itemsPerPage, + sort: this.getSortQueryParam(predicate, ascending), + }; + + filterOptions?.forEach(filterOption => { + queryParamsObj[filterOption.nameAsQueryParam()] = filterOption.values; + }); + + this.ngZone.run(() => + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }), + ); + } + + protected getSortQueryParam(predicate = this.predicate, ascending = this.ascending): string[] { + const ascendingQueryParam = ascending ? ASC : DESC; + if (predicate === '') { + return []; + } else { + return [predicate + ',' + ascendingQueryParam]; + } + } +} diff --git a/simulator-ui/src/main/webapp/app/entities/test-parameter/route/test-parameter-routing-resolve.service.spec.ts b/simulator-ui/src/main/webapp/app/entities/test-parameter/route/test-parameter-routing-resolve.service.spec.ts new file mode 100644 index 000000000..64e6160bb --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-parameter/route/test-parameter-routing-resolve.service.spec.ts @@ -0,0 +1,101 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRouteSnapshot, ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { ITestParameter } from '../test-parameter.model'; +import { TestParameterService } from '../service/test-parameter.service'; + +import testParameterResolve from './test-parameter-routing-resolve.service'; + +describe('TestParameter routing resolve service', () => { + let mockRouter: Router; + let mockActivatedRouteSnapshot: ActivatedRouteSnapshot; + let service: TestParameterService; + let resultTestParameter: ITestParameter | null | undefined; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, RouterTestingModule.withRoutes([])], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({}), + }, + }, + }, + ], + }); + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + mockActivatedRouteSnapshot = TestBed.inject(ActivatedRoute).snapshot; + service = TestBed.inject(TestParameterService); + resultTestParameter = undefined; + }); + + describe('resolve', () => { + it('should return ITestParameter returned by find', () => { + const expectedResult: ITestParameter = { key: 'key', testResult: { id: 123 } }; + + // GIVEN + service.find = jest.fn((testResultId, key) => of(new HttpResponse({ body: { key, testResult: { id: testResultId } } }))); + mockActivatedRouteSnapshot.params = { testResultId: expectedResult.testResult.id, key: expectedResult.key }; + + // WHEN + TestBed.runInInjectionContext(() => { + testParameterResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultTestParameter = result; + }, + }); + }); + + // THEN + expect(service.find).toBeCalledWith(expectedResult.testResult.id, expectedResult.key); + expect(resultTestParameter).toEqual(expectedResult); + }); + + it('should return null if id is not provided', () => { + // GIVEN + service.find = jest.fn(); + mockActivatedRouteSnapshot.params = {}; + + // WHEN + TestBed.runInInjectionContext(() => { + testParameterResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultTestParameter = result; + }, + }); + }); + + // THEN + expect(service.find).not.toBeCalled(); + expect(resultTestParameter).toEqual(null); + }); + + it('should route to 404 page if data not found in server', () => { + // GIVEN + jest.spyOn(service, 'find').mockReturnValue(of(new HttpResponse({ body: null }))); + mockActivatedRouteSnapshot.params = { testResultId: 123, key: 'key' }; + + // WHEN + TestBed.runInInjectionContext(() => { + testParameterResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultTestParameter = result; + }, + }); + }); + + // THEN + expect(service.find).toBeCalledWith(123, 'key'); + expect(resultTestParameter).toEqual(undefined); + expect(mockRouter.navigate).toHaveBeenCalledWith(['404']); + }); + }); +}); diff --git a/simulator-ui/src/main/webapp/app/entities/test-parameter/route/test-parameter-routing-resolve.service.ts b/simulator-ui/src/main/webapp/app/entities/test-parameter/route/test-parameter-routing-resolve.service.ts new file mode 100644 index 000000000..e270ca90b --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-parameter/route/test-parameter-routing-resolve.service.ts @@ -0,0 +1,31 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { ITestParameter } from '../test-parameter.model'; +import { TestParameterService } from '../service/test-parameter.service'; + +export const testParameterResolve = (route: ActivatedRouteSnapshot): Observable => { + const testResultId = route.params['testResultId']; + const key = route.params['key']; + + if (key && testResultId) { + return inject(TestParameterService) + .find(testResultId, key) + .pipe( + mergeMap((testParameter: HttpResponse) => { + if (testParameter.body) { + return of(testParameter.body); + } else { + inject(Router).navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default testParameterResolve; diff --git a/simulator-ui/src/main/webapp/app/entities/test-parameter/service/test-parameter.service.spec.ts b/simulator-ui/src/main/webapp/app/entities/test-parameter/service/test-parameter.service.spec.ts new file mode 100644 index 000000000..200f748e9 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-parameter/service/test-parameter.service.spec.ts @@ -0,0 +1,165 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { ITestParameter } from '../test-parameter.model'; +import { sampleWithRequiredData, sampleWithPartialData, sampleWithFullData } from '../test-parameter.test-samples'; + +import { TestParameterService, RestTestParameter } from './test-parameter.service'; + +const requireRestSample: RestTestParameter = { + ...sampleWithRequiredData, + createdDate: sampleWithRequiredData.createdDate?.toJSON(), + lastModifiedDate: sampleWithRequiredData.lastModifiedDate?.toJSON(), +}; + +describe('TestParameter Service', () => { + let service: TestParameterService; + let httpMock: HttpTestingController; + let expectedResult: ITestParameter | ITestParameter[] | boolean | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + expectedResult = null; + service = TestBed.inject(TestParameterService); + httpMock = TestBed.inject(HttpTestingController); + }); + + describe('Service methods', () => { + it('should find an element', () => { + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.find(123, 'key').subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should return a list of TestParameter', () => { + const returnedFromService = { ...requireRestSample }; + + const expected = { ...sampleWithRequiredData }; + + service.query().subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush([returnedFromService]); + httpMock.verify(); + expect(expectedResult).toMatchObject([expected]); + }); + + describe('addTestParameterToCollectionIfMissing', () => { + it('should add a TestParameter to an empty array', () => { + const testParameter: ITestParameter = sampleWithRequiredData; + expectedResult = service.addTestParameterToCollectionIfMissing([], testParameter); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(testParameter); + }); + + it('should not add a TestParameter to an array that contains it', () => { + const testParameter: ITestParameter = sampleWithRequiredData; + const testParameterCollection: ITestParameter[] = [ + { + ...testParameter, + }, + sampleWithPartialData, + ]; + expectedResult = service.addTestParameterToCollectionIfMissing(testParameterCollection, testParameter); + expect(expectedResult).toHaveLength(2); + }); + + it("should add a TestParameter to an array that doesn't contain it", () => { + const testParameter: ITestParameter = sampleWithRequiredData; + const testParameterCollection: ITestParameter[] = [sampleWithPartialData]; + expectedResult = service.addTestParameterToCollectionIfMissing(testParameterCollection, testParameter); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(testParameter); + }); + + it('should add only unique TestParameter to an array', () => { + const testParameterArray: ITestParameter[] = [sampleWithRequiredData, sampleWithPartialData, sampleWithFullData]; + const testParameterCollection: ITestParameter[] = [sampleWithRequiredData]; + expectedResult = service.addTestParameterToCollectionIfMissing(testParameterCollection, ...testParameterArray); + expect(expectedResult).toHaveLength(3); + }); + + it('should accept varargs', () => { + const testParameter: ITestParameter = sampleWithRequiredData; + const testParameter2: ITestParameter = sampleWithPartialData; + expectedResult = service.addTestParameterToCollectionIfMissing([], testParameter, testParameter2); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(testParameter); + expect(expectedResult).toContain(testParameter2); + }); + + it('should accept null and undefined values', () => { + const testParameter: ITestParameter = sampleWithRequiredData; + expectedResult = service.addTestParameterToCollectionIfMissing([], null, testParameter, undefined); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(testParameter); + }); + + it('should return initial array if no TestParameter is added', () => { + const testParameterCollection: ITestParameter[] = [sampleWithRequiredData]; + expectedResult = service.addTestParameterToCollectionIfMissing(testParameterCollection, undefined, null); + expect(expectedResult).toEqual(testParameterCollection); + }); + }); + + describe('compareTestParameter', () => { + it('Should return true if both entities are null', () => { + const entity1 = null; + const entity2 = null; + + const compareResult = service.compareTestParameter(entity1, entity2); + + expect(compareResult).toEqual(true); + }); + + it('Should return false if one entity is null', () => { + const entity1 = { key: 'key', testResult: { id: 123 } }; + const entity2 = null; + + const compareResult1 = service.compareTestParameter(entity1, entity2); + const compareResult2 = service.compareTestParameter(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey differs', () => { + const entity1 = { key: 'key', testResult: { id: 123 } }; + const entity2 = { key: 'another key', testResult: { id: 123 } }; + const entity3 = { key: 'key', testResult: { id: 234 } }; + + const compareResult1 = service.compareTestParameter(entity1, entity2); + const compareResult2 = service.compareTestParameter(entity1, entity3); + const compareResult3 = service.compareTestParameter(entity2, entity1); + const compareResult4 = service.compareTestParameter(entity2, entity3); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + expect(compareResult3).toEqual(false); + expect(compareResult4).toEqual(false); + }); + + it('Should return true if primaryKey matches', () => { + const entity1 = { key: 'key', testResult: { id: 123 } }; + const entity2 = { key: 'key', testResult: { id: 123 } }; + + const compareResult1 = service.compareTestParameter(entity1, entity2); + const compareResult2 = service.compareTestParameter(entity2, entity1); + + expect(compareResult1).toEqual(true); + expect(compareResult2).toEqual(true); + }); + }); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/simulator-ui/src/main/webapp/app/entities/test-parameter/service/test-parameter.service.ts b/simulator-ui/src/main/webapp/app/entities/test-parameter/service/test-parameter.service.ts new file mode 100644 index 000000000..23981a213 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-parameter/service/test-parameter.service.ts @@ -0,0 +1,128 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { map } from 'rxjs/operators'; + +import dayjs from 'dayjs/esm'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { ITestParameter, NewTestParameter } from '../test-parameter.model'; + +type RestOf = Omit & { + createdDate?: string | null; + lastModifiedDate?: string | null; +}; + +export type RestTestParameter = RestOf; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class TestParameterService { + protected resourceUrl = this.applicationConfigService.getEndpointFor('api/test-parameters'); + + constructor( + protected http: HttpClient, + protected applicationConfigService: ApplicationConfigService, + ) {} + + find(testResultId: number, key: string): Observable { + return this.http + .get(`${this.resourceUrl}/${testResultId}/${key}`, { observe: 'response' }) + .pipe(map(res => this.convertResponseFromServer(res))); + } + + query(req?: any): Observable { + const options = createRequestOption(req); + return this.http + .get(this.resourceUrl, { params: options, observe: 'response' }) + .pipe(map(res => this.convertResponseArrayFromServer(res))); + } + + getTestParameterIdentifier(testParameter: Pick): number { + return this.hash((testParameter.testResult.id ? testParameter.testResult.id : 0) + '-' + testParameter.key); + } + + compareTestParameter( + o1: Pick | null, + o2: Pick | null, + ): boolean { + return o1 && o2 ? this.getTestParameterIdentifier(o1) === this.getTestParameterIdentifier(o2) : o1 === o2; + } + + addTestParameterToCollectionIfMissing>( + testParameterCollection: Type[], + ...testParametersToCheck: (Type | null | undefined)[] + ): Type[] { + const testParameters: Type[] = testParametersToCheck.filter(isPresent); + if (testParameters.length > 0) { + const testParameterCollectionIdentifiers = testParameterCollection.map( + testParameterItem => this.getTestParameterIdentifier(testParameterItem)!, + ); + const testParametersToAdd = testParameters.filter(testParameterItem => { + const testParameterIdentifier = this.getTestParameterIdentifier(testParameterItem); + if (testParameterCollectionIdentifiers.includes(testParameterIdentifier)) { + return false; + } + testParameterCollectionIdentifiers.push(testParameterIdentifier); + return true; + }); + return [...testParametersToAdd, ...testParameterCollection]; + } + return testParameterCollection; + } + + protected convertDateFromClient(testParameter: T): RestOf { + return { + ...testParameter, + createdDate: testParameter.createdDate?.toJSON() ?? null, + lastModifiedDate: testParameter.lastModifiedDate?.toJSON() ?? null, + }; + } + + protected convertDateFromServer(restTestParameter: RestTestParameter): ITestParameter { + return { + ...restTestParameter, + createdDate: restTestParameter.createdDate ? dayjs(restTestParameter.createdDate) : undefined, + lastModifiedDate: restTestParameter.lastModifiedDate ? dayjs(restTestParameter.lastModifiedDate) : undefined, + }; + } + + protected convertResponseFromServer(res: HttpResponse): HttpResponse { + return res.clone({ + body: res.body ? this.convertDateFromServer(res.body) : null, + }); + } + + protected convertResponseArrayFromServer(res: HttpResponse): HttpResponse { + return res.clone({ + body: res.body ? res.body.map(item => this.convertDateFromServer(item)) : null, + }); + } + + /** + * Compute a "java-like" hash from the given string. Required because we cannot simply return a numerical identifier + * for the {@link ITestParameter} type: It uses a composed primary key. + * + * @param composedKey The composed key: `{@link ITestParameter#testResult#id} + '-' + {@link ITestResult#key}` + * @private + */ + private hash(composedKey: string): number { + let hash = 0; + if (composedKey.length === 0) { + return hash; + } + for (let i = 0; i < composedKey.length; i++) { + const char = composedKey.charCodeAt(i); + // eslint-disable-next-line no-bitwise + hash = (hash << 5) - hash + char; + // eslint-disable-next-line no-bitwise + hash |= 0; // Convert to 32bit integer + } + return hash; + } +} diff --git a/simulator-ui/src/main/webapp/app/entities/test-parameter/test-parameter.model.ts b/simulator-ui/src/main/webapp/app/entities/test-parameter/test-parameter.model.ts new file mode 100644 index 000000000..7687c730f --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-parameter/test-parameter.model.ts @@ -0,0 +1,12 @@ +import dayjs from 'dayjs/esm'; +import { ITestResult } from 'app/entities/test-result/test-result.model'; + +export interface ITestParameter { + key?: string | null; + value?: string | null; + createdDate?: dayjs.Dayjs | null; + lastModifiedDate?: dayjs.Dayjs | null; + testResult: Pick; +} + +export type NewTestParameter = ITestParameter; diff --git a/simulator-ui/src/main/webapp/app/entities/test-parameter/test-parameter.routes.ts b/simulator-ui/src/main/webapp/app/entities/test-parameter/test-parameter.routes.ts new file mode 100644 index 000000000..4761c0c82 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-parameter/test-parameter.routes.ts @@ -0,0 +1,25 @@ +import { Routes } from '@angular/router'; + +import { ASC } from 'app/config/navigation.constants'; +import { TestParameterComponent } from './list/test-parameter.component'; +import { TestParameterDetailComponent } from './detail/test-parameter-detail.component'; +import TestParameterResolve from './route/test-parameter-routing-resolve.service'; + +const testParameterRoute: Routes = [ + { + path: '', + component: TestParameterComponent, + data: { + defaultSort: 'createdDate,' + ASC, + }, + }, + { + path: ':testResultId/:key/view', + component: TestParameterDetailComponent, + resolve: { + testParameter: TestParameterResolve, + }, + }, +]; + +export default testParameterRoute; diff --git a/simulator-ui/src/main/webapp/app/entities/test-parameter/test-parameter.test-samples.ts b/simulator-ui/src/main/webapp/app/entities/test-parameter/test-parameter.test-samples.ts new file mode 100644 index 000000000..b0a4d6808 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-parameter/test-parameter.test-samples.ts @@ -0,0 +1,48 @@ +import dayjs from 'dayjs/esm'; + +import { ITestParameter, NewTestParameter } from './test-parameter.model'; + +export const sampleWithRequiredData: ITestParameter = { + key: 'indeed on chance', + value: 'too junior', + createdDate: dayjs('2023-09-26T08:48'), + lastModifiedDate: dayjs('2023-09-26T12:17'), + testResult: { + id: 79374, + }, +}; + +export const sampleWithPartialData: ITestParameter = { + key: 'gel supposing for', + value: 'yuck whereas as', + createdDate: dayjs('2023-09-26T16:06'), + lastModifiedDate: dayjs('2023-09-26T06:13'), + testResult: { + id: 84283, + }, +}; + +export const sampleWithFullData: ITestParameter = { + key: 'inter hm ew', + value: 'gently', + createdDate: dayjs('2023-09-26T13:02'), + lastModifiedDate: dayjs('2023-09-26T17:35'), + testResult: { + id: 15826, + }, +}; + +export const sampleWithNewData: NewTestParameter = { + key: 'righteously yet likewise', + value: 'memorable', + createdDate: dayjs('2023-09-26T11:57'), + lastModifiedDate: dayjs('2023-09-26T17:27'), + testResult: { + id: 84692, + }, +}; + +Object.freeze(sampleWithNewData); +Object.freeze(sampleWithRequiredData); +Object.freeze(sampleWithPartialData); +Object.freeze(sampleWithFullData); diff --git a/simulator-ui/src/main/webapp/app/entities/test-result/detail/test-result-detail.component.html b/simulator-ui/src/main/webapp/app/entities/test-result/detail/test-result-detail.component.html new file mode 100644 index 000000000..d8218a1fe --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-result/detail/test-result-detail.component.html @@ -0,0 +1,56 @@ +
+
+
+

Test Result

+ +
+ + + + + +
+
ID
+
+ {{ testResult.id }} +
+
Status
+
+ {{ testResult.status }} +
+
Test Name
+
+ {{ testResult.testName }} +
+
Class Name
+
+ {{ testResult.className }} +
+
Error Message
+
+ {{ testResult.errorMessage }} +
+
Failure Stack
+
+ {{ testResult.failureStack }} +
+
Failure Type
+
+ {{ testResult.failureType }} +
+
Created Date
+
+ {{ testResult.createdDate | formatMediumDatetime }} +
+
Last Modified Date
+
+ {{ testResult.lastModifiedDate | formatMediumDatetime }} +
+
+ + +
+
+
diff --git a/simulator-ui/src/main/webapp/app/entities/test-result/detail/test-result-detail.component.spec.ts b/simulator-ui/src/main/webapp/app/entities/test-result/detail/test-result-detail.component.spec.ts new file mode 100644 index 000000000..94364f117 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-result/detail/test-result-detail.component.spec.ts @@ -0,0 +1,38 @@ +import { TestBed } from '@angular/core/testing'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness, RouterTestingModule } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { TestResultDetailComponent } from './test-result-detail.component'; + +describe('TestResult Management Detail Component', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TestResultDetailComponent, RouterTestingModule.withRoutes([], { bindToComponentInputs: true })], + providers: [ + provideRouter( + [ + { + path: '**', + component: TestResultDetailComponent, + resolve: { testResult: () => of({ id: 123 }) }, + }, + ], + withComponentInputBinding(), + ), + ], + }) + .overrideTemplate(TestResultDetailComponent, '') + .compileComponents(); + }); + + describe('OnInit', () => { + it('Should load testResult on init', async () => { + const harness = await RouterTestingHarness.create(); + const instance = await harness.navigateByUrl('/', TestResultDetailComponent); + + // THEN + expect(instance.testResult).toEqual(expect.objectContaining({ id: 123 })); + }); + }); +}); diff --git a/simulator-ui/src/main/webapp/app/entities/test-result/detail/test-result-detail.component.ts b/simulator-ui/src/main/webapp/app/entities/test-result/detail/test-result-detail.component.ts new file mode 100644 index 000000000..34019db86 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-result/detail/test-result-detail.component.ts @@ -0,0 +1,22 @@ +import { Component, Input } from '@angular/core'; +import { ActivatedRoute, RouterModule } from '@angular/router'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { ITestResult } from '../test-result.model'; + +@Component({ + standalone: true, + selector: 'jhi-test-result-detail', + templateUrl: './test-result-detail.component.html', + imports: [SharedModule, RouterModule, DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe], +}) +export class TestResultDetailComponent { + @Input() testResult: ITestResult | null = null; + + constructor(protected activatedRoute: ActivatedRoute) {} + + previousState(): void { + window.history.back(); + } +} diff --git a/simulator-ui/src/main/webapp/app/entities/test-result/list/test-result.component.html b/simulator-ui/src/main/webapp/app/entities/test-result/list/test-result.component.html new file mode 100644 index 000000000..9741b3ee4 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-result/list/test-result.component.html @@ -0,0 +1,147 @@ +
+

+ Test Results + +
+ +
+

+ + + + + + + +
+ No Test Results found +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ ID + +
+
+
+ Status + +
+
+
+ Test Name + +
+
+
+ Class Name + +
+
+
+ Error Message + +
+
+
+ Failure Stack + +
+
+
+ Failure Type + +
+
+
+ Created Date + +
+
+
+ Last Modified Date + +
+
+ {{ testResult.id }} + {{ testResult.status }}{{ testResult.testName }}{{ testResult.className }}{{ testResult.errorMessage }}{{ testResult.failureStack }}{{ testResult.failureType }}{{ testResult.createdDate | formatMediumDatetime }}{{ testResult.lastModifiedDate | formatMediumDatetime }} +
+ + +
+
+
+ +
+
+ +
+ +
+ +
+
+
diff --git a/simulator-ui/src/main/webapp/app/entities/test-result/list/test-result.component.spec.ts b/simulator-ui/src/main/webapp/app/entities/test-result/list/test-result.component.spec.ts new file mode 100644 index 000000000..87b2a6172 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-result/list/test-result.component.spec.ts @@ -0,0 +1,125 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRoute } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { TestResultService } from '../service/test-result.service'; + +import { TestResultComponent } from './test-result.component'; +import SpyInstance = jest.SpyInstance; + +describe('TestResult Management Component', () => { + let comp: TestResultComponent; + let fixture: ComponentFixture; + let service: TestResultService; + let routerNavigateSpy: SpyInstance>; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule.withRoutes([{ path: 'test-result', component: TestResultComponent }]), + HttpClientTestingModule, + TestResultComponent, + ], + providers: [ + { + provide: ActivatedRoute, + useValue: { + data: of({ + defaultSort: 'id,asc', + }), + queryParamMap: of( + jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + 'filter[someId.in]': 'dc4279ea-cfb9-11ec-9d64-0242ac120002', + }), + ), + snapshot: { queryParams: {} }, + }, + }, + ], + }) + .overrideTemplate(TestResultComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(TestResultComponent); + comp = fixture.componentInstance; + service = TestBed.inject(TestResultService); + routerNavigateSpy = jest.spyOn(comp.router, 'navigate'); + + const headers = new HttpHeaders(); + jest.spyOn(service, 'query').mockReturnValue( + of( + new HttpResponse({ + body: [{ id: 123 }], + headers, + }), + ), + ); + }); + + it('Should call load all on init', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenCalled(); + expect(comp.testResults?.[0]).toEqual(expect.objectContaining({ id: 123 })); + }); + + describe('trackId', () => { + it('Should forward to testResultService', () => { + const entity = { id: 123 }; + jest.spyOn(service, 'getTestResultIdentifier'); + const id = comp.trackId(0, entity); + expect(service.getTestResultIdentifier).toHaveBeenCalledWith(entity); + expect(id).toBe(entity.id); + }); + }); + + it('should load a page', () => { + // WHEN + comp.navigateToPage(1); + + // THEN + expect(routerNavigateSpy).toHaveBeenCalled(); + }); + + it('should calculate the sort attribute for an id', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenLastCalledWith(expect.objectContaining({ sort: ['id,desc'] })); + }); + + it('should calculate the sort attribute for a non-id attribute', () => { + // GIVEN + comp.predicate = 'name'; + + // WHEN + comp.navigateToWithComponentValues(); + + // THEN + expect(routerNavigateSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + queryParams: expect.objectContaining({ + sort: ['name,asc'], + }), + }), + ); + }); + + it('should calculate the filter attribute', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenLastCalledWith(expect.objectContaining({ 'someId.in': ['dc4279ea-cfb9-11ec-9d64-0242ac120002'] })); + }); +}); diff --git a/simulator-ui/src/main/webapp/app/entities/test-result/list/test-result.component.ts b/simulator-ui/src/main/webapp/app/entities/test-result/list/test-result.component.ts new file mode 100644 index 000000000..f458307c9 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-result/list/test-result.component.ts @@ -0,0 +1,154 @@ +import { Component, NgZone, OnInit } from '@angular/core'; +import { HttpHeaders } from '@angular/common/http'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, Observable, switchMap, tap } from 'rxjs'; + +import SharedModule from 'app/shared/shared.module'; +import { SortDirective, SortByDirective } from 'app/shared/sort'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { ItemCountComponent } from 'app/shared/pagination'; +import { FormsModule } from '@angular/forms'; + +import { ITEMS_PER_PAGE, PAGE_HEADER, TOTAL_COUNT_RESPONSE_HEADER } from 'app/config/pagination.constants'; +import { ASC, DESC, SORT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { FilterComponent, FilterOptions, IFilterOptions, IFilterOption } from 'app/shared/filter'; +import { EntityArrayResponseType, TestResultService } from '../service/test-result.service'; +import { ITestResult } from '../test-result.model'; + +@Component({ + standalone: true, + selector: 'jhi-test-result', + templateUrl: './test-result.component.html', + imports: [ + RouterModule, + FormsModule, + SharedModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + FilterComponent, + ItemCountComponent, + ], +}) +export class TestResultComponent implements OnInit { + testResults?: ITestResult[]; + isLoading = false; + + predicate = 'id'; + ascending = true; + filters: IFilterOptions = new FilterOptions(); + + itemsPerPage = ITEMS_PER_PAGE; + totalItems = 0; + page = 1; + + constructor( + protected testResultService: TestResultService, + protected activatedRoute: ActivatedRoute, + private ngZone: NgZone, + public router: Router, + ) {} + + trackId = (_index: number, item: ITestResult): number => this.testResultService.getTestResultIdentifier(item); + + ngOnInit(): void { + this.load(); + + this.filters.filterChanges.subscribe(filterOptions => this.handleNavigation(1, this.predicate, this.ascending, filterOptions)); + } + + load(): void { + this.loadFromBackendWithRouteInformations().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(): void { + this.handleNavigation(this.page, this.predicate, this.ascending, this.filters.filterOptions); + } + + navigateToPage(page = this.page): void { + this.handleNavigation(page, this.predicate, this.ascending, this.filters.filterOptions); + } + + protected loadFromBackendWithRouteInformations(): Observable { + return combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.data]).pipe( + tap(([params, data]) => this.fillComponentAttributeFromRoute(params, data)), + switchMap(() => this.queryBackend(this.page, this.predicate, this.ascending, this.filters.filterOptions)), + ); + } + + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + const page = params.get(PAGE_HEADER); + this.page = +(page ?? 1); + const sort = (params.get(SORT) ?? data[DEFAULT_SORT_DATA]).split(','); + this.predicate = sort[0]; + this.ascending = sort[1] === ASC; + this.filters.initializeFromParams(params); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + this.fillComponentAttributesFromResponseHeader(response.headers); + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.testResults = dataFromBody; + } + + protected fillComponentAttributesFromResponseBody(data: ITestResult[] | null): ITestResult[] { + return data ?? []; + } + + protected fillComponentAttributesFromResponseHeader(headers: HttpHeaders): void { + this.totalItems = Number(headers.get(TOTAL_COUNT_RESPONSE_HEADER)); + } + + protected queryBackend( + page?: number, + predicate?: string, + ascending?: boolean, + filterOptions?: IFilterOption[], + ): Observable { + this.isLoading = true; + const pageToLoad: number = page ?? 1; + const queryObject: any = { + page: pageToLoad - 1, + size: this.itemsPerPage, + sort: this.getSortQueryParam(predicate, ascending), + }; + filterOptions?.forEach(filterOption => { + queryObject[filterOption.name] = filterOption.values; + }); + return this.testResultService.query(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected handleNavigation(page = this.page, predicate?: string, ascending?: boolean, filterOptions?: IFilterOption[]): void { + const queryParamsObj: any = { + page, + size: this.itemsPerPage, + sort: this.getSortQueryParam(predicate, ascending), + }; + + filterOptions?.forEach(filterOption => { + queryParamsObj[filterOption.nameAsQueryParam()] = filterOption.values; + }); + + this.ngZone.run(() => + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }), + ); + } + + protected getSortQueryParam(predicate = this.predicate, ascending = this.ascending): string[] { + const ascendingQueryParam = ascending ? ASC : DESC; + if (predicate === '') { + return []; + } else { + return [predicate + ',' + ascendingQueryParam]; + } + } +} diff --git a/simulator-ui/src/main/webapp/app/entities/test-result/route/test-result-routing-resolve.service.spec.ts b/simulator-ui/src/main/webapp/app/entities/test-result/route/test-result-routing-resolve.service.spec.ts new file mode 100644 index 000000000..0e5d2f5fe --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-result/route/test-result-routing-resolve.service.spec.ts @@ -0,0 +1,99 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRouteSnapshot, ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { ITestResult } from '../test-result.model'; +import { TestResultService } from '../service/test-result.service'; + +import testResultResolve from './test-result-routing-resolve.service'; + +describe('TestResult routing resolve service', () => { + let mockRouter: Router; + let mockActivatedRouteSnapshot: ActivatedRouteSnapshot; + let service: TestResultService; + let resultTestResult: ITestResult | null | undefined; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, RouterTestingModule.withRoutes([])], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({}), + }, + }, + }, + ], + }); + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + mockActivatedRouteSnapshot = TestBed.inject(ActivatedRoute).snapshot; + service = TestBed.inject(TestResultService); + resultTestResult = undefined; + }); + + describe('resolve', () => { + it('should return ITestResult returned by find', () => { + // GIVEN + service.find = jest.fn(id => of(new HttpResponse({ body: { id } }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + testResultResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultTestResult = result; + }, + }); + }); + + // THEN + expect(service.find).toBeCalledWith(123); + expect(resultTestResult).toEqual({ id: 123 }); + }); + + it('should return null if id is not provided', () => { + // GIVEN + service.find = jest.fn(); + mockActivatedRouteSnapshot.params = {}; + + // WHEN + TestBed.runInInjectionContext(() => { + testResultResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultTestResult = result; + }, + }); + }); + + // THEN + expect(service.find).not.toBeCalled(); + expect(resultTestResult).toEqual(null); + }); + + it('should route to 404 page if data not found in server', () => { + // GIVEN + jest.spyOn(service, 'find').mockReturnValue(of(new HttpResponse({ body: null }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + testResultResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultTestResult = result; + }, + }); + }); + + // THEN + expect(service.find).toBeCalledWith(123); + expect(resultTestResult).toEqual(undefined); + expect(mockRouter.navigate).toHaveBeenCalledWith(['404']); + }); + }); +}); diff --git a/simulator-ui/src/main/webapp/app/entities/test-result/route/test-result-routing-resolve.service.ts b/simulator-ui/src/main/webapp/app/entities/test-result/route/test-result-routing-resolve.service.ts new file mode 100644 index 000000000..b05a4a064 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-result/route/test-result-routing-resolve.service.ts @@ -0,0 +1,29 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { ITestResult } from '../test-result.model'; +import { TestResultService } from '../service/test-result.service'; + +export const testResultResolve = (route: ActivatedRouteSnapshot): Observable => { + const id = route.params['id']; + if (id) { + return inject(TestResultService) + .find(id) + .pipe( + mergeMap((testResult: HttpResponse) => { + if (testResult.body) { + return of(testResult.body); + } else { + inject(Router).navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default testResultResolve; diff --git a/simulator-ui/src/main/webapp/app/entities/test-result/service/test-result.service.spec.ts b/simulator-ui/src/main/webapp/app/entities/test-result/service/test-result.service.spec.ts new file mode 100644 index 000000000..15336203e --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-result/service/test-result.service.spec.ts @@ -0,0 +1,174 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { ITestResult } from '../test-result.model'; +import { sampleWithRequiredData, sampleWithPartialData, sampleWithFullData } from '../test-result.test-samples'; + +import { TestResultService, RestTestResult, TestResultsByStatus } from './test-result.service'; + +const requireRestSample: RestTestResult = { + ...sampleWithRequiredData, + createdDate: sampleWithRequiredData.createdDate?.toJSON(), + lastModifiedDate: sampleWithRequiredData.lastModifiedDate?.toJSON(), +}; + +describe('TestResult Service', () => { + let service: TestResultService; + let httpMock: HttpTestingController; + let expectedResult: ITestResult | ITestResult[] | boolean | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + expectedResult = null; + service = TestBed.inject(TestResultService); + httpMock = TestBed.inject(HttpTestingController); + }); + + describe('Service methods', () => { + it('should find an element', () => { + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.find(123).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should return a list of TestResult', () => { + const returnedFromService = { ...requireRestSample }; + + const expected = { ...sampleWithRequiredData }; + + service.query().subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush([returnedFromService]); + httpMock.verify(); + expect(expectedResult).toMatchObject([expected]); + }); + + it('should return results count by status', () => { + const returnedFromService: TestResultsByStatus = { total: 3, successful: 2, failed: 1 }; + + let actualResult: TestResultsByStatus | null; + service.countByStatus().subscribe(resp => (actualResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush([returnedFromService]); + httpMock.verify(); + + // @ts-ignore: Usage before assignment is ok + expect(actualResult).toMatchObject(actualResult); + }); + + describe('addTestResultToCollectionIfMissing', () => { + it('should add a TestResult to an empty array', () => { + const testResult: ITestResult = sampleWithRequiredData; + expectedResult = service.addTestResultToCollectionIfMissing([], testResult); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(testResult); + }); + + it('should not add a TestResult to an array that contains it', () => { + const testResult: ITestResult = sampleWithRequiredData; + const testResultCollection: ITestResult[] = [ + { + ...testResult, + }, + sampleWithPartialData, + ]; + expectedResult = service.addTestResultToCollectionIfMissing(testResultCollection, testResult); + expect(expectedResult).toHaveLength(2); + }); + + it("should add a TestResult to an array that doesn't contain it", () => { + const testResult: ITestResult = sampleWithRequiredData; + const testResultCollection: ITestResult[] = [sampleWithPartialData]; + expectedResult = service.addTestResultToCollectionIfMissing(testResultCollection, testResult); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(testResult); + }); + + it('should add only unique TestResult to an array', () => { + const testResultArray: ITestResult[] = [sampleWithRequiredData, sampleWithPartialData, sampleWithFullData]; + const testResultCollection: ITestResult[] = [sampleWithRequiredData]; + expectedResult = service.addTestResultToCollectionIfMissing(testResultCollection, ...testResultArray); + expect(expectedResult).toHaveLength(3); + }); + + it('should accept varargs', () => { + const testResult: ITestResult = sampleWithRequiredData; + const testResult2: ITestResult = sampleWithPartialData; + expectedResult = service.addTestResultToCollectionIfMissing([], testResult, testResult2); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(testResult); + expect(expectedResult).toContain(testResult2); + }); + + it('should accept null and undefined values', () => { + const testResult: ITestResult = sampleWithRequiredData; + expectedResult = service.addTestResultToCollectionIfMissing([], null, testResult, undefined); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(testResult); + }); + + it('should return initial array if no TestResult is added', () => { + const testResultCollection: ITestResult[] = [sampleWithRequiredData]; + expectedResult = service.addTestResultToCollectionIfMissing(testResultCollection, undefined, null); + expect(expectedResult).toEqual(testResultCollection); + }); + }); + + describe('compareTestResult', () => { + it('Should return true if both entities are null', () => { + const entity1 = null; + const entity2 = null; + + const compareResult = service.compareTestResult(entity1, entity2); + + expect(compareResult).toEqual(true); + }); + + it('Should return false if one entity is null', () => { + const entity1 = { id: 123 }; + const entity2 = null; + + const compareResult1 = service.compareTestResult(entity1, entity2); + const compareResult2 = service.compareTestResult(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey differs', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 456 }; + + const compareResult1 = service.compareTestResult(entity1, entity2); + const compareResult2 = service.compareTestResult(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return true if primaryKey matches', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 123 }; + + const compareResult1 = service.compareTestResult(entity1, entity2); + const compareResult2 = service.compareTestResult(entity2, entity1); + + expect(compareResult1).toEqual(true); + expect(compareResult2).toEqual(true); + }); + }); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/simulator-ui/src/main/webapp/app/entities/test-result/service/test-result.service.ts b/simulator-ui/src/main/webapp/app/entities/test-result/service/test-result.service.ts new file mode 100644 index 000000000..319024991 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-result/service/test-result.service.ts @@ -0,0 +1,111 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { map } from 'rxjs/operators'; + +import dayjs from 'dayjs/esm'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { ITestResult, NewTestResult } from '../test-result.model'; + +type RestOf = Omit & { + createdDate?: string | null; + lastModifiedDate?: string | null; +}; + +export type RestTestResult = RestOf; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +export type TestResultsByStatus = { + total: number; + successful: number; + failed: number; +}; + +@Injectable({ providedIn: 'root' }) +export class TestResultService { + protected resourceUrl = this.applicationConfigService.getEndpointFor('api/test-results'); + + constructor( + protected http: HttpClient, + protected applicationConfigService: ApplicationConfigService, + ) {} + + find(id: number): Observable { + return this.http + .get(`${this.resourceUrl}/${id}`, { observe: 'response' }) + .pipe(map(res => this.convertResponseFromServer(res))); + } + + query(req?: any): Observable { + const options = createRequestOption(req); + return this.http + .get(this.resourceUrl, { params: options, observe: 'response' }) + .pipe(map(res => this.convertResponseArrayFromServer(res))); + } + + countByStatus(): Observable> { + return this.http.get(`${this.resourceUrl}/count-by-status`, { observe: 'response' }); + } + + getTestResultIdentifier(testResult: Pick): number { + return testResult.id; + } + + compareTestResult(o1: Pick | null, o2: Pick | null): boolean { + return o1 && o2 ? this.getTestResultIdentifier(o1) === this.getTestResultIdentifier(o2) : o1 === o2; + } + + addTestResultToCollectionIfMissing>( + testResultCollection: Type[], + ...testResultsToCheck: (Type | null | undefined)[] + ): Type[] { + const testResults: Type[] = testResultsToCheck.filter(isPresent); + if (testResults.length > 0) { + const testResultCollectionIdentifiers = testResultCollection.map(testResultItem => this.getTestResultIdentifier(testResultItem)!); + const testResultsToAdd = testResults.filter(testResultItem => { + const testResultIdentifier = this.getTestResultIdentifier(testResultItem); + if (testResultCollectionIdentifiers.includes(testResultIdentifier)) { + return false; + } + testResultCollectionIdentifiers.push(testResultIdentifier); + return true; + }); + return [...testResultsToAdd, ...testResultCollection]; + } + return testResultCollection; + } + + protected convertDateFromClient(testResult: T): RestOf { + return { + ...testResult, + createdDate: testResult.createdDate?.toJSON() ?? null, + lastModifiedDate: testResult.lastModifiedDate?.toJSON() ?? null, + }; + } + + protected convertDateFromServer(restTestResult: RestTestResult): ITestResult { + return { + ...restTestResult, + createdDate: restTestResult.createdDate ? dayjs(restTestResult.createdDate) : undefined, + lastModifiedDate: restTestResult.lastModifiedDate ? dayjs(restTestResult.lastModifiedDate) : undefined, + }; + } + + protected convertResponseFromServer(res: HttpResponse): HttpResponse { + return res.clone({ + body: res.body ? this.convertDateFromServer(res.body) : null, + }); + } + + protected convertResponseArrayFromServer(res: HttpResponse): HttpResponse { + return res.clone({ + body: res.body ? res.body.map(item => this.convertDateFromServer(item)) : null, + }); + } +} diff --git a/simulator-ui/src/main/webapp/app/entities/test-result/test-result.model.ts b/simulator-ui/src/main/webapp/app/entities/test-result/test-result.model.ts new file mode 100644 index 000000000..8c353d7a8 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-result/test-result.model.ts @@ -0,0 +1,15 @@ +import dayjs from 'dayjs/esm'; + +export interface ITestResult { + id: number; + status?: number | null; + testName?: string | null; + className?: string | null; + errorMessage?: string | null; + failureStack?: string | null; + failureType?: string | null; + createdDate?: dayjs.Dayjs | null; + lastModifiedDate?: dayjs.Dayjs | null; +} + +export type NewTestResult = Omit & { id: null }; diff --git a/simulator-ui/src/main/webapp/app/entities/test-result/test-result.routes.ts b/simulator-ui/src/main/webapp/app/entities/test-result/test-result.routes.ts new file mode 100644 index 000000000..add6b6a07 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-result/test-result.routes.ts @@ -0,0 +1,25 @@ +import { Routes } from '@angular/router'; + +import { ASC } from 'app/config/navigation.constants'; +import { TestResultComponent } from './list/test-result.component'; +import { TestResultDetailComponent } from './detail/test-result-detail.component'; +import TestResultResolve from './route/test-result-routing-resolve.service'; + +const testResultRoute: Routes = [ + { + path: '', + component: TestResultComponent, + data: { + defaultSort: 'id,' + ASC, + }, + }, + { + path: ':id/view', + component: TestResultDetailComponent, + resolve: { + testResult: TestResultResolve, + }, + }, +]; + +export default testResultRoute; diff --git a/simulator-ui/src/main/webapp/app/entities/test-result/test-result.test-samples.ts b/simulator-ui/src/main/webapp/app/entities/test-result/test-result.test-samples.ts new file mode 100644 index 000000000..6e3115064 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-result/test-result.test-samples.ts @@ -0,0 +1,49 @@ +import dayjs from 'dayjs/esm'; + +import { ITestResult, NewTestResult } from './test-result.model'; + +export const sampleWithRequiredData: ITestResult = { + id: 22758, + status: 18375, + testName: 'breadcrumb', + className: 'newspaper', + createdDate: dayjs('2023-09-26T09:11'), + lastModifiedDate: dayjs('2023-09-26T14:25'), +}; + +export const sampleWithPartialData: ITestResult = { + id: 1008, + status: 25893, + testName: 'zowie', + className: 'regarding openly', + errorMessage: 'toward', + failureStack: 'whose', + createdDate: dayjs('2023-09-26T13:39'), + lastModifiedDate: dayjs('2023-09-26T15:03'), +}; + +export const sampleWithFullData: ITestResult = { + id: 11970, + status: 5871, + testName: 'peruse probable display', + className: 'dining', + errorMessage: 'reproachfully better what', + failureStack: 'flugelhorn over', + failureType: 'aha', + createdDate: dayjs('2023-09-26T07:09'), + lastModifiedDate: dayjs('2023-09-26T09:18'), +}; + +export const sampleWithNewData: NewTestResult = { + status: 22262, + testName: 'um finally', + className: 'supporter vastly', + createdDate: dayjs('2023-09-25T22:19'), + lastModifiedDate: dayjs('2023-09-26T03:16'), + id: null, +}; + +Object.freeze(sampleWithNewData); +Object.freeze(sampleWithRequiredData); +Object.freeze(sampleWithPartialData); +Object.freeze(sampleWithFullData); diff --git a/simulator-ui/src/main/webapp/app/home/home.component.html b/simulator-ui/src/main/webapp/app/home/home.component.html index 6582f7123..fc6758998 100644 --- a/simulator-ui/src/main/webapp/app/home/home.component.html +++ b/simulator-ui/src/main/webapp/app/home/home.component.html @@ -4,13 +4,32 @@
-

Citrus Simulator

+
+

+ {{ simulatorInfo!.name }} -

- If you like Citrus and the Simulator, don't forget to give us a star on - GitHub! -

+ + Citrus Simulator + +

+ +

+ +

+ +

+ If you like Citrus and the Simulator, don't forget to give us a star on + + GitHub + + ! +

+
+ +
+
+ +
+
diff --git a/simulator-ui/src/main/webapp/app/home/home.component.spec.ts b/simulator-ui/src/main/webapp/app/home/home.component.spec.ts new file mode 100644 index 000000000..6408d14f9 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/home/home.component.spec.ts @@ -0,0 +1,56 @@ +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; + +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { InfoResponse } from 'app/layouts/profiles/profile-info.model'; + +import HomeComponent from './home.component'; + +describe('Home Component', () => { + let applicationConfigService: ApplicationConfigService; + let httpMock: HttpTestingController; + + let fixture: ComponentFixture; + let component: HomeComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HomeComponent, HttpClientTestingModule], + providers: [ApplicationConfigService], + }) + .overrideTemplate(HomeComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + httpMock = TestBed.inject(HttpTestingController); + applicationConfigService = TestBed.inject(ApplicationConfigService); + }); + + afterEach(() => { + httpMock.verify(); // Ensure that there are no outstanding HTTP requests. + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should fetch simulatorInfo on initialization', fakeAsync(() => { + const mockInfoResponse: InfoResponse = { + simulator: { + name: 'Citrus Simulator', + version: '1.2.3', + }, + }; + + component.ngOnInit(); + + const req = httpMock.expectOne(applicationConfigService.getEndpointFor('api/manage/info')); + expect(req.request.method).toBe('GET'); + req.flush(mockInfoResponse); + + tick(); + + expect(component.simulatorInfo).toEqual(mockInfoResponse.simulator); + })); +}); diff --git a/simulator-ui/src/main/webapp/app/home/home.component.ts b/simulator-ui/src/main/webapp/app/home/home.component.ts index 8a6ac0cf4..946d1a4a0 100644 --- a/simulator-ui/src/main/webapp/app/home/home.component.ts +++ b/simulator-ui/src/main/webapp/app/home/home.component.ts @@ -1,13 +1,33 @@ -import { Component } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Component, OnInit } from '@angular/core'; import { RouterModule } from '@angular/router'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { InfoResponse, SimulatorInfo } from 'app/layouts/profiles/profile-info.model'; import SharedModule from 'app/shared/shared.module'; +import TestResultSummaryComponent from './test-result-summary.component'; + @Component({ standalone: true, selector: 'jhi-home', templateUrl: './home.component.html', styleUrls: ['./home.component.scss'], - imports: [SharedModule, RouterModule], + imports: [RouterModule, SharedModule, TestResultSummaryComponent], }) -export default class HomeComponent {} +export default class HomeComponent implements OnInit { + simulatorInfo: SimulatorInfo | null = null; + + private infoUrl = this.applicationConfigService.getEndpointFor('api/manage/info'); + + constructor( + private applicationConfigService: ApplicationConfigService, + private http: HttpClient, + ) {} + + ngOnInit(): void { + this.http.get(this.infoUrl).subscribe((response: InfoResponse) => { + this.simulatorInfo = response.simulator ?? null; + }); + } +} diff --git a/simulator-ui/src/main/webapp/app/home/test-result-summary.component.html b/simulator-ui/src/main/webapp/app/home/test-result-summary.component.html new file mode 100644 index 000000000..e9987901b --- /dev/null +++ b/simulator-ui/src/main/webapp/app/home/test-result-summary.component.html @@ -0,0 +1,45 @@ +
+

+ + Test Results +

+ +
+
+ +

Total:

+
{{ testResults?.total ?? 0 }} (100 %)
+
+
+ +
+ +

Successful:

+
+ {{ testResults?.successful ?? 0 }} ({{ successfulPercentage }} %) +
+
+
+ +
+ +

Failed:

+
+ {{ testResults?.failed ?? 0 }} ({{ failedPercentage }} %) +
+
+
+
+ + + + No simulations ran yet! Try starting one: + Documentation + + +
diff --git a/simulator-ui/src/main/webapp/app/home/test-result-summary.component.spec.ts b/simulator-ui/src/main/webapp/app/home/test-result-summary.component.spec.ts new file mode 100644 index 000000000..e0f7de1dc --- /dev/null +++ b/simulator-ui/src/main/webapp/app/home/test-result-summary.component.spec.ts @@ -0,0 +1,77 @@ +import { HttpResponse } from '@angular/common/http'; +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; + +import { TranslateService } from '@ngx-translate/core'; + +import { of } from 'rxjs'; + +import { TestResultsByStatus, TestResultService } from 'app/entities/test-result/service/test-result.service'; + +import TestResultSummaryComponent from './test-result-summary.component'; + +import Mocked = jest.Mocked; + +describe('TestResultSummaryComponent', () => { + let testResultService: Mocked; + + let fixture: ComponentFixture; + let component: TestResultSummaryComponent; + + beforeEach(async () => { + testResultService = { + countByStatus: jest.fn(), + } as unknown as Mocked; + + await TestBed.configureTestingModule({ + imports: [TestResultSummaryComponent], + providers: [TranslateService, { provide: TestResultService, useValue: testResultService }], + }) + .overrideTemplate(TestResultSummaryComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(TestResultSummaryComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('ngOnInit', () => { + it('should correctly calculate percentages', fakeAsync(() => { + const mockData = new HttpResponse({ + body: { + total: 2, + successful: 1, + failed: 1, + }, + }); + + testResultService.countByStatus.mockReturnValue(of(mockData)); + + component.ngOnInit(); + tick(); + + expect(component.testResults).toEqual(mockData.body); + expect(component.successfulPercentage).toEqual(50); + expect(component.failedPercentage).toEqual(50); + })); + + it('default to a zero-result', fakeAsync(() => { + const mockData = new HttpResponse({ body: null }); + + testResultService.countByStatus.mockReturnValue(of(mockData)); + + component.ngOnInit(); + tick(); + + expect(component.testResults).toEqual({ + total: 0, + successful: 0, + failed: 0, + }); + expect(component.successfulPercentage).toEqual(0); + expect(component.failedPercentage).toEqual(0); + })); + }); +}); diff --git a/simulator-ui/src/main/webapp/app/home/test-result-summary.component.ts b/simulator-ui/src/main/webapp/app/home/test-result-summary.component.ts new file mode 100644 index 000000000..a9cf52556 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/home/test-result-summary.component.ts @@ -0,0 +1,39 @@ +import { Component, OnInit } from '@angular/core'; + +import { map } from 'rxjs/operators'; + +import { TestResultsByStatus, TestResultService } from 'app/entities/test-result/service/test-result.service'; +import SharedModule from 'app/shared/shared.module'; + +@Component({ + standalone: true, + selector: 'app-test-result-summary', + templateUrl: './test-result-summary.component.html', + imports: [SharedModule], +}) +export default class TestResultSummaryComponent implements OnInit { + testResults: TestResultsByStatus | null = null; + + successfulPercentage = 0; + failedPercentage = 0; + + constructor(private testResultService: TestResultService) {} + + ngOnInit(): void { + this.load(); + } + + private load(): void { + this.testResultService + .countByStatus() + .pipe(map(response => response.body ?? { total: 0, successful: 0, failed: 0 })) + .subscribe((testResults: TestResultsByStatus) => { + this.testResults = testResults; + + if (testResults.total > 0) { + this.successfulPercentage = (testResults.successful / testResults.total) * 100; + this.failedPercentage = (testResults.failed / testResults.total) * 100; + } + }); + } +} diff --git a/simulator-ui/src/main/webapp/app/layouts/navbar/navbar.component.html b/simulator-ui/src/main/webapp/app/layouts/navbar/navbar.component.html index 5b2d17f91..dd88daa03 100644 --- a/simulator-ui/src/main/webapp/app/layouts/navbar/navbar.component.html +++ b/simulator-ui/src/main/webapp/app/layouts/navbar/navbar.component.html @@ -42,6 +42,30 @@ diff --git a/simulator-ui/src/main/webapp/app/layouts/profiles/profile-info.model.ts b/simulator-ui/src/main/webapp/app/layouts/profiles/profile-info.model.ts index 14e920f1a..0daee0bcf 100644 --- a/simulator-ui/src/main/webapp/app/layouts/profiles/profile-info.model.ts +++ b/simulator-ui/src/main/webapp/app/layouts/profiles/profile-info.model.ts @@ -3,6 +3,7 @@ export interface InfoResponse { git?: any; build?: any; activeProfiles?: string[]; + simulator?: SimulatorInfo; } export class ProfileInfo { @@ -13,3 +14,10 @@ export class ProfileInfo { public openAPIEnabled?: boolean, ) {} } + +export class SimulatorInfo { + constructor( + public name?: string, + public version?: string, + ) {} +} diff --git a/simulator-ui/src/main/webapp/i18n/en/global.json b/simulator-ui/src/main/webapp/i18n/en/global.json index f37e1a7cd..9b2490675 100644 --- a/simulator-ui/src/main/webapp/i18n/en/global.json +++ b/simulator-ui/src/main/webapp/i18n/en/global.json @@ -7,6 +7,8 @@ "jhipster-needle-menu-add-element": "JHipster will add additional menu entries here (do not translate!)", "entities": { "main": "Entities", + "testParameter": "Test Parameter", + "testResult": "Test Result", "jhipster-needle-menu-add-entry": "JHipster will add additional entities here (do not translate!)" }, "language": "Language" diff --git a/simulator-ui/src/main/webapp/i18n/en/home.json b/simulator-ui/src/main/webapp/i18n/en/home.json index 635433e95..3549ae811 100755 --- a/simulator-ui/src/main/webapp/i18n/en/home.json +++ b/simulator-ui/src/main/webapp/i18n/en/home.json @@ -1,7 +1,18 @@ { "home": { "title": "Citrus Simulator", + "version": "Version {{version}}", "like": "If you like Citrus and the Simulator, don't forget to give us a star on", - "github": "GitHub" + "github": "GitHub", + "simulations": { + "title": "Simulations", + "documentation": "Documentation", + "noSimulationsRanYet": "No simulations ran yet! Try starting one:", + "results": { + "total": "Total:", + "successful": "Successful:", + "failed": "Failed:" + } + } } } diff --git a/simulator-ui/src/main/webapp/i18n/en/testParameter.json b/simulator-ui/src/main/webapp/i18n/en/testParameter.json new file mode 100644 index 000000000..0558ca11c --- /dev/null +++ b/simulator-ui/src/main/webapp/i18n/en/testParameter.json @@ -0,0 +1,20 @@ +{ + "citrusSimulatorApp": { + "testParameter": { + "home": { + "title": "Test Parameters", + "refreshListLabel": "Refresh list", + "notFound": "No Test Parameters found" + }, + "detail": { + "title": "Test Parameter" + }, + "id": "ID", + "key": "Key", + "value": "Value", + "createdDate": "Created Date", + "lastModifiedDate": "Last Modified Date", + "testResult": "Test Result" + } + } +} diff --git a/simulator-ui/src/main/webapp/i18n/en/testResult.json b/simulator-ui/src/main/webapp/i18n/en/testResult.json new file mode 100644 index 000000000..3949c3e28 --- /dev/null +++ b/simulator-ui/src/main/webapp/i18n/en/testResult.json @@ -0,0 +1,24 @@ +{ + "citrusSimulatorApp": { + "testResult": { + "home": { + "title": "Test Results", + "refreshListLabel": "Refresh list", + "notFound": "No Test Results found" + }, + "detail": { + "title": "Test Result" + }, + "id": "ID", + "status": "Status", + "testName": "Test Name", + "className": "Class Name", + "errorMessage": "Error Message", + "failureStack": "Failure Stack", + "failureType": "Failure Type", + "createdDate": "Created Date", + "lastModifiedDate": "Last Modified Date", + "testParameter": "Test Parameter" + } + } +} diff --git a/simulator-ui/src/test/java/org/citrusframework/simulator/ui/IntegrationTest.java b/simulator-ui/src/test/java/org/citrusframework/simulator/ui/IntegrationTest.java index 8408aef57..b44843605 100644 --- a/simulator-ui/src/test/java/org/citrusframework/simulator/ui/IntegrationTest.java +++ b/simulator-ui/src/test/java/org/citrusframework/simulator/ui/IntegrationTest.java @@ -1,6 +1,6 @@ package org.citrusframework.simulator.ui; -import org.citrusframework.simulator.ui.config.SimulatorUiAutoconfiguration; +import org.citrusframework.simulator.ui.test.TestApplication; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.DirtiesContext; @@ -14,7 +14,7 @@ */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) -@SpringBootTest(classes = {SimulatorUiAutoconfiguration.class}) +@SpringBootTest(classes = {TestApplication.class}) @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) public @interface IntegrationTest { } diff --git a/simulator-ui/src/test/java/org/citrusframework/simulator/ui/SimulatorStarter.java b/simulator-ui/src/test/java/org/citrusframework/simulator/ui/SimulatorStarter.java deleted file mode 100644 index 22b778428..000000000 --- a/simulator-ui/src/test/java/org/citrusframework/simulator/ui/SimulatorStarter.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.citrusframework.simulator.ui; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class SimulatorStarter { - - public static void main(String[] args) { - SpringApplication.run(SimulatorStarter.class, args); - } -} diff --git a/simulator-ui/src/test/java/org/citrusframework/simulator/ui/config/InfoEndpointConfigurationTest.java b/simulator-ui/src/test/java/org/citrusframework/simulator/ui/config/InfoEndpointConfigurationTest.java new file mode 100644 index 000000000..2b18fb764 --- /dev/null +++ b/simulator-ui/src/test/java/org/citrusframework/simulator/ui/config/InfoEndpointConfigurationTest.java @@ -0,0 +1,34 @@ +package org.citrusframework.simulator.ui.config; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.actuate.info.Info; +import org.springframework.core.env.Environment; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class InfoEndpointConfigurationTest { + + @InjectMocks + private InfoEndpointConfiguration infoEndpointConfiguration; + + @Mock + private Environment environmentMock; + + @Test + void shouldContributeActiveProfilesToInfoBuilder() { + String[] activeProfiles = {"dev", "local"}; + when(environmentMock.getActiveProfiles()).thenReturn(activeProfiles); + + Info.Builder builder = new Info.Builder(); + infoEndpointConfiguration.contribute(builder); + + Info info = builder.build(); + assertEquals(activeProfiles, info.getDetails().get("activeProfiles")); + } +} diff --git a/simulator-ui/src/test/java/org/citrusframework/simulator/ui/config/ServletUtilsTest.java b/simulator-ui/src/test/java/org/citrusframework/simulator/ui/config/ServletUtilsTest.java new file mode 100644 index 000000000..97aa0a5fa --- /dev/null +++ b/simulator-ui/src/test/java/org/citrusframework/simulator/ui/config/ServletUtilsTest.java @@ -0,0 +1,27 @@ +package org.citrusframework.simulator.ui.config; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ServletUtilsTest { + + static Stream extractContextPath() { + return Stream.of( + Arguments.of("/a/b", "/a/b"), + Arguments.of("/a/b/*", "/a/b"), + Arguments.of("/a/b/**", "/a/b"), + Arguments.of("/a/b/**/*", "/a/b") + ); + } + + @MethodSource + @ParameterizedTest + void extractContextPath(String urlMapping, String contextPath) { + assertEquals(contextPath, ServletUtils.extractContextPath(urlMapping)); + } +} diff --git a/simulator-ui/src/test/java/org/citrusframework/simulator/ui/config/SimulatorUiAutoconfigurationIT.java b/simulator-ui/src/test/java/org/citrusframework/simulator/ui/config/SimulatorUiAutoconfigurationIT.java new file mode 100644 index 000000000..f16d540b6 --- /dev/null +++ b/simulator-ui/src/test/java/org/citrusframework/simulator/ui/config/SimulatorUiAutoconfigurationIT.java @@ -0,0 +1,19 @@ +package org.citrusframework.simulator.ui.config; + +import org.citrusframework.simulator.ui.IntegrationTest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@IntegrationTest +public class SimulatorUiAutoconfigurationIT { + + @Autowired + private SimulatorUiAutoconfiguration simulatorUiAutoconfiguration; + + @Test + void isEnabledByDefault() { + assertNotNull(simulatorUiAutoconfiguration, "Simulator UI autoconfiguration is enabled by default, whenever simulator-ui is on the classpath."); + } +} diff --git a/simulator-ui/src/test/java/org/citrusframework/simulator/ui/filter/SpaWebFilterIT.java b/simulator-ui/src/test/java/org/citrusframework/simulator/ui/filter/SpaWebFilterIT.java index a36501075..cf4c8aa2a 100644 --- a/simulator-ui/src/test/java/org/citrusframework/simulator/ui/filter/SpaWebFilterIT.java +++ b/simulator-ui/src/test/java/org/citrusframework/simulator/ui/filter/SpaWebFilterIT.java @@ -7,6 +7,7 @@ import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -14,6 +15,9 @@ @AutoConfigureMockMvc class SpaWebFilterIT { + private static final String REST_URL_MAPPING = "/simulator/rest"; + private static final String WS_SERVLET_PATH = "/simulator/ws"; + @Autowired private MockMvc mockMvc; @@ -22,26 +26,14 @@ void testFilterForwardsToIndex() throws Exception { mockMvc.perform(get("/")).andExpect(status().isOk()).andExpect(forwardedUrl("/index.html")); } - // TODO: Use valid endpoint - // @Test - // void testFilterDoesNotForwardToIndexForApi() throws Exception { - // mockMvc.perform(get("/api/authenticate")).andExpect(status().isOk()).andExpect(forwardedUrl(null)); - // } - - // TODO: Maybe add swagger UI? - // @Test - // void testFilterDoesNotForwardToIndexForV3ApiDocs() throws Exception { - // mockMvc.perform(get("/v3/api-docs")).andExpect(status().isOk()).andExpect(forwardedUrl(null)); - // } - @Test - void testFilterDoesNotForwardToIndexForDotFile() throws Exception { - mockMvc.perform(get("/file.js")).andExpect(status().isNotFound()); + void testFilterDoesNotForwardToIndexForApi() throws Exception { + mockMvc.perform(get("/api/test-results")).andExpect(status().isOk()).andExpect(forwardedUrl(null)); } @Test - void getBackendEndpoint() throws Exception { - mockMvc.perform(get("/test")).andExpect(status().isOk()).andExpect(forwardedUrl("/index.html")); + void testFilterDoesNotForwardToIndexForDotFile() throws Exception { + mockMvc.perform(get("/file.js")).andExpect(status().isNotFound()); } @Test @@ -65,17 +57,17 @@ void forwardUnmappedDeepMapping() throws Exception { } @Test - void getUnmappedFirstLevelFile() throws Exception { - mockMvc.perform(get("/foo.js")).andExpect(status().isNotFound()); + void getUnmappedThirdLevelFile() throws Exception { + mockMvc.perform(get("/foo/another/bar.js")).andExpect(status().isNotFound()); } @Test - void getUnmappedSecondLevelFile() throws Exception { - mockMvc.perform(get("/foo/bar.js")).andExpect(status().isNotFound()); + void executeRestSimulation() throws Exception { + mockMvc.perform(get(REST_URL_MAPPING)).andExpect(status().isOk()).andExpect(forwardedUrl(null)); } @Test - void getUnmappedThirdLevelFile() throws Exception { - mockMvc.perform(get("/foo/another/bar.js")).andExpect(status().isNotFound()); + void executeWsSimulation() throws Exception { + mockMvc.perform(post(WS_SERVLET_PATH)).andExpect(status().isNotFound()).andExpect(forwardedUrl(null)); } } diff --git a/simulator-ui/src/test/java/org/citrusframework/simulator/ui/filter/SpaWebFilterTest.java b/simulator-ui/src/test/java/org/citrusframework/simulator/ui/filter/SpaWebFilterTest.java new file mode 100644 index 000000000..ee2e306c4 --- /dev/null +++ b/simulator-ui/src/test/java/org/citrusframework/simulator/ui/filter/SpaWebFilterTest.java @@ -0,0 +1,104 @@ +package org.citrusframework.simulator.ui.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.web.util.matcher.RequestMatcher; + +import java.io.IOException; +import java.util.stream.Stream; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class SpaWebFilterTest { + + @Mock(name = "simulatorRestRequestMatcher") + private RequestMatcher simulatorRestRequestMatcherMock; + + @Mock + private HttpServletRequest requestMock; + + @Mock + private HttpServletResponse responseMock; + + @Mock + private FilterChain filterChainMock; + + @Mock + private RequestDispatcher requestDispatcherMock; + + @InjectMocks + private SpaWebFilter fixture; + + public static Stream shouldNotForwardPathToIndexHtml() { + return Stream.of( + Arguments.of("/api", ""), + Arguments.of("/api/somepath", ""), + Arguments.of("/v3/api-docs", ""), + Arguments.of("/v3/api-docs/somepath", ""), + Arguments.of("/some/absolute/path.", ""), + Arguments.of("path/without/leading/slash", ""), + Arguments.of("/server-1/api", "/server-1"), + Arguments.of("/server-1/api/somepath", "/server-1"), + Arguments.of("/server-1/v3/api-docs", "/server-1"), + Arguments.of("/server-1/v3/api-docs/somepath", "/server-1"), + Arguments.of("/server-1/some/absolute/path.", "/server-1"), + Arguments.of("/server-1path/without/leading/slash", "/server-1") + ); + } + + @MethodSource + @ParameterizedTest + void shouldNotForwardPathToIndexHtml(String requestUri, String contextPath) throws ServletException, IOException { + when(requestMock.getRequestURI()).thenReturn(requestUri); + when(requestMock.getContextPath()).thenReturn(contextPath); + + fixture.doFilterInternal(requestMock, responseMock, filterChainMock); + + verify(filterChainMock).doFilter(requestMock, responseMock); + verify(requestDispatcherMock, never()).forward(requestMock, responseMock); + } + + @Test + void shouldNotForwardRequestMatchingPathToIndexHtml() throws ServletException, IOException { + String requestUri = "/request-path"; + + when(requestMock.getRequestURI()).thenReturn(requestUri); + when(requestMock.getContextPath()).thenReturn(""); + + doReturn(true).when(simulatorRestRequestMatcherMock).matches(requestMock); + + fixture.doFilterInternal(requestMock, responseMock, filterChainMock); + + verify(filterChainMock).doFilter(requestMock, responseMock); + verify(requestDispatcherMock, never()).forward(requestMock, responseMock); + } + + @Test + void shouldForwardInvalidPathToIndexHtml() throws ServletException, IOException { + when(requestMock.getRequestDispatcher("/index.html")).thenReturn(requestDispatcherMock); + + when(requestMock.getRequestURI()).thenReturn("/somepath"); + when(requestMock.getContextPath()).thenReturn(""); + when(simulatorRestRequestMatcherMock.matches(requestMock)).thenReturn(false); + + fixture.doFilterInternal(requestMock, responseMock, filterChainMock); + + verify(requestDispatcherMock).forward(requestMock, responseMock); + verify(filterChainMock, never()).doFilter(requestMock, responseMock); + } +} diff --git a/simulator-ui/src/test/java/org/citrusframework/simulator/ui/test/TestApplication.java b/simulator-ui/src/test/java/org/citrusframework/simulator/ui/test/TestApplication.java new file mode 100644 index 000000000..0ae348048 --- /dev/null +++ b/simulator-ui/src/test/java/org/citrusframework/simulator/ui/test/TestApplication.java @@ -0,0 +1,26 @@ +package org.citrusframework.simulator.ui.test; + +import org.citrusframework.simulator.scenario.AbstractSimulatorScenario; +import org.citrusframework.simulator.scenario.SimulatorScenario; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +/** + * Note, this may not rest in the root package of the {@code simulator-ui} because of the automatic package scan + * which {@link SpringBootApplication} performs. Hence, the {@code test} package. + */ +@SpringBootApplication +public class TestApplication { + + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } + + @Bean("DEFAULT_SCENARIO") + public SimulatorScenario defaultScenario() { + return new AbstractSimulatorScenario() { + + }; + } +} diff --git a/simulator-ui/src/test/resources/application.properties b/simulator-ui/src/test/resources/application.properties new file mode 100644 index 000000000..5302dc8ee --- /dev/null +++ b/simulator-ui/src/test/resources/application.properties @@ -0,0 +1,5 @@ +citrus.simulator.rest.enabled=true +citrus.simulator.rest.url-mapping=/simulator/rest + +citrus.simulator.ws.enabled=true +citrus.simulator.ws.servlet-mapping=/simulator/ws

+ * Pagination uses the same principles as the GitHub API, + * and follow RFC 5988 (Link header). + */ +public interface PaginationUtil { + + String HEADER_X_TOTAL_COUNT = "X-Total-Count"; + String HEADER_LINK_FORMAT = "<{0}>; rel=\"{1}\""; + + /** + * Generate pagination headers for a Spring Data {@link org.springframework.data.domain.Page} object. + * + * @param uriBuilder The URI builder. + * @param page The page. + * @param The type of object. + * @return http header. + */ + static HttpHeaders generatePaginationHttpHeaders(UriComponentsBuilder uriBuilder, Page page) { + HttpHeaders headers = new HttpHeaders(); + headers.add(HEADER_X_TOTAL_COUNT, Long.toString(page.getTotalElements())); + int pageNumber = page.getNumber(); + int pageSize = page.getSize(); + StringBuilder link = new StringBuilder(); + if (pageNumber < page.getTotalPages() - 1) { + link.append(prepareLink(uriBuilder, pageNumber + 1, pageSize, "next")) + .append(","); + } + if (pageNumber > 0) { + link.append(prepareLink(uriBuilder, pageNumber - 1, pageSize, "prev")) + .append(","); + } + link.append(prepareLink(uriBuilder, page.getTotalPages() - 1, pageSize, "last")) + .append(",") + .append(prepareLink(uriBuilder, 0, pageSize, "first")); + headers.add(HttpHeaders.LINK, link.toString()); + return headers; + } + + private static String prepareLink(UriComponentsBuilder uriBuilder, int pageNumber, int pageSize, String relType) { + return MessageFormat.format(HEADER_LINK_FORMAT, preparePageUri(uriBuilder, pageNumber, pageSize), relType); + } + + private static String preparePageUri(UriComponentsBuilder uriBuilder, int pageNumber, int pageSize) { + return uriBuilder.replaceQueryParam("page", Integer.toString(pageNumber)) + .replaceQueryParam("size", Integer.toString(pageSize)) + .toUriString() + .replace(",", "%2C") + .replace(";", "%3B"); + } +} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/web/util/ResponseUtil.java b/simulator-starter/src/main/java/org/citrusframework/simulator/web/util/ResponseUtil.java new file mode 100644 index 000000000..3ed02565c --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/web/util/ResponseUtil.java @@ -0,0 +1,41 @@ +package org.citrusframework.simulator.web.util; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.server.ResponseStatusException; + +import java.util.Optional; + + +/** + * Utility class for ResponseEntity creation. + */ +public interface ResponseUtil { + + /** + * Wrap the optional into a {@link org.springframework.http.ResponseEntity} with an {@link org.springframework.http.HttpStatus#OK} status, or if it's empty, it + * returns a {@link org.springframework.http.ResponseEntity} with {@link org.springframework.http.HttpStatus#NOT_FOUND}. + * + * @param type of the response + * @param maybeResponse response to return if present + * @return response containing {@code maybeResponse} if present or {@link org.springframework.http.HttpStatus#NOT_FOUND} + */ + static ResponseEntity wrapOrNotFound(Optional maybeResponse) { + return wrapOrNotFound(maybeResponse, null); + } + + /** + * Wrap the optional into a {@link org.springframework.http.ResponseEntity} with an {@link org.springframework.http.HttpStatus#OK} status with the headers, or if it's + * empty, throws a {@link org.springframework.web.server.ResponseStatusException} with status {@link org.springframework.http.HttpStatus#NOT_FOUND}. + * + * @param type of the response + * @param maybeResponse response to return if present + * @param header headers to be added to the response + * @return response containing {@code maybeResponse} if present + */ + static ResponseEntity wrapOrNotFound(Optional maybeResponse, HttpHeaders header) { + return maybeResponse.map(response -> ResponseEntity.ok().headers(header).body(response)) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + } +} diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/IntegrationTest.java b/simulator-starter/src/test/java/org/citrusframework/simulator/IntegrationTest.java new file mode 100644 index 000000000..5fc15912e --- /dev/null +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/IntegrationTest.java @@ -0,0 +1,20 @@ +package org.citrusframework.simulator; + +import org.citrusframework.simulator.test.TestApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Base composite annotation for integration tests. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@SpringBootTest(classes = { TestApplication.class}) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +public @interface IntegrationTest { +} diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/repository/TestResultRepositoryIT.java b/simulator-starter/src/test/java/org/citrusframework/simulator/repository/TestResultRepositoryIT.java new file mode 100644 index 000000000..ed6ce6866 --- /dev/null +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/repository/TestResultRepositoryIT.java @@ -0,0 +1,56 @@ +package org.citrusframework.simulator.repository; + +import org.citrusframework.simulator.IntegrationTest; +import org.citrusframework.simulator.model.TestResult; +import org.citrusframework.simulator.service.dto.TestResultByStatus; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Testing custom methods in {@link TestResultRepository}. + */ +@IntegrationTest +class TestResultRepositoryIT { + + @Autowired + private TestResultRepository testResultRepository; + + private List testResults; + + @BeforeEach + void beforeEachSetup(){ + testResults = testResultRepository.saveAll( + List.of( + TestResult.builder() + .testName("Test-1") + .className(getClass().getSimpleName()) + .status(TestResult.Status.SUCCESS.getId()).build(), + TestResult.builder() + .testName("Test-2") + .className(getClass().getSimpleName()) + .status(TestResult.Status.FAILURE.getId()).build() + ) + ); + } + @Test + @Transactional + void countByStatus(){ + TestResultByStatus testResultByStatus = testResultRepository.countByStatus(); + + assertEquals(2, testResultByStatus.total()); + assertEquals(1, testResultByStatus.successful()); + assertEquals(1, testResultByStatus.failed()); + } + + @AfterEach + void afterEachTeardown(){ + testResultRepository.deleteAll(testResults); + } +} diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/service/MessageRepositoryTest.java b/simulator-starter/src/test/java/org/citrusframework/simulator/service/MessageRepositoryTest.java deleted file mode 100644 index 68b750640..000000000 --- a/simulator-starter/src/test/java/org/citrusframework/simulator/service/MessageRepositoryTest.java +++ /dev/null @@ -1,197 +0,0 @@ -package org.citrusframework.simulator.service; - -import org.citrusframework.simulator.SimulatorAutoConfiguration; -import org.citrusframework.simulator.config.SimulatorConfigurationProperties; -import org.citrusframework.simulator.model.Message; -import org.citrusframework.simulator.model.Message.Direction; -import org.citrusframework.simulator.model.MessageFilter; -import org.citrusframework.simulator.repository.MessageRepository; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; -import org.testng.Assert; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -@DataJpaTest -@ContextConfiguration(classes = { SimulatorAutoConfiguration.class }) -class MessageRepositoryTest extends AbstractTestNGSpringContextTests{ - - private static final QueryFilterAdapterFactory QUERY_FILTER_ADAPTER_FACTORY = new QueryFilterAdapterFactory( - new SimulatorConfigurationProperties()); - - private static final String PAYLOAD = "This is a test message!"; - private static final String HEADER_NAME = "h1"; - private static final String HEADER_NAME2 = "h2"; - - @Autowired - private MessageService service; - - @Autowired - private MessageRepository messageRepository; - - @Test - void testFindByHeader() { - String uid = createTestMessage(); - - MessageFilter filter = new MessageFilter(); - filter.setDirectionOutbound(false); - filter.setHeaderFilter(HEADER_NAME + ":" + uid); - - MessageFilter filterAdapter = QUERY_FILTER_ADAPTER_FACTORY.getQueryAdapter(filter); - - List result = messageRepository.find(filterAdapter); - - Assert.assertEquals(1, result.size()); - Assert.assertEquals(uid, result.get(0).getCitrusMessageId()); - Assert.assertEquals(PAYLOAD, result.get(0).getPayload()); - - filter.setHeaderFilter(HEADER_NAME + ":" + uid + "_3"); - result = messageRepository.find(filterAdapter); - Assert.assertEquals(0, result.size()); - } - - @Test - void testFindByHeaderMulti() { - String uid = createTestMessage(); - - MessageFilter filter = new MessageFilter(); - filter.setDirectionOutbound(false); - filter.setHeaderFilter(HEADER_NAME + ":" + uid + ";" + HEADER_NAME2 + ":" + uid + "_2"); - - MessageFilter filterAdapter = QUERY_FILTER_ADAPTER_FACTORY.getQueryAdapter(filter); - - List result = messageRepository.find(filterAdapter); - - Assert.assertEquals(1, result.size()); - Assert.assertEquals(uid, result.get(0).getCitrusMessageId()); - Assert.assertEquals(PAYLOAD, result.get(0).getPayload()); - - filter.setHeaderFilter(HEADER_NAME + ":" + uid + ";" + HEADER_NAME2 + ":" + uid + "_3"); - - result = messageRepository.find(filterAdapter); - Assert.assertEquals(0, result.size()); - } - - @Test - void testFindByHeaderLike() { - String innerUid = UUID.randomUUID().toString(); - String uid = createTestMessage("PRE" + innerUid + "POST", PAYLOAD); - - MessageFilter filter = new MessageFilter(); - filter.setDirectionOutbound(false); - - filter.setHeaderFilter(HEADER_NAME + ":" + "%" + innerUid + "%"); - - MessageFilter filterAdapter = QUERY_FILTER_ADAPTER_FACTORY.getQueryAdapter(filter); - List result = messageRepository.find(filterAdapter); - - Assert.assertEquals(1, result.size()); - Assert.assertEquals(uid, result.get(0).getCitrusMessageId()); - Assert.assertEquals(PAYLOAD, result.get(0).getPayload()); - } - - @Test - void testFindByPayloadLike() { - String uid = UUID.randomUUID().toString(); - String specificPayload = "Pay" + uid + "LOAD"; - uid = createTestMessage(uid, specificPayload); - - MessageFilter filter = new MessageFilter(); - filter.setDirectionOutbound(false); - filter.setContainingText("%" + uid + "%"); - - MessageFilter filterAdapter = QUERY_FILTER_ADAPTER_FACTORY.getQueryAdapter(filter); - List result = messageRepository.find(filterAdapter); - - Assert.assertEquals(1, result.size()); - Assert.assertEquals(uid, result.get(0).getCitrusMessageId()); - Assert.assertEquals(specificPayload, result.get(0).getPayload()); - } - - @Test - void testFindByPayload() { - String uid = UUID.randomUUID().toString(); - String payload = PAYLOAD+" "+uid; - uid = createTestMessage(uid, payload); - - MessageFilter filter = new MessageFilter(); - filter.setDirectionOutbound(false); - filter.setContainingText("%"+uid+"%"); - - MessageFilter filterAdapter = QUERY_FILTER_ADAPTER_FACTORY.getQueryAdapter(filter); - - List result = messageRepository.find(filterAdapter); - - Assert.assertEquals(1, result.size()); - Assert.assertEquals(uid, result.get(0).getCitrusMessageId()); - Assert.assertEquals(payload, result.get(0).getPayload()); - } - - @Test - void testFindByAllParams() { - Instant startSavingDate = now(); - String uid = createTestMessage(); - Instant endSavingDate = now(); - - // Filter by all valid - MessageFilter filter = new MessageFilter(); - filter.setFromDate(startSavingDate); - filter.setToDate(endSavingDate); - filter.setDirectionOutbound(false); - filter.setContainingText(PAYLOAD); - filter.setHeaderFilter(HEADER_NAME + ":" + uid); - - MessageFilter filterAdapter = QUERY_FILTER_ADAPTER_FACTORY.getQueryAdapter(filter); - List result = messageRepository.find(filterAdapter); - - Assert.assertEquals(1, result.size()); - Assert.assertEquals(PAYLOAD, result.get(0).getPayload()); - } - - @Test - void testPaging() { - String uniquePayload = "PagingPayload" + UUID.randomUUID().toString(); - for (int i = 0; i < 100; i++) { - createTestMessage(UUID.randomUUID().toString(), uniquePayload); - } - - MessageFilter filter = new MessageFilter(); - filter.setDirectionOutbound(false); - filter.setContainingText(uniquePayload); - filter.setPageNumber(0); - filter.setPageSize(33); - - MessageFilter filterAdapter = QUERY_FILTER_ADAPTER_FACTORY.getQueryAdapter(filter); - List result = messageRepository.find(filterAdapter); - - Assert.assertEquals(33, result.size()); - } - - private Instant now() { - return LocalDateTime.now().toInstant(ZoneOffset.UTC); - } - - private String createTestMessage() { - return createTestMessage(UUID.randomUUID().toString(), PAYLOAD); - } - - private String createTestMessage(String uid, String payload) { - - Map headers = new HashMap(); - headers.put(HEADER_NAME, uid); - headers.put(HEADER_NAME2, uid + "_2"); - - service.saveMessage(Direction.INBOUND, payload, uid, headers); - - return uid; - } -} diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/service/MessageServiceTest.java b/simulator-starter/src/test/java/org/citrusframework/simulator/service/MessageServiceTest.java index adca5d9eb..f1d79ff54 100644 --- a/simulator-starter/src/test/java/org/citrusframework/simulator/service/MessageServiceTest.java +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/service/MessageServiceTest.java @@ -24,12 +24,10 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; -import org.springframework.data.domain.Pageable; import org.testng.Assert; import org.testng.annotations.AfterMethod; import java.time.Instant; -import java.util.Collection; import java.util.Collections; import java.util.List; @@ -41,8 +39,6 @@ public class MessageServiceTest { new SimulatorConfigurationProperties()); private MessageRepository messageRepository; - private ArgumentCaptor> directionsCaptor; - private ArgumentCaptor pageableCaptor; private MessageService sut; private ArgumentCaptor messageFilterCaptor; @@ -52,8 +48,6 @@ public class MessageServiceTest { public void init() { messageFilterCaptor = ArgumentCaptor.forClass(MessageFilter.class); messageRepository = Mockito.mock(MessageRepository.class); - directionsCaptor = ArgumentCaptor.forClass(Collection.class); - pageableCaptor = ArgumentCaptor.forClass(Pageable.class); sut = new MessageService(messageRepository, queryFilterAdapterFactory); } @@ -116,5 +110,4 @@ private void assertDirectionMatches(boolean inbound, boolean outbound) { Assert.assertEquals(messageFilterCaptor.getValue().getDirectionInbound(), (Boolean) inbound); Assert.assertEquals(messageFilterCaptor.getValue().getDirectionOutbound(), (Boolean) outbound); } - } diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/service/ScenarioExecutionRepositoryTest.java b/simulator-starter/src/test/java/org/citrusframework/simulator/service/ScenarioExecutionRepositoryTest.java deleted file mode 100644 index 9aff6a0e6..000000000 --- a/simulator-starter/src/test/java/org/citrusframework/simulator/service/ScenarioExecutionRepositoryTest.java +++ /dev/null @@ -1,265 +0,0 @@ -/* - * Copyright 2006-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.citrusframework.simulator.service; - -import jakarta.persistence.EntityManager; -import org.citrusframework.DefaultTestCase; -import org.citrusframework.simulator.SimulatorAutoConfiguration; -import org.citrusframework.simulator.config.SimulatorConfigurationProperties; -import org.citrusframework.simulator.model.Message.Direction; -import org.citrusframework.simulator.model.ScenarioExecution; -import org.citrusframework.simulator.model.ScenarioExecution.Status; -import org.citrusframework.simulator.model.ScenarioExecutionFilter; -import org.citrusframework.simulator.repository.ScenarioExecutionRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; -import org.testng.annotations.Test; - -import java.time.Instant; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import static org.testng.AssertJUnit.assertEquals; - -@DataJpaTest -@ContextConfiguration(classes = {SimulatorAutoConfiguration.class}) -public class ScenarioExecutionRepositoryTest extends AbstractTestNGSpringContextTests { - - private static final String TEST_SCENARIO= "test-scenario"; - - private static final SimulatorConfigurationProperties PROPERTIES = new SimulatorConfigurationProperties(); - - private static final QueryFilterAdapterFactory queryFilterAdapterFactory = new QueryFilterAdapterFactory(PROPERTIES); - - private static final String PAYLOAD = "test-payload"; - - private static final String IN_HEADER_NAME1 = "IH1"; - private static final String IN_HEADER_NAME2 = "IH2"; - - private static final String OUT_HEADER_NAME1 = "OH1"; - private static final String OUT_HEADER_NAME2 = "OH2"; - - @Autowired - private ActivityService activityService; - - @Autowired - private EntityManager entityManager; - - @Autowired - private ScenarioExecutionRepository scenarioExecutionRepository; - - @Test - void testFindByScenarioName() { - String inUid = UUID.randomUUID().toString(); - String outUid = UUID.randomUUID().toString(); - - String uniqueScenarioName = TEST_SCENARIO + UUID.randomUUID().toString(); - createTestScenarioExecution(uniqueScenarioName, inUid, PAYLOAD, outUid, PAYLOAD, Status.SUCCESS); - - ScenarioExecutionFilter scenarioExecutionFilter = new ScenarioExecutionFilter(); - scenarioExecutionFilter.setScenarioName(uniqueScenarioName); - - ScenarioExecutionFilter queryFilter = queryFilterAdapterFactory.getQueryAdapter(scenarioExecutionFilter); - - assertEquals(1, scenarioExecutionRepository.find(queryFilter).size()); - } - - @Test - void testFindByScenarioStatus() { - String inUid = UUID.randomUUID().toString(); - String outUid = UUID.randomUUID().toString(); - - String uniqueScenarioName = TEST_SCENARIO + UUID.randomUUID(); - createTestScenarioExecution(uniqueScenarioName, inUid + 1, PAYLOAD, outUid + 1, PAYLOAD, Status.SUCCESS); - Long failedScenarioExecutionId = createTestScenarioExecution(uniqueScenarioName, inUid + 2, PAYLOAD, outUid + 2, PAYLOAD, Status.FAILED); - - ScenarioExecutionFilter scenarioExecutionFilter = new ScenarioExecutionFilter(); - scenarioExecutionFilter.setExecutionStatus(new Status[] {Status.FAILED}); - - ScenarioExecutionFilter queryFilter = queryFilterAdapterFactory.getQueryAdapter(scenarioExecutionFilter); - - List result = scenarioExecutionRepository.find(queryFilter); - assertEquals(1, result.size()); - assertEquals(failedScenarioExecutionId, result.get(0).getExecutionId()); - } - - @Test - void testFindByHeader() { - String inUid = UUID.randomUUID().toString(); - String outUid = UUID.randomUUID().toString(); - createTestScenarioExecution(inUid, PAYLOAD, outUid, PAYLOAD); - - ScenarioExecutionFilter scenarioExecutionFilter = new ScenarioExecutionFilter(); - scenarioExecutionFilter.setHeaderFilter(IN_HEADER_NAME1 + ":" + inUid); - - - ScenarioExecutionFilter queryFilter = queryFilterAdapterFactory.getQueryAdapter(scenarioExecutionFilter); - - assertEquals(1, scenarioExecutionRepository.find(queryFilter).size()); - - scenarioExecutionFilter.setHeaderFilter(IN_HEADER_NAME1 + ":" + inUid + "mod"); - - assertEquals(0, scenarioExecutionRepository.find(queryFilter).size()); - } - - @Test - void testFindByHeaderMulti() { - String inUid = UUID.randomUUID().toString(); - String outUid = UUID.randomUUID().toString(); - createTestScenarioExecution(inUid, PAYLOAD, outUid, PAYLOAD); - - ScenarioExecutionFilter scenarioExecutionFilter = new ScenarioExecutionFilter(); - scenarioExecutionFilter.setHeaderFilter(IN_HEADER_NAME1 + ":" + inUid + ";" + IN_HEADER_NAME2 + ":" + inUid + "_2"); - - ScenarioExecutionFilter queryFilter = queryFilterAdapterFactory.getQueryAdapter(scenarioExecutionFilter); - - assertEquals(1, scenarioExecutionRepository.find(queryFilter).size()); - - scenarioExecutionFilter.setHeaderFilter(IN_HEADER_NAME1 + ":" + inUid + ";" + IN_HEADER_NAME2 + ":" + inUid + "_3"); - - assertEquals(0, scenarioExecutionRepository.find(queryFilter).size()); - } - - @Test - void testFindByPayload() { - String inUid = UUID.randomUUID().toString(); - String outUid = UUID.randomUUID().toString(); - String inPayload = PAYLOAD + inUid + "-in"; - String outPayload = PAYLOAD + outUid + "-out"; - - createTestScenarioExecution(inUid, inPayload, outUid, outPayload); - - ScenarioExecutionFilter scenarioExecutionFilter = new ScenarioExecutionFilter(); - - ScenarioExecutionFilter queryFilter = queryFilterAdapterFactory.getQueryAdapter(scenarioExecutionFilter); - - scenarioExecutionFilter.setContainingText(inPayload); - assertEquals(1, scenarioExecutionRepository.find(queryFilter).size()); - - scenarioExecutionFilter.setContainingText(inPayload + "mod"); - assertEquals(0, scenarioExecutionRepository.find(queryFilter).size()); - - scenarioExecutionFilter.setDirectionInbound(false); - scenarioExecutionFilter.setContainingText(inPayload); - assertEquals(0, scenarioExecutionRepository.find(queryFilter).size()); - - scenarioExecutionFilter.setDirectionInbound(true); - scenarioExecutionFilter.setContainingText(inPayload); - assertEquals(1, scenarioExecutionRepository.find(queryFilter).size()); - - scenarioExecutionFilter.setDirectionOutbound(true); - scenarioExecutionFilter.setContainingText(outPayload); - assertEquals(1, scenarioExecutionRepository.find(queryFilter).size()); - - scenarioExecutionFilter.setDirectionOutbound(false); - scenarioExecutionFilter.setContainingText(outPayload); - assertEquals(0, scenarioExecutionRepository.find(queryFilter).size()); - } - - @Test - void testPaging() { - String uniquePayload = "PagingPayload" + UUID.randomUUID().toString(); - for (int i = 0; i < 100; i++) { - createTestScenarioExecution(UUID.randomUUID().toString(), uniquePayload + "-in", UUID.randomUUID().toString(), uniquePayload + "-out"); - } - - ScenarioExecutionFilter scenarioExecutionFilter = new ScenarioExecutionFilter(); - scenarioExecutionFilter.setPageNumber(0); - scenarioExecutionFilter.setPageSize(33); - - ScenarioExecutionFilter queryFilter = queryFilterAdapterFactory.getQueryAdapter(scenarioExecutionFilter); - List result = scenarioExecutionRepository.find(queryFilter); - - assertEquals(result.size(), 33); - } - - @Test - void testFindByDate() { - String uniquePayload = "FindByDatePayload" + UUID.randomUUID(); - - Instant t1 = Instant.now(); - - int batch1Size = 100; - for (int i = 0; i < batch1Size; i++) { - createTestScenarioExecution(UUID.randomUUID().toString(), uniquePayload + "-in", UUID.randomUUID().toString(), uniquePayload + "-out"); - } - - entityManager.flush(); - - Instant t2 = Instant.now(); - - int batch2Size = 50; - for (int i = 0; i < batch2Size; i++) { - createTestScenarioExecution(UUID.randomUUID().toString(), uniquePayload + "-in", UUID.randomUUID().toString(), uniquePayload + "-out"); - } - - entityManager.flush(); - - Instant t3 = Instant.now(); - - ScenarioExecutionFilter scenarioExecutionFilter = new ScenarioExecutionFilter(); - scenarioExecutionFilter.setPageNumber(0); - scenarioExecutionFilter.setPageSize(1000); - - ScenarioExecutionFilter queryFilter = queryFilterAdapterFactory.getQueryAdapter(scenarioExecutionFilter); - - scenarioExecutionFilter.setFromDate(t1); - scenarioExecutionFilter.setToDate(t2); - assertEquals(batch1Size, scenarioExecutionRepository.find(queryFilter).size()); - - scenarioExecutionFilter.setFromDate(t1); - scenarioExecutionFilter.setToDate(t3); - assertEquals(batch1Size + batch2Size, scenarioExecutionRepository.find(queryFilter).size()); - - scenarioExecutionFilter.setFromDate(t2); - scenarioExecutionFilter.setToDate(t3); - assertEquals(batch2Size, scenarioExecutionRepository.find(queryFilter).size()); - } - - private Long createTestScenarioExecution(String inUid, String inPayload, String outUid, String outPayload) { - return createTestScenarioExecution(TEST_SCENARIO, inUid, inPayload, outUid, outPayload, Status.SUCCESS); - } - - private Long createTestScenarioExecution(String scenarioName, String inUid, String inPayload, String outUid, String outPayload, Status status) { - Map inHeaders = new HashMap<>(); - inHeaders.put(IN_HEADER_NAME1, inUid); - inHeaders.put(IN_HEADER_NAME2, inUid + "_2"); - - ScenarioExecution scenarioExecution = activityService.createExecutionScenario(scenarioName, Collections.emptySet()); - activityService.saveScenarioMessage(scenarioExecution.getExecutionId(), Direction.INBOUND, inPayload, inUid, inHeaders); - - Map outHeaders = new HashMap<>(); - outHeaders.put(OUT_HEADER_NAME1, outUid); - outHeaders.put(OUT_HEADER_NAME2, outUid + "_2"); - activityService.saveScenarioMessage(scenarioExecution.getExecutionId(), Direction.OUTBOUND, outPayload, outUid, outHeaders); - - DefaultTestCase testCase = new DefaultTestCase(); - testCase.getVariableDefinitions().put(ScenarioExecution.EXECUTION_ID, scenarioExecution.getExecutionId()); - - if (Status.SUCCESS == status) { - activityService.completeScenarioExecutionSuccess(testCase); - } else if (Status.FAILED == status) { - activityService.completeScenarioExecutionFailure(testCase, null); - } - - return scenarioExecution.getExecutionId(); - } -} diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/service/criteria/TestParameterCriteriaTest.java b/simulator-starter/src/test/java/org/citrusframework/simulator/service/criteria/TestParameterCriteriaTest.java new file mode 100644 index 000000000..658f63033 --- /dev/null +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/service/criteria/TestParameterCriteriaTest.java @@ -0,0 +1,92 @@ +package org.citrusframework.simulator.service.criteria; + +import org.citrusframework.simulator.service.filter.InstantFilter; +import org.citrusframework.simulator.service.filter.LongFilter; +import org.citrusframework.simulator.service.filter.StringFilter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class TestParameterCriteriaTest { + + private TestParameterCriteria criteria; + + @BeforeEach + void beforeEachSetup() { + criteria = new TestParameterCriteria(); + } + + @Test + void testKey() { + assertNull(criteria.getKey()); + + StringFilter keyFilter = criteria.key(); + assertNotNull(keyFilter); + assertSame(keyFilter, criteria.getKey()); + + StringFilter mockKeyFilter = mock(StringFilter.class); + criteria.setKey(mockKeyFilter); + assertSame(mockKeyFilter, criteria.key()); + } + + @Test + void testValue() { + assertNull(criteria.getValue()); + + StringFilter valueFilter = criteria.value(); + assertNotNull(valueFilter); + assertSame(valueFilter, criteria.getValue()); + + StringFilter mockValueFilter = mock(StringFilter.class); + criteria.setValue(mockValueFilter); + assertSame(mockValueFilter, criteria.value()); + } + + @Test + void testCreatedDate() { + assertNull(criteria.getCreatedDate()); + + InstantFilter createdDateFilter = criteria.createdDate(); + assertNotNull(createdDateFilter); + assertSame(createdDateFilter, criteria.getCreatedDate()); + + InstantFilter mockCreatedDateFilter = mock(InstantFilter.class); + criteria.setCreatedDate(mockCreatedDateFilter); + assertSame(mockCreatedDateFilter, criteria.createdDate()); + } + + @Test + void testLastModifiedDate() { + assertNull(criteria.getLastModifiedDate()); + + InstantFilter lastModifiedDateFilter = criteria.lastModifiedDate(); + assertNotNull(lastModifiedDateFilter); + assertSame(lastModifiedDateFilter, criteria.getLastModifiedDate()); + + InstantFilter mockLastModifiedDateFilter = mock(InstantFilter.class); + criteria.setLastModifiedDate(mockLastModifiedDateFilter); + assertSame(mockLastModifiedDateFilter, criteria.lastModifiedDate()); + } + + @Test + void testTestResultId() { + assertNull(criteria.getTestResultId()); + + LongFilter testResultIdFilter = criteria.testResultId(); + assertNotNull(testResultIdFilter); + assertSame(testResultIdFilter, criteria.getTestResultId()); + + LongFilter mockTestResultIdFilter = mock(LongFilter.class); + criteria.setTestResultId(mockTestResultIdFilter); + assertSame(mockTestResultIdFilter, criteria.testResultId()); + } + + @Test + void testCopy() { + TestParameterCriteria copiedCriteria = criteria.copy(); + assertNotSame(copiedCriteria, criteria); + assertEquals(copiedCriteria, criteria); + } +} diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/service/criteria/TestResultCriteriaTest.java b/simulator-starter/src/test/java/org/citrusframework/simulator/service/criteria/TestResultCriteriaTest.java new file mode 100644 index 000000000..0c9d76982 --- /dev/null +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/service/criteria/TestResultCriteriaTest.java @@ -0,0 +1,162 @@ +package org.citrusframework.simulator.service.criteria; + +import org.citrusframework.simulator.service.filter.InstantFilter; +import org.citrusframework.simulator.service.filter.IntegerFilter; +import org.citrusframework.simulator.service.filter.LongFilter; +import org.citrusframework.simulator.service.filter.StringFilter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.mock; + +class TestResultCriteriaTest { + + private TestResultCriteria criteria; + + @BeforeEach + void beforeEachSetup() { + criteria = new TestResultCriteria(); + } + + @Test + void testId() { + assertNull(criteria.getId()); + + LongFilter idFilter = criteria.id(); + assertNotNull(idFilter); + assertSame(idFilter, criteria.getId()); + + LongFilter mockIdFilter = mock(LongFilter.class); + criteria.setId(mockIdFilter); + assertSame(mockIdFilter, criteria.id()); + } + + @Test + void testStatus() { + assertNull(criteria.getStatus()); + + IntegerFilter statusFilter = criteria.status(); + assertNotNull(statusFilter); + assertSame(statusFilter, criteria.getStatus()); + + IntegerFilter mockStatusFilter = mock(IntegerFilter.class); + criteria.setStatus(mockStatusFilter); + assertSame(mockStatusFilter, criteria.status()); + } + + @Test + void testTestName() { + assertNull(criteria.getTestName()); + + StringFilter testNameFilter = criteria.testName(); + assertNotNull(testNameFilter); + assertSame(testNameFilter, criteria.getTestName()); + + StringFilter mockTestNameFilter = mock(StringFilter.class); + criteria.setTestName(mockTestNameFilter); + assertSame(mockTestNameFilter, criteria.testName()); + } + + @Test + void testClassName() { + assertNull(criteria.getClassName()); + + StringFilter classNameFilter = criteria.className(); + assertNotNull(classNameFilter); + assertSame(classNameFilter, criteria.getClassName()); + + StringFilter mockClassNameFilter = mock(StringFilter.class); + criteria.setClassName(mockClassNameFilter); + assertSame(mockClassNameFilter, criteria.className()); + } + + @Test + void testErrorMessage() { + assertNull(criteria.getErrorMessage()); + + StringFilter errorMessageFilter = criteria.errorMessage(); + assertNotNull(errorMessageFilter); + assertSame(errorMessageFilter, criteria.getErrorMessage()); + + StringFilter mockErrorMessageFilter = mock(StringFilter.class); + criteria.setErrorMessage(mockErrorMessageFilter); + assertSame(mockErrorMessageFilter, criteria.errorMessage()); + } + + @Test + void testFailureStack() { + assertNull(criteria.getFailureStack()); + + StringFilter failureStackFilter = criteria.failureStack(); + assertNotNull(failureStackFilter); + assertSame(failureStackFilter, criteria.getFailureStack()); + + StringFilter mockFailureStackFilter = mock(StringFilter.class); + criteria.setFailureStack(mockFailureStackFilter); + assertSame(mockFailureStackFilter, criteria.failureStack()); + } + + @Test + void testFailureType() { + assertNull(criteria.getFailureType()); + + StringFilter failureTypeFilter = criteria.failureType(); + assertNotNull(failureTypeFilter); + assertSame(failureTypeFilter, criteria.getFailureType()); + + StringFilter mockFailureTypeFilter = mock(StringFilter.class); + criteria.setFailureType(mockFailureTypeFilter); + assertSame(mockFailureTypeFilter, criteria.failureType()); + } + + @Test + void testCreatedDate() { + assertNull(criteria.getCreatedDate()); + + InstantFilter createdDateFilter = criteria.createdDate(); + assertNotNull(createdDateFilter); + assertSame(createdDateFilter, criteria.getCreatedDate()); + + InstantFilter mockCreatedDateFilter = mock(InstantFilter.class); + criteria.setCreatedDate(mockCreatedDateFilter); + assertSame(mockCreatedDateFilter, criteria.createdDate()); + } + + @Test + void testLastModifiedDate() { + assertNull(criteria.getLastModifiedDate()); + + InstantFilter lastModifiedDateFilter = criteria.lastModifiedDate(); + assertNotNull(lastModifiedDateFilter); + assertSame(lastModifiedDateFilter, criteria.getLastModifiedDate()); + + InstantFilter mockLastModifiedDateFilter = mock(InstantFilter.class); + criteria.setLastModifiedDate(mockLastModifiedDateFilter); + assertSame(mockLastModifiedDateFilter, criteria.lastModifiedDate()); + } + + @Test + void testTestParameterKey() { + assertNull(criteria.getTestParameterKey()); + + StringFilter testParameterKeyFilter = criteria.testParameterId(); + assertNotNull(testParameterKeyFilter); + assertSame(testParameterKeyFilter, criteria.getTestParameterKey()); + + StringFilter mockTestParameterKeyFilter = mock(StringFilter.class); + criteria.setTestParameterKey(mockTestParameterKeyFilter); + assertSame(mockTestParameterKeyFilter, criteria.testParameterId()); + } + + @Test + void testCopy() { + TestResultCriteria copiedCriteria = criteria.copy(); + assertNotSame(copiedCriteria, criteria); + assertEquals(copiedCriteria, criteria); + } +} diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/service/dto/TestResultByStatusTest.java b/simulator-starter/src/test/java/org/citrusframework/simulator/service/dto/TestResultByStatusTest.java new file mode 100644 index 000000000..df889a6ef --- /dev/null +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/service/dto/TestResultByStatusTest.java @@ -0,0 +1,37 @@ +package org.citrusframework.simulator.service.dto; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TestResultByStatusTest { + + static Stream constructorCalculatesTotal() { + return Stream.of( + Arguments.of(0, 0, 0), + Arguments.of(0, 1, 1), + Arguments.of(1, 0, 1), + Arguments.of(1, 2, 3), + Arguments.of(2, 3, 5) + ); + } + + @MethodSource + @ParameterizedTest + void constructorCalculatesTotal(int succeeded, int failed, int total) { + assertEquals(total, new TestResultByStatus((long) succeeded, (long) failed).total()); + } + + @Test + void constructorIsNullResistant() { + TestResultByStatus testResultByStatus = new TestResultByStatus(null, null); + assertEquals(0, testResultByStatus.total()); + assertEquals(0, testResultByStatus.successful()); + assertEquals(0, testResultByStatus.failed()); + } +} diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/service/filter/FilterTest.java b/simulator-starter/src/test/java/org/citrusframework/simulator/service/filter/FilterTest.java new file mode 100644 index 000000000..766ff2bc7 --- /dev/null +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/service/filter/FilterTest.java @@ -0,0 +1,96 @@ +package org.citrusframework.simulator.service.filter; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class FilterTest { + + @Test + void testConstructor() { + Filter filter = new Filter<>(); + assertNull(filter.getEquals()); + assertNull(filter.getNotEquals()); + assertNull(filter.getSpecified()); + assertNull(filter.getIn()); + assertNull(filter.getNotIn()); + } + + @Test + void testCopyConstructor() { + Filter original = new Filter<>(); + original.setEquals("testEquals"); + original.setNotEquals("testNotEquals"); + original.setSpecified(true); + original.setIn(Arrays.asList("test1", "test2")); + original.setNotIn(Arrays.asList("test3", "test4")); + + Filter copy = new Filter<>(original); + + assertEquals(original, copy); + assertNotSame(original.getIn(), copy.getIn()); + assertNotSame(original.getNotIn(), copy.getNotIn()); + } + + @Test + void testGettersAndSetters() { + Filter filter = new Filter<>(); + String testValue = "testValue"; + List testList = Arrays.asList("test1", "test2"); + + filter.setEquals(testValue); + assertEquals(testValue, filter.getEquals()); + + filter.setNotEquals(testValue); + assertEquals(testValue, filter.getNotEquals()); + + filter.setSpecified(true); + assertTrue(filter.getSpecified()); + + filter.setIn(testList); + assertEquals(testList, filter.getIn()); + + filter.setNotIn(testList); + assertEquals(testList, filter.getNotIn()); + } + + @Test + void testCopy() { + Filter original = new Filter<>(); + original.setEquals("testEquals"); + Filter copy = original.copy(); + + assertEquals(original, copy); + assertNotSame(original, copy); + } + + @Test + void testEqualsAndHashCode() { + Filter filter1 = new Filter<>(); + Filter filter2 = new Filter<>(); + + assertEquals(filter1, filter2); + assertEquals(filter1.hashCode(), filter2.hashCode()); + + filter1.setEquals("test"); + assertNotEquals(filter1, filter2); + + filter2.setEquals("test"); + assertEquals(filter1, filter2); + assertEquals(filter1.hashCode(), filter2.hashCode()); + } + + @Test + void testToString() { + Filter filter = new Filter<>(); + filter.setEquals("testEquals"); + assertTrue(filter.toString().contains("testEquals")); + } +} diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/service/filter/InstantFilterTest.java b/simulator-starter/src/test/java/org/citrusframework/simulator/service/filter/InstantFilterTest.java new file mode 100644 index 000000000..263a22c91 --- /dev/null +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/service/filter/InstantFilterTest.java @@ -0,0 +1,75 @@ +package org.citrusframework.simulator.service.filter; + +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class InstantFilterTest { + + @Test + void testConstructor() { + InstantFilter filter = new InstantFilter(); + assertNull(filter.getEquals()); + assertNull(filter.getNotEquals()); + assertNull(filter.getGreaterThan()); + assertNull(filter.getLessThan()); + assertNull(filter.getGreaterThanOrEqual()); + assertNull(filter.getLessThanOrEqual()); + assertNull(filter.getIn()); + assertNull(filter.getNotIn()); + } + + @Test + void testCopyConstructor() { + InstantFilter original = new InstantFilter(); + Instant now = Instant.now(); + original.setEquals(now); + original.setNotEquals(now.plusSeconds(10)); + original.setGreaterThan(now.plusSeconds(20)); + original.setLessThan(now.plusSeconds(30)); + original.setGreaterThanOrEqual(now.plusSeconds(40)); + original.setLessThanOrEqual(now.plusSeconds(50)); + original.setIn(Arrays.asList(now, now.plusSeconds(60))); + original.setNotIn(Arrays.asList(now.plusSeconds(70), now.plusSeconds(80))); + + InstantFilter copy = new InstantFilter(original); + + assertEquals(original, copy); + assertNotSame(original.getIn(), copy.getIn()); + assertNotSame(original.getNotIn(), copy.getNotIn()); + } + + @Test + void testCopy() { + InstantFilter original = new InstantFilter(); + Instant now = Instant.now(); + original.setEquals(now); + InstantFilter copy = original.copy(); + + assertEquals(original, copy); + assertNotSame(original, copy); + } + + @Test + void testSetters() { + InstantFilter filter = new InstantFilter(); + Instant now = Instant.now(); + List testList = Arrays.asList(now, now.plusSeconds(10)); + + assertTrue(filter.setEquals(now) instanceof InstantFilter); + assertTrue(filter.setNotEquals(now) instanceof InstantFilter); + assertTrue(filter.setGreaterThan(now) instanceof InstantFilter); + assertTrue(filter.setLessThan(now) instanceof InstantFilter); + assertTrue(filter.setGreaterThanOrEqual(now) instanceof InstantFilter); + assertTrue(filter.setLessThanOrEqual(now) instanceof InstantFilter); + assertTrue(filter.setIn(testList) instanceof InstantFilter); + assertTrue(filter.setNotIn(testList) instanceof InstantFilter); + } +} diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/service/filter/IntegerFilterTest.java b/simulator-starter/src/test/java/org/citrusframework/simulator/service/filter/IntegerFilterTest.java new file mode 100644 index 000000000..b1c7e0483 --- /dev/null +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/service/filter/IntegerFilterTest.java @@ -0,0 +1,54 @@ +package org.citrusframework.simulator.service.filter; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; + +class IntegerFilterTest { + + @Test + void testConstructor() { + IntegerFilter filter = new IntegerFilter(); + assertNull(filter.getEquals()); + assertNull(filter.getNotEquals()); + assertNull(filter.getGreaterThan()); + assertNull(filter.getLessThan()); + assertNull(filter.getGreaterThanOrEqual()); + assertNull(filter.getLessThanOrEqual()); + assertNull(filter.getIn()); + assertNull(filter.getNotIn()); + } + + @Test + void testCopyConstructor() { + IntegerFilter original = new IntegerFilter(); + original.setEquals(1); + original.setNotEquals(2); + original.setGreaterThan(3); + original.setLessThan(4); + original.setGreaterThanOrEqual(5); + original.setLessThanOrEqual(6); + original.setIn(Arrays.asList(7, 8)); + original.setNotIn(Arrays.asList(9, 10)); + + IntegerFilter copy = new IntegerFilter(original); + + assertEquals(original, copy); + assertNotSame(original.getIn(), copy.getIn()); + assertNotSame(original.getNotIn(), copy.getNotIn()); + } + + @Test + void testCopy() { + IntegerFilter original = new IntegerFilter(); + original.setEquals(1); + IntegerFilter copy = original.copy(); + + assertEquals(original, copy); + assertNotSame(original, copy); + } +} diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/service/filter/LongFilterTest.java b/simulator-starter/src/test/java/org/citrusframework/simulator/service/filter/LongFilterTest.java new file mode 100644 index 000000000..bfd294c7d --- /dev/null +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/service/filter/LongFilterTest.java @@ -0,0 +1,54 @@ +package org.citrusframework.simulator.service.filter; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; + +class LongFilterTest { + + @Test + void testConstructor() { + LongFilter filter = new LongFilter(); + assertNull(filter.getEquals()); + assertNull(filter.getNotEquals()); + assertNull(filter.getGreaterThan()); + assertNull(filter.getLessThan()); + assertNull(filter.getGreaterThanOrEqual()); + assertNull(filter.getLessThanOrEqual()); + assertNull(filter.getIn()); + assertNull(filter.getNotIn()); + } + + @Test + void testCopyConstructor() { + LongFilter original = new LongFilter(); + original.setEquals(1L); + original.setNotEquals(2L); + original.setGreaterThan(3L); + original.setLessThan(4L); + original.setGreaterThanOrEqual(5L); + original.setLessThanOrEqual(6L); + original.setIn(Arrays.asList(7L, 8L)); + original.setNotIn(Arrays.asList(9L, 10L)); + + LongFilter copy = new LongFilter(original); + + assertEquals(original, copy); + assertNotSame(original.getIn(), copy.getIn()); + assertNotSame(original.getNotIn(), copy.getNotIn()); + } + + @Test + void testCopy() { + LongFilter original = new LongFilter(); + original.setEquals(1L); + LongFilter copy = original.copy(); + + assertEquals(original, copy); + assertNotSame(original, copy); + } +} diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/service/filter/RangeFilterTest.java b/simulator-starter/src/test/java/org/citrusframework/simulator/service/filter/RangeFilterTest.java new file mode 100644 index 000000000..9433d43e9 --- /dev/null +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/service/filter/RangeFilterTest.java @@ -0,0 +1,111 @@ +package org.citrusframework.simulator.service.filter; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RangeFilterTest { + + @Test + void testConstructor() { + RangeFilter filter = new RangeFilter<>(); + assertNull(filter.getEquals()); + assertNull(filter.getNotEquals()); + assertNull(filter.getGreaterThan()); + assertNull(filter.getLessThan()); + assertNull(filter.getGreaterThanOrEqual()); + assertNull(filter.getLessThanOrEqual()); + assertNull(filter.getIn()); + assertNull(filter.getNotIn()); + } + + @Test + void testCopyConstructor() { + RangeFilter original = new RangeFilter<>(); + original.setEquals("testEquals"); + original.setNotEquals("testNotEquals"); + original.setGreaterThan("testGreaterThan"); + original.setLessThan("testLessThan"); + original.setGreaterThanOrEqual("testGreaterThanOrEqual"); + original.setLessThanOrEqual("testLessThanOrEqual"); + original.setIn(Arrays.asList("test1", "test2")); + original.setNotIn(Arrays.asList("test3", "test4")); + + RangeFilter copy = new RangeFilter<>(original); + + assertEquals(original, copy); + assertNotSame(original.getIn(), copy.getIn()); + assertNotSame(original.getNotIn(), copy.getNotIn()); + } + + @Test + void testGettersAndSetters() { + RangeFilter filter = new RangeFilter<>(); + String testValue = "testValue"; + List testList = Arrays.asList("test1", "test2"); + + filter.setEquals(testValue); + assertEquals(testValue, filter.getEquals()); + + filter.setNotEquals(testValue); + assertEquals(testValue, filter.getNotEquals()); + + filter.setGreaterThan(testValue); + assertEquals(testValue, filter.getGreaterThan()); + + filter.setLessThan(testValue); + assertEquals(testValue, filter.getLessThan()); + + filter.setGreaterThanOrEqual(testValue); + assertEquals(testValue, filter.getGreaterThanOrEqual()); + + filter.setLessThanOrEqual(testValue); + assertEquals(testValue, filter.getLessThanOrEqual()); + + filter.setIn(testList); + assertEquals(testList, filter.getIn()); + + filter.setNotIn(testList); + assertEquals(testList, filter.getNotIn()); + } + + @Test + void testEqualsAndHashCode() { + RangeFilter filter1 = new RangeFilter<>(); + RangeFilter filter2 = new RangeFilter<>(); + + assertEquals(filter1, filter2); + assertEquals(filter1.hashCode(), filter2.hashCode()); + + filter1.setEquals("test"); + assertNotEquals(filter1, filter2); + + filter2.setEquals("test"); + assertEquals(filter1, filter2); + assertEquals(filter1.hashCode(), filter2.hashCode()); + } + + @Test + void testToString() { + RangeFilter filter = new RangeFilter<>(); + filter.setEquals("testEquals"); + assertTrue(filter.toString().contains("testEquals")); + } + + @Test + void testCopy() { + RangeFilter original = new RangeFilter<>(); + original.setEquals("testEquals"); + RangeFilter copy = original.copy(); + + assertEquals(original, copy); + assertNotSame(original, copy); + } +} diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/service/filter/StringFilterTest.java b/simulator-starter/src/test/java/org/citrusframework/simulator/service/filter/StringFilterTest.java new file mode 100644 index 000000000..b06a08926 --- /dev/null +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/service/filter/StringFilterTest.java @@ -0,0 +1,101 @@ +package org.citrusframework.simulator.service.filter; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class StringFilterTest { + + @Test + void testConstructor() { + StringFilter filter = new StringFilter(); + assertNull(filter.getEquals()); + assertNull(filter.getNotEquals()); + assertNull(filter.getIn()); + assertNull(filter.getNotIn()); + assertNull(filter.getContains()); + assertNull(filter.getDoesNotContain()); + } + + @Test + void testCopyConstructor() { + StringFilter original = new StringFilter(); + original.setEquals("testEquals"); + original.setNotEquals("testNotEquals"); + original.setIn(Arrays.asList("test1", "test2")); + original.setNotIn(Arrays.asList("test3", "test4")); + original.setContains("testContains"); + original.setDoesNotContain("testDoesNotContain"); + + StringFilter copy = new StringFilter(original); + + assertEquals(original, copy); + assertNotSame(original.getIn(), copy.getIn()); + assertNotSame(original.getNotIn(), copy.getNotIn()); + } + + @Test + void testGettersAndSetters() { + StringFilter filter = new StringFilter(); + String testValue = "testValue"; + List testList = Arrays.asList("test1", "test2"); + + filter.setEquals(testValue); + assertEquals(testValue, filter.getEquals()); + + filter.setNotEquals(testValue); + assertEquals(testValue, filter.getNotEquals()); + + filter.setIn(testList); + assertEquals(testList, filter.getIn()); + + filter.setNotIn(testList); + assertEquals(testList, filter.getNotIn()); + + filter.setContains(testValue); + assertEquals(testValue, filter.getContains()); + + filter.setDoesNotContain(testValue); + assertEquals(testValue, filter.getDoesNotContain()); + } + + @Test + void testCopy() { + StringFilter original = new StringFilter(); + original.setEquals("testEquals"); + StringFilter copy = original.copy(); + + assertEquals(original, copy); + assertNotSame(original, copy); + } + + @Test + void testEqualsAndHashCode() { + StringFilter filter1 = new StringFilter(); + StringFilter filter2 = new StringFilter(); + + assertEquals(filter1, filter2); + assertEquals(filter1.hashCode(), filter2.hashCode()); + + filter1.setEquals("test"); + assertNotEquals(filter1, filter2); + + filter2.setEquals("test"); + assertEquals(filter1, filter2); + assertEquals(filter1.hashCode(), filter2.hashCode()); + } + + @Test + void testToString() { + StringFilter filter = new StringFilter(); + filter.setEquals("testEquals"); + assertTrue(filter.toString().contains("testEquals")); + } +} diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/service/impl/TestParameterServiceImplTest.java b/simulator-starter/src/test/java/org/citrusframework/simulator/service/impl/TestParameterServiceImplTest.java new file mode 100644 index 000000000..f375205f8 --- /dev/null +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/service/impl/TestParameterServiceImplTest.java @@ -0,0 +1,102 @@ +package org.citrusframework.simulator.service.impl; + +import org.citrusframework.simulator.model.TestParameter; +import org.citrusframework.simulator.model.TestResult; +import org.citrusframework.simulator.repository.TestParameterRepository; +import org.citrusframework.simulator.service.TestResultService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +@ExtendWith(MockitoExtension.class) +class TestParameterServiceImplTest { + + @Mock + private TestResultService testResultServiceMock; + + @Mock + private TestParameterRepository testParameterRepositoryMock; + + private TestParameterServiceImpl fixture; + + @BeforeEach + void beforeEachSetup() { + fixture = new TestParameterServiceImpl(testResultServiceMock, testParameterRepositoryMock); + } + + @Test + void testSave() { + TestParameter testParameter = new TestParameter(); + doReturn(testParameter).when(testParameterRepositoryMock).save(testParameter); + + TestParameter result = fixture.save(testParameter); + + assertEquals(testParameter, result); + } + + @Test + void testFindAll() { + Pageable pageable = mock(Pageable.class); + Page page = mock(Page.class); + doReturn(page).when(testParameterRepositoryMock).findAll(pageable); + + Page result = fixture.findAll(pageable); + + assertEquals(page, result); + } + + @Test + void testFindOneWithExistingTestResult() { + Long testResultId = 1L; + String key = "key"; + + TestResult testResult = new TestResult().id(testResultId); + Optional optionalTestResult = Optional.of(testResult); + doReturn(optionalTestResult).when(testResultServiceMock).findOne(testResultId); + + TestParameter testParameter = new TestParameter(); + doReturn(Optional.of(testParameter)).when(testParameterRepositoryMock).findById(any(TestParameter.TestParameterId.class)); + + Optional maybeTestParameter = fixture.findOne(testResultId, key); + + assertTrue(maybeTestParameter.isPresent()); + assertEquals(testParameter, maybeTestParameter.get()); + + ArgumentCaptor testParameterIdArgumentCaptor = ArgumentCaptor.forClass(TestParameter.TestParameterId.class); + verify(testParameterRepositoryMock).findById(testParameterIdArgumentCaptor.capture()); + + TestParameter.TestParameterId testParameterId = testParameterIdArgumentCaptor.getValue(); + assertEquals(testResultId, testParameterId.testResultId); + assertEquals(key, testParameterId.key); + } + + @Test + void testFindOneWithoutTestResult() { + Long testResultId = 1L; + String key = "key"; + + doReturn(Optional.empty()).when(testResultServiceMock).findOne(testResultId); + + Optional maybeTestParameter = fixture.findOne(testResultId, key); + + assertFalse(maybeTestParameter.isPresent()); + + verifyNoInteractions(testParameterRepositoryMock); + } +} diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/service/impl/TestResultServiceImplTest.java b/simulator-starter/src/test/java/org/citrusframework/simulator/service/impl/TestResultServiceImplTest.java new file mode 100644 index 000000000..3fe38c220 --- /dev/null +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/service/impl/TestResultServiceImplTest.java @@ -0,0 +1,90 @@ +package org.citrusframework.simulator.service.impl; + +import org.citrusframework.simulator.model.TestResult; +import org.citrusframework.simulator.repository.TestResultRepository; +import org.citrusframework.simulator.service.dto.TestResultByStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +class TestResultServiceImplTest { + + @Mock + private TestResultRepository testResultRepository; + + private TestResultServiceImpl fixture; + + @BeforeEach + void beforeEachSetup() { + fixture = new TestResultServiceImpl(testResultRepository); + } + + @Test + void testTransformAndSave() { + org.citrusframework.TestResult citrusTestResult = org.citrusframework.TestResult.success("TestResult", TestResultServiceImpl.class.getSimpleName()); + + TestResult testResult = new TestResult(citrusTestResult); + doReturn(testResult).when(testResultRepository).save(any(TestResult.class)); + + TestResult result = fixture.transformAndSave(citrusTestResult); + + assertEquals(testResult, result); + } + + @Test + void testSave() { + TestResult testResult = new TestResult(); + doReturn(testResult).when(testResultRepository).save(testResult); + + TestResult result = fixture.save(testResult); + + assertEquals(testResult, result); + } + + @Test + void testFindAll() { + Pageable pageable = mock(Pageable.class); + Page page = mock(Page.class); + doReturn(page).when(testResultRepository).findAll(pageable); + + Page result = fixture.findAll(pageable); + + assertEquals(page, result); + } + + @Test + void testFindOne() { + Long id = 1L; + + TestResult testResult = new TestResult(); + Optional optionalTestResult = Optional.of(testResult); + doReturn(optionalTestResult).when(testResultRepository).findById(id); + + Optional maybeTestResult = fixture.findOne(id); + + assertTrue(maybeTestResult.isPresent()); + assertEquals(testResult, maybeTestResult.get()); + } + + @Test + void testCountByStatus() { + TestResultByStatus testResultByStatus = new TestResultByStatus(1L, 1L); + doReturn(testResultByStatus).when(testResultRepository).countByStatus(); + + TestResultByStatus result = fixture.countByStatus(); + assertEquals(testResultByStatus, result); + } +} diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/test/TestApplication.java b/simulator-starter/src/test/java/org/citrusframework/simulator/test/TestApplication.java new file mode 100644 index 000000000..a39ca08b0 --- /dev/null +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/test/TestApplication.java @@ -0,0 +1,17 @@ +package org.citrusframework.simulator.test; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Note that this class may not be placed into the root package {@code org.citrusframework.simulator}. If done so, the + * {@link SpringBootApplication} annotation would scan the classpath given the package and that would defy the sense + * of all {@link org.springframework.boot.autoconfigure.AutoConfiguration}. + */ +@SpringBootApplication +public class TestApplication { + + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } +} diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/TestParameterResourceIT.java b/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/TestParameterResourceIT.java new file mode 100644 index 000000000..49785389e --- /dev/null +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/TestParameterResourceIT.java @@ -0,0 +1,510 @@ +package org.citrusframework.simulator.web.rest; + +import jakarta.persistence.EntityManager; +import org.citrusframework.simulator.IntegrationTest; +import org.citrusframework.simulator.model.TestParameter; +import org.citrusframework.simulator.model.TestResult; +import org.citrusframework.simulator.repository.TestParameterRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; + +import static org.citrusframework.simulator.web.rest.TestUtil.findAll; +import static org.citrusframework.simulator.web.rest.TestUtil.sameInstant; +import static org.hamcrest.Matchers.hasItem; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Integration tests for the {@link TestParameterResource} REST controller. + */ +@IntegrationTest +@AutoConfigureMockMvc +class TestParameterResourceIT { + + private static final String DEFAULT_KEY = "AAAAAAAAAA"; + private static final String UPDATED_KEY = "BBBBBBBBBB"; + + private static final String DEFAULT_VALUE = "AAAAAAAAAA"; + private static final String UPDATED_VALUE = "BBBBBBBBBB"; + + private static final ZonedDateTime DEFAULT_CREATED_DATE = ZonedDateTime.ofInstant(Instant.ofEpochMilli(0L), ZoneOffset.UTC); + private static final ZonedDateTime UPDATED_CREATED_DATE = ZonedDateTime.now(ZoneId.systemDefault()).withNano(0); + private static final ZonedDateTime SMALLER_CREATED_DATE = ZonedDateTime.ofInstant(Instant.ofEpochMilli(-1L), ZoneOffset.UTC); + + private static final ZonedDateTime DEFAULT_LAST_MODIFIED_DATE = ZonedDateTime.ofInstant(Instant.ofEpochMilli(0L), ZoneOffset.UTC); + private static final ZonedDateTime UPDATED_LAST_MODIFIED_DATE = ZonedDateTime.now(ZoneId.systemDefault()).withNano(0); + private static final ZonedDateTime SMALLER_LAST_MODIFIED_DATE = ZonedDateTime.ofInstant(Instant.ofEpochMilli(-1L), ZoneOffset.UTC); + + private static final String ENTITY_API_URL = "/api/test-parameters"; + private static final String ENTITY_API_URL_ID = ENTITY_API_URL + "/{testResultId}/{key}"; + + @Autowired + private TestParameterRepository testParameterRepository; + + @Autowired + private EntityManager em; + + @Autowired + private MockMvc restTestParameterMockMvc; + + private TestParameter testParameter; + + /** + * Create an entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static TestParameter createEntity(EntityManager em) { + TestParameter testParameter = TestParameter.builder() + .key(DEFAULT_KEY) + .value(DEFAULT_VALUE) + .createdDate(DEFAULT_CREATED_DATE) + .lastModifiedDate(DEFAULT_LAST_MODIFIED_DATE) + .build(); + + TestResult testResult; + if (findAll(em, TestResult.class).isEmpty()) { + testResult = TestResultResourceIT.createEntity(em); + em.persist(testResult); + em.flush(); + } else { + testResult = findAll(em, TestResult.class).get(0); + } + testParameter.setTestResult(testResult); + + return testParameter; + } + + @BeforeEach + public void initTest() { + testParameter = createEntity(em); + } + + @Test + @Transactional + void getAllTestParameters() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get all the testParameterList + restTestParameterMockMvc + .perform(get(ENTITY_API_URL + "?createDate=id,desc")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[*].key").value(hasItem(DEFAULT_KEY))) + .andExpect(jsonPath("$.[*].value").value(hasItem(DEFAULT_VALUE))) + .andExpect(jsonPath("$.[*].createdDate").value(hasItem(sameInstant(DEFAULT_CREATED_DATE)))) + .andExpect(jsonPath("$.[*].lastModifiedDate").value(hasItem(sameInstant(DEFAULT_LAST_MODIFIED_DATE)))); + } + + @Test + @Transactional + void getTestParameter() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get the testParameter + restTestParameterMockMvc + .perform(get(ENTITY_API_URL_ID, testParameter.getTestResult().getId(), testParameter.getKey())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.key").value(DEFAULT_KEY)) + .andExpect(jsonPath("$.value").value(DEFAULT_VALUE)) + .andExpect(jsonPath("$.createdDate").value(sameInstant(DEFAULT_CREATED_DATE))) + .andExpect(jsonPath("$.lastModifiedDate").value(sameInstant(DEFAULT_LAST_MODIFIED_DATE))); + } + + @Test + @Transactional + void getAllTestParametersByKeyIsEqualToSomething() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get all the testParameterList where key equals to DEFAULT_KEY + defaultTestParameterShouldBeFound("key.equals=" + DEFAULT_KEY); + + // Get all the testParameterList where key equals to UPDATED_KEY + defaultTestParameterShouldNotBeFound("key.equals=" + UPDATED_KEY); + } + + @Test + @Transactional + void getAllTestParametersByKeyIsInShouldWork() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get all the testParameterList where key in DEFAULT_KEY or UPDATED_KEY + defaultTestParameterShouldBeFound("key.in=" + DEFAULT_KEY + "," + UPDATED_KEY); + + // Get all the testParameterList where key equals to UPDATED_KEY + defaultTestParameterShouldNotBeFound("key.in=" + UPDATED_KEY); + } + + @Test + @Transactional + void getAllTestParametersByKeyIsNullOrNotNull() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get all the testParameterList where key is not null + defaultTestParameterShouldBeFound("key.specified=true"); + + // Get all the testParameterList where key is null + defaultTestParameterShouldNotBeFound("key.specified=false"); + } + + @Test + @Transactional + void getAllTestParametersByKeyContainsSomething() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get all the testParameterList where key contains DEFAULT_KEY + defaultTestParameterShouldBeFound("key.contains=" + DEFAULT_KEY); + + // Get all the testParameterList where key contains UPDATED_KEY + defaultTestParameterShouldNotBeFound("key.contains=" + UPDATED_KEY); + } + + @Test + @Transactional + void getAllTestParametersByKeyNotContainsSomething() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get all the testParameterList where key does not contain DEFAULT_KEY + defaultTestParameterShouldNotBeFound("key.doesNotContain=" + DEFAULT_KEY); + + // Get all the testParameterList where key does not contain UPDATED_KEY + defaultTestParameterShouldBeFound("key.doesNotContain=" + UPDATED_KEY); + } + + @Test + @Transactional + void getAllTestParametersByValueIsEqualToSomething() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get all the testParameterList where value equals to DEFAULT_VALUE + defaultTestParameterShouldBeFound("value.equals=" + DEFAULT_VALUE); + + // Get all the testParameterList where value equals to UPDATED_VALUE + defaultTestParameterShouldNotBeFound("value.equals=" + UPDATED_VALUE); + } + + @Test + @Transactional + void getAllTestParametersByValueIsInShouldWork() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get all the testParameterList where value in DEFAULT_VALUE or UPDATED_VALUE + defaultTestParameterShouldBeFound("value.in=" + DEFAULT_VALUE + "," + UPDATED_VALUE); + + // Get all the testParameterList where value equals to UPDATED_VALUE + defaultTestParameterShouldNotBeFound("value.in=" + UPDATED_VALUE); + } + + @Test + @Transactional + void getAllTestParametersByValueIsNullOrNotNull() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get all the testParameterList where value is not null + defaultTestParameterShouldBeFound("value.specified=true"); + + // Get all the testParameterList where value is null + defaultTestParameterShouldNotBeFound("value.specified=false"); + } + + @Test + @Transactional + void getAllTestParametersByValueContainsSomething() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get all the testParameterList where value contains DEFAULT_VALUE + defaultTestParameterShouldBeFound("value.contains=" + DEFAULT_VALUE); + + // Get all the testParameterList where value contains UPDATED_VALUE + defaultTestParameterShouldNotBeFound("value.contains=" + UPDATED_VALUE); + } + + @Test + @Transactional + void getAllTestParametersByValueNotContainsSomething() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get all the testParameterList where value does not contain DEFAULT_VALUE + defaultTestParameterShouldNotBeFound("value.doesNotContain=" + DEFAULT_VALUE); + + // Get all the testParameterList where value does not contain UPDATED_VALUE + defaultTestParameterShouldBeFound("value.doesNotContain=" + UPDATED_VALUE); + } + + @Test + @Transactional + void getAllTestParametersByCreatedDateIsEqualToSomething() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get all the testParameterList where createdDate equals to DEFAULT_CREATED_DATE + defaultTestParameterShouldBeFound("createdDate.equals=" + DEFAULT_CREATED_DATE); + + // Get all the testParameterList where createdDate equals to UPDATED_CREATED_DATE + defaultTestParameterShouldNotBeFound("createdDate.equals=" + UPDATED_CREATED_DATE); + } + + @Test + @Transactional + void getAllTestParametersByCreatedDateIsInShouldWork() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get all the testParameterList where createdDate in DEFAULT_CREATED_DATE or UPDATED_CREATED_DATE + defaultTestParameterShouldBeFound("createdDate.in=" + DEFAULT_CREATED_DATE + "," + UPDATED_CREATED_DATE); + + // Get all the testParameterList where createdDate equals to UPDATED_CREATED_DATE + defaultTestParameterShouldNotBeFound("createdDate.in=" + UPDATED_CREATED_DATE); + } + + @Test + @Transactional + void getAllTestParametersByCreatedDateIsNullOrNotNull() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get all the testParameterList where createdDate is not null + defaultTestParameterShouldBeFound("createdDate.specified=true"); + + // Get all the testParameterList where createdDate is null + defaultTestParameterShouldNotBeFound("createdDate.specified=false"); + } + + @Test + @Transactional + void getAllTestParametersByCreatedDateIsGreaterThanOrEqualToSomething() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get all the testParameterList where createdDate is greater than or equal to DEFAULT_CREATED_DATE + defaultTestParameterShouldBeFound("createdDate.greaterThanOrEqual=" + DEFAULT_CREATED_DATE); + + // Get all the testParameterList where createdDate is greater than or equal to UPDATED_CREATED_DATE + defaultTestParameterShouldNotBeFound("createdDate.greaterThanOrEqual=" + UPDATED_CREATED_DATE); + } + + @Test + @Transactional + void getAllTestParametersByCreatedDateIsLessThanOrEqualToSomething() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get all the testParameterList where createdDate is less than or equal to DEFAULT_CREATED_DATE + defaultTestParameterShouldBeFound("createdDate.lessThanOrEqual=" + DEFAULT_CREATED_DATE); + + // Get all the testParameterList where createdDate is less than or equal to SMALLER_CREATED_DATE + defaultTestParameterShouldNotBeFound("createdDate.lessThanOrEqual=" + SMALLER_CREATED_DATE); + } + + @Test + @Transactional + void getAllTestParametersByCreatedDateIsLessThanSomething() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get all the testParameterList where createdDate is less than DEFAULT_CREATED_DATE + defaultTestParameterShouldNotBeFound("createdDate.lessThan=" + DEFAULT_CREATED_DATE); + + // Get all the testParameterList where createdDate is less than UPDATED_CREATED_DATE + defaultTestParameterShouldBeFound("createdDate.lessThan=" + UPDATED_CREATED_DATE); + } + + @Test + @Transactional + void getAllTestParametersByCreatedDateIsGreaterThanSomething() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get all the testParameterList where createdDate is greater than DEFAULT_CREATED_DATE + defaultTestParameterShouldNotBeFound("createdDate.greaterThan=" + DEFAULT_CREATED_DATE); + + // Get all the testParameterList where createdDate is greater than SMALLER_CREATED_DATE + defaultTestParameterShouldBeFound("createdDate.greaterThan=" + SMALLER_CREATED_DATE); + } + + @Test + @Transactional + void getAllTestParametersByLastModifiedDateIsEqualToSomething() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get all the testParameterList where lastModifiedDate equals to DEFAULT_LAST_MODIFIED_DATE + defaultTestParameterShouldBeFound("lastModifiedDate.equals=" + DEFAULT_LAST_MODIFIED_DATE); + + // Get all the testParameterList where lastModifiedDate equals to UPDATED_LAST_MODIFIED_DATE + defaultTestParameterShouldNotBeFound("lastModifiedDate.equals=" + UPDATED_LAST_MODIFIED_DATE); + } + + @Test + @Transactional + void getAllTestParametersByLastModifiedDateIsInShouldWork() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get all the testParameterList where lastModifiedDate in DEFAULT_LAST_MODIFIED_DATE or UPDATED_LAST_MODIFIED_DATE + defaultTestParameterShouldBeFound("lastModifiedDate.in=" + DEFAULT_LAST_MODIFIED_DATE + "," + UPDATED_LAST_MODIFIED_DATE); + + // Get all the testParameterList where lastModifiedDate equals to UPDATED_LAST_MODIFIED_DATE + defaultTestParameterShouldNotBeFound("lastModifiedDate.in=" + UPDATED_LAST_MODIFIED_DATE); + } + + @Test + @Transactional + void getAllTestParametersByLastModifiedDateIsNullOrNotNull() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get all the testParameterList where lastModifiedDate is not null + defaultTestParameterShouldBeFound("lastModifiedDate.specified=true"); + + // Get all the testParameterList where lastModifiedDate is null + defaultTestParameterShouldNotBeFound("lastModifiedDate.specified=false"); + } + + @Test + @Transactional + void getAllTestParametersByLastModifiedDateIsGreaterThanOrEqualToSomething() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get all the testParameterList where lastModifiedDate is greater than or equal to DEFAULT_LAST_MODIFIED_DATE + defaultTestParameterShouldBeFound("lastModifiedDate.greaterThanOrEqual=" + DEFAULT_LAST_MODIFIED_DATE); + + // Get all the testParameterList where lastModifiedDate is greater than or equal to UPDATED_LAST_MODIFIED_DATE + defaultTestParameterShouldNotBeFound("lastModifiedDate.greaterThanOrEqual=" + UPDATED_LAST_MODIFIED_DATE); + } + + @Test + @Transactional + void getAllTestParametersByLastModifiedDateIsLessThanOrEqualToSomething() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get all the testParameterList where lastModifiedDate is less than or equal to DEFAULT_LAST_MODIFIED_DATE + defaultTestParameterShouldBeFound("lastModifiedDate.lessThanOrEqual=" + DEFAULT_LAST_MODIFIED_DATE); + + // Get all the testParameterList where lastModifiedDate is less than or equal to SMALLER_LAST_MODIFIED_DATE + defaultTestParameterShouldNotBeFound("lastModifiedDate.lessThanOrEqual=" + SMALLER_LAST_MODIFIED_DATE); + } + + @Test + @Transactional + void getAllTestParametersByLastModifiedDateIsLessThanSomething() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get all the testParameterList where lastModifiedDate is less than DEFAULT_LAST_MODIFIED_DATE + defaultTestParameterShouldNotBeFound("lastModifiedDate.lessThan=" + DEFAULT_LAST_MODIFIED_DATE); + + // Get all the testParameterList where lastModifiedDate is less than UPDATED_LAST_MODIFIED_DATE + defaultTestParameterShouldBeFound("lastModifiedDate.lessThan=" + UPDATED_LAST_MODIFIED_DATE); + } + + @Test + @Transactional + void getAllTestParametersByLastModifiedDateIsGreaterThanSomething() throws Exception { + // Initialize the database + testParameterRepository.saveAndFlush(testParameter); + + // Get all the testParameterList where lastModifiedDate is greater than DEFAULT_LAST_MODIFIED_DATE + defaultTestParameterShouldNotBeFound("lastModifiedDate.greaterThan=" + DEFAULT_LAST_MODIFIED_DATE); + + // Get all the testParameterList where lastModifiedDate is greater than SMALLER_LAST_MODIFIED_DATE + defaultTestParameterShouldBeFound("lastModifiedDate.greaterThan=" + SMALLER_LAST_MODIFIED_DATE); + } + + @Test + @Transactional + void getAllTestParametersByTestResultIsEqualToSomething() throws Exception { + TestResult testResult; + if (TestUtil.findAll(em, TestResult.class).isEmpty()) { + testParameterRepository.saveAndFlush(testParameter); + testResult = TestResultResourceIT.createEntity(em); + } else { + testResult = TestUtil.findAll(em, TestResult.class).get(0); + } + em.persist(testResult); + em.flush(); + testParameter.setTestResult(testResult); + testParameterRepository.saveAndFlush(testParameter); + Long testResultId = testResult.getId(); + // Get all the testParameterList where testResult equals to testResultId + defaultTestParameterShouldBeFound("testResultId.equals=" + testResultId); + + // Get all the testParameterList where testResult equals to (testResultId + 1) + defaultTestParameterShouldNotBeFound("testResultId.equals=" + (testResultId + 1)); + } + + /** + * Executes the search, and checks that the default entity is returned. + */ + private void defaultTestParameterShouldBeFound(String filter) throws Exception { + restTestParameterMockMvc + .perform(get(ENTITY_API_URL + "?createDate=id,desc&" + filter)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[*].key").value(hasItem(DEFAULT_KEY))) + .andExpect(jsonPath("$.[*].value").value(hasItem(DEFAULT_VALUE))) + .andExpect(jsonPath("$.[*].createdDate").value(hasItem(sameInstant(DEFAULT_CREATED_DATE)))) + .andExpect(jsonPath("$.[*].lastModifiedDate").value(hasItem(sameInstant(DEFAULT_LAST_MODIFIED_DATE)))); + + // Check, that the count call also returns 1 + restTestParameterMockMvc + .perform(get(ENTITY_API_URL + "/count?createDate=id,desc&" + filter)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(content().string("1")); + } + + /** + * Executes the search, and checks that the default entity is not returned. + */ + private void defaultTestParameterShouldNotBeFound(String filter) throws Exception { + restTestParameterMockMvc + .perform(get(ENTITY_API_URL + "?createDate=id,desc&" + filter)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$").isEmpty()); + + // Check, that the count call also returns 0 + restTestParameterMockMvc + .perform(get(ENTITY_API_URL + "/count?createDate=id,desc&" + filter)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(content().string("0")); + } + + @Test + @Transactional + void getNonExistingTestParameter() throws Exception { + // Get the testParameter + restTestParameterMockMvc.perform(get(ENTITY_API_URL_ID, testParameter.getTestResult().getId(), Long.MAX_VALUE)).andExpect(status().isNotFound()); + } +} diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/TestResultResourceIT.java b/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/TestResultResourceIT.java new file mode 100644 index 000000000..601bc8f36 --- /dev/null +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/TestResultResourceIT.java @@ -0,0 +1,781 @@ +package org.citrusframework.simulator.web.rest; + +import jakarta.persistence.EntityManager; +import org.citrusframework.simulator.IntegrationTest; +import org.citrusframework.simulator.model.TestParameter; +import org.citrusframework.simulator.model.TestResult; +import org.citrusframework.simulator.repository.TestResultRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; + +import static org.citrusframework.simulator.web.rest.TestUtil.sameInstant; +import static org.hamcrest.Matchers.hasItem; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Integration tests for the {@link TestResultResource} REST controller. + */ +@IntegrationTest +@AutoConfigureMockMvc +class TestResultResourceIT { + + private static final TestResult.Status DEFAULT_STATUS = TestResult.Status.SUCCESS; // Integer value: 1 + private static final TestResult.Status UPDATED_STATUS = TestResult.Status.FAILURE; // Integer value: 2 + + private static final String DEFAULT_TEST_NAME = "AAAAAAAAAA"; + private static final String UPDATED_TEST_NAME = "BBBBBBBBBB"; + + private static final String DEFAULT_CLASS_NAME = "AAAAAAAAAA"; + private static final String UPDATED_CLASS_NAME = "BBBBBBBBBB"; + + private static final String DEFAULT_ERROR_MESSAGE = "AAAAAAAAAA"; + private static final String UPDATED_ERROR_MESSAGE = "BBBBBBBBBB"; + + private static final String DEFAULT_FAILURE_STACK = "AAAAAAAAAA"; + private static final String UPDATED_FAILURE_STACK = "BBBBBBBBBB"; + + private static final String DEFAULT_FAILURE_TYPE = "AAAAAAAAAA"; + private static final String UPDATED_FAILURE_TYPE = "BBBBBBBBBB"; + + private static final ZonedDateTime DEFAULT_CREATED_DATE = ZonedDateTime.ofInstant(Instant.ofEpochMilli(0L), ZoneOffset.UTC); + private static final ZonedDateTime UPDATED_CREATED_DATE = ZonedDateTime.now(ZoneId.systemDefault()).withNano(0); + private static final ZonedDateTime SMALLER_CREATED_DATE = ZonedDateTime.ofInstant(Instant.ofEpochMilli(-1L), ZoneOffset.UTC); + + private static final ZonedDateTime DEFAULT_LAST_MODIFIED_DATE = ZonedDateTime.ofInstant(Instant.ofEpochMilli(0L), ZoneOffset.UTC); + private static final ZonedDateTime UPDATED_LAST_MODIFIED_DATE = ZonedDateTime.now(ZoneId.systemDefault()).withNano(0); + private static final ZonedDateTime SMALLER_LAST_MODIFIED_DATE = ZonedDateTime.ofInstant(Instant.ofEpochMilli(-1L), ZoneOffset.UTC); + + private static final String ENTITY_API_URL = "/api/test-results"; + private static final String ENTITY_API_URL_ID = ENTITY_API_URL + "/{id}"; + + @Autowired + private TestResultRepository testResultRepository; + + @Autowired + private EntityManager em; + + @Autowired + private MockMvc restTestResultMockMvc; + + private TestResult testResult; + + /** + * Create an entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static TestResult createEntity(EntityManager em) { + TestResult testResult = TestResult.builder() + .status(DEFAULT_STATUS.getId()) + .testName(DEFAULT_TEST_NAME) + .className(DEFAULT_CLASS_NAME) + .errorMessage(DEFAULT_ERROR_MESSAGE) + .failureStack(DEFAULT_FAILURE_STACK) + .failureType(DEFAULT_FAILURE_TYPE) + .createdDate(DEFAULT_CREATED_DATE) + .lastModifiedDate(DEFAULT_LAST_MODIFIED_DATE) + .build(); + return testResult; + } + + @BeforeEach + public void initTest() { + testResult = createEntity(em); + } + + @Test + @Transactional + void getAllTestResults() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList + restTestResultMockMvc + .perform(get(ENTITY_API_URL + "?sort=id,desc")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[*].id").value(hasItem(testResult.getId().intValue()))) + .andExpect(jsonPath("$.[*].status").value(hasItem(DEFAULT_STATUS.toString()))) + .andExpect(jsonPath("$.[*].testName").value(hasItem(DEFAULT_TEST_NAME))) + .andExpect(jsonPath("$.[*].className").value(hasItem(DEFAULT_CLASS_NAME))) + .andExpect(jsonPath("$.[*].errorMessage").value(hasItem(DEFAULT_ERROR_MESSAGE))) + .andExpect(jsonPath("$.[*].failureStack").value(hasItem(DEFAULT_FAILURE_STACK))) + .andExpect(jsonPath("$.[*].failureType").value(hasItem(DEFAULT_FAILURE_TYPE))) + .andExpect(jsonPath("$.[*].createdDate").value(hasItem(sameInstant(DEFAULT_CREATED_DATE)))) + .andExpect(jsonPath("$.[*].lastModifiedDate").value(hasItem(sameInstant(DEFAULT_LAST_MODIFIED_DATE)))); + } + + @Test + @Transactional + void getTestResult() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get the testResult + restTestResultMockMvc + .perform(get(ENTITY_API_URL_ID, testResult.getId())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.id").value(testResult.getId().intValue())) + .andExpect(jsonPath("$.status").value(DEFAULT_STATUS.toString())) + .andExpect(jsonPath("$.testName").value(DEFAULT_TEST_NAME)) + .andExpect(jsonPath("$.className").value(DEFAULT_CLASS_NAME)) + .andExpect(jsonPath("$.errorMessage").value(DEFAULT_ERROR_MESSAGE)) + .andExpect(jsonPath("$.failureStack").value(DEFAULT_FAILURE_STACK)) + .andExpect(jsonPath("$.failureType").value(DEFAULT_FAILURE_TYPE)) + .andExpect(jsonPath("$.createdDate").value(sameInstant(DEFAULT_CREATED_DATE))) + .andExpect(jsonPath("$.lastModifiedDate").value(sameInstant(DEFAULT_LAST_MODIFIED_DATE))); + } + + @Test + @Transactional + void getTestResultsByIdFiltering() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + Long id = testResult.getId(); + + defaultTestResultShouldBeFound("id.equals=" + id); + defaultTestResultShouldNotBeFound("id.notEquals=" + id); + + defaultTestResultShouldBeFound("id.greaterThanOrEqual=" + id); + defaultTestResultShouldNotBeFound("id.greaterThan=" + id); + + defaultTestResultShouldBeFound("id.lessThanOrEqual=" + id); + defaultTestResultShouldNotBeFound("id.lessThan=" + id); + } + + @Test + @Transactional + void getAllTestResultsByStatusIsEqualToSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where status equals to DEFAULT_STATUS + defaultTestResultShouldBeFound("status.equals=" + DEFAULT_STATUS.getId()); + + // Get all the testResultList where status equals to UPDATED_STATUS + defaultTestResultShouldNotBeFound("status.equals=" + UPDATED_STATUS.getId()); + } + + @Test + @Transactional + void getAllTestResultsByStatusIsInShouldWork() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where status in DEFAULT_STATUS or UPDATED_STATUS + defaultTestResultShouldBeFound("status.in=" + DEFAULT_STATUS.getId() + "," + UPDATED_STATUS.getId()); + + // Get all the testResultList where status equals to UPDATED_STATUS + defaultTestResultShouldNotBeFound("status.in=" + UPDATED_STATUS.getId()); + } + + @Test + @Transactional + void getAllTestResultsByStatusIsNullOrNotNull() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where status is not null + defaultTestResultShouldBeFound("status.specified=true"); + + // Get all the testResultList where status is null + defaultTestResultShouldNotBeFound("status.specified=false"); + } + + @Test + @Transactional + void getAllTestResultsByTestNameIsEqualToSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where testName equals to DEFAULT_TEST_NAME + defaultTestResultShouldBeFound("testName.equals=" + DEFAULT_TEST_NAME); + + // Get all the testResultList where testName equals to UPDATED_TEST_NAME + defaultTestResultShouldNotBeFound("testName.equals=" + UPDATED_TEST_NAME); + } + + @Test + @Transactional + void getAllTestResultsByTestNameIsInShouldWork() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where testName in DEFAULT_TEST_NAME or UPDATED_TEST_NAME + defaultTestResultShouldBeFound("testName.in=" + DEFAULT_TEST_NAME + "," + UPDATED_TEST_NAME); + + // Get all the testResultList where testName equals to UPDATED_TEST_NAME + defaultTestResultShouldNotBeFound("testName.in=" + UPDATED_TEST_NAME); + } + + @Test + @Transactional + void getAllTestResultsByTestNameIsNullOrNotNull() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where testName is not null + defaultTestResultShouldBeFound("testName.specified=true"); + + // Get all the testResultList where testName is null + defaultTestResultShouldNotBeFound("testName.specified=false"); + } + + @Test + @Transactional + void getAllTestResultsByTestNameContainsSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where testName contains DEFAULT_TEST_NAME + defaultTestResultShouldBeFound("testName.contains=" + DEFAULT_TEST_NAME); + + // Get all the testResultList where testName contains UPDATED_TEST_NAME + defaultTestResultShouldNotBeFound("testName.contains=" + UPDATED_TEST_NAME); + } + + @Test + @Transactional + void getAllTestResultsByTestNameNotContainsSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where testName does not contain DEFAULT_TEST_NAME + defaultTestResultShouldNotBeFound("testName.doesNotContain=" + DEFAULT_TEST_NAME); + + // Get all the testResultList where testName does not contain UPDATED_TEST_NAME + defaultTestResultShouldBeFound("testName.doesNotContain=" + UPDATED_TEST_NAME); + } + + @Test + @Transactional + void getAllTestResultsByClassNameIsEqualToSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where className equals to DEFAULT_CLASS_NAME + defaultTestResultShouldBeFound("className.equals=" + DEFAULT_CLASS_NAME); + + // Get all the testResultList where className equals to UPDATED_CLASS_NAME + defaultTestResultShouldNotBeFound("className.equals=" + UPDATED_CLASS_NAME); + } + + @Test + @Transactional + void getAllTestResultsByClassNameIsInShouldWork() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where className in DEFAULT_CLASS_NAME or UPDATED_CLASS_NAME + defaultTestResultShouldBeFound("className.in=" + DEFAULT_CLASS_NAME + "," + UPDATED_CLASS_NAME); + + // Get all the testResultList where className equals to UPDATED_CLASS_NAME + defaultTestResultShouldNotBeFound("className.in=" + UPDATED_CLASS_NAME); + } + + @Test + @Transactional + void getAllTestResultsByClassNameIsNullOrNotNull() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where className is not null + defaultTestResultShouldBeFound("className.specified=true"); + + // Get all the testResultList where className is null + defaultTestResultShouldNotBeFound("className.specified=false"); + } + + @Test + @Transactional + void getAllTestResultsByClassNameContainsSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where className contains DEFAULT_CLASS_NAME + defaultTestResultShouldBeFound("className.contains=" + DEFAULT_CLASS_NAME); + + // Get all the testResultList where className contains UPDATED_CLASS_NAME + defaultTestResultShouldNotBeFound("className.contains=" + UPDATED_CLASS_NAME); + } + + @Test + @Transactional + void getAllTestResultsByClassNameNotContainsSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where className does not contain DEFAULT_CLASS_NAME + defaultTestResultShouldNotBeFound("className.doesNotContain=" + DEFAULT_CLASS_NAME); + + // Get all the testResultList where className does not contain UPDATED_CLASS_NAME + defaultTestResultShouldBeFound("className.doesNotContain=" + UPDATED_CLASS_NAME); + } + + @Test + @Transactional + void getAllTestResultsByErrorMessageIsEqualToSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where errorMessage equals to DEFAULT_ERROR_MESSAGE + defaultTestResultShouldBeFound("errorMessage.equals=" + DEFAULT_ERROR_MESSAGE); + + // Get all the testResultList where errorMessage equals to UPDATED_ERROR_MESSAGE + defaultTestResultShouldNotBeFound("errorMessage.equals=" + UPDATED_ERROR_MESSAGE); + } + + @Test + @Transactional + void getAllTestResultsByErrorMessageIsInShouldWork() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where errorMessage in DEFAULT_ERROR_MESSAGE or UPDATED_ERROR_MESSAGE + defaultTestResultShouldBeFound("errorMessage.in=" + DEFAULT_ERROR_MESSAGE + "," + UPDATED_ERROR_MESSAGE); + + // Get all the testResultList where errorMessage equals to UPDATED_ERROR_MESSAGE + defaultTestResultShouldNotBeFound("errorMessage.in=" + UPDATED_ERROR_MESSAGE); + } + + @Test + @Transactional + void getAllTestResultsByErrorMessageIsNullOrNotNull() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where errorMessage is not null + defaultTestResultShouldBeFound("errorMessage.specified=true"); + + // Get all the testResultList where errorMessage is null + defaultTestResultShouldNotBeFound("errorMessage.specified=false"); + } + + @Test + @Transactional + void getAllTestResultsByErrorMessageContainsSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where errorMessage contains DEFAULT_ERROR_MESSAGE + defaultTestResultShouldBeFound("errorMessage.contains=" + DEFAULT_ERROR_MESSAGE); + + // Get all the testResultList where errorMessage contains UPDATED_ERROR_MESSAGE + defaultTestResultShouldNotBeFound("errorMessage.contains=" + UPDATED_ERROR_MESSAGE); + } + + @Test + @Transactional + void getAllTestResultsByErrorMessageNotContainsSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where errorMessage does not contain DEFAULT_ERROR_MESSAGE + defaultTestResultShouldNotBeFound("errorMessage.doesNotContain=" + DEFAULT_ERROR_MESSAGE); + + // Get all the testResultList where errorMessage does not contain UPDATED_ERROR_MESSAGE + defaultTestResultShouldBeFound("errorMessage.doesNotContain=" + UPDATED_ERROR_MESSAGE); + } + + @Test + @Transactional + void getAllTestResultsByFailureStackIsEqualToSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where failureStack equals to DEFAULT_FAILURE_STACK + defaultTestResultShouldBeFound("failureStack.equals=" + DEFAULT_FAILURE_STACK); + + // Get all the testResultList where failureStack equals to UPDATED_FAILURE_STACK + defaultTestResultShouldNotBeFound("failureStack.equals=" + UPDATED_FAILURE_STACK); + } + + @Test + @Transactional + void getAllTestResultsByFailureStackIsInShouldWork() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where failureStack in DEFAULT_FAILURE_STACK or UPDATED_FAILURE_STACK + defaultTestResultShouldBeFound("failureStack.in=" + DEFAULT_FAILURE_STACK + "," + UPDATED_FAILURE_STACK); + + // Get all the testResultList where failureStack equals to UPDATED_FAILURE_STACK + defaultTestResultShouldNotBeFound("failureStack.in=" + UPDATED_FAILURE_STACK); + } + + @Test + @Transactional + void getAllTestResultsByFailureStackIsNullOrNotNull() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where failureStack is not null + defaultTestResultShouldBeFound("failureStack.specified=true"); + + // Get all the testResultList where failureStack is null + defaultTestResultShouldNotBeFound("failureStack.specified=false"); + } + + @Test + @Transactional + void getAllTestResultsByFailureStackContainsSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where failureStack contains DEFAULT_FAILURE_STACK + defaultTestResultShouldBeFound("failureStack.contains=" + DEFAULT_FAILURE_STACK); + + // Get all the testResultList where failureStack contains UPDATED_FAILURE_STACK + defaultTestResultShouldNotBeFound("failureStack.contains=" + UPDATED_FAILURE_STACK); + } + + @Test + @Transactional + void getAllTestResultsByFailureStackNotContainsSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where failureStack does not contain DEFAULT_FAILURE_STACK + defaultTestResultShouldNotBeFound("failureStack.doesNotContain=" + DEFAULT_FAILURE_STACK); + + // Get all the testResultList where failureStack does not contain UPDATED_FAILURE_STACK + defaultTestResultShouldBeFound("failureStack.doesNotContain=" + UPDATED_FAILURE_STACK); + } + + @Test + @Transactional + void getAllTestResultsByFailureTypeIsEqualToSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where failureType equals to DEFAULT_FAILURE_TYPE + defaultTestResultShouldBeFound("failureType.equals=" + DEFAULT_FAILURE_TYPE); + + // Get all the testResultList where failureType equals to UPDATED_FAILURE_TYPE + defaultTestResultShouldNotBeFound("failureType.equals=" + UPDATED_FAILURE_TYPE); + } + + @Test + @Transactional + void getAllTestResultsByFailureTypeIsInShouldWork() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where failureType in DEFAULT_FAILURE_TYPE or UPDATED_FAILURE_TYPE + defaultTestResultShouldBeFound("failureType.in=" + DEFAULT_FAILURE_TYPE + "," + UPDATED_FAILURE_TYPE); + + // Get all the testResultList where failureType equals to UPDATED_FAILURE_TYPE + defaultTestResultShouldNotBeFound("failureType.in=" + UPDATED_FAILURE_TYPE); + } + + @Test + @Transactional + void getAllTestResultsByFailureTypeIsNullOrNotNull() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where failureType is not null + defaultTestResultShouldBeFound("failureType.specified=true"); + + // Get all the testResultList where failureType is null + defaultTestResultShouldNotBeFound("failureType.specified=false"); + } + + @Test + @Transactional + void getAllTestResultsByFailureTypeContainsSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where failureType contains DEFAULT_FAILURE_TYPE + defaultTestResultShouldBeFound("failureType.contains=" + DEFAULT_FAILURE_TYPE); + + // Get all the testResultList where failureType contains UPDATED_FAILURE_TYPE + defaultTestResultShouldNotBeFound("failureType.contains=" + UPDATED_FAILURE_TYPE); + } + + @Test + @Transactional + void getAllTestResultsByFailureTypeNotContainsSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where failureType does not contain DEFAULT_FAILURE_TYPE + defaultTestResultShouldNotBeFound("failureType.doesNotContain=" + DEFAULT_FAILURE_TYPE); + + // Get all the testResultList where failureType does not contain UPDATED_FAILURE_TYPE + defaultTestResultShouldBeFound("failureType.doesNotContain=" + UPDATED_FAILURE_TYPE); + } + + @Test + @Transactional + void getAllTestResultsByCreatedDateIsEqualToSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where createdDate equals to DEFAULT_CREATED_DATE + defaultTestResultShouldBeFound("createdDate.equals=" + DEFAULT_CREATED_DATE); + + // Get all the testResultList where createdDate equals to UPDATED_CREATED_DATE + defaultTestResultShouldNotBeFound("createdDate.equals=" + UPDATED_CREATED_DATE); + } + + @Test + @Transactional + void getAllTestResultsByCreatedDateIsInShouldWork() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where createdDate in DEFAULT_CREATED_DATE or UPDATED_CREATED_DATE + defaultTestResultShouldBeFound("createdDate.in=" + DEFAULT_CREATED_DATE + "," + UPDATED_CREATED_DATE); + + // Get all the testResultList where createdDate equals to UPDATED_CREATED_DATE + defaultTestResultShouldNotBeFound("createdDate.in=" + UPDATED_CREATED_DATE); + } + + @Test + @Transactional + void getAllTestResultsByCreatedDateIsNullOrNotNull() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where createdDate is not null + defaultTestResultShouldBeFound("createdDate.specified=true"); + + // Get all the testResultList where createdDate is null + defaultTestResultShouldNotBeFound("createdDate.specified=false"); + } + + @Test + @Transactional + void getAllTestResultsByCreatedDateIsGreaterThanOrEqualToSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where createdDate is greater than or equal to DEFAULT_CREATED_DATE + defaultTestResultShouldBeFound("createdDate.greaterThanOrEqual=" + DEFAULT_CREATED_DATE); + + // Get all the testResultList where createdDate is greater than or equal to UPDATED_CREATED_DATE + defaultTestResultShouldNotBeFound("createdDate.greaterThanOrEqual=" + UPDATED_CREATED_DATE); + } + + @Test + @Transactional + void getAllTestResultsByCreatedDateIsLessThanOrEqualToSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where createdDate is less than or equal to DEFAULT_CREATED_DATE + defaultTestResultShouldBeFound("createdDate.lessThanOrEqual=" + DEFAULT_CREATED_DATE); + + // Get all the testResultList where createdDate is less than or equal to SMALLER_CREATED_DATE + defaultTestResultShouldNotBeFound("createdDate.lessThanOrEqual=" + SMALLER_CREATED_DATE); + } + + @Test + @Transactional + void getAllTestResultsByCreatedDateIsLessThanSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where createdDate is less than DEFAULT_CREATED_DATE + defaultTestResultShouldNotBeFound("createdDate.lessThan=" + DEFAULT_CREATED_DATE); + + // Get all the testResultList where createdDate is less than UPDATED_CREATED_DATE + defaultTestResultShouldBeFound("createdDate.lessThan=" + UPDATED_CREATED_DATE); + } + + @Test + @Transactional + void getAllTestResultsByCreatedDateIsGreaterThanSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where createdDate is greater than DEFAULT_CREATED_DATE + defaultTestResultShouldNotBeFound("createdDate.greaterThan=" + DEFAULT_CREATED_DATE); + + // Get all the testResultList where createdDate is greater than SMALLER_CREATED_DATE + defaultTestResultShouldBeFound("createdDate.greaterThan=" + SMALLER_CREATED_DATE); + } + + @Test + @Transactional + void getAllTestResultsByLastModifiedDateIsEqualToSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where lastModifiedDate equals to DEFAULT_LAST_MODIFIED_DATE + defaultTestResultShouldBeFound("lastModifiedDate.equals=" + DEFAULT_LAST_MODIFIED_DATE); + + // Get all the testResultList where lastModifiedDate equals to UPDATED_LAST_MODIFIED_DATE + defaultTestResultShouldNotBeFound("lastModifiedDate.equals=" + UPDATED_LAST_MODIFIED_DATE); + } + + @Test + @Transactional + void getAllTestResultsByLastModifiedDateIsInShouldWork() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where lastModifiedDate in DEFAULT_LAST_MODIFIED_DATE or UPDATED_LAST_MODIFIED_DATE + defaultTestResultShouldBeFound("lastModifiedDate.in=" + DEFAULT_LAST_MODIFIED_DATE + "," + UPDATED_LAST_MODIFIED_DATE); + + // Get all the testResultList where lastModifiedDate equals to UPDATED_LAST_MODIFIED_DATE + defaultTestResultShouldNotBeFound("lastModifiedDate.in=" + UPDATED_LAST_MODIFIED_DATE); + } + + @Test + @Transactional + void getAllTestResultsByLastModifiedDateIsNullOrNotNull() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where lastModifiedDate is not null + defaultTestResultShouldBeFound("lastModifiedDate.specified=true"); + + // Get all the testResultList where lastModifiedDate is null + defaultTestResultShouldNotBeFound("lastModifiedDate.specified=false"); + } + + @Test + @Transactional + void getAllTestResultsByLastModifiedDateIsGreaterThanOrEqualToSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where lastModifiedDate is greater than or equal to DEFAULT_LAST_MODIFIED_DATE + defaultTestResultShouldBeFound("lastModifiedDate.greaterThanOrEqual=" + DEFAULT_LAST_MODIFIED_DATE); + + // Get all the testResultList where lastModifiedDate is greater than or equal to UPDATED_LAST_MODIFIED_DATE + defaultTestResultShouldNotBeFound("lastModifiedDate.greaterThanOrEqual=" + UPDATED_LAST_MODIFIED_DATE); + } + + @Test + @Transactional + void getAllTestResultsByLastModifiedDateIsLessThanOrEqualToSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where lastModifiedDate is less than or equal to DEFAULT_LAST_MODIFIED_DATE + defaultTestResultShouldBeFound("lastModifiedDate.lessThanOrEqual=" + DEFAULT_LAST_MODIFIED_DATE); + + // Get all the testResultList where lastModifiedDate is less than or equal to SMALLER_LAST_MODIFIED_DATE + defaultTestResultShouldNotBeFound("lastModifiedDate.lessThanOrEqual=" + SMALLER_LAST_MODIFIED_DATE); + } + + @Test + @Transactional + void getAllTestResultsByLastModifiedDateIsLessThanSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where lastModifiedDate is less than DEFAULT_LAST_MODIFIED_DATE + defaultTestResultShouldNotBeFound("lastModifiedDate.lessThan=" + DEFAULT_LAST_MODIFIED_DATE); + + // Get all the testResultList where lastModifiedDate is less than UPDATED_LAST_MODIFIED_DATE + defaultTestResultShouldBeFound("lastModifiedDate.lessThan=" + UPDATED_LAST_MODIFIED_DATE); + } + + @Test + @Transactional + void getAllTestResultsByLastModifiedDateIsGreaterThanSomething() throws Exception { + // Initialize the database + testResultRepository.saveAndFlush(testResult); + + // Get all the testResultList where lastModifiedDate is greater than DEFAULT_LAST_MODIFIED_DATE + defaultTestResultShouldNotBeFound("lastModifiedDate.greaterThan=" + DEFAULT_LAST_MODIFIED_DATE); + + // Get all the testResultList where lastModifiedDate is greater than SMALLER_LAST_MODIFIED_DATE + defaultTestResultShouldBeFound("lastModifiedDate.greaterThan=" + SMALLER_LAST_MODIFIED_DATE); + } + + @Test + @Transactional + void getAllTestResultsByTestParameterIsEqualToSomething() throws Exception { + TestParameter testParameter; + if (TestUtil.findAll(em, TestParameter.class).isEmpty()) { + testResultRepository.saveAndFlush(testResult); + testParameter = TestParameterResourceIT.createEntity(em); + } else { + testParameter = TestUtil.findAll(em, TestParameter.class).get(0); + } + em.persist(testParameter); + em.flush(); + testResult.addTestParameter(testParameter); + testResultRepository.saveAndFlush(testResult); + String testParameterKey = testParameter.getKey(); + // Get all the testResultList where testParameter equals to testParameterKey + defaultTestResultShouldBeFound("testParameterKey.equals=" + testParameterKey); + + // Get all the testResultList where testParameter equals to (testParameterKey + 1) + defaultTestResultShouldNotBeFound("testParameterKey.equals=" + (testParameterKey + 1)); + } + + /** + * Executes the search, and checks that the default entity is returned. + */ + private void defaultTestResultShouldBeFound(String filter) throws Exception { + restTestResultMockMvc + .perform(get(ENTITY_API_URL + "?sort=id,desc&" + filter)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[*].id").value(hasItem(testResult.getId().intValue()))) + .andExpect(jsonPath("$.[*].status").value(hasItem(DEFAULT_STATUS.toString()))) + .andExpect(jsonPath("$.[*].testName").value(hasItem(DEFAULT_TEST_NAME))) + .andExpect(jsonPath("$.[*].className").value(hasItem(DEFAULT_CLASS_NAME))) + .andExpect(jsonPath("$.[*].errorMessage").value(hasItem(DEFAULT_ERROR_MESSAGE))) + .andExpect(jsonPath("$.[*].failureStack").value(hasItem(DEFAULT_FAILURE_STACK))) + .andExpect(jsonPath("$.[*].failureType").value(hasItem(DEFAULT_FAILURE_TYPE))) + .andExpect(jsonPath("$.[*].createdDate").value(hasItem(sameInstant(DEFAULT_CREATED_DATE)))) + .andExpect(jsonPath("$.[*].lastModifiedDate").value(hasItem(sameInstant(DEFAULT_LAST_MODIFIED_DATE)))); + + // Check, that the count call also returns 1 + restTestResultMockMvc + .perform(get(ENTITY_API_URL + "/count?sort=id,desc&" + filter)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(content().string("1")); + } + + /** + * Executes the search, and checks that the default entity is not returned. + */ + private void defaultTestResultShouldNotBeFound(String filter) throws Exception { + restTestResultMockMvc + .perform(get(ENTITY_API_URL + "?sort=id,desc&" + filter)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$").isEmpty()); + + // Check, that the count call also returns 0 + restTestResultMockMvc + .perform(get(ENTITY_API_URL + "/count?sort=id,desc&" + filter)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(content().string("0")); + } + + @Test + @Transactional + void getNonExistingTestResult() throws Exception { + // Get the testResult + restTestResultMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + } +} diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/TestUtil.java b/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/TestUtil.java new file mode 100644 index 000000000..84d94d627 --- /dev/null +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/TestUtil.java @@ -0,0 +1,76 @@ +package org.citrusframework.simulator.web.rest; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeDiagnosingMatcher; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeParseException; +import java.util.List; + +/** + * Utility class for testing REST controllers. + */ +public class TestUtil { + + /** + * Executes a query on the EntityManager finding all stored objects. + * + * @param The type of objects to be searched + * @param em The instance of the EntityManager + * @param clazz The class type to be searched + * @return A list of all found objects + */ + public static List findAll(EntityManager em, Class clazz) { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(clazz); + Root rootEntry = cq.from(clazz); + CriteriaQuery all = cq.select(rootEntry); + TypedQuery allQuery = em.createQuery(all); + return allQuery.getResultList(); + } + + /** + * Creates a matcher that matches when the examined string represents the same instant as the reference datetime. + * + * @param date the reference datetime against which the examined string is checked. + */ + public static ZonedDateTimeMatcher sameInstant(ZonedDateTime date) { + return new ZonedDateTimeMatcher(date); + } + + /** + * A matcher that tests that the examined string represents the same instant as the reference datetime. + */ + public static class ZonedDateTimeMatcher extends TypeSafeDiagnosingMatcher { + + private final ZonedDateTime date; + + public ZonedDateTimeMatcher(ZonedDateTime date) { + this.date = date; + } + + @Override + protected boolean matchesSafely(String item, Description mismatchDescription) { + try { + if (!date.isEqual(ZonedDateTime.parse(item))) { + mismatchDescription.appendText("was ").appendValue(item); + return false; + } + return true; + } catch (DateTimeParseException e) { + mismatchDescription.appendText("was ").appendValue(item).appendText(", which could not be parsed as a ZonedDateTime"); + return false; + } + } + + @Override + public void describeTo(Description description) { + description.appendText("a String representing the same Instant as ").appendValue(date); + } + } +} diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/web/util/ResponseUtilTest.java b/simulator-starter/src/test/java/org/citrusframework/simulator/web/util/ResponseUtilTest.java new file mode 100644 index 000000000..95501d712 --- /dev/null +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/web/util/ResponseUtilTest.java @@ -0,0 +1,77 @@ +package org.citrusframework.simulator.web.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.server.ResponseStatusException; + +class ResponseUtilTest { + + @Test + void wrapOrNotFoundWithPresentOptionalShouldReturnResponseEntityWithOkStatus() { + String expectedBody = "test"; + ResponseEntity response = ResponseUtil.wrapOrNotFound(Optional.of(expectedBody)); + + assertNotNull(response); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(expectedBody, response.getBody()); + } + + @Test + void wrapOrNotFoundWithEmptyOptionalShouldThrowResponseStatusException() { + var maybeResponse = Optional.empty(); + ResponseStatusException responseStatusException = assertThrows( + ResponseStatusException.class, + () -> ResponseUtil.wrapOrNotFound(maybeResponse) + ); + assertEquals(HttpStatus.NOT_FOUND, responseStatusException.getStatusCode()); + } + + @Test + void wrapOrNotFoundWithPresentOptionalAndHeadersShouldReturnResponseEntityWithOkStatusAndHeaders() { + String headerKey = "Test-Header"; + String headerValue = "HeaderValue"; + + String expectedBody = "test"; + + HttpHeaders headers = new HttpHeaders(); + headers.add(headerKey, headerValue); + + ResponseEntity response = ResponseUtil.wrapOrNotFound(Optional.of(expectedBody), headers); + assertNotNull(response); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(expectedBody, response.getBody()); + + HttpHeaders responseHeaders = response.getHeaders(); + assertTrue(responseHeaders.containsKey(headerKey)); + + List testHeaders = responseHeaders.get(headerKey); + assertNotNull(testHeaders); + assertEquals(1, testHeaders.size()); + assertEquals(headerValue, testHeaders.get(0)); + } + + @Test + void wrapOrNotFoundWithEmptyOptionalAndHeadersShouldThrowResponseStatusException() { + HttpHeaders headers = new HttpHeaders(); + headers.add("Test-Header", "HeaderValue"); + + var maybeResponse = Optional.empty(); + ResponseStatusException responseStatusException = assertThrows( + ResponseStatusException.class, + () -> ResponseUtil.wrapOrNotFound(maybeResponse, headers) + ); + assertEquals(HttpStatus.NOT_FOUND, responseStatusException.getStatusCode()); + + assertEquals(0, responseStatusException.getHeaders().size()); + } +} diff --git a/simulator-ui/.eslintrc.json b/simulator-ui/.eslintrc.json index 96996aea9..6ec524114 100644 --- a/simulator-ui/.eslintrc.json +++ b/simulator-ui/.eslintrc.json @@ -24,7 +24,7 @@ "error", { "type": "element", - "prefix": "jhi", + "prefix": ["app", "jhi"], "style": "kebab-case" } ], @@ -95,5 +95,13 @@ "object-shorthand": ["error", "always", { "avoidExplicitReturnArrows": true }], "radix": "error", "spaced-comment": ["warn", "always"] - } + }, + "overrides": [ + { + "files": ["./src/main/webapp/**/*.spec.ts"], + "rules": { + "@typescript-eslint/ban-ts-comment": "off" + } + } + ] } diff --git a/simulator-ui/.jhipster/TestParameter.json b/simulator-ui/.jhipster/TestParameter.json new file mode 100644 index 000000000..fa31d3bb4 --- /dev/null +++ b/simulator-ui/.jhipster/TestParameter.json @@ -0,0 +1,34 @@ +{ + "changelogDate": "20230926184642", + "dto": "no", + "entityTableName": "test_parameter", + "fields": [ + { + "fieldName": "key", + "fieldType": "String", + "fieldValidateRules": ["required"] + }, + { + "fieldName": "value", + "fieldType": "String", + "fieldValidateRules": ["required"] + }, + { + "fieldName": "createdDate", + "fieldType": "ZonedDateTime", + "fieldValidateRules": ["required"] + }, + { + "fieldName": "lastModifiedDate", + "fieldType": "ZonedDateTime", + "fieldValidateRules": ["required"] + } + ], + "jpaMetamodelFiltering": true, + "name": "TestParameter", + "pagination": "pagination", + "readOnly": true, + "relationships": [], + "searchEngine": "no", + "service": "serviceImpl" +} diff --git a/simulator-ui/.jhipster/TestResult.json b/simulator-ui/.jhipster/TestResult.json new file mode 100644 index 000000000..52bd60d24 --- /dev/null +++ b/simulator-ui/.jhipster/TestResult.json @@ -0,0 +1,59 @@ +{ + "changelogDate": "20230926185108", + "dto": "no", + "entityTableName": "test_result", + "fields": [ + { + "fieldName": "status", + "fieldType": "Integer", + "fieldValidateRules": ["required"] + }, + { + "fieldName": "testName", + "fieldType": "String", + "fieldValidateRules": ["required"] + }, + { + "fieldName": "className", + "fieldType": "String", + "fieldValidateRules": ["required"] + }, + { + "fieldName": "errorMessage", + "fieldType": "String" + }, + { + "fieldName": "failureStack", + "fieldType": "String" + }, + { + "fieldName": "failureType", + "fieldType": "String" + }, + { + "fieldName": "createdDate", + "fieldType": "ZonedDateTime", + "fieldValidateRules": ["required"] + }, + { + "fieldName": "lastModifiedDate", + "fieldType": "ZonedDateTime", + "fieldValidateRules": ["required"] + } + ], + "jpaMetamodelFiltering": true, + "name": "TestResult", + "pagination": "pagination", + "readOnly": true, + "relationships": [ + { + "otherEntityName": "testParameter", + "otherEntityRelationshipName": "testResult", + "relationshipName": "testParameter", + "relationshipSide": "left", + "relationshipType": "one-to-many" + } + ], + "searchEngine": "no", + "service": "serviceImpl" +} diff --git a/simulator-ui/.yo-rc.json b/simulator-ui/.yo-rc.json index 0b1e33214..0cfa57807 100644 --- a/simulator-ui/.yo-rc.json +++ b/simulator-ui/.yo-rc.json @@ -8,7 +8,7 @@ "creationTimestamp": 1694794967405, "devServerPort": 4200, "enableTranslation": true, - "entities": [], + "entities": ["TestParameter", "TestResult"], "jhipsterVersion": "8.0.0-beta.3", "languages": ["en"], "microfrontend": false, diff --git a/simulator-ui/README.md b/simulator-ui/README.md index a54197486..406908214 100644 --- a/simulator-ui/README.md +++ b/simulator-ui/README.md @@ -120,7 +120,7 @@ To build the final jar and optimize the `citrus-simulator` application for produ root directory of `citrus-simulator`: ``` -./mvnw install -DfrontendSkip=false +./mvnw install -DskipFrontend=false ``` This will concatenate and minify the client CSS and JavaScript files. It will also modify `index.html` so it references diff --git a/simulator-ui/pom.xml b/simulator-ui/pom.xml index 49f898c78..87ad635d7 100644 --- a/simulator-ui/pom.xml +++ b/simulator-ui/pom.xml @@ -14,10 +14,16 @@ ${project.artifactId} - true + true + + org.citrusframework + citrus-simulator-starter + + + org.springframework.security spring-security-config @@ -27,6 +33,7 @@ spring-security-web + org.springframework.boot spring-boot-starter-actuator @@ -38,9 +45,16 @@ provided + + + org.springframework.boot + spring-boot-configuration-processor + provided + + org.citrusframework - citrus-simulator-starter + citrus-ws test @@ -55,22 +69,23 @@ maven-clean-plugin - - - node_modules - - **/* - - - + + + node_modules + + **/* + + + + com.github.eirslett frontend-maven-plugin target - ${frontendSkip} + ${skipFrontend} @@ -114,11 +129,42 @@ + + + org.apache.maven.plugins + maven-surefire-plugin + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${java.version} + ${java.version} + + + org.springframework.boot + spring-boot-configuration-processor + ${spring-boot.version} + + + + + development @@ -138,9 +184,9 @@ ui-node-modules-skip-clean diff --git a/simulator-ui/src/main/java/org/citrusframework/simulator/ui/config/SecurityConfiguration.java b/simulator-ui/src/main/java/org/citrusframework/simulator/ui/config/SecurityConfiguration.java index 58eae8e47..53ccaafa1 100644 --- a/simulator-ui/src/main/java/org/citrusframework/simulator/ui/config/SecurityConfiguration.java +++ b/simulator-ui/src/main/java/org/citrusframework/simulator/ui/config/SecurityConfiguration.java @@ -1,45 +1,82 @@ package org.citrusframework.simulator.ui.config; -import org.citrusframework.simulator.ui.filter.CookieCsrfFilter; +import jakarta.annotation.Nullable; +import jakarta.servlet.http.HttpServlet; +import org.citrusframework.simulator.http.SimulatorRestAdapter; +import org.citrusframework.simulator.http.SimulatorRestConfigurationProperties; import org.citrusframework.simulator.ui.filter.SpaWebFilter; +import org.citrusframework.simulator.ws.SimulatorWebServiceConfigurationProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; -import org.springframework.security.web.csrf.CookieCsrfTokenRepository; -import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import org.springframework.security.web.util.matcher.OrRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; -import static org.springframework.security.config.Customizer.withDefaults; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; @Configuration public class SecurityConfiguration { private final SimulatorUiConfigurationProperties simulatorUiConfigurationProperties; - public SecurityConfiguration(SimulatorUiConfigurationProperties simulatorUiConfigurationProperties) { + private final @Nullable SimulatorRestConfigurationProperties simulatorRestConfigurationProperties; + private final @Nullable SimulatorRestAdapter simulatorRestAdapter; + private final @Nullable SimulatorWebServiceConfigurationProperties simulatorWebServiceConfigurationProperties; + private final @Nullable ServletRegistrationBean simulatorServletRegistrationBean; + + public SecurityConfiguration( + SimulatorUiConfigurationProperties simulatorUiConfigurationProperties, + @Autowired(required = false) @Nullable SimulatorRestConfigurationProperties simulatorRestConfigurationProperties, + @Autowired(required = false) @Nullable SimulatorRestAdapter simulatorRestAdapter, + @Autowired(required = false) @Nullable SimulatorWebServiceConfigurationProperties simulatorWebServiceConfigurationProperties, + @Autowired(required = false) @Nullable @Qualifier("simulatorServletRegistrationBean") ServletRegistrationBean simulatorServletRegistrationBean) { this.simulatorUiConfigurationProperties = simulatorUiConfigurationProperties; + + this.simulatorRestConfigurationProperties = simulatorRestConfigurationProperties; + this.simulatorRestAdapter = simulatorRestAdapter; + this.simulatorWebServiceConfigurationProperties = simulatorWebServiceConfigurationProperties; + this.simulatorServletRegistrationBean = simulatorServletRegistrationBean; + } + + private static void addPathWithinApplicationAwareServletMatchers(MvcRequestMatcher.Builder mvc, String urlMapping, List requestMatchers) { + String pathWithinApplication = urlMapping.substring(ServletUtils.extractContextPath(urlMapping).length()); + if (!pathWithinApplication.isEmpty()) { + requestMatchers.add(mvc.pattern(pathWithinApplication)); + } else { + requestMatchers.add(mvc.pattern(urlMapping)); + } } @Bean public SecurityFilterChain filterChain(HttpSecurity http, MvcRequestMatcher.Builder mvc) throws Exception { + RequestMatcher simulationEndpointsRequestMatcher = getSimulationEndpointsRequestMatcher(mvc); + http - .cors(withDefaults()) - .csrf(csrf -> - csrf - .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) - // See https://stackoverflow.com/q/74447118/65681 - .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) - ) - .addFilterAfter(new SpaWebFilter(), BasicAuthenticationFilter.class) - .addFilterAfter(new CookieCsrfFilter(), BasicAuthenticationFilter.class) + .cors(AbstractHttpConfigurer::disable) + + // TODO: https://github.com/citrusframework/citrus-simulator/issues/21 + // .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + // See https://stackoverflow.com/q/74447118/65681 + // .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) + // .ignoringRequestMatchers(simulationEndpointsRequestMatcher) + .csrf(AbstractHttpConfigurer::disable) + + .addFilterAfter(new SpaWebFilter(simulationEndpointsRequestMatcher), BasicAuthenticationFilter.class) .headers(headers -> headers - .contentSecurityPolicy(csp -> csp.policyDirectives(simulatorUiConfigurationProperties.getSecurity().getContentSecurityPolicy())) + .contentSecurityPolicy(contentSecurity -> contentSecurity.policyDirectives(simulatorUiConfigurationProperties.getSecurity().getContentSecurityPolicy())) .frameOptions(FrameOptionsConfig::sameOrigin) .referrerPolicy(referrer -> referrer.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)) .permissionsPolicy(permissions -> @@ -56,6 +93,25 @@ public SecurityFilterChain filterChain(HttpSecurity http, MvcRequestMatcher.Buil return http.build(); } + private RequestMatcher getSimulationEndpointsRequestMatcher(MvcRequestMatcher.Builder mvc) { + List requestMatchers = new ArrayList<>(); + + if (!Objects.isNull(simulatorRestConfigurationProperties) && !Objects.isNull(simulatorRestAdapter)) { + requestMatchers.add(mvc.pattern(simulatorRestAdapter.urlMapping(simulatorRestConfigurationProperties))); + } else if (!Objects.isNull(simulatorRestConfigurationProperties)) { + requestMatchers.add(mvc.pattern(simulatorRestConfigurationProperties.getUrlMapping())); + } + if (!Objects.isNull(simulatorServletRegistrationBean) && simulatorServletRegistrationBean.isEnabled()) { + simulatorServletRegistrationBean.getUrlMappings().forEach(urlMapping -> addPathWithinApplicationAwareServletMatchers(mvc, urlMapping, requestMatchers)); + } + + if (requestMatchers.isEmpty()) { + requestMatchers.add(mvc.pattern("*")); + } + + return new OrRequestMatcher(requestMatchers.toArray(new RequestMatcher[0])); + } + @Bean MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) { return new MvcRequestMatcher.Builder(introspector); diff --git a/simulator-ui/src/main/java/org/citrusframework/simulator/ui/config/ServletUtils.java b/simulator-ui/src/main/java/org/citrusframework/simulator/ui/config/ServletUtils.java new file mode 100644 index 000000000..0592cf7e7 --- /dev/null +++ b/simulator-ui/src/main/java/org/citrusframework/simulator/ui/config/ServletUtils.java @@ -0,0 +1,22 @@ +package org.citrusframework.simulator.ui.config; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class ServletUtils { + + private static final String CONTEXT_PATH_PATTERN_STRING = "(.*?)(/\\*\\*?(/\\*)?)?$"; + private static final Pattern CONTEXT_PATH_PATTERN = Pattern.compile(CONTEXT_PATH_PATTERN_STRING); + + private ServletUtils() { + // Static utility class + } + + static CharSequence extractContextPath(CharSequence urlMapping) { + Matcher matcher = CONTEXT_PATH_PATTERN.matcher(urlMapping); + if (matcher.matches()) { + return matcher.group(1); + } + return urlMapping; + } +} diff --git a/simulator-ui/src/main/java/org/citrusframework/simulator/ui/filter/CookieCsrfFilter.java b/simulator-ui/src/main/java/org/citrusframework/simulator/ui/filter/CookieCsrfFilter.java deleted file mode 100644 index 755275ac9..000000000 --- a/simulator-ui/src/main/java/org/citrusframework/simulator/ui/filter/CookieCsrfFilter.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.citrusframework.simulator.ui.filter; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.security.web.csrf.CsrfToken; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; - -/** - * Spring Security 6 doesn't set a XSRF-TOKEN cookie by default. This solution is - * recommended by Spring Security. - */ -public class CookieCsrfFilter extends OncePerRequestFilter { - - /** - * {@inheritDoc} - */ - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { - CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); - response.setHeader(csrfToken.getHeaderName(), csrfToken.getToken()); - filterChain.doFilter(request, response); - } -} diff --git a/simulator-ui/src/main/java/org/citrusframework/simulator/ui/filter/SpaWebFilter.java b/simulator-ui/src/main/java/org/citrusframework/simulator/ui/filter/SpaWebFilter.java index 73f9ee7fc..ca0b8ad04 100644 --- a/simulator-ui/src/main/java/org/citrusframework/simulator/ui/filter/SpaWebFilter.java +++ b/simulator-ui/src/main/java/org/citrusframework/simulator/ui/filter/SpaWebFilter.java @@ -4,26 +4,27 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; public class SpaWebFilter extends OncePerRequestFilter { + private final RequestMatcher simulatorRestRequestMatcher; + + public SpaWebFilter(RequestMatcher simulatorRestRequestMatcher) { + this.simulatorRestRequestMatcher = simulatorRestRequestMatcher; + } + /** * Forwards any unmapped paths (except those containing a period) to the client {@code index.html}. */ @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { - // Request URI includes the contextPath if any, removed it. + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + // Request URI includes the contextPath: if any, removed it. String path = request.getRequestURI().substring(request.getContextPath().length()); - if ( - !path.startsWith("/api") && - !path.startsWith("/v3/api-docs") && - !path.contains(".") && - path.matches("/(.*)") - ) { + if (!path.startsWith("/api") && !path.startsWith("/v3/api-docs") && !path.contains(".") && !simulatorRestRequestMatcher.matches(request) && path.matches("/(.*)")) { request.getRequestDispatcher("/index.html").forward(request, response); return; } diff --git a/simulator-ui/src/main/webapp/app/config/font-awesome-icons.ts b/simulator-ui/src/main/webapp/app/config/font-awesome-icons.ts index 40c449bb2..2d445905b 100644 --- a/simulator-ui/src/main/webapp/app/config/font-awesome-icons.ts +++ b/simulator-ui/src/main/webapp/app/config/font-awesome-icons.ts @@ -6,6 +6,7 @@ import { faBell, faBook, faCalendarAlt, + faChartPie, faCheck, faCloud, faCogs, @@ -50,6 +51,7 @@ export const fontAwesomeIcons = [ faBell, faBook, faCalendarAlt, + faChartPie, faCheck, faCloud, faCogs, diff --git a/simulator-ui/src/main/webapp/app/core/config/application-config.service.spec.ts b/simulator-ui/src/main/webapp/app/core/config/application-config.service.spec.ts index 4451c9bb8..0d2e00266 100644 --- a/simulator-ui/src/main/webapp/app/core/config/application-config.service.spec.ts +++ b/simulator-ui/src/main/webapp/app/core/config/application-config.service.spec.ts @@ -18,10 +18,6 @@ describe('ApplicationConfigService', () => { it('should return correctly', () => { expect(service.getEndpointFor('api')).toEqual('api'); }); - - it('should return correctly when passing microservice', () => { - expect(service.getEndpointFor('api', 'microservice')).toEqual('services/microservice/api'); - }); }); describe('with prefix', () => { @@ -32,9 +28,5 @@ describe('ApplicationConfigService', () => { it('should return correctly', () => { expect(service.getEndpointFor('api')).toEqual('prefix/api'); }); - - it('should return correctly when passing microservice', () => { - expect(service.getEndpointFor('api', 'microservice')).toEqual('prefix/services/microservice/api'); - }); }); }); diff --git a/simulator-ui/src/main/webapp/app/core/config/application-config.service.ts b/simulator-ui/src/main/webapp/app/core/config/application-config.service.ts index 0102e5f03..7c1b86d70 100644 --- a/simulator-ui/src/main/webapp/app/core/config/application-config.service.ts +++ b/simulator-ui/src/main/webapp/app/core/config/application-config.service.ts @@ -5,24 +5,12 @@ import { Injectable } from '@angular/core'; }) export class ApplicationConfigService { private endpointPrefix = ''; - private microfrontend = false; setEndpointPrefix(endpointPrefix: string): void { this.endpointPrefix = endpointPrefix; } - setMicrofrontend(microfrontend = true): void { - this.microfrontend = microfrontend; - } - - isMicrofrontend(): boolean { - return this.microfrontend; - } - - getEndpointFor(api: string, microservice?: string): string { - if (microservice) { - return `${this.endpointPrefix}services/${microservice}/${api}`; - } + getEndpointFor(api: string): string { return `${this.endpointPrefix}${api}`; } } diff --git a/simulator-ui/src/main/webapp/app/core/util/alert.service.spec.ts b/simulator-ui/src/main/webapp/app/core/util/alert.service.spec.ts index 3a7980e15..1a52a0906 100644 --- a/simulator-ui/src/main/webapp/app/core/util/alert.service.spec.ts +++ b/simulator-ui/src/main/webapp/app/core/util/alert.service.spec.ts @@ -101,10 +101,10 @@ describe('Alert service test', () => { it('should produce an alert object with correct id', inject([AlertService], (service: AlertService) => { service.addAlert({ type: 'info', message: 'Hello Citrus info' }); - expect(service.addAlert({ type: 'success', message: 'Hello Citrus success' })).toEqual( + expect(service.addAlert({ type: 'success', message: 'Hello Citrus succeed' })).toEqual( expect.objectContaining({ type: 'success', - message: 'Hello Citrus success', + message: 'Hello Citrus succeed', id: 1, } as Alert), ); @@ -113,7 +113,7 @@ describe('Alert service test', () => { expect(service.get()[1]).toEqual( expect.objectContaining({ type: 'success', - message: 'Hello Citrus success', + message: 'Hello Citrus succeed', id: 1, } as Alert), ); @@ -122,11 +122,11 @@ describe('Alert service test', () => { it('should close an alert correctly', inject([AlertService], (service: AlertService) => { const alert0 = service.addAlert({ type: 'info', message: 'Hello Citrus info' }); const alert1 = service.addAlert({ type: 'info', message: 'Hello Citrus info 2' }); - const alert2 = service.addAlert({ type: 'success', message: 'Hello Citrus success' }); + const alert2 = service.addAlert({ type: 'success', message: 'Hello Citrus succeed' }); expect(alert2).toEqual( expect.objectContaining({ type: 'success', - message: 'Hello Citrus success', + message: 'Hello Citrus succeed', id: 2, } as Alert), ); @@ -146,7 +146,7 @@ describe('Alert service test', () => { expect(service.get()[0]).not.toEqual( expect.objectContaining({ type: 'success', - message: 'Hello Citrus success', + message: 'Hello Citrus succeed', id: 2, } as Alert), ); @@ -199,7 +199,7 @@ describe('Alert service test', () => { expect(service.get().length).toBe(0); })); - it('should produce a success message', inject([AlertService], (service: AlertService) => { + it('should produce a succeed message', inject([AlertService], (service: AlertService) => { expect(service.addAlert({ type: 'success', message: 'Hello Citrus' })).toEqual( expect.objectContaining({ type: 'success', @@ -208,7 +208,7 @@ describe('Alert service test', () => { ); })); - it('should produce a success message with custom position', inject([AlertService], (service: AlertService) => { + it('should produce a succeed message with custom position', inject([AlertService], (service: AlertService) => { expect(service.addAlert({ type: 'success', message: 'Hello Citrus', position: 'bottom left' })).toEqual( expect.objectContaining({ type: 'success', diff --git a/simulator-ui/src/main/webapp/app/entities/entity-routing.module.ts b/simulator-ui/src/main/webapp/app/entities/entity-routing.module.ts index fe1354ddd..ba8bd679b 100644 --- a/simulator-ui/src/main/webapp/app/entities/entity-routing.module.ts +++ b/simulator-ui/src/main/webapp/app/entities/entity-routing.module.ts @@ -4,6 +4,16 @@ import { RouterModule } from '@angular/router'; @NgModule({ imports: [ RouterModule.forChild([ + { + path: 'test-parameter', + data: { pageTitle: 'citrusSimulatorApp.testParameter.home.title' }, + loadChildren: () => import('./test-parameter/test-parameter.routes'), + }, + { + path: 'test-result', + data: { pageTitle: 'citrusSimulatorApp.testResult.home.title' }, + loadChildren: () => import('./test-result/test-result.routes'), + }, /* jhipster-needle-add-entity-route - JHipster will add entity modules routes here */ ]), ], diff --git a/simulator-ui/src/main/webapp/app/entities/test-parameter/detail/test-parameter-detail.component.html b/simulator-ui/src/main/webapp/app/entities/test-parameter/detail/test-parameter-detail.component.html new file mode 100644 index 000000000..a91a38d47 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/entities/test-parameter/detail/test-parameter-detail.component.html @@ -0,0 +1,42 @@ +