diff --git a/build.gradle b/build.gradle index 20ee360e81f..a2183633cdb 100644 --- a/build.gradle +++ b/build.gradle @@ -193,7 +193,7 @@ task copyTestResourcesToClasses2 { into layout.buildDirectory.dir("classes/java/test/") include 'freenet/client/filter/*/**' include 'freenet/crypt/ciphers/rijndael-gladman-test-data/**' - include 'freenet/l10n/*properties' + include 'freenet/**/*.properties' include 'freenet/clients/http/templates/**' } } @@ -263,10 +263,13 @@ dependencies { implementation "org.unbescape:unbescape:1.1.6.RELEASE" implementation "org.slf4j:slf4j-api:1.7.25" - testImplementation 'junit:junit:4.13.2' - testImplementation "org.mockito:mockito-core:1.9.5" + testImplementation('junit:junit:4.13.2') { + exclude(group: "org.hamcrest", module: "hamcrest-core") + } + testImplementation "org.mockito:mockito-inline:2.28.2" testImplementation "org.hamcrest:hamcrest:3.0" - testImplementation "org.objenesis:objenesis:1.0" + testImplementation "org.jsoup:jsoup:1.21.2" + testImplementation "jakarta.activation:jakarta.activation-api:2.1.4" } task tar(type: Tar) { diff --git a/gradle/verification-keyring.gpg b/gradle/verification-keyring.gpg index d48a88918b7..5075aa5ca6a 100644 Binary files a/gradle/verification-keyring.gpg and b/gradle/verification-keyring.gpg differ diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys index 7cb57aff144..5374aaa0663 100644 --- a/gradle/verification-keyring.keys +++ b/gradle/verification-keyring.keys @@ -1,7 +1,169 @@ +pub 80C08B1C29100955 +uid Jake Wharton + +sub CF771F914C2A4A73 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGiBE2fCWARBAC3v9wYo5kmynmVP+43ccamidflSLQjjpsXpSDLPFokGxeuw0OC +QJy46m8b5ACoCqRlfwnRRcEHxiSlaBATJA6hi7NRO41R39C62JXsIxNJR16JNQ5k +oG/NOAraw0E1RQIFslznQexfxPg4yFIVrsFp1wkpCRrCklatPMNap2DuNwCg7PWJ +1vV93YIsaH0O2fnXz3E+6zsD/3cTzUWuySEDiCLNO3JYJm97v4NDQ93encP1Ooxh +n+PSIP4GvjrAObh3FfWUucv8UGqcw5bAL7dA1z8SgKeyFk/afs2XofXdvC+PhZqC +DwU0NiE0D/tDWqX0qIG4ezTU2uk+5dE/WVl3R10nOBgquQdWIdYKGfV4FNTiEduD +Uw7fA/0XcwFom7eyR9eBonQmgIadljztm4gkv11lY33V1ZfJNndPKNzwevDwX+om +/VEHvpEfPx5toD4H523BPx55ZtfowuMtFHZI718alpCo3h6xaDhGwXvsg3s9k03k +rfxzCjf9qcJX1gb2JVZ2+2jCwUDQZeEwV2vivjGNiN9rShWW+7QkSmFrZSBXaGFy +dG9uIDxqYWtld2hhcnRvbkBnbWFpbC5jb20+uQINBE2fCWAQCAC3lOHYBShJ/G9x +NS1E9ubL71FF9pLlS0OU86JAus41kKz3oTpbGMfnsvEUjf+gOawS7Z9c++a6Kxd/ +rzeZCzwM/Mdk4egsXepb59w0B37wy9GB5adAGc4R8eaIMicXWqsMzctCVsWr2hRj +em/G260dTX9PJKv3eXTXvjRlXQV3LqlKOo7dVvrZG57ZJUuXvgff0P+C/HWJJfMV +0SDALisOofI+CxV7HusZ3VnlrUyJH2SJs9H+3C0FeV+0Er/dItSn80/YS4lI6cKj +8nbUzbgevoHWuuIxTYVXL4FvoezB+hU/80rLNiYdng0eBxa7+xCUkvHSgrz6McvB +JfKUDBv/AAMFCACF6vq8sOOL6B/yHW700LSuLef5afzo/W+0KzEd7v4kMTOCt3au +61GwPIY3TDN4HzLtdweKcxraXH9uW+DSCEJIREvfglA0A2BxfoMJK8BGrHG3X2GY +iwB7XDI1tToPiIU2PMA7iU+CNaarpks1lfFMVSXLajVE2NnlO9efKA+fqa3QnHkC +288aChbbtOepGdvuOr+mwJfIP8PFqFTkVLh67rryqITFTx8DUMLOyhwhcvWAq+Mi +EH6gvIPKCE6pYFGXGWl8eivjZ3cAOiwKFyS9HnWmTmQbhauR+xZK8NO/jiEBai0x +nBaUddgMw4fZZrciUPJ1X2+iIwDksAT5RGVkiEkEGBECAAkFAk2fCWACGwwACgkQ +gMCLHCkQCVXefACdGAPMb9FOTnZvLttQb8sYxkt7QWQAoIBej+io4VD6SAGE0ur9 +07c4ZCXa +=7AnO +-----END PGP PUBLIC KEY BLOCK----- + +pub 85911F425EC61B51 +uid Marc Philipp + +sub 8B2A34A7D4A9B8B3 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +PXCe8La72ukmaxEGbpJ8mdzS2PJko7mm +=Xe8l +-----END PGP PUBLIC KEY BLOCK----- + +pub 86E02C5A42196CA8 +uid Christian Grobmeier (Apache Codesigning) + +sub B4038E8B427473DA +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGiBEoAFh0RBACoIMutlZW5Isr0+btD/zsuxnCw5Ee2J+DANmA6Gvf4wiXPYw8X +evehIDiitZFCB/imcxF+1Qr+30c/+2V/j8TrDAqrj4UvQ8ryyejfR8aPT31C4lYF ++ktWT2PZA3ZOL03q8MtsLZiXdgRE5RwpUAGg2iAKlq4kx0RtDKJM3DHkQwCg9a7s +erb8gudgyDo7vjmATzu9d5cD/imS3/8CPWHnIfQMQNFkniShm0XoVCuZfQASf4I7 +ksRX+RfERc/RQ/vWoKW95DGupuBKm0KZYWjHgkd4NQcTvjtgVBpSksGuTg2dtY2R +96azS8Va0igUaUthZFCCD6TCHnOAsBCt47Fdv2ymlQiUZYXR1CPTkew/fXkBJJNw +mtH2A/4mT16lRfma4n5uBpjNMRD4juW4hI7/7/QS7xol4Zb74i9rcYldWY3U6AGt +lGUHeZ1ue9JZ+2SicJ25v+XAehbVsr+9NiiBqfOwGXij/d1UQTNsrsfvwYOC0foU +zw0xQFW5lYqJ7romLcly42jSC/lDkACDrL6MfeT5LWPId/cMKrQ/Q2hyaXN0aWFu +IEdyb2JtZWllciAoQXBhY2hlIENvZGVzaWduaW5nKSA8Z3JvYm1laWVyQGFwYWNo +ZS5vcmc+uQINBEoAFh0QCADPnNYl1TZ5usGUhYd3L9mfgkP+gQTKQpdoR6B256Wk +/D3gAgiHphduoOykZdPGarR7pdNYXlmpJyF9z4im9y3GtYoqzjg3k81IuiKC3XJo +5pTBskysqMfkr8/SvYTw3WkjnccxOy7/ZHTR95Kk4QE7v7ADuKM49vG6qqbrIjK5 +jQ8Xb9vuQjLr2l2dlOcLnQB6+hyhPzGZpnt2OYMCmLFZyDXatJj3Rrh/Rmoz9jVy +N2cVKk2Ho/+JtjzbfwjjJaBBUlAsZPQBnk+raAn4IeNqSSCrOokq4h5IFi9BQdrk +6sV1PPQ00zxoRCUMA1YDmFmNSriqkpvrdsL3kVEUZct/AAMFCADNSCoj84hdKkXX +O49G4Z2p0wX9mNfaQbAUZx3Y74ph4BXAZsjgikpbml0MeZnrWt7H4P5NBAzRPQ2C +hxT8QhYfNRayN102jNdnNkT3X3pfFrF/SDQq9lvqhjfYYz1vPY37tpdmW/IpOuER +8b4EBw5V7HWeIIlcDpHYZvOFv3yuJr1MyHwN1f50FAoJc9ZNxee1cKgAZkR1ZE66 +iTpjiTqdNmKZG8S6c1v6FLAB4ogFQmQ5WB7g2iQTDLj4k16suBFB9f3eSgtChg66 +dnOufYQgUCOTReT5+FfYU/x1E5YO+XRYR/HBP9SImvO9E8E/+HRGfqf2IzHTF5mD +fGsA0rk0iEkEGBECAAkFAkoAFh0CGwwACgkQhuAsWkIZbKjrlgCghD5yrAugimWH +OYDtyKs53wcqVgUAoL1bX0lIWF44l1wm2/d0FeLEU0QxiEkEGBECAAkFAkoAFh0C +GwwACgkQhuAsWkIZbKjrlgCg1B586HzsSXw7vgHwRNaNxBg/VAwAoJwP7QFH9Zf2 +XI00GEUinLYxPKZ0 +=Oa5t +-----END PGP PUBLIC KEY BLOCK----- + +pub A1B4460D8BA7B9AF +uid Mockito (http://mockito.org) + +sub BA6D22590B3F9BEA +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBE4waOEBCADHDHNTq1NRR5TSooIrKY0BTQnaLfjKZfcJOwp+btBJrOUO7+e/ +V3M4DZQclj/e8SBiVmRPK8Oyrv6i5B5+Ee/qNlLjWiO10AJ/PLRjYdoW1V6PlTm7 +2aUxQ/wNXxDcOoH8M7Y4fEqZJ3rFuta8ogeIYnkUwUk3gc5YKk7KJDbiOXQfRxlY +9MeVKyoYfj+2Szaaz98W097Pqf9+7i3WjB3TKR1pnWlzF6rLWIGBJqcJuTGLWUAB +hwOMoNiIBAqyGu8E4rT8pP9OueprOeIjyKgOBWV5yctIxd7uu51sGR47TM2wmMmm +r/Kz8FkMr1efof7Bx4mJuup6wQXonRMVBqpLABEBAAG0M01vY2tpdG8gKGh0dHA6 +Ly9tb2NraXRvLm9yZykgPG1vY2tpdG8ucGxAZ21haWwuY29tPrkBDQROMGjhAQgA +xDjlr6Oakgs+5d5NcIYpJ/8S5BIUaVlmjKoaFJoAy46WzvvpaTC022js1ZSvrM8w +lSxSk+kD6/FAJw2pJwRgVrolOYJQmqLqgTA2QSN+leWo0x8gC1+QXzrquCOd7m/+ +h8FHUDaxc29XQ+7+HtNvsSxY9dtDzgQGXjPXSppVCSiDNy1IuIAxmEAFSxok12Gl +xq/n2DFx6OnVB+5vvt0C8nQ6w0hQAQKPUmO7fNav5lZMfKgR95NrvF1Hu5V6DZvp +bXg6+NWfA8LsCampARrou5qzy67hb/+KKGHTDMOvZZlGw6oSnPngqmh06QqY7P4P +KVkOo3Vb6+02ltyZ95RgmQARAQABiQEfBBgBAgAJBQJOMGjhAhsMAAoJEKG0Rg2L +p7mv2b0IAI5pDzLtBGxYhmO1rSDgY+JY7ZAr4srW/7K6Zc0RmWv48JsZOf7T78kI +rif6jrF+ZwBLrdP93umMr9aJ/mJglr+0oag0ZVhEW6Jb7dgiRn8F9+qcCs6lyozA +G805oW2lULkwqSJ1xNs79v9RtOWlI5ruLVbS7XhGv0qH+ly1xpIuYOzNvpOgj1pC +3Pv+NuL2hrLUZVg1vM9eCM3TDVbDyJZrxKdMemMSGHHsXWHtn8t4BT88kice4weD +q8GBl1Dx+ApO1+iHj779nMX1DrfaxtJLTRqH9fhHesISmte8fPiydGT/HCA9quKU +uxyN4W6Uwu+Uov12cGV2sAI+H5qPhF4= +=92yh +-----END PGP PUBLIC KEY BLOCK----- + +pub A6ADFC93EF34893E +sub 9C4C23E6FFE405BD +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBE+xZxIBCACzKctn4ez8xOC0pGThhAwjYWGkzcwK4HNaC1usHThBFz3/t8JN +OqUXRixLyi5wELN6GHlsGVUQS3IfB4JtuhScsieSB8PTree68/knMq6JI08mJqZr +9nFrAB4eDW0UMbSL9kPmclUm/yN+qcCZBrsVn0q6CWb/Kcd8EEXEu6sGILzOGqGe +d433t5O+tGXWL2TjAz+Scsk2Hf4zcuDeQcxELAMnVaVgKuGuEZvibrjsdIvJDGI+ +0BzWIu8ZP8ldBl4SVtzGpEVzLvDUo3mOqBeTkj3rP7xLtFDN/3AFtowbLfL7L2Pg +SMcTnKK+jfFHRfbHP1Ih3rQ4ilLzhCnY/QIZABEBAAG5AQ0ET7FnEgEIAM3i3e1s +jwrx2PN8XYMPQWG+/YTtw1BYDl2+iYE+LaZvtq1hpbgeCLgEVwXrCJ4spLP1rFXo +gWqKrkJ0LRjlpdKhKBvyH1ex4grh3cWN/bIDJcJ7JA4I/Bhqhlh8hYycS9pGFeS+ +MR3aFIsii+vadrwYYvuVYGeWvdZhB7mJKYevj5Ms0OpYTfZd95Pzo4o//lNpDnrG +7Xd3tgTNU/fkpw6rFB/2Ib1Qlk+Kz1z6JNsp+tOPGGCBrzwfwglcikTuqS+xyRgC +9cHh5eCol11uSoWPKcQR2Ar8Eo56nxv/UApdu15iJ7R8cA5guKeeS4jt0CGCPs2P +huggDxI73Xvl4zsAEQEAAYkBHwQYAQIACQUCT7FnEgIbDAAKCRCmrfyT7zSJPuyl +B/9iwtIQeexMWBmQNdDe0md8HLulDfcujPtklrvYHtXMJQFaGA0Vafq0oT9MhBfb +1YCP79uF0qgswSxINYCOJx4nTPIP9BOdTwqfGo7ul27REgNq4lIUW0GkMgZAUA2f +t/vc0u/I0PqnhKCi4Pq79hLIx7eiX2ySfXfYfLXRVzbMWKMoi7lWXseQqbM0RvCA +54J1qAi6Ew+JyoYGQ7OvXdL5Eh5Tkm2cpIADyqCkp/aFDe5lqZiU1zS2fU6mpOf/ +o0co+GoYkieIxxibDCmt3BioLgmyzpGUsMNwh4pAIQUGkcxd4spC0KIWdDEvq/QJ +EEIhZlI/ojefaZkRseFrtl3X +=pJaU +-----END PGP PUBLIC KEY BLOCK----- + pub B341DDB020FCB6AB sub 315693699F8D102F -----BEGIN PGP PUBLIC KEY BLOCK----- -Version: BCPG v1.68 mQGiBEowbDsRBAD2jx/Q2jNuCkgiS3fzIj6EzDP+2kipIKH2LEnpnTiBlds2PFYM xYibVab/grgQODxTdDnAKifbJA/4h1/T7ba+OV+xIUoSI5MbgaF3USidiDHPX0pY @@ -28,154 +190,271 @@ JI97g3GZQ2CHkQwJ3+WcPw== =OasT -----END PGP PUBLIC KEY BLOCK----- -pub 7C7D8456294423BA -sub 9842FE565AA0601E +pub BE096E29EDB8D141 +uid Eric Lafortune (Key for signing ProGuard artefacts) + +sub E05A9780475FAB55 -----BEGIN PGP PUBLIC KEY BLOCK----- -Version: BCPG v1.68 -mQGiBEvsZw4RBADH20nX+H1xvMBYmXRj1Aae4dRr6Y6qI7QRWHO6Z7/dxr9bk/NN -Yjq5KsVOQxZzloVdtqx75rznT7fZq98g7Nq9IeEtB6k4tnh6XQLhljJMk0a3mzdt -q3VzdxeVbwCaPJ0zixv8XPTAH6MpRJUvP9XjzxwaYHrjwcQ1LslW4TrIzwCgi5rf -jChLCyKcaL05gqUjl4lmefED/iqOwYZw5pJ8+X+OHUViiOB43wsJt1brAhPj4KgB -ODStcE6WlHFKi7YzcYNLzYMebSGYn6bj65b3qNf6rybWD1hGUFK4122Q7+HCH9Ic -J+rr8HwjGFo/yxI0/mkyaF0BthXYPy4WtdsdTM2kgx8Zr3Q2rSt1jBPuV3q8d27z -FZMiA/9cWPkRx0RfAJmBPKmKkbBkEtBbNau3G7MY1OEAkEkRnzmnyyjr5IP84A7K -RdjTCvkbiQrOQH00Ki4sHIg+9Xv1gDg1XLkFDzRARKA1TxjL0OeS4RWF3iia7Swk -MOnTdhR50pjb18W8kB4mEMZY7duP4nwDfQwHMwbFZGHrjImaurkCDQRL7GcQEAgA -thrbFDDbIGq97dbX1ZEwNGpjILdeumxZzaCc5PgldHGA86TL2V9iPpONJtv7/Csr -1c/rvH0IA/BFu+0Kde9UP+z9JycmfJpIsd2Qtxfhn9R4+Td0VtN9NNkpt1+JcThb -zyQq4UU82uBKwSG/wzubOI4Fn+7ypIuNgDkqK0STIL3ucKF/RV77PsOBeLHkCGKI -VhUUfY1D51BWZKKB1efaVK7PLdnsi64lAhbaexA43dwUUtVwQOvDzJVtkycV2IMw -XwyHonlK+4vbcwIH2BAU46LIZy+VFaI+lNNmR9+xeWH49oJYnMWIXif4RzvO1ag2 -KKwlG2sacuVTtpWVqKayBwADBQf7BGl4POKtluAbX3a/KGnX1YyibPT2e4pdOxqb -KTwuRc7ILIK/LVcejVuLd+eOF1QmjToI7Y3qjlzvtaxSemfkxbIQpdus0SyHEEAG -H/qjNuTs8ZwEl9tPPrKaJHbJ9kQrlDPwkufA0NDAXTE76UhcHPY0DeykAKEw2rav -EqI7Y0nXR2WSmRLFChmQ+aNTm/9UGI/IoRdS/dgo1eaLD/7RLFeL5tGDe+2nj+B6 -j+52nZEmaRCWWU7O9jYL1cQu5WTurtneCLtIqehLsjuVz9ihBysYuUcTOV/6W0AI -clQoMEiFR9Kyv0owDk6LtX2DgibJuAvvLlnajsiGm5nyVh3G1IhJBBgRAgAJBQJL -7GcQAhsMAAoJEHx9hFYpRCO6iXUAnRcLw17lBhe/WcKFRkzETSxaJcjIAJ4/P1NN -On/eScLdx27sje7q3sBENw== -=TzHJ +mQENBFFGZXQBCADeZK9xuCrDwJ7v37y8RITlchzBfJEWv7cSbrSIBlFNAsUUoshW +Y8U6xYKe0GdiLVta2F8bzs0Si4LcDeglQNi9Fxvh3/jfs0MEJUfSeZ4z1Mn5WY35 +utbdlHe6i+7qvhpHveREwWoWuvFGKeSCV8SioolG1mzxrDVLkQefPZ+Kr+xd2cYZ +SIkbjdhCvJHcUkD9gs8Wz9q9eDyc7dGJmfBYaQ7Xa6lbd852qj8LjQ7Gd41yHU6T +U3oo0kKfAPFkW7rX4zeJEBgpncK0i4ertd5to/y2hauqPU3VMExqNS0/L2GqKNmb +Q2Me02pBSBCrLLCUJkm8jZorPNxlQxHpFLHDABEBAAG0T0VyaWMgTGFmb3J0dW5l +IChLZXkgZm9yIHNpZ25pbmcgUHJvR3VhcmQgYXJ0ZWZhY3RzKSA8ZXJpY0BncmFw +aGljcy5jb3JuZWxsLmVkdT65AQ0EUUZldAEIAL0CmXXJLZWMbKXEROAYFnUqQ4eN +LUcLY1NfL9xrF2KP6apMXLJdftwzJtcenfD5vra8w5JJBn6cvY4Dr4zmueDSsN3C +pgr7c0trDNspAQMyUGmg4Cw7lAMruIX2om1TTa5ALsQ1PfupnLFLFKbYd2MmfwEV +HgEBAzqmr89RoG/NnlV4uxvWumb/OrLzzizGLHdLFjnq3Szl6wQPJwmxCtWHvzW/ +Y+HYclm+wp0sufvrXJ/G1RrHKxwhxaDVyLKsb5wkUC6SZmE477SP6ZCokoAGHqEK ++5JnM9MYXreQ7N6NggRUseCokFaqckze5wVBXmPuVAg76ScPFQd4JzLkGzUAEQEA +AYkBHwQYAQIACQUCUUZldAIbDAAKCRC+CW4p7bjRQSkUCACPWdIvMXPtSQFnFR8o +UIGxvcp9MYdealHNYkf3qgJTuWbHTeD8WpE53immju4hitOq/qDevKOWhcfSaLqC +tdRyYQFFDWQt4SsiGdWfCvKiBMSMnrDQI9f+XCv7J3ute5alyMgv9Uo1XLyyiwhz +9kJ9mzg+ENvy152PucI4TN63qMRQMQbkAenrTzIer+5Q9ZY4TILC8pVVAIPQjTHu +hdh8VMclTdkHab2PCFC57/3djJyY44PLhN3hLI4CElAGLUwBZLRa6OFOvwW5naiv +wgk0zeeTqq/jTOWPYiy2dyyX1pRX8bJh5et4mXyM7HLHRp+FblgWxpFWNvMCEpxu +jLsG +=C9rK -----END PGP PUBLIC KEY BLOCK----- -pub 3B17BFC1FCC1B644 -sub AB1D1A77026CB606 +pub BEDE11EAF1164480 +uid Joe Schmetzer + +sub 4BE257B370130000 -----BEGIN PGP PUBLIC KEY BLOCK----- -Version: BCPG v1.68 -mQENBFro+cgBCADCxtYd7ZFChfyOnmp/rCtWS0DGY48wCmJTcgOyBFuTy8UJ+PdK -e8aBsbpx6I5OM2WqvsGkqs/vboAbZxJ+G0br0qriXrz2SUQEJ7EMJJsUgkVSb/iO -DEiqTr+XASuqVoCKsUnB+7eCWycsfODVYZbEjo52PZIJwiNS2yXBtYjf6t6i1rx0 -KrMF9dy9ZRWKe48UGK4KkZ5l9/yiM5CscJM3Wt5fBdFKpp8JmJQHMUivwURjT7dP -o7ZDBLmvgs1/U55AJw9Uo7FwmNSoxr77OtGFaN6rmQS2w1jEt0ieujmuvj+z91sw -FKQ8vxVBSLDtzDNP4NMA4Ie4G4sLDAzi0+1xABEBAAG5AQ0EWuj5yAEIALC3ywW3 -q+flFxeSpSuhLPNrJ1xzp5Ox8sZ/56f7PBEHYR4XAqyF5RLTaslHjxVkdq7JfQsi -saoW48ToFGU4wt21A9Wgh+k6LpnO2CLPvZZmLmXYvikoU74JReCaLjxWLqgD5aQG -6LakGNGgB4qHngFhccJozJn7irGUb2rJ58TkwAfbvghIKOvp2RI6dInKFEuIeWBz -98v5xZp0HRbMnq0bd4RqNGXdJ3yJ2VyC4nVCw+IaZafVgI8MWSaQCiOgwVgS73kL -wgX3xTFfsKMie8Kwo6zCYFyFekKdImG7w9yVavQmgSid2K+Xkb3qxpfjTZSM7qsK -23nL/IoKChLu7eUAEQEAAYkBHwQYAQgACQUCWuj5yAIbDAAKCRA7F7/B/MG2RF3+ -B/46dH70jqBWl/8UtnV4/Tb3hNXb2vU3w7BTawg7qHmUPj0R6BBtRPQqvPxH7K62 -8ARhgBdEaVKMDA6ijSet7zJm2mU9DlBz+Yunz5a1jlDN/vXAzOQyA7jEd9iyI4Dn -lWp+0Gy8EXEysV1mrXRQd0EwRp8kyozLpjAk+P3Xtu6OEHEWOJHZ67OzVLx/bOfU -DIn5Sv8Dxi3cmaAj5ZjnCfM7a35nseGhFcezd+HjQy2v7W1X+zCJaDejdGe32pPU -5Bx0+rqt7BzGjCtQ9N952neH8mAeLLhcpVQQqHx/YjIqJ1qfMPX6vXsur7fxJ1t0 -oY3gZlwqvKcRwJwExFAQotNX -=Ql65 +mQGNBFv1EEwBDAC61jyEM99KH18hI3zlfuqvGoNjTLIh0wge5vXAH8VxMR0ndOID +HYSBT2+L6OeiqKlyhCgF1km48F/dMzyJdTASkNO1Ni+B2Ric1sBxjsSPufkjl4en +yMOl/FuQOB2myht1fCXhlynmOoiRia5J6xzCsCNVGOVYfSru8vpoT9QKcD1OlwoD +WhfyBx/bXsoRvD1CMjQdalcGxv1aJRWfhRumXQwhMPZlFeARAzeDmWNpglqrMnuG +/VADZXZsbLv8VWaequ4wEWiwTOeA6YYElx648OTSv7NjMM7iyPPPWbbUvkVbA3Em +lLBLlGYZTx2nI0B/322SsREcEDwaBzO53GStIzP1XvaRosM/98/Y9ITwB+Oh7ZwZ +dYmmabxN6F5O3v+TNndEW7wgP0lkbsOWZ6YNmFhvoEtd1RxZiSNov5CxokYUrug1 +cS+/vsa9oIecUwxYOG2D1v/pwYhQnr3qasYz4nEEBWHnnkhyr1BbUSuen7w2SiK+ +64cQn6V9aeZYi6cAEQEAAbQfSm9lIFNjaG1ldHplciA8am9lQGV4dWJlcm8uY29t +PrkBjQRb9RBMAQwA7UCAsQ8KxX8nYO4Sy2pzlh9W5FMPwGluuokPA2A6g2Fz3vF6 +2RqeaE4HrRQMpijQCsN3JTJVwDid41X84XCMItkdAxMjmn5zeF/yCcRuHe2Ci/+a +e5BzrBaKE/VWRAkaZSZWJ1MoDdpSxJhLHNFnVrwTkM/SeSNUBk9ZDEC+43b0hcie +fX9bFlc6XPHgV+yr5ohhwcNcrZ/gbAhhN3/xIVmvKoibmb+ZIajhiCP1OOH+GpZA +PT93w9qZWq3+2gvP4ZZ7bO+8N8Gmz24GL3/0eYI6aMUMwWGjy5J+iRiFjb6E+Iv/ +zToyZFWm2VOuOUqy5t4u+Vyk5bl0hATpJICmKa5OFtQwG5Uvfztk6rujjat90xv8 +yzsBvoEUqKqzIzjHdN36qop5hLMnBljdLdFY+Rk9CHdF7MW8Nf0YWbP/3uUk19ut +GW686Lolt8gvBQc4B5N7VtNoXFCKM/I3ufgnHQvDlf8pgdJOcyx/a90V/DpUI1AN +lwg6IsmFZXbBQw7tABEBAAGJAbwEGAEKACYWIQTjqflQeehM4gH3z2C+3hHq8RZE +gAUCW/UQTAIbDAUJA8JnAAAKCRC+3hHq8RZEgEy+C/4lsgrKCmq2Nc7eTdN1AxwM +kj28XQFmkqO8orfJm1hAtVK1KRizkX52RNeRN6QX3pX9s1e3DjJi3Hpa1UWqeicP +A0kKTi2ytUlxR/iZDkaQkLyCCZtWnGHr/eRBdOjblprl5O+v/tcyrmQGC04TqOnt +Mumuk7JNjZ0QAVkZUxdmfi9bHaF5W5vlcaFYT5gdWpkOQ0YaWXXw5ynh6Ookjhq0 +g4pZNjl2rdWWyTC59YIvC9THx0+vuyN7xnSWIb8J1IjEEYvPqRfpd8s1Vf2AA0JR +PjUG2UV8MZqu8k8x4iC2gbdji/vyg/ycdlRT/ULyNprz1nTLMfhBT0Wmy8B5lFVm +e3URmld8T90RPln6Dy+c+IKb/79z3FPujuSbipXzx3QvGwVYyP80JFn7CJluOl/u +8vxi2EVFN6aVqdzwoswFE3+0W0AfbpHUUT4oeBW5OBTJ5i1Qb0DT6WXk3Y2j1Z08 +xxhY1RITnc2C33wjXAW0h+qq7/7Yq3w3/7ncv9sWIzU= +=3nku -----END PGP PUBLIC KEY BLOCK----- -pub A6ADFC93EF34893E -sub 9C4C23E6FFE405BD +pub ECA081A9790D4FC4 +uid Ralph Schaer + +sub CEC8F362662576CF -----BEGIN PGP PUBLIC KEY BLOCK----- -Version: BCPG v1.68 -mQENBE+xZxIBCACzKctn4ez8xOC0pGThhAwjYWGkzcwK4HNaC1usHThBFz3/t8JN -OqUXRixLyi5wELN6GHlsGVUQS3IfB4JtuhScsieSB8PTree68/knMq6JI08mJqZr -9nFrAB4eDW0UMbSL9kPmclUm/yN+qcCZBrsVn0q6CWb/Kcd8EEXEu6sGILzOGqGe -d433t5O+tGXWL2TjAz+Scsk2Hf4zcuDeQcxELAMnVaVgKuGuEZvibrjsdIvJDGI+ -0BzWIu8ZP8ldBl4SVtzGpEVzLvDUo3mOqBeTkj3rP7xLtFDN/3AFtowbLfL7L2Pg -SMcTnKK+jfFHRfbHP1Ih3rQ4ilLzhCnY/QIZABEBAAG5AQ0ET7FnEgEIAM3i3e1s -jwrx2PN8XYMPQWG+/YTtw1BYDl2+iYE+LaZvtq1hpbgeCLgEVwXrCJ4spLP1rFXo -gWqKrkJ0LRjlpdKhKBvyH1ex4grh3cWN/bIDJcJ7JA4I/Bhqhlh8hYycS9pGFeS+ -MR3aFIsii+vadrwYYvuVYGeWvdZhB7mJKYevj5Ms0OpYTfZd95Pzo4o//lNpDnrG -7Xd3tgTNU/fkpw6rFB/2Ib1Qlk+Kz1z6JNsp+tOPGGCBrzwfwglcikTuqS+xyRgC -9cHh5eCol11uSoWPKcQR2Ar8Eo56nxv/UApdu15iJ7R8cA5guKeeS4jt0CGCPs2P -huggDxI73Xvl4zsAEQEAAYkBHwQYAQIACQUCT7FnEgIbDAAKCRCmrfyT7zSJPuyl -B/9iwtIQeexMWBmQNdDe0md8HLulDfcujPtklrvYHtXMJQFaGA0Vafq0oT9MhBfb -1YCP79uF0qgswSxINYCOJx4nTPIP9BOdTwqfGo7ul27REgNq4lIUW0GkMgZAUA2f -t/vc0u/I0PqnhKCi4Pq79hLIx7eiX2ySfXfYfLXRVzbMWKMoi7lWXseQqbM0RvCA -54J1qAi6Ew+JyoYGQ7OvXdL5Eh5Tkm2cpIADyqCkp/aFDe5lqZiU1zS2fU6mpOf/ -o0co+GoYkieIxxibDCmt3BioLgmyzpGUsMNwh4pAIQUGkcxd4spC0KIWdDEvq/QJ -EEIhZlI/ojefaZkRseFrtl3X -=pJaU +mQENBEwwaQIBCAC/SIIRRSgKRDy1LGdgMiwnXSFCw6GmVHmcKw/EIZSgRwpr5c5G +jIsdubpDLgICSCQcqiSYArx46nyBazksvvKji95oSBAdnuNMFD0XC5MdHBn5OE6I +poa1CBnz/g2XTskrK85Ct+LelP6migrebK2pAKC5LuHn6nyFpb8C/ceQ3ou77yYj +5i4/TP9QDFqnWL4tkdAhnoACmBil5sH8Kt0pR9fPtb+wDOnjtHDpb5FH5pPIN5zV +EdP0k9MA6pp0wtWXavdMspJoxJctiqJ+AL8ejNqI7PbvXcUQ2A4k/oUYJUm2JU6g +UGzs17pWDUE+xekjmTJEEKwhIVR8I8+dTzohABEBAAG0JFJhbHBoIFNjaGFlciA8 +cmFscGhzY2hhZXJAZ21haWwuY29tPrkBDQRMMGkCAQgAyLpmVFisy7DFYkTnvnDj +AzkmjvtBKyjYytG/Le81L60wBbVePhVAq1MaNikGtdm5ofjvQ9j7K/z8Mpn8KYEx +sNkNcrREZcuBswlOJZZvqsSDy8RYEUQ7rR7SgQEqSpBCqGVe96q08hoam0D2ohvi +okocoFr6JiGXYaXTnYsJKwZCgY3Ir1FxVaXvkqqFwLGvPlGL28L6un5GTbplRJTh +OujaWtiOpjWt63GnJ35ukpOOKxtOH0MoYZwx8A/IN8J9nH7fR0c6y8pgloOcrDFR +BacGSUJtxdy+qK/R225xF8eudO/GCXrVyxM1oMGL/MtnXfU1euZl0VENjLyDE+G+ +DwARAQABiQEfBBgBAgAJBQJMMGkCAhsMAAoJEOyggal5DU/EKtQIAKQW/u8UPr7f +Bs6ir/xSURK649reQdCREqEoZccXnwRUJAJwz75g2jNJeag/4VjgAVmoKELu8PnH +By5TeTeoDKhVfop8iwIZazdWG4rilTWIdVdqq4viw7jktaVSyPo17wUJc1+YgKMp +rSk9fNptU9gD00P0RDcdrEmBzDe6mrZXlmNr9ecOU3hJRrAi6DDMdMsBCQTqownQ +qIKLX13akKk63TaSzO1/y/UcmLbjVbGAJ2YeyoirsXbAm5565Esrd+ZzFtUlYySF +sma7MtE3Ry/U259VP9kcx6hET6XqQ2QTcPh8Hht67/q6SyQGbHuvZZ17kvVHvYBP +21xQPgPV5Kc= +=Dl8f -----END PGP PUBLIC KEY BLOCK----- -pub 2189CA6247F3DEE5 -sub DC032628E927D006 +pub F42E87F9665015C9 +uid Jonathan Hedley + +sub 6064B04A9DC688E0 -----BEGIN PGP PUBLIC KEY BLOCK----- -Version: BCPG v1.68 -mQENBFEsDjYBCADKUrv1zJObnlCQ/2AyexoofOuSSzj+L8AIRm2tm2qdPgYNYZHJ -II661t4G5mg6QSGRlO1+NaD1S4GlcEpYvC860Uv26M9FbUAjlMJlWqLoNME9N1Oe -eSLKs+C6RroY4qDuuA0UaLE1SQ9i5cm5bxPIfbm6aCcIzecTbfD18bxIq3fZrO2S -yHek0H2upHAipKd2g2Ed4CikWZV2f8MoMZr7CLs4XgrmpEF01Ab5RBX01kX2MYaa -Vk1BAc3FgLwA/azW3X1pdiyD4iAesHN6SlAaD7x2tZs8CrnRCaWLu31t+x+Sc4Uc -zShjTGdB+euZykFpJW3t/D9BLicjRDTGjBfZABEBAAG5AQ0EUSwONgEIANijDStd -yc4CSVYSXmX+ZQnWTF3gjYMkszD41yT129Xw1RTw//ayxYeBYruIgZb24eAULVhp -hEEW9XgkXvPT1gvAlU27GJ5DYswuh+EIJGGjZMngvORS4Dvr+lkWXU347dgBIIHw -vVl+rw2hUrnhn6Y/nBGWHRFWIXAMwQC2w61iwa2E1aLUJ1u3OvrC8U7Llmb76YOe -qLGbOFiQN9vcr5orrqP/dAdTr4MoyS6dG7Lh8McfWwEDzq0Ygy5d0EJDudusApwc -SQ9set/SO6b3F7IiQHAz2+sXlGJB1Kscx6tpbt5NRegKQp3qyZG71pp31QRePgbf -O99L+AAv19DsQ40AEQEAAYkCRAQYAQIADwUCUSwONgIbLgUJB4YfgAEpCRAhicpi -R/Pe5cBdIAQZAQIABgUCUSwONgAKCRDcAyYo6SfQBtuJB/sH31Gs3MKbEJksDtR3 -VKXGp+kll7BYSoVYv+llFcx7CxkAJNUQ/LH8l9Zbd/j8MA7e1V/ts/QkuLcLqtbe -IdPWcHqXNSjFgOCIC46zGfV8dOmtjlblhfG8YvUZNnlEtJAFBtxOV+kKnMV7YWnS -xm/slrbEFyaaQpicdtP+hw5ubWIAIzK+N3PbjXgKlIywSljU8iXsch2oCms8dAMY -TQeqhnbq5VRjpIyGVrYzqubRLojeXWdCJ60rDvFHnqCyEJgKtxiWX4lQBGfdsnqK -pmJNVv0X4CDHxyPUwChTtFLgNiJvJ2OoYJi5oEg0IZ0rt/9JNNwNnUYlB3qYCXU8 -3771PnwH/iql4tb/K9HUZMcZdjJ9cwljVbwL0Lk4SPeVuPkhTsRmSsrm9FHjOJkX -RitmKnkQWbhBChV3oxVwklJTstYoGXHsUrHnmYV21C8y0HVGEuu2bGyWD7YaOH5c -ayTK1jWK3Jm3BqFE1ORHQjPLMYd+ysGZsCFGcAGEL/3XBu0Tx1poq24uckus5XVk -zq9Kt/d/C8lZR+TSi8In64cpKSl8ZuowN7h7gvldhC3A1jLu2vwyA2IBEaew3EbZ -P0Trh3QbPrrKYy8ljK3LXa6TrtCpkO2JdQDC2Ih/3IS1ed2w6MML/Ss5trXwUuAN -a2WwWwzksx7K6171af7wQ5jdvQaFDVM= -=KJKl +mQGiBEtsF2oRBACcai1CJgjBfgteTh61OuTg4dxFwvLSxXy8uM1ouJw5sMx+OKR9 +Uq6pAZ1+NAUckUrha9J6qhQ+WQtaO5PI1Cz2f9rY+FBRx3O+jeTaCgGxM8mGUM5e +9lFqWQOAuCIWB1XPzoy5iTRDquD2q9NrgldpcwLX3EVtloIPKF7QLq72cwCgrb5X +R25dB8PUdZKUt2TtJbjB+SMD/1UzAPirgX0/RpL9wUR1i14yIrTfpFP/yM9PE4ij +qcZ1yafVdw64E1k5W4k+Pyl4D8DvSJvbJHvYjg8/G9V66WzaKcv+987fetUuePvY +/rwxBPztqq8y6+hjBc8QVhZGWmAoGGEFO6MIGsSyN5ohqPMpNXkczIo+NMvDxGzz +ld5ZA/9awGTsigBdpBK2F6GOmbvBv+Xebu9rbaJvBvP+npNx01s/f5sHPCxmBTFk +m1vtaMdZ29RovrWPSZRj8WWes0bcisw80250r1CBlYzGzqEVZ7b0Hh2RfkfaxbYh +wikyfTfA2iX8TUGBgirsZbyegjUadElhwFNDASnvLTEuQKeVLLQlSm9uYXRoYW4g +SGVkbGV5IDxqb25hdGhhbkBoZWRsZXkubmV0PrkCDQRLbBdqEAgA0sZ0JZvWoKIG +b+o6MOwI6p3uMb+iWBwdYfoh2RPnUZdBwGhJjp32CiTt2Y3qYEcqC5NvF5FWdx1m +5KOQe1O+QFoqPKnC1bPj9uZOjLVql7x5tSwCePIaMNB+fMxEh5hYwLWtBz8nrdCP +gwm+nAwecoE8YfrpmrXZk/YLak54FOeEwLYaP8E4u2FHiEqN+WmKMjIRwLzVpYAr +WRCbTLhSSKyRBy7UxEovUH9mIa4YuU4Pb2R64LwopMHCBm5ow0U8kCw8vpW40GrB +c/2eaIeXCX2XJ77E9s9ZPgW6MoJ6Ic1xV6voLJKIEV8t44deKNSwDfVNZHxyemaK +a8/GgpjU5wADBQf/UzL5lXRmyTdJqRvHIfUV3g4A3X77d3vOroab8KKw4MFy2LiT +ioN7btKKxE97Jjp21YZFd7Kpmfu2i/kr9QVJo+DSxe2p2xcQozyS+layPK8h/61L +hyh8vjzV5AUWA5Zup+P7Jh/WRlh9Gxs0k0vimYMFKImw3mZr4EA8UCj2e85XIHNH +Bd0B1VIukq4OjU4QhRrutNebIy3GZ35ylcaXT5v18Rq/iRJAuJFoCzXUaE90/V9/ +2ob8A1CYEKGLocvOQgBsj7+2gP5WOP+WxI4TWPENRKMVchVBE8zV+7YZiahPCwOQ +r9TQWMaUIJxZ85yr7O8DhJOBX3B7EHIfpoADXYhgBBgRAgAJBQJLbBdqAhsMACEJ +EPQuh/lmUBXJFiEE8xhLzVX00BbjDUyb9C6H+WZQFcl+zwCcDKIILbGBUNHRGY57 +mmZ5xKMWbCsAnRbmM18GlK1TKRcOcqqEPWSusurHiGAEGBECAAkFAktsF2oCGwwA +IQkQ9C6H+WZQFckWIQTzGEvNVfTQFuMNTJv0Lof5ZlAVyX7PAJ9ztvyEP04cy6zP +9lHt0qXdrucDfgCgh1OIUk0pFzNYBt3PXvOeyD5FQbk= +=dtK1 -----END PGP PUBLIC KEY BLOCK----- -pub 3D52C2966A51F8F4 -sub 888C89049BDEB64C +pub 152888E10EF880B3 +uid Daniel Fernandez + +sub 1CB037909F7E059F -----BEGIN PGP PUBLIC KEY BLOCK----- -Version: BCPG v1.68 -mQGiBEqVGHgRBACyrZHA3gvzM5WeoD6WwQSIPdTPuWw5EUonHytf4m7pgX8Xkj2G -enqbzW70POgJDUTUpvHDSCaAWiPY+ld1h0j+ghXgKD7IQjqFQTvnkdBT/qbOL7P1 -Umv5OLGSKeyx/BhxNiD0uF/huyP9b1HWC9dbIdVebhDcyBDkBAFtJMOxjwCgjnxw -sUI5vJ5FMPDjUdRPTVQxwlkD/1PGN4Ycdn8ncmZ1OG0T+vmL+C+yGMIdbWIizffC -Li05HMziwK33NHk5ABkeyIijm4Mq7JUlSSwt6gQXJTGlidHEcmKqSTg4AQhybPL5 -w40KcjNyAeQhIFgHAfMzy+Ikxzlt8fNunMTf6utWrPq2IU4uE4lJT4kSL1ic89vx -a4CLA/4g4MJVuzTegJXS5vhMnEI2Nhmc2MIAsEhPcIwJjhUN4rJJMcVxQO+Cmrfl -0rcosxA6wtjFDdPcuXYiAoJBJQrVYp6+w4GyHW45/WR6iGas/Rm0ZRQqnf9WnlpX -o5d7uEwzLfck4iVw+z298y6qk52coFKaKPH0bGVxguifMNdbwbkCDQRKlRh4EAgA -oSWTikKPMX99ZIqYgjAKlM82x6H5xxkisLGwI13m/nvRgOBy8gYzfJCdxrEupnvN -k4PK7twUka18oAoaKfKFBdIKQ0bSyUNV/RzkJDhkBCCU0T8DbN7Xe4rP35l1PvSK -OwtGmIxovip+Spm/j38KKnSTWyZ0JIBTOYEoB8wzyna/fcibwIy/M/wj+lrurXE/ -6QnAxqJjAsL2MtuTurC5IMfRPQqUL0yMk+l+wUpuBaWhg70NhbsTisdJy9TP+Nsu -7tCJu3GumEuYKd3E46xIFedyA1etFHr9n7GkjQkrthlqaza2bUFOv2xOpTJOvZYr -15zOCpHslcIQiWT5zWCQcwADBgf/WBNDvc3jNmAyTP3nLRbA2JP+tVBVwdQkHktY -4EjaLC7HjUA5TblQFTycXtZyKJ+LUwXM2W47h4HMXhSRq7E/wzMUHGhUMBf21MwY -//8mirKgRiU1rkArBKtr+bHTpsJ1UEoCaGzXxvTGxWZSLLrV1nBvbBXXOOhFKvCy -mFcudN12i1MfBrubGNT905obseKThEc1umWzeRRWOlFEZkCONLR9PgSwCok7TT3O -lkaGKRQB4cu54e/faOaLvFh2BgqSIoMgGHfVLJBTbssV6GL6T4YoU2Bq0wtLZ2k9 -tMgJHvPziWrk0rI4oesVWDmbEoRgAsYlTkdev0u98g3c5zAHLYhJBBgRAgAJBQJK -lRh4AhsMAAoJED1SwpZqUfj05WEAnRNjXTAHWA3uXnDtK4XDHeCkqzUIAJwO+CnW -RTDq7K7Q63sS3rHl9X9ZgA== -=lGHK +mQENBEtnRE8BCACzVmIkAALxSHQAbp5HCXCLqL16UnxJcrcUIcnRQSXiwjAcNsjS +S1mRwr0WrmaquTQYDk1W8owUdmAXdTfSHgtJKFRKdJ20IymeXx1VrmUiiCNY9Jf1 +xYhaJAopMTeEp65q8enAZcxjyqVxk7wOPgKD6uakQDdwiJtWHsug7zlmUtzZ6Phk +iCxhrJSasatOqQoJon+RMlsFYWLulDFv7CmDJq/GLS6bMDajKt+SXBsnKL/gUWZ4 +hT0sL4Ux6lkshyey1BSbmDbMnHmzR2Ng8oIzKziA900FFaAWsyg3S6vi6I4P/x4N +PfOR2BiUwcPi7lA6Tp0lwPM0IdiUep9Nl13vABEBAAG0M0RhbmllbCBGZXJuYW5k +ZXogPGRmZXJuYW5kZXpAdXNlcnMuc291cmNlZm9yZ2UubmV0PrkBDQRLZ0RPAQgA +rznjhUDrXc+Rj3JK5c9Kz7rIkRmjfvXAz/fWjgPGE75brZPcicuSY/DIMXglvYg4 +Nn2ELpfcdpA/2NbI9IbvBRvMm7lzun9mYlq4Id6kvpBhQbCg3ksyy6aailNB3oLN +9Z3DFw2CnndDE9YuAkLHtSwR80ylM1Ain0OhHh+cPU9eoQSTZiPI4nmNFoLlIUmL +HIqmGIndEsdkWg+BczFsc7ARKLn41xHVg7/o1TBleyxUrLVp2LatNWZZdqD4pZfe +AalrrmgTJHP89X++Qgtdt1baMWiomMUusUjxM+WuUSzPmYt8C2LG1dp8Dfvwr8DD +98MrHwf1AkNfGGNSTWTWbQARAQABiQEfBBgBAgAJBQJLZ0RPAhsMAAoJEBUoiOEO ++ICzSdEIAIkL3iyzDsl8s76qY5EIJPodAYBeN85uA4V0tFdmnObUzas4Qkl8btGi +YScQaeTBH+MepUqJiae59yOES2Yhy6bf9t1+WNkYUbPqvazSQgK1rujjlpvK33sz +MwaAr4OsiCXgLXjiiyUihNTu9yOn/kaV+ry1D1dVd05NgH/kHsGcXC7+bX9DrVYM +zZd8vIDTK1cYEFSk/4f/ej5TkD1bVyuZpWG381vO/zchFeEFO68wd7KmVuUpfemZ +x4vaG6u/wA9DCulCVIhScBIXmLdwUWihEmfbGu3sZ3xuqmInxEJa8pPgjoTHTQ1K +cUx8QDUSXg0Ne49z1DnRbB0ieTIVdqA= +=7bzB +-----END PGP PUBLIC KEY BLOCK----- + +pub 15C71C0A4E0B8EDD +sub 891E4C2D471515FE +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +tiwy+wOBjaK9g3Kk8jsN3I8t +=8Tiq +-----END PGP PUBLIC KEY BLOCK----- + +pub 164BD2247B936711 +uid Marc Philipp (JUnit Development, 2014) + +sub EFE8086F9E93774E +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFPU8TIBCADGNvExYTJpVuNGCF9NuWw+IkitjAD7WzF7QkvFCSw9VftzgTUZ +3PYrThRiaDdmHQAke4Sp+nYyAJ7iUcQqg/5/ONiMdzXEv5Kwy5WJN8+o2aXSunIT +PALIw29DemZdVerw8Xc9jcdNYGxbPgtfSjr/DBCBs5Guq+RuuwsBvyTGLiHpc6hb +91D/wX6Y0NQB4UlY9f3k6AgqiR5OGq602hXYvL10pH6eT3gSmCvxp1WvNJDBmaRi +ZdgW76Y+bGQkmygWdAb3Q2DUEQHVH30YTBx5VFoiCfx1Jud4mD90Tv1hhPn0fS50 +oLGqI6f+VB14iJ4XLaSfyk/UMl343iF9PAg9ABEBAAG0PE1hcmMgUGhpbGlwcCAo +SlVuaXQgRGV2ZWxvcG1lbnQsIDIwMTQpIDxtYWlsQG1hcmNwaGlsaXBwLmRlPrkB +DQRT1PEyAQgA0A5RG31MshHFziVkue970s1KNN7eDzww5KgIj8SzI5Hi20yZzs2o +2ALBhfK42VqT6NIa6ktuJQJoWr6KhhY+kMaZlkt+I0U+Llmby4l2jlG6lFPNZYIa +vLOFvU8eWvU934fgLutK4HimBNo8HTqn+jsfoNTf8cpvcASQnjQVOElf5xTYQ6RW +z331ZDpfJpuPyPWK9/GMHQrurhK4QYSla9sYdYIZM27OmNxdi9haPLSkaAmeYF9p +ng0CoPqanI3HfIN9W9Uu7kPczW3Zr1DgXq4pto7rlK69M01GewKMVuTS2MJqzWza +P9BCMj0jI+icpSRV0696A/1eZCR7Vab9NwARAQABiQI+BBgBAgAJBQJT1PEyAhsu +ASkJEBZL0iR7k2cRwF0gBBkBAgAGBQJT1PEyAAoJEO/oCG+ek3dOenoH/1vTfHTz +f4Z1odIE7lC98k0zF7jtvPHndsUysKp3zMOtruEb8CE0cuw9Z0rYzIBzVTzNm305 +So7G1xv3HVOd6rVcMtpsZpo2dAyFEbBTyOqbDKfWrGx4T9e61m7syNCCjadGlTAE +r7XLkJwLgjhHGwTN9/VEWz207JfL6u1XdfaGGSxPgZRqDw7c+kKax1fU10wV9wv5 +bs6t/fnPqJppZSR607BiHNUAt8Cpn7kEC3Rm8mS2ufqg5LX+quLlgl4K7L+EYecq +5Ox/UG4lRCNGa6e1TM/LyoM8GpMC0OOK079GceNjR+r1Jx5fxD8Qi04rKo8U4LZd +8Kig//NPGjBx2ytpcQf+J0linycedx3OyhGifM1j7SfrWpeQT/TJZG08qVw7mr7e +LzYgtPfz7YzsxCAcGo+IvxSaxVF8c3BMe0liwwcSDL3SF5XWYt+3X04e9YTODKim +D4bqhZto/DsJHR2Md8m8Q8Za+V1bS8clogD1f6Wbo3adzy2w+braL+KDyjm2NQsB +SuLNBqcK/ztXGqxK3bhWH2wT541djd98y/jB52X+eVoeCEEPZi21wW6UDCgHqfC1 +Xxhsq70BbH5RCwIMx2iyVVoKEyxNhaOgApSfq9mko0NAlpAuT9HbTm6GPVgoEzTA +HBhD37R+PsUfSEPep+pmyBlX2nrKxxoRNsl5KWNiQw== +=Phg6 +-----END PGP PUBLIC KEY BLOCK----- + +pub 1861C322C56014B2 +uid Niall Pemberton (Code signing) + +sub 9A347756830C4541 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGiBEGVK0ERBADwhGhmOMvSgvGaqHW3ial0NS80ZXyE1EeNL6ke/WrXHB4dT6if +inoAuUgRz3v9Na4rjSQ8YVFjn3NaZq1i8RM2KJOUU8ZkJ2AsrH6fqStjofLTd5ng +iQaq2wLM5rTQfdjLsGQb1iDNDyCXU56+/l4mFZ2JiEGCq3Re9EFHelRBKQCg/7k0 +BIfyoFVTHw39PKaDBmCYF2sD/3CMD0J/WldhBM7gLOndH9/YEQaEMy+PJNXcfTWN +WLygGPSOQdNBulyLkvcFIZjUTxFVakh7FbjMq3dsAP1oqthwmK7L8A1zsvRgp/TI +4YMHkZSkgcs0PBBby+h/lpgRrWmtwjTPzql/K5xX4VSRS+umHye/7R+4ZEH7mABa +m/ZABADkPGG2ECvfjNosPQDHZVFuF9oVaske0OjzxI+rU6sR1fJKtbOPl9d+bgz+ +df7rZ3mwppkPX2r/muUbd0CM1Se10sK7ZAoPYmWqHX+vB/oj3Jq+iqb6M5mDZzEz +6h4hdnaGERyxbktZvhRV7Psco9CGi8o7RFUQTR79Y5ElnBavqLQyTmlhbGwgUGVt +YmVydG9uIChDb2RlIHNpZ25pbmcpIDxuaWFsbHBAYXBhY2hlLm9yZz65Ag0EQZUr +QRAIAPZCV7cIfwgXcqK61qlC8wXo+VMROU+28W65Szgg2gGnVqMU6Y9AVfPQB8bL +Q6mUrfdMZIZJ+AyDvWXpF9Sh01D49Vlf3HZSTz09jdvOmeFXklnN/biudE/F/Ha8 +g8VHMGHOfMlm/xX5u/2RXscBqtNbno2gpXI61Brwv0YAWCvl9Ij9WE5J280gtJ3k +kQc2azNsOA1FHQ98iLMcfFstjvbzySPAQ/ClWxiNjrtVjLhdONM0/XwXV0OjHRhs +3jMhLLUq/zzhsSlAGBGNfISnCnLWhsQDGcgHKXrKlQzZlp+r0ApQmwJG0wg9ZqRd +QZ+cfL2JSyIZJrqrol7DVekyCzsAAgIIAMOpC9D4T2fxDyNJj35iGHmilIEwfKuO +5wymOOsA7NZf5rFSRwFcBhAR7sZmwu6/6nwIH0lvvy4+F20iwqmmSFkrupX2ONyD +iGFw+KXuXAp1pEIo/ody8UUerg5ga+XMIXhxLgdrDBVOZSYokEdfnl2JDzSuBYFU +KRy1nm6DgwJU39I/IEUfmk7OXll6n0gPlBLF6Z+uOd232ShC15EZhM52+MchliWi +1tjJZmkZWrmL3OtTOxSvM9eUkmW6uqInLaZxHH5IJwcXDc2t42UFe1jlmdwZQ9Zy +kOutbG5kvNf5y58W6L/QErNuStqJVwbB5BEYYzJxEXnb7+2r5ZgcItaITAQYEQIA +DAUCQZUrQQUbDAAAAAAKCRAYYcMixWAUshk0AJ9rvwrz8ysEb9RVJD2Ee+dY/Txs +AwCeJD7drnKPGp/aTvf6KlOr5WVK9/M= +=+Q1z -----END PGP PUBLIC KEY BLOCK----- pub 2189CA6247F3DEE5 sub DC032628E927D006 -----BEGIN PGP PUBLIC KEY BLOCK----- -Version: BCPG v1.68 mQENBFEsDjYBCADKUrv1zJObnlCQ/2AyexoofOuSSzj+L8AIRm2tm2qdPgYNYZHJ II661t4G5mg6QSGRlO1+NaD1S4GlcEpYvC860Uv26M9FbUAjlMJlWqLoNME9N1Oe @@ -204,259 +483,122 @@ a2WwWwzksx7K6171af7wQ5jdvQaFDVM= =KJKl -----END PGP PUBLIC KEY BLOCK----- -pub 86E02C5A42196CA8 -uid Christian Grobmeier (Apache Codesigning) - -sub B4038E8B427473DA ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: BCPG v1.68 - -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 -=B9qr ------END PGP PUBLIC KEY BLOCK----- - -pub 7C25280EAE63EBE5 -uid Oleg Kalnichevski -uid Oleg Kalnichevski - -sub 926DFB2EDB329089 ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: BCPG v1.68 - -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 -ZNfGiQCfdH3+RPBbKSQofz2bGx7/niTd9qg= -=CP3a ------END PGP PUBLIC KEY BLOCK----- - -pub ECA081A9790D4FC4 -uid Ralph Schaer +pub 2C7B12F2A511E325 +uid Ceki Gulcu -sub CEC8F362662576CF +sub 10DA72CD7FBFA159 -----BEGIN PGP PUBLIC KEY BLOCK----- -Version: BCPG v1.68 -mQENBEwwaQIBCAC/SIIRRSgKRDy1LGdgMiwnXSFCw6GmVHmcKw/EIZSgRwpr5c5G -jIsdubpDLgICSCQcqiSYArx46nyBazksvvKji95oSBAdnuNMFD0XC5MdHBn5OE6I -poa1CBnz/g2XTskrK85Ct+LelP6migrebK2pAKC5LuHn6nyFpb8C/ceQ3ou77yYj -5i4/TP9QDFqnWL4tkdAhnoACmBil5sH8Kt0pR9fPtb+wDOnjtHDpb5FH5pPIN5zV -EdP0k9MA6pp0wtWXavdMspJoxJctiqJ+AL8ejNqI7PbvXcUQ2A4k/oUYJUm2JU6g -UGzs17pWDUE+xekjmTJEEKwhIVR8I8+dTzohABEBAAG0JFJhbHBoIFNjaGFlciA8 -cmFscGhzY2hhZXJAZ21haWwuY29tPokBOAQTAQIAIgUCTDBpAgIbAwYLCQgHAwIG -FQgCCQoLBBYCAwECHgECF4AACgkQ7KCBqXkNT8RRHQgAlacmTmvQ2sti+9Sbwmzq -lLwb0Lp9T4UXb9rmPt3dwpNOQmT1FAtjiGBjJ4/lm0wra4f7pQUOyhLr9ZmbD7QV -m6z3/SjyBajDAGPQbmtASjhl6QU0EFetJhhIpiznuvCdX+DDltV3uMWHMdIp/XOJ -IO6nu8guV07k85Ud0OmjXyBfqV54HbqQL5a9rEqYuBWDMGlQGjjbWbrMe0SZZUVt -e2g9z6Ki4oisSvBS7ZQ3x4Uzz45fcfWWqIKstzyjfMJ5obITkkHXqEO/CQ+uR06f -0NoB2CYGe59Ee4cRJ8R32pQJ1bfw4ehQsnOrcB3bj1taLspkPn/RG7+qYYVck1kU -z7kBDQRMMGkCAQgAyLpmVFisy7DFYkTnvnDjAzkmjvtBKyjYytG/Le81L60wBbVe -PhVAq1MaNikGtdm5ofjvQ9j7K/z8Mpn8KYExsNkNcrREZcuBswlOJZZvqsSDy8RY -EUQ7rR7SgQEqSpBCqGVe96q08hoam0D2ohviokocoFr6JiGXYaXTnYsJKwZCgY3I -r1FxVaXvkqqFwLGvPlGL28L6un5GTbplRJThOujaWtiOpjWt63GnJ35ukpOOKxtO -H0MoYZwx8A/IN8J9nH7fR0c6y8pgloOcrDFRBacGSUJtxdy+qK/R225xF8eudO/G -CXrVyxM1oMGL/MtnXfU1euZl0VENjLyDE+G+DwARAQABiQEfBBgBAgAJBQJMMGkC -AhsMAAoJEOyggal5DU/EKtQIAKQW/u8UPr7fBs6ir/xSURK649reQdCREqEoZccX -nwRUJAJwz75g2jNJeag/4VjgAVmoKELu8PnHBy5TeTeoDKhVfop8iwIZazdWG4ri -lTWIdVdqq4viw7jktaVSyPo17wUJc1+YgKMprSk9fNptU9gD00P0RDcdrEmBzDe6 -mrZXlmNr9ecOU3hJRrAi6DDMdMsBCQTqownQqIKLX13akKk63TaSzO1/y/UcmLbj -VbGAJ2YeyoirsXbAm5565Esrd+ZzFtUlYySFsma7MtE3Ry/U259VP9kcx6hET6Xq -Q2QTcPh8Hht67/q6SyQGbHuvZZ17kvVHvYBP21xQPgPV5Kc= -=ane8 +mQENBE+ZO+EBCAC3fZOOuYKthr0GcUge0PH2bh18sbM9XUmPKQz/W15l1NA/2ARS +2gUXM0R+SunMlun9KsqjnojJ2ObVPvbm1Hg/66JSRgR3JWfIpSlJxLicpfu8rCfN +bOjh4v9ZipD+px8w3o/RNrnZH/KRsoJg9yER6pf+pUZqTJfdg5lXezc1WF+/1qVo +ypldMGfrkfLsPrUZTT689ubbig978e7eYmJEqldtaIwaAzHQnB70wIJyg/rEwFUM +ldsvs6t6czSuJ4zPMvmh8TMpTg9e6+DMktPl1CWRONl8RPpgYMIC96gb4OnfDDjk +Ex6clSCwgbDwdeAyOjjR6pVq+pCNTo1Pcj5jABEBAAG0GENla2kgR3VsY3UgPGNl +a2lAcW9zLmNoPrkBDQRPmTvhAQgAtrGiCYnW3tqvDzaStXsguVw67pou65dO7LTc +rX+NTvejJZ9SrC89JsfiKBwtvyS3X/qiB+S7RP21PH7SYOy+orwDw1nacNNeiTdP +nxQCDQVNeWpSpmbLlA+0b6K3aPf/EaCKndXmnQyXVOoSXZJ9bqAe0um0NRbO7M+L +1KArVkWW56ms+DvHAeZaGnSDDHQpJI5haUqgSWWP/VoPEU1x0qiBZwY3lokSwRMI +SC4E/uiUvvm7rvfbBzfOiVrjNPLlsVPiQRgOTfQO7dUZAmt2yqWJt1Clliby4fgB +VcOYUx0QCMiz8MZGtSB17+hSrC2Cb1T6n0ypxuYyh4sV2LtqMQARAQABiQEfBBgB +AgAJBQJPmTvhAhsMAAoJECx7EvKlEeMlX0UIAKS+4ZAKrGG9jbWfzTTDbu9zzkXg +V13suMD+XcGz10DkdluTUBXj8wWlp289fXNm4E49ipsNK+dcZ+gOATjUvb1Llh6D +6bHz1QM7olxBCeU2feTmYYKBH8GYY9JZzfAXNMQhcNiiPj+ntZqePy/EFA4uZHM7 +We7vl2c7CBcDAq1NNeEczo0KvG7AWt6QoaMVmbvA14EKadNzrmEy9apkag1BKvwz +XInYCvIHMa9ZqicOSUcI5QCYu5TufvIE7Eq3Khh2Ex1FiOaEA+57LMrt6NsSKXrB +8JNYbI5pqE1rxJXZnYtx3ZpPAAEfLjPdi1AOkWhvhsoPmiGFC6ebYQ5eVbI= +=xA7Z -----END PGP PUBLIC KEY BLOCK----- -pub BE096E29EDB8D141 -uid Eric Lafortune (Key for signing ProGuard artefacts) +pub 33CD6733AF5EC452 +uid Dennis Lundberg (CODE SIGNING KEY) -sub E05A9780475FAB55 +sub 3624EE1473A843C2 -----BEGIN PGP PUBLIC KEY BLOCK----- -Version: BCPG v1.68 -mQENBFFGZXQBCADeZK9xuCrDwJ7v37y8RITlchzBfJEWv7cSbrSIBlFNAsUUoshW -Y8U6xYKe0GdiLVta2F8bzs0Si4LcDeglQNi9Fxvh3/jfs0MEJUfSeZ4z1Mn5WY35 -utbdlHe6i+7qvhpHveREwWoWuvFGKeSCV8SioolG1mzxrDVLkQefPZ+Kr+xd2cYZ -SIkbjdhCvJHcUkD9gs8Wz9q9eDyc7dGJmfBYaQ7Xa6lbd852qj8LjQ7Gd41yHU6T -U3oo0kKfAPFkW7rX4zeJEBgpncK0i4ertd5to/y2hauqPU3VMExqNS0/L2GqKNmb -Q2Me02pBSBCrLLCUJkm8jZorPNxlQxHpFLHDABEBAAG0T0VyaWMgTGFmb3J0dW5l -IChLZXkgZm9yIHNpZ25pbmcgUHJvR3VhcmQgYXJ0ZWZhY3RzKSA8ZXJpY0BncmFw -aGljcy5jb3JuZWxsLmVkdT6JATgEEwECACIFAlFGZXQCGwMGCwkIBwMCBhUIAgkK -CwQWAgMBAh4BAheAAAoJEL4JbintuNFBUQ0H/1ahtWOeS+EEZqUxxgLE3aiuw6XR -mcFY/9kRlnBsKpWGGyV0DRkLThXSV5fWwiP0+moef04Miv81txVXR3U5f4MY3U5J -SDgeB1GK+BvcYz1CXxGcErgaMIDXu/K03OyQNp1lD2kjmDDz8SHdVqpv/aFfxWpL -uMaogbStXfbDiMjeMg2zRcAzXqu7JlMt3z33uiTUZk9jubAPOBnP5/Yhuc2wcnfu -cLUUL9xNE6gpl0Noy8qJ2TaVoHLfDEdSgVbLDNpippNQCIEXn8VtlBCG9FBtTUR7 -z8fGU8f+ijNYHMa6jNsDlSo45/RV+alQSSZhx3hPCIgO2klEkawBs0ikEjS5AQ0E -UUZldAEIAL0CmXXJLZWMbKXEROAYFnUqQ4eNLUcLY1NfL9xrF2KP6apMXLJdftwz -JtcenfD5vra8w5JJBn6cvY4Dr4zmueDSsN3Cpgr7c0trDNspAQMyUGmg4Cw7lAMr -uIX2om1TTa5ALsQ1PfupnLFLFKbYd2MmfwEVHgEBAzqmr89RoG/NnlV4uxvWumb/ -OrLzzizGLHdLFjnq3Szl6wQPJwmxCtWHvzW/Y+HYclm+wp0sufvrXJ/G1RrHKxwh -xaDVyLKsb5wkUC6SZmE477SP6ZCokoAGHqEK+5JnM9MYXreQ7N6NggRUseCokFaq -ckze5wVBXmPuVAg76ScPFQd4JzLkGzUAEQEAAYkBHwQYAQIACQUCUUZldAIbDAAK -CRC+CW4p7bjRQSkUCACPWdIvMXPtSQFnFR8oUIGxvcp9MYdealHNYkf3qgJTuWbH -TeD8WpE53immju4hitOq/qDevKOWhcfSaLqCtdRyYQFFDWQt4SsiGdWfCvKiBMSM -nrDQI9f+XCv7J3ute5alyMgv9Uo1XLyyiwhz9kJ9mzg+ENvy152PucI4TN63qMRQ -MQbkAenrTzIer+5Q9ZY4TILC8pVVAIPQjTHuhdh8VMclTdkHab2PCFC57/3djJyY -44PLhN3hLI4CElAGLUwBZLRa6OFOvwW5naivwgk0zeeTqq/jTOWPYiy2dyyX1pRX -8bJh5et4mXyM7HLHRp+FblgWxpFWNvMCEpxujLsG -=B/UT +mQGiBETvOwgRBADbar1BLaiJnuVnDEg0aej0Q01fdOnMB8e9fxe3TJZ266LLGljS +FNekCafvn52nx1KyVvkgdgMxqBfw70FKQXdrBBMzowuVAz1ZAcpDjkXeyKa3n/iW +J7VtuhdhIE/+rUiE1go2vkQhdIaad8om/kQDsovbgqxfX6eU2hWL51bJZwCg59D5 +0lXm78y8dlbvGaW0EVdgBesEAJ6rcNAA6rjsi7BUXNIpZe+KF/G/slcLJETgylmw +g0vquZP7n0fVhZZqB68zSmTcukxo36rd0jr9eSlhPj/6j9xs7gpk/UFWLWsziZDO +7kZVLv58v6ktK1Dk8u0F9o75pDBjgxOGR7VPVTblur1dIJ+U14ffJ9fn6wzKY8hx +hZrgA/wIpuJ/aSSns2ccKsErDMPRJP/TGygvrb1Mpfk3tLeGF7owI0sL8L8adKMK +g9kT/8VFoLzeSeEwUDOKDVm2xB/A5pazoUcgxLdwYs6g/XJzA7y4Sbfcac2W5IZ0 +4WGGUobf5Gp/b3NeVuff+V/2UN1YIr/hlOnBOMQDlNie4loQhrQ3RGVubmlzIEx1 +bmRiZXJnIChDT0RFIFNJR05JTkcgS0VZKSA8ZGVubmlzbEBhcGFjaGUub3JnPrkC +DQRE7zskEAgAi+8rrcd671s/hYlH3mgNtPbmb6KY21axOKIkwqwVdFoG5nbO5Sss +W99d+naj6UDJ+x+mbF8zTb+0uFgEnHlrhjJ3cOSzuSPZ2UY+5X1GJn6tkHN4e9kn +yQzoAFKNoCbnYHzbMkEEyKpziDzcyAuFHEKp1JoHhXD465eX9b/RFcSaaXmjttiG +n8bjGhgAiRXD5GAP/BLkD+nW0psTbLTraJ3GP+vVka1kwnDN0i7yvpvdSjntNi5x +s+tljAtePbs4ogWQBeQV3xFD7GwadVhH/3yHRlISuEUNRO/DMK4YFsDYjU+zaBLk ++lDka38QnAIjQYxhe8eypB8EuiOarDU3dwADBQf+KBZfOx/iCvuPcV3LFiUseNOA +bF6xRbmAoJaqlxIy3E8fX0oiuEp4OVHz8U02BLsBtH4LIpiEj2/gyELx0P25GhS3 +S5UtfkNvcG2AfNZOtNlWW40GI6gj2KxyJqOaXAkfW+WcPJrgd+gH+9JpJzVfoTJy +qkq6r61r6UNogQI2nrhroPjv225TyA8xFuTyShdhRo0WoMP8tqZxDiWXsGzdFciP +aLJ3GPwevVT4dCpbCvR2XHh/vVuytwC2p84O/Wrh9TUoT35Xe6vY2zkgGg9adhXd +j47MDZG0BWqFs4TCb+yfQ4PZxLgcdPaTzb39m9Zj9eSITnbdZ0ZoQKIXkp/crIhJ +BBgRAgAJBQJE7zskAhsMAAoJEDPNZzOvXsRSDZYAniVhMgZQDbg2AGd7sWBAvTK1 +F8GKAJ4wlBaXv5JsdMMyJvQc+xtjXg7OVA== +=AG3B -----END PGP PUBLIC KEY BLOCK----- -pub 15C71C0A4E0B8EDD -sub 891E4C2D471515FE +pub 3B17BFC1FCC1B644 +sub AB1D1A77026CB606 -----BEGIN PGP PUBLIC KEY BLOCK----- -Version: BCPG v1.68 -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 -tiwy+wOBjaK9g3Kk8jsN3I8t -=8Tiq +mQENBFro+cgBCADCxtYd7ZFChfyOnmp/rCtWS0DGY48wCmJTcgOyBFuTy8UJ+PdK +e8aBsbpx6I5OM2WqvsGkqs/vboAbZxJ+G0br0qriXrz2SUQEJ7EMJJsUgkVSb/iO +DEiqTr+XASuqVoCKsUnB+7eCWycsfODVYZbEjo52PZIJwiNS2yXBtYjf6t6i1rx0 +KrMF9dy9ZRWKe48UGK4KkZ5l9/yiM5CscJM3Wt5fBdFKpp8JmJQHMUivwURjT7dP +o7ZDBLmvgs1/U55AJw9Uo7FwmNSoxr77OtGFaN6rmQS2w1jEt0ieujmuvj+z91sw +FKQ8vxVBSLDtzDNP4NMA4Ie4G4sLDAzi0+1xABEBAAG5AQ0EWuj5yAEIALC3ywW3 +q+flFxeSpSuhLPNrJ1xzp5Ox8sZ/56f7PBEHYR4XAqyF5RLTaslHjxVkdq7JfQsi +saoW48ToFGU4wt21A9Wgh+k6LpnO2CLPvZZmLmXYvikoU74JReCaLjxWLqgD5aQG +6LakGNGgB4qHngFhccJozJn7irGUb2rJ58TkwAfbvghIKOvp2RI6dInKFEuIeWBz +98v5xZp0HRbMnq0bd4RqNGXdJ3yJ2VyC4nVCw+IaZafVgI8MWSaQCiOgwVgS73kL +wgX3xTFfsKMie8Kwo6zCYFyFekKdImG7w9yVavQmgSid2K+Xkb3qxpfjTZSM7qsK +23nL/IoKChLu7eUAEQEAAYkBHwQYAQgACQUCWuj5yAIbDAAKCRA7F7/B/MG2RF3+ +B/46dH70jqBWl/8UtnV4/Tb3hNXb2vU3w7BTawg7qHmUPj0R6BBtRPQqvPxH7K62 +8ARhgBdEaVKMDA6ijSet7zJm2mU9DlBz+Yunz5a1jlDN/vXAzOQyA7jEd9iyI4Dn +lWp+0Gy8EXEysV1mrXRQd0EwRp8kyozLpjAk+P3Xtu6OEHEWOJHZ67OzVLx/bOfU +DIn5Sv8Dxi3cmaAj5ZjnCfM7a35nseGhFcezd+HjQy2v7W1X+zCJaDejdGe32pPU +5Bx0+rqt7BzGjCtQ9N952neH8mAeLLhcpVQQqHx/YjIqJ1qfMPX6vXsur7fxJ1t0 +oY3gZlwqvKcRwJwExFAQotNX +=Ql65 -----END PGP PUBLIC KEY BLOCK----- -pub 80C08B1C29100955 -uid Jake Wharton - -sub CF771F914C2A4A73 +pub 3D52C2966A51F8F4 +sub 888C89049BDEB64C -----BEGIN PGP PUBLIC KEY BLOCK----- -Version: BCPG v1.68 -mQGiBE2fCWARBAC3v9wYo5kmynmVP+43ccamidflSLQjjpsXpSDLPFokGxeuw0OC -QJy46m8b5ACoCqRlfwnRRcEHxiSlaBATJA6hi7NRO41R39C62JXsIxNJR16JNQ5k -oG/NOAraw0E1RQIFslznQexfxPg4yFIVrsFp1wkpCRrCklatPMNap2DuNwCg7PWJ -1vV93YIsaH0O2fnXz3E+6zsD/3cTzUWuySEDiCLNO3JYJm97v4NDQ93encP1Ooxh -n+PSIP4GvjrAObh3FfWUucv8UGqcw5bAL7dA1z8SgKeyFk/afs2XofXdvC+PhZqC -DwU0NiE0D/tDWqX0qIG4ezTU2uk+5dE/WVl3R10nOBgquQdWIdYKGfV4FNTiEduD -Uw7fA/0XcwFom7eyR9eBonQmgIadljztm4gkv11lY33V1ZfJNndPKNzwevDwX+om -/VEHvpEfPx5toD4H523BPx55ZtfowuMtFHZI718alpCo3h6xaDhGwXvsg3s9k03k -rfxzCjf9qcJX1gb2JVZ2+2jCwUDQZeEwV2vivjGNiN9rShWW+7QkSmFrZSBXaGFy -dG9uIDxqYWtld2hhcnRvbkBnbWFpbC5jb20+iGAEExECACAFAk2fCWACGwMGCwkI -BwMCBBUCCAMEFgIDAQIeAQIXgAAKCRCAwIscKRAJVQoeAKDLeCOKKnt5aqlHD/7+ -QPOgX4iYPACgvGRCubBztEBkU2xXbr9J2cQY5em5Ag0ETZ8JYBAIALeU4dgFKEn8 -b3E1LUT25svvUUX2kuVLQ5TzokC6zjWQrPehOlsYx+ey8RSN/6A5rBLtn1z75ror -F3+vN5kLPAz8x2Th6Cxd6lvn3DQHfvDL0YHlp0AZzhHx5ogyJxdaqwzNy0JWxava -FGN6b8bbrR1Nf08kq/d5dNe+NGVdBXcuqUo6jt1W+tkbntklS5e+B9/Q/4L8dYkl -8xXRIMAuKw6h8j4LFXse6xndWeWtTIkfZImz0f7cLQV5X7QSv90i1KfzT9hLiUjp -wqPydtTNuB6+gda64jFNhVcvgW+h7MH6FT/zSss2Jh2eDR4HFrv7EJSS8dKCvPox -y8El8pQMG/8AAwUIAIXq+ryw44voH/IdbvTQtK4t5/lp/Oj9b7QrMR3u/iQxM4K3 -dq7rUbA8hjdMM3gfMu13B4pzGtpcf25b4NIIQkhES9+CUDQDYHF+gwkrwEascbdf -YZiLAHtcMjW1Og+IhTY8wDuJT4I1pqumSzWV8UxVJctqNUTY2eU7158oD5+prdCc -eQLbzxoKFtu056kZ2+46v6bAl8g/w8WoVORUuHruuvKohMVPHwNQws7KHCFy9YCr -4yIQfqC8g8oITqlgUZcZaXx6K+NndwA6LAoXJL0edaZOZBuFq5H7Fkrw07+OIQFq -LTGcFpR12AzDh9lmtyJQ8nVfb6IjAOSwBPlEZWSISQQYEQIACQUCTZ8JYAIbDAAK -CRCAwIscKRAJVd58AJ0YA8xv0U5Odm8u21BvyxjGS3tBZACggF6P6KjhUPpIAYTS -6v3TtzhkJdo= -=Zirr +mQGiBEqVGHgRBACyrZHA3gvzM5WeoD6WwQSIPdTPuWw5EUonHytf4m7pgX8Xkj2G +enqbzW70POgJDUTUpvHDSCaAWiPY+ld1h0j+ghXgKD7IQjqFQTvnkdBT/qbOL7P1 +Umv5OLGSKeyx/BhxNiD0uF/huyP9b1HWC9dbIdVebhDcyBDkBAFtJMOxjwCgjnxw +sUI5vJ5FMPDjUdRPTVQxwlkD/1PGN4Ycdn8ncmZ1OG0T+vmL+C+yGMIdbWIizffC +Li05HMziwK33NHk5ABkeyIijm4Mq7JUlSSwt6gQXJTGlidHEcmKqSTg4AQhybPL5 +w40KcjNyAeQhIFgHAfMzy+Ikxzlt8fNunMTf6utWrPq2IU4uE4lJT4kSL1ic89vx +a4CLA/4g4MJVuzTegJXS5vhMnEI2Nhmc2MIAsEhPcIwJjhUN4rJJMcVxQO+Cmrfl +0rcosxA6wtjFDdPcuXYiAoJBJQrVYp6+w4GyHW45/WR6iGas/Rm0ZRQqnf9WnlpX +o5d7uEwzLfck4iVw+z298y6qk52coFKaKPH0bGVxguifMNdbwbkCDQRKlRh4EAgA +oSWTikKPMX99ZIqYgjAKlM82x6H5xxkisLGwI13m/nvRgOBy8gYzfJCdxrEupnvN +k4PK7twUka18oAoaKfKFBdIKQ0bSyUNV/RzkJDhkBCCU0T8DbN7Xe4rP35l1PvSK +OwtGmIxovip+Spm/j38KKnSTWyZ0JIBTOYEoB8wzyna/fcibwIy/M/wj+lrurXE/ +6QnAxqJjAsL2MtuTurC5IMfRPQqUL0yMk+l+wUpuBaWhg70NhbsTisdJy9TP+Nsu +7tCJu3GumEuYKd3E46xIFedyA1etFHr9n7GkjQkrthlqaza2bUFOv2xOpTJOvZYr +15zOCpHslcIQiWT5zWCQcwADBgf/WBNDvc3jNmAyTP3nLRbA2JP+tVBVwdQkHktY +4EjaLC7HjUA5TblQFTycXtZyKJ+LUwXM2W47h4HMXhSRq7E/wzMUHGhUMBf21MwY +//8mirKgRiU1rkArBKtr+bHTpsJ1UEoCaGzXxvTGxWZSLLrV1nBvbBXXOOhFKvCy +mFcudN12i1MfBrubGNT905obseKThEc1umWzeRRWOlFEZkCONLR9PgSwCok7TT3O +lkaGKRQB4cu54e/faOaLvFh2BgqSIoMgGHfVLJBTbssV6GL6T4YoU2Bq0wtLZ2k9 +tMgJHvPziWrk0rI4oesVWDmbEoRgAsYlTkdev0u98g3c5zAHLYhJBBgRAgAJBQJK +lRh4AhsMAAoJED1SwpZqUfj05WEAnRNjXTAHWA3uXnDtK4XDHeCkqzUIAJwO+CnW +RTDq7K7Q63sS3rHl9X9ZgA== +=lGHK -----END PGP PUBLIC KEY BLOCK----- pub 5F69AD087600B22C sub 0440006D577EAE4B -----BEGIN PGP PUBLIC KEY BLOCK----- -Version: BCPG v1.68 mQENBE7JURcBCADO+9Dc4/JnB+wX+fq+Fr2zUGSPOT6/qjE5kXL4FEbJKsqDSAKG VnbtRrsIUdmNIFQmz71bBDFhRBbrSrkz927k8eUPhYtxE2NmmWSuKgrjF4qviPQv @@ -479,45 +621,9 @@ hsWaeXOM0n2j759uNb/Nd2XA =2J9s -----END PGP PUBLIC KEY BLOCK----- -pub 2C7B12F2A511E325 -uid Ceki Gulcu - -sub 10DA72CD7FBFA159 ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: BCPG v1.68 - -mQENBE+ZO+EBCAC3fZOOuYKthr0GcUge0PH2bh18sbM9XUmPKQz/W15l1NA/2ARS -2gUXM0R+SunMlun9KsqjnojJ2ObVPvbm1Hg/66JSRgR3JWfIpSlJxLicpfu8rCfN -bOjh4v9ZipD+px8w3o/RNrnZH/KRsoJg9yER6pf+pUZqTJfdg5lXezc1WF+/1qVo -ypldMGfrkfLsPrUZTT689ubbig978e7eYmJEqldtaIwaAzHQnB70wIJyg/rEwFUM -ldsvs6t6czSuJ4zPMvmh8TMpTg9e6+DMktPl1CWRONl8RPpgYMIC96gb4OnfDDjk -Ex6clSCwgbDwdeAyOjjR6pVq+pCNTo1Pcj5jABEBAAG0GENla2kgR3VsY3UgPGNl -a2lAcW9zLmNoPokBOAQTAQIAIgUCT5k74QIbAwYLCQgHAwIGFQgCCQoLBBYCAwEC -HgECF4AACgkQLHsS8qUR4yVwIwgAlj6R9Lxk/GnclmSvgtTyk1r5GxJ75iSCzs7q -DlNuM6EfFbuZvLWORGgP8KtdQdh/bdICtRbh2eoW0vEKJKkl0ZI0/RnVMYKYpIHi -KVUSjPq0svwDk5tUa7taNSdHfi9o1G1doBHyPu1u1xDVzMSWsbEvx6aVr2HkBDRy -A+VcmnX6tokZjyRX4C9ldX3tR+ffn7v2Iy0bIesM92nMPnH/fPXWlnbQKTxQeMj2 -qYSl3H/NGp57G7KNSl1na5U8ueMcUtYb+eHphMsmaEb9xw8dM96CzIPZh1xMHNoQ -NNJibURysbUpFlGUTOv6K0A392QkX3GwuCQGOeLWHOoQHD6Jt7kBDQRPmTvhAQgA -trGiCYnW3tqvDzaStXsguVw67pou65dO7LTcrX+NTvejJZ9SrC89JsfiKBwtvyS3 -X/qiB+S7RP21PH7SYOy+orwDw1nacNNeiTdPnxQCDQVNeWpSpmbLlA+0b6K3aPf/ -EaCKndXmnQyXVOoSXZJ9bqAe0um0NRbO7M+L1KArVkWW56ms+DvHAeZaGnSDDHQp -JI5haUqgSWWP/VoPEU1x0qiBZwY3lokSwRMISC4E/uiUvvm7rvfbBzfOiVrjNPLl -sVPiQRgOTfQO7dUZAmt2yqWJt1Clliby4fgBVcOYUx0QCMiz8MZGtSB17+hSrC2C -b1T6n0ypxuYyh4sV2LtqMQARAQABiQEfBBgBAgAJBQJPmTvhAhsMAAoJECx7EvKl -EeMlX0UIAKS+4ZAKrGG9jbWfzTTDbu9zzkXgV13suMD+XcGz10DkdluTUBXj8wWl -p289fXNm4E49ipsNK+dcZ+gOATjUvb1Llh6D6bHz1QM7olxBCeU2feTmYYKBH8GY -Y9JZzfAXNMQhcNiiPj+ntZqePy/EFA4uZHM7We7vl2c7CBcDAq1NNeEczo0KvG7A -Wt6QoaMVmbvA14EKadNzrmEy9apkag1BKvwzXInYCvIHMa9ZqicOSUcI5QCYu5Tu -fvIE7Eq3Khh2Ex1FiOaEA+57LMrt6NsSKXrB8JNYbI5pqE1rxJXZnYtx3ZpPAAEf -LjPdi1AOkWhvhsoPmiGFC6ebYQ5eVbI= -=9O4j ------END PGP PUBLIC KEY BLOCK----- - pub 7457CA33C3CE9E15 sub ABE9F3126BB741C1 -----BEGIN PGP PUBLIC KEY BLOCK----- -Version: BCPG v1.68 mQENBFIXyRQBCADe285y3Pu7KzoKyP6wqeNXtvvuwMatAmPm5x/i+S8MlryqzsYa x6twUmXV1yKjjtGrO+9fHvTOWBfSSP+fP9KTaTQYSasoJq2Mw4cQDy1i0zrxNZUw @@ -546,443 +652,62 @@ Glx6pKOJQ05u+9X/BzoVWrw= =ajY9 -----END PGP PUBLIC KEY BLOCK----- -pub 164BD2247B936711 -uid Marc Philipp (JUnit Development, 2014) - -sub EFE8086F9E93774E ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: BCPG v1.68 - -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 -Qw== -=iceo ------END PGP PUBLIC KEY BLOCK----- - -pub 1861C322C56014B2 -uid Niall Pemberton (Code signing) - -sub 9A347756830C4541 ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: BCPG v1.68 - -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 -co8an9pO9/oqU6vlZUr38w== -=g8ME ------END PGP PUBLIC KEY BLOCK----- - -pub A1B4460D8BA7B9AF -uid Mockito (http://mockito.org) - -sub BA6D22590B3F9BEA ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: BCPG v1.68 - -mQENBE4waOEBCADHDHNTq1NRR5TSooIrKY0BTQnaLfjKZfcJOwp+btBJrOUO7+e/ -V3M4DZQclj/e8SBiVmRPK8Oyrv6i5B5+Ee/qNlLjWiO10AJ/PLRjYdoW1V6PlTm7 -2aUxQ/wNXxDcOoH8M7Y4fEqZJ3rFuta8ogeIYnkUwUk3gc5YKk7KJDbiOXQfRxlY -9MeVKyoYfj+2Szaaz98W097Pqf9+7i3WjB3TKR1pnWlzF6rLWIGBJqcJuTGLWUAB -hwOMoNiIBAqyGu8E4rT8pP9OueprOeIjyKgOBWV5yctIxd7uu51sGR47TM2wmMmm -r/Kz8FkMr1efof7Bx4mJuup6wQXonRMVBqpLABEBAAG0M01vY2tpdG8gKGh0dHA6 -Ly9tb2NraXRvLm9yZykgPG1vY2tpdG8ucGxAZ21haWwuY29tPokBOAQTAQIAIgUC -TjBo4QIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQobRGDYunua/0Ewf/ -QRv2EXl6rlgV9LgXifdgD4ctYsMvhmJV0uaAaTvTcNpbYL0lG8FpCs0KHvBLWkoq -esOdJYT4q/9msnBAMXS/9ckteDism8BwZR0qi9qzASzzMJEH7cdpr1te17JR0fl7 -8cgbGyw6UGfRSay9c3HyV0rCuQSFPMwQkG5YVpRiN6NSnYqDczS+aSjd6JCKu/4I -LVgu4jH8oaUcGJEaWLmExSUzrUv9HFZXQJxLzrFT4k6AVVzBQCuEYW9XtgosKwfo -0GweBXkC7iuO+bfNi84vyRLTkK50h4+rVNkyvNlgqknbeGrpvOvGErYJy24API/5 -TNP+ZLCTl7Y+qQejTFVJ4bkBDQROMGjhAQgAxDjlr6Oakgs+5d5NcIYpJ/8S5BIU -aVlmjKoaFJoAy46WzvvpaTC022js1ZSvrM8wlSxSk+kD6/FAJw2pJwRgVrolOYJQ -mqLqgTA2QSN+leWo0x8gC1+QXzrquCOd7m/+h8FHUDaxc29XQ+7+HtNvsSxY9dtD -zgQGXjPXSppVCSiDNy1IuIAxmEAFSxok12Glxq/n2DFx6OnVB+5vvt0C8nQ6w0hQ -AQKPUmO7fNav5lZMfKgR95NrvF1Hu5V6DZvpbXg6+NWfA8LsCampARrou5qzy67h -b/+KKGHTDMOvZZlGw6oSnPngqmh06QqY7P4PKVkOo3Vb6+02ltyZ95RgmQARAQAB -iQEfBBgBAgAJBQJOMGjhAhsMAAoJEKG0Rg2Lp7mv2b0IAI5pDzLtBGxYhmO1rSDg -Y+JY7ZAr4srW/7K6Zc0RmWv48JsZOf7T78kIrif6jrF+ZwBLrdP93umMr9aJ/mJg -lr+0oag0ZVhEW6Jb7dgiRn8F9+qcCs6lyozAG805oW2lULkwqSJ1xNs79v9RtOWl -I5ruLVbS7XhGv0qH+ly1xpIuYOzNvpOgj1pC3Pv+NuL2hrLUZVg1vM9eCM3TDVbD -yJZrxKdMemMSGHHsXWHtn8t4BT88kice4weDq8GBl1Dx+ApO1+iHj779nMX1Drfa -xtJLTRqH9fhHesISmte8fPiydGT/HCA9quKUuxyN4W6Uwu+Uov12cGV2sAI+H5qP -hF4= -=FXjQ ------END PGP PUBLIC KEY BLOCK----- - -pub 33CD6733AF5EC452 -uid Dennis Lundberg (CODE SIGNING KEY) - -sub 3624EE1473A843C2 ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: BCPG v1.68 - -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 -v5JsdMMyJvQc+xtjXg7OVA== -=RVv2 ------END PGP PUBLIC KEY BLOCK----- - -pub 85911F425EC61B51 -uid Marc Philipp -uid Open Source Development -uid Marc Philipp -uid Marc Philipp - -sub 8B2A34A7D4A9B8B3 ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: BCPG v1.68 - -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 -bpJ8mdzS2PJko7mm -=7SBx ------END PGP PUBLIC KEY BLOCK----- - -pub 152888E10EF880B3 -uid Daniel Fernandez +pub 7C25280EAE63EBE5 +uid Oleg Kalnichevski -sub 1CB037909F7E059F +sub 926DFB2EDB329089 -----BEGIN PGP PUBLIC KEY BLOCK----- -Version: BCPG v1.68 -mQENBEtnRE8BCACzVmIkAALxSHQAbp5HCXCLqL16UnxJcrcUIcnRQSXiwjAcNsjS -S1mRwr0WrmaquTQYDk1W8owUdmAXdTfSHgtJKFRKdJ20IymeXx1VrmUiiCNY9Jf1 -xYhaJAopMTeEp65q8enAZcxjyqVxk7wOPgKD6uakQDdwiJtWHsug7zlmUtzZ6Phk -iCxhrJSasatOqQoJon+RMlsFYWLulDFv7CmDJq/GLS6bMDajKt+SXBsnKL/gUWZ4 -hT0sL4Ux6lkshyey1BSbmDbMnHmzR2Ng8oIzKziA900FFaAWsyg3S6vi6I4P/x4N -PfOR2BiUwcPi7lA6Tp0lwPM0IdiUep9Nl13vABEBAAG0M0RhbmllbCBGZXJuYW5k -ZXogPGRmZXJuYW5kZXpAdXNlcnMuc291cmNlZm9yZ2UubmV0PokBOAQTAQIAIgUC -S2dETwIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQFSiI4Q74gLNZ6Qf9 -G6Yr02yGjtoQEiKBhSRwyY44W+FBRLESFZ+M2yrVr74li0/1RVX0e0zyYUWQTWcY -6ySzuvWdfqBlEeh+o7PPV5VwoBp/xHvbL8ufEREqjbL/0c/4O24ojTEWeOb7nGKr -z5dxvy+fV6EzJfK37g3vYZoqy3uv1O8205UgzF84XpVW3pAmZRHz8G5+NapQD/nt -p+zW1PqPK1FvRFxcMDsFhE0F4qAPB5W1jst+E/G3eyOeS5OxXePYiLaSlJo3XovP -TgZKMZAu1WusPfrcTL+rzb8Rr/TQinb1jNkVCOyfqHk0Jfunda4dW8tgWLkau/A4 -eki1H5pOYEm7/lOQ3HRlLLkBDQRLZ0RPAQgArznjhUDrXc+Rj3JK5c9Kz7rIkRmj -fvXAz/fWjgPGE75brZPcicuSY/DIMXglvYg4Nn2ELpfcdpA/2NbI9IbvBRvMm7lz -un9mYlq4Id6kvpBhQbCg3ksyy6aailNB3oLN9Z3DFw2CnndDE9YuAkLHtSwR80yl -M1Ain0OhHh+cPU9eoQSTZiPI4nmNFoLlIUmLHIqmGIndEsdkWg+BczFsc7ARKLn4 -1xHVg7/o1TBleyxUrLVp2LatNWZZdqD4pZfeAalrrmgTJHP89X++Qgtdt1baMWio -mMUusUjxM+WuUSzPmYt8C2LG1dp8Dfvwr8DD98MrHwf1AkNfGGNSTWTWbQARAQAB -iQEfBBgBAgAJBQJLZ0RPAhsMAAoJEBUoiOEO+ICzSdEIAIkL3iyzDsl8s76qY5EI -JPodAYBeN85uA4V0tFdmnObUzas4Qkl8btGiYScQaeTBH+MepUqJiae59yOES2Yh -y6bf9t1+WNkYUbPqvazSQgK1rujjlpvK33szMwaAr4OsiCXgLXjiiyUihNTu9yOn -/kaV+ry1D1dVd05NgH/kHsGcXC7+bX9DrVYMzZd8vIDTK1cYEFSk/4f/ej5TkD1b -VyuZpWG381vO/zchFeEFO68wd7KmVuUpfemZx4vaG6u/wA9DCulCVIhScBIXmLdw -UWihEmfbGu3sZ3xuqmInxEJa8pPgjoTHTQ1KcUx8QDUSXg0Ne49z1DnRbB0ieTIV -dqA= -=llhr +mQGiBEPonucRBACtbhYckAoyz1tuSXYX4XiqGa5390gIMcxe2hJ+Ncx9o3zX09Im +f8PW27BnMrz7EIydgB2wphhjfK4vkNNtm5ZDWH/zJStsk1Fe7lNuuxs8XorX1+8D +bhhFEuc2B85vNf2o9Y4V5GFwbD+tFNy4u24n7zg6/VgE2WDvYJ8JRqCEkwCggyLj +ba0lsZ2XtSINh/W8ok+9f0sD/A8WhqBfDTEBuG9gnuCYXM0j7XBBPdPS+FXmmfea +zyP+URKRprLCdt0ThZAMllIxZJrkbv7aeXVpM6KSZ/XvvaFQ/gha4o4iJFvpoKt1 +Er2j4Tz/STKztHGsMt6pqfrMNPWovu4tLuLZQmojtbIk+IwmcYxMy99owH8oV1WC +U4HeA/9MlUxzmlmrQF7VLqFTGEEqQaEJqz95wNPj/t1DmI97hshPzXLD4zwKwa9m +qZJPStRHM0a6xW2dztF12aXhrmYg1gIGNnsHtq+t8ZhfINZUurSWn0m65WT5notA +15s6hwyDACHWWOgFQ9jmWuGDh0ZpiaBe7BxeTV+MsswY81sOn7QgT2xlZyBLYWxu +aWNoZXZza2kgPG9sZWdAdXJhbC5ydT65Ag0EQ+ifBxAIALIr1cwH89+EBPkuFk1w +rpJ5mIkhBJ8k8JPWsG+dr62JYaENrdhIUQpXP7UekG4TTIWjMT7dTmHrrTr2TeKt +teiBNksGcyAw03IWAKT/26wXSA3+C/1xlhSKn7Qkp/r529CwM2DVbjV7TcEZ4tqv +32d4NcJ4lTGydjKyTbdlYS6z/Sv4qRc+yQiYrbTJ4Jpgxe5hCoZ66gUTi7mxbFHz +Zs9akQuj4hMMsggnRvuJvHyIksqp2twjJGa/1GlX8bhmR1jPB2BpokI2n6ni6qPG +km1nq8w2LRdvr+IQALfXg8HYGMvKDcVxL8O0PhuZYsfuklDGFejSizVGwWH35vcI +1bsAAwUH/A2wO0ac4mVi7+wx40d9QAOZ5xhHrndHQkTvK8H0DZ2kL7iavLIkZlN7 +jwC9jcA4DGmtHE9cbe6eP/P0VNDfaLVap85CzFE7qqv8LUK0LdDlrBCUcsXplhcI +i/WaVOHk4OPXHzkF44Nqt328fQ9V6+gbvz5+1A1PK/Rmw0rRie0d2dJJIbQhK1px +jV27qpVXfLhLMnjNh2KTO+gZh//LzION5TicsoeiHdtGU59x4Fs0SOUGgydSWAYT +LlQoW0z1AnDKMfqoIh+CrUSAI7fpt8NAMY7KqNxBT8HlhqUX6jMolHBEreF/2fVd +jX+NTR/p0O+L90b6T+xFin/RXxLRaa+IRgQYEQIABgUCQ+ifBwAKCRB8JSgOrmPr +5Te9AJ9XRqxecT5oej+N1PfmcCTLZNfGiQCfdH3+RPBbKSQofz2bGx7/niTd9qg= +=dQMM -----END PGP PUBLIC KEY BLOCK----- -pub BEDE11EAF1164480 -uid Joe Schmetzer - -sub 4BE257B370130000 +pub 7C7D8456294423BA +sub 9842FE565AA0601E -----BEGIN PGP PUBLIC KEY BLOCK----- -Version: BCPG v1.68 -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 -33wjXAW0h+qq7/7Yq3w3/7ncv9sWIzU= -=NtIH +mQGiBEvsZw4RBADH20nX+H1xvMBYmXRj1Aae4dRr6Y6qI7QRWHO6Z7/dxr9bk/NN +Yjq5KsVOQxZzloVdtqx75rznT7fZq98g7Nq9IeEtB6k4tnh6XQLhljJMk0a3mzdt +q3VzdxeVbwCaPJ0zixv8XPTAH6MpRJUvP9XjzxwaYHrjwcQ1LslW4TrIzwCgi5rf +jChLCyKcaL05gqUjl4lmefED/iqOwYZw5pJ8+X+OHUViiOB43wsJt1brAhPj4KgB +ODStcE6WlHFKi7YzcYNLzYMebSGYn6bj65b3qNf6rybWD1hGUFK4122Q7+HCH9Ic +J+rr8HwjGFo/yxI0/mkyaF0BthXYPy4WtdsdTM2kgx8Zr3Q2rSt1jBPuV3q8d27z +FZMiA/9cWPkRx0RfAJmBPKmKkbBkEtBbNau3G7MY1OEAkEkRnzmnyyjr5IP84A7K +RdjTCvkbiQrOQH00Ki4sHIg+9Xv1gDg1XLkFDzRARKA1TxjL0OeS4RWF3iia7Swk +MOnTdhR50pjb18W8kB4mEMZY7duP4nwDfQwHMwbFZGHrjImaurkCDQRL7GcQEAgA +thrbFDDbIGq97dbX1ZEwNGpjILdeumxZzaCc5PgldHGA86TL2V9iPpONJtv7/Csr +1c/rvH0IA/BFu+0Kde9UP+z9JycmfJpIsd2Qtxfhn9R4+Td0VtN9NNkpt1+JcThb +zyQq4UU82uBKwSG/wzubOI4Fn+7ypIuNgDkqK0STIL3ucKF/RV77PsOBeLHkCGKI +VhUUfY1D51BWZKKB1efaVK7PLdnsi64lAhbaexA43dwUUtVwQOvDzJVtkycV2IMw +XwyHonlK+4vbcwIH2BAU46LIZy+VFaI+lNNmR9+xeWH49oJYnMWIXif4RzvO1ag2 +KKwlG2sacuVTtpWVqKayBwADBQf7BGl4POKtluAbX3a/KGnX1YyibPT2e4pdOxqb +KTwuRc7ILIK/LVcejVuLd+eOF1QmjToI7Y3qjlzvtaxSemfkxbIQpdus0SyHEEAG +H/qjNuTs8ZwEl9tPPrKaJHbJ9kQrlDPwkufA0NDAXTE76UhcHPY0DeykAKEw2rav +EqI7Y0nXR2WSmRLFChmQ+aNTm/9UGI/IoRdS/dgo1eaLD/7RLFeL5tGDe+2nj+B6 +j+52nZEmaRCWWU7O9jYL1cQu5WTurtneCLtIqehLsjuVz9ihBysYuUcTOV/6W0AI +clQoMEiFR9Kyv0owDk6LtX2DgibJuAvvLlnajsiGm5nyVh3G1IhJBBgRAgAJBQJL +7GcQAhsMAAoJEHx9hFYpRCO6iXUAnRcLw17lBhe/WcKFRkzETSxaJcjIAJ4/P1NN +On/eScLdx27sje7q3sBENw== +=TzHJ -----END PGP PUBLIC KEY BLOCK----- diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 70d6d62ab8a..b05055062fe 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -6,37 +6,50 @@ + - - - + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -210,12 +223,12 @@ - - - + + + - - + + @@ -236,7 +249,28 @@ - + + + + + + + + + + + + + + + + + + + + + + @@ -257,10 +291,10 @@ - + - + @@ -357,6 +391,22 @@ + + + + + + + + + + + + + + + + @@ -365,6 +415,12 @@ + + + + + + @@ -375,7 +431,18 @@ - + + + + + + + + + + + + @@ -387,24 +454,24 @@ - - - - - - + + + + + + - - - + + + - - + + @@ -415,6 +482,22 @@ + + + + + + + + + + + + + + + + @@ -423,9 +506,22 @@ + + + + + + + + + + + + + - + @@ -473,7 +569,7 @@ - + diff --git a/src/freenet/clients/http/FProxyToadlet.java b/src/freenet/clients/http/FProxyToadlet.java index 5aa36993d93..025a4d02d1b 100644 --- a/src/freenet/clients/http/FProxyToadlet.java +++ b/src/freenet/clients/http/FProxyToadlet.java @@ -936,8 +936,9 @@ public void run() { } option = optionList.addChild("li"); // FIXME: is this safe? See bug #131 - MediaType textMediaType = new MediaType("text/plain"); - textMediaType.setParameter("charset", (e.getExpectedMimeType() != null) ? MediaType.getCharsetRobust(e.getExpectedMimeType()) : null); + String expectedCharset = (e.getExpectedMimeType() != null) ? MediaType.getCharsetRobust(e.getExpectedMimeType()) : null; + MediaType textMediaType = new MediaType("text/plain") + .setParameter("charset", expectedCharset != null ? expectedCharset : "utf-8"); NodeL10n.getBase().addL10nSubstitution(option, "FProxyToadlet.openAsText", new String[] { "link" }, new HTMLNode[] { HTMLNode.link(getLink(key, textMediaType.toString(), maxSize, null, false, maxRetries, overrideSize)) }); option = optionList.addChild("li"); NodeL10n.getBase().addL10nSubstitution(option, "FProxyToadlet.openForceDisk", new String[] { "link" }, new HTMLNode[] { HTMLNode.link(getLink(key, mime, maxSize, null, true, maxRetries, overrideSize)) }); diff --git a/src/freenet/l10n/BaseL10n.java b/src/freenet/l10n/BaseL10n.java index 72e6bf2aec3..4b599d1c773 100644 --- a/src/freenet/l10n/BaseL10n.java +++ b/src/freenet/l10n/BaseL10n.java @@ -312,6 +312,13 @@ public void setLanguage(final LANGUAGE selectedLanguage) throws MissingResourceE * @throws IOException */ private void loadOverrideFileOrBackup() throws IOException { + try (InputStream overrideInputStream = cl.getResourceAsStream(getL10nOverrideFileName(lang))) { + if (overrideInputStream != null) { + Logger.normal(this, "Override file found in classpath."); + this.translationOverride = SimpleFieldSet.readFrom(overrideInputStream, false, false); + return; + } + } final File tmpFile = new File(this.getL10nOverrideFileName(this.lang)); if (tmpFile.exists() && tmpFile.canRead() && tmpFile.length() > 0) { Logger.normal(this, "Override file detected : let's try to load it"); diff --git a/test/freenet/clients/http/FProxyToadletTest.java b/test/freenet/clients/http/FProxyToadletTest.java new file mode 100644 index 00000000000..217f1bf60c8 --- /dev/null +++ b/test/freenet/clients/http/FProxyToadletTest.java @@ -0,0 +1,289 @@ +package freenet.clients.http; + +import freenet.client.ClientMetadata; +import freenet.client.FetchContext; +import freenet.client.FetchException; +import freenet.client.FetchResult; +import freenet.client.HighLevelSimpleClient; +import freenet.client.filter.ContentFilter; +import freenet.client.filter.KnownUnsafeContentTypeException; +import freenet.clients.http.TestToadletContext.TestToadletContextBuilder; +import freenet.keys.FreenetURI; +import freenet.l10n.BaseL10nTest; +import freenet.node.NodeClientCore; +import freenet.node.RequestClientBuilder; +import freenet.support.io.ArrayBucket; +import java.io.File; +import java.net.URI; +import java.util.function.Consumer; +import org.hamcrest.Matcher; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.internal.util.reflection.FieldSetter; + +import static freenet.test.HtmlMatchers.hasAttribute; +import static freenet.test.HtmlMatchers.hasElement; +import static freenet.test.HtmlMatchers.hasTitle; +import static freenet.test.LinkMatchers.hasBaseType; +import static freenet.test.LinkMatchers.hasParameter; +import static freenet.test.LinkMatchers.hasQuery; +import static freenet.test.LinkMatchers.isKeyValuePairs; +import static freenet.test.LinkMatchers.isMimeType; +import static freenet.test.LinkMatchers.isURI; +import static freenet.test.ToadletContextMatchers.hasBodyText; +import static freenet.test.ToadletContextMatchers.hasContentType; +import static freenet.test.ToadletContextMatchers.hasHeader; +import static freenet.test.ToadletContextMatchers.hasStatus; +import static freenet.test.ToadletContextMatchers.isHtml; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.equalToIgnoringCase; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.lessThan; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class FProxyToadletTest { + + @Test + public void whenOfferingFilterOptionsPlainTextMediaTypeHasCharsetUtf8() throws Exception { + BaseL10nTest.useTestTranslation("freenet/clients/http/FProxyToadletTest.properties"); + FetchException fetchException = (FetchException) new FetchException(FetchException.FetchExceptionMode.UNKNOWN_METADATA, 4, true, "application/pdf") + .initCause(new KnownUnsafeContentTypeException(ContentFilter.getMIMEType("application/pdf"))); + when(fetchTracker.makeFetcher(any(), anyLong(), any(), any())).thenThrow(fetchException); + createToadletContextExecuteRequestAndVerifyContext(builder -> builder.requesting("/KSK@test"), allOf( + hasStatus(equalTo(500)), + isHtml( + hasElement("li a:contains(open-as-text)", + hasAttribute("href", + isURI(hasQuery(isKeyValuePairs(hasEntry(equalTo("type"), contains(isMimeType(hasParameter("charset", equalToIgnoringCase("utf-8"))))))))))) + )); + } + + @Test + public void downloadedFileIsDeliveredImmediately() throws Exception { + when(fetchTracker.makeFetcher(any(), anyLong(), any(), any())).then(invocation -> { + FProxyFetchInProgress fetchInProgress = new FProxyFetchInProgress(fetchTracker, invocation.getArgument(0, FreenetURI.class), invocation.getArgument(1, Long.class), 0, null, invocation.getArgument(2, FetchContext.class), new RequestClientBuilder().build(), invocation.getArgument(3, FProxyFetchInProgress.REFILTER_POLICY.class)); + fetchInProgress.onSuccess(new FetchResult(new ClientMetadata("text/plain"), new ArrayBucket("test".getBytes(UTF_8))), null); + return new FProxyFetchWaiter(fetchInProgress); + }); + createToadletContextExecuteRequestAndVerifyContext(builder -> builder.requesting("/KSK@test"), allOf( + hasStatus(equalTo(200)), + hasBodyText(equalTo("test")), + hasHeader("Content-Type", contains(isMimeType(hasBaseType("text/plain")))) + )); + } + + @Test + public void requestingARedirectResultsInPermanentRedirect() throws Exception { + when(fetchTracker.makeFetcher(any(), anyLong(), any(), any())).then(invocation -> { + FProxyFetchInProgress fetchInProgress = new FProxyFetchInProgress(fetchTracker, invocation.getArgument(0, FreenetURI.class), invocation.getArgument(1, Long.class), 0, null, invocation.getArgument(2, FetchContext.class), new RequestClientBuilder().build(), invocation.getArgument(3, FProxyFetchInProgress.REFILTER_POLICY.class)); + fetchInProgress.onFailure(new FetchException(FetchException.FetchExceptionMode.PERMANENT_REDIRECT, new FreenetURI("KSK@test2")), null); + return new FProxyFetchWaiter(fetchInProgress); + }); + verifyPermanentRedirect("/KSK@test", "/freenet:KSK@test2"); + } + + @Test + public void requestingARedirectWhenAcceptingImagesFollowsTheRedirect() throws Exception { + when(fetchTracker.makeFetcher(eq(new FreenetURI("KSK@test")), anyLong(), any(), any())).then(invocation -> { + FProxyFetchInProgress fetchInProgress = new FProxyFetchInProgress(fetchTracker, invocation.getArgument(0, FreenetURI.class), invocation.getArgument(1, Long.class), 0, null, invocation.getArgument(2, FetchContext.class), new RequestClientBuilder().build(), invocation.getArgument(3, FProxyFetchInProgress.REFILTER_POLICY.class)); + fetchInProgress.onFailure(new FetchException(FetchException.FetchExceptionMode.PERMANENT_REDIRECT, new FreenetURI("KSK@test2")), null); + return new FProxyFetchWaiter(fetchInProgress); + }); + when(fetchTracker.makeFetcher(eq(new FreenetURI("KSK@test2")), anyLong(), any(), any())).then(invocation -> { + FProxyFetchInProgress fetchInProgress = new FProxyFetchInProgress(fetchTracker, invocation.getArgument(0, FreenetURI.class), invocation.getArgument(1, Long.class), 0, null, invocation.getArgument(2, FetchContext.class), new RequestClientBuilder().build(), invocation.getArgument(3, FProxyFetchInProgress.REFILTER_POLICY.class)); + fetchInProgress.onSuccess(new FetchResult(new ClientMetadata("text/css"), new ArrayBucket("test".getBytes(UTF_8))), null); + return new FProxyFetchWaiter(fetchInProgress); + }); + createToadletContextExecuteRequestAndVerifyContext(builder -> builder.requesting("/KSK@test").withHeader("Accept", "text/css"), allOf( + hasStatus(equalTo(200)), + hasBodyText(equalTo("test")) + )); + } + + @Test + public void requestingInvalidKeyViaParameterResultsInErrorPage() throws Exception { + createToadletContextExecuteRequestAndVerifyContext(builder -> builder.requesting("/?key=INV@lid"), hasStatus(equalTo(404))); + } + + @Test + public void requestingAValidKeyViaParameterResultsInRedirect() throws Exception { + verifyTemporaryRedirect("/?key=KSK@test", "/freenet:KSK@test"); + } + + @Test + public void requestingTheRootUrlRedirectsToTheWelcomeToadlet() { + verifyInternalRedirect("/", "/welcome/"); + } + + @Test + public void requestingFaviconIcoRedirectsToStaticToadlet() { + verifyInternalRedirect("/favicon.ico", "/static/favicon.ico"); + } + + @Test + public void requestingFeedSendsTheAtomFeed() throws Exception { + when(nodeClientCore.getAlerts().getAtom(any())).thenReturn("atom-feed"); + createToadletContextExecuteRequestAndVerifyContext(builder -> builder.requesting("/feed"), allOf( + hasStatus(equalTo(200)), + hasContentType(hasBaseType("application/atom+xml")), + hasBodyText(equalTo("atom-feed")) + )); + } + + @Test + public void requestingFeedGeneratesUrlPrefixCorrectly() throws Exception { + when(nodeClientCore.getAlerts().getAtom(any())).thenReturn("atom-feed"); + TestToadletContext toadletContext = TestToadletContext.builder() + .forToadlet(fProxyToadlet) + .requesting("/feed/") + .withNode(nodeClientCore.getNode()) + .build(); + toadletContext.handleRequest(); + ArgumentCaptor urlPrefixCaptor = ArgumentCaptor.forClass(String.class); + verify(nodeClientCore.getAlerts()).getAtom(urlPrefixCaptor.capture()); + assertThat(urlPrefixCaptor.getValue(), equalTo("http://127.0.0.1:8888")); + } + + @Test + public void requestingRobotsFileReturnsADisallowForEverything() throws Exception { + createToadletContextExecuteRequestAndVerifyContext(builder -> builder.requesting("/robots.txt").doRobots(), allOf( + hasStatus(equalTo(200)), + hasContentType(hasBaseType("text/plain")), + hasBodyText(equalToIgnoringCase("User-Agent: *\nDisallow: /")) + )); + } + + @Test + public void requestingRobotsFileWithRobotsFileDisabledReturnsClientError() throws Exception { + createToadletContextExecuteRequestAndVerifyContext(builder -> builder.requesting("/robots.txt"), hasStatus(allOf(greaterThanOrEqualTo(400), lessThan(500)))); + } + + @Test + public void requestDarknetPageWithoutSlashRedirectsToFriendsPage() throws Exception { + verifyPermanentRedirect("/darknet", "/friends/"); + } + + @Test + public void requestDarknetPageWithSlashRedirectsToFriendsPage() throws Exception { + verifyPermanentRedirect("/darknet/", "/friends/"); + } + + @Test + public void requestOpennetPageWithoutSlashRedirectsToStrangersPage() throws Exception { + verifyPermanentRedirect("/opennet", "/strangers/"); + } + + @Test + public void requestOpennetPageWithSlashRedirectsToStrangersPage() throws Exception { + verifyPermanentRedirect("/opennet/", "/strangers/"); + } + + @Test + public void requestQueuePageRedirectsToDownloadsPage() throws Exception { + verifyPermanentRedirect("/queue/", "/downloads/"); + } + + @Test + public void requestConfigPageRedirectsToConfigNodePage() throws Exception { + verifyPermanentRedirect("/config/", "/config/node"); + } + + @Test + public void requestingAFailedDownloadReturnsErrorPage() throws Exception { + BaseL10nTest.useTestTranslation(); + when(fetchTracker.makeFetcher(any(), anyLong(), any(), any())).then(invocation -> { + FProxyFetchInProgress fetchInProgress = new FProxyFetchInProgress(fetchTracker, invocation.getArgument(0, FreenetURI.class), invocation.getArgument(1, Long.class), 0, null, invocation.getArgument(2, FetchContext.class), new RequestClientBuilder().build(), invocation.getArgument(3, FProxyFetchInProgress.REFILTER_POLICY.class)); + fetchInProgress.onFailure(new FetchException(FetchException.FetchExceptionMode.ALL_DATA_NOT_FOUND), null); + return new FProxyFetchWaiter(fetchInProgress); + }); + createToadletContextExecuteRequestAndVerifyContext(builder -> builder.requesting("/KSK@failed"), allOf( + hasStatus(equalTo(500)), + isHtml(allOf( + hasTitle(equalTo("FetchException.shortError.28 - Freenet")), + hasElement("p:contains(FProxyToadlet.unableToRetrieve)") + )) + )); + } + + @Test + public void requestingAnInProgressKeyReturnsProgressPage() throws Exception { + BaseL10nTest.useTestTranslation(); + when(fetchTracker.makeFetcher(any(), anyLong(), any(), any())).then(invocation -> { + FProxyFetchInProgress fetchInProgress = new FProxyFetchInProgress(fetchTracker, invocation.getArgument(0, FreenetURI.class), invocation.getArgument(1, Long.class), 0, null, invocation.getArgument(2, FetchContext.class), new RequestClientBuilder().build(), invocation.getArgument(3, FProxyFetchInProgress.REFILTER_POLICY.class)); + fetchInProgress.setHasWaited(); + return new FProxyFetchWaiter(fetchInProgress); + }); + createToadletContextExecuteRequestAndVerifyContext(builder -> builder.requesting("/KSK@test").withHeader("User-Agent", "Mozilla/Test"), allOf( + isHtml( + hasTitle(equalTo("FProxyToadlet.fetchingPageTitle - Freenet")) + ) + )); + } + + private void verifyTemporaryRedirect(String fromUri, String toUri) throws Exception { + createToadletContextExecuteRequestAndVerifyContext(builder -> builder.requesting(fromUri), allOf( + hasStatus(equalTo(302)), + hasHeader("Location", contains(equalTo(toUri))) + )); + } + + private void verifyPermanentRedirect(String fromUri, String toUri) throws Exception { + createToadletContextExecuteRequestAndVerifyContext(builder -> builder.requesting(fromUri), allOf( + hasStatus(equalTo(301)), + hasHeader("Location", contains(equalTo(toUri))) + )); + } + + private void verifyInternalRedirect(String fromUri, String toUri) { + TestToadletContext toadletContext = TestToadletContext.builder() + .forToadlet(fProxyToadlet) + .requesting(fromUri) + .withNode(nodeClientCore.getNode()).build(); + RedirectException redirectException = assertThrows(RedirectException.class, toadletContext::handleRequest); + assertThat(redirectException.getTarget(), equalTo(URI.create(toUri))); + } + + private void createToadletContextExecuteRequestAndVerifyContext(Consumer builderCustomizer, Matcher toadletContextMatcher) throws Exception { + TestToadletContextBuilder builder = TestToadletContext.builder() + .forToadlet(fProxyToadlet) + .withNode(nodeClientCore.getNode()); + builderCustomizer.accept(builder); + TestToadletContext toadletContext = builder.build(); + toadletContext.handleRequest(); + assertThat(toadletContext, toadletContextMatcher); + } + + private final HighLevelSimpleClient highLevelSimpleClient = mock(HighLevelSimpleClient.class, RETURNS_DEEP_STUBS); + private final NodeClientCore nodeClientCore = mock(NodeClientCore.class, RETURNS_DEEP_STUBS); + private final FProxyFetchTracker fetchTracker = mock(FProxyFetchTracker.class, RETURNS_DEEP_STUBS); + + { + when(nodeClientCore.getNode().getClientCore()).thenReturn(nodeClientCore); + when(nodeClientCore.getNode().getConfig().get("fproxy").getOption("port").getValueString()).thenReturn("8888"); + when(nodeClientCore.getNode().getConfig().get("fproxy").getOption("bindTo").getValueString()).thenReturn("127.0.0.1,0:0:0:0:0:0:0:1"); + when(nodeClientCore.getAllowedDownloadDirs()).thenReturn(new File[] { new File("/test") }); + } + + private final FProxyToadlet fProxyToadlet = new FProxyToadlet(highLevelSimpleClient, nodeClientCore, fetchTracker); + + { + try { + FieldSetter.setField(fProxyToadlet, FProxyToadlet.class.getDeclaredField("random"), new byte[0]); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/test/freenet/clients/http/FProxyToadletTest.properties b/test/freenet/clients/http/FProxyToadletTest.properties new file mode 100644 index 00000000000..20a4ba6134b --- /dev/null +++ b/test/freenet/clients/http/FProxyToadletTest.properties @@ -0,0 +1 @@ +FProxyToadlet.openAsText=${link}open-as-text${/link} diff --git a/test/freenet/clients/http/TestToadletContext.java b/test/freenet/clients/http/TestToadletContext.java new file mode 100644 index 00000000000..b8e54028f7c --- /dev/null +++ b/test/freenet/clients/http/TestToadletContext.java @@ -0,0 +1,429 @@ +package freenet.clients.http; + +import freenet.clients.http.bookmark.BookmarkManager; +import freenet.node.Node; +import freenet.node.useralerts.UserAlertManager; +import freenet.support.HTMLNode; +import freenet.support.MultiValueTable; +import freenet.support.api.Bucket; +import freenet.support.api.BucketFactory; +import freenet.support.api.HTTPRequest; +import freenet.support.io.FileUtil; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; + +/** + * A test implementation of {@link ToadletContext}. Can and should only be + * used for testing {@link Toadlet} implementations. + * + *

Usage

+ * + *

+ * The {@link TestToadletContext} takes care of creating all necessary + * objects for a call of a {@code handleMethod*} method and even runs the + * method in question itself. The HTTP status code, response headers, and + * body data can afterward be retrieved from the {@link TestToadletContext} + * instance, like this: + *

+ *
+ *     TestToadletContext toadletContext = TestToadletContext.build()
+ *     	.forToadlet(toadlet)
+ *     	.build();
+ *     toadletContext.handleRequest();
+ *     assertThat(toadletContext.getStatusCode(), equalTo(404));
+ * 
+ *

+ * Implementation note: This class should live in + * {@link freenet.test}, but because + * {@link PageMaker#PageMaker(PageMaker.THEME, Node) this constructor} + * is {@code protected}, it has to live in this package. + *

+ *

+ * This implementation is quite incomplete! Many of the methods + * are empty and/or return {@code null}! If you find that a specific + * method is causing problems in your test, I guess you will have to + * continue this implementation. 😄(I have put comments into methods + * that are currently unimplemented; if you implement them, don’t + * forget to remove the comment!) + *

+ */ +public class TestToadletContext implements ToadletContext { + + /** + * Returns a new {@link TestToadletContextBuilder}. + * + * @return A new builder + */ + public static TestToadletContextBuilder builder() { + return new TestToadletContextBuilder(); + } + + /** + * Builder for {@link TestToadletContext} instances. + *

+ * Instances of this class can theoretically be reused; created instances + * will be independent of one another. However, the syntax has been + * geared towards a fluid builder interface. + *

+ *

Usage

+ *
+	 * TestToadletContext.build()
+	 * 	.forToadlet(toadletUnderTest)
+	 * 	.requesting("/test-uri")
+	 * 	.withHeader("Some-Header", "Some Value")
+	 * 	.build();
+	 * 
+ *

+ * The {@link #withNode(Node)} and + * {@link #withContainer(ToadletContainer)} methods can be used to + * set these objects to specific values, e.g. when given to the toadlet + * by different means; if unused, default mock instances will be used. + *

+ *

+ * In addition to things you would expect, this builder also does a + * number of things that one might not expect, such as setting the + * {@link Toadlet#container} field of the + * {@link #forToadlet(Toadlet) configured Toadlet} to the given + * {@link #withContainer(ToadletContainer) ToadletContainer}. + *

+ *

+ * Instances of this class should be retrieved from + * {@link TestToadletContext#builder() TestToadletContext.builder()}. + *

+ */ + public static class TestToadletContextBuilder { + + public TestToadletContextBuilder forToadlet(Toadlet toadlet) { + this.activeToadlet = toadlet; + return this; + } + + public TestToadletContextBuilder requesting(String uri) { + this.uri = URI.create(uri); + return this; + } + + public TestToadletContextBuilder withHeader(String name, String value) { + requestHeaders.computeIfAbsent(name.toLowerCase(), _ignored -> new ArrayList<>()).add(value); + return this; + } + + public TestToadletContextBuilder doRobots() { + doRobots = true; + return this; + } + + public TestToadletContextBuilder withNode(Node node) { + this.node = node; + return this; + } + + public TestToadletContextBuilder withContainer(ToadletContainer toadletContainer) { + this.toadletContainer = toadletContainer; + return this; + } + + public TestToadletContext build() { + activeToadlet.container = toadletContainer; + return new TestToadletContext(node, toadletContainer, activeToadlet, uri, method, fromMap(requestHeaders), doRobots); + } + + private static MultiValueTable fromMap(Map> headers) { + MultiValueTable multiValueTable = new MultiValueTable<>(); + headers.forEach(multiValueTable::putAll); + return multiValueTable; + } + + private URI uri; + private String method = "GET"; + private Toadlet activeToadlet = mock(Toadlet.class, RETURNS_DEEP_STUBS); + private final Map> requestHeaders = new HashMap<>(); + private Node node = mock(Node.class, RETURNS_DEEP_STUBS); + private ToadletContainer toadletContainer = mock(ToadletContainer.class, RETURNS_DEEP_STUBS); + private boolean doRobots = false; + + } + + /** + * Returns the HTTP status code of the response. + * + * @return The HTTP status code + */ + public int getStatusCode() { + return statusCode; + } + + /** + * Returns the reason phrase from the HTTP status line. + * + * @return The reason phrase + */ + public String getReasonPhrase() { + return reasonPhrase; + } + + /** + * Returns the headers of the toadlet’s response. All keys are converted + * to lower-case! + * + * @return The response headers + */ + public Map> getResponseHeaders() { + return responseHeaders; + } + + /** + * Returns the body set by the {@link Toadlet}. + * + * @return The body, or {@code null} if no body is present + */ + public byte[] getBody() { + return body; + } + + /** + * Returns the content of the body as a {@link String}, assuming an + * {@link StandardCharsets#UTF_8 UTF-8} encoding. + * + * @return The body as {@link String}, or {@code null} if no body is + * present + */ + public String getBodyText() { + return (body != null) ? new String(body, UTF_8) : null; + } + + public void handleRequest() throws ToadletContextClosedException, IOException, RedirectException { + HTTPRequest httpRequest = new HTTPRequestImpl(uri, method); + if (method.equals("GET")) { + activeToadlet.handleMethodGET(getUri(), httpRequest, this); + } + } + + @Override + public void sendReplyHeaders(int code, String desc, MultiValueTable mvt, String mimeType, long length) throws ToadletContextClosedException, IOException { + sendReplyHeaders(code, desc, mvt, mimeType, length, false); + } + + @Override + public void sendReplyHeaders(int code, String desc, MultiValueTable mvt, String mimeType, long length, boolean forceDisableJavascript) throws ToadletContextClosedException, IOException { + sendReplyHeaders(code, desc, mvt, mimeType, length, new Date()); + } + + @Override + public void sendReplyHeaders(int code, String desc, MultiValueTable mvt, String mimeType, long length, Date mTime) throws ToadletContextClosedException, IOException { + sendReplyHeadersStatic(code, desc, mvt, mimeType, length, mTime); + } + + @Override + public void sendReplyHeadersStatic(int code, String desc, MultiValueTable mvt, String mimeType, long length, Date mTime) throws ToadletContextClosedException, IOException { + statusCode = code; + reasonPhrase = desc; + if (mvt != null) { + mvt.entrySet().forEach(header -> header.getValue().forEach(value -> { + responseHeaders.computeIfAbsent(header.getKey().toLowerCase(), _ignored -> new ArrayList<>()).add(value); + })); + } + if (mimeType != null) { + responseHeaders.computeIfAbsent("content-type", _ignored -> new ArrayList<>()).add(mimeType); + } + if (length >= 0) { + responseHeaders.computeIfAbsent("content-length", _ignored -> new ArrayList<>()).add(Long.toString(length)); + } + } + + @Override + public void sendReplyHeadersFProxy(int code, String desc, MultiValueTable mvt, String mimeType, long length) throws ToadletContextClosedException, IOException { + sendReplyHeadersStatic(code, desc, mvt, mimeType, length, new Date()); + } + + @Override + public void writeData(byte[] data, int offset, int length) throws ToadletContextClosedException, IOException { + body = Arrays.copyOfRange(data, offset, length); + } + + @Override + public void forceDisconnect() { + // Not yet implemented + } + + @Override + public void writeData(byte[] data) throws ToadletContextClosedException, IOException { + writeData(data, 0, data.length); + } + + @Override + public void writeData(Bucket data) throws ToadletContextClosedException, IOException { + try (ByteArrayOutputStream byteOutput = new ByteArrayOutputStream()) { + try (InputStream inputStream = data.getInputStream()) { + FileUtil.copy(inputStream, byteOutput, data.size()); + } + writeData(byteOutput.toByteArray()); + } + } + + @Override + public PageMaker getPageMaker() { + return new PageMaker(PageMaker.THEME.getDefault(), node); + } + + @Override + public String getFormPassword() { + // Not yet implemented + return ""; + } + + @Override + public boolean checkFormPassword(HTTPRequest request, String redirectTo) throws ToadletContextClosedException, IOException { + // Not yet implemented + return false; + } + + @Override + public boolean checkFormPassword(HTTPRequest request) throws ToadletContextClosedException, IOException { + // Not yet implemented + return false; + } + + @Override + public boolean hasFormPassword(HTTPRequest request) throws IOException { + // Not yet implemented + return false; + } + + @Override + public boolean checkFullAccess(Toadlet toadlet) throws ToadletContextClosedException, IOException { + // Not yet implemented + return false; + } + + @Override + public UserAlertManager getAlertManager() { + return userAlertManager; + } + + @Override + public BookmarkManager getBookmarkManager() { + // Not yet implemented + return null; + } + + @Override + public BucketFactory getBucketFactory() { + // Not yet implemented + return null; + } + + @Override + public MultiValueTable getHeaders() { + return requestHeaders; + } + + @Override + public ReceivedCookie getCookie(URI domain, URI path, String name) throws ParseException { + // Not yet implemented + return null; + } + + @Override + public void setCookie(Cookie newCookie) { + // Not yet implemented + } + + @Override + public HTMLNode addFormChild(HTMLNode parentNode, String target, String id) { + HTMLNode formNode = parentNode.addChild("form"); + formNode.addAttribute("target", target); + formNode.addAttribute("id", id); + return formNode; + } + + @Override + public boolean isAllowedFullAccess() { + // Not yet implemented + return false; + } + + @Override + public boolean isAdvancedModeEnabled() { + // Not yet implemented + return false; + } + + @Override + public boolean doRobots() { + return doRobots; + } + + @Override + public ToadletContainer getContainer() { + return toadletContainer; + } + + @Override + public boolean disableProgressPage() { + // Not yet implemented + return false; + } + + @Override + public Toadlet activeToadlet() { + return activeToadlet; + } + + @Override + public String getUniqueId() { + // Not yet implemented + return ""; + } + + @Override + public URI getUri() { + return uri; + } + + @Override + public FProxyFetchInProgress.REFILTER_POLICY getReFilterPolicy() { + return null; + } + + private TestToadletContext(Node node, ToadletContainer toadletContainer, Toadlet activeToadlet, URI uri, String method, MultiValueTable requestHeaders, boolean doRobots) { + this.node = node; + this.toadletContainer = toadletContainer; + this.userAlertManager = node.getClientCore().getAlerts(); + this.activeToadlet = activeToadlet; + this.uri = uri; + this.method = method; + this.requestHeaders = requestHeaders; + this.doRobots = doRobots; + } + + private final Node node; + private final ToadletContainer toadletContainer; + private final UserAlertManager userAlertManager; + private final Toadlet activeToadlet; + private final URI uri; + private final String method; + private final MultiValueTable requestHeaders; + private final boolean doRobots; + + private final Map> responseHeaders = new HashMap<>(); + private int statusCode = -1; + private String reasonPhrase; + private byte[] body = null; + +} diff --git a/test/freenet/l10n/BaseL10nTest.java b/test/freenet/l10n/BaseL10nTest.java index df6860408d1..4060a02bd34 100644 --- a/test/freenet/l10n/BaseL10nTest.java +++ b/test/freenet/l10n/BaseL10nTest.java @@ -1,5 +1,7 @@ package freenet.l10n; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.*; import java.io.File; @@ -249,6 +251,12 @@ public void testStrings() throws Exception { } } + @Test + public void overrideFileCanBeReadFromClasspath() { + BaseL10n l10n = new BaseL10n("freenet/l10n", "freenet.l10n.${lang}.properties", "freenet/l10n/BaseL10nTest.properties"); + assertThat(l10n.getString("override-from-classpath"), equalTo("overridden in classpath")); + } + public static final BaseL10n createL10n(LANGUAGE lang) { File overrideFile = new File(TestProperty.L10nPath_main, "freenet.l10n.${lang}.override.properties"); return new BaseL10n("freenet/l10n/", "freenet.l10n.${lang}.properties", @@ -261,6 +269,20 @@ public static final BaseL10n createTestL10n(LANGUAGE lang) { overrideFile.getPath(), lang); } + /** + * Creates a new {@link BaseL10n} for the given language, using the + * given override file. The override file can be used to use a different + * translation for every test, by supplying resource names from the + * classpath for files named differently for each test. + * + * @param lang The language to use + * @param overrideFile The override file + * @return A {@link BaseL10n} object + */ + private static BaseL10n createTestL10n(LANGUAGE lang, String overrideFile) { + return new BaseL10n("freenet/l10n/", "freenet.l10n.${lang}.test.properties", overrideFile, lang); + } + /** * Installs a {@link #createTestL10n(LANGUAGE) BaseL10n} with * translations read from the test classpath into the global @@ -270,4 +292,22 @@ public static void useTestTranslation() { NodeL10n.setBase(createTestL10n(LANGUAGE.ENGLISH)); } + /** + * Installs a {@link #createTestL10n(LANGUAGE) BaseL10n} with + * translations read from the test classpath into the global + * {@link NodeL10n}, allowing tests for translation keys. By using + * a different file for each test, test-specific translations can be used + * to verify translations with substitutions (such as the ones we use + * for HTML). By not including the magic string {@code "${lang}"} + * in the override file, the language used will be ignored for building + * the actual override filename. + * + * @param overrideFile The name of a resource from the classpath, + * or a file in the local filesystem, containing translation + * properties + */ + public static void useTestTranslation(String overrideFile) { + NodeL10n.setBase(createTestL10n(LANGUAGE.ENGLISH, overrideFile)); + } + } diff --git a/test/freenet/l10n/BaseL10nTest.properties b/test/freenet/l10n/BaseL10nTest.properties new file mode 100644 index 00000000000..22470f1d256 --- /dev/null +++ b/test/freenet/l10n/BaseL10nTest.properties @@ -0,0 +1 @@ +override-from-classpath=overridden in classpath diff --git a/test/freenet/test/HtmlMatchers.java b/test/freenet/test/HtmlMatchers.java new file mode 100644 index 00000000000..c50e582ed1c --- /dev/null +++ b/test/freenet/test/HtmlMatchers.java @@ -0,0 +1,143 @@ +package freenet.test; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeDiagnosingMatcher; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; + +/** + * That class contains {@link Matcher} implementations that can verify + * certain properties of an HTML {@link Document}. + */ +public class HtmlMatchers { + + /** + * Returns a {@link Matcher} for a {@link Document} which verifies that + * the document has a title, and then verifies the title against the given + * matcher. + * + * @param titleMatcher A matcher for the title, if present + * @return A matcher for a {@link Document} + */ + public static Matcher hasTitle(Matcher titleMatcher) { + return new TypeSafeDiagnosingMatcher() { + @Override + protected boolean matchesSafely(Document document, Description mismatchDescription) { + Element titleElement = document.select("head title").first(); + if (titleElement == null) { + mismatchDescription.appendText("no title present"); + return false; + } + if (!titleMatcher.matches(titleElement.text())) { + mismatchDescription.appendText("title "); + titleMatcher.describeMismatch(titleElement.text(), mismatchDescription); + return false; + } + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("has title matching ").appendDescriptionOf(titleMatcher); + } + }; + } + + /** + * Returns a {@link Matcher} for a {@link Document} that locates an + * {@link Element} using a {@link Document#select(String) selector} + * and matches if at least one such {@link Element} can be found. + * + * @param selector The selector for the element to locate + * @return A matcher for a {@link Document} + */ + public static Matcher hasElement(String selector) { + return new TypeSafeDiagnosingMatcher() { + @Override + protected boolean matchesSafely(Document document, Description mismatchDescription) { + Element element = document.select(selector).first(); + if (element == null) { + mismatchDescription.appendText("not found: ").appendValue(selector); + return false; + } + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("has element ").appendValue(selector); + } + }; + } + + /** + * Returns a {@link Matcher} for a {@link Document} that locates an + * {@link Element} using a {@link Document#select(String) selector} + * and verifies the found element with the given matcher. + *

+ * If {@link Document#select(String)} locates multiple elements, only + * the first one is matched! + *

+ * + * @param selector The selector for the element to locate + * @param elementMatcher The matcher for the element, if present + * @return A matcher for a {@link Document} + */ + public static Matcher hasElement(String selector, Matcher elementMatcher) { + return new TypeSafeDiagnosingMatcher() { + @Override + protected boolean matchesSafely(Document document, Description mismatchDescription) { + Element element = document.select(selector).first(); + if (element == null) { + mismatchDescription.appendText("not found: ").appendValue(selector); + return false; + } + if (!elementMatcher.matches(element)) { + mismatchDescription.appendText("element "); + elementMatcher.describeMismatch(element, mismatchDescription); + return false; + } + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("has element ").appendValue(selector).appendText(" matching ").appendDescriptionOf(elementMatcher); + } + }; + } + + /** + * Returns a {@link Matcher} for an {@link Element}, which verifies that + * the attribute with the given name exists, and if it does, verifies it against + * the given matcher. + * + * @param attributeName The name of the attribute to verify + * @param valueMatcher The matcher for the attribute’s value, if present + * @return A matcher for an {@link Element} + */ + public static Matcher hasAttribute(String attributeName, Matcher valueMatcher) { + return new TypeSafeDiagnosingMatcher() { + @Override + protected boolean matchesSafely(Element element, Description mismatchDescription) { + if (!element.hasAttr(attributeName)) { + mismatchDescription.appendText("no attribute ").appendValue(attributeName); + return false; + } + if (!valueMatcher.matches(element.attr(attributeName))) { + mismatchDescription.appendText("attribute "); + valueMatcher.describeMismatch(element.attr(attributeName), mismatchDescription); + return false; + } + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("has attribute ").appendValue(attributeName).appendText(" matching ").appendDescriptionOf(valueMatcher); + } + }; + } + +} diff --git a/test/freenet/test/HtmlMatchersTest.java b/test/freenet/test/HtmlMatchersTest.java new file mode 100644 index 00000000000..ed2485ca682 --- /dev/null +++ b/test/freenet/test/HtmlMatchersTest.java @@ -0,0 +1,149 @@ +package freenet.test; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.StringDescription; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.junit.Test; + +import static freenet.test.HtmlMatchers.hasAttribute; +import static freenet.test.HtmlMatchers.hasElement; +import static freenet.test.HtmlMatchers.hasTitle; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.any; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +public class HtmlMatchersTest { + + @Test + public void hasTitleMatcherRecognizesIfNoTitleHasBeenSet() { + assertThat(hasTitle(any(String.class)).matches(documentWithoutTitle), equalTo(false)); + } + + @Test + public void hasTitleMatcherDescribesMismatch() { + hasTitle(any(String.class)).describeMismatch(documentWithoutTitle, description); + assertThat(description.toString(), equalTo("no title present")); + } + + @Test + public void hasTitleMatcherDescribesMismatchOfTitleMatcher() { + hasTitle(nullValue()).describeMismatch(documentWithTitle, description); + assertThat(description.toString(), equalTo("title was \"Test!\"")); + } + + @Test + public void hasTitleMatcherRecognizesIfTitleHasBeenSet() { + assertThat(hasTitle(any(String.class)).matches(documentWithTitle), equalTo(true)); + } + + @Test + public void hasTitleMatcherDescribesItself() { + hasTitle(any(String.class)).describeTo(description); + assertThat(description.toString(), equalTo("has title matching an instance of java.lang.String")); + } + + @Test + public void titleMatcherIsNotConsultedIfNoTitleIsPresent() { + Matcher titleMatcher = mock(Matcher.class); + hasTitle(titleMatcher).matches(documentWithoutTitle); + verifyZeroInteractions(titleMatcher); + } + + @Test + public void titleMatcherIsConsultedIfTitleIsPresent() { + Matcher titleMatcher = mock(Matcher.class); + hasTitle(titleMatcher).matches(documentWithTitle); + verify(titleMatcher).matches(eq("Test!")); + } + + @Test + public void hasElementMatcherDoesNotMatchIfElementDoesNotExist() { + assertThat(hasElement("some element").matches(documentWithTitle), equalTo(false)); + } + + @Test + public void hasElementMatcherDescribesMismatchBecauseOfMissingElement() { + hasElement("some element").describeMismatch(documentWithTitle, description); + assertThat(description.toString(), equalTo("not found: \"some element\"")); + } + + @Test + public void hasElementMatcherMatchesIfElementExists() { + assertThat(hasElement("head title").matches(documentWithTitle), equalTo(true)); + } + + @Test + public void hasElementMatcherDescribesItself() { + hasElement("head title").describeTo(description); + assertThat(description.toString(), equalTo("has element \"head title\"")); + } + + @Test + public void hasElementMatcherWithElementMatcherDoesNotMatchIfElementDoesNotExist() { + assertThat(hasElement("some element", any(Element.class)).matches(documentWithTitle), equalTo(false)); + } + + @Test + public void hasElementMatcherWithElementMatcherDescribesMismatchBecauseOfMissingElement() { + hasElement("some element", any(Element.class)).describeMismatch(documentWithTitle, description); + assertThat(description.toString(), equalTo("not found: \"some element\"")); + } + + @Test + public void hasElementMatcherWithElementMatcherDescribesMismatchOfElementMatcher() { + hasElement("head title", nullValue()).describeMismatch(documentWithTitle, description); + assertThat(description.toString(), equalTo("element was <Test!>")); + } + + @Test + public void hasElementMatcherWithElementMatcherMatchesIfElementExists() { + assertThat(hasElement("head title", any(Element.class)).matches(documentWithTitle), equalTo(true)); + } + + @Test + public void hasElementMatcherWithElementMatcherDescribesItself() { + hasElement("head title", any(Element.class)).describeTo(description); + assertThat(description.toString(), equalTo("has element \"head title\" matching an instance of org.jsoup.nodes.Element")); + } + + @Test + public void hasAttributeMatcherDoesNotMatchIfAttributeDoesNotExist() { + assertThat(hasAttribute("foo", any(String.class)).matches(new Element("test")), equalTo(false)); + } + + @Test + public void hasAttributeMatcherDescribesMismatch() { + hasAttribute("foo", any(String.class)).describeMismatch(new Element("test"), description); + assertThat(description.toString(), equalTo("no attribute \"foo\"")); + } + + @Test + public void hasAttributeMatcherDescribesMismatchOfValueMatcher() { + hasAttribute("foo", nullValue()).describeMismatch(new Element("test").attr("foo", "bar"), description); + assertThat(description.toString(), equalTo("attribute was \"bar\"")); + } + + @Test + public void hasAttributeMatcherDoesMatchIfAttributeIsPresent() { + assertThat(hasAttribute("foo", any(String.class)).matches(new Element("test").attr("foo", "bar")), equalTo(true)); + } + + @Test + public void hasAttributeMatcherDescribesItself() { + hasAttribute("foo", any(String.class)).describeTo(description); + assertThat(description.toString(), equalTo("has attribute \"foo\" matching an instance of java.lang.String")); + } + + private final Description description = new StringDescription(); + private final Document documentWithoutTitle = Jsoup.parse(""); + private final Document documentWithTitle = Jsoup.parse("Test!"); + +} diff --git a/test/freenet/test/LinkMatchers.java b/test/freenet/test/LinkMatchers.java new file mode 100644 index 00000000000..54ef3e4c36a --- /dev/null +++ b/test/freenet/test/LinkMatchers.java @@ -0,0 +1,292 @@ +package freenet.test; + +import freenet.clients.http.HTTPRequestImpl; +import jakarta.activation.MimeType; +import jakarta.activation.MimeTypeParseException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeDiagnosingMatcher; + +/** + * This class contains Hamcrest {@link Matcher}s that can be used to verify + * links in HTML documents. The links need to be HTML-decoded (e.g. by + * using Jsoup or any other actual HTML parser) before these matchers can + * be applied. + * + *

Usage

+ *

+ * The matchers in this class are designed to be used in a “drill-down” + * manner; first, a {@link String} is verified as and converted into a URI + * by {@link #isURI(Matcher)}, then matchers for the URI can verify the + * parts of a URI (such as the scheme, or the query), then the parts are + * verified and again converted into appropriate subtypes (e.g. query + * parameters are parsed into a Map<String, List<String>>), and + * these values can then again be verified and converted into other + * subtypes, such as {@link MimeType}s. + *

+ *

+ * To verify that a link is a valid URI: + *

+ *
+ * assertThat(link, isURI(any(URI.class)));
+ * 
+ *

+ * To verify that a link is an HTTPS link: + *

+ *
+ * assertThat(link, isURI(hasScheme(equalTo("https"))));
+ * 
+ *

+ * Generally, the {@code is*} matchers take a {@link String} and verify + * that it conforms to some specification, while the {@code has*} matchers + * then verify object-specific properties on an instance of the actual + * object; e.g. the {@link #isMimeType(Matcher)} matcher verifies that + * a {@link String} actually represents a MIME type, before the + * {@link #hasBaseType(String)} matcher then operates on a + * {@link MimeType} instance for further verification. + *

+ *

+ * The matchers can be combined like any other {@link Matcher}, too. This + * would check for a link that has either an HTTP or an HTTPS scheme, + * and a query part that begins with the {@link String} {@code "foo"}. + *

+ *
+ * assertThat(link, isURI(allOf(
+ * 	anyOf(
+ * 		hasScheme(equalTo("http")),
+ * 		hasScheme(equalTo("https"))
+ * 	),
+ * 	hasQuery(startsWith("foo"))
+ * )));
+ * 
+ */ +public class LinkMatchers { + + /** + * Returns a {@link Matcher} that can verify that a {@link String} + * represents a valid URI, and that the URI conforms to the given + * secondary {@link Matcher}. + * + * @param uriMatcher The matcher for the URI + * @return A matcher matching URIs + */ + public static Matcher isURI(Matcher uriMatcher) { + return new TypeSafeDiagnosingMatcher() { + @Override + protected boolean matchesSafely(String uriString, Description mismatchDescription) { + try { + URI uri = new URI(uriString); + if (!uriMatcher.matches(uri)) { + mismatchDescription.appendText("URI "); + uriMatcher.describeMismatch(uri, mismatchDescription); + return false; + } + return true; + } catch (URISyntaxException e) { + mismatchDescription.appendText("not a URI: ").appendValue(uriString); + return false; + } + } + + @Override + public void describeTo(Description description) { + description.appendText("is a URI matching ").appendDescriptionOf(uriMatcher); + } + }; + } + + /** + * Returns a {@link Matcher} that verifies the presence of a + * {@link URI#getScheme()}, and that is matches the given + * {@link Matcher}. + * + * @param schemeMatcher The matcher for the scheme, if present + * @return A matcher for a URI + */ + public static Matcher hasScheme(Matcher schemeMatcher) { + return new TypeSafeDiagnosingMatcher() { + @Override + protected boolean matchesSafely(URI uri, Description mismatchDescription) { + if (uri.getScheme() == null) { + mismatchDescription.appendText("no scheme in URI ").appendValue(uri); + return false; + } + if (!schemeMatcher.matches(uri.getScheme())) { + mismatchDescription.appendText("scheme "); + schemeMatcher.describeMismatch(uri.getScheme(), mismatchDescription); + return false; + } + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("has a scheme matching ").appendDescriptionOf(schemeMatcher); + } + }; + } + + /** + * Returns a {@link Matcher} that verifies the presence of a + * {@link URI#getQuery()}, and that it matches the given + * {@link Matcher}. + * + * @param queryMatcher The matcher for the query, if present + * @return A matcher for a URI + */ + public static Matcher hasQuery(Matcher queryMatcher) { + return new TypeSafeDiagnosingMatcher() { + @Override + protected boolean matchesSafely(URI uri, Description mismatchDescription) { + if (uri.getQuery() == null) { + mismatchDescription.appendText("no query in URI ").appendValue(uri); + return false; + } + if (!queryMatcher.matches(uri.getQuery())) { + mismatchDescription.appendText("query "); + queryMatcher.describeMismatch(uri.getQuery(), mismatchDescription); + return false; + } + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("has a query matching ").appendDescriptionOf(queryMatcher); + } + }; + } + + /** + * Returns a {@link Matcher} that verifies the presence of key-value + * pairs, and that they match the given {@link Matcher}. + *

+ * An empty {@link String}, or a {@link String} consisting solely of + * whitespace (according to {@link String#trim()}) is not recognized as + * key-value pairs. + *

+ * + * @param keyValuePairsMatcher The matcher for the key-value pairs, + * if present + * @return A matcher for a {@link String} containing key-value pairs + */ + public static Matcher isKeyValuePairs(Matcher>> keyValuePairsMatcher) { + return new TypeSafeDiagnosingMatcher() { + @Override + protected boolean matchesSafely(String keyValuePairsString, Description mismatchDescription) { + if (keyValuePairsString.trim().isEmpty()) { + mismatchDescription.appendText("not key-value pairs: ").appendValue(keyValuePairsString); + return false; + } + Map> uriParameters = HTTPRequestImpl.parseUriParameters(keyValuePairsString, false); + if (!keyValuePairsMatcher.matches(uriParameters)) { + mismatchDescription.appendText("key-value pairs "); + keyValuePairsMatcher.describeMismatch(uriParameters, mismatchDescription); + return false; + } + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("is key-value pairs matching ").appendDescriptionOf(keyValuePairsMatcher); + } + }; + } + + /** + * Returns a {@link Matcher} that verifies the presence of a MIME type, + * and that it matches the given {@link Matcher}. + * + * @param mimeTypeMatcher The matcher for the MIME type, if present + * @return A matcher for a {@link String} containing a MIME type + */ + public static Matcher isMimeType(Matcher mimeTypeMatcher) { + return new TypeSafeDiagnosingMatcher() { + @Override + protected boolean matchesSafely(String mimeTypeString, Description mismatchDescription) { + try { + MimeType mimeType = new MimeType(mimeTypeString); + if (!mimeTypeMatcher.matches(mimeType)) { + mismatchDescription.appendText("MIME type "); + mimeTypeMatcher.describeMismatch(mimeType, mismatchDescription); + return false; + } + return true; + } catch (MimeTypeParseException e) { + mismatchDescription.appendText("not a MIME type: ").appendValue(mimeTypeString); + return false; + } + } + + @Override + public void describeTo(Description description) { + description.appendText("is a MIME type matching ").appendDescriptionOf(mimeTypeMatcher); + } + }; + } + + /** + * Returns a {@link Matcher} for a MIME type’s base type. The base type + * comprises the primary and the sub type, ignoring all parameters. + * + * @param baseType The base type to match + * @return A matcher for a MIME type + */ + public static Matcher hasBaseType(String baseType) { + return new TypeSafeDiagnosingMatcher() { + @Override + protected boolean matchesSafely(MimeType mimeType, Description mismatchDescription) { + try { + if (!mimeType.match(baseType)) { + mismatchDescription.appendText("was ").appendValue(mimeType); + return false; + } + } catch (MimeTypeParseException e) { + throw new RuntimeException(e); + } + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("has base type ").appendValue(baseType); + } + }; + } + + /** + * Returns a {@link Matcher} for a parameter of a MIME type. + * + * @param name The name of the parameter + * @param valueMatcher The matcher for the value of the parameter + * @return A matcher for a MIME type + */ + public static Matcher hasParameter(String name, Matcher valueMatcher) { + return new TypeSafeDiagnosingMatcher() { + @Override + protected boolean matchesSafely(MimeType mimeType, Description mismatchDescription) { + if (mimeType.getParameter(name) == null) { + mismatchDescription.appendText("parameter not found: ").appendValue(name); + return false; + } + if (!valueMatcher.matches(mimeType.getParameter(name))) { + mismatchDescription.appendText("parameter ").appendValue(name).appendText(" "); + valueMatcher.describeMismatch(mimeType.getParameter(name), mismatchDescription); + return false; + } + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("has parameter ").appendValue(name).appendText(" matching ").appendDescriptionOf(valueMatcher); + } + }; + } + +} diff --git a/test/freenet/test/LinkMatchersTest.java b/test/freenet/test/LinkMatchersTest.java new file mode 100644 index 00000000000..b0b84ce3387 --- /dev/null +++ b/test/freenet/test/LinkMatchersTest.java @@ -0,0 +1,322 @@ +package freenet.test; + +import jakarta.activation.MimeType; +import jakarta.activation.MimeTypeParseException; +import java.net.URI; +import java.util.Map; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.StringDescription; +import org.junit.Test; +import org.mockito.ArgumentMatchers; + +import static freenet.test.LinkMatchers.hasBaseType; +import static freenet.test.LinkMatchers.hasParameter; +import static freenet.test.LinkMatchers.hasQuery; +import static freenet.test.LinkMatchers.hasScheme; +import static freenet.test.LinkMatchers.isKeyValuePairs; +import static freenet.test.LinkMatchers.isMimeType; +import static freenet.test.LinkMatchers.isURI; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.any; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +public class LinkMatchersTest { + + @Test + public void invalidURIsAreRecognized() { + assertThat(isURI(any(URI.class)).matches("not a URI"), equalTo(false)); + } + + @Test + public void invalidURIsAreDescribedCorrectly() { + isURI(any(URI.class)).describeMismatch("not a URI", description); + assertThat(description.toString(), equalTo("not a URI: \"not a URI\"")); + } + + @Test + public void isUriMatcherDescribesMismatchOfGivenMatcher() { + isURI(nullValue()).describeMismatch("/", description); + assertThat(description.toString(), equalTo("URI was ")); + } + + @Test + public void validURIsAreRecognized() { + assertThat(isURI(any(URI.class)).matches("scheme://authority/path?query#fragment"), equalTo(true)); + } + + @Test + public void uriMatcherIsNotConsultedOnInvalidURIs() { + Matcher uriMatcher = mock(Matcher.class); + isURI(uriMatcher).matches("not a URI"); + verifyZeroInteractions(uriMatcher); + } + + @Test + public void uriMatcherIsConsultedOnValidURIs() { + Matcher uriMatcher = mock(Matcher.class); + isURI(uriMatcher).matches("scheme://authority/path?query#fragment"); + verify(uriMatcher).matches(ArgumentMatchers.any(URI.class)); + } + + @Test + public void uriMatcherDescribesItself() { + isURI(any(URI.class)).describeTo(description); + assertThat(description.toString(), equalTo("is a URI matching an instance of java.net.URI")); + } + + @Test + public void schemeMatcherDoesNotMatchIfThereIsNoScheme() { + assertThat(hasScheme(any(String.class)).matches(URI.create("./no-scheme")), equalTo(false)); + } + + @Test + public void schemeMatcherDescribesMismatch() { + hasScheme(any(String.class)).describeMismatch(URI.create("./no-scheme"), description); + assertThat(description.toString(), equalTo("no scheme in URI <./no-scheme>")); + } + + @Test + public void schemeMatcherDescribeMismatchOfGivenMatcher() { + hasScheme(nullValue()).describeMismatch(URI.create("http://scheme"), description); + assertThat(description.toString(), equalTo("scheme was \"http\"")); + } + + @Test + public void schemeMatcherMatchesIfThereIsAScheme() { + assertThat(hasScheme(any(String.class)).matches(URI.create("scheme://authority/path?query#fragment")), equalTo(true)); + } + + @Test + public void schemeMatcherDescribesItself() { + hasScheme(any(String.class)).describeTo(description); + assertThat(description.toString(), equalTo("has a scheme matching an instance of java.lang.String")); + } + + @Test + public void schemeMatcherIsNotConsultedIfThereIsNoScheme() { + Matcher schemeMatcher = mock(Matcher.class); + hasScheme(schemeMatcher).matches(URI.create("./no-scheme")); + verifyZeroInteractions(schemeMatcher); + } + + @Test + public void schemeMatcherIsConsultedIfThereIsAScheme() { + Matcher schemeMatcher = mock(Matcher.class); + hasScheme(schemeMatcher).matches(URI.create("scheme://authority/path?query#fragment")); + verify(schemeMatcher).matches(anyString()); + } + + @Test + public void queryMatcherDoesNotMatcherIfThereIsNoQuery() { + assertThat(hasQuery(any(String.class)).matches(URI.create("./no-query")), equalTo(false)); + } + + @Test + public void queryMatcherDescribesMismatch() { + hasQuery(any(String.class)).describeMismatch(URI.create("./no-query"), description); + assertThat(description.toString(), equalTo("no query in URI <./no-query>")); + } + + @Test + public void queryMatcherDescribesMismatchOfGivenMatcher() { + hasQuery(nullValue()).describeMismatch(URI.create("./no?query"), description); + assertThat(description.toString(), equalTo("query was \"query\"")); + } + + @Test + public void queryMatcherDoesMatchIfThereIsAQuery() { + assertThat(hasQuery(any(String.class)).matches(URI.create("./no-query?query")), equalTo(true)); + } + + @Test + public void queryMatcherDescribesItself() { + hasQuery(any(String.class)).describeTo(description); + assertThat(description.toString(), equalTo("has a query matching an instance of java.lang.String")); + } + + @Test + public void queryMatcherIsNotConsultedIfThereIsNoQuery() { + Matcher queryMatcher = mock(Matcher.class); + hasQuery(queryMatcher).matches(URI.create("./no-query")); + verifyZeroInteractions(queryMatcher); + } + + @Test + public void queryMatcherIsConsultedIfThereIsAQuery() { + Matcher queryMatcher = mock(Matcher.class); + hasQuery(queryMatcher).matches(URI.create("./no-query?query")); + verify(queryMatcher).matches(anyString()); + } + + @Test + public void keyValuePairsMatcherDoesNotRecognizeEmptyStringAsKeyValuePairs() { + assertThat(isKeyValuePairs(any(Map.class)).matches(""), equalTo(false)); + } + + @Test + public void keyValuePairsMatcherDescribesMismatch() { + isKeyValuePairs(any(Map.class)).describeMismatch("", description); + assertThat(description.toString(), equalTo("not key-value pairs: \"\"")); + } + + @Test + public void keyValuePairsMatcherDescribesMismatchOfGivenMatcher() { + isKeyValuePairs(nullValue()).describeMismatch("foo=bar&baz=quo", description); + assertThat(description.toString(), equalTo("key-value pairs was <{foo=[bar], baz=[quo]}>")); + } + + @Test + public void keyValuePairsMatcherDoesNotRecognizeStringOfWhitespaceOnlyAsKeyValuePairs() { + assertThat(isKeyValuePairs(any(Map.class)).matches(" \t \r \n "), equalTo(false)); + } + + @Test + public void keyValuePairsMatcherRecognizesIfThereAreKeyValuePairs() { + assertThat(isKeyValuePairs(any(Map.class)).matches("foo=bar&baz=quo"), equalTo(true)); + } + + @Test + public void keyValuePairsMatcherDescribesItself() { + isKeyValuePairs(any(Map.class)).describeTo(description); + assertThat(description.toString(), equalTo("is key-value pairs matching an instance of java.util.Map")); + } + + @Test + public void keyValuePairsMatcherIsNotConsultedIfThereAreNoKeyValuePairs() { + Matcher> keyValuePairsMatcher = mock(Matcher.class); + isKeyValuePairs(keyValuePairsMatcher).matches(""); + verifyZeroInteractions(keyValuePairsMatcher); + } + + @Test + public void keyValuePairsMatcherIsConsultedIfThereAreKeyValuePairs() { + Matcher> keyValuePairsMatcher = mock(Matcher.class); + isKeyValuePairs(keyValuePairsMatcher).matches("foo=bar&baz=quo"); + verify(keyValuePairsMatcher).matches(ArgumentMatchers.any()); + } + + @Test + public void mimeTypeMatcherRecognizesIfThereIsNoMimeType() { + assertThat(isMimeType(any(MimeType.class)).matches("random words"), equalTo(false)); + } + + @Test + public void mimeTypeMatcherDescribesMismatch() { + isMimeType(any(MimeType.class)).describeMismatch("random words", description); + assertThat(description.toString(), equalTo("not a MIME type: \"random words\"")); + } + + @Test + public void mimeTypeMatcherDescribesMismatchOfGivenMatcher() { + isMimeType(nullValue()).describeMismatch("x.test/mime-type", description); + assertThat(description.toString(), equalTo("MIME type was ")); + } + + @Test + public void mimeTypeMatcherRecognizesIfThereIsAMimeType() { + assertThat(isMimeType(any(MimeType.class)).matches("application/x-test"), equalTo(true)); + } + + @Test + public void mimeTypeMatcherDescribesItself() { + isMimeType(any(MimeType.class)).describeTo(description); + assertThat(description.toString(), equalTo("is a MIME type matching an instance of jakarta.activation.MimeType")); + } + + @Test + public void mimeTypeMatcherIsNotConsultedIfThereIsNoMimeType() { + Matcher mimeTypeMatcher = mock(Matcher.class); + isMimeType(mimeTypeMatcher).matches("random words"); + verifyZeroInteractions(mimeTypeMatcher); + } + + @Test + public void mimeTypeMatcherIsConsultedIfThereIsAMimeType() { + Matcher mimeTypeMatcher = mock(Matcher.class); + isMimeType(mimeTypeMatcher).matches("application/x-test"); + verify(mimeTypeMatcher).matches(ArgumentMatchers.any()); + } + + @Test + public void hasBaseTypeMatcherDoesNotMatchADifferentPrimaryType() throws MimeTypeParseException { + assertThat(hasBaseType("x.test/type").matches(new MimeType("text/plain")), equalTo(false)); + } + + @Test + public void hasBaseTypeMatcherDoesNotMatchADifferentSubType() throws MimeTypeParseException { + assertThat(hasBaseType("x.test/type").matches(new MimeType("x.test/different")), equalTo(false)); + } + + @Test + public void hasBaseTypeMatcherDescribesMismatch() throws MimeTypeParseException { + hasBaseType("x.test/type").describeMismatch(new MimeType("text/plain"), description); + assertThat(description.toString(), equalTo("was ")); + } + + @Test + public void hasBaseTypeMatcherMatchesIfBaseTypeMatches() throws MimeTypeParseException { + assertThat(hasBaseType("x.test/type").matches(new MimeType("x.test/type")), equalTo(true)); + } + + @Test + public void hasBaseTypeMatcherMatchesIgnoringParameters() throws MimeTypeParseException { + assertThat(hasBaseType("x.test/type").matches(new MimeType("x.test/type; foo=bar")), equalTo(true)); + } + + @Test + public void hasBaseTypeMatcherDescribesItself() { + hasBaseType("x.test/type").describeTo(description); + assertThat(description.toString(), equalTo("has base type \"x.test/type\"")); + } + + @Test + public void hasParameterMatcherRecognizesIfParameterDoesNotExist() throws MimeTypeParseException { + assertThat(hasParameter("foo", any(String.class)).matches(new MimeType("x.test/mime-type")), equalTo(false)); + } + + @Test + public void hasParameterMatcherDescribesMismatchCorrectly() throws MimeTypeParseException { + hasParameter("foo", any(String.class)).describeMismatch(new MimeType("x.test/mime-type"), description); + assertThat(description.toString(), equalTo("parameter not found: \"foo\"")); + } + + @Test + public void hasParameterMatcherDescribesMismatchOfGivenMatcher() throws MimeTypeParseException { + hasParameter("foo", nullValue()).describeMismatch(new MimeType("x.test/mime-type; foo=bar"), description); + assertThat(description.toString(), equalTo("parameter \"foo\" was \"bar\"")); + } + + @Test + public void hasParameterMatcherRecognizesIfParameterDoesExist() throws MimeTypeParseException { + assertThat(hasParameter("foo", any(String.class)).matches(new MimeType("x.test/mime-type; foo=bar")), equalTo(true)); + } + + @Test + public void hasParameterMatcherDescribesItself() { + hasParameter("foo", any(String.class)).describeTo(description); + assertThat(description.toString(), equalTo("has parameter \"foo\" matching an instance of java.lang.String")); + } + + @Test + public void valueMatcherIsNotConsultedIfParameterDoesNotExist() throws MimeTypeParseException { + Matcher valueMatcher = mock(Matcher.class); + hasParameter("foo", valueMatcher).matches(new MimeType("x.test/mime-type")); + verifyZeroInteractions(valueMatcher); + } + + @Test + public void valueMatcherIsConsultedIfParameterExists() throws MimeTypeParseException { + Matcher valueMatcher = mock(Matcher.class); + hasParameter("foo", valueMatcher).matches(new MimeType("x.test/mime-type; foo=bar")); + verify(valueMatcher).matches(ArgumentMatchers.any()); + } + + private final Description description = new StringDescription(); + +} diff --git a/test/freenet/test/ToadletContextMatchers.java b/test/freenet/test/ToadletContextMatchers.java new file mode 100644 index 00000000000..869c02958b9 --- /dev/null +++ b/test/freenet/test/ToadletContextMatchers.java @@ -0,0 +1,191 @@ +package freenet.test; + +import freenet.clients.http.TestToadletContext; +import freenet.clients.http.ToadletContext; +import jakarta.activation.MimeType; +import jakarta.activation.MimeTypeParseException; +import java.util.List; +import java.util.Map; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeDiagnosingMatcher; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; + +/** + * Matchers for certain properties of a {@link TestToadletContext}. As + * {@link TestToadletContext}s are only used during, it is not possible to + * verify real {@link ToadletContext}s with it. + */ +public class ToadletContextMatchers { + + /** + * Returns a {@link Matcher} that verifies that a status has been set on + * the {@link TestToadletContext}, and then verifies it using the given + * {@link Matcher}. + * + * @param statusCodeMatcher The matcher for the status code, if present + * @return A matcher for a {@link TestToadletContext} + */ + public static Matcher hasStatus(Matcher statusCodeMatcher) { + return new TypeSafeDiagnosingMatcher() { + @Override + protected boolean matchesSafely(TestToadletContext toadletContext, Description mismatchDescription) { + if (toadletContext.getStatusCode() == -1) { + mismatchDescription.appendText("no status set"); + return false; + } + if (!statusCodeMatcher.matches(toadletContext.getStatusCode())) { + mismatchDescription.appendText("status code "); + statusCodeMatcher.describeMismatch(toadletContext.getStatusCode(), mismatchDescription); + return false; + } + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("has status matching ").appendDescriptionOf(statusCodeMatcher); + } + }; + } + + /** + * Returns a {@link Matcher} for a {@link TestToadletContext} which + * verifies that a {@code Content-Type} header has been set, and then + * verifies the header using the given {@link MimeType} matcher. + * + * @param mimeTypeMatcher The matcher for the MIME type, if present + * @return A matcher for a {@link TestToadletContext} + */ + public static Matcher hasContentType(Matcher mimeTypeMatcher) { + return new TypeSafeDiagnosingMatcher() { + @Override + protected boolean matchesSafely(TestToadletContext toadletContext, Description mismatchDescription) { + if (!toadletContext.getResponseHeaders().containsKey("content-type")) { + mismatchDescription.appendText("no content type set"); + return false; + } + try { + MimeType mimeType = new MimeType(toadletContext.getResponseHeaders().get("content-type").get(0)); + if (!mimeTypeMatcher.matches(mimeType)) { + mismatchDescription.appendText("content type "); + mimeTypeMatcher.describeMismatch(mimeType, mismatchDescription); + return false; + } + } catch (MimeTypeParseException e) { + throw new RuntimeException(e); + } + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("has content type matching ").appendDescriptionOf(mimeTypeMatcher); + } + }; + } + + /** + * Returns a {@link Matcher} that verifies that a body has been set on the + * {@link TestToadletContext}, and then verifies it using the given + * {@link Matcher}. + * + * @param bodyTextMatcher The matcher for the body text, if present + * @return A matcher for a {@link TestToadletContext} + */ + public static Matcher hasBodyText(Matcher bodyTextMatcher) { + return new TypeSafeDiagnosingMatcher() { + @Override + protected boolean matchesSafely(TestToadletContext toadletContext, Description mismatchDescription) { + if (toadletContext.getBodyText() == null) { + mismatchDescription.appendText("no body text present"); + return false; + } + if (!bodyTextMatcher.matches(toadletContext.getBodyText())) { + mismatchDescription.appendText("body text "); + bodyTextMatcher.describeMismatch(toadletContext.getBodyText(), mismatchDescription); + return false; + } + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("has text body matching ").appendDescriptionOf(bodyTextMatcher); + } + }; + } + + /** + * Returns a {@link Matcher} that uses the given matcher on the given + * header’s values. This matcher assumes that the response headers of + * the {@link TestToadletContext} have already been written; if they + * have not yet been written, this matcher will not match. + * + * @param name The name of the header to match + * @param valueMatcher A matcher for the header’s values + * @return A matcher for a {@link TestToadletContext} + */ + public static Matcher hasHeader(String name, Matcher> valueMatcher) { + return new TypeSafeDiagnosingMatcher() { + @Override + protected boolean matchesSafely(TestToadletContext toadletContext, Description mismatchDescription) { + Map> headers = toadletContext.getResponseHeaders(); + if (!headers.containsKey(name.toLowerCase())) { + mismatchDescription.appendText("header ").appendValue(name).appendText(" not present"); + return false; + } + if (!valueMatcher.matches(headers.get(name.toLowerCase()))) { + mismatchDescription.appendText("header ").appendValue(name).appendText(" "); + valueMatcher.describeMismatch(headers.get(name.toLowerCase()), mismatchDescription); + return false; + } + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("has header ").appendValue(name).appendText(" matching ").appendDescriptionOf(valueMatcher); + } + }; + } + + /** + * Returns a {@link Matcher} that verifies that a + * {@link TestToadletContext} has written HTML to its body, then parses + * that body and verifies it using the given {@link Document} matcher. + * + * @param documentMatcher The matcher for the HTML document, if present + * @return A matcher for a {@link TestToadletContext} + */ + public static Matcher isHtml(Matcher documentMatcher) { + return new TypeSafeDiagnosingMatcher() { + @Override + protected boolean matchesSafely(TestToadletContext toadletContext, Description mismatchDescription) { + try { + MimeType mimeType = new MimeType(toadletContext.getResponseHeaders().get("content-type").get(0)); + if (!mimeType.match("text/html")) { + mismatchDescription.appendText("MIME type was ").appendValue(mimeType); + return false; + } + Document document = Jsoup.parse(toadletContext.getBodyText()); + if (!documentMatcher.matches(document)) { + mismatchDescription.appendText("Document "); + documentMatcher.describeMismatch(document, mismatchDescription); + return false; + } + return true; + } catch (MimeTypeParseException e) { + throw new RuntimeException(e); + } + } + + @Override + public void describeTo(Description description) { + description.appendText("is HTML matching ").appendDescriptionOf(documentMatcher); + } + }; + } + +} diff --git a/test/freenet/test/ToadletContextMatchersTest.java b/test/freenet/test/ToadletContextMatchersTest.java new file mode 100644 index 00000000000..605193fcd1a --- /dev/null +++ b/test/freenet/test/ToadletContextMatchersTest.java @@ -0,0 +1,276 @@ +package freenet.test; + +import freenet.clients.http.TestToadletContext; +import freenet.clients.http.ToadletContextClosedException; +import freenet.support.MultiValueTable; +import jakarta.activation.MimeType; +import java.io.IOException; +import java.util.Date; +import java.util.List; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.StringDescription; +import org.jsoup.nodes.Document; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; + +import static freenet.test.ToadletContextMatchers.hasBodyText; +import static freenet.test.ToadletContextMatchers.hasContentType; +import static freenet.test.ToadletContextMatchers.hasHeader; +import static freenet.test.ToadletContextMatchers.hasStatus; +import static freenet.test.ToadletContextMatchers.isHtml; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Arrays.asList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.any; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.startsWith; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +public class ToadletContextMatchersTest { + + @Test + public void hasStatusMatcherRecognizesThatStatusHasNotBeenSet() { + assertThat(hasStatus(any(Integer.class)).matches(toadletContext), equalTo(false)); + } + + @Test + public void hasStatusMatcherDescribesMismatchCorrectly() { + hasStatus(any(Integer.class)).describeMismatch(toadletContext, description); + assertThat(description.toString(), equalTo("no status set")); + } + + @Test + public void hasStatusMatcherDescribesMismatchOfGivenMatcher() throws ToadletContextClosedException, IOException { + toadletContext.sendReplyHeadersStatic(404, "Not Found", null, null, 0, new Date()); + hasStatus(equalTo(200)).describeMismatch(toadletContext, description); + assertThat(description.toString(), equalTo("status code was <404>")); + } + + @Test + public void hasStatusMatcherRecognizesThatStatusHasBeenSet() throws ToadletContextClosedException, IOException { + toadletContext.sendReplyHeadersStatic(200, "OK", null, null, 0, new Date()); + assertThat(hasStatus(any(Integer.class)).matches(toadletContext), equalTo(true)); + } + + @Test + public void hasStatusMatcherDescribesItself() { + hasStatus(any(Integer.class)).describeTo(description); + assertThat(description.toString(), equalTo("has status matching an instance of java.lang.Integer")); + } + + @Test + public void statusMatcherIsNotConsultedIfStatusHasNotBeenSet() { + hasStatus(statusMatcher).matches(toadletContext); + verifyZeroInteractions(statusMatcher); + } + + @Test + public void statusMatcherIsConsultedIfStatusHasBeenSet() throws ToadletContextClosedException, IOException { + toadletContext.sendReplyHeadersStatic(301, "OK", null, null, 0, new Date()); + hasStatus(statusMatcher).matches(toadletContext); + verify(statusMatcher).matches(eq(301)); + } + + @Test + public void hasContentTypeMatcherDoesNotMatchIfNoContentTypeHeaderIsPresent() { + assertThat(hasContentType(any(MimeType.class)).matches(toadletContext), equalTo(false)); + } + + @Test + public void hasContentTypeMatcherDescribesMismatch() { + hasContentType(any(MimeType.class)).describeMismatch(toadletContext, description); + assertThat(description.toString(), equalTo("no content type set")); + } + + @Test + public void hasContentTypeMatcherDescribesMismatchOfMimeTypeMatcher() throws Exception { + toadletContext.sendReplyHeadersStatic(200, "OK", null, "text/html", 0, null); + hasContentType(nullValue()).describeMismatch(toadletContext, description); + assertThat(description.toString(), equalTo("content type was ")); + } + + @Test + public void hasContentTypeMatcherMatchesIfContentTypeIsPresent() throws Exception { + toadletContext.sendReplyHeadersStatic(200, "OK", null, "text/html", 0, null); + assertThat(hasContentType(any(MimeType.class)).matches(toadletContext), equalTo(true)); + } + + @Test + public void hasContentTypeMatcherDescribesItself() { + hasContentType(any(MimeType.class)).describeTo(description); + assertThat(description.toString(), equalTo("has content type matching an instance of jakarta.activation.MimeType")); + } + + @Test + public void mimeTypeMatcherIsNotConsultedIfThereIsNoContentType() { + hasContentType(mimeTypeMatcher).matches(toadletContext); + verifyZeroInteractions(mimeTypeMatcher); + } + + @Test + public void mimeTypeMatcherIsConsultedIfThereIsAContentType() throws Exception { + toadletContext.sendReplyHeadersStatic(200, "OK", null, "text/html", 0, null); + hasContentType(mimeTypeMatcher).matches(toadletContext); + ArgumentCaptor mimeTypeCaptor = ArgumentCaptor.forClass(MimeType.class); + verify(mimeTypeMatcher).matches(mimeTypeCaptor.capture()); + assertThat(mimeTypeCaptor.getValue().getBaseType(), equalTo("text/html")); + assertThat(mimeTypeCaptor.getValue().getParameters().isEmpty(), equalTo(true)); + } + + @Test + public void hasBodyTextMatcherDoesNotMatchIfBodyHasNotBeenWritten() { + assertThat(hasBodyText(any(String.class)).matches(toadletContext), equalTo(false)); + } + + @Test + public void hasBodyTextMatcherDescribesMismatch() { + hasBodyText(any(String.class)).describeMismatch(toadletContext, description); + assertThat(description.toString(), equalTo("no body text present")); + } + + @Test + public void hasBodyTextMatcherDescribesMismatchOfGivenMatcher() throws ToadletContextClosedException, IOException { + toadletContext.writeData("test".getBytes(UTF_8)); + hasBodyText(startsWith("foo")).describeMismatch(toadletContext, description); + assertThat(description.toString(), equalTo("body text was \"test\"")); + } + + @Test + public void hasBodyTextMatcherMatchesIfBodyIsPresent() throws ToadletContextClosedException, IOException { + toadletContext.writeData("test".getBytes(UTF_8)); + assertThat(hasBodyText(any(String.class)).matches(toadletContext), equalTo(true)); + } + + @Test + public void hasBodyTextMatcherDescribesItselfCorrectly() { + hasBodyText(any(String.class)).describeTo(description); + assertThat(description.toString(), equalTo("has text body matching an instance of java.lang.String")); + } + + @Test + public void bodyTextMatcherIsNotConsultedIfBodyIsNotPresent() { + hasBodyText(bodyTextMatcher).matches(toadletContext); + verifyZeroInteractions(bodyTextMatcher); + } + + @Test + public void bodyTextMatcherIsConsultedIfBodyIsPresent() throws ToadletContextClosedException, IOException { + toadletContext.writeData("test".getBytes(UTF_8)); + hasBodyText(bodyTextMatcher).matches(toadletContext); + verify(bodyTextMatcher).matches(eq("test")); + } + + @Test + public void hasHeaderDoesNotMatchIfHeaderDoesNotExist() { + assertThat(hasHeader("foo", any(List.class)).matches(toadletContext), equalTo(false)); + } + + @Test + public void hasHeaderDescribesMismatch() { + hasHeader("foo", any(List.class)).describeMismatch(toadletContext, description); + assertThat(description.toString(), equalTo("header \"foo\" not present")); + } + + @Test + public void hasHeaderDescribesMismatchOfGivenMatcher() throws ToadletContextClosedException, IOException { + MultiValueTable headers = new MultiValueTable<>(); + headers.put("foo", "bar"); + toadletContext.sendReplyHeadersStatic(200, "OK", headers, null, 0, null); + hasHeader("foo", nullValue()).describeMismatch(toadletContext, description); + assertThat(description.toString(), equalTo("header \"foo\" was <[bar]>")); + } + + @Test + public void hasHeaderMatchesIfHeaderExists() throws ToadletContextClosedException, IOException { + MultiValueTable headers = new MultiValueTable<>(); + headers.put("foo", "bar"); + toadletContext.sendReplyHeadersStatic(200, "OK", headers, null, 0, null); + assertThat(hasHeader("foo", any(List.class)).matches(toadletContext), equalTo(true)); + } + + @Test + public void hasHeaderDescribeItself() { + hasHeader("foo", any(List.class)).describeTo(description); + assertThat(description.toString(), equalTo("has header \"foo\" matching an instance of java.util.List")); + } + + @Test + public void valueMatcherIsNotConsultedIfHeaderDoesNotExist() { + hasHeader("foo", headerMatcher).matches(toadletContext); + verifyZeroInteractions(headerMatcher); + } + + @Test + public void valueMatcherIsConsultedIfHeaderExists() throws ToadletContextClosedException, IOException { + MultiValueTable headers = new MultiValueTable<>(); + headers.put("foo", "bar"); + toadletContext.sendReplyHeadersStatic(200, "OK", headers, null, 0, null); + hasHeader("foo", headerMatcher).matches(toadletContext); + verify(headerMatcher).matches(eq(asList("bar"))); + } + + @Test + public void isHtmlMatcherDoesNotMatchIfContentTypeIsNotHtml() throws Exception { + toadletContext.sendReplyHeadersStatic(200, "OK", null, "text/plain", 0, null); + assertThat(isHtml(any(Document.class)).matches(toadletContext), equalTo(false)); + } + + @Test + public void isHtmlMatcherDescribesMismatch() throws Exception { + toadletContext.sendReplyHeadersStatic(200, "OK", null, "text/plain", 0, null); + isHtml(any(Document.class)).describeMismatch(toadletContext, description); + assertThat(description.toString(), equalTo("MIME type was ")); + } + + @Test + public void isHtmlMatcherMatchesIfContentTypeIsHtml() throws Exception { + toadletContext.sendReplyHeadersStatic(200, "OK", null, "text/html; charset=utf-8", 0, null); + toadletContext.writeData("test".getBytes(UTF_8)); + assertThat(isHtml(any(Document.class)).matches(toadletContext), equalTo(true)); + } + + @Test + public void isHtmlMatcherDescribesItself() { + isHtml(any(Document.class)).describeTo(description); + assertThat(description.toString(), equalTo("is HTML matching an instance of org.jsoup.nodes.Document")); + } + + @Test + public void documentMatcherIsNotConsultedIfMimeTypeIsNotHtml() throws Exception { + Matcher documentMatcher = mock(Matcher.class); + toadletContext.sendReplyHeadersStatic(200, "OK", null, "text/plain", 0, null); + isHtml(documentMatcher).matches(toadletContext); + verifyZeroInteractions(documentMatcher); + } + + @Test + public void isHtmlMatcherDescribesMismatchOfDocumentMatcher() throws ToadletContextClosedException, IOException { + toadletContext.sendReplyHeadersStatic(200, "OK", null, "text/html; charset=utf-8", 0, null); + toadletContext.writeData("test".getBytes(UTF_8)); + isHtml(nullValue()).describeMismatch(toadletContext, description); + assertThat(description.toString(), equalTo("Document was <\n \n test\n>")); + } + + @Test + public void documentMatcherIsConsultedIfMimeTypeIsHtml() throws Exception { + Matcher documentMatcher = mock(Matcher.class); + toadletContext.sendReplyHeadersStatic(200, "OK", null, "text/html; charset=utf-8", 0, null); + toadletContext.writeData("test".getBytes(UTF_8)); + isHtml(documentMatcher).matches(toadletContext); + verify(documentMatcher).matches(ArgumentMatchers.any(Document.class)); + } + + private final TestToadletContext toadletContext = TestToadletContext.builder().build(); + private final Description description = new StringDescription(); + private final Matcher statusMatcher = mock(Matcher.class); + private final Matcher mimeTypeMatcher = mock(Matcher.class); + private final Matcher bodyTextMatcher = mock(Matcher.class); + private final Matcher> headerMatcher = mock(Matcher.class); + +}