From fa295da4dd18a8e138709364c51e677d9c1f4472 Mon Sep 17 00:00:00 2001 From: akwizgran Date: Wed, 5 Dec 2012 20:41:01 +0000 Subject: [PATCH] Merged prototype-test repo into prototype repo, as a separate Eclipse project. --- briar-tests/.classpath | 12 + briar-tests/.gitignore | 1 + briar-tests/.project | 17 + briar-tests/libs/hamcrest-core-1.1.jar | Bin 0 -> 33395 bytes briar-tests/libs/hamcrest-library-1.1.jar | Bin 0 -> 46692 bytes briar-tests/libs/jmock-2.5.1.jar | Bin 0 -> 241000 bytes briar-tests/libs/junit-4.9b3.jar | Bin 0 -> 247280 bytes briar-tests/src/.gitignore | 2 + briar-tests/src/build.xml | 118 + .../src/net/sf/briar/BriarTestCase.java | 20 + .../src/net/sf/briar/LockFairnessTest.java | 161 ++ .../net/sf/briar/ProtocolIntegrationTest.java | 264 +++ .../src/net/sf/briar/TestDatabaseConfig.java | 33 + .../src/net/sf/briar/TestDatabaseModule.java | 29 + briar-tests/src/net/sf/briar/TestUtils.java | 76 + .../net/sf/briar/crypto/CounterModeTest.java | 156 ++ .../net/sf/briar/crypto/ErasableKeyTest.java | 79 + .../net/sf/briar/crypto/KeyAgreementTest.java | 25 + .../sf/briar/crypto/KeyDerivationTest.java | 76 + .../src/net/sf/briar/db/BasicH2Test.java | 192 ++ .../sf/briar/db/DatabaseCleanerImplTest.java | 67 + .../briar/db/DatabaseComponentImplTest.java | 151 ++ .../sf/briar/db/DatabaseComponentTest.java | 1606 +++++++++++++ .../src/net/sf/briar/db/H2DatabaseTest.java | 2044 +++++++++++++++++ .../src/net/sf/briar/db/TestGroup.java | 29 + .../src/net/sf/briar/db/TestGroupFactory.java | 20 + .../src/net/sf/briar/db/TestMessage.java | 89 + .../lifecycle/ShutdownManagerImplTest.java | 33 + .../WindowsShutdownManagerImplTest.java | 39 + .../sf/briar/plugins/DuplexClientTest.java | 101 + .../sf/briar/plugins/DuplexServerTest.java | 103 + .../src/net/sf/briar/plugins/DuplexTest.java | 98 + .../sf/briar/plugins/ImmediateExecutor.java | 10 + .../briar/plugins/PluginManagerImplTest.java | 63 + .../bluetooth/BluetoothClientTest.java | 44 + .../bluetooth/BluetoothServerTest.java | 35 + .../plugins/bluetooth/BluetoothTest.java | 13 + .../file/LinuxRemovableDriveFinderTest.java | 25 + .../file/MacRemovableDriveFinderTest.java | 23 + .../PollingRemovableDriveMonitorTest.java | 95 + .../file/RemovableDrivePluginTest.java | 355 +++ .../file/UnixRemovableDriveMonitorTest.java | 100 + .../briar/plugins/tcp/LanTcpClientTest.java | 45 + .../briar/plugins/tcp/LanTcpPluginTest.java | 142 ++ .../briar/plugins/tcp/LanTcpServerTest.java | 32 + .../sf/briar/plugins/tor/TorPluginTest.java | 175 ++ .../net/sf/briar/protocol/AckReaderTest.java | 124 + .../sf/briar/protocol/BatchReaderTest.java | 137 ++ .../net/sf/briar/protocol/ConstantsTest.java | 193 ++ .../net/sf/briar/protocol/ConsumersTest.java | 105 + .../sf/briar/protocol/OfferReaderTest.java | 124 + .../protocol/ProtocolIntegrationTest.java | 133 ++ .../protocol/ProtocolWriterImplTest.java | 87 + .../sf/briar/protocol/RequestReaderTest.java | 146 ++ .../protocol/UnverifiedBatchImplTest.java | 244 ++ .../OutgoingSimplexConnectionTest.java | 177 ++ .../SimplexProtocolIntegrationTest.java | 223 ++ .../simplex/TestSimplexTransportReader.java | 39 + .../simplex/TestSimplexTransportWriter.java | 48 + .../net/sf/briar/serial/ReaderImplTest.java | 556 +++++ .../net/sf/briar/serial/WriterImplTest.java | 291 +++ .../transport/ConnectionReaderImplTest.java | 107 + .../transport/ConnectionRegistryImplTest.java | 73 + .../briar/transport/ConnectionWindowTest.java | 157 ++ .../transport/ConnectionWriterImplTest.java | 124 + .../IncomingEncryptionLayerTest.java | 183 ++ .../OutgoingEncryptionLayerTest.java | 159 ++ .../TransportConnectionRecogniserTest.java | 141 ++ .../transport/TransportIntegrationTest.java | 173 ++ .../src/net/sf/briar/util/ByteUtilsTest.java | 66 + .../src/net/sf/briar/util/FileUtilsTest.java | 165 ++ .../net/sf/briar/util/StringUtilsTest.java | 44 + .../src/net/sf/briar/util/ZipUtilsTest.java | 202 ++ 73 files changed, 11019 insertions(+) create mode 100644 briar-tests/.classpath create mode 100644 briar-tests/.gitignore create mode 100644 briar-tests/.project create mode 100644 briar-tests/libs/hamcrest-core-1.1.jar create mode 100644 briar-tests/libs/hamcrest-library-1.1.jar create mode 100644 briar-tests/libs/jmock-2.5.1.jar create mode 100644 briar-tests/libs/junit-4.9b3.jar create mode 100644 briar-tests/src/.gitignore create mode 100644 briar-tests/src/build.xml create mode 100644 briar-tests/src/net/sf/briar/BriarTestCase.java create mode 100644 briar-tests/src/net/sf/briar/LockFairnessTest.java create mode 100644 briar-tests/src/net/sf/briar/ProtocolIntegrationTest.java create mode 100644 briar-tests/src/net/sf/briar/TestDatabaseConfig.java create mode 100644 briar-tests/src/net/sf/briar/TestDatabaseModule.java create mode 100644 briar-tests/src/net/sf/briar/TestUtils.java create mode 100644 briar-tests/src/net/sf/briar/crypto/CounterModeTest.java create mode 100644 briar-tests/src/net/sf/briar/crypto/ErasableKeyTest.java create mode 100644 briar-tests/src/net/sf/briar/crypto/KeyAgreementTest.java create mode 100644 briar-tests/src/net/sf/briar/crypto/KeyDerivationTest.java create mode 100644 briar-tests/src/net/sf/briar/db/BasicH2Test.java create mode 100644 briar-tests/src/net/sf/briar/db/DatabaseCleanerImplTest.java create mode 100644 briar-tests/src/net/sf/briar/db/DatabaseComponentImplTest.java create mode 100644 briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java create mode 100644 briar-tests/src/net/sf/briar/db/H2DatabaseTest.java create mode 100644 briar-tests/src/net/sf/briar/db/TestGroup.java create mode 100644 briar-tests/src/net/sf/briar/db/TestGroupFactory.java create mode 100644 briar-tests/src/net/sf/briar/db/TestMessage.java create mode 100644 briar-tests/src/net/sf/briar/lifecycle/ShutdownManagerImplTest.java create mode 100644 briar-tests/src/net/sf/briar/lifecycle/WindowsShutdownManagerImplTest.java create mode 100644 briar-tests/src/net/sf/briar/plugins/DuplexClientTest.java create mode 100644 briar-tests/src/net/sf/briar/plugins/DuplexServerTest.java create mode 100644 briar-tests/src/net/sf/briar/plugins/DuplexTest.java create mode 100644 briar-tests/src/net/sf/briar/plugins/ImmediateExecutor.java create mode 100644 briar-tests/src/net/sf/briar/plugins/PluginManagerImplTest.java create mode 100644 briar-tests/src/net/sf/briar/plugins/bluetooth/BluetoothClientTest.java create mode 100644 briar-tests/src/net/sf/briar/plugins/bluetooth/BluetoothServerTest.java create mode 100644 briar-tests/src/net/sf/briar/plugins/bluetooth/BluetoothTest.java create mode 100644 briar-tests/src/net/sf/briar/plugins/file/LinuxRemovableDriveFinderTest.java create mode 100644 briar-tests/src/net/sf/briar/plugins/file/MacRemovableDriveFinderTest.java create mode 100644 briar-tests/src/net/sf/briar/plugins/file/PollingRemovableDriveMonitorTest.java create mode 100644 briar-tests/src/net/sf/briar/plugins/file/RemovableDrivePluginTest.java create mode 100644 briar-tests/src/net/sf/briar/plugins/file/UnixRemovableDriveMonitorTest.java create mode 100644 briar-tests/src/net/sf/briar/plugins/tcp/LanTcpClientTest.java create mode 100644 briar-tests/src/net/sf/briar/plugins/tcp/LanTcpPluginTest.java create mode 100644 briar-tests/src/net/sf/briar/plugins/tcp/LanTcpServerTest.java create mode 100644 briar-tests/src/net/sf/briar/plugins/tor/TorPluginTest.java create mode 100644 briar-tests/src/net/sf/briar/protocol/AckReaderTest.java create mode 100644 briar-tests/src/net/sf/briar/protocol/BatchReaderTest.java create mode 100644 briar-tests/src/net/sf/briar/protocol/ConstantsTest.java create mode 100644 briar-tests/src/net/sf/briar/protocol/ConsumersTest.java create mode 100644 briar-tests/src/net/sf/briar/protocol/OfferReaderTest.java create mode 100644 briar-tests/src/net/sf/briar/protocol/ProtocolIntegrationTest.java create mode 100644 briar-tests/src/net/sf/briar/protocol/ProtocolWriterImplTest.java create mode 100644 briar-tests/src/net/sf/briar/protocol/RequestReaderTest.java create mode 100644 briar-tests/src/net/sf/briar/protocol/UnverifiedBatchImplTest.java create mode 100644 briar-tests/src/net/sf/briar/protocol/simplex/OutgoingSimplexConnectionTest.java create mode 100644 briar-tests/src/net/sf/briar/protocol/simplex/SimplexProtocolIntegrationTest.java create mode 100644 briar-tests/src/net/sf/briar/protocol/simplex/TestSimplexTransportReader.java create mode 100644 briar-tests/src/net/sf/briar/protocol/simplex/TestSimplexTransportWriter.java create mode 100644 briar-tests/src/net/sf/briar/serial/ReaderImplTest.java create mode 100644 briar-tests/src/net/sf/briar/serial/WriterImplTest.java create mode 100644 briar-tests/src/net/sf/briar/transport/ConnectionReaderImplTest.java create mode 100644 briar-tests/src/net/sf/briar/transport/ConnectionRegistryImplTest.java create mode 100644 briar-tests/src/net/sf/briar/transport/ConnectionWindowTest.java create mode 100644 briar-tests/src/net/sf/briar/transport/ConnectionWriterImplTest.java create mode 100644 briar-tests/src/net/sf/briar/transport/IncomingEncryptionLayerTest.java create mode 100644 briar-tests/src/net/sf/briar/transport/OutgoingEncryptionLayerTest.java create mode 100644 briar-tests/src/net/sf/briar/transport/TransportConnectionRecogniserTest.java create mode 100644 briar-tests/src/net/sf/briar/transport/TransportIntegrationTest.java create mode 100644 briar-tests/src/net/sf/briar/util/ByteUtilsTest.java create mode 100644 briar-tests/src/net/sf/briar/util/FileUtilsTest.java create mode 100644 briar-tests/src/net/sf/briar/util/StringUtilsTest.java create mode 100644 briar-tests/src/net/sf/briar/util/ZipUtilsTest.java diff --git a/briar-tests/.classpath b/briar-tests/.classpath new file mode 100644 index 000000000..5e92e5c69 --- /dev/null +++ b/briar-tests/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/briar-tests/.gitignore b/briar-tests/.gitignore new file mode 100644 index 000000000..ba077a403 --- /dev/null +++ b/briar-tests/.gitignore @@ -0,0 +1 @@ +bin diff --git a/briar-tests/.project b/briar-tests/.project new file mode 100644 index 000000000..fd15c6bba --- /dev/null +++ b/briar-tests/.project @@ -0,0 +1,17 @@ + + + briar-tests + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/briar-tests/libs/hamcrest-core-1.1.jar b/briar-tests/libs/hamcrest-core-1.1.jar new file mode 100644 index 0000000000000000000000000000000000000000..5f1d5ce0c3d60472692cda24885c92042a693ac0 GIT binary patch literal 33395 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íK>0FSofW22mD9Rv65WKwY%;O@%t50!DG)4nkbEx2XU%(jPHf#t6W;7 zS%uAG=xZtM=q>iy3alm;^u!jI;4LgjIca}p3g%nReJ8Qhm2jU+dn}Bboo~XBX+xwP zsWJM=UR{e8M`&G%>O%#vucO}kv-;^@b7MgDNmGpDNrGsyhB(f50B_uy1g?>$QU)f` zz*qNnHuJQ@#k@-7=uxu*p}NX+aHn`n{!BXcjlb??L0_@kEtBSMM`9iVv?pJA7Vg+r zDIt$jT~h*^?urv5#x+a4jD&sB#HhRy`cyncs^Z%9WA*5O4d(8Xibq|ty!(u(H-HF;_ zzydf6IXM~rqdc9eqU$^_j>dNuKjMT&31&>?NYqUJjYE_e%_7GjOW0x_2V@1FK9k?( zoE*7Gl0efi>#66SvY9dGR9gRbW=b%?jiTS#U&3Ev{YPWrhk|E?9aj)BVGg@~ z-Lxu|tHD-1Tw9+*;|!#oRVcAzza7ew!(#nFPR%6y?!rqJAa}NMbLR-g9q#3l2lX;c z=zYp%pWS924Su45QP$Qec3(!*iPFc>fALqZ_-JsmSy2i-3RHj?-ryVL~il%VEYjZW${_+brH*KEVC znaQJ4Ol!aqL1yn!U~tx!K&ByCik5T7K8p#gGO-l){-`_qb(* zc34W*k#lC61y0;P$EG^GdwumTRU^!$ReQKJ4ugSKFhe@F3~hT|L0D%{9eec-Fu*+Z zIX5qV(w%I)&8NNES-2m&OJ4n6_B-|-IQ4gv{$~1F+Q5AL)pFtlDc_6atS%AlTEpK2 zz)tYjZi^Pl)l=eI5x;w(GsVT^?v-g>Q`)rptlX{%(Dl?$dA!nec1Qe!^q@oQpj;V6 zz8Zi4qH+VsaL`NWks@X=?vcdef0C)(S& z7AaxvsO7tWKRysIa-ffrcD5-f+aWPK)7 zeSu#6ZNV?UVmbZ$(0WNdY~?TEBkiO;2SyWeunMn!1d+EYn*B?D1>Zcq*$jDW66R`Af~az6aQj zzA>6tJ=W(yo%2yWbq0BKdIxFDqUuUcDGY_b6{|Y}__U`2KbcPFgr<#Kb!v{UIYIqt zXyZq~v?~;Q2Mv0(K&iPCj?|7i*dAx8_gW-hto8OW%DB4HHsEIq&EV47TX9;;@njj18RBai5~L@I ze1`~2KeL2z)}$}5l+O{qY9ZT`tY=FJobQ5e=%4SH5|}T~{Ub7WT#>SxIKVtNr%jaCaP8 zB8e%lSyg&p(nuU^O154o8~xwtioTUBbhtj5ZwufGOiMJXrIA}E;OKNiql#{A3o7z( zK%PQ99#MnE{4^8lWROwcZv^$QF(0enhEwSyDdnIuR(gFDk*HFXc%C#dexbHnd+i7t z<2m};U{SZa2Kv$sso&XR+-8DN�AXD_w45daGL99;F%HDn%FWYPHhFl9dM}^hUT+ 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

aDFp%+4BH=*^Y-v>G~}Wn`;I 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 @_Hgha@QhXHHOV}HykhI0v>slD2r7`>cqHLH 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 H2Y>w!sRTga literal 0 HcmV?d00001 diff --git a/briar-tests/libs/jmock-2.5.1.jar b/briar-tests/libs/jmock-2.5.1.jar new file mode 100644 index 0000000000000000000000000000000000000000..4415dfbc94f8515bef4495976bcdb0fa5e6b5981 GIT binary patch literal 241000 zcmd?S2b>hwl`ej&CscP&%?uNWLum3a0T@6a0YYSHBtTf8fGt^SW*TT|n8wqC069vQ zY)j7Bk{so=aFA^U1Cp^Vrgowd?0Hs$1uro6bGw+;h&|dreJkR`~CgH_f~b0w}SaFQ9 zZIQCY;nBj#aDK>oAx=a`@(V}vqiCIxFw}M%k$mBrKaB7)8H~@yg7KR|ZPM z1II^33d5sgytZJh*Aki|-qtOV_sge-NAo8wbS(bEo(JlFxI%<~vm?cKoIF+-7|pY0 z<>doI`EvQYzxv`=P^8cKP*x5&8P~~XCFNu?a+A}*4=;<4M%QV|itA+f(cIunaa#D% z+TgS~?fjU^2h$py>COz-nVEKGWo3%f!JD(Q&Kzehfz0DauECk_Ea25bK3K$$#r#;p zdrSGWlUK|5(Z!E$Lh0dEPuA&mmS<(Hvw}ZYW}QA~6|Yujoi)xSyjq)e);X8*>awh} z-r2ybjalb%XA`fk$U2*yExg*Ab+$R%d9{NdJBi~;*SU(o`(5YitX#_PYqGMQZL%vX z8=c**vnMN?`1{(dY%64a;>+H|UPUm`l+~7JlW~HA$`uXFgtn6|Q zxXw$nvWMR<%gSEoW}>~tbzYv8ea@}?aT`DKBl9q)kn0?FogjZ<*;hO~oF6?tQgCEj?D583;XDooS^w~zr2#b< ztZ}hbqu8(s5Sz?!HuoIqn=}|l>NggLi=&$ysp(vHgCn)uN`oM2gf zddv~y$YOBhfjf5P%cB<98}sFz$A^Ya?E~v69x4_F9dQdM!Cr?>IkKp;-$rEE*>#11 zLh;VR$hM*U@p1w6ts5vEA0BnY>Bn_xxNz+uN0xNzwi(I~A6{|o!Q0V>87Di7L z3d3lT&i)BiFnzQ*yiM0JeKdd4T-N7Dcb5n-ogW%1ohS|;c4VC%Wk>Qy2Sy-3tk|vB zK~z1Q;iegP7eM{?EC)2#g3 zDuZE+m-Ha1TA!CX(w+RaHR!gMN<)SGup(i3a&0yy7GDPNRZ(wsWCE~GjTRK%?g^J& z2EbL3?(m;H#p(dLq5Nt2BIgqP=X&E(74INOw-8mv;`8@)Pc5 zzwk1Eu8MX$Ci}R(yuEZBB8IAM@AzFDlwJnlwVf~JjHC-@$cl=T?q)2Y

yp`oB|TIsS@a|$Z; z7@QE^K|tXZ$MYx*&Q`IYX(k+>U1RVrE@a7#W|&@W8O#|HTz(j9_A(nKR+nDMhcLvDeY{oXs>Xkj1yz?a3j-j(V2N2#Gm5P!VSf85hHiY@Nmg!anW`R z(c-04p5r9H2f`O6h&`oYec3QlfNVT`gC4=v2T;WT#PEa20kIRB!RV2Z(uo}>2MWjZ zc*HykyJtTiQi_kr1J+Q6V5$IU$$x6%t3aX&_2phiz!DV_rS%xO8MC1}maGwf)s!&< zF8(rFHxa6qb^4M~hCP_jO5L3=vuYHGrlEWmX^FZb4K|R15?;atdKqSgBde;6P(Ib8 zG=Yv5-F`cUj~}Jxp!vpNX$!~03ecEHP*rHIW<8*EdX-lpU6&xm4wnl9XyiK!+aR@~ zcgu4u;j{fP$!U?=XE~7Basg6Mbt}7FnBeSXk zMN4Ok!~4Kn3&R5_Io*cn-&U~F<)Bcz1uiH9oiYT3Mwk(4@1U5{i1Q;OiyWD1w`x!T zl^ZGy<8j7E2w>TLrQ;(5g`Gv}%`KL$RbI~d;mM%jL0dRiwv3GAPti=^$;;(dPY%j} zC#P6wMM9s9rR8}}QHEUScF(!P8SVul3|L z{CESOz0rA%CvWD}8|0OI`c_Zg!l!TH)j>Jw$$R9zuJc;Yd7X2&=iDRT_T&TdLDzXb z@7&8DA90=g_~Q-y@rdiZkw4zVAD?iY`}yMm{`jQpyqQ1V!XKY;owxGG+xX*iuJd;O zcn5zx!H)+$=biHF;9KmwUMuU`5}BL6=lupIXXiCq3E{ES7VV9`_Gp z0%2ZUdx-GgT?M{gWTqzp*i*s)9F+Hh8St@YS(dT&x^Q9IV&t4$VnMaQ=prQLJrGI~ z%V~iA0usLPUcU7{M|#G1%VL{j6SJKhHH7_saIHkJx`;osBgg}G*gvc$$h9M|@#KQ*FV-UyC8Nr?KoRa~c{1l41C= zKU%OBv{v4Qapy?Byr*!I2s)_>)DEj3?LLsGX{m9vWVII!Yu0LE$pP_Y*Ln$rEDakY zsT%?lB5YN0_%;Kw)c|*-;TJ}2cu9p&g?V8W60LlY zS4)SKK`>%4s{*Yh9ID1Lf{sx-8BK%Jprlz6m~T5l5v4WlFZrWI_0?9`H?Ueli?iRM z(F11c5RWo4K;nmrXO+{Y2Y<2EdVp_3;HtmPE*u&Quxy8r`?c0jx>5Iv`yfM)Z3UgW zH>thaE~ymq2(?OCCbc{5a$1Fs%yg~Ya$Ib;OB$tD$sK{2IWl(qxr8y*ZqG5L2B{%6 z!@h0l1b74byQ*hg80QF`mMA{s!K{c3WltVFoQm0A$u4q(strSdYlS#FUO{(~4Mj@*W=x0K}Z2Z;>Vm*O5| zH+&w#uPdbnS0Yop&WY1?UQ!3nNzECl?Q+gY-4p87!}x4MF_B&%SSiiYCr9wh(=SR< z?Lwz0NL}|?$>6(|*Uv~cB{zmxrb!*%S_3R=fn}Xc z2eFIgGKFoLhG?(>wj7mV;B%xT#|+;0052h|Qh0&n8EJ52PwzA0Ir5;)!9}AZUln(K zZO_bFP}1~_G^gbB^M4YjVIyeRBn#vUSuUGV-z|!UHGsYV^?n5!vKDQ!P)4MzT4q&1 z*D4v6o|h>HT3gOZ>$AM^IG&^=Z13&j$_{Cf ztE~2(7PL1+OapZbz6rINK%SEVRoGX>2=kfm1*72$n=!3wl%7@ z^@{qNEofc<&6C&3-FSWvO6w8FJFmx`+i)i(_u}fl0M9Oir@jllJ0p%^z`{@;V5qOa zz&lY4kj;UIHHkFN%qop@7>#qpLYCAxZBQ}Vbt7J1<&}S=3N1v$f%hcA2qMBFidXLyenw22!?ra z4EI#Q5O1q`iMQ3egF1vW41H2%hP}bUWB18;ZIz-5YWid!zB&IhU1ueSNj9HcMo(kS z7#_e-db5Qn6O7Z0yf4=Iyc2f*`_cK(JPo3k7({MY_c>YMgl(qLybZkg?G_q0K;u@R z;hl$qG4`-R^T9+kHIKz=^KPJd&m_=%C_wXJh2|p@L-PTkd2kYFJ{q9;m_qZ&#L#>Q zXg)j%G#?Mp{F*}ZiHV_k1ZX}!2{exeXih6MpPU$)(?IjdNuc>ufacQ*&1VwPWIh`k z0*?XBXD5N?u>j3y6`Ic_qG|YC49ycj^W-GZd_F+)xI*(pBATYp$IzSwnsbvt^JIYL zDTU?>_~aU}8R*CdJPHX5pO!^uWicm@mS6OWe%``k^P4kr7En0ZA#_$kTcV&XjYE63 zDzv8!v}cl_bt-7f;?TZP71|dKwDU>Ox)ikTIJ9q7h4v)_?aN8fdK9$YIJECog{I}I z56D*nXj*jey5>DC%N4X09KUuie7`ERXANy%4WRjbiVj_=p!IQ>+R%Pj724Mfw66!y zeA<>iEvpo?)f|mBwBN4^?HdM~FAH3Y_w>5ctw5~_7X&@ner$oV*PJsk@i8N3V#3`w z<<||#TLL6{al0fGq4lcsGc0a@QU%d-O773dx8&OZqh-Y!0LEH*Ue+FfPO>ge^Is}Z zTC62qccbt<_zUm69=~qGUrN3M`R}_)pf4rp%i^GaK1tBulix5@Z^t*OPM??cP~0}e zA^*JvIW4~_zlE|P(Y*70+_?>ZDft12{%!dk138Cp{<9%FmIm;T=B*#%tsjA&+ITB& zWK}PZ6Z9VzL757Ic;|P4^!F6%+whr^-P zRR1!G>V8svbsY4<2l}$BkYp2%Ec|@l6Os!qDzOHK~EIl{g zTkD*D{ud#dRJome&BFEv5Bqg;nJepEfs6e**_Z`wm&12mHo0;|gKU;9u55K> zn=9L4B{MF#g~9z*oq=rMb>s7_ZRvC`(w-Q#!)oKwcHlMJ7w}7=oA{!Yu!~!}E2e%h zp9e1Jm)Z7Q|G8yx*S2fxhP6R*L5}O$uyGyn`iF;=Q*&AEaLuix)#?k74X^HGx8zC> zEH+Ux+3C@et_02WwuL`zydvTN?!Xp^@i+`UNkT^eIGKd|62|hcd?ow{f~Z#oZS&U4 zC>d<|mtBNMNc(_x(6@G{xGS^4?w`QkZWS_q{mJCG1}OH+tfn0cOw5(SGeg37QOuB; zp3IUfJ((ucJ!zFTSFZA;U#|A#8pLx*UpScpEV^;D>$OGrRycPL(AjxS!aHG8?YDOe z_mAX<%R_Ye_h1{|Lt3uo$KJ?Jj~cVCyu^bccOM+0L-?jP)}V^){Afzy8MKB&eV10qoe0j+Ed?q`gekWDT}>{8*2c@O?uI8azKRP$*-vuEccRV)wTxvcc** zpB^2hlHBV)z*PhYcdZJX6%JDUM~}g)9vkdsxY~vk^$|cMh{LDI-kc2@Li-kM*MS(y znNDx;10p>BLMnn6tkK{PW2;zfJInaBN9UsI7PaRzD}=Aw$f^-NkHVjh?6ryU8&pHG z=CtN~whNlRlQ8ckhjpMCE7KHge{eGi69elHLmg8x6@Qp55C*1TwPK7M*QQz0f%}X_ zJBDwrU+rDpXRxVtPEuXHXQZa92m4xFBWq+1KGhltnIHqHpb0WT8>E7{xUa42?J`d- zWel$*Zpt3*3Tq9veSD#u&q$4?YZ0hejEyNw5R_PfZ!EKF*Q-X0J4CCM)n6%8TFt97 zsK8RB(^TPdd=n+N${Z-XW@{(_0O5eNY8fXJ(3+8D(gm#Dc*ZvA!Cx=7urx)tV(}!< zZUTK;>bWs@UeX85@60*Lo|6VVtgZR7c-SkeReLlk)m(~hr`|%_9iZ(-Wmf`83Oi-& zpj8kZM6$Tkgq1rdsi?Rx^hdwF<)6_+{6jBRiVIO0!C968Kl~IwK8;W}`Ak->md9NA zY!-6MW4!&jjC@`m&&U(0a){4@)d z>SqlC1*t)x?4;$-`TZ9SP*eUYEq|Ssze&r_dFvN^=`+0lTcq=E`QqOpI8grHm0$A5 zKVn)qfc#r zVzc1WpV+fF1lFY4F+LPykh2P*gWisjk~$bHIZwYpz_;3IBJ>gLrkWZEiH|G1*AMN~8DWe;6A9aXG-;mWAvE%}Z; z*i?8!baAaRD%lKk@OJe95B?s88v8)w5sbcMkjk=e~ARLWkU&Fz2nDI z^i}a4PcE;6)evPEgO;RTSY?6^9!pIdSL4YPsfuXfQ-|@yrs>|1(n*AnYHa<3a2;d_ zFXEjiRY!|%!xVL`EvN2Iy8-+{7Q6D@eK=9!6+24K9GN{y@2GNkBdvVxz~HL6Ct1V9 zxLB&;;T1wg41D8hIsU6y>fxef!JL4bV1tFqkq)coWK|X{RHp?xdSPU=zq}7Y24x-M zn;k5a2S$nqF+4F%^fU-umA9*BDsn{Ct3ekwp(NTpKd~^H&gbM8psqSta_rddSWOg{ zM|I%B&M1J%YqnUY4k3c8e|URgG+$)sLqwZML}#lOi3Lz)0xiJB3}n67%2Vnjbe`17 zz7X__rv>@MUbtdrhdIP_bLhGr-RhJFz6Te&gT70p5{$0Omyhf#zyW%XTck zB{a0%Y6u92X70%NrUw#`{M7#?kTM7tUDVEoJOqk+2^E@XJo%t}$a5Ati#%tsv&0`>HEM<2-C=Zd6PRx<8Rumgx|4p;x%7!dYm ztveHY2mCu0eX$L_J+uu$1yM0?aSU`~F+j^;>C{p(oYTV^a7eHLwImV+-886;LL6rh zt;dz;jHrO+L(N}bv!k210h0SB&M#F^@DiUWm5_loajU$)X751Cbs$j6@ z*sr0JlDMTcUqbeNkXd`ICrPr0F;6C;wcZS3Nwj2Ew1qky@UkkQG74xFexU$aEF+1V z+7^WjBA%4p8x2t*y&Rzuvc zqJaDeWyOUf9UZ`qzFiTu!=Mz5Igm5PmZM+G*8LD^LZYD9n~ zM&ZC6=n{s&<~o`>Ok=K!95NEvcPu7w@D7O@m{a1^OOBW~A|45@JNl{o{C|0kv441Dp< zOXC4rch5=F8JNA#OEZd3IU_BM=V-<6HvDcsBUANW@gNX%!B7jEFwpeCu-FTm@^Tn_ zSI8n5b64ZPO$f-?hJcJc*wineq&8Y#i>>Ly_~l?=svyIBU4z*eM{`=|o|Sp%=XtF;e83kRj@r+jmxTwqx_i#aqCMul#R&2_FH7*&(gSVM z+1q5QkIyW$8XpgD3l*ksiJ0psO-V`8~WA9rrp6%-16t=w1xPH$W$Q zv#Q=2OoBm|8N36@-=v8I-uN*<_jy&@?qugLFHes;z>BLF2_4+ z2j;x=(k)~;TR3I4@CU3G&IBZ66k_(-eIQhiC3`cc1;cYi9MmVq0i{DlNEO4iNL4d? zKZU=RXJlm!Ha>jX^!R)n8!t~-K$`>ln&D3J0O(?b$Xg73p}z{?fxf;tIBXKhmn?90 zb>E7Z1cG~ez_D&YuW%wS)jS6<&&sNYWEQB+o|n}JTGx>7b8^WuvNk2phRF1e+}H8? zH^7O$iT#u36jIFvmYO;^h!|j)2%Fqh){xqiH<&JRE1%+bT`J7B=Sfo-ne#3s&ffjj;{Ho$?AoKN$OjX_&( zly}LyL5Uw8L5ltq1FQz$`+%VJIZ5-2v0ZG}D|QnrrtD_W*Yo)9F8s`PVWwh7b8hw46z zH@Ue;>`)I)SL6YU74Kjdqp~}?y1yVh;g+|%2ft9Gt9rs=uetKyV{iAD*uVV;MT}k% zIs$YZiWq2UGAy!6&2}QR` zbA=P_&!PbTX5cSW4t?7~bgxu0uDCAT%VZ|>x-z8P;_wy~N*+d!#bzB+ojjmShm{IK*OT>KBhyM8} zg8}$A3x6S@RFAZ(7Hdm1B!`d7ag>DwZCghh%p1Df+moPOK-H^x2E{5}b+Ekl>f>KO ziGjkuc@>qkMslpC@Uz0enc_+domnb1(5bMxl}?m*!)6vv42&sPr-j5-Bo&ZvKJdMW zXf`Qcu+LjyZ!Nph8taBBcAa!zs3a|k+v69|AxX|wrZYHiG3+qzYB_W9U2neCIK=Oa zVr301h1pG(*NE$sG(~6lpZ%=<&!%7m^Ka2uou;(ySkEEPa-%HC0QypXAeXZz$mQH6 z-Ms2)pvOyEkiU7Q^rdB017x8-S5~{S#+6GzR3Wm#H7BZfM`tEgZ}y_@)p7g}gU$38 zRn<-$r<`w)rB=ZK^e^ z^q}L^^57w~)`JMr>Omr%!k_J~tn(lqUg{w+!94gUdyo$|Vr^TWbLDbRa&VT0`FsMl zeQQHOVH|IfsJ1pm^C;<_vJ60h!(x@yC&7ZtVQCsXPVq6CErXvBynq^Dnso_;l2ub}*(^@ERL=y zKmEmP{`HUXfPc&J7nM%plN)wN}B2<6zDa462|L_99okRuAh>3 zG=CCfoPW8BhO#%-VolSeWo%5zfxNu|mzta`MU`88-ta|?F%wXo?8{W8C zes@v$!gn`AcJSSk0kgz+ZwB)k-^*QD;R;+o0=eh~I2odzUNo|$!k^Tt6V@+#@LjIQ zUf{H`JAcgfSq*wKX^7HGGSa`z;UZ-ez~&vp14E@UcCB?phAVv@R;Fnl{GeC+9wTbt znY`ADC7f`h?r9 zX0kCRE!YrK_aE5118>D!5e-;#>^S_LF12}p@#V7$n>+Sio4JJ%%;lc{z_i%3_@?9d zXVk(hatroGxD%3U6C^*Wy%CpnZBlP8-8M;^%S@YO&1FNIc;>RPO`6PQbDK;tmo075 z%1bRr(DkzQbKetzvTym@AyYlpML~V$+xt65}V0?LHGFkGLSRg zz>hcb<4vyIpN0MR&8}?Z@3*+})-05^`&jmEyn1_D-jRiU^<8OsH-Ep)mG@*(^1WG_ z!;Vn3fOo&PUSn(X@OlEsyZ-$Fnf5y_s)+f)5{c<#ZO6{v?0E zo&VGHeffFy6tBKOTF*3KpW~b>Pp9e2pOG(8!8;GbG)n;S zm(%hUSDwYOI&z;YUx((o=h|EMZnR zxyG}`q>)ndh^`aXA*9N`SZd`IXiiF)2**@3C^8s&R!M(g&5ndm#(E0O6|jR-Ow0i; zo_&w)6v^)xAajb4ol)&}yEv?*%14l|MYiCB*IalW44tBbZN!-BcR=MpgC<;nEw0yk z;#VVFIrrz8V>z&LVG`8CXp39umf$XIU*CX(pz>qZev%9z*j74%rVz>qn-YfL%I!}5wu*_X>E4_O&E8>@>Y z)94|Ih0Vj(;mw|Gp;e}ux2gw^jlCZ7zC4Fn@iJGw#T(zoH5_QG8YMie?)zQ&4!^H+ z<-7d;o`)^^8~E`XuKcEl9mDSf%5o3ehI?_*iRsXj!v*q!BM*wA4#_t?`Jwy>Efo%f zap#D~FfxysOI-OwZt@-Tkm7IH<9@S;{k}i&=yBj-ulseb{KSJI{8JBJ1Gw`u`4dmJ zGEn|cJ;aY4o=D2{9whCB&nPmW>-mw(=hTGC9U3Ve<&P4wrE4y}KF43|Pp_Xq?vgJ9*q(;-fMzjMAAEnrE&wJaj6g3TtsSd1w(l&LLC9^oFrF zVv>yCSYsgH)5Y$B9U(7@iEc*a=<$~kJuQ?Jq4+!2&t(yZDZ{^$)_yRk zMPjQPxKLzOx5N7A2^aEe$f%K$ddY}riZ@{51-e!mZ>utBwvb|T2b)?^rjCxCQpAd1 z<`ItY(c6isT-A7Exd}rK#i9UIF$Z8%XIo*@eG$e4MT9ofQOoE(M`J7wdBf*Sn$Mxt zw34-}Z*X_@4F-ST2$E_l%^qGS`ncb*|xMP|LcYOmxGB(F0553OJNe(W61RlL;8`&8yv@ zjk;$i+~78Y4qlNy8iuiw7;1Lo2}{98Dg*zhEQo42D_(0OK9D#LN~YJ^jE}Q2MLQUf z5N(ECtq^OCaiKx%aGf!r{RE;jy1ynZ55iNV`>XV;X?;-K+S3TwehiRO@F4KvU8>+| zBkGKxA~WF5!HN*_egnCH_gJC6?&oAVEVwE7ZKNKRg*4{k5(;(OgDM4Z8o$y=2wV;2 zMhj(MfU*xLcPW%uCk@K2rk!RG-B|@3Xr|qRpFlAEo_J-pS&idY#(|sNO$Od8F^*V; zb;>L1tTZUExEs4$XPOJv<*dx|Z+EoLZkXb#UfY49 zS*@?c8}KIU?fL-qZSY~#bZ;E2-wn5@h0Iq-r_e@N5%gf!jzk6A zIgCy_qCj)gr3oWKJEj6qKye!!Njd!Q02CTkhw!Jfh?Dkj;62X!bwqqt7VYkNQWigo zgb3JIT5?(zbbm|D=%uG+IyO5x_jL8pk?|pHd+?%*tffZHz~$K75CBfY)vy_^i0#No zF&#du9q?M62OrY~kkppKJ9H&n6W784(PueJRq11@_Vze?g(4bT+IXM2Ey{q-nXSu@ zqP%`KihC*LcdA?RPCQ{twu9X62SGf&GJ77QXRb;)sj1aX_-fddUyEkDJ7_}YyYLxc zyTZ)IZ&&HoLmGHzM0w5XXQV;1YNkGi{E1ICEb&a z33lI|QhDClpLa!?fp<5LDh36gkq)k5CSKksLD26AYL${%{D2E5T!1&X;97WDbZmkNh?X%^V7RR zWNe&BE-Igj+me6Y-kSq$JQe{x6K|d5V6#|L!9`3r)dh#3*?TjQ7SOOhan^{9 zu?Fp^LMz{!a1dRE^`L5_G6P?gCjI=3;z#_Q=cSP*4LmEO`K)5?8EiRUlze58QP>tE zL^sy>VXz5Ml>_<%+ayPRo|GY|TPv*q)XhY1x^DvT#*e`dRjBg1E+&T{s8> zrl&2tc3pd8|DG$INXWj`Z2?hG57Q$sMHg{wAOtO_!tet;Sv-0i8dP9+uj4`7Flk+o z!&Z1+`NYmTsvFQpc_*C+juX!(a==k=^mo+%kw?iYT`|}VEblAr%)`cB%_p*%nzP=7 zGDhi(Wn8zch1yIR8Q)W^Gouur^{}(H+JlZc%Y*cVk0r7+;+d7y*L-OXI#@Y?76x5r z#ejve1KYI3r<-jpq+`(Tp*cH*(NEh%tua>!=FNuH!MWYD*@v)etc1r2Xl@%@4|d zJ!UkFOb{_mXXl6v(v8Mn?G$7Y~%PBFO&!fUGI-b4#3p({hahYB(x#awAOu;Q9jV;GjLBk zEk95$XuQ^ic-&IWLo5Q*fG+Y`KB@hvOy?tf;~7%k>1XfcGv5EG%;|1z)VGyw0Y)6Z z&=3m{F^V)N7wIRQhge`rSfIv&(P@ApbcvX|TU(RLX`>a}{&qF&R>IMy4()>tYG&$*b|?b+Q?;Ut18fv=vb_+YtG(9b51_ zjWF%v?F()1&R@LI3_7lcW#*0Jm!3)cu>oH~*zkv9_nA zT}$ubNTCBPyHfB>)Wv~*-3@6K)j%b1&% zc?}?bK7TB5WuYsJTv-f`jku6;Pv#BQ^2l~_K#z*bP$4lFw8kfYpEyg9vBW3fF4@JhJ)(H=M=~7-#$(dRb!6vSG zNsCW-7+z#|F4`?zWsw}@PSwd5X>G(rV59@tElmAv3k+=9fPH6W!DQ0xiN;@t!NVy% zeVb8I+Y{1Y^%?w^#WeP(7+qcH+DwK{sAQqq#kmRhE>*wrjQr#osd0=yEn#XExe@T1 z6g<5``S2jKVG`4gT41Htl)-No2jkTl<_KdlJKzl$V-KS?L zn&{S?l3CG--SfBSj{XfE@UI(xp$Z=KnB@*@b_-&&I?#$*E>_x!$he=LK#Hv~0jTWpL>7AYkON)8usB{T-u$SE+KE4Sx zh$);fwUB<1BwD*ggn z;ig~RKIIn~5&lK3HNiyMnZKi;H{$f(Z#mWkZy+bQh8co)@oIOL$tp771usd{hhbk9 zPxkZX^}K?zy;p8@<)*ZB@%MoYwm47{9uhaR&@H_A@+@xNnvvW1B+oksx!?|Dd8{ar z9OBhs24ktKBUzp!;L07YAeXl}%6Mi%D3Nx*wiN1dGVol`8f@o-t=@{%_y0>IwGUk7 z#&kK1I88xf@D5>EFh8Akc#CYq`rEd!VW?vJgWUb&d3Xg1UY6i@@DO-tflKY}c>}z1 zXPg!Vg>GkYI6qXp3p#TmRrCldmVG~l>ulJ*i;RA_!NQ^JhV~6ZsqF!v%aDwS%U65OlX%cObd{jds64fF$=bu)|Qpof?sF(0I zg4ePKUexVM$-~~mD_p_B0cE)Y-4;4n#+?y7s3HiB!@;=9_1(j+(1{3Z)cxf_lUe67 zk~i@BtXNe&rtI(_5nVAzL&y8^5@mEa6r68TLQFJ^@;LzcV>Qz0+P-*y4sJ(!~pg+jR^Z5)B z_4|i-8((`1n4Wla=dk@T%yfh@p}e>Bx#ba~jR6UMXIX865`nbrjwp$jxY)^3JA<;&Swm~xik7iD`wr7nOOXJD>Uzk!Lmz*#@_1tDrX6#{l} zt+jx(fkbmmFXVL6^beqsR$Ctuqk%Bpd7R`! z_dxa{ZE0f-^v3yg>>|v7&M^~p?|_+MHn_)Jc#+J5R52gh+Y4aMScv^i8qWl+uWA#t z*#m$=QxRexl(B-R5$N6i)wQ(@yulBiFTPHFgO8|h@C}r@hkVMk)BEtv-HcAuX(o0` zXFVu$aoGW1qS+5h8!qR}tbJPM+TJiVpvmut)rvZup{O&UuG1J$=s&axMj0B~I$@aE zqH58pe4T352D;X{SZaqUwH4#37KR#XTvwrn{p#xKsP6TlKUnqII{}q{3sE5CUm=?d zOt~>xaVa&=nXb&DPC1hw9ch`(?>TAORoZxokY_0T^DQY&nqRbfjzi-1khL3$#b(~|^5Y4lm>k+5!2x2MBtowwm?T1Lgyt7DmersR{ESwJjAeV+ zt?V1hr!9s3`I=c^8?AM4sGC*$QAxMDIE1$qo<8-M`FbMLh@e5&Tc8&zLj)CI25zNf zMs(P&{N|a~A7Uc&kMlL5)seu42E1!W263v+@L?PdR95M2Mq{J6XM*;?CS?!Yiw(UZ zwyq$LE=(GX3TIFV12V7>LRwCsGFJYAw*!|a0k7HwpsP*iE1_(mcjESxAY)-}QsVv3 zFdfdVN?kAqVK&4Fb!92Cb|~}V32i=vb37I}V?OLjXw|Bq6DMGj$)-x0Zz^6Ah zV2|YhKVIs}%Nnrfcr!n4;m6Bexiu}fWg*1m;UnqFfGgNV#@LCao=G^r#a8%C2$k~U zPddxRGB|Bum~x|HXeJ*pG}G03c}aXfSZviuH}5DY0e^b|%esBwK!7Q!b3pZrm&kZ5 zwcWGZ2H9hixND>-z#Wj38#`P!m!}G7w>r7|WDc>oSbR9@zoB`4st8HO#I30zpi=YV zoIkYr3pwCo;`xvwMyW$F|E-xx!@(>RO7PP9br{0J8Q?(&fRMIOa)@C{SSIWqr2KZ+ zl_QM8$EgRm3+nyoa4DKdzkIAPP~>4E>vMw$XgXCozI^%eY_2zljW;OjbkV@T89@sE zVf#gW#6dnckPjS$asw(=+ZB8n2Wk%G^kE3_6*;P6T@@&W?G#e*7jl6aDOWlagMI}} zr5$JYqiNe}q<3R^+guSLo*=V$#r9;_gsNB%bg2{l= zH$!iNG9%OI(1Qp9m$_W)kVQF-jaQQjHH517Tic;`(ek5h8q*9^+J&@DBLmEIz@U1k z9WaQAd01&j@PMEeCKVzGsivI6lvE86M+jTn6_mh@U$hj_CPFEYHW5wxyJ7j@TUQby zJW@;&U1W7aDkr$t^ubrAwXu~RLU`Pa5E|A^6?}+kp{AIJxW$JdF>|qfjuf##Ive`n zJdiXWL@fXT%$~8tBJj$f@>kN>s|ci#Mia<9)ok2eO#xCk?n1?pDe*z+z(t!QUzPUV zXzZyDGQQT;Ja+o||52+YTei6$`!xqhmcX*siC*hQ8~bfP2d%|cNhu2)Z9)!_1-+~- zH*EX&Vl>p^oB4#%6d;6H6(I|7p$Xa33!0`ed3FP@ro-fQ`uQK9ml+3IXJTZ`dKLgu zAi^hX6~;2nM{8inT8oapR1wCYv^h}ENmvaC>omqHrmCiSkpC_hnv!rP6t3w9z72od zagD&asJ{KZ`rEV1>tS)jUzH7Kn|tdMb7G?OHd-5y{U^)RKRDfzAIo`9r(Vo*h%!gV zng;auC9bSR<~&y}g`hfEz-mb)UdOB_?ZMcBaMe@if}{a<0>i-OU89mSO=iFGg`XL6 zK~}KDGuA4b9|BC(1O+`w0&OJ-|G2(O>=j1sjOiz6F$+*+$3uBNjc0;_d87!*;6L-A z@5^w)b8?0+Yk@RlqH5h@!s+zNmNTO1H0FD3lqQoCvR$UCjE^{@UhQ$=aC(!SV2f;} zH4$dTBx^(^{6x9vzrgO(7#lX+XxZ-1WHpGY#V=1fi3pYjG7=gwYryi*;JBtuf(Jsk zDl4V`8(Es5b2AYn{jhi}G>nV*^)7sqGq(qRSSl+d^Fyk{igZ5q8Eykgda!yDPZQSQ zX60t7qk!9$c!8q8qUqoj?W9~G+ZAqoptN4KS)OjMy8IN-q|5UNX$ibT!4+ zOqx3{6cgZZ8sgHj&2-eBi7T}eww4sQ2Sc39+_(s`K1|}c*4X)s*tzS$Tkq!8J!yG8f85I- z_obQnYPG!4l{axT;f2+5zbg-9QBC#Z&Ajs#e!R5-#~8dli=Cx+q~)CrOzfYQ_oPkQ ztF(Ne0joD2J;ZW4QUB)?ovH`pC43vKGUQ`D)NS z)e@+@IkcAl-cZ7pck zN}I8=kTsNxgM}T34xxzwyieCUP$TUVmjg(l1FM2i885?#q8G#Rmgr%eIHk~9O*m7@ z8adbjv5j2;@m3Kf1Wdtex>tgJoEbZO)jUwetI90KyQ^XtF|Q`nbE45M5&;^BElDHU zPUcuK@Cd3Mpn|kn;pj6Vo)rWn4nM+~3X@>vps04hZe>YTFPxVU-@yXsOq$gR05G{h zLpYqk_Y?apETM2_!yyaHSrRVm1@Hy5m!4CwAldLK5t_-y_L!7Cb1Z}8Bo60IvUOIA zOPhGE{)t;PkwT?^cw1>?1k+!25Sk%4Y6B@!E?fW((sLzhGZAa*7vOPX!UMvN12c;3 zg3n5v_ePM#@nx=@@sLsx+03E&yK>Hxr{x*tS_QdlV}AIQFkaKqXRw=tm(>m59bYi*`q_L zN}%P+S3I17@>L9f6?doXk4V}GF5w?7TU2_-AbbKYIZkC{|0blX znpVFf-%S?FD<>SFYyimbB|8FyZBGO9Z_sF~KH%x9Qh^~u*ilyF`|<;o{)>n20{;*B zaZ0YH;~Z?-Ka$_|9X}s-xOP|aD{zV@5n^VxkftRCig|N&5q{+ zGMV+xnME7=*HJ7C$yOG-ii4r>h{HOo3z{FB=bxryDwZ|@<%^(3TotPXbLzR z6;5v{W#FU=sqKmDp|qr6QB0Asp^W}b_b;%s;5S{gXj5ZZorp@8qtO-oDkw)+Kd>&8 zYsNkQa7Y>msg{adqtsBOC`RBaQbE<8k-Dzd`lnS=s;<_Iy2!%p+yEaN?~F9!vI&>X zXK=1TaM{vTgG0i!IdvC+E`iR6)r8Tx| z=>CsiJn-)&_`C+!k)saHu?d8(Eywd=3tpfgXnnBWK%kS4wir9=_{5Zg<;NJQV-uC_ zyav1FKCH!nwWJCx-6{J~R#S>le`W5~&3!#+y#YRe30PE4FB{7;VCkxYMR&E0MO$|@ zmK%ZPrerMY*hU*mFR(1Hf<+G?8w)MQ{t!EWE1m$VTi>?cPoeEPu-=F5)`#9*W#Ljs z&ZDK+Ry;$L76g5UDErff*ir@JC4k7pR^fht9v6hD(?U5WyHmvoX$;4o9wC>ZCDvQ8 z>=B~P(GC}KU0>(j=tJ6NenxWyEEL!-z&U-ej&l#8RmJE|*S}B8tnSts`kKaz)~5ye zyjZfk6{)<;D|M)0M@qU|X9u^^`W9Zv23H8cMNB`9^K-dNki*412fGBhJ>9MI^_#TK zFTlBM&;N073m_VCcOeWJ>U_aO5R0N9aM~q6gpKraOiWtl)0k*27zQ)IPv(Q8ECToF zMQ3fm&c}A}jy>QWH-dBI5rcaNCexcRao&k${Q#akj7j@zn6jT^#!*bSA7POG7#ZAt zid+Kh#7&rtZ-QxzgP{g4vP;w{dMT8hqPnvN5}bbzO&?6*pQ$8S96tK=Rd)7TbPzcf zx07n{2VdSLkKuPpO=NA9x=CTx6V)G#OsB=OMXSJA8i#RnA`G35i!gZNDa}~hkpisq zLD>XS>M&lGJtJM*N?~rLG=8Id)bF|YoyBi-toq#qhTCg9=i2n&rf_S9wmoQ#?JA8C zlP5t;Gc`GZZ-1vrk0I_fsWHTzCOw8Ytd>{oG;#jw%mhW)%x=eq%^lcl8B(ZeE95TI zh&pAFe%?%g#%Av|lk?WD`4yz!J1IUIxrgG2CSgkIaBSK*qq~`wJD?})Al@e0pwKB#SVD|zDdPW;6=bX z=0eaE)BL7D(zIt}m4g}ZSK$oMjc^hbJ_RG?T^Nb43<#NnkxPcdt+v@gBPf54g-!pOObM*k6T7uutB~52U|` zRS-5q{(x04CGX_NyBcutC-UN_0@W{FAa%D67Yc*r?S(`6<3pj%U?6{N^f)|g1EY(PdXtTDwi&WJ(VP~So@tg; z7Qu}sb@U5xR_jYLha_rUq3d?zXvrE@2}j8LMiL8l%>M@35cWYM%~sW{d4YzyNgIya zU|WiJSzrN*_kl7j84C*?NUEuERqcOcS)9kQ4ZbLbxQN+i;s&^RXe9U}BN-I4t|yrc zaM)4kcI6jT&6O^j*XWv;P-fz$w|Z?~`OmQjPBrsckNNOigYg1Jjdl*Z)X~zw9lkD^ zXx?hkDBu&|pPz)qMs48FP2Sz@#6x}jl9$IRodd82X>WoHK4!*|mDN#MdmFyZK~Zysgo504Klh+*E43?&IfP~UP$(~GeSbz?FKV{ai;Hdg|9XzYY=o{EM} zcr!DM%;(n?4)fd$dvX{983K|{6i1I>m>`8a;U#k7aC_J>bhQF_R@-_Lo2fG0>8hnGNLj1Z1N%J6_0mEJi5SzRe2rC*LDIrh6g0M6D zhlUD=^Fw}pgFK(eniU*KfE^RtL%57%3Xt(_;EtT0NM%~TpM!s30_LN7pLRu<(bDuF z-@(BY-{wgbuvr_Q;G|XyKqy~h^E%Ad9!|1{C3>^`7LCvV^8@+qv55$WXF$a7$PWqP zM?8T7Aj&*>VzW7m!jnIgKXBwT!zK7g!|VB&fAmo~cLK+fqyc{P3VY zg#x2>UPad`$8oD)7ZyUl>)eTaIY)ShDE@||c`#{!$Bx;fsyleDLC~0pUU%h>cwom* zAOj@LW=5;ugX4D-MoQx3Fj8samOzs4ktMjAF(28xG3&u~g@FS4ZbWn6$o5CViV>Mo z=6N`hd>E57=qDa5|6(3%AK6LpcO$sXYjbjcwa5L0jqmn|WQ22xO2a%W$t-l*Lm^-> zl`S7Rd)Hu!q`2~3@O(*O@^*M+{RxMAYx6K_)2SHMTi0T0}Q&#ky~E9@e)-7Ho{{=j#uG52A2uIWnk3V12FnT3~rYQ*51%JJp&_=XRO;bFn;>G0zI&U(GcoGtQ-ay+k ze7l?cENLOYH85ITVgYOjC~t__A;BqS|I{N(o2jp} z@Nm0MkE)cxL&IVm51)m2cyd5VW!PuKydMpd!uK`09cCUtAI>@_9Y_d)I~ipWeNg2i zMJvta(|PCA;T71(;29DN^m}s;u=F`uh+{3np3;#KH-G`%1e@OhG{Q^4mTm?!x&_U3 zt2G#|2`X|8OtK>&k*6{67yuG^BbtIlGTACfLJT12So{&wjZ4^#AyPf!4#GG&fZi%t zNIC)}9R`U5>#@)8wi|xe3M6%Eh%7zO+KIz3DqtT`ec1xbH+x)f7_=JL4y?`op!1Kn z!M-7m(g??p?#B^}vig9sdee{LDB+#3_h`b`_G~uHEoc@tOf!1{ZK8*-g-SQZotSxkRhGKGe~$qM3nmtO2wLia<+v{&&*c=+p8^1*#J`(Hu5MY z-gy<^>nZSRg^33#5YrYjG`Vfu&H0J#fZlL1(5-m4g{Uz=)TmC1a3FL->@^~PXGtpjQ@_)Hg)#jH3h zE5lK$o9ccv)dLEV9z{MAbFlHnnS6hAi{S7xfBn$EfGhBi3;3AQ^nTl#TlH*sB$hyq zS!R%*!;iTQj7Dw1`ntf%GV27xf(SSXJ8;K>#+NTG%SFt0?}8##)>`la zu{P97sR|*!-qZmZ(Om7pfoys< zPeF0L85rihV%FLg+}F}TGoH8M7u{%xhAg3(J)zJr&O=90@T4_z^QquaOe0mRcIazx zg}k9U0LQz7X~4E zAlyM%kTe-1RAA!O6^93DSByBAC#SB7zIx#&Ssqs6R#Mz7Gt(0 zs+jCLrd8KTc_>f93prx&Y>l*>;Umq`qgaRv7jvc8V+=29UmVW)U6^xarAHOiqXz28 zLc9FDYrT0%^VLQAcwx~qv2Bw-H7$}s>i&m~&sg8sm?~M)WERl=mo9UC& zM{?FCu8NTgL<}y*la5 zfb0Q?Ud7LR?#9g7{HJ-E=59npCOSj0AEH^SenVa+cf%ht>zn9h;?UCEN(EQTLR^~& zm&~ifkh%vj$RVe7x7Mi?--HD>Lv?LJvI$MI$Ti@smnevuMNTsi$ujjp94V+JWQ_B0 zhi`E)U{bGaj8UHb?f3ly^gjMALBa9HM~oL<$5e=`viLyg@Dy3&3b-TE_VM=G1{!Dh zahW>$CnFoOjDhBzO|D$gfG*qY$`A1#&EpNAS${RcV3mkwQnE=PHNWqIu%5YZu0uM|u>e%=S8-K)(JGKY^gvFAiEwE6& zw{)x*DP&6@empgr&M`38S4Jc$o2y~8D4w6vD-|A3AqMZ zd@ZcSbu=79gf=-F8F#DK7n+P2rL4I$`LZwALLcmh2p!@L4aQ z5{^#2!UhcUXnsK)tz^W(==HeHfJwoO$Byq)D=XJbu46biB)Vq)5Kw%`dh<4IYkz+c$`K~O$GMz=^^AH>y++u~9UX1MetCOVp zv5AKSKY<(_7npLsv8cnsGzn4nieLiQmADrT*_>(ZcaEE%{vs#WpRTvB;Zj)~wh!^3 zxNWD0t>%@0_Kd7h3&(DLqpm;#Hs{_=$njyPwa*SOMo=2{en9>-6FpeY>F0yo%4g;? zO%C&=mEjeU=7D^DlOFLCPT-y_R~dr_=9*#xzW5)Bg*oT*-3FA>QnCvUnpQ>+pIvAr zfl#!PPzyDo74w`1l5O}V)2jnZPw5;#1f{u-r@&|~T4!_(6a}Mb$7{SI3umZ=UNs1i zQArf(69KkZbX5YZHQL3vV5W@&Mk^TOf?lB-TvbG0$G9i*)296+ZpSn&*ka)H zN~Ky&tck~&CWu)bBtLNW+E*Alv|S$o1~18auI>323DHLPkq|;2FLOHoNLG%Lvr)EP>w|&k%PaO4uwJ2bE z31|p_H5g!c6-xlK4Eaj{!G=q~SxIR|83LSUQCAlL)JfgRAhdxg0;cQ#a$W#^^%|rZolpn)9I=aM(hha=$9)XRLGGdD{P9L)#gjL=D)UfU-t5X-TzRW2Z-X#8QYiaP zQQmq=yRSD}0WiDs$H3EU`_j7TQ`>DBUUua&U34vfFC*eIRK_bFJ%$sAkvqRkS$DQ2 zs4FKk>nTbHcBy%`O|_yR9mp=oLe*=b50sp00|fMF<3cmMZ2j;6T@7ZBj^4Rg7XJ*w zKCV;!rR?6JPn~M_ghp2hQgjbjPjlE{4S_1!N!9onliXP)bqG2iKFPMjUvR&2P>{a= zlx^7&HScvBMD}Rk4wpcER-s`Ex_#sR~%kfKD z#IhPOLCMuPzv;Tdp&{gPf!C%zh*T;+98{qRFX!@9VaT+!2eB*f@DQ2%E?3^|$$R9z zuDs8Ka}eYIJY*Yv*h6Bl4|(!o`G_YUm5+Jyh&=4d$2~;we!`VUJvl9(#HK=Q1bOl) z`LrjWq3!N5`K$-8pKDzCoS&vx_43wJqd1pjqkKN{`o1Hj5u_2>D3ANipbGcv!=m`$ zi3pTkrQyT8`=oD~)FbE!T(<|nANc602!fqMrMyCbv@_Nky}fk&;E>OVE$FxbGMjvYt#pBE5(G_iyn z+5`mt9kB(({CcsM6ppUwGwjtPm4}{DdP=rMAZc#Kq;m*_$QOsp*Az}w@rXwBr#6gE zI1CJq#VY5I;^%ms-pt!dSb2saig$m<;wZpj*<;C8qD#Hk^P;bD&$@&Mmw4 z?Z8;J*tMBXSOlBGw$Z1l)pl-GD{$vN{iXNpJFnKiw&|~(`fK~L8z4Kcfo#QeC^Sp! z>>a+ha|&)!JOhjJbm;5MbOMxwXU097VV15#V7_r*y#htFH}gEmnX@5E=O78shs3!+ zHmhd}2)wYl)z6tXV(5Oi{o&G_vr0zOT2EM$IZhdVL`)>gbr%CqV`c_zP(!|R1Wzg$tI z;bnt5X1S@O;DM6^3Texuj?@YoVF0h~MyR}3qe0eKB=$kASfxN}-Jmf5*~qOOfCK;) zAfG}?2;Zxc&zR9g_osFp+;7lgJ3*nn7EXwbXQhd$$ZLVUnTZsXJ2HMuf#VF^23sB! z4_~c3&7G-2nX(Ap*VO~}Jm%}D#cD(SLleG0={(G`vkubsrRe?ju>Nm=bbUGg z+XSMwSQWZ9sL-|8_qYK>r_tg!qEuKRedi^(a}%)8H|GF8v5w;kE$O^UYh+G+*OM~+ zNtuB|+Hf_?(cc~CW%hxZb23N&oO@p8@n=r|oPS;x@FzTQ`O>2EviJZTa+m0zOV3Lu zpDxosl}BZR@~C9t-t)5jKogQL@M)W@G^pETwvDn)=GiFQWPy#cO%~fI+oaP**(TVE z=|142Y?I|tlzpwMH15^sWi@t*Smkh3UWa+dg9o!{(N>JW`51$AxV{2oa1VyaEokK- zT)h$*Mef7!c@GB7$Kh%EB>XhL29MPrphtd&KKXm}$G^x;P955PGa#z@=G2>>bCAA< z^XlfPndxl9z%Sko!yW&6Q6OZ!fsDVekRKU1!t=YRuIwu1kqa@V0OE)qoW?^}sB5wu z@I23JPXp-P%a511=GVWS=)uCsey$S~ z^-DM=TSAL&O~xPaMfGP&#@LSYS<$1i#pov6mkEK7;}d zMpF(FW>lke6Yx5%`i|{p67{-^%-C1|2I8w$)t5@4)7$mOlLR?i*Mg5+Ls;Cv@ja|WJ=36+l zr*LA2cESm>2!WfKm=BQ&bio6^`H z3W80`I3EnU=|o_%ono7NGZXnK$BGBxYtZBF5sn_iN$dih4;$#}L?j%??$J^P!9HZ#SAfE7 zRs~b*3Gk|h=Liorr4LC;z5kFAir04`eyt9DJE&YEzbfu7eWPyh^4Sklu1mu&#(##Y zWOm(~dj9nDf7z|&Ut-j{&onf|4A49iw9SANJUggW2kNA~VyNdVfNV@x9v%x!@AGB& zrRn6eF8t!&WeQFt_hV))H}o>CpRRAsDpWZeV%PP}Me4f(QlD6m)9_yD3m~sjkXLKS z4xQ@Rk=L8_le>GM*)={bP2GEXzp}QrW^LWnx~a8~$c)~pb*t9b_sqnp0(^XL9o%*P z3Wr4F{8#?&d0Lv!$`pOoqQ6wP)WCVj&sMnzqh<+QJD1|K6GSbAHo6kx=^6;CYax)b z*#v?sd{xk0JnW_(O|=luR$_QeL-Ve~6Nb#rM%g78Ky;qE4CVOmdNl7!G*cR4?gqe4 zsU}#1QX1OE09v0KY|2LzW)J|*Ec)iY`o(4TE^_=z% zhCYWwwlOBBhNE9&-i8L;j%L2n!aOaQS<|3LZAWEyfC}nXJ56P&_g;m2Dd~?&_I;Vx z&3ppp-aOubvWpkKMS($btSqDNvIKbmGc_m)>w!{~2 z)BN#o-g*z4m49=q)R`v|k5Sz;vozhQP8ZheL8QJ}m3G2Z)iVEuRVDbuS|(*MQP6^; ziYMnF@Ws;7qp!zQ;vbn)QvZ?|b5iJ{zF4KVadUMkKj3`UAj`Nl*2Rx*oDLnuJu^lu=;vgSmN?6I} z6CqM%fy9Rs9zqGq@8_hgN7V)tuJZ#Xyk!+~F4%V7L^QVQAAqmGXoJO2zGQr`wnP}{ z3<=vQ^++d|A3{YVZf?e^tVWQjkMelGNP8iWsr5Ri^<9RQ zg)&~Va%)y1A^n}TSV+T_7G>tci0F$%^m67&O%(bV+d?o*+D%c2W>kmEA;dppnI#Zy zE1m_JiayIepvC}@4ruY|As_~YGtkC)TIyib)*cZdJ##HEAw6(_GW6VNmgad(6#*Cs zvVlu=E8_3*MbO&CI;^IqZ~>MThS2!7Xol9Xssg}T&aHtw$kmv>)oe@3kc?HND>0qt z@GrVf|Jx%k-TF(kIsYcY$5+Mp_@)emu0{MHYXdksL{psiCp~HDb!E9L zD-gc+|6>-uiY)xUFvsrWF2ZUL5@<7>O&dMT$2B;JQJupWaBdvRsE@8gWyq|V=xPEC z8{E-iN51SezEl!PHI>Lx8M0c2vahliRbB&88Z3lelMU^Ql!x~jsn_kB+V$W*$DKF?=y6(XhjV!h{uvZZl-FN8Tj?ME>->!mL+9jQl>j4?#@A#THwkfL z?Ep4EpV@F0(E9^ns|YUPho1^3?We-QHd;Yt=q(KT4w17gOHb705#mQ6ERB~9^@0q5 zi4jVP)deYy`T-Ih94ZXKwQ@{~U;}gz1#__^lZfbOS&fuvVcTQPGstWEiid~uJlg}O zSt&s;h)<*cCM#un)^@sx^+&}ncB}40e@y5xUDbt^+APv>L3Z}QW@d5snbrsovmH7w z7K@0hcc5e@b_g74xvW>KM6yFDgzLcY=3+mU8D>~A(Q0>*7I~~aTsA3C@#q{lTw&#b za0NXOlLNmPfJBbp2ro4U0jOMkjiHSyqTVI)pqw<~B_`qWSKdZtuftC~Az&_+dIcs} zo{vYc%n!@syK1$qj3D*;)@p&u1fVilLp3NJ&Pq^M8c@w#lW;P5ot>J70JX+94b)Ay zfHns}n;{DzW7m`|ppRN=l)mn-(lySF)0ehD+4O16GzB4pe4n;Gr9_5&{;r#5Tz31K zcieCeD*^wwK!hS;m0`xA;ScKp?)rcCS6}=JUhI<{S+u)0tx@{HW(k6aXlX;5K@4bK zt!Uto#&!w2fg2_;!^0PuZY6v3qj1$9hJLQup7K!`;SmBW%p;rYMdrD$rd_CtxZKk3 z#;-NV825OC$+E=YJc%)R{(pC-IZZQ7dxVj3dJvdl)WfO1I8o7T)xfixQ5~1NdV^*& zcV}RmfU6C*fgxUn3PF@pEAO63rnn!MqiUr|YipImRy#5tE7C`D4DF@zMGUlYd2x7J z(P~>U+J?{&4_m-O!v#PmtRYy zgn;!jFx3Ls3}K$T9Mt9XCJqJ(7_9~vUk3E%iBUTUP%i^BLGi3AK-EqRmVp`2>NL=B znZh!VL3VV#421MiQ1C093FymUS!KE$?zoWsFH#1(o^y>0KN~6G6VGjarIDeRZ>|#~p;WI_W;)NDQF@G-7J#Cw3ZiI7Zaa&!b&_`CByBr((oWpUWNSN)WjmTU?n#fG zNvD%ZGTElrB-5QQ`M&mKCYhxB>*@CV|2cO#uikrA1rV|`O{of1ukLd0x#ymHwtMc^ zUC8VQd8&|!(c=@7b$G`E&l#vRI4sA5nC(>=%m_*Uus3~pp6x1sxz@CIZD{XWsOs9DN~~-z#b03A*dWulTg}i&-m487MI`U@<29@X3(N*z1SA~bC_vW`>z$7e(Il2mP3!2Nn@g(mCubhJu ztGgl7N5F_Y!&^@dr?qxB6AUfKRc_cYw#-S6nM*tr4O>$kj@SaGbFC?Piq_^grkkFv z;odAcT)lrv&wUo4Bjs(lAwyh_1!ECX^M?kMdu0G{j{ENSwY&e5# ze&b*K1X9c*NV*R}()S>H)3hY<`wJHw&5krUdJC#&*2LhrHH&}r?s)dBXbGc#`+--`o^VDTG!6GU>m0dJ7) z#>$n?oGEy^J-Au z;F|A)SwCBKNbTXV>g_pIy-kAFeC|7A>3!$%`R`l050k>z1PVl9lvxgr;S5rpYxz{Y zUaJ@0Z2*C0=P-qHnp+`>4ko(?l3VD*#$5;fG1JrF)Vb-IW9Lqs0&yBUK)as6vWJ@W z%5q~-{It-#B7{7VZPFPEj9vqis9t@t#=y)T=#ZsOy&2=MYNmsSXBXYA!YRhIyAYLJ zjwvlVT@s@F2nZX3Dpq#6*tk4Kvs1ZQH7R93!SxDO7R&~FE?-Di>k zHbH&buWE+VH~}!d0N?$=ANi{Z2z`9@!vINPmS?|CQ-i~SpY|Lt0 zs&|@2Kyu543F|b-lDZPsBsfAUkX$LWONn@wIxZ(Chw{m2xCsZsOpO{?{*lDt2C^_L z5|3Ed);kd>al_*50=V)N_R!hNrG^M4ef$IlfIC=nV#hKE{NIrTyhlG zJ7R4zsi<7rwUA0RBWTqeCDUfM3nWm}rH%sWZ0`|?yO zMTo;Z=Oh35re-b79a>(8po)#NFf|?n9WtpoQWEJswt76)=igzr5gd$nG{d4N#@+^Q zdM$VaL+8j%RY;<}PgPU$CCmR@;yQZ%mH%x)B_9agkA0pp7GOP$1WwoBIbKn=b;8W;V>>RTK>y^Ic3?3&r0t^KZ_<>&n*w6i=i05lG;V=jL+DoK(Ai2*}yPH#lE4poG>n zJ=y?Mn;va|RLG-g*8aZJkiTS-3Q3=Ubbn0EhJtV@76WG2#%Ze)b*NSd#hTNN0OhHe zOjzVhC+)$x<8F*2mCOLK_93g_wDqz%Q$=x@PL93MMpU@^i1fXLRK>84SP?M%*qv_} z*)1*t`XYr;BYK#0c}A^lKk;9OoM@_qq;iO?iH2ZfE52(F z_R)xLzpVAP?Ub70dWK-39t&)eIBp?Ytv`Mt{E?oGUNPddJ?pkLaJ-UOpHwqts4H(* zqFh4Z?nb<51=9wf1#d@qUBZi>duednJFyQ2H?ei8+8kTGOJjbsRG|lE*vp^FSV! z4su%`!%tXPm+DoG*}pF{TX_kcGEJckpPKM z-8N@}kmyW*cw<$2TdQEgR?iHC%dJ{7=nf>0$%XN$@Xz)fh`~}@ayQYoX+xo)Pa(BV zeZDe_rPOW4B=mN*sm0b!Tr@{!yrjS!|B$V=Ala&Xj2TzYXUNHA*~-w)1BUK= zyBP^2Z+A`2guen{2${$#S7vLM0M@vL!L7n6)0=;UM5x%rxS!Cx%S}{IqfYfht| zc;v%3d=oa}aG6Q+e#jS86zt8}?35Y}nT~@x{+4LvlO0 zqD&70-G%PQfR=H9Jt3g0^s*y>z5^4+3;>jm4ggG=Q#K308w{8%MU-l(y`dBVRxs~I zy?exvMl>6xbQntMrq3iJYKDoVa6KA%1B&+2;Eul(grJ7IbS*fL7u|A*qYLU z(3wi3`BbewXN{jNA`ntn;>&G}_Z5?&jNMv6-ydj=wsu0grDo#(+k9m;Uk&10vaX9^L@p zI`|6)E@L{xOK>&f)hhe~Tb*BkL+ZvGaHA>z1kUt{8-d}fv}PN==Sj>Cj*pYb}1Lrct|DZduI1xsWlndMclgU#f$k6)e z2olQT4Sb>}8ST74{%$!Jx;5X2xpL-vET<@nA>@YsiKPmz`mCndc^eKdt?HMlbKWR!^xKMiQP~#``&S| z?-k@ZRqyDl@-H41?PEZxQ|QMl`n*7RO!S+`F-PFv-cSNE|YXr@A3=L*Z7) z?cq%*$M_0<(u^woV6(anZo+0VigFuJo6*0lL7h}LPnN#}toTkmp*h}PILSdR;v0?e zw_s4ieMsc@aW(5>P@BZ8j|6^S&6xG~&?*S1%YbwYa9OkdEdfqmM0p$fg}of5xP@AO z!n^Q|BW5A0dXQ%5F z4GT)xsdm2spRn)%5C@*$eP40+2T}b#bbmj(e-oNGw1)1v>1+2^q`HRtsE$;nq6ejE zFZt-mc*N6JHd}F#M}+=*ntySpW_s)W>sx;6^Pk6z^F@M4a~9lpP_MPwckiYlrzD3D zkvIa`BIyXh`9qjK_=(}Zv`dtDEU`S0j145?Dc6Wio4_qA(;xP$7UsLUZPIOxYu=PQ zZkAuC4X|j-xgmow*>9YFX7m)@>Eh9TTIg{cP@-fovQBTh-RCz1<-U9NcFR^h&=8RU1A0!O)s`_`|M4=M0W#AM~(?Ux(L&wNkaw z9f2mq=RopQZFanth@WN~UAB6w3lB`U>*@J|iTja~wUTZ~hK3otoUnhcqC(JE4F$~^^( znJ|s{$88!69LdM6X1ZOoPpLKHD0dbBv5VZ!bZneRtF+KVG2orQYQjfIu2Cjh%=;~l zE2Q6fy^ai~JWAKKkRk;`W~zJG`($KRLk8`E5XN&e?{-JL+dy)L2lH@G6s!>g8n@#u zUlJPKicN_=ba8<3x7O$YEL9{`Swh%!$q{b}kS#3oegeWb-9s+1X`%!V5-;*y#(p_N z;U4~#mN22*+0JFQ`Ccfs%WHH`#0D1Asxx@+bXtd(Fsek)iR;o(7mCN5G@yWjnsh#_w$-J44X2S0$a`~unk zeYat9eKZi0yE7<l%4Z)@vk|EKgVReLK%qRuQ!LyU-gS0;) zNB6#vYzPVb7~!K-&pZ$}n2;e!P>EkDB@Uo3u4UR~dz0(aR6qF0Gl%~QT!F7WD3D9_ zCu_$R7judJXw0`TkZhKe3p|s!cTf_V4l?5ck_~NQ47O&cmShW2j%y>S_)L83FGh`; zEyB>886_4)2)9|aq~wp%buTe()cZ=)tRZZ2sX(BEt6Z*W*3 zFBNPN6&7F79@{1-(Q{=~BHxXcDN~I0oJD9ecI7^vT;t!}_dp=%?@i8r+XlJMLSt#z zm!4ePftu8^Z^C~x!|j$nX`tg(hm6mmrExU$PKzZ)AEqGHC>u&HloU#MX@UVlbk<1< z+xKqUxNTtDAYp-L>hgrfn=CkhGB~1QRKno4TDZumcSikKOK-L4ZrY| zcE7l}vG-Ybdf=>q$7hbuMaR1LsEeLr=8|&L+EilHOeME$QJ}51Wct&&T9v4_3f{l! zO-_=TrNq5m1=H$wkFC7R07f;xCm`y~U1xA(U9Pe&RpUY_)n2UDOL05#r*N?FQ@J5J z6V-a{E9a;E8+R$N}4QaFLw30pG>#f+5uctYIB;&1kM)@|K?n0v&bYI#IK7RvbW@fzh`_eF;&^1F=uC4Qghnipcgf>v^ZzuLb zo=#Q?IFrB!8=*VD&3!;S+=&k3O$REZYuX-mc#YeNGokt`U^g$@9x_#+ZHtIlT;!`~o&ArMfzx~|Q*abB3u%EFP-OolbPHZFWd+6Y6Svl_uJ#wKN%u4`R;}Uk6w(MrTzB^tCJKk-k zr>oiVsOoxjuVKgg*uVP2qk~X}`I>0Ij3Gsk^>TCNV}+tkjpyXdNHl zl$4pv4rf#cm2Wpnh-MjK%pRln_Tfg4HZ0n-w_g0Y4TE$}t~J@ep{W>kqU9bqPhld{ z@F|T0Cf|qLEw8SH+p{82n&ORLm_&;oh6iY3kxX=EP$hF%4nYK_=zt z+c!yXh!ZELQn8%P`H=G13~x8kX8O^*cf`@cq<0!HS0@|f4ZIZfVPSGv@0?*C)-IO- zFT#5>AlilI7(4Fa-5xsP6$lKzX68=H(7V$z^!o;X{SOdCmGHF(1#+oK&Kc(Nl9z|k zEbfrJIXTHY5s(}Cbn@^pasf>7>m$5F@|y;eGn<&Ef^UxwBQL;P_}k+HiJTidF__Fy znL;X+UCERD`KiI=t%J$agUQpTdRYub25|JM!PmzQAK%Cu&QVU~4?T zxX{GOj_s*bXL4Feob=vBu^oncAf3pc#Bytai6&mM?&UaY@$ zmF4EbQZ*EV6(#;CiNksdyg3GY>$;J};F@TaMF+SdfZstT{<4Z)USUX`($-%=4uuK1 zF#%{SjM$MFJV@4PtcwTz&f^T_6q7h&!PVuc+KyDef(9}^c1ez%w;6&J=vMlT*oae8 zD&wLp8;9?3Y6M|?WE*Bt`HdZ?@j`Uwge~BM6il6#g*XWGx_+)p;mRYmV~+6AC%Kd4dHkg; za4YX)`Y^8ffhCP`orveBxPiFNr}$SxVc3y|;Ou}LbR$0BjQxKXEPcMSTO|kvUje=b zzwE_liXaSJ(SUGvYeW!S?KcFu!-*inK?I4TDnSHMM_248L({ zzZ=laD6{v#p|@l2u6uMin?4M$p``EmbKb)whq^Yv%l=#gVUj@t2-GkJnMgg93v4@Ko)$d~!dYya`Ww5j)>Y{)u-E{i|rF4}a%g zORhmo>$PVmwrzej+49tc0=V_nWSe~5b{+rh{o+TGYbLf`&(E(Ucfm)=@!Y!md{a1Y?W7vp;)z8%D; z`*4Wu{^TfNei#rxj_+^9F+q-;KFx!Gog*h!=fcS6P=_NQOrB-JIgBvkfWM`2fE@^B ziG!2)+k?M1am@cT+4+%VtA4sEeQzY^MdjS%0*PYxCf9#Yaty!sCdbo4=LdiH#lQVw zz{yuO=D(tWzfaZ{=5l3D1CZ03YJ_6YSV1XSYs*%vWeIBel^tDAfJj4B^%J6+bKfzvGj zK^0K}q0>?czz5jMrW-cD*KO|uL+JyFsTR3Y^gZQ}QH#t8(~lY-4?lQ5LyeD0D5K?h z8yU5`kdn#WXclYo@@>zk>BVAAt{+Y4OVbe<^$mD2K0cFc}7tt4-ea)kofUxj#wxx)=S& z$s`%f)|TDXkNZznVRlAZO@!|*%$|CwI=h1K_?Bo#L@jJi#s_%04$f)uT=q&vvHl-jXAd|}6T#GmxWNmOc4Yr~i z+h={FFSr-BB)ZRIhRWj*DkuzSaF;t=6oR|x6j$MvP6e{C>%mq$1LSp{Ow8?Xj-glj zUVLMFwCwjGgsUfsXDd~hxW%*}>8+&&Odsm8g-Bs5Thoge~MM(5O3&DV4|g6 zz}cuU+k4%p`XSoh4#rMjx{g?%_$xs@yrQ> zwJ?kz#o=P@Jf>`(`Sl5gxu$@)9R;Y*F_`)cC$s$P|8H$P>vlc7-$-hDtO3Wb3C2N&mv_YaZTux)*Zpmh#y00 zBfL4EOq_8L?gFMvzt^QBl*EKEJRrm!7h~{VL8Kjgc%2sKD)u-(BEL(^VhqUhXC!hq zWBi1bj0m(orxu35h_`{e@zlaLa4}63L@v4+>HO5Y!p%yZGjtXE_~Gm28fzF) zddDYlq1yO9w}EXCm#p&NPePFV$KbuMK;r%>Y&MB7ejb|Q30VHSUizlIeZBO)LHMb1 zSC*n!tiNprq!gKXwuuK|Z^b*|@tQTmxOjP{p30?cuUp^GO24W{54wKi%@%yZK6(Jz zZRmk5p2Ih8r@R50H)tSS3R^;KSNDQXVQil^ZS!(Yjw!ayCv2PgKlO~;9sGBMk*Ga| z`5L&vYhJ5RJ%m`O$o~$nNE|{K2AovBD{MPm?0=(32Rd=*6?HLLZhn%2U6*hQ?dp$IxE;LTc zA%&6o6<+%Lc(t*zh!c|4Pnc(C#pSSX6+h^2d} zQpW`sI0918vUFUXdqD1jK)GA;TlK}Tl8{#v%N1ah-^Lt-+m|X=Br#!qk z4>n*_-AOCo9i^3f%MP!tR1rqyg5G3m0~Z*ePp-Do97w(gCsI1MAyTwcGs~Pb=4#J% z>mW^%#PVObIx#9N*3-h$l(zFIsKwOKUc$nKEfl5RE@3oGM6z!LhW9s&P}34?CVxdj zw!gogf>x1`&c41E8g8^=csJL=;#?J;-c!6F8 zjYwG25w&5ZrospA%x#=&4vQ)vYW1RTPx87dnZe=O=F( z)4#|ihAXqP2&}l{zyTzjudII0&`byFux~LchfB>tNX^PyR;nv>kWUoSQBHZWBP`)) zPys6gul57Dn*(@pT?b&+?DUo8Sp;e<*H#+Y<%?0ZiD$|;OGcGVbYLh{OcSm*B@e(k zLBmplzsw#Q&RgJj1Q+6YFBzl!Rafq~e7+mc!7E`H+ya~48}W=sa1O$J)eG}gl6)@| z2e|l>JEQ7_qc9uBc`$Umhe;%^Pd0uQ2U?(z4XIg<{#eaM_sh^%K92^!0A1`g=onvw z2K6g2vwaD^PwykS6JFaR_(nG(0q;c&js5K?f0EyrPgH?ECLkWiJ3*X*@sA)5kM9L6 zxIRbH7i|!~u65uUwa;PndBXLn`Wk;biq?n(>Ur$phoINNik9pvL_&!u zcrDrV^xmC)xax5v2N_>(LB?00RQ?*~=6?~uXq`j{0kC~Cn^aQw35b1y76p-|j^dXd z?j;Vy=TM&thhp0{eRflZZ^1Xd{9hClNu5UAO*61+71dh{pWZ-vSrDBIOxCPT04O z_i=grV$$>ZWCu*dH%7E5E&gjz(!VhsZFQ*O!kxxs+m85~A_*HZkV9H*xQV_Cee`_< zX(6Snoxg&={a;Qtzn0weG(UbWxtWi4#p%`q)qesb%%8%({~usR_%ra2zc90OS7w&@ zgaAB=*G7!*Dd^C;1m6lkXOK=*fqDYp7;e#z5$}#&Dj|GTx=;e|52Ni*-UfC>lI87E zXV6fbK9)L)U)b#+eqp!IxQ?Gioryq;djC8bxB z+ggYB+0b|bPURWoP*TA@V~LW#p>Y7KlyE+o#kXDrtUQRnbdZn2;&K?DHsNor)v)vf zH-Gy5|FM)Ld~v%e+}aC_s7Ve^r}*}1zWu^L@|j^U^k3wc&$8yL!+87LK=S#4@H+I*dLsjn&P?=4q} z7-QpL2$j(K^RMGTbS5qo7`wb8@xZ!aZE)6kiwoC4=o5bSeA}8Yad_rQ!OvA2Shbn& zIQ(S({V}=9ej14N!H*x%b)JZiHBT?RRGoWVF6DkMpA6QgYY_AZFVtSBEQ)Cc$}ucS z3pMx{-tl_&tMqq+AMjHP&5JT*2gLOHEhlIEo@&Aksk;z+Ls2-!o+21uFJS3h*PNm#n#2J|gxdsf|g;{qFnc&hV&~v$>rAp(vA}%P6-?|nZCHvoU2_%q`?SI zXt_mA=jdP~{Ibb^JXgC6qpuGMk(qErRn9zgt4u~4Tvj4{;=<}BigCDHEYft&LQE}r z^Lr2?I__|K<=NXiZr*bQr#vHTm z1VH;hF!go6r7iB#17>Vd{*0#4HPEchKo0=p?m`iOlGVGeuFLj3%2>2 z$+3~-x03&LB>8QAolbsdAo<;qpT&k2P zh?Tq;I;YP@l7E@}tJQn)29ke00@Llk9ZCKj-|kEP*gID{FF%NaehHTdad6w_Iyv{{ z?mj_R_2KE$)%uGIJc-%Sv8hhwR#7hN14h(9Zr52CXdhgM;DFet!SP%~{Dw=lONF5h z8fFklzJp$7mUG7%<)+mzP(>;3k=*ecSBZ$-$L7aKg#iR z>)80MxHNL(eSI+c_ukNl)4lkJ>$>2Ms)@;B{}pN`_73=RKdy!8Zj_(6n`p2RZ-d}7ye;MnsD!+Ks}Q#h}{ zPBmWkJ~)K#hpGNe80{f5!p&iX*tuMjSI|W-YTm9x8TnFj%^4hJz80^|9~OUL@7`Zb zw&3elbp^+<9Yq(%fy9&WJDq~j`(f!ygKlqkU2TGkSlx_M=!6bPeM0|Wk2r}G(Whmk zaC|5j+diUQ!?`*1p^qDHBo769H|Jw!XuHDD)H_IGI01(Whqh^a@6azKI0?MnC23(3 z>baZ2-zN;%JeNTlK&3=oggxAK(F_~>ZZt@0xP=Z`o`SyNkmQybFYW9Db?od1l6Jbt zVfLZ4_Exm@H0bng($BqUgT8d%&n>PWKAXmb6X09mhC2a-Yy#mt0Y_GU5|PIzPvVKv z|5HFB_I(H6hg#v=(G7fWM?cR4zIPPB_cS0UX?B87C;k?U^%;~`*q;rMK8I&-ms9h^ zItc?wkQ9J1v}`cQ|xyfpHT5SK zfC|o5zjV4GxronXA`5uEGkL-JBfkmXNNN4d3(lrDm~~Z`rgb{|!x;N<3E&c{c~WE- zi%59VW4h2$)TFCRuB+$qjU=Dj)r-=Vvcqgw?1JMFNZp#~-2J=IVO@Ocq_nmp{pK}{ zls@x?r0HgCMLK)2o3Vb&I*paya{_SLf$$PQxFW$49IrhBeN!vZd_X|ZFc6RK9t6}g z(xPMK@6N6Kp0Ja&?fLt1D}QtHGO*dP=O4(atfcl{01oxUHS!qfg|vdGA!YbCYurNI zGJaTEQqsnyV~*DkgJ+S)McU$%_v4B7@-5QKx4K>)0RGv_!1&&tS2Y^LF3gXZE(Ssu z1CB#zgWo0%e!FY%d2p-^XmB$GmfhG_5bku#tI4gcuzy?{))nt}a#RYeXLIs_w#A9O85;)KN3(!E4m`0o1=ZVL$j zm47h#P^L1tWfzr!#~%`G{xHHBG=Ab5#1I^Sc(xmVaanfyV9sxS?$Qf?g7NY7Fe+TN zg57dBXWKCFw|OAhf`~%M_CBpfQ7q`14sD!R7o%9r?q!d}xHwg)ruLShea1M}HZId0 z+QSV#>tnp=Hk=zGe6K8Q>J;3{d3>AlBlEqGgCPAj2uf83N<%7>j-cE%BC05#+KAAd zVf;$5S5sXeegNNklWWst^zIX5uO0*=;fv{H3dtxZP~?ttwYk=79C1hMMrLs7Pj2N~ ziEpxAWSgSB+Kp*+Ia=ryf}{^iPYP27dG+1ykO17(=7rTucg**j}vZ&?7F;dyld)sDos zC3o`d%`0D|4GZYj1fZ=8<2DLFd?D9|pZTVDk3CSdkrzDxD$i)c`MwFBix=wKuB(vuU&A8KX-?#;TZmgyn9eARE-fUWA%E`O%OB_F+5KzT1wLaXb zhIitZsd-Gx{nU6Ztjxjb@mSn)kDRR%SbHAE?oYve<+Wtv)BA4yTrzN|uSadJhc@oq z_~m4B=SCdd*x84-4}C0wnfPgb<+Cad3+&u@NWKgn+W5-Xes^d8SCWzOoqd@1SCZkK zsD%SaY-H!ojZ%%RNHv%)@so-_M9gs!ZElC)b0=H-tWB^X-UkxyLq8W# z^A;fBdYE6q*DyVthI{dZHFX;J>5Z^A_?o3qlY*hvETN`O*CF(U%Ol-%{Ul1Tt^1X1 z`-gsxAAo~faXx7qKNBmr5+g%DcdPrYdYm7I@JtLEI=!Q!O&!5hJp}pa7(~Y7Sac__ zuucOj4`XC+mhn?moW!Dh5Tl^bcz?je{m|KniAZStO;^N2fpotIqoZCYDOmv0H)g(%D*!3jbZzx)075BEPxqW94CnTJIoZAsAnpBRa?QT8_%wL#m9PEB zeP2oXH~cKWeL1;~e~4%N?XLi>6JJ3>6Fw9&-87*e2i?gUQsWWWq!OI(>4jfn0CaX8 zB+%{f3EYHSr@KLOx4|TTdvYGMISaa*NBIkwhl`-jcVgH}GGMg`PY3)@W41WpUX&o! z@ebZIpy^wHJkCihPb6S~D6i@NS-up?bA}Z$bfsZy!#i2SALBV^{CQqt{W-e8;N82{ z9JZ5WzGo-CoZKtvWG;jJ9ze$ul?rUZ9AB5 z79KS5ap-JMVktpA0AV(;JG_X2Tn0(Lgn?WEzj_bM*6+g!wfAEn-vab~TOdy+9#)<= z2oxisKL&6MNSdhV6;voau$0mRe(GCfP|9bi$n+nCN5282O81*!ca>s96+W#?3JYNTLM_IpsOFWmG zCu+!%UB@Yp=3^J@l}5GEqNVK|NOmBzvkbWj2@CLaKHbvb6qYqb85nYKzs%GD49lh8H$=yeHXP0E-cTj$dh3 z*%C?@wuC?qBoog=k_Bt}B620LDHJbk3Yjdr7~#}%vw8tjfW}avurX*%U5xNdZTSLQ zLg~VmPIooJ(~Gr=j1a{On>vXc;GL0mvbMtP^8gFwQw!^sV}*3At86^G%8F{VUvh(P z$%O?c-v_WHxtI6ijwCM0q40fJzHbWON96l8;rq4neRKG}MZVKar0s2!@7IOz*W(jS zLcPfi>81WBdw=qGUI1I*i(5u!z$Yqoq-?G5{)}9%cEdn&{XnuEkxSK=DznW+go)o# zp0dH9jx#=9ovkiFN7hzgogAn%&r~Z7sJjE_tIf;RD%7q!%Try|-B@X!t+DC`NgTB& z$4KeoV2gRmEd@Mp%~8?v*bggcr6D^s#P!72elDgq&ibcDaWt%%Lp_z>lI7a<>ilALwz*#+BtyzlPfjnO zJqcA5O7_V~x~iF?m8~hn6iE@bP}T+^mcT+<2Gn`}zs`o-t} zue)KXqP0qd&RTcK^!$Tq1UxwL8sH) z2avmU7^m@Q}* z!=Bt(jrb{3w4K5-HYfd zhR7f&9E>RZ{`o(>|JvXBi=TQIlx@D4DKt|3sh2K+^U6`lhIj=p;kqK+msLWMNy*Rj z!0<3^q!6o11bC7W4mxKDO?mbn+97u4egp4KfWRf>Do1KO1p`z)!UVq3$L2hN9#y*hlz@M zaQjLx!t33XRCIwHy8up?silRMu}Xos9J3{nr(zPqIeK<8GF^SwN)=fhbHFuaPu6f8 zxNr(hLA?p|)G~|$nlGaJO8y?>?rWF}LfeCHE?~_i})E)a8pwfkTG)~cT1?+OPbnLOINwzct|J#xX(G6_}*XqnNVNGeS zwtQE!v{ZSaTB`k(GTw@AtiLG3&o^Rlu>=KSfl@^wHKq?fxi0cD!k07jZXb8P{ z_*v~}Wd(N!)=SHkCcfhT{lk}FD!_PkB!($8O5#AH&8tx|&)w>s7%t&oX2vH4f=nh zdE439eBRJmW64RQibVZn%Au7*;=NIbW$JMxS9vU0Xpo(4SecCN6Qzl9MsQoJo$GG7(xh)iTQIoq4&(dxm(F6xYsdtH87@&;z1qpD!gX~ zg(}O~#jpe7pWAW{&m1Yu@ZqELAs-v?z5;!%yykmZjlqWL8Lu zctfup7GO~?ZcZ#G*4Rc@S|0(7E|y3NYoUYHV$JzE3@_;pVd9hMG#t;aTIVDO zTTc9yeG9@>u(vvBQq-WXV@m_+-Kac9sU*|HlB?$_Am;UDr2ir(dMul9%UWg|X0*!UOs%YiayXju z|G3`d(|R;%R+WBSk4EF#kx%Q2RP7S?WtG@sXE3;V5 z%XLX5gt|09gtU*)M4Td=qbGyPYWEB`d`~~BxgIH1%8Ftr5N%N$on>Vy=jlkg`2Rf; zmah^E-7VM(Cx0Etoz1L;)E)V)2L!by0Z5rLry=f>6SWQs<1wWYUmhwK`xZE!Kp zB4XpkDl^A=uS&;0wE^!6FYXqL94F_nal@gB#EQ=6pW2h`Mx+82jw6<*8L<=m@+HUs z8ZcuQTSm5mZ6O<0pqyw~8qE1Mwp@rbfeId02UUsNWop&u0TM$k1OO<~*&vLvw$#4k zh2?-|{{fz1jX~&y4a8cwk~t#}jrf(BnTgnSqwE_Z2k>BpS_XEa_TTf>I8GEaj+(I9l_7l0A9>f*seWZv0-5}i#?pm(mh#`1Nx!A@S4vYa!smOo#sE+J-V$l# zr^N&rrP-Lsu#;PJ)14f1&-^Q&nwkBiBW-*$Pqy>t3z+r2}5$iwm%Wd2wU;c^Sr9JSY&OhB!=s#Rxo$}g4$r7bSW3y|CqAP5VXrtv`V zjZ??O^d~tzB=!c&l>H&x!vhebabP}TTV^F=q48gY_9$WtD@)Rm{|Ud7cS+v_2Wx-! zB7=4u0Eb_!)Q>3KhxhYyXw)^PzqK@{V8>P@IZ4F@Op>L#)}>qNf}gYl(-b(*g51iZ zs8ZO#Mm!pPGZ9P7e!{Hl=}09Hb1Gg#6oLjXqLlb7Bx!5WL`nBXbk}O4 zNJV$i->S`s32nlc_o6u`Lq=z4LsbeT54(P7SDfQKAY|qeqA0-R!k&VsoM}E|8NllY z80eC7>R?MVIq|JCLyWL?v?MLd4l`fDUC)ShYZ8Ng79lsxnM$-Ez6|@OVu!=7plvQ5{JMbyB1endU`1@c>D{p z14VnnN9YnOQ#24#5NX6$@qaB1q#@CcQ=MXm##zs5ecx=QLHUIQCcM?RwpeXK&h_Gu z%$UF+l!2uk2~^6EmmRMl+g)WEEQ{V#P|kUc_yRQ``Q0MGA3Yz7jFvzeD2YYS@pd!R zTtYc#_QLX6fTfGqK2GhI8In<4h0WA{?RF&_CS0mgiz^{ZfYZ+c9r#Bnp6$^swaEUk zM%-tSBO$mlnm|_Xd}Uz~(ZvnFZUWI9zv37FgxwE(LzF>@;sC&$2#9N{A0s;u9|nbl3@k;KTE{XIW~yBLlO3O6AlAyzlL5J3n$FD3LHUb}Hj!F>laB9VeO9sinsN-59x7n6YZxG1#=_QA+QPC9+vX|; zHc?W&LwKK^Y6cB+pd1I%fI!{D36e&c-Hh3Cy5GR-AhNWJzV^o?qKjhG&7DBaMi^}^ zo#M|Z;sY5>dj;eJJ60n#xw~BCjGd$v}CCP9o}{d3dQb^JIvP zsnpHl@|LVHUib)jfH1S1+%%9<&H>zD0<$F89b#$cM-1ca#f4?=Z{K+AT7bGhu`S*` zo>{A*P7av;+u}HC;FCbNxLD3Ax{8tQNx`2?QUyjgGZ9O@C7C|}<_;!cV-?X9P}jn$(G|Ed+r%De_iI0Q`-g{|V@^~`v8$m6D?K=iwXH2~mmw}c zBLXxg1^iP{ylSKm5KS0+NnkHSf;O}NADf#4U-7`E=Hu%-9pGbnNKymZ$?QaO1$MFR zb3q0u^~54@;S3xTFdAM$Y>=C9PZ@FAc$5)G4Cc{N1IJDB4`s-VVCOOzwewZF(4tKtha7?pxNGKLkx^EHNgaWoafZ}?tE zQ}k=ZjF(fAo?T9uUHIM3R}o}tTyUIp?~bzrF^_Tn{!(>$&Q!w5qKJ$!r6`U({&RI^ z;0q{^KD&IGtm~q*gGF?{3MRrWWTA1$N?f5KqB~<+oOBo3l`~Rc7FO+-x*0aB3%A)$ zbgv_%Rm8QMP<%ehwnURBM^GvO<>oKGWgtvV1DiAZsyB2y@zJBt9Gny>PrZg?N<$3@ z6fUa9z)9p7QY|=8r$iXbMOiZ^r(`L%H> zaDL$>tavJ4@Y0peLqOo=zoqlQr(1?GigqqB<(G=a_{Z~2(AakM;L zGd3;wCcq6#v!c2J2#U6u5d|eRf{wkC(i0Ui#6{etmKii|N2)#g#hPg~O)iMWvrksqNv2CWGE5oV{!+=b&CTLoRZb>?d-nfQ}L&VQxm$f(iM z&a1iP8*-Rhf)*xP6hgy0JmyYFG_pHuSDm9PAd;moWTff%V<29)cSTsDFF~eI8ZERU zko?6OE&3QdTF`xBC*jxF5@scSi6tW!3V#?!3nhm>%^9=$4- z0wt#d*PP?Qq9<3^uu+&U+w*Kk11z8JdRBu<45dPNFyEZ6j^!@FCsuwiofldF0!CSw z|G1KM)J5SJE_}`JBn5FGNVgyWK}fRs5XP{mGJVELH@x37P8)6Y$P@2A_@BS{J)b&+ zv|4=aNJU$DeY{h+6(gMrgJ2A5W2%*7LB%I~tQwi{0V6U%w}S=%h8xTrV7uUWV!xoB zqXHeaz5v~eK?Q34P|ZPt3h=87jROa42~CisCLzusc(_V_$Zq+o&gP9Q#~SA#`^AVm zL%(K}b~r%4Mfa!Y_TT@X-u_)bvVB96$SaL*QOmnQc8EJm$3f(T*tiTmqPP3hj3(lx zk+L6jWB&X{yD|9n1jEcxH?0 zsvH+4Kmo=5<~jo&Gd=4@4N}c9sZBK-7@9AkHfbFk8>}}Q$ATrt0^!#G+6KN1tadZ% zP_A{z{Gp5S>S$!GK@zLs(2?IoJpe81=x-_y9(>}yKl|Hjz_R$_ZA>vpG-eBzwX`2o zaH4kUN?qi^^6XgYo&yIC?8Do=nk8uxRh76})7~*lTTFe|HlTAjt|}NP7KpIY={O zE5KZqTfCWBZmP?*l?xaB+!<&Zj_=bJz*IKiXtopCvtWpN$gdIqH?!6cQ%yM1tgj&C zMLtqZAg`##ZBl}MkeP%zqjAfRz~mb&k-TUa)$;f~@?~IJo0kLdzI|8}s`bl={Y|qM zf5tau7{YQOM>9JLHNK=M9p^TBNp5yBcdg%~s{$f+95y zl+;~b50E?<&ZjM6MQ~_akceQrav8KI0ehB6A^AJ(sLa|#V#R6$sTFNYLU|M}iA?S) zw9?E;|zhN=->{QhDo-ZOv)l5`E(&N{5&8Rf3?3psN8_)SA@qv<~xy z6F+hA))$}u!%5twz}M~V6u3C67R{4}l(~a5_%ZOVu)s#>4NvhYlE1II|E=PbZ zHV8|0m{Q1UI5UHZah^qWPf-dJ^&|wbd$A*k8ESAcOy8MyorI>}j1elWv^c@TSuNlP zt1PytH5Sl@nsrj8$v#y^GOQNiGC~KgWGi%D#L+ z*)`_4JQtIguME>h&}C>U8mOd{nGJ?u+pk(u4H~N&R7bekvj-dDE-=e7QJF`l8^{_u zZi7vWwsiM)$O=prfMB>&hVN##oz>3q3@u{T2U1h&Gn{2!XgOb+@?pb3-i6a`Xh_S+ z-eO&6a54mgu%QgPfFh<#1ume1Hb)PYtUrvD7TZD)SpeA7H+}1bN-aQP(Y{(Zo?UK* z!?xB9BnWD=YTAGKg_qv=&}*A#tGI%OuYDcaZ45SS%Xpna=1U_U#6apJJ7a{YMtF+Q z0r?uHHBIz6E3A+{ZEiR7J|?pZCK+vRr=o->tkHvneK%vV@xK9F?&=z?R}^;&8i2Ks zjGf%x@0+q&w(hIAcpL5M$*tCrkS;REButDHC6+=E=%8^iqfyc{(wVMC51UG4Zf{Z% zv1>?U8WsX&KL!sEMt;v8Yi@M1eFmegU6UD_AsX^u! zXm|Ep9KBC?T0-+$`06La6vf_Kn z%IKI-hhU~qm;i+7ye<}C^FV2gO{<1G0g0Tk3LR_@m~AuE=q+PIj)4nwR_PWWDyK96 zBmrI)9hhdC=4y5NYa5w2*J^Su6|?NNX>k}O~8@j6#>xx2^t`Wz>;J3UO}!C z)jfI?8X!>VIs_|~oK=~WrILF~n|58GIzV3a^~o1WCO+0v7WwBcYZ z>6|OB&7fMFt}M^O+OtrpUzx$&1yvo`c@!;y^YW=o6dMizmZosNX$1#OVK)J3YrGx? zI(G%$Ph7CVM7=nbqIP^=i?*-O8F}Vn}3U5*hf}h(b)r!6NhYuZL?uV9{YGLrx$1dLYg`autFHc>U zBz*18GWZ}b3wz%`zE1XcNyNa*7ADwU{DBZW44ZZVbcZTeKt_1h{4oF zH*ZFsh_jVTIlstLUm@ilI2j|*LcLUhv_eo&CP#(!IxI=Zt-d*p^uZp$aFk#f;g&|K z!SU>U%>@VmzML};8v!|}w4iVh_1%*P@T5hZjm(jY)5lMwhmlrY9eAq2D)l|{TjFEpoKC1O=cku!JFh)JARL-^ z)k$^&%(!%ppqXQ(7Zz9MaPJY1IRbTv=W}Kyc#>5t0H>(ncp?n^HG}~PhQWA=cN&1` z84#3*Tm$Y(4FgOHDdj65EDy;d4tNX1nx-a3WQ+#pX6Axi)=KNn@K^3}U%2G^L4P zbBi&9r7y<>uDOLIC8aMViC1%rX^u%>PP3(^3cmcZqXVTx%}g3;-sd6Xq5(BdlpNJ9 z$t^Gb3^2K83w5<>_Y(OO=o$3yT2#vB6pHb~J$1bxj7R!uDb&&x8Y^cWJ(yl_V@?2M zNBH-U2~TQ`t8M2!+8}K-j6H2IYagwL?mzNhvjVv5r#Eq=PtBPk%~>I-{cGIcWGAEi`~& z4Kl{bmdB1`xS*?CdcdNmi{|19csEl~WDfJGG2%wJP%<^J(%U^U+`wYN)^ctfp+H-Z zo!v}g5RAFVY~wbL+RmA=C|N64dN{$fW@Bm#YK~4?6H-AT*StdlM4xj|@exHHIYhRi zYENJ?1Zu(R{-$hM``XBM6-ZV|GFg%lw>LZjUP+roM+e{s;_ zAj0AmgfhCXhzUYpZG-N!5%TG!=!i>6t*i}B6pX+SUNokMC-M!_h5;v#7P)VEGFRm#}nyO^Rb<&#<^OPX;f25nprZUQqFa-6U}B) zaB)+%<;Us^OSpRM#jtJXH2aM%AYc$uwAy9Rv@o}kK*e9Ea-#wg09 z>gR$bn{t-4{h~>&YK^b3v!k;pQ5OY1CFMDc)6NY}D33x7hw)6TYvc^j0Cg2gqz1n5 zhQB2d(BX!HZ0dlBM4L;d1;a8#W{?@CAx`gFnNxd0?u3jKuOQiGutSC3B1kaP5jv>&%$1GK>YM7ZJR%8sJ;W&hrJMTs{aweE$ zT<8uQ8ni091)`0`d5x_FsoFYN=n%Z|z`K6?-@zHk*REXqo0$F9=w0zPWEjvpIZj}K z>=X3vS4h(UFNE)eX4&~!+OFM9Yv?7{ zhmX7jbE3USMJ&%mB)HSD6<4zfo*SAyD#U9dx>9f-GmvnsuvkfX%47zGzpFV~Y$s$SFs3ebUj>@&@4fB{Q4#P#RGN`8EN4SWyrVL2U4%3fEt=CQ} zQ%o4e+RR*5m`%GLxiWtJso(lXzln%&zQ!^u<7{xGfa74un#QS{Wpctfm6oAqsh5J* zbLak}*AVU`gqCUQBskLDIdx#ay394Ao;aNALwyXq`$dxva&`rsgi8Y5YY)!fWk8G} ze1yPHNO%50&0=DwpDT(aiQ$oO*iSrS?^jc61$S&olZnt_>XO*h)P?G3Bny(#hchR+ zMlgMn4=8YcLgSJPrHNwmqknM!9slP$F24`Nz}FtUqCZ-g6O@Ae?$p-_c+HV&xPT&j zX;=z}{bk6Jy``IQGvvtH`JzpvL;FK#=`lR~+DGqu?5B5p^u0q#!q@FZW2nzj!@;4` z&I52>j+9U0AkX-Nw3KpVXiSQ9XoU8(Wj=)Jq&p&UR|iNn~*R>R=IXssSmwJ{m{dXwWq-#t zlnYBMOSTPFQBAi_8MV#9B`yjopSdQ|A`3DW`mO=2{}{zgu^hXaD3Ag}B`O_|0^=MJ z1}1D_*nEp_i*;L(zjagYv&6v-xl~qRNbBP{?R)&%QQIN5bm2$Hc*AD(0?dwQkG=Kz zCy$+Z;#3fgI*iCeYkOowUoM7S_tXRoR-l6EbR#d6PHeZGo9jb;O(W8`4E8|#=A%*@ zvvhD6QLt%D*G^Yb_g}jnJp84@Hri5)=S`gNO1<8#=N!_VinT3g3L{ujqIB5fF0Vro z>eXJ^Z7L7d>U3VHMINA;Jdi$eC}#-9V4f08);y~DHDu7dd@3%hsA6}OT|?VZjtBY4 zw#1HgTjVGF>okl57PVTX7<9RBw6(hkJgWyYQlJUo?(P=c-MyMlYta1H&}Yti=rlaX zoH7Vyy@J_14V|LWWN6}~Y(-%yTSGbP7hq{M1l0zNQ3!{NRR*cFVyaE0tA*Xp@T$lN zz%C(%?5be((`!~y0h-^FS`TgT;a7BUrT781&Sjf6MyUf`%^Y+bml9Gsgtz(T^9RQs zIrRMJp_A~H4&enqmD?`%=iVQ)4uYJQRGs9McS}u5hC&jEQ%Ks3Q}sGGY58VaPKlN!b^iuAXv|NR3Gv3MiR*b97q#Si`1L zz(q!(yDq25M*BBAmZ=$p18cM)3H;2C*JeHo$(^t9dF+t9+ zDMZHTHhUD&=bh7O7JOwB%xOztu#yK|zfIUy+|6as=iL2cwK`B>2Tv93gMT=7`;TGJ ze03hYQ>E58_^Dv0p`z9f1Fj!O=z;+a!88*ghV$5@5x820`D;B7931|=pMU0q7$RR2 zc;#rCrs;sXH4fA?)nTebPj;wGUT2zs^pBW+MycX{)AS7TQl=k2{2LqoYGab{mA0$| z4t5%6>{Q`ZcUhzBajhXt&xYMa!jBV`3aLvM5aXz!jmr53L*bF2NtDaXFf4my2O3kE z#~tIZOnrbZ$kZGKW|}qenJjIkF5!=oJ%P7suqDGCbHgjT2)AKFCfX*|rh{=!RyZSB z>%9B3nO5DPRa>xAozp^|Yja=}gBKd^`H~B%0$KTZtx+GuF`DrqIvt0{JKyYMMiv4C zFum%~QXvvi>I-sl1`5S%0K`ilZ2_6_2dUN*5T}&|NHn4c!5ca-=W4b@M9doWoYDZO zwlLYlZp2-Te>h;ptZooTZW@3U!^Xn9p((Ti$cZ~GR;zbc&H1nP6K6)I;iG)Oe=UIN z64jpqE@T-H3oqCX-L@SY@zO-S)&y6CA{V#>2vd#|2YO4b8W*XG_H^-Q>$|1Okq1gy zWfNXSXB%!_^IGu4;Udk7L6WU6H5GZy#WATx%tH|tO=|fY7o1Wd9mJ{JX0b#naFXRx z_YOKDR>nZecsxXCl*fFaTI?ee(H1MxZXB!76rL%Z4u6YP!%`6Nn9DAN{wTnKpIX_6 zrrY2B>kChN*=ICgC@{(%x57)AL^OVn#S}VL?e?mm;erd>-G(A6j+^*xfri+^7^Vfr zQ;JKoyg*~h-G~bvrNTUlAW-=blE`CK*x{Xf@7%#VKX%=(?SUhbueJnmbzrBqojU0RlC3o!28e#bh^y=lOfN%arA1^oo}cbgEO3$#I= z=fRb9!S;y}IPB7N<^4)$JLwGa?X)(qv7w=ARO1@bt9d{KBb&CxifsL6^%=soW>w3o zD6X4EagSEx)Y7HqmF}}x;|@F9fIm%MojZiA%yYa03=#p(((e^WiG())MoatX@P=GK z^B!=F(_0dc5!6}Doq|j&IPK{?q}26rn*AYk(h{^&&=Y%M=!A1yLm*VyK>C!!@XY#! zLyA5SnGVo27!O(g*H%>3`LL-IxU9;f7HNlC6L>=xu)iAu_Xsj}@jA)=&4EfvCm?QbB{=sU|OBCO&T zqMS_D709Rj*9twKxzfzk=9ZODli`HtkfAp6SZh84xiC@&@k#h2j&m;GHILO`VbUkv zS_tBRjCV+`lrXT{IxJhH60u4i3c~V~p$JDj71@tQ^p37TG)AbIY-bQ0XZ8lJEH%dw1v8D%s2XW02Nfs!!7@hE^ zap$VwNZf;{O(^mCEmgw;W=a}@pZ8nrL#;)2S!Dr?cXFjVohbP(G{KJo%?2&=7dbUm z4_jgCdt5nER-r~A3qA>|$jHUtWB}`I=nCzwld(&zR)GMG#p0}3u%KsROFHSNRVEq| zIrOU(wk&Q5q2nqGv0;C*L-A+WYFv>yj%n9U*VYcZ`lz*j@9+QBfB5L;B;l*TV-+dO zEc&h&por0_L8Cz`YyIAhO*~ri2t~CFAiA$V^W2drHw{FcM|mjIT#Hol+9;LFqTuw-Z2}=2Y55zbO)lM<0UfRjn1H( zv$Cg>Rsv&BLqpO>E8(TSV=Q@{DABccl1qxFr&bb8eJ|)K7$rR!h9R{fRnupr%SgtV zXVQ0O&Y*{8$*G)ultY+yB6G-Al-BHXxAA$qygBp-P$DIk<3~fgwo60Ns+9){4_&m1 z3Z8s)t(H?u);2996IQZ*@WhzgrfON;M!gvl@0?05tH5bw8jk%M86qvjxK)Z}YDY!L zi&~SQGg;>d0k;je6;6s;^Ea20Z=k&tq-~%u=5vknYT-NizN;c2(zygv3x4fbPaWz` zE$z;2W>#e9DxyVpwHTp4XV+TF{n7^UqYq@JDBDXj0;_|@xzoR$0=NA z?kpb;d8kIkmWs?>v6soV2?a6-ecizIxsjXf>D4@FTc`p^7j zPocf9pK9;@`OZ>{lPy+3%>k&O0s|U~2birC4nVG=QsimHmE1j3ItrM&;9AKUgew&r z>{Rk`z2cHHrsLJ7og(SUYkw!Ay`dW$g=v`(+S*2*Yp-)cwQZe=pyKl;O66@9H-!@@x~{L# zcBoye_EA!>5ZH0&H)ZN|0wmtHm(^yq$4aPq)|My}bT)BVVA9r+C?@2L*oi5cN!Ah4Dd)R$!t{*EC2NN- zl4F@C!GmxIZ?LA&bNu#%9sX_Q%>Y&&dj_FE2ruXdZBZbk)%F;4}FCmOu=FDx<@6qX%OP3((HlC_TVwXl4u3ncmVpO#rzs)E`RV;M5 zwRyk5=otKv7WObmqBZAV5rSQ=OWmbe&mpNiGnU0iTRya!Lm@_#Zy~mnh^NG6g}(gg zXRQPsHgDFs=p313wuv_39W8K$4JH_o8QGM5YP2F78g4XQ17qB9b9|FRB%uSh&_tm# zNHC3Y^417+A$lxA6f=u%va0j7LxQxxNwmC z1niseEG@hg>oPyJmdWyLI~B&zFEjB}rG@pT!dzt^YGuxLMo3GU9cP*$O=i0>#D;9o zayM;hp3--WRLcWeIMseSSov?fG<^k^a6mAfEj3myRQwGjn$SebxJU$LkG1)-WqcNB z$GIFOol0-Xkyi$$ZX35s41B~^e5_fos_oB=pTbOfn@rbI#>w%vVm+mBuz(u{_b|_H zEueuR`1^X7)ClMJ!(kmri!Zkg)LK4l|8S-NK35Xev! z9HX#Hyr@F$)$a zp*&afvKvfrW+#*pgh93%p?>j+dw=j#I6c7En>rbx3a&@*I_zW>HlE1cb8H#@3YiW; zVFlpY$O(2+93ZBPUF5$LDlB>(UDuEov7BO((yFzDCFMAz0>JEv(3butq#;shSO4+= z%7|p*Fm-GV{K9>TJVky_jR~bv-#sTGOm?JBD&)orGjANX*=n;1#SFdb=k1XeM3fK&r0HsD^-X z_P|{`b}Xpu3|le`D@G{ADZ|A$Q2&rKDroW$L!gStHLsXY#C$_fL7F*+z>qo?mQ_x{ zHDx#^$>gNjxTi`^TB#Iz?Gp7Ucl6=coSOX1HjrMDVq7!B$59A>Z-$2Xgy~2fn8>rH zr64OhtRRbJ86<)=!aqezLpGIw%ZybdBHqAm=g4w)jq;hRzJxyg=$u>|S8@0Owg`K2 zz^olZ0yI?R7<;&BwY3Iu_>3wBFnL>*6Y0Hy)VNV+YE!I)HOh!3RyTY=Tx;?RMtIRurd10(I?%at6#(2w`x~uY&mq;Y8JpQE2&5eSQ_7E#) z<-mG7tXL|}BGrsDP17lQ3#%y3Xu5PjoI4dqpq*&j4z_ZBFPfh#GZAc5DBQ1U%!mIN z0Sl$1K8omEg{{lE-&xE-=m{d%*J%*MDsF0gw{~VD5s`o*K-msu`Dvg-w}C(GyC^h* z{LgVY1Y_7c2>jzoGJ_OIY%je5F~!57H+zc+fSe{Evb5n2$B_kY7Wvt@ z)3iOjntRHP3@2BNYTp}JQnR{@>)TG_6tpdh3*EAkfQsK)nA>Dw&`WG30AAynb8g0EAFi=cC-~+$%W%J8gd^ew{mY8!!89e~^Cl z{c?e%7;cei+L<68JmN#lc)$|w_KNVR@Pm^ia0!S=0siD z47^z)y}JLV^5DUzKKV<3{gYVLd=(t0$!-vi+I-R*hL54<7bI0vir0Y*@dDPAmOCUf@p84%ilVHuoj-+1umbq#ch=x zdRjHhbKOi|X*8=#lL$H%k_#L&HP!%vwUx|P|MuXS>gk_ojwA_R2lDsm$0a)kI8tS0 zvH4_WaiyxNxU6MZCrc-4m#)+oE?jKFdoWhI=fHsj`|x&e>78^QO~OEeLn1i-(Jl#v z+sCtEJ)+VCb4k=ZIn4FCL@};A9a!itZQJpVHe7{SFDeq$_?!Yfe)9+fn#aSX9_bc? zGOGwjqGNaM0W%+iExQSICrWVdhYLY#;(*^|$yb71{2~wO@G=P=&XEPFX2Zs0>22;n zk0=YS0dxdAzETtPTq-CK$Mtd!cj2kK#Eb3G%ufAy`9e!Q+T;MTUW4Qh4^5UNnF-)W z8c5Z;*$Af_R9gos#R0(KRV3m9!}53I#xnJn(;)JS=?dGqh}%9M9KG0VUOKdY|K-b< zCui@O#0A&;XD=-5zr66m!b7|UM_Kad5q@@i8`8;Fj!f!Jv6le^+setNPZaZB2_z$z ztpC_Yxdz;{I3q&s#cI92Fo)$w)Ul;-mpNx98X%71o|cP1?sA@~K&LOe$gIBQEMQI; zb()*n4AK`r>WGiH`IRNxI4<|(LV=;w;8u^h|ItG3LWVUgHOZcC26IxVaaqSlVZaaz1ALxQgYD>b|sc8uu( z1J(A&)~+Y*Aa!PJajzqUJ=YCw4fG$mVRMNQM|{yvy}~TornBT z`bY@R4%$3+^f<{|Lu~`5NZ7M15K59JcS zCzj?45r8-?G|w1_{h(kAYa$&{}CgXP1pK?|yfv^?h0*F?fkYX-&0P}%t zm)X1-t|$3*l2pm03(&AlC+w3qZsAJK<)#jF0>^gRc5Uew@g=FN)gxF%<4)B!UnD4H z!c0&x5bp^y!P#)5%uC_snM2U6g_!f%iIE>8wL4MNXP`UoF2Oy0?98-J9#0Tr1$N*| z6CyK+SjgU&OTK&@cjlJm7TvK_g1lHGbtNvV$hElTvOF)U1ko~pB34hyeqS!=!rqh1 z9(Y%7`M?=(15FRpUgpcf;X>c?UC$o4E5AuaLfNmo@apT~^7j z;pazm*ol(=T0C9Eyv0%HnK1VePiXEQZ0^ktGco1z$UC?=6!)lTHsVdi+DqHn2-vqn z2@l&$iMi=myXiC~_R3?e(P>Illk$L#`aLF1PRibWA)-g0m~Gf1w4uOQ~46|Cw3^ex0r@5mDy|?v)_ixBQlO+q<45`2VW=7C1SJD&Lle zKM07hvOE^WW(Z+`$s`dnftX~OOeQ*P5<)T{iin-*o}{5?I&{x42{FpJi+Ww<s>&uD?XOJSFVVn!ioy6EcgEZ^*mKyece4F@w%-=ru(b!Usb10 zojP^u)G3NLgb^}kcJ-PHRWQ5>7Xd*|Og3=m-Gu&z`6cvAPgUQo`9+hA;Ig`#)(quy znjT@+Z>e>Zf3}$w)uc^*NKr82+Al@<+o>4W_=A5XXI@2#O#jGN76>Ewa zK?O}JQ*WgJguiItuDJ^YG2P@B>?tK*dlfgDmgxP(9{7 zQ6sOOxVjm#>moGM&4jLoAg7NyeUqI$LM<*C)YEjYf6ZjYZRGG;HLvZ8YT$C6M$--k zD3q+0vNHutm*omgi0ttP!4%8vDvW*O2niivXmm|wYEbX_b{inx@`H*f^^MKbEqC&g z5Q;q%J{8{)9^IpBUQkiy%PnD~6?&r2d3z}$r@@_XbKK?Itn>9~`%$qv(X7-)5poTycRKxH-yS~Nv=}s%<;!eZYH~=_4U^n{ zRXJ71TQ)c56jO|>hR>xB=^m*Nr?DxE8MPjgE5yooWn^GR#3k( zQBKwL$>l15SK}OIssKxsUM9k7DqrBZuvtJ0Jk|1wzw5y!1ZbWeUXg{v9a=zHeF?oh z)Yc#_KBLG0N36t#YuRlcMYRv+>zqq(?b=Id7`<$SE;_kk%=RRlxebz1AbNf6H3Tn4 zR+2z(ZKkX5ILT5=s*JokDrZqg2^jIOlp!d$x%*PYSUTES$X1($|{ zXovzhDeEMDbpe~OdNjLi9^a{-$=x+IcEqXSnEv-{dhCia=+5+t9QxCPx;^QQNla9c z@A=yCn~P3t*jkTlSexx-FS2^KmFJ!Op~34v_vGG%0@Lgk)ibeG+t*TbzGILTIRSl* z4;(9wc82);^@&*#6C6;jGR^o)WspAbUrnR40+X4Q*J0m zyqMFPeS2pyQ`aV<&rRT{hlQ(-<68MrO+A&ua6<(e9Rur&$6g#7S;4aPk3up^YpFr8 z#DI{>6ciJ}=CQaLdcZ&3T)Jpr(WHssRw5@8OEymejM6&Ks}aE*a{)mj=uR~YIOH$3;5lirVs zf@w~R3BxhX&P^HNTJFxHnw zeJqP`A`L3*$P~Tg4hU`2vcs$o^t*UT#~{{UwWN_UCMcpi`H~dI+Fqu!=>?z=yJ~I? z3&dO<{*aM%(aJy{d$XOcdkyicM=Q9W*VJxIR>;f>VIfvTjU|>(;#{2`terZe<_ztq zWGwjBK)Y~jyl@oKA#J~R^*#7bFe+2d#UUZH5>GO$IZL2*-&}8%nJ};9Zgu-ZgXg+> z*WwaUT++B(nOiijLk2gsQm+24h7_k+wie)4fg1-gGpE-0qlI0HBfzTJ^w8K`tyRQ% zwOzry%9k<0!J-bf>$=BOW*QOL67ZCHBAf~0hVR-r%aUTk`G~m@wSDbh?YY-1%p!#X zx%xZjHLrgX>r|$8Tph<6n6VfRWa#oz(+-cckPW18Bw*vJI35(JON`S=v~V$7{Qekf zLb%58-ImkjGg`kRrIwt%|CH8QeJr6P+cht00z9$${Qd(Sh^tdM)I zKJU=NzIzo^YQsHiU8p%J6Z5_H>|pXz%N?Y^t zsaV>|CluwIE*qgSD;sFQv7Uj9*-lSfndX}wKj&OG(P~F3$pCuR#%YOC&9tkzvfeO9 zk}75Mg6kb8QG*8^qlIsv^j7Sys7%qHZP(e@);VkY`wrdC7iSTa32YVLg;=wHJ;CML+dYl85CbA#CY3ssGPaMAgebBR* zP6$>!@N%8kkIlIT!;h;QJBAyj@n)?wQP10R$17fl1j0gBHJE3FEzSGw4tlGpDl(87 z`PqEUP`=Ur>~9t4h{_t}V{C6?^Jk(?Gpcc1^=p}s4#=r2vds$mrBve^YOiRNOfyw) zT%b-Hj?^Y`r_jdYX=lRZRXj=g+YVz(Y0YbTiU?<8HlVPt#`o3cQ^Lb^b7SiWom%mK zXU5giF|3kpEO)TRHrvCbF*QZ5ZE{jGqDlK17!_5pk)Jv%ek^j7n4zNYqYP0z6*yMH<;UXDv-|FS-(a-@i!oDo zW=IU6k81`65Bx#+>(jlISG0GfNQbhUhKrB~qghy!ZP3}>b+LRd5CSUl4 zH|pbC$F#ROmku>ZSVT@yg|bXIXBRQ+Vwx`TVUKDnOJi$ZJ3PF0pjL$=MqD7h#`P1o z2QmyhGL4LEW=KZ{l47J9<0k9ga6a93#6`m`+3j+yr90GW#CmcH##tIM8f3{#sS)Pp z0_E7g20cOjMQy8b^N>^=uCtDE&M_}FmB`>b7}`Fk+M0EbY@r-nkdwvQ2(ARhUpUly z`shk}F03N+aAd5iaCzA6Va1%U#-+l5$F0Ih#FRI4p$G8#iq{!D5^95>3{6aasF4L- zP<+c{Fs|ac=TC~x; z2Y=#;y!T9f#Y?VK(|G29baO<7tU3XD60e&i`J5lDmT>HmR(+A_X~_oaWz-Z`a1fdw z_XmVdt%6IqakyO&K&z3c5_iF!TO!<+eRV}{ikN;Lc+c#P=B%q*$n6;G1t=)}<5tm#LH!K1U27WUdZjsj>_o9# zhZ=$F1kFUsGYU+MyB$6_6pJd0$#Hj3J&~0 z^3ruIRK0y=Tp`&E=HPz@0MZI3*HW!F<#HtyC^xzyXi+^gx}X!NVZ(uxQT;6eRhzdP zA2s87#!kX{J+;rlJDU2}4rND`RVcFKim&^(CEdh$Dh9uA(~O)NJ)&L3Xq@C16pjxK zYqu7nb0C2(A*T-`q^USy1{ktVCKg(Zqt2J{I`AF)-kdHpySn5Y9TJP0tu~a{+v0J% zqE>f@Q4{;4_UFVA8F(F5OAU2)E?Dpj=V~ZQFuGuU&GX-?x!?;p^ukXN;UrWHTOC2Q z2U0i$^s|`2Tcurs)q2hRy|kF(S{l;n0yEWQr96SDaY<&k zc~gwS8oTFw(P*0|ED#1aHGo;$uuEV}0;{Pdndc>QBD_TV=?KndlVH5a5>NCj*1 zUA6Q^F}1Q;b?d??mG!zGdLMiZ%_7_=f_bZO&VSrKs!sydEG45jEREl^4+|aC(-rl*{mU1 z)6Q1$Sa)KaHq5G&a*b}%)utPt!VG{rbJRl66qj8NDkO+OSqp1apsWyBJ!(jC-F<>s zx^GYUAtz8>?Q&7Q5cac5EK9`_*INW2a9|ZsWnwovSioCpODgI9Hl8SA^)4vi(VAV< z!7j?@3WB)h`?BKv>|#B()7TD5t<>kvO_3yCptoyUNcc<(b%Zy|A7-c#)0!yEBVQHY zK-e$X!kOD3c1_Ei#RdTisX zfU;C?=N+FHOleWuU`iM{y&Bmou1aNAerIYIT6<*}%SiMrb#!?M`WLrtov!TPY2n}@ zw%Oc^;po&Y&v_Wz0CTeWRj41;UK%p9bg2=p(@On@VEP{Go6iG7Omf`-_BGSr5Zuq<=k6_1;i z6bA)n3FYZzeuqZHrKRh7TU59D#T|XNC}Q=eP;7+&BT;+p?{>V&#j>t&xbXKqh3VK@ z6I-tFl`3e+ffH5DHRwxjy=-%u6iH-;sjUty8O&wP0HP>A>u=Zk)4uxf7>N^eTXz@y z!PRZ|mWUXZ)mmvot5aS&7W*{*$PPeF!wgD z|K|M%z8Ui>Q|OD%xmCYCZ(y=HQ6KA9{G(>qe96;3j9W_baKLwX)QN%bbxkmmtrtc= z&%^UTkRMtqWW8PQ8GEaK5Bn3=dI(Zs+8aZ>Ky2he$w$$A5R3S)>$ zRt1;Yp<1NB4|d;nezp;$Xvx9aIio4X0OE`Sj;8LuIl#;zuud}I zo;;Z7_?Lfx;xHR<7aMtuAEK?HUf3ob5FM<^3kSp26Nj65+#hE8(MY>>Z0Xs6d$>41 z4kbp<3uZcKIhTi;>D1P#MYsOwh+}TSsACHKfyL~g^{BydT$%@!1WIbZpD42az$x($ zI|iLry}me5$67|SVsFI*`_bvM>J~Y*&p+@OiHD-L9&~pPd8=^3Kx((necQGd85y!z z!X#wdo}*s6xkT%hIpl9U@;!a%y5mdDj{Texy7Le+lZ*$)u=8K?hCyC=dXOtsmU|Vg zi-|a0y^vR+;0DrydfQH2+S1TVLBX-zzO`<8a}vpHeC?7MRe4_Pp`WdVX01RFB%|j>rJ-h&za#wiD zzvDkK&#L;X?eWT%k%~Hv-Z9^bwzuV2m6s+cST~GEOjf58fZd4~GQ8`cpuxGntvwvKB1}2*15{kXdSGh^TFQ%O^)iu?QT)t>&eT z>K;89o#i94o$5oj*<>?Yhm=h^4^l6z7Ofl=Lw$fjiUq~}Eo{*xsq*^Z?viBp8@ zLYAXamFtleVg`O9Dj6P9#gl=3R&gSfXJ8yZ$&AXC-3=W%=~G>O z?8e`|>m^WCnX=pZMqND%anb)1&-q$KR5y4tptK!4O7iLvL=_-XQhl5A=X!1JN0-oW zrj5AbwP^F(KXLLS>2%9^Qg3l68;H4K)(&sQIZChC=6KXFijM>*& z>4IY+(d^(0ZIwUR^u3cme!%?S{~w%kU^+2luE5rg3C=Wc1&>_Arxd$p)sew1m66Go zXN?(m)4b}f43XV}hL?nuG3RFw*lUXivfIn@)?*EG>nDu=*HyuT6jKk#GBXPfSvsj#*2ovO7fjEF~n;_IKrWGyRX}Od2|QY z+n_)2e;jJS#q>Bd%8<8Ba`j!DRMFgAn!=v@2Ix^68;l}a zoG3LfNO+EslD%EQ#4B}iSIJJ-e-<52)oGp18J+E_ayZTu``I4dJaY}eq3$KcpzDDwFfodg?!*?n&+{$e}LXob^~A!BcGh;*o< zdA0z&+Qcb*O*)=_E>EHZ+*9flx_hvnp^pgLOl6--g_d1X8C!->cT48sUkiw?F{8DN zNYEHb>;<@2z{!LIl)cw#d~z^aD3MQ4cf0MKfWAuHf+w;A-V-`!EDYDd{K`77$A;Ha z98Fq|77|id8Gk5^%nl|?uEq9`rYbAe0ZlQzsJ`uVVrsp}%qns~vHKm3T^${r?Uiyno8N=k&rjZO8Dr+Cn2 zxk+H!+Z~fu!rSKZ8f)x6sNVF0Na|pmG)up@>fUd46$(rzbv$V@90A8miwGTUX3YV& z$+c6{<^To}HIc;2Vsc$ksg<@?%B?dEs#z7I+^Msj1!RXibWKk570cnO(sU3j*Llo)yrQiI&3N|fQ8B~e4>eZW$_i@le<&?=6;h^T ztC`e_I5VX}ghy+f;9cAEL0B34W6bf9Zl~8ip0k#?_lBWZVq<6-%wv}AFu`#>-72@8 ze3a0SCmb$&IlFOMfD*eEb`W*Sy;z>qt`9v{Yk-PI-)Sq2Re%+WOm$(VopXH8;C-dl zDQgM~!MU?)y{!HD2$WIWlp+l{RD!yNg|wY%_39#TqQvQ9gmK4V7OLs+Ffb~Jn(4H* zh*&~I(G^nc&B^z89XuZ_K=stlgRZPoRRAm3c3TDWGigO}Vt&rfo^IGU1V4V%1kAVP z)W#tOKh81Sl}BR15M3X}C~J0m>f1uijK@ak^6>{;V!Rr}Clbo@NUy4wdnS<7JHgV( z&)IzDO4`mSI#4IO1*(b~ZFE|mAaMmK%aaDu6TgCqIWN$3MJFo>qPq>uP#r^ToGPqD z(0v*AUnw=F1OAxGO;)tpVMRJw{l?KOZmd_9wf-VTk6y2&8f@7!%V^NHQWX;15X%f- zAcY7xO?2eu`exY}*XgcP_)4tCx?D6iMmcdjxWs!&C99J(qD<;?skc^8!|4fYD;TyV zp_SPhuB&E6YT?ytw2R}tYq4F8Iw@vVXvb$(+#y7WWi$J4%XMfch&>jG#NbCSP=h6H zCpfld0b{gZiVE_(#V9SE;&=R6!V;AuI5U7}s<&JETb*4Uz_#i{@A|=k)7Pw6?-Y5G zLy^Z6v);_%Wv%GZAHo5~s|8-f1@WmWFC^|{ByAm}nn!6uM1Utf8aRh09^0x54iv;9 z4?BUa`BoK)nFpxi9c#b9uX$8ma@I0m0n=pswokriSZ{^IWuOcO8&9P8jEui zm`1rR^p9_CLrgtS0;5Hb_u%Xj4F}TDp=Xe7;p;oi3d6SZ+ts%XeZ%Uc`8U-X&HvIg zvFqAr2Vk|ie$(VcxjqFOWo8UD&7<}s$HUpd%rW)?0F~j_mj8l~BJ;4tTvMN#F>H#d zov6T-ha1MK6Wu~KxS3i~BASKVj5{;+NKBf#*mz<|6{&L?S&v{w{#I_WPBZ!!2W+No zaUtMPEc=uroak%YLR}nHD@zmmet)?H>D@pXe%duSPB4378|NNv7tz+Hd_cK*W^=JE zUMSdzMjFpni91hfH%(6rEIrIZp`=>9| z=dve*q-!Lf>b~S(u7Y8JZ9cMEwhG4yvx9cY2}T67nXa_e@L_|6rXak_4C=6Ae1IA8 z?M$np7Zkc7dAzoiC978Y=4TdwVJB(saO#*+^wIh5&|@>BWDhUvq5Frv+;p3xm}!aT z5o4y|s#a-*zB)lR+y7)YGsr|v!^K`w4$%EeML0kJEACXb%DjU2Am-*9E{-aT`?`o) zmN>oxMsb)Irg=bl4+oMvZg@b#P^Ot3n5Zu`=c<*zq7h1v0aaLr%3=tE#~BhO7}v6? zT@12A{dh76QqIS zm1xWh9q6V~Vw@xXLl?w=Ysu0s6;IcF?}WCN=QRurLlNz)l<}qLtGXRiW<`wZAUL7v zwOj6zPg5KIVT#CSj>D;ezKxZ#TNb#X!ISU2x7cK`ZaGBA7S9uxpcTx;2oK$@Dgk3< za5u{CD%uhg8!iwZ-S#VXHr&!2UKn+8iVt3M z)n*%!kIwKC-&I8{(NZdI#0az{VvCu=F0Q&KXJ;!r=I6%`5`J?W#V;()%EO#neVI+K zCpk#73l96fpbcMXQK%ttT3%-vZMaCfDsJD2;Ft}&fbxkXqC?<@hw2(=qV7fVdUh+J8xKEdeL+P^Ki z`EK>k#&)%3v!TL$-ln>fqi#D@xfp~!%N=3f_5uIHU^d5&Noorpzec4-M`_dckWjxFlB=r_^z1;HAck* zhX=8BDCnq95i3H!NUs;uI=%GrKz`9}WrOkU)*08pf!1fC_%k?q$u+ExL3*ifo@(Jn ztAFN^MI5J7V!QPXGk2RpjQMUHac+WGQmm9(vs_fF!Si%!TqXA$`V#y~P$Ut2y{xEa zzCcLYnr@<#v#hDkr9I94v}*U*x4C)ENJRa<1d!q=hJh6?7YRyN2_|?@A(ePeOHmsz8jaT=5Fq^c_5lkRC%&8mZq2`_$SGV`NI6#t+My% z9YUe2IfB}^5xwxDphs98`_M2J+v_{oK^O-KmC0z6^orp3UdOKC)Kh=!EmtBRa{%R!6L2-2~cWQ#Bpel5)-sS*p)l?)h z-2rTD>P`0!E1_zgqkyZXX!0q7sLs_C^0+~>_{CCEA!|DN+;&(7C!ZB}dH)@&D+Tg^ zb(KRdCYTwO^H;@Xu}X?o7QuA!GAMUODGLf>el@CG!(BEH%}nh}+~z3F{Hm3VAgv;^ zh|-Jj)8{Z2Ly`IU@xP->W!w1BRKP;|+BqUKmyzAWMdfKCI3}h?_8^^Kd<`X!j5$58 zz8SK*qH1fS9BS_{c>N)S9&|g#)V{dVsB0+@uJ2h(ejK$_T{mM*zf==I$T#NGY@i>7 z1I zq#E5$2)26Hv6~@?U+M{-32l02ESbo8NyEK8UR$k|aWJR3L5pH{>Q?c{pPXH<%;sjfXtU-&Z*=Yt)W5m-f!J9D)swn{&9mU-ELmcdYf>S|jl-W!a7<{9@>#iV9_hudry> zxw2Uf$S?}>`L5SxhRc8p_I!2UJrk!q_-?H8m~a}RfM{Uzlso&p=GO6v(iRhe7d;M# z?Hs*d=Y*P{3}m9uv5G~2S{IBWnt%xqtFgmnkP8^8R6CwHGV%9JtUmOnJk;@(EvptD zan?tU_&8RrO!Ja0X>3HWw(*I9Iu7+V;7&WSc2fhlTr_9UEElfRTwaT2nZYJnl~;~w zBKdw;1<&j@DEwtiUwmuW)GGFjknM6Uuh(mp(s(BC)`?08w;aF3xtg1}nhx;R^QSl; z+7ad&!ZL1Sr4i>-YC-_F3jTF*uF+Z@#~*`iEBRBLuUyAUAZo=5^_$EoyZqq(<%>RW z@7y^r!#QfE@G2FgA-ky?dwbdrjOTEMk-Yp#td&GjQGezjR88i zC4l2vrkkT|C0q=F+acOz+fbR9f~`Yk!-?Jj0@5XoM;ErJZ+Y;maphA6UC9m2l(e7@ zt4mb0E11C%Ibh#Z2kBGUbfgi}WS7zMIGi&XHC+RnAG7$?jpKE7`(vUS0lv#PGyHMJ z8I3D1`D)=63;XI1y#LpaLxeCTg`%uUhtflxovxL}H)6X)U24{=K|-;aV7iQak%b#} zTWjysZW`*3sXL>b%b*d{ z9Hh+T5o32qs~{w$sclNq#_-PImLQRtNQ5}m*OzU!q%-B%Ei9LR4Q?mb69-hDkOm{8 z&-8u%6Q?cwG}_J-#&a+-C>-Ovm;xbqFu~MEV>Wl%MnE3q6GN!J;e3Q3^&BF*oVQoq zUwq?Z_oJyy$)Tabbr?3BL1Xn1I><0{{~&{YxFdPwhg62O4V?SHh6-{f=3!YAP(}$c z^&2d|)_h9tMyL8`4icGQ42$!T9534A;5}!d`Gvx^vkyB({bByIZvl51sLz3gU-t8l z)q}(RbBETfocH{Fg@SzbV!V1!`GD7)k4#JlGq?Hdm}VS1_v-5YHA5>0*AMruUMUf_ zMupp=3k4SbO(1YM$~Rc1Erb8+M({7b@iYAE2e5oEF6jWiqCR}@An05EJp?Kb%8cKA znSE82xiom!_I>~mSNezmU_1>lrj@CdKw&FB5dbmHucSa+i9Zi2@lfVn6UL<##Nh$N zi=n}IBC>d4fo}!FF{nT6i>0k#lH`s1=O7EG`e%E9X9Sw84qtz$=iPfh1dW^NKh0D0Nj62x6nGQ!ny+_ZBSUAXXI@Cnl)0m?qF4^Sxh za>#S`b-x?4Q_Wul7;E1kF*pWI^q^#2-gesLSGS-ZOs@=Z7M>I1h)zqB=h1(C@uLSK z_zV;0Kl|A+{qr}r08>Gd(uj{d@z!Vm1u#sEz-vD{rk9MgLqwFi@n((pv81s-Te#w- zpG32niWV6E?wGEWW9%V+LuwA%z`s9m*=2hc3QQr|UyWwaT$G{N3(|Fa&jaNvevNs6 z>ByjNhhm2C(t+LUV-}^cHXM1;LyrQ3=@kLi;M-DI>fYBh)Xxu|Tig%iVmdy68o4Bf z@&ghUA@~+?hjy%z#{JA;)sNb4J~qI;eOlr=u{8jHV7Mh>`OdT6Y0zsA*l_W8Aw8Hv zZhZLdIiPNZm_~fEG`;e@=wGIg8=ri48gUbp{EAXE)>q4wLBzHjnMh;z{Nj!=PP{}=>g?x6Vl5|Lh@f0~4kt_t`Q1hmRxyRRqn6%IOW5T7l~APig$) zR(2#$}{Ne{@J%H((X`!PwJMg$~b&lO~yllMd@ee(N?qq^y z;66L1SN-ocMGV(P1KLeLk9^_rL+*v5&NSdkVNiUH=%XPOOM?C z;MGr|vzg`u1%CbK?F&rz$B#a-;9I{%1DF;Cn9u%O5>pORL9FGlTaDx~)XU!a^*3^j z$rN(x@@LY8bU3>H=joSz{1DWfDdf~QJevewtFQOgjTkD!fDCJ5nlk$i-?aDbFDVq5 z=u=}qJEre>E{VNP1xQ;_87(1B^jY*EX{EQ_zTe0bmqREpg@WNLvtA?=1UF-*??X?i zrwjJI%9vF zrchJZc33{vYTRCjo2l}&t>1s_4a=Vd4={zG14p(8eO0=qH+`*mTVWqG8Ho|S?PtgI z)X{B0(~uv&>E@g70S;4G&o7*i59!PpY0&RKd(@I^(Qu}45Zv991horlZ8*PR#aVZtrCvW=(693Ahu3p*=amf_o zfAQ5V@x3*&CH}h6%f{@zUOkpU@b8Z4zU301Mu2|Yq=p;b5T>iqs5e^8E~Na$S6=gU z4{FVHT!4JXV2nHfGS_&=duv6Vum=qu@G*N4ZC?O;{Hhq%%10sQJF5CYvxgs91BPLm zAC&R()8jHu!)=4>v<)4Uk*l=SSZ50=-~Z2OvePMODZucCXwhW;Ee7^BFuW~@V#yZXE24_ zv8WnD8vB)=jI7R)ZvP(F+`j!bsMbuOeCpnu11oRqOM~un+Br*}hHPS*6Ey$GW)7&h zsx6KACJZ5ddChTW+OMZ)aZgg->`eOJ@*h3jHv*dpmN55$?0;KJ ze0!ZsEAZ=n^zf5Whq&cqA6i0WzI|o_$Xt%2E zl@DF?%+=srrjUE*zdr}_EFbYY$-PJY>a7)jQN^e>Br0#mr8s@^f<0@Fp@f5+wvehiZl(`$k@-SlvaB0BYdPZxUez;U&vZ+|nl`zA?w^fjFQYbTSnt;Gy5%JnuSCgeja{kN!mpFwcJ_UELp_u;V)yK=?C- z6W{?)CD1b-6KT|?uY9m;D_DmqB*K?|oj{d4nrXn5zkkP1js=)X0adsDCIRS`xmJ|D z=)8YhfLX4>uv|IHs`SA9yAKDK|N@fR~Rxdh!AH|NN5| z&c^*%NPhE-0{(~3N@11rNRZpg^G<&DP5w#U%fVl5%kTF z=B|Mo`d)d_c@OvQ4Fwe`?D#onw7}9EDRJfg@b_C5{^i{-9jRbYGKJ=X_n*}cP%JfK z=wID-O5ckM|N7Y-s3%jngEaX17+NamnTD_L-?ZqiPu>00Anb-r;aEeG3Jqka0e^B?r^*dBlM9@i^|Z z#pm6Lb~A;HbN1WQfXOY76!fPKy?glcXggDAS0TXP)%FThWtD(iLdO^yHV?l#sX25z~m%;-#PY;m?Pd?}! zzhD0|%qmRb=p6Z%BzkhgAx7pDbK#0>7M_afg{dpxkgI>1M)vy+DZG0&J$6MII831q zamdfocxt;LhN5=)p~34v_ayWhrrAN&KHmb$>^5Y0myNyQxzC*RejqW;2@oH9GQV=( zgdf+k`N2o}mK=Y{$_?NrrjVz;^i&da%(%pb7W^2R!(hL6^*x9wsK6A8{MyqUKuZID z3jRCiHLrgX-Od!kFaKR@_>BEN1%LHv5yg-0wdiZ(ciwjf1U*y8iR<=>i#RRUbe|kJjp_2#qtFbP zjty%6s{>-_q-{N}FBO9a_q*yFb~_^+--jF;gJw;W1)q zgxfjBJui2a%dWfroA)31W^@}=j=2SsGE+F-u9%$znP*l{!9RcU{)bfn%@l6kytlh0d}LLRu}O{i=5R}p z9n;HtT4GD1dJLSp*x3U=+nWO{ZR#;3>l$Ah=^=j!E!XRYV)sIr)MLcd&4C_q$tf{n zTZ?+CfcTarU%-W@c3Ob7W$Qbaq#=k+h=bMm65cb?qJ)lZ%&AtzaZ`Mmli<;?BLvx; z&hAaj8IhMdkLlsxa-oDz8GqyIrcOnAyp@d8h&sY6O0@2;r?o>v2m;Wx%i9s2Q@|F9*28QFbf#MHSOkN9_66NrJ?SL5xC T`L|HGhX2E^#N8J|U>5!#JIVK2 literal 0 HcmV?d00001 diff --git a/briar-tests/libs/junit-4.9b3.jar b/briar-tests/libs/junit-4.9b3.jar new file mode 100644 index 0000000000000000000000000000000000000000..8c784e5810eed4a350abcd9d48afe20b8f20eb2c GIT binary patch literal 247280 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

^_~(r@0$O<9a=#?h#$GYP>| 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%0D%aa=dyL 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 T-ggQ6lB5GLrGkS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/briar-tests/src/net/sf/briar/BriarTestCase.java b/briar-tests/src/net/sf/briar/BriarTestCase.java new file mode 100644 index 000000000..32f496aef --- /dev/null +++ b/briar-tests/src/net/sf/briar/BriarTestCase.java @@ -0,0 +1,20 @@ +package net.sf.briar; + + +import java.lang.Thread.UncaughtExceptionHandler; + +import junit.framework.TestCase; + +public abstract class BriarTestCase extends TestCase { + + public BriarTestCase() { + super(); + // Ensure exceptions thrown on worker threads cause tests to fail + UncaughtExceptionHandler fail = new UncaughtExceptionHandler() { + public void uncaughtException(Thread thread, Throwable throwable) { + fail(); + } + }; + Thread.setDefaultUncaughtExceptionHandler(fail); + } +} diff --git a/briar-tests/src/net/sf/briar/LockFairnessTest.java b/briar-tests/src/net/sf/briar/LockFairnessTest.java new file mode 100644 index 000000000..5560855a0 --- /dev/null +++ b/briar-tests/src/net/sf/briar/LockFairnessTest.java @@ -0,0 +1,161 @@ +package net.sf.briar; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.junit.Test; + +public class LockFairnessTest extends BriarTestCase { + + @Test + public void testReadersCanShareTheLock() throws Exception { + // Use a fair lock + final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); + final CountDownLatch firstReaderHasLock = new CountDownLatch(1); + final CountDownLatch firstReaderHasFinished = new CountDownLatch(1); + final CountDownLatch secondReaderHasLock = new CountDownLatch(1); + final CountDownLatch secondReaderHasFinished = new CountDownLatch(1); + // First reader + Thread first = new Thread() { + @Override + public void run() { + try { + // Acquire the lock + lock.readLock().lock(); + try { + // Allow the second reader to acquire the lock + firstReaderHasLock.countDown(); + // Wait for the second reader to acquire the lock + assertTrue(secondReaderHasLock.await(10, SECONDS)); + } finally { + // Release the lock + lock.readLock().unlock(); + } + } catch(InterruptedException e) { + fail(); + } + firstReaderHasFinished.countDown(); + } + }; + first.start(); + // Second reader + Thread second = new Thread() { + @Override + public void run() { + try { + // Wait for the first reader to acquire the lock + assertTrue(firstReaderHasLock.await(10, SECONDS)); + // Acquire the lock + lock.readLock().lock(); + try { + // Allow the first reader to release the lock + secondReaderHasLock.countDown(); + } finally { + // Release the lock + lock.readLock().unlock(); + } + } catch(InterruptedException e) { + fail(); + } + secondReaderHasFinished.countDown(); + } + }; + second.start(); + // Wait for both readers to finish + assertTrue(firstReaderHasFinished.await(10, SECONDS)); + assertTrue(secondReaderHasFinished.await(10, SECONDS)); + } + + @Test + public void testWritersDoNotStarve() throws Exception { + // Use a fair lock + final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); + final CountDownLatch firstReaderHasLock = new CountDownLatch(1); + final CountDownLatch firstReaderHasFinished = new CountDownLatch(1); + final CountDownLatch secondReaderHasFinished = new CountDownLatch(1); + final CountDownLatch writerHasFinished = new CountDownLatch(1); + final AtomicBoolean secondReaderHasHeldLock = new AtomicBoolean(false); + final AtomicBoolean writerHasHeldLock = new AtomicBoolean(false); + // First reader + Thread first = new Thread() { + @Override + public void run() { + try { + // Acquire the lock + lock.readLock().lock(); + try { + // Allow the other threads to acquire the lock + firstReaderHasLock.countDown(); + // Wait for both other threads to wait for the lock + while(lock.getQueueLength() < 2) Thread.sleep(10); + // No other thread should have acquired the lock + assertFalse(secondReaderHasHeldLock.get()); + assertFalse(writerHasHeldLock.get()); + } finally { + // Release the lock + lock.readLock().unlock(); + } + } catch(InterruptedException e) { + fail(); + } + firstReaderHasFinished.countDown(); + } + }; + first.start(); + // Writer + Thread writer = new Thread() { + @Override + public void run() { + try { + // Wait for the first reader to acquire the lock + assertTrue(firstReaderHasLock.await(10, SECONDS)); + // Acquire the lock + lock.writeLock().lock(); + try { + writerHasHeldLock.set(true); + // The second reader should not overtake the writer + assertFalse(secondReaderHasHeldLock.get()); + } finally { + lock.writeLock().unlock(); + } + } catch(InterruptedException e) { + fail(); + } + writerHasFinished.countDown(); + } + }; + writer.start(); + // Second reader + Thread second = new Thread() { + @Override + public void run() { + try { + // Wait for the first reader to acquire the lock + assertTrue(firstReaderHasLock.await(10, SECONDS)); + // Wait for the writer to wait for the lock + while(lock.getQueueLength() < 1) Thread.sleep(10); + // Acquire the lock + lock.readLock().lock(); + try { + secondReaderHasHeldLock.set(true); + // The second reader should not overtake the writer + assertTrue(writerHasHeldLock.get()); + } finally { + lock.readLock().unlock(); + } + } catch(InterruptedException e) { + fail(); + } + secondReaderHasFinished.countDown(); + } + }; + second.start(); + // Wait for all the threads to finish + assertTrue(firstReaderHasFinished.await(10, SECONDS)); + assertTrue(secondReaderHasFinished.await(10, SECONDS)); + assertTrue(writerHasFinished.await(10, SECONDS)); + } +} diff --git a/briar-tests/src/net/sf/briar/ProtocolIntegrationTest.java b/briar-tests/src/net/sf/briar/ProtocolIntegrationTest.java new file mode 100644 index 000000000..bbd7af2f9 --- /dev/null +++ b/briar-tests/src/net/sf/briar/ProtocolIntegrationTest.java @@ -0,0 +1,264 @@ +package net.sf.briar; + +import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH; +import static org.junit.Assert.assertArrayEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.KeyPair; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Random; + +import net.sf.briar.api.ContactId; +import net.sf.briar.api.crypto.CryptoComponent; +import net.sf.briar.api.protocol.Ack; +import net.sf.briar.api.protocol.Author; +import net.sf.briar.api.protocol.AuthorFactory; +import net.sf.briar.api.protocol.Batch; +import net.sf.briar.api.protocol.BatchId; +import net.sf.briar.api.protocol.Group; +import net.sf.briar.api.protocol.GroupFactory; +import net.sf.briar.api.protocol.GroupId; +import net.sf.briar.api.protocol.Message; +import net.sf.briar.api.protocol.MessageFactory; +import net.sf.briar.api.protocol.MessageId; +import net.sf.briar.api.protocol.Offer; +import net.sf.briar.api.protocol.PacketFactory; +import net.sf.briar.api.protocol.ProtocolReader; +import net.sf.briar.api.protocol.ProtocolReaderFactory; +import net.sf.briar.api.protocol.ProtocolWriter; +import net.sf.briar.api.protocol.ProtocolWriterFactory; +import net.sf.briar.api.protocol.RawBatch; +import net.sf.briar.api.protocol.Request; +import net.sf.briar.api.protocol.SubscriptionUpdate; +import net.sf.briar.api.protocol.Transport; +import net.sf.briar.api.protocol.TransportId; +import net.sf.briar.api.protocol.TransportUpdate; +import net.sf.briar.api.transport.ConnectionContext; +import net.sf.briar.api.transport.ConnectionReader; +import net.sf.briar.api.transport.ConnectionReaderFactory; +import net.sf.briar.api.transport.ConnectionWriter; +import net.sf.briar.api.transport.ConnectionWriterFactory; +import net.sf.briar.clock.ClockModule; +import net.sf.briar.crypto.CryptoModule; +import net.sf.briar.db.DatabaseModule; +import net.sf.briar.lifecycle.LifecycleModule; +import net.sf.briar.protocol.ProtocolModule; +import net.sf.briar.protocol.duplex.DuplexProtocolModule; +import net.sf.briar.protocol.simplex.SimplexProtocolModule; +import net.sf.briar.serial.SerialModule; +import net.sf.briar.transport.TransportModule; + +import org.junit.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +public class ProtocolIntegrationTest extends BriarTestCase { + + private final BatchId ack = new BatchId(TestUtils.getRandomId()); + private final long timestamp = System.currentTimeMillis(); + + private final ConnectionReaderFactory connectionReaderFactory; + private final ConnectionWriterFactory connectionWriterFactory; + private final ProtocolReaderFactory protocolReaderFactory; + private final ProtocolWriterFactory protocolWriterFactory; + private final PacketFactory packetFactory; + private final CryptoComponent crypto; + private final ContactId contactId; + private final TransportId transportId; + private final byte[] secret; + private final Author author; + private final Group group, group1; + private final Message message, message1, message2, message3; + private final String authorName = "Alice"; + private final String subject = "Hello"; + private final String messageBody = "Hello world"; + private final Collection transports; + + public ProtocolIntegrationTest() throws Exception { + super(); + Injector i = Guice.createInjector(new ClockModule(), new CryptoModule(), + new DatabaseModule(), new LifecycleModule(), + new ProtocolModule(), new SerialModule(), + new TestDatabaseModule(), new SimplexProtocolModule(), + new TransportModule(), new DuplexProtocolModule()); + connectionReaderFactory = i.getInstance(ConnectionReaderFactory.class); + connectionWriterFactory = i.getInstance(ConnectionWriterFactory.class); + protocolReaderFactory = i.getInstance(ProtocolReaderFactory.class); + protocolWriterFactory = i.getInstance(ProtocolWriterFactory.class); + packetFactory = i.getInstance(PacketFactory.class); + crypto = i.getInstance(CryptoComponent.class); + contactId = new ContactId(234); + transportId = new TransportId(TestUtils.getRandomId()); + // Create a shared secret + Random r = new Random(); + secret = new byte[32]; + r.nextBytes(secret); + // Create two groups: one restricted, one unrestricted + GroupFactory groupFactory = i.getInstance(GroupFactory.class); + group = groupFactory.createGroup("Unrestricted group", null); + KeyPair groupKeyPair = crypto.generateSignatureKeyPair(); + group1 = groupFactory.createGroup("Restricted group", + groupKeyPair.getPublic().getEncoded()); + // Create an author + AuthorFactory authorFactory = i.getInstance(AuthorFactory.class); + KeyPair authorKeyPair = crypto.generateSignatureKeyPair(); + author = authorFactory.createAuthor(authorName, + authorKeyPair.getPublic().getEncoded()); + // Create two messages to each group: one anonymous, one pseudonymous + MessageFactory messageFactory = i.getInstance(MessageFactory.class); + message = messageFactory.createMessage(null, group, subject, + messageBody.getBytes("UTF-8")); + message1 = messageFactory.createMessage(null, group1, + groupKeyPair.getPrivate(), subject, + messageBody.getBytes("UTF-8")); + message2 = messageFactory.createMessage(null, group, author, + authorKeyPair.getPrivate(), subject, + messageBody.getBytes("UTF-8")); + message3 = messageFactory.createMessage(null, group1, + groupKeyPair.getPrivate(), author, authorKeyPair.getPrivate(), + subject, messageBody.getBytes("UTF-8")); + // Create some transports + TransportId transportId = new TransportId(TestUtils.getRandomId()); + Transport transport = new Transport(transportId, + Collections.singletonMap("bar", "baz")); + transports = Collections.singletonList(transport); + } + + @Test + public void testWriteAndRead() throws Exception { + read(write()); + } + + private byte[] write() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ConnectionContext ctx = new ConnectionContext(contactId, transportId, + secret.clone(), 0L, true); + ConnectionWriter conn = connectionWriterFactory.createConnectionWriter( + out, Long.MAX_VALUE, ctx, false, true); + OutputStream out1 = conn.getOutputStream(); + ProtocolWriter writer = protocolWriterFactory.createProtocolWriter(out1, + false); + + Ack a = packetFactory.createAck(Collections.singletonList(ack)); + writer.writeAck(a); + + Collection batch = Arrays.asList(message.getSerialised(), + message1.getSerialised(), message2.getSerialised(), + message3.getSerialised()); + RawBatch b = packetFactory.createBatch(batch); + writer.writeBatch(b); + + Collection offer = Arrays.asList(message.getId(), + message1.getId(), message2.getId(), message3.getId()); + Offer o = packetFactory.createOffer(offer); + writer.writeOffer(o); + + BitSet requested = new BitSet(4); + requested.set(1); + requested.set(3); + Request r = packetFactory.createRequest(requested, 4); + writer.writeRequest(r); + + // Use a LinkedHashMap for predictable iteration order + Map subs = new LinkedHashMap(); + subs.put(group, 0L); + subs.put(group1, 0L); + SubscriptionUpdate s = packetFactory.createSubscriptionUpdate( + Collections.emptyMap(), subs, 0L, timestamp); + writer.writeSubscriptionUpdate(s); + + TransportUpdate t = packetFactory.createTransportUpdate(transports, + timestamp); + writer.writeTransportUpdate(t); + + writer.flush(); + return out.toByteArray(); + } + + private void read(byte[] connectionData) throws Exception { + InputStream in = new ByteArrayInputStream(connectionData); + byte[] tag = new byte[TAG_LENGTH]; + assertEquals(TAG_LENGTH, in.read(tag, 0, TAG_LENGTH)); + // FIXME: Check that the expected tag was received + ConnectionContext ctx = new ConnectionContext(contactId, transportId, + secret.clone(), 0L, false); + ConnectionReader conn = connectionReaderFactory.createConnectionReader( + in, ctx, true, true); + InputStream in1 = conn.getInputStream(); + ProtocolReader reader = protocolReaderFactory.createProtocolReader(in1); + + // Read the ack + assertTrue(reader.hasAck()); + Ack a = reader.readAck(); + assertEquals(Collections.singletonList(ack), a.getBatchIds()); + + // Read and verify the batch + assertTrue(reader.hasBatch()); + Batch b = reader.readBatch().verify(); + Collection messages = b.getMessages(); + assertEquals(4, messages.size()); + Iterator it = messages.iterator(); + checkMessageEquality(message, it.next()); + checkMessageEquality(message1, it.next()); + checkMessageEquality(message2, it.next()); + checkMessageEquality(message3, it.next()); + + // Read the offer + assertTrue(reader.hasOffer()); + Offer o = reader.readOffer(); + Collection offered = o.getMessageIds(); + assertEquals(4, offered.size()); + Iterator it1 = offered.iterator(); + assertEquals(message.getId(), it1.next()); + assertEquals(message1.getId(), it1.next()); + assertEquals(message2.getId(), it1.next()); + assertEquals(message3.getId(), it1.next()); + + // Read the request + assertTrue(reader.hasRequest()); + Request req = reader.readRequest(); + BitSet requested = req.getBitmap(); + assertFalse(requested.get(0)); + assertTrue(requested.get(1)); + assertFalse(requested.get(2)); + assertTrue(requested.get(3)); + // If there are any padding bits, they should all be zero + assertEquals(2, requested.cardinality()); + + // Read the subscription update + assertTrue(reader.hasSubscriptionUpdate()); + SubscriptionUpdate s = reader.readSubscriptionUpdate(); + Map subs = s.getSubscriptions(); + assertEquals(2, subs.size()); + assertEquals(Long.valueOf(0L), subs.get(group)); + assertEquals(Long.valueOf(0L), subs.get(group1)); + assertTrue(s.getTimestamp() == timestamp); + + // Read the transport update + assertTrue(reader.hasTransportUpdate()); + TransportUpdate t = reader.readTransportUpdate(); + assertEquals(transports, t.getTransports()); + assertTrue(t.getTimestamp() == timestamp); + + in.close(); + } + + private void checkMessageEquality(Message m1, Message m2) { + assertEquals(m1.getId(), m2.getId()); + assertEquals(m1.getParent(), m2.getParent()); + assertEquals(m1.getGroup(), m2.getGroup()); + assertEquals(m1.getAuthor(), m2.getAuthor()); + assertEquals(m1.getTimestamp(), m2.getTimestamp()); + assertArrayEquals(m1.getSerialised(), m2.getSerialised()); + } +} diff --git a/briar-tests/src/net/sf/briar/TestDatabaseConfig.java b/briar-tests/src/net/sf/briar/TestDatabaseConfig.java new file mode 100644 index 000000000..fdfedb413 --- /dev/null +++ b/briar-tests/src/net/sf/briar/TestDatabaseConfig.java @@ -0,0 +1,33 @@ +package net.sf.briar; + +import java.io.File; + +import net.sf.briar.api.crypto.Password; +import net.sf.briar.api.db.DatabaseConfig; + +public class TestDatabaseConfig implements DatabaseConfig { + + private final File dir; + private final long maxSize; + + public TestDatabaseConfig(File dir, long maxSize) { + this.dir = dir; + this.maxSize = maxSize; + } + + public File getDataDirectory() { + return dir; + } + + public Password getPassword() { + return new Password() { + public char[] getPassword() { + return "foo bar".toCharArray(); + } + }; + } + + public long getMaxSize() { + return maxSize; + } +} diff --git a/briar-tests/src/net/sf/briar/TestDatabaseModule.java b/briar-tests/src/net/sf/briar/TestDatabaseModule.java new file mode 100644 index 000000000..5479d9c6b --- /dev/null +++ b/briar-tests/src/net/sf/briar/TestDatabaseModule.java @@ -0,0 +1,29 @@ +package net.sf.briar; + +import java.io.File; + +import net.sf.briar.api.db.DatabaseConfig; + +import com.google.inject.AbstractModule; + +public class TestDatabaseModule extends AbstractModule { + + private final DatabaseConfig config; + + public TestDatabaseModule() { + this(new File("."), Long.MAX_VALUE); + } + + public TestDatabaseModule(File dir) { + this(dir, Long.MAX_VALUE); + } + + public TestDatabaseModule(File dir, long maxSize) { + this.config = new TestDatabaseConfig(dir, maxSize); + } + + @Override + protected void configure() { + bind(DatabaseConfig.class).toInstance(config); + } +} diff --git a/briar-tests/src/net/sf/briar/TestUtils.java b/briar-tests/src/net/sf/briar/TestUtils.java new file mode 100644 index 000000000..e5c4f6cfe --- /dev/null +++ b/briar-tests/src/net/sf/briar/TestUtils.java @@ -0,0 +1,76 @@ +package net.sf.briar; + + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; + +import junit.framework.TestCase; +import net.sf.briar.api.protocol.UniqueId; + +public class TestUtils { + + private static final AtomicInteger nextTestDir = + new AtomicInteger((int) (Math.random() * 1000 * 1000)); + private static final Random random = new Random(); + + public static void delete(File f) { + if(f.isDirectory()) for(File child : f.listFiles()) delete(child); + f.delete(); + } + + public static void createFile(File f, String s) throws IOException { + f.getParentFile().mkdirs(); + PrintStream out = new PrintStream(new FileOutputStream(f)); + out.print(s); + out.flush(); + out.close(); + } + + public static File getTestDirectory() { + int name = nextTestDir.getAndIncrement(); + File testDir = new File("test.tmp/" + name); + return testDir; + } + + public static void deleteTestDirectory(File testDir) { + delete(testDir); + testDir.getParentFile().delete(); // Delete if empty + } + + public static File getBuildDirectory() { + File build = new File("build"); // Ant + if(build.exists() && build.isDirectory()) return build; + File bin = new File("bin"); // Eclipse + if(bin.exists() && bin.isDirectory()) return bin; + throw new RuntimeException("Could not find build directory"); + } + + public static File getFontDirectory() { + File f = new File("i18n"); + if(f.exists() && f.isDirectory()) return f; + f = new File("../i18n"); + if(f.exists() && f.isDirectory()) return f; + throw new RuntimeException("Could not find font directory"); + } + + public static byte[] getRandomId() { + byte[] b = new byte[UniqueId.LENGTH]; + random.nextBytes(b); + return b; + } + + public static void readFully(InputStream in, byte[] b) throws IOException { + int offset = 0; + while(offset < b.length) { + int read = in.read(b, offset, b.length - offset); + if(read == -1) break; + offset += read; + } + TestCase.assertEquals(b.length, offset); + } +} diff --git a/briar-tests/src/net/sf/briar/crypto/CounterModeTest.java b/briar-tests/src/net/sf/briar/crypto/CounterModeTest.java new file mode 100644 index 000000000..96cde2001 --- /dev/null +++ b/briar-tests/src/net/sf/briar/crypto/CounterModeTest.java @@ -0,0 +1,156 @@ +package net.sf.briar.crypto; + +import java.security.GeneralSecurityException; +import java.security.SecureRandom; +import java.security.Security; +import java.util.HashSet; +import java.util.Set; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.Bytes; + +import org.junit.Test; +import org.spongycastle.jce.provider.BouncyCastleProvider; + +public class CounterModeTest extends BriarTestCase { + + private static final String CIPHER_ALGO = "AES"; + private static final String CIPHER_MODE = "AES/CTR/NoPadding"; + private static final String PROVIDER = "SC"; + private static final int KEY_SIZE_BYTES = 32; // AES-256 + private static final int BLOCK_SIZE_BYTES = 16; + + private final SecureRandom random; + private final byte[] keyBytes; + private final SecretKeySpec key; + + public CounterModeTest() { + super(); + Security.addProvider(new BouncyCastleProvider()); + random = new SecureRandom(); + keyBytes = new byte[KEY_SIZE_BYTES]; + random.nextBytes(keyBytes); + key = new SecretKeySpec(keyBytes, CIPHER_ALGO); + } + + @Test + public void testEveryBitOfIvIsSignificant() + throws GeneralSecurityException { + // Set each bit of the IV in turn, encrypt the same plaintext and check + // that all the resulting ciphertexts are distinct + byte[] plaintext = new byte[BLOCK_SIZE_BYTES]; + random.nextBytes(plaintext); + Set ciphertexts = new HashSet(); + for(int i = 0; i < BLOCK_SIZE_BYTES * 8; i++) { + // Set the i^th bit of the IV + byte[] ivBytes = new byte[BLOCK_SIZE_BYTES]; + ivBytes[i / 8] |= (byte) (128 >> i % 8); + IvParameterSpec iv = new IvParameterSpec(ivBytes); + // Encrypt the plaintext + Cipher cipher = Cipher.getInstance(CIPHER_MODE, PROVIDER); + cipher.init(Cipher.ENCRYPT_MODE, key, iv); + byte[] ciphertext = + new byte[cipher.getOutputSize(plaintext.length)]; + cipher.doFinal(plaintext, 0, plaintext.length, ciphertext); + ciphertexts.add(new Bytes(ciphertext)); + } + // All the ciphertexts should be distinct using Arrays.equals() + assertEquals(BLOCK_SIZE_BYTES * 8, ciphertexts.size()); + } + + @Test + public void testRepeatedIvsProduceRepeatedCiphertexts() + throws GeneralSecurityException { + // This is the inverse of the previous test, to check that the + // distinct ciphertexts were due to using distinct IVs + byte[] plaintext = new byte[BLOCK_SIZE_BYTES]; + random.nextBytes(plaintext); + byte[] ivBytes = new byte[BLOCK_SIZE_BYTES]; + random.nextBytes(ivBytes); + IvParameterSpec iv = new IvParameterSpec(ivBytes); + Set ciphertexts = new HashSet(); + for(int i = 0; i < BLOCK_SIZE_BYTES * 8; i++) { + Cipher cipher = Cipher.getInstance(CIPHER_MODE, PROVIDER); + cipher.init(Cipher.ENCRYPT_MODE, key, iv); + byte[] ciphertext = + new byte[cipher.getOutputSize(plaintext.length)]; + cipher.doFinal(plaintext, 0, plaintext.length, ciphertext); + ciphertexts.add(new Bytes(ciphertext)); + } + assertEquals(1, ciphertexts.size()); + } + + @Test + public void testLeastSignificantBitsUsedAsCounter() + throws GeneralSecurityException { + // Initialise the least significant 16 bits of the IV to zero and + // encrypt ten blocks of zeroes + byte[] plaintext = new byte[BLOCK_SIZE_BYTES * 10]; + byte[] ivBytes = new byte[BLOCK_SIZE_BYTES]; + random.nextBytes(ivBytes); + ivBytes[BLOCK_SIZE_BYTES - 2] = 0; + ivBytes[BLOCK_SIZE_BYTES - 1] = 0; + IvParameterSpec iv = new IvParameterSpec(ivBytes); + Cipher cipher = Cipher.getInstance(CIPHER_MODE, PROVIDER); + cipher.init(Cipher.ENCRYPT_MODE, key, iv); + byte[] ciphertext = new byte[cipher.getOutputSize(plaintext.length)]; + cipher.doFinal(plaintext, 0, plaintext.length, ciphertext); + // Make sure the IV array hasn't been modified + assertEquals(0, ivBytes[BLOCK_SIZE_BYTES - 2]); + assertEquals(0, ivBytes[BLOCK_SIZE_BYTES - 1]); + // Initialise the least significant 16 bits of the IV to one and + // encrypt another ten blocks of zeroes + ivBytes[BLOCK_SIZE_BYTES - 1] = 1; + iv = new IvParameterSpec(ivBytes); + cipher = Cipher.getInstance(CIPHER_MODE, PROVIDER); + cipher.init(Cipher.ENCRYPT_MODE, key, iv); + byte[] ciphertext1 = new byte[cipher.getOutputSize(plaintext.length)]; + cipher.doFinal(plaintext, 0, plaintext.length, ciphertext1); + // The last nine blocks of the first ciphertext should be identical to + // the first nine blocks of the second ciphertext + for(int i = 0; i < BLOCK_SIZE_BYTES * 9; i++) { + assertEquals(ciphertext[i + BLOCK_SIZE_BYTES], ciphertext1[i]); + } + } + + @Test + public void testCounterUsesMoreThan16Bits() + throws GeneralSecurityException { + // Initialise the least significant bits of the IV to 2^16-1 and + // encrypt ten blocks of zeroes + byte[] plaintext = new byte[BLOCK_SIZE_BYTES * 10]; + byte[] ivBytes = new byte[BLOCK_SIZE_BYTES]; + random.nextBytes(ivBytes); + ivBytes[BLOCK_SIZE_BYTES - 3] = 0; + ivBytes[BLOCK_SIZE_BYTES - 2] = (byte) 255; + ivBytes[BLOCK_SIZE_BYTES - 1] = (byte) 255; + IvParameterSpec iv = new IvParameterSpec(ivBytes); + Cipher cipher = Cipher.getInstance(CIPHER_MODE, PROVIDER); + cipher.init(Cipher.ENCRYPT_MODE, key, iv); + byte[] ciphertext = new byte[cipher.getOutputSize(plaintext.length)]; + cipher.doFinal(plaintext, 0, plaintext.length, ciphertext); + // Make sure the IV array hasn't been modified + assertEquals(0, ivBytes[BLOCK_SIZE_BYTES - 3]); + assertEquals((byte) 255, ivBytes[BLOCK_SIZE_BYTES - 2]); + assertEquals((byte) 255, ivBytes[BLOCK_SIZE_BYTES - 1]); + // Initialise the least significant bits of the IV to 2^16 and + // encrypt another ten blocks of zeroes + ivBytes[BLOCK_SIZE_BYTES - 3] = 1; + ivBytes[BLOCK_SIZE_BYTES - 2] = 0; + ivBytes[BLOCK_SIZE_BYTES - 1] = 0; + iv = new IvParameterSpec(ivBytes); + cipher = Cipher.getInstance(CIPHER_MODE, PROVIDER); + cipher.init(Cipher.ENCRYPT_MODE, key, iv); + byte[] ciphertext1 = new byte[cipher.getOutputSize(plaintext.length)]; + cipher.doFinal(plaintext, 0, plaintext.length, ciphertext1); + // The last nine blocks of the first ciphertext should be identical to + // the first nine blocks of the second ciphertext + for(int i = 0; i < BLOCK_SIZE_BYTES * 9; i++) { + assertEquals(ciphertext[i + BLOCK_SIZE_BYTES], ciphertext1[i]); + } + } +} diff --git a/briar-tests/src/net/sf/briar/crypto/ErasableKeyTest.java b/briar-tests/src/net/sf/briar/crypto/ErasableKeyTest.java new file mode 100644 index 000000000..eb448a550 --- /dev/null +++ b/briar-tests/src/net/sf/briar/crypto/ErasableKeyTest.java @@ -0,0 +1,79 @@ +package net.sf.briar.crypto; + +import static org.junit.Assert.assertArrayEquals; + +import java.util.Random; + +import javax.crypto.Cipher; +import javax.crypto.Mac; +import javax.crypto.spec.IvParameterSpec; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.crypto.ErasableKey; + +import org.junit.Test; + +public class ErasableKeyTest extends BriarTestCase { + + private static final String CIPHER = "AES"; + private static final String CIPHER_MODE = "AES/CTR/NoPadding"; + private static final int IV_BYTES = 16; // 128 bits + private static final int KEY_BYTES = 32; // 256 bits + private static final String MAC = "HMacSHA384"; + + private final Random random = new Random(); + + @Test + public void testCopiesAreErased() { + byte[] master = new byte[KEY_BYTES]; + random.nextBytes(master); + ErasableKey k = new ErasableKeyImpl(master, CIPHER); + byte[] copy = k.getEncoded(); + assertArrayEquals(master, copy); + k.erase(); + byte[] blank = new byte[KEY_BYTES]; + assertArrayEquals(blank, master); + assertArrayEquals(blank, copy); + } + + @Test + public void testErasureDoesNotAffectCipher() throws Exception { + byte[] key = new byte[KEY_BYTES]; + random.nextBytes(key); + ErasableKey k = new ErasableKeyImpl(key, CIPHER); + Cipher c = Cipher.getInstance(CIPHER_MODE); + IvParameterSpec iv = new IvParameterSpec(new byte[IV_BYTES]); + c.init(Cipher.ENCRYPT_MODE, k, iv); + // Encrypt a blank plaintext + byte[] plaintext = new byte[123]; + byte[] ciphertext = c.doFinal(plaintext); + // Erase the key and encrypt again - erase() was called after doFinal() + k.erase(); + byte[] ciphertext1 = c.doFinal(plaintext); + // Encrypt again - this time erase() was called before doFinal() + byte[] ciphertext2 = c.doFinal(plaintext); + // The ciphertexts should match + assertArrayEquals(ciphertext, ciphertext1); + assertArrayEquals(ciphertext, ciphertext2); + } + + @Test + public void testErasureDoesNotAffectMac() throws Exception { + byte[] key = new byte[KEY_BYTES]; + random.nextBytes(key); + ErasableKey k = new ErasableKeyImpl(key, CIPHER); + Mac m = Mac.getInstance(MAC); + m.init(k); + // Authenticate a blank plaintext + byte[] plaintext = new byte[123]; + byte[] mac = m.doFinal(plaintext); + // Erase the key and authenticate again + k.erase(); + byte[] mac1 = m.doFinal(plaintext); + // Authenticate again + byte[] mac2 = m.doFinal(plaintext); + // The MACs should match + assertArrayEquals(mac, mac1); + assertArrayEquals(mac, mac2); + } +} diff --git a/briar-tests/src/net/sf/briar/crypto/KeyAgreementTest.java b/briar-tests/src/net/sf/briar/crypto/KeyAgreementTest.java new file mode 100644 index 000000000..01a893940 --- /dev/null +++ b/briar-tests/src/net/sf/briar/crypto/KeyAgreementTest.java @@ -0,0 +1,25 @@ +package net.sf.briar.crypto; + +import static org.junit.Assert.assertArrayEquals; + +import java.security.KeyPair; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.crypto.CryptoComponent; + +import org.junit.Test; + +public class KeyAgreementTest extends BriarTestCase { + + @Test + public void testKeyAgreement() throws Exception { + CryptoComponent crypto = new CryptoComponentImpl(); + KeyPair a = crypto.generateAgreementKeyPair(); + byte[] aPub = a.getPublic().getEncoded(); + KeyPair b = crypto.generateAgreementKeyPair(); + byte[] bPub = b.getPublic().getEncoded(); + byte[] aSecret = crypto.deriveInitialSecret(aPub, b, true); + byte[] bSecret = crypto.deriveInitialSecret(bPub, a, false); + assertArrayEquals(aSecret, bSecret); + } +} diff --git a/briar-tests/src/net/sf/briar/crypto/KeyDerivationTest.java b/briar-tests/src/net/sf/briar/crypto/KeyDerivationTest.java new file mode 100644 index 000000000..b05f536a4 --- /dev/null +++ b/briar-tests/src/net/sf/briar/crypto/KeyDerivationTest.java @@ -0,0 +1,76 @@ +package net.sf.briar.crypto; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.crypto.CryptoComponent; +import net.sf.briar.api.crypto.ErasableKey; + +import org.junit.Test; + +public class KeyDerivationTest extends BriarTestCase { + + private final CryptoComponent crypto; + private final byte[] secret; + + public KeyDerivationTest() { + super(); + crypto = new CryptoComponentImpl(); + secret = new byte[32]; + new Random().nextBytes(secret); + } + + @Test + public void testKeysAreDistinct() { + List keys = new ArrayList(); + keys.add(crypto.deriveFrameKey(secret, 0, false, false)); + keys.add(crypto.deriveFrameKey(secret, 0, false, true)); + keys.add(crypto.deriveFrameKey(secret, 0, true, false)); + keys.add(crypto.deriveFrameKey(secret, 0, true, true)); + keys.add(crypto.deriveTagKey(secret, true)); + keys.add(crypto.deriveTagKey(secret, false)); + for(int i = 0; i < 4; i++) { + byte[] keyI = keys.get(i).getEncoded(); + for(int j = 0; j < 4; j++) { + byte[] keyJ = keys.get(j).getEncoded(); + assertEquals(i == j, Arrays.equals(keyI, keyJ)); + } + } + } + + @Test + public void testSecretAffectsDerivation() { + Random r = new Random(); + List secrets = new ArrayList(); + for(int i = 0; i < 20; i++) { + byte[] b = new byte[32]; + r.nextBytes(b); + secrets.add(crypto.deriveNextSecret(b, 0)); + } + for(int i = 0; i < 20; i++) { + byte[] secretI = secrets.get(i); + for(int j = 0; j < 20; j++) { + byte[] secretJ = secrets.get(j); + assertEquals(i == j, Arrays.equals(secretI, secretJ)); + } + } + } + + @Test + public void testConnectionNumberAffectsDerivation() { + List secrets = new ArrayList(); + for(int i = 0; i < 20; i++) { + secrets.add(crypto.deriveNextSecret(secret.clone(), i)); + } + for(int i = 0; i < 20; i++) { + byte[] secretI = secrets.get(i); + for(int j = 0; j < 20; j++) { + byte[] secretJ = secrets.get(j); + assertEquals(i == j, Arrays.equals(secretI, secretJ)); + } + } + } +} diff --git a/briar-tests/src/net/sf/briar/db/BasicH2Test.java b/briar-tests/src/net/sf/briar/db/BasicH2Test.java new file mode 100644 index 000000000..76e2384f6 --- /dev/null +++ b/briar-tests/src/net/sf/briar/db/BasicH2Test.java @@ -0,0 +1,192 @@ +package net.sf.briar.db; + +import java.io.File; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class BasicH2Test extends BriarTestCase { + + private static final String CREATE_TABLE = + "CREATE TABLE foo" + + " (uniqueId BINARY(32)," + + " name VARCHAR NOT NULL)"; + + private final File testDir = TestUtils.getTestDirectory(); + private final File db = new File(testDir, "db"); + private final String url = "jdbc:h2:" + db.getPath(); + + private Connection connection = null; + + @Before + public void setUp() throws Exception { + testDir.mkdirs(); + Class.forName("org.h2.Driver"); + connection = DriverManager.getConnection(url); + } + + @Test + public void testCreateTableAndAddRow() throws Exception { + // Create the table + createTable(connection); + // Generate an ID + byte[] id = new byte[32]; + new Random().nextBytes(id); + // Insert the ID and name into the table + addRow(id, "foo"); + } + + @Test + public void testCreateTableAddAndRetrieveRow() throws Exception { + // Create the table + createTable(connection); + // Generate an ID + byte[] id = new byte[32]; + new Random().nextBytes(id); + // Insert the ID and name into the table + addRow(id, "foo"); + // Check that the name can be retrieved using the ID + assertEquals("foo", getName(id)); + } + + @Test + public void testSortOrder() throws Exception { + byte[] first = new byte[] { + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -128 + }; + byte[] second = new byte[] { + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 + }; + byte[] third = new byte[] { + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 127 + }; + // Create the table + createTable(connection); + // Insert the rows + addRow(first, "first"); + addRow(second, "second"); + addRow(third, "third"); + addRow(null, "null"); + // Check the ordering of the < operator: the null ID is not comparable + assertNull(getPredecessor(first)); + assertEquals("first", getPredecessor(second)); + assertEquals("second", getPredecessor(third)); + assertNull(getPredecessor(null)); + // Check the ordering of ORDER BY: nulls come first + List names = getNames(); + assertEquals(4, names.size()); + assertEquals("null", names.get(0)); + assertEquals("first", names.get(1)); + assertEquals("second", names.get(2)); + assertEquals("third", names.get(3)); + } + + private void createTable(Connection connection) throws SQLException { + try { + Statement s = connection.createStatement(); + s.executeUpdate(CREATE_TABLE); + s.close(); + } catch(SQLException e) { + connection.close(); + throw e; + } + } + + private void addRow(byte[] id, String name) throws SQLException { + String sql = "INSERT INTO foo (uniqueId, name) VALUES (?, ?)"; + try { + PreparedStatement ps = connection.prepareStatement(sql); + if(id == null) ps.setNull(1, Types.BINARY); + else ps.setBytes(1, id); + ps.setString(2, name); + int rowsAffected = ps.executeUpdate(); + ps.close(); + assertEquals(1, rowsAffected); + } catch(SQLException e) { + connection.close(); + throw e; + } + } + + private String getName(byte[] id) throws SQLException { + String sql = "SELECT name FROM foo WHERE uniqueID = ?"; + try { + PreparedStatement ps = connection.prepareStatement(sql); + if(id != null) ps.setBytes(1, id); + ResultSet rs = ps.executeQuery(); + assertTrue(rs.next()); + String name = rs.getString(1); + assertFalse(rs.next()); + rs.close(); + ps.close(); + return name; + } catch(SQLException e) { + connection.close(); + throw e; + } + } + + private String getPredecessor(byte[] id) throws SQLException { + String sql = "SELECT name FROM foo WHERE uniqueId < ?" + + " ORDER BY uniqueId DESC LIMIT ?"; + try { + PreparedStatement ps = connection.prepareStatement(sql); + ps.setBytes(1, id); + ps.setInt(2, 1); + ResultSet rs = ps.executeQuery(); + String name = rs.next() ? rs.getString(1) : null; + assertFalse(rs.next()); + rs.close(); + ps.close(); + return name; + } catch(SQLException e) { + connection.close(); + throw e; + } + } + + private List getNames() throws SQLException { + String sql = "SELECT name FROM foo ORDER BY uniqueId"; + List names = new ArrayList(); + try { + PreparedStatement ps = connection.prepareStatement(sql); + ResultSet rs = ps.executeQuery(); + while(rs.next()) names.add(rs.getString(1)); + rs.close(); + ps.close(); + return names; + } catch(SQLException e) { + connection.close(); + throw e; + } + } + + @After + public void tearDown() throws Exception { + if(connection != null) connection.close(); + TestUtils.deleteTestDirectory(testDir); + } +} diff --git a/briar-tests/src/net/sf/briar/db/DatabaseCleanerImplTest.java b/briar-tests/src/net/sf/briar/db/DatabaseCleanerImplTest.java new file mode 100644 index 000000000..cbe77eda2 --- /dev/null +++ b/briar-tests/src/net/sf/briar/db/DatabaseCleanerImplTest.java @@ -0,0 +1,67 @@ +package net.sf.briar.db; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.util.concurrent.CountDownLatch; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.clock.SystemTimer; +import net.sf.briar.api.clock.Timer; +import net.sf.briar.api.db.DbException; +import net.sf.briar.db.DatabaseCleaner.Callback; + +import org.junit.Test; + +// FIXME: Use a mock timer +public class DatabaseCleanerImplTest extends BriarTestCase { + + @Test + public void testCleanerRunsPeriodically() throws Exception { + final CountDownLatch latch = new CountDownLatch(5); + Callback callback = new Callback() { + + public void checkFreeSpaceAndClean() throws DbException { + latch.countDown(); + } + + public boolean shouldCheckFreeSpace() { + return true; + } + }; + Timer timer = new SystemTimer(); + DatabaseCleanerImpl cleaner = new DatabaseCleanerImpl(timer); + // Start the cleaner + cleaner.startCleaning(callback, 10L); + // The database should be cleaned five times (allow 5s for system load) + assertTrue(latch.await(5, SECONDS)); + // Stop the cleaner + cleaner.stopCleaning(); + } + + @Test + public void testStoppingCleanerWakesItUp() throws Exception { + final CountDownLatch latch = new CountDownLatch(1); + Callback callback = new Callback() { + + public void checkFreeSpaceAndClean() throws DbException { + latch.countDown(); + } + + public boolean shouldCheckFreeSpace() { + return true; + } + }; + Timer timer = new SystemTimer(); + DatabaseCleanerImpl cleaner = new DatabaseCleanerImpl(timer); + long start = System.currentTimeMillis(); + // Start the cleaner + cleaner.startCleaning(callback, 10L * 1000L); + // The database should be cleaned once at startup + assertTrue(latch.await(5, SECONDS)); + // Stop the cleaner (it should be waiting between sweeps) + cleaner.stopCleaning(); + long end = System.currentTimeMillis(); + // Check that much less than 10 seconds expired + assertTrue(end - start < 10L * 1000L); + } +} diff --git a/briar-tests/src/net/sf/briar/db/DatabaseComponentImplTest.java b/briar-tests/src/net/sf/briar/db/DatabaseComponentImplTest.java new file mode 100644 index 000000000..389bb46a6 --- /dev/null +++ b/briar-tests/src/net/sf/briar/db/DatabaseComponentImplTest.java @@ -0,0 +1,151 @@ +package net.sf.briar.db; + +import static net.sf.briar.db.DatabaseConstants.BYTES_PER_SWEEP; +import static net.sf.briar.db.DatabaseConstants.MIN_FREE_SPACE; + +import java.util.Collections; + +import net.sf.briar.api.clock.SystemClock; +import net.sf.briar.api.db.DatabaseComponent; +import net.sf.briar.api.db.DbException; +import net.sf.briar.api.lifecycle.ShutdownManager; +import net.sf.briar.api.protocol.PacketFactory; +import net.sf.briar.db.DatabaseCleaner.Callback; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.Test; + +/** + * Tests that use the DatabaseCleaner.Callback interface of + * DatabaseComponentImpl. + */ +public class DatabaseComponentImplTest extends DatabaseComponentTest { + + @Test + public void testNotCleanedIfEnoughFreeSpace() throws DbException { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + oneOf(database).getFreeSpace(); + will(returnValue(MIN_FREE_SPACE)); + }}); + Callback db = createDatabaseComponentImpl(database, cleaner, shutdown, + packetFactory); + + db.checkFreeSpaceAndClean(); + + context.assertIsSatisfied(); + } + + @Test + public void testCleanedIfNotEnoughFreeSpace() throws DbException { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + oneOf(database).getFreeSpace(); + will(returnValue(MIN_FREE_SPACE - 1)); + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).getOldMessages(txn, BYTES_PER_SWEEP); + will(returnValue(Collections.emptyList())); + oneOf(database).commitTransaction(txn); + // As if by magic, some free space has appeared + oneOf(database).getFreeSpace(); + will(returnValue(MIN_FREE_SPACE)); + }}); + Callback db = createDatabaseComponentImpl(database, cleaner, shutdown, + packetFactory); + + db.checkFreeSpaceAndClean(); + + context.assertIsSatisfied(); + } + + @Test + public void testExpiringUnsendableMessageDoesNotTriggerBackwardInclusion() + throws DbException { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + oneOf(database).getFreeSpace(); + will(returnValue(MIN_FREE_SPACE - 1)); + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).getOldMessages(txn, BYTES_PER_SWEEP); + will(returnValue(Collections.singletonList(messageId))); + oneOf(database).getSendability(txn, messageId); + will(returnValue(0)); + oneOf(database).removeMessage(txn, messageId); + oneOf(database).commitTransaction(txn); + oneOf(database).getFreeSpace(); + will(returnValue(MIN_FREE_SPACE)); + }}); + Callback db = createDatabaseComponentImpl(database, cleaner, shutdown, + packetFactory); + + db.checkFreeSpaceAndClean(); + + context.assertIsSatisfied(); + } + + @Test + public void testExpiringSendableMessageTriggersBackwardInclusion() + throws DbException { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + oneOf(database).getFreeSpace(); + will(returnValue(MIN_FREE_SPACE - 1)); + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).getOldMessages(txn, BYTES_PER_SWEEP); + will(returnValue(Collections.singletonList(messageId))); + oneOf(database).getSendability(txn, messageId); + will(returnValue(1)); + oneOf(database).getGroupMessageParent(txn, messageId); + will(returnValue(null)); + oneOf(database).removeMessage(txn, messageId); + oneOf(database).commitTransaction(txn); + oneOf(database).getFreeSpace(); + will(returnValue(MIN_FREE_SPACE)); + }}); + Callback db = createDatabaseComponentImpl(database, cleaner, shutdown, + packetFactory); + + db.checkFreeSpaceAndClean(); + + context.assertIsSatisfied(); + } + + @Override + protected DatabaseComponent createDatabaseComponent( + Database database, DatabaseCleaner cleaner, + ShutdownManager shutdown, PacketFactory packetFactory) { + return createDatabaseComponentImpl(database, cleaner, shutdown, + packetFactory); + } + + private DatabaseComponentImpl createDatabaseComponentImpl( + Database database, DatabaseCleaner cleaner, + ShutdownManager shutdown, PacketFactory packetFactory) { + return new DatabaseComponentImpl(database, cleaner, shutdown, + packetFactory, new SystemClock()); + } +} diff --git a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java new file mode 100644 index 000000000..73a4545df --- /dev/null +++ b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java @@ -0,0 +1,1606 @@ +package net.sf.briar.db; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collection; +import java.util.Collections; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.Rating; +import net.sf.briar.api.TransportProperties; +import net.sf.briar.api.db.DatabaseComponent; +import net.sf.briar.api.db.NoSuchContactException; +import net.sf.briar.api.db.NoSuchContactTransportException; +import net.sf.briar.api.db.event.ContactAddedEvent; +import net.sf.briar.api.db.event.ContactRemovedEvent; +import net.sf.briar.api.db.event.DatabaseListener; +import net.sf.briar.api.db.event.MessagesAddedEvent; +import net.sf.briar.api.db.event.RatingChangedEvent; +import net.sf.briar.api.db.event.SubscriptionsUpdatedEvent; +import net.sf.briar.api.lifecycle.ShutdownManager; +import net.sf.briar.api.protocol.Ack; +import net.sf.briar.api.protocol.AuthorId; +import net.sf.briar.api.protocol.Batch; +import net.sf.briar.api.protocol.BatchId; +import net.sf.briar.api.protocol.Group; +import net.sf.briar.api.protocol.GroupId; +import net.sf.briar.api.protocol.Message; +import net.sf.briar.api.protocol.MessageId; +import net.sf.briar.api.protocol.Offer; +import net.sf.briar.api.protocol.PacketFactory; +import net.sf.briar.api.protocol.RawBatch; +import net.sf.briar.api.protocol.Request; +import net.sf.briar.api.protocol.SubscriptionUpdate; +import net.sf.briar.api.protocol.Transport; +import net.sf.briar.api.protocol.TransportId; +import net.sf.briar.api.protocol.TransportUpdate; +import net.sf.briar.api.transport.ContactTransport; +import net.sf.briar.api.transport.TemporarySecret; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.Test; + +public abstract class DatabaseComponentTest extends BriarTestCase { + + protected final Object txn = new Object(); + protected final AuthorId authorId; + protected final BatchId batchId; + protected final ContactId contactId; + protected final GroupId groupId; + protected final MessageId messageId, parentId; + private final String subject; + private final long timestamp; + private final int size; + private final byte[] raw; + private final Message message, privateMessage; + private final Group group; + private final TransportId transportId; + private final Collection transports; + private final ContactTransport contactTransport; + private final TemporarySecret temporarySecret; + + public DatabaseComponentTest() { + super(); + authorId = new AuthorId(TestUtils.getRandomId()); + batchId = new BatchId(TestUtils.getRandomId()); + contactId = new ContactId(234); + groupId = new GroupId(TestUtils.getRandomId()); + messageId = new MessageId(TestUtils.getRandomId()); + parentId = new MessageId(TestUtils.getRandomId()); + subject = "Foo"; + timestamp = System.currentTimeMillis(); + size = 1234; + raw = new byte[size]; + message = new TestMessage(messageId, null, groupId, authorId, subject, + timestamp, raw); + privateMessage = new TestMessage(messageId, null, null, null, subject, + timestamp, raw); + group = new TestGroup(groupId, "The really exciting group", null); + transportId = new TransportId(TestUtils.getRandomId()); + TransportProperties properties = new TransportProperties( + Collections.singletonMap("foo", "bar")); + Transport transport = new Transport(transportId, properties); + transports = Collections.singletonList(transport); + contactTransport = new ContactTransport(contactId, transportId, 123L, + 234L, 345L, true); + temporarySecret = new TemporarySecret(contactId, transportId, 1L, 2L, + 3L, false, 4L, new byte[32], 5L, 6L, new byte[4]); + } + + protected abstract DatabaseComponent createDatabaseComponent( + Database database, DatabaseCleaner cleaner, + ShutdownManager shutdown, PacketFactory packetFactory); + + @Test + @SuppressWarnings("unchecked") + public void testSimpleCalls() throws Exception { + final int shutdownHandle = 12345; + Mockery context = new Mockery(); + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final Group group = context.mock(Group.class); + final DatabaseListener listener = context.mock(DatabaseListener.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + // open(false) + oneOf(database).open(false); + oneOf(cleaner).startCleaning( + with(any(DatabaseCleaner.Callback.class)), + with(any(long.class))); + oneOf(shutdown).addShutdownHook(with(any(Runnable.class))); + will(returnValue(shutdownHandle)); + // getRating(authorId) + oneOf(database).getRating(txn, authorId); + will(returnValue(Rating.UNRATED)); + // setRating(authorId, Rating.GOOD) + oneOf(database).setRating(txn, authorId, Rating.GOOD); + will(returnValue(Rating.UNRATED)); + oneOf(database).getMessagesByAuthor(txn, authorId); + will(returnValue(Collections.emptyList())); + oneOf(listener).eventOccurred(with(any(RatingChangedEvent.class))); + // setRating(authorId, Rating.GOOD) again + oneOf(database).setRating(txn, authorId, Rating.GOOD); + will(returnValue(Rating.GOOD)); + // addContact() + oneOf(database).addContact(txn); + will(returnValue(contactId)); + oneOf(listener).eventOccurred(with(any(ContactAddedEvent.class))); + // getContacts() + oneOf(database).getContacts(txn); + will(returnValue(Collections.singletonList(contactId))); + // getTransportProperties(transportId) + oneOf(database).getRemoteProperties(txn, transportId); + will(returnValue(Collections.emptyMap())); + // subscribe(group) + oneOf(group).getId(); + will(returnValue(groupId)); + oneOf(database).containsSubscription(txn, groupId); + will(returnValue(false)); + oneOf(database).addSubscription(txn, group); + // subscribe(group) again + oneOf(group).getId(); + will(returnValue(groupId)); + oneOf(database).containsSubscription(txn, groupId); + will(returnValue(true)); + // getMessageHeaders(groupId) + oneOf(database).getMessageHeaders(txn, groupId); + will(returnValue(Collections.emptyList())); + // getSubscriptions() + oneOf(database).getSubscriptions(txn); + will(returnValue(Collections.singletonList(groupId))); + // unsubscribe(groupId) + oneOf(database).containsSubscription(txn, groupId); + will(returnValue(true)); + oneOf(database).getVisibility(txn, groupId); + will(returnValue(Collections.emptyList())); + oneOf(database).removeSubscription(txn, groupId); + // unsubscribe(groupId) again + oneOf(database).containsSubscription(txn, groupId); + will(returnValue(false)); + // removeContact(contactId) + oneOf(database).containsContact(txn, contactId); + will(returnValue(true)); + oneOf(database).removeContact(txn, contactId); + oneOf(listener).eventOccurred(with(any(ContactRemovedEvent.class))); + // close() + oneOf(shutdown).removeShutdownHook(shutdownHandle); + oneOf(cleaner).stopCleaning(); + oneOf(database).close(); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.open(false); + db.addListener(listener); + assertEquals(Rating.UNRATED, db.getRating(authorId)); + db.setRating(authorId, Rating.GOOD); // First time - listeners called + db.setRating(authorId, Rating.GOOD); // Second time - not called + assertEquals(contactId, db.addContact()); + assertEquals(Collections.singletonList(contactId), db.getContacts()); + assertEquals(Collections.emptyMap(), + db.getRemoteProperties(transportId)); + db.subscribe(group); // First time - listeners called + db.subscribe(group); // Second time - not called + assertEquals(Collections.emptyList(), db.getMessageHeaders(groupId)); + assertEquals(Collections.singletonList(groupId), db.getSubscriptions()); + db.unsubscribe(groupId); // First time - listeners called + db.unsubscribe(groupId); // Second time - not called + db.removeContact(contactId); + db.removeListener(listener); + db.close(); + + context.assertIsSatisfied(); + } + + @Test + public void testNullParentStopsBackwardInclusion() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + // setRating(authorId, Rating.GOOD) + allowing(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).setRating(txn, authorId, Rating.GOOD); + will(returnValue(Rating.UNRATED)); + // The sendability of the author's messages should be incremented + oneOf(database).getMessagesByAuthor(txn, authorId); + will(returnValue(Collections.singletonList(messageId))); + oneOf(database).getSendability(txn, messageId); + will(returnValue(0)); + oneOf(database).setSendability(txn, messageId, 1); + // Backward inclusion stops when the message has no parent + oneOf(database).getGroupMessageParent(txn, messageId); + will(returnValue(null)); + oneOf(database).commitTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.setRating(authorId, Rating.GOOD); + + context.assertIsSatisfied(); + } + + @Test + public void testUnaffectedParentStopsBackwardInclusion() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + // setRating(authorId, Rating.GOOD) + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).setRating(txn, authorId, Rating.GOOD); + will(returnValue(Rating.UNRATED)); + // The sendability of the author's messages should be incremented + oneOf(database).getMessagesByAuthor(txn, authorId); + will(returnValue(Collections.singletonList(messageId))); + oneOf(database).getSendability(txn, messageId); + will(returnValue(0)); + oneOf(database).setSendability(txn, messageId, 1); + // The parent exists, is in the DB, and is in the same group + oneOf(database).getGroupMessageParent(txn, messageId); + will(returnValue(parentId)); + // The parent is already sendable + oneOf(database).getSendability(txn, parentId); + will(returnValue(1)); + oneOf(database).setSendability(txn, parentId, 2); + oneOf(database).commitTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.setRating(authorId, Rating.GOOD); + + context.assertIsSatisfied(); + } + + @Test + public void testAffectedParentContinuesBackwardInclusion() + throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + // setRating(authorId, Rating.GOOD) + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).setRating(txn, authorId, Rating.GOOD); + will(returnValue(Rating.UNRATED)); + // The sendability of the author's messages should be incremented + oneOf(database).getMessagesByAuthor(txn, authorId); + will(returnValue(Collections.singletonList(messageId))); + oneOf(database).getSendability(txn, messageId); + will(returnValue(0)); + oneOf(database).setSendability(txn, messageId, 1); + // The parent exists, is in the DB, and is in the same group + oneOf(database).getGroupMessageParent(txn, messageId); + will(returnValue(parentId)); + // The parent is not already sendable + oneOf(database).getSendability(txn, parentId); + will(returnValue(0)); + oneOf(database).setSendability(txn, parentId, 1); + // The parent has no parent + oneOf(database).getGroupMessageParent(txn, parentId); + will(returnValue(null)); + oneOf(database).commitTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.setRating(authorId, Rating.GOOD); + + context.assertIsSatisfied(); + } + + @Test + public void testGroupMessagesAreNotStoredUnlessSubscribed() + throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + // addLocalGroupMessage(message) + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).containsSubscription(txn, groupId, timestamp); + will(returnValue(false)); + oneOf(database).commitTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addLocalGroupMessage(message); + + context.assertIsSatisfied(); + } + + @Test + public void testDuplicateGroupMessagesAreNotStored() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + // addLocalGroupMessage(message) + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).containsSubscription(txn, groupId, timestamp); + will(returnValue(true)); + oneOf(database).addGroupMessage(txn, message); + will(returnValue(false)); + oneOf(database).commitTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addLocalGroupMessage(message); + + context.assertIsSatisfied(); + } + + @Test + public void testAddLocalGroupMessage() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + // addLocalGroupMessage(message) + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).containsSubscription(txn, groupId, timestamp); + will(returnValue(true)); + oneOf(database).addGroupMessage(txn, message); + will(returnValue(true)); + oneOf(database).getContacts(txn); + will(returnValue(Collections.singletonList(contactId))); + oneOf(database).setStatus(txn, contactId, messageId, Status.NEW); + // The author is unrated and there are no sendable children + oneOf(database).getRating(txn, authorId); + will(returnValue(Rating.UNRATED)); + oneOf(database).getNumberOfSendableChildren(txn, messageId); + will(returnValue(0)); + oneOf(database).setSendability(txn, messageId, 0); + oneOf(database).commitTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addLocalGroupMessage(message); + + context.assertIsSatisfied(); + } + + @Test + public void testAddingSendableMessageTriggersBackwardInclusion() + throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + // addLocalGroupMessage(message) + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).containsSubscription(txn, groupId, timestamp); + will(returnValue(true)); + oneOf(database).addGroupMessage(txn, message); + will(returnValue(true)); + oneOf(database).getContacts(txn); + will(returnValue(Collections.singletonList(contactId))); + oneOf(database).setStatus(txn, contactId, messageId, Status.NEW); + // The author is rated GOOD and there are two sendable children + oneOf(database).getRating(txn, authorId); + will(returnValue(Rating.GOOD)); + oneOf(database).getNumberOfSendableChildren(txn, messageId); + will(returnValue(2)); + oneOf(database).setSendability(txn, messageId, 3); + // The sendability of the message's ancestors should be updated + oneOf(database).getGroupMessageParent(txn, messageId); + will(returnValue(null)); + oneOf(database).commitTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addLocalGroupMessage(message); + + context.assertIsSatisfied(); + } + + @Test + public void testDuplicatePrivateMessagesAreNotStored() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // addLocalPrivateMessage(privateMessage, contactId) + oneOf(database).addPrivateMessage(txn, privateMessage, contactId); + will(returnValue(false)); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addLocalPrivateMessage(privateMessage, contactId); + + context.assertIsSatisfied(); + } + + @Test + public void testAddLocalPrivateMessage() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // addLocalPrivateMessage(privateMessage, contactId) + oneOf(database).addPrivateMessage(txn, privateMessage, contactId); + will(returnValue(true)); + oneOf(database).setStatus(txn, contactId, messageId, Status.NEW); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addLocalPrivateMessage(privateMessage, contactId); + + context.assertIsSatisfied(); + } + + @Test + public void testVariousMethodsThrowExceptionIfContactIsMissing() + throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final Ack ack = context.mock(Ack.class); + final Batch batch = context.mock(Batch.class); + final Offer offer = context.mock(Offer.class); + final SubscriptionUpdate subscriptionUpdate = + context.mock(SubscriptionUpdate.class); + final TransportUpdate transportUpdate = + context.mock(TransportUpdate.class); + context.checking(new Expectations() {{ + // Check whether the contact is in the DB (which it's not) + exactly(16).of(database).startTransaction(); + will(returnValue(txn)); + exactly(16).of(database).containsContact(txn, contactId); + will(returnValue(false)); + exactly(16).of(database).abortTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + try { + db.addContactTransport(contactTransport); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.addLocalPrivateMessage(privateMessage, contactId); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.generateAck(contactId, 123); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.generateBatch(contactId, 123); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.generateBatch(contactId, 123, + Collections.emptyList()); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.generateOffer(contactId, 123); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.generateSubscriptionUpdate(contactId); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.generateTransportUpdate(contactId); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.hasSendableMessages(contactId); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.receiveAck(contactId, ack); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.receiveBatch(contactId, batch); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.receiveOffer(contactId, offer); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.receiveSubscriptionUpdate(contactId, subscriptionUpdate); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.receiveTransportUpdate(contactId, transportUpdate); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.removeContact(contactId); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.setSeen(contactId, Collections.singletonList(messageId)); + fail(); + } catch(NoSuchContactException expected) {} + + context.assertIsSatisfied(); + } + + @Test + public void testVariousMethodsThrowExceptionIfContactTransportIsMissing() + throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + // Check whether the contact transport is in the DB (which it's not) + exactly(2).of(database).startTransaction(); + will(returnValue(txn)); + exactly(2).of(database).containsContactTransport(txn, contactId, + transportId); + will(returnValue(false)); + exactly(2).of(database).abortTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + try { + db.incrementConnectionCounter(contactId, transportId, 0L); + fail(); + } catch(NoSuchContactTransportException expected) {} + + try { + db.setConnectionWindow(contactId, transportId, 0L, 0L, new byte[4]); + fail(); + } catch(NoSuchContactTransportException expected) {} + + context.assertIsSatisfied(); + } + + @Test + public void testGenerateAck() throws Exception { + final BatchId batchId1 = new BatchId(TestUtils.getRandomId()); + final Collection batchesToAck = new ArrayList(); + batchesToAck.add(batchId); + batchesToAck.add(batchId1); + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final Ack ack = context.mock(Ack.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Get the batches to ack + oneOf(database).getBatchesToAck(txn, contactId, 123); + will(returnValue(batchesToAck)); + // Create the packet + oneOf(packetFactory).createAck(batchesToAck); + will(returnValue(ack)); + // Record the batches that were acked + oneOf(database).removeBatchesToAck(txn, contactId, batchesToAck); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + assertEquals(ack, db.generateAck(contactId, 123)); + + context.assertIsSatisfied(); + } + + @Test + public void testGenerateBatch() throws Exception { + final MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + final byte[] raw1 = new byte[size]; + final Collection sendable = Arrays.asList(messageId, + messageId1); + final Collection messages = Arrays.asList(raw, raw1); + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final RawBatch batch = context.mock(RawBatch.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Get the sendable messages + oneOf(database).getSendableMessages(txn, contactId, size * 2); + will(returnValue(sendable)); + oneOf(database).getMessage(txn, messageId); + will(returnValue(raw)); + oneOf(database).getMessage(txn, messageId1); + will(returnValue(raw1)); + // Create the packet + oneOf(packetFactory).createBatch(messages); + will(returnValue(batch)); + // Record the outstanding batch + oneOf(batch).getId(); + will(returnValue(batchId)); + oneOf(database).addOutstandingBatch(txn, contactId, batchId, + sendable); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + assertEquals(batch, db.generateBatch(contactId, size * 2)); + + context.assertIsSatisfied(); + } + + @Test + public void testGenerateBatchFromRequest() throws Exception { + final MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + final MessageId messageId2 = new MessageId(TestUtils.getRandomId()); + final byte[] raw1 = new byte[size]; + final Collection requested = new ArrayList(); + requested.add(messageId); + requested.add(messageId1); + requested.add(messageId2); + final Collection msgs = Arrays.asList(raw1); + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final RawBatch batch = context.mock(RawBatch.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Try to get the requested messages + oneOf(database).getMessageIfSendable(txn, contactId, messageId); + will(returnValue(null)); // Message is not sendable + oneOf(database).getMessageIfSendable(txn, contactId, messageId1); + will(returnValue(raw1)); // Message is sendable + oneOf(database).getMessageIfSendable(txn, contactId, messageId2); + will(returnValue(null)); // Message is not sendable + // Create the packet + oneOf(packetFactory).createBatch(msgs); + will(returnValue(batch)); + // Record the outstanding batch + oneOf(batch).getId(); + will(returnValue(batchId)); + oneOf(database).addOutstandingBatch(txn, contactId, batchId, + Collections.singletonList(messageId1)); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + assertEquals(batch, db.generateBatch(contactId, size * 3, requested)); + + context.assertIsSatisfied(); + } + + @Test + public void testGenerateOffer() throws Exception { + final MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + final Collection offerable = new ArrayList(); + offerable.add(messageId); + offerable.add(messageId1); + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final Offer offer = context.mock(Offer.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Get the sendable message IDs + oneOf(database).getOfferableMessages(txn, contactId, 123); + will(returnValue(offerable)); + // Create the packet + oneOf(packetFactory).createOffer(offerable); + will(returnValue(offer)); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + assertEquals(offer, db.generateOffer(contactId, 123)); + + context.assertIsSatisfied(); + } + + @Test + public void testGenerateSubscriptionUpdate() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final SubscriptionUpdate subscriptionUpdate = + context.mock(SubscriptionUpdate.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Get the visible holes and subscriptions + oneOf(database).getVisibleHoles(with(txn), with(contactId), + with(any(long.class))); + will(returnValue(Collections.emptyMap())); + oneOf(database).getVisibleSubscriptions(with(txn), with(contactId), + with(any(long.class))); + will(returnValue(Collections.singletonMap(group, 0L))); + // Get the expiry time + oneOf(database).getExpiryTime(txn); + will(returnValue(0L)); + // Create the packet + oneOf(packetFactory).createSubscriptionUpdate( + with(Collections.emptyMap()), + with(Collections.singletonMap(group, 0L)), + with(any(long.class)), + with(any(long.class))); + will(returnValue(subscriptionUpdate)); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + assertEquals(subscriptionUpdate, + db.generateSubscriptionUpdate(contactId)); + + context.assertIsSatisfied(); + } + + @Test + public void testTransportUpdateNotSentUnlessDue() throws Exception { + final long now = System.currentTimeMillis(); + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Check whether an update is due + oneOf(database).getTransportsModified(txn); + will(returnValue(now - 1L)); + oneOf(database).getTransportsSent(txn, contactId); + will(returnValue(now)); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + assertNull(db.generateTransportUpdate(contactId)); + + context.assertIsSatisfied(); + } + + @Test + public void testGenerateTransportUpdate() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final TransportUpdate transportUpdate = + context.mock(TransportUpdate.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Check whether an update is due + oneOf(database).getTransportsModified(txn); + will(returnValue(0L)); + oneOf(database).getTransportsSent(txn, contactId); + will(returnValue(0L)); + // Get the local transport properties + oneOf(database).getLocalTransports(txn); + will(returnValue(transports)); + oneOf(database).setTransportsSent(with(txn), with(contactId), + with(any(long.class))); + // Create the packet + oneOf(packetFactory).createTransportUpdate(with(transports), + with(any(long.class))); + will(returnValue(transportUpdate)); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + assertEquals(transportUpdate, db.generateTransportUpdate(contactId)); + + context.assertIsSatisfied(); + } + + @Test + public void testReceiveAck() throws Exception { + final BatchId batchId1 = new BatchId(TestUtils.getRandomId()); + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final Ack ack = context.mock(Ack.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Get the acked batches + oneOf(ack).getBatchIds(); + will(returnValue(Collections.singletonList(batchId))); + oneOf(database).removeAckedBatch(txn, contactId, batchId); + // Find lost batches + oneOf(database).getLostBatches(txn, contactId); + will(returnValue(Collections.singletonList(batchId1))); + oneOf(database).removeLostBatch(txn, contactId, batchId1); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.receiveAck(contactId, ack); + + context.assertIsSatisfied(); + } + + @Test + public void testReceiveBatchStoresPrivateMessage() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final Batch batch = context.mock(Batch.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + oneOf(batch).getMessages(); + will(returnValue(Collections.singletonList(privateMessage))); + // The message is stored + oneOf(database).addPrivateMessage(txn, privateMessage, contactId); + will(returnValue(true)); + oneOf(database).setStatus(txn, contactId, messageId, Status.SEEN); + // The batch must be acked + oneOf(batch).getId(); + will(returnValue(batchId)); + oneOf(database).addBatchToAck(txn, contactId, batchId); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.receiveBatch(contactId, batch); + + context.assertIsSatisfied(); + } + + @Test + public void testReceiveBatchWithDuplicatePrivateMessage() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final Batch batch = context.mock(Batch.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + oneOf(batch).getMessages(); + will(returnValue(Collections.singletonList(privateMessage))); + // The message is stored, but it's a duplicate + oneOf(database).addPrivateMessage(txn, privateMessage, contactId); + will(returnValue(false)); + // The batch must still be acked + oneOf(batch).getId(); + will(returnValue(batchId)); + oneOf(database).addBatchToAck(txn, contactId, batchId); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.receiveBatch(contactId, batch); + + context.assertIsSatisfied(); + } + + @Test + public void testReceiveBatchDoesNotStoreGroupMessageUnlessSubscribed() + throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final Batch batch = context.mock(Batch.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Only store messages belonging to visible, subscribed groups + oneOf(batch).getMessages(); + will(returnValue(Collections.singletonList(message))); + oneOf(database).containsVisibleSubscription(txn, groupId, + contactId, timestamp); + will(returnValue(false)); + // The message is not stored but the batch must still be acked + oneOf(batch).getId(); + will(returnValue(batchId)); + oneOf(database).addBatchToAck(txn, contactId, batchId); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.receiveBatch(contactId, batch); + + context.assertIsSatisfied(); + } + + @Test + public void testReceiveBatchDoesNotCalculateSendabilityForDuplicates() + throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final Batch batch = context.mock(Batch.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Only store messages belonging to visible, subscribed groups + oneOf(batch).getMessages(); + will(returnValue(Collections.singletonList(message))); + oneOf(database).containsVisibleSubscription(txn, groupId, + contactId, timestamp); + will(returnValue(true)); + // The message is stored, but it's a duplicate + oneOf(database).addGroupMessage(txn, message); + will(returnValue(false)); + oneOf(database).setStatus(txn, contactId, messageId, Status.SEEN); + // The batch needs to be acknowledged + oneOf(batch).getId(); + will(returnValue(batchId)); + oneOf(database).addBatchToAck(txn, contactId, batchId); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.receiveBatch(contactId, batch); + + context.assertIsSatisfied(); + } + + @Test + public void testReceiveBatchCalculatesSendability() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final Batch batch = context.mock(Batch.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Only store messages belonging to visible, subscribed groups + oneOf(batch).getMessages(); + will(returnValue(Collections.singletonList(message))); + oneOf(database).containsVisibleSubscription(txn, groupId, + contactId, timestamp); + will(returnValue(true)); + // The message is stored, and it's not a duplicate + oneOf(database).addGroupMessage(txn, message); + will(returnValue(true)); + oneOf(database).setStatus(txn, contactId, messageId, Status.SEEN); + // Set the status to NEW for all other contacts (there are none) + oneOf(database).getContacts(txn); + will(returnValue(Collections.singletonList(contactId))); + // Calculate the sendability - zero, so ancestors aren't updated + oneOf(database).getRating(txn, authorId); + will(returnValue(Rating.UNRATED)); + oneOf(database).getNumberOfSendableChildren(txn, messageId); + will(returnValue(0)); + oneOf(database).setSendability(txn, messageId, 0); + // The batch needs to be acknowledged + oneOf(batch).getId(); + will(returnValue(batchId)); + oneOf(database).addBatchToAck(txn, contactId, batchId); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.receiveBatch(contactId, batch); + + context.assertIsSatisfied(); + } + + @Test + public void testReceiveBatchUpdatesAncestorSendability() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final Batch batch = context.mock(Batch.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Only store messages belonging to visible, subscribed groups + oneOf(batch).getMessages(); + will(returnValue(Collections.singletonList(message))); + oneOf(database).containsVisibleSubscription(txn, groupId, + contactId, timestamp); + will(returnValue(true)); + // The message is stored, and it's not a duplicate + oneOf(database).addGroupMessage(txn, message); + will(returnValue(true)); + oneOf(database).setStatus(txn, contactId, messageId, Status.SEEN); + // Set the status to NEW for all other contacts (there are none) + oneOf(database).getContacts(txn); + will(returnValue(Collections.singletonList(contactId))); + // Calculate the sendability - ancestors are updated + oneOf(database).getRating(txn, authorId); + will(returnValue(Rating.GOOD)); + oneOf(database).getNumberOfSendableChildren(txn, messageId); + will(returnValue(1)); + oneOf(database).setSendability(txn, messageId, 2); + oneOf(database).getGroupMessageParent(txn, messageId); + will(returnValue(null)); + // The batch needs to be acknowledged + oneOf(batch).getId(); + will(returnValue(batchId)); + oneOf(database).addBatchToAck(txn, contactId, batchId); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.receiveBatch(contactId, batch); + + context.assertIsSatisfied(); + } + + @Test + public void testReceiveOffer() throws Exception { + final MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + final MessageId messageId2 = new MessageId(TestUtils.getRandomId()); + final Collection offered = new ArrayList(); + offered.add(messageId); + offered.add(messageId1); + offered.add(messageId2); + final BitSet expectedRequest = new BitSet(3); + expectedRequest.set(0); + expectedRequest.set(2); + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final Offer offer = context.mock(Offer.class); + final Request request = context.mock(Request.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Get the offered messages + oneOf(offer).getMessageIds(); + will(returnValue(offered)); + oneOf(database).setStatusSeenIfVisible(txn, contactId, messageId); + will(returnValue(false)); // Not visible - request message # 0 + oneOf(database).setStatusSeenIfVisible(txn, contactId, messageId1); + will(returnValue(true)); // Visible - do not request message # 1 + oneOf(database).setStatusSeenIfVisible(txn, contactId, messageId2); + will(returnValue(false)); // Not visible - request message # 2 + // Create the packet + oneOf(packetFactory).createRequest(expectedRequest, 3); + will(returnValue(request)); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + assertEquals(request, db.receiveOffer(contactId, offer)); + + context.assertIsSatisfied(); + } + + @Test + public void testReceiveSubscriptionUpdate() throws Exception { + final GroupId start = new GroupId(TestUtils.getRandomId()); + final GroupId end = new GroupId(TestUtils.getRandomId()); + final long expiry = 1234L, timestamp = 5678L; + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final SubscriptionUpdate subscriptionUpdate = + context.mock(SubscriptionUpdate.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Get the contents of the update + oneOf(subscriptionUpdate).getHoles(); + will(returnValue(Collections.singletonMap(start, end))); + oneOf(subscriptionUpdate).getSubscriptions(); + will(returnValue(Collections.singletonMap(group, 0L))); + oneOf(subscriptionUpdate).getExpiryTime(); + will(returnValue(expiry)); + oneOf(subscriptionUpdate).getTimestamp(); + will(returnValue(timestamp)); + // Store the contents of the update + oneOf(database).removeSubscriptions(txn, contactId, start, end); + oneOf(database).addSubscription(txn, contactId, group, 0L); + oneOf(database).setExpiryTime(txn, contactId, expiry); + oneOf(database).setSubscriptionsReceived(txn, contactId, timestamp); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.receiveSubscriptionUpdate(contactId, subscriptionUpdate); + + context.assertIsSatisfied(); + } + + @Test + public void testReceiveTransportUpdate() throws Exception { + final long timestamp = 1234L; + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final TransportUpdate transportUpdate = + context.mock(TransportUpdate.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Get the contents of the update + oneOf(transportUpdate).getTransports(); + will(returnValue(transports)); + oneOf(transportUpdate).getTimestamp(); + will(returnValue(timestamp)); + oneOf(database).setTransports(txn, contactId, transports, + timestamp); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.receiveTransportUpdate(contactId, transportUpdate); + + context.assertIsSatisfied(); + } + + @Test + public void testAddingGroupMessageCallsListeners() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final DatabaseListener listener = context.mock(DatabaseListener.class); + context.checking(new Expectations() {{ + // addLocalGroupMessage(message) + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).containsSubscription(txn, groupId, timestamp); + will(returnValue(true)); + oneOf(database).addGroupMessage(txn, message); + will(returnValue(true)); + oneOf(database).getContacts(txn); + will(returnValue(Collections.singletonList(contactId))); + oneOf(database).setStatus(txn, contactId, messageId, Status.NEW); + oneOf(database).getRating(txn, authorId); + will(returnValue(Rating.UNRATED)); + oneOf(database).getNumberOfSendableChildren(txn, messageId); + will(returnValue(0)); + oneOf(database).setSendability(txn, messageId, 0); + oneOf(database).commitTransaction(txn); + // The message was added, so the listener should be called + oneOf(listener).eventOccurred(with(any(MessagesAddedEvent.class))); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addListener(listener); + db.addLocalGroupMessage(message); + + context.assertIsSatisfied(); + } + + @Test + public void testAddingPrivateMessageCallsListeners() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final DatabaseListener listener = context.mock(DatabaseListener.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // addLocalPrivateMessage(privateMessage, contactId) + oneOf(database).addPrivateMessage(txn, privateMessage, contactId); + will(returnValue(true)); + oneOf(database).setStatus(txn, contactId, messageId, Status.NEW); + // The message was added, so the listener should be called + oneOf(listener).eventOccurred(with(any(MessagesAddedEvent.class))); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addListener(listener); + db.addLocalPrivateMessage(privateMessage, contactId); + + context.assertIsSatisfied(); + } + + @Test + public void testAddingDuplicateGroupMessageDoesNotCallListeners() + throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final DatabaseListener listener = context.mock(DatabaseListener.class); + context.checking(new Expectations() {{ + // addLocalGroupMessage(message) + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).containsSubscription(txn, groupId, timestamp); + will(returnValue(true)); + oneOf(database).addGroupMessage(txn, message); + will(returnValue(false)); + oneOf(database).commitTransaction(txn); + // The message was not added, so the listener should not be called + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addListener(listener); + db.addLocalGroupMessage(message); + + context.assertIsSatisfied(); + } + + @Test + public void testAddingDuplicatePrivateMessageDoesNotCallListeners() + throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final DatabaseListener listener = context.mock(DatabaseListener.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // addLocalPrivateMessage(privateMessage, contactId) + oneOf(database).addPrivateMessage(txn, privateMessage, contactId); + will(returnValue(false)); + // The message was not added, so the listener should not be called + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addListener(listener); + db.addLocalPrivateMessage(privateMessage, contactId); + + context.assertIsSatisfied(); + } + + @Test + public void testTransportPropertiesChangedCallsListeners() + throws Exception { + final TransportProperties properties = + new TransportProperties(Collections.singletonMap("bar", "baz")); + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).getLocalProperties(txn, transportId); + will(returnValue(new TransportProperties())); + oneOf(database).mergeLocalProperties(txn, transportId, properties); + oneOf(database).setTransportsModified(with(txn), + with(any(long.class))); + oneOf(database).commitTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.mergeLocalProperties(transportId, properties); + + context.assertIsSatisfied(); + } + + @Test + public void testTransportPropertiesUnchangedDoesNotCallListeners() + throws Exception { + final TransportProperties properties = + new TransportProperties(Collections.singletonMap("bar", "baz")); + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final DatabaseListener listener = context.mock(DatabaseListener.class); + context.checking(new Expectations() {{ + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).getLocalProperties(txn, transportId); + will(returnValue(properties)); + oneOf(database).commitTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addListener(listener); + db.mergeLocalProperties(transportId, properties); + + context.assertIsSatisfied(); + } + + @Test + public void testSetSeen() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // setSeen(contactId, Collections.singletonList(messageId)) + oneOf(database).setStatusSeenIfVisible(txn, contactId, messageId); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.setSeen(contactId, Collections.singletonList(messageId)); + + context.assertIsSatisfied(); + } + + @Test + public void testVisibilityChangedCallsListeners() throws Exception { + final ContactId contactId1 = new ContactId(123); + final Collection both = Arrays.asList(contactId, contactId1); + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final DatabaseListener listener = context.mock(DatabaseListener.class); + context.checking(new Expectations() {{ + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).getVisibility(txn, groupId); + will(returnValue(both)); + oneOf(database).getContacts(txn); + will(returnValue(both)); + oneOf(database).removeVisibility(txn, contactId1, groupId); + oneOf(database).commitTransaction(txn); + oneOf(listener).eventOccurred(with(any( + SubscriptionsUpdatedEvent.class))); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addListener(listener); + db.setVisibility(groupId, Collections.singletonList(contactId)); + + context.assertIsSatisfied(); + } + + @Test + public void testVisibilityUnchangedDoesNotCallListeners() throws Exception { + final ContactId contactId1 = new ContactId(234); + final Collection both = Arrays.asList(contactId, contactId1); + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final DatabaseListener listener = context.mock(DatabaseListener.class); + context.checking(new Expectations() {{ + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).getVisibility(txn, groupId); + will(returnValue(both)); + oneOf(database).getContacts(txn); + will(returnValue(both)); + oneOf(database).commitTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addListener(listener); + db.setVisibility(groupId, both); + + context.assertIsSatisfied(); + } + + @Test + public void testTemporarySecrets() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + // addSecrets() + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).containsContactTransport(txn, contactId, + transportId); + will(returnValue(true)); + oneOf(database).addSecrets(txn, + Collections.singletonList(temporarySecret)); + oneOf(database).commitTransaction(txn); + // getSecrets() + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).getSecrets(txn); + will(returnValue(Collections.singletonList(temporarySecret))); + oneOf(database).commitTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addSecrets(Collections.singletonList(temporarySecret)); + assertEquals(Collections.singletonList(temporarySecret), + db.getSecrets()); + + context.assertIsSatisfied(); + } +} diff --git a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java new file mode 100644 index 000000000..842caa434 --- /dev/null +++ b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java @@ -0,0 +1,2044 @@ +package net.sf.briar.db; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static net.sf.briar.db.DatabaseConstants.RETRANSMIT_THRESHOLD; +import static org.junit.Assert.assertArrayEquals; + +import java.io.File; +import java.sql.Connection; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestDatabaseConfig; +import net.sf.briar.TestUtils; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.Rating; +import net.sf.briar.api.TransportConfig; +import net.sf.briar.api.TransportProperties; +import net.sf.briar.api.clock.SystemClock; +import net.sf.briar.api.db.DbException; +import net.sf.briar.api.db.MessageHeader; +import net.sf.briar.api.protocol.AuthorId; +import net.sf.briar.api.protocol.BatchId; +import net.sf.briar.api.protocol.Group; +import net.sf.briar.api.protocol.GroupFactory; +import net.sf.briar.api.protocol.GroupId; +import net.sf.briar.api.protocol.Message; +import net.sf.briar.api.protocol.MessageId; +import net.sf.briar.api.protocol.Transport; +import net.sf.briar.api.protocol.TransportId; +import net.sf.briar.api.transport.ContactTransport; +import net.sf.briar.api.transport.TemporarySecret; + +import org.apache.commons.io.FileSystemUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class H2DatabaseTest extends BriarTestCase { + + private static final int ONE_MEGABYTE = 1024 * 1024; + private static final int MAX_SIZE = 5 * ONE_MEGABYTE; + + private final File testDir = TestUtils.getTestDirectory(); + private final Random random = new Random(); + private final GroupFactory groupFactory; + private final Group group; + private final AuthorId authorId; + private final BatchId batchId; + private final ContactId contactId; + private final GroupId groupId; + private final MessageId messageId, privateMessageId; + private final String subject; + private final long timestamp; + private final int size; + private final byte[] raw; + private final Message message, privateMessage; + private final TransportId transportId; + + public H2DatabaseTest() throws Exception { + super(); + groupFactory = new TestGroupFactory(); + authorId = new AuthorId(TestUtils.getRandomId()); + batchId = new BatchId(TestUtils.getRandomId()); + contactId = new ContactId(1); + groupId = new GroupId(TestUtils.getRandomId()); + messageId = new MessageId(TestUtils.getRandomId()); + privateMessageId = new MessageId(TestUtils.getRandomId()); + group = new TestGroup(groupId, "Foo", null); + subject = "Foo"; + timestamp = System.currentTimeMillis(); + size = 1234; + raw = new byte[size]; + random.nextBytes(raw); + message = new TestMessage(messageId, null, groupId, authorId, subject, + timestamp, raw); + privateMessage = new TestMessage(privateMessageId, null, null, null, + subject, timestamp, raw); + transportId = new TransportId(TestUtils.getRandomId()); + } + + @Before + public void setUp() { + testDir.mkdirs(); + } + + @Test + public void testPersistence() throws Exception { + // Store some records + Database db = open(false); + Connection txn = db.startTransaction(); + assertFalse(db.containsContact(txn, contactId)); + assertEquals(contactId, db.addContact(txn)); + assertTrue(db.containsContact(txn, contactId)); + assertFalse(db.containsSubscription(txn, groupId)); + db.addSubscription(txn, group); + assertTrue(db.containsSubscription(txn, groupId)); + assertFalse(db.containsMessage(txn, messageId)); + db.addGroupMessage(txn, message); + assertTrue(db.containsMessage(txn, messageId)); + assertFalse(db.containsMessage(txn, privateMessageId)); + db.addPrivateMessage(txn, privateMessage, contactId); + assertTrue(db.containsMessage(txn, privateMessageId)); + db.commitTransaction(txn); + db.close(); + + // Check that the records are still there + db = open(true); + txn = db.startTransaction(); + assertTrue(db.containsContact(txn, contactId)); + assertTrue(db.containsSubscription(txn, groupId)); + assertTrue(db.containsMessage(txn, messageId)); + byte[] raw1 = db.getMessage(txn, messageId); + assertArrayEquals(raw, raw1); + assertTrue(db.containsMessage(txn, privateMessageId)); + raw1 = db.getMessage(txn, privateMessageId); + assertArrayEquals(raw, raw1); + // Delete the records + db.removeMessage(txn, messageId); + db.removeMessage(txn, privateMessageId); + db.removeContact(txn, contactId); + db.removeSubscription(txn, groupId); + db.commitTransaction(txn); + db.close(); + + // Check that the records are gone + db = open(true); + txn = db.startTransaction(); + assertFalse(db.containsContact(txn, contactId)); + assertEquals(Collections.emptyMap(), + db.getRemoteProperties(txn, transportId)); + assertFalse(db.containsSubscription(txn, groupId)); + assertFalse(db.containsMessage(txn, messageId)); + assertFalse(db.containsMessage(txn, privateMessageId)); + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testContactIdsIncrease() throws Exception { + ContactId contactId1 = new ContactId(2); + ContactId contactId2 = new ContactId(3); + ContactId contactId3 = new ContactId(4); + Database db = open(false); + Connection txn = db.startTransaction(); + + // Create three contacts + assertFalse(db.containsContact(txn, contactId)); + assertEquals(contactId, db.addContact(txn)); + assertTrue(db.containsContact(txn, contactId)); + assertFalse(db.containsContact(txn, contactId1)); + assertEquals(contactId1, db.addContact(txn)); + assertTrue(db.containsContact(txn, contactId1)); + assertFalse(db.containsContact(txn, contactId2)); + assertEquals(contactId2, db.addContact(txn)); + assertTrue(db.containsContact(txn, contactId2)); + // Delete the contact with the highest ID + db.removeContact(txn, contactId2); + assertFalse(db.containsContact(txn, contactId2)); + // Add another contact - a new ID should be created + assertFalse(db.containsContact(txn, contactId3)); + assertEquals(contactId3, db.addContact(txn)); + assertTrue(db.containsContact(txn, contactId3)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testRatings() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Unknown authors should be unrated + assertEquals(Rating.UNRATED, db.getRating(txn, authorId)); + // Store a rating + db.setRating(txn, authorId, Rating.GOOD); + // Check that the rating was stored + assertEquals(Rating.GOOD, db.getRating(txn, authorId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testUnsubscribingRemovesGroupMessage() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to a group and store a message + db.addSubscription(txn, group); + db.addGroupMessage(txn, message); + + // Unsubscribing from the group should remove the message + assertTrue(db.containsMessage(txn, messageId)); + db.removeSubscription(txn, groupId); + assertFalse(db.containsMessage(txn, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testRemovingContactRemovesPrivateMessage() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact and store a private message + assertEquals(contactId, db.addContact(txn)); + db.addPrivateMessage(txn, privateMessage, contactId); + + // Removing the contact should remove the message + assertTrue(db.containsMessage(txn, privateMessageId)); + db.removeContact(txn, contactId); + assertFalse(db.containsMessage(txn, privateMessageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSendablePrivateMessagesMustHaveStatusNew() + throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact and store a private message + assertEquals(contactId, db.addContact(txn)); + db.addPrivateMessage(txn, privateMessage, contactId); + + // The message has no status yet, so it should not be sendable + assertFalse(db.hasSendableMessages(txn, contactId)); + Iterator it = + db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + // Changing the status to NEW should make the message sendable + db.setStatus(txn, contactId, privateMessageId, Status.NEW); + assertTrue(db.hasSendableMessages(txn, contactId)); + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertTrue(it.hasNext()); + assertEquals(privateMessageId, it.next()); + assertFalse(it.hasNext()); + + // Changing the status to SENT should make the message unsendable + db.setStatus(txn, contactId, privateMessageId, Status.SENT); + assertFalse(db.hasSendableMessages(txn, contactId)); + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + // Changing the status to SEEN should also make the message unsendable + db.setStatus(txn, contactId, privateMessageId, Status.SEEN); + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSendablePrivateMessagesMustFitCapacity() + throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact and store a private message + assertEquals(contactId, db.addContact(txn)); + db.addPrivateMessage(txn, privateMessage, contactId); + db.setStatus(txn, contactId, privateMessageId, Status.NEW); + + // The message is sendable, but too large to send + assertTrue(db.hasSendableMessages(txn, contactId)); + Iterator it = + db.getSendableMessages(txn, contactId, size - 1).iterator(); + assertFalse(it.hasNext()); + + // The message is just the right size to send + assertTrue(db.hasSendableMessages(txn, contactId)); + it = db.getSendableMessages(txn, contactId, size).iterator(); + assertTrue(it.hasNext()); + assertEquals(privateMessageId, it.next()); + assertFalse(it.hasNext()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSendableGroupMessagesMustHavePositiveSendability() + throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); + db.addSubscription(txn, contactId, group, 0L); + db.addGroupMessage(txn, message); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // The message should not be sendable + assertFalse(db.hasSendableMessages(txn, contactId)); + Iterator it = + db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + // Changing the sendability to > 0 should make the message sendable + db.setSendability(txn, messageId, 1); + assertTrue(db.hasSendableMessages(txn, contactId)); + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertTrue(it.hasNext()); + assertEquals(messageId, it.next()); + assertFalse(it.hasNext()); + + // Changing the sendability to 0 should make the message unsendable + db.setSendability(txn, messageId, 0); + assertFalse(db.hasSendableMessages(txn, contactId)); + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSendableGroupMessagesMustHaveStatusNew() + throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); + db.addSubscription(txn, contactId, group, 0L); + db.addGroupMessage(txn, message); + db.setSendability(txn, messageId, 1); + + // The message has no status yet, so it should not be sendable + assertFalse(db.hasSendableMessages(txn, contactId)); + Iterator it = + db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + // Changing the status to Status.NEW should make the message sendable + db.setStatus(txn, contactId, messageId, Status.NEW); + assertTrue(db.hasSendableMessages(txn, contactId)); + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertTrue(it.hasNext()); + assertEquals(messageId, it.next()); + assertFalse(it.hasNext()); + + // Changing the status to SENT should make the message unsendable + db.setStatus(txn, contactId, messageId, Status.SENT); + assertFalse(db.hasSendableMessages(txn, contactId)); + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + // Changing the status to SEEN should also make the message unsendable + db.setStatus(txn, contactId, messageId, Status.SEEN); + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSendableGroupMessagesMustBeSubscribed() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); + db.addGroupMessage(txn, message); + db.setSendability(txn, messageId, 1); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // The contact is not subscribed, so the message should not be sendable + assertFalse(db.hasSendableMessages(txn, contactId)); + Iterator it = + db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + // The contact subscribing should make the message sendable + db.addSubscription(txn, contactId, group, 0L); + assertTrue(db.hasSendableMessages(txn, contactId)); + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertTrue(it.hasNext()); + assertEquals(messageId, it.next()); + assertFalse(it.hasNext()); + + // The contact unsubscribing should make the message unsendable + db.removeSubscriptions(txn, contactId, null, null); + assertFalse(db.hasSendableMessages(txn, contactId)); + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSendableGroupMessagesMustBeNewerThanSubscriptions() + throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); + db.addGroupMessage(txn, message); + db.setSendability(txn, messageId, 1); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // The message is older than the contact's subscription, so it should + // not be sendable + db.addSubscription(txn, contactId, group, timestamp + 1); + assertFalse(db.hasSendableMessages(txn, contactId)); + Iterator it = + db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + // Changing the contact's subscription should make the message sendable + db.removeSubscriptions(txn, contactId, null, null); + db.addSubscription(txn, contactId, group, timestamp); + assertTrue(db.hasSendableMessages(txn, contactId)); + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertTrue(it.hasNext()); + assertEquals(messageId, it.next()); + assertFalse(it.hasNext()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSendableGroupMessagesMustFitCapacity() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); + db.addSubscription(txn, contactId, group, 0L); + db.addGroupMessage(txn, message); + db.setSendability(txn, messageId, 1); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // The message is sendable, but too large to send + assertTrue(db.hasSendableMessages(txn, contactId)); + Iterator it = + db.getSendableMessages(txn, contactId, size - 1).iterator(); + assertFalse(it.hasNext()); + + // The message is just the right size to send + assertTrue(db.hasSendableMessages(txn, contactId)); + it = db.getSendableMessages(txn, contactId, size).iterator(); + assertTrue(it.hasNext()); + assertEquals(messageId, it.next()); + assertFalse(it.hasNext()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSendableGroupMessagesMustBeVisible() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addSubscription(txn, contactId, group, 0L); + db.addGroupMessage(txn, message); + db.setSendability(txn, messageId, 1); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // The subscription is not visible to the contact, so the message + // should not be sendable + assertFalse(db.hasSendableMessages(txn, contactId)); + Iterator it = + db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + // Making the subscription visible should make the message sendable + db.addVisibility(txn, contactId, groupId); + assertTrue(db.hasSendableMessages(txn, contactId)); + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertTrue(it.hasNext()); + assertEquals(messageId, it.next()); + assertFalse(it.hasNext()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testBatchesToAck() throws Exception { + BatchId batchId1 = new BatchId(TestUtils.getRandomId()); + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact and some batches to ack + assertEquals(contactId, db.addContact(txn)); + db.addBatchToAck(txn, contactId, batchId); + db.addBatchToAck(txn, contactId, batchId1); + + // Both batch IDs should be returned + Collection acks = db.getBatchesToAck(txn, contactId, 1234); + assertEquals(2, acks.size()); + assertTrue(acks.contains(batchId)); + assertTrue(acks.contains(batchId1)); + + // Remove the batch IDs + db.removeBatchesToAck(txn, contactId, acks); + + // Both batch IDs should have been removed + acks = db.getBatchesToAck(txn, contactId, 1234); + assertEquals(0, acks.size()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testDuplicateBatchesReceived() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact and receive the same batch twice + assertEquals(contactId, db.addContact(txn)); + db.addBatchToAck(txn, contactId, batchId); + db.addBatchToAck(txn, contactId, batchId); + + // The batch ID should only be returned once + Collection acks = db.getBatchesToAck(txn, contactId, 1234); + assertEquals(1, acks.size()); + assertTrue(acks.contains(batchId)); + + // Remove the batch ID + db.removeBatchesToAck(txn, contactId, acks); + + // The batch ID should have been removed + acks = db.getBatchesToAck(txn, contactId, 1234); + assertEquals(0, acks.size()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSameBatchCannotBeSentTwice() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addGroupMessage(txn, message); + + // Add an outstanding batch + db.addOutstandingBatch(txn, contactId, batchId, + Collections.singletonList(messageId)); + + // It should not be possible to add the same outstanding batch again + try { + db.addOutstandingBatch(txn, contactId, batchId, + Collections.singletonList(messageId)); + fail(); + } catch(DbException expected) {} + + db.abortTransaction(txn); + db.close(); + } + + @Test + public void testSameBatchCanBeSentToDifferentContacts() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add two contacts, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + ContactId contactId1 = db.addContact(txn); + db.addSubscription(txn, group); + db.addGroupMessage(txn, message); + + // Add an outstanding batch for the first contact + db.addOutstandingBatch(txn, contactId, batchId, + Collections.singletonList(messageId)); + + // Add the same outstanding batch for the second contact + db.addOutstandingBatch(txn, contactId1, batchId, + Collections.singletonList(messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testRemoveAckedBatch() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); + db.addSubscription(txn, contactId, group, 0L); + db.addGroupMessage(txn, message); + db.setSendability(txn, messageId, 1); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // Retrieve the message from the database and mark it as sent + Iterator it = + db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertTrue(it.hasNext()); + assertEquals(messageId, it.next()); + assertFalse(it.hasNext()); + db.setStatus(txn, contactId, messageId, Status.SENT); + db.addOutstandingBatch(txn, contactId, batchId, + Collections.singletonList(messageId)); + + // The message should no longer be sendable + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + // Pretend that the batch was acked + db.removeAckedBatch(txn, contactId, batchId); + + // The message still should not be sendable + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testRemoveLostBatch() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); + db.addSubscription(txn, contactId, group, 0L); + db.addGroupMessage(txn, message); + db.setSendability(txn, messageId, 1); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // Get the message and mark it as sent + Iterator it = + db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertTrue(it.hasNext()); + assertEquals(messageId, it.next()); + assertFalse(it.hasNext()); + db.setStatus(txn, contactId, messageId, Status.SENT); + db.addOutstandingBatch(txn, contactId, batchId, + Collections.singletonList(messageId)); + + // The message should no longer be sendable + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + // Pretend that the batch was lost + db.removeLostBatch(txn, contactId, batchId); + + // The message should be sendable again + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertTrue(it.hasNext()); + assertEquals(messageId, it.next()); + assertFalse(it.hasNext()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testRetransmission() throws Exception { + BatchId[] ids = new BatchId[RETRANSMIT_THRESHOLD + 5]; + for(int i = 0; i < ids.length; i++) { + ids[i] = new BatchId(TestUtils.getRandomId()); + } + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact + assertEquals(contactId, db.addContact(txn)); + + // Add some outstanding batches, a few ms apart + for(int i = 0; i < ids.length; i++) { + db.addOutstandingBatch(txn, contactId, ids[i], + Collections.emptyList()); + Thread.sleep(5); + } + + // The contact acks the batches in reverse order. The first + // RETRANSMIT_THRESHOLD - 1 acks should not trigger any retransmissions + for(int i = 0; i < RETRANSMIT_THRESHOLD - 1; i++) { + db.removeAckedBatch(txn, contactId, ids[ids.length - i - 1]); + Collection lost = db.getLostBatches(txn, contactId); + assertEquals(Collections.emptyList(), lost); + } + + // The next ack should trigger the retransmission of the remaining + // five outstanding batches + int index = ids.length - RETRANSMIT_THRESHOLD; + db.removeAckedBatch(txn, contactId, ids[index]); + Collection lost = db.getLostBatches(txn, contactId); + for(int i = 0; i < index; i++) { + assertTrue(lost.contains(ids[i])); + } + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testNoRetransmission() throws Exception { + BatchId[] ids = new BatchId[RETRANSMIT_THRESHOLD * 2]; + for(int i = 0; i < ids.length; i++) { + ids[i] = new BatchId(TestUtils.getRandomId()); + } + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact + assertEquals(contactId, db.addContact(txn)); + + // Add some outstanding batches, a few ms apart + for(int i = 0; i < ids.length; i++) { + db.addOutstandingBatch(txn, contactId, ids[i], + Collections.emptyList()); + Thread.sleep(5); + } + + // The contact acks the batches in the order they were sent - nothing + // should be retransmitted + for(int i = 0; i < ids.length; i++) { + db.removeAckedBatch(txn, contactId, ids[i]); + Collection lost = db.getLostBatches(txn, contactId); + assertEquals(Collections.emptyList(), lost); + } + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetMessagesByAuthor() throws Exception { + AuthorId authorId1 = new AuthorId(TestUtils.getRandomId()); + MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + Message message1 = new TestMessage(messageId1, null, groupId, authorId1, + subject, timestamp, raw); + Database db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to a group and store two messages + db.addSubscription(txn, group); + db.addGroupMessage(txn, message); + db.addGroupMessage(txn, message1); + + // Check that each message is retrievable via its author + Iterator it = + db.getMessagesByAuthor(txn, authorId).iterator(); + assertTrue(it.hasNext()); + assertEquals(messageId, it.next()); + assertFalse(it.hasNext()); + it = db.getMessagesByAuthor(txn, authorId1).iterator(); + assertTrue(it.hasNext()); + assertEquals(messageId1, it.next()); + assertFalse(it.hasNext()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetNumberOfSendableChildren() throws Exception { + MessageId childId1 = new MessageId(TestUtils.getRandomId()); + MessageId childId2 = new MessageId(TestUtils.getRandomId()); + MessageId childId3 = new MessageId(TestUtils.getRandomId()); + GroupId groupId1 = new GroupId(TestUtils.getRandomId()); + Group group1 = groupFactory.createGroup(groupId1, "Another group name", + null); + Message child1 = new TestMessage(childId1, messageId, groupId, + authorId, subject, timestamp, raw); + Message child2 = new TestMessage(childId2, messageId, groupId, + authorId, subject, timestamp, raw); + // The third child is in a different group + Message child3 = new TestMessage(childId3, messageId, groupId1, + authorId, subject, timestamp, raw); + Database db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to the groups and store the messages + db.addSubscription(txn, group); + db.addSubscription(txn, group1); + db.addGroupMessage(txn, message); + db.addGroupMessage(txn, child1); + db.addGroupMessage(txn, child2); + db.addGroupMessage(txn, child3); + // Make all the children sendable + db.setSendability(txn, childId1, 1); + db.setSendability(txn, childId2, 5); + db.setSendability(txn, childId3, 3); + + // There should be two sendable children + assertEquals(2, db.getNumberOfSendableChildren(txn, messageId)); + // Make one of the children unsendable + db.setSendability(txn, childId1, 0); + // Now there should be one sendable child + assertEquals(1, db.getNumberOfSendableChildren(txn, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetOldMessages() throws Exception { + MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + Message message1 = new TestMessage(messageId1, null, groupId, authorId, + subject, timestamp + 1000, raw); + Database db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to a group and store two messages + db.addSubscription(txn, group); + db.addGroupMessage(txn, message); + db.addGroupMessage(txn, message1); + + // Allowing enough capacity for one message should return the older one + Iterator it = db.getOldMessages(txn, size).iterator(); + assertTrue(it.hasNext()); + assertEquals(messageId, it.next()); + assertFalse(it.hasNext()); + + // Allowing enough capacity for both messages should return both + Collection ids = new HashSet(); + for(MessageId id : db.getOldMessages(txn, size * 2)) ids.add(id); + assertEquals(2, ids.size()); + assertTrue(ids.contains(messageId)); + assertTrue(ids.contains(messageId1)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetFreeSpace() throws Exception { + byte[] largeBody = new byte[ONE_MEGABYTE]; + for(int i = 0; i < largeBody.length; i++) largeBody[i] = (byte) i; + Message message1 = new TestMessage(messageId, null, groupId, authorId, + subject, timestamp, largeBody); + Database db = open(false); + + // Sanity check: there should be enough space on disk for this test + String path = testDir.getAbsolutePath(); + assertTrue(FileSystemUtils.freeSpaceKb(path) * 1024L > MAX_SIZE); + + // The free space should not be more than the allowed maximum size + long free = db.getFreeSpace(); + assertTrue(free <= MAX_SIZE); + assertTrue(free > 0); + + // Storing a message should reduce the free space + Connection txn = db.startTransaction(); + db.addSubscription(txn, group); + db.addGroupMessage(txn, message1); + db.commitTransaction(txn); + assertTrue(db.getFreeSpace() < free); + + db.close(); + } + + @Test + public void testCloseWaitsForCommit() throws Exception { + final CountDownLatch closing = new CountDownLatch(1); + final CountDownLatch closed = new CountDownLatch(1); + final AtomicBoolean transactionFinished = new AtomicBoolean(false); + final AtomicBoolean error = new AtomicBoolean(false); + final Database db = open(false); + + // Start a transaction + Connection txn = db.startTransaction(); + // In another thread, close the database + Thread close = new Thread() { + public void run() { + try { + closing.countDown(); + db.close(); + if(!transactionFinished.get()) error.set(true); + closed.countDown(); + } catch(Exception e) { + error.set(true); + } + } + }; + close.start(); + closing.await(); + // Do whatever the transaction needs to do + Thread.sleep(10); + transactionFinished.set(true); + // Commit the transaction + db.commitTransaction(txn); + // The other thread should now terminate + assertTrue(closed.await(5, SECONDS)); + // Check that the other thread didn't encounter an error + assertFalse(error.get()); + } + + @Test + public void testCloseWaitsForAbort() throws Exception { + final CountDownLatch closing = new CountDownLatch(1); + final CountDownLatch closed = new CountDownLatch(1); + final AtomicBoolean transactionFinished = new AtomicBoolean(false); + final AtomicBoolean error = new AtomicBoolean(false); + final Database db = open(false); + + // Start a transaction + Connection txn = db.startTransaction(); + // In another thread, close the database + Thread close = new Thread() { + public void run() { + try { + closing.countDown(); + db.close(); + if(!transactionFinished.get()) error.set(true); + closed.countDown(); + } catch(Exception e) { + error.set(true); + } + } + }; + close.start(); + closing.await(); + // Do whatever the transaction needs to do + Thread.sleep(10); + transactionFinished.set(true); + // Abort the transaction + db.abortTransaction(txn); + // The other thread should now terminate + assertTrue(closed.await(5, SECONDS)); + // Check that the other thread didn't encounter an error + assertFalse(error.get()); + } + + @Test + public void testUpdateTransportProperties() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact with a transport + TransportProperties properties = + new TransportProperties(Collections.singletonMap("foo", "bar")); + Transport transport = new Transport(transportId, properties); + assertEquals(contactId, db.addContact(txn)); + db.setTransports(txn, contactId, + Collections.singletonList(transport), 1); + assertEquals(Collections.singletonMap(contactId, properties), + db.getRemoteProperties(txn, transportId)); + + // Replace the transport properties + TransportProperties properties1 = + new TransportProperties(Collections.singletonMap("baz", "bam")); + Transport transport1 = new Transport(transportId, properties1); + db.setTransports(txn, contactId, + Collections.singletonList(transport1), 2); + assertEquals(Collections.singletonMap(contactId, properties1), + db.getRemoteProperties(txn, transportId)); + + // Remove the transport properties + TransportProperties properties2 = new TransportProperties(); + Transport transport2 = new Transport(transportId, properties2); + db.setTransports(txn, contactId, + Collections.singletonList(transport2), 3); + assertEquals(Collections.emptyMap(), + db.getRemoteProperties(txn, transportId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testLocalTransports() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Set the transport properties + TransportProperties properties = new TransportProperties(); + properties.put("foo", "foo"); + properties.put("bar", "bar"); + db.mergeLocalProperties(txn, transportId, properties); + assertEquals(Collections.singletonList(properties), + db.getLocalTransports(txn)); + + // Update one of the properties and add another + TransportProperties properties1 = new TransportProperties(); + properties1.put("bar", "baz"); + properties1.put("bam", "bam"); + db.mergeLocalProperties(txn, transportId, properties1); + TransportProperties expected = new TransportProperties(); + expected.put("foo", "foo"); + expected.put("bar", "baz"); + expected.put("bam", "bam"); + assertEquals(Collections.singletonList(expected), + db.getLocalTransports(txn)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testUpdateTransportConfig() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Set the transport config + TransportConfig config = new TransportConfig(); + config.put("foo", "foo"); + config.put("bar", "bar"); + db.mergeConfig(txn, transportId, config); + assertEquals(config, db.getConfig(txn, transportId)); + + // Update one of the properties and add another + TransportConfig config1 = new TransportConfig(); + config1.put("bar", "baz"); + config1.put("bam", "bam"); + db.mergeConfig(txn, transportId, config1); + TransportConfig expected = new TransportConfig(); + expected.put("foo", "foo"); + expected.put("bar", "baz"); + expected.put("bam", "bam"); + assertEquals(expected, db.getConfig(txn, transportId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testTransportsNotUpdatedIfTimestampIsOld() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact with a transport + TransportProperties properties = + new TransportProperties(Collections.singletonMap("foo", "bar")); + Transport transport = new Transport(transportId, properties); + assertEquals(contactId, db.addContact(txn)); + db.setTransports(txn, contactId, + Collections.singletonList(transport), 1); + assertEquals(Collections.singletonMap(contactId, properties), + db.getRemoteProperties(txn, transportId)); + + // Replace the transport properties using a timestamp of 2 + TransportProperties properties1 = + new TransportProperties(Collections.singletonMap("baz", "bam")); + Transport transport1 = new Transport(transportId, properties1); + db.setTransports(txn, contactId, + Collections.singletonList(transport1), 2); + assertEquals(Collections.singletonMap(contactId, properties1), + db.getRemoteProperties(txn, transportId)); + + // Try to replace the transport properties using a timestamp of 1 + TransportProperties properties2 = + new TransportProperties(Collections.singletonMap("quux", "etc")); + Transport transport2 = new Transport(transportId, properties2); + db.setTransports(txn, contactId, + Collections.singletonList(transport2), 1); + + // The old properties should still be there + assertEquals(Collections.singletonMap(contactId, properties1), + db.getRemoteProperties(txn, transportId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetMessageIfSendableReturnsNullIfNotInDatabase() + throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact and subscribe to a group + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addSubscription(txn, contactId, group, 0L); + + // The message is not in the database + assertNull(db.getMessageIfSendable(txn, contactId, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetMessageIfSendableReturnsNullIfSeen() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addSubscription(txn, contactId, group, 0L); + db.addGroupMessage(txn, message); + + // Set the sendability to > 0 and the status to SEEN + db.setSendability(txn, messageId, 1); + db.setStatus(txn, contactId, messageId, Status.SEEN); + + // The message is not sendable because its status is SEEN + assertNull(db.getMessageIfSendable(txn, contactId, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetMessageIfSendableReturnsNullIfNotSendable() + throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addSubscription(txn, contactId, group, 0L); + db.addGroupMessage(txn, message); + + // Set the sendability to 0 and the status to NEW + db.setSendability(txn, messageId, 0); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // The message is not sendable because its sendability is 0 + assertNull(db.getMessageIfSendable(txn, contactId, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetMessageIfSendableReturnsNullIfOld() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message - + // the message is older than the contact's subscription + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); + db.addSubscription(txn, contactId, group, timestamp + 1); + db.addGroupMessage(txn, message); + + // Set the sendability to > 0 and the status to NEW + db.setSendability(txn, messageId, 1); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // The message is not sendable because it's too old + assertNull(db.getMessageIfSendable(txn, contactId, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetMessageIfSendableReturnsMessage() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); + db.addSubscription(txn, contactId, group, 0L); + db.addGroupMessage(txn, message); + + // Set the sendability to > 0 and the status to NEW + db.setSendability(txn, messageId, 1); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // The message is sendable so it should be returned + byte[] b = db.getMessageIfSendable(txn, contactId, messageId); + assertArrayEquals(raw, b); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSetStatusSeenIfVisibleRequiresMessageInDatabase() + throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact and subscribe to a group + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); + db.addSubscription(txn, contactId, group, 0L); + + // The message is not in the database + assertFalse(db.setStatusSeenIfVisible(txn, contactId, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSetStatusSeenIfVisibleRequiresLocalSubscription() + throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact with a subscription + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, contactId, group, 0L); + + // There's no local subscription for the group + assertFalse(db.setStatusSeenIfVisible(txn, contactId, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSetStatusSeenIfVisibleRequiresContactSubscription() + throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addGroupMessage(txn, message); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // There's no contact subscription for the group + assertFalse(db.setStatusSeenIfVisible(txn, contactId, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSetStatusSeenIfVisibleRequiresVisibility() + throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addGroupMessage(txn, message); + db.addSubscription(txn, contactId, group, 0L); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // The subscription is not visible + assertFalse(db.setStatusSeenIfVisible(txn, contactId, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSetStatusSeenIfVisibleReturnsTrueIfAlreadySeen() + throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); + db.addSubscription(txn, contactId, group, 0L); + db.addGroupMessage(txn, message); + + // The message has already been seen by the contact + db.setStatus(txn, contactId, messageId, Status.SEEN); + + assertTrue(db.setStatusSeenIfVisible(txn, contactId, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSetStatusSeenIfVisibleReturnsTrueIfNew() + throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); + db.addSubscription(txn, contactId, group, 0L); + db.addGroupMessage(txn, message); + + // The message has not been seen by the contact + db.setStatus(txn, contactId, messageId, Status.NEW); + + assertTrue(db.setStatusSeenIfVisible(txn, contactId, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testVisibility() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact and subscribe to a group + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + // The group should not be visible to the contact + assertEquals(Collections.emptyList(), db.getVisibility(txn, groupId)); + // Make the group visible to the contact + db.addVisibility(txn, contactId, groupId); + assertEquals(Collections.singletonList(contactId), + db.getVisibility(txn, groupId)); + // Make the group invisible again + db.removeVisibility(txn, contactId, groupId); + assertEquals(Collections.emptyList(), db.getVisibility(txn, groupId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetGroupMessageParentWithNoParent() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to a group + db.addSubscription(txn, group); + + // A message with no parent should return null + MessageId childId = new MessageId(TestUtils.getRandomId()); + Message child = new TestMessage(childId, null, groupId, null, subject, + timestamp, raw); + db.addGroupMessage(txn, child); + assertTrue(db.containsMessage(txn, childId)); + assertNull(db.getGroupMessageParent(txn, childId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetGroupMessageParentWithAbsentParent() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to a group + db.addSubscription(txn, group); + + // A message with an absent parent should return null + MessageId childId = new MessageId(TestUtils.getRandomId()); + MessageId parentId = new MessageId(TestUtils.getRandomId()); + Message child = new TestMessage(childId, parentId, groupId, null, + subject, timestamp, raw); + db.addGroupMessage(txn, child); + assertTrue(db.containsMessage(txn, childId)); + assertFalse(db.containsMessage(txn, parentId)); + assertNull(db.getGroupMessageParent(txn, childId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetGroupMessageParentWithParentInAnotherGroup() + throws Exception { + GroupId groupId1 = new GroupId(TestUtils.getRandomId()); + Group group1 = groupFactory.createGroup(groupId1, "Group name", null); + Database db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to two groups + db.addSubscription(txn, group); + db.addSubscription(txn, group1); + + // A message with a parent in another group should return null + MessageId childId = new MessageId(TestUtils.getRandomId()); + MessageId parentId = new MessageId(TestUtils.getRandomId()); + Message child = new TestMessage(childId, parentId, groupId, null, + subject, timestamp, raw); + Message parent = new TestMessage(parentId, null, groupId1, null, + subject, timestamp, raw); + db.addGroupMessage(txn, child); + db.addGroupMessage(txn, parent); + assertTrue(db.containsMessage(txn, childId)); + assertTrue(db.containsMessage(txn, parentId)); + assertNull(db.getGroupMessageParent(txn, childId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetGroupMessageParentWithPrivateParent() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact and subscribe to a group + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + + // A message with a private parent should return null + MessageId childId = new MessageId(TestUtils.getRandomId()); + Message child = new TestMessage(childId, privateMessageId, groupId, + null, subject, timestamp, raw); + db.addGroupMessage(txn, child); + db.addPrivateMessage(txn, privateMessage, contactId); + assertTrue(db.containsMessage(txn, childId)); + assertTrue(db.containsMessage(txn, privateMessageId)); + assertNull(db.getGroupMessageParent(txn, childId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetGroupMessageParentWithParentInSameGroup() + throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to a group + db.addSubscription(txn, group); + + // A message with a parent in the same group should return the parent + MessageId childId = new MessageId(TestUtils.getRandomId()); + MessageId parentId = new MessageId(TestUtils.getRandomId()); + Message child = new TestMessage(childId, parentId, groupId, null, + subject, timestamp, raw); + Message parent = new TestMessage(parentId, null, groupId, null, + subject, timestamp, raw); + db.addGroupMessage(txn, child); + db.addGroupMessage(txn, parent); + assertTrue(db.containsMessage(txn, childId)); + assertTrue(db.containsMessage(txn, parentId)); + assertEquals(parentId, db.getGroupMessageParent(txn, childId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetMessageBody() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact and subscribe to a group + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + + // Store a couple of messages + int bodyLength = raw.length - 20; + Message message1 = new TestMessage(messageId, null, groupId, null, + subject, timestamp, raw, 5, bodyLength); + Message privateMessage1 = new TestMessage(privateMessageId, null, null, + null, subject, timestamp, raw, 10, bodyLength); + db.addGroupMessage(txn, message1); + db.addPrivateMessage(txn, privateMessage1, contactId); + + // Calculate the expected message bodies + byte[] expectedBody = new byte[bodyLength]; + System.arraycopy(raw, 5, expectedBody, 0, bodyLength); + assertFalse(Arrays.equals(expectedBody, new byte[bodyLength])); + byte[] expectedBody1 = new byte[bodyLength]; + System.arraycopy(raw, 10, expectedBody1, 0, bodyLength); + System.arraycopy(raw, 10, expectedBody1, 0, bodyLength); + + // Retrieve the raw messages + assertArrayEquals(raw, db.getMessage(txn, messageId)); + assertArrayEquals(raw, db.getMessage(txn, privateMessageId)); + + // Retrieve the message bodies + byte[] body = db.getMessageBody(txn, messageId); + assertArrayEquals(expectedBody, body); + byte[] body1 = db.getMessageBody(txn, privateMessageId); + assertArrayEquals(expectedBody1, body1); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetMessageHeaders() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to a group + db.addSubscription(txn, group); + + // Store a couple of messages + db.addGroupMessage(txn, message); + MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + MessageId parentId = new MessageId(TestUtils.getRandomId()); + long timestamp1 = System.currentTimeMillis(); + Message message1 = new TestMessage(messageId1, parentId, groupId, + authorId, subject, timestamp1, raw); + db.addGroupMessage(txn, message1); + // Mark one of the messages read + assertFalse(db.setRead(txn, messageId, true)); + + // Retrieve the message headers + Collection headers = db.getMessageHeaders(txn, groupId); + Iterator it = headers.iterator(); + boolean messageFound = false, message1Found = false; + // First header (order is undefined) + assertTrue(it.hasNext()); + MessageHeader header = it.next(); + if(messageId.equals(header.getId())) { + assertHeadersMatch(message, header); + assertTrue(header.getRead()); + assertFalse(header.getStarred()); + messageFound = true; + } else if(messageId1.equals(header.getId())) { + assertHeadersMatch(message1, header); + assertFalse(header.getRead()); + assertFalse(header.getStarred()); + message1Found = true; + } else { + fail(); + } + // Second header + assertTrue(it.hasNext()); + header = it.next(); + if(messageId.equals(header.getId())) { + assertHeadersMatch(message, header); + assertTrue(header.getRead()); + assertFalse(header.getStarred()); + messageFound = true; + } else if(messageId1.equals(header.getId())) { + assertHeadersMatch(message1, header); + assertFalse(header.getRead()); + assertFalse(header.getStarred()); + message1Found = true; + } else { + fail(); + } + // No more headers + assertFalse(it.hasNext()); + assertTrue(messageFound); + assertTrue(message1Found); + + db.commitTransaction(txn); + db.close(); + } + + private void assertHeadersMatch(Message m, MessageHeader h) { + assertEquals(m.getId(), h.getId()); + if(m.getParent() == null) assertNull(h.getParent()); + else assertEquals(m.getParent(), h.getParent()); + if(m.getGroup() == null) assertNull(h.getGroup()); + else assertEquals(m.getGroup(), h.getGroup()); + if(m.getAuthor() == null) assertNull(h.getAuthor()); + else assertEquals(m.getAuthor(), h.getAuthor()); + assertEquals(m.getSubject(), h.getSubject()); + assertEquals(m.getTimestamp(), h.getTimestamp()); + } + + @Test + public void testReadFlag() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to a group and store a message + db.addSubscription(txn, group); + db.addGroupMessage(txn, message); + + // The message should be unread by default + assertFalse(db.getRead(txn, messageId)); + // Marking the message read should return the old value + assertFalse(db.setRead(txn, messageId, true)); + assertTrue(db.setRead(txn, messageId, true)); + // The message should be read + assertTrue(db.getRead(txn, messageId)); + // Marking the message unread should return the old value + assertTrue(db.setRead(txn, messageId, false)); + assertFalse(db.setRead(txn, messageId, false)); + // Unsubscribe from the group + db.removeSubscription(txn, groupId); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testStarredFlag() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to a group and store a message + db.addSubscription(txn, group); + db.addGroupMessage(txn, message); + + // The message should be unstarred by default + assertFalse(db.getStarred(txn, messageId)); + // Starring the message should return the old value + assertFalse(db.setStarred(txn, messageId, true)); + assertTrue(db.setStarred(txn, messageId, true)); + // The message should be starred + assertTrue(db.getStarred(txn, messageId)); + // Unstarring the message should return the old value + assertTrue(db.setStarred(txn, messageId, false)); + assertFalse(db.setStarred(txn, messageId, false)); + // Unsubscribe from the group + db.removeSubscription(txn, groupId); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetUnreadMessageCounts() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to a couple of groups + db.addSubscription(txn, group); + GroupId groupId1 = new GroupId(TestUtils.getRandomId()); + Group group1 = groupFactory.createGroup(groupId1, "Another group", + null); + db.addSubscription(txn, group1); + + // Store two messages in the first group + db.addGroupMessage(txn, message); + MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + Message message1 = new TestMessage(messageId1, null, groupId, + authorId, subject, timestamp, raw); + db.addGroupMessage(txn, message1); + + // Store one message in the second group + MessageId messageId2 = new MessageId(TestUtils.getRandomId()); + Message message2 = new TestMessage(messageId2, null, groupId1, + authorId, subject, timestamp, raw); + db.addGroupMessage(txn, message2); + + // Mark one of the messages in the first group read + assertFalse(db.setRead(txn, messageId, true)); + + // There should be one unread message in each group + Map counts = db.getUnreadMessageCounts(txn); + assertEquals(2, counts.size()); + Integer count = counts.get(groupId); + assertNotNull(count); + assertEquals(1, count.intValue()); + count = counts.get(groupId1); + assertNotNull(count); + assertEquals(1, count.intValue()); + + // Mark the read message unread (it will now be false rather than null) + assertTrue(db.setRead(txn, messageId, false)); + + // Mark the message in the second group read + assertFalse(db.setRead(txn, messageId2, true)); + + // There should be two unread messages in the first group, none in + // the second group + counts = db.getUnreadMessageCounts(txn); + assertEquals(1, counts.size()); + count = counts.get(groupId); + assertNotNull(count); + assertEquals(2, count.intValue()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testMultipleSubscriptionsAndUnsubscriptions() throws Exception { + // Create some groups + List groups = new ArrayList(); + for(int i = 0; i < 100; i++) { + GroupId id = new GroupId(TestUtils.getRandomId()); + groups.add(groupFactory.createGroup(id, "Group name", null)); + } + + Database db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to the groups and add a contact + for(Group g : groups) db.addSubscription(txn, g); + assertEquals(contactId, db.addContact(txn)); + + // Make the groups visible to the contact + Collections.shuffle(groups); + for(Group g : groups) db.addVisibility(txn, contactId, g.getId()); + + // Make some of the groups invisible to the contact and remove them all + Collections.shuffle(groups); + for(Group g : groups) { + if(Math.random() < 0.5) + db.removeVisibility(txn, contactId, g.getId()); + db.removeSubscription(txn, g.getId()); + } + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testTemporarySecrets() throws Exception { + // Create a contact transport and three consecutive temporary secrets + long epoch = 123L, clockDiff = 234L, latency = 345L; + boolean alice = false; + long outgoing1 = 456L, centre1 = 567L; + long outgoing2 = 678L, centre2 = 789L; + long outgoing3 = 890L, centre3 = 901L; + ContactTransport ct = new ContactTransport(contactId, transportId, + epoch, clockDiff, latency, alice); + Random random = new Random(); + byte[] secret1 = new byte[32], bitmap1 = new byte[4]; + random.nextBytes(secret1); + random.nextBytes(bitmap1); + TemporarySecret s1 = new TemporarySecret(contactId, transportId, epoch, + clockDiff, latency, alice, 0L, secret1, outgoing1, centre1, + bitmap1); + byte[] secret2 = new byte[32], bitmap2 = new byte[4]; + random.nextBytes(secret2); + random.nextBytes(bitmap2); + TemporarySecret s2 = new TemporarySecret(contactId, transportId, epoch, + clockDiff, latency, alice, 1L, secret2, outgoing2, centre2, + bitmap2); + byte[] secret3 = new byte[32], bitmap3 = new byte[4]; + random.nextBytes(secret3); + random.nextBytes(bitmap3); + TemporarySecret s3 = new TemporarySecret(contactId, transportId, epoch, + clockDiff, latency, alice, 2L, secret3, outgoing3, centre3, + bitmap3); + + Database db = open(false); + Connection txn = db.startTransaction(); + + // Initially there should be no secrets in the database + assertEquals(Collections.emptyList(), db.getSecrets(txn)); + + // Add the contact transport and the first two secrets + assertEquals(contactId, db.addContact(txn)); + db.addContactTransport(txn, ct); + db.addSecrets(txn, Arrays.asList(s1, s2)); + + // Retrieve the first two secrets + Collection secrets = db.getSecrets(txn); + assertEquals(2, secrets.size()); + boolean foundFirst = false, foundSecond = false; + for(TemporarySecret s : secrets) { + assertEquals(contactId, s.getContactId()); + assertEquals(transportId, s.getTransportId()); + assertEquals(epoch, s.getEpoch()); + assertEquals(clockDiff, s.getClockDifference()); + assertEquals(latency, s.getLatency()); + assertEquals(alice, s.getAlice()); + if(s.getPeriod() == 0L) { + assertArrayEquals(secret1, s.getSecret()); + assertEquals(outgoing1, s.getOutgoingConnectionCounter()); + assertEquals(centre1, s.getWindowCentre()); + assertArrayEquals(bitmap1, s.getWindowBitmap()); + foundFirst = true; + } else if(s.getPeriod() == 1L) { + assertArrayEquals(secret2, s.getSecret()); + assertEquals(outgoing2, s.getOutgoingConnectionCounter()); + assertEquals(centre2, s.getWindowCentre()); + assertArrayEquals(bitmap2, s.getWindowBitmap()); + foundSecond = true; + } else { + fail(); + } + } + assertTrue(foundFirst); + assertTrue(foundSecond); + + // Adding the third secret (period 2) should delete the first (period 0) + db.addSecrets(txn, Arrays.asList(s3)); + secrets = db.getSecrets(txn); + assertEquals(2, secrets.size()); + foundSecond = false; + boolean foundThird = false; + for(TemporarySecret s : secrets) { + assertEquals(contactId, s.getContactId()); + assertEquals(transportId, s.getTransportId()); + assertEquals(epoch, s.getEpoch()); + assertEquals(clockDiff, s.getClockDifference()); + assertEquals(latency, s.getLatency()); + assertEquals(alice, s.getAlice()); + if(s.getPeriod() == 1L) { + assertArrayEquals(secret2, s.getSecret()); + assertEquals(outgoing2, s.getOutgoingConnectionCounter()); + assertEquals(centre2, s.getWindowCentre()); + assertArrayEquals(bitmap2, s.getWindowBitmap()); + foundSecond = true; + } else if(s.getPeriod() == 2L) { + assertArrayEquals(secret3, s.getSecret()); + assertEquals(outgoing3, s.getOutgoingConnectionCounter()); + assertEquals(centre3, s.getWindowCentre()); + assertArrayEquals(bitmap3, s.getWindowBitmap()); + foundThird = true; + } else { + fail(); + } + } + assertTrue(foundSecond); + assertTrue(foundThird); + + // Removing the contact should remove the secrets + db.removeContact(txn, contactId); + assertEquals(Collections.emptyList(), db.getSecrets(txn)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testIncrementConnectionCounter() throws Exception { + // Create a contact transport and a temporary secret + long epoch = 123L, clockDiff = 234L, latency = 345L; + boolean alice = false; + long period = 456L, outgoing = 567L, centre = 678L; + ContactTransport ct = new ContactTransport(contactId, transportId, + epoch, clockDiff, latency, alice); + Random random = new Random(); + byte[] secret = new byte[32], bitmap = new byte[4]; + random.nextBytes(secret); + TemporarySecret s = new TemporarySecret(contactId, transportId, epoch, + clockDiff, latency, alice, period, secret, outgoing, centre, + bitmap); + + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add the contact transport and the temporary secret + assertEquals(contactId, db.addContact(txn)); + db.addContactTransport(txn, ct); + db.addSecrets(txn, Arrays.asList(s)); + + // Retrieve the secret + Collection secrets = db.getSecrets(txn); + assertEquals(1, secrets.size()); + s = secrets.iterator().next(); + assertEquals(contactId, s.getContactId()); + assertEquals(transportId, s.getTransportId()); + assertEquals(period, s.getPeriod()); + assertArrayEquals(secret, s.getSecret()); + assertEquals(outgoing, s.getOutgoingConnectionCounter()); + assertEquals(centre, s.getWindowCentre()); + assertArrayEquals(bitmap, s.getWindowBitmap()); + + // Increment the connection counter twice and retrieve the secret again + assertEquals(outgoing, db.incrementConnectionCounter(txn, + s.getContactId(), s.getTransportId(), s.getPeriod())); + assertEquals(outgoing + 1L, db.incrementConnectionCounter(txn, + s.getContactId(), s.getTransportId(), s.getPeriod())); + secrets = db.getSecrets(txn); + assertEquals(1, secrets.size()); + s = secrets.iterator().next(); + assertEquals(contactId, s.getContactId()); + assertEquals(transportId, s.getTransportId()); + assertEquals(period, s.getPeriod()); + assertArrayEquals(secret, s.getSecret()); + assertEquals(outgoing + 2L, s.getOutgoingConnectionCounter()); + assertEquals(centre, s.getWindowCentre()); + assertArrayEquals(bitmap, s.getWindowBitmap()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSetConnectionWindow() throws Exception { + // Create a contact transport and a temporary secret + long epoch = 123L, clockDiff = 234L, latency = 345L; + boolean alice = false; + long period = 456L, outgoing = 567L, centre = 678L; + ContactTransport ct = new ContactTransport(contactId, transportId, + epoch, clockDiff, latency, alice); + Random random = new Random(); + byte[] secret = new byte[32], bitmap = new byte[4]; + random.nextBytes(secret); + TemporarySecret s = new TemporarySecret(contactId, transportId, epoch, + clockDiff, latency, alice, period, secret, outgoing, centre, + bitmap); + + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add the contact transport and the temporary secret + assertEquals(contactId, db.addContact(txn)); + db.addContactTransport(txn, ct); + db.addSecrets(txn, Arrays.asList(s)); + + // Retrieve the secret + Collection secrets = db.getSecrets(txn); + assertEquals(1, secrets.size()); + s = secrets.iterator().next(); + assertEquals(contactId, s.getContactId()); + assertEquals(transportId, s.getTransportId()); + assertEquals(period, s.getPeriod()); + assertArrayEquals(secret, s.getSecret()); + assertEquals(outgoing, s.getOutgoingConnectionCounter()); + assertEquals(centre, s.getWindowCentre()); + assertArrayEquals(bitmap, s.getWindowBitmap()); + + // Update the connection window and retrieve the secret again + random.nextBytes(bitmap); + db.setConnectionWindow(txn, contactId, transportId, period, centre, + bitmap); + secrets = db.getSecrets(txn); + assertEquals(1, secrets.size()); + s = secrets.iterator().next(); + assertEquals(contactId, s.getContactId()); + assertEquals(transportId, s.getTransportId()); + assertEquals(period, s.getPeriod()); + assertArrayEquals(secret, s.getSecret()); + assertEquals(outgoing, s.getOutgoingConnectionCounter()); + assertEquals(centre, s.getWindowCentre()); + assertArrayEquals(bitmap, s.getWindowBitmap()); + + // Updating a nonexistent window should not throw an exception + db.setConnectionWindow(txn, contactId, transportId, period + 1L, 1L, + bitmap); + // The nonexistent window should not have been created + secrets = db.getSecrets(txn); + assertEquals(1, secrets.size()); + s = secrets.iterator().next(); + assertEquals(contactId, s.getContactId()); + assertEquals(transportId, s.getTransportId()); + assertEquals(period, s.getPeriod()); + assertArrayEquals(secret, s.getSecret()); + assertEquals(outgoing, s.getOutgoingConnectionCounter()); + assertEquals(centre, s.getWindowCentre()); + assertArrayEquals(bitmap, s.getWindowBitmap()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testContactTransports() throws Exception { + // Create some contact transports + long epoch1 = 123L, clockDiff1 = 234L, latency1 = 345L; + long epoch2 = 456L, clockDiff2 = 567L, latency2 = 678L; + boolean alice1 = true, alice2 = false; + TransportId transportId1 = new TransportId(TestUtils.getRandomId()); + TransportId transportId2 = new TransportId(TestUtils.getRandomId()); + ContactTransport ct1 = new ContactTransport(contactId, transportId1, + epoch1, clockDiff1, latency1, alice1); + ContactTransport ct2 = new ContactTransport(contactId, transportId2, + epoch2, clockDiff2, latency2, alice2); + + Database db = open(false); + Connection txn = db.startTransaction(); + + // Initially there should be no contact transports in the database + assertEquals(Collections.emptyList(), db.getContactTransports(txn)); + + // Add a contact and the contact transports + assertEquals(contactId, db.addContact(txn)); + db.addContactTransport(txn, ct1); + db.addContactTransport(txn, ct2); + + // Retrieve the contact transports + Collection cts = db.getContactTransports(txn); + assertEquals(2, cts.size()); + boolean foundFirst = false, foundSecond = false; + for(ContactTransport ct : cts) { + assertEquals(contactId, ct.getContactId()); + if(ct.getTransportId().equals(transportId1)) { + assertEquals(epoch1, ct.getEpoch()); + assertEquals(clockDiff1, ct.getClockDifference()); + assertEquals(latency1, ct.getLatency()); + assertEquals(alice1, ct.getAlice()); + foundFirst = true; + } else if(ct.getTransportId().equals(transportId2)) { + assertEquals(epoch2, ct.getEpoch()); + assertEquals(clockDiff2, ct.getClockDifference()); + assertEquals(latency2, ct.getLatency()); + assertEquals(alice2, ct.getAlice()); + foundSecond = true; + } else { + fail(); + } + } + assertTrue(foundFirst); + assertTrue(foundSecond); + + // Removing the contact should remove the contact transports + db.removeContact(txn, contactId); + assertEquals(Collections.emptyList(), db.getContactTransports(txn)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testExceptionHandling() throws Exception { + Database db = open(false); + Connection txn = db.startTransaction(); + try { + // Ask for a nonexistent message - an exception should be thrown + db.getMessage(txn, messageId); + fail(); + } catch(DbException expected) { + // It should be possible to abort the transaction without error + db.abortTransaction(txn); + } + // It should be possible to close the database cleanly + db.close(); + } + + private Database open(boolean resume) throws Exception { + Database db = new H2Database( + new TestDatabaseConfig(testDir, MAX_SIZE), groupFactory, + new SystemClock()); + db.open(resume); + return db; + } + + @After + public void tearDown() { + TestUtils.deleteTestDirectory(testDir); + } +} diff --git a/briar-tests/src/net/sf/briar/db/TestGroup.java b/briar-tests/src/net/sf/briar/db/TestGroup.java new file mode 100644 index 000000000..4e78e0788 --- /dev/null +++ b/briar-tests/src/net/sf/briar/db/TestGroup.java @@ -0,0 +1,29 @@ +package net.sf.briar.db; + +import net.sf.briar.api.protocol.Group; +import net.sf.briar.api.protocol.GroupId; + +class TestGroup implements Group { + + private final GroupId id; + private final String name; + private final byte[] publicKey; + + public TestGroup(GroupId id, String name, byte[] publicKey) { + this.id = id; + this.name = name; + this.publicKey = publicKey; + } + + public GroupId getId() { + return id; + } + + public String getName() { + return name; + } + + public byte[] getPublicKey() { + return publicKey; + } +} diff --git a/briar-tests/src/net/sf/briar/db/TestGroupFactory.java b/briar-tests/src/net/sf/briar/db/TestGroupFactory.java new file mode 100644 index 000000000..89f29b92d --- /dev/null +++ b/briar-tests/src/net/sf/briar/db/TestGroupFactory.java @@ -0,0 +1,20 @@ +package net.sf.briar.db; + +import java.io.IOException; + +import net.sf.briar.TestUtils; +import net.sf.briar.api.protocol.Group; +import net.sf.briar.api.protocol.GroupFactory; +import net.sf.briar.api.protocol.GroupId; + +class TestGroupFactory implements GroupFactory { + + public Group createGroup(String name, byte[] publicKey) throws IOException { + GroupId id = new GroupId(TestUtils.getRandomId()); + return new TestGroup(id, name, publicKey); + } + + public Group createGroup(GroupId id, String name, byte[] publicKey) { + return new TestGroup(id, name, publicKey); + } +} \ No newline at end of file diff --git a/briar-tests/src/net/sf/briar/db/TestMessage.java b/briar-tests/src/net/sf/briar/db/TestMessage.java new file mode 100644 index 000000000..4dbe7584a --- /dev/null +++ b/briar-tests/src/net/sf/briar/db/TestMessage.java @@ -0,0 +1,89 @@ +package net.sf.briar.db; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +import net.sf.briar.api.protocol.AuthorId; +import net.sf.briar.api.protocol.GroupId; +import net.sf.briar.api.protocol.Message; +import net.sf.briar.api.protocol.MessageId; + +class TestMessage implements Message { + + private final MessageId id, parent; + private final GroupId group; + private final AuthorId author; + private final String subject; + private final long timestamp; + private final byte[] raw; + private final int bodyStart, bodyLength; + + public TestMessage(MessageId id, MessageId parent, GroupId group, + AuthorId author, String subject, long timestamp, byte[] raw) { + this(id, parent, group, author, subject, timestamp, raw, 0, raw.length); + } + + public TestMessage(MessageId id, MessageId parent, GroupId group, + AuthorId author, String subject, long timestamp, byte[] raw, + int bodyStart, int bodyLength) { + this.id = id; + this.parent = parent; + this.group = group; + this.author = author; + this.subject = subject; + this.timestamp = timestamp; + this.raw = raw; + this.bodyStart = bodyStart; + this.bodyLength = bodyLength; + } + + public MessageId getId() { + return id; + } + + public MessageId getParent() { + return parent; + } + + public GroupId getGroup() { + return group; + } + + public AuthorId getAuthor() { + return author; + } + + public String getSubject() { + return subject; + } + + public long getTimestamp() { + return timestamp; + } + + public byte[] getSerialised() { + return raw; + } + + public int getBodyStart() { + return bodyStart; + } + + public int getBodyLength() { + return bodyLength; + } + + public InputStream getSerialisedStream() { + return new ByteArrayInputStream(raw); + } + + @Override + public boolean equals(Object o) { + return o instanceof Message && id.equals(((Message)o).getId()); + } + + @Override + public int hashCode() { + return id.hashCode(); + } +} diff --git a/briar-tests/src/net/sf/briar/lifecycle/ShutdownManagerImplTest.java b/briar-tests/src/net/sf/briar/lifecycle/ShutdownManagerImplTest.java new file mode 100644 index 000000000..da3821569 --- /dev/null +++ b/briar-tests/src/net/sf/briar/lifecycle/ShutdownManagerImplTest.java @@ -0,0 +1,33 @@ +package net.sf.briar.lifecycle; + +import java.util.HashSet; +import java.util.Set; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.lifecycle.ShutdownManager; + +import org.junit.Test; + +public class ShutdownManagerImplTest extends BriarTestCase { + + @Test + public void testAddAndRemove() { + ShutdownManager s = createShutdownManager(); + Set handles = new HashSet(); + for(int i = 0; i < 100; i++) { + int handle = s.addShutdownHook(new Runnable() { + public void run() {} + }); + // The handles should all be distinct + assertTrue(handles.add(handle)); + } + // The hooks should be removable + for(int handle : handles) assertTrue(s.removeShutdownHook(handle)); + // The hooks should no longer be removable + for(int handle : handles) assertFalse(s.removeShutdownHook(handle)); + } + + protected ShutdownManager createShutdownManager() { + return new ShutdownManagerImpl(); + } +} diff --git a/briar-tests/src/net/sf/briar/lifecycle/WindowsShutdownManagerImplTest.java b/briar-tests/src/net/sf/briar/lifecycle/WindowsShutdownManagerImplTest.java new file mode 100644 index 000000000..ce872e97d --- /dev/null +++ b/briar-tests/src/net/sf/briar/lifecycle/WindowsShutdownManagerImplTest.java @@ -0,0 +1,39 @@ +package net.sf.briar.lifecycle; + +import net.sf.briar.api.lifecycle.ShutdownManager; + +import org.junit.Test; + +public class WindowsShutdownManagerImplTest extends ShutdownManagerImplTest { + + @Override + protected ShutdownManager createShutdownManager() { + return new WindowsShutdownManagerImpl(); + } + + @Test + public void testManagerWaitsForHooksToRun() { + WindowsShutdownManagerImpl s = new WindowsShutdownManagerImpl(); + SlowHook[] hooks = new SlowHook[10]; + for(int i = 0; i < hooks.length; i++) { + hooks[i] = new SlowHook(); + s.addShutdownHook(hooks[i]); + } + s.runShutdownHooks(); + for(int i = 0; i < hooks.length; i++) assertTrue(hooks[i].finished); + } + + private static class SlowHook implements Runnable { + + private volatile boolean finished = false; + + public void run() { + try { + Thread.sleep(100); + finished = true; + } catch(InterruptedException e) { + fail(); + } + } + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/DuplexClientTest.java b/briar-tests/src/net/sf/briar/plugins/DuplexClientTest.java new file mode 100644 index 000000000..2ffdace5e --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/DuplexClientTest.java @@ -0,0 +1,101 @@ +package net.sf.briar.plugins; + +import java.io.IOException; +import java.util.Map; + +import net.sf.briar.api.ContactId; +import net.sf.briar.api.TransportConfig; +import net.sf.briar.api.TransportProperties; +import net.sf.briar.api.plugins.duplex.DuplexPluginCallback; +import net.sf.briar.api.plugins.duplex.DuplexTransportConnection; + +public abstract class DuplexClientTest extends DuplexTest { + + protected ClientCallback callback = null; + + protected void run() throws IOException { + assert plugin != null; + // Start the plugin + System.out.println("Starting plugin"); + plugin.start(); + // Try to connect to the server + System.out.println("Creating connection"); + DuplexTransportConnection d = plugin.createConnection(contactId); + if(d == null) { + System.out.println("Connection failed"); + } else { + System.out.println("Connection created"); + receiveChallengeSendResponse(d); + } + // Try to send an invitation + System.out.println("Sending invitation"); + d = plugin.sendInvitation(getPseudoRandom(123), INVITATION_TIMEOUT); + if(d == null) { + System.out.println("Connection failed"); + } else { + System.out.println("Connection created"); + receiveChallengeSendResponse(d); + } + // Try to accept an invitation + System.out.println("Accepting invitation"); + d = plugin.acceptInvitation(getPseudoRandom(456), INVITATION_TIMEOUT); + if(d == null) { + System.out.println("Connection failed"); + } else { + System.out.println("Connection created"); + sendChallengeReceiveResponse(d); + } + // Stop the plugin + System.out.println("Stopping plugin"); + plugin.stop(); + } + + protected static class ClientCallback implements DuplexPluginCallback { + + private TransportConfig config = null; + private TransportProperties local = null; + private Map remote = null; + + public ClientCallback(TransportConfig config, TransportProperties local, + Map remote) { + this.config = config; + this.local = local; + this.remote = remote; + } + + public TransportConfig getConfig() { + return config; + } + + public TransportProperties getLocalProperties() { + return local; + } + + public Map getRemoteProperties() { + return remote; + } + + public void mergeConfig(TransportConfig c) { + config = c; + } + + public void mergeLocalProperties(TransportProperties p) { + local = p; + } + + public int showChoice(String[] options, String... message) { + return -1; + } + + public boolean showConfirmationMessage(String... message) { + return false; + } + + public void showMessage(String... message) {} + + public void incomingConnectionCreated(DuplexTransportConnection d) {} + + public void outgoingConnectionCreated(ContactId contactId, + DuplexTransportConnection d) {} + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/DuplexServerTest.java b/briar-tests/src/net/sf/briar/plugins/DuplexServerTest.java new file mode 100644 index 000000000..956eea2d3 --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/DuplexServerTest.java @@ -0,0 +1,103 @@ +package net.sf.briar.plugins; + +import java.util.Map; +import java.util.concurrent.CountDownLatch; + +import net.sf.briar.api.ContactId; +import net.sf.briar.api.TransportConfig; +import net.sf.briar.api.TransportProperties; +import net.sf.briar.api.plugins.duplex.DuplexPluginCallback; +import net.sf.briar.api.plugins.duplex.DuplexTransportConnection; + +public abstract class DuplexServerTest extends DuplexTest { + + protected ServerCallback callback = null; + + protected void run() throws Exception { + assert callback != null; + assert plugin != null; + // Start the plugin + System.out.println("Starting plugin"); + plugin.start(); + // Wait for a connection + System.out.println("Waiting for connection"); + callback.latch.await(); + // Try to accept an invitation + System.out.println("Accepting invitation"); + DuplexTransportConnection d = plugin.acceptInvitation( + getPseudoRandom(123), INVITATION_TIMEOUT); + if(d == null) { + System.out.println("Connection failed"); + } else { + System.out.println("Connection created"); + sendChallengeReceiveResponse(d); + } + // Try to send an invitation + System.out.println("Sending invitation"); + d = plugin.sendInvitation(getPseudoRandom(456), INVITATION_TIMEOUT); + if(d == null) { + System.out.println("Connection failed"); + } else { + System.out.println("Connection created"); + receiveChallengeSendResponse(d); + } + // Stop the plugin + System.out.println("Stopping plugin"); + plugin.stop(); + } + + protected class ServerCallback implements DuplexPluginCallback { + + private final CountDownLatch latch = new CountDownLatch(1); + + private TransportConfig config; + private TransportProperties local; + private Map remote; + + public ServerCallback(TransportConfig config, TransportProperties local, + Map remote) { + this.config = config; + this.local = local; + this.remote = remote; + } + + public TransportConfig getConfig() { + return config; + } + + public TransportProperties getLocalProperties() { + return local; + } + + public Map getRemoteProperties() { + return remote; + } + + public void mergeConfig(TransportConfig c) { + config = c; + } + + public void mergeLocalProperties(TransportProperties p) { + local = p; + } + + public int showChoice(String[] options, String... message) { + return -1; + } + + public boolean showConfirmationMessage(String... message) { + return false; + } + + public void showMessage(String... message) {} + + public void incomingConnectionCreated(DuplexTransportConnection d) { + System.out.println("Connection received"); + sendChallengeReceiveResponse(d); + latch.countDown(); + } + + public void outgoingConnectionCreated(ContactId c, + DuplexTransportConnection d) {} + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/DuplexTest.java b/briar-tests/src/net/sf/briar/plugins/DuplexTest.java new file mode 100644 index 000000000..80eee278f --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/DuplexTest.java @@ -0,0 +1,98 @@ +package net.sf.briar.plugins; + +import java.io.IOException; +import java.io.PrintStream; +import java.util.Random; +import java.util.Scanner; + +import net.sf.briar.api.ContactId; +import net.sf.briar.api.crypto.PseudoRandom; +import net.sf.briar.api.plugins.duplex.DuplexPlugin; +import net.sf.briar.api.plugins.duplex.DuplexTransportConnection; + +abstract class DuplexTest { + + protected static final String CHALLENGE = "Carrots!"; + protected static final String RESPONSE = "Potatoes!"; + protected static final long INVITATION_TIMEOUT = 30 * 1000; + + protected final ContactId contactId = new ContactId(234); + + protected DuplexPlugin plugin = null; + + protected void sendChallengeReceiveResponse(DuplexTransportConnection d) { + assert plugin != null; + try { + PrintStream out = new PrintStream(d.getOutputStream()); + out.println(CHALLENGE); + System.out.println("Sent challenge: " + CHALLENGE); + Scanner in = new Scanner(d.getInputStream()); + if(in.hasNextLine()) { + String response = in.nextLine(); + System.out.println("Received response: " + response); + if(RESPONSE.equals(response)) { + System.out.println("Correct response"); + } else { + System.out.println("Incorrect response"); + } + } else { + System.out.println("No response"); + } + d.dispose(false, true); + } catch(IOException e) { + e.printStackTrace(); + try { + d.dispose(true, true); + } catch(IOException e1) { + e1.printStackTrace(); + } + } + } + + protected void receiveChallengeSendResponse(DuplexTransportConnection d) { + assert plugin != null; + try { + Scanner in = new Scanner(d.getInputStream()); + if(in.hasNextLine()) { + String challenge = in.nextLine(); + System.out.println("Received challenge: " + challenge); + if(CHALLENGE.equals(challenge)) { + PrintStream out = new PrintStream(d.getOutputStream()); + out.println(RESPONSE); + System.out.println("Sent response: " + RESPONSE); + } else { + System.out.println("Incorrect challenge"); + } + } else { + System.out.println("No challenge"); + } + d.dispose(false, true); + } catch(IOException e) { + e.printStackTrace(); + try { + d.dispose(true, true); + } catch(IOException e1) { + e1.printStackTrace(); + } + } + } + + protected PseudoRandom getPseudoRandom(int seed) { + return new TestPseudoRandom(seed); + } + + private static class TestPseudoRandom implements PseudoRandom { + + private final Random r; + + private TestPseudoRandom(int seed) { + r = new Random(seed); + } + + public byte[] nextBytes(int bytes) { + byte[] b = new byte[bytes]; + r.nextBytes(b); + return b; + } + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/ImmediateExecutor.java b/briar-tests/src/net/sf/briar/plugins/ImmediateExecutor.java new file mode 100644 index 000000000..fe21666e0 --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/ImmediateExecutor.java @@ -0,0 +1,10 @@ +package net.sf.briar.plugins; + +import java.util.concurrent.Executor; + +public class ImmediateExecutor implements Executor { + + public void execute(Runnable r) { + r.run(); + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/PluginManagerImplTest.java b/briar-tests/src/net/sf/briar/plugins/PluginManagerImplTest.java new file mode 100644 index 000000000..f7bad8c34 --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/PluginManagerImplTest.java @@ -0,0 +1,63 @@ +package net.sf.briar.plugins; + +import java.util.Collection; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.TransportConfig; +import net.sf.briar.api.TransportProperties; +import net.sf.briar.api.android.AndroidExecutor; +import net.sf.briar.api.db.DatabaseComponent; +import net.sf.briar.api.lifecycle.ShutdownManager; +import net.sf.briar.api.protocol.TransportId; +import net.sf.briar.api.transport.ConnectionDispatcher; +import net.sf.briar.api.ui.UiCallback; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.Test; + +public class PluginManagerImplTest extends BriarTestCase { + + @SuppressWarnings("unchecked") + @Test + public void testStartAndStop() throws Exception { + Mockery context = new Mockery(); + final AndroidExecutor androidExecutor = + context.mock(AndroidExecutor.class); + final ShutdownManager shutdownManager = + context.mock(ShutdownManager.class); + final DatabaseComponent db = context.mock(DatabaseComponent.class); + final Poller poller = context.mock(Poller.class); + final ConnectionDispatcher dispatcher = + context.mock(ConnectionDispatcher.class); + final UiCallback uiCallback = context.mock(UiCallback.class); + context.checking(new Expectations() {{ + // Start + oneOf(poller).start(with(any(Collection.class))); + allowing(db).getConfig(with(any(TransportId.class))); + will(returnValue(new TransportConfig())); + allowing(db).getLocalProperties(with(any(TransportId.class))); + will(returnValue(new TransportProperties())); + allowing(db).getRemoteProperties(with(any(TransportId.class))); + will(returnValue(new TransportProperties())); + allowing(db).mergeLocalProperties(with(any(TransportId.class)), + with(any(TransportProperties.class))); + // Stop + oneOf(poller).stop(); + oneOf(androidExecutor).shutdown(); + }}); + ExecutorService executor = Executors.newCachedThreadPool(); + PluginManagerImpl p = new PluginManagerImpl(executor, androidExecutor, + shutdownManager, db, poller, dispatcher, uiCallback); + // We expect either 3 or 4 plugins to be started, depending on whether + // the test machine has a Bluetooth device + int started = p.start(null); + int stopped = p.stop(); + assertEquals(started, stopped); + assertTrue(started >= 3); + assertTrue(started <= 4); + context.assertIsSatisfied(); + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/bluetooth/BluetoothClientTest.java b/briar-tests/src/net/sf/briar/plugins/bluetooth/BluetoothClientTest.java new file mode 100644 index 000000000..402b9509f --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/bluetooth/BluetoothClientTest.java @@ -0,0 +1,44 @@ +package net.sf.briar.plugins.bluetooth; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import net.sf.briar.api.ContactId; +import net.sf.briar.api.TransportConfig; +import net.sf.briar.api.TransportProperties; +import net.sf.briar.api.clock.SystemClock; +import net.sf.briar.plugins.DuplexClientTest; + +// This is not a JUnit test - it has to be run manually while the server test +// is running on another machine +public class BluetoothClientTest extends DuplexClientTest { + + private BluetoothClientTest(Executor executor, String serverAddress) { + // Store the server's Bluetooth address and UUID + TransportProperties p = new TransportProperties(); + p.put("address", serverAddress); + p.put("uuid", BluetoothTest.getUuid()); + Map remote = + Collections.singletonMap(contactId, p); + // Create the plugin + callback = new ClientCallback(new TransportConfig(), + new TransportProperties(), remote); + plugin = new BluetoothPlugin(executor, new SystemClock(), callback, 0L); + } + + public static void main(String[] args) throws Exception { + if(args.length != 1) { + System.err.println("Please specify the server's Bluetooth address"); + System.exit(1); + } + ExecutorService executor = Executors.newCachedThreadPool(); + try { + new BluetoothClientTest(executor, args[0]).run(); + } finally { + executor.shutdown(); + } + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/bluetooth/BluetoothServerTest.java b/briar-tests/src/net/sf/briar/plugins/bluetooth/BluetoothServerTest.java new file mode 100644 index 000000000..d59dda8f5 --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/bluetooth/BluetoothServerTest.java @@ -0,0 +1,35 @@ +package net.sf.briar.plugins.bluetooth; + +import java.util.Collections; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import net.sf.briar.api.TransportConfig; +import net.sf.briar.api.TransportProperties; +import net.sf.briar.api.clock.SystemClock; +import net.sf.briar.plugins.DuplexServerTest; + +// This is not a JUnit test - it has to be run manually while the client test +// is running on another machine +public class BluetoothServerTest extends DuplexServerTest { + + private BluetoothServerTest(Executor executor) { + // Store the UUID + TransportProperties local = new TransportProperties(); + local.put("uuid", BluetoothTest.getUuid()); + // Create the plugin + callback = new ServerCallback(new TransportConfig(), local, + Collections.singletonMap(contactId, new TransportProperties())); + plugin = new BluetoothPlugin(executor, new SystemClock(), callback, 0L); + } + + public static void main(String[] args) throws Exception { + ExecutorService executor = Executors.newCachedThreadPool(); + try { + new BluetoothServerTest(executor).run(); + } finally { + executor.shutdown(); + } + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/bluetooth/BluetoothTest.java b/briar-tests/src/net/sf/briar/plugins/bluetooth/BluetoothTest.java new file mode 100644 index 000000000..17676bb08 --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/bluetooth/BluetoothTest.java @@ -0,0 +1,13 @@ +package net.sf.briar.plugins.bluetooth; + +import java.util.UUID; + +class BluetoothTest { + + private static final String EMPTY_UUID = + UUID.nameUUIDFromBytes(new byte[0]).toString().replaceAll("-", ""); + + static String getUuid() { + return EMPTY_UUID; + } +} \ No newline at end of file diff --git a/briar-tests/src/net/sf/briar/plugins/file/LinuxRemovableDriveFinderTest.java b/briar-tests/src/net/sf/briar/plugins/file/LinuxRemovableDriveFinderTest.java new file mode 100644 index 000000000..6634adc65 --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/file/LinuxRemovableDriveFinderTest.java @@ -0,0 +1,25 @@ +package net.sf.briar.plugins.file; + +import net.sf.briar.BriarTestCase; + +import org.junit.Test; + +public class LinuxRemovableDriveFinderTest extends BriarTestCase { + + @Test + public void testParseMountPoint() { + LinuxRemovableDriveFinder f = new LinuxRemovableDriveFinder(); + String line = "/dev/sda3 on / type ext3" + + " (rw,errors=remount-ro,commit=0)"; + assertEquals("/", f.parseMountPoint(line)); + line = "gvfs-fuse-daemon on /home/alice/.gvfs" + + " type fuse.gvfs-fuse-daemon (rw,nosuid,nodev,user=alice)"; + assertEquals(null, f.parseMountPoint(line)); // Can't be parsed + line = "fusectl on /sys/fs/fuse/connections type fusectl (rw)"; + assertEquals(null, f.parseMountPoint(line)); // Can't be parsed + line = "/dev/sdd1 on /media/HAZ SPACE(!) type vfat" + + " (rw,nosuid,nodev,uhelper=udisks,uid=1000,gid=1000," + + "shortname=mixed,dmask=0077,utf8=1,showexec,flush)"; + assertEquals("/media/HAZ SPACE(!)", f.parseMountPoint(line)); + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/file/MacRemovableDriveFinderTest.java b/briar-tests/src/net/sf/briar/plugins/file/MacRemovableDriveFinderTest.java new file mode 100644 index 000000000..5b0fae470 --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/file/MacRemovableDriveFinderTest.java @@ -0,0 +1,23 @@ +package net.sf.briar.plugins.file; + +import net.sf.briar.BriarTestCase; + +import org.junit.Test; + +public class MacRemovableDriveFinderTest extends BriarTestCase { + + @Test + public void testParseMountPoint() { + MacRemovableDriveFinder f = new MacRemovableDriveFinder(); + String line = "/dev/disk0s3 on / (local, journaled)"; + assertEquals("/", f.parseMountPoint(line)); + line = "devfs on /dev (local)"; + assertEquals(null, f.parseMountPoint(line)); // Can't be parsed + line = " on /.vol"; + assertEquals(null, f.parseMountPoint(line)); // Can't be parsed + line = "automount -nsl [117] on /Network (automounted)"; + assertEquals(null, f.parseMountPoint(line)); // Can't be parsed + line = "/dev/disk1s1 on /Volumes/HAZ SPACE(!) (local, nodev, nosuid)"; + assertEquals("/Volumes/HAZ SPACE(!)", f.parseMountPoint(line)); + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/file/PollingRemovableDriveMonitorTest.java b/briar-tests/src/net/sf/briar/plugins/file/PollingRemovableDriveMonitorTest.java new file mode 100644 index 000000000..f4968b8e1 --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/file/PollingRemovableDriveMonitorTest.java @@ -0,0 +1,95 @@ +package net.sf.briar.plugins.file; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.plugins.file.RemovableDriveMonitor.Callback; + +import org.junit.Test; + +public class PollingRemovableDriveMonitorTest extends BriarTestCase { + + @Test + public void testOneCallbackPerFile() throws Exception { + // Create a finder that returns no files the first time, then two files + final File file1 = new File("foo"); + final File file2 = new File("bar"); + final RemovableDriveFinder finder = new RemovableDriveFinder() { + + private AtomicBoolean firstCall = new AtomicBoolean(true); + + public Collection findRemovableDrives() throws IOException { + if(firstCall.getAndSet(false)) return Collections.emptyList(); + else return Arrays.asList(file1, file2); + } + }; + // Create a callback that waits for two files + final CountDownLatch latch = new CountDownLatch(2); + final List detected = new ArrayList(); + Callback callback = new Callback() { + + public void driveInserted(File f) { + detected.add(f); + latch.countDown(); + } + + public void exceptionThrown(IOException e) { + fail(); + } + }; + // Create the monitor and start it + final RemovableDriveMonitor monitor = new PollingRemovableDriveMonitor( + Executors.newCachedThreadPool(), finder, 1); + monitor.start(callback); + // Wait for the monitor to detect the files + assertTrue(latch.await(10, SECONDS)); + monitor.stop(); + // Check that both files were detected + assertEquals(2, detected.size()); + assertTrue(detected.contains(file1)); + assertTrue(detected.contains(file2)); + } + + @Test + public void testExceptionCallback() throws Exception { + // Create a finder that throws an exception the second time it's polled + final RemovableDriveFinder finder = new RemovableDriveFinder() { + + private AtomicBoolean firstCall = new AtomicBoolean(true); + + public Collection findRemovableDrives() throws IOException { + if(firstCall.getAndSet(false)) return Collections.emptyList(); + else throw new IOException(); + } + }; + // Create a callback that waits for an exception + final CountDownLatch latch = new CountDownLatch(1); + Callback callback = new Callback() { + + public void driveInserted(File root) { + fail(); + } + + public void exceptionThrown(IOException e) { + latch.countDown(); + } + }; + // Create the monitor and start it + final RemovableDriveMonitor monitor = new PollingRemovableDriveMonitor( + Executors.newCachedThreadPool(), finder, 1); + monitor.start(callback); + assertTrue(latch.await(10, SECONDS)); + monitor.stop(); + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/file/RemovableDrivePluginTest.java b/briar-tests/src/net/sf/briar/plugins/file/RemovableDrivePluginTest.java new file mode 100644 index 000000000..f7cbe893b --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/file/RemovableDrivePluginTest.java @@ -0,0 +1,355 @@ +package net.sf.briar.plugins.file; + +import static net.sf.briar.api.transport.TransportConstants.MIN_CONNECTION_LENGTH; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Executor; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.plugins.simplex.SimplexPluginCallback; +import net.sf.briar.api.plugins.simplex.SimplexTransportWriter; +import net.sf.briar.plugins.ImmediateExecutor; +import net.sf.briar.plugins.file.RemovableDriveMonitor.Callback; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class RemovableDrivePluginTest extends BriarTestCase { + + private final File testDir = TestUtils.getTestDirectory(); + private final ContactId contactId = new ContactId(234); + + @Before + public void setUp() { + testDir.mkdirs(); + } + + @Test + public void testWriterIsNullIfNoDrivesAreFound() throws Exception { + final List drives = Collections.emptyList(); + + Mockery context = new Mockery(); + final Executor executor = context.mock(Executor.class); + final SimplexPluginCallback callback = + context.mock(SimplexPluginCallback.class); + final RemovableDriveFinder finder = + context.mock(RemovableDriveFinder.class); + final RemovableDriveMonitor monitor = + context.mock(RemovableDriveMonitor.class); + + context.checking(new Expectations() {{ + oneOf(monitor).start(with(any(Callback.class))); + oneOf(finder).findRemovableDrives(); + will(returnValue(drives)); + }}); + + RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor, + callback, finder, monitor); + plugin.start(); + + assertNull(plugin.createWriter(contactId)); + + context.assertIsSatisfied(); + } + + @Test + public void testWriterIsNullIfNoDriveIsChosen() throws Exception { + final File drive1 = new File(testDir, "1"); + final File drive2 = new File(testDir, "2"); + final List drives = new ArrayList(); + drives.add(drive1); + drives.add(drive2); + + Mockery context = new Mockery(); + final Executor executor = context.mock(Executor.class); + final SimplexPluginCallback callback = + context.mock(SimplexPluginCallback.class); + final RemovableDriveFinder finder = + context.mock(RemovableDriveFinder.class); + final RemovableDriveMonitor monitor = + context.mock(RemovableDriveMonitor.class); + + context.checking(new Expectations() {{ + oneOf(monitor).start(with(any(Callback.class))); + oneOf(finder).findRemovableDrives(); + will(returnValue(drives)); + oneOf(callback).showChoice(with(any(String[].class)), + with(any(String.class))); + will(returnValue(-1)); // The user cancelled the choice + }}); + + RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor, + callback, finder, monitor); + plugin.start(); + + assertNull(plugin.createWriter(contactId)); + File[] files = drive1.listFiles(); + assertTrue(files == null || files.length == 0); + + context.assertIsSatisfied(); + } + + @Test + public void testWriterIsNullIfOutputDirDoesNotExist() throws Exception { + final File drive1 = new File(testDir, "1"); + final File drive2 = new File(testDir, "2"); + final List drives = new ArrayList(); + drives.add(drive1); + drives.add(drive2); + + Mockery context = new Mockery(); + final Executor executor = context.mock(Executor.class); + final SimplexPluginCallback callback = + context.mock(SimplexPluginCallback.class); + final RemovableDriveFinder finder = + context.mock(RemovableDriveFinder.class); + final RemovableDriveMonitor monitor = + context.mock(RemovableDriveMonitor.class); + + context.checking(new Expectations() {{ + oneOf(monitor).start(with(any(Callback.class))); + oneOf(finder).findRemovableDrives(); + will(returnValue(drives)); + oneOf(callback).showChoice(with(any(String[].class)), + with(any(String.class))); + will(returnValue(0)); // The user chose drive1 but it doesn't exist + }}); + + RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor, + callback, finder, monitor); + plugin.start(); + + assertNull(plugin.createWriter(contactId)); + File[] files = drive1.listFiles(); + assertTrue(files == null || files.length == 0); + + context.assertIsSatisfied(); + } + + @Test + public void testWriterIsNullIfOutputDirIsAFile() throws Exception { + final File drive1 = new File(testDir, "1"); + final File drive2 = new File(testDir, "2"); + final List drives = new ArrayList(); + drives.add(drive1); + drives.add(drive2); + // Create drive1 as a file rather than a directory + assertTrue(drive1.createNewFile()); + + Mockery context = new Mockery(); + final Executor executor = context.mock(Executor.class); + final SimplexPluginCallback callback = + context.mock(SimplexPluginCallback.class); + final RemovableDriveFinder finder = + context.mock(RemovableDriveFinder.class); + final RemovableDriveMonitor monitor = + context.mock(RemovableDriveMonitor.class); + + context.checking(new Expectations() {{ + oneOf(monitor).start(with(any(Callback.class))); + oneOf(finder).findRemovableDrives(); + will(returnValue(drives)); + oneOf(callback).showChoice(with(any(String[].class)), + with(any(String.class))); + will(returnValue(0)); // The user chose drive1 but it's not a dir + }}); + + RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor, + callback, finder, monitor); + plugin.start(); + + assertNull(plugin.createWriter(contactId)); + File[] files = drive1.listFiles(); + assertTrue(files == null || files.length == 0); + + context.assertIsSatisfied(); + } + + @Test + public void testWriterIsNotNullIfOutputDirIsADir() throws Exception { + final File drive1 = new File(testDir, "1"); + final File drive2 = new File(testDir, "2"); + final List drives = new ArrayList(); + drives.add(drive1); + drives.add(drive2); + // Create drive1 as a directory + assertTrue(drive1.mkdir()); + + Mockery context = new Mockery(); + final Executor executor = context.mock(Executor.class); + final SimplexPluginCallback callback = + context.mock(SimplexPluginCallback.class); + final RemovableDriveFinder finder = + context.mock(RemovableDriveFinder.class); + final RemovableDriveMonitor monitor = + context.mock(RemovableDriveMonitor.class); + + context.checking(new Expectations() {{ + oneOf(monitor).start(with(any(Callback.class))); + oneOf(finder).findRemovableDrives(); + will(returnValue(drives)); + oneOf(callback).showChoice(with(any(String[].class)), + with(any(String.class))); + will(returnValue(0)); // The user chose drive1 + }}); + + RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor, + callback, finder, monitor); + plugin.start(); + + assertNotNull(plugin.createWriter(contactId)); + // The output file should exist and should be empty + File[] files = drive1.listFiles(); + assertNotNull(files); + assertEquals(1, files.length); + assertEquals(0L, files[0].length()); + + context.assertIsSatisfied(); + } + + @Test + public void testWritingToWriter() throws Exception { + final File drive1 = new File(testDir, "1"); + final File drive2 = new File(testDir, "2"); + final List drives = new ArrayList(); + drives.add(drive1); + drives.add(drive2); + // Create drive1 as a directory + assertTrue(drive1.mkdir()); + + Mockery context = new Mockery(); + final Executor executor = context.mock(Executor.class); + final SimplexPluginCallback callback = + context.mock(SimplexPluginCallback.class); + final RemovableDriveFinder finder = + context.mock(RemovableDriveFinder.class); + final RemovableDriveMonitor monitor = + context.mock(RemovableDriveMonitor.class); + + context.checking(new Expectations() {{ + oneOf(monitor).start(with(any(Callback.class))); + oneOf(finder).findRemovableDrives(); + will(returnValue(drives)); + oneOf(callback).showChoice(with(any(String[].class)), + with(any(String.class))); + will(returnValue(0)); // The user chose drive1 + oneOf(callback).showMessage(with(any(String.class))); + }}); + + RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor, + callback, finder, monitor); + plugin.start(); + + SimplexTransportWriter writer = plugin.createWriter(contactId); + assertNotNull(writer); + // The output file should exist and should be empty + File[] files = drive1.listFiles(); + assertNotNull(files); + assertEquals(1, files.length); + assertEquals(0L, files[0].length()); + // Writing to the output stream should increase the size of the file + OutputStream out = writer.getOutputStream(); + out.write(new byte[123]); + out.flush(); + out.close(); + // Disposing of the writer should not delete the file + writer.dispose(false); + assertTrue(files[0].exists()); + assertEquals(123L, files[0].length()); + + context.assertIsSatisfied(); + } + + @Test + public void testEmptyDriveIsIgnored() throws Exception { + Mockery context = new Mockery(); + final Executor executor = context.mock(Executor.class); + final SimplexPluginCallback callback = + context.mock(SimplexPluginCallback.class); + final RemovableDriveFinder finder = + context.mock(RemovableDriveFinder.class); + final RemovableDriveMonitor monitor = + context.mock(RemovableDriveMonitor.class); + + context.checking(new Expectations() {{ + oneOf(monitor).start(with(any(Callback.class))); + }}); + + RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor, + callback, finder, monitor); + plugin.start(); + + plugin.driveInserted(testDir); + + context.assertIsSatisfied(); + } + + @Test + public void testFilenames() { + Mockery context = new Mockery(); + final Executor executor = context.mock(Executor.class); + final SimplexPluginCallback callback = + context.mock(SimplexPluginCallback.class); + final RemovableDriveFinder finder = + context.mock(RemovableDriveFinder.class); + final RemovableDriveMonitor monitor = + context.mock(RemovableDriveMonitor.class); + + RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor, + callback, finder, monitor); + + assertFalse(plugin.isPossibleConnectionFilename("abcdefg.dat")); + assertFalse(plugin.isPossibleConnectionFilename("abcdefghi.dat")); + assertFalse(plugin.isPossibleConnectionFilename("abcdefgh_dat")); + assertFalse(plugin.isPossibleConnectionFilename("abcdefgh.rat")); + assertTrue(plugin.isPossibleConnectionFilename("abcdefgh.dat")); + assertTrue(plugin.isPossibleConnectionFilename("ABCDEFGH.DAT")); + + context.assertIsSatisfied(); + } + + @Test + public void testReaderIsCreated() throws Exception { + Mockery context = new Mockery(); + final SimplexPluginCallback callback = + context.mock(SimplexPluginCallback.class); + final RemovableDriveFinder finder = + context.mock(RemovableDriveFinder.class); + final RemovableDriveMonitor monitor = + context.mock(RemovableDriveMonitor.class); + + context.checking(new Expectations() {{ + oneOf(monitor).start(with(any(Callback.class))); + oneOf(callback).readerCreated(with(any(FileTransportReader.class))); + }}); + + RemovableDrivePlugin plugin = new RemovableDrivePlugin( + new ImmediateExecutor(), callback, finder, monitor); + plugin.start(); + + File f = new File(testDir, "abcdefgh.dat"); + OutputStream out = new FileOutputStream(f); + out.write(new byte[MIN_CONNECTION_LENGTH]); + out.flush(); + out.close(); + assertEquals(MIN_CONNECTION_LENGTH, f.length()); + plugin.driveInserted(testDir); + + context.assertIsSatisfied(); + } + + @After + public void tearDown() { + TestUtils.deleteTestDirectory(testDir); + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/file/UnixRemovableDriveMonitorTest.java b/briar-tests/src/net/sf/briar/plugins/file/UnixRemovableDriveMonitorTest.java new file mode 100644 index 000000000..17bdba120 --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/file/UnixRemovableDriveMonitorTest.java @@ -0,0 +1,100 @@ +package net.sf.briar.plugins.file; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.plugins.file.RemovableDriveMonitor.Callback; +import net.sf.briar.util.OsUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class UnixRemovableDriveMonitorTest extends BriarTestCase { + + private final File testDir = TestUtils.getTestDirectory(); + + @Before + public void setUp() { + testDir.mkdirs(); + } + + @Test + public void testNonexistentDir() throws Exception { + if(!(OsUtils.isLinux() || OsUtils.isMacLeopardOrNewer())) { + System.err.println("Warning: Skipping test"); + return; + } + File doesNotExist = new File(testDir, "doesNotExist"); + RemovableDriveMonitor monitor = createMonitor(doesNotExist); + monitor.start(new Callback() { + + public void driveInserted(File root) { + fail(); + } + + public void exceptionThrown(IOException e) { + fail(); + } + }); + monitor.stop(); + } + + @Test + public void testOneCallbackPerFile() throws Exception { + if(!(OsUtils.isLinux() || OsUtils.isMacLeopardOrNewer())) { + System.err.println("Warning: Skipping test"); + return; + } + // Create a callback that will wait for two files before stopping + final List detected = new ArrayList(); + final CountDownLatch latch = new CountDownLatch(2); + final Callback callback = new Callback() { + + public void driveInserted(File f) { + detected.add(f); + latch.countDown(); + } + + public void exceptionThrown(IOException e) { + fail(); + } + }; + // Create the monitor and start it + RemovableDriveMonitor monitor = createMonitor(testDir); + monitor.start(callback); + // Create two files in the test directory + File file1 = new File(testDir, "1"); + File file2 = new File(testDir, "2"); + assertTrue(file1.createNewFile()); + assertTrue(file2.createNewFile()); + // Wait for the monitor to detect the files + assertTrue(latch.await(5, SECONDS)); + monitor.stop(); + // Check that both files were detected + assertEquals(2, detected.size()); + assertTrue(detected.contains(file1)); + assertTrue(detected.contains(file2)); + } + + @After + public void tearDown() { + TestUtils.deleteTestDirectory(testDir); + } + + private RemovableDriveMonitor createMonitor(final File dir) { + return new UnixRemovableDriveMonitor() { + @Override + protected String[] getPathsToWatch() { + return new String[] { dir.getPath() }; + } + }; + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/tcp/LanTcpClientTest.java b/briar-tests/src/net/sf/briar/plugins/tcp/LanTcpClientTest.java new file mode 100644 index 000000000..da1f8641d --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/tcp/LanTcpClientTest.java @@ -0,0 +1,45 @@ +package net.sf.briar.plugins.tcp; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import net.sf.briar.api.ContactId; +import net.sf.briar.api.TransportConfig; +import net.sf.briar.api.TransportProperties; +import net.sf.briar.plugins.DuplexClientTest; +import net.sf.briar.plugins.tcp.LanTcpPlugin; + +// This is not a JUnit test - it has to be run manually while the server test +// is running on another machine +public class LanTcpClientTest extends DuplexClientTest { + + private LanTcpClientTest(Executor executor, String serverAddress, + String serverPort) { + // Store the server's internal address and port + TransportProperties p = new TransportProperties(); + p.put("internal", serverAddress); + p.put("port", serverPort); + Map remote = + Collections.singletonMap(contactId, p); + // Create the plugin + callback = new ClientCallback(new TransportConfig(), + new TransportProperties(), remote); + plugin = new LanTcpPlugin(executor, callback, 0L); + } + + public static void main(String[] args) throws Exception { + if(args.length != 2) { + System.err.println("Please specify the server's address and port"); + System.exit(1); + } + ExecutorService executor = Executors.newCachedThreadPool(); + try { + new LanTcpClientTest(executor, args[0], args[1]).run(); + } finally { + executor.shutdown(); + } + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/tcp/LanTcpPluginTest.java b/briar-tests/src/net/sf/briar/plugins/tcp/LanTcpPluginTest.java new file mode 100644 index 000000000..7c2a1350f --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/tcp/LanTcpPluginTest.java @@ -0,0 +1,142 @@ +package net.sf.briar.plugins.tcp; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.Hashtable; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.TransportConfig; +import net.sf.briar.api.TransportProperties; +import net.sf.briar.api.plugins.duplex.DuplexPlugin; +import net.sf.briar.api.plugins.duplex.DuplexPluginCallback; +import net.sf.briar.api.plugins.duplex.DuplexTransportConnection; +import net.sf.briar.plugins.tcp.LanTcpPlugin; + +import org.junit.Test; + +public class LanTcpPluginTest extends BriarTestCase { + + private final ContactId contactId = new ContactId(234); + + @Test + public void testIncomingConnection() throws Exception { + Callback callback = new Callback(); + callback.local.put("address", "127.0.0.1"); + callback.local.put("port", "0"); + Executor e = Executors.newCachedThreadPool(); + DuplexPlugin plugin = new LanTcpPlugin(e, callback, 0L); + plugin.start(); + // The plugin should have bound a socket and stored the port number + assertTrue(callback.propertiesLatch.await(5, SECONDS)); + String host = callback.local.get("address"); + assertNotNull(host); + assertEquals("127.0.0.1", host); + String portString = callback.local.get("port"); + assertNotNull(portString); + int port = Integer.valueOf(portString); + assertTrue(port > 0 && port < 65536); + // The plugin should be listening on the port + InetSocketAddress addr = new InetSocketAddress(host, port); + Socket s = new Socket(); + s.connect(addr, 100); + assertTrue(callback.connectionsLatch.await(5, SECONDS)); + s.close(); + // Stop the plugin + plugin.stop(); + } + + @Test + public void testOutgoingConnection() throws Exception { + Callback callback = new Callback(); + Executor e = Executors.newCachedThreadPool(); + DuplexPlugin plugin = new LanTcpPlugin(e, callback, 0L); + plugin.start(); + // Listen on a local port + final ServerSocket ss = new ServerSocket(); + ss.bind(new InetSocketAddress("127.0.0.1", 0), 10); + int port = ss.getLocalPort(); + final CountDownLatch latch = new CountDownLatch(1); + final AtomicBoolean error = new AtomicBoolean(false); + new Thread() { + @Override + public void run() { + try { + ss.accept(); + latch.countDown(); + } catch(IOException e) { + error.set(true); + } + } + }.start(); + // Tell the plugin about the port + TransportProperties p = new TransportProperties(); + p.put("address", "127.0.0.1"); + p.put("port", String.valueOf(port)); + callback.remote.put(contactId, p); + // Connect to the port + DuplexTransportConnection d = plugin.createConnection(contactId); + assertNotNull(d); + // Check that the connection was accepted + assertTrue(latch.await(5, SECONDS)); + assertFalse(error.get()); + // Clean up + d.dispose(false, true); + ss.close(); + plugin.stop(); + } + + private static class Callback implements DuplexPluginCallback { + + private final Map remote = + new Hashtable(); + private final CountDownLatch propertiesLatch = new CountDownLatch(1); + private final CountDownLatch connectionsLatch = new CountDownLatch(1); + private final TransportProperties local = new TransportProperties(); + + public TransportConfig getConfig() { + return new TransportConfig(); + } + + public TransportProperties getLocalProperties() { + return local; + } + + public Map getRemoteProperties() { + return remote; + } + + public void mergeConfig(TransportConfig c) {} + + public void mergeLocalProperties(TransportProperties p) { + local.putAll(p); + propertiesLatch.countDown(); + } + + public int showChoice(String[] options, String... message) { + return -1; + } + + public boolean showConfirmationMessage(String... message) { + return false; + } + + public void showMessage(String... message) {} + + public void incomingConnectionCreated(DuplexTransportConnection d) { + connectionsLatch.countDown(); + } + + public void outgoingConnectionCreated(ContactId c, + DuplexTransportConnection d) {} + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/tcp/LanTcpServerTest.java b/briar-tests/src/net/sf/briar/plugins/tcp/LanTcpServerTest.java new file mode 100644 index 000000000..5efe68677 --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/tcp/LanTcpServerTest.java @@ -0,0 +1,32 @@ +package net.sf.briar.plugins.tcp; + +import java.util.Collections; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import net.sf.briar.api.TransportConfig; +import net.sf.briar.api.TransportProperties; +import net.sf.briar.plugins.DuplexServerTest; +import net.sf.briar.plugins.tcp.LanTcpPlugin; + +// This is not a JUnit test - it has to be run manually while the client test +// is running on another machine +public class LanTcpServerTest extends DuplexServerTest { + + private LanTcpServerTest(Executor executor) { + callback = new ServerCallback(new TransportConfig(), + new TransportProperties(), + Collections.singletonMap(contactId, new TransportProperties())); + plugin = new LanTcpPlugin(executor, callback, 0L); + } + + public static void main(String[] args) throws Exception { + ExecutorService executor = Executors.newCachedThreadPool(); + try { + new LanTcpServerTest(executor).run(); + } finally { + executor.shutdown(); + } + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/tor/TorPluginTest.java b/briar-tests/src/net/sf/briar/plugins/tor/TorPluginTest.java new file mode 100644 index 000000000..a876cb496 --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/tor/TorPluginTest.java @@ -0,0 +1,175 @@ +package net.sf.briar.plugins.tor; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.io.PrintStream; +import java.util.Hashtable; +import java.util.Map; +import java.util.Scanner; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.TransportConfig; +import net.sf.briar.api.TransportProperties; +import net.sf.briar.api.plugins.duplex.DuplexPluginCallback; +import net.sf.briar.api.plugins.duplex.DuplexTransportConnection; + +import org.junit.Test; + +public class TorPluginTest extends BriarTestCase { + + private final ContactId contactId = new ContactId(234); + + @Test + public void testHiddenService() throws Exception { + System.err.println("======== testHiddenService ========"); + Executor e = Executors.newCachedThreadPool(); + TorPlugin serverPlugin = null, clientPlugin = null; + try { + // Create a plugin instance for the server + Callback serverCallback = new Callback(); + serverPlugin = new TorPlugin(e, serverCallback, 0L); + System.out.println("Starting server plugin"); + serverPlugin.start(); + // The plugin should create a hidden service... eventually + assertTrue(serverCallback.latch.await(600, SECONDS)); + System.out.println("Started server plugin"); + String onion = serverCallback.local.get("onion"); + assertNotNull(onion); + assertTrue(onion.endsWith(".onion")); + // Create another plugin instance for the client + Callback clientCallback = new Callback(); + clientCallback.config.put("noHiddenService", ""); + TransportProperties p = new TransportProperties(); + p.put("onion", onion); + clientCallback.remote.put(contactId, p); + clientPlugin = new TorPlugin(e, clientCallback, 0L); + System.out.println("Starting client plugin"); + clientPlugin.start(); + // The plugin should start without creating a hidden service + assertTrue(clientCallback.latch.await(600, SECONDS)); + System.out.println("Started client plugin"); + // Connect to the server's hidden service + System.out.println("Connecting to hidden service"); + DuplexTransportConnection clientEnd = + clientPlugin.createConnection(contactId); + assertNotNull(clientEnd); + DuplexTransportConnection serverEnd = + serverCallback.incomingConnection; + assertNotNull(serverEnd); + System.out.println("Connected to hidden service"); + // Send some data through the Tor connection + PrintStream out = new PrintStream(clientEnd.getOutputStream()); + out.println("Hello world"); + out.flush(); + Scanner in = new Scanner(serverEnd.getInputStream()); + assertTrue(in.hasNextLine()); + assertEquals("Hello world", in.nextLine()); + serverEnd.dispose(false, false); + clientEnd.dispose(false, false); + } finally { + // Stop the plugins + System.out.println("Stopping plugins"); + if(serverPlugin != null) serverPlugin.stop(); + if(clientPlugin != null) clientPlugin.stop(); + System.out.println("Stopped plugins"); + } + } + + @Test + public void testStoreAndRetrievePrivateKey() throws Exception { + System.err.println("======== testStoreAndRetrievePrivateKey ========"); + Executor e = Executors.newCachedThreadPool(); + TorPlugin plugin = null; + try { + // Start a plugin instance with no private key + Callback callback = new Callback(); + plugin = new TorPlugin(e, callback, 0L); + System.out.println("Starting plugin without private key"); + plugin.start(); + // The plugin should create a hidden service... eventually + assertTrue(callback.latch.await(600, SECONDS)); + System.out.println("Started plugin"); + String onion = callback.local.get("onion"); + assertNotNull(onion); + assertTrue(onion.endsWith(".onion")); + // Get the PEM-encoded private key + String privateKey = callback.config.get("privateKey"); + assertNotNull(privateKey); + // Stop the plugin + System.out.println("Stopping plugin"); + plugin.stop(); + System.out.println("Stopped plugin"); + // Start another instance, reusing the private key + callback = new Callback(); + callback.config.put("privateKey", privateKey); + plugin = new TorPlugin(e, callback, 0L); + System.out.println("Starting plugin with private key"); + plugin.start(); + // The plugin should create a hidden service... eventually + assertTrue(callback.latch.await(600, SECONDS)); + System.out.println("Started plugin"); + // The onion URL should be the same + assertEquals(onion, callback.local.get("onion")); + // The private key should be the same + assertEquals(privateKey, callback.config.get("privateKey")); + } finally { + // Stop the plugin + System.out.println("Stopping plugin"); + if(plugin != null) plugin.stop(); + System.out.println("Stopped plugin"); + } + } + + private static class Callback implements DuplexPluginCallback { + + private final Map remote = + new Hashtable(); + private final CountDownLatch latch = new CountDownLatch(1); + private final TransportConfig config = new TransportConfig(); + private final TransportProperties local = new TransportProperties(); + + private volatile DuplexTransportConnection incomingConnection = null; + + public TransportConfig getConfig() { + return config; + } + + public TransportProperties getLocalProperties() { + return local; + } + + public Map getRemoteProperties() { + return remote; + } + + public void mergeConfig(TransportConfig c) { + config.putAll(c); + } + + public void mergeLocalProperties(TransportProperties p) { + local.putAll(p); + latch.countDown(); + } + + public int showChoice(String[] options, String... message) { + return -1; + } + + public boolean showConfirmationMessage(String... message) { + return false; + } + + public void showMessage(String... message) {} + + public void incomingConnectionCreated(DuplexTransportConnection d) { + incomingConnection = d; + } + + public void outgoingConnectionCreated(ContactId c, + DuplexTransportConnection d) {} + } +} diff --git a/briar-tests/src/net/sf/briar/protocol/AckReaderTest.java b/briar-tests/src/net/sf/briar/protocol/AckReaderTest.java new file mode 100644 index 000000000..a40a1f297 --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/AckReaderTest.java @@ -0,0 +1,124 @@ +package net.sf.briar.protocol; + +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PACKET_LENGTH; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.Collection; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.api.FormatException; +import net.sf.briar.api.protocol.Ack; +import net.sf.briar.api.protocol.PacketFactory; +import net.sf.briar.api.protocol.Types; +import net.sf.briar.api.serial.Reader; +import net.sf.briar.api.serial.ReaderFactory; +import net.sf.briar.api.serial.SerialComponent; +import net.sf.briar.api.serial.Writer; +import net.sf.briar.api.serial.WriterFactory; +import net.sf.briar.serial.SerialModule; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +public class AckReaderTest extends BriarTestCase { + + // FIXME: This is an integration test, not a unit test + + private final SerialComponent serial; + private final ReaderFactory readerFactory; + private final WriterFactory writerFactory; + private final Mockery context; + + public AckReaderTest() throws Exception { + super(); + Injector i = Guice.createInjector(new SerialModule()); + serial = i.getInstance(SerialComponent.class); + readerFactory = i.getInstance(ReaderFactory.class); + writerFactory = i.getInstance(WriterFactory.class); + context = new Mockery(); + } + + @Test + public void testFormatExceptionIfAckIsTooLarge() throws Exception { + PacketFactory packetFactory = context.mock(PacketFactory.class); + AckReader ackReader = new AckReader(packetFactory); + + byte[] b = createAck(true); + ByteArrayInputStream in = new ByteArrayInputStream(b); + Reader reader = readerFactory.createReader(in); + reader.addStructReader(Types.ACK, ackReader); + + try { + reader.readStruct(Types.ACK, Ack.class); + fail(); + } catch(FormatException expected) {} + context.assertIsSatisfied(); + } + + @Test + @SuppressWarnings("unchecked") + public void testNoFormatExceptionIfAckIsMaximumSize() throws Exception { + final PacketFactory packetFactory = context.mock(PacketFactory.class); + AckReader ackReader = new AckReader(packetFactory); + final Ack ack = context.mock(Ack.class); + context.checking(new Expectations() {{ + oneOf(packetFactory).createAck(with(any(Collection.class))); + will(returnValue(ack)); + }}); + + byte[] b = createAck(false); + ByteArrayInputStream in = new ByteArrayInputStream(b); + Reader reader = readerFactory.createReader(in); + reader.addStructReader(Types.ACK, ackReader); + + assertEquals(ack, reader.readStruct(Types.ACK, Ack.class)); + context.assertIsSatisfied(); + } + + @Test + public void testEmptyAck() throws Exception { + final PacketFactory packetFactory = context.mock(PacketFactory.class); + AckReader ackReader = new AckReader(packetFactory); + + byte[] b = createEmptyAck(); + ByteArrayInputStream in = new ByteArrayInputStream(b); + Reader reader = readerFactory.createReader(in); + reader.addStructReader(Types.ACK, ackReader); + + try { + reader.readStruct(Types.ACK, Ack.class); + fail(); + } catch(FormatException expected) {} + context.assertIsSatisfied(); + } + + private byte[] createAck(boolean tooBig) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer w = writerFactory.createWriter(out); + w.writeStructId(Types.ACK); + w.writeListStart(); + while(out.size() + serial.getSerialisedUniqueIdLength() + < MAX_PACKET_LENGTH) { + w.writeBytes(TestUtils.getRandomId()); + } + if(tooBig) w.writeBytes(TestUtils.getRandomId()); + w.writeListEnd(); + assertEquals(tooBig, out.size() > MAX_PACKET_LENGTH); + return out.toByteArray(); + } + + private byte[] createEmptyAck() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer w = writerFactory.createWriter(out); + w.writeStructId(Types.ACK); + w.writeListStart(); + w.writeListEnd(); + return out.toByteArray(); + } +} diff --git a/briar-tests/src/net/sf/briar/protocol/BatchReaderTest.java b/briar-tests/src/net/sf/briar/protocol/BatchReaderTest.java new file mode 100644 index 000000000..6323638b0 --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/BatchReaderTest.java @@ -0,0 +1,137 @@ +package net.sf.briar.protocol; + +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PACKET_LENGTH; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Collections; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.FormatException; +import net.sf.briar.api.protocol.Types; +import net.sf.briar.api.protocol.UnverifiedBatch; +import net.sf.briar.api.serial.Reader; +import net.sf.briar.api.serial.ReaderFactory; +import net.sf.briar.api.serial.StructReader; +import net.sf.briar.api.serial.Writer; +import net.sf.briar.api.serial.WriterFactory; +import net.sf.briar.serial.SerialModule; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +public class BatchReaderTest extends BriarTestCase { + + // FIXME: This is an integration test, not a unit test + + private final ReaderFactory readerFactory; + private final WriterFactory writerFactory; + private final Mockery context; + private final UnverifiedMessage message; + private final StructReader messageReader; + + public BatchReaderTest() throws Exception { + super(); + Injector i = Guice.createInjector(new SerialModule()); + readerFactory = i.getInstance(ReaderFactory.class); + writerFactory = i.getInstance(WriterFactory.class); + context = new Mockery(); + message = context.mock(UnverifiedMessage.class); + messageReader = new TestMessageReader(); + } + + @Test + public void testFormatExceptionIfBatchIsTooLarge() throws Exception { + UnverifiedBatchFactory batchFactory = + context.mock(UnverifiedBatchFactory.class); + BatchReader batchReader = new BatchReader(messageReader, batchFactory); + + byte[] b = createBatch(MAX_PACKET_LENGTH + 1); + ByteArrayInputStream in = new ByteArrayInputStream(b); + Reader reader = readerFactory.createReader(in); + reader.addStructReader(Types.BATCH, batchReader); + + try { + reader.readStruct(Types.BATCH, UnverifiedBatch.class); + fail(); + } catch(FormatException expected) {} + context.assertIsSatisfied(); + } + + @Test + public void testNoFormatExceptionIfBatchIsMaximumSize() throws Exception { + final UnverifiedBatchFactory batchFactory = + context.mock(UnverifiedBatchFactory.class); + BatchReader batchReader = new BatchReader(messageReader, batchFactory); + final UnverifiedBatch batch = context.mock(UnverifiedBatch.class); + context.checking(new Expectations() {{ + oneOf(batchFactory).createUnverifiedBatch( + Collections.singletonList(message)); + will(returnValue(batch)); + }}); + + byte[] b = createBatch(MAX_PACKET_LENGTH); + ByteArrayInputStream in = new ByteArrayInputStream(b); + Reader reader = readerFactory.createReader(in); + reader.addStructReader(Types.BATCH, batchReader); + + assertEquals(batch, reader.readStruct(Types.BATCH, + UnverifiedBatch.class)); + context.assertIsSatisfied(); + } + + @Test + public void testEmptyBatch() throws Exception { + final UnverifiedBatchFactory batchFactory = + context.mock(UnverifiedBatchFactory.class); + BatchReader batchReader = new BatchReader(messageReader, batchFactory); + + byte[] b = createEmptyBatch(); + ByteArrayInputStream in = new ByteArrayInputStream(b); + Reader reader = readerFactory.createReader(in); + reader.addStructReader(Types.BATCH, batchReader); + + try { + reader.readStruct(Types.BATCH, UnverifiedBatch.class); + fail(); + } catch(FormatException expected) {} + context.assertIsSatisfied(); + } + + private byte[] createBatch(int size) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(size); + Writer w = writerFactory.createWriter(out); + w.writeStructId(Types.BATCH); + w.writeListStart(); + // We're using a fake message reader, so it's OK to use a fake message + w.writeStructId(Types.MESSAGE); + w.writeBytes(new byte[size - 10]); + w.writeListEnd(); + byte[] b = out.toByteArray(); + assertEquals(size, b.length); + return b; + } + + private byte[] createEmptyBatch() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer w = writerFactory.createWriter(out); + w.writeStructId(Types.BATCH); + w.writeListStart(); + w.writeListEnd(); + return out.toByteArray(); + } + + private class TestMessageReader implements StructReader { + + public UnverifiedMessage readStruct(Reader r) throws IOException { + r.readStructId(Types.MESSAGE); + r.readBytes(); + return message; + } + } +} diff --git a/briar-tests/src/net/sf/briar/protocol/ConstantsTest.java b/briar-tests/src/net/sf/briar/protocol/ConstantsTest.java new file mode 100644 index 000000000..e4c6af3a1 --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/ConstantsTest.java @@ -0,0 +1,193 @@ +package net.sf.briar.protocol; + +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_AUTHOR_NAME_LENGTH; +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_BODY_LENGTH; +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_GROUP_NAME_LENGTH; +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PACKET_LENGTH; +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PROPERTIES_PER_TRANSPORT; +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PROPERTY_LENGTH; +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PUBLIC_KEY_LENGTH; +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_SUBJECT_LENGTH; +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_TRANSPORTS; + +import java.io.ByteArrayOutputStream; +import java.security.PrivateKey; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.api.crypto.CryptoComponent; +import net.sf.briar.api.protocol.Ack; +import net.sf.briar.api.protocol.Author; +import net.sf.briar.api.protocol.AuthorFactory; +import net.sf.briar.api.protocol.BatchId; +import net.sf.briar.api.protocol.Group; +import net.sf.briar.api.protocol.GroupFactory; +import net.sf.briar.api.protocol.Message; +import net.sf.briar.api.protocol.MessageFactory; +import net.sf.briar.api.protocol.MessageId; +import net.sf.briar.api.protocol.Offer; +import net.sf.briar.api.protocol.PacketFactory; +import net.sf.briar.api.protocol.ProtocolWriter; +import net.sf.briar.api.protocol.ProtocolWriterFactory; +import net.sf.briar.api.protocol.RawBatch; +import net.sf.briar.api.protocol.Transport; +import net.sf.briar.api.protocol.TransportId; +import net.sf.briar.api.protocol.TransportUpdate; +import net.sf.briar.api.protocol.UniqueId; +import net.sf.briar.crypto.CryptoModule; +import net.sf.briar.serial.SerialModule; + +import org.junit.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +public class ConstantsTest extends BriarTestCase { + + private final CryptoComponent crypto; + private final GroupFactory groupFactory; + private final AuthorFactory authorFactory; + private final MessageFactory messageFactory; + private final PacketFactory packetFactory; + private final ProtocolWriterFactory protocolWriterFactory; + + public ConstantsTest() throws Exception { + super(); + Injector i = Guice.createInjector(new CryptoModule(), + new ProtocolModule(), new SerialModule()); + crypto = i.getInstance(CryptoComponent.class); + groupFactory = i.getInstance(GroupFactory.class); + authorFactory = i.getInstance(AuthorFactory.class); + messageFactory = i.getInstance(MessageFactory.class); + packetFactory = i.getInstance(PacketFactory.class); + protocolWriterFactory = i.getInstance(ProtocolWriterFactory.class); + } + + @Test + public void testBatchesFitIntoLargeAck() throws Exception { + testBatchesFitIntoAck(MAX_PACKET_LENGTH); + } + + @Test + public void testBatchesFitIntoSmallAck() throws Exception { + testBatchesFitIntoAck(1000); + } + + private void testBatchesFitIntoAck(int length) throws Exception { + // Create an ack with as many batch IDs as possible + ByteArrayOutputStream out = new ByteArrayOutputStream(length); + ProtocolWriter writer = protocolWriterFactory.createProtocolWriter(out, + true); + int maxBatches = writer.getMaxBatchesForAck(length); + Collection acked = new ArrayList(); + for(int i = 0; i < maxBatches; i++) { + acked.add(new BatchId(TestUtils.getRandomId())); + } + Ack a = packetFactory.createAck(acked); + writer.writeAck(a); + // Check the size of the serialised ack + assertTrue(out.size() <= length); + } + + @Test + public void testMessageFitsIntoBatch() throws Exception { + // Create a maximum-length group + String groupName = createRandomString(MAX_GROUP_NAME_LENGTH); + byte[] groupPublic = new byte[MAX_PUBLIC_KEY_LENGTH]; + Group group = groupFactory.createGroup(groupName, groupPublic); + // Create a maximum-length author + String authorName = createRandomString(MAX_AUTHOR_NAME_LENGTH); + byte[] authorPublic = new byte[MAX_PUBLIC_KEY_LENGTH]; + Author author = authorFactory.createAuthor(authorName, authorPublic); + // Create a maximum-length message + PrivateKey groupPrivate = crypto.generateSignatureKeyPair().getPrivate(); + PrivateKey authorPrivate = crypto.generateSignatureKeyPair().getPrivate(); + String subject = createRandomString(MAX_SUBJECT_LENGTH); + byte[] body = new byte[MAX_BODY_LENGTH]; + Message message = messageFactory.createMessage(null, group, + groupPrivate, author, authorPrivate, subject, body); + // Add the message to a batch + ByteArrayOutputStream out = + new ByteArrayOutputStream(MAX_PACKET_LENGTH); + ProtocolWriter writer = protocolWriterFactory.createProtocolWriter(out, + true); + RawBatch b = packetFactory.createBatch(Collections.singletonList( + message.getSerialised())); + writer.writeBatch(b); + // Check the size of the serialised batch + assertTrue(out.size() > UniqueId.LENGTH + MAX_GROUP_NAME_LENGTH + + MAX_PUBLIC_KEY_LENGTH + MAX_AUTHOR_NAME_LENGTH + + MAX_PUBLIC_KEY_LENGTH + MAX_BODY_LENGTH); + assertTrue(out.size() <= MAX_PACKET_LENGTH); + } + + @Test + public void testMessagesFitIntoLargeOffer() throws Exception { + testMessagesFitIntoOffer(MAX_PACKET_LENGTH); + } + + @Test + public void testMessagesFitIntoSmallOffer() throws Exception { + testMessagesFitIntoOffer(1000); + } + + private void testMessagesFitIntoOffer(int length) throws Exception { + // Create an offer with as many message IDs as possible + ByteArrayOutputStream out = new ByteArrayOutputStream(length); + ProtocolWriter writer = protocolWriterFactory.createProtocolWriter(out, + true); + int maxMessages = writer.getMaxMessagesForOffer(length); + Collection offered = new ArrayList(); + for(int i = 0; i < maxMessages; i++) { + offered.add(new MessageId(TestUtils.getRandomId())); + } + Offer o = packetFactory.createOffer(offered); + writer.writeOffer(o); + // Check the size of the serialised offer + assertTrue(out.size() <= length); + } + + @Test + public void testTransportsFitIntoUpdate() throws Exception { + // Create the maximum number of plugins, each with the maximum number + // of maximum-length properties + Collection transports = new ArrayList(); + for(int i = 0; i < MAX_TRANSPORTS; i++) { + TransportId id = new TransportId(TestUtils.getRandomId()); + Transport t = new Transport(id); + for(int j = 0; j < MAX_PROPERTIES_PER_TRANSPORT; j++) { + String key = createRandomString(MAX_PROPERTY_LENGTH); + String value = createRandomString(MAX_PROPERTY_LENGTH); + t.put(key, value); + } + transports.add(t); + } + // Add the transports to an update + ByteArrayOutputStream out = + new ByteArrayOutputStream(MAX_PACKET_LENGTH); + ProtocolWriter writer = protocolWriterFactory.createProtocolWriter(out, + true); + TransportUpdate t = packetFactory.createTransportUpdate(transports, + Long.MAX_VALUE); + writer.writeTransportUpdate(t); + // Check the size of the serialised update + assertTrue(out.size() > MAX_TRANSPORTS * (UniqueId.LENGTH + 4 + + (MAX_PROPERTIES_PER_TRANSPORT * MAX_PROPERTY_LENGTH * 2)) + + 8); + assertTrue(out.size() <= MAX_PACKET_LENGTH); + } + + private static String createRandomString(int length) throws Exception { + StringBuilder s = new StringBuilder(length); + for(int i = 0; i < length; i++) { + int digit = (int) (Math.random() * 10); + s.append((char) ('0' + digit)); + } + String string = s.toString(); + assertEquals(length, string.getBytes("UTF-8").length); + return string; + } +} diff --git a/briar-tests/src/net/sf/briar/protocol/ConsumersTest.java b/briar-tests/src/net/sf/briar/protocol/ConsumersTest.java new file mode 100644 index 000000000..0bb89d6d2 --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/ConsumersTest.java @@ -0,0 +1,105 @@ +package net.sf.briar.protocol; + +import static org.junit.Assert.assertArrayEquals; + +import java.security.GeneralSecurityException; +import java.util.Random; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.FormatException; +import net.sf.briar.api.crypto.MessageDigest; +import net.sf.briar.api.serial.CopyingConsumer; +import net.sf.briar.api.serial.CountingConsumer; +import net.sf.briar.api.serial.DigestingConsumer; + +import org.junit.Test; + +public class ConsumersTest extends BriarTestCase { + + @Test + public void testDigestingConsumer() throws Exception { + byte[] data = new byte[1234]; + // Generate some random data and digest it + new Random().nextBytes(data); + MessageDigest messageDigest = new TestMessageDigest(); + messageDigest.update(data); + byte[] dig = messageDigest.digest(); + // Check that feeding a DigestingConsumer generates the same digest + DigestingConsumer dc = new DigestingConsumer(messageDigest); + dc.write(data[0]); + dc.write(data, 1, data.length - 2); + dc.write(data[data.length - 1]); + byte[] dig1 = messageDigest.digest(); + assertArrayEquals(dig, dig1); + } + + @Test + public void testCountingConsumer() throws Exception { + byte[] data = new byte[1234]; + CountingConsumer cc = new CountingConsumer(data.length); + cc.write(data[0]); + cc.write(data, 1, data.length - 2); + cc.write(data[data.length - 1]); + assertEquals(data.length, cc.getCount()); + try { + cc.write((byte) 0); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testCopyingConsumer() throws Exception { + byte[] data = new byte[1234]; + new Random().nextBytes(data); + // Check that a CopyingConsumer creates a faithful copy + CopyingConsumer cc = new CopyingConsumer(); + cc.write(data[0]); + cc.write(data, 1, data.length - 2); + cc.write(data[data.length - 1]); + assertArrayEquals(data, cc.getCopy()); + } + + private static class TestMessageDigest implements MessageDigest { + + private final java.security.MessageDigest delegate; + + private TestMessageDigest() throws GeneralSecurityException { + delegate = java.security.MessageDigest.getInstance("SHA-256"); + } + + public byte[] digest() { + return delegate.digest(); + } + + public byte[] digest(byte[] input) { + return delegate.digest(input); + } + + public int digest(byte[] buf, int offset, int len) { + byte[] digest = digest(); + len = Math.min(len, digest.length); + System.arraycopy(digest, 0, buf, offset, len); + return len; + } + + public int getDigestLength() { + return delegate.getDigestLength(); + } + + public void reset() { + delegate.reset(); + } + + public void update(byte input) { + delegate.update(input); + } + + public void update(byte[] input) { + delegate.update(input); + } + + public void update(byte[] input, int offset, int len) { + delegate.update(input, offset, len); + } + } +} diff --git a/briar-tests/src/net/sf/briar/protocol/OfferReaderTest.java b/briar-tests/src/net/sf/briar/protocol/OfferReaderTest.java new file mode 100644 index 000000000..d3ecbc0fb --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/OfferReaderTest.java @@ -0,0 +1,124 @@ +package net.sf.briar.protocol; + +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PACKET_LENGTH; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.Collection; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.api.FormatException; +import net.sf.briar.api.protocol.Offer; +import net.sf.briar.api.protocol.PacketFactory; +import net.sf.briar.api.protocol.Types; +import net.sf.briar.api.serial.Reader; +import net.sf.briar.api.serial.ReaderFactory; +import net.sf.briar.api.serial.SerialComponent; +import net.sf.briar.api.serial.Writer; +import net.sf.briar.api.serial.WriterFactory; +import net.sf.briar.serial.SerialModule; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +public class OfferReaderTest extends BriarTestCase { + + // FIXME: This is an integration test, not a unit test + + private final SerialComponent serial; + private final ReaderFactory readerFactory; + private final WriterFactory writerFactory; + private final Mockery context; + + public OfferReaderTest() throws Exception { + super(); + Injector i = Guice.createInjector(new SerialModule()); + serial = i.getInstance(SerialComponent.class); + readerFactory = i.getInstance(ReaderFactory.class); + writerFactory = i.getInstance(WriterFactory.class); + context = new Mockery(); + } + + @Test + public void testFormatExceptionIfOfferIsTooLarge() throws Exception { + PacketFactory packetFactory = context.mock(PacketFactory.class); + OfferReader offerReader = new OfferReader(packetFactory); + + byte[] b = createOffer(true); + ByteArrayInputStream in = new ByteArrayInputStream(b); + Reader reader = readerFactory.createReader(in); + reader.addStructReader(Types.OFFER, offerReader); + + try { + reader.readStruct(Types.OFFER, Offer.class); + fail(); + } catch(FormatException expected) {} + context.assertIsSatisfied(); + } + + @Test + @SuppressWarnings("unchecked") + public void testNoFormatExceptionIfOfferIsMaximumSize() throws Exception { + final PacketFactory packetFactory = context.mock(PacketFactory.class); + OfferReader offerReader = new OfferReader(packetFactory); + final Offer offer = context.mock(Offer.class); + context.checking(new Expectations() {{ + oneOf(packetFactory).createOffer(with(any(Collection.class))); + will(returnValue(offer)); + }}); + + byte[] b = createOffer(false); + ByteArrayInputStream in = new ByteArrayInputStream(b); + Reader reader = readerFactory.createReader(in); + reader.addStructReader(Types.OFFER, offerReader); + + assertEquals(offer, reader.readStruct(Types.OFFER, Offer.class)); + context.assertIsSatisfied(); + } + + @Test + public void testEmptyOffer() throws Exception { + final PacketFactory packetFactory = context.mock(PacketFactory.class); + OfferReader offerReader = new OfferReader(packetFactory); + + byte[] b = createEmptyOffer(); + ByteArrayInputStream in = new ByteArrayInputStream(b); + Reader reader = readerFactory.createReader(in); + reader.addStructReader(Types.OFFER, offerReader); + + try { + reader.readStruct(Types.OFFER, Offer.class); + fail(); + } catch(FormatException expected) {} + context.assertIsSatisfied(); + } + + private byte[] createOffer(boolean tooBig) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer w = writerFactory.createWriter(out); + w.writeStructId(Types.OFFER); + w.writeListStart(); + while(out.size() + serial.getSerialisedUniqueIdLength() + < MAX_PACKET_LENGTH) { + w.writeBytes(TestUtils.getRandomId()); + } + if(tooBig) w.writeBytes(TestUtils.getRandomId()); + w.writeListEnd(); + assertEquals(tooBig, out.size() > MAX_PACKET_LENGTH); + return out.toByteArray(); + } + + private byte[] createEmptyOffer() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer w = writerFactory.createWriter(out); + w.writeStructId(Types.OFFER); + w.writeListStart(); + w.writeListEnd(); + return out.toByteArray(); + } +} diff --git a/briar-tests/src/net/sf/briar/protocol/ProtocolIntegrationTest.java b/briar-tests/src/net/sf/briar/protocol/ProtocolIntegrationTest.java new file mode 100644 index 000000000..4c939bc9c --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/ProtocolIntegrationTest.java @@ -0,0 +1,133 @@ +package net.sf.briar.protocol; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.BitSet; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.api.protocol.Ack; +import net.sf.briar.api.protocol.Batch; +import net.sf.briar.api.protocol.BatchId; +import net.sf.briar.api.protocol.Group; +import net.sf.briar.api.protocol.GroupFactory; +import net.sf.briar.api.protocol.GroupId; +import net.sf.briar.api.protocol.Message; +import net.sf.briar.api.protocol.MessageFactory; +import net.sf.briar.api.protocol.Offer; +import net.sf.briar.api.protocol.PacketFactory; +import net.sf.briar.api.protocol.ProtocolReader; +import net.sf.briar.api.protocol.ProtocolReaderFactory; +import net.sf.briar.api.protocol.ProtocolWriter; +import net.sf.briar.api.protocol.ProtocolWriterFactory; +import net.sf.briar.api.protocol.RawBatch; +import net.sf.briar.api.protocol.Request; +import net.sf.briar.api.protocol.SubscriptionUpdate; +import net.sf.briar.api.protocol.Transport; +import net.sf.briar.api.protocol.TransportId; +import net.sf.briar.api.protocol.TransportUpdate; +import net.sf.briar.crypto.CryptoModule; +import net.sf.briar.serial.SerialModule; + +import org.junit.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +public class ProtocolIntegrationTest extends BriarTestCase { + + private final ProtocolReaderFactory readerFactory; + private final ProtocolWriterFactory writerFactory; + private final PacketFactory packetFactory; + private final BatchId batchId; + private final Group group; + private final Message message; + private final String subject = "Hello"; + private final String messageBody = "Hello world"; + private final BitSet bitSet; + private final Map subscriptions; + private final Collection transports; + private final long timestamp = System.currentTimeMillis(); + + public ProtocolIntegrationTest() throws Exception { + super(); + Injector i = Guice.createInjector(new CryptoModule(), + new ProtocolModule(), new SerialModule()); + readerFactory = i.getInstance(ProtocolReaderFactory.class); + writerFactory = i.getInstance(ProtocolWriterFactory.class); + packetFactory = i.getInstance(PacketFactory.class); + batchId = new BatchId(TestUtils.getRandomId()); + GroupFactory groupFactory = i.getInstance(GroupFactory.class); + group = groupFactory.createGroup("Unrestricted group", null); + MessageFactory messageFactory = i.getInstance(MessageFactory.class); + message = messageFactory.createMessage(null, group, subject, + messageBody.getBytes("UTF-8")); + bitSet = new BitSet(); + bitSet.set(3); + bitSet.set(7); + subscriptions = Collections.singletonMap(group, 123L); + TransportId transportId = new TransportId(TestUtils.getRandomId()); + Transport transport = new Transport(transportId, + Collections.singletonMap("bar", "baz")); + transports = Collections.singletonList(transport); + } + + @Test + public void testWriteAndRead() throws Exception { + // Write + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ProtocolWriter writer = writerFactory.createProtocolWriter(out, true); + + Ack a = packetFactory.createAck(Collections.singletonList(batchId)); + writer.writeAck(a); + + RawBatch b = packetFactory.createBatch(Collections.singletonList( + message.getSerialised())); + writer.writeBatch(b); + + Offer o = packetFactory.createOffer(Collections.singletonList( + message.getId())); + writer.writeOffer(o); + + Request r = packetFactory.createRequest(bitSet, 10); + writer.writeRequest(r); + + SubscriptionUpdate s = packetFactory.createSubscriptionUpdate( + Collections.emptyMap(), subscriptions, 0L, + timestamp); + writer.writeSubscriptionUpdate(s); + + TransportUpdate t = packetFactory.createTransportUpdate(transports, + timestamp); + writer.writeTransportUpdate(t); + + // Read + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + ProtocolReader reader = readerFactory.createProtocolReader(in); + + a = reader.readAck(); + assertEquals(Collections.singletonList(batchId), a.getBatchIds()); + + Batch b1 = reader.readBatch().verify(); + assertEquals(Collections.singletonList(message), b1.getMessages()); + + o = reader.readOffer(); + assertEquals(Collections.singletonList(message.getId()), + o.getMessageIds()); + + r = reader.readRequest(); + assertEquals(bitSet, r.getBitmap()); + assertEquals(10, r.getLength()); + + s = reader.readSubscriptionUpdate(); + assertEquals(subscriptions, s.getSubscriptions()); + assertEquals(timestamp, s.getTimestamp()); + + t = reader.readTransportUpdate(); + assertEquals(transports, t.getTransports()); + assertEquals(timestamp, t.getTimestamp()); + } +} diff --git a/briar-tests/src/net/sf/briar/protocol/ProtocolWriterImplTest.java b/briar-tests/src/net/sf/briar/protocol/ProtocolWriterImplTest.java new file mode 100644 index 000000000..4f2343b19 --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/ProtocolWriterImplTest.java @@ -0,0 +1,87 @@ +package net.sf.briar.protocol; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.BitSet; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.protocol.PacketFactory; +import net.sf.briar.api.protocol.ProtocolWriter; +import net.sf.briar.api.protocol.Request; +import net.sf.briar.api.serial.SerialComponent; +import net.sf.briar.api.serial.WriterFactory; +import net.sf.briar.crypto.CryptoModule; +import net.sf.briar.serial.SerialModule; +import net.sf.briar.util.StringUtils; + +import org.junit.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +public class ProtocolWriterImplTest extends BriarTestCase { + + // FIXME: This is an integration test, not a unit test + + private final PacketFactory packetFactory; + private final SerialComponent serial; + private final WriterFactory writerFactory; + + public ProtocolWriterImplTest() { + super(); + Injector i = Guice.createInjector(new CryptoModule(), + new ProtocolModule(), new SerialModule()); + packetFactory = i.getInstance(PacketFactory.class); + serial = i.getInstance(SerialComponent.class); + writerFactory = i.getInstance(WriterFactory.class); + } + + @Test + public void testWriteBitmapNoPadding() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ProtocolWriter w = new ProtocolWriterImpl(serial, writerFactory, out, + true); + BitSet b = new BitSet(); + // 11011001 = 0xD9 + b.set(0); + b.set(1); + b.set(3); + b.set(4); + b.set(7); + // 01011001 = 0x59 + b.set(9); + b.set(11); + b.set(12); + b.set(15); + Request r = packetFactory.createRequest(b, 16); + w.writeRequest(r); + // Short user tag 6, 0 as uint7, short bytes with length 2, 0xD959 + byte[] output = out.toByteArray(); + assertEquals("C6" + "00" + "92" + "D959", + StringUtils.toHexString(output)); + } + + @Test + public void testWriteBitmapWithPadding() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ProtocolWriter w = new ProtocolWriterImpl(serial, writerFactory, out, + true); + BitSet b = new BitSet(); + // 01011001 = 0x59 + b.set(1); + b.set(3); + b.set(4); + b.set(7); + // 11011xxx = 0xD8, after padding + b.set(8); + b.set(9); + b.set(11); + b.set(12); + Request r = packetFactory.createRequest(b, 13); + w.writeRequest(r); + // Short user tag 6, 3 as uint7, short bytes with length 2, 0x59D8 + byte[] output = out.toByteArray(); + assertEquals("C6" + "03" + "92" + "59D8", + StringUtils.toHexString(output)); + } +} diff --git a/briar-tests/src/net/sf/briar/protocol/RequestReaderTest.java b/briar-tests/src/net/sf/briar/protocol/RequestReaderTest.java new file mode 100644 index 000000000..7dc377ecd --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/RequestReaderTest.java @@ -0,0 +1,146 @@ +package net.sf.briar.protocol; + +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PACKET_LENGTH; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.BitSet; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.FormatException; +import net.sf.briar.api.protocol.PacketFactory; +import net.sf.briar.api.protocol.Request; +import net.sf.briar.api.protocol.Types; +import net.sf.briar.api.serial.Reader; +import net.sf.briar.api.serial.ReaderFactory; +import net.sf.briar.api.serial.Writer; +import net.sf.briar.api.serial.WriterFactory; +import net.sf.briar.crypto.CryptoModule; +import net.sf.briar.serial.SerialModule; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +public class RequestReaderTest extends BriarTestCase { + + // FIXME: This is an integration test, not a unit test + + private final ReaderFactory readerFactory; + private final WriterFactory writerFactory; + private final PacketFactory packetFactory; + private final Mockery context; + + public RequestReaderTest() throws Exception { + super(); + Injector i = Guice.createInjector(new CryptoModule(), + new ProtocolModule(), new SerialModule()); + readerFactory = i.getInstance(ReaderFactory.class); + writerFactory = i.getInstance(WriterFactory.class); + packetFactory = i.getInstance(PacketFactory.class); + context = new Mockery(); + } + + @Test + public void testFormatExceptionIfRequestIsTooLarge() throws Exception { + PacketFactory packetFactory = context.mock(PacketFactory.class); + RequestReader requestReader = new RequestReader(packetFactory); + + byte[] b = createRequest(true); + ByteArrayInputStream in = new ByteArrayInputStream(b); + Reader reader = readerFactory.createReader(in); + reader.addStructReader(Types.REQUEST, requestReader); + + try { + reader.readStruct(Types.REQUEST, Request.class); + fail(); + } catch(FormatException expected) {} + context.assertIsSatisfied(); + } + + @Test + public void testNoFormatExceptionIfRequestIsMaximumSize() throws Exception { + final PacketFactory packetFactory = context.mock(PacketFactory.class); + RequestReader requestReader = new RequestReader(packetFactory); + final Request request = context.mock(Request.class); + context.checking(new Expectations() {{ + oneOf(packetFactory).createRequest(with(any(BitSet.class)), + with(any(int.class))); + will(returnValue(request)); + }}); + + byte[] b = createRequest(false); + ByteArrayInputStream in = new ByteArrayInputStream(b); + Reader reader = readerFactory.createReader(in); + reader.addStructReader(Types.REQUEST, requestReader); + + assertEquals(request, reader.readStruct(Types.REQUEST, + Request.class)); + context.assertIsSatisfied(); + } + + @Test + public void testBitmapDecoding() throws Exception { + // Test sizes from 0 to 1000 bits + for(int i = 0; i < 1000; i++) { + // Create a BitSet of size i with one in ten bits set (on average) + BitSet requested = new BitSet(i); + for(int j = 0; j < i; j++) if(Math.random() < 0.1) requested.set(j); + // Encode the BitSet as a bitmap + int bytes = i % 8 == 0 ? i / 8 : i / 8 + 1; + byte[] bitmap = new byte[bytes]; + for(int j = 0; j < i; j++) { + if(requested.get(j)) { + int offset = j / 8; + byte bit = (byte) (128 >> j % 8); + bitmap[offset] |= bit; + } + } + // Create a serialised request containing the bitmap + byte[] b = createRequest(bitmap); + // Deserialise the request + ByteArrayInputStream in = new ByteArrayInputStream(b); + Reader reader = readerFactory.createReader(in); + RequestReader requestReader = new RequestReader(packetFactory); + reader.addStructReader(Types.REQUEST, requestReader); + Request r = reader.readStruct(Types.REQUEST, Request.class); + BitSet decoded = r.getBitmap(); + // Check that the decoded BitSet matches the original - we can't + // use equals() because of padding, but the first i bits should + // match and the cardinalities should be equal, indicating that no + // padding bits are set + for(int j = 0; j < i; j++) { + assertEquals(requested.get(j), decoded.get(j)); + } + assertEquals(requested.cardinality(), decoded.cardinality()); + } + } + + private byte[] createRequest(boolean tooBig) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer w = writerFactory.createWriter(out); + w.writeStructId(Types.REQUEST); + // Allow one byte for the REQUEST tag, one byte for the padding length + // as a uint7, one byte for the BYTES tag, and five bytes for the + // length of the byte array as an int32 + int size = MAX_PACKET_LENGTH - 8; + if(tooBig) size++; + assertTrue(size > Short.MAX_VALUE); + w.writeUint7((byte) 0); + w.writeBytes(new byte[size]); + assertEquals(tooBig, out.size() > MAX_PACKET_LENGTH); + return out.toByteArray(); + } + + private byte[] createRequest(byte[] bitmap) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer w = writerFactory.createWriter(out); + w.writeStructId(Types.REQUEST); + w.writeUint7((byte) 0); + w.writeBytes(bitmap); + return out.toByteArray(); + } +} diff --git a/briar-tests/src/net/sf/briar/protocol/UnverifiedBatchImplTest.java b/briar-tests/src/net/sf/briar/protocol/UnverifiedBatchImplTest.java new file mode 100644 index 000000000..126f8a983 --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/UnverifiedBatchImplTest.java @@ -0,0 +1,244 @@ +package net.sf.briar.protocol; + +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.Signature; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.Random; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.api.crypto.CryptoComponent; +import net.sf.briar.api.crypto.MessageDigest; +import net.sf.briar.api.protocol.Author; +import net.sf.briar.api.protocol.AuthorId; +import net.sf.briar.api.protocol.Batch; +import net.sf.briar.api.protocol.BatchId; +import net.sf.briar.api.protocol.Group; +import net.sf.briar.api.protocol.GroupId; +import net.sf.briar.api.protocol.Message; +import net.sf.briar.api.protocol.MessageId; +import net.sf.briar.api.protocol.UnverifiedBatch; +import net.sf.briar.crypto.CryptoModule; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +public class UnverifiedBatchImplTest extends BriarTestCase { + + // FIXME: This is an integration test, not a unit test + + private final CryptoComponent crypto; + private final byte[] raw, raw1; + private final String subject; + private final long timestamp; + + public UnverifiedBatchImplTest() { + super(); + Injector i = Guice.createInjector(new CryptoModule()); + crypto = i.getInstance(CryptoComponent.class); + Random r = new Random(); + raw = new byte[123]; + r.nextBytes(raw); + raw1 = new byte[1234]; + r.nextBytes(raw1); + subject = "Unit tests are exciting"; + timestamp = System.currentTimeMillis(); + } + + @Test + public void testIds() throws Exception { + // Calculate the expected batch and message IDs + MessageDigest messageDigest = crypto.getMessageDigest(); + messageDigest.update(raw); + messageDigest.update(raw1); + BatchId batchId = new BatchId(messageDigest.digest()); + messageDigest.update(raw); + MessageId messageId = new MessageId(messageDigest.digest()); + messageDigest.update(raw1); + MessageId messageId1 = new MessageId(messageDigest.digest()); + // Verify the batch + Mockery context = new Mockery(); + final UnverifiedMessage message = + context.mock(UnverifiedMessage.class, "message"); + final UnverifiedMessage message1 = + context.mock(UnverifiedMessage.class, "message1"); + context.checking(new Expectations() {{ + // First message + oneOf(message).getRaw(); + will(returnValue(raw)); + oneOf(message).getAuthor(); + will(returnValue(null)); + oneOf(message).getGroup(); + will(returnValue(null)); + oneOf(message).getParent(); + will(returnValue(null)); + oneOf(message).getSubject(); + will(returnValue(subject)); + oneOf(message).getTimestamp(); + will(returnValue(timestamp)); + oneOf(message).getBodyStart(); + will(returnValue(10)); + oneOf(message).getBodyLength(); + will(returnValue(100)); + // Second message + oneOf(message1).getRaw(); + will(returnValue(raw1)); + oneOf(message1).getAuthor(); + will(returnValue(null)); + oneOf(message1).getGroup(); + will(returnValue(null)); + oneOf(message1).getParent(); + will(returnValue(null)); + oneOf(message1).getSubject(); + will(returnValue(subject)); + oneOf(message1).getTimestamp(); + will(returnValue(timestamp)); + oneOf(message1).getBodyStart(); + will(returnValue(10)); + oneOf(message1).getBodyLength(); + will(returnValue(1000)); + }}); + Collection messages = Arrays.asList(message, + message1); + UnverifiedBatch batch = new UnverifiedBatchImpl(crypto, messages); + Batch verifiedBatch = batch.verify(); + // Check that the batch and message IDs match + assertEquals(batchId, verifiedBatch.getId()); + Collection verifiedMessages = verifiedBatch.getMessages(); + assertEquals(2, verifiedMessages.size()); + Iterator it = verifiedMessages.iterator(); + Message verifiedMessage = it.next(); + assertEquals(messageId, verifiedMessage.getId()); + Message verifiedMessage1 = it.next(); + assertEquals(messageId1, verifiedMessage1.getId()); + context.assertIsSatisfied(); + } + + @Test + public void testSignatures() throws Exception { + final int signedByAuthor = 100, signedByGroup = 110; + final KeyPair authorKeyPair = crypto.generateSignatureKeyPair(); + final KeyPair groupKeyPair = crypto.generateSignatureKeyPair(); + Signature signature = crypto.getSignature(); + // Calculate the expected author and group signatures + signature.initSign(authorKeyPair.getPrivate()); + signature.update(raw, 0, signedByAuthor); + final byte[] authorSignature = signature.sign(); + signature.initSign(groupKeyPair.getPrivate()); + signature.update(raw, 0, signedByGroup); + final byte[] groupSignature = signature.sign(); + // Verify the batch + Mockery context = new Mockery(); + final UnverifiedMessage message = + context.mock(UnverifiedMessage.class, "message"); + final Author author = context.mock(Author.class); + final Group group = context.mock(Group.class); + final UnverifiedMessage message1 = + context.mock(UnverifiedMessage.class, "message1"); + context.checking(new Expectations() {{ + // First message + oneOf(message).getRaw(); + will(returnValue(raw)); + oneOf(message).getAuthor(); + will(returnValue(author)); + oneOf(author).getPublicKey(); + will(returnValue(authorKeyPair.getPublic().getEncoded())); + oneOf(message).getLengthSignedByAuthor(); + will(returnValue(signedByAuthor)); + oneOf(message).getAuthorSignature(); + will(returnValue(authorSignature)); + oneOf(message).getGroup(); + will(returnValue(group)); + exactly(2).of(group).getPublicKey(); + will(returnValue(groupKeyPair.getPublic().getEncoded())); + oneOf(message).getLengthSignedByGroup(); + will(returnValue(signedByGroup)); + oneOf(message).getGroupSignature(); + will(returnValue(groupSignature)); + oneOf(author).getId(); + will(returnValue(new AuthorId(TestUtils.getRandomId()))); + oneOf(group).getId(); + will(returnValue(new GroupId(TestUtils.getRandomId()))); + oneOf(message).getParent(); + will(returnValue(null)); + oneOf(message).getSubject(); + will(returnValue(subject)); + oneOf(message).getTimestamp(); + will(returnValue(timestamp)); + oneOf(message).getBodyStart(); + will(returnValue(10)); + oneOf(message).getBodyLength(); + will(returnValue(100)); + // Second message + oneOf(message1).getRaw(); + will(returnValue(raw1)); + oneOf(message1).getAuthor(); + will(returnValue(null)); + oneOf(message1).getGroup(); + will(returnValue(null)); + oneOf(message1).getParent(); + will(returnValue(null)); + oneOf(message1).getSubject(); + will(returnValue(subject)); + oneOf(message1).getTimestamp(); + will(returnValue(timestamp)); + oneOf(message1).getBodyStart(); + will(returnValue(10)); + oneOf(message1).getBodyLength(); + will(returnValue(1000)); + }}); + Collection messages = Arrays.asList(message, + message1); + UnverifiedBatch batch = new UnverifiedBatchImpl(crypto, messages); + batch.verify(); + context.assertIsSatisfied(); + } + + @Test + public void testExceptionThrownIfMessageIsModified() throws Exception { + final int signedByAuthor = 100; + final KeyPair authorKeyPair = crypto.generateSignatureKeyPair(); + Signature signature = crypto.getSignature(); + // Calculate the expected author signature + signature.initSign(authorKeyPair.getPrivate()); + signature.update(raw, 0, signedByAuthor); + final byte[] authorSignature = signature.sign(); + // Modify the message + raw[signedByAuthor / 2] ^= 0xff; + // Verify the batch + Mockery context = new Mockery(); + final UnverifiedMessage message = + context.mock(UnverifiedMessage.class, "message"); + final Author author = context.mock(Author.class); + final UnverifiedMessage message1 = + context.mock(UnverifiedMessage.class, "message1"); + context.checking(new Expectations() {{ + // First message - verification will fail at the author's signature + oneOf(message).getRaw(); + will(returnValue(raw)); + oneOf(message).getAuthor(); + will(returnValue(author)); + oneOf(author).getPublicKey(); + will(returnValue(authorKeyPair.getPublic().getEncoded())); + oneOf(message).getLengthSignedByAuthor(); + will(returnValue(signedByAuthor)); + oneOf(message).getAuthorSignature(); + will(returnValue(authorSignature)); + }}); + Collection messages = Arrays.asList(message, + message1); + UnverifiedBatch batch = new UnverifiedBatchImpl(crypto, messages); + try { + batch.verify(); + fail(); + } catch(GeneralSecurityException expected) {} + context.assertIsSatisfied(); + } +} diff --git a/briar-tests/src/net/sf/briar/protocol/simplex/OutgoingSimplexConnectionTest.java b/briar-tests/src/net/sf/briar/protocol/simplex/OutgoingSimplexConnectionTest.java new file mode 100644 index 000000000..bc0491b2e --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/simplex/OutgoingSimplexConnectionTest.java @@ -0,0 +1,177 @@ +package net.sf.briar.protocol.simplex; + +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PACKET_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.HEADER_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.MAC_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.MIN_CONNECTION_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH; + +import java.io.ByteArrayOutputStream; +import java.util.Collections; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.db.DatabaseComponent; +import net.sf.briar.api.db.DatabaseExecutor; +import net.sf.briar.api.protocol.Ack; +import net.sf.briar.api.protocol.BatchId; +import net.sf.briar.api.protocol.ProtocolWriterFactory; +import net.sf.briar.api.protocol.RawBatch; +import net.sf.briar.api.protocol.TransportId; +import net.sf.briar.api.protocol.UniqueId; +import net.sf.briar.api.transport.ConnectionContext; +import net.sf.briar.api.transport.ConnectionRegistry; +import net.sf.briar.api.transport.ConnectionWriterFactory; +import net.sf.briar.clock.ClockModule; +import net.sf.briar.crypto.CryptoModule; +import net.sf.briar.protocol.ProtocolModule; +import net.sf.briar.protocol.duplex.DuplexProtocolModule; +import net.sf.briar.serial.SerialModule; +import net.sf.briar.transport.TransportModule; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.Test; + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; + +public class OutgoingSimplexConnectionTest extends BriarTestCase { + + // FIXME: This is an integration test, not a unit test + + private final Mockery context; + private final DatabaseComponent db; + private final ConnectionRegistry connRegistry; + private final ConnectionWriterFactory connFactory; + private final ProtocolWriterFactory protoFactory; + private final ContactId contactId; + private final TransportId transportId; + private final byte[] secret; + + public OutgoingSimplexConnectionTest() { + super(); + context = new Mockery(); + db = context.mock(DatabaseComponent.class); + Module testModule = new AbstractModule() { + @Override + public void configure() { + bind(DatabaseComponent.class).toInstance(db); + bind(Executor.class).annotatedWith( + DatabaseExecutor.class).toInstance( + Executors.newCachedThreadPool()); + } + }; + Injector i = Guice.createInjector(testModule, new ClockModule(), + new CryptoModule(), new SerialModule(), new TransportModule(), + new SimplexProtocolModule(), new ProtocolModule(), + new DuplexProtocolModule()); + connRegistry = i.getInstance(ConnectionRegistry.class); + connFactory = i.getInstance(ConnectionWriterFactory.class); + protoFactory = i.getInstance(ProtocolWriterFactory.class); + contactId = new ContactId(234); + transportId = new TransportId(TestUtils.getRandomId()); + secret = new byte[32]; + } + + @Test + public void testConnectionTooShort() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + TestSimplexTransportWriter transport = new TestSimplexTransportWriter( + out, MAX_PACKET_LENGTH, true); + ConnectionContext ctx = new ConnectionContext(contactId, transportId, + secret, 0L, true); + OutgoingSimplexConnection connection = new OutgoingSimplexConnection(db, + connRegistry, connFactory, protoFactory, ctx, transport); + connection.write(); + // Nothing should have been written + assertEquals(0, out.size()); + // The transport should have been disposed with exception == true + assertTrue(transport.getDisposed()); + assertTrue(transport.getException()); + } + + @Test + public void testNothingToSend() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + TestSimplexTransportWriter transport = new TestSimplexTransportWriter( + out, MIN_CONNECTION_LENGTH, true); + ConnectionContext ctx = new ConnectionContext(contactId, transportId, + secret, 0L, true); + OutgoingSimplexConnection connection = new OutgoingSimplexConnection(db, + connRegistry, connFactory, protoFactory, ctx, transport); + context.checking(new Expectations() {{ + // No transports to send + oneOf(db).generateTransportUpdate(contactId); + will(returnValue(null)); + // No subscriptions to send + oneOf(db).generateSubscriptionUpdate(contactId); + will(returnValue(null)); + // No acks to send + oneOf(db).generateAck(with(contactId), with(any(int.class))); + will(returnValue(null)); + // No batches to send + oneOf(db).generateBatch(with(contactId), with(any(int.class))); + will(returnValue(null)); + }}); + connection.write(); + // Nothing should have been written + assertEquals(0, out.size()); + // The transport should have been disposed with exception == false + assertTrue(transport.getDisposed()); + assertFalse(transport.getException()); + context.assertIsSatisfied(); + } + + @Test + public void testSomethingToSend() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + TestSimplexTransportWriter transport = new TestSimplexTransportWriter( + out, MIN_CONNECTION_LENGTH, true); + ConnectionContext ctx = new ConnectionContext(contactId, transportId, + secret, 0L, true); + OutgoingSimplexConnection connection = new OutgoingSimplexConnection(db, + connRegistry, connFactory, protoFactory, ctx, transport); + final Ack ack = context.mock(Ack.class); + final BatchId batchId = new BatchId(TestUtils.getRandomId()); + final RawBatch batch = context.mock(RawBatch.class); + final byte[] message = new byte[1234]; + context.checking(new Expectations() {{ + // No transports to send + oneOf(db).generateTransportUpdate(contactId); + will(returnValue(null)); + // No subscriptions to send + oneOf(db).generateSubscriptionUpdate(contactId); + will(returnValue(null)); + // One ack to send + oneOf(db).generateAck(with(contactId), with(any(int.class))); + will(returnValue(ack)); + oneOf(ack).getBatchIds(); + will(returnValue(Collections.singletonList(batchId))); + // No more acks + oneOf(db).generateAck(with(contactId), with(any(int.class))); + will(returnValue(null)); + // One batch to send + oneOf(db).generateBatch(with(contactId), with(any(int.class))); + will(returnValue(batch)); + oneOf(batch).getMessages(); + will(returnValue(Collections.singletonList(message))); + // No more batches + oneOf(db).generateBatch(with(contactId), with(any(int.class))); + will(returnValue(null)); + }}); + connection.write(); + // Something should have been written + int overhead = TAG_LENGTH + HEADER_LENGTH + MAC_LENGTH; + assertTrue(out.size() > overhead + UniqueId.LENGTH + message.length); + // The transport should have been disposed with exception == false + assertTrue(transport.getDisposed()); + assertFalse(transport.getException()); + context.assertIsSatisfied(); + } +} diff --git a/briar-tests/src/net/sf/briar/protocol/simplex/SimplexProtocolIntegrationTest.java b/briar-tests/src/net/sf/briar/protocol/simplex/SimplexProtocolIntegrationTest.java new file mode 100644 index 000000000..302b37e3d --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/simplex/SimplexProtocolIntegrationTest.java @@ -0,0 +1,223 @@ +package net.sf.briar.protocol.simplex; + +import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.util.Collection; +import java.util.Collections; +import java.util.Random; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestDatabaseModule; +import net.sf.briar.TestUtils; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.crypto.KeyManager; +import net.sf.briar.api.db.DatabaseComponent; +import net.sf.briar.api.db.event.DatabaseEvent; +import net.sf.briar.api.db.event.DatabaseListener; +import net.sf.briar.api.db.event.MessagesAddedEvent; +import net.sf.briar.api.protocol.Message; +import net.sf.briar.api.protocol.MessageFactory; +import net.sf.briar.api.protocol.ProtocolReaderFactory; +import net.sf.briar.api.protocol.ProtocolWriterFactory; +import net.sf.briar.api.protocol.Transport; +import net.sf.briar.api.protocol.TransportId; +import net.sf.briar.api.protocol.TransportUpdate; +import net.sf.briar.api.transport.ConnectionContext; +import net.sf.briar.api.transport.ConnectionReaderFactory; +import net.sf.briar.api.transport.ConnectionRecogniser; +import net.sf.briar.api.transport.ConnectionRegistry; +import net.sf.briar.api.transport.ConnectionWriterFactory; +import net.sf.briar.api.transport.ContactTransport; +import net.sf.briar.clock.ClockModule; +import net.sf.briar.crypto.CryptoModule; +import net.sf.briar.db.DatabaseModule; +import net.sf.briar.lifecycle.LifecycleModule; +import net.sf.briar.plugins.ImmediateExecutor; +import net.sf.briar.protocol.ProtocolModule; +import net.sf.briar.protocol.duplex.DuplexProtocolModule; +import net.sf.briar.serial.SerialModule; +import net.sf.briar.transport.TransportModule; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +public class SimplexProtocolIntegrationTest extends BriarTestCase { + + private static final long CLOCK_DIFFERENCE = 60 * 1000L; + private static final long LATENCY = 60 * 1000L; + + private final File testDir = TestUtils.getTestDirectory(); + private final File aliceDir = new File(testDir, "alice"); + private final File bobDir = new File(testDir, "bob"); + private final TransportId transportId; + private final byte[] initialSecret; + private final long epoch; + + private Injector alice, bob; + + public SimplexProtocolIntegrationTest() throws Exception { + super(); + transportId = new TransportId(TestUtils.getRandomId()); + // Create matching secrets for Alice and Bob + initialSecret = new byte[32]; + new Random().nextBytes(initialSecret); + long rotationPeriod = 2 * CLOCK_DIFFERENCE + LATENCY; + epoch = System.currentTimeMillis() - 2 * rotationPeriod; + } + + @Before + public void setUp() { + testDir.mkdirs(); + alice = createInjector(aliceDir); + bob = createInjector(bobDir); + } + + private Injector createInjector(File dir) { + return Guice.createInjector(new ClockModule(), new CryptoModule(), + new DatabaseModule(), new LifecycleModule(), + new ProtocolModule(), new SerialModule(), + new TestDatabaseModule(dir), new SimplexProtocolModule(), + new TransportModule(), new DuplexProtocolModule()); + } + + @Test + public void testInjection() { + DatabaseComponent aliceDb = alice.getInstance(DatabaseComponent.class); + DatabaseComponent bobDb = bob.getInstance(DatabaseComponent.class); + assertFalse(aliceDb == bobDb); + } + + @Test + public void testWriteAndRead() throws Exception { + read(write()); + } + + private byte[] write() throws Exception { + // Open Alice's database + DatabaseComponent db = alice.getInstance(DatabaseComponent.class); + db.open(false); + // Start Alice's key manager + KeyManager km = alice.getInstance(KeyManager.class); + km.start(); + // Add Bob as a contact + ContactId contactId = db.addContact(); + ContactTransport ct = new ContactTransport(contactId, transportId, + epoch, CLOCK_DIFFERENCE, LATENCY, true); + db.addContactTransport(ct); + km.contactTransportAdded(ct, initialSecret.clone()); + // Send Bob a message + String subject = "Hello"; + byte[] body = "Hi Bob!".getBytes("UTF-8"); + MessageFactory messageFactory = alice.getInstance(MessageFactory.class); + Message message = messageFactory.createMessage(null, subject, body); + db.addLocalPrivateMessage(message, contactId); + // Create an outgoing simplex connection + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ConnectionRegistry connRegistry = + alice.getInstance(ConnectionRegistry.class); + ConnectionWriterFactory connFactory = + alice.getInstance(ConnectionWriterFactory.class); + ProtocolWriterFactory protoFactory = + alice.getInstance(ProtocolWriterFactory.class); + TestSimplexTransportWriter transport = new TestSimplexTransportWriter( + out, Long.MAX_VALUE, false); + ConnectionContext ctx = km.getConnectionContext(contactId, transportId); + assertNotNull(ctx); + OutgoingSimplexConnection simplex = new OutgoingSimplexConnection(db, + connRegistry, connFactory, protoFactory, ctx, transport); + // Write whatever needs to be written + simplex.write(); + assertTrue(transport.getDisposed()); + assertFalse(transport.getException()); + // Clean up + km.stop(); + db.close(); + // Return the contents of the simplex connection + return out.toByteArray(); + } + + private void read(byte[] b) throws Exception { + // Open Bob's database + DatabaseComponent db = bob.getInstance(DatabaseComponent.class); + db.open(false); + // Start Bob's key manager + KeyManager km = bob.getInstance(KeyManager.class); + km.start(); + // Add Alice as a contact + ContactId contactId = db.addContact(); + ContactTransport ct = new ContactTransport(contactId, transportId, + epoch, CLOCK_DIFFERENCE, LATENCY, false); + db.addContactTransport(ct); + km.contactTransportAdded(ct, initialSecret.clone()); + // Set up a database listener + MessageListener listener = new MessageListener(); + db.addListener(listener); + // Fake a transport update from Alice + TransportUpdate transportUpdate = new TransportUpdate() { + + public Collection getTransports() { + Transport t = new Transport(transportId); + return Collections.singletonList(t); + } + + public long getTimestamp() { + return System.currentTimeMillis(); + } + }; + db.receiveTransportUpdate(contactId, transportUpdate); + // Create a connection recogniser and recognise the connection + ByteArrayInputStream in = new ByteArrayInputStream(b); + ConnectionRecogniser rec = bob.getInstance(ConnectionRecogniser.class); + byte[] tag = new byte[TAG_LENGTH]; + int read = in.read(tag); + assertEquals(tag.length, read); + ConnectionContext ctx = rec.acceptConnection(transportId, tag); + assertNotNull(ctx); + // Create an incoming simplex connection + ConnectionRegistry connRegistry = + bob.getInstance(ConnectionRegistry.class); + ConnectionReaderFactory connFactory = + bob.getInstance(ConnectionReaderFactory.class); + ProtocolReaderFactory protoFactory = + bob.getInstance(ProtocolReaderFactory.class); + TestSimplexTransportReader transport = + new TestSimplexTransportReader(in); + IncomingSimplexConnection simplex = new IncomingSimplexConnection( + new ImmediateExecutor(), new ImmediateExecutor(), db, + connRegistry, connFactory, protoFactory, ctx, transport); + // No messages should have been added yet + assertFalse(listener.messagesAdded); + // Read whatever needs to be read + simplex.read(); + assertTrue(transport.getDisposed()); + assertFalse(transport.getException()); + assertTrue(transport.getRecognised()); + // The private message from Alice should have been added + assertTrue(listener.messagesAdded); + // Clean up + km.stop(); + db.close(); + } + + @After + public void tearDown() { + TestUtils.deleteTestDirectory(testDir); + } + + private static class MessageListener implements DatabaseListener { + + private boolean messagesAdded = false; + + public void eventOccurred(DatabaseEvent e) { + if(e instanceof MessagesAddedEvent) + messagesAdded = true; + } + } +} diff --git a/briar-tests/src/net/sf/briar/protocol/simplex/TestSimplexTransportReader.java b/briar-tests/src/net/sf/briar/protocol/simplex/TestSimplexTransportReader.java new file mode 100644 index 000000000..1d85ed3ec --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/simplex/TestSimplexTransportReader.java @@ -0,0 +1,39 @@ +package net.sf.briar.protocol.simplex; + +import java.io.InputStream; + +import net.sf.briar.api.plugins.simplex.SimplexTransportReader; + +class TestSimplexTransportReader implements SimplexTransportReader { + + private final InputStream in; + + private boolean disposed = false, exception = false, recognised = false; + + TestSimplexTransportReader(InputStream in) { + this.in = in; + } + + public InputStream getInputStream() { + return in; + } + + public void dispose(boolean exception, boolean recognised) { + assert !disposed; + disposed = true; + this.exception = exception; + this.recognised = recognised; + } + + boolean getDisposed() { + return disposed; + } + + boolean getException() { + return exception; + } + + boolean getRecognised() { + return recognised; + } +} \ No newline at end of file diff --git a/briar-tests/src/net/sf/briar/protocol/simplex/TestSimplexTransportWriter.java b/briar-tests/src/net/sf/briar/protocol/simplex/TestSimplexTransportWriter.java new file mode 100644 index 000000000..7a1524cb5 --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/simplex/TestSimplexTransportWriter.java @@ -0,0 +1,48 @@ +package net.sf.briar.protocol.simplex; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; + +import net.sf.briar.api.plugins.simplex.SimplexTransportWriter; + +class TestSimplexTransportWriter implements SimplexTransportWriter { + + private final ByteArrayOutputStream out; + private final long capacity; + private final boolean flush; + + private boolean disposed = false, exception = false; + + TestSimplexTransportWriter(ByteArrayOutputStream out, long capacity, + boolean flush) { + this.out = out; + this.capacity = capacity; + this.flush = flush; + } + + public long getCapacity() { + return capacity; + } + + public OutputStream getOutputStream() { + return out; + } + + public boolean shouldFlush() { + return flush; + } + + public void dispose(boolean exception) { + assert !disposed; + disposed = true; + this.exception = exception; + } + + boolean getDisposed() { + return disposed; + } + + boolean getException() { + return exception; + } +} \ No newline at end of file diff --git a/briar-tests/src/net/sf/briar/serial/ReaderImplTest.java b/briar-tests/src/net/sf/briar/serial/ReaderImplTest.java new file mode 100644 index 000000000..a79cdb56b --- /dev/null +++ b/briar-tests/src/net/sf/briar/serial/ReaderImplTest.java @@ -0,0 +1,556 @@ +package net.sf.briar.serial; + +import static org.junit.Assert.assertArrayEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.Bytes; +import net.sf.briar.api.FormatException; +import net.sf.briar.api.serial.Consumer; +import net.sf.briar.api.serial.StructReader; +import net.sf.briar.api.serial.Reader; +import net.sf.briar.util.StringUtils; + +import org.junit.Test; + +public class ReaderImplTest extends BriarTestCase { + + private ByteArrayInputStream in = null; + private ReaderImpl r = null; + + @Test + public void testReadBoolean() throws Exception { + setContents("FFFE"); + assertFalse(r.readBoolean()); + assertTrue(r.readBoolean()); + assertTrue(r.eof()); + } + + @Test + public void testReadInt8() throws Exception { + setContents("FD00" + "FDFF" + "FD7F" + "FD80"); + assertEquals((byte) 0, r.readInt8()); + assertEquals((byte) -1, r.readInt8()); + assertEquals(Byte.MAX_VALUE, r.readInt8()); + assertEquals(Byte.MIN_VALUE, r.readInt8()); + assertTrue(r.eof()); + } + + @Test + public void testReadInt16() throws Exception { + setContents("FC0000" + "FCFFFF" + "FC7FFF" + "FC8000"); + assertEquals((short) 0, r.readInt16()); + assertEquals((short) -1, r.readInt16()); + assertEquals(Short.MAX_VALUE, r.readInt16()); + assertEquals(Short.MIN_VALUE, r.readInt16()); + assertTrue(r.eof()); + } + + @Test + public void testReadInt32() throws Exception { + setContents("FB00000000" + "FBFFFFFFFF" + "FB7FFFFFFF" + "FB80000000"); + assertEquals(0, r.readInt32()); + assertEquals(-1, r.readInt32()); + assertEquals(Integer.MAX_VALUE, r.readInt32()); + assertEquals(Integer.MIN_VALUE, r.readInt32()); + assertTrue(r.eof()); + } + + @Test + public void testReadInt64() throws Exception { + setContents("FA0000000000000000" + "FAFFFFFFFFFFFFFFFF" + + "FA7FFFFFFFFFFFFFFF" + "FA8000000000000000"); + assertEquals(0L, r.readInt64()); + assertEquals(-1L, r.readInt64()); + assertEquals(Long.MAX_VALUE, r.readInt64()); + assertEquals(Long.MIN_VALUE, r.readInt64()); + assertTrue(r.eof()); + } + + @Test + public void testReadIntAny() throws Exception { + setContents("00" + "7F" + "FD80" + "FDFF" + "FC0080" + "FC7FFF" + + "FB00008000" + "FB7FFFFFFF" + "FA0000000080000000"); + assertEquals(0L, r.readIntAny()); + assertEquals(127L, r.readIntAny()); + assertEquals(-128L, r.readIntAny()); + assertEquals(-1L, r.readIntAny()); + assertEquals(128L, r.readIntAny()); + assertEquals(32767L, r.readIntAny()); + assertEquals(32768L, r.readIntAny()); + assertEquals(2147483647L, r.readIntAny()); + assertEquals(2147483648L, r.readIntAny()); + assertTrue(r.eof()); + } + + @Test + public void testReadFloat32() throws Exception { + // http://babbage.cs.qc.edu/IEEE-754/Decimal.html + // http://steve.hollasch.net/cgindex/coding/ieeefloat.html + setContents("F900000000" + "F93F800000" + "F940000000" + "F9BF800000" + + "F980000000" + "F9FF800000" + "F97F800000" + "F97FC00000"); + assertEquals(0F, r.readFloat32()); + assertEquals(1F, r.readFloat32()); + assertEquals(2F, r.readFloat32()); + assertEquals(-1F, r.readFloat32()); + assertEquals(-0F, r.readFloat32()); + assertEquals(Float.NEGATIVE_INFINITY, r.readFloat32()); + assertEquals(Float.POSITIVE_INFINITY, r.readFloat32()); + assertTrue(Float.isNaN(r.readFloat32())); + assertTrue(r.eof()); + } + + @Test + public void testReadFloat64() throws Exception { + setContents("F80000000000000000" + "F83FF0000000000000" + + "F84000000000000000" + "F8BFF0000000000000" + + "F88000000000000000" + "F8FFF0000000000000" + + "F87FF0000000000000" + "F87FF8000000000000"); + assertEquals(0.0, r.readFloat64()); + assertEquals(1.0, r.readFloat64()); + assertEquals(2.0, r.readFloat64()); + assertEquals(-1.0, r.readFloat64()); + assertEquals(-0.0, r.readFloat64()); + assertEquals(Double.NEGATIVE_INFINITY, r.readFloat64()); + assertEquals(Double.POSITIVE_INFINITY, r.readFloat64()); + assertTrue(Double.isNaN(r.readFloat64())); + assertTrue(r.eof()); + } + + @Test + public void testReadString() throws Exception { + setContents("F703666F6F" + "83666F6F" + "F700" + "80"); + assertEquals("foo", r.readString()); + assertEquals("foo", r.readString()); + assertEquals("", r.readString()); + assertEquals("", r.readString()); + assertTrue(r.eof()); + } + + @Test + public void testReadStringMaxLength() throws Exception { + setContents("83666F6F" + "83666F6F"); + assertEquals("foo", r.readString(3)); + try { + r.readString(2); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testReadBytes() throws Exception { + setContents("F603010203" + "93010203" + "F600" + "90"); + assertArrayEquals(new byte[] {1, 2, 3}, r.readBytes()); + assertArrayEquals(new byte[] {1, 2, 3}, r.readBytes()); + assertArrayEquals(new byte[] {}, r.readBytes()); + assertArrayEquals(new byte[] {}, r.readBytes()); + assertTrue(r.eof()); + } + + @Test + public void testReadBytesMaxLength() throws Exception { + setContents("93010203" + "93010203"); + assertArrayEquals(new byte[] {1, 2, 3}, r.readBytes(3)); + try { + r.readBytes(2); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testReadShortList() throws Exception { + setContents("A" + "3" + "01" + "83666F6F" + "FC0080"); + List l = r.readList(Object.class); + assertNotNull(l); + assertEquals(3, l.size()); + assertEquals((byte) 1, l.get(0)); + assertEquals("foo", l.get(1)); + assertEquals((short) 128, l.get(2)); + assertTrue(r.eof()); + } + + @Test + public void testReadList() throws Exception { + setContents("F5" + "01" + "83666F6F" + "FC0080" + "F3"); + List l = r.readList(Object.class); + assertNotNull(l); + assertEquals(3, l.size()); + assertEquals((byte) 1, l.get(0)); + assertEquals("foo", l.get(1)); + assertEquals((short) 128, l.get(2)); + assertTrue(r.eof()); + } + + @Test + public void testReadListTypeSafe() throws Exception { + setContents("A" + "3" + "01" + "02" + "03"); + List l = r.readList(Byte.class); + assertNotNull(l); + assertEquals(3, l.size()); + assertEquals(Byte.valueOf((byte) 1), l.get(0)); + assertEquals(Byte.valueOf((byte) 2), l.get(1)); + assertEquals(Byte.valueOf((byte) 3), l.get(2)); + assertTrue(r.eof()); + } + + @Test + public void testReadListTypeSafeThrowsFormatException() throws Exception { + setContents("A" + "3" + "01" + "83666F6F" + "03"); + // Trying to read a mixed list as a list of bytes should throw a + // FormatException + try { + r.readList(Byte.class); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testReadShortMap() throws Exception { + setContents("B" + "2" + "83666F6F" + "7B" + "90" + "F2"); + Map m = r.readMap(Object.class, Object.class); + assertNotNull(m); + assertEquals(2, m.size()); + assertEquals((byte) 123, m.get("foo")); + Bytes b = new Bytes(new byte[] {}); + assertTrue(m.containsKey(b)); + assertNull(m.get(b)); + assertTrue(r.eof()); + } + + @Test + public void testReadMap() throws Exception { + setContents("F4" + "83666F6F" + "7B" + "90" + "F2" + "F3"); + Map m = r.readMap(Object.class, Object.class); + assertNotNull(m); + assertEquals(2, m.size()); + assertEquals((byte) 123, m.get("foo")); + Bytes b = new Bytes(new byte[] {}); + assertTrue(m.containsKey(b)); + assertNull(m.get(b)); + assertTrue(r.eof()); + } + + @Test + public void testReadMapTypeSafe() throws Exception { + setContents("B" + "2" + "83666F6F" + "7B" + "80" + "F2"); + Map m = r.readMap(String.class, Byte.class); + assertNotNull(m); + assertEquals(2, m.size()); + assertEquals(Byte.valueOf((byte) 123), m.get("foo")); + assertTrue(m.containsKey("")); + assertNull(m.get("")); + assertTrue(r.eof()); + } + + @Test + public void testMapKeysMustBeUnique() throws Exception { + setContents("B" + "2" + "83666F6F" + "01" + "83626172" + "02" + + "B" + "2" + "83666F6F" + "01" + "83666F6F" + "02"); + // The first map has unique keys + Map m = r.readMap(String.class, Byte.class); + assertNotNull(m); + assertEquals(2, m.size()); + assertEquals(Byte.valueOf((byte) 1), m.get("foo")); + assertEquals(Byte.valueOf((byte) 2), m.get("bar")); + // The second map has a duplicate key + try { + r.readMap(String.class, Byte.class); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testReadDelimitedList() throws Exception { + setContents("F5" + "01" + "83666F6F" + "FC0080" + "F3"); + List l = r.readList(Object.class); + assertNotNull(l); + assertEquals(3, l.size()); + assertEquals((byte) 1, l.get(0)); + assertEquals("foo", l.get(1)); + assertEquals((short) 128, l.get(2)); + assertTrue(r.eof()); + } + + @Test + public void testReadDelimitedListElements() throws Exception { + setContents("F5" + "01" + "83666F6F" + "FC0080" + "F3"); + assertTrue(r.hasListStart()); + r.readListStart(); + assertFalse(r.hasListEnd()); + assertEquals((byte) 1, r.readIntAny()); + assertFalse(r.hasListEnd()); + assertEquals("foo", r.readString()); + assertFalse(r.hasListEnd()); + assertEquals((short) 128, r.readIntAny()); + assertTrue(r.hasListEnd()); + r.readListEnd(); + assertTrue(r.eof()); + } + + @Test + public void testReadDelimitedListTypeSafe() throws Exception { + setContents("F5" + "01" + "02" + "03" + "F3"); + List l = r.readList(Byte.class); + assertNotNull(l); + assertEquals(3, l.size()); + assertEquals(Byte.valueOf((byte) 1), l.get(0)); + assertEquals(Byte.valueOf((byte) 2), l.get(1)); + assertEquals(Byte.valueOf((byte) 3), l.get(2)); + assertTrue(r.eof()); + } + + @Test + public void testReadDelimitedMap() throws Exception { + setContents("F4" + "83666F6F" + "7B" + "90" + "F2" + "F3"); + Map m = r.readMap(Object.class, Object.class); + assertNotNull(m); + assertEquals(2, m.size()); + assertEquals((byte) 123, m.get("foo")); + Bytes b = new Bytes(new byte[] {}); + assertTrue(m.containsKey(b)); + assertNull(m.get(b)); + assertTrue(r.eof()); + } + + @Test + public void testReadDelimitedMapEntries() throws Exception { + setContents("F4" + "83666F6F" + "7B" + "90" + "F2" + "F3"); + assertTrue(r.hasMapStart()); + r.readMapStart(); + assertFalse(r.hasMapEnd()); + assertEquals("foo", r.readString()); + assertFalse(r.hasMapEnd()); + assertEquals((byte) 123, r.readIntAny()); + assertFalse(r.hasMapEnd()); + assertArrayEquals(new byte[] {}, r.readBytes()); + assertFalse(r.hasMapEnd()); + assertTrue(r.hasNull()); + r.readNull(); + assertTrue(r.hasMapEnd()); + r.readMapEnd(); + assertTrue(r.eof()); + } + + @Test + public void testReadDelimitedMapTypeSafe() throws Exception { + setContents("F4" + "83666F6F" + "7B" + "80" + "F2" + "F3"); + Map m = r.readMap(String.class, Byte.class); + assertNotNull(m); + assertEquals(2, m.size()); + assertEquals(Byte.valueOf((byte) 123), m.get("foo")); + assertTrue(m.containsKey("")); + assertNull(m.get("")); + assertTrue(r.eof()); + } + + @Test + @SuppressWarnings("unchecked") + public void testReadNestedMapsAndLists() throws Exception { + setContents("B" + "1" + "B" + "1" + "83666F6F" + "7B" + + "A" + "1" + "01"); + Map m = r.readMap(Object.class, Object.class); + assertNotNull(m); + assertEquals(1, m.size()); + Entry e = m.entrySet().iterator().next(); + Map m1 = (Map) e.getKey(); + assertNotNull(m1); + assertEquals(1, m1.size()); + assertEquals((byte) 123, m1.get("foo")); + List l = (List) e.getValue(); + assertNotNull(l); + assertEquals(1, l.size()); + assertEquals((byte) 1, l.get(0)); + assertTrue(r.eof()); + } + + @Test + public void testReadStruct() throws Exception { + setContents("C0" + "83666F6F" + "F1" + "FF" + "83666F6F"); + // Add readers for two structs + r.addStructReader(0, new StructReader() { + public Foo readStruct(Reader r) throws IOException { + r.readStructId(0); + return new Foo(r.readString()); + } + }); + r.addStructReader(255, new StructReader() { + public Bar readStruct(Reader r) throws IOException { + r.readStructId(255); + return new Bar(r.readString()); + } + }); + // Test both ID formats, short and long + assertTrue(r.hasStruct(0)); + assertEquals("foo", r.readStruct(0, Foo.class).s); + assertTrue(r.hasStruct(255)); + assertEquals("foo", r.readStruct(255, Bar.class).s); + } + + @Test + public void testReadStructWithConsumer() throws Exception { + setContents("C0" + "83666F6F" + "F1" + "FF" + "83666F6F"); + // Add readers for two structs + r.addStructReader(0, new StructReader() { + public Foo readStruct(Reader r) throws IOException { + r.readStructId(0); + return new Foo(r.readString()); + } + }); + r.addStructReader(255, new StructReader() { + public Bar readStruct(Reader r) throws IOException { + r.readStructId(255); + return new Bar(r.readString()); + } + }); + // Add a consumer + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + r.addConsumer(new Consumer() { + + public void write(byte b) throws IOException { + out.write(b); + } + + public void write(byte[] b, int off, int len) throws IOException { + out.write(b, off, len); + } + }); + // Test both ID formats, short and long + assertTrue(r.hasStruct(0)); + assertEquals("foo", r.readStruct(0, Foo.class).s); + assertTrue(r.hasStruct(255)); + assertEquals("foo", r.readStruct(255, Bar.class).s); + // Check that everything was passed to the consumer + assertEquals("C0" + "83666F6F" + "F1" + "FF" + "83666F6F", + StringUtils.toHexString(out.toByteArray())); + } + + @Test + public void testUnknownStructIdThrowsFormatException() throws Exception { + setContents("C0" + "83666F6F"); + assertTrue(r.hasStruct(0)); + // No reader has been added for struct ID 0 + try { + r.readStruct(0, Foo.class); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testWrongClassThrowsFormatException() throws Exception { + setContents("C0" + "83666F6F"); + // Add a reader for struct ID 0, class Foo + r.addStructReader(0, new StructReader() { + public Foo readStruct(Reader r) throws IOException { + r.readStructId(0); + return new Foo(r.readString()); + } + }); + assertTrue(r.hasStruct(0)); + // Trying to read the struct as class Bar should throw a FormatException + try { + r.readStruct(0, Bar.class); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testReadListUsingStructReader() throws Exception { + setContents("A" + "1" + "C0" + "83666F6F"); + // Add a reader for a struct + r.addStructReader(0, new StructReader() { + public Foo readStruct(Reader r) throws IOException { + r.readStructId(0); + return new Foo(r.readString()); + } + }); + // Check that the reader is used for lists + List l = r.readList(Foo.class); + assertEquals(1, l.size()); + assertEquals("foo", l.get(0).s); + } + + @Test + public void testReadMapUsingStructReader() throws Exception { + setContents("B" + "1" + "C0" + "83666F6F" + "C1" + "83626172"); + // Add readers for two structs + r.addStructReader(0, new StructReader() { + public Foo readStruct(Reader r) throws IOException { + r.readStructId(0); + return new Foo(r.readString()); + } + }); + r.addStructReader(1, new StructReader() { + public Bar readStruct(Reader r) throws IOException { + r.readStructId(1); + return new Bar(r.readString()); + } + }); + // Check that the readers are used for maps + Map m = r.readMap(Foo.class, Bar.class); + assertEquals(1, m.size()); + Entry e = m.entrySet().iterator().next(); + assertEquals("foo", e.getKey().s); + assertEquals("bar", e.getValue().s); + } + + @Test + public void testMaxLengthAppliesInsideMap() throws Exception { + setContents("B" + "1" + "83666F6F" + "93010203"); + r.setMaxStringLength(3); + r.setMaxBytesLength(3); + Map m = r.readMap(String.class, Bytes.class); + String key = "foo"; + Bytes value = new Bytes(new byte[] {1, 2, 3}); + assertEquals(Collections.singletonMap(key, value), m); + // The max string length should be applied inside the map + setContents("B" + "1" + "83666F6F" + "93010203"); + r.setMaxStringLength(2); + try { + r.readMap(String.class, Bytes.class); + fail(); + } catch(FormatException expected) {} + // The max bytes length should be applied inside the map + setContents("B" + "1" + "83666F6F" + "93010203"); + r.setMaxBytesLength(2); + try { + r.readMap(String.class, Bytes.class); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testReadEmptyInput() throws Exception { + setContents(""); + assertTrue(r.eof()); + } + + private void setContents(String hex) { + in = new ByteArrayInputStream(StringUtils.fromHexString(hex)); + r = new ReaderImpl(in); + } + + private static class Foo { + + private final String s; + + private Foo(String s) { + this.s = s; + } + } + + private static class Bar { + + private final String s; + + private Bar(String s) { + this.s = s; + } + } +} diff --git a/briar-tests/src/net/sf/briar/serial/WriterImplTest.java b/briar-tests/src/net/sf/briar/serial/WriterImplTest.java new file mode 100644 index 000000000..7c9112e9d --- /dev/null +++ b/briar-tests/src/net/sf/briar/serial/WriterImplTest.java @@ -0,0 +1,291 @@ +package net.sf.briar.serial; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.util.StringUtils; + +import org.junit.Before; +import org.junit.Test; + +public class WriterImplTest extends BriarTestCase { + + private ByteArrayOutputStream out = null; + private WriterImpl w = null; + + @Before + public void setUp() { + out = new ByteArrayOutputStream(); + w = new WriterImpl(out); + } + + @Test + public void testWriteBoolean() throws IOException { + w.writeBoolean(true); + w.writeBoolean(false); + // TRUE tag, FALSE tag + checkContents("FE" + "FF"); + } + + @Test + public void testWriteUint7() throws IOException { + w.writeUint7((byte) 0); + w.writeUint7(Byte.MAX_VALUE); + // 0, 127 + checkContents("00" + "7F"); + } + + @Test + public void testWriteInt8() throws IOException { + w.writeInt8((byte) 0); + w.writeInt8((byte) -1); + w.writeInt8(Byte.MIN_VALUE); + w.writeInt8(Byte.MAX_VALUE); + // INT8 tag, 0, INT8 tag, -1, INT8 tag, -128, INT8 tag, 127 + checkContents("FD" + "00" + "FD" + "FF" + "FD" + "80" + "FD" + "7F"); + } + + @Test + public void testWriteInt16() throws IOException { + w.writeInt16((short) 0); + w.writeInt16((short) -1); + w.writeInt16(Short.MIN_VALUE); + w.writeInt16(Short.MAX_VALUE); + // INT16 tag, 0, INT16 tag, -1, INT16 tag, -32768, INT16 tag, 32767 + checkContents("FC" + "0000" + "FC" + "FFFF" + "FC" + "8000" + + "FC" + "7FFF"); + } + + @Test + public void testWriteInt32() throws IOException { + w.writeInt32(0); + w.writeInt32(-1); + w.writeInt32(Integer.MIN_VALUE); + w.writeInt32(Integer.MAX_VALUE); + // INT32 tag, 0, INT32 tag, -1, etc + checkContents("FB" + "00000000" + "FB" + "FFFFFFFF" + "FB" + "80000000" + + "FB" + "7FFFFFFF"); + } + + @Test + public void testWriteInt64() throws IOException { + w.writeInt64(0L); + w.writeInt64(-1L); + w.writeInt64(Long.MIN_VALUE); + w.writeInt64(Long.MAX_VALUE); + // INT64 tag, 0, INT64 tag, -1, etc + checkContents("FA" + "0000000000000000" + "FA" + "FFFFFFFFFFFFFFFF" + + "FA" + "8000000000000000" + "FA" + "7FFFFFFFFFFFFFFF"); + } + + @Test + public void testWriteIntAny() throws IOException { + w.writeIntAny(0); // uint7 + w.writeIntAny(-1); // int8 + w.writeIntAny(Byte.MAX_VALUE); // uint7 + w.writeIntAny(Byte.MAX_VALUE + 1); // int16 + w.writeIntAny(Short.MAX_VALUE); // int16 + w.writeIntAny(Short.MAX_VALUE + 1); // int32 + w.writeIntAny(Integer.MAX_VALUE); // int32 + w.writeIntAny(Integer.MAX_VALUE + 1L); // int64 + checkContents("00" + "FDFF" + "7F" + "FC0080" + "FC7FFF" + + "FB00008000" + "FB7FFFFFFF" + "FA0000000080000000"); + } + + @Test + public void testWriteFloat32() throws IOException { + // http://babbage.cs.qc.edu/IEEE-754/Decimal.html + // 1 bit for sign, 8 for exponent, 23 for significand + w.writeFloat32(0F); // 0 0 0 -> 0x00000000 + w.writeFloat32(1F); // 0 127 1 -> 0x3F800000 + w.writeFloat32(2F); // 0 128 1 -> 0x40000000 + w.writeFloat32(-1F); // 1 127 1 -> 0xBF800000 + w.writeFloat32(-0F); // 1 0 0 -> 0x80000000 + // http://steve.hollasch.net/cgindex/coding/ieeefloat.html + w.writeFloat32(Float.NEGATIVE_INFINITY); // 1 255 0 -> 0xFF800000 + w.writeFloat32(Float.POSITIVE_INFINITY); // 0 255 0 -> 0x7F800000 + w.writeFloat32(Float.NaN); // 0 255 1 -> 0x7FC00000 + checkContents("F9" + "00000000" + "F9" + "3F800000" + "F9" + "40000000" + + "F9" + "BF800000" + "F9" + "80000000" + "F9" + "FF800000" + + "F9" + "7F800000" + "F9" + "7FC00000"); + } + + @Test + public void testWriteFloat64() throws IOException { + // 1 bit for sign, 11 for exponent, 52 for significand + w.writeFloat64(0.0); // 0 0 0 -> 0x0000000000000000 + w.writeFloat64(1.0); // 0 1023 1 -> 0x3FF0000000000000 + w.writeFloat64(2.0); // 0 1024 1 -> 0x4000000000000000 + w.writeFloat64(-1.0); // 1 1023 1 -> 0xBFF0000000000000 + w.writeFloat64(-0.0); // 1 0 0 -> 0x8000000000000000 + w.writeFloat64(Double.NEGATIVE_INFINITY); // 1 2047 0 -> 0xFFF00000... + w.writeFloat64(Double.POSITIVE_INFINITY); // 0 2047 0 -> 0x7FF00000... + w.writeFloat64(Double.NaN); // 0 2047 1 -> 0x7FF8000000000000 + checkContents("F8" + "0000000000000000" + "F8" + "3FF0000000000000" + + "F8" + "4000000000000000" + "F8" + "BFF0000000000000" + + "F8" + "8000000000000000" + "F8" + "FFF0000000000000" + + "F8" + "7FF0000000000000" + "F8" + "7FF8000000000000"); + } + + @Test + public void testWriteShortString() throws IOException { + w.writeString("foo bar baz bam"); + // SHORT_STRING tag, length 15, UTF-8 bytes + checkContents("8" + "F" + "666F6F206261722062617A2062616D"); + } + + @Test + public void testWriteString() throws IOException { + w.writeString("foo bar baz bam "); + // STRING tag, length 16 as uint7, UTF-8 bytes + checkContents("F7" + "10" + "666F6F206261722062617A2062616D20"); + } + + @Test + public void testWriteShortBytes() throws IOException { + w.writeBytes(new byte[] { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 + }); + // SHORT_BYTES tag, length 15, bytes + checkContents("9" + "F" + "000102030405060708090A0B0C0D0E"); + } + + @Test + public void testWriteBytes() throws IOException { + w.writeBytes(new byte[] { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 + }); + // BYTES tag, length 16 as uint7, bytes + checkContents("F6" + "10" + "000102030405060708090A0B0C0D0E0F"); + } + + @Test + public void testWriteShortList() throws IOException { + List l = new ArrayList(); + for(int i = 0; i < 15; i++) l.add(i); + w.writeList(l); + // SHORT_LIST tag, length, elements as uint7 + checkContents("A" + "F" + "000102030405060708090A0B0C0D0E"); + } + + @Test + public void testWriteList() throws IOException { + List l = new ArrayList(); + for(int i = 0; i < 16; i++) l.add(i); + w.writeList(l); + // LIST tag, elements as uint7, END tag + checkContents("F5" + "000102030405060708090A0B0C0D0E0F" + "F3"); + } + + @Test + public void testListCanContainNull() throws IOException { + List l = new ArrayList(); + l.add(1); + l.add(null); + l.add(2); + w.writeList(l); + // SHORT_LIST tag, length, 1 as uint7, null, 2 as uint7 + checkContents("A" + "3" + "01" + "F2" + "02"); + } + + @Test + public void testWriteShortMap() throws IOException { + // Use LinkedHashMap to get predictable iteration order + Map m = new LinkedHashMap(); + for(int i = 0; i < 15; i++) m.put(i, i + 1); + w.writeMap(m); + // SHORT_MAP tag, size, entries as uint7 + checkContents("B" + "F" + "0001" + "0102" + "0203" + "0304" + "0405" + + "0506" + "0607" + "0708" + "0809" + "090A" + "0A0B" + "0B0C" + + "0C0D" + "0D0E" + "0E0F"); + } + + @Test + public void testWriteMap() throws IOException { + // Use LinkedHashMap to get predictable iteration order + Map m = new LinkedHashMap(); + for(int i = 0; i < 16; i++) m.put(i, i + 1); + w.writeMap(m); + // MAP tag, entries as uint7, END tag + checkContents("F4" + "0001" + "0102" + "0203" + "0304" + "0405" + + "0506" + "0607" + "0708" + "0809" + "090A" + "0A0B" + "0B0C" + + "0C0D" + "0D0E" + "0E0F" + "0F10" + "F3"); + } + + @Test + public void testWriteDelimitedList() throws IOException { + w.writeListStart(); + w.writeIntAny((byte) 1); // Written as uint7 + w.writeString("foo"); // Written as short string + w.writeIntAny(128L); // Written as an int16 + w.writeListEnd(); + // LIST tag, 1 as uint7, "foo" as short string, 128 as int16, + // END tag + checkContents("F5" + "01" + "83666F6F" + "FC0080" + "F3"); + } + + @Test + public void testWriteDelimitedMap() throws IOException { + w.writeMapStart(); + w.writeString("foo"); // Written as short string + w.writeIntAny(123); // Written as a uint7 + w.writeBytes(new byte[] {}); // Written as short bytes + w.writeNull(); + w.writeMapEnd(); + // MAP tag, "foo" as short string, 123 as uint7, + // byte[] {} as short bytes, NULL tag, END tag + checkContents("F4" + "83666F6F" + "7B" + "90" + "F2" + "F3"); + } + + @Test + public void testWriteNestedMapsAndLists() throws IOException { + Map m = new LinkedHashMap(); + m.put("foo", Integer.valueOf(123)); + List l = new ArrayList(); + l.add(Byte.valueOf((byte) 1)); + Map m1 = new LinkedHashMap(); + m1.put(m, l); + w.writeMap(m1); + // SHORT_MAP tag, length 1, SHORT_MAP tag, length 1, + // "foo" as short string, 123 as uint7, SHORT_LIST tag, length 1, + // 1 as uint7 + checkContents("B" + "1" + "B" + "1" + "83666F6F" + "7B" + "A1" + "01"); + } + + @Test + public void testWriteNull() throws IOException { + w.writeNull(); + checkContents("F2"); + } + + @Test + public void testWriteShortStructId() throws IOException { + w.writeStructId(0); + w.writeStructId(31); + // SHORT_STRUCT tag (3 bits), 0 (5 bits), SHORT_STRUCT tag (3 bits), + // 31 (5 bits) + checkContents("C0" + "DF"); + } + + @Test + public void testWriteStructId() throws IOException { + w.writeStructId(32); + w.writeStructId(255); + // STRUCT tag, 32 as uint8, STRUCT tag, 255 as uint8 + checkContents("F1" + "20" + "F1" + "FF"); + } + + private void checkContents(String hex) throws IOException { + out.flush(); + out.close(); + byte[] expected = StringUtils.fromHexString(hex); + assertTrue(StringUtils.toHexString(out.toByteArray()), + Arrays.equals(expected, out.toByteArray())); + } +} diff --git a/briar-tests/src/net/sf/briar/transport/ConnectionReaderImplTest.java b/briar-tests/src/net/sf/briar/transport/ConnectionReaderImplTest.java new file mode 100644 index 000000000..4bec12fc6 --- /dev/null +++ b/briar-tests/src/net/sf/briar/transport/ConnectionReaderImplTest.java @@ -0,0 +1,107 @@ +package net.sf.briar.transport; + +import static net.sf.briar.api.transport.TransportConstants.HEADER_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.MAC_LENGTH; +import net.sf.briar.BriarTestCase; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.Test; + +public class ConnectionReaderImplTest extends BriarTestCase { + + private static final int FRAME_LENGTH = 1024; + private static final int MAX_PAYLOAD_LENGTH = + FRAME_LENGTH - HEADER_LENGTH - MAC_LENGTH; + + @Test + public void testEmptyFramesAreSkipped() throws Exception { + Mockery context = new Mockery(); + final FrameReader reader = context.mock(FrameReader.class); + context.checking(new Expectations() {{ + oneOf(reader).readFrame(with(any(byte[].class))); + will(returnValue(0)); // Empty frame + oneOf(reader).readFrame(with(any(byte[].class))); + will(returnValue(2)); // Non-empty frame with two payload bytes + oneOf(reader).readFrame(with(any(byte[].class))); + will(returnValue(0)); // Empty frame + oneOf(reader).readFrame(with(any(byte[].class))); + will(returnValue(-1)); // No more frames + }}); + ConnectionReaderImpl c = new ConnectionReaderImpl(reader, FRAME_LENGTH); + assertEquals(0, c.read()); // Skip the first empty frame, read a byte + assertEquals(0, c.read()); // Read another byte + assertEquals(-1, c.read()); // Skip the second empty frame, reach EOF + assertEquals(-1, c.read()); // Still at EOF + context.assertIsSatisfied(); + } + + @Test + public void testEmptyFramesAreSkippedWithBuffer() throws Exception { + Mockery context = new Mockery(); + final FrameReader reader = context.mock(FrameReader.class); + context.checking(new Expectations() {{ + oneOf(reader).readFrame(with(any(byte[].class))); + will(returnValue(0)); // Empty frame + oneOf(reader).readFrame(with(any(byte[].class))); + will(returnValue(2)); // Non-empty frame with two payload bytes + oneOf(reader).readFrame(with(any(byte[].class))); + will(returnValue(0)); // Empty frame + oneOf(reader).readFrame(with(any(byte[].class))); + will(returnValue(-1)); // No more frames + }}); + ConnectionReaderImpl c = new ConnectionReaderImpl(reader, FRAME_LENGTH); + byte[] buf = new byte[MAX_PAYLOAD_LENGTH]; + // Skip the first empty frame, read the two payload bytes + assertEquals(2, c.read(buf)); + // Skip the second empty frame, reach EOF + assertEquals(-1, c.read(buf)); + // Still at EOF + assertEquals(-1, c.read(buf)); + context.assertIsSatisfied(); + } + + @Test + public void testMultipleReadsPerFrame() throws Exception { + Mockery context = new Mockery(); + final FrameReader reader = context.mock(FrameReader.class); + context.checking(new Expectations() {{ + oneOf(reader).readFrame(with(any(byte[].class))); + will(returnValue(MAX_PAYLOAD_LENGTH)); // Nice long frame + oneOf(reader).readFrame(with(any(byte[].class))); + will(returnValue(-1)); // No more frames + }}); + ConnectionReaderImpl c = new ConnectionReaderImpl(reader, FRAME_LENGTH); + byte[] buf = new byte[MAX_PAYLOAD_LENGTH / 2]; + // Read the first half of the payload + assertEquals(MAX_PAYLOAD_LENGTH / 2, c.read(buf)); + // Read the second half of the payload + assertEquals(MAX_PAYLOAD_LENGTH / 2, c.read(buf)); + // Reach EOF + assertEquals(-1, c.read(buf, 0, buf.length)); + context.assertIsSatisfied(); + } + + @Test + public void testMultipleReadsPerFrameWithOffsets() throws Exception { + Mockery context = new Mockery(); + final FrameReader reader = context.mock(FrameReader.class); + context.checking(new Expectations() {{ + oneOf(reader).readFrame(with(any(byte[].class))); + will(returnValue(MAX_PAYLOAD_LENGTH)); // Nice long frame + oneOf(reader).readFrame(with(any(byte[].class))); + will(returnValue(-1)); // No more frames + }}); + ConnectionReaderImpl c = new ConnectionReaderImpl(reader, FRAME_LENGTH); + byte[] buf = new byte[MAX_PAYLOAD_LENGTH]; + // Read the first half of the payload + assertEquals(MAX_PAYLOAD_LENGTH / 2, c.read(buf, MAX_PAYLOAD_LENGTH / 2, + MAX_PAYLOAD_LENGTH / 2)); + // Read the second half of the payload + assertEquals(MAX_PAYLOAD_LENGTH / 2, c.read(buf, 123, + MAX_PAYLOAD_LENGTH / 2)); + // Reach EOF + assertEquals(-1, c.read(buf, 0, buf.length)); + context.assertIsSatisfied(); + } +} diff --git a/briar-tests/src/net/sf/briar/transport/ConnectionRegistryImplTest.java b/briar-tests/src/net/sf/briar/transport/ConnectionRegistryImplTest.java new file mode 100644 index 000000000..af5e853ac --- /dev/null +++ b/briar-tests/src/net/sf/briar/transport/ConnectionRegistryImplTest.java @@ -0,0 +1,73 @@ +package net.sf.briar.transport; + +import java.util.Arrays; +import java.util.Collections; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.protocol.TransportId; +import net.sf.briar.api.transport.ConnectionRegistry; + +import org.junit.Test; + +public class ConnectionRegistryImplTest extends BriarTestCase { + + private final ContactId contactId, contactId1; + private final TransportId transportId, transportId1; + + public ConnectionRegistryImplTest() { + super(); + contactId = new ContactId(1); + contactId1 = new ContactId(2); + transportId = new TransportId(TestUtils.getRandomId()); + transportId1 = new TransportId(TestUtils.getRandomId()); + } + + @Test + public void testRegisterAndUnregister() { + ConnectionRegistry c = new ConnectionRegistryImpl(); + // The registry should be empty + assertEquals(Collections.emptyList(), + c.getConnectedContacts(transportId)); + assertEquals(Collections.emptyList(), + c.getConnectedContacts(transportId1)); + // Check that a registered connection shows up + c.registerConnection(contactId, transportId); + assertEquals(Collections.singletonList(contactId), + c.getConnectedContacts(transportId)); + assertEquals(Collections.emptyList(), + c.getConnectedContacts(transportId1)); + // Register an identical connection - lookup should be unaffected + c.registerConnection(contactId, transportId); + assertEquals(Collections.singletonList(contactId), + c.getConnectedContacts(transportId)); + assertEquals(Collections.emptyList(), + c.getConnectedContacts(transportId1)); + // Unregister one of the connections - lookup should be unaffected + c.unregisterConnection(contactId, transportId); + assertEquals(Collections.singletonList(contactId), + c.getConnectedContacts(transportId)); + assertEquals(Collections.emptyList(), + c.getConnectedContacts(transportId1)); + // Unregister the other connection - lookup should be affected + c.unregisterConnection(contactId, transportId); + assertEquals(Collections.emptyList(), + c.getConnectedContacts(transportId)); + assertEquals(Collections.emptyList(), + c.getConnectedContacts(transportId1)); + // Try to unregister the connection again - exception should be thrown + try { + c.unregisterConnection(contactId, transportId); + fail(); + } catch(IllegalArgumentException expected) {} + // Register both contacts with one transport, one contact with both + c.registerConnection(contactId, transportId); + c.registerConnection(contactId1, transportId); + c.registerConnection(contactId1, transportId1); + assertEquals(Arrays.asList(contactId, contactId1), + c.getConnectedContacts(transportId)); + assertEquals(Collections.singletonList(contactId1), + c.getConnectedContacts(transportId1)); + } +} diff --git a/briar-tests/src/net/sf/briar/transport/ConnectionWindowTest.java b/briar-tests/src/net/sf/briar/transport/ConnectionWindowTest.java new file mode 100644 index 000000000..808f6a810 --- /dev/null +++ b/briar-tests/src/net/sf/briar/transport/ConnectionWindowTest.java @@ -0,0 +1,157 @@ +package net.sf.briar.transport; + +import static net.sf.briar.api.transport.TransportConstants.CONNECTION_WINDOW_SIZE; +import static net.sf.briar.util.ByteUtils.MAX_32_BIT_UNSIGNED; +import static org.junit.Assert.assertArrayEquals; + +import java.util.Collection; + +import net.sf.briar.BriarTestCase; + +import org.junit.Test; + +public class ConnectionWindowTest extends BriarTestCase { + + @Test + public void testWindowSliding() { + ConnectionWindow w = new ConnectionWindow(); + for(int i = 0; i < 100; i++) { + assertFalse(w.isSeen(i)); + w.setSeen(i); + assertTrue(w.isSeen(i)); + } + } + + @Test + public void testWindowJumping() { + ConnectionWindow w = new ConnectionWindow(); + for(int i = 0; i < 100; i += 13) { + assertFalse(w.isSeen(i)); + w.setSeen(i); + assertTrue(w.isSeen(i)); + } + } + + @Test + public void testWindowUpperLimit() { + ConnectionWindow w = new ConnectionWindow(); + // Centre is 0, highest value in window is 15 + w.setSeen(15); + // Centre is 16, highest value in window is 31 + w.setSeen(31); + try { + // Centre is 32, highest value in window is 47 + w.setSeen(48); + fail(); + } catch(IllegalArgumentException expected) {} + // Centre is max - 1, highest value in window is max + byte[] bitmap = new byte[CONNECTION_WINDOW_SIZE / 8]; + w = new ConnectionWindow(MAX_32_BIT_UNSIGNED - 1, bitmap); + assertFalse(w.isSeen(MAX_32_BIT_UNSIGNED - 1)); + assertFalse(w.isSeen(MAX_32_BIT_UNSIGNED)); + // Values greater than max should never be allowed + try { + w.setSeen(MAX_32_BIT_UNSIGNED + 1); + fail(); + } catch(IllegalArgumentException expected) {} + w.setSeen(MAX_32_BIT_UNSIGNED); + assertTrue(w.isSeen(MAX_32_BIT_UNSIGNED)); + // Centre should have moved to max + 1 + assertEquals(MAX_32_BIT_UNSIGNED + 1, w.getCentre()); + // The bit corresponding to max should be set + byte[] expectedBitmap = new byte[CONNECTION_WINDOW_SIZE / 8]; + expectedBitmap[expectedBitmap.length / 2 - 1] = 1; // 00000001 + assertArrayEquals(expectedBitmap, w.getBitmap()); + // Values greater than max should never be allowed even if centre > max + try { + w.setSeen(MAX_32_BIT_UNSIGNED + 1); + fail(); + } catch(IllegalArgumentException expected) {} + } + + @Test + public void testWindowLowerLimit() { + ConnectionWindow w = new ConnectionWindow(); + // Centre is 0, negative values should never be allowed + try { + w.setSeen(-1); + fail(); + } catch(IllegalArgumentException expected) {} + // Slide the window + w.setSeen(15); + // Centre is 16, lowest value in window is 0 + w.setSeen(0); + // Slide the window + w.setSeen(16); + // Centre is 17, lowest value in window is 1 + w.setSeen(1); + try { + w.setSeen(0); + fail(); + } catch(IllegalArgumentException expected) {} + // Slide the window + w.setSeen(25); + // Centre is 26, lowest value in window is 10 + w.setSeen(10); + try { + w.setSeen(9); + fail(); + } catch(IllegalArgumentException expected) {} + // Centre should still be 26 + assertEquals(26, w.getCentre()); + // The bits corresponding to 10, 15, 16 and 25 should be set + byte[] expectedBitmap = new byte[CONNECTION_WINDOW_SIZE / 8]; + expectedBitmap[0] = (byte) 134; // 10000110 + expectedBitmap[1] = 1; // 00000001 + assertArrayEquals(expectedBitmap, w.getBitmap()); + } + + @Test + public void testCannotSetSeenTwice() { + ConnectionWindow w = new ConnectionWindow(); + w.setSeen(15); + try { + w.setSeen(15); + fail(); + } catch(IllegalArgumentException expected) {} + } + + @Test + public void testGetUnseenConnectionNumbers() { + ConnectionWindow w = new ConnectionWindow(); + // Centre is 0; window should cover 0 to 15, inclusive, with none seen + Collection unseen = w.getUnseen(); + assertEquals(16, unseen.size()); + for(int i = 0; i < 16; i++) { + assertTrue(unseen.contains(Long.valueOf(i))); + assertFalse(w.isSeen(i)); + } + w.setSeen(3); + w.setSeen(4); + // Centre is 5; window should cover 0 to 20, inclusive, with two seen + unseen = w.getUnseen(); + assertEquals(19, unseen.size()); + for(int i = 0; i < 21; i++) { + if(i == 3 || i == 4) { + assertFalse(unseen.contains(Long.valueOf(i))); + assertTrue(w.isSeen(i)); + } else { + assertTrue(unseen.contains(Long.valueOf(i))); + assertFalse(w.isSeen(i)); + } + } + w.setSeen(19); + // Centre is 20; window should cover 4 to 35, inclusive, with two seen + unseen = w.getUnseen(); + assertEquals(30, unseen.size()); + for(int i = 4; i < 36; i++) { + if(i == 4 || i == 19) { + assertFalse(unseen.contains(Long.valueOf(i))); + assertTrue(w.isSeen(i)); + } else { + assertTrue(unseen.contains(Long.valueOf(i))); + assertFalse(w.isSeen(i)); + } + } + } +} diff --git a/briar-tests/src/net/sf/briar/transport/ConnectionWriterImplTest.java b/briar-tests/src/net/sf/briar/transport/ConnectionWriterImplTest.java new file mode 100644 index 000000000..663700326 --- /dev/null +++ b/briar-tests/src/net/sf/briar/transport/ConnectionWriterImplTest.java @@ -0,0 +1,124 @@ +package net.sf.briar.transport; + +import static net.sf.briar.api.transport.TransportConstants.HEADER_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.MAC_LENGTH; +import net.sf.briar.BriarTestCase; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.Test; + + +public class ConnectionWriterImplTest extends BriarTestCase { + + private static final int FRAME_LENGTH = 1024; + private static final int MAX_PAYLOAD_LENGTH = + FRAME_LENGTH - HEADER_LENGTH - MAC_LENGTH; + + @Test + public void testCloseWithoutWritingWritesFinalFrame() throws Exception { + Mockery context = new Mockery(); + final FrameWriter writer = context.mock(FrameWriter.class); + context.checking(new Expectations() {{ + // Write an empty final frame + oneOf(writer).writeFrame(with(any(byte[].class)), with(0), + with(true)); + // Flush the stream + oneOf(writer).flush(); + }}); + ConnectionWriterImpl c = new ConnectionWriterImpl(writer, FRAME_LENGTH); + c.close(); + context.assertIsSatisfied(); + } + + @Test + public void testFlushWithoutBufferedDataWritesFrame() throws Exception { + Mockery context = new Mockery(); + final FrameWriter writer = context.mock(FrameWriter.class); + ConnectionWriterImpl c = new ConnectionWriterImpl(writer, FRAME_LENGTH); + context.checking(new Expectations() {{ + // Flush the stream + oneOf(writer).flush(); + }}); + c.flush(); + context.assertIsSatisfied(); + } + + @Test + public void testFlushWithBufferedDataWritesFrameAndFlushes() + throws Exception { + Mockery context = new Mockery(); + final FrameWriter writer = context.mock(FrameWriter.class); + ConnectionWriterImpl c = new ConnectionWriterImpl(writer, FRAME_LENGTH); + context.checking(new Expectations() {{ + // Write a non-final frame with one payload byte + oneOf(writer).writeFrame(with(any(byte[].class)), with(1), + with(false)); + // Flush the stream + oneOf(writer).flush(); + }}); + c.write(0); + c.flush(); + context.assertIsSatisfied(); + } + + @Test + public void testSingleByteWritesWriteFullFrame() throws Exception { + Mockery context = new Mockery(); + final FrameWriter writer = context.mock(FrameWriter.class); + ConnectionWriterImpl c = new ConnectionWriterImpl(writer, FRAME_LENGTH); + context.checking(new Expectations() {{ + // Write a full non-final frame + oneOf(writer).writeFrame(with(any(byte[].class)), + with(MAX_PAYLOAD_LENGTH), with(false)); + }}); + for(int i = 0; i < MAX_PAYLOAD_LENGTH; i++) { + c.write(0); + } + context.assertIsSatisfied(); + } + + @Test + public void testMultiByteWritesWriteFullFrames() throws Exception { + Mockery context = new Mockery(); + final FrameWriter writer = context.mock(FrameWriter.class); + ConnectionWriterImpl c = new ConnectionWriterImpl(writer, FRAME_LENGTH); + context.checking(new Expectations() {{ + // Write two full non-final frames + exactly(2).of(writer).writeFrame(with(any(byte[].class)), + with(MAX_PAYLOAD_LENGTH), with(false)); + }}); + // Sanity check + assertEquals(0, MAX_PAYLOAD_LENGTH % 2); + // Write two full payloads using four multi-byte writes + byte[] b = new byte[MAX_PAYLOAD_LENGTH / 2]; + c.write(b); + c.write(b); + c.write(b); + c.write(b); + context.assertIsSatisfied(); + } + + @Test + public void testLargeMultiByteWriteWritesFullFrames() throws Exception { + Mockery context = new Mockery(); + final FrameWriter writer = context.mock(FrameWriter.class); + ConnectionWriterImpl c = new ConnectionWriterImpl(writer, FRAME_LENGTH); + context.checking(new Expectations() {{ + // Write two full non-final frames + exactly(2).of(writer).writeFrame(with(any(byte[].class)), + with(MAX_PAYLOAD_LENGTH), with(false)); + // Write a final frame with a one-byte payload + oneOf(writer).writeFrame(with(any(byte[].class)), with(1), + with(true)); + // Flush the stream + oneOf(writer).flush(); + }}); + // Write two full payloads using one large multi-byte write + byte[] b = new byte[MAX_PAYLOAD_LENGTH * 2 + 1]; + c.write(b); + // There should be one byte left in the buffer + c.close(); + context.assertIsSatisfied(); + } +} diff --git a/briar-tests/src/net/sf/briar/transport/IncomingEncryptionLayerTest.java b/briar-tests/src/net/sf/briar/transport/IncomingEncryptionLayerTest.java new file mode 100644 index 000000000..7638bea2c --- /dev/null +++ b/briar-tests/src/net/sf/briar/transport/IncomingEncryptionLayerTest.java @@ -0,0 +1,183 @@ +package net.sf.briar.transport; + +import static javax.crypto.Cipher.ENCRYPT_MODE; +import static net.sf.briar.api.transport.TransportConstants.AAD_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.HEADER_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.IV_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.MAC_LENGTH; + +import java.io.ByteArrayInputStream; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.FormatException; +import net.sf.briar.api.crypto.AuthenticatedCipher; +import net.sf.briar.api.crypto.CryptoComponent; +import net.sf.briar.api.crypto.ErasableKey; +import net.sf.briar.crypto.CryptoModule; + +import org.junit.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +public class IncomingEncryptionLayerTest extends BriarTestCase { + + // FIXME: This is an integration test, not a unit test + + private static final int FRAME_LENGTH = 1024; + private static final int MAX_PAYLOAD_LENGTH = + FRAME_LENGTH - HEADER_LENGTH - MAC_LENGTH; + + private final CryptoComponent crypto; + private final AuthenticatedCipher frameCipher; + private final ErasableKey frameKey; + + public IncomingEncryptionLayerTest() { + super(); + Injector i = Guice.createInjector(new CryptoModule()); + crypto = i.getInstance(CryptoComponent.class); + frameCipher = crypto.getFrameCipher(); + frameKey = crypto.generateTestKey(); + } + + @Test + public void testReadValidFrames() throws Exception { + // Generate two valid frames + byte[] frame = generateFrame(0L, FRAME_LENGTH, 123, false, false); + byte[] frame1 = generateFrame(1L, FRAME_LENGTH, 123, false, false); + // Concatenate the frames + byte[] valid = new byte[FRAME_LENGTH * 2]; + System.arraycopy(frame, 0, valid, 0, FRAME_LENGTH); + System.arraycopy(frame1, 0, valid, FRAME_LENGTH, FRAME_LENGTH); + // Read the frames + ByteArrayInputStream in = new ByteArrayInputStream(valid); + IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher, + frameKey, FRAME_LENGTH); + byte[] buf = new byte[FRAME_LENGTH - MAC_LENGTH]; + assertEquals(123, i.readFrame(buf)); + assertEquals(123, i.readFrame(buf)); + } + + @Test + public void testTruncatedFrameThrowsException() throws Exception { + // Generate a valid frame + byte[] frame = generateFrame(0L, FRAME_LENGTH, 123, false, false); + // Chop off the last byte + byte[] truncated = new byte[FRAME_LENGTH - 1]; + System.arraycopy(frame, 0, truncated, 0, FRAME_LENGTH - 1); + // Try to read the frame, which should fail due to truncation + ByteArrayInputStream in = new ByteArrayInputStream(truncated); + IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher, + frameKey, FRAME_LENGTH); + try { + i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testModifiedFrameThrowsException() throws Exception { + // Generate a valid frame + byte[] frame = generateFrame(0L, FRAME_LENGTH, 123, false, false); + // Modify a randomly chosen byte of the frame + frame[(int) (Math.random() * FRAME_LENGTH)] ^= 1; + // Try to read the frame, which should fail due to modification + ByteArrayInputStream in = new ByteArrayInputStream(frame); + IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher, + frameKey, FRAME_LENGTH); + try { + i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testShortNonFinalFrameThrowsException() throws Exception { + // Generate a short non-final frame + byte[] frame = generateFrame(0L, FRAME_LENGTH - 1, 123, false, false); + // Try to read the frame, which should fail due to invalid length + ByteArrayInputStream in = new ByteArrayInputStream(frame); + IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher, + frameKey, FRAME_LENGTH); + try { + i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testShortFinalFrameDoesNotThrowException() throws Exception { + // Generate a short final frame + byte[] frame = generateFrame(0L, FRAME_LENGTH - 1, 123, true, false); + // Read the frame + ByteArrayInputStream in = new ByteArrayInputStream(frame); + IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher, + frameKey, FRAME_LENGTH); + int length = i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]); + assertEquals(123, length); + } + + @Test + public void testInvalidPayloadLengthThrowsException() throws Exception { + // Generate a frame with an invalid payload length + byte[] frame = generateFrame(0L, FRAME_LENGTH, MAX_PAYLOAD_LENGTH + 1, + false, false); + // Try to read the frame, which should fail due to invalid length + ByteArrayInputStream in = new ByteArrayInputStream(frame); + IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher, + frameKey, FRAME_LENGTH); + try { + i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testNonZeroPaddingThrowsException() throws Exception { + // Generate a frame with bad padding + byte[] frame = generateFrame(0L, FRAME_LENGTH, 123, false, true); + // Try to read the frame, which should fail due to bad padding + ByteArrayInputStream in = new ByteArrayInputStream(frame); + IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher, + frameKey, FRAME_LENGTH); + try { + i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testCannotReadBeyondFinalFrame() throws Exception { + // Generate a valid final frame and another valid final frame after it + byte[] frame = generateFrame(0L, FRAME_LENGTH, MAX_PAYLOAD_LENGTH, true, + false); + byte[] frame1 = generateFrame(1L, FRAME_LENGTH, 123, true, false); + // Concatenate the frames + byte[] extraFrame = new byte[FRAME_LENGTH * 2]; + System.arraycopy(frame, 0, extraFrame, 0, FRAME_LENGTH); + System.arraycopy(frame1, 0, extraFrame, FRAME_LENGTH, FRAME_LENGTH); + // Read the final frame, which should first read the tag + ByteArrayInputStream in = new ByteArrayInputStream(extraFrame); + IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher, + frameKey, FRAME_LENGTH); + byte[] buf = new byte[FRAME_LENGTH - MAC_LENGTH]; + assertEquals(MAX_PAYLOAD_LENGTH, i.readFrame(buf)); + // The frame after the final frame should not be read + assertEquals(-1, i.readFrame(buf)); + } + + private byte[] generateFrame(long frameNumber, int frameLength, + int payloadLength, boolean finalFrame, boolean badPadding) + throws Exception { + byte[] iv = new byte[IV_LENGTH], aad = new byte[AAD_LENGTH]; + byte[] plaintext = new byte[frameLength - MAC_LENGTH]; + byte[] ciphertext = new byte[frameLength]; + FrameEncoder.encodeIv(iv, frameNumber); + FrameEncoder.encodeAad(aad, frameNumber, plaintext.length); + frameCipher.init(ENCRYPT_MODE, frameKey, iv, aad); + FrameEncoder.encodeHeader(plaintext, finalFrame, payloadLength); + if(badPadding) plaintext[HEADER_LENGTH + payloadLength] = 1; + frameCipher.doFinal(plaintext, 0, plaintext.length, ciphertext, 0); + return ciphertext; + } +} diff --git a/briar-tests/src/net/sf/briar/transport/OutgoingEncryptionLayerTest.java b/briar-tests/src/net/sf/briar/transport/OutgoingEncryptionLayerTest.java new file mode 100644 index 000000000..475e5d9ad --- /dev/null +++ b/briar-tests/src/net/sf/briar/transport/OutgoingEncryptionLayerTest.java @@ -0,0 +1,159 @@ +package net.sf.briar.transport; + +import static javax.crypto.Cipher.ENCRYPT_MODE; +import static net.sf.briar.api.transport.TransportConstants.AAD_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.HEADER_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.IV_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.MAC_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH; + +import java.io.ByteArrayOutputStream; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.crypto.AuthenticatedCipher; +import net.sf.briar.api.crypto.CryptoComponent; +import net.sf.briar.api.crypto.ErasableKey; +import net.sf.briar.crypto.CryptoModule; + +import org.junit.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +public class OutgoingEncryptionLayerTest extends BriarTestCase { + + // FIXME: This is an integration test, not a unit test + + private static final int FRAME_LENGTH = 1024; + private static final int MAX_PAYLOAD_LENGTH = + FRAME_LENGTH - HEADER_LENGTH - MAC_LENGTH; + + private final CryptoComponent crypto; + private final AuthenticatedCipher frameCipher; + private final byte[] tag; + + public OutgoingEncryptionLayerTest() { + super(); + Injector i = Guice.createInjector(new CryptoModule()); + crypto = i.getInstance(CryptoComponent.class); + frameCipher = crypto.getFrameCipher(); + tag = new byte[TAG_LENGTH]; + } + + @Test + public void testEncryption() throws Exception { + int payloadLength = 123; + byte[] iv = new byte[IV_LENGTH], aad = new byte[AAD_LENGTH]; + byte[] plaintext = new byte[FRAME_LENGTH - MAC_LENGTH]; + byte[] ciphertext = new byte[FRAME_LENGTH]; + ErasableKey frameKey = crypto.generateTestKey(); + // Calculate the expected ciphertext + FrameEncoder.encodeIv(iv, 0); + FrameEncoder.encodeAad(aad, 0, plaintext.length); + frameCipher.init(ENCRYPT_MODE, frameKey, iv, aad); + FrameEncoder.encodeHeader(plaintext, false, payloadLength); + frameCipher.doFinal(plaintext, 0, plaintext.length, ciphertext, 0); + // Check that the actual tag and ciphertext match what's expected + ByteArrayOutputStream out = new ByteArrayOutputStream(); + OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out, + 10 * FRAME_LENGTH, frameCipher, frameKey, FRAME_LENGTH, tag); + o.writeFrame(new byte[FRAME_LENGTH - MAC_LENGTH], payloadLength, false); + byte[] actual = out.toByteArray(); + assertEquals(TAG_LENGTH + FRAME_LENGTH, actual.length); + for(int i = 0; i < TAG_LENGTH; i++) assertEquals(tag[i], actual[i]); + for(int i = 0; i < FRAME_LENGTH; i++) { + assertEquals("" + i, ciphertext[i], actual[TAG_LENGTH + i]); + } + } + + @Test + public void testInitiatorClosesConnectionWithoutWriting() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // Initiator's constructor + OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out, + 10 * FRAME_LENGTH, frameCipher, crypto.generateTestKey(), + FRAME_LENGTH, tag); + // Write an empty final frame without having written any other frames + o.writeFrame(new byte[FRAME_LENGTH - MAC_LENGTH], 0, true); + // Nothing should be written to the output stream + assertEquals(0, out.size()); + } + + @Test + public void testResponderClosesConnectionWithoutWriting() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // Responder's constructor + OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out, + 10 * FRAME_LENGTH, frameCipher, crypto.generateTestKey(), + FRAME_LENGTH); + // Write an empty final frame without having written any other frames + o.writeFrame(new byte[FRAME_LENGTH - MAC_LENGTH], 0, true); + // An empty final frame should be written to the output stream + assertEquals(HEADER_LENGTH + MAC_LENGTH, out.size()); + } + + @Test + public void testRemainingCapacityWithTag() throws Exception { + int MAX_PAYLOAD_LENGTH = FRAME_LENGTH - HEADER_LENGTH - MAC_LENGTH; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // Initiator's constructor + OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out, + 10 * FRAME_LENGTH, frameCipher, crypto.generateTestKey(), + FRAME_LENGTH, tag); + // There should be space for nine full frames and one partial frame + byte[] frame = new byte[FRAME_LENGTH - MAC_LENGTH]; + assertEquals(10 * MAX_PAYLOAD_LENGTH - TAG_LENGTH, + o.getRemainingCapacity()); + // Write nine frames, each containing a partial payload + for(int i = 0; i < 9; i++) { + o.writeFrame(frame, 123, false); + assertEquals((9 - i) * MAX_PAYLOAD_LENGTH - TAG_LENGTH, + o.getRemainingCapacity()); + } + // Write the final frame, which will not be padded + o.writeFrame(frame, 123, true); + int finalFrameLength = HEADER_LENGTH + 123 + MAC_LENGTH; + assertEquals(MAX_PAYLOAD_LENGTH - TAG_LENGTH - finalFrameLength, + o.getRemainingCapacity()); + } + + @Test + public void testRemainingCapacityWithoutTag() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // Responder's constructor + OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out, + 10 * FRAME_LENGTH, frameCipher, crypto.generateTestKey(), + FRAME_LENGTH); + // There should be space for ten full frames + assertEquals(10 * MAX_PAYLOAD_LENGTH, o.getRemainingCapacity()); + // Write nine frames, each containing a partial payload + byte[] frame = new byte[FRAME_LENGTH - MAC_LENGTH]; + for(int i = 0; i < 9; i++) { + o.writeFrame(frame, 123, false); + assertEquals((9 - i) * MAX_PAYLOAD_LENGTH, + o.getRemainingCapacity()); + } + // Write the final frame, which will not be padded + o.writeFrame(frame, 123, true); + int finalFrameLength = HEADER_LENGTH + 123 + MAC_LENGTH; + assertEquals(MAX_PAYLOAD_LENGTH - finalFrameLength, + o.getRemainingCapacity()); + } + + @Test + public void testRemainingCapacityLimitedByFrameNumbers() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // The connection has plenty of space so we're limited by frame numbers + OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out, + Long.MAX_VALUE, frameCipher, crypto.generateTestKey(), + FRAME_LENGTH); + // There should be enough frame numbers for 2^32 frames + assertEquals((1L << 32) * MAX_PAYLOAD_LENGTH, o.getRemainingCapacity()); + // Write a frame containing a partial payload + byte[] frame = new byte[FRAME_LENGTH - MAC_LENGTH]; + o.writeFrame(frame, 123, false); + // There should be enough frame numbers for 2^32 - 1 frames + assertEquals(((1L << 32) - 1) * MAX_PAYLOAD_LENGTH, + o.getRemainingCapacity()); + } +} diff --git a/briar-tests/src/net/sf/briar/transport/TransportConnectionRecogniserTest.java b/briar-tests/src/net/sf/briar/transport/TransportConnectionRecogniserTest.java new file mode 100644 index 000000000..9e8739719 --- /dev/null +++ b/briar-tests/src/net/sf/briar/transport/TransportConnectionRecogniserTest.java @@ -0,0 +1,141 @@ +package net.sf.briar.transport; + +import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH; +import static org.junit.Assert.assertArrayEquals; + +import java.util.Random; + +import javax.crypto.Cipher; +import javax.crypto.NullCipher; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.crypto.CryptoComponent; +import net.sf.briar.api.crypto.ErasableKey; +import net.sf.briar.api.db.DatabaseComponent; +import net.sf.briar.api.protocol.TransportId; +import net.sf.briar.api.transport.ConnectionContext; +import net.sf.briar.api.transport.TemporarySecret; +import net.sf.briar.util.ByteUtils; + +import org.hamcrest.Description; +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.jmock.api.Action; +import org.jmock.api.Invocation; +import org.junit.Test; + +public class TransportConnectionRecogniserTest extends BriarTestCase { + + private final ContactId contactId = new ContactId(234); + private final TransportId transportId = + new TransportId(TestUtils.getRandomId()); + + @Test + public void testAddAndRemoveSecret() { + Mockery context = new Mockery(); + final CryptoComponent crypto = context.mock(CryptoComponent.class); + final Cipher tagCipher = new NullCipher(); + final byte[] secret = new byte[32]; + new Random().nextBytes(secret); + final boolean alice = false; + final ErasableKey tagKey = context.mock(ErasableKey.class); + final DatabaseComponent db = context.mock(DatabaseComponent.class); + context.checking(new Expectations() {{ + // Add secret + oneOf(crypto).getTagCipher(); + will(returnValue(tagCipher)); + oneOf(crypto).deriveTagKey(secret, !alice); + will(returnValue(tagKey)); + exactly(16).of(crypto).encodeTag(with(any(byte[].class)), + with(tagCipher), with(tagKey), with(any(long.class))); + will(new EncodeTagAction()); + oneOf(tagKey).erase(); + // Remove secret + oneOf(crypto).getTagCipher(); + will(returnValue(tagCipher)); + oneOf(crypto).deriveTagKey(secret, !alice); + will(returnValue(tagKey)); + exactly(16).of(crypto).encodeTag(with(any(byte[].class)), + with(tagCipher), with(tagKey), with(any(long.class))); + will(new EncodeTagAction()); + oneOf(tagKey).erase(); + }}); + TemporarySecret s = new TemporarySecret(contactId, transportId, 0L, + 0L, 0L, alice, 0L, secret, 0L, 0L, new byte[4]); + TransportConnectionRecogniser recogniser = + new TransportConnectionRecogniser(crypto, db, transportId); + recogniser.addSecret(s); + recogniser.removeSecret(contactId, 0L); + // The secret should have been erased + assertArrayEquals(new byte[32], secret); + context.assertIsSatisfied(); + } + + @Test + public void testAcceptConnection() throws Exception { + Mockery context = new Mockery(); + final CryptoComponent crypto = context.mock(CryptoComponent.class); + final Cipher tagCipher = new NullCipher(); + final byte[] secret = new byte[32]; + new Random().nextBytes(secret); + final boolean alice = false; + final ErasableKey tagKey = context.mock(ErasableKey.class); + final DatabaseComponent db = context.mock(DatabaseComponent.class); + context.checking(new Expectations() {{ + // Add secret + oneOf(crypto).getTagCipher(); + will(returnValue(tagCipher)); + oneOf(crypto).deriveTagKey(secret, !alice); + will(returnValue(tagKey)); + exactly(16).of(crypto).encodeTag(with(any(byte[].class)), + with(tagCipher), with(tagKey), with(any(long.class))); + will(new EncodeTagAction()); + oneOf(tagKey).erase(); + // Accept connection + oneOf(crypto).getTagCipher(); + will(returnValue(tagCipher)); + oneOf(crypto).deriveTagKey(secret, !alice); + will(returnValue(tagKey)); + // The window should slide to include connection 16 + oneOf(crypto).encodeTag(with(any(byte[].class)), with(tagCipher), + with(tagKey), with(16L)); + will(new EncodeTagAction()); + // The updated window should be stored + oneOf(db).setConnectionWindow(contactId, transportId, 0L, 1L, + new byte[] {0, 1, 0, 0}); + oneOf(tagKey).erase(); + // Accept connection again - no expectations + }}); + TemporarySecret s = new TemporarySecret(contactId, transportId, 0L, + 0L, 0L, alice, 0L, secret, 0L, 0L, new byte[4]); + TransportConnectionRecogniser recogniser = + new TransportConnectionRecogniser(crypto, db, transportId); + recogniser.addSecret(s); + // Connection 0 should be expected + byte[] tag = new byte[TAG_LENGTH]; + ConnectionContext ctx = recogniser.acceptConnection(tag); + assertNotNull(ctx); + assertEquals(contactId, ctx.getContactId()); + assertEquals(transportId, ctx.getTransportId()); + assertArrayEquals(secret, ctx.getSecret()); + assertEquals(0L, ctx.getConnectionNumber()); + assertEquals(alice, ctx.getAlice()); + context.assertIsSatisfied(); + } + + private static class EncodeTagAction implements Action { + + public void describeTo(Description description) { + description.appendText("Encodes a tag"); + } + + public Object invoke(Invocation invocation) throws Throwable { + byte[] tag = (byte[]) invocation.getParameter(0); + long connection = (Long) invocation.getParameter(3); + ByteUtils.writeUint32(connection, tag, 0); + return null; + } + } +} diff --git a/briar-tests/src/net/sf/briar/transport/TransportIntegrationTest.java b/briar-tests/src/net/sf/briar/transport/TransportIntegrationTest.java new file mode 100644 index 000000000..daa99b5b0 --- /dev/null +++ b/briar-tests/src/net/sf/briar/transport/TransportIntegrationTest.java @@ -0,0 +1,173 @@ +package net.sf.briar.transport; + +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PACKET_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.MIN_CONNECTION_LENGTH; +import static org.junit.Assert.assertArrayEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Random; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.crypto.AuthenticatedCipher; +import net.sf.briar.api.crypto.CryptoComponent; +import net.sf.briar.api.crypto.ErasableKey; +import net.sf.briar.api.protocol.TransportId; +import net.sf.briar.api.transport.ConnectionContext; +import net.sf.briar.api.transport.ConnectionReader; +import net.sf.briar.api.transport.ConnectionWriter; +import net.sf.briar.api.transport.ConnectionWriterFactory; +import net.sf.briar.crypto.CryptoModule; +import net.sf.briar.transport.ConnectionReaderImpl; +import net.sf.briar.transport.ConnectionWriterFactoryImpl; +import net.sf.briar.transport.ConnectionWriterImpl; +import net.sf.briar.transport.IncomingEncryptionLayer; +import net.sf.briar.transport.OutgoingEncryptionLayer; + +import org.junit.Test; + + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; + +public class TransportIntegrationTest extends BriarTestCase { + + private final int FRAME_LENGTH = 2048; + + private final CryptoComponent crypto; + private final ConnectionWriterFactory connectionWriterFactory; + private final ContactId contactId; + private final TransportId transportId; + private final AuthenticatedCipher frameCipher; + private final Random random; + private final byte[] secret; + private final ErasableKey frameKey; + + public TransportIntegrationTest() { + super(); + Module testModule = new AbstractModule() { + @Override + public void configure() { + bind(ConnectionWriterFactory.class).to( + ConnectionWriterFactoryImpl.class); + } + }; + Injector i = Guice.createInjector(testModule, new CryptoModule()); + crypto = i.getInstance(CryptoComponent.class); + connectionWriterFactory = i.getInstance(ConnectionWriterFactory.class); + contactId = new ContactId(234); + transportId = new TransportId(TestUtils.getRandomId()); + frameCipher = crypto.getFrameCipher(); + random = new Random(); + // Since we're sending frames to ourselves, we only need outgoing keys + secret = new byte[32]; + random.nextBytes(secret); + frameKey = crypto.deriveFrameKey(secret, 0L, true, true); + } + + @Test + public void testInitiatorWriteAndRead() throws Exception { + testWriteAndRead(true); + } + + @Test + public void testResponderWriteAndRead() throws Exception { + testWriteAndRead(false); + } + + private void testWriteAndRead(boolean initiator) throws Exception { + // Generate two random frames + byte[] frame = new byte[1234]; + random.nextBytes(frame); + byte[] frame1 = new byte[321]; + random.nextBytes(frame1); + // Copy the frame key - the copy will be erased + ErasableKey frameCopy = frameKey.copy(); + // Write the frames + ByteArrayOutputStream out = new ByteArrayOutputStream(); + FrameWriter encryptionOut = new OutgoingEncryptionLayer(out, + Long.MAX_VALUE, frameCipher, frameCopy, FRAME_LENGTH); + ConnectionWriter writer = new ConnectionWriterImpl(encryptionOut, + FRAME_LENGTH); + OutputStream out1 = writer.getOutputStream(); + out1.write(frame); + out1.flush(); + out1.write(frame1); + out1.flush(); + byte[] output = out.toByteArray(); + assertEquals(FRAME_LENGTH * 2, output.length); + // Read the tag and the frames back + ByteArrayInputStream in = new ByteArrayInputStream(output); + FrameReader encryptionIn = new IncomingEncryptionLayer(in, frameCipher, + frameKey, FRAME_LENGTH); + ConnectionReader reader = new ConnectionReaderImpl(encryptionIn, + FRAME_LENGTH); + InputStream in1 = reader.getInputStream(); + byte[] recovered = new byte[frame.length]; + int offset = 0; + while(offset < recovered.length) { + int read = in1.read(recovered, offset, recovered.length - offset); + if(read == -1) break; + offset += read; + } + assertEquals(recovered.length, offset); + assertArrayEquals(frame, recovered); + byte[] recovered1 = new byte[frame1.length]; + offset = 0; + while(offset < recovered1.length) { + int read = in1.read(recovered1, offset, recovered1.length - offset); + if(read == -1) break; + offset += read; + } + assertEquals(recovered1.length, offset); + assertArrayEquals(frame1, recovered1); + } + + @Test + public void testOverheadWithTag() throws Exception { + ByteArrayOutputStream out = + new ByteArrayOutputStream(MIN_CONNECTION_LENGTH); + ConnectionContext ctx = new ConnectionContext(contactId, transportId, + secret, 0L, true); + ConnectionWriter w = connectionWriterFactory.createConnectionWriter(out, + MIN_CONNECTION_LENGTH, ctx, false, true); + // Check that the connection writer thinks there's room for a packet + long capacity = w.getRemainingCapacity(); + assertTrue(capacity > MAX_PACKET_LENGTH); + assertTrue(capacity < MIN_CONNECTION_LENGTH); + // Check that there really is room for a packet + byte[] payload = new byte[MAX_PACKET_LENGTH]; + w.getOutputStream().write(payload); + w.getOutputStream().close(); + long used = out.size(); + assertTrue(used > MAX_PACKET_LENGTH); + assertTrue(used <= MIN_CONNECTION_LENGTH); + } + + @Test + public void testOverheadWithoutTag() throws Exception { + ByteArrayOutputStream out = + new ByteArrayOutputStream(MIN_CONNECTION_LENGTH); + ConnectionContext ctx = new ConnectionContext(contactId, transportId, + secret, 0L, true); + ConnectionWriter w = connectionWriterFactory.createConnectionWriter(out, + MIN_CONNECTION_LENGTH, ctx, false, false); + // Check that the connection writer thinks there's room for a packet + long capacity = w.getRemainingCapacity(); + assertTrue(capacity > MAX_PACKET_LENGTH); + assertTrue(capacity < MIN_CONNECTION_LENGTH); + // Check that there really is room for a packet + byte[] payload = new byte[MAX_PACKET_LENGTH]; + w.getOutputStream().write(payload); + w.getOutputStream().close(); + long used = out.size(); + assertTrue(used > MAX_PACKET_LENGTH); + assertTrue(used <= MIN_CONNECTION_LENGTH); + } +} diff --git a/briar-tests/src/net/sf/briar/util/ByteUtilsTest.java b/briar-tests/src/net/sf/briar/util/ByteUtilsTest.java new file mode 100644 index 000000000..c672bc0e2 --- /dev/null +++ b/briar-tests/src/net/sf/briar/util/ByteUtilsTest.java @@ -0,0 +1,66 @@ +package net.sf.briar.util; + +import net.sf.briar.BriarTestCase; + +import org.junit.Test; + +public class ByteUtilsTest extends BriarTestCase { + + @Test + public void testReadUint16() { + byte[] b = StringUtils.fromHexString("000000"); + assertEquals(0, ByteUtils.readUint16(b, 1)); + b = StringUtils.fromHexString("000001"); + assertEquals(1, ByteUtils.readUint16(b, 1)); + b = StringUtils.fromHexString("00FFFF"); + assertEquals(65535, ByteUtils.readUint16(b, 1)); + } + + @Test + public void testReadUint32() { + byte[] b = StringUtils.fromHexString("0000000000"); + assertEquals(0L, ByteUtils.readUint32(b, 1)); + b = StringUtils.fromHexString("0000000001"); + assertEquals(1L, ByteUtils.readUint32(b, 1)); + b = StringUtils.fromHexString("00FFFFFFFF"); + assertEquals(4294967295L, ByteUtils.readUint32(b, 1)); + } + + + @Test + public void testWriteUint16() { + byte[] b = new byte[3]; + ByteUtils.writeUint16(0, b, 1); + assertEquals("000000", StringUtils.toHexString(b)); + ByteUtils.writeUint16(1, b, 1); + assertEquals("000001", StringUtils.toHexString(b)); + ByteUtils.writeUint16(65535, b, 1); + assertEquals("00FFFF", StringUtils.toHexString(b)); + } + + @Test + public void testWriteUint32() { + byte[] b = new byte[5]; + ByteUtils.writeUint32(0L, b, 1); + assertEquals("0000000000", StringUtils.toHexString(b)); + ByteUtils.writeUint32(1L, b, 1); + assertEquals("0000000001", StringUtils.toHexString(b)); + ByteUtils.writeUint32(4294967295L, b, 1); + assertEquals("00FFFFFFFF", StringUtils.toHexString(b)); + } + + @Test + public void testReadUint() { + byte[] b = new byte[1]; + b[0] = (byte) 128; + for(int i = 0; i < 8; i++) { + assertEquals(1 << i, ByteUtils.readUint(b, i + 1)); + } + b = new byte[2]; + for(int i = 0; i < 65535; i++) { + ByteUtils.writeUint16(i, b, 0); + assertEquals(i, ByteUtils.readUint(b, 16)); + assertEquals(i >> 1, ByteUtils.readUint(b, 15)); + } + } +} diff --git a/briar-tests/src/net/sf/briar/util/FileUtilsTest.java b/briar-tests/src/net/sf/briar/util/FileUtilsTest.java new file mode 100644 index 000000000..251d78a6e --- /dev/null +++ b/briar-tests/src/net/sf/briar/util/FileUtilsTest.java @@ -0,0 +1,165 @@ +package net.sf.briar.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Scanner; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.util.FileUtils.Callback; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class FileUtilsTest extends BriarTestCase { + + private final File testDir = TestUtils.getTestDirectory(); + + @Before + public void setUp() { + testDir.mkdirs(); + } + + @Test + public void testCreateTempFile() throws IOException { + File temp = FileUtils.createTempFile(); + assertTrue(temp.exists()); + assertTrue(temp.isFile()); + assertEquals(0L, temp.length()); + temp.delete(); + } + + @Test + public void testCopy() throws IOException { + File src = new File(testDir, "src"); + File dest = new File(testDir, "dest"); + TestUtils.createFile(src, "Foo bar\r\nBar foo\r\n"); + long length = src.length(); + + FileUtils.copy(src, dest); + + assertEquals(length, dest.length()); + Scanner in = new Scanner(dest); + assertTrue(in.hasNextLine()); + assertEquals("Foo bar", in.nextLine()); + assertTrue(in.hasNextLine()); + assertEquals("Bar foo", in.nextLine()); + assertFalse(in.hasNext()); + in.close(); + } + + @Test + public void testCopyFromStream() throws IOException { + File src = new File(testDir, "src"); + File dest = new File(testDir, "dest"); + TestUtils.createFile(src, "Foo bar\r\nBar foo\r\n"); + long length = src.length(); + InputStream is = new FileInputStream(src); + is.skip(4); + + FileUtils.copy(is, dest); + + assertEquals(length - 4, dest.length()); + Scanner in = new Scanner(dest); + assertTrue(in.hasNextLine()); + assertEquals("bar", in.nextLine()); + assertTrue(in.hasNextLine()); + assertEquals("Bar foo", in.nextLine()); + assertFalse(in.hasNext()); + in.close(); + } + + @Test + public void testCopyRecursively() throws IOException { + final File dest1 = new File(testDir, "dest/abc/def/1"); + final File dest2 = new File(testDir, "dest/abc/def/2"); + final File dest3 = new File(testDir, "dest/abc/3"); + Mockery context = new Mockery(); + final Callback callback = context.mock(Callback.class); + context.checking(new Expectations() {{ + oneOf(callback).processingFile(dest1); + oneOf(callback).processingFile(dest2); + oneOf(callback).processingFile(dest3); + }}); + + copyRecursively(callback); + + context.assertIsSatisfied(); + } + + @Test + public void testCopyRecursivelyNoCallback() throws IOException { + copyRecursively(null); + } + + private void copyRecursively(Callback callback) throws IOException { + TestUtils.createFile(new File(testDir, "abc/def/1"), "one one one"); + TestUtils.createFile(new File(testDir, "abc/def/2"), "two two two"); + TestUtils.createFile(new File(testDir, "abc/3"), "three three three"); + + File dest = new File(testDir, "dest"); + dest.mkdir(); + + FileUtils.copyRecursively(new File(testDir, "abc"), dest, callback); + + File dest1 = new File(testDir, "dest/abc/def/1"); + assertTrue(dest1.exists()); + assertTrue(dest1.isFile()); + assertEquals("one one one".length(), dest1.length()); + File dest2 = new File(testDir, "dest/abc/def/2"); + assertTrue(dest2.exists()); + assertTrue(dest2.isFile()); + assertEquals("two two two".length(), dest2.length()); + File dest3 = new File(testDir, "dest/abc/3"); + assertTrue(dest3.exists()); + assertTrue(dest3.isFile()); + assertEquals("three three three".length(), dest3.length()); + } + + @Test + public void testDeleteFile() throws IOException { + File foo = new File(testDir, "foo"); + foo.createNewFile(); + assertTrue(foo.exists()); + + FileUtils.delete(foo); + + assertFalse(foo.exists()); + } + + @Test + public void testDeleteDirectory() throws IOException { + File f1 = new File(testDir, "abc/def/1"); + File f2 = new File(testDir, "abc/def/2"); + File f3 = new File(testDir, "abc/3"); + File abc = new File(testDir, "abc"); + File def = new File(testDir, "abc/def"); + TestUtils.createFile(f1, "one one one"); + TestUtils.createFile(f2, "two two two"); + TestUtils.createFile(f3, "three three three"); + + assertTrue(f1.exists()); + assertTrue(f2.exists()); + assertTrue(f3.exists()); + assertTrue(abc.exists()); + assertTrue(def.exists()); + + FileUtils.delete(def); + + assertFalse(f1.exists()); + assertFalse(f2.exists()); + assertTrue(f3.exists()); + assertTrue(abc.exists()); + assertFalse(def.exists()); + } + + @After + public void tearDown() { + TestUtils.deleteTestDirectory(testDir); + } +} diff --git a/briar-tests/src/net/sf/briar/util/StringUtilsTest.java b/briar-tests/src/net/sf/briar/util/StringUtilsTest.java new file mode 100644 index 000000000..d4465e062 --- /dev/null +++ b/briar-tests/src/net/sf/briar/util/StringUtilsTest.java @@ -0,0 +1,44 @@ +package net.sf.briar.util; + +import static org.junit.Assert.assertArrayEquals; +import net.sf.briar.BriarTestCase; + +import org.junit.Test; + +public class StringUtilsTest extends BriarTestCase { + + @Test + public void testHead() { + String head = StringUtils.head("123456789", 5); + assertEquals("12345...", head); + } + + @Test + public void testTail() { + String tail = StringUtils.tail("987654321", 5); + assertEquals("...54321", tail); + } + + @Test + public void testToHexString() { + byte[] b = new byte[] {1, 2, 3, 127, -128}; + String s = StringUtils.toHexString(b); + assertEquals("0102037F80", s); + } + + @Test + public void testFromHexString() { + try { + StringUtils.fromHexString("12345"); + fail(); + } catch(IllegalArgumentException expected) {} + try { + StringUtils.fromHexString("ABCDEFGH"); + fail(); + } catch(IllegalArgumentException expected) {} + byte[] b = StringUtils.fromHexString("0102037F80"); + assertArrayEquals(new byte[] {1, 2, 3, 127, -128}, b); + b = StringUtils.fromHexString("0a0b0c0d0e0f"); + assertArrayEquals(new byte[] {10, 11, 12, 13, 14, 15}, b); + } +} diff --git a/briar-tests/src/net/sf/briar/util/ZipUtilsTest.java b/briar-tests/src/net/sf/briar/util/ZipUtilsTest.java new file mode 100644 index 000000000..c08d4a370 --- /dev/null +++ b/briar-tests/src/net/sf/briar/util/ZipUtilsTest.java @@ -0,0 +1,202 @@ +package net.sf.briar.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.util.ZipUtils.Callback; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class ZipUtilsTest extends BriarTestCase { + + private final File testDir = TestUtils.getTestDirectory(); + + private final File f1 = new File(testDir, "abc/def/1"); + private final File f2 = new File(testDir, "abc/def/2"); + private final File f3 = new File(testDir, "abc/3"); + + @Before + public void setUp() { + testDir.mkdirs(); + } + + @Test + public void testCopyToZip() throws IOException { + File src = new File(testDir, "src"); + File dest = new File(testDir, "dest"); + TestUtils.createFile(src, "foo bar baz"); + ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(dest)); + + ZipUtils.copyToZip("abc/def", src, zip); + zip.flush(); + zip.close(); + + Map expected = Collections.singletonMap("abc/def", + "foo bar baz"); + checkZipEntries(dest, expected); + } + + private void checkZipEntries(File f, Map expected) + throws IOException { + Map found = new HashMap(); + assertTrue(f.exists()); + assertTrue(f.isFile()); + ZipInputStream unzip = new ZipInputStream(new FileInputStream(f)); + ZipEntry entry; + while((entry = unzip.getNextEntry()) != null) { + String name = entry.getName(); + Scanner s = new Scanner(unzip); + assertTrue(s.hasNextLine()); + String contents = s.nextLine(); + assertFalse(s.hasNextLine()); + unzip.closeEntry(); + found.put(name, contents); + } + unzip.close(); + assertEquals(expected.size(), found.size()); + for(String name : expected.keySet()) { + String contents = found.get(name); + assertNotNull(contents); + assertEquals(expected.get(name), contents); + } + } + + @Test + public void testCopyToZipRecursively() throws IOException { + Mockery context = new Mockery(); + final Callback callback = context.mock(Callback.class); + context.checking(new Expectations() {{ + oneOf(callback).processingFile(f1); + oneOf(callback).processingFile(f2); + oneOf(callback).processingFile(f3); + }}); + + copyRecursively(callback); + + context.assertIsSatisfied(); + } + + @Test + public void testCopyToZipRecursivelyNoCallback() throws IOException { + copyRecursively(null); + } + + private void copyRecursively(Callback callback) throws IOException { + TestUtils.createFile(f1, "one one one"); + TestUtils.createFile(f2, "two two two"); + TestUtils.createFile(f3, "three three three"); + File src = new File(testDir, "abc"); + File dest = new File(testDir, "dest"); + ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(dest)); + + ZipUtils.copyToZipRecursively("ghi", src, zip, callback); + zip.flush(); + zip.close(); + + Map expected = new HashMap(); + expected.put("ghi/def/1", "one one one"); + expected.put("ghi/def/2", "two two two"); + expected.put("ghi/3", "three three three"); + checkZipEntries(dest, expected); + } + + @Test + public void testUnzipStream() throws IOException { + Mockery context = new Mockery(); + final Callback callback = context.mock(Callback.class); + context.checking(new Expectations() {{ + oneOf(callback).processingFile(f1); + oneOf(callback).processingFile(f2); + oneOf(callback).processingFile(f3); + }}); + + unzipStream(null, callback); + + context.assertIsSatisfied(); + + assertTrue(f1.exists()); + assertTrue(f1.isFile()); + assertEquals("one one one".length(), f1.length()); + assertTrue(f2.exists()); + assertTrue(f2.isFile()); + assertEquals("two two two".length(), f2.length()); + assertTrue(f3.exists()); + assertTrue(f3.isFile()); + assertEquals("three three three".length(), f3.length()); + } + + @Test + public void testUnzipStreamWithRegex() throws IOException { + Mockery context = new Mockery(); + final Callback callback = context.mock(Callback.class); + context.checking(new Expectations() {{ + oneOf(callback).processingFile(f1); + oneOf(callback).processingFile(f2); + }}); + + unzipStream("^abc/def/.*", callback); + + context.assertIsSatisfied(); + + assertTrue(f1.exists()); + assertTrue(f1.isFile()); + assertEquals("one one one".length(), f1.length()); + assertTrue(f2.exists()); + assertTrue(f2.isFile()); + assertEquals("two two two".length(), f2.length()); + assertFalse(f3.exists()); + } + + @Test + public void testUnzipStreamNoCallback() throws IOException { + unzipStream(null, null); + + assertTrue(f1.exists()); + assertTrue(f1.isFile()); + assertEquals("one one one".length(), f1.length()); + assertTrue(f2.exists()); + assertTrue(f2.isFile()); + assertEquals("two two two".length(), f2.length()); + assertTrue(f3.exists()); + assertTrue(f3.isFile()); + assertEquals("three three three".length(), f3.length()); + } + + private void unzipStream(String regex, Callback callback) + throws IOException { + TestUtils.createFile(f1, "one one one"); + TestUtils.createFile(f2, "two two two"); + TestUtils.createFile(f3, "three three three"); + File src = new File(testDir, "abc"); + File dest = new File(testDir, "dest"); + ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(dest)); + ZipUtils.copyToZipRecursively(src.getName(), src, zip, null); + zip.flush(); + zip.close(); + TestUtils.delete(src); + + InputStream in = new FileInputStream(dest); + ZipUtils.unzipStream(in, testDir, regex, callback); + } + + @After + public void tearDown() { + TestUtils.deleteTestDirectory(testDir); + } +}