From b8ee6cd04409afe48c350f27a86e28bd1cd15d59 Mon Sep 17 00:00:00 2001 From: Ronald Holshausen Date: Sat, 13 Jun 2020 17:17:13 +1000 Subject: [PATCH] refactor: move content detection to OptionalBody and ContentType classes --- .../pact/consumer/groovy/PactBuilder.groovy | 12 ++- .../consumer/junit5/PostImageBodyTest.groovy | 57 ++++++++++ consumer/junit5/src/test/resources/RAT.JPG | Bin 0 -> 28058 bytes consumer/junit5/src/test/resources/ron.jpg | Bin 0 -> 13819 bytes .../pact/consumer/dsl/PactDslRequestBase.java | 4 +- .../com/dius/pact/consumer/KTorMockServer.kt | 2 +- .../matchers/MultipartMessageBodyMatcher.kt | 4 +- core/model/build.gradle | 1 + .../com/dius/pact/core/model/BaseRequest.kt | 2 +- .../com/dius/pact/core/model/ContentType.kt | 67 ++++++++---- .../au/com/dius/pact/core/model/HttpPart.kt | 40 +++---- .../com/dius/pact/core/model/OptionalBody.kt | 100 +++++++++++++----- .../au/com/dius/pact/core/model/Request.kt | 2 +- .../core/model/RequestResponseInteraction.kt | 6 +- .../au/com/dius/pact/core/model/Response.kt | 2 +- .../dius/pact/core/model/messaging/Message.kt | 53 ++-------- .../pact/core/model/ContentTypeSpec.groovy | 18 ++++ .../dius/pact/core/model/HttpPartSpec.groovy | 2 +- .../pact/core/model/OptionalBodySpec.groovy | 20 ++++ .../core/model/messaging/MessageSpec.groovy | 21 ++-- core/model/src/test/resources/RAT.JPG | Bin 0 -> 28058 bytes .../specification/BaseRequestSpec.groovy | 5 +- .../specification/BaseResponseSpec.groovy | 5 +- .../dius/pact/provider/ProviderVerifier.kt | 6 +- .../dius/pact/provider/ResponseComparison.kt | 26 ++--- .../provider/MessageComparisonSpec.groovy | 4 +- .../groovysupport/ProviderClientSpec.groovy | 4 +- .../groovysupport/ProviderClientTest.groovy | 6 +- 28 files changed, 299 insertions(+), 170 deletions(-) create mode 100644 consumer/junit5/src/test/groovy/au/com/dius/pact/consumer/junit5/PostImageBodyTest.groovy create mode 100644 consumer/junit5/src/test/resources/RAT.JPG create mode 100644 consumer/junit5/src/test/resources/ron.jpg create mode 100644 core/model/src/test/resources/RAT.JPG diff --git a/consumer/groovy/src/main/groovy/au/com/dius/pact/consumer/groovy/PactBuilder.groovy b/consumer/groovy/src/main/groovy/au/com/dius/pact/consumer/groovy/PactBuilder.groovy index 5bd21dc63f..47396ced87 100644 --- a/consumer/groovy/src/main/groovy/au/com/dius/pact/consumer/groovy/PactBuilder.groovy +++ b/consumer/groovy/src/main/groovy/au/com/dius/pact/consumer/groovy/PactBuilder.groovy @@ -124,15 +124,17 @@ class PactBuilder extends BaseBuilder { Map query = setupQueryParameters(requestData[i].query ?: [:], requestMatchers, requestGenerators) Map responseHeaders = setupHeaders(responseData[i].headers ?: [:], responseMatchers, responseGenerators) String path = setupPath(requestData[i].path ?: '/', requestMatchers, requestGenerators) + def requestBody = requestData[i].body instanceof String ? requestData[i].body.bytes : requestData[i].body + def responseBody = responseData[i].body instanceof String ? responseData[i].body.bytes : responseData[i].body interactions << new RequestResponseInteraction( requestDescription, providerStates, new Request(requestData[i].method ?: 'get', path, query, headers, - requestData[i].containsKey(BODY) ? OptionalBody.body(requestData[i].body.bytes, contentType(headers)) : + requestData[i].containsKey(BODY) ? OptionalBody.body(requestBody, contentType(headers)) : OptionalBody.missing(), requestMatchers, requestGenerators), new Response(responseData[i].status ?: 200, responseHeaders, - responseData[i].containsKey(BODY) ? OptionalBody.body(responseData[i].body.bytes, + responseData[i].containsKey(BODY) ? OptionalBody.body(responseBody, contentType(responseHeaders)) : OptionalBody.missing(), responseMatchers, responseGenerators), null ) @@ -402,16 +404,16 @@ class PactBuilder extends BaseBuilder { .setMode(HttpMultipartMode.BROWSER_COMPATIBLE) .addBinaryBody(partName, data, ContentType.create(fileContentType), fileName) .build() - def os = new ByteArrayOutputStream() + ByteArrayOutputStream os = new ByteArrayOutputStream() multipart.writeTo(os) if (requestState) { - requestData.last().body = os.toString() + requestData.last().body = os.toByteArray() requestData.last().headers = requestData.last().headers ?: [:] requestData.last().headers[CONTENT_TYPE] = multipart.contentType.value Category category = requestData.last().matchers.addCategory(HEADER) category.addRule(CONTENT_TYPE, new RegexMatcher(Headers.MULTIPART_HEADER_REGEX, multipart.contentType.value)) } else { - responseData.last().body = os.toString() + responseData.last().body = os.toByteArray() responseData.last().headers = responseData.last().headers ?: [:] responseData.last().headers[CONTENT_TYPE] = multipart.contentType.value Category category = responseData.last().matchers.addCategory(HEADER) diff --git a/consumer/junit5/src/test/groovy/au/com/dius/pact/consumer/junit5/PostImageBodyTest.groovy b/consumer/junit5/src/test/groovy/au/com/dius/pact/consumer/junit5/PostImageBodyTest.groovy new file mode 100644 index 0000000000..876e4500bd --- /dev/null +++ b/consumer/junit5/src/test/groovy/au/com/dius/pact/consumer/junit5/PostImageBodyTest.groovy @@ -0,0 +1,57 @@ +package au.com.dius.pact.consumer.junit5 + +import au.com.dius.pact.consumer.MockServer +import au.com.dius.pact.consumer.dsl.PactDslJsonBody +import au.com.dius.pact.consumer.dsl.PactDslWithProvider +import au.com.dius.pact.core.model.RequestResponsePact +import au.com.dius.pact.core.model.annotations.Pact +import org.apache.http.client.methods.RequestBuilder +import org.apache.http.entity.ContentType +import org.apache.http.entity.mime.HttpMultipartMode +import org.apache.http.entity.mime.MultipartEntityBuilder +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.impl.client.HttpClients +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(PactConsumerTestExt) +@PactTestFor(providerName = 'ProviderThatAcceptsImages') +class PostImageBodyTest { + @Pact(consumer = 'Consumer') + RequestResponsePact pact(PactDslWithProvider builder) { + PostImageBodyTest.getResourceAsStream('/ron.jpg').withCloseable { stream -> + builder + .uponReceiving('a request with an image') + .method('POST') + .path('/images') + .withFileUpload('photo', 'ron.jpg', 'image/jpeg', stream.bytes) + .willRespondWith() + .status(200) + .body(new PactDslJsonBody() + .integerType('version', 1) + .integerType('status', 0) + .stringValue('errorMessage', '') + .array('issues').closeArray()) + .toPact() + } + } + + @Test + void testFiles(MockServer mockServer) { + CloseableHttpClient httpclient = HttpClients.createDefault() + def result = httpclient.withCloseable { + PostImageBodyTest.getResourceAsStream('/RAT.JPG').withCloseable { stream -> + def data = MultipartEntityBuilder.create() + .setMode(HttpMultipartMode.BROWSER_COMPATIBLE) + .addBinaryBody('photo', stream, ContentType.create('image/jpeg'), 'ron.jpg') + .build() + def request = RequestBuilder + .post(mockServer.url + '/images') + .setEntity(data) + .build() + httpclient.execute(request) + } + } + assert result.statusLine.statusCode == 200 + } +} diff --git a/consumer/junit5/src/test/resources/RAT.JPG b/consumer/junit5/src/test/resources/RAT.JPG new file mode 100644 index 0000000000000000000000000000000000000000..4eb2392321658cff4f77c61d7ab7406791529420 GIT binary patch literal 28058 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 literal 0 HcmV?d00001 diff --git a/consumer/junit5/src/test/resources/ron.jpg b/consumer/junit5/src/test/resources/ron.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3670d8ceb7480ad90aa274e5245dc6c2d28e08f1 GIT binary patch literal 13819 zcmbWd1z40%_b`0#F0gbi9WLFqEDgGJOGrs6-AGCZxP%hYDIqQmN=u6hNOuc}(pZ2< zNvM1`{+{Q3p6h$B_y5K0&faHd&N(yZ+-K&@%$bY%ixq%UTU|>XfIuLCCinp^RvARp zf?S;dKub#iAOrw_0APaP0}v201vB%%U>gwTgTVflLqS*^0tLvxCrAjOKo|i&{lV-S zgZCFF7KC%b3^wui|G%FaT6zXZl!&;Ph`0<$DTWf06O)n?M_6T-~0+z%6)jI<4FTsDowEu!H@gUHD_z-~j(0{>K=r0*u zwsQHo_-`Fu@&z$3-UBKCAprrL0G|*JhZ7MI5|bjxNJ&UY8L4R~5UfmB*jSlZSdd&I zyhsiqP8JqEX?`IwaWoozg;!Ql2Bjb(fks_2fe;Z9k&=)ykdZN<*jd<7|DV%ECqPAb z*(Vr;1AtONU{sKc9^eYtPkiu=UcS_SB?uIThfe?}BqAmO5t=9gP*X4{9tb! zmXVcHQB_md(A3htVQgY*W`5Jc-oeqy*~Qh(&p+U9U{G*KRP=*~F|l#+Y3Ui6S=oDuoe%sUA*FP{gG&MalJNIsWVR3bBePeU$)8{W=f9xL| z9v%NYIX%0S>r&2tr+)P{2{YS1vfD{G+UmlDKxCR^< z@Xus8Vd)a4oDwEr6{9>ZqkxD@NfDMVUYif{55R}RFGVaxn3Q+K<8yDQT>wFASm6mW zzqFGUy?9$@t3IMH2hOLa56Y_XN9@#65MvU#z%0U83nepD$5Bv423(E5WbtpNGe(9O z5-l}h03Gj&I8)|>HoN?7R+zD*1P3>o_EQ7o(N1acfCWFl3)qEH95nSYA6E+SRgd&; zLhk+r5c^tQic0FKEk~T?LyvuU6X1UGVol^};rB=)qkFikU-KVa-Rii1^G8;B)tHTY z3&}6O7$Dr*{b_-Efr{jCmYuJGAi7hPkOpN2f71wdAm3K;4n}S{K z>{@cwu9K_$>A11|i?(9SY*d+kn&q7d{O#ao-F{cVhcWF#9H|~je@m9sse6>nm_jpG z>Pdn7^e^dk5=AEwW{36fsy)za#6Ldhcec-E$}1M41PbzktMbCa5A-^WbEV>g#>vYG zuvd#U$R(zpT1iJKShF>G6zOu|LWks>=U!s{FeEx!YJe~z!vH#|h5(GFK&`293j;Sm zWtIGWNuvm$t#ni>3(KnR3Tn#Lwb1}p>59)O)g%0w50?KUt0R}~f#P)C`ljLUt;5<} zukYn=I?}eBK5!-JZ@Eu|qud{cU0>EtQrYY*&4>NWzW~mXKUoFFY*VjoGC8o2;{$F% z?#j}BTrJvCpJZxnCOvLPN*ZI}WrB|sm7O%UKWkAML8vRqWQw*W!lSZD$7Xk>|gQSKd%@1u}LQ zu=odD05Y{Ww4#=zo&ZUSf*i~$uxFgS^iTS6MG?W;Qr3+m&3?{P)B9aJ7CMPiI{ZW& zC}oH9LmY)kQF@awfG|`85JpB5kiS|J=0-qt&>1;@6-AJzgCQRkam7tdz+PyRp^ZR8 z2dxSpv0yF~tjPbiS}9bs$%BOnIKAJ0kn_vLhh27DH%yN=X7_kW{fRm06}S66B>D{} zxsC>RBhEHvuF^M2(_gh+5#_-u>Zp27IlQxWxaw19Mpk}H&7L<$0zV;SZres=`nD4L zH4&c_7ucJIaCLWM|FzavqP-y|d+%9Rq&+`;D{pzN{sj=* z0qj7VOpM7J4Xn6!=Ym$$os|Y!Tzt}sqnuMIVvxWTdo}#W20i`a!*Xh5k3?dJUy|R= zKQvK;jgGbt;RNl;Y(iNUjjuHYKT(cp*to+r9m(XP(pWwn;K-btsIfag>GSOxM5-mj z48)q%5)rEgdwPN4SJKF^n(_C11)nGDPH);G<6t2Va1_k#ds{Dx3CmmT`TNY-W*1MG ztvlM=EOkbT?aSQT^Hd^TbhOwotfw`~%t4&*Xm=u?3$By&;RF7!MIIiZ_ZGvKWok_o zhz|rsN!;Yho=ttY-oUfDq(V6)|6}4$z>ZN*3tjyw7M0mW@La8M@hxu-DLccFV4n*t zWA8XQ*qQTfDE1kpz0X*9Q~kbPAs~B>E z3?>Xe1_y^}d#fQ4tvCwBAG=u=n%~7A4~Vz+-_rkPbIWPi^SKUn*xL!SiI7)S(pl7R zQJdyw5!RM!o@+UUaSNGR8xwFXTVJ~X{O18M*D|#R_hJQf5REjS) zf#uop+*;(?hd-g&TinxJmn+ZGP5823gF1cn8V1oIf@7T-tjc0t2aEHVK5-|C9`$OHI{0n7F$o> z*)ej@op17QiHlLF7)LX&S$w$rXv>Ganq4Ia{ zex1$j03vFC9i~&lw1m0~WT9k~8v!)t6s2M5|9a1%4_diiUnMXUqcTj%=#@+J>zu|{ zFn~pr!*oG2q#!6DQ|Djk90e3wp%ou=nqE5r>zsJ1GE&+;$vv=7?&kFF{l>x@o6QQdf11X!w0w|V1X!U(o#T77#)4U# zTxa)8@~rJFNox7ubF%7BS37uQ52_8^Qah~JdPLpq@S(%&%S39v0sJO?eCcqJG`#Is zWlp+5M?^=r6?hv+-|nv_8;_DSJCpr5N$=$XN7iyw_zB|sQWY$- zRZ|$!=0T2wJ~GzNz$hcAG15uU5=C4_xq1F7lE+m7bQp+5s|f-mdc_Ut+Bt!Tn~vyJ zB~xGm9y57MGNAYVrB4|IDQz-u9S&Y0*4ZBLj*ruWj>`_y!$cx}BzO*slOt+E~rfUk% zuf?v0`k*~x4$*Fbl=F)U(=7O`@EhBFFwUDvlZTZL){9h)BP}{2@7b3xIq@vJ^2;;a zJ+9WLbdzpBIu(5_gIMD`=sY{0eT71IOqWdU7L)L3X^Brv5D4~+M68d^!eow)o=I?4 zc}V0poBNMTzWX9)yu++MNrhb5FfF-CdXtCrt0gZl2Ubi$whXI=Zp(zV?@>7#;n=Q9ymCq;a%66X8lpOc&;-OHD=_fwp&CGo*(8(ud@ z*f(dHBx%VeGVFH-{IP2+5F0Ki>X_Tz6{Y@?^=<+)sqgGd$$t&?Jn#bOR|~OX?}#S# zeXp+nnh*M-zolF|sU~xrVXfE&Mp|nDJlDz%P0je!q=6v-*FxYVg2*XG1C363xe^!# zm;BSJd?zWZrD1>l6o>fyET%+|Wz zvhBsfYNX>;*vF_~^XPt@L*1SNb=5 z6yGd}#W9d?CQjBbD!1JxH)iNLKUn}ha~|AT?!jv?f4ze6%=F67wZY9d&FkRo_lUPa z-c!%3*w+L!w1NF|kYCvWmuuH2wxb-8N7DJEE~-Ov_TPz*0_%)=sJfoD$Z6!T*#o-y zg+FoRSM*zRhi-cbOY?vAR-%1>Q$eD$p$N<1V_IC_JmaClu*VXPob!Cuk!vz+sd?Xj zfo}$4+9xZiPv1~?0cg}4%=hCb>kqZx+5QY|2)_MrBkE=WIhH}RW^l0JOYGIRB+0+- zwkx*TV_1exe1vORcOK3=txne=H?DlLmJ;K8KbM82JR(TAJvkemQJjh)4SHM1@a<*- zaXq}rZC0n^tPV#EuC@?UE&w{Gga=$+Y)@_F@DEn&Fs_;01=KA}c@h7$0zsAQO>q6zoEVc%9C_J<=sde(5`o(u zoaKnsoksidAg;$Rt^AbfqdFP&fTApp^f!0Qty@ozJO2dKP)I&1JP6)01l%_AYmqPE^p$u3pB(1*mCaH zsk1ca!ZY`m{x4gC=0k^vwz9TFrKsT_;R0b~TCg}fGo(XOSaJwZx_WNtT&hO+?X^1Z z(#tE~EbjNYkDK0EJ9BuP=aA(8Xy|Aj+_ixul0lhio&zldp^q4Q5_9k(O`b zNT<9t%eJ1#yl4Me@dBK0z*$Et7qFVl^>THA<%6@cGMS7daMN_Hgy7k8dG~;>~n;ThD;Q+dEwTAxu@EzHUv{_@g)W z3HHMT)26~z{gkNR?Mt(mb9ULqAykP0UM^I@#~O|&_9S9_uCXYcrlH^{b@jZtt&Zl} z$CNC|px3+~zGLXizN8xV`RXY&e||$8F9)PNDPam`uX9ZtD68zk5shoxZrq4iOT*Gs zD^cujrCsgcx-wTLN%}@84!P|eo&n)b_iBd!F1vE_qtZc!BL^zv{eTsvzvZVQW%&g| z4$rmvH8Zk?4aN~kj3P!Rtn8}+o{$;p0s*5X6IV+dj`%0%1Gne7ZDJpYj<+ho4GzDA z8bFYct2>Ed8E`XDQ)YUS3*<97&b+p+icxGWdsrcw{5Z8+?xg|2rw7%M?_ae1Fbay@ zZgv@NRRZPd)mw)hNzCz4tJdCBQ>aVYU^r~gi zW3Tq3BPRlqxq9A|)Q!|9XeRw^DO*FYpQ}1~qrzdJ*VHrotC(_n>NgBqDQVZ*%Jj@y z$GYwh$^EehDUA-R5AM2FR$^u#b+U*l!K=cis7{vkxkfs*fF%k7EKAvw+yp9p0xV3| z%Q#9O{T4&y)mt4ilRPsWXCedVDT=1dL#(^|oqjr-8H-C%1H@RSBTgT%GnCbH;sp?3 zdB)LGuBCC?_>zx4VOw#Kfqst4mv*`##5o&+`Ne?z593^njRrg(coGY4n2Ae*B+)f$ zZT&_91lCxnug@HoSVC`<5r;p@ST?nVl@*>@nI-rhVlV@_Eh${(w0=YFE2Z;~3YEF1 zF{{g5FRrX97{E0M7mM1KsRu$|#njitvJuue_bi-Re#D9f8N=(duYLTJ=_~ExwII>; z#WOBQaFR`U)C-sHddO zN+MdTRb3VaY)s1^J{vgV`DK8&P{LDm03(f3|KkGHtRq=(`?Oy+)a&i1!yL9NKgo{o z0*MpzNSI3RHI=#PD`353fY;T^GOc6m$!VqYvq#Ta``nJ?L!xl+ zc#d?IIUp9^Wc@z$>zfZ-SYg>Ate|bhdViN+$dV=v)^|NQSM)r!odZZpH42Y zE)g$ne6#cehB`|==-XoZ2PO)UUbA8|es{)#Ei_x3W}&@hSJv7)~xvL z(=n{yPOwG0u>{D3`1!0oR1G-_<^E`@%1WT)gG8EONz~8$f(*!WkMo>Y0-Xx8I`^}A zs+@Cy0;$v3<5AghT4GH^{viy#RqWFHZkQli_m^GAEr2&d zImxfxubRSBO}Vo1oaHqKB(=4QFzTD1mJHyMRpoiL7%c@{ABC=&l7kLQdckt0y-nX0 z!$^m&Zn5>f>cuKUJe5=0*4o!;-Pr{It3xp3rDLcX{^1`|GY8jK0yNAu}PPsX5ZP zL6(_GUHn!vs~TAx1y{0~*s>0Tm2F+v^YC~254j1@+gx=Q!VBj2-te#?$S~>Gip?8>LY;YO?SEdW%2KZ1Cl{(0{qtv;(?^z zESI}l$$j-rPvnQW*IBM;H#acMVaoHFs@bYpCK~;yQv1;mZ2B-0Cyz8gXYQ*z!Xux^ zkWPxP1m%uH>*>CdTUOJVW~U8s9mI#h5s*op*Mg`T-~RZPE5jZ|FDyd{*8)F&7FvR8(#m80o8P>8gRJ@!&~1&coi*4^IpLJiYvV zjWm>z;K48wz5#|3NWshnh}+uxc`F$h=>Ge7`hR~O&t0Ch1HiQKC9nUz{(meXb8z&t z2amOpAQr~n+ZPN(KyHJuRG`22CHxqK5%w;&4j^0w!a}}a2SIrLvfS<;c>5A|{tI8m zHvojMv5^WW+ht$_>HJ@?-G9OMF20^14jRPaa`5y5`9sbBfgLX4&`a3UBLM8%-*hP< zsiT+44e-eYW)?sL&;s-T0{{uw0Rez3-~k3vgu$m5SmFm5f%VG&2YRNz^adc69Z2O0 z*nDI9T)qPk2TcFbt)G(w>TeN*R2cy9KVDoMa03859RPl(TwI(NUR?Yx z0wXC)0MP06ANo5u0FXHX%TxYS#`P2cD53$Nx#vG+cG&>X5(5AX3*NTAwtw5X47xy_ zz#!3nIRKE|1OS=|03f#fM{i)=Wj#>*4gicnUFnVjKn@r*;&BFfTm2v827^KW(cAxs z^Dq7Vjo!eZ(918F@W3xV;bj~IpBN4&AS5LwB_$yyAt58DrX(Y$A}1lCq@$#wLD16D zl2Xt!&>k#v8VZG#_RxC6QxS^HU`h9SNA!=V_Z&&|kDCi!KTu~Ubir~whOjB5qy!!0pSrx3DwTaLKS-vD z1&xUxBNOyvqa-RJ_#g*|hVwVeC;pg&0LlQd7zG!bLNGs7goH*O3s-N>7kDEU14N^F z{C(K@*^xi`#gpWcg~Z^pA@>HqIEzgHE?63rrlzj8rhgVrykC5!M)2@w6r3BrMNR($>-cfc}{O(y7V{^KBJ4(h5}0I(k< z8Xy|=v2eLQ`Ie{?qYa^ejQC_mGI1hge8P?L-V#{kD$itaKBS_6urm($(XIdqe5)}y zhA0>N5!o8avVsH6qd5>sVvU^Ocm0F z9!u7!Zkmy?gGq9g4xB=Mjj+~eTe1Aunr)Aotbs$KxG(#qk%~})0a`E+hYte{`LB^e z0SGwEsHh1jY1k1+4l!{!Cx(_Ag;tV~1kINa#Dd^Me+C>M{zNfaoDAFvQ)fMtS@$>p zC{Uh8sW^HPs^9Zt#ofU-t0BvMS|Vhg`R?t4S0gvQ-!KCCM(Dy5>77J@w$R45{1}Sc zkw*QK(NG3ei%<_bc)Sebvlie}!SDNBXL+e1^ZBI8es*2W6ZbVk-O9<<_Flu2-oU>W zteGqJI#cR3q{1p3oSp6(F~qM4;QJ-yt?ykeRGrp%Yv6I?8$*KZO~=*O+)Z4+l-xcL z@+)QMsAywG{!lP0+t9Gw3M4mV38=42clg$Ii^B(R&$N&=fcM^?x975M!YQR<`HUeF zGOVGGdTn-%W=!|mf_d}2KRI&tbGR-b_T*&2%Z96C&y5Gu{dUWqx9*yJS>$VOp|u;6re|ard{7r~lpa&`dBBj@ zQ!&xBGTkpWPZy5&sEIh$<N9xSiY_tUPi-lT2y(I}s8xK%Oc7H&Ra2n6+dtot@a2g_ zRs9_~;nkV2v1`9gPXi~5w_lSjFrXtKvSdmN^rh+QO9I5VdsJ@oN?wj!>U#TMU z5vJ;3`C{($}msk5TDKQfI;h3_xG0U$mU<}Xl=#5=Xu-C7S#s4=^C>V$APmtE%(^!pE`YN z6BkgLYuQ_{dFMgbOd3b^MqE@$V)~c-Fr8l z*6_}Wgp}{XDAn znC}nXqdEhs`iXjZHYt9|eyzCgyP;R*SbsNzzwgLt&7;Toh4^M!bIZxyYLX$7x3}NL z6;TRXKOy_c=$5C3%JI#SzWd=ycb?A<{DZl>r)1(-_S3se zwYX^1Euk{9qC{QT7|*yx8Q0{^BTBJ}6M@Y9Rb!)`Q;nL1r9DwifgO_A!zePzGu;P; z6uOdUvF~p!3_3C9hS__}Qhc2LHWI{*d-Lwz6(vNo-xT>2w-5{ZIdjCf;VzF0fKaN# zpk3ujneNqwK6eSE{tK}`%qAB=<(tSt{g-{?2?>Wt?a~Iok9~-8=J_ zQd0>DzAx3}QGp4|O3bcRY-Qmqa%cK41j?DBD{9O?lU1)&&MNhlz5OHr%X18(onUm( z5z=;nwvK3rZN|39{&a&5nSR!ftki!hYHpZOY$aQ<+gP;{?XQag{dF3za78+syuWkKv5 z<#FE_txMheNOIoIE#iA}G^u(%lpEZK%U30FAH)Atsi+?zJFiPjBsKGy^TxNrgM`Gl zhQ7A)Z>IzWoySlghXqVH=IF;elgzT_46iV9)_FGKpUQmYQN+-EXlD5?LI1m`xyW&6 z#`hasZM)cPrGhjhlN@2KyV9%gcq3DMe5AKh<&@U1MPT=PV2GI(wH)j@O9rwjdSNC)`tvl>#nVa(;aO% z|3Ji0{><>BZqhNIso4&1{@oILjJT2IFhnaWzbLasdPhLzXxl|z`w8jzM;jU;H0g59 z(eFsUxw$;lp^yBi+|bU69=iUlk9j%w{=lEti`6zd{Gx&%(Tn&e#;Z16FSjIeTss2i z52=5RC@DHEgqfC%R*NK&HRbNU9~n89%spldTX{;=$d;;G*6pcKU-V{2p>(>yXk(bB zHO%QAmb92bI=$%AV$2Y?ethQZm7;a+7!vT;=y$a`2wa6E9g^ z#9_84QuXvWpQpcJ&E1K51kWSxUcbBZ9NFTv$GWBk25}~y-Od4e;}7+%w83Jrcld4R5zF8cPiXMV1|L&9(D6vozd# zEHM1*i_W*+o&f)^eYFx~uc#LY6A#Qi4El?iuQ(a3KXb?5BlmTqL)WN&LH#jK`9mzHYk}M;&m;e(F}Llx6Nd?zUgYn&{d|4cGUX0pM2unPvAV}pzrz3VCY4YB}MCCoLdj7 zg~B#(mP&z$JbLQKHNjbJ!+1k+qF7yBZ%4t&L^Y%$dhe&X)LY0-9F z_K#XQ^B0dI%4o4xCp*FABQ=xm{-{se8%|}Kb|vZ%rfWj=Ud*4ZUgO2#?vm<)~Np$PMTbj(>_f1XvWkLh9GQJf|@OSrl$FgNGA5+xs?b6!4h{L(hS zyL4UKC>FOw$}V0W-C$eaY~}uvXyGa}Uim>mTJjR*q#Wh-J+hGdeyAjyGs!oqJCoXt zd1>v)4?2mf+J<-xwj_Bze)j9{^xn)UaaV5U&X!iCGQab@GM`dm>dSvbsE&Qk9I6py zykE9G%)8^PwqBX5{b0~L%0e)nwB%q}_Qv<} zw*$WnOr>f?ODQ+pRsTE?(<3W=apgwuEVcDR3&p?(lo3>XPn21%Tej9lY>!9CE;=$Z zAJM6?aOU>($yw&)(9dw(x!N6(*0QJ>})eO;B{cB<<4FT8t2ozYDsNM4VG)k9A-!Zve@OAP?uuf-J9d`)oM~TdXVxj(( z(42VE&R232frTmy&c}`fWt7*44f+h-l_)ld?q29BwGakW%#(m$aJ#P)*>O0l2mX>&(&BsK505yFC9PIjG`TLzLVaj4<(cnfa_dXHBj8@fO91guFM}CXzg9D&gLI zkRj9SqBCN}oqU$IAnNxcxQM*QrY0fl1~!$%)LLZWVdo>1$)F^O^Go%@oDIi!k=c$K zcDG~Hm=t()(hUud5@S2)suI@JO^c*dbKS^gYHAfk7*(f!o)#$@GMaES1yiC|_x0|G zpBdq!tP-Gi_~SFeZ)yncKeC$W`x2P`J2jAu+T3I^xJr^t?vyVlh+)%^GoLX@Bc9Au z&00U7NBB({o7lHBi75?tTq6;`@7^Q$a~lZuX~?_twj!1J>wh|$w$Y00sqG#^qDyrO zwMmHbe;)w+Jw2g}1b|3Fyc8uaYE7<4LS&>S9yJ62zm$Y{kwoAHTrO%NBnVJqxUhuC z6fI;51VqCF54F;ixUd-T7z!_i2uTS+MiK(xb~z2gm8JwxW4O|^0IU`s$Q-JPq}D`2 zHSyAjE`Om~cqtelMF{|uz=l9H@Ma)Dt%Qe#;9-?OK3pIxY9g=@Nv*^MlB8)~HVuW~ zQG*R>aYbVAD2V{5637802fGcng+~cC4f0V!LNUllEwJ-oi6#To3-#Kv*h50*C=p6$s-gpC_+iX4jT{ZZ zqr$slY>s7#x8>YkwPOxH1NGY5eqN#L8 z9L_ed{p^Z*V6K$W(G{)u#bhhfcaQa^yW2`>>wTN%VMwYJ`L&OGc2UZd7XXEd2=q)l zk&QuJG~qQ(SysXN!cQxqwPgQiVYTAsA#T=P*`L)Jk+*w4hlmW|RoA|5dEah@K;|#r z|L9m>3r%^{`7St(nS(f-qZ8(zEx<UTgG?2 zQJHXDyY+`Kl9Z!CFhZO?7VlXkF-v}sO`UoNhtZtG7^Gps{?$E8p`+qE&(yx0eb*in z^sH&#{FyN)K)GTme3x{lCXP5pP{J6chI_Uow$LpvOME>(GU}bpBN3}X>7(nv1!ryv z4jSBNLAt!?7(wF0#}4CTIsZe(@0a_&5$KVmeO&_~6hmMfiIWoFNR{IA`ZUX3A984|CJTJ8(lo>J*kUxag2DB_U$2*$3 z*PYE{(X~80IyZA?I4$lxR;?Kl@kz9r(1pZ(@c%v$%~I0W`z!aeVGkLZnoi%;+Hf}W z1+X^QNM5lQ>^KvX1J*>dTtP3b0*9)-U?qdUtA6N&dS0$kivzCkC%M?5`LrO z?$yoM`qg~Cj-=>3YA8D3&BJz5T@SzG#3x@D5%}Bv1e{tKrngGFou(Cv8V0j{SZ|AC zNW25$jro|f>zf1modYKqmuK+l9v<0WiRJ4QG}I)#7XL)Q$5iv#qJ>DC`m5R|`e<)~ zY^k8$)s5K)^;Hf+iglu z*M`MPWl)mD&|kk+{?$~~)VEAH^wtO^0o$6}{PS183_9jbV?1jCN0*EVIv5Lk)0blA z#b&|{qb)&Z8m55QJ?XyVM@uy}FDHRmf;L4GoXI?&Sp}JNg2+)ct=D%?LA;uS1b!P+ zMVb<6HKrHIr-Y4~2tp4>8kg?4bkQu`16AYev2)f2EZvYYS`U-&BM9+Xu{axdpt@7V zrJjSY5KUD6IqOtvt)jHXbm`UYHDP_k{tglAho0>mz3kVj!5Pwz7<_`|*4M@A0deEk zwtIT(&u(8MJdQ{#G`Yq9w1Z8B6k_MtgL7NCT8yw;OXxGK8rBdUI_0r0wT3F}|5{2WKHq_6p8uPd&$nkDb3{+Ft;;kH0C$k~^7; zt91mLpWF!Qya4L5laEGdd0U+X#~rBXy@R^?BjnIa7U0;99^|Rq$3+m&nqa(8$ZhbuNt(+qWBTgkb zxX({|5_AENsIoRcKBpoTE!~a$%4mS|wc)f)A3<*QNzq7(dt_6#7}`Jn5f=u1bpltfN+@qA8z71qdO>7jBBtPCHo5h?Plg~+nW~K_ZWVk578=Nh2~abq4n`2 zw>drN1-PV}77_!-T}rEse^FMxECy!Hj-9^^1P!Qy=)ySKrV9Wn4TzF)E2!WTGUQbaZBoZ>k&r(z+l+*0Jyy;3H^x98B?pSH zy&U`%aRC6VxwJB)Ly$-Ns9S6D?nMb1OZdu3k{=~^326`-x7~lriJTulve~$^#@vO| zp?-b@k9mmTPMUXY_;whtOp|8KXEKDOwsf5{chj%C_d!pN_VoesBjY_q;;Wv?a=NVI zCb9`ZCUsS_EEhnFjF$fsq&O8cPwnbPWuDDzrvA=;MoDE7Ps{|<9hKJnO_ccN+nD(3no4QFPW!*S0o*!#7rrdT=2@(ZU>fwtB_{Yi#^ z5M!o#6*Ixii`0sj+t#k}T(srpqGGn){-NRXAzRszI2E&%(%M(uPdUn86jI5V6oxx# zhL#i0bR6ZazL}xs?`4jRpR4<O>SW-8BAco@Y={*s6 emptyList() else -> { - val expectedMultipart = parseMultipart(expected.valueAsString(), expected.contentType.contentType!!) - val actualMultipart = parseMultipart(actual.valueAsString(), actual.contentType.contentType!!) + val expectedMultipart = parseMultipart(expected.valueAsString(), expected.contentType.toString()) + val actualMultipart = parseMultipart(actual.valueAsString(), actual.contentType.toString()) compareHeaders(expectedMultipart, actualMultipart) + compareContents(expectedMultipart, actualMultipart) } } diff --git a/core/model/build.gradle b/core/model/build.gradle index 636a213f76..c3e835ad77 100644 --- a/core/model/build.gradle +++ b/core/model/build.gradle @@ -25,6 +25,7 @@ dependencies { compile "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" implementation "org.slf4j:slf4j-api:${project.slf4jVersion}" api "com.google.code.gson:gson:${project.gsonVersion}" + implementation 'org.apache.tika:tika-core:1.24.1' testCompile "ch.qos.logback:logback-classic:${project.logbackVersion}" testCompile "io.github.http-builder-ng:http-builder-ng-apache:${project.httpBuilderVersion}" diff --git a/core/model/src/main/kotlin/au/com/dius/pact/core/model/BaseRequest.kt b/core/model/src/main/kotlin/au/com/dius/pact/core/model/BaseRequest.kt index b0a9e7d359..4cc9c411a2 100644 --- a/core/model/src/main/kotlin/au/com/dius/pact/core/model/BaseRequest.kt +++ b/core/model/src/main/kotlin/au/com/dius/pact/core/model/BaseRequest.kt @@ -16,7 +16,7 @@ abstract class BaseRequest : HttpPart() { * @param contents File contents */ fun withMultipartFileUpload(partName: String, filename: String, contentType: ContentType, contents: String) = - withMultipartFileUpload(partName, filename, contentType.contentType!!, contents) + withMultipartFileUpload(partName, filename, contentType.toString(), contents) /** * Sets up the request as a multipart file upload diff --git a/core/model/src/main/kotlin/au/com/dius/pact/core/model/ContentType.kt b/core/model/src/main/kotlin/au/com/dius/pact/core/model/ContentType.kt index 563df24165..55f02058bc 100644 --- a/core/model/src/main/kotlin/au/com/dius/pact/core/model/ContentType.kt +++ b/core/model/src/main/kotlin/au/com/dius/pact/core/model/ContentType.kt @@ -1,43 +1,72 @@ package au.com.dius.pact.core.model +import au.com.dius.pact.core.support.isNotEmpty import mu.KLogging +import org.apache.tika.mime.MediaType import java.nio.charset.Charset -private val jsonRegex = Regex("application\\/.*json") -private val xmlRegex = Regex("application\\/.*xml") +private val jsonRegex = Regex(".*json") +private val xmlRegex = Regex(".*xml") -data class ContentType(val contentType: String?) { +data class ContentType(val contentType: MediaType?) { - @Suppress("TooGenericExceptionCaught") - private val parsedContentType: org.apache.http.entity.ContentType? = try { - if (contentType.isNullOrEmpty()) { - null - } else { - org.apache.http.entity.ContentType.parse(contentType) - } - } catch (e: Exception) { - logger.debug { "Failed to parse content type '$contentType'" } - null - } + constructor(contentType: String) : this(MediaType.parse(contentType)) + + fun isJson(): Boolean = if (contentType != null) jsonRegex.matches(contentType.subtype.toLowerCase()) else false - fun isJson(): Boolean = if (contentType != null) jsonRegex.matches(contentType.toLowerCase()) else false + fun isXml(): Boolean = if (contentType != null) xmlRegex.matches(contentType.subtype.toLowerCase()) else false - fun isXml(): Boolean = if (contentType != null) xmlRegex.matches(contentType.toLowerCase()) else false + fun isOctetStream(): Boolean = if (contentType != null) + contentType.baseType.toString() == "application/octet-stream" + else false override fun toString() = contentType.toString() - fun asCharset(): Charset = parsedContentType?.charset ?: Charset.defaultCharset() + fun asString() = contentType?.toString() + + fun asCharset(): Charset { + return if (contentType != null && contentType.hasParameters()) { + val cs = contentType.parameters["charset"] + if (cs.isNotEmpty()) { + Charset.forName(cs) + } else { + Charset.defaultCharset() + } + } else { + Charset.defaultCharset() + } + } + + fun or(other: ContentType) = if (contentType == null) { + other + } else { + this + } - fun asMimeType() = parsedContentType?.mimeType ?: contentType + fun getBaseType() = contentType?.baseType?.toString() companion object : KLogging() { @JvmStatic - val UNKNOWN = ContentType("") + fun fromString(contentType: String?) = if (contentType.isNullOrEmpty()) { + UNKNOWN + } else { + ContentType(contentType) + } + + val XMLREGEXP = """^\s*<\?xml\s*version.*""".toRegex() + val HTMLREGEXP = """^\s*().*""".toRegex() + val JSONREGEXP = """^\s*(true|false|null|[0-9]+|"\w*|\{\s*(}|"\w+)|\[\s*).*""".toRegex() + val XMLREGEXP2 = """^\s*<\w+\s*(:\w+=[\"”][^\"”]+[\"”])?.*""".toRegex() + + @JvmStatic + val UNKNOWN = ContentType(null) @JvmStatic val TEXT_PLAIN = ContentType("text/plain") @JvmStatic val HTML = ContentType("text/html") @JvmStatic val JSON = ContentType("application/json") + @JvmStatic + val XML = ContentType("application/xml") } } diff --git a/core/model/src/main/kotlin/au/com/dius/pact/core/model/HttpPart.kt b/core/model/src/main/kotlin/au/com/dius/pact/core/model/HttpPart.kt index 7a8d1651b9..67655eb495 100644 --- a/core/model/src/main/kotlin/au/com/dius/pact/core/model/HttpPart.kt +++ b/core/model/src/main/kotlin/au/com/dius/pact/core/model/HttpPart.kt @@ -1,6 +1,7 @@ package au.com.dius.pact.core.model import au.com.dius.pact.core.model.matchingrules.MatchingRules +import au.com.dius.pact.core.support.isNotEmpty import mu.KLogging import java.nio.charset.Charset @@ -14,14 +15,11 @@ abstract class HttpPart { abstract var matchingRules: MatchingRules fun contentType(): String? = contentTypeHeader()?.split(Regex("\\s*;\\s*"))?.first() + ?: body.contentType.asString() fun contentTypeHeader(): String? { val contentTypeKey = headers.keys.find { CONTENT_TYPE.equals(it, ignoreCase = true) } - return if (contentTypeKey.isNullOrEmpty()) { - detectContentType() - } else { - headers[contentTypeKey]?.first() - } + return headers[contentTypeKey]?.first() } fun jsonBody(): Boolean { @@ -34,24 +32,8 @@ abstract class HttpPart { return contentType?.matches(Regex("application\\/.*xml")) ?: false } - fun detectContentType(): String? = when { - body.isPresent() -> { - val s = body.value!!.take(32).map { - if (it == '\n'.toByte()) ' ' else it.toChar() - }.joinToString("") - when { - s.matches(XMLREGEXP) -> "application/xml" - s.toUpperCase().matches(HTMLREGEXP) -> "text/html" - s.matches(JSONREGEXP) -> "application/json" - s.matches(XMLREGEXP2) -> "application/xml" - else -> "text/plain" - } - } - else -> null - } - fun setDefaultContentType(contentType: String) { - if (!headers.containsKey(CONTENT_TYPE)) { + if (headers.keys.find { it.equals(CONTENT_TYPE, ignoreCase = true) } == null) { headers[CONTENT_TYPE] = listOf(contentType) } } @@ -59,16 +41,18 @@ abstract class HttpPart { fun charset(): Charset? { return when { body.isPresent() -> body.contentType.asCharset() - else -> ContentType(contentTypeHeader()).asCharset() + else -> { + val contentType = contentTypeHeader() + if (contentType.isNotEmpty()) { + ContentType(contentType!!).asCharset() + } else { + null + } + } } } companion object : KLogging() { private const val CONTENT_TYPE = "Content-Type" - - val XMLREGEXP = """^\s*<\?xml\s*version.*""".toRegex() - val HTMLREGEXP = """^\s*().*""".toRegex() - val JSONREGEXP = """^\s*(true|false|null|[0-9]+|"\w*|\{\s*(}|"\w+)|\[\s*).*""".toRegex() - val XMLREGEXP2 = """^\s*<\w+\s*(:\w+=[\"”][^\"”]+[\"”])?.*""".toRegex() } } diff --git a/core/model/src/main/kotlin/au/com/dius/pact/core/model/OptionalBody.kt b/core/model/src/main/kotlin/au/com/dius/pact/core/model/OptionalBody.kt index f9c69b6f36..245c309599 100644 --- a/core/model/src/main/kotlin/au/com/dius/pact/core/model/OptionalBody.kt +++ b/core/model/src/main/kotlin/au/com/dius/pact/core/model/OptionalBody.kt @@ -1,43 +1,35 @@ package au.com.dius.pact.core.model +import au.com.dius.pact.core.model.ContentType.Companion.HTMLREGEXP +import au.com.dius.pact.core.model.ContentType.Companion.JSONREGEXP +import au.com.dius.pact.core.model.ContentType.Companion.XMLREGEXP +import au.com.dius.pact.core.model.ContentType.Companion.XMLREGEXP2 +import org.apache.tika.config.TikaConfig +import org.apache.tika.io.TikaInputStream +import org.apache.tika.metadata.Metadata + /** * Class to represent missing, empty, null and present bodies */ data class OptionalBody( val state: State, val value: ByteArray? = null, - val contentType: ContentType = ContentType.UNKNOWN + var contentType: ContentType = ContentType.UNKNOWN ) { - enum class State { - MISSING, EMPTY, NULL, PRESENT - } - - companion object { - - @JvmStatic fun missing(): OptionalBody { - return OptionalBody(State.MISSING) - } - - @JvmStatic fun empty(): OptionalBody { - return OptionalBody(State.EMPTY, ByteArray(0)) - } - - @JvmStatic fun nullBody(): OptionalBody { - return OptionalBody(State.NULL) - } - - @JvmStatic - @JvmOverloads - fun body(body: ByteArray?, contentType: ContentType = ContentType.UNKNOWN): OptionalBody { - return when { - body == null -> nullBody() - body.isEmpty() -> empty() - else -> OptionalBody(State.PRESENT, body, contentType) + init { + if (contentType == ContentType.UNKNOWN) { + val detectedContentType = detectContentType() + if (detectedContentType != null) { + this.contentType = detectedContentType } } } + enum class State { + MISSING, EMPTY, NULL, PRESENT + } + fun isMissing(): Boolean { return state == State.MISSING } @@ -114,6 +106,62 @@ data class OptionalBody( State.MISSING -> "" } } + + fun detectContentType(): ContentType? = when { + this.isPresent() -> { + val metadata = Metadata() + val mimetype = tika.detector.detect(TikaInputStream.get(value!!), metadata) + if (mimetype.baseType.type == "text") { + detectStandardTextContentType() ?: ContentType(mimetype) + } else { + ContentType(mimetype) + } + } + else -> null + } + + private fun detectStandardTextContentType(): ContentType? = when { + isPresent() -> { + val s = value!!.take(32).map { + if (it == '\n'.toByte()) ' ' else it.toChar() + }.joinToString("") + when { + s.matches(XMLREGEXP) -> ContentType.XML + s.toUpperCase().matches(HTMLREGEXP) -> ContentType.HTML + s.matches(JSONREGEXP) -> ContentType.JSON + s.matches(XMLREGEXP2) -> ContentType.XML + else -> null + } + } + else -> null + } + + companion object { + + @JvmStatic fun missing(): OptionalBody { + return OptionalBody(State.MISSING) + } + + @JvmStatic fun empty(): OptionalBody { + return OptionalBody(State.EMPTY, ByteArray(0)) + } + + @JvmStatic fun nullBody(): OptionalBody { + return OptionalBody(State.NULL) + } + + @JvmStatic + @JvmOverloads + fun body(body: ByteArray?, contentType: ContentType = ContentType.UNKNOWN): OptionalBody { + return when { + body == null -> nullBody() + body.isEmpty() -> empty() + else -> OptionalBody(State.PRESENT, body, contentType) + } + } + + private val tika = TikaConfig() + } } fun OptionalBody?.isMissing() = this == null || this.isMissing() diff --git a/core/model/src/main/kotlin/au/com/dius/pact/core/model/Request.kt b/core/model/src/main/kotlin/au/com/dius/pact/core/model/Request.kt index 13c8f4bef1..1ed34cc82a 100644 --- a/core/model/src/main/kotlin/au/com/dius/pact/core/model/Request.kt +++ b/core/model/src/main/kotlin/au/com/dius/pact/core/model/Request.kt @@ -40,7 +40,7 @@ class Request @JvmOverloads constructor( generators.applyGenerator(Category.QUERY, mode) { key, g -> r.query[key] = r.query.getOrElse(key) { emptyList() }.map { g.generate(context).toString() } } - r.body = generators.applyBodyGenerators(r.body, ContentType(contentType()), context, mode) + r.body = generators.applyBodyGenerators(r.body, ContentType.fromString(contentType()), context, mode) return r } diff --git a/core/model/src/main/kotlin/au/com/dius/pact/core/model/RequestResponseInteraction.kt b/core/model/src/main/kotlin/au/com/dius/pact/core/model/RequestResponseInteraction.kt index 5befda21c9..8ca53ce149 100644 --- a/core/model/src/main/kotlin/au/com/dius/pact/core/model/RequestResponseInteraction.kt +++ b/core/model/src/main/kotlin/au/com/dius/pact/core/model/RequestResponseInteraction.kt @@ -82,7 +82,7 @@ open class RequestResponseInteraction @JvmOverloads constructor( map["query"] = if (pactSpecVersion >= PactSpecVersion.V3) request.query else mapToQueryStr(request.query) } if (request.body.isPresent()) { - map["body"] = parseBody(request) + map["body"] = setupBodyForJson(request) } if (request.matchingRules.isNotEmpty()) { map["matchingRules"] = request.matchingRules.toMap(pactSpecVersion) @@ -100,7 +100,7 @@ open class RequestResponseInteraction @JvmOverloads constructor( map["headers"] = response.headers.entries.associate { (key, value) -> key to value.joinToString(COMMA) } } if (response.body.isPresent()) { - map["body"] = parseBody(response) + map["body"] = setupBodyForJson(response) } if (response.matchingRules.isNotEmpty()) { map["matchingRules"] = response.matchingRules.toMap(pactSpecVersion) @@ -117,7 +117,7 @@ open class RequestResponseInteraction @JvmOverloads constructor( } } - private fun parseBody(httpPart: HttpPart): Any? { + private fun setupBodyForJson(httpPart: HttpPart): Any? { return if (httpPart.jsonBody() && httpPart.body.isPresent()) { val body = Json.fromJson(JsonParser.parseString(httpPart.body.valueAsString())) if (body is String) { diff --git a/core/model/src/main/kotlin/au/com/dius/pact/core/model/Response.kt b/core/model/src/main/kotlin/au/com/dius/pact/core/model/Response.kt index f696c5472c..7cdd51279d 100644 --- a/core/model/src/main/kotlin/au/com/dius/pact/core/model/Response.kt +++ b/core/model/src/main/kotlin/au/com/dius/pact/core/model/Response.kt @@ -34,7 +34,7 @@ class Response @JvmOverloads constructor( generators.applyGenerator(Category.HEADER, mode) { key, g -> r.headers[key] = listOf(g.generate(context).toString()) } - r.body = generators.applyBodyGenerators(r.body, ContentType(contentType()), context, mode) + r.body = generators.applyBodyGenerators(r.body, ContentType.fromString(contentType()), context, mode) return r } diff --git a/core/model/src/main/kotlin/au/com/dius/pact/core/model/messaging/Message.kt b/core/model/src/main/kotlin/au/com/dius/pact/core/model/messaging/Message.kt index fe000d31b5..4492d8de7e 100644 --- a/core/model/src/main/kotlin/au/com/dius/pact/core/model/messaging/Message.kt +++ b/core/model/src/main/kotlin/au/com/dius/pact/core/model/messaging/Message.kt @@ -1,5 +1,6 @@ package au.com.dius.pact.core.model.messaging +import au.com.dius.pact.core.model.ContentType import au.com.dius.pact.core.model.Interaction import au.com.dius.pact.core.model.OptionalBody import au.com.dius.pact.core.model.PactSpecVersion @@ -15,7 +16,6 @@ import au.com.dius.pact.core.support.json.JsonValue import mu.KLogging import org.apache.commons.codec.binary.Base64 import org.apache.commons.lang3.StringUtils -import org.apache.http.entity.ContentType /** * Message in a Message Pact @@ -34,14 +34,7 @@ class Message @JvmOverloads constructor( fun contentsAsString() = contents.valueAsString() - fun getContentType() = if (contents.isPresent() && contents.contentType.contentType.isNotEmpty()) { - contents.contentType.contentType - } else { - contentType(metaData) - } - - @Deprecated("Use the content type associated with the message body") - fun getParsedContentType() = parseContentType(this.getContentType() ?: "") + fun getContentType() = contentType(metaData).or(contents.contentType) override fun toMap(pactSpecVersion: PactSpecVersion): Map { val map: MutableMap = mutableMapOf( @@ -80,12 +73,7 @@ class Message @JvmOverloads constructor( private fun isJsonContents(): Boolean { return if (contents.isPresent()) { - val contentType = contentType(metaData) - if (contentType.isNotEmpty()) { - isJson(contentType) - } else { - isJson(contents.contentType.asMimeType()) - } + contentType(metaData).or(contents.contentType).isJson() } else { false } @@ -93,10 +81,10 @@ class Message @JvmOverloads constructor( fun formatContents(): String { return if (contents.isPresent()) { - val contentType = contentType(metaData) ?: contents.contentType.asMimeType() + val contentType = contentType(metaData).or(contents.contentType) when { - isJson(contentType) -> Json.gsonPretty.toJson(JsonParser.parseString(contents.valueAsString()).toGson()) - isOctetStream(contentType) -> Base64.encodeBase64String(contentsAsBytes()) + contentType.isJson() -> Json.gsonPretty.toJson(JsonParser.parseString(contents.valueAsString()).toGson()) + contentType.isOctetStream() -> Base64.encodeBase64String(contentsAsBytes()) else -> contents.valueAsString() } } else { @@ -146,8 +134,6 @@ class Message @JvmOverloads constructor( } companion object : KLogging() { - const val JSON = "application/json" - const val TEXT = "text/plain" /** * Builds a message from a Map @@ -165,7 +151,7 @@ class Message @JvmOverloads constructor( else emptyMap() - val contentType = au.com.dius.pact.core.model.ContentType(contentType(metaData)) + val contentType = contentType(metaData) val contents = if (json.has("contents")) { when (val contents = json["contents"]) { is JsonValue.Null -> OptionalBody.nullBody() @@ -187,29 +173,10 @@ class Message @JvmOverloads constructor( contents, matchingRules, generators, metaData.toMutableMap(), Json.toString(json["_id"])) } - @Suppress("TooGenericExceptionCaught") - private fun parseContentType(contentType: String?): ContentType? { - return if (contentType.isNotEmpty()) { - try { - ContentType.parse(contentType) - } catch (e: RuntimeException) { - logger.debug(e) { "Failed to parse content type '$contentType'" } - null - } - } else { - null - } - } - - fun contentType(metaData: Map): String? { - return parseContentType(metaData.entries.find { + fun contentType(metaData: Map): ContentType { + return ContentType.fromString(metaData.entries.find { it.key.toLowerCase() == "contenttype" || it.key.toLowerCase() == "content-type" - }?.value?.toString())?.mimeType + }?.value?.toString()) } - - private fun isJson(contentType: String?) = - contentType != null && contentType.matches(Regex("application/.*json")) - - private fun isOctetStream(contentType: String?) = contentType == "application/octet-stream" } } diff --git a/core/model/src/test/groovy/au/com/dius/pact/core/model/ContentTypeSpec.groovy b/core/model/src/test/groovy/au/com/dius/pact/core/model/ContentTypeSpec.groovy index d65ddf892c..b38458bfdb 100644 --- a/core/model/src/test/groovy/au/com/dius/pact/core/model/ContentTypeSpec.groovy +++ b/core/model/src/test/groovy/au/com/dius/pact/core/model/ContentTypeSpec.groovy @@ -3,6 +3,8 @@ package au.com.dius.pact.core.model import spock.lang.Specification import spock.lang.Unroll +import java.nio.charset.Charset + @SuppressWarnings('UnnecessaryBooleanExpression') class ContentTypeSpec extends Specification { @@ -42,4 +44,20 @@ class ContentTypeSpec extends Specification { contentType = new ContentType(value) } + @Unroll + def '"#value" charset -> #result'() { + expect: + contentType.asCharset() == result + + where: + + value || result + '' || Charset.defaultCharset() + 'text/plain' || Charset.defaultCharset() + 'application/pdf;a=b' || Charset.defaultCharset() + 'application/xml ; charset=UTF-16' || Charset.forName('UTF-16') + + contentType = new ContentType(value) + } + } diff --git a/core/model/src/test/groovy/au/com/dius/pact/core/model/HttpPartSpec.groovy b/core/model/src/test/groovy/au/com/dius/pact/core/model/HttpPartSpec.groovy index 3e7571a675..46d2984738 100644 --- a/core/model/src/test/groovy/au/com/dius/pact/core/model/HttpPartSpec.groovy +++ b/core/model/src/test/groovy/au/com/dius/pact/core/model/HttpPartSpec.groovy @@ -38,7 +38,7 @@ class HttpPartSpec extends Specification { where: request | charset - new Request('Get', '') | Charset.defaultCharset() + new Request('Get', '') | null new Request('Get', '', [:], ['Content-Type': ['text/html']]) | Charset.defaultCharset() new Request('Get', '', [:], ['Content-Type': ['application/json; charset=UTF-16']]) | Charset.forName('UTF-16') } diff --git a/core/model/src/test/groovy/au/com/dius/pact/core/model/OptionalBodySpec.groovy b/core/model/src/test/groovy/au/com/dius/pact/core/model/OptionalBodySpec.groovy index 6c0c3d76b2..728fed1176 100644 --- a/core/model/src/test/groovy/au/com/dius/pact/core/model/OptionalBodySpec.groovy +++ b/core/model/src/test/groovy/au/com/dius/pact/core/model/OptionalBodySpec.groovy @@ -137,4 +137,24 @@ class OptionalBodySpec extends Specification { OptionalBody.body('{}'.bytes, new ContentType('application/json; charset=UTF-16')) | Charset.forName('UTF-16') } + @Unroll + def 'detect content type test'() { + expect: + body.contentType.toString() == contentType + + where: + body | contentType + OptionalBody.missing() | 'null' + OptionalBody.body(''.bytes, ContentType.UNKNOWN) | 'null' + OptionalBody.body('{}'.bytes, ContentType.UNKNOWN) | 'application/json' + bodyFromFile('/1070-ApiConsumer-ApiProvider.json') | 'application/json' + bodyFromFile('/logback-test.xml') | 'application/xml' + bodyFromFile('/RAT.JPG') | 'image/jpeg' + } + + private static OptionalBody bodyFromFile(String file) { + OptionalBodySpec.getResourceAsStream(file).withCloseable { stream -> + OptionalBody.body(stream.bytes, ContentType.UNKNOWN) + } + } } diff --git a/core/model/src/test/groovy/au/com/dius/pact/core/model/messaging/MessageSpec.groovy b/core/model/src/test/groovy/au/com/dius/pact/core/model/messaging/MessageSpec.groovy index b559b74b06..72805f1dc5 100644 --- a/core/model/src/test/groovy/au/com/dius/pact/core/model/messaging/MessageSpec.groovy +++ b/core/model/src/test/groovy/au/com/dius/pact/core/model/messaging/MessageSpec.groovy @@ -118,8 +118,9 @@ class MessageSpec extends Specification { body | contentType | contents '{"A": "Value A", "B": "Value B"}' | 'application/json' | [A: 'Value A', B: 'Value B'] - '{"A": "Value A", "B": "Value B"}' | '' | '{"A": "Value A", "B": "Value B"}' + '{"A": "Value A", "B": "Value B"}' | '' | [A: 'Value A', B: 'Value B'] '1 2 3 4' | 'text/plain' | '1 2 3 4' + '1 2 3 4' | '' | '1 2 3 4' new String([1, 2, 3, 4] as byte[]) | 'application/octet-stream' | 'AQIDBA==' message = new Message('test', [], OptionalBody.body(body.bytes, new ContentType(contentType)), @@ -135,8 +136,10 @@ class MessageSpec extends Specification { body | contentType | contents '{"A": "Value A", "B": "Value B"}' | 'application/json' | [A: 'Value A', B: 'Value B'] - '{"A": "Value A", "B": "Value B"}' | '' | '{"A": "Value A", "B": "Value B"}' + '{"A": "Value A", "B": "Value B"}' | '' | [A: 'Value A', B: 'Value B'] + '{"A": "Value A", "B": "Value B"}' | 'text/plain' | '{"A": "Value A", "B": "Value B"}' '1 2 3 4' | 'text/plain' | '1 2 3 4' + '1 2 3 4' | '' | '1 2 3 4' new String([1, 2, 3, 4] as byte[]) | 'application/octet-stream' | 'AQIDBA==' message = new Message('test', [], OptionalBody.body(body.bytes), @@ -152,8 +155,10 @@ class MessageSpec extends Specification { body | contentType | contents '{"A": "Value A", "B": "Value B"}' | 'application/json' | [A: 'Value A', B: 'Value B'] - '{"A": "Value A", "B": "Value B"}' | '' | '{"A": "Value A", "B": "Value B"}' + '{"A": "Value A", "B": "Value B"}' | '' | [A: 'Value A', B: 'Value B'] + '{"A": "Value A", "B": "Value B"}' | 'text/plain' | '{"A": "Value A", "B": "Value B"}' '1 2 3 4' | 'text/plain' | '1 2 3 4' + '1 2 3 4' | '' | '1 2 3 4' new String([1, 2, 3, 4] as byte[]) | 'application/octet-stream' | 'AQIDBA==' message = new Message('test', [], OptionalBody.body(body.bytes, new ContentType(contentType)), @@ -163,7 +168,7 @@ class MessageSpec extends Specification { @Unroll def 'get content type test'() { expect: - message.contentType == result + message.contentType.toString() == result where: @@ -171,7 +176,7 @@ class MessageSpec extends Specification { 'contentType' | 'application/json' | 'application/json' 'Content-Type' | 'text/plain' | 'text/plain' 'contenttype' | 'application/octet-stream' | 'application/octet-stream' - 'none' | 'none' | null + 'none' | 'none' | 'null' message = new Message('Test').withMetaData([(key): contentType]) } @@ -191,11 +196,11 @@ class MessageSpec extends Specification { 'text/plain' | '{"a": 100.0, "b": "test"}' 'application/octet-stream;charset=UTF-8' | 'eyJhIjogMTAwLjAsICJiIjogInRlc3QifQ==' 'application/octet-stream' | 'eyJhIjogMTAwLjAsICJiIjogInRlc3QifQ==' - '' | '{"a": 100.0, "b": "test"}' - null | '{"a": 100.0, "b": "test"}' + '' | '{\n "a": 100.0,\n "b": "test"\n}' + null | '{\n "a": 100.0,\n "b": "test"\n}' message = new Message('test', [], OptionalBody.body('{"a": 100.0, "b": "test"}'.bytes, - new ContentType(contentType)), + ContentType.fromString(contentType)), new MatchingRulesImpl(), new Generators(), ['contentType': contentType]) } diff --git a/core/model/src/test/resources/RAT.JPG b/core/model/src/test/resources/RAT.JPG new file mode 100644 index 0000000000000000000000000000000000000000..4eb2392321658cff4f77c61d7ab7406791529420 GIT binary patch literal 28058 zcmeFZbzD?k_cwg#5NQykOQc~4>6Gr46c~oip+Q7RY3T;(M!HczI;BAYrKJ@FMCv`G zUiWqVe$V}Tp7(kGeeXS=efIaPz1G@muN`w{X0N%KzxfQ{E6FL!0Z3qCM`8znn-z{m zS#Mh_08mt91uy{szyWRn$N(;gvcY^C2^qKzmK9*e12H;Swt^WS#6)1(31&hh6aWb< z5&13>+OKjN#1wzgnws?oKS7xN5(5AY z0k|O$Zj_h5tqun1sDE%Oh>_xc^B{peL5};arvgEb$NptUJ&2Lxf79C$vVZ73AV&K` z9|18&{BKX7f$d@a!3H44{=+j3@;r&h0Fc3Q5zL_=MvUDu!oXL5?BwC*Mtb$PN0DFs z_Gl^w0HFNAwAcWEoA=wEO;8r^4?X}fe%^0cP(5(x9~j8T_|+E_kd9RFn+Jpd@;@-j zKQL;+Z~dpBPV|4!5i*Fe5d||fm_bv(5#a$x1R@OKrbUd4fG|JbzojAkf{^V1GcA~5 ze@d_b5O)1_pAO#8g7-MT=!m%s2Pr7v3*o4zAifP^dJu<#7!#x-v_3x}1VG;cq#zyw z>BxWhKZ6+k5B&>>@&E7x01Fb)FKKYDf^;O1pA(ct2OA*-F(rsCK#U4vL|f^I#{XNg z0uZ(Wz(W8FkVi#&_FrGpzqo^L5Cy_dmp9nJvzvA#ArKRy>i&{M)FI{wGazRRcl0uc z!)Wzja5r0LCve7dbAxuLqs$_iA%Sa0_@AqUR!Kt^Ttt78F~|qbMev142tyGb`R!Fi zi3j!p2|%MoV6Z1Bo1lMf!Ce3ez@$Y`esfa;lIV!J_51E708s~ z=iA%>Vvj;tDG5#taP=Vl9{o;`iu4B~lpupU7sA1opi5Bx>|%(Sg#Hgq@&_aMkdS}t zu><*$|6rN>UhMMXwB_%B*Co?TQI|C)<{Rb@U+&p}Id^F5LVuBD+PF_9;LJ1NU78cGe9J1TD z$slx;bddk+bkhkCV1gEPq9D-&$OK3z1V}eM01ap-8sc3839R~^z)cPn4IKj$3;Pzx z(14Hd6bdpb3K|+JIB$^r5t|??0U9A4j}$tQra1<^D>3AGTrMVqbWInD*2GsvUJH02 z7B=Y}GV;4j_n7aqu=4Q>JP;HTmU$>EC$FHWq^+Z?r~k;n5NZjtvbM3cb949b^z!!c z4GInk4SNwD5ucEll$?^9mY$biP*_x4Qd(A9SKrXs)ZEhA{jR6CuYX{0XmV{gELm>bpfP*KJqf`|t!eKN&@yqUh>yL0ZkXV3SF4)WWgPCAo#8a2 zW~zqrc@M82&@2|3wWPheCrEZB5U!|>h|2c(B zqJ^Ykl}c;yd!KGgc+5(ti)}sI7q?|bm5)EnZh)zi*Vn=J9Y;5Si^S4v{%@o;mb1&0 z$Yv^DjU$}|oj7hL3Ep)|3eO;Km-gx(dGYM$-vBZ**OfOwuI>$hTqMec9IQgOxqZR`TB0vgWj{GeJVqaBEBEDG?6zArbpYhZhbMYCXSQXFG;b`z z=}6wyz0+1^bBm;Bck*CrO~W_gve#p72R*y_fGvDh{U;v)_j>TXie2KH!M25N2?M$w zBSF+pZ7o|zn4*PaRdG<%Oew=X%Kb|_3`rD}`MJD_b%vI&IXzeQ$}Nw!oExMip6#ox zH9xl7xBSsmv~%k^g=Qg@Ditj!r^~*N@2lvlf2oyb=nYU+FvninJJkQsB-d@afrij_ zE*Zi{w8apc7`w?fa-hF|CqCG`!Nu*QOhjJ7P}#d6E|s2_#yq`5Q)3~4HS+`S0>1?D z&K-eklOCL!b;60#Jt-=C3jJ@mrC$7DKlu-nh)cXGR2QDFwqmt&lo@wxzCeiq?3(O9 zk4mjZ!?LP7(AWC6P1MxwDK87PS8r==EaWO%t^!kd5V>FmaBO&_$lHPu(L9#j?O9Ge>+o_&J-#;!)UiaJYx6n!{`2gXnRl6Qr+0$+%Rn~>zh zcW!_Xk>LrAmxlAuX%6bIHa=)ona7bY)?==^28{bN-M*L4-o0z|vEuOpd@xYW@>#;* zPyM2TRRK3iVgTImL+MbK( zzzAypn4I=~*vNaFeVjlSWq!5o;|)OI?c41bZ(>-V|3ijT*6Q((Ci&)i_X{0p3<;I| zNvUl|>OtJ7iGjCOHw!d-cqAGsZW&ISo`HeVw}SHkg4Kb)@#6e z^3`lZ`Gfk_7=cAY0hG8e_EA$#M5-BQEdksPWN@JO8v4Vl2>CCVMi(UmFke0k0PLKJMzK*CZ4+B{u{&|g zqvpiO1gG0X*k(72hy5~PJ zx(9!n_`}LwCMwR5Z2t814}%f${ZbkYiN2I;Qj?X7m#==7+yHlv&3a=n3{wXNFe>fc z*rg@H_ z*QB?7HF_&&#Ct4V!zkOb^A*uN<)%3u0j@uWKCMYFo2t`Ba~63)pNYO3wW1&8>lR#F zmK=j%kj(Nnh1UNdz4y4uxUV*TI8*LjvFo%)DZDXAk;u?yb&RQX?zJJB=nL`zYwDi6 zVVd6*c>FVzAJ=sXOI+690B=IzKSNGNDkhZ~LK%qdlJ^A zFffHP$NHvNcCuEp`fXkCT`B5m&+|c}pll+@tT>ncfZBy4o+b=sY>kKyEP#?U72lc3LA@{PL}^ zFU1XV0}N{f(a1cSo-pp94#QxIN@2(TDv}=Ir@*mdpmi!l8AaA9L;}@xX8pG6P(rp& zW3Fj5U(sJB>(rpn>q}Be!^XluLq}ZoR8nF)HP3rAU!mZ{zdqjM@y|`xyJuDQsw@9?LonZX_%q=MJ2!Z)goj_j!wnu?&js(!4ZVYY_FXc@ClJE}RM6(aL}DzN7;A zICE>V!Y8f-l`sH1T`*fmC?#g~)uZdNPrDRUp{!!KvZXoT{qgFAC~i zmTqaZX*O8-7uOiNWLt1IafZD`F8|n*W6oeLIhC`|RjMTL&LLzfr+r5T?b6?_aHZ|- z+w_$gm%Wkl_9w40pZ}bp=urDk`Y`C{k1!< z(UIx3Higp|glFpcqe|@01k(}k9cz11P|8u-&av~(QKP;W;{`jzk;r%mmr$nCX2x{P z(E4X{2)9J4rS=b1uo9SQp?vf7ch)V^Xs}u3_Ur`v2k=@7c=JP-h>-a~aS>RD zrneEkinV;%8mon7WsrzJ7u_e0FYaoi&#kR;%#{pH|@mF)6`_-Vi|#d z6*G~bc1kejQ-VIHp6bEFeP>UXV+dED?U#9vac%W$_oon&Ye3p znh6u-S%I)X$xVJ25$=JisqP2%dAZY!;mS*t4>P>ay-@n!&wA*7o}}~=vPa@+YHm8p ztWCNm>h}$Of^NQR5*zNscg3vN+A%rW-k4(aq#6)$$yHauZK)DgQ7Bu^uf+EYqKjGy zGi_ksX{YoMDnaTJEN6dXso>9}nnX1e-6!5~m-5wPH}#j9$1%8qCAu$AsJ~Ry#+WyBb@0Fsoki)*i;{Nk%phv%2_ zLY0*{EflYY;?B+jl!nt6<@S^a$c2{D?>2U3ouPRqiF=Pz@$_n?EocNrb$X5~HHHnD z89PN?tQK0;x?W_{e7cvnF+X10-Xwb-^V#nK|Lu2O?ES-0H$bNv3%?0_GGWiW^FeHP z-OKxo1HG1}xA_<4dWiB~Db@}i_z({l2J#)h!s*QJD>qV#A1fP8?l<<>n#}SX$Fis* z_a%tZHVUGy?uUzRpc%;1o|R*4#%@w5e6j1uj2J+s?vPb@YVw(~^oqp;qsH*`z1o*= zpTou_l*Z!GLSDRdUu|-buJ^9jRTv%qA@#Cf25>yOEOqmgPLG!F{T9B6Ei2L{6#E$cB8>%an7WkV=@znB6Y}Wl8?W z9P;z{C%6iNRa2{!V`pEHc^WXT16emJ;v$3`O_#lyG%EIii?PXX4b{C7MmGgVOs~)^bN*%nK21)+%wvu##5b`s^ za=BN?DAQ}OBoQWSkWE-iy06iDrmCp!_NMM3hCSTgb5?>qG-~a`s~|=9PlC5Ms(AZJ z3UPRnM7OA-U48QF z-;5JSR9l8%p{ZMTVYSaC8-JW*W*EiV^*6yQHrVo}(7e-_udg2L%N;_pb@hxoo%+VR z&hUnfok79^$D^1Z7271HvT$?F|fmT5j_8g4PM(nsYlk0}x{Vx|W<+6Q7*`fM#Pnq3=01WlOZaSzUW zHJOl*q6fqxgXM4cgek;PPG&R|7vZgqBx zH;7LQXL$2=ZgsmmRXKAn{(SU9<41(0%p((V(arsKGp1(@{y0MT2lsNL2a??8JPZ;y z9-vI?k_tLJkJ62|&w#Zwn5mJAo7oe{O}=|SJ5x>=%%!KnMwf5tg|PaKr*v%dQnG1Y7my1az(vwGoloUfd8W+_ndvOGTG%;{(P z=)xE4POGLd_|vzONl_IlFGky1n^bfrlW}oEblueLUj%#WaP;e<@W1A0Bss7iXMk8E zDSSVv*uET8s>Mt7A}RSAM#(epvp%a`_hAeZNB+!SNM38|{jl=$t?DfDX?MXv8l_ty z%_BDI4i*&(6Ie~HX!56IWW}lbhD(+a9-JNAi`RTDas~1(xigRG!%#9kZh#XV^-cCC zRXR~|%8a!uuxOndz?&djBqC~gVDUKtXwCOxr|15P6SoY_-R3eUyQAw5mtvpJP<6Z; zsA|5o=vn-Zt-gP1}pEjuETos)PdfkNK0!GMeqS+cczQE zB}+9{?XG`Abg4(W6+n|Uu=VXVLq}WPo7rN5?re=OpK-F$Jbo63b2>R(_uGV><@8Cd zVTegp32Z4ODAmO8)#juEBFDXUYihWIsl_ZDnmOFENzZ_?Mjo9)poPu_YmaqkfN|++ zZLow1C;uaVO`27o3bFG+g62-~70;WXb8Yh!zohQhCkgJdK z%~yr%%|5u?u9s!dC#BRp&kr?>db-8EM|IwYafNKQIb8{&O3F>Oqu3Jhq1g#GTuNqqcW)BNt;wF$(s;2_8VO-$pmHcuoIaJ{YTgkP1wywd%t~ohn5gW04@QPjEN0g*z3iepo-Dtn zy`#mDZRugNdYi%v$4!?(qe6EO*IRo&y*9Z|(#TWYc(rOfd06Zy*Hi;pXvb|fxJo3_ zJb!WEJY6@|WySM^cw?%tE-e{)G5*+Y_m1o*{PPzQTg#uNot1xJdNWbTShoDl1Luh^Q&g{&p~ zxEpagDZ8B>fter5i?1%e-Mp1|P2bFtHRIS)XCHuRj4VV#2jBTzo#MEVolttU1f@IrTcf8sOMto>ibotJc-Yb)n zlgM4@#lRmQ2xn4y6rBhY$4l?T^{_wG0;InS?T*=xIm75;-VF!bcC@=&-y|um25d+4 zG&k7HjdTr?uj|YYw(YziA=k*tRd{ULfNGW&MB4SLu8CQ2oZn({p^90TYMi*z!r85I z0##pto@0S_&fXb^_gTlYT~i-hdsmB%=d5_nhC2?g(<(A->BhC5)=wFwm-1C8yN_l0#XoK98%&T_88AYq(dh=hj;K$jckDagKb;pJ{?;C5!)`RGHRv`^i!k?Fse z8teW>^-DpxN%3wjyZe6nrm@==l4=5|SlF8U`S>}U2FDSdNC zq>nh*bG0vBAA;X{uCNLGxsrXTH(@62 zbIU7?VG318pw&7g+orPMZF~A>YrI=>o!_$GDs4DTWYr0AF25eUva9DFu%vz%O%&jR zJ1N0xJk&RoVWKw6MZ7I=_EhyvIFa(wq1lIbv9HfpOYI-LE~$>o4-trBt#T1h5`9V| zPG4i9wlK*ua8Fhg7Z;sd49#+&b`J&ZD$G#n$p`Ww0lKD4x#&$+OZT&KR?(T|)UA?z zaDOjpBz)WP3@3)WgVm4C)2#ae_{CDXBN5z0L_2#*TN%nZ@UfKR8LP<07wvcJl{5%JZ6ai%i{BP2**F;ze(CZqn%P z1FMG~Y+=lldeiaJPx7BPThn?}SsNvx=e@Gz{1)Z4t&!Xs-4a!dm{}S5XNQm`Vd$u7a^!B3L8+3)O>XHv^ zA(H#f^u=cpl)5io;Pl+LqC&xmFmPsW_AsE5rWUv-d093n=BHs?y62~NyGM2qu6A%T zX$rMcwH|idEM*lO^-0XA*%>Std6Zd@-+ZV&3jXr=o?CKMe1onj5m+$qBIC!wH{K!I zYl!s{+(DNR{#ZD;CF@tPY@7UEG(I@oOK^{^tm$jw#7fo6!JHLTymfouk1CPsfnqO! zEl;2RLcyg^{| zVRO^5AST$Vex+7Vg>k-LvoS^UJ1^m50Vy2Y&}-e9g%A7+7$5!r-^o+9~BY?u<`$Q5=8+vP*lTv>d4b zYmS-7WHJ}|l~Hqh0jPrX98vq%c;x5Qf)Yx*^^K5iH?_~}Bz^K?sxrdz{zsx_6*2(z zOs_1w*xh5sNnH*u>^-4IEtpksfGtRqGt!BY56!+K7hP~f zK4YZeCL3cZWvb5Pl&2aY)t4h}vpG9zgD%C|O~rgp|H;6}bESZeow{|D3(m3HHlUM|n%?Pi7ZxNMDkf|O%u-%R*Qg(g<>b$dntWK8I;)F%0}yie8;&%@ zY?2qfi+r*8bwC*!k?kJQ8xnK{9bv&cu>-Qv}ec5~^y%r5kgf?ZF{T;E@; z)+^n<7#fN3;fPV&c{o*d?8e>}`)I#KBCfKWxwd7{DCr9iWef^lNs;PG%R#}N!ZqF- zz~e}MBO-fP(#FE#>+27A`nURz#+8KF4+%R8*KA4n;k$%J6}$>zqQOI3fe^c#;H{2^ z=_-rXcLmSdNXMiATvij$evOFtsZNDsa_FZgB;iopTxb8=??xFO_Fz~+n-V6&j~}Ot zM2I%li43dCsK0IVCiFh*6Ng!uV)no7u-c(^&VYXvT8!xC*M5iZU2!^L!mr=j7-tb- z+*bRwXKUKCvnpbYOm6U2{yvhL&$`#o>7qi*nF#v?ogt-tj_l3I&3Pn~_N@*#!<{4)tRa#1|r*A=#NR{NHXv|9&Uf}(DnMFp$p|e|cW(%Ay^Iq=s@R<6z^&mBTDhNMA_i;oQJDBr=Z4s=?SYy_)Uz9a?ql4Knd9=H8LHd8KVmp)#Lq;ng^+8jD((hA)?rIPceYzL ztB?og2iliLH9{93WgQwAX%{#~dW{<22mA_fK}LHd%BN#ic*Ik|>MTwUi$2^T3j`!uZCG2iy{ zH^UD4g%s@p{*M}ssTf!45|b^pr5l7U6pd_cGZm8_?T@a_2gZ#l=TPVc9-+Kntt&bt z9!j77Nw807!k2ang@r<2jN2!#d)+{V!%}HM-DAiN~!z z#n~}YpF%}SPhvm8gRqeyVIN6kr7(BpKrw|P+nTqhAw6y^kJjU|xKnh^^7K) zJuc$rH9@P6-tQ@czbmvR&ZsHKXjpix8|9<QF*w z!+Kq}K}_eOJLWg*RF_w|XS9kYK*_S-=W6KN(2~sYl~N@~z(3S3y5X{yw|* zVgPR9N0+CkB{`8R6KS6KZ7kuTb82r^o1Ca!kBr#;UeNFu$LY4OUr)4R%a39QuPshk zd+TKQ#Zc5-`RTbnsnzOO)yengurgpbulHW$ROnnQH0yVzPNMWYQw7fmB&+y~vH9O? zN;jb1R)VZi=BV3^O|R9qM-_jfQ`8>lbyrj57dl$FIv~_ZZK`<5hIXj!*H7Wo zoS2i+0J%l}wBPPqvI~Ki94_Q(OF6!6f}&^LeF84&FpQi^`FeOrYvO7?L)dgtyqJ|j z5+-3xR%?Q>YLA>yq1@-7oUx98-gF#$D=zgf^I0^nG6sRvn{n6ti}S_g>d;RE%psePMo|#&6f4KYjnM zqTFC2;~Xd5WL{hN1JY)l515|`a+DTp-aJsmX{sSk2z^}aloPG{zCOcVnZfZM49kVd zyz~+YVV#k%l%lpV|94mjVo9FZdcJS{Szac(Z3X>cW{Mu_&oorqm8P znI%l81(rLLnpl?dq%%4P`zSBGNYG~^h?7644)rJY2}F5}`=-tM6vi>#+SnJn4G}RZ zdGeBsWy9$CqkS$fJ$t`$F-*>pN*tAbw&vO3Ti}sa5r6Lzy62WPY5h8kvCwoQ+schr znpN{=PF&NV{21{7qal5eB1Dgzsjn3@H$ianLdTR zfQXy>$SauWAdDCC#IH$P_9}u z?ZA14uRqfx`I(2R+DIEBHW%5={HkNCSv%*Ey8#%XTp zYysnhIy-WCo4auFaB_12q7vRN=1_Z>JFNxG+SW;o{;=&WJ*};!7`;BfDz~bO49vz> z!50qG_Epn?`r1Q3fOlf_e@ylA^5XR3<#dKybMXiX z32|{lxF8S?kb}d`$I0E?o5RVC;V%sjVQx^kt&2P204qYHxrMWbyBIy#{x8G88mO!{ zNcf`~asNxPo4X~KB6!0Gb_8VLf^c&Sa&SX9AVLV+e|O5!MOF1*lK<9!j*kE0c5|2Y z{JWih%k8G);{xN-g1I?+z@ac%PneTC!(WZLIC!}Ivn>xd7=rZ6RtHNfF3`+hBlZ_R z;*9#g_z@1Xwsmy*#f{+ni^~%FFCG^UxWlh{ODGr20p?*C#x zqUOIe4mR=cIsckhi1tC}2|sj(dLUX-d?-ebFi_ai8ER`O{0j;^5a6}4Fz4s6;Ik0q z0J9K>kU1|u2j2rLb4!SYpam2n_?I3hMU9(&8=Zv zb}le$dRm0zg*9xwVGfTTf?Ayr$FxDMLVSGxtTXsW9RzIgSEq$#;4pJ{XSj~Dvx6A@ zzaknC%jVCT5LR)9nj_ZDFHV@{pCu`6Zp{TkuK$_qzt@0q6kbmM2kF0f{*sk~yL&mq z?bYGx7IrYG`~M*QzjFO0uLbT_Ztid&#sA6d|HANlGAe))&TyYUZE3??{~!lj+Fy%T z*c^&*hZwz^xhD)9^4~#jj`++WWtz5zAkEUN$h$7&b(# z12_GzF+pr_zwJis2jKepyGw}8LRb+oceFiRTpVm+@W1={OZML~{7w8bUH(U*zw3X= z$T++BfNRReUERy+-#7KYaQu=}v4w)u*!kZY_m4U*L{I;*wS#8;B7yH02>7qJA933M zr2lx}KOXpx2ma%M|9Id(9{B&C2mby53v&V=(!9Wrr#Jh6js_Up>+ELZ>_W>61_BF8 zE2<)*&A=o`iTnV-M#S`Ta0_to@KPd60T_S6hAENd0JLAB!<5LP0PbJWz8t^6%_?%W zqKwRA4J~y!MHN{v_zw)+d*uLibVKC^07oZxxR$&$Ef|MJi?I&If8GWoB!vKSbEun( zl!k`N?^w8hcRig&1oi>I6bC}rzvKAd&~96TAAi9pI$DrN66ykX2eBE51-#r{5I7aY z#NZRGC5RER+3ax8KoHL(=oY{69s*nc!ib`^0SmwbjL38V z+yM@-bOI@EfEHLU{XfXx{UxsfN?CwXwqV33Eg1Xh3^)SjzvKbL7=S!r`rEf|R=kj3 zBqSU#N)~PT=H{3Q44_T`fUB1`Hy61#H&=OJ;3gP@-s$wWyz?t?!kmEgmw(e3bHMP{ zU;t?B`I}~u0su{606@0jVh%U|)eZ`HMYgg6L)MGI@L@3Gmgqeglxy_cZ(tpw9>|{q z03Fa*Dx(09mJR^-tU=wz|3kkKk=y_B+y9p5kNYv$A|J<406+G08P@-Wt~m!p5ajY^0+oM_DZqFi1PS5Q}qWzgX^naw4$ z(+39e9ZpZaRX=7^2!I$*5#*NV6m5xqHCmCMWMh;dY_2va$wZ^m%;U(l+qc`|UB1qX zDd6?3Zz?dJ&!q)Ey@I*gpcX~%H3*4?mF%9h(X{mRT9p#{8uxy%+Rd;#rZWD1XV4Mj zov%yV>+Xjfk@v)0nlI4dJJ(^t$jk2CCby(Rj4Z0%Kiz7x=CgQjyE>3ZBzeKFycOB# z=B>*LLpsWSqqNkH+cFb{Ix2(0ecVU(@kOP9nO?C>T>6fVbBrk7Q+TW73_Bz>uc&He zsHyi^Ki0*fWdr%h>f>57y`_eC2j9QI*NA^!sr;yu)Q96l7W#{%34AMx3Le8I(MCpV z6kLjFa+@Zyc{=Za0nOF6ey%7;lPOyt`MK2Tg~L+uF^xv7IZL)knL>GP1j>rmJ{@F) z*h-KnJFp7+hyrEvX_{5(Ub_J%%*2lNY8SZ&nTkT_a_6au>-#~zLW037=2Z{+lg#Qq zzQ&gY{EN7rWkwI;wT~yeq(k{g!Mh`+?0A-JmcK6gSoX#Z+aj@rh@pZ% zspo8Wequy6c`*M>uJ{hp4FJv^CNRtx85s!`jLAm&J%3P8kZ30@?yZ9rpvm*ey4<;I7$I*(%qDWQ==sp&$h--<(vMB^Q?Dgwb;XoOjaEp zZGSYKDElz)QRi4sN=EKu&#`(I_WDy+dWYCjzecPAXP?qRgu$x!m+}y{4$7+I!)LSn ztTS2DRb^ajXlcqn8mFu2xoWDL>1IcVMN(VgyvwP}i zc>U2{s0bl z<;X`oCafqa<<@B`zaEW-(5$cOq6SWp$wScfNG6CIc6d@8ttH(K$xz6qFqd$6F2%@J zNV+>2Y*Uq^f@qvv(!B4JDYrHmw(dk3)p`fdaCXO%a;jp$DcJE~y+!{Yk z&AVUniZDGRFoO2EQ%*Kt_|QnQ11aT>(yhSJIQg8jko=LogzfGd;OS7s3sF0NQ+7EQ z3z9a?DFsW?ceY|06{Fa)Y-MY9LgkHfV<%W@yhz-xR#R536_I+Uq-!36=g-QP)9v~x zecv?{I%=%hKAUC9xa(?e%)g^sq8n!W^r2DeNqei*Gc;D+N-Xu)_FqWKh)p&^kb3)U zM=E>K8?iQpBM)2j6=QRqHB)e|G|J=VS*(JXNBZPkX_-laaDZX{jFg1RwHb?(3$aI> z39}sZEo#MQ8@*wsg9;*qlf{f)qZWN738*5GT^Q}~AjTP1@!9@L#bXA;)=UQrm5MKA zhO|Y+^_q32elpcRlKFPN-dmXtr@yqqIqNBm*Y8l}MpofWe4$CpL~d{vdnclurt=U5 z^104zr06^gGJx&;>Ajw{ZY_@VxAOjP(_dJV0gI!E`_t{W(P3;F5kq@cS#W)->MF)t zn#iMN!-OS51`oKaIZ72SxWFaHLjWKG`_Yh5(SH9c1o&73hL#i3q4MAp(MxHXyFz&5 zz7k76ubDt&$n9GBwfw;CUjixqX_-pJ(Uh7J?huzga_KZF{@Xm10p5fGBy@+JlZGAg)pA1YML82u&~^8lZb?-LI|>a$KZ&sR*~QljP6r)cum)0n zzY63ydclg9g#A7UsT;?P6?Ma1+VK@@5TxX7^7a|{1HImP2(boC-eEE{qGc~8QVXv; zOQ}I%qx^OEuGWdiLq&FMf)%w$AXOE=c0YxK)Ivd*FFF9Guk_P8xaw0e#zRcXyRM0I zpK7D-KMQxix6vYLs`{}~EtR^cWQ5I$Y`^<(zdwe0FmH;b0c-Cy>V{y_f;iC_o!$K& zA#Hl~GB&j?&C_$O>;t1*;lk}uBQ&1*hk<&`w>;EThTM&&Gu=ObSV=q*;SdR8<4Y>B zH!|retAuP3Mg^v7ERdP#4PXjQ7DOi$G~N2TkM38g^0+&Hc*)f;^k}3kCNPawN$`Ff z>x;|Da{l_oLfc`Kp9w)imOpg6+C)8C!|Dp0T4G(uh6>rK)juV!TH6?HKneuz@IqSL zlKh+XM+J3r``x`(wACmti?+Ax%(6Ed#HqROxKBVL(YFh-rhZoIjjGw((devX-|f)I z+bgzX<8OLcEM6Fp3_Tng$lu9AT`@wRJ~G@;R(E5v6MlS4pv*U5bV0}RRMCC6b1-Ymy9VOLQv8B;DyLcOqh$hBZqff|7NW z7B>|?n*==EYWk}E!jmRsgzw{R@b61TJ2!-z{oDfmMJoBh1K(vcD63AopKrbBwwH={ zrf{}E6Nz1YAfQ4Zjn}%MD`r`?@Aty>D*^s<^$4;U)N z6rKL8lp*7xDAPH|b;UUJz?s(cPzYYbAy`6J+CT$W2Ji2c0siF$=C7wd@Sz2Klp#Q@ z48+2~H+N-##MOL7U=isuZukg1bNq!&Z%T`C7JlZrm; z#0h`h;i`WD(H`aH2`P_mr{96m)Y1Di69yThv1W;Q-Zr*gOW9p*jyICTfAaS6`vmpzlRd$YB2tg>KkTBWN`yv;#!KaH9<7_fPz>!JNz z(C~q7NJnBtv-Z?x+5sy5&jHzqCNI1Q;bsgSe&2vosc&Fxs^6`IR#EYtxHG+;`qSu# zL8F9$*oxVzL|MkKWZ?SvUeYR)WnxBYGqPpSz9;wd#HVw9-mjSJ`aFkOU9FR6s7AVh zjlsM!&$YyT(#>GKV|n#FNhUovgcdnb^%GW{y{)@bf6n(jTp+N} zapGk@V;`2G%lv11k$d=G3&>4uYJ+75YDzmPNyzS$IumG?%Xr-qzwKC=_pD`9leaeE zUZnf{RHSp>K za{>Cxz%rwqJXdQD{EzRH3^#T1&NR7a_`fIkmaPa*K&&4bsZQnFXwIbBxt3Qq$3`+Q z?Oqm9>qs8dYUurES}-bxXBw`}!>3wYHE*gK9!bo?noJY~xXjYH3Mr zBJ;+Prf?O9tgRp}YS_f!#7lhar6REPvO?t}2-!;tOeiml;4!!?W>+MJM{;zZ(=Y ztai75;Jxs5mxF%$u2KMB<@4rUor!k0AaCB6>eJ2ory`qA5 zVUBTar1#7(`G{+9UVoB9<-wca8{k8)@%?chLZogkBo11dw-Fg>ef#L%O9@Zkcc4RT z7JK{lmnFUo7tB$nk+X6OlzgslojE0z^l^-d+$#ip!*75WN=~Ab)j!no+CkAmZCsz!*30WO(xvd~R>`e9tyMcGD-R+*uqyU&{}@la2-mY3jqFg@+l@F(ik5pI57 zPTHwe5g$I1oi#8vT}t;dyqC+7QJYbRS$}s9GyS5`YGpdz!cfc}_!e(V@m)nrvX+{z zMKrTP{_QB-W;1d3(~{v24B9?4ceU5UT-T!JloTpCgIO`$5^p>5*FrA@rjcDc#C;tT zjw)gcrl1$O@+&5<3-7et05S4Q%I{X!^lKIA7F0j#?`yfT6!CJXglW8PX_{?hSx1&*1CyE^A#lE`t4Tayj^2wUuNmX^j~h{_-(S2z4(=CZ=*~A2j=&Hn zsq~GG(F^9VfIMlp8FysaJ4-S;`gB+*t2*P?-(h^=B~}wyxprM|!fzwS`%vzK^3@50 z(!%Y8xY=ef26rgkX}a;X5f0C{@{3y%1)`A6O>dL@rnb3c(RHC$sK;zk#>SsGn%@e?1SQT-9!-9CFNUteRMC-S{(cVo>oeU#2V z&{WlX+;GrzJX(?1HbkEmFIsH++h-m@p|3^v7356EA~wb_XVxM%r5NsIx6r-ef6-(< z6LB?tc!wQ>Jc`#dV^IQIV0Rg|yAq{eNN3#o<2*e=(0!YeRQr(rO&8-h_M4(WtNE#3 zToNjs7oB}s)fh+D?u_A$Gb;OA((~28Jb)N5_?>9$-MXsMk8P>%md!M!40+maX}$!YQvcg-&Dm_w3o_MFHsdwso(^ zN3tbxht<#EV)!htL!HMwYt|exwGn8n zVGM*H?|V`5q?yzcIot&*w#hQb0NcR***NjsRP%J(*F?ZpBByZZw zj>kgU7D?hf-0X>7($s%FeGRKQeWu&i_c?j1dC6;*&5SS_{a9-!UN5ZJH0zJQ3XV93 z49%qugZ2ai3)Yn~wiE+a(`JvWt-c;rR}@UnaMp^aLJ#RW>tbHtxaM zbLcmXX4ZCgqeHsotpkpV3_};l-_GCs1+asSsck;%$Z1<>xdk6AStjKb>1vEFm$yr> z8=JD(F!S^7c}(o_)iwyxIGdDtawKTVc^`fG^_88>PnB{AcXBh7fvD}a>4If4j~e~} zWm?2-w~7%`!f)D?6C;cwZSh)G=w*R)9a{GGMG)>RWYa4@jD9q`4oV+8)FFv_L zv3}xJX+qTNI#@Q#kPw)BJ)MlaKCcHcwJravHdwwwIc)XPh_qXAm%j^%ho7?!y7@y( z)_&x-r7SYB=yM?3pOxKK5pb7oYR~F-)7d*cSM&smGOV9Z>yeF|E)piB5$o%>cpsOK z#E%nuSLJ@q9v8f%9Pctb2Ds~AK;7S5=11^Z`(crP{V%pPv#G+ z3#e8Xl9|Z6L))tEig*P1sTVfV>L+xj*Akq#;GtpE%a~UnkvNLTXi$KaF+%%jIr5iI z0pXC{XI(LvBP`jYNlw4IA|QS|W#e$&`9^C9`VC=~$ zsQPBRav=T(JW^q#HL1ayZJlN^y>gZ)A%%YGsclY}hC7Y3d~?)@&xF$p^79mrPyf5q zb7DxMh~9C*ji-gMm(LsCU+gP$Lm5CqTti`o5GS1TBQ1Bh3I?rS@}Mj1 z9d^Y>v@H}_3^J{ymD8S=?J8qbaK~=?Wj}z%HeW1$f^y2qYFZr1;$A4AYClbcz2RN` z$8(v76)eXJ*tdi1KqoPXUo;LQ@a5I<&lCGEx%j7X@aX`2|2@oK=JuNU&o-z3*AbJT z%jj(CQwH+oO`0n|cU%o=sVQ$hnHLmD9<{Uk_}w;@a1CsOaDMniF&HNxr^Hxiy47z$3RVEnB_Nt5#6~1o zWzhKjSsWRoB~PkkVO{huL~#k&g%A7#TOzHM#Y0~2&$GKIhuzcSgotq<^x~TepG3_> zQR_yxzX;y-=iyux?y)WV{b> zEb~B`cY?cI^fVT;nfv=p#8(lgiFZ)vX|u!eZ=Sx-Dr}7GcY}ddP86dtYGGUin5XU` zs8ytiWJ2hOVxRdj%SIPu@F20b<&W>j@OY*6dO24)*c$QfEm#9Oz!&l4l6pz7gJ4(2 z0OnUeFZAwogNa9A-ii@$^mt@#Gn#vt%!0 zKo`LxZTT+isiAyxGu0k6vw5p|R%}Y!g~s@1QaSu`cPRPy&JkS5 zB3bhn?eGp;SnX>CF)}vy`;E8C|$^0sqx}*9mM2K6Fx+W4mF7jCh!Sg^9buGWwM!S zNyn#$&p(iziN+cvwVk$oe)PA4lu2_>l6TqJgZP+Jg*0Pn;yVPUh~6+xdoylP!WxbqQkbb(W>kFDder#b3f0G<`ovt@tFW)4q?rT_!cI zPcnBN9^xUlrBQj$&mcnq49V(%p|z74`FaZRO8+Kp3P2bXR7GZbqS)0m-QeL67LA4Q;v(8Ffc`bEvZBmo^`Qnx@Cyv#gZWp#bXxH=E1y^ zYlEY0C#@+k>>IpghxhD;75d1u2}~fg0CWgKus7R~nUdtG&V3pCj8;%<#Ch_f4XKiR zdDioSK|^IemY*f7*`C+<@tG7$=yI!WskVe!oFp~2)6yHO-Y|CV|3rK^Pf}*g!;5bW z&B6bbY0=MOBLe}+Qsl~=5<;VYF@o7o%l;?3n>91c zO`%_q#+7WYZ5`ptYNxRb6U$!FFY`5CdiAEg;!cK+D~=qkP69Nw9Xr*s z2aD}~Qq52ZOV4oJ4NeOyJKDUtOtL9s$K6f4L_dof%zKX?#CbLLL8M6m*!svj^$nBc zOlZe{;QJ$R2x4|vP|JlxlT3KFl~+Ru;6&-OePNt`6Quv*P@vh;h*#|Fip7R%!t2ue zs}RNt3ge9T(RA*CME4sf{!FYP)xSW~xnt#~q@vwpX~;Y%J}1@_j#4LBpCU5;0u-3b z9mliU$IR`tQ~kJHjCE!XS7lz9j|u;U`4A9M|H~Txhxq{bFLSg1nD6X``ldI}|54w6 zZZuHMxcNA%{2YQN?H4?N?!0^z()n9nW#Sq}_=-j=^zuG^0p8ql(xQT*GhQA1kBTkP8GZ&Aw@~xoxo85XviTP@W5p^ycz% z_b;!H*~qnom)CiSCIoFEIHa$QiqlqYs7kg3XWKkX_gcSN%!qbXcz<>achRLh-{l+?A|<)#&Lyd0 z?`E&V*+h3)3mCxGMG||7_V9~JUF2~`{XAWYdd@NXh&0H$Ml$e!8TJ{@J+EF>do^e} zk85fg2^VNHkAF8h?Cti+4l`b|g30buV`Y+uDXkt!(6!UFCN+Jz4`$}nCp7b)fGoR6 z^qipA^))kij*nB>`zdesZQXSSODx?~MCK4Wz2z&t#S&x=pY46J!eUS(b5oiPD+M74 z2KE8i-tP`wo|80loR!|Zs%gnMaAjE^$uAB~Cuy(08&c?$%ZKhg0KAGnx)<_tigF~} z2gn%ge5aP$`yUyvdVCKXbs8!I9fP@GZSglDh;fryf2y5b{fzzste9#OPd-@=xQERK z^TQfyC$t;VPk*tOMiWz(E`j|EGqAQWmTsSiyMPU&#S$VJ&M~R@eftybiH0(E%-Vsi z9ZEIb!?uCGFJ?wPWWgVm;?esd114M(Bs#$RmVSPGMMgne>~EJz`N#3FH7R-X$cG}-*s{*43>R--9rTEGz5c;UqGFoR2*p%vOZ zqjbd~fUb$<_27*87cd0EdUFW0Hsyg4Q}uclRw;0ctnV?#HGQ}qT~YhfsGG)Pv&w|k zghZc%R`dR*13APSFD^Z6)e={JMfzIQ#tiM9Eib>91e5#05Kb|9W;HvJ@=bc;7vDDh z<3qMIdaO#ZJmrYxBoovgIiXt#M-HFPNGQ^EQg&y0I6FZ2X{^H;%hs47LXx#WxN}%YOnu25!&9n(19@w-Hbe*f$ znN+*YdCd0R!fY7}H;>5lRx2FuOKt3REFOYTXIv>XnwEq>lAr){}c9n1P5}#dW`M8mu5$8zv~Q1Owc%i_}a5-`zVB zh{)-R3JQ)|eIKvv+q;caUR>) zcK>{$!MSCEMxhXntR?yuf8Z3_dk(yU8cg%Lh}MkFk`g$nTn073PTKeQ2mQv2k0+^M`r?5u~D6;OQ75YhEib9Dc7-65c=DU$${+d|99EacD-<14Bf?qBQJ@Dm_{5O{VpNRN(QeK)&|EnQhQoE*q*W{&5 zA_2r8fZG{LZPH^U4DAD-r^uqWq~s@s(^dq?G>zc$`GVvHiCFlv2)3 zbA%*ZM4CFPxjfB2kb>SQD#?#nvZ9z+M3jcJAyOMPrG|?320Y^OAOPLV{(g||J8TF~ z4@ffNpb3qYk&}u7CkJaKrDGSU`5D#QgxfW1EcztGSKgQBoQ!?)ocy@7d%7_ABaoQ? zN*;uzuBag45{PPo2sjV#zj-2EcCKH%WL3fX^w=$hnjMo_A zJZV({UoKPumzqctlu%|ozmYaNn`nx+DfP;iZVe`g`1V1xN5!rtHWSa1sVgm8+2Ab= z7(&9TDmrk%I!-M4;Yjr3iWJ#YU*Y43G%y6Ttt-TFHaZd)g3Pp6z`$}!zA=+iXO@U# zb}N_}2f)I3N}$+2$vfJXqi>=tXekEg8QqW1*IxDs`#PHCZF1&D@;wGwKW+HxR^l+G z>~U2@go1KQO>lE8<_{RLWo6WZqefiSlGuqIPQ=d!TzQIP9;#l-H`1qDmW#)%wP#2P zhxZ*y!@s}O&2OEL>Fubn#xVWp634rB(XqB>Q_?`MDj~1mAn8xTjQ);wLOl^a#;ALm zYQ%Dz*QYp9E$qsd4QA*!mfCJ;fNuQcE%6fz7zpdILqsaV|8UA@glTf)cBgAov)yczOC>y!(+MtB3XbC zzuyCoU(@p*6q}kT__`lJMH7>hpTddzofWRw&4DkBgwgQh8j^eWPDJhSmkDog#lq3I z1ckQj+5zMa*d77xmV#TA@hmRur!YV^3A7E(;2*|IJ5EugelJXi0F)s=&gsdnva;S@n`>ngCK}?g_ zz+10xI1n80DVc}b(^hvUYh+6#l}ux7Ep1MbIlyfp%@iW$s~gP8-*AUJrlDUP0a`QDnXo@o8m@i0{yz{Mm|LL~t&pcVw5n2% z?b>8T#QbQn_e#5k(N;^h!c}!z1Q?hg{pBMqR6y>#eYMT4(-+U59Q?L8+2{9TVZRVD zOpslTA9w7kxQfuiR2_sneCyTRkXDsT%A3ix$ z?Dt@jVkf0k0rjQ0BDffyH6uKf``1kpd8J4;zFqbL|L`*Sh%fwFitpRb%xEC z-Yu2bX7W22>((9l#+X(=0JB86d_d?ZG4dEOo?cS}Z0%7jE|rmKrO)+;0jEz5-TYOw zf38-H@q2`soF*Sitxc_}BP8=$N{IJ6sy4+7tXpP`@1k{SUNILXmndKGG}1q)-Ulpj zAu#uOhh`?uq@o>Y#A@IcGDNWOksTSbLnGp0PRqj-&=3dpMZfS0nKZt7 z%}^raIyD`U5F)3ND7oNy{R zt|_UZVbDk05{zX@8#Q8RE#|Y-x-@(2zu^u-Rvd05HhL2PLGT}^E6R96ci`8lL#5Rs zl;nD}@Hb)oW}jiQ#bcRGkqt5rA04Qiz9yo2_Km# z$Wj15XBd4;cCdy2lJqM4uHFP4MJJ_&&XR(@hNLQ9r67w+_E(6~w?-^~xo*Tm9I0!Z zPh~Tm^&wybTJ(7fn1ZLs(3_>?b-*!!Of}Mbb4KNiCV44#)*}1XL8)KPh$AIIIYCXY zOC6KS$Z10NHXg|d^fspJeT1%bt?lUm;JexGH3F7DNfGM!XINBo69-a*t4ON#%xTtN zulc2zW8^Bo$-ez4O;TaSrgO$eQrM(XO29~=On&SP#4W;>uE{gVf8J4)wA0|uAw}Qv z?_(&nt@odmLVp%|vMh(ZwlrfwjxLOpXB#!LL6nYA4&;$#Y@v-w z*KRM^3I}XjhrT2^rjDpgY(fHkJcQp@8po7seK;bQbq6qSZKT_P$2lkoC>^@^+yUf> z^kEwAs47*`MG){(l8oVtbA*e73bm_>d_9JtLrnZ*z8+Ij&5KUYKjnB2Thgr2mk;8 literal 0 HcmV?d00001 diff --git a/pact-specification-test/src/test/groovy/specification/BaseRequestSpec.groovy b/pact-specification-test/src/test/groovy/specification/BaseRequestSpec.groovy index 46d02c9f22..0b3a06533c 100644 --- a/pact-specification-test/src/test/groovy/specification/BaseRequestSpec.groovy +++ b/pact-specification-test/src/test/groovy/specification/BaseRequestSpec.groovy @@ -18,9 +18,10 @@ class BaseRequestSpec extends Specification { def expected = DefaultPactReader.extractRequest(json.asObject().get('expected').asObject()) def actual = DefaultPactReader.extractRequest(json.asObject().get('actual').asObject()) if (expected.body.present) { - expected.setDefaultContentType(expected.detectContentType()) + expected.setDefaultContentType(expected.body.detectContentType().toString()) } - actual.setDefaultContentType(actual.body.present ? actual.detectContentType() : 'application/json') + actual.setDefaultContentType(actual.body.present ? actual.body.detectContentType().toString() : + 'application/json') result << [d.name, f.name, jsonMap.comment, jsonMap.match, jsonMap.match ? 'should match' : 'should not match', expected, actual] } diff --git a/pact-specification-test/src/test/groovy/specification/BaseResponseSpec.groovy b/pact-specification-test/src/test/groovy/specification/BaseResponseSpec.groovy index ce13c8ff13..1abc52dfa6 100644 --- a/pact-specification-test/src/test/groovy/specification/BaseResponseSpec.groovy +++ b/pact-specification-test/src/test/groovy/specification/BaseResponseSpec.groovy @@ -20,9 +20,10 @@ class BaseResponseSpec extends Specification { def expected = DefaultPactReader.extractResponse(json.asObject().get('expected').asObject()) def actual = DefaultPactReader.extractResponse(json.asObject().get('actual').asObject()) if (expected.body.present) { - expected.setDefaultContentType(expected.detectContentType()) + expected.setDefaultContentType(expected.body.detectContentType().toString()) } - actual.setDefaultContentType(actual.body.present ? actual.detectContentType() : 'application/json') + actual.setDefaultContentType(actual.body.present ? actual.body.detectContentType().toString() : + 'application/json') result << [d.name, f.name, jsonMap.comment, jsonMap.match, jsonMap.match ? 'should match' : 'should not match', expected, actual] } diff --git a/provider/src/main/kotlin/au/com/dius/pact/provider/ProviderVerifier.kt b/provider/src/main/kotlin/au/com/dius/pact/provider/ProviderVerifier.kt index 0bb324f629..4365ddbda9 100644 --- a/provider/src/main/kotlin/au/com/dius/pact/provider/ProviderVerifier.kt +++ b/provider/src/main/kotlin/au/com/dius/pact/provider/ProviderVerifier.kt @@ -372,17 +372,17 @@ open class ProviderVerifier @JvmOverloads constructor ( when (messageResult) { is MessageAndMetadata -> { messageMetadata = messageResult.metadata - contentType = ContentType(Message.contentType(messageResult.metadata)) + contentType = Message.contentType(messageResult.metadata) actualMessage = messageResult.messageData } is Pair<*, *> -> { messageMetadata = messageResult.second as Map - contentType = ContentType(Message.contentType(messageMetadata)) + contentType = Message.contentType(messageMetadata) actualMessage = messageResult.first.toString().toByteArray(contentType.asCharset()) } is org.apache.commons.lang3.tuple.Pair<*, *> -> { messageMetadata = messageResult.right as Map - contentType = ContentType(Message.contentType(messageMetadata)) + contentType = Message.contentType(messageMetadata) actualMessage = messageResult.left.toString().toByteArray(contentType.asCharset()) } else -> { diff --git a/provider/src/main/kotlin/au/com/dius/pact/provider/ResponseComparison.kt b/provider/src/main/kotlin/au/com/dius/pact/provider/ResponseComparison.kt index 8f7924f3b2..94653ab3b9 100755 --- a/provider/src/main/kotlin/au/com/dius/pact/provider/ResponseComparison.kt +++ b/provider/src/main/kotlin/au/com/dius/pact/provider/ResponseComparison.kt @@ -1,6 +1,5 @@ package au.com.dius.pact.provider -import com.github.michaelbull.result.Result import au.com.dius.pact.core.matchers.BodyMismatch import au.com.dius.pact.core.matchers.BodyTypeMismatch import au.com.dius.pact.core.matchers.HeaderMismatch @@ -20,9 +19,9 @@ import au.com.dius.pact.core.support.Json import au.com.dius.pact.core.support.jsonObject import com.github.michaelbull.result.Err import com.github.michaelbull.result.Ok +import com.github.michaelbull.result.Result import com.google.gson.JsonParser import mu.KLogging -import java.nio.charset.Charset data class BodyComparisonResult( val mismatches: Map> = emptyMap(), @@ -48,7 +47,7 @@ class ResponseComparison( val expectedHeaders: Map>, val expectedBody: OptionalBody, val isJsonBody: Boolean, - val actualResponseContentType: org.apache.http.entity.ContentType, + val actualResponseContentType: ContentType, val actualBody: String? ) { @@ -76,18 +75,17 @@ class ResponseComparison( .groupBy { bm -> bm.path } val contentType = this.actualResponseContentType - val diff = generateFullDiff(actualBody.orEmpty(), contentType.mimeType.toString(), - expectedBody.valueAsString(), isJsonBody) + val diff = generateFullDiff(actualBody.orEmpty(), contentType, expectedBody.valueAsString(), isJsonBody) Ok(BodyComparisonResult(bodyMismatches, diff)) } } companion object : KLogging() { - private fun generateFullDiff(actual: String, mimeType: String, response: String, jsonBody: Boolean): List { + private fun generateFullDiff(actual: String, contentType: ContentType, response: String, jsonBody: Boolean): List { var actualBodyString = "" if (actual.isNotEmpty()) { - actualBodyString = if (mimeType.matches(Regex("application/.*json"))) { + actualBodyString = if (contentType.isJson()) { Json.gsonPretty.toJson(JsonParser.parseString(actual)) } else { actual @@ -114,12 +112,12 @@ class ResponseComparison( actualHeaders: Map>, actualBody: String? ): ComparisonResult { - val actualResponseContentType = actualResponse["contentType"] as org.apache.http.entity.ContentType + val actualResponseContentType = ContentType.fromString(actualResponse["contentType"]?.toString()) val comparison = ResponseComparison(response.headers, response.body, response.jsonBody(), actualResponseContentType, actualBody) val mismatches = ResponseMatching.responseMismatches(response, Response(actualStatus, actualHeaders.toMutableMap(), OptionalBody.body(actualBody?.toByteArray( - actualResponseContentType.charset ?: Charset.defaultCharset()))), true) + actualResponseContentType.asCharset()))), true) return ComparisonResult(comparison.statusResult(mismatches), comparison.headerResult(mismatches), comparison.bodyResult(mismatches)) } @@ -134,19 +132,17 @@ class ResponseComparison( else -> Matching.compareMessageMetadata(message.metaData, metadata, message.matchingRules) } - val messageContentType = message.getContentType() - val contentType = if (messageContentType.isNullOrEmpty()) Message.TEXT else messageContentType + val messageContentType = message.getContentType().or(ContentType.TEXT_PLAIN) val responseComparison = ResponseComparison( - mapOf("Content-Type" to listOf(contentType)), message.contents, - contentType == ContentType.JSON.contentType, - org.apache.http.entity.ContentType.parse(contentType), actual.valueAsString()) + mapOf("Content-Type" to listOf(messageContentType.toString())), message.contents, + messageContentType.isJson(), messageContentType, actual.valueAsString()) return ComparisonResult(bodyMismatches = responseComparison.bodyResult(bodyMismatches), metadataMismatches = metadataMismatches.groupBy { it.key }) } @JvmStatic private fun compareMessageBody(message: Message, actual: OptionalBody): MutableList { - val result = MatchingConfig.lookupBodyMatcher(message.getContentType().orEmpty()) + val result = MatchingConfig.lookupBodyMatcher(message.getContentType().getBaseType()) var bodyMismatches = mutableListOf() if (result != null) { bodyMismatches = result.matchBody(message.contents, actual, true, message.matchingRules) diff --git a/provider/src/test/groovy/au/com/dius/pact/provider/MessageComparisonSpec.groovy b/provider/src/test/groovy/au/com/dius/pact/provider/MessageComparisonSpec.groovy index 926ca929d9..7a5ce927b6 100644 --- a/provider/src/test/groovy/au/com/dius/pact/provider/MessageComparisonSpec.groovy +++ b/provider/src/test/groovy/au/com/dius/pact/provider/MessageComparisonSpec.groovy @@ -9,7 +9,7 @@ import spock.lang.Specification class MessageComparisonSpec extends Specification { - def 'compares the message contents as TEXT by default'() { + def 'compares the message contents as JSON'() { given: def message = new Message('test', [], OptionalBody.body('{"a":1,"b":"2"}'.bytes)) def actual = OptionalBody.body('{"a":1,"b":"3"}'.bytes) @@ -20,7 +20,7 @@ class MessageComparisonSpec extends Specification { then: result instanceof Ok result.value.mismatches.collectEntries { [ it.key, it.value*.description() ] } == [ - '/': ['BodyMismatch: Actual body \'{"a":1,"b":"3"}\' is not equal to the expected body \'{"a":1,"b":"2"}\''] + '$.b': ['BodyMismatch: Expected \'2\' but received \'3\''] ] } diff --git a/provider/src/test/groovy/au/com/dius/pact/provider/groovysupport/ProviderClientSpec.groovy b/provider/src/test/groovy/au/com/dius/pact/provider/groovysupport/ProviderClientSpec.groovy index 475ce7041e..e01851ccea 100644 --- a/provider/src/test/groovy/au/com/dius/pact/provider/groovysupport/ProviderClientSpec.groovy +++ b/provider/src/test/groovy/au/com/dius/pact/provider/groovysupport/ProviderClientSpec.groovy @@ -92,7 +92,7 @@ class ProviderClientSpec extends Specification { B: ['b'], C: ['c'] ] - request = new Request('PUT', '/', [:], headers, OptionalBody.body('{}'.bytes)) + request = new Request('PUT', '/', [:], headers, OptionalBody.body('this is some text'.bytes)) when: client.setupHeaders(request, httpRequest) @@ -102,7 +102,7 @@ class ProviderClientSpec extends Specification { headers.each { 1 * httpRequest.addHeader(it.key, it.value[0]) } - 1 * httpRequest.addHeader('Content-Type', 'text/plain; charset=ISO-8859-1') + 1 * httpRequest.addHeader('Content-Type', 'text/plain') 0 * httpRequest._ } diff --git a/provider/src/test/groovy/au/com/dius/pact/provider/groovysupport/ProviderClientTest.groovy b/provider/src/test/groovy/au/com/dius/pact/provider/groovysupport/ProviderClientTest.groovy index 5f578ca860..eba095cf47 100644 --- a/provider/src/test/groovy/au/com/dius/pact/provider/groovysupport/ProviderClientTest.groovy +++ b/provider/src/test/groovy/au/com/dius/pact/provider/groovysupport/ProviderClientTest.groovy @@ -71,7 +71,7 @@ class ProviderClientTest { } @Test - void 'setupBody() needs to take Content-Type headegr into account (UTF-8)'() { + void 'setupBody() needs to take Content-Type header into account (UTF-8)'() { def contentType = 'text/plain; charset=UTF-8' def headers = ['Content-Type': [contentType]] def body = 'ÄÉÌÕÛ' @@ -95,8 +95,8 @@ class ProviderClientTest { } @Test - void 'setupBody() Content-Type defaults to plain text without encoding'() { - def contentType = 'text/plain' + void 'setupBody() Content-Type defaults to plain text with encoding'() { + def contentType = 'text/plain; charset=ISO-8859-1' def body = 'ÄÉÌÕÛ' def request = new Request('PUT', '/', [:], [:], OptionalBody.body(body.bytes)) def method = new BasicHttpEntityEnclosingRequest('PUT', '/')