From 61b1cb58b9f91b4bc84847a48239cc36ecfd7536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 26 May 2023 09:04:08 +0200 Subject: [PATCH 001/157] Add project structure --- .dockerignore | 5 + .gitignore | 39 ++ COPYING.md | 661 ++++++++++++++++++ README.md | 7 + build.gradle | 58 ++ gradle.properties | 5 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 58910 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 185 +++++ gradlew.bat | 104 +++ settings.gradle | 13 + src/main/docker/Dockerfile.jvm | 95 +++ src/main/docker/Dockerfile.legacy-jar | 91 +++ src/main/docker/Dockerfile.native | 27 + src/main/docker/Dockerfile.native-micro | 30 + .../kotlin/app/fyreplace/HealthResource.kt | 11 + src/main/resources/application.properties | 0 .../kotlin/app/fyreplace/HealthResourceIT.kt | 6 + .../app/fyreplace/HealthResourceTest.kt | 16 + 19 files changed, 1358 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 COPYING.md create mode 100644 README.md create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/docker/Dockerfile.jvm create mode 100644 src/main/docker/Dockerfile.legacy-jar create mode 100644 src/main/docker/Dockerfile.native create mode 100644 src/main/docker/Dockerfile.native-micro create mode 100644 src/main/kotlin/app/fyreplace/HealthResource.kt create mode 100644 src/main/resources/application.properties create mode 100644 src/native-test/kotlin/app/fyreplace/HealthResourceIT.kt create mode 100644 src/test/kotlin/app/fyreplace/HealthResourceTest.kt diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4361d2f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +* +!build/*-runner +!build/*-runner.jar +!build/lib/* +!build/quarkus-app/* \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..216783d --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# Gradle +.gradle/ +build/ + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Local environment +.env + +# Plugin directory +/.quarkus/cli/plugins/ diff --git a/COPYING.md b/COPYING.md new file mode 100644 index 0000000..bd9d11c --- /dev/null +++ b/COPYING.md @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + + Preamble + +The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + +A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + +The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + +An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + +The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU Affero General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +1. Source Code. + +The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +13. Remote Network Interaction; Use with the GNU General Public License. + +Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b9ceab7 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Fyreplace API + +Future API for [Fyreplace](https://fyreplace.net). + +## License + +Fyreplace is made available under the AGPLv3+ license (see COPYING.md). diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..53db27e --- /dev/null +++ b/build.gradle @@ -0,0 +1,58 @@ +plugins { + id "java" + id "org.jetbrains.kotlin.jvm" version "1.8.21" + id "org.jetbrains.kotlin.plugin.allopen" version "1.8.21" + id "com.palantir.git-version" version "3.0.0" + id "io.quarkus" +} + +repositories { + mavenCentral() + mavenLocal() +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") + implementation "io.quarkus:quarkus-kotlin" + implementation "io.quarkus:quarkus-arc" + implementation "io.quarkus:quarkus-resteasy-reactive" + testImplementation "io.quarkus:quarkus-junit5" + testImplementation "io.rest-assured:rest-assured" +} + +group "app.fyreplace" +version gitVersion() + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +test { + systemProperty "java.util.logging.manager", "org.jboss.logmanager.LogManager" +} + +allOpen { + annotation("jakarta.ws.rs.Path") + annotation("jakarta.enterprise.context.ApplicationScoped") + annotation("io.quarkus.test.junit.QuarkusTest") +} + +compileJava { + options.encoding = "UTF-8" + options.compilerArgs << "-parameters" +} + +compileTestJava { + options.encoding = "UTF-8" +} + +compileKotlin { + kotlinOptions.jvmTarget = JavaVersion.VERSION_17 + kotlinOptions.javaParameters = true +} + +compileTestKotlin { + kotlinOptions.jvmTarget = JavaVersion.VERSION_17 +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..d98e927 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,5 @@ +quarkusPluginId=io.quarkus +quarkusPluginVersion=3.0.4.Final +quarkusPlatformGroupId=io.quarkus.platform +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformVersion=3.0.4.Final diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..62d4c053550b91381bbd28b1afc82d634bf73a8a GIT binary patch literal 58910 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 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..fae0804 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..fbd7c51 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..a9f778a --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,104 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..62f3129 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,13 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + mavenLocal() + } + + plugins { + id "${quarkusPluginId}" version "${quarkusPluginVersion}" + } +} + +rootProject.name="fyreplace-api" diff --git a/src/main/docker/Dockerfile.jvm b/src/main/docker/Dockerfile.jvm new file mode 100644 index 0000000..e4bef80 --- /dev/null +++ b/src/main/docker/Dockerfile.jvm @@ -0,0 +1,95 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./gradlew build +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/fyreplace-api-jvm . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/fyreplace-api-jvm +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005. +# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005 +# when running the container +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/fyreplace-api-jvm +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-17:1.15 + +ENV LANGUAGE='en_US:en' + + +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=185 build/quarkus-app/lib/ /deployments/lib/ +COPY --chown=185 build/quarkus-app/*.jar /deployments/ +COPY --chown=185 build/quarkus-app/app/ /deployments/app/ +COPY --chown=185 build/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 185 +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" + diff --git a/src/main/docker/Dockerfile.legacy-jar b/src/main/docker/Dockerfile.legacy-jar new file mode 100644 index 0000000..7a789e3 --- /dev/null +++ b/src/main/docker/Dockerfile.legacy-jar @@ -0,0 +1,91 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.type=legacy-jar +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/fyreplace-api-legacy-jar . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/fyreplace-api-legacy-jar +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005. +# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005 +# when running the container +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/fyreplace-api-legacy-jar +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-17:1.15 + +ENV LANGUAGE='en_US:en' + + +COPY build/lib/* /deployments/lib/ +COPY build/*-runner.jar /deployments/quarkus-run.jar + +EXPOSE 8080 +USER 185 +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" diff --git a/src/main/docker/Dockerfile.native b/src/main/docker/Dockerfile.native new file mode 100644 index 0000000..9780b84 --- /dev/null +++ b/src/main/docker/Dockerfile.native @@ -0,0 +1,27 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.type=native +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native -t quarkus/fyreplace-api . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/fyreplace-api +# +### +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root build/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/src/main/docker/Dockerfile.native-micro b/src/main/docker/Dockerfile.native-micro new file mode 100644 index 0000000..9063844 --- /dev/null +++ b/src/main/docker/Dockerfile.native-micro @@ -0,0 +1,30 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# It uses a micro base image, tuned for Quarkus native executables. +# It reduces the size of the resulting container image. +# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image. +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.type=native +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/fyreplace-api . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/fyreplace-api +# +### +FROM quay.io/quarkus/quarkus-micro-image:2.0 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root build/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/src/main/kotlin/app/fyreplace/HealthResource.kt b/src/main/kotlin/app/fyreplace/HealthResource.kt new file mode 100644 index 0000000..3d90d1c --- /dev/null +++ b/src/main/kotlin/app/fyreplace/HealthResource.kt @@ -0,0 +1,11 @@ +package app.fyreplace + +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path + +@Path("/health") +@Suppress("unused") +class HealthResource { + @GET + fun list() = Unit +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..e69de29 diff --git a/src/native-test/kotlin/app/fyreplace/HealthResourceIT.kt b/src/native-test/kotlin/app/fyreplace/HealthResourceIT.kt new file mode 100644 index 0000000..53c1e40 --- /dev/null +++ b/src/native-test/kotlin/app/fyreplace/HealthResourceIT.kt @@ -0,0 +1,6 @@ +package app.fyreplace + +import io.quarkus.test.junit.QuarkusIntegrationTest + +@QuarkusIntegrationTest +class HealthResourceIT : HealthResourceTest() diff --git a/src/test/kotlin/app/fyreplace/HealthResourceTest.kt b/src/test/kotlin/app/fyreplace/HealthResourceTest.kt new file mode 100644 index 0000000..0b4ebab --- /dev/null +++ b/src/test/kotlin/app/fyreplace/HealthResourceTest.kt @@ -0,0 +1,16 @@ +package app.fyreplace + +import io.quarkus.test.junit.QuarkusTest +import io.restassured.RestAssured.given +import org.junit.jupiter.api.Test + +@QuarkusTest +class HealthResourceTest { + @Test + fun testListEndpoint() { + given() + .`when`().get("/health") + .then() + .statusCode(204) + } +} From 17d0be9532739f74f84bf44f53b75e63520cb7fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 4 Jun 2023 12:49:15 +0200 Subject: [PATCH 002/157] Centralize versioning --- build.gradle | 12 ++++++------ gradle.properties | 2 ++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index 53db27e..6854316 100644 --- a/build.gradle +++ b/build.gradle @@ -1,11 +1,14 @@ plugins { id "java" - id "org.jetbrains.kotlin.jvm" version "1.8.21" - id "org.jetbrains.kotlin.plugin.allopen" version "1.8.21" - id "com.palantir.git-version" version "3.0.0" + id "org.jetbrains.kotlin.jvm" version "${kotlinVersion}" + id "org.jetbrains.kotlin.plugin.allopen" version "${kotlinVersion}" + id "com.palantir.git-version" version "${gitPluginVersion}" id "io.quarkus" } +group = "app.fyreplace" +version = gitVersion() + repositories { mavenCentral() mavenLocal() @@ -21,9 +24,6 @@ dependencies { testImplementation "io.rest-assured:rest-assured" } -group "app.fyreplace" -version gitVersion() - java { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 diff --git a/gradle.properties b/gradle.properties index d98e927..35d4829 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,5 @@ +kotlinVersion=1.8.21 +gitPluginVersion=3.0.0 quarkusPluginId=io.quarkus quarkusPluginVersion=3.0.4.Final quarkusPlatformGroupId=io.quarkus.platform From 7315e425c6c99da7cd5bab4cc3074515b893bcf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 4 Jun 2023 12:51:24 +0200 Subject: [PATCH 003/157] Add Hibernate --- build.gradle | 17 ++++++++++------- src/main/kotlin/app/fyreplace/HealthResource.kt | 11 ----------- src/main/resources/application.properties | 2 ++ .../kotlin/app/fyreplace/HealthResourceIT.kt | 6 ------ .../kotlin/app/fyreplace/HealthResourceTest.kt | 16 ---------------- 5 files changed, 12 insertions(+), 40 deletions(-) delete mode 100644 src/main/kotlin/app/fyreplace/HealthResource.kt delete mode 100644 src/native-test/kotlin/app/fyreplace/HealthResourceIT.kt delete mode 100644 src/test/kotlin/app/fyreplace/HealthResourceTest.kt diff --git a/build.gradle b/build.gradle index 6854316..584ec6c 100644 --- a/build.gradle +++ b/build.gradle @@ -15,13 +15,16 @@ repositories { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" - implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") - implementation "io.quarkus:quarkus-kotlin" - implementation "io.quarkus:quarkus-arc" - implementation "io.quarkus:quarkus-resteasy-reactive" - testImplementation "io.quarkus:quarkus-junit5" - testImplementation "io.rest-assured:rest-assured" + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")) + implementation("io.quarkus:quarkus-kotlin") + implementation("io.quarkus:quarkus-arc") + implementation("io.quarkus:quarkus-resteasy-reactive") + implementation("io.quarkus:quarkus-hibernate-orm-panache") + implementation("io.quarkus:quarkus-jdbc-postgresql") + implementation("io.quarkus:quarkus-smallrye-health") + testImplementation("io.quarkus:quarkus-junit5") + testImplementation("io.rest-assured:rest-assured") } java { diff --git a/src/main/kotlin/app/fyreplace/HealthResource.kt b/src/main/kotlin/app/fyreplace/HealthResource.kt deleted file mode 100644 index 3d90d1c..0000000 --- a/src/main/kotlin/app/fyreplace/HealthResource.kt +++ /dev/null @@ -1,11 +0,0 @@ -package app.fyreplace - -import jakarta.ws.rs.GET -import jakarta.ws.rs.Path - -@Path("/health") -@Suppress("unused") -class HealthResource { - @GET - fun list() = Unit -} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e69de29..3319e28 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -0,0 +1,2 @@ +quarkus.hibernate-orm.physical-naming-strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy +%dev.quarkus.hibernate-orm.database.generation=drop-and-create diff --git a/src/native-test/kotlin/app/fyreplace/HealthResourceIT.kt b/src/native-test/kotlin/app/fyreplace/HealthResourceIT.kt deleted file mode 100644 index 53c1e40..0000000 --- a/src/native-test/kotlin/app/fyreplace/HealthResourceIT.kt +++ /dev/null @@ -1,6 +0,0 @@ -package app.fyreplace - -import io.quarkus.test.junit.QuarkusIntegrationTest - -@QuarkusIntegrationTest -class HealthResourceIT : HealthResourceTest() diff --git a/src/test/kotlin/app/fyreplace/HealthResourceTest.kt b/src/test/kotlin/app/fyreplace/HealthResourceTest.kt deleted file mode 100644 index 0b4ebab..0000000 --- a/src/test/kotlin/app/fyreplace/HealthResourceTest.kt +++ /dev/null @@ -1,16 +0,0 @@ -package app.fyreplace - -import io.quarkus.test.junit.QuarkusTest -import io.restassured.RestAssured.given -import org.junit.jupiter.api.Test - -@QuarkusTest -class HealthResourceTest { - @Test - fun testListEndpoint() { - given() - .`when`().get("/health") - .then() - .statusCode(204) - } -} From 118fb44691d234fdc3ddb70b4fba7b18cbf1663d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 4 Jun 2023 12:52:48 +0200 Subject: [PATCH 004/157] Add Sentry --- build.gradle | 2 + gradle.properties | 2 + quarkus-sentry/build.gradle | 19 +++++++++ quarkus-sentry/deployment/build.gradle | 12 ++++++ .../app/fyreplace/sentry/SentryProcessor.java | 30 +++++++++++++ .../sentry/SentrySpanProcessorProducer.java | 14 +++++++ quarkus-sentry/runtime/build.gradle | 18 ++++++++ .../app/fyreplace/sentry/SentryConfig.java | 35 ++++++++++++++++ .../sentry/SentryConfigurablePropagator.java | 19 +++++++++ .../app/fyreplace/sentry/SentryRecorder.java | 42 +++++++++++++++++++ ...nfigure.spi.ConfigurablePropagatorProvider | 1 + .../src/main/resources/application.properties | 2 + settings.gradle | 5 ++- 13 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 quarkus-sentry/build.gradle create mode 100644 quarkus-sentry/deployment/build.gradle create mode 100644 quarkus-sentry/deployment/src/main/java/app/fyreplace/sentry/SentryProcessor.java create mode 100644 quarkus-sentry/deployment/src/main/java/app/fyreplace/sentry/SentrySpanProcessorProducer.java create mode 100644 quarkus-sentry/runtime/build.gradle create mode 100644 quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryConfig.java create mode 100644 quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryConfigurablePropagator.java create mode 100644 quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryRecorder.java create mode 100644 quarkus-sentry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider create mode 100644 quarkus-sentry/runtime/src/main/resources/application.properties diff --git a/build.gradle b/build.gradle index 584ec6c..7f15a34 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,8 @@ dependencies { implementation("io.quarkus:quarkus-hibernate-orm-panache") implementation("io.quarkus:quarkus-jdbc-postgresql") implementation("io.quarkus:quarkus-smallrye-health") + implementation(project(":quarkus-sentry:deployment")) + implementation(project(":quarkus-sentry:runtime")) testImplementation("io.quarkus:quarkus-junit5") testImplementation("io.rest-assured:rest-assured") } diff --git a/gradle.properties b/gradle.properties index 35d4829..eaca3c6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,9 @@ kotlinVersion=1.8.21 gitPluginVersion=3.0.0 quarkusPluginId=io.quarkus +quarkusExtensionPluginId=io.quarkus.extension quarkusPluginVersion=3.0.4.Final quarkusPlatformGroupId=io.quarkus.platform quarkusPlatformArtifactId=quarkus-bom quarkusPlatformVersion=3.0.4.Final +sentryVersion=6.21.0 diff --git a/quarkus-sentry/build.gradle b/quarkus-sentry/build.gradle new file mode 100644 index 0000000..a73c369 --- /dev/null +++ b/quarkus-sentry/build.gradle @@ -0,0 +1,19 @@ +plugins { + id "com.palantir.git-version" version "${gitPluginVersion}" +} + +subprojects { + apply plugin: "java-library" + group = "app.fyreplace" + version = gitVersion() + dependencies { + implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")) + implementation("io.quarkus:quarkus-opentelemetry") + implementation("io.sentry:sentry-opentelemetry-core:${sentryVersion}") + annotationProcessor("io.quarkus:quarkus-extension-processor") + } +} + +repositories { + mavenCentral() +} diff --git a/quarkus-sentry/deployment/build.gradle b/quarkus-sentry/deployment/build.gradle new file mode 100644 index 0000000..b85a7b0 --- /dev/null +++ b/quarkus-sentry/deployment/build.gradle @@ -0,0 +1,12 @@ +plugins { +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("io.quarkus:quarkus-core-deployment") + implementation("io.quarkus:quarkus-arc-deployment") + implementation(project(":quarkus-sentry:runtime")) +} diff --git a/quarkus-sentry/deployment/src/main/java/app/fyreplace/sentry/SentryProcessor.java b/quarkus-sentry/deployment/src/main/java/app/fyreplace/sentry/SentryProcessor.java new file mode 100644 index 0000000..e7ef3a3 --- /dev/null +++ b/quarkus-sentry/deployment/src/main/java/app/fyreplace/sentry/SentryProcessor.java @@ -0,0 +1,30 @@ +package app.fyreplace.sentry; + +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.LogHandlerBuildItem; + +final class SentryProcessor { + private static final String FEATURE = "sentry"; + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + LogHandlerBuildItem addSentryHandler(final SentryConfig config, final SentryRecorder recorder) { + return new LogHandlerBuildItem(recorder.create(config)); + } + + @BuildStep + AdditionalBeanBuildItem addAdditionalBeans() { + return AdditionalBeanBuildItem.builder() + .addBeanClass(SentrySpanProcessorProducer.class) + .build(); + } +} diff --git a/quarkus-sentry/deployment/src/main/java/app/fyreplace/sentry/SentrySpanProcessorProducer.java b/quarkus-sentry/deployment/src/main/java/app/fyreplace/sentry/SentrySpanProcessorProducer.java new file mode 100644 index 0000000..3252f51 --- /dev/null +++ b/quarkus-sentry/deployment/src/main/java/app/fyreplace/sentry/SentrySpanProcessorProducer.java @@ -0,0 +1,14 @@ +package app.fyreplace.sentry; + +import io.sentry.opentelemetry.SentrySpanProcessor; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; + +@ApplicationScoped +public final class SentrySpanProcessorProducer { + @Produces + @ApplicationScoped + public SentrySpanProcessor produceSentrySpanProcessor() { + return new SentrySpanProcessor(); + } +} diff --git a/quarkus-sentry/runtime/build.gradle b/quarkus-sentry/runtime/build.gradle new file mode 100644 index 0000000..8c4bae8 --- /dev/null +++ b/quarkus-sentry/runtime/build.gradle @@ -0,0 +1,18 @@ +plugins { + id "io.quarkus.extension" +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("io.quarkus:quarkus-core") + implementation("io.quarkus:quarkus-arc") + implementation("io.sentry:sentry-jul:${sentryVersion}") + implementation("io.opentelemetry.instrumentation:opentelemetry-jdbc") +} + +quarkusExtension { + deploymentModule = ":quarkus-sentry:deployment" +} diff --git a/quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryConfig.java b/quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryConfig.java new file mode 100644 index 0000000..3c735e3 --- /dev/null +++ b/quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryConfig.java @@ -0,0 +1,35 @@ +package app.fyreplace.sentry; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +import java.util.Optional; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +@ConfigRoot(phase = ConfigPhase.RUN_TIME, name = "sentry") +public final class SentryConfig { + /** + * Sentry Data Source Name. + */ + @ConfigItem + public Optional dsn = Optional.empty(); + + /** + * Environment the events are tagged with. + */ + @ConfigItem + public Optional environment = Optional.empty(); + + /** + * Which code release the events will belong to. + */ + @ConfigItem + public Optional release = Optional.empty(); + + /** + * Percentage of performance events sent to Sentry. + */ + @ConfigItem(defaultValue = "0.0") + public Optional tracesSampleRate = Optional.empty(); +} diff --git a/quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryConfigurablePropagator.java b/quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryConfigurablePropagator.java new file mode 100644 index 0000000..115913b --- /dev/null +++ b/quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryConfigurablePropagator.java @@ -0,0 +1,19 @@ +package app.fyreplace.sentry; + +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider; +import io.sentry.opentelemetry.SentryPropagator; +import jakarta.annotation.Nonnull; + +public final class SentryConfigurablePropagator implements ConfigurablePropagatorProvider { + @Override + public TextMapPropagator getPropagator(@Nonnull final ConfigProperties config) { + return new SentryPropagator(); + } + + @Override + public String getName() { + return "sentry"; + } +} diff --git a/quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryRecorder.java b/quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryRecorder.java new file mode 100644 index 0000000..05ca3f8 --- /dev/null +++ b/quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryRecorder.java @@ -0,0 +1,42 @@ +package app.fyreplace.sentry; + +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; +import io.sentry.Instrumenter; +import io.sentry.Sentry; +import io.sentry.SentryOptions; +import io.sentry.jul.SentryHandler; +import io.sentry.opentelemetry.OpenTelemetryLinkErrorEventProcessor; + +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Handler; +import java.util.logging.Level; + +@Recorder +public class SentryRecorder { + public RuntimeValue> create(final SentryConfig config) { + if (config.dsn.isEmpty()) { + return new RuntimeValue<>(Optional.empty()); + } + + final var options = new AtomicReference(); + Sentry.init(it -> { + it.setDsn(config.dsn.get()); + config.environment.ifPresent(it::setEnvironment); + config.release.ifPresent(it::setRelease); + config.tracesSampleRate.ifPresent(it::setTracesSampleRate); + it.addInAppInclude("app.fyreplace"); + it.setInstrumenter(Instrumenter.OTEL); + it.addEventProcessor(new OpenTelemetryLinkErrorEventProcessor()); + options.set(it); + }); + + final var handler = new SentryHandler(options.get()); + handler.setPrintfStyle(true); + handler.setLevel(Level.WARNING); + handler.setMinimumEventLevel(Level.WARNING); + handler.setMinimumBreadcrumbLevel(Level.INFO); + return new RuntimeValue<>(Optional.of(handler)); + } +} diff --git a/quarkus-sentry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider b/quarkus-sentry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider new file mode 100644 index 0000000..2dd1d7b --- /dev/null +++ b/quarkus-sentry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider @@ -0,0 +1 @@ +app.fyreplace.sentry.SentryConfigurablePropagator diff --git a/quarkus-sentry/runtime/src/main/resources/application.properties b/quarkus-sentry/runtime/src/main/resources/application.properties new file mode 100644 index 0000000..a22e06e --- /dev/null +++ b/quarkus-sentry/runtime/src/main/resources/application.properties @@ -0,0 +1,2 @@ +quarkus.datasource.jdbc.telemetry=true +quarkus.otel.propagators=tracecontext,baggage,sentry diff --git a/settings.gradle b/settings.gradle index 62f3129..755653c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,7 +7,10 @@ pluginManagement { plugins { id "${quarkusPluginId}" version "${quarkusPluginVersion}" + id "${quarkusExtensionPluginId}" version "${quarkusPluginVersion}" } } -rootProject.name="fyreplace-api" +rootProject.name = "fyreplace-api" + +include(":quarkus-sentry:runtime", ":quarkus-sentry:deployment") From 132a2cba15b7cb1008fa9a922c8df9336ea387fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 4 Jun 2023 12:55:18 +0200 Subject: [PATCH 005/157] Update dependencies --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index eaca3c6..b2a0c6d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,8 +2,8 @@ kotlinVersion=1.8.21 gitPluginVersion=3.0.0 quarkusPluginId=io.quarkus quarkusExtensionPluginId=io.quarkus.extension -quarkusPluginVersion=3.0.4.Final +quarkusPluginVersion=3.1.0.Final quarkusPlatformGroupId=io.quarkus.platform quarkusPlatformArtifactId=quarkus-bom -quarkusPlatformVersion=3.0.4.Final +quarkusPlatformVersion=3.1.0.Final sentryVersion=6.21.0 From b78372622ee6d4a3d227816823bf2e528ad55533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Mon, 5 Jun 2023 22:04:52 +0200 Subject: [PATCH 006/157] Use a single Dockerfile --- .dockerignore | 14 ++-- Dockerfile | 24 +++++++ src/main/docker/Dockerfile.jvm | 95 ------------------------- src/main/docker/Dockerfile.legacy-jar | 91 ----------------------- src/main/docker/Dockerfile.native | 27 ------- src/main/docker/Dockerfile.native-micro | 30 -------- 6 files changed, 34 insertions(+), 247 deletions(-) create mode 100644 Dockerfile delete mode 100644 src/main/docker/Dockerfile.jvm delete mode 100644 src/main/docker/Dockerfile.legacy-jar delete mode 100644 src/main/docker/Dockerfile.native delete mode 100644 src/main/docker/Dockerfile.native-micro diff --git a/.dockerignore b/.dockerignore index 4361d2f..433711f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,11 @@ * -!build/*-runner -!build/*-runner.jar -!build/lib/* -!build/quarkus-app/* \ No newline at end of file +!.git +!.gitignore +!gradle +!gradlew +!gradlew.bat +!gradle.properties +!build.gradle +!settings.gradle +!quarkus-sentry +!src diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f74a88f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM eclipse-temurin:17-jdk AS build + +RUN apt-get update && apt-get install -y git + +WORKDIR /app + +COPY . ./ +RUN git fetch --unshallow || echo "Nothing to do" +RUN ./gradlew build + +FROM eclipse-temurin:17-jre AS run + +ENV LANGUAGE="en_US:en" +ENV JAVA_OPTS="$JAVA_OPTS -Dquarkus.http.host=0.0.0.0" +ENV JAVA_OPTS="$JAVA_OPTS -Djava.util.logging.manager=org.jboss.logmanager.LogManager" + +COPY --from=build --chown=nobody /app/build/quarkus-app/lib/ /deployments/lib +COPY --from=build --chown=nobody /app/build/quarkus-app/*.jar /deployments/ +COPY --from=build --chown=nobody /app/build/quarkus-app/app/ /deployments/app +COPY --from=build --chown=nobody /app/build/quarkus-app/quarkus/ /deployments/quarkus + +EXPOSE 8080 +USER nobody +CMD java -jar /deployments/quarkus-run.jar diff --git a/src/main/docker/Dockerfile.jvm b/src/main/docker/Dockerfile.jvm deleted file mode 100644 index e4bef80..0000000 --- a/src/main/docker/Dockerfile.jvm +++ /dev/null @@ -1,95 +0,0 @@ -#### -# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode -# -# Before building the container image run: -# -# ./gradlew build -# -# Then, build the image with: -# -# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/fyreplace-api-jvm . -# -# Then run the container using: -# -# docker run -i --rm -p 8080:8080 quarkus/fyreplace-api-jvm -# -# If you want to include the debug port into your docker image -# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005. -# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005 -# when running the container -# -# Then run the container using : -# -# docker run -i --rm -p 8080:8080 quarkus/fyreplace-api-jvm -# -# This image uses the `run-java.sh` script to run the application. -# This scripts computes the command line to execute your Java application, and -# includes memory/GC tuning. -# You can configure the behavior using the following environment properties: -# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") -# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options -# in JAVA_OPTS (example: "-Dsome.property=foo") -# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is -# used to calculate a default maximal heap memory based on a containers restriction. -# If used in a container without any memory constraints for the container then this -# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio -# of the container available memory as set here. The default is `50` which means 50% -# of the available memory is used as an upper boundary. You can skip this mechanism by -# setting this value to `0` in which case no `-Xmx` option is added. -# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This -# is used to calculate a default initial heap memory based on the maximum heap memory. -# If used in a container without any memory constraints for the container then this -# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio -# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` -# is used as the initial heap size. You can skip this mechanism by setting this value -# to `0` in which case no `-Xms` option is added (example: "25") -# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. -# This is used to calculate the maximum value of the initial heap memory. If used in -# a container without any memory constraints for the container then this option has -# no effect. If there is a memory constraint then `-Xms` is limited to the value set -# here. The default is 4096MB which means the calculated value of `-Xms` never will -# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") -# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output -# when things are happening. This option, if set to true, will set -# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). -# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: -# true"). -# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). -# - CONTAINER_CORE_LIMIT: A calculated core limit as described in -# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") -# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). -# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. -# (example: "20") -# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. -# (example: "40") -# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. -# (example: "4") -# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus -# previous GC times. (example: "90") -# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") -# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") -# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should -# contain the necessary JRE command-line options to specify the required GC, which -# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). -# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") -# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") -# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be -# accessed directly. (example: "foo.example.com,bar.example.com") -# -### -FROM registry.access.redhat.com/ubi8/openjdk-17:1.15 - -ENV LANGUAGE='en_US:en' - - -# We make four distinct layers so if there are application changes the library layers can be re-used -COPY --chown=185 build/quarkus-app/lib/ /deployments/lib/ -COPY --chown=185 build/quarkus-app/*.jar /deployments/ -COPY --chown=185 build/quarkus-app/app/ /deployments/app/ -COPY --chown=185 build/quarkus-app/quarkus/ /deployments/quarkus/ - -EXPOSE 8080 -USER 185 -ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" -ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" - diff --git a/src/main/docker/Dockerfile.legacy-jar b/src/main/docker/Dockerfile.legacy-jar deleted file mode 100644 index 7a789e3..0000000 --- a/src/main/docker/Dockerfile.legacy-jar +++ /dev/null @@ -1,91 +0,0 @@ -#### -# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode -# -# Before building the container image run: -# -# ./gradlew build -Dquarkus.package.type=legacy-jar -# -# Then, build the image with: -# -# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/fyreplace-api-legacy-jar . -# -# Then run the container using: -# -# docker run -i --rm -p 8080:8080 quarkus/fyreplace-api-legacy-jar -# -# If you want to include the debug port into your docker image -# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005. -# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005 -# when running the container -# -# Then run the container using : -# -# docker run -i --rm -p 8080:8080 quarkus/fyreplace-api-legacy-jar -# -# This image uses the `run-java.sh` script to run the application. -# This scripts computes the command line to execute your Java application, and -# includes memory/GC tuning. -# You can configure the behavior using the following environment properties: -# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") -# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options -# in JAVA_OPTS (example: "-Dsome.property=foo") -# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is -# used to calculate a default maximal heap memory based on a containers restriction. -# If used in a container without any memory constraints for the container then this -# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio -# of the container available memory as set here. The default is `50` which means 50% -# of the available memory is used as an upper boundary. You can skip this mechanism by -# setting this value to `0` in which case no `-Xmx` option is added. -# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This -# is used to calculate a default initial heap memory based on the maximum heap memory. -# If used in a container without any memory constraints for the container then this -# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio -# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` -# is used as the initial heap size. You can skip this mechanism by setting this value -# to `0` in which case no `-Xms` option is added (example: "25") -# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. -# This is used to calculate the maximum value of the initial heap memory. If used in -# a container without any memory constraints for the container then this option has -# no effect. If there is a memory constraint then `-Xms` is limited to the value set -# here. The default is 4096MB which means the calculated value of `-Xms` never will -# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") -# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output -# when things are happening. This option, if set to true, will set -# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). -# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: -# true"). -# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). -# - CONTAINER_CORE_LIMIT: A calculated core limit as described in -# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") -# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). -# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. -# (example: "20") -# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. -# (example: "40") -# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. -# (example: "4") -# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus -# previous GC times. (example: "90") -# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") -# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") -# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should -# contain the necessary JRE command-line options to specify the required GC, which -# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). -# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") -# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") -# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be -# accessed directly. (example: "foo.example.com,bar.example.com") -# -### -FROM registry.access.redhat.com/ubi8/openjdk-17:1.15 - -ENV LANGUAGE='en_US:en' - - -COPY build/lib/* /deployments/lib/ -COPY build/*-runner.jar /deployments/quarkus-run.jar - -EXPOSE 8080 -USER 185 -ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" -ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" diff --git a/src/main/docker/Dockerfile.native b/src/main/docker/Dockerfile.native deleted file mode 100644 index 9780b84..0000000 --- a/src/main/docker/Dockerfile.native +++ /dev/null @@ -1,27 +0,0 @@ -#### -# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. -# -# Before building the container image run: -# -# ./gradlew build -Dquarkus.package.type=native -# -# Then, build the image with: -# -# docker build -f src/main/docker/Dockerfile.native -t quarkus/fyreplace-api . -# -# Then run the container using: -# -# docker run -i --rm -p 8080:8080 quarkus/fyreplace-api -# -### -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6 -WORKDIR /work/ -RUN chown 1001 /work \ - && chmod "g+rwX" /work \ - && chown 1001:root /work -COPY --chown=1001:root build/*-runner /work/application - -EXPOSE 8080 -USER 1001 - -CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/src/main/docker/Dockerfile.native-micro b/src/main/docker/Dockerfile.native-micro deleted file mode 100644 index 9063844..0000000 --- a/src/main/docker/Dockerfile.native-micro +++ /dev/null @@ -1,30 +0,0 @@ -#### -# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. -# It uses a micro base image, tuned for Quarkus native executables. -# It reduces the size of the resulting container image. -# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image. -# -# Before building the container image run: -# -# ./gradlew build -Dquarkus.package.type=native -# -# Then, build the image with: -# -# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/fyreplace-api . -# -# Then run the container using: -# -# docker run -i --rm -p 8080:8080 quarkus/fyreplace-api -# -### -FROM quay.io/quarkus/quarkus-micro-image:2.0 -WORKDIR /work/ -RUN chown 1001 /work \ - && chmod "g+rwX" /work \ - && chown 1001:root /work -COPY --chown=1001:root build/*-runner /work/application - -EXPOSE 8080 -USER 1001 - -CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] From 775fde2c43a31fe651c41003301f75ccdab652cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Tue, 6 Jun 2023 21:27:48 +0200 Subject: [PATCH 007/157] Go full Java --- build.gradle | 21 +-------------------- gradle.properties | 1 - 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/build.gradle b/build.gradle index 7f15a34..e016a7b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,7 @@ plugins { id "java" - id "org.jetbrains.kotlin.jvm" version "${kotlinVersion}" - id "org.jetbrains.kotlin.plugin.allopen" version "${kotlinVersion}" - id "com.palantir.git-version" version "${gitPluginVersion}" id "io.quarkus" + id "com.palantir.git-version" version "${gitPluginVersion}" } group = "app.fyreplace" @@ -15,9 +13,7 @@ repositories { } dependencies { - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")) - implementation("io.quarkus:quarkus-kotlin") implementation("io.quarkus:quarkus-arc") implementation("io.quarkus:quarkus-resteasy-reactive") implementation("io.quarkus:quarkus-hibernate-orm-panache") @@ -38,12 +34,6 @@ test { systemProperty "java.util.logging.manager", "org.jboss.logmanager.LogManager" } -allOpen { - annotation("jakarta.ws.rs.Path") - annotation("jakarta.enterprise.context.ApplicationScoped") - annotation("io.quarkus.test.junit.QuarkusTest") -} - compileJava { options.encoding = "UTF-8" options.compilerArgs << "-parameters" @@ -52,12 +42,3 @@ compileJava { compileTestJava { options.encoding = "UTF-8" } - -compileKotlin { - kotlinOptions.jvmTarget = JavaVersion.VERSION_17 - kotlinOptions.javaParameters = true -} - -compileTestKotlin { - kotlinOptions.jvmTarget = JavaVersion.VERSION_17 -} diff --git a/gradle.properties b/gradle.properties index b2a0c6d..e7ea2f3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,3 @@ -kotlinVersion=1.8.21 gitPluginVersion=3.0.0 quarkusPluginId=io.quarkus quarkusExtensionPluginId=io.quarkus.extension From fbf7e2dc9649edcb2f3db7e985d9720ab383d74f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Tue, 6 Jun 2023 21:34:15 +0200 Subject: [PATCH 008/157] Add Spotless --- build.gradle | 12 ++++++++++++ gradle.properties | 1 + .../main/java/app/fyreplace/sentry/SentryConfig.java | 1 - .../java/app/fyreplace/sentry/SentryRecorder.java | 1 - 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index e016a7b..6a47d0a 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id "java" id "io.quarkus" id "com.palantir.git-version" version "${gitPluginVersion}" + id "com.diffplug.spotless" version "${spotlessPluginVersion}" } group = "app.fyreplace" @@ -42,3 +43,14 @@ compileJava { compileTestJava { options.encoding = "UTF-8" } + +spotless { + java { + importOrder() + removeUnusedImports() + palantirJavaFormat() + target "**/src/*/java/**/*.java" + } +} + +compileJava.dependsOn "spotlessApply" diff --git a/gradle.properties b/gradle.properties index e7ea2f3..da5d9a5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,5 @@ gitPluginVersion=3.0.0 +spotlessPluginVersion=6.19.0 quarkusPluginId=io.quarkus quarkusExtensionPluginId=io.quarkus.extension quarkusPluginVersion=3.1.0.Final diff --git a/quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryConfig.java b/quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryConfig.java index 3c735e3..0e5cf37 100644 --- a/quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryConfig.java +++ b/quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryConfig.java @@ -3,7 +3,6 @@ import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; - import java.util.Optional; @SuppressWarnings("OptionalUsedAsFieldOrParameterType") diff --git a/quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryRecorder.java b/quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryRecorder.java index 05ca3f8..b5a1c9d 100644 --- a/quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryRecorder.java +++ b/quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryRecorder.java @@ -7,7 +7,6 @@ import io.sentry.SentryOptions; import io.sentry.jul.SentryHandler; import io.sentry.opentelemetry.OpenTelemetryLinkErrorEventProcessor; - import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Handler; From 0630f196237ca51eae56e9c8d7e539f6976dfeec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Wed, 7 Jun 2023 21:31:29 +0200 Subject: [PATCH 009/157] Build native images --- Dockerfile | 17 ++++++----------- ....spi.traces.ConfigurableSpanExporterProvider | 1 + 2 files changed, 7 insertions(+), 11 deletions(-) create mode 100644 quarkus-sentry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider diff --git a/Dockerfile b/Dockerfile index f74a88f..8ff85ac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,19 @@ -FROM eclipse-temurin:17-jdk AS build +FROM ghcr.io/graalvm/jdk:ol9-java17 AS build -RUN apt-get update && apt-get install -y git +RUN microdnf install -y git gcc glibc-devel libstdc++-devel zlib-devel WORKDIR /app COPY . ./ RUN git fetch --unshallow || echo "Nothing to do" -RUN ./gradlew build +RUN ./gradlew build -Dquarkus.package.type=native -FROM eclipse-temurin:17-jre AS run +FROM oraclelinux:9-slim AS run ENV LANGUAGE="en_US:en" -ENV JAVA_OPTS="$JAVA_OPTS -Dquarkus.http.host=0.0.0.0" -ENV JAVA_OPTS="$JAVA_OPTS -Djava.util.logging.manager=org.jboss.logmanager.LogManager" -COPY --from=build --chown=nobody /app/build/quarkus-app/lib/ /deployments/lib -COPY --from=build --chown=nobody /app/build/quarkus-app/*.jar /deployments/ -COPY --from=build --chown=nobody /app/build/quarkus-app/app/ /deployments/app -COPY --from=build --chown=nobody /app/build/quarkus-app/quarkus/ /deployments/quarkus +COPY --from=build --chown=nobody /app/build/*-runner /deployments/application EXPOSE 8080 USER nobody -CMD java -jar /deployments/quarkus-run.jar +CMD /deployments/application -Dquarkus.http.host=0.0.0.0 diff --git a/quarkus-sentry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider b/quarkus-sentry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider new file mode 100644 index 0000000..d8b7cec --- /dev/null +++ b/quarkus-sentry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider @@ -0,0 +1 @@ +io.quarkus.opentelemetry.runtime.tracing.spi.SpanExporterCDIProvider From 681ee67db110498dc58357cc85363d61d2fa1d43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Wed, 7 Jun 2023 21:59:09 +0200 Subject: [PATCH 010/157] Appease linter --- quarkus-sentry/deployment/src/test/java/.gitkeep | 0 quarkus-sentry/deployment/src/test/resources/.gitkeep | 0 quarkus-sentry/runtime/src/test/java/.gitkeep | 0 quarkus-sentry/runtime/src/test/resources/.gitkeep | 0 src/integrationTest/java/.gitkeep | 0 src/integrationTest/resources/.gitkeep | 0 src/main/java/.gitkeep | 0 src/test/java/.gitkeep | 0 src/test/resources/.gitkeep | 0 9 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 quarkus-sentry/deployment/src/test/java/.gitkeep create mode 100644 quarkus-sentry/deployment/src/test/resources/.gitkeep create mode 100644 quarkus-sentry/runtime/src/test/java/.gitkeep create mode 100644 quarkus-sentry/runtime/src/test/resources/.gitkeep create mode 100644 src/integrationTest/java/.gitkeep create mode 100644 src/integrationTest/resources/.gitkeep create mode 100644 src/main/java/.gitkeep create mode 100644 src/test/java/.gitkeep create mode 100644 src/test/resources/.gitkeep diff --git a/quarkus-sentry/deployment/src/test/java/.gitkeep b/quarkus-sentry/deployment/src/test/java/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/quarkus-sentry/deployment/src/test/resources/.gitkeep b/quarkus-sentry/deployment/src/test/resources/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/quarkus-sentry/runtime/src/test/java/.gitkeep b/quarkus-sentry/runtime/src/test/java/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/quarkus-sentry/runtime/src/test/resources/.gitkeep b/quarkus-sentry/runtime/src/test/resources/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/integrationTest/java/.gitkeep b/src/integrationTest/java/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/integrationTest/resources/.gitkeep b/src/integrationTest/resources/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/.gitkeep b/src/main/java/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/test/java/.gitkeep b/src/test/java/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/test/resources/.gitkeep b/src/test/resources/.gitkeep new file mode 100644 index 0000000..e69de29 From 906eefbf45d3e30b50c93812a4d6aefbbca88ff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Wed, 7 Jun 2023 22:25:03 +0200 Subject: [PATCH 011/157] Add basic workflows --- .github/workflows/formatting.yml | 24 ++++++++++++++++++++++++ .github/workflows/scanning.yml | 23 +++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 .github/workflows/formatting.yml create mode 100644 .github/workflows/scanning.yml diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml new file mode 100644 index 0000000..9ee2052 --- /dev/null +++ b/.github/workflows/formatting.yml @@ -0,0 +1,24 @@ +name: Formatting + +on: + push: + branches: + - develop + +jobs: + check: + name: Check formatting + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: '17' + cache: gradle + + - name: Run Spotless + run: ./gradlew spotlessCheck diff --git a/.github/workflows/scanning.yml b/.github/workflows/scanning.yml new file mode 100644 index 0000000..c7e12af --- /dev/null +++ b/.github/workflows/scanning.yml @@ -0,0 +1,23 @@ +name: Scanning + +on: + push: + branches: + - develop + +jobs: + scan: + name: Run CodeQL + runs-on: ubuntu-latest + permissions: + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up CodeQL + uses: github/codeql-action/init@v2 + + - name: Run analysis + uses: github/codeql-action/analyze@v2 From 6ecafc154838170a4fddae4146b02a3e3cacfc06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Wed, 7 Jun 2023 22:32:24 +0200 Subject: [PATCH 012/157] Add CodeQL autobuild --- .github/workflows/scanning.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/scanning.yml b/.github/workflows/scanning.yml index c7e12af..80b8007 100644 --- a/.github/workflows/scanning.yml +++ b/.github/workflows/scanning.yml @@ -19,5 +19,8 @@ jobs: - name: Set up CodeQL uses: github/codeql-action/init@v2 + - name: Run autobuild + uses: github/codeql-action/autobuild@v2 + - name: Run analysis uses: github/codeql-action/analyze@v2 From 0c74f3f4c41649ae434be8964f4e916ec1ade11b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 9 Jun 2023 19:56:40 +0200 Subject: [PATCH 013/157] Move Sentry plugin under app.fyreplace.api --- .../java/app/fyreplace/{ => api}/sentry/SentryProcessor.java | 2 +- .../fyreplace/{ => api}/sentry/SentrySpanProcessorProducer.java | 2 +- .../main/java/app/fyreplace/{ => api}/sentry/SentryConfig.java | 2 +- .../{ => api}/sentry/SentryConfigurablePropagator.java | 2 +- .../java/app/fyreplace/{ => api}/sentry/SentryRecorder.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename quarkus-sentry/deployment/src/main/java/app/fyreplace/{ => api}/sentry/SentryProcessor.java (96%) rename quarkus-sentry/deployment/src/main/java/app/fyreplace/{ => api}/sentry/SentrySpanProcessorProducer.java (91%) rename quarkus-sentry/runtime/src/main/java/app/fyreplace/{ => api}/sentry/SentryConfig.java (96%) rename quarkus-sentry/runtime/src/main/java/app/fyreplace/{ => api}/sentry/SentryConfigurablePropagator.java (94%) rename quarkus-sentry/runtime/src/main/java/app/fyreplace/{ => api}/sentry/SentryRecorder.java (97%) diff --git a/quarkus-sentry/deployment/src/main/java/app/fyreplace/sentry/SentryProcessor.java b/quarkus-sentry/deployment/src/main/java/app/fyreplace/api/sentry/SentryProcessor.java similarity index 96% rename from quarkus-sentry/deployment/src/main/java/app/fyreplace/sentry/SentryProcessor.java rename to quarkus-sentry/deployment/src/main/java/app/fyreplace/api/sentry/SentryProcessor.java index e7ef3a3..f702685 100644 --- a/quarkus-sentry/deployment/src/main/java/app/fyreplace/sentry/SentryProcessor.java +++ b/quarkus-sentry/deployment/src/main/java/app/fyreplace/api/sentry/SentryProcessor.java @@ -1,4 +1,4 @@ -package app.fyreplace.sentry; +package app.fyreplace.api.sentry; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.deployment.annotations.BuildStep; diff --git a/quarkus-sentry/deployment/src/main/java/app/fyreplace/sentry/SentrySpanProcessorProducer.java b/quarkus-sentry/deployment/src/main/java/app/fyreplace/api/sentry/SentrySpanProcessorProducer.java similarity index 91% rename from quarkus-sentry/deployment/src/main/java/app/fyreplace/sentry/SentrySpanProcessorProducer.java rename to quarkus-sentry/deployment/src/main/java/app/fyreplace/api/sentry/SentrySpanProcessorProducer.java index 3252f51..7cde92e 100644 --- a/quarkus-sentry/deployment/src/main/java/app/fyreplace/sentry/SentrySpanProcessorProducer.java +++ b/quarkus-sentry/deployment/src/main/java/app/fyreplace/api/sentry/SentrySpanProcessorProducer.java @@ -1,4 +1,4 @@ -package app.fyreplace.sentry; +package app.fyreplace.api.sentry; import io.sentry.opentelemetry.SentrySpanProcessor; import jakarta.enterprise.context.ApplicationScoped; diff --git a/quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryConfig.java b/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/SentryConfig.java similarity index 96% rename from quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryConfig.java rename to quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/SentryConfig.java index 0e5cf37..9de9b41 100644 --- a/quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryConfig.java +++ b/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/SentryConfig.java @@ -1,4 +1,4 @@ -package app.fyreplace.sentry; +package app.fyreplace.api.sentry; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; diff --git a/quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryConfigurablePropagator.java b/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/SentryConfigurablePropagator.java similarity index 94% rename from quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryConfigurablePropagator.java rename to quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/SentryConfigurablePropagator.java index 115913b..a2a680d 100644 --- a/quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryConfigurablePropagator.java +++ b/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/SentryConfigurablePropagator.java @@ -1,4 +1,4 @@ -package app.fyreplace.sentry; +package app.fyreplace.api.sentry; import io.opentelemetry.context.propagation.TextMapPropagator; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; diff --git a/quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryRecorder.java b/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/SentryRecorder.java similarity index 97% rename from quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryRecorder.java rename to quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/SentryRecorder.java index b5a1c9d..94a08dc 100644 --- a/quarkus-sentry/runtime/src/main/java/app/fyreplace/sentry/SentryRecorder.java +++ b/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/SentryRecorder.java @@ -1,4 +1,4 @@ -package app.fyreplace.sentry; +package app.fyreplace.api.sentry; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; From b5198b9333b0b0b48fd98905aebd4baee190d2b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 10 Jun 2023 12:43:10 +0200 Subject: [PATCH 014/157] Fix Sentry plugin --- ...lemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quarkus-sentry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider b/quarkus-sentry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider index 2dd1d7b..847dbc4 100644 --- a/quarkus-sentry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider +++ b/quarkus-sentry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider @@ -1 +1 @@ -app.fyreplace.sentry.SentryConfigurablePropagator +app.fyreplace.api.sentry.SentryConfigurablePropagator From 27345bfa86d1097d7b5a84ec66c7036547c040c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 2 Jul 2023 15:13:02 +0200 Subject: [PATCH 015/157] Update dependencies --- gradle.properties | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index da5d9a5..3aa0f8a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,8 +2,8 @@ gitPluginVersion=3.0.0 spotlessPluginVersion=6.19.0 quarkusPluginId=io.quarkus quarkusExtensionPluginId=io.quarkus.extension -quarkusPluginVersion=3.1.0.Final +quarkusPluginVersion=3.1.3.Final quarkusPlatformGroupId=io.quarkus.platform quarkusPlatformArtifactId=quarkus-bom -quarkusPlatformVersion=3.1.0.Final -sentryVersion=6.21.0 +quarkusPlatformVersion=3.1.3.Final +sentryVersion=6.24.0 From 37788f930b440613180eadfe431c78720d129120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 8 Jul 2023 18:50:24 +0200 Subject: [PATCH 016/157] Add missing directory --- quarkus-sentry/deployment/src/main/resources/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 quarkus-sentry/deployment/src/main/resources/.gitkeep diff --git a/quarkus-sentry/deployment/src/main/resources/.gitkeep b/quarkus-sentry/deployment/src/main/resources/.gitkeep new file mode 100644 index 0000000..e69de29 From 79a27a53c4fef7cd3157491d9eee3419ea32878b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 8 Jul 2023 19:47:53 +0200 Subject: [PATCH 017/157] Split Sentry components into various packages --- .../fyreplace/api/sentry/SentrySpanProcessorProducer.java | 3 --- .../api/sentry/{ => processors}/SentryProcessor.java | 5 ++++- .../app/fyreplace/api/sentry/{ => config}/SentryConfig.java | 2 +- .../api/sentry/{ => otel}/SentryConfigurablePropagator.java | 2 +- .../fyreplace/api/sentry/{ => recorders}/SentryRecorder.java | 3 ++- ...etry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider | 2 +- 6 files changed, 9 insertions(+), 8 deletions(-) rename quarkus-sentry/deployment/src/main/java/app/fyreplace/api/sentry/{ => processors}/SentryProcessor.java (81%) rename quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/{ => config}/SentryConfig.java (95%) rename quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/{ => otel}/SentryConfigurablePropagator.java (93%) rename quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/{ => recorders}/SentryRecorder.java (93%) diff --git a/quarkus-sentry/deployment/src/main/java/app/fyreplace/api/sentry/SentrySpanProcessorProducer.java b/quarkus-sentry/deployment/src/main/java/app/fyreplace/api/sentry/SentrySpanProcessorProducer.java index 7cde92e..4093764 100644 --- a/quarkus-sentry/deployment/src/main/java/app/fyreplace/api/sentry/SentrySpanProcessorProducer.java +++ b/quarkus-sentry/deployment/src/main/java/app/fyreplace/api/sentry/SentrySpanProcessorProducer.java @@ -2,11 +2,8 @@ import io.sentry.opentelemetry.SentrySpanProcessor; import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.inject.Produces; -@ApplicationScoped public final class SentrySpanProcessorProducer { - @Produces @ApplicationScoped public SentrySpanProcessor produceSentrySpanProcessor() { return new SentrySpanProcessor(); diff --git a/quarkus-sentry/deployment/src/main/java/app/fyreplace/api/sentry/SentryProcessor.java b/quarkus-sentry/deployment/src/main/java/app/fyreplace/api/sentry/processors/SentryProcessor.java similarity index 81% rename from quarkus-sentry/deployment/src/main/java/app/fyreplace/api/sentry/SentryProcessor.java rename to quarkus-sentry/deployment/src/main/java/app/fyreplace/api/sentry/processors/SentryProcessor.java index f702685..6af1684 100644 --- a/quarkus-sentry/deployment/src/main/java/app/fyreplace/api/sentry/SentryProcessor.java +++ b/quarkus-sentry/deployment/src/main/java/app/fyreplace/api/sentry/processors/SentryProcessor.java @@ -1,5 +1,8 @@ -package app.fyreplace.api.sentry; +package app.fyreplace.api.sentry.processors; +import app.fyreplace.api.sentry.SentrySpanProcessorProducer; +import app.fyreplace.api.sentry.config.SentryConfig; +import app.fyreplace.api.sentry.recorders.SentryRecorder; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; diff --git a/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/SentryConfig.java b/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/config/SentryConfig.java similarity index 95% rename from quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/SentryConfig.java rename to quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/config/SentryConfig.java index 9de9b41..9aece10 100644 --- a/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/SentryConfig.java +++ b/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/config/SentryConfig.java @@ -1,4 +1,4 @@ -package app.fyreplace.api.sentry; +package app.fyreplace.api.sentry.config; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; diff --git a/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/SentryConfigurablePropagator.java b/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/otel/SentryConfigurablePropagator.java similarity index 93% rename from quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/SentryConfigurablePropagator.java rename to quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/otel/SentryConfigurablePropagator.java index a2a680d..6d53666 100644 --- a/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/SentryConfigurablePropagator.java +++ b/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/otel/SentryConfigurablePropagator.java @@ -1,4 +1,4 @@ -package app.fyreplace.api.sentry; +package app.fyreplace.api.sentry.otel; import io.opentelemetry.context.propagation.TextMapPropagator; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; diff --git a/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/SentryRecorder.java b/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/recorders/SentryRecorder.java similarity index 93% rename from quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/SentryRecorder.java rename to quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/recorders/SentryRecorder.java index 94a08dc..dc80e5a 100644 --- a/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/SentryRecorder.java +++ b/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/recorders/SentryRecorder.java @@ -1,5 +1,6 @@ -package app.fyreplace.api.sentry; +package app.fyreplace.api.sentry.recorders; +import app.fyreplace.api.sentry.config.SentryConfig; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; import io.sentry.Instrumenter; diff --git a/quarkus-sentry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider b/quarkus-sentry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider index 847dbc4..a72bc0b 100644 --- a/quarkus-sentry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider +++ b/quarkus-sentry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider @@ -1 +1 @@ -app.fyreplace.api.sentry.SentryConfigurablePropagator +app.fyreplace.api.sentry.otel.SentryConfigurablePropagator From f747350d7cf9432ceb3ff5a8f4ead717f6c6987f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 2 Jul 2023 15:50:53 +0200 Subject: [PATCH 018/157] Add users and emails --- .env-example | 12 + .github/workflows/formatting.yml | 24 -- .github/workflows/validation.yml | 43 +++ Dockerfile | 1 + Makefile | 6 + build.gradle | 37 ++- gradle.properties | 10 +- quarkus-sentry/build.gradle | 2 +- settings.gradle | 4 +- src/main/java/.gitkeep | 0 .../java/app/fyreplace/api/data/Block.java | 26 ++ .../java/app/fyreplace/api/data/Email.java | 30 ++ .../fyreplace/api/data/EmailActivation.java | 5 + .../app/fyreplace/api/data/EmailCreation.java | 7 + .../app/fyreplace/api/data/EntityBase.java | 18 ++ .../fyreplace/api/data/NewTokenCreation.java | 5 + .../app/fyreplace/api/data/RandomCode.java | 31 ++ .../app/fyreplace/api/data/StoredFile.java | 70 +++++ .../app/fyreplace/api/data/TokenCreation.java | 5 + .../java/app/fyreplace/api/data/User.java | 159 +++++++++++ .../app/fyreplace/api/data/UserCreation.java | 10 + .../fyreplace/api/data/dev/DataSeeder.java | 68 +++++ .../fyreplace/api/data/validators/Regex.java | 23 ++ .../api/data/validators/RegexValidator.java | 19 ++ .../app/fyreplace/api/emails/EmailBase.java | 69 +++++ .../api/emails/EmailVerificationEmail.java | 30 ++ .../api/emails/UserActivationEmail.java | 34 +++ .../api/emails/UserConnectionEmail.java | 30 ++ .../api/endpoints/EmailsEndpoint.java | 142 +++++++++ .../api/endpoints/TokensEndpoint.java | 96 +++++++ .../api/endpoints/UsersDevEndpoint.java | 27 ++ .../api/endpoints/UsersEndpoint.java | 269 ++++++++++++++++++ .../api/exceptions/ConflictException.java | 18 ++ .../api/exceptions/ExceptionMappers.java | 21 ++ .../api/exceptions/ExplainableException.java | 5 + .../api/exceptions/ForbiddenException.java | 15 + .../fyreplace/api/exceptions/Responses.java | 16 ++ .../UnsupportedMediaTypeException.java | 18 ++ .../fyreplace/api/services/JwtService.java | 26 ++ .../api/services/MimeTypeService.java | 23 ++ .../fyreplace/api/services/RandomService.java | 20 ++ .../api/services/StorageService.java | 12 + .../api/services/mimetype/KnownMimeTypes.java | 13 + .../storage/local/LocalStorageConfig.java | 8 + .../storage/local/LocalStorageService.java | 44 +++ .../services/storage/s3/S3StorageConfig.java | 11 + .../services/storage/s3/S3StorageService.java | 47 +++ .../app/fyreplace/api/tasks/CleanupTasks.java | 28 ++ src/main/resources/META-INF/branding/logo.png | Bin 0 -> 7293 bytes .../resources/META-INF/resources/robots.txt | 2 + src/main/resources/application.properties | 2 - src/main/resources/application.yaml | 93 ++++++ src/main/resources/keys/.gitignore | 2 + src/main/resources/templates/.gitignore | 1 + .../EmailVerificationEmail/html.html.mjml | 15 + .../templates/EmailVerificationEmail/text.txt | 7 + .../UserActivationEmail/html.html.mjml | 16 ++ .../templates/UserActivationEmail/text.txt | 9 + .../UserConnectionEmail/html.html.mjml | 15 + .../templates/UserConnectionEmail/text.txt | 7 + .../templates/emails/_attributes.mjml | 6 + .../templates/emails/_link_end_notice.mjml | 1 + .../resources/templates/emails/_logo.mjml | 2 + src/test/java/.gitkeep | 0 .../app/fyreplace/api/testing/Assertions.java | 14 + .../api/testing/DatabaseTestResource.java | 18 ++ .../api/testing/TransactionalTests.java | 35 +++ .../endpoints/emails/ActivateTests.java | 99 +++++++ .../testing/endpoints/emails/CreateTests.java | 89 ++++++ .../testing/endpoints/emails/DeleteTests.java | 64 +++++ .../testing/endpoints/emails/ListTests.java | 69 +++++ .../endpoints/emails/SetMainTests.java | 82 ++++++ .../endpoints/tokens/CreateNewTests.java | 59 ++++ .../testing/endpoints/tokens/CreateTests.java | 147 ++++++++++ .../endpoints/tokens/RetrieveNewTests.java | 31 ++ .../api/testing/endpoints/users/BanTests.java | 131 +++++++++ .../endpoints/users/CreateBlockTests.java | 55 ++++ .../testing/endpoints/users/CreateTests.java | 151 ++++++++++ .../endpoints/users/DeleteBlockTests.java | 58 ++++ .../endpoints/users/DeleteMeAvatarTests.java | 47 +++ .../endpoints/users/DeleteMeTests.java | 33 +++ .../endpoints/users/ListBlockedTests.java | 69 +++++ .../endpoints/users/RetrieveMeTests.java | 41 +++ .../endpoints/users/RetrieveTests.java | 42 +++ .../endpoints/users/UpdateMeAvatarTests.java | 109 +++++++ .../endpoints/users/UpdateMeBioTests.java | 49 ++++ .../users/dev/RetrieveTokenTests.java | 18 ++ .../cleanup/RemoveOldInactiveUsersTests.java | 36 +++ .../cleanup/RemoveOldRandomCodesTests.java | 44 +++ src/test/resources/.gitkeep | 0 .../resources/META-INF/resources/image.gif | Bin 0 -> 392 bytes .../resources/META-INF/resources/image.jpeg | Bin 0 -> 1298 bytes .../resources/META-INF/resources/image.png | Bin 0 -> 103 bytes .../resources/META-INF/resources/image.txt | 1 + .../resources/META-INF/resources/image.webp | Bin 0 -> 38 bytes 95 files changed, 3369 insertions(+), 37 deletions(-) create mode 100644 .env-example delete mode 100644 .github/workflows/formatting.yml create mode 100644 .github/workflows/validation.yml create mode 100644 Makefile delete mode 100644 src/main/java/.gitkeep create mode 100644 src/main/java/app/fyreplace/api/data/Block.java create mode 100644 src/main/java/app/fyreplace/api/data/Email.java create mode 100644 src/main/java/app/fyreplace/api/data/EmailActivation.java create mode 100644 src/main/java/app/fyreplace/api/data/EmailCreation.java create mode 100644 src/main/java/app/fyreplace/api/data/EntityBase.java create mode 100644 src/main/java/app/fyreplace/api/data/NewTokenCreation.java create mode 100644 src/main/java/app/fyreplace/api/data/RandomCode.java create mode 100644 src/main/java/app/fyreplace/api/data/StoredFile.java create mode 100644 src/main/java/app/fyreplace/api/data/TokenCreation.java create mode 100644 src/main/java/app/fyreplace/api/data/User.java create mode 100644 src/main/java/app/fyreplace/api/data/UserCreation.java create mode 100644 src/main/java/app/fyreplace/api/data/dev/DataSeeder.java create mode 100644 src/main/java/app/fyreplace/api/data/validators/Regex.java create mode 100644 src/main/java/app/fyreplace/api/data/validators/RegexValidator.java create mode 100644 src/main/java/app/fyreplace/api/emails/EmailBase.java create mode 100644 src/main/java/app/fyreplace/api/emails/EmailVerificationEmail.java create mode 100644 src/main/java/app/fyreplace/api/emails/UserActivationEmail.java create mode 100644 src/main/java/app/fyreplace/api/emails/UserConnectionEmail.java create mode 100644 src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java create mode 100644 src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java create mode 100644 src/main/java/app/fyreplace/api/endpoints/UsersDevEndpoint.java create mode 100644 src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java create mode 100644 src/main/java/app/fyreplace/api/exceptions/ConflictException.java create mode 100644 src/main/java/app/fyreplace/api/exceptions/ExceptionMappers.java create mode 100644 src/main/java/app/fyreplace/api/exceptions/ExplainableException.java create mode 100644 src/main/java/app/fyreplace/api/exceptions/ForbiddenException.java create mode 100644 src/main/java/app/fyreplace/api/exceptions/Responses.java create mode 100644 src/main/java/app/fyreplace/api/exceptions/UnsupportedMediaTypeException.java create mode 100644 src/main/java/app/fyreplace/api/services/JwtService.java create mode 100644 src/main/java/app/fyreplace/api/services/MimeTypeService.java create mode 100644 src/main/java/app/fyreplace/api/services/RandomService.java create mode 100644 src/main/java/app/fyreplace/api/services/StorageService.java create mode 100644 src/main/java/app/fyreplace/api/services/mimetype/KnownMimeTypes.java create mode 100644 src/main/java/app/fyreplace/api/services/storage/local/LocalStorageConfig.java create mode 100644 src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java create mode 100644 src/main/java/app/fyreplace/api/services/storage/s3/S3StorageConfig.java create mode 100644 src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java create mode 100644 src/main/java/app/fyreplace/api/tasks/CleanupTasks.java create mode 100644 src/main/resources/META-INF/branding/logo.png create mode 100644 src/main/resources/META-INF/resources/robots.txt delete mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/application.yaml create mode 100644 src/main/resources/keys/.gitignore create mode 100644 src/main/resources/templates/.gitignore create mode 100644 src/main/resources/templates/EmailVerificationEmail/html.html.mjml create mode 100644 src/main/resources/templates/EmailVerificationEmail/text.txt create mode 100644 src/main/resources/templates/UserActivationEmail/html.html.mjml create mode 100644 src/main/resources/templates/UserActivationEmail/text.txt create mode 100644 src/main/resources/templates/UserConnectionEmail/html.html.mjml create mode 100644 src/main/resources/templates/UserConnectionEmail/text.txt create mode 100644 src/main/resources/templates/emails/_attributes.mjml create mode 100644 src/main/resources/templates/emails/_link_end_notice.mjml create mode 100644 src/main/resources/templates/emails/_logo.mjml delete mode 100644 src/test/java/.gitkeep create mode 100644 src/test/java/app/fyreplace/api/testing/Assertions.java create mode 100644 src/test/java/app/fyreplace/api/testing/DatabaseTestResource.java create mode 100644 src/test/java/app/fyreplace/api/testing/TransactionalTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/tokens/RetrieveNewTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/users/BanTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeAvatarTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeBioTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/users/dev/RetrieveTokenTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldInactiveUsersTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldRandomCodesTests.java delete mode 100644 src/test/resources/.gitkeep create mode 100644 src/test/resources/META-INF/resources/image.gif create mode 100644 src/test/resources/META-INF/resources/image.jpeg create mode 100644 src/test/resources/META-INF/resources/image.png create mode 100644 src/test/resources/META-INF/resources/image.txt create mode 100644 src/test/resources/META-INF/resources/image.webp diff --git a/.env-example b/.env-example new file mode 100644 index 0000000..ea46b75 --- /dev/null +++ b/.env-example @@ -0,0 +1,12 @@ +QUARKUS_DATASOURCE_USERNAME=fyreplace +QUARKUS_DATASOURCE_PASSWORD=fyreplace +QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://localhost/fyreplace + +QUARKUS_SENTRY_DSN=https://sentry.example.org +QUARKUS_SENTRY_ENVIRONMENT=local +QUARKUS_SENTRY_TRACES_SAMPLE_RATE=1.0 + +MP_JWT_VERIFY_PUBLICKEY=public-key-content +SMALLRYE_JWT_SIGN_KEY=private-key-content + +APP_URL=https://fyreplace.example.org diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml deleted file mode 100644 index 9ee2052..0000000 --- a/.github/workflows/formatting.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Formatting - -on: - push: - branches: - - develop - -jobs: - check: - name: Check formatting - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - uses: actions/setup-java@v3 - with: - distribution: temurin - java-version: '17' - cache: gradle - - - name: Run Spotless - run: ./gradlew spotlessCheck diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml new file mode 100644 index 0000000..92b556c --- /dev/null +++ b/.github/workflows/validation.yml @@ -0,0 +1,43 @@ +name: Validation + +on: + push: + branches: + - develop + +jobs: + formatting: + name: Check formatting + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: '17' + cache: gradle + + - name: Run Spotless + run: ./gradlew spotlessCheck + + tests: + name: Run tests + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: '17' + cache: gradle + + - name: Run tests + run: | + ./gradlew compileMjml + ./gradlew test diff --git a/Dockerfile b/Dockerfile index 8ff85ac..34dd03c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,7 @@ WORKDIR /app COPY . ./ RUN git fetch --unshallow || echo "Nothing to do" +RUN ./gradlew compileMjml RUN ./gradlew build -Dquarkus.package.type=native FROM oraclelinux:9-slim AS run diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7139049 --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +.PHONY: keygen-rsa + +keygen-rsa: + openssl genrsa > src/main/resources/keys/jwt.rsa 2048 + openssl pkcs8 -topk8 -nocrypt -inform pem -in src/main/resources/keys/jwt.rsa -outform pem > src/main/resources/keys/jwt.rsa.pem + openssl rsa -in src/main/resources/keys/jwt.rsa -pubout > src/main/resources/keys/jwt.rsa.pub diff --git a/build.gradle b/build.gradle index 6a47d0a..0aea64f 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,7 @@ plugins { id "io.quarkus" id "com.palantir.git-version" version "${gitPluginVersion}" id "com.diffplug.spotless" version "${spotlessPluginVersion}" + id "io.freefair.lombok" version "${lombokPluginVersion}" } group = "app.fyreplace" @@ -14,16 +15,37 @@ repositories { } dependencies { - implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")) + implementation(enforcedPlatform("io.quarkus:quarkus-bom:${quarkusVersion}")) + implementation(enforcedPlatform("io.quarkiverse.amazonservices:quarkus-amazon-services-bom:${quarkusAmazonVersion}")) + implementation(enforcedPlatform("org.apache.tika:tika-bom:${tikaVersion}")) implementation("io.quarkus:quarkus-arc") - implementation("io.quarkus:quarkus-resteasy-reactive") + implementation("io.quarkus:quarkus-config-yaml") implementation("io.quarkus:quarkus-hibernate-orm-panache") + implementation("io.quarkus:quarkus-hibernate-validator") implementation("io.quarkus:quarkus-jdbc-postgresql") + implementation("io.quarkus:quarkus-jdbc-h2") + implementation("io.quarkus:quarkus-mailer") + implementation("io.quarkus:quarkus-resteasy-reactive") + implementation("io.quarkus:quarkus-resteasy-reactive-jackson") + implementation("io.quarkus:quarkus-resteasy-reactive-qute") + implementation("io.quarkus:quarkus-scheduler") + implementation("io.quarkus:quarkus-security") implementation("io.quarkus:quarkus-smallrye-health") + implementation("io.quarkus:quarkus-smallrye-openapi") + implementation("io.quarkus:quarkus-smallrye-jwt") + implementation("io.quarkus:quarkus-smallrye-jwt-build") + implementation("io.quarkiverse.amazonservices:quarkus-amazon-s3") + implementation("org.jboss.logmanager:log4j2-jboss-logmanager") + implementation("org.apache.tika:tika-core") + implementation("org.apache.tika:tika-parsers-standard-package") + implementation("software.amazon.awssdk:url-connection-client") implementation(project(":quarkus-sentry:deployment")) implementation(project(":quarkus-sentry:runtime")) testImplementation("io.quarkus:quarkus-junit5") + testImplementation("io.quarkus:quarkus-test-h2") + testImplementation("io.quarkus:quarkus-test-security") testImplementation("io.rest-assured:rest-assured") + testImplementation("org.apiguardian:apiguardian-api:+") } java { @@ -44,6 +66,17 @@ compileTestJava { options.encoding = "UTF-8" } +tasks.register("compileMjml") { + doLast { + fileTree("src/main/resources/templates").include("**/*.html.mjml").each { file -> + exec { + workingDir "$projectDir" + commandLine "npx", "mjml", "-r", file.getPath(), "-o", "${file.getPath().replace(".mjml", "")}" + } + } + } +} + spotless { java { importOrder() diff --git a/gradle.properties b/gradle.properties index 3aa0f8a..89abd4d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,7 @@ +quarkusVersion=3.2.0.Final +quarkusAmazonVersion=2.4.0 gitPluginVersion=3.0.0 spotlessPluginVersion=6.19.0 -quarkusPluginId=io.quarkus -quarkusExtensionPluginId=io.quarkus.extension -quarkusPluginVersion=3.1.3.Final -quarkusPlatformGroupId=io.quarkus.platform -quarkusPlatformArtifactId=quarkus-bom -quarkusPlatformVersion=3.1.3.Final +lombokPluginVersion=8.1.0 sentryVersion=6.24.0 +tikaVersion=2.8.0 diff --git a/quarkus-sentry/build.gradle b/quarkus-sentry/build.gradle index a73c369..865c24b 100644 --- a/quarkus-sentry/build.gradle +++ b/quarkus-sentry/build.gradle @@ -7,7 +7,7 @@ subprojects { group = "app.fyreplace" version = gitVersion() dependencies { - implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")) + implementation(enforcedPlatform("io.quarkus:quarkus-bom:${quarkusVersion}")) implementation("io.quarkus:quarkus-opentelemetry") implementation("io.sentry:sentry-opentelemetry-core:${sentryVersion}") annotationProcessor("io.quarkus:quarkus-extension-processor") diff --git a/settings.gradle b/settings.gradle index 755653c..23e1939 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,8 +6,8 @@ pluginManagement { } plugins { - id "${quarkusPluginId}" version "${quarkusPluginVersion}" - id "${quarkusExtensionPluginId}" version "${quarkusPluginVersion}" + id "io.quarkus" version "${quarkusVersion}" + id "io.quarkus.extension" version "${quarkusVersion}" } } diff --git a/src/main/java/.gitkeep b/src/main/java/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/app/fyreplace/api/data/Block.java b/src/main/java/app/fyreplace/api/data/Block.java new file mode 100644 index 0000000..868ca50 --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/Block.java @@ -0,0 +1,26 @@ +package app.fyreplace.api.data; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.util.UUID; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +@Entity +@Table(name = "blocks") +public class Block extends EntityBase { + @Id + @GeneratedValue + public UUID id; + + @ManyToOne(optional = false) + @OnDelete(action = OnDeleteAction.CASCADE) + public User source; + + @ManyToOne(optional = false) + @OnDelete(action = OnDeleteAction.CASCADE) + public User target; +} diff --git a/src/main/java/app/fyreplace/api/data/Email.java b/src/main/java/app/fyreplace/api/data/Email.java new file mode 100644 index 0000000..b3d619b --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/Email.java @@ -0,0 +1,30 @@ +package app.fyreplace.api.data; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +@Entity +@Table(name = "emails") +public class Email extends EntityBase { + @ManyToOne(optional = false) + @OnDelete(action = OnDeleteAction.CASCADE) + @JsonIgnore + public User user; + + @Column(length = 255, unique = true, nullable = false) + public String email; + + @Column(nullable = false) + public boolean isVerified = false; + + @JsonProperty("isMain") + public boolean isMain() { + return id.equals(user.mainEmail.id); + } +} diff --git a/src/main/java/app/fyreplace/api/data/EmailActivation.java b/src/main/java/app/fyreplace/api/data/EmailActivation.java new file mode 100644 index 0000000..989582a --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/EmailActivation.java @@ -0,0 +1,5 @@ +package app.fyreplace.api.data; + +import jakarta.validation.constraints.NotNull; + +public final record EmailActivation(@NotNull String email, @NotNull String code) {} diff --git a/src/main/java/app/fyreplace/api/data/EmailCreation.java b/src/main/java/app/fyreplace/api/data/EmailCreation.java new file mode 100644 index 0000000..a528726 --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/EmailCreation.java @@ -0,0 +1,7 @@ +package app.fyreplace.api.data; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; +import org.hibernate.validator.constraints.Length; + +public final record EmailCreation(@NotNull @Length(min = 3, max = 254) @NotNull @Email String email) {} diff --git a/src/main/java/app/fyreplace/api/data/EntityBase.java b/src/main/java/app/fyreplace/api/data/EntityBase.java new file mode 100644 index 0000000..4874c2d --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/EntityBase.java @@ -0,0 +1,18 @@ +package app.fyreplace.api.data; + +import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import java.util.UUID; + +@MappedSuperclass +public abstract class EntityBase extends PanacheEntityBase { + @Id + @GeneratedValue + public UUID id; + + public void refresh() { + getEntityManager().refresh(this); + } +} diff --git a/src/main/java/app/fyreplace/api/data/NewTokenCreation.java b/src/main/java/app/fyreplace/api/data/NewTokenCreation.java new file mode 100644 index 0000000..25b3377 --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/NewTokenCreation.java @@ -0,0 +1,5 @@ +package app.fyreplace.api.data; + +import jakarta.validation.constraints.NotNull; + +public final record NewTokenCreation(@NotNull String identifier) {} diff --git a/src/main/java/app/fyreplace/api/data/RandomCode.java b/src/main/java/app/fyreplace/api/data/RandomCode.java new file mode 100644 index 0000000..b9b09d8 --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/RandomCode.java @@ -0,0 +1,31 @@ +package app.fyreplace.api.data; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.time.Instant; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import org.hibernate.annotations.SourceType; + +@Entity +@Table(name = "random_codes") +public class RandomCode extends EntityBase { + @ManyToOne(optional = false) + @OnDelete(action = OnDeleteAction.CASCADE) + public Email email; + + @Column(nullable = false) + public String code; + + @Column(nullable = false) + @CreationTimestamp(source = SourceType.DB) + public Instant dateCreated; + + @Override + public String toString() { + return code; + } +} diff --git a/src/main/java/app/fyreplace/api/data/StoredFile.java b/src/main/java/app/fyreplace/api/data/StoredFile.java new file mode 100644 index 0000000..50412ee --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/StoredFile.java @@ -0,0 +1,70 @@ +package app.fyreplace.api.data; + +import app.fyreplace.api.services.StorageService; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import io.quarkus.arc.Arc; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.PostPersist; +import jakarta.persistence.PreRemove; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import java.io.IOException; + +@Entity +@Table(name = "remote_files") +public class StoredFile extends EntityBase { + @Transient + final StorageService storageService = + Arc.container().instance(StorageService.class).get(); + + @Column(unique = true, nullable = false) + public String path; + + @Transient + private byte[] data; + + public StoredFile() { + data = null; + } + + public StoredFile(final String path, final byte[] data) { + this.path = path; + this.data = data; + } + + @Override + public String toString() { + return storageService.getUri(path).toString(); + } + + public void store(final byte[] data) throws IOException { + if (data != null) { + storageService.store(path, data); + } + } + + @PostPersist + final void postPersist() throws IOException { + if (data != null) { + store(data); + data = null; + } + } + + @PreRemove + final void preDestroy() throws IOException { + storageService.remove(path); + } + + public static final class Serializer extends JsonSerializer { + @Override + public void serialize( + final StoredFile value, final JsonGenerator generator, final SerializerProvider serializers) + throws IOException { + generator.writeString(value.toString()); + } + } +} diff --git a/src/main/java/app/fyreplace/api/data/TokenCreation.java b/src/main/java/app/fyreplace/api/data/TokenCreation.java new file mode 100644 index 0000000..f14a943 --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/TokenCreation.java @@ -0,0 +1,5 @@ +package app.fyreplace.api.data; + +import jakarta.validation.constraints.NotNull; + +public record TokenCreation(@NotNull String identifier, @NotNull String code) {} diff --git a/src/main/java/app/fyreplace/api/data/User.java b/src/main/java/app/fyreplace/api/data/User.java new file mode 100644 index 0000000..03167f1 --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/User.java @@ -0,0 +1,159 @@ +package app.fyreplace.api.data; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import jakarta.annotation.Nullable; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.NotAuthorizedException; +import jakarta.ws.rs.core.SecurityContext; +import java.time.Instant; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import org.hibernate.annotations.SourceType; + +@Entity +@Table(name = "users") +public class User extends EntityBase { + public static final Set forbiddenUsernames = new HashSet(Arrays.asList( + "admin", + "admins", + "administrator", + "administrators", + "anonymous", + "author", + "authors", + "fyreplace", + "fyreplaces", + "management", + "managements", + "manager", + "managers", + "mod", + "mods", + "moderator", + "moderators", + "nil", + "none", + "nul", + "null", + "root", + "roots", + "superuser", + "superusers", + "sysadmin", + "system", + "systems", + "systemadmin", + "systemadmins", + "systemadministrator", + "systemadministrators", + "systemsadmin", + "systemsadmins", + "systemsadministrator", + "systemsadministrators", + "user", + "users", + "void", + "voids")); + + @Column(length = 100, unique = true, nullable = false) + public String username; + + @ManyToOne + @OnDelete(action = OnDeleteAction.SET_NULL) + @JsonIgnore + public Email mainEmail; + + @Column(nullable = false) + @JsonIgnore + public boolean isActive = false; + + @Column(nullable = false) + public Rank rank = Rank.CITIZEN; + + @OneToOne(cascade = CascadeType.PERSIST) + @OnDelete(action = OnDeleteAction.SET_NULL) + @JsonSerialize(using = StoredFile.Serializer.class) + @Schema(implementation = String.class) + public StoredFile avatar; + + @Column(length = 3000, nullable = false) + public String bio = ""; + + @Column(nullable = false) + public boolean isBanned = false; + + @Column(nullable = false) + @JsonIgnore + public BanCount banCount = BanCount.NEVER; + + @JsonIgnore + public Instant dateBanEnd; + + @Column(nullable = false) + @CreationTimestamp(source = SourceType.DB) + public Instant dateCreated; + + public Set getGroups() { + return Arrays.stream(Rank.values()) + .filter(group -> group.ordinal() <= rank.ordinal()) + .map(Rank::name) + .collect(Collectors.toSet()); + } + + @JsonIgnore + public Profile getProfile() { + return new Profile(id, username, avatar != null ? avatar.toString() : null); + } + + @PostRemove + final void preDestroy() { + if (avatar != null) { + avatar.delete(); + } + } + + public static User findByUsername(final String username) { + return findByUsername(username, null); + } + + public static User findByUsername(final String username, @Nullable final LockModeType lock) { + return User.find("username", username).withLock(lock).firstResult(); + } + + public static User getFromSecurityContext(final SecurityContext context) { + return getFromSecurityContext(context, null); + } + + public static User getFromSecurityContext(final SecurityContext context, final LockModeType lock) { + final var user = findByUsername(context.getUserPrincipal().getName(), lock); + + if (user == null) { + throw new NotAuthorizedException("Bearer"); + } + + return user; + } + + public enum Rank { + CITIZEN, + MODERATOR, + ADMINISTRATOR; + } + + public enum BanCount { + NEVER, + ONCE, + ONE_TOO_MANY; + } + + public static final record Profile(@NotNull UUID id, @NotNull String username, String avatar) {} +} diff --git a/src/main/java/app/fyreplace/api/data/UserCreation.java b/src/main/java/app/fyreplace/api/data/UserCreation.java new file mode 100644 index 0000000..d09c808 --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/UserCreation.java @@ -0,0 +1,10 @@ +package app.fyreplace.api.data; + +import app.fyreplace.api.data.validators.Regex; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; +import org.hibernate.validator.constraints.Length; + +public record UserCreation( + @NotNull @Length(min = 3, max = 254) @Email String email, + @NotNull @Length(min = 3, max = 100) @Regex(pattern = "^[\\w.@+-]+\\Z") String username) {} diff --git a/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java b/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java new file mode 100644 index 0000000..fa8f37b --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java @@ -0,0 +1,68 @@ +package app.fyreplace.api.data.dev; + +import static java.util.stream.IntStream.range; + +import app.fyreplace.api.data.Email; +import app.fyreplace.api.data.RandomCode; +import app.fyreplace.api.data.StoredFile; +import app.fyreplace.api.data.User; +import io.quarkus.runtime.LaunchMode; +import io.quarkus.runtime.ShutdownEvent; +import io.quarkus.runtime.StartupEvent; +import io.quarkus.runtime.configuration.ProfileManager; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; +import jakarta.transaction.Transactional; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +@ApplicationScoped +public class DataSeeder { + @ConfigProperty(name = "app.use-example-data") + private boolean useExampleData; + + public void onStartup(@Observes final StartupEvent event) { + if (shouldUseExampleData()) { + insertData(); + } + } + + public void onShutdown(@Observes final ShutdownEvent event) { + if (shouldUseExampleData()) { + deleteData(); + } + } + + @Transactional + public void insertData() { + range(0, 100).forEach(i -> createUser("user_" + i, true)); + range(0, 10).forEach(i -> createUser("user_inactive_" + i, false)); + } + + @Transactional + public void deleteData() { + Email.deleteAll(); + RandomCode.deleteAll(); + User.deleteAll(); + StoredFile.streamAll().forEach(StoredFile::delete); + } + + private boolean shouldUseExampleData() { + return useExampleData && ProfileManager.getLaunchMode() == LaunchMode.DEVELOPMENT; + } + + private void createUser(final String username, final boolean isActive) { + final var user = new User(); + user.username = username; + user.isActive = isActive; + user.persist(); + + final var email = new Email(); + email.user = user; + email.email = username + "@example.org"; + email.isVerified = isActive; + email.persist(); + + user.mainEmail = email; + user.persist(); + } +} diff --git a/src/main/java/app/fyreplace/api/data/validators/Regex.java b/src/main/java/app/fyreplace/api/data/validators/Regex.java new file mode 100644 index 0000000..ea9e35f --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/validators/Regex.java @@ -0,0 +1,23 @@ +package app.fyreplace.api.data.validators; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target({FIELD, PARAMETER}) +@Constraint(validatedBy = RegexValidator.class) +public @interface Regex { + String message() default "Pattern not matched"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + String pattern(); +} diff --git a/src/main/java/app/fyreplace/api/data/validators/RegexValidator.java b/src/main/java/app/fyreplace/api/data/validators/RegexValidator.java new file mode 100644 index 0000000..d2e607a --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/validators/RegexValidator.java @@ -0,0 +1,19 @@ +package app.fyreplace.api.data.validators; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import java.util.regex.Pattern; + +public final class RegexValidator implements ConstraintValidator { + private Pattern pattern; + + @Override + public void initialize(final Regex constraintAnnotation) { + pattern = Pattern.compile(constraintAnnotation.pattern()); + } + + @Override + public boolean isValid(final String value, final ConstraintValidatorContext context) { + return pattern.matcher(value).matches(); + } +} diff --git a/src/main/java/app/fyreplace/api/emails/EmailBase.java b/src/main/java/app/fyreplace/api/emails/EmailBase.java new file mode 100644 index 0000000..0bac110 --- /dev/null +++ b/src/main/java/app/fyreplace/api/emails/EmailBase.java @@ -0,0 +1,69 @@ +package app.fyreplace.api.emails; + +import app.fyreplace.api.data.Email; +import app.fyreplace.api.data.RandomCode; +import app.fyreplace.api.services.RandomService; +import io.quarkus.mailer.Mail; +import io.quarkus.mailer.Mailer; +import io.quarkus.qute.TemplateInstance; +import io.smallrye.common.annotation.Blocking; +import jakarta.inject.Inject; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.logging.Logger; + +public abstract class EmailBase extends Mail { + @ConfigProperty(name = "app.url") + String appUrl; + + @Inject + Mailer mailer; + + @Inject + RandomService randomService; + + private String code; + + private Email email; + + private final Logger logger = Logger.getLogger(this.getClass()); + + protected abstract String getAction(); + + protected abstract TemplateInstance textTemplate(); + + protected abstract TemplateInstance htmlTemplate(); + + @Blocking + public void sendTo(final Email email) { + this.email = email; + mailer.send(this.setText(textTemplate().render()) + .setHtml(htmlTemplate().render()) + .setTo(List.of(email.email))); + } + + protected String getRandomCode() { + if (code != null) { + return code; + } + + final var randomCode = new RandomCode(); + randomCode.email = email; + randomCode.code = randomService.generateCode(); + randomCode.persist(); + code = randomCode.toString(); + return code; + } + + protected String getLink() { + try { + final var url = new URL(new URL(appUrl), "?action=" + getAction()); + return String.format("%s#%s:%s", url, email.user.username, getRandomCode()); + } catch (final MalformedURLException e) { + logger.error("Could not generate link", e); + return null; + } + } +} diff --git a/src/main/java/app/fyreplace/api/emails/EmailVerificationEmail.java b/src/main/java/app/fyreplace/api/emails/EmailVerificationEmail.java new file mode 100644 index 0000000..5e6b22c --- /dev/null +++ b/src/main/java/app/fyreplace/api/emails/EmailVerificationEmail.java @@ -0,0 +1,30 @@ +package app.fyreplace.api.emails; + +import io.quarkus.qute.CheckedTemplate; +import io.quarkus.qute.TemplateInstance; +import jakarta.enterprise.context.Dependent; + +@Dependent +public final class EmailVerificationEmail extends EmailBase { + @Override + protected String getAction() { + return "connect"; + } + + @Override + protected TemplateInstance textTemplate() { + return Templates.text(getRandomCode(), getLink()); + } + + @Override + protected TemplateInstance htmlTemplate() { + return Templates.html(getRandomCode(), getLink()); + } + + @CheckedTemplate + public static class Templates { + public static native TemplateInstance text(String code, String link); + + public static native TemplateInstance html(String code, String link); + } +} diff --git a/src/main/java/app/fyreplace/api/emails/UserActivationEmail.java b/src/main/java/app/fyreplace/api/emails/UserActivationEmail.java new file mode 100644 index 0000000..1f5daca --- /dev/null +++ b/src/main/java/app/fyreplace/api/emails/UserActivationEmail.java @@ -0,0 +1,34 @@ +package app.fyreplace.api.emails; + +import io.quarkus.qute.CheckedTemplate; +import io.quarkus.qute.TemplateInstance; +import jakarta.enterprise.context.Dependent; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +@Dependent +public final class UserActivationEmail extends EmailBase { + @ConfigProperty(name = "app.name") + String appName; + + @Override + protected String getAction() { + return "connect"; + } + + @Override + protected TemplateInstance textTemplate() { + return Templates.text(appName, getRandomCode(), getLink()); + } + + @Override + protected TemplateInstance htmlTemplate() { + return Templates.html(appName, getRandomCode(), getLink()); + } + + @CheckedTemplate + public static class Templates { + public static native TemplateInstance text(String appName, String code, String link); + + public static native TemplateInstance html(String appName, String code, String link); + } +} diff --git a/src/main/java/app/fyreplace/api/emails/UserConnectionEmail.java b/src/main/java/app/fyreplace/api/emails/UserConnectionEmail.java new file mode 100644 index 0000000..cd9fd89 --- /dev/null +++ b/src/main/java/app/fyreplace/api/emails/UserConnectionEmail.java @@ -0,0 +1,30 @@ +package app.fyreplace.api.emails; + +import io.quarkus.qute.CheckedTemplate; +import io.quarkus.qute.TemplateInstance; +import jakarta.enterprise.context.Dependent; + +@Dependent +public class UserConnectionEmail extends EmailBase { + @Override + protected String getAction() { + return "connect"; + } + + @Override + protected TemplateInstance textTemplate() { + return Templates.text(getRandomCode(), getLink()); + } + + @Override + protected TemplateInstance htmlTemplate() { + return Templates.html(getRandomCode(), getLink()); + } + + @CheckedTemplate + public static class Templates { + public static native TemplateInstance text(String code, String link); + + public static native TemplateInstance html(String code, String link); + } +} diff --git a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java new file mode 100644 index 0000000..3c99ed1 --- /dev/null +++ b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java @@ -0,0 +1,142 @@ +package app.fyreplace.api.endpoints; + +import app.fyreplace.api.data.Email; +import app.fyreplace.api.data.EmailActivation; +import app.fyreplace.api.data.EmailCreation; +import app.fyreplace.api.data.RandomCode; +import app.fyreplace.api.data.User; +import app.fyreplace.api.emails.EmailVerificationEmail; +import app.fyreplace.api.exceptions.ConflictException; +import app.fyreplace.api.exceptions.ForbiddenException; +import io.quarkus.panache.common.Sort; +import io.quarkus.security.Authenticated; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.core.SecurityContext; +import java.util.List; +import java.util.UUID; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; + +@Path("emails") +public final class EmailsEndpoint { + @ConfigProperty(name = "app.paging.size") + int pagingSize; + + @Inject + EmailVerificationEmail emailVerificationEmail; + + @GET + @Authenticated + @APIResponse(responseCode = "200") + @APIResponse(responseCode = "401") + public List list(@Context final SecurityContext context, @QueryParam("page") final int page) { + final var user = User.getFromSecurityContext(context); + return Email.find("user", Sort.by("email"), user).page(page, pagingSize).list(); + } + + @POST + @Authenticated + @Transactional + @Consumes(MediaType.APPLICATION_JSON) + @APIResponse( + responseCode = "201", + content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Email.class))) + @APIResponse(responseCode = "400") + @APIResponse(responseCode = "401") + @APIResponse(responseCode = "409") + public Response create(@Context final SecurityContext context, @Valid @NotNull final EmailCreation input) { + if (Email.count("email", input.email()) > 0) { + throw new ConflictException("email_taken"); + } + + final var email = new Email(); + email.user = User.getFromSecurityContext(context); + email.email = input.email(); + email.persist(); + emailVerificationEmail.sendTo(email); + return Response.status(Status.CREATED).entity(email).build(); + } + + @DELETE + @Path("{id}") + @Authenticated + @Transactional + @APIResponse(responseCode = "204") + @APIResponse(responseCode = "401") + @APIResponse(responseCode = "403") + @APIResponse(responseCode = "404") + public void delete(@Context final SecurityContext context, @PathParam("id") final UUID id) { + final var user = User.getFromSecurityContext(context); + final var email = Email.find("user = ?1 and id = ?2", user, id).firstResult(); + + if (email == null) { + throw new NotFoundException(); + } else if (email.isMain()) { + throw new ForbiddenException("email_is_main"); + } + + email.delete(); + } + + @POST + @Path("{id}/isMain") + @Authenticated + @Transactional + @APIResponse(responseCode = "200") + @APIResponse(responseCode = "403") + @APIResponse(responseCode = "404") + public Response setMain(@Context final SecurityContext context, @PathParam("id") final UUID id) { + final var user = User.getFromSecurityContext(context); + final var email = Email.find("user = ?1 and id = ?2", user, id).firstResult(); + + if (email == null) { + throw new NotFoundException(); + } else if (!email.isVerified) { + throw new ForbiddenException("email_not_verified"); + } + + user.mainEmail = email; + user.persist(); + return Response.ok().build(); + } + + @POST + @Path("activation") + @Authenticated + @Transactional + @APIResponse(responseCode = "200") + @APIResponse(responseCode = "400") + @APIResponse(responseCode = "401") + @APIResponse(responseCode = "404") + public Response activate(@Context final SecurityContext context, @NotNull @Valid final EmailActivation input) { + var email = Email.find("email", input.email()).firstResult(); + final var randomCode = RandomCode.find("email = ?1 and code = ?2", email, input.code()) + .firstResult(); + + if (randomCode == null) { + throw new NotFoundException(); + } + + randomCode.email.isVerified = true; + randomCode.email.persist(); + randomCode.delete(); + return Response.ok().build(); + } +} diff --git a/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java new file mode 100644 index 0000000..070f9d2 --- /dev/null +++ b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java @@ -0,0 +1,96 @@ +package app.fyreplace.api.endpoints; + +import app.fyreplace.api.data.Email; +import app.fyreplace.api.data.NewTokenCreation; +import app.fyreplace.api.data.RandomCode; +import app.fyreplace.api.data.TokenCreation; +import app.fyreplace.api.data.User; +import app.fyreplace.api.emails.UserConnectionEmail; +import app.fyreplace.api.services.JwtService; +import io.quarkus.security.Authenticated; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.core.SecurityContext; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; + +@Path("tokens") +public final class TokensEndpoint { + @Inject + JwtService jwtService; + + @Inject + UserConnectionEmail userConnectionEmail; + + @POST + @Transactional + @APIResponse( + responseCode = "201", + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) + @APIResponse(responseCode = "400") + @APIResponse(responseCode = "404") + public Response create(@Valid @NotNull final TokenCreation input) { + final var email = getEmail(input.identifier()); + final var randomCode = RandomCode.find("email = ?1 and code = ?2", email, input.code()) + .firstResult(); + + if (randomCode == null) { + throw new NotFoundException(); + } + + randomCode.email.isVerified = true; + randomCode.email.persist(); + randomCode.delete(); + return Response.status(Status.CREATED).entity(jwtService.makeJwt(email)).build(); + } + + @GET + @Path("new") + @Authenticated + @APIResponse( + responseCode = "200", + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) + @APIResponse(responseCode = "401") + public String retrieveNew(@Context final SecurityContext context) { + return jwtService.makeJwt(User.getFromSecurityContext(context)); + } + + @POST + @Path("new") + @Transactional + @APIResponse(responseCode = "200") + @APIResponse(responseCode = "400") + @APIResponse(responseCode = "404") + public Response createNew(@NotNull @Valid final NewTokenCreation input) { + final var email = getEmail(input.identifier()); + userConnectionEmail.sendTo(email); + return Response.ok().build(); + } + + private Email getEmail(final String identifier) { + final var email = Email.find("email", identifier).firstResult(); + + if (email != null) { + return email; + } + + final var user = User.findByUsername(identifier); + + if (user != null) { + return user.mainEmail; + } + + throw new NotFoundException(); + } +} diff --git a/src/main/java/app/fyreplace/api/endpoints/UsersDevEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/UsersDevEndpoint.java new file mode 100644 index 0000000..2de2b60 --- /dev/null +++ b/src/main/java/app/fyreplace/api/endpoints/UsersDevEndpoint.java @@ -0,0 +1,27 @@ +package app.fyreplace.api.endpoints; + +import app.fyreplace.api.data.User; +import app.fyreplace.api.services.JwtService; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; + +@Path("dev/users") +public final class UsersDevEndpoint { + @Inject + JwtService jwtService; + + @GET + @Path("{username}/token") + @APIResponse( + responseCode = "200", + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) + public String retrieveToken(@PathParam("username") final String username) { + return jwtService.makeJwt(User.findByUsername(username)); + } +} diff --git a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java new file mode 100644 index 0000000..0544da8 --- /dev/null +++ b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java @@ -0,0 +1,269 @@ +package app.fyreplace.api.endpoints; + +import app.fyreplace.api.data.Block; +import app.fyreplace.api.data.Email; +import app.fyreplace.api.data.StoredFile; +import app.fyreplace.api.data.User; +import app.fyreplace.api.data.UserCreation; +import app.fyreplace.api.emails.UserActivationEmail; +import app.fyreplace.api.exceptions.ConflictException; +import app.fyreplace.api.exceptions.ForbiddenException; +import app.fyreplace.api.services.MimeTypeService; +import app.fyreplace.api.services.mimetype.KnownMimeTypes; +import io.quarkus.panache.common.Sort; +import io.quarkus.security.Authenticated; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.persistence.LockModeType; +import jakarta.transaction.Transactional; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.core.SecurityContext; +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.hibernate.validator.constraints.Length; + +@Path("users") +public final class UsersEndpoint { + @ConfigProperty(name = "app.paging.size") + int pagingSize; + + @Inject + MimeTypeService mimeTypeService; + + @Inject + UserActivationEmail userActivationEmail; + + @POST + @Transactional + @APIResponse( + responseCode = "201", + content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = User.class))) + @APIResponse(responseCode = "400") + @APIResponse(responseCode = "403") + @APIResponse(responseCode = "409") + public Response create(@Valid @NotNull final UserCreation input) { + if (User.forbiddenUsernames.contains(input.username())) { + throw new ForbiddenException("username_forbidden"); + } else if (User.count("username", input.username()) > 0) { + throw new ConflictException("username_taken"); + } else if (Email.count("email", input.email()) > 0) { + throw new ConflictException("email_taken"); + } + + final var user = new User(); + user.username = input.username(); + user.persist(); + + final var email = new Email(); + email.user = user; + email.email = input.email(); + email.persist(); + user.mainEmail = email; + user.persist(); + + userActivationEmail.sendTo(email); + return Response.status(Status.CREATED).entity(user).build(); + } + + @GET + @Path("{id}") + @APIResponse( + responseCode = "200", + content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = User.class))) + @APIResponse(responseCode = "404") + public User retrieve(@PathParam("id") final UUID id) { + final var user = User.findById(id); + + if (user == null) { + throw new NotFoundException(); + } else { + return user; + } + } + + @PUT + @Path("{id}/isBlocked") + @Authenticated + @Transactional + @APIResponse(responseCode = "200") + @APIResponse(responseCode = "401") + @APIResponse(responseCode = "403") + @APIResponse(responseCode = "404") + public Response createBlock(@Context final SecurityContext context, @PathParam("id") final UUID id) { + final var source = User.getFromSecurityContext(context); + final var target = User.findById(id); + + if (target == null) { + throw new NotFoundException(); + } else if (source.id.equals(target.id)) { + throw new ForbiddenException("user_is_self"); + } else if (Block.count("source = ?1 and target = ?2", source, target) == 0) { + final var block = new Block(); + block.source = source; + block.target = target; + block.persist(); + } + + return Response.ok().build(); + } + + @DELETE + @Path("{id}/isBlocked") + @Authenticated + @Transactional + @APIResponse(responseCode = "204") + @APIResponse(responseCode = "401") + @APIResponse(responseCode = "404") + public void deleteBlock(@Context final SecurityContext context, @PathParam("id") final UUID id) { + final var source = User.getFromSecurityContext(context); + final var target = User.findById(id); + + if (target == null) { + throw new NotFoundException(); + } + + Block.delete("source = ?1 and target = ?2", source, target); + } + + @PUT + @Path("{id}/isBanned") + @RolesAllowed({"ADMINISTRATOR", "MODERATOR"}) + @Transactional + @APIResponse(responseCode = "200") + @APIResponse(responseCode = "401") + @APIResponse(responseCode = "403") + @APIResponse(responseCode = "404") + public Response ban(@Context final SecurityContext context, @PathParam("id") final UUID id) { + final var user = User.findById(id, LockModeType.PESSIMISTIC_WRITE); + + if (user == null) { + throw new NotFoundException(); + } else if (!user.isBanned) { + if (user.banCount == User.BanCount.NEVER) { + user.dateBanEnd = Instant.now().plus(Duration.ofDays(7)); + user.banCount = User.BanCount.ONCE; + } else { + user.banCount = User.BanCount.ONE_TOO_MANY; + } + + user.isBanned = true; + user.persist(); + } + + return Response.ok().build(); + } + + @GET + @Path("me") + @Authenticated + @APIResponse( + responseCode = "200", + content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = User.class))) + @APIResponse(responseCode = "401") + public User retrieveMe(@Context final SecurityContext context) { + return retrieve(User.getFromSecurityContext(context).id); + } + + @PUT + @Path("me/bio") + @Authenticated + @Transactional + @Consumes(MediaType.TEXT_PLAIN) + @APIResponse( + responseCode = "200", + content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = User.class))) + @APIResponse(responseCode = "400") + @APIResponse(responseCode = "401") + public String updateMeBio(@Context final SecurityContext context, @NotNull @Length(max = 3000) final String input) { + final var user = User.getFromSecurityContext(context, LockModeType.PESSIMISTIC_READ); + user.bio = input; + user.persist(); + return user.bio; + } + + @PUT + @Path("me/avatar") + @Authenticated + @Transactional + @Consumes(MediaType.APPLICATION_OCTET_STREAM) + @APIResponse( + responseCode = "200", + content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = User.class))) + @APIResponse(responseCode = "401") + @APIResponse(responseCode = "413") + @APIResponse(responseCode = "415") + public String updateMeAvatar(@Context final SecurityContext context, final byte[] input) throws IOException { + mimeTypeService.validate(input, KnownMimeTypes.IMAGE); + final var user = User.getFromSecurityContext(context, LockModeType.PESSIMISTIC_WRITE); + + if (user.avatar == null) { + user.avatar = new StoredFile("avatars/" + user.id.toString(), input); + } else { + user.avatar.store(input); + } + + user.persist(); + return user.avatar.toString(); + } + + @DELETE + @Path("me/avatar") + @Authenticated + @Transactional + @APIResponse(responseCode = "204") + @APIResponse(responseCode = "401") + public void deleteMeAvatar(@Context final SecurityContext context) { + final var user = User.getFromSecurityContext(context, LockModeType.PESSIMISTIC_WRITE); + + if (user.avatar != null) { + user.avatar.delete(); + user.avatar = null; + user.persist(); + } + } + + @DELETE + @Path("me") + @Authenticated + @Transactional + @APIResponse(responseCode = "204") + @APIResponse(responseCode = "401") + public void deleteMe(@Context final SecurityContext context) { + User.delete("username", context.getUserPrincipal().getName()); + } + + @GET + @Path("blocked") + @Authenticated + @APIResponse(responseCode = "200") + @APIResponse(responseCode = "401") + public List listBlocked(@Context final SecurityContext context, @QueryParam("page") final int page) { + return Block.find("source", Sort.by("id"), User.getFromSecurityContext(context)) + .page(page, pagingSize) + .stream() + .map(block -> block.target.getProfile()) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/app/fyreplace/api/exceptions/ConflictException.java b/src/main/java/app/fyreplace/api/exceptions/ConflictException.java new file mode 100644 index 0000000..246cd06 --- /dev/null +++ b/src/main/java/app/fyreplace/api/exceptions/ConflictException.java @@ -0,0 +1,18 @@ +package app.fyreplace.api.exceptions; + +import jakarta.ws.rs.ClientErrorException; +import jakarta.ws.rs.core.Response; + +public final class ConflictException extends ClientErrorException implements ExplainableException { + private String explanationValue; + + public ConflictException(final String explanationValue) { + super(Response.Status.CONFLICT); + this.explanationValue = explanationValue; + } + + @Override + public Object getExplanationValue() { + return explanationValue; + } +} diff --git a/src/main/java/app/fyreplace/api/exceptions/ExceptionMappers.java b/src/main/java/app/fyreplace/api/exceptions/ExceptionMappers.java new file mode 100644 index 0000000..7ec2778 --- /dev/null +++ b/src/main/java/app/fyreplace/api/exceptions/ExceptionMappers.java @@ -0,0 +1,21 @@ +package app.fyreplace.api.exceptions; + +import jakarta.ws.rs.core.Response; +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; + +public final class ExceptionMappers { + @ServerExceptionMapper + public Response handleForbiddenException(final ForbiddenException exception) { + return Responses.makeFrom(exception); + } + + @ServerExceptionMapper + public Response handleConflictException(final ConflictException exception) { + return Responses.makeFrom(exception); + } + + @ServerExceptionMapper + public Response handleUnsupportedMediaTypeException(final UnsupportedMediaTypeException exception) { + return Responses.makeFrom(exception); + } +} diff --git a/src/main/java/app/fyreplace/api/exceptions/ExplainableException.java b/src/main/java/app/fyreplace/api/exceptions/ExplainableException.java new file mode 100644 index 0000000..c45e05d --- /dev/null +++ b/src/main/java/app/fyreplace/api/exceptions/ExplainableException.java @@ -0,0 +1,5 @@ +package app.fyreplace.api.exceptions; + +public interface ExplainableException { + Object getExplanationValue(); +} diff --git a/src/main/java/app/fyreplace/api/exceptions/ForbiddenException.java b/src/main/java/app/fyreplace/api/exceptions/ForbiddenException.java new file mode 100644 index 0000000..d0bed70 --- /dev/null +++ b/src/main/java/app/fyreplace/api/exceptions/ForbiddenException.java @@ -0,0 +1,15 @@ +package app.fyreplace.api.exceptions; + +public final class ForbiddenException extends jakarta.ws.rs.ForbiddenException implements ExplainableException { + private final String explanation; + + public ForbiddenException(final String explanation) { + super(); + this.explanation = explanation; + } + + @Override + public Object getExplanationValue() { + return explanation; + } +} diff --git a/src/main/java/app/fyreplace/api/exceptions/Responses.java b/src/main/java/app/fyreplace/api/exceptions/Responses.java new file mode 100644 index 0000000..897b98f --- /dev/null +++ b/src/main/java/app/fyreplace/api/exceptions/Responses.java @@ -0,0 +1,16 @@ +package app.fyreplace.api.exceptions; + +import io.vertx.core.json.JsonObject; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; + +public final class Responses { + public static Response makeFrom(final E exception) { + final var response = exception.getResponse(); + final var body = new JsonObject() + .put("title", response.getStatusInfo().getReasonPhrase()) + .put("status", response.getStatus()) + .put("reason", exception.getExplanationValue()); + return Response.status(response.getStatus()).entity(body).build(); + } +} diff --git a/src/main/java/app/fyreplace/api/exceptions/UnsupportedMediaTypeException.java b/src/main/java/app/fyreplace/api/exceptions/UnsupportedMediaTypeException.java new file mode 100644 index 0000000..e835051 --- /dev/null +++ b/src/main/java/app/fyreplace/api/exceptions/UnsupportedMediaTypeException.java @@ -0,0 +1,18 @@ +package app.fyreplace.api.exceptions; + +import jakarta.ws.rs.ClientErrorException; +import jakarta.ws.rs.core.Response; + +public final class UnsupportedMediaTypeException extends ClientErrorException implements ExplainableException { + private String explanationValue; + + public UnsupportedMediaTypeException(final String explanationValue) { + super(Response.Status.UNSUPPORTED_MEDIA_TYPE); + this.explanationValue = explanationValue; + } + + @Override + public Object getExplanationValue() { + return explanationValue; + } +} diff --git a/src/main/java/app/fyreplace/api/services/JwtService.java b/src/main/java/app/fyreplace/api/services/JwtService.java new file mode 100644 index 0000000..e1ef930 --- /dev/null +++ b/src/main/java/app/fyreplace/api/services/JwtService.java @@ -0,0 +1,26 @@ +package app.fyreplace.api.services; + +import app.fyreplace.api.data.Email; +import app.fyreplace.api.data.User; +import io.smallrye.jwt.build.Jwt; +import jakarta.enterprise.context.ApplicationScoped; +import java.time.Duration; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +@ApplicationScoped +public final class JwtService { + @ConfigProperty(name = "app.url") + String appUrl; + + public String makeJwt(final User user) { + return makeJwt(user.mainEmail); + } + + public String makeJwt(final Email email) { + return Jwt.issuer(appUrl) + .subject(email.user.username) + .groups(email.user.getGroups()) + .expiresIn(Duration.ofDays(3)) + .sign(); + } +} diff --git a/src/main/java/app/fyreplace/api/services/MimeTypeService.java b/src/main/java/app/fyreplace/api/services/MimeTypeService.java new file mode 100644 index 0000000..a1bdcad --- /dev/null +++ b/src/main/java/app/fyreplace/api/services/MimeTypeService.java @@ -0,0 +1,23 @@ +package app.fyreplace.api.services; + +import app.fyreplace.api.exceptions.UnsupportedMediaTypeException; +import app.fyreplace.api.services.mimetype.KnownMimeTypes; +import jakarta.enterprise.context.ApplicationScoped; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import org.apache.tika.Tika; + +@ApplicationScoped +public final class MimeTypeService { + public void validate(final byte[] data, final KnownMimeTypes types) throws IOException { + final var tika = new Tika(); + + try (final var stream = new ByteArrayInputStream(data)) { + final var mimeType = tika.detect(stream); + + if (mimeType == null || !types.types.contains(mimeType)) { + throw new UnsupportedMediaTypeException("invalid_media_type"); + } + } + } +} diff --git a/src/main/java/app/fyreplace/api/services/RandomService.java b/src/main/java/app/fyreplace/api/services/RandomService.java new file mode 100644 index 0000000..b86c7d1 --- /dev/null +++ b/src/main/java/app/fyreplace/api/services/RandomService.java @@ -0,0 +1,20 @@ +package app.fyreplace.api.services; + +import jakarta.enterprise.context.ApplicationScoped; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Random; + +@ApplicationScoped +public final class RandomService { + private final Random random; + + public RandomService() throws NoSuchAlgorithmException { + random = SecureRandom.getInstanceStrong(); + } + + public String generateCode() { + final var code = random.nextInt(1000000); + return String.format("%06d", code); + } +} diff --git a/src/main/java/app/fyreplace/api/services/StorageService.java b/src/main/java/app/fyreplace/api/services/StorageService.java new file mode 100644 index 0000000..4c205bc --- /dev/null +++ b/src/main/java/app/fyreplace/api/services/StorageService.java @@ -0,0 +1,12 @@ +package app.fyreplace.api.services; + +import java.io.IOException; +import java.net.URI; + +public interface StorageService { + void store(final String path, final byte[] data) throws IOException; + + void remove(final String path) throws IOException; + + URI getUri(final String path); +} diff --git a/src/main/java/app/fyreplace/api/services/mimetype/KnownMimeTypes.java b/src/main/java/app/fyreplace/api/services/mimetype/KnownMimeTypes.java new file mode 100644 index 0000000..ef8590b --- /dev/null +++ b/src/main/java/app/fyreplace/api/services/mimetype/KnownMimeTypes.java @@ -0,0 +1,13 @@ +package app.fyreplace.api.services.mimetype; + +import java.util.Set; + +public enum KnownMimeTypes { + IMAGE(Set.of("image/jpeg", "image/png", "image/webp")); + + public final Set types; + + KnownMimeTypes(final Set types) { + this.types = types; + } +} diff --git a/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageConfig.java b/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageConfig.java new file mode 100644 index 0000000..e82fbe1 --- /dev/null +++ b/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageConfig.java @@ -0,0 +1,8 @@ +package app.fyreplace.api.services.storage.local; + +import io.smallrye.config.ConfigMapping; + +@ConfigMapping(prefix = "app.storage.local") +public interface LocalStorageConfig { + String path(); +} diff --git a/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java b/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java new file mode 100644 index 0000000..5d85708 --- /dev/null +++ b/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java @@ -0,0 +1,44 @@ +package app.fyreplace.api.services.storage.local; + +import app.fyreplace.api.services.StorageService; +import io.quarkus.arc.Unremovable; +import io.quarkus.arc.properties.IfBuildProperty; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Paths; + +@ApplicationScoped +@Unremovable +@IfBuildProperty(name = "app.storage.type", stringValue = "local") +public final class LocalStorageService implements StorageService { + @Inject + LocalStorageConfig config; + + @Override + public void store(final String path, final byte[] data) throws IOException { + final var file = getFile(path); + file.getParentFile().mkdirs(); + + try (final var writer = new FileOutputStream(file)) { + writer.write(data); + } + } + + @Override + public void remove(final String path) throws IOException { + getFile(path).delete(); + } + + @Override + public URI getUri(final String path) { + return getFile(path).toURI(); + } + + private File getFile(final String path) { + return Paths.get(config.path(), path).toFile(); + } +} diff --git a/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageConfig.java b/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageConfig.java new file mode 100644 index 0000000..7bbcb79 --- /dev/null +++ b/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageConfig.java @@ -0,0 +1,11 @@ +package app.fyreplace.api.services.storage.s3; + +import io.smallrye.config.ConfigMapping; +import java.net.URI; + +@ConfigMapping(prefix = "app.storage.s3") +public interface S3StorageConfig { + String bucket(); + + URI customDomain(); +} diff --git a/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java b/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java new file mode 100644 index 0000000..da44f3c --- /dev/null +++ b/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java @@ -0,0 +1,47 @@ +package app.fyreplace.api.services.storage.s3; + +import app.fyreplace.api.services.StorageService; +import io.quarkus.arc.Unremovable; +import io.quarkus.arc.properties.IfBuildProperty; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import java.net.URI; +import java.net.URISyntaxException; +import org.jboss.logging.Logger; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; + +@ApplicationScoped +@Unremovable +@IfBuildProperty(name = "app.storage.type", stringValue = "s3") +public final class S3StorageService implements StorageService { + private static final Logger logger = Logger.getLogger(S3StorageService.class); + + @Inject + S3StorageConfig config; + + @Inject + S3Client client; + + @Override + public void store(final String path, final byte[] data) { + client.putObject(b -> b.bucket(config.bucket()).key(path), RequestBody.fromBytes(data)); + } + + @Override + public void remove(final String path) { + client.deleteObject(b -> b.bucket(config.bucket()).key(path)); + } + + @Override + public URI getUri(final String path) { + try { + return client.utilities() + .getUrl(b -> b.bucket(config.bucket()).key(path)) + .toURI(); + } catch (final URISyntaxException e) { + logger.error("Failed to get URI for S3 object", e); + return null; + } + } +} diff --git a/src/main/java/app/fyreplace/api/tasks/CleanupTasks.java b/src/main/java/app/fyreplace/api/tasks/CleanupTasks.java new file mode 100644 index 0000000..ede9392 --- /dev/null +++ b/src/main/java/app/fyreplace/api/tasks/CleanupTasks.java @@ -0,0 +1,28 @@ +package app.fyreplace.api.tasks; + +import app.fyreplace.api.data.RandomCode; +import app.fyreplace.api.data.User; +import io.quarkus.scheduler.Scheduled; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.transaction.Transactional; +import java.time.Duration; +import java.time.Instant; + +@ApplicationScoped +public final class CleanupTasks { + @Scheduled(cron = "0 0 * * * ?") + @Transactional + public void removeOldInactiveUsers() { + User.delete("isActive = false and dateCreated < ?1", oneDayAgo()); + } + + @Scheduled(cron = "0 5 * * * ?") + @Transactional + public void removeOldRandomCodes() { + RandomCode.delete("dateCreated < ?1", oneDayAgo()); + } + + private Instant oneDayAgo() { + return Instant.now().minus(Duration.ofDays(1)); + } +} diff --git a/src/main/resources/META-INF/branding/logo.png b/src/main/resources/META-INF/branding/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f339748c3a4e1b08e19128a4aa3b6770a12f3ae8 GIT binary patch literal 7293 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 WZ^!8avq$HNf$u@@mfo>``M&@KkE)LV literal 0 HcmV?d00001 diff --git a/src/main/resources/META-INF/resources/robots.txt b/src/main/resources/META-INF/resources/robots.txt new file mode 100644 index 0000000..1f53798 --- /dev/null +++ b/src/main/resources/META-INF/resources/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 3319e28..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1,2 +0,0 @@ -quarkus.hibernate-orm.physical-naming-strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy -%dev.quarkus.hibernate-orm.database.generation=drop-and-create diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..88d1177 --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,93 @@ +# Main profile + +quarkus: + datasource: + db-kind: postgresql + + hibernate-orm: + physical-naming-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy + + qute: + content-types: + mjml: text/plain + + http: + auth: + permission: + dev: + paths: /dev/* + policy: deny + + cors: + ~: true + + limits: + max-body-size: 1M + + s3: + devservices: + enabled: false + +mp: + jwt: + verify: + issuer: ${app.url} + +app: + url: "" + name: Fyreplace + use-example-data: false + paging: + size: 12 + + storage: + type: local + + local: + path: "" + + s3: + bucket: "" + custom-domain: "" + +# Dev profile + +"%dev": + quarkus: + hibernate-orm: + database: + generation: + ~: drop-and-create + + http: + auth: + permission: + dev: + paths: /dev/* + policy: permit + + app: + use-example-data: true + +# Test profile + +"%test": + quarkus: + datasource: + db-kind: h2 + jdbc: + user: h2 + password: h2 + url: jdbc:h2:mem:fyreplace + + hibernate-orm: + database: + generation: + ~: drop-and-create + + http: + cors: + origins: /.*/ + + scheduler: + enabled: false diff --git a/src/main/resources/keys/.gitignore b/src/main/resources/keys/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/src/main/resources/keys/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/src/main/resources/templates/.gitignore b/src/main/resources/templates/.gitignore new file mode 100644 index 0000000..2d19fc7 --- /dev/null +++ b/src/main/resources/templates/.gitignore @@ -0,0 +1 @@ +*.html diff --git a/src/main/resources/templates/EmailVerificationEmail/html.html.mjml b/src/main/resources/templates/EmailVerificationEmail/html.html.mjml new file mode 100644 index 0000000..b60520a --- /dev/null +++ b/src/main/resources/templates/EmailVerificationEmail/html.html.mjml @@ -0,0 +1,15 @@ + + + + + + + You can verify your email by using this code: + {code} + Or by clicking on this link: + Activate account + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/EmailVerificationEmail/text.txt b/src/main/resources/templates/EmailVerificationEmail/text.txt new file mode 100644 index 0000000..95985cc --- /dev/null +++ b/src/main/resources/templates/EmailVerificationEmail/text.txt @@ -0,0 +1,7 @@ +You can verify your email by using this code: +{code} + +Or by clicking on this link: +{link} + +The code and link will expire in 24 hours. diff --git a/src/main/resources/templates/UserActivationEmail/html.html.mjml b/src/main/resources/templates/UserActivationEmail/html.html.mjml new file mode 100644 index 0000000..b40e7f2 --- /dev/null +++ b/src/main/resources/templates/UserActivationEmail/html.html.mjml @@ -0,0 +1,16 @@ + + + + + + + Welcome to {appName}! + You can activate your account by using this code: + {code} + Or by clicking on this link: + Activate account + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/UserActivationEmail/text.txt b/src/main/resources/templates/UserActivationEmail/text.txt new file mode 100644 index 0000000..da13cd0 --- /dev/null +++ b/src/main/resources/templates/UserActivationEmail/text.txt @@ -0,0 +1,9 @@ +Welcome to {appName}! + +You can activate your account by using this code: +{code} + +Or by clicking on this link: +{link} + +The code and link will expire in 24 hours. diff --git a/src/main/resources/templates/UserConnectionEmail/html.html.mjml b/src/main/resources/templates/UserConnectionEmail/html.html.mjml new file mode 100644 index 0000000..6d7146e --- /dev/null +++ b/src/main/resources/templates/UserConnectionEmail/html.html.mjml @@ -0,0 +1,15 @@ + + + + + + + You can connect by using this code: + {code} + Or by clicking on this link: + Activate account + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/UserConnectionEmail/text.txt b/src/main/resources/templates/UserConnectionEmail/text.txt new file mode 100644 index 0000000..968dc31 --- /dev/null +++ b/src/main/resources/templates/UserConnectionEmail/text.txt @@ -0,0 +1,7 @@ +You can connect by using this code: +{code} + +Or by clicking on this link: +{link} + +The code and link will expire in 24 hours. diff --git a/src/main/resources/templates/emails/_attributes.mjml b/src/main/resources/templates/emails/_attributes.mjml new file mode 100644 index 0000000..63598c0 --- /dev/null +++ b/src/main/resources/templates/emails/_attributes.mjml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/emails/_link_end_notice.mjml b/src/main/resources/templates/emails/_link_end_notice.mjml new file mode 100644 index 0000000..963e3ec --- /dev/null +++ b/src/main/resources/templates/emails/_link_end_notice.mjml @@ -0,0 +1 @@ +This link will expire in 24 hours. \ No newline at end of file diff --git a/src/main/resources/templates/emails/_logo.mjml b/src/main/resources/templates/emails/_logo.mjml new file mode 100644 index 0000000..4d18b60 --- /dev/null +++ b/src/main/resources/templates/emails/_logo.mjml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/test/java/.gitkeep b/src/test/java/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/test/java/app/fyreplace/api/testing/Assertions.java b/src/test/java/app/fyreplace/api/testing/Assertions.java new file mode 100644 index 0000000..993966f --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/Assertions.java @@ -0,0 +1,14 @@ +package app.fyreplace.api.testing; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +import io.quarkus.mailer.Mail; +import java.util.List; + +public final class Assertions { + public static void assertSingleEmail(final Class emailClass, final List mails) { + assertEquals(1, mails.size()); + assertInstanceOf(emailClass, mails.get(0)); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/DatabaseTestResource.java b/src/test/java/app/fyreplace/api/testing/DatabaseTestResource.java new file mode 100644 index 0000000..ea24386 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/DatabaseTestResource.java @@ -0,0 +1,18 @@ +package app.fyreplace.api.testing; + +import io.quarkus.test.h2.H2DatabaseTestResource; +import java.util.Map; + +public final class DatabaseTestResource extends H2DatabaseTestResource { + @Override + public Map start() { + super.start(); + return Map.of( + "quarkus.datasource.jdbc.username", + "h2", + "quarkus.datasource.jdbc.password", + "h2", + "quarkus.datasource.jdbc.url", + "jdbc:h2:mem:fyreplace"); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/TransactionalTests.java b/src/test/java/app/fyreplace/api/testing/TransactionalTests.java new file mode 100644 index 0000000..39da0bc --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/TransactionalTests.java @@ -0,0 +1,35 @@ +package app.fyreplace.api.testing; + +import app.fyreplace.api.data.Email; +import app.fyreplace.api.data.dev.DataSeeder; +import io.quarkus.mailer.Mail; +import io.quarkus.mailer.MockMailbox; +import io.quarkus.test.common.QuarkusTestResource; +import jakarta.inject.Inject; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +@QuarkusTestResource(DatabaseTestResource.class) +public abstract class TransactionalTests { + @Inject + DataSeeder seeder; + + @Inject + MockMailbox mailbox; + + @BeforeEach + public final void beforeEach_insertData() { + seeder.insertData(); + } + + @AfterEach + public final void afterEach_deleteData() { + seeder.deleteData(); + mailbox.clear(); + } + + protected List getMailsSentTo(final Email email) { + return mailbox.getMailsSentTo(email.email); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java new file mode 100644 index 0000000..39fbd55 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java @@ -0,0 +1,99 @@ +package app.fyreplace.api.testing.endpoints.emails; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import app.fyreplace.api.data.Email; +import app.fyreplace.api.data.EmailActivation; +import app.fyreplace.api.data.RandomCode; +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.EmailsEndpoint; +import app.fyreplace.api.services.RandomService; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.core.MediaType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(EmailsEndpoint.class) +public final class ActivateTests extends TransactionalTests { + @Inject + RandomService randomService; + + private Email newEmail; + + private RandomCode randomCode; + + @Test + @TestSecurity(user = "user_0") + public void activate() { + given().contentType(MediaType.APPLICATION_JSON) + .body(new EmailActivation(newEmail.email, randomCode.code)) + .post("activation") + .then() + .statusCode(200); + assertEquals(0, RandomCode.count("id", randomCode.id)); + } + + @Test + @TestSecurity(user = "user_0") + public void activateWithInvalidEmail() { + given().contentType(MediaType.APPLICATION_JSON) + .body(new EmailActivation("invalid", randomCode.code)) + .post("activation") + .then() + .statusCode(404); + assertEquals(1, RandomCode.count("id", randomCode.id)); + } + + @Test + @TestSecurity(user = "user_0") + public void activateWithOtherEmail() { + final var otherUser = User.findByUsername("user_1"); + given().contentType(MediaType.APPLICATION_JSON) + .body(new EmailActivation(otherUser.mainEmail.email, randomCode.code)) + .post("activation") + .then() + .statusCode(404); + assertEquals(1, RandomCode.count("id", randomCode.id)); + } + + @Test + @TestSecurity(user = "user_0") + public void activateWithInvalidCode() { + given().contentType(MediaType.APPLICATION_JSON) + .body(new EmailActivation(newEmail.email, "invalid")) + .post("activation") + .then() + .statusCode(404); + assertEquals(1, RandomCode.count("id", randomCode.id)); + } + + @Test + @TestSecurity(user = "user_0") + public void activateWithEmptyInput() { + given().contentType(MediaType.APPLICATION_JSON) + .post("activation") + .then() + .statusCode(400); + assertEquals(1, RandomCode.count("id", randomCode.id)); + } + + @BeforeEach + @Transactional + public void beforeEach_createEmail() { + newEmail = new Email(); + newEmail.user = User.findByUsername("user_0"); + newEmail.email = "new_email@example.org"; + newEmail.persist(); + randomCode = new RandomCode(); + randomCode.email = newEmail; + randomCode.code = randomService.generateCode(); + randomCode.persist(); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java new file mode 100644 index 0000000..a082190 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java @@ -0,0 +1,89 @@ +package app.fyreplace.api.testing.endpoints.emails; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import app.fyreplace.api.data.Email; +import app.fyreplace.api.data.EmailCreation; +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.EmailsEndpoint; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import jakarta.ws.rs.core.MediaType; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(EmailsEndpoint.class) +public final class CreateTests extends TransactionalTests { + @Test + @TestSecurity(user = "user_0") + public void create() { + final var email = "some_new_email@example.org"; + final var emailCount = Email.count(); + given().contentType(MediaType.APPLICATION_JSON) + .body(new EmailCreation(email)) + .post() + .then() + .contentType(MediaType.APPLICATION_JSON) + .statusCode(201) + .body("email", equalTo(email)) + .body("isVerified", equalTo(false)) + .body("isMain", equalTo(false)); + assertEquals(emailCount + 1, Email.count()); + } + + @Test + @TestSecurity(user = "user_0") + public void createWithInvalidEmail() { + final var emailCount = Email.count(); + given().contentType(MediaType.APPLICATION_JSON) + .body(new EmailCreation("invalid")) + .post() + .then() + .contentType(MediaType.APPLICATION_JSON) + .statusCode(400); + assertEquals(emailCount, Email.count()); + } + + @Test + @TestSecurity(user = "user_0") + public void createWithEmptyEmail() { + final var emailCount = Email.count(); + given().contentType(MediaType.APPLICATION_JSON) + .body(new EmailCreation("")) + .post() + .then() + .contentType(MediaType.APPLICATION_JSON) + .statusCode(400); + assertEquals(emailCount, Email.count()); + } + + @Test + @TestSecurity(user = "user_0") + public void createWithExistingEmail() { + final var existingUser = User.findByUsername("user_1"); + final var emailCount = Email.count(); + given().contentType(MediaType.APPLICATION_JSON) + .body(new EmailCreation(existingUser.mainEmail.email)) + .post() + .then() + .contentType(MediaType.APPLICATION_JSON) + .statusCode(409); + assertEquals(emailCount, Email.count()); + } + + @Test + @TestSecurity(user = "user_0") + public void createWithEmptyInput() { + final var emailCount = Email.count(); + given().contentType(MediaType.APPLICATION_JSON) + .post() + .then() + .contentType(MediaType.APPLICATION_JSON) + .statusCode(400); + assertEquals(emailCount, Email.count()); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java new file mode 100644 index 0000000..4dbfe93 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java @@ -0,0 +1,64 @@ +package app.fyreplace.api.testing.endpoints.emails; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import app.fyreplace.api.data.Email; +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.EmailsEndpoint; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(EmailsEndpoint.class) +public final class DeleteTests extends TransactionalTests { + private Email newEmail; + + @Test + @TestSecurity(user = "user_0") + public void delete() { + given().delete(newEmail.id.toString()).then().statusCode(204); + assertEquals(0, Email.count("id", newEmail.id)); + } + + @Test + @TestSecurity(user = "user_0") + public void deleteMainEmail() { + final var user = User.findByUsername("user_0"); + final var emailCount = Email.count(); + given().delete(user.mainEmail.id.toString()).then().statusCode(403); + assertEquals(emailCount, Email.count()); + } + + @Test + @TestSecurity(user = "user_0") + public void deleteOtherEmail() { + final var otherUser = User.findByUsername("user_1"); + final var emailCount = Email.count(); + given().delete(otherUser.mainEmail.id.toString()).then().statusCode(404); + assertEquals(emailCount, Email.count()); + } + + @Test + @TestSecurity(user = "user_0") + public void deleteNonExistentEmail() { + final var emailCount = Email.count(); + given().delete("nope").then().statusCode(404); + assertEquals(emailCount, Email.count()); + } + + @BeforeEach + @Transactional + public void beforeEach_createEmail() { + newEmail = new Email(); + newEmail.user = User.findByUsername("user_0"); + newEmail.email = "new_email@example.org"; + newEmail.isVerified = true; + newEmail.persist(); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java new file mode 100644 index 0000000..6f47256 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java @@ -0,0 +1,69 @@ +package app.fyreplace.api.testing.endpoints.emails; + +import static io.restassured.RestAssured.given; +import static java.util.stream.IntStream.range; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.in; + +import app.fyreplace.api.data.Email; +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.EmailsEndpoint; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(EmailsEndpoint.class) +public final class ListTests extends TransactionalTests { + @ConfigProperty(name = "app.paging.size") + int pagingSize; + + @Test + @TestSecurity(user = "user_0") + public void list() { + final var user = User.findByUsername("user_0"); + final var response = given().queryParam("page", 0) + .get() + .then() + .statusCode(200) + .contentType(MediaType.APPLICATION_JSON) + .body("size()", equalTo(pagingSize)); + + range(0, pagingSize) + .forEach(i -> response.body( + "[" + i + "].email", + in(Email.stream("user", user) + .map(email -> email.email) + .toList()))); + } + + @Test + @TestSecurity(user = "user_0") + public void listOutOfBounds() { + given().queryParam("page", 500) + .get() + .then() + .statusCode(200) + .contentType(MediaType.APPLICATION_JSON) + .body(equalTo("[]")); + } + + @BeforeEach + @Transactional + public void beforeEach_makeEmails() { + final var user = User.findByUsername("user_0"); + range(0, 100).forEach(i -> { + final var email = new Email(); + email.user = user; + email.email = user.username + "_" + i + "@example.com"; + email.isVerified = i % 5 == 0; + email.persist(); + }); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java new file mode 100644 index 0000000..05a4854 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java @@ -0,0 +1,82 @@ +package app.fyreplace.api.testing.endpoints.emails; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import app.fyreplace.api.data.Email; +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.EmailsEndpoint; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(EmailsEndpoint.class) +public final class SetMainTests extends TransactionalTests { + private Email secondaryEmail; + private Email unverifiedEmail; + + @Test + @TestSecurity(user = "user_0") + public void setMain() { + assertFalse(secondaryEmail.isMain()); + given().post(secondaryEmail.id + "/isMain").then().statusCode(200); + secondaryEmail = Email.findById(secondaryEmail.id); + assertTrue(secondaryEmail.isMain()); + } + + @Test + @TestSecurity(user = "user_0") + public void setMainTwice() { + assertFalse(secondaryEmail.isMain()); + given().post(secondaryEmail.id + "/isMain").then().statusCode(200); + given().post(secondaryEmail.id + "/isMain").then().statusCode(200); + secondaryEmail = Email.findById(secondaryEmail.id); + assertTrue(secondaryEmail.isMain()); + } + + @Test + @TestSecurity(user = "user_0") + public void setMainWithUnverifiedEmail() { + given().post(unverifiedEmail.id + "/isMain").then().statusCode(403); + secondaryEmail = Email.findById(secondaryEmail.id); + assertFalse(secondaryEmail.isMain()); + } + + @Test + @TestSecurity(user = "user_0") + public void setMainWithOtherEmail() { + final var otherUser = User.findByUsername("user_1"); + given().post(otherUser.mainEmail.id + "/isMain").then().statusCode(404); + secondaryEmail = Email.findById(secondaryEmail.id); + assertFalse(secondaryEmail.isMain()); + } + + @Test + @TestSecurity(user = "user_0") + public void setMainWithNonExistentEmail() { + given().post("invalid" + "/isMain").then().statusCode(404); + secondaryEmail = Email.findById(secondaryEmail.id); + assertFalse(secondaryEmail.isMain()); + } + + @BeforeEach + @Transactional + public void beforeEach_createEmail() { + secondaryEmail = new Email(); + secondaryEmail.user = User.findByUsername("user_0"); + secondaryEmail.email = "new_email@example.org"; + secondaryEmail.isVerified = true; + secondaryEmail.persist(); + unverifiedEmail = new Email(); + unverifiedEmail.user = secondaryEmail.user; + unverifiedEmail.email = "unverified@example.org"; + unverifiedEmail.isVerified = false; + unverifiedEmail.persist(); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTests.java new file mode 100644 index 0000000..3567a84 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTests.java @@ -0,0 +1,59 @@ +package app.fyreplace.api.testing.endpoints.tokens; + +import static app.fyreplace.api.testing.Assertions.assertSingleEmail; +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import app.fyreplace.api.data.NewTokenCreation; +import app.fyreplace.api.data.User; +import app.fyreplace.api.emails.UserConnectionEmail; +import app.fyreplace.api.endpoints.TokensEndpoint; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.ws.rs.core.MediaType; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(TokensEndpoint.class) +public final class CreateNewTests extends TransactionalTests { + @Test + public void createNewWithUsername() { + final var user = User.findByUsername("user_0"); + given().contentType(MediaType.APPLICATION_JSON) + .body(new NewTokenCreation(user.username)) + .post("new") + .then() + .statusCode(200); + assertSingleEmail(UserConnectionEmail.class, getMailsSentTo(user.mainEmail)); + } + + @Test + public void createNewWithEmail() { + final var user = User.findByUsername("user_0"); + given().contentType(MediaType.APPLICATION_JSON) + .body(new NewTokenCreation(user.mainEmail.email)) + .post("new") + .then() + .statusCode(200); + assertSingleEmail(UserConnectionEmail.class, getMailsSentTo(user.mainEmail)); + } + + @Test + public void createNewWithInvalidIdentifier() { + final var user = User.findByUsername("user_0"); + given().contentType(MediaType.APPLICATION_JSON) + .body(new NewTokenCreation("invalid")) + .post("new") + .then() + .statusCode(404); + assertEquals(0, getMailsSentTo(user.mainEmail).size()); + } + + @Test + public void createNewWithEmptyInput() { + final var user = User.findByUsername("user_0"); + given().contentType(MediaType.APPLICATION_JSON).post("new").then().statusCode(400); + assertEquals(0, getMailsSentTo(user.mainEmail).size()); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java new file mode 100644 index 0000000..1534189 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java @@ -0,0 +1,147 @@ +package app.fyreplace.api.testing.endpoints.tokens; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.isA; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import app.fyreplace.api.data.Email; +import app.fyreplace.api.data.RandomCode; +import app.fyreplace.api.data.TokenCreation; +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.TokensEndpoint; +import app.fyreplace.api.services.RandomService; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.core.MediaType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(TokensEndpoint.class) +public final class CreateTests extends TransactionalTests { + @Inject + RandomService randomService; + + private RandomCode normalUserRandomCode; + private RandomCode otherNormalUserRandomCode; + private RandomCode newUserRandomCode; + + @Test + public void createWithUsername() { + final var randomCodeCount = RandomCode.count(); + given().contentType(ContentType.JSON) + .body(new TokenCreation(normalUserRandomCode.email.user.username, normalUserRandomCode.code)) + .post() + .then() + .statusCode(201) + .body(isA(String.class)); + assertEquals(randomCodeCount - 1, RandomCode.count()); + } + + @Test + @Transactional + public void createWithNewUsername() { + final var randomCodeCount = RandomCode.count(); + assertFalse(newUserRandomCode.email.isVerified); + given().contentType(ContentType.JSON) + .body(new TokenCreation(newUserRandomCode.email.user.username, newUserRandomCode.code)) + .post() + .then() + .statusCode(201) + .contentType(MediaType.TEXT_PLAIN) + .body(isA(String.class)); + assertEquals(randomCodeCount - 1, RandomCode.count()); + final var email = Email.find("id", newUserRandomCode.email.id).firstResult(); + assertTrue(email.isVerified); + } + + @Test + public void createWithEmail() { + final var randomCodeCount = RandomCode.count(); + given().contentType(ContentType.JSON) + .body(new TokenCreation(normalUserRandomCode.email.email, normalUserRandomCode.code)) + .post() + .then() + .statusCode(201) + .contentType(MediaType.TEXT_PLAIN) + .body(isA(String.class)); + assertEquals(randomCodeCount - 1, RandomCode.count()); + } + + @Test + public void createWithNewEmail() { + final var randomCodeCount = RandomCode.count(); + assertFalse(newUserRandomCode.email.isVerified); + given().contentType(ContentType.JSON) + .body(new TokenCreation(newUserRandomCode.email.email, newUserRandomCode.code)) + .post() + .then() + .statusCode(201) + .contentType(MediaType.TEXT_PLAIN) + .body(isA(String.class)); + assertEquals(randomCodeCount - 1, RandomCode.count()); + final var email = Email.find("id", newUserRandomCode.email.id).firstResult(); + assertTrue(email.isVerified); + } + + @Test + public void createWithInvalidUsername() { + given().contentType(ContentType.JSON) + .body(new TokenCreation("bad", normalUserRandomCode.code)) + .post() + .then() + .statusCode(404); + } + + @Test + public void createWithInvalidCode() { + given().contentType(ContentType.JSON) + .body(new TokenCreation(normalUserRandomCode.email.user.username, "bad")) + .post() + .then() + .statusCode(404); + } + + @Test + public void createTwice() { + final var input = new TokenCreation(normalUserRandomCode.email.user.username, normalUserRandomCode.code); + given().contentType(ContentType.JSON).body(input).post().then().statusCode(201); + given().contentType(ContentType.JSON).body(input).post().then().statusCode(404); + } + + @Test + public void createWithOtherCode() { + given().contentType(ContentType.JSON) + .body(new TokenCreation(normalUserRandomCode.email.user.username, otherNormalUserRandomCode.code)) + .post() + .then() + .statusCode(404); + } + + @Test + public void createWithEmptyInput() { + given().contentType(ContentType.JSON).post().then().statusCode(400); + } + + @BeforeEach + @Transactional + public void beforeEach_createRandomCode() { + normalUserRandomCode = makeRandomCode("user_0"); + otherNormalUserRandomCode = makeRandomCode("user_1"); + newUserRandomCode = makeRandomCode("user_inactive_0"); + } + + private RandomCode makeRandomCode(final String username) { + final var code = new RandomCode(); + code.email = User.findByUsername(username).mainEmail; + code.code = randomService.generateCode(); + code.persist(); + return code; + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/RetrieveNewTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/RetrieveNewTests.java new file mode 100644 index 0000000..4e6ab8b --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/RetrieveNewTests.java @@ -0,0 +1,31 @@ +package app.fyreplace.api.testing.endpoints.tokens; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.isA; + +import app.fyreplace.api.endpoints.TokensEndpoint; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import jakarta.ws.rs.core.MediaType; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(TokensEndpoint.class) +public final class RetrieveNewTests extends TransactionalTests { + @Test + @TestSecurity(user = "user_0") + public void retrieveNew() { + given().get("new") + .then() + .statusCode(200) + .contentType(MediaType.TEXT_PLAIN) + .body(isA(String.class)); + } + + @Test + public void retrieveNewUnauthenticated() { + given().get("new").then().statusCode(401); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/BanTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/BanTests.java new file mode 100644 index 0000000..f06152d --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/BanTests.java @@ -0,0 +1,131 @@ +package app.fyreplace.api.testing.endpoints.users; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.UsersEndpoint; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(UsersEndpoint.class) +public final class BanTests extends TransactionalTests { + @Test + @TestSecurity(user = "user_0", roles = "ADMINISTRATOR") + @Transactional + public void banWithAdministrator() { + final var user = User.findByUsername("user_1"); + given().put(user.id + "/isBanned").then().statusCode(200); + user.refresh(); + assertTrue(user.isBanned); + assertEquals(User.BanCount.ONCE, user.banCount); + } + + @Test + @TestSecurity(user = "user_0", roles = "MODERATOR") + @Transactional + public void banWithModerator() { + final var user = User.findByUsername("user_1"); + given().put(user.id + "/isBanned").then().statusCode(200); + user.refresh(); + assertTrue(user.isBanned); + assertEquals(User.BanCount.ONCE, user.banCount); + } + + @Test + @TestSecurity(user = "user_0") + @Transactional + public void banWithUser() { + final var user = User.findByUsername("user_1"); + given().put(user.id + "/isBanned").then().statusCode(403); + user.refresh(); + assertFalse(user.isBanned); + assertEquals(User.BanCount.NEVER, user.banCount); + } + + @Test + @Transactional + public void banUnauthenticated() { + final var user = User.findByUsername("user_1"); + given().put(user.id + "/isBanned").then().statusCode(401); + user.refresh(); + assertFalse(user.isBanned); + assertEquals(User.BanCount.NEVER, user.banCount); + } + + @Test + @TestSecurity(user = "user_0", roles = "ADMINISTRATOR") + @Transactional + public void banTwiceWithAdministrator() { + final var user = User.findByUsername("user_2"); + given().put(user.id + "/isBanned").then().statusCode(200); + user.refresh(); + assertTrue(user.isBanned); + assertEquals(User.BanCount.ONE_TOO_MANY, user.banCount); + } + + @Test + @TestSecurity(user = "user_0", roles = "MODERATOR") + @Transactional + public void banTwiceWithModerator() { + final var user = User.findByUsername("user_2"); + given().put(user.id + "/isBanned").then().statusCode(200); + user.refresh(); + assertTrue(user.isBanned); + assertEquals(User.BanCount.ONE_TOO_MANY, user.banCount); + } + + @Test + @TestSecurity(user = "user_0") + @Transactional + public void banTwiceWithUser() { + final var user = User.findByUsername("user_2"); + given().put(user.id + "/isBanned").then().statusCode(403); + user.refresh(); + assertFalse(user.isBanned); + assertEquals(User.BanCount.ONCE, user.banCount); + } + + @Test + @Transactional + public void banTwiceUnauthenticated() { + final var user = User.findByUsername("user_2"); + given().put(user.id + "/isBanned").then().statusCode(401); + user.refresh(); + assertFalse(user.isBanned); + assertEquals(User.BanCount.ONCE, user.banCount); + } + + @Test + @TestSecurity(user = "user_0", roles = "ADMINISTRATOR") + @Transactional + public void banAlreadyBanned() { + final var user = User.findByUsername("user_3"); + given().put(user.id + "/isBanned").then().statusCode(200); + user.refresh(); + assertTrue(user.isBanned); + assertEquals(User.BanCount.ONCE, user.banCount); + } + + @BeforeEach + @Transactional + public void beforeEach_banUser() { + var user = User.findByUsername("user_2"); + user.isBanned = false; + user.banCount = User.BanCount.ONCE; + user.persist(); + + user = User.findByUsername("user_3"); + user.isBanned = true; + user.banCount = User.BanCount.ONCE; + user.persist(); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java new file mode 100644 index 0000000..5f8a03e --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java @@ -0,0 +1,55 @@ +package app.fyreplace.api.testing.endpoints.users; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import app.fyreplace.api.data.Block; +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.UsersEndpoint; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(UsersEndpoint.class) +public final class CreateBlockTests extends TransactionalTests { + @Test + @TestSecurity(user = "user_0") + public void createBlock() { + final var user = User.findByUsername("user_0"); + final var otherUser = User.findByUsername("user_2"); + assertEquals(0, Block.count("source = ?1 and target = ?2", user, otherUser)); + given().put(otherUser.id + "/isBlocked").then().statusCode(200); + assertEquals(1, Block.count("source = ?1 and target = ?2", user, otherUser)); + } + + @Test + @TestSecurity(user = "user_0") + public void createBlockTwice() { + final var user = User.findByUsername("user_0"); + final var otherUser = User.findByUsername("user_1"); + assertEquals(0, Block.count("source = ?1 and target = ?2", user, otherUser)); + given().put(otherUser.id + "/isBlocked").then().statusCode(200); + given().put(otherUser.id + "/isBlocked").then().statusCode(200); + assertEquals(1, Block.count("source = ?1 and target = ?2", user, otherUser)); + } + + @Test + @TestSecurity(user = "user_0") + public void createBlockWithInvalidUser() { + final var user = User.findByUsername("user_0"); + final var blockCount = Block.count("source", user); + given().put("invalid/isBlocked").then().statusCode(404); + assertEquals(blockCount, Block.count("source", user)); + } + + @Test + @TestSecurity(user = "user_0") + public void createBlockWithSelf() { + final var user = User.findByUsername("user_0"); + given().put(user.id + "/isBlocked").then().statusCode(403); + assertEquals(0, Block.count("source = ?1 and target = ?2", user, user)); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java new file mode 100644 index 0000000..d5d1aef --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java @@ -0,0 +1,151 @@ +package app.fyreplace.api.testing.endpoints.users; + +import static app.fyreplace.api.testing.Assertions.assertSingleEmail; +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import app.fyreplace.api.data.Email; +import app.fyreplace.api.data.User; +import app.fyreplace.api.data.UserCreation; +import app.fyreplace.api.emails.UserActivationEmail; +import app.fyreplace.api.endpoints.UsersEndpoint; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; +import jakarta.ws.rs.core.MediaType; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(UsersEndpoint.class) +public final class CreateTests extends TransactionalTests { + @Test + public void create() { + final var userCount = User.count(); + final var emailCount = Email.count(); + given().contentType(ContentType.JSON) + .body(new UserCreation("new@example.org", "new_user")) + .post() + .then() + .contentType(MediaType.APPLICATION_JSON) + .statusCode(201) + .body("username", equalTo("new_user")) + .body("rank", equalTo(User.Rank.CITIZEN.name())) + .body("isBanned", equalTo(false)) + .body("avatar", nullValue()) + .body("bio", equalTo("")) + .body("isBanned", equalTo(false)) + .body("dateCreated", notNullValue()); + assertEquals(userCount + 1, User.count()); + assertEquals(emailCount + 1, Email.count()); + final var user = User.findByUsername("new_user"); + assertNotNull(user); + assertEquals("new@example.org", user.mainEmail.email); + assertFalse(user.mainEmail.isVerified); + final var mails = getMailsSentTo(user.mainEmail); + assertSingleEmail(UserActivationEmail.class, mails); + } + + @Test + public void createWithInvalidUsername() { + final var userCount = User.count(); + given().contentType(ContentType.JSON) + .body(new UserCreation("new@example.org", "no spaces allowed")) + .post() + .then() + .contentType(MediaType.APPLICATION_JSON) + .statusCode(400); + assertEquals(userCount, User.count()); + } + + @Test + public void createWithUsernameTooShort() { + final var userCount = User.count(); + given().contentType(ContentType.JSON) + .body(new UserCreation("new@example.org", "a")) + .post() + .then() + .statusCode(400); + assertEquals(userCount, User.count()); + } + + @Test + public void createWithUsernameTooLong() { + final var userCount = User.count(); + given().contentType(ContentType.JSON) + .body(new UserCreation("new@example.org", "a".repeat(150))) + .post() + .then() + .statusCode(400); + assertEquals(userCount, User.count()); + } + + @Test + public void createWithForbiddenUsername() { + final var userCount = User.count(); + given().contentType(ContentType.JSON) + .body(new UserCreation("new@example.org", "admin")) + .post() + .then() + .statusCode(403); + assertEquals(userCount, User.count()); + } + + @Test + public void createWithInvalidEmail() { + final var userCount = User.count(); + given().contentType(ContentType.JSON) + .body(new UserCreation("not-an-email", "new_user")) + .post() + .then() + .statusCode(400); + assertEquals(userCount, User.count()); + } + + @Test + public void createWithEmptyEmail() { + final var userCount = User.count(); + given().contentType(ContentType.JSON) + .body(new UserCreation("", "new_user")) + .post() + .then() + .statusCode(400); + assertEquals(userCount, User.count()); + } + + @Test + public void createWithExistingUsername() { + final var userCount = User.count(); + final var existingUser = User.findAll().firstResult(); + given().contentType(ContentType.JSON) + .body(new UserCreation("email@example.org", existingUser.username)) + .post() + .then() + .statusCode(409); + assertEquals(userCount, User.count()); + } + + @Test + public void createWithExistingEmail() { + final var userCount = User.count(); + final var existingEmail = Email.findAll().firstResult(); + given().contentType(ContentType.JSON) + .body(new UserCreation(existingEmail.email, "new_user")) + .post() + .then() + .statusCode(409); + assertEquals(userCount, User.count()); + } + + @Test + public void createWithEmptyInput() { + final var userCount = User.count(); + given().contentType(ContentType.JSON).post().then().statusCode(400); + assertEquals(userCount, User.count()); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java new file mode 100644 index 0000000..51160f6 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java @@ -0,0 +1,58 @@ +package app.fyreplace.api.testing.endpoints.users; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import app.fyreplace.api.data.Block; +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.UsersEndpoint; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(UsersEndpoint.class) +public final class DeleteBlockTests extends TransactionalTests { + @Test + @TestSecurity(user = "user_0") + public void deleteBlock() { + final var user = User.findByUsername("user_0"); + final var otherUser = User.findByUsername("user_1"); + given().delete(otherUser.id + "/isBlocked").then().statusCode(204); + assertEquals(0, Block.count("source = ?1 and target = ?2", user, otherUser)); + } + + @Test + @TestSecurity(user = "user_0") + public void deleteBlockTwice() { + final var user = User.findByUsername("user_0"); + final var otherUser = User.findByUsername("user_1"); + given().delete(otherUser.id + "/isBlocked").then().statusCode(204); + given().delete(otherUser.id + "/isBlocked").then().statusCode(204); + assertEquals(0, Block.count("source = ?1 and target = ?2", user, otherUser)); + } + + @Test + @TestSecurity(user = "user_0") + public void deleteBlockWithInvalidUser() { + final var user = User.findByUsername("user_0"); + final var blockCount = Block.count("source", user); + given().delete("invalid/isBlocked").then().statusCode(404); + assertEquals(blockCount, Block.count("source", user)); + } + + @BeforeEach + @Transactional + public void beforeEach_createBlock() { + final var user = User.findByUsername("user_0"); + final var otherUser = User.findByUsername("user_1"); + final var block = new Block(); + block.source = user; + block.target = otherUser; + block.persist(); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeAvatarTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeAvatarTests.java new file mode 100644 index 0000000..73cda79 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeAvatarTests.java @@ -0,0 +1,47 @@ +package app.fyreplace.api.testing.endpoints.users; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import app.fyreplace.api.data.StoredFile; +import app.fyreplace.api.endpoints.UsersEndpoint; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import java.io.IOException; +import java.net.URL; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(UsersEndpoint.class) +public final class DeleteMeAvatarTests extends TransactionalTests { + @TestHTTPResource("image.jpeg") + URL jpeg; + + @Test + @TestSecurity(user = "user_0") + public void deleteMeAvatar() throws IOException { + try (final var stream = jpeg.openStream()) { + given().contentType(ContentType.BINARY) + .body(stream.readAllBytes()) + .put("me/avatar") + .then() + .statusCode(200); + } + + final var remoteFileCount = StoredFile.count(); + given().delete("me/avatar").then().statusCode(204); + assertEquals(remoteFileCount - 1, StoredFile.count()); + } + + @Test + @TestSecurity(user = "user_0") + public void deleteMeAvatarWithoutAvatar() throws IOException { + final var remoteFileCount = StoredFile.count(); + given().delete("me/avatar").then().statusCode(204); + assertEquals(remoteFileCount, StoredFile.count()); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeTests.java new file mode 100644 index 0000000..f4681dd --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeTests.java @@ -0,0 +1,33 @@ +package app.fyreplace.api.testing.endpoints.users; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.UsersEndpoint; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(UsersEndpoint.class) +public final class DeleteMeTests extends TransactionalTests { + @Test + @TestSecurity(user = "user_0") + public void deleteMe() { + final var userCount = User.count(); + given().delete("me").then().statusCode(204); + assertEquals(userCount - 1, User.count()); + assertNull(User.findByUsername("user_0")); + } + + @Test + public void deleteMeWithoutAuthentication() { + final var userCount = User.count(); + given().delete("me").then().statusCode(401); + assertEquals(userCount, User.count()); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java new file mode 100644 index 0000000..7170cad --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java @@ -0,0 +1,69 @@ +package app.fyreplace.api.testing.endpoints.users; + +import static io.restassured.RestAssured.given; +import static java.util.stream.IntStream.range; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.in; + +import app.fyreplace.api.data.Block; +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.UsersEndpoint; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(UsersEndpoint.class) +public final class ListBlockedTests extends TransactionalTests { + @ConfigProperty(name = "app.paging.size") + int pagingSize; + + @Test + @TestSecurity(user = "user_0") + public void list() { + final var user = User.findByUsername("user_0"); + final var response = given().queryParam("page", 0) + .get("blocked") + .then() + .statusCode(200) + .contentType(MediaType.APPLICATION_JSON) + .body("size()", equalTo(pagingSize)); + + range(0, pagingSize) + .forEach(i -> response.body( + "[" + i + "].username", + in(Block.stream("source", user) + .map(block -> block.target.username) + .toList()))); + } + + @Test + @TestSecurity(user = "user_0") + public void listOutOfBounds() { + given().queryParam("page", 500) + .get("blocked") + .then() + .statusCode(200) + .contentType(MediaType.APPLICATION_JSON) + .body(equalTo("[]")); + } + + @BeforeEach + @Transactional + public void beforeEach_createBlocks() { + final var user = User.findByUsername("user_0"); + range(10, 50).forEach(i -> { + final var otherUser = User.findByUsername("user_" + i); + final var block = new Block(); + block.source = user; + block.target = otherUser; + block.persist(); + }); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java new file mode 100644 index 0000000..e5d0d23 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java @@ -0,0 +1,41 @@ +package app.fyreplace.api.testing.endpoints.users; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.UsersEndpoint; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import jakarta.ws.rs.core.MediaType; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(UsersEndpoint.class) +public final class RetrieveMeTests extends TransactionalTests { + @Test + @TestSecurity(user = "user_10") + public void retrieveMe() { + final var user = User.findByUsername("user_10"); + given().get("/me") + .then() + .contentType(MediaType.APPLICATION_JSON) + .statusCode(200) + .body("id", equalTo(user.id.toString())) + .body("username", equalTo(user.username)) + .body("rank", equalTo(User.Rank.CITIZEN.name())) + .body("avatar", nullValue()) + .body("bio", equalTo("")) + .body("isBanned", equalTo(false)) + .body("dateCreated", notNullValue()); + } + + @Test + public void retrieveMeUnauthenticated() { + given().get("/me").then().statusCode(401); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java new file mode 100644 index 0000000..c96dda7 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java @@ -0,0 +1,42 @@ +package app.fyreplace.api.testing.endpoints.users; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.UsersEndpoint; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.ws.rs.core.MediaType; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@QuarkusTest +@TestHTTPEndpoint(UsersEndpoint.class) +public final class RetrieveTests extends TransactionalTests { + @ParameterizedTest + @ValueSource(strings = {"user_0", "user_12", "user_50"}) + public void retrieve(final String username) { + final var user = User.findByUsername(username); + given().get(user.id.toString()) + .then() + .contentType(MediaType.APPLICATION_JSON) + .statusCode(200) + .body("id", equalTo(user.id.toString())) + .body("username", equalTo(user.username)) + .body("rank", equalTo(User.Rank.CITIZEN.name())) + .body("avatar", nullValue()) + .body("bio", equalTo("")) + .body("isBanned", equalTo(false)) + .body("dateCreated", notNullValue()); + } + + @ParameterizedTest + @ValueSource(strings = {"nope", "fake", "@", "admin"}) + public void retrieveNonExistent(final String userId) { + given().get(userId).then().statusCode(404); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java new file mode 100644 index 0000000..49098b2 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java @@ -0,0 +1,109 @@ +package app.fyreplace.api.testing.endpoints.users; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.isA; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import app.fyreplace.api.data.StoredFile; +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.UsersEndpoint; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import jakarta.ws.rs.core.MediaType; +import java.io.IOException; +import java.net.URL; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@QuarkusTest +@TestHTTPEndpoint(UsersEndpoint.class) +public final class UpdateMeAvatarTests extends TransactionalTests { + @TestHTTPResource("image.jpeg") + URL jpeg; + + @TestHTTPResource("image.png") + URL png; + + @TestHTTPResource("image.webp") + URL webp; + + @TestHTTPResource("image.gif") + URL gif; + + @TestHTTPResource("image.txt") + URL text; + + @ParameterizedTest + @ValueSource(strings = {"jpeg", "png", "webp"}) + @TestSecurity(user = "user_0") + public void updateMeAvatar(final String fileType) throws IOException { + final var remoteFileCount = StoredFile.count(); + + try (final var stream = getUrl(fileType).openStream()) { + given().contentType(ContentType.BINARY) + .body(stream.readAllBytes()) + .put("me/avatar") + .then() + .contentType(MediaType.TEXT_PLAIN) + .statusCode(200) + .body(isA(String.class)); + } + + assertEquals(remoteFileCount + 1, StoredFile.count()); + final var user = User.findByUsername("user_0"); + assertNotNull(user.avatar); + } + + @ParameterizedTest + @ValueSource(strings = {"gif", "text"}) + @TestSecurity(user = "user_0") + public void updateMeAvatarWithInvalidType(final String fileType) throws IOException { + final var remoteFileCount = StoredFile.count(); + + try (final var stream = getUrl(fileType).openStream()) { + given().contentType(ContentType.BINARY) + .body(stream.readAllBytes()) + .put("me/avatar") + .then() + .statusCode(415); + } + + assertEquals(remoteFileCount, StoredFile.count()); + final var user = User.findByUsername("user_0"); + assertNull(user.avatar); + } + + @Test + @TestSecurity(user = "user_0") + public void updateMeAvatarWithEmptyBody() { + final var remoteFileCount = StoredFile.count(); + + given().contentType(ContentType.BINARY) + .body(new byte[0]) + .put("me/avatar") + .then() + .statusCode(415); + + assertEquals(remoteFileCount, StoredFile.count()); + final var user = User.findByUsername("user_0"); + assertNull(user.avatar); + } + + private URL getUrl(final String fileType) { + return switch (fileType) { + case "jpeg" -> jpeg; + case "png" -> png; + case "webp" -> webp; + case "gif" -> gif; + case "text" -> text; + default -> null; + }; + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeBioTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeBioTests.java new file mode 100644 index 0000000..a47a19a --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeBioTests.java @@ -0,0 +1,49 @@ +package app.fyreplace.api.testing.endpoints.users; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.UsersEndpoint; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@QuarkusTest +@TestHTTPEndpoint(UsersEndpoint.class) +public final class UpdateMeBioTests extends TransactionalTests { + @ParameterizedTest + @ValueSource(strings = {"Test", "Some random bio", ""}) + @TestSecurity(user = "user_0") + public void updateBio(final String bio) { + given().contentType(ContentType.TEXT).body(bio).put("me/bio").then().statusCode(200); + final var user = User.findByUsername("user_0"); + assertEquals(bio, user.bio); + } + + @Test + @TestSecurity(user = "user_0") + @Transactional + public void updateBioWithBioTooLong() { + final var user = User.findByUsername("user_0"); + final var bio = user.bio; + given().contentType(ContentType.TEXT) + .body("a".repeat(3001)) + .put("me/bio") + .then() + .statusCode(400); + user.refresh(); + assertEquals(bio, user.bio); + } + + @Test + public void UpdateMeBioWithoutAuthentication() { + given().contentType(ContentType.TEXT).body("Test").put("me/bio").then().statusCode(401); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/dev/RetrieveTokenTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/dev/RetrieveTokenTests.java new file mode 100644 index 0000000..b2005a7 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/dev/RetrieveTokenTests.java @@ -0,0 +1,18 @@ +package app.fyreplace.api.testing.endpoints.users.dev; + +import static io.restassured.RestAssured.given; + +import app.fyreplace.api.endpoints.UsersDevEndpoint; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(UsersDevEndpoint.class) +public final class RetrieveTokenTests extends TransactionalTests { + @Test + public void retrieveToken() { + given().get("user_0/token").then().statusCode(401); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldInactiveUsersTests.java b/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldInactiveUsersTests.java new file mode 100644 index 0000000..a5b0a6b --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldInactiveUsersTests.java @@ -0,0 +1,36 @@ +package app.fyreplace.api.testing.tasks.cleanup; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import app.fyreplace.api.data.User; +import app.fyreplace.api.tasks.CleanupTasks; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import java.time.Duration; +import java.time.Instant; +import org.junit.jupiter.api.Test; + +@QuarkusTest +public final class RemoveOldInactiveUsersTests extends TransactionalTests { + @Inject + CleanupTasks cleanupTasks; + + @Test + @Transactional + public void removeOldInactiveUsers() { + final var totalUserCount = User.count(); + final var inactiveUserCount = User.count("isActive = false"); + User.update("dateCreated = ?1 where isActive = false", Instant.now().minus(Duration.ofDays(2))); + cleanupTasks.removeOldInactiveUsers(); + assertEquals(totalUserCount - inactiveUserCount, User.count()); + } + + @Test + public void removeNewInactiveUsers() { + final var totalUserCount = User.count(); + cleanupTasks.removeOldInactiveUsers(); + assertEquals(totalUserCount, User.count()); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldRandomCodesTests.java b/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldRandomCodesTests.java new file mode 100644 index 0000000..88bb043 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldRandomCodesTests.java @@ -0,0 +1,44 @@ +package app.fyreplace.api.testing.tasks.cleanup; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import app.fyreplace.api.data.RandomCode; +import app.fyreplace.api.data.User; +import app.fyreplace.api.services.RandomService; +import app.fyreplace.api.tasks.CleanupTasks; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import java.time.Duration; +import java.time.Instant; +import org.junit.jupiter.api.Test; + +@QuarkusTest +public final class RemoveOldRandomCodesTests extends TransactionalTests { + @Inject + RandomService randomService; + + @Inject + CleanupTasks cleanupTasks; + + @Test + @Transactional + public void removeOldRandomCodes() { + final var randomCode = new RandomCode(); + randomCode.email = User.findByUsername("user_0").mainEmail; + randomCode.code = randomService.generateCode(); + randomCode.persist(); + final var randomCodeCount = RandomCode.count(); + RandomCode.update("dateCreated", Instant.now().minus(Duration.ofDays(2))); + cleanupTasks.removeOldRandomCodes(); + assertEquals(randomCodeCount - 1, RandomCode.count()); + } + + @Test + public void removeNewRandomCodes() { + final var randomCodeCount = RandomCode.count(); + cleanupTasks.removeOldRandomCodes(); + assertEquals(randomCodeCount, RandomCode.count()); + } +} diff --git a/src/test/resources/.gitkeep b/src/test/resources/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/test/resources/META-INF/resources/image.gif b/src/test/resources/META-INF/resources/image.gif new file mode 100644 index 0000000000000000000000000000000000000000..1f3b4ab7f2688cce0582f0d428415249cc0cbe91 GIT binary patch literal 392 zcmV;30eAjKNk%w1VE_RD0e}Gj|AIsRZa^XV1OW;F0RSuj000000RRC20{(=LsmtvT zqnxzbi?iOm`wxcVNS5Y_rs~SJ?hD8AOxN~}=lag~{tpZahs2`sh)gP%%%<}RjY_A~ zs`ZM^YPa03_X`e-$K-YS={|^`_I7nD%c!-#&xX9S( z_y`#(IZ0V*d5M{+xyjk-`3V{-I!an`U)E>J4;(@dyAW^yUW|_`wJW_ zJWO0{e2ko|yv*F}{0to}JxyJ0eT|*1z0KY2{S6*2K2Bb4evY25zRuq6{th26KTlt8 ze~+K9zt7+A{|_*rz<~q{8a#+Fp~8g>8#;UlF`~qY6f0W1h%uwaV2vC*di)47q{xvZ zOPV~1GNsCuEL*yK2{We5nKWzKyoocX&Ye7a`uqtrsL-KAiyA$OG^x_1Oq)7=3N@5KG8-`USxJi}Xp5PBF-@r)O&^mF)yReGJqYV|(Owo1*VEO{Wt~$(697{!7sCJm literal 0 HcmV?d00001 diff --git a/src/test/resources/META-INF/resources/image.txt b/src/test/resources/META-INF/resources/image.txt new file mode 100644 index 0000000..9d3f9d9 --- /dev/null +++ b/src/test/resources/META-INF/resources/image.txt @@ -0,0 +1 @@ +This is not an image file diff --git a/src/test/resources/META-INF/resources/image.webp b/src/test/resources/META-INF/resources/image.webp new file mode 100644 index 0000000000000000000000000000000000000000..f001df23eda8dfa2da4ea394f4307194ecc7cc7d GIT binary patch literal 38 scmWIYbaRtqU| Date: Fri, 28 Jul 2023 13:21:40 +0200 Subject: [PATCH 019/157] Update dependencies --- gradle.properties | 8 ++++---- ...oconfigure.spi.traces.ConfigurableSpanExporterProvider | 1 - .../runtime/src/main/resources/application.properties | 1 + 3 files changed, 5 insertions(+), 5 deletions(-) delete mode 100644 quarkus-sentry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider diff --git a/gradle.properties b/gradle.properties index 89abd4d..6d4598b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ -quarkusVersion=3.2.0.Final -quarkusAmazonVersion=2.4.0 +quarkusVersion=3.2.2.Final +quarkusAmazonVersion=2.4.2 gitPluginVersion=3.0.0 -spotlessPluginVersion=6.19.0 +spotlessPluginVersion=6.20.0 lombokPluginVersion=8.1.0 -sentryVersion=6.24.0 +sentryVersion=6.27.0 tikaVersion=2.8.0 diff --git a/quarkus-sentry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider b/quarkus-sentry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider deleted file mode 100644 index d8b7cec..0000000 --- a/quarkus-sentry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider +++ /dev/null @@ -1 +0,0 @@ -io.quarkus.opentelemetry.runtime.tracing.spi.SpanExporterCDIProvider diff --git a/quarkus-sentry/runtime/src/main/resources/application.properties b/quarkus-sentry/runtime/src/main/resources/application.properties index a22e06e..4e7f76c 100644 --- a/quarkus-sentry/runtime/src/main/resources/application.properties +++ b/quarkus-sentry/runtime/src/main/resources/application.properties @@ -1,2 +1,3 @@ quarkus.datasource.jdbc.telemetry=true quarkus.otel.propagators=tracecontext,baggage,sentry +quarkus.otel.traces.exporter=none From 132a4a9a8a9269d3dcd9bed98de142b049654c88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 30 Jul 2023 11:27:23 +0200 Subject: [PATCH 020/157] Add test environment variables --- .env-example | 6 +++--- .github/workflows/validation.yml | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.env-example b/.env-example index ea46b75..1539c8f 100644 --- a/.env-example +++ b/.env-example @@ -2,9 +2,9 @@ QUARKUS_DATASOURCE_USERNAME=fyreplace QUARKUS_DATASOURCE_PASSWORD=fyreplace QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://localhost/fyreplace -QUARKUS_SENTRY_DSN=https://sentry.example.org -QUARKUS_SENTRY_ENVIRONMENT=local -QUARKUS_SENTRY_TRACES_SAMPLE_RATE=1.0 +# QUARKUS_SENTRY_DSN=https://sentry.example.org +# QUARKUS_SENTRY_ENVIRONMENT=local +# QUARKUS_SENTRY_TRACES_SAMPLE_RATE=1.0 MP_JWT_VERIFY_PUBLICKEY=public-key-content SMALLRYE_JWT_SIGN_KEY=private-key-content diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 92b556c..75c4698 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -26,6 +26,7 @@ jobs: tests: name: Run tests runs-on: ubuntu-latest + environment: test steps: - name: Checkout repository @@ -41,3 +42,7 @@ jobs: run: | ./gradlew compileMjml ./gradlew test + env: + MP_JWT_VERIFY_PUBLICKEY: ${{ vars.MP_JWT_VERIFY_PUBLICKEY }} + SMALLRYE_JWT_SIGN_KEY: ${{ secrets.SMALLRYE_JWT_SIGN_KEY }} + APP_URL: ${{ vars.APP_URL }} From 84f540b3e957834573151f39b5fd7224adb8e5a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 30 Jul 2023 11:43:56 +0200 Subject: [PATCH 021/157] Add missing storage configuration --- .env-example | 16 +++++++++++++--- .github/workflows/validation.yml | 3 ++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.env-example b/.env-example index 1539c8f..929330b 100644 --- a/.env-example +++ b/.env-example @@ -2,11 +2,21 @@ QUARKUS_DATASOURCE_USERNAME=fyreplace QUARKUS_DATASOURCE_PASSWORD=fyreplace QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://localhost/fyreplace -# QUARKUS_SENTRY_DSN=https://sentry.example.org -# QUARKUS_SENTRY_ENVIRONMENT=local -# QUARKUS_SENTRY_TRACES_SAMPLE_RATE=1.0 +QUARKUS_S3_ENDPOINT_OVERRIDE=https://fra1.digitaloceanspaces.com +QUARKUS_S3_AWS_REGION=eu-west-1 +QUARKUS_S3_AWS_CREDENTIALS_TYPE=static +QUARKUS_S3_AWS_CREDENTIALS_STATIC_PROVIDER_ACCESS_KEY_ID=key-id +QUARKUS_S3_AWS_CREDENTIALS_STATIC_PROVIDER_SECRET_ACCESS_KEY=secret-key + +QUARKUS_SENTRY_DSN=https://sentry.example.org +QUARKUS_SENTRY_ENVIRONMENT=local +QUARKUS_SENTRY_TRACES_SAMPLE_RATE=1.0 MP_JWT_VERIFY_PUBLICKEY=public-key-content SMALLRYE_JWT_SIGN_KEY=private-key-content APP_URL=https://fyreplace.example.org + +APP_STORAGE_TYPE=s3 +APP_STORAGE_S3_BUCKET=fyreplace +APP_STORAGE_S3_CUSTOM_DOMAIN=storage.fyreplace.example.org diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 75c4698..75f71d9 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -45,4 +45,5 @@ jobs: env: MP_JWT_VERIFY_PUBLICKEY: ${{ vars.MP_JWT_VERIFY_PUBLICKEY }} SMALLRYE_JWT_SIGN_KEY: ${{ secrets.SMALLRYE_JWT_SIGN_KEY }} - APP_URL: ${{ vars.APP_URL }} + APP_URL: https://fyreplace.example.org + APP_STORAGE_LOCAL_PATH: /tmp/fyreplace/storage From 3746788a4c6c3d815388e489836abb6d45d89b93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 30 Jul 2023 18:48:30 +0200 Subject: [PATCH 022/157] Set Sentry release automatically --- .../api/sentry/config/SentryConfig.java | 6 ------ .../api/sentry/recorders/SentryRecorder.java | 19 ++++++++++++------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/config/SentryConfig.java b/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/config/SentryConfig.java index 9aece10..5a3c764 100644 --- a/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/config/SentryConfig.java +++ b/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/config/SentryConfig.java @@ -20,12 +20,6 @@ public final class SentryConfig { @ConfigItem public Optional environment = Optional.empty(); - /** - * Which code release the events will belong to. - */ - @ConfigItem - public Optional release = Optional.empty(); - /** * Percentage of performance events sent to Sentry. */ diff --git a/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/recorders/SentryRecorder.java b/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/recorders/SentryRecorder.java index dc80e5a..24e7f72 100644 --- a/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/recorders/SentryRecorder.java +++ b/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/recorders/SentryRecorder.java @@ -12,21 +12,26 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Handler; import java.util.logging.Level; +import org.eclipse.microprofile.config.ConfigProvider; @Recorder public class SentryRecorder { - public RuntimeValue> create(final SentryConfig config) { - if (config.dsn.isEmpty()) { + public RuntimeValue> create(final SentryConfig sentryConfig) { + if (sentryConfig.dsn.isEmpty()) { return new RuntimeValue<>(Optional.empty()); } + final var config = ConfigProvider.getConfig(); + final var appName = config.getValue("quarkus.application.name", String.class); + final var appVersion = config.getValue("quarkus.application.version", String.class); final var options = new AtomicReference(); + Sentry.init(it -> { - it.setDsn(config.dsn.get()); - config.environment.ifPresent(it::setEnvironment); - config.release.ifPresent(it::setRelease); - config.tracesSampleRate.ifPresent(it::setTracesSampleRate); - it.addInAppInclude("app.fyreplace"); + sentryConfig.dsn.ifPresent(it::setDsn); + sentryConfig.environment.ifPresent(it::setEnvironment); + sentryConfig.tracesSampleRate.ifPresent(it::setTracesSampleRate); + it.setRelease(appName + '@' + appVersion); + it.addInAppInclude("app.fyreplace.api"); it.setInstrumenter(Instrumenter.OTEL); it.addEventProcessor(new OpenTelemetryLinkErrorEventProcessor()); options.set(it); From 5591e85950fd2638448876df4fee87d195e18b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 30 Jul 2023 21:19:29 +0200 Subject: [PATCH 023/157] Use Sentry BOM --- quarkus-sentry/build.gradle | 3 ++- quarkus-sentry/runtime/build.gradle | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/quarkus-sentry/build.gradle b/quarkus-sentry/build.gradle index 865c24b..90d0581 100644 --- a/quarkus-sentry/build.gradle +++ b/quarkus-sentry/build.gradle @@ -8,8 +8,9 @@ subprojects { version = gitVersion() dependencies { implementation(enforcedPlatform("io.quarkus:quarkus-bom:${quarkusVersion}")) + implementation(enforcedPlatform("io.sentry:sentry-bom:${sentryVersion}")) implementation("io.quarkus:quarkus-opentelemetry") - implementation("io.sentry:sentry-opentelemetry-core:${sentryVersion}") + implementation("io.sentry:sentry-opentelemetry-core") annotationProcessor("io.quarkus:quarkus-extension-processor") } } diff --git a/quarkus-sentry/runtime/build.gradle b/quarkus-sentry/runtime/build.gradle index 8c4bae8..09d5c48 100644 --- a/quarkus-sentry/runtime/build.gradle +++ b/quarkus-sentry/runtime/build.gradle @@ -9,7 +9,7 @@ repositories { dependencies { implementation("io.quarkus:quarkus-core") implementation("io.quarkus:quarkus-arc") - implementation("io.sentry:sentry-jul:${sentryVersion}") + implementation("io.sentry:sentry-jul") implementation("io.opentelemetry.instrumentation:opentelemetry-jdbc") } From fc3d1035017c85bdbc6c3123de26e39aa7bcfca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Tue, 1 Aug 2023 21:33:01 +0200 Subject: [PATCH 024/157] Remove redundant id --- src/main/java/app/fyreplace/api/data/Block.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/main/java/app/fyreplace/api/data/Block.java b/src/main/java/app/fyreplace/api/data/Block.java index 868ca50..8caec9a 100644 --- a/src/main/java/app/fyreplace/api/data/Block.java +++ b/src/main/java/app/fyreplace/api/data/Block.java @@ -1,21 +1,14 @@ package app.fyreplace.api.data; import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; -import java.util.UUID; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; @Entity @Table(name = "blocks") public class Block extends EntityBase { - @Id - @GeneratedValue - public UUID id; - @ManyToOne(optional = false) @OnDelete(action = OnDeleteAction.CASCADE) public User source; From af66ba818abfd941ea06c1846df2c0d20617fb10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Wed, 2 Aug 2023 21:16:41 +0200 Subject: [PATCH 025/157] Use common TimestampedEntityBase --- .../java/app/fyreplace/api/data/RandomCode.java | 9 +-------- .../fyreplace/api/data/TimestampedEntityBase.java | 14 ++++++++++++++ src/main/java/app/fyreplace/api/data/User.java | 8 +------- 3 files changed, 16 insertions(+), 15 deletions(-) create mode 100644 src/main/java/app/fyreplace/api/data/TimestampedEntityBase.java diff --git a/src/main/java/app/fyreplace/api/data/RandomCode.java b/src/main/java/app/fyreplace/api/data/RandomCode.java index b9b09d8..c17d348 100644 --- a/src/main/java/app/fyreplace/api/data/RandomCode.java +++ b/src/main/java/app/fyreplace/api/data/RandomCode.java @@ -4,15 +4,12 @@ import jakarta.persistence.Entity; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; -import java.time.Instant; -import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; -import org.hibernate.annotations.SourceType; @Entity @Table(name = "random_codes") -public class RandomCode extends EntityBase { +public class RandomCode extends TimestampedEntityBase { @ManyToOne(optional = false) @OnDelete(action = OnDeleteAction.CASCADE) public Email email; @@ -20,10 +17,6 @@ public class RandomCode extends EntityBase { @Column(nullable = false) public String code; - @Column(nullable = false) - @CreationTimestamp(source = SourceType.DB) - public Instant dateCreated; - @Override public String toString() { return code; diff --git a/src/main/java/app/fyreplace/api/data/TimestampedEntityBase.java b/src/main/java/app/fyreplace/api/data/TimestampedEntityBase.java new file mode 100644 index 0000000..ce4357e --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/TimestampedEntityBase.java @@ -0,0 +1,14 @@ +package app.fyreplace.api.data; + +import jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; +import java.time.Instant; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.SourceType; + +@MappedSuperclass +public abstract class TimestampedEntityBase extends EntityBase { + @Column(nullable = false) + @CreationTimestamp(source = SourceType.DB) + public Instant dateCreated; +} diff --git a/src/main/java/app/fyreplace/api/data/User.java b/src/main/java/app/fyreplace/api/data/User.java index 03167f1..4b3e7a6 100644 --- a/src/main/java/app/fyreplace/api/data/User.java +++ b/src/main/java/app/fyreplace/api/data/User.java @@ -14,14 +14,12 @@ import java.util.UUID; import java.util.stream.Collectors; import org.eclipse.microprofile.openapi.annotations.media.Schema; -import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; -import org.hibernate.annotations.SourceType; @Entity @Table(name = "users") -public class User extends EntityBase { +public class User extends TimestampedEntityBase { public static final Set forbiddenUsernames = new HashSet(Arrays.asList( "admin", "admins", @@ -98,10 +96,6 @@ public class User extends EntityBase { @JsonIgnore public Instant dateBanEnd; - @Column(nullable = false) - @CreationTimestamp(source = SourceType.DB) - public Instant dateCreated; - public Set getGroups() { return Arrays.stream(Rank.values()) .filter(group -> group.ordinal() <= rank.ordinal()) From 55a175d5dc1e120237b9aace0aeaa86bc2c7a99b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Wed, 2 Aug 2023 21:21:19 +0200 Subject: [PATCH 026/157] Code cleanup --- src/main/java/app/fyreplace/api/data/dev/DataSeeder.java | 2 +- .../{UsersDevEndpoint.java => DevUsersEndpoint.java} | 2 +- src/main/resources/application.yaml | 1 + .../api/testing/endpoints/users/dev/RetrieveTokenTests.java | 4 ++-- 4 files changed, 5 insertions(+), 4 deletions(-) rename src/main/java/app/fyreplace/api/endpoints/{UsersDevEndpoint.java => DevUsersEndpoint.java} (95%) diff --git a/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java b/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java index fa8f37b..cfd5dc9 100644 --- a/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java +++ b/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java @@ -18,7 +18,7 @@ @ApplicationScoped public class DataSeeder { @ConfigProperty(name = "app.use-example-data") - private boolean useExampleData; + boolean useExampleData; public void onStartup(@Observes final StartupEvent event) { if (shouldUseExampleData()) { diff --git a/src/main/java/app/fyreplace/api/endpoints/UsersDevEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/DevUsersEndpoint.java similarity index 95% rename from src/main/java/app/fyreplace/api/endpoints/UsersDevEndpoint.java rename to src/main/java/app/fyreplace/api/endpoints/DevUsersEndpoint.java index 2de2b60..d5e6169 100644 --- a/src/main/java/app/fyreplace/api/endpoints/UsersDevEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/DevUsersEndpoint.java @@ -12,7 +12,7 @@ import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; @Path("dev/users") -public final class UsersDevEndpoint { +public final class DevUsersEndpoint { @Inject JwtService jwtService; diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 88d1177..df47254 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -37,6 +37,7 @@ app: url: "" name: Fyreplace use-example-data: false + paging: size: 12 diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/dev/RetrieveTokenTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/dev/RetrieveTokenTests.java index b2005a7..4dab984 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/dev/RetrieveTokenTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/dev/RetrieveTokenTests.java @@ -2,14 +2,14 @@ import static io.restassured.RestAssured.given; -import app.fyreplace.api.endpoints.UsersDevEndpoint; +import app.fyreplace.api.endpoints.DevUsersEndpoint; import app.fyreplace.api.testing.TransactionalTests; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import org.junit.jupiter.api.Test; @QuarkusTest -@TestHTTPEndpoint(UsersDevEndpoint.class) +@TestHTTPEndpoint(DevUsersEndpoint.class) public final class RetrieveTokenTests extends TransactionalTests { @Test public void retrieveToken() { From 18b3ce78b8acaa1469fb20ab28cdcade84aa93ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 13 Aug 2023 11:31:46 +0200 Subject: [PATCH 027/157] Ignore autogenerated docs --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 216783d..e2b5781 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,6 @@ nb-configuration.xml # Plugin directory /.quarkus/cli/plugins/ + +# AsciiDoc +target From 187ca1bdbb3ee1638854dc5c4e3aea2dc0233853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 13 Aug 2023 12:59:00 +0200 Subject: [PATCH 028/157] Factorize security contexts --- .../api/endpoints/EmailsEndpoint.java | 13 +++++++----- .../api/endpoints/TokensEndpoint.java | 5 ++++- .../api/endpoints/UsersEndpoint.java | 21 +++++++++++-------- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java index 3c99ed1..ee4fde9 100644 --- a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java @@ -42,11 +42,14 @@ public final class EmailsEndpoint { @Inject EmailVerificationEmail emailVerificationEmail; + @Context + SecurityContext context; + @GET @Authenticated @APIResponse(responseCode = "200") @APIResponse(responseCode = "401") - public List list(@Context final SecurityContext context, @QueryParam("page") final int page) { + public List list(@QueryParam("page") final int page) { final var user = User.getFromSecurityContext(context); return Email.find("user", Sort.by("email"), user).page(page, pagingSize).list(); } @@ -61,7 +64,7 @@ public List list(@Context final SecurityContext context, @QueryParam("pag @APIResponse(responseCode = "400") @APIResponse(responseCode = "401") @APIResponse(responseCode = "409") - public Response create(@Context final SecurityContext context, @Valid @NotNull final EmailCreation input) { + public Response create(@Valid @NotNull final EmailCreation input) { if (Email.count("email", input.email()) > 0) { throw new ConflictException("email_taken"); } @@ -82,7 +85,7 @@ public Response create(@Context final SecurityContext context, @Valid @NotNull f @APIResponse(responseCode = "401") @APIResponse(responseCode = "403") @APIResponse(responseCode = "404") - public void delete(@Context final SecurityContext context, @PathParam("id") final UUID id) { + public void delete(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context); final var email = Email.find("user = ?1 and id = ?2", user, id).firstResult(); @@ -102,7 +105,7 @@ public void delete(@Context final SecurityContext context, @PathParam("id") fina @APIResponse(responseCode = "200") @APIResponse(responseCode = "403") @APIResponse(responseCode = "404") - public Response setMain(@Context final SecurityContext context, @PathParam("id") final UUID id) { + public Response setMain(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context); final var email = Email.find("user = ?1 and id = ?2", user, id).firstResult(); @@ -125,7 +128,7 @@ public Response setMain(@Context final SecurityContext context, @PathParam("id") @APIResponse(responseCode = "400") @APIResponse(responseCode = "401") @APIResponse(responseCode = "404") - public Response activate(@Context final SecurityContext context, @NotNull @Valid final EmailActivation input) { + public Response activate(@NotNull @Valid final EmailActivation input) { var email = Email.find("email", input.email()).firstResult(); final var randomCode = RandomCode.find("email = ?1 and code = ?2", email, input.code()) .firstResult(); diff --git a/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java index 070f9d2..f379819 100644 --- a/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java @@ -33,6 +33,9 @@ public final class TokensEndpoint { @Inject UserConnectionEmail userConnectionEmail; + @Context + SecurityContext context; + @POST @Transactional @APIResponse( @@ -62,7 +65,7 @@ public Response create(@Valid @NotNull final TokenCreation input) { responseCode = "200", content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) @APIResponse(responseCode = "401") - public String retrieveNew(@Context final SecurityContext context) { + public String retrieveNew() { return jwtService.makeJwt(User.getFromSecurityContext(context)); } diff --git a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java index 0544da8..5217a63 100644 --- a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java @@ -55,6 +55,9 @@ public final class UsersEndpoint { @Inject UserActivationEmail userActivationEmail; + @Context + SecurityContext context; + @POST @Transactional @APIResponse( @@ -111,7 +114,7 @@ public User retrieve(@PathParam("id") final UUID id) { @APIResponse(responseCode = "401") @APIResponse(responseCode = "403") @APIResponse(responseCode = "404") - public Response createBlock(@Context final SecurityContext context, @PathParam("id") final UUID id) { + public Response createBlock(@PathParam("id") final UUID id) { final var source = User.getFromSecurityContext(context); final var target = User.findById(id); @@ -136,7 +139,7 @@ public Response createBlock(@Context final SecurityContext context, @PathParam(" @APIResponse(responseCode = "204") @APIResponse(responseCode = "401") @APIResponse(responseCode = "404") - public void deleteBlock(@Context final SecurityContext context, @PathParam("id") final UUID id) { + public void deleteBlock(@PathParam("id") final UUID id) { final var source = User.getFromSecurityContext(context); final var target = User.findById(id); @@ -155,7 +158,7 @@ public void deleteBlock(@Context final SecurityContext context, @PathParam("id") @APIResponse(responseCode = "401") @APIResponse(responseCode = "403") @APIResponse(responseCode = "404") - public Response ban(@Context final SecurityContext context, @PathParam("id") final UUID id) { + public Response ban(@PathParam("id") final UUID id) { final var user = User.findById(id, LockModeType.PESSIMISTIC_WRITE); if (user == null) { @@ -182,7 +185,7 @@ public Response ban(@Context final SecurityContext context, @PathParam("id") fin responseCode = "200", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = User.class))) @APIResponse(responseCode = "401") - public User retrieveMe(@Context final SecurityContext context) { + public User retrieveMe() { return retrieve(User.getFromSecurityContext(context).id); } @@ -196,7 +199,7 @@ public User retrieveMe(@Context final SecurityContext context) { content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = User.class))) @APIResponse(responseCode = "400") @APIResponse(responseCode = "401") - public String updateMeBio(@Context final SecurityContext context, @NotNull @Length(max = 3000) final String input) { + public String updateMeBio(@NotNull @Length(max = 3000) final String input) { final var user = User.getFromSecurityContext(context, LockModeType.PESSIMISTIC_READ); user.bio = input; user.persist(); @@ -214,7 +217,7 @@ public String updateMeBio(@Context final SecurityContext context, @NotNull @Leng @APIResponse(responseCode = "401") @APIResponse(responseCode = "413") @APIResponse(responseCode = "415") - public String updateMeAvatar(@Context final SecurityContext context, final byte[] input) throws IOException { + public String updateMeAvatar(final byte[] input) throws IOException { mimeTypeService.validate(input, KnownMimeTypes.IMAGE); final var user = User.getFromSecurityContext(context, LockModeType.PESSIMISTIC_WRITE); @@ -234,7 +237,7 @@ public String updateMeAvatar(@Context final SecurityContext context, final byte[ @Transactional @APIResponse(responseCode = "204") @APIResponse(responseCode = "401") - public void deleteMeAvatar(@Context final SecurityContext context) { + public void deleteMeAvatar() { final var user = User.getFromSecurityContext(context, LockModeType.PESSIMISTIC_WRITE); if (user.avatar != null) { @@ -250,7 +253,7 @@ public void deleteMeAvatar(@Context final SecurityContext context) { @Transactional @APIResponse(responseCode = "204") @APIResponse(responseCode = "401") - public void deleteMe(@Context final SecurityContext context) { + public void deleteMe() { User.delete("username", context.getUserPrincipal().getName()); } @@ -259,7 +262,7 @@ public void deleteMe(@Context final SecurityContext context) { @Authenticated @APIResponse(responseCode = "200") @APIResponse(responseCode = "401") - public List listBlocked(@Context final SecurityContext context, @QueryParam("page") final int page) { + public List listBlocked(@QueryParam("page") final int page) { return Block.find("source", Sort.by("id"), User.getFromSecurityContext(context)) .page(page, pagingSize) .stream() From c2924c9f7f902e654179f549e935f224c4720985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 4 Aug 2023 12:58:15 +0200 Subject: [PATCH 029/157] Better constrain blocks --- src/main/java/app/fyreplace/api/data/Block.java | 3 ++- src/main/java/app/fyreplace/api/data/User.java | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/app/fyreplace/api/data/Block.java b/src/main/java/app/fyreplace/api/data/Block.java index 8caec9a..070562a 100644 --- a/src/main/java/app/fyreplace/api/data/Block.java +++ b/src/main/java/app/fyreplace/api/data/Block.java @@ -3,11 +3,12 @@ import jakarta.persistence.Entity; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; @Entity -@Table(name = "blocks") +@Table(name = "blocks", uniqueConstraints = @UniqueConstraint(columnNames = {"source_id", "target_id"})) public class Block extends EntityBase { @ManyToOne(optional = false) @OnDelete(action = OnDeleteAction.CASCADE) diff --git a/src/main/java/app/fyreplace/api/data/User.java b/src/main/java/app/fyreplace/api/data/User.java index 4b3e7a6..bd0531a 100644 --- a/src/main/java/app/fyreplace/api/data/User.java +++ b/src/main/java/app/fyreplace/api/data/User.java @@ -96,6 +96,7 @@ public class User extends TimestampedEntityBase { @JsonIgnore public Instant dateBanEnd; + @JsonIgnore public Set getGroups() { return Arrays.stream(Rank.values()) .filter(group -> group.ordinal() <= rank.ordinal()) From 71f7468192ed5c6dcd1efa97839ff095731c6bec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Mon, 14 Aug 2023 11:27:01 +0200 Subject: [PATCH 030/157] Fix OpenAPI schema --- src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java index 5217a63..5f4650e 100644 --- a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java @@ -196,7 +196,7 @@ public User retrieveMe() { @Consumes(MediaType.TEXT_PLAIN) @APIResponse( responseCode = "200", - content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = User.class))) + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) @APIResponse(responseCode = "400") @APIResponse(responseCode = "401") public String updateMeBio(@NotNull @Length(max = 3000) final String input) { @@ -213,7 +213,7 @@ public String updateMeBio(@NotNull @Length(max = 3000) final String input) { @Consumes(MediaType.APPLICATION_OCTET_STREAM) @APIResponse( responseCode = "200", - content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = User.class))) + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) @APIResponse(responseCode = "401") @APIResponse(responseCode = "413") @APIResponse(responseCode = "415") From 2b327b52a46840fc17268a95a0d6dfc52a4cc5a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Mon, 14 Aug 2023 11:29:10 +0200 Subject: [PATCH 031/157] Code cleanup --- .../java/app/fyreplace/api/endpoints/TokensEndpoint.java | 6 +++--- .../fyreplace/api/testing/endpoints/users/CreateTests.java | 4 ++-- .../api/testing/endpoints/users/RetrieveMeTests.java | 4 ++-- .../api/testing/endpoints/users/RetrieveTests.java | 4 ++-- .../api/testing/endpoints/users/UpdateMeAvatarTests.java | 6 +----- 5 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java index f379819..de402d9 100644 --- a/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java @@ -90,10 +90,10 @@ private Email getEmail(final String identifier) { final var user = User.findByUsername(identifier); - if (user != null) { + if (user == null) { + throw new NotFoundException(); + } else { return user.mainEmail; } - - throw new NotFoundException(); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java index d5d1aef..03cc37c 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java @@ -34,13 +34,13 @@ public void create() { .then() .contentType(MediaType.APPLICATION_JSON) .statusCode(201) + .body("dateCreated", notNullValue()) .body("username", equalTo("new_user")) .body("rank", equalTo(User.Rank.CITIZEN.name())) .body("isBanned", equalTo(false)) .body("avatar", nullValue()) .body("bio", equalTo("")) - .body("isBanned", equalTo(false)) - .body("dateCreated", notNullValue()); + .body("isBanned", equalTo(false)); assertEquals(userCount + 1, User.count()); assertEquals(emailCount + 1, Email.count()); final var user = User.findByUsername("new_user"); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java index e5d0d23..918d1bb 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java @@ -26,12 +26,12 @@ public void retrieveMe() { .contentType(MediaType.APPLICATION_JSON) .statusCode(200) .body("id", equalTo(user.id.toString())) + .body("dateCreated", notNullValue()) .body("username", equalTo(user.username)) .body("rank", equalTo(User.Rank.CITIZEN.name())) .body("avatar", nullValue()) .body("bio", equalTo("")) - .body("isBanned", equalTo(false)) - .body("dateCreated", notNullValue()); + .body("isBanned", equalTo(false)); } @Test diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java index c96dda7..04f86c3 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java @@ -26,12 +26,12 @@ public void retrieve(final String username) { .contentType(MediaType.APPLICATION_JSON) .statusCode(200) .body("id", equalTo(user.id.toString())) + .body("dateCreated", notNullValue()) .body("username", equalTo(user.username)) .body("rank", equalTo(User.Rank.CITIZEN.name())) .body("avatar", nullValue()) .body("bio", equalTo("")) - .body("isBanned", equalTo(false)) - .body("dateCreated", notNullValue()); + .body("isBanned", equalTo(false)); } @ParameterizedTest diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java index 49098b2..43e0b09 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java @@ -85,11 +85,7 @@ public void updateMeAvatarWithInvalidType(final String fileType) throws IOExcept public void updateMeAvatarWithEmptyBody() { final var remoteFileCount = StoredFile.count(); - given().contentType(ContentType.BINARY) - .body(new byte[0]) - .put("me/avatar") - .then() - .statusCode(415); + given().contentType(ContentType.BINARY).put("me/avatar").then().statusCode(415); assertEquals(remoteFileCount, StoredFile.count()); final var user = User.findByUsername("user_0"); From 93a6ac3368343b5777cf510e5bd7a8b140a49aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Mon, 14 Aug 2023 11:29:19 +0200 Subject: [PATCH 032/157] Update dependencies --- gradle.properties | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 6d4598b..b2b4a2e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ -quarkusVersion=3.2.2.Final -quarkusAmazonVersion=2.4.2 +quarkusVersion=3.2.4.Final +quarkusAmazonVersion=2.4.3 gitPluginVersion=3.0.0 spotlessPluginVersion=6.20.0 -lombokPluginVersion=8.1.0 -sentryVersion=6.27.0 +lombokPluginVersion=8.2.2 +sentryVersion=6.28.0 tikaVersion=2.8.0 From fc208ee2b49398b29f04507ff02ec5708fbdde66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 19 Aug 2023 12:46:05 +0200 Subject: [PATCH 033/157] Code cleanup --- .../app/fyreplace/api/data/StoredFile.java | 2 ++ .../java/app/fyreplace/api/data/User.java | 2 +- .../endpoints/emails/ActivateTests.java | 15 +++++-------- .../testing/endpoints/emails/CreateTests.java | 22 +++++++++---------- .../testing/endpoints/emails/ListTests.java | 6 ++--- .../endpoints/tokens/CreateNewTests.java | 10 ++++----- .../testing/endpoints/tokens/CreateTests.java | 7 +++--- .../endpoints/tokens/RetrieveNewTests.java | 8 ++----- .../testing/endpoints/users/CreateTests.java | 5 ++--- .../endpoints/users/ListBlockedTests.java | 6 ++--- .../endpoints/users/RetrieveMeTests.java | 4 ++-- .../endpoints/users/RetrieveTests.java | 4 ++-- .../endpoints/users/UpdateMeAvatarTests.java | 7 ++---- 13 files changed, 44 insertions(+), 54 deletions(-) diff --git a/src/main/java/app/fyreplace/api/data/StoredFile.java b/src/main/java/app/fyreplace/api/data/StoredFile.java index 50412ee..771269f 100644 --- a/src/main/java/app/fyreplace/api/data/StoredFile.java +++ b/src/main/java/app/fyreplace/api/data/StoredFile.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import io.quarkus.arc.Arc; +import jakarta.annotation.Nullable; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.PostPersist; @@ -24,6 +25,7 @@ public class StoredFile extends EntityBase { public String path; @Transient + @Nullable private byte[] data; public StoredFile() { diff --git a/src/main/java/app/fyreplace/api/data/User.java b/src/main/java/app/fyreplace/api/data/User.java index bd0531a..15d9e86 100644 --- a/src/main/java/app/fyreplace/api/data/User.java +++ b/src/main/java/app/fyreplace/api/data/User.java @@ -110,7 +110,7 @@ public Profile getProfile() { } @PostRemove - final void preDestroy() { + final void postRemove() { if (avatar != null) { avatar.delete(); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java index 39fbd55..1586c6b 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java @@ -13,9 +13,9 @@ import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; import jakarta.inject.Inject; import jakarta.transaction.Transactional; -import jakarta.ws.rs.core.MediaType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -32,7 +32,7 @@ public final class ActivateTests extends TransactionalTests { @Test @TestSecurity(user = "user_0") public void activate() { - given().contentType(MediaType.APPLICATION_JSON) + given().contentType(ContentType.JSON) .body(new EmailActivation(newEmail.email, randomCode.code)) .post("activation") .then() @@ -43,7 +43,7 @@ public void activate() { @Test @TestSecurity(user = "user_0") public void activateWithInvalidEmail() { - given().contentType(MediaType.APPLICATION_JSON) + given().contentType(ContentType.JSON) .body(new EmailActivation("invalid", randomCode.code)) .post("activation") .then() @@ -55,7 +55,7 @@ public void activateWithInvalidEmail() { @TestSecurity(user = "user_0") public void activateWithOtherEmail() { final var otherUser = User.findByUsername("user_1"); - given().contentType(MediaType.APPLICATION_JSON) + given().contentType(ContentType.JSON) .body(new EmailActivation(otherUser.mainEmail.email, randomCode.code)) .post("activation") .then() @@ -66,7 +66,7 @@ public void activateWithOtherEmail() { @Test @TestSecurity(user = "user_0") public void activateWithInvalidCode() { - given().contentType(MediaType.APPLICATION_JSON) + given().contentType(ContentType.JSON) .body(new EmailActivation(newEmail.email, "invalid")) .post("activation") .then() @@ -77,10 +77,7 @@ public void activateWithInvalidCode() { @Test @TestSecurity(user = "user_0") public void activateWithEmptyInput() { - given().contentType(MediaType.APPLICATION_JSON) - .post("activation") - .then() - .statusCode(400); + given().contentType(ContentType.JSON).post("activation").then().statusCode(400); assertEquals(1, RandomCode.count("id", randomCode.id)); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java index a082190..9d1f401 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java @@ -12,7 +12,7 @@ import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; -import jakarta.ws.rs.core.MediaType; +import io.restassured.http.ContentType; import org.junit.jupiter.api.Test; @QuarkusTest @@ -23,11 +23,11 @@ public final class CreateTests extends TransactionalTests { public void create() { final var email = "some_new_email@example.org"; final var emailCount = Email.count(); - given().contentType(MediaType.APPLICATION_JSON) + given().contentType(ContentType.JSON) .body(new EmailCreation(email)) .post() .then() - .contentType(MediaType.APPLICATION_JSON) + .contentType(ContentType.JSON) .statusCode(201) .body("email", equalTo(email)) .body("isVerified", equalTo(false)) @@ -39,11 +39,11 @@ public void create() { @TestSecurity(user = "user_0") public void createWithInvalidEmail() { final var emailCount = Email.count(); - given().contentType(MediaType.APPLICATION_JSON) + given().contentType(ContentType.JSON) .body(new EmailCreation("invalid")) .post() .then() - .contentType(MediaType.APPLICATION_JSON) + .contentType(ContentType.JSON) .statusCode(400); assertEquals(emailCount, Email.count()); } @@ -52,11 +52,11 @@ public void createWithInvalidEmail() { @TestSecurity(user = "user_0") public void createWithEmptyEmail() { final var emailCount = Email.count(); - given().contentType(MediaType.APPLICATION_JSON) + given().contentType(ContentType.JSON) .body(new EmailCreation("")) .post() .then() - .contentType(MediaType.APPLICATION_JSON) + .contentType(ContentType.JSON) .statusCode(400); assertEquals(emailCount, Email.count()); } @@ -66,11 +66,11 @@ public void createWithEmptyEmail() { public void createWithExistingEmail() { final var existingUser = User.findByUsername("user_1"); final var emailCount = Email.count(); - given().contentType(MediaType.APPLICATION_JSON) + given().contentType(ContentType.JSON) .body(new EmailCreation(existingUser.mainEmail.email)) .post() .then() - .contentType(MediaType.APPLICATION_JSON) + .contentType(ContentType.JSON) .statusCode(409); assertEquals(emailCount, Email.count()); } @@ -79,10 +79,10 @@ public void createWithExistingEmail() { @TestSecurity(user = "user_0") public void createWithEmptyInput() { final var emailCount = Email.count(); - given().contentType(MediaType.APPLICATION_JSON) + given().contentType(ContentType.JSON) .post() .then() - .contentType(MediaType.APPLICATION_JSON) + .contentType(ContentType.JSON) .statusCode(400); assertEquals(emailCount, Email.count()); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java index 6f47256..1864a29 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java @@ -12,8 +12,8 @@ import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; import jakarta.transaction.Transactional; -import jakarta.ws.rs.core.MediaType; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -32,7 +32,7 @@ public void list() { .get() .then() .statusCode(200) - .contentType(MediaType.APPLICATION_JSON) + .contentType(ContentType.JSON) .body("size()", equalTo(pagingSize)); range(0, pagingSize) @@ -50,7 +50,7 @@ public void listOutOfBounds() { .get() .then() .statusCode(200) - .contentType(MediaType.APPLICATION_JSON) + .contentType(ContentType.JSON) .body(equalTo("[]")); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTests.java index 3567a84..02a6034 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTests.java @@ -11,7 +11,7 @@ import app.fyreplace.api.testing.TransactionalTests; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; -import jakarta.ws.rs.core.MediaType; +import io.restassured.http.ContentType; import org.junit.jupiter.api.Test; @QuarkusTest @@ -20,7 +20,7 @@ public final class CreateNewTests extends TransactionalTests { @Test public void createNewWithUsername() { final var user = User.findByUsername("user_0"); - given().contentType(MediaType.APPLICATION_JSON) + given().contentType(ContentType.JSON) .body(new NewTokenCreation(user.username)) .post("new") .then() @@ -31,7 +31,7 @@ public void createNewWithUsername() { @Test public void createNewWithEmail() { final var user = User.findByUsername("user_0"); - given().contentType(MediaType.APPLICATION_JSON) + given().contentType(ContentType.JSON) .body(new NewTokenCreation(user.mainEmail.email)) .post("new") .then() @@ -42,7 +42,7 @@ public void createNewWithEmail() { @Test public void createNewWithInvalidIdentifier() { final var user = User.findByUsername("user_0"); - given().contentType(MediaType.APPLICATION_JSON) + given().contentType(ContentType.JSON) .body(new NewTokenCreation("invalid")) .post("new") .then() @@ -53,7 +53,7 @@ public void createNewWithInvalidIdentifier() { @Test public void createNewWithEmptyInput() { final var user = User.findByUsername("user_0"); - given().contentType(MediaType.APPLICATION_JSON).post("new").then().statusCode(400); + given().contentType(ContentType.JSON).post("new").then().statusCode(400); assertEquals(0, getMailsSentTo(user.mainEmail).size()); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java index 1534189..5070d56 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java @@ -18,7 +18,6 @@ import io.restassured.http.ContentType; import jakarta.inject.Inject; import jakarta.transaction.Transactional; -import jakarta.ws.rs.core.MediaType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -54,7 +53,7 @@ public void createWithNewUsername() { .post() .then() .statusCode(201) - .contentType(MediaType.TEXT_PLAIN) + .contentType(ContentType.TEXT) .body(isA(String.class)); assertEquals(randomCodeCount - 1, RandomCode.count()); final var email = Email.find("id", newUserRandomCode.email.id).firstResult(); @@ -69,7 +68,7 @@ public void createWithEmail() { .post() .then() .statusCode(201) - .contentType(MediaType.TEXT_PLAIN) + .contentType(ContentType.TEXT) .body(isA(String.class)); assertEquals(randomCodeCount - 1, RandomCode.count()); } @@ -83,7 +82,7 @@ public void createWithNewEmail() { .post() .then() .statusCode(201) - .contentType(MediaType.TEXT_PLAIN) + .contentType(ContentType.TEXT) .body(isA(String.class)); assertEquals(randomCodeCount - 1, RandomCode.count()); final var email = Email.find("id", newUserRandomCode.email.id).firstResult(); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/RetrieveNewTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/RetrieveNewTests.java index 4e6ab8b..97d6cc5 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/RetrieveNewTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/RetrieveNewTests.java @@ -8,7 +8,7 @@ import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; -import jakarta.ws.rs.core.MediaType; +import io.restassured.http.ContentType; import org.junit.jupiter.api.Test; @QuarkusTest @@ -17,11 +17,7 @@ public final class RetrieveNewTests extends TransactionalTests { @Test @TestSecurity(user = "user_0") public void retrieveNew() { - given().get("new") - .then() - .statusCode(200) - .contentType(MediaType.TEXT_PLAIN) - .body(isA(String.class)); + given().get("new").then().statusCode(200).contentType(ContentType.TEXT).body(isA(String.class)); } @Test diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java index 03cc37c..43c554e 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java @@ -18,7 +18,6 @@ import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.restassured.http.ContentType; -import jakarta.ws.rs.core.MediaType; import org.junit.jupiter.api.Test; @QuarkusTest @@ -32,7 +31,7 @@ public void create() { .body(new UserCreation("new@example.org", "new_user")) .post() .then() - .contentType(MediaType.APPLICATION_JSON) + .contentType(ContentType.JSON) .statusCode(201) .body("dateCreated", notNullValue()) .body("username", equalTo("new_user")) @@ -58,7 +57,7 @@ public void createWithInvalidUsername() { .body(new UserCreation("new@example.org", "no spaces allowed")) .post() .then() - .contentType(MediaType.APPLICATION_JSON) + .contentType(ContentType.JSON) .statusCode(400); assertEquals(userCount, User.count()); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java index 7170cad..1882942 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java @@ -12,8 +12,8 @@ import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; import jakarta.transaction.Transactional; -import jakarta.ws.rs.core.MediaType; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -32,7 +32,7 @@ public void list() { .get("blocked") .then() .statusCode(200) - .contentType(MediaType.APPLICATION_JSON) + .contentType(ContentType.JSON) .body("size()", equalTo(pagingSize)); range(0, pagingSize) @@ -50,7 +50,7 @@ public void listOutOfBounds() { .get("blocked") .then() .statusCode(200) - .contentType(MediaType.APPLICATION_JSON) + .contentType(ContentType.JSON) .body(equalTo("[]")); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java index 918d1bb..e169aaf 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java @@ -11,7 +11,7 @@ import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; -import jakarta.ws.rs.core.MediaType; +import io.restassured.http.ContentType; import org.junit.jupiter.api.Test; @QuarkusTest @@ -23,7 +23,7 @@ public void retrieveMe() { final var user = User.findByUsername("user_10"); given().get("/me") .then() - .contentType(MediaType.APPLICATION_JSON) + .contentType(ContentType.JSON) .statusCode(200) .body("id", equalTo(user.id.toString())) .body("dateCreated", notNullValue()) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java index 04f86c3..66bf603 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java @@ -10,7 +10,7 @@ import app.fyreplace.api.testing.TransactionalTests; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; -import jakarta.ws.rs.core.MediaType; +import io.restassured.http.ContentType; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -23,7 +23,7 @@ public void retrieve(final String username) { final var user = User.findByUsername(username); given().get(user.id.toString()) .then() - .contentType(MediaType.APPLICATION_JSON) + .contentType(ContentType.JSON) .statusCode(200) .body("id", equalTo(user.id.toString())) .body("dateCreated", notNullValue()) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java index 43e0b09..322e9c8 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java @@ -15,7 +15,6 @@ import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; import io.restassured.http.ContentType; -import jakarta.ws.rs.core.MediaType; import java.io.IOException; import java.net.URL; import org.junit.jupiter.api.Test; @@ -51,7 +50,7 @@ public void updateMeAvatar(final String fileType) throws IOException { .body(stream.readAllBytes()) .put("me/avatar") .then() - .contentType(MediaType.TEXT_PLAIN) + .contentType(ContentType.TEXT) .statusCode(200) .body(isA(String.class)); } @@ -82,11 +81,9 @@ public void updateMeAvatarWithInvalidType(final String fileType) throws IOExcept @Test @TestSecurity(user = "user_0") - public void updateMeAvatarWithEmptyBody() { + public void updateMeAvatarWithoutInput() { final var remoteFileCount = StoredFile.count(); - given().contentType(ContentType.BINARY).put("me/avatar").then().statusCode(415); - assertEquals(remoteFileCount, StoredFile.count()); final var user = User.findByUsername("user_0"); assertNull(user.avatar); From 80d4ee39eaa7cef74c2a6ce61c92d205312cc1be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 19 Aug 2023 12:46:47 +0200 Subject: [PATCH 034/157] Simplify mime type handling --- .../app/fyreplace/api/endpoints/UsersEndpoint.java | 9 ++++++--- .../app/fyreplace/api/services/MimeTypeService.java | 13 +++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java index 5f4650e..7052757 100644 --- a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java @@ -32,7 +32,9 @@ import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; import jakarta.ws.rs.core.SecurityContext; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.time.Duration; import java.time.Instant; import java.util.List; @@ -217,14 +219,15 @@ public String updateMeBio(@NotNull @Length(max = 3000) final String input) { @APIResponse(responseCode = "401") @APIResponse(responseCode = "413") @APIResponse(responseCode = "415") - public String updateMeAvatar(final byte[] input) throws IOException { + public String updateMeAvatar(final File input) throws IOException { mimeTypeService.validate(input, KnownMimeTypes.IMAGE); final var user = User.getFromSecurityContext(context, LockModeType.PESSIMISTIC_WRITE); + final var data = Files.readAllBytes(input.toPath()); if (user.avatar == null) { - user.avatar = new StoredFile("avatars/" + user.id.toString(), input); + user.avatar = new StoredFile("avatars/" + user.id, data); } else { - user.avatar.store(input); + user.avatar.store(data); } user.persist(); diff --git a/src/main/java/app/fyreplace/api/services/MimeTypeService.java b/src/main/java/app/fyreplace/api/services/MimeTypeService.java index a1bdcad..f47b293 100644 --- a/src/main/java/app/fyreplace/api/services/MimeTypeService.java +++ b/src/main/java/app/fyreplace/api/services/MimeTypeService.java @@ -3,21 +3,18 @@ import app.fyreplace.api.exceptions.UnsupportedMediaTypeException; import app.fyreplace.api.services.mimetype.KnownMimeTypes; import jakarta.enterprise.context.ApplicationScoped; -import java.io.ByteArrayInputStream; +import java.io.File; import java.io.IOException; import org.apache.tika.Tika; @ApplicationScoped public final class MimeTypeService { - public void validate(final byte[] data, final KnownMimeTypes types) throws IOException { + public void validate(final File file, final KnownMimeTypes types) throws IOException { final var tika = new Tika(); + final var mimeType = tika.detect(file); - try (final var stream = new ByteArrayInputStream(data)) { - final var mimeType = tika.detect(stream); - - if (mimeType == null || !types.types.contains(mimeType)) { - throw new UnsupportedMediaTypeException("invalid_media_type"); - } + if (mimeType == null || !types.types.contains(mimeType)) { + throw new UnsupportedMediaTypeException("invalid_media_type"); } } } From bf63a7f01b40d3c5d90f2810142b68ef063b9462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 25 Aug 2023 10:50:06 +0200 Subject: [PATCH 035/157] Update dependencies --- build.gradle | 2 -- gradle.properties | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 0aea64f..7eb0b35 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,6 @@ plugins { id "io.quarkus" id "com.palantir.git-version" version "${gitPluginVersion}" id "com.diffplug.spotless" version "${spotlessPluginVersion}" - id "io.freefair.lombok" version "${lombokPluginVersion}" } group = "app.fyreplace" @@ -45,7 +44,6 @@ dependencies { testImplementation("io.quarkus:quarkus-test-h2") testImplementation("io.quarkus:quarkus-test-security") testImplementation("io.rest-assured:rest-assured") - testImplementation("org.apiguardian:apiguardian-api:+") } java { diff --git a/gradle.properties b/gradle.properties index b2b4a2e..ef5eeba 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,6 @@ quarkusVersion=3.2.4.Final -quarkusAmazonVersion=2.4.3 +quarkusAmazonVersion=2.5.0 gitPluginVersion=3.0.0 spotlessPluginVersion=6.20.0 -lombokPluginVersion=8.2.2 sentryVersion=6.28.0 tikaVersion=2.8.0 From 91729a2dccdc3ace48ce082caae97f09dfec19ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 25 Aug 2023 15:00:40 +0200 Subject: [PATCH 036/157] Localize emails --- .../app/fyreplace/api/emails/EmailBase.java | 16 +++++++-- .../api/emails/EmailVerificationEmail.java | 13 ++++---- .../api/emails/UserActivationEmail.java | 11 ++++--- .../api/emails/UserConnectionEmail.java | 11 ++++--- .../fyreplace/api/services/LocaleService.java | 33 +++++++++++++++++++ src/main/resources/application.yaml | 4 +++ .../i18n/EmailVerificationEmail.properties | 5 +++ .../i18n/UserActivationEmail.properties | 6 ++++ .../i18n/UserConnectionEmail.properties | 5 +++ .../EmailVerificationEmail/html.html.mjml | 6 ++-- .../templates/EmailVerificationEmail/text.txt | 6 ++-- .../UserActivationEmail/html.html.mjml | 8 ++--- .../templates/UserActivationEmail/text.txt | 8 ++--- .../UserConnectionEmail/html.html.mjml | 6 ++-- .../templates/UserConnectionEmail/text.txt | 6 ++-- .../templates/emails/_link_end_notice.mjml | 2 +- 16 files changed, 106 insertions(+), 40 deletions(-) create mode 100644 src/main/java/app/fyreplace/api/services/LocaleService.java create mode 100644 src/main/resources/i18n/EmailVerificationEmail.properties create mode 100644 src/main/resources/i18n/UserActivationEmail.properties create mode 100644 src/main/resources/i18n/UserConnectionEmail.properties diff --git a/src/main/java/app/fyreplace/api/emails/EmailBase.java b/src/main/java/app/fyreplace/api/emails/EmailBase.java index 0bac110..b05fd8c 100644 --- a/src/main/java/app/fyreplace/api/emails/EmailBase.java +++ b/src/main/java/app/fyreplace/api/emails/EmailBase.java @@ -2,6 +2,7 @@ import app.fyreplace.api.data.Email; import app.fyreplace.api.data.RandomCode; +import app.fyreplace.api.services.LocaleService; import app.fyreplace.api.services.RandomService; import io.quarkus.mailer.Mail; import io.quarkus.mailer.Mailer; @@ -11,6 +12,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.List; +import java.util.ResourceBundle; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.logging.Logger; @@ -24,13 +26,16 @@ public abstract class EmailBase extends Mail { @Inject RandomService randomService; + @Inject + LocaleService localeService; + private String code; private Email email; private final Logger logger = Logger.getLogger(this.getClass()); - protected abstract String getAction(); + protected abstract String action(); protected abstract TemplateInstance textTemplate(); @@ -39,7 +44,8 @@ public abstract class EmailBase extends Mail { @Blocking public void sendTo(final Email email) { this.email = email; - mailer.send(this.setText(textTemplate().render()) + mailer.send(this.setSubject(getResourceBundle().getString("subject")) + .setText(textTemplate().render()) .setHtml(htmlTemplate().render()) .setTo(List.of(email.email))); } @@ -59,11 +65,15 @@ protected String getRandomCode() { protected String getLink() { try { - final var url = new URL(new URL(appUrl), "?action=" + getAction()); + final var url = new URL(new URL(appUrl), "?action=" + action()); return String.format("%s#%s:%s", url, email.user.username, getRandomCode()); } catch (final MalformedURLException e) { logger.error("Could not generate link", e); return null; } } + + protected ResourceBundle getResourceBundle() { + return localeService.getResourceBundle(this.getClass().getSimpleName()); + } } diff --git a/src/main/java/app/fyreplace/api/emails/EmailVerificationEmail.java b/src/main/java/app/fyreplace/api/emails/EmailVerificationEmail.java index 5e6b22c..e5b55ba 100644 --- a/src/main/java/app/fyreplace/api/emails/EmailVerificationEmail.java +++ b/src/main/java/app/fyreplace/api/emails/EmailVerificationEmail.java @@ -3,28 +3,29 @@ import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.TemplateInstance; import jakarta.enterprise.context.Dependent; +import java.util.ResourceBundle; @Dependent public final class EmailVerificationEmail extends EmailBase { @Override - protected String getAction() { - return "connect"; + protected String action() { + return "email"; } @Override protected TemplateInstance textTemplate() { - return Templates.text(getRandomCode(), getLink()); + return Templates.text(getResourceBundle(), getRandomCode(), getLink()); } @Override protected TemplateInstance htmlTemplate() { - return Templates.html(getRandomCode(), getLink()); + return Templates.html(getResourceBundle(), getRandomCode(), getLink()); } @CheckedTemplate public static class Templates { - public static native TemplateInstance text(String code, String link); + public static native TemplateInstance text(ResourceBundle res, String code, String link); - public static native TemplateInstance html(String code, String link); + public static native TemplateInstance html(ResourceBundle res, String code, String link); } } diff --git a/src/main/java/app/fyreplace/api/emails/UserActivationEmail.java b/src/main/java/app/fyreplace/api/emails/UserActivationEmail.java index 1f5daca..3b10503 100644 --- a/src/main/java/app/fyreplace/api/emails/UserActivationEmail.java +++ b/src/main/java/app/fyreplace/api/emails/UserActivationEmail.java @@ -3,6 +3,7 @@ import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.TemplateInstance; import jakarta.enterprise.context.Dependent; +import java.util.ResourceBundle; import org.eclipse.microprofile.config.inject.ConfigProperty; @Dependent @@ -11,24 +12,24 @@ public final class UserActivationEmail extends EmailBase { String appName; @Override - protected String getAction() { + protected String action() { return "connect"; } @Override protected TemplateInstance textTemplate() { - return Templates.text(appName, getRandomCode(), getLink()); + return Templates.text(getResourceBundle(), appName, getRandomCode(), getLink()); } @Override protected TemplateInstance htmlTemplate() { - return Templates.html(appName, getRandomCode(), getLink()); + return Templates.html(getResourceBundle(), appName, getRandomCode(), getLink()); } @CheckedTemplate public static class Templates { - public static native TemplateInstance text(String appName, String code, String link); + public static native TemplateInstance text(ResourceBundle res, String appName, String code, String link); - public static native TemplateInstance html(String appName, String code, String link); + public static native TemplateInstance html(ResourceBundle res, String appName, String code, String link); } } diff --git a/src/main/java/app/fyreplace/api/emails/UserConnectionEmail.java b/src/main/java/app/fyreplace/api/emails/UserConnectionEmail.java index cd9fd89..b4904ce 100644 --- a/src/main/java/app/fyreplace/api/emails/UserConnectionEmail.java +++ b/src/main/java/app/fyreplace/api/emails/UserConnectionEmail.java @@ -3,28 +3,29 @@ import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.TemplateInstance; import jakarta.enterprise.context.Dependent; +import java.util.ResourceBundle; @Dependent public class UserConnectionEmail extends EmailBase { @Override - protected String getAction() { + protected String action() { return "connect"; } @Override protected TemplateInstance textTemplate() { - return Templates.text(getRandomCode(), getLink()); + return Templates.text(getResourceBundle(), getRandomCode(), getLink()); } @Override protected TemplateInstance htmlTemplate() { - return Templates.html(getRandomCode(), getLink()); + return Templates.html(getResourceBundle(), getRandomCode(), getLink()); } @CheckedTemplate public static class Templates { - public static native TemplateInstance text(String code, String link); + public static native TemplateInstance text(ResourceBundle res, String code, String link); - public static native TemplateInstance html(String code, String link); + public static native TemplateInstance html(ResourceBundle res, String code, String link); } } diff --git a/src/main/java/app/fyreplace/api/services/LocaleService.java b/src/main/java/app/fyreplace/api/services/LocaleService.java new file mode 100644 index 0000000..4befee4 --- /dev/null +++ b/src/main/java/app/fyreplace/api/services/LocaleService.java @@ -0,0 +1,33 @@ +package app.fyreplace.api.services; + +import jakarta.annotation.Nullable; +import jakarta.enterprise.context.Dependent; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.HttpHeaders; +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +@Dependent +public final class LocaleService { + @Context + HttpHeaders headers; + + public ResourceBundle getResourceBundle(final String name) { + final var path = "i18n." + name; + return headers.getAcceptableLanguages().stream() + .map(locale -> getResourceBundleOrNull(path, locale)) + .filter(bundle -> bundle != null) + .findFirst() + .orElse(ResourceBundle.getBundle(path)); + } + + @Nullable + private ResourceBundle getResourceBundleOrNull(final String name, final Locale locale) { + try { + return ResourceBundle.getBundle(name, locale); + } catch (final MissingResourceException e) { + return null; + } + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index df47254..da01808 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -24,6 +24,10 @@ quarkus: limits: max-body-size: 1M + mailer: + port: 587 + start-tls: REQUIRED + s3: devservices: enabled: false diff --git a/src/main/resources/i18n/EmailVerificationEmail.properties b/src/main/resources/i18n/EmailVerificationEmail.properties new file mode 100644 index 0000000..7e4dc40 --- /dev/null +++ b/src/main/resources/i18n/EmailVerificationEmail.properties @@ -0,0 +1,5 @@ +subject=Email verification +codeDescription=You can verify your email by using this code: +linkDescription=Or by clicking on this link: +button=Verify email +linkEndNotice=This link will expire in 24 hours. diff --git a/src/main/resources/i18n/UserActivationEmail.properties b/src/main/resources/i18n/UserActivationEmail.properties new file mode 100644 index 0000000..1f89101 --- /dev/null +++ b/src/main/resources/i18n/UserActivationEmail.properties @@ -0,0 +1,6 @@ +subject=Account activation +title=Welcome to $1! +codeDescription=You can activate your account by using this code: +linkDescription=Or by clicking on this link: +button=Activate account +linkEndNotice=This link will expire in 24 hours. diff --git a/src/main/resources/i18n/UserConnectionEmail.properties b/src/main/resources/i18n/UserConnectionEmail.properties new file mode 100644 index 0000000..408e5e3 --- /dev/null +++ b/src/main/resources/i18n/UserConnectionEmail.properties @@ -0,0 +1,5 @@ +subject=Account connection +codeDescription=You can connect by using this code: +linkDescription=Or by clicking on this link: +button=Connect +linkEndNotice=This link will expire in 24 hours. diff --git a/src/main/resources/templates/EmailVerificationEmail/html.html.mjml b/src/main/resources/templates/EmailVerificationEmail/html.html.mjml index b60520a..5140c85 100644 --- a/src/main/resources/templates/EmailVerificationEmail/html.html.mjml +++ b/src/main/resources/templates/EmailVerificationEmail/html.html.mjml @@ -4,10 +4,10 @@ - You can verify your email by using this code: + {res.getString("codeDescription")} {code} - Or by clicking on this link: - Activate account + {res.getString("linkDescription")} + {res.getString("button")} diff --git a/src/main/resources/templates/EmailVerificationEmail/text.txt b/src/main/resources/templates/EmailVerificationEmail/text.txt index 95985cc..46f6d33 100644 --- a/src/main/resources/templates/EmailVerificationEmail/text.txt +++ b/src/main/resources/templates/EmailVerificationEmail/text.txt @@ -1,7 +1,7 @@ -You can verify your email by using this code: +{res.getString("codeDescription")} {code} -Or by clicking on this link: +{res.getString("linkDescription")} {link} -The code and link will expire in 24 hours. +{res.getString("linkEndNotice")} diff --git a/src/main/resources/templates/UserActivationEmail/html.html.mjml b/src/main/resources/templates/UserActivationEmail/html.html.mjml index b40e7f2..04aef09 100644 --- a/src/main/resources/templates/UserActivationEmail/html.html.mjml +++ b/src/main/resources/templates/UserActivationEmail/html.html.mjml @@ -4,11 +4,11 @@ - Welcome to {appName}! - You can activate your account by using this code: + {res.getString("title")} + {res.getString("codeDescription")} {code} - Or by clicking on this link: - Activate account + {res.getString("linkDescription")} + {res.getString("button")} diff --git a/src/main/resources/templates/UserActivationEmail/text.txt b/src/main/resources/templates/UserActivationEmail/text.txt index da13cd0..47f8c36 100644 --- a/src/main/resources/templates/UserActivationEmail/text.txt +++ b/src/main/resources/templates/UserActivationEmail/text.txt @@ -1,9 +1,9 @@ -Welcome to {appName}! +{res.getString("title").replace("$1", appName)} -You can activate your account by using this code: +{res.getString("codeDescription")} {code} -Or by clicking on this link: +{res.getString("linkDescription")} {link} -The code and link will expire in 24 hours. +{res.getString("linkEndNotice")} diff --git a/src/main/resources/templates/UserConnectionEmail/html.html.mjml b/src/main/resources/templates/UserConnectionEmail/html.html.mjml index 6d7146e..5140c85 100644 --- a/src/main/resources/templates/UserConnectionEmail/html.html.mjml +++ b/src/main/resources/templates/UserConnectionEmail/html.html.mjml @@ -4,10 +4,10 @@ - You can connect by using this code: + {res.getString("codeDescription")} {code} - Or by clicking on this link: - Activate account + {res.getString("linkDescription")} + {res.getString("button")} diff --git a/src/main/resources/templates/UserConnectionEmail/text.txt b/src/main/resources/templates/UserConnectionEmail/text.txt index 968dc31..46f6d33 100644 --- a/src/main/resources/templates/UserConnectionEmail/text.txt +++ b/src/main/resources/templates/UserConnectionEmail/text.txt @@ -1,7 +1,7 @@ -You can connect by using this code: +{res.getString("codeDescription")} {code} -Or by clicking on this link: +{res.getString("linkDescription")} {link} -The code and link will expire in 24 hours. +{res.getString("linkEndNotice")} diff --git a/src/main/resources/templates/emails/_link_end_notice.mjml b/src/main/resources/templates/emails/_link_end_notice.mjml index 963e3ec..ffbca77 100644 --- a/src/main/resources/templates/emails/_link_end_notice.mjml +++ b/src/main/resources/templates/emails/_link_end_notice.mjml @@ -1 +1 @@ -This link will expire in 24 hours. \ No newline at end of file +{res.getString("linkEndNotice")} \ No newline at end of file From 2ebf42610d2cb02c763b37de56d69c1db8538f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 25 Aug 2023 15:21:02 +0200 Subject: [PATCH 037/157] Forbid blank strings as much as possible --- src/main/java/app/fyreplace/api/data/EmailActivation.java | 4 ++-- src/main/java/app/fyreplace/api/data/EmailCreation.java | 4 ++-- src/main/java/app/fyreplace/api/data/NewTokenCreation.java | 4 ++-- src/main/java/app/fyreplace/api/data/TokenCreation.java | 4 ++-- src/main/java/app/fyreplace/api/data/User.java | 3 ++- src/main/java/app/fyreplace/api/data/UserCreation.java | 6 +++--- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/main/java/app/fyreplace/api/data/EmailActivation.java b/src/main/java/app/fyreplace/api/data/EmailActivation.java index 989582a..37f949e 100644 --- a/src/main/java/app/fyreplace/api/data/EmailActivation.java +++ b/src/main/java/app/fyreplace/api/data/EmailActivation.java @@ -1,5 +1,5 @@ package app.fyreplace.api.data; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; -public final record EmailActivation(@NotNull String email, @NotNull String code) {} +public final record EmailActivation(@NotBlank String email, @NotBlank String code) {} diff --git a/src/main/java/app/fyreplace/api/data/EmailCreation.java b/src/main/java/app/fyreplace/api/data/EmailCreation.java index a528726..39a4bcc 100644 --- a/src/main/java/app/fyreplace/api/data/EmailCreation.java +++ b/src/main/java/app/fyreplace/api/data/EmailCreation.java @@ -1,7 +1,7 @@ package app.fyreplace.api.data; import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; import org.hibernate.validator.constraints.Length; -public final record EmailCreation(@NotNull @Length(min = 3, max = 254) @NotNull @Email String email) {} +public final record EmailCreation(@Length(min = 3, max = 254) @NotBlank @Email String email) {} diff --git a/src/main/java/app/fyreplace/api/data/NewTokenCreation.java b/src/main/java/app/fyreplace/api/data/NewTokenCreation.java index 25b3377..e2bc529 100644 --- a/src/main/java/app/fyreplace/api/data/NewTokenCreation.java +++ b/src/main/java/app/fyreplace/api/data/NewTokenCreation.java @@ -1,5 +1,5 @@ package app.fyreplace.api.data; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; -public final record NewTokenCreation(@NotNull String identifier) {} +public final record NewTokenCreation(@NotBlank String identifier) {} diff --git a/src/main/java/app/fyreplace/api/data/TokenCreation.java b/src/main/java/app/fyreplace/api/data/TokenCreation.java index f14a943..23f6e8d 100644 --- a/src/main/java/app/fyreplace/api/data/TokenCreation.java +++ b/src/main/java/app/fyreplace/api/data/TokenCreation.java @@ -1,5 +1,5 @@ package app.fyreplace.api.data; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; -public record TokenCreation(@NotNull String identifier, @NotNull String code) {} +public record TokenCreation(@NotBlank String identifier, @NotBlank String code) {} diff --git a/src/main/java/app/fyreplace/api/data/User.java b/src/main/java/app/fyreplace/api/data/User.java index 15d9e86..e3b0e6e 100644 --- a/src/main/java/app/fyreplace/api/data/User.java +++ b/src/main/java/app/fyreplace/api/data/User.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import jakarta.annotation.Nullable; import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.ws.rs.NotAuthorizedException; import jakarta.ws.rs.core.SecurityContext; @@ -150,5 +151,5 @@ public enum BanCount { ONE_TOO_MANY; } - public static final record Profile(@NotNull UUID id, @NotNull String username, String avatar) {} + public static final record Profile(@NotNull UUID id, @NotBlank String username, String avatar) {} } diff --git a/src/main/java/app/fyreplace/api/data/UserCreation.java b/src/main/java/app/fyreplace/api/data/UserCreation.java index d09c808..f88a2d6 100644 --- a/src/main/java/app/fyreplace/api/data/UserCreation.java +++ b/src/main/java/app/fyreplace/api/data/UserCreation.java @@ -2,9 +2,9 @@ import app.fyreplace.api.data.validators.Regex; import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; import org.hibernate.validator.constraints.Length; public record UserCreation( - @NotNull @Length(min = 3, max = 254) @Email String email, - @NotNull @Length(min = 3, max = 100) @Regex(pattern = "^[\\w.@+-]+\\Z") String username) {} + @NotBlank @Length(min = 3, max = 254) @Email String email, + @NotBlank @Length(min = 3, max = 100) @Regex(pattern = "^[\\w.@+-]+\\Z") String username) {} From 77abd2f58d7240dec3722a58e6d2ca9e8aa3a142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 26 Aug 2023 11:26:44 +0200 Subject: [PATCH 038/157] Update Gradle --- gradle/wrapper/gradle-wrapper.jar | Bin 58910 -> 62076 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 276 ++++++++++++++--------- gradlew.bat | 34 +-- 4 files changed, 181 insertions(+), 132 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 62d4c053550b91381bbd28b1afc82d634bf73a8a..c1962a79e29d3e0ab67b14947c167a862655af9b 100644 GIT binary patch delta 42313 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�&DnW5akX->!2y79K zh;M!eimArMmlDsNbt2h61|t+=zkc9R{)za`bkNm%^Yd=;@tpkKHMJ%0n}?kr119j> z+DG&hRY^cx1)F`@((IJvQqUT28aMg!CtI{(!WyOeASzxwu`pj=QFM1;zX7Ra>sKk? z9XAw2(g5q*KWM?^lxTZfN5%sfG@|T`_y^{65Mt@6z(zn6B4VJMLkh{B3CAtP@7d63 z2Wu651J6P0=iKZ;T}hOrEu|Ku1f%asd`PO-m6rB3i>LG}3pCcC7N%m0ZJNgnCRfME08}S-dePpU|Dq3_} zDl_?b2A8fYH1yjVi@VLz!@H-PY!`Gt-4P|b77Ksp{4q&bvEkz##3fm?QNmN)T3%8qQcC=_HpS$NQE#wjx=_4u^ z3tuX=!1LrZFe5BY1=r|!d_~$X$;HLvLT$C2M6i=aM+m%ma7gA!xnN&`+TYu6sMWTGGiK=#3gZ;406eZM z6|>_fm5tW?$-IvU2da~Odo0;Z(lG}KM4Ny6BDfT+)U!|ECMFCL@-7DZLLRde8sQvs zU$K!Z4pjg@e)eMt?!Bl9qi!IeG6`bx4_P8a=jIZncUoLltW&Vf{%|ke1sj2=WWwE- zevR;Fzuos@^PC--{zm~Ef>*zE&l5by=miw%CH zR@-D6)JZO~JpdUWZ*t4@3=ucVi_39-4a`yIS8A--hey{iZ^?PPx2e1R0vIV1<6J}b zb3)(Z+L=CYI8=89EiCHP`yR1B@S@Qkr(~+9T4`Sd-|yYZ5vE)fFnWgd3Ux`_e#3kH ze*cc{7?Q3g73p3?Y0;y{KpW1|2zU`=fF9gv?v(cxnwFveBJ*qTGC}^jw!R+ZW6#C1 zv?KDj0~Jmi=_JQ5Wm_1f2CRoS73#>ah9f&QSU)m3JB14f-0A zb@~w?N=z0j)oZ$fnfHdY=RY>gZ6Ct2pqYDZnvWyM@EOA<4rsgqD-b~V>WFt@;UVo_ z@HuZsPY8S?YKPDzP1j_ZNMjkPCF!pvo7AXa(DTeU^bjWesmT%~4G5;!P;I^=krJxF zK{Ip3;7S-`=P03}lG@oBbCDtlY9c`iYMNHMFQ%KHBD`87@%$WM?%d~&Yo~>d<7w$T zCKx67P4F~y##+m5Xt~|c5cq6uARgFb6h3fIB6ANUe>+6jUqYINQ7r%w%j<%ghqQFU zF3g+RB^!^Jwf+)GBK-azd-Oj{c4L2oCJP1vat#3jBJocok|#4JG60%BwaRrXAS(-D zNM(A>3YbQkt(Pb%<vUlVs?8y!!0QoXRZ9=2Be6bRm?Yg<8C3QmtJ< zMp|c5nq5SB523uf0)PnC*SOW=84^vEyaO{TDp@vN%ESVmhUXcea&@-FcEs^`+HmMIZ%#khq*d=VTrD-gQcCfQ zloY>ZGKfGcUOHEW-2{4V83}{5&ust~`Ut9`5%a!Cl3LP!kh(>PKCI0GHg$Rvy(B2)GrxA^3(umh5V) z^pVBm-Agvy_I2Oc+P>a?zdHYVMz$1$oC|lxovfFa!01h@*O#Dab}78rB>~Kc21DmW zgY26gK}C;j zVl~_DKK-? z2T+jCqnSr)6d7Pc%T2w>?)xqDnN2sdb#`Pvo_oaKd^a~T2uGyhLZ~1L4!Uhyasdp; z>JO^P9ax+?%{O8Q=pVcXyKh|HGT!)O!05c~BitXWRg@3P00&4bQZJYUgaxMph5IRo zAn+J36?kD+t|QNd)ifbC;e(I{!RJormg_iJDTDMnLU9PB4t1XUi$Z4u_dUIW` zm5h!z_3k@fYzIIx?FaV#K~i0g2;b_)WpP(?T{7OlV?px~*|J`@`9Lepq99Onf?=XL z8IgcwA}ig+90HINU}@=NTsjOoQ~&<(Di`_xsTcw~geS~?~N2;jGTRw=giG#C!rTiM> zgzz4ogIXhZX&TgXu4!5PlK6Z6rx`%mpBV_12WKx8B-< z*Q0QWA*1q}vRXLpRZ?^1zQpPD5?y!3pAs%ry0AX^X2$+$m_;eVbm~hn@3a81)cG!U zT7@EVAFPtkGE_WK?PgqbzB1G-?PX#QNhqK zOXYdPl2F0^>aT?pNtow=nDsK2W4%Fc+QkjmGKa;EYGW{UiZzj_zd7WwCEGMMFo?fQ z6Na4cKJ4i+uk-b@fByEG|!s!JnSO zfUV9M8={ zEx+}|8u5b^DjQwruD!+D5S3gsqL+!Vbl$s>o135C;tsN14|;6|D(v#tURzt?jV2vn z_uelrDJaD>`e~NEp82einA8Xb-0JG2S?<}BArQ|P=W@rZ@BdgM77QT~92LA(^s(`e zrwsX{#~4awd@8Ur(2(%2re;au-!XFf59*uo#iRY4+AhE9H?kYnnMWet?YDAcoCg#R8F3Jk(%H2G7gjH zu~Xt>7=&NPfK=2!a6b)RZ_W(tDDWwOvx~k$nF2TiR-w_9e+f@ zH6^IG^kV|Q4V`a~6F$iPn>4EgK=+H2w=<;y`ihH27^B~o_**NOxU|3ODIJ;}9`fuW%LwpKvN!Ut``{H5vSyyr^!ex@J4jl0rmC`QeFG#`iFA$1l4gdI;V| zq~0X~$q4RaUK1G=Z~BxZ#MND}?5 z_WbD<__XpFL#9d4{)5)ls0W$vXj@>aA4FTSHG@BmqgKFBOV%XqrnM~6^Em^lmGr5h zka02e_5g_Tpk8+Bkf})DLNO?5SE|N_fIwWu*oJt*ZEl1_2KBeWXqS``7Z@@RAjg7r z?PfqVz9avnn9A-73T?Q$B$gvE67@>BD@OLhr}7*R2Ct4SD=b+`C`Y- zKB}FDA_XV=!55JT(}5%0Vq%RAh;(Gg9eEbsuqUV)o_8X`219a`9`c^KujvfJ&wk!2^jrJ2wh{u3@e*c9m+Ml&qlIB)F?qi*n-4oB&O%P<^2 zPEey<{qB4HJ6y_`q~=#aEJMRd7~yjVT43&eF&r2VKKSzi+RZ}>(SfEg!m=BN+@o%ey{DV%x} zB`L+&W%dg{X}X=E>ecd?wQnqu6iJdI5LAmMCoS09xcJUz~Qw8t!-zA$vnJ-r;H^QE-S#i*ndX3N_-XUP*&^o5H*B4&` z_%JgVMbE}tEqR_Mkg$7n#t(>wAKf=NhiyVSYo&oQ)WkDmQftC zK!XbP+d+ho?oE|T&cTOU?&ouH#3erYap5K|z(6jm+^4L)`ldM!a_;?uoo`%Gn(y3E zVt#*F?XAvbKfdA(t7G$2ubf7ER;6qX-b|JHqWhw&cgJvs>831CgYNgy7f~C4^IYGC z>KG4kgof;)ed$ybZy-P0kvJm-T``v#1FsO2Ih&+P>KAj3G+8ISMSUF%u4*R?;BGc} zMkU)kzk!8(jXQkocOgzr!6X0Nwsb6tvq4||Y?oB4n0U+uHuVprS5geIDBAQKY~Sr@!j^T7Ygvg)~%} zWXiP12^Nw&e9x8opDGx0q6)Px6p0OK!$BS$L>V%L*(RA^CQ!B1ci*N)nJ6qe_v9`` z`q3wj|BXq6#$qAbqc_SQ;VUyij)cmMd3Y+jj*oLVN2Q#} zSCKXe0Q3r}!RJFb7X=|AGHz1hPRGj4uf50ix2x$oddnie49r6MAp+4Wes6@&E}fP^ zX3gBZk_VZu;Kh?az@ER?5bEzGWNFNsLCj<-2znwTKe0kY_&F0K!qb1tKw;I7F}q+r zzs4P`T=ey!)8hgoS)e5PSD8!qXhcs%x^P*)0k@i5ZI?D~6Ydxs^SiLFPDEgv;`t*c3R?{dNGmWl;s&l1$4m0CR<4BXvCz?03Q&8 ziD{fx54q5SruLnH&jNy(=etDuKr z02ZwAFIfwkgT(Klm+U@3A{}wwA?*)Pq>Y~v^X8pFKKUf#8Q&E3FH{2LTx4MAOJ#{9 z;M=ATc>Ou3ldn16OilLnn_;!wqWXZ|Ag0g2PAru-2$im|6x5s@8>%`LeRblj#bHru zU&{rO#c7SP+n?-m&CrE=tpva3T++qpxsrzn<=mNz6lPfmX)Msubc78iz$A_WFmxUT z^nw)l3KS_{k^dX@q2J2RsmbT%s(_M7ELsuu=3uRYk};K^Ff=3y|E!(L^reIod+Mx4 z;6>6F!>wUZG8a>~SCANbk>)q0sb7<>GQG}0!F|9K*R}uk`BcE`(-&Ewc=d^tu|xzT zHX>j6b;>F#a3%>fM86d(m_ExG|aMIswb5!bYeLHCL|; zO-IoJqpVqR_GnK^K;Oo7wms;yyxtC#FkxE>RE`{)CFG$7X2QMJDID$7X$)Jpk?6`) zig%*E;V05alndVgDek8`Nx;u_H!_TAO~oN)&p)!UZJx;07Fz#nJ02F9(~j{ZCJhgu zyD#Azn_aZhxJ?hWwFK!LV6up0Kl(wl?Q?yBuO{Q@W#4Np(0XLGp6hXcmjDWG4KAzo zuLF#mm_{pMsx$Gm9E>8K8kT*@DBp<9My0_`$5_n*j;HJ2@~}?;Dh*on;qz5XCh2G2 zigOgs^lg||lC$9sf6RK>wDLX6LW~m(D&QEyO00g7CexE;r3*jim`BHl(-ZcyU>;PU?P>g(z696HLbRCA+Cs^nf&p0r!&%u5uiYW0q^Puls_ItaSmxv-pc8>BAp)!wUcG;T-$$Kx0|@v6a|%q0Wv}F9Ew{KGl0Z~s zxX>y`XJryN6|aM-@Hq%I@(3=F)w=Xq5a_~r)_)X!@Sg#sboBpxp%U@t%_W~cnSzIH zH!Tp+d?ZD6X8ksh(RBAi4QK4Na|h>ek&r>~U&VdNU4$JmC5h+|Rng3%bwr0Vy)C(R zDK0FEPsElQ(CPMN6oehcb~$_?JsMZ;be0+O*`pw*SdyyB?nl)C;~BOn5T>U5;@B#4 z&789C^vVX{I;Rz2)d7T@PrCB()BI0L&Gm|Fx)z*7mpdMDZ^u`}qy+T69wP3y zPOF&|lKpHk;_TduhK}Z_A=-O}bm0El%nxusfy02r!L+qfPe9+xNE~QZ9&S!CtINeP zI>?54pBV-9h!{mHtaMftg8UYJNzdSiPlyFsskS09to%i)GxSiXEf61lXOQw3_PK6} z;~@PyYofI@MOj39E(Mt?jA;!oxxpf8Qq^Zc(`Ugev<6s>cJSkXb3?xPWtvk%E{m?U zp&$1tC>*;H0Q%qX*hu{jN@l1Hg{{Figy(YDJi%`2sLccWzos4v-+HbL(Lg{>X+S_I z{~6k307g?gV^`N4ZEqvB<@Rs?@u|)6X-E;`KI+La()AE()B!v!4jC{!h(S_?q%5;B zW^@Zmk^6@1)&c`o*Pn6KXFaMzKMe6uw zhy6o;P{`$$QgEf<0G*JByKqmHaL}0@d(?>^``sxRIf$~*G{VfALU)wf*?ND4MT6g9 zeyLMazy%|$wn)~c+oU|v_>4s_Hs(f#$UHI8V0_gqCC>UKkZ;mT9?lJ8AG|Kcn9g(= zV4AJ7ry8${5u%E@b9=@jJZ@c+l^#9F8i`D>I#gGrqOfSz7`M(Uo?MYBx5%QYu&PY4 zh-qFxDb9UZn?@V%I8I)dGEb{gYeCz^DthS>u4z&EB&R=OfA+S_Tn}e5WYGGAKE-}z9 zRpXN!HFv2!`?nd-JpbOk{b$;_3Z1)wwy^ODns&uxm`kGO+43Q+JR=qk$6h_IJ{sJR ziG>`qg}5X?4(#e^QNPo9(%9d)pX?~{VjS8G>CNOQY_J)-iH7-&&U<)-CLjRsbfGPS+- zU{qAywPAf}BC>^YTX4eS@e1VDaPZq1Y7V_L9=_QCM96^}OwN$@NwWEss1>Vx9y1O0 zuwYEstu*MtQih97$U`1h6gctL6{xn1X;@d-6mv=K1Kh9usqzY1-oh2Y#)1+391g3s z*ld>(H;y6k7$d!j8!$~QQ=US|v+{g%(@H{P&YjFQlcKCl;CKkn^!yg%Fw(%zwR=k`0#^AF>I4vE1f_iQ;9h zm5n|KJqY{lvs*zM%)7dTgy3*tA{Urkwd6NAi-BL zKz(w;)Db{7FkXAL$6zIc*G1!OjDATI-C9qWuG=MB2inFPqj~{a@;)lNYp@( zoJ!$L7F9%yG|*&uuO~D^hdVG)_2mq!2y?18tze`cJu6ZElkFAIv`CeBSy7(f_X@5x z#0pxr9pl=ckwJDJ4n~7_HBX*T8iF^>6bGqus%Y3DS;@zXGv|SZ$`~UB`C>6+{`s+$!P#E zy4(aM#6_@Ty-+TlObOw;S}K(Q%2mf4JD(&)ZN{k$x{I-hM0`YfM(0>YwiY!aZ&r9S zlNnRuw*WwXBey7(K~yxgl#O@&^Ilac1Z=(zH*i6Eg`4Rr2^s zR5L1|Gc%{O?9Qrj*&r6-pe2JyiRQI8EFNmIYcz6sQc}SuqR69pqQX3hey^9c8~iwp z;cVbk`NeMmQsy!|+E=q?zDv1jw>w$+tiCA`dbY9O^|Ly5!^#A2fwTtdYQ!Jx+o+x| z5PESWffE+s0=-^LbQwG$9FISGrci@q-MF4mFNScMzz{AbP9g(1#zp(BfYVH7Wu}RSfFc zqj-CN0{#)DsDnoMOOhSeA=YanNvByE$-gv(Xb-6&$06r%W$zBuP$Nm(Bq4n@fsxAa zh%0V@J|>6!o4V5o%4A{Bc_`~;&YAOX62s)qWEskOwoM04DzJEM9IR+t-Wat$xaF&on$8ox?-N44u{C3Yvd!Kr7yuDb;>kb zSv}d2?kz&ww-{4bYAx-% z!=5Rky|!ayw5OG65CeNsn?I}^6w2-EN{qpzb>Fh=GPU?oV&B`K4(z{9%@-#THv{nj z9NtR_qldnk{IzI~(MDS+Jb%I+qe2freHc*Atl&c{6xi1e9c_vtGU=h`DK;lVkr&z<6*`@wl%%7>JU*V^a1MQ;Pe0k@4|U zLKUW7WSr{BgrjF;Tuhothq5!SkSVamy$lK)G}X7=M5_bXp^J8F+()ItE;>A|R^RI2 z9J*0Vteooo`<&u!HLhO`GuUo3&Bz7Bcpq@wFD*|9wrw9lD)sqrrFMdpqTR&+rlsL( z29zqcw&5Bk>go$U<8yO2<3v^O>^7dchA_K{f8Oiux7dFu+mp#S5glmkFD>*}3dQ49Vu& zyMOgu@ScThR!2u?xk{{yWo#3`rUOz?0}l?1ul?`+RZbRntO<5Gd@YrcIkod(;0UATJ^e&6-eij3 zdJUY8f@hRKS>Mdx@u^+wSP5Rf6^LcQ*Hi7f9_e!KEI}_7D$>RuR^*BR0o$3u=}OUp z*`HXY5ekqho^Oe`r?vnRbj(G=A(|aLAUyEt_)nx#^0-d zqW|(b)ym)DCyvN})`*N*ahmp*uV;6ZRX|)HhOP{qG&BCmOI$<(5bUxy7ZC;Gmz7uk z?xuIgkJy#f_{RG3VrhG8tK@Xd?hw86$FcU(J-+woVfe}Eo6RT)0MT@2+C+rj^@h`a zuTJ&6sc3?<=nq>5CJPLfj_SYQZPmIW{3ENisAa6t>(R2bC0Y#{~9KH$P zSK$dnb*G3(7{w0-Odj)wCI)ZaMV%eN=MI+BdS=XSh0J#zDd+L^=0lr$kkw(E;x8>Y zyKlKLMfeMFn{z3Ul+4QStNJqcOdQ-l7=gjolJ<*5=Wia*WbK4VrxIl~24Vz=SG}_T z*wOo~ZL4@t=-O`8*mA-iy>g_GkW@2!DLpU$C!WSv>E~Gmpm;lE(?G6>PT^bdcS89@ z(bAJQlCclgrGUik9l1wy@gP3zwtA*`1qF2-!?`#KF6I&TbG{hr?}vP~mV>G;K}X~X z53-$O`AS)3rzSkcI#Cr!>KDUyHp5&czk6k7%_^w0&S=t(-1%IK--qHm8WI|% zI)IU{4s!T%07mg*UB)csRewJTZR8nh|IH{AOvQKQiQ_Pwh-l`pNTpbH?jP9q?RcQo z#x8onjO<2yE3B9+ELSTB!ghVhK#cG3STDridz!_5$gwZbnNDEStO%ts;?Z(Ht6t_Q z#n;F!i}cS>lib(nvrrO|+{TvINV&*pkx3}yZ$4@40I8O96imBt-k7Un*A4oDeMqk` ztCM9w;kbj+YWmXBs831eBo}WdA)?)T1-SvP`rqBqECYCp)EUOJDp*+x2M)=%@_z6g zL-h&GzKNn0cs1Hg&(qQmQ8#Erq&X6}7cB-hPb3{Ye}!B4V;!50HP%z61fQOEjV(60 zU%`7q0N1QsWn+%i$3;`Uqe-U@%Kq^JM?v0Wd~s-qJa`^fYn~WQ>@jzF9R3%|7>%9? z9DX(@yfTq-M*XM~2UOe)=P>*irRMF3YVYfS;DClbD=bM26|3RevIPAPFCmOS;(Lzr zq9hCj;Z95cW?hV&`Ls9p`u27B!-%rNfEAr1fQCmnMF3BLtK>tr_iQ|N0iyHq%w~(f9+J z6*iMSYa);<#Gb<6lf={%(Z4&bNAoUMe4L6@Xm1++l`k;lP?$$mt0OmtM820xCM>W23-d~bAH@Zi$f+5*VX!a6j@XmwD zf-8lfk6_;c^VZ7&e9v_obgGU{7WAJ{lF#7NkkG2Dc0}7Wr7V6Ui*ty%EUPtS+Ek%o zhm{-)Sr8gUAx$YeZiY)D+|+*f;)13J(5Ny1+i(vP#H@v8UO#pRD--38nCf7uaO^PJ zeTaI7o?C0PxHY1KHuVzL&qlEyI-Ju8Mb2%Z-9Z_-sjQ@P`q z26`35qW~~{Vwy(&tTodH6NP8>(9neLWQN$$iGuqNyyE6Sy4~9Es8QQrGH?|xzd!{? z6j9j#ltmU4PpB-JJ2uD`xaP_Ybv_BHbByo60Z^7qpMfcC{ z_Xnd{iI~PoKeW;g%(a8^P$mrkiG(i|n<1JqVRYsW)`dcri`>O`Aul1y^+@wRm@esFfgrU>a z-&?t;QT*oeiB^1;4~R;%^`~n*3tiY}%0+~sWAiy^1*`JrH%DzTNtV2HABZwE;dtY& zu)3l~#W|^~09nwP=+wTcLt`vK*UJgA^pZJE%^!8>M}H*k1ng$7hjqfIJ9Jiqld$*3 zg#g~SB-M&_Ma&G!lIraMstaKS8d00UOci%d!%ClZ_zuOcVVpl}5!Od=^whD~j?S}f zG$(waS%&vZ0l#0Kh;A>rv_f}GSOO$Jzv;ppORAA}`T^x^QOB=!KroGK{WlfkHIv#; zMcH`q!+N^Un8p*pq=qyLQmrXYg>el*m<4W-MAdhw%UapY^{;2Z*5>NSJF$Nmy)&@5 z_2DM?LPj&lcEG@nfx&6I+C`W6S|LTX$3Sl-;fGXlUgsAv9&&!9cxG_c-@~qTK!A`= zN{f!~Tq~6@`h{ZC;&9VJseu}2`PnN7=W?13b9z9E9K9@Pa) zwBN2GPOQ+q+bcW({u$Z2y|YGt~QwX8xGE=t*k=zG&Veht**Q zsp(EChc!aAY-(?arYX8d?KO>GwfW#i2YOstjrk`PVSh^eo?ti_T_$dl>Rs21V`;6w zie|lfoGRr^K=F>gvBn?V%aQN$oc0&MEdVLH>CKQm8~Yp3doaTqnuF`ztEiuWrOg(| z)Y}bBLWEH30Bp?bOZk|FMhyUC0KZo7b{jvy>ZCVKbh<&t8vilh{{*)ZfI6H&s?p@K zH9)7IVjpv<*XrDwW5-0*S!37MaB1QjCOevHSGWJJ)vDRl9e(NILNxOPZ&zpTn?ncB zw{}JT`DlmSYejODctfuBc1ekxBo&HHO@PC<|{6pNPubGbjmEviiTliQ0 z#nbUo{-uQ+T`ZX_U5w4_%$eNXtnHY<|C0^^;QoIsTRXU!yEqvCXWxG&Ji9tEtpfhV zfN-Gx8Djr`2X(BSRPC&t{>Ls0Qvl)(6+Y$O}j@PbhpD5}6ClEz5TAr2!4utSPi zS<8E+hf)?tN3wB?nvbWB6rYx6eMPM#b!|t44Qp7O$wXz$!ldU`m|uso4-5ONpHFd z3#-!UvDoVf^M>c5r^j$YH$>V6&}}Z1)-m{*77on;#3fXwS#S=}ClT6Ty+oNWn8u~( zIc?5Kz57;07r0A+1B2$dQVgCcJS{Y;;qxllY9TfQTq3CKDG1=WG>KyT2pZhi;#hM(sXu5g; zlH4_|R%#X2%No1m+$=u zr5DcGOF)zKtNC!r5-!&Zf&YiAbBfX&YS(SK%eHOXwz_QFw!SXgUAAr8wr$(i>HQz> zGe*Y9Rqm1}Yvq~m3^<>*B#ZU9OP-qg#$u+*ov7l(B6%R-sk~wF@H*omgO@6j0Gqfx z%m$1W$=2oCMR`X)sgwz|LU>DVzg?15c!NlW*Bg-)%va{9YwnU6aW6kY<}O+C*mIrj zYKx!mesJV5>|s_PUv>tyii8xrk)`TM?+;&z2myb7lYlb(K+%#Nk3yK@`Y#9M67Cdr z1G8o%dkkLy59T+@a75z7SMX=VUXuIV56JP(w*8@@iQ>x~e7o&Ft0Lw_1ix@POJl5Q z_a#{0?5MkeAGy-H<+yd9mC?L2gcc<$(>d4BrNiIbD{|Z!EE~fecnJ48z%R3F^}{H0 z@6!=Vn%Pry2lP;f7?T_)sM8*{wIP%8NGSoi2FD}-xnL|Hbdtf_Boh>DS#;aunCq9e zL%PbZ6NO@&`^b5Nz%Ro@k2Xaih>AZ=uU2qWIDTE zcRf6;ybu6o8wvC>+g0tm{o!f7Fy0v9Barh?GGDiN(aBx5%7OF<^(+Ug9$qiyShy2h zb9ahkS_pd(HKUk6XU?YJSEnj)}hqDJf$aJCFsSeT}lohBwFhS$$U^125b zJJ2?r@mOZnd;j)AB}^2R%j~tI;ZGzmEqpjFnpUw#H=Tzq=x$ht^}c%fn`T8*`^@lH zIto{v`iBGx?90arMU@!5c^bt@p?>u#UaG#6@Is?aG5fuh!01;8kxwNwz-C(O+|0=; zA-;YqaQYW>Rcsd`!mUEC@7^%@IpA+MUS?Sk%Ia%JSv`?fKKL_(Mhab~RpPL-e+?PS z28^*SLriryCo?dB77xZD<)Z3g`42hoHwArcj&*qGgP}{fo+yn)3eOHm4T$O0b+1zAG_W5=AF`_7Wv-vbAVf$x0h8f@6DuKRkFfFw!?G817 zvby?9niMJ29f);S+M0JxSI^f~!^@b!&(}9*FV!b0$)Ccg#8yf}_AnV_`iazmPFMnT z(oQ-65gAF87?!$&>M&a;NirFhP@$>L2wWZQFGpp`Z8&0*z2qP!tSpWAJN#|?5nCU^ zy#Yd%JquM@&ALNU2P|J=FHCFXd)V<#LkF6&ismWXkY&rM*-R`5eb(m5-QVe4(+-S9 zSfA2mwc}M6=jHMV3geUI@AX!e`yIrOJQvu2x3D5vnw(K>EBTXf?_@1nCmP2-R=kb8 zSTR+W`4}s%&2%Hk)%_$x#`c8v0=rTuUAFu1Mq^8Hzwty1v;BnP2P%wMt0SuZc=BP` zdJLy&TEt|~rwpuh-m3sBr1wfoPM-zG1^G0qm_)|QE5_Eu*1EstisApdpsNT>_?1C` zn;3E9EQ6Ez)+E#8BBq9q+=(W|-r4BA2A`w$5VdDkv|WjW*=ToQ>-e^=N+cSKryI*e zWQ-YhbC4_bRJz%rlGWH~wey5rHN$$ZHVH~Zx4HZquFjf$Zk665ZNIISHY&4iawRQ> zv{9KR(MT{TI4GtHV>Cy2K$lLkbVvaJ?e!l?-yqdcctRXCg~DVU9y;nT)9L^*&Am- zO}S43l?;Lt6Of<*QA@O!9Ux~z^@^E;*x}MV(pA8!H^hDIj~YbzikdUWg?0k)FSVsC z3z%`#@5g}d9#lu}R=b6RsBobERK7s^B7SUv?j8UI-#GwgTY`CB+zkpKxFs35g|W-E zTNlw(E)t2x6CW^~YJ{eCy;wW_{;sfHiK2`&*IU&rp~PW(9sLh6>tumth3x+&wti# zXbeOF~wXkQo7&WxEpKoDV2}SWast#cnlj`nJoZ({t_!*hrE%5Sd!1%EEKuISEg!zI9ymDYyB`TAx*SmQ3g>gL>Rt|PDfCQC6%VBIzkV13_WIiinp?U-1eP!So${ckg*4|5Nb^!;h^3N!36CE^pDcZh+Hr@mp^ zbGnbF&2#$VCQRQL-21U#J%SM*@_a_Io5&H3-jG0);cn3Q$!g;eGT2_#MbJbAimzxDyS^r8? zhN)$dwaGQ>x;ZxZf5Z9SmNEFwQIs%ryZq}iWIu{ozN|l=@>$$?Uy%J=pgHjp$J#hv zHl4d~I!-RP%x3^TAiH$`VEynHt=SMJ!{OkuGn461N1+k1P68q56u{uPDV{nbQXgqC z>mi#@E=dG}NJ=uH05^0h`f+&(7TgN!By*5Wi`Egoi2B@9`C7TTnoYLu%jA;kwQ1yh znlYq$>r_SY4AwM$qvO1;yUj|a&dX-`$i-8>>Ps7mNNkOl&y*9+(-z+*jq`}P0eHWF zJy$Xye$VP15~K!<3sBpOnCU<>*wkdf^{HiSn&$m*TrWoxp0CY*1-{TQ_;YMDTchb3 zk}c&oVYB*en1&S@>+$yl3$ge;A~WCb>auae?Vakin-&*~9haCEPDOrJuM0A&1Aii5e( zh5L3nfV2*|IS7}a^Z69{b_?Vj2Ih@z+YeieX?N&7%%iNmw*w0G+6Mw+(U2hVBWR=8 z-Zz#b6Kui*Q$G=;T7-!!H1=B0Y5h+|?KhF@%-jrShz5``E#;bN7)#z&JS4E zCZWCHG{jLiQF?_!!X3L-@kxc$n`WXDf3>+6;GKOAdF5)oG+l{c7nTX8c+uWL$w9tM zdp6vy^M{ETnAS{*I%{8~F{VO*sC}`_j`;a89*kfJGpaah1rb66lW94?uGlz8jB#8` zN*I+3eE>C5hUz3tCu-DOsx$((YeoFFm{!(LT~9B<$cbE(N=XHC{1gpNJji7ZZf=oH zS)yFjg;`JW{rYLsInko)VF^9PmgsKF?o5Oj$C{^b@g&_ss*_|6q)=A0J96bHBZZy- zRVweB;lD4@#H|!Z^Z#v|{r=?(>;K6Y@MTOulB%tzPf|mc)l)TrSmwjhj-6O&q7M`4<8Lq$?EZ`5;FNG0=Z(xDf@V!gh zb*Xvzh%A?v%&n75z7sC<7k;nrg&83E0+U@z@dDON_SsPPBro!am)q_He#Tp(X>3fu zKx>RML_Rd)kOWeu`a`gt&R|fC3=%4F>Z0SCAVM{!$ z;?MpygA5>fl0&CvvC+H@eqc&fEXCYYQ#!wTY7=rRE7b^aqA zA5oMVb7fJfRse$63AU`#NIoJ(idFwpLx34`oju#k zG#f7!q9CnEP-2&!sJ~>A6on38r-@i(=UduTp$jF|j#J4zoY=*LpPHW9)G*_Gs`tyt zCoRV~iD+EJMXN%L;JAR@I?umkcf5h`Zkmd9HwZsxkX2Tz7@Ol|vv~gFZnWV$)pCiu zh49o*86;vSuH+rNjU!j^w7B)Kl~o+k#936Xs3fY>Fp$>NG5@HI%bpAf1Kpvkgt6~` z7$oPDEqj#qPxs4lP=h`rgPrHul(1)u99g0xrUH}Gr9MAgk>U|a7jKBidI#1rgUPd( zT3{43#xVRd;t?p0KMEngOP4-S-hhMuO-3i{Os6dI%7gn3fi9)%dO9@uGK)`mk2rTk zhbiLfLcs~=I_NiO2-gG1HMe?IkGcYlZ0RqS@DowrAAJ-dO%9bf_b`R&dZAiXzxf4K zX7GCs1N=2h*hiQkuH+HUE2QBSwBi-T{T4&^GaC9EDq&2ax8ihY!vu}^k*-q&nIyxp zP*N#q5xcDT><6;(gwgfU=I0wUHjoq7ijh$Yf@nzH%mT*dA2Ti5f&^D0ja_V2D~K(` zUMfsqJ9!{Sc|%mbiMeKpO8+%Pf~rSQ%<$av|2aECYgOL2{()hC{()gc|9PPGmf-On1YB8(dRc+?a#|;e>``uJZtWJzw6n)3!H4PB{0puyni%(PPU!+oba4% zq$Iws-{g45hb7<7+?NIUo#e%yz5wtihnX0|{n4f$;xh2?y&|%}FOA%R`NkqJRe~R2 zB#mQ319S+@w1D~gf}t^>!a&{cS(#8H^F$46LZv<1H6{@UWQB57jx_fdZIXPUXYOWs z`NsyF-%JqjPCSiL8D>9?IEO@Reaibws5*N^B0cj$(eH>6x&|VL+|n?|RRPtvv)VdT zA=FGNk$K{V^{7KwQ1y!M)9|&XCv-`_(NNHCq4m!4INoTJE_iijDhBhG zKP`F>k8u84efsvgfV z-`hDl#P)lmZI2W-g$#%{QWcIEiAPy}YZ(j1hE6uc^X(~!-t3@8!ve&kH7e;aS>Onn zJ%QId?BzCbnfuLZe{+xk$zPnUuFka;8GjrfR|{I3|C(KQJMVZ}kHg2WgiD<>@Tko$ ztEEDYN%LCWtPI@`8BbxP)7aRoFD|L__LgvdNuI8b-ssTY$l&pAZ)s_1Zfb%^&-mAp zL!`DY2{pE+mgUx!6ak9bti5wxtyu(32UQx&Dx7uIoe68ZEcsp?AZb|IMB@8|`fjCc z<4+B)3{-9qu$sK{-$%ocs%#T845a$nMF`jt;Zkkc^Xm*qCslZvrEbr4UVJjM9h@C) z2#yC>vKk*jxy3R<>LKBZt-Dp@b32T#2dADkeGwQNqoI58t~yF zn&4d0MT8{1?p4M?X5@bKr?tSX8{UccwiN6P5{fZWOetBq6!rPXK;>k|W(WGc9ITzou+V0n@mXB#qiA4R&!NAHl8^O-^ z?7FGo!swR#LT|M>)#BnIZ8%Ce;5P zI@1TZ5^{#AaMT5iMqN;%NLuww;Y!C}&5hbM@#$7Q4*~ilZDkluB)4e106mC6RE#)i zrujQzX??Z6?uyTvV9pQQ{Kld-`wor?X4b2$`G$N!pm(WAx-1r}Nzjg6B>aqo<$~MI z;)w7gp&hV&CjMAVPYM}G_^hGKcFgaIgxh}>(`d=%;#&#*X`yug)U2i1wB&JyAe|UM z<1&Y3CjhqLMc>`9inI2VCqZY3So|!dd#78ukU8=^MF@u7^cXXxEmv*f%ddNfQEn-6YYgIM<@|lVomC_l~5^wk*-~otCfw-BOH)_U)1b|qZL8ujnknTso zincsb%bJ;H#_4KjZ2lRmHl-FCJBti;SvZxl++W;W9!J-4oH zsai1>X;repoqvVsR@}>A{bt>%yrc7}7zX6*ZKM{TP@Z7#Ot)BmYQ8HR@C;tcf8%1$ zQ4P+Xs=Y^vx6CzRZ^<*7exZ)S@aIYW8u;{#=9h7|aN*d>oojG`#W@1J3c?>EL4m~n{02m-jB0EElM59OoMhdmfcjoV43umpbuS(z_aUd8yS zLsAffzk%_%&uShrmb1Q@>mJ|^=k_$(cCRuC z%WvLB!u3yETN$6s2OkZ}-2wsR+clwikEwH^5Pdn*=K+%SVSimSKAE$%M86_7G-^eu=^l$ha5!%E9Ga*u)r>XFo181 zZi$BN8|jA=hQZUtE5@tu5M#EB%{x?tjVEC5JH4sBEW^WzRda^q%$TILw^74d)R;7z z8^h#X7rtxw(>vgJ7S#=A_;WApEOtmjoS1Oo=9dk@eKla<*zI)78ekfa%@e6{R>X?a zI#$izUj)d%>maT+eJfqF#XyM}EOF=;wwL~BFHi15T zegrteQjXp^$RXxAlJu&wR_;0-Q$89bJ6%?Agl>QaF8p1iZ3uFKLB`_5H=nR)ZQet= z9ApPyKCuO9O5W%I0q`=Dx0rCsy3VsOiG9M&N^8g(edc>>5BUQbC|hivV!FItX_cTE27{LzdFe*FWZBktAr8u4 zonBU}4BW8cITCi7uAVH%&yAJv@Er}Pmq1SGz$DZ7g*+tOBv(-3w>aAMA7L1vUp>%ltT-< z_2iHf&|o{CHmOa{w{8yD#Q%!~dlPdR@HIW27iFv5p++qL6U=taa>NkfEJf)T%-T|E zN!^2@7wf=$aIieWY~X#3=K?7Xt$9E^FBeKB zCuv@&jc{OL-0O$F6hvOyw4#$7X;VOiWf5u9BzGLKYQldGqeNsDq8yYKUfEa!1c2z8 zKVIK;F_Y6vOv1ccJmC}M5lfRg9#QPBJr_B+EJOUVqNZ;UbQ-cs&=_m6`fWF_V0shK zBP1G+#f1NzIyJ(+e92*(%Du*K>Z<(U#&mWiP}kYD1b{#(L!sECn3t?mYk0TluUl11 zK3~oCW19OAUYv`H-umb3{_TTRYXI?y#G&dbRB$$KiIWrc>sGo3b=8kde)X|^qYd;JSZ&t6OY0(9ICL-h1{lDE4A&o$ zBcS90NCp=vDK?xR`O%a5H7~9Dr>LoA>pwL6_D5(1hgGG#q6;+T2y;=;Ie-Vmsmj|n zcqH{GMN50rCCVvo(FPi`c7%9@QRn$ghJ2qWy4-H~hNrmPB(qtF!5MKaSz8skt41(( zWTmQ>(xO7G^aLwQ>GC3~vgG1IEh`x^v;K=stb4ktgqw?3&t&*DA%oqvy*=?8>F0XnsKlRceu8DQ!H{IPDa+D)?`$Fskl&M=Z5 z7o}D6_)Z$+Wzw@$P~5IE!3x`!XQd7F{0K4gk#Ea?Pt3GG81d?=UT~xL4j&RQl`$VM z5(aU-^PuiRcbr=TFLaVZErkNmm)k}x6mKx;uEF=}6{&A-+fY;#PXLvrk3_&K{XVL$ z5i@61&s0$5cU*b!`Tkqvpp8iojVOUM^^$rD2X*YX3!S7F#CZ^P_Wb#qg7NA$s&f@{#+cMle+XY?44!E*z~X4d97SawGB*xqUG_4=HM% zgS^U(+zVDT7_#6DDKZQb{O^GaDwi7g5zHA* zSgMZ$aXv7^6rlxmRXd#Pam2s248_+(5$!E?KV8?pCkWi(?8SpdWK!p+talPGdm4-ghyeM5#L7#s`Zbf_0lq-cZJQue1B;>M)~&#QUJ^Iz+IrFpmBR1hhd+c7u> z1DLQQ_wMush|Jc3cR(+l+?)|GUI^#Eft&S9*h!teqnD{a?nx5=NdAD~2Zzv5+2bDK z%|{~%_MkE$;)lL)DiZ>cqDuIQ^)&OnK~)AZWtdb&jcY;4HBOk)wq2HTi$BCm{I2JN zWUFawr57@5IuiP|XMYz+MNpsKk`iAY1L(`q6O#5#5=EB!kXM$@CO_MO-86lisej81 zC4CYacUN!~$cL!7=s&iPYoX1ds`F0gA+6|0tZW`T%C-@DE(VLA@EtSFQm$r@s?v4x zwY0i=)>EsLmgpOf60GUX!8#4@0QcJBSI{R&Y^;zS)LZ1BZb}fSN<{yIQMFjEd-^rhH;Gb_gk(KQX_5a!r z9D3-O+5SNbDKLpl?392cwKHW@b;KV&H$wwlv_29KWHLidfACn*{dhvjL7@BzqV&|s zY5tUWLo*f@n)K98f++KIr)yM@{2&_!QTi8G%v=7Eg1JU4s2552BMZ?s1=S~2Rek5s)u`HH3W1m4n zA2=Fls?uCwBrN^Xo+j@|#{_lu2+U+Yi;Q%zeZN~K0)jf)BxNn? zB=n;GLKXT1lgmYZ8XhAZRjuJ^xu4wcRQZ6r0+5ST3R^F~o-=4xdc*3p@wZ~**pB7; zIJ&RF*Eb>L^+AA5h_OBs3zxb%zkf5)$P_7ab#}9f(ezS-<{3HpKvT`%q(kdZ%LVH* zvMT~NWA5FNkXu;*!!zBc_^9RN;KF$y!5qIr&bBr z8`GJSX=+*t)wtD`sROS5k|it!dk_a%9#R7ntz~;?5H-wKY@OA6aGn3uT%A?BcyKrS zd~i1hpx^{nuaE@RuR(1BL*~%@E4Sd?Dz`}?HFtpM5PL^u1Mj)W2d)hc^CPF_HF7GC zsI09vtsaG(O4*LyYSjn&+4n!X!Yw_eK5HCKpWpW?A_Gb73?G&zkdG>zMM*a+<94xw zuj5rSk^q$xp#EwFNP;=@qw#Fmi&2yS*9}YmnANV47im=L-vLeCC<$QCL)6hx%wmV@ z+zWsLBq=QSO&tFYjIs8!U_U!j0dM7O<0Bug@{fhTm|Kj65+c=+!#ZYD2Nk?gY?}`O zYj*4#-RWyiQZZ&y&p;Du)uyIvNlmnt^M`OVDUYaPg)%b}$)?ka5tAjah5Xw4PeRQ; zK2ymP+WB<>aOZ`z#^_HE$X zEG^x;M;fP1wlml8qzoJFHwEh=52pG7T+I<|Vwh_)k0psi+{9T~0-O)R*?{wRFZ@xU zs;c0mVW--86L_`s^~WpJJbeme(j~DDCY8L9<>S|H&oGY<-tv9Chp@qn{D-jNjB>z< zBA&kB)XboAuw4KfDb(Wv2WD-O1!-g;CoC9GGtp_zF=bXfxFgn*|5S&2uIvy%L>0fu zU4f$sh;4BBD37g@B5ouE-0LhHd#vCaJ?3)8c!ACZMTn7!*Fr<|z~O_KeMgv%PTTG$ zpsLYs;(%!1KAqMjk=Ls@T za%E5CckvYi{GtV=b<&Kz}Q|HVqo73K=$ohk5%ql0}A#EbAuDzh`g-{ zE&VO{MeyI-#yXRd1+RY^R*Zj;?tcMcc!?+cXn=pKN&>q=blyvByB#h+0ede(Rg*1LI_AIbx7c^DjHh-A|nE_?6nosUkw zlCOuC5e6U_4fZ%O&5$(QU`?#+2^V<@v6u%4Cym*>30mOWJPgToyY)u@wrUwg09#6E z9we5U48_uds(^)79n^(LuQnZ$22|0iTmkobdXJQtcts3_UegOUvBv;TwbRkYX4OM) zyr+}m4i9TpsO?5^kYSNa=YUYTIRQ^L3V_cr_0s>*vix7_*oIQWEA=__w?k&qN!TzK z$}HGp-8Y48bX;^Mv6?z8NjY{a8K%-@VbN=r#U{ZU2!W_U06pSIi_GSORl&n@`vt%o zP9)JsBB&}oZ1~fe%uU~=Cqkm6IIImqPNy-(a~K+|4!Um6)(tYciVKie@0P&YxwL8Ed;?P~8iPIHpZ zd50=H2hv*womupPBO?q)$v=?BD-jG|Fp1m}6X3{$;Kt`!W0cl7AezU^_> z_t>tUQxv-yk28d9pMRs__Rv+MJ=OXr;;ttBe$PbH%R{t~g$}widJ4;$c@by;9Fg2V z!WPU?c6iqBVqSTaO2Qwcd0iXVwv61;Ik!&|A{H=}JAM<;2F!|of&RNc(ZK%i%m2iZ zQ)uZQcYeW^7$Zah_)lxLByciyPoDV8gs8U`UXs+rR03I41er7yOjZL{!!)6jv>-9f z(F|m;$+axsqH0;I(e*;O3a63HUrEfmV!7F>YU|R<`o;H6tI4YSr|o7+93trZJa+SC zTX&jo;)nJnW~S$(h$74)zIjfDtWzr;!V4| z{X7q|xqqZ)_b`9Zsh1ANopVqGVwWO@$8GIqj&If zvcT{PI0A){;&G<$lQ_-cT_*SX$^nq{vWssi z?etxpXt}r5gMN?@E!{AUjrDqk&k`Wd^6q+c>;@=v-`wAh9KAs?>`c9Qyy{`K{l;w7 zC-<8l$hp5Sd1I-+zP66)$VI1uaQ(G!J#I}ZfVOK0%# zWCkdnj2v=Z775|cz2&C3g})<=J;==4EWOpH^i0OeJOriCpAaDs+}$SX`9<_3jQoC3 z4+iAac)ntJ^3!DSmOQL$PJ&?_<1*d3p<)^%Op|xcaTh z5X6JG+_{@9(Gd5#cj=H=ZA3%Z?<~B1$cj62WxpFil!j^no3Hz)waQo0c{+lc)#ldohSf|ge@P1NzDD2bKw((96a2s)|YmbjdCZ$h^Y7u z{jD~wbZv1viYk~0sgB7wds;x$ePV7)R%Tl{!+E5#AHJP&4l}BV{r7}f=~N3u!983S zFoKy%vQSTOmsx&?Kt|w{>A&(Xn9J%ihz%Yr3MRwscPG;kVJ8}(7*lg@@EMSoE)y_^ zU7Nm-^qFFE-cld{gm8t43*_PrY5?r=m>TXF4syVa?0+4p1*#ZKoTVS!6BW-GIJiau zpJvl`aso}YJt)XpWX2Jk#mRB0=ZB(DDOF592zJqd_QV?@h}j<<+x8vu(l0tu?GQ^Dm6pdg87t0NrHpff;G zp)lpSRbV|E{4O}Ip4>K8&6*ZiexpFF;XuA%UkNLUA=5(!^sj;ipGNy@c%nt z%9k?H)D=se=ff2C7-f9TEEt+Jl?Xv;?f6@l+Gtym7w+eb@g5i^(*=lVWe3`vszo1} zy6)3pWKvm(7>%f1$Gl`o(!=dS?q{|^ih(Dujl{x?lbvG=8by-?OT8D|UdIc!ft9HE z6-d+tnRSX1t=zi2ZXFGq?d_bj?eXT$;&3?q!gVPoUBSaXG(*;ATwjpxkL&S>~DzZe0o# zT7vcO=Y~p(EHnU$(J<<|UA^R390jBC5>iC6HjSJ)hBd{~a#$v=Ks#j*hhEtt$I$g1 zs=>P51QXmG-ee@68v1cLE7PGmjTDcU^ywa;<93p{yeNlW72RYO*+X>6Oc5C2EO>BY z2}zX;I7-(U-C6@+W&P>FWZKN{6ei(~Cc*UJd0J`bI#EDtSQabBPaw6(Mw3Q5nWbJb zG=nNdUu4{!Tj{Rx!DO~N-2&`%2?5rl>U+A7{k%2=-*^w$S7Ldm>D&bbpKL5^Itv*2&K3Lkq-D48Z=D;Pr(vA;vf%Sok^?;wStER*yxD>6PJAJt)qY zf&JC~k_B*@0_LU{tn=yqT_JQcUn;aifg4eb-@(puD&|GSC9+rI%a{oXpIV0HkYjAZ zVlrcr&*=S`<~hvZxMqJV@R~FKNwB$25cM&D^b;dUxq9#m!%Dgd@~ft>ligvrZOJj| z8t%L0^cpVySkVtf#VN&*5|tUv%+)A7^;OYtVi)l92c(CNoPJudipY>d6hee=mtjx6 zsJKS}@#du<_wM2|rtjvSGT)`CIzF%N_M#8{PO^2`-Xc9Nn@;QBRW*6#Qi*DNNHVxa z@+AUD{vnx{%&a9L!*D28?Z?1*;^E`7EH2n0{ZWGVo{>f!iJck4lgc(!DwqLd;uv(Ru=k~Hj%;acI!>0R zmdb|ww&{6tDPPSwMYAckXP!lOp5NJFSn9<4kjao>1K)xklta8mZcRO7@?XTR4E6Cl zo57z1*!K*wFq&|-MN#(-Pd4Q?;+?KUnIZqAoENRKj%c(L}#twmqC;`5))CQ80yDR_r++gJ6*A+^$qD<06@;jVGBd;JBc7sEf^UzqPc>0temt34QGQo%$No*Vy%DnP7YyB91rv%=QC;yq_Oxkx%uD`x3CYtgKI91KFQ5()O*cpzC=>N!f zzw(Jw`7jUF@EGK@ojdD+QOXdl^j>qukXd2(M(mPb;15b?J4NtGUjm}M|Aw1#*~@SC zwUTvGAW03gw$xIWOqAoU02!AR#x`p{*cQid^pIlswz&mi;(!p>3}wXFUqE_~d+#J0 z_&Rze`#Mu};kWHrE0(yZyHg^MIBUVn6xs7UdE~BaqQVeAC*!#mxw4-6j&z2n4Cock z2y0p2Z;RlTb<^NO!2!r+fmu>vigkRIG>?77n88NVFkP)O+F-t~VJjq><&-HMv`;>b zGX>}Gr>J?hk>vdsHciFEPvwyZhar{uJ=FP;u7}ZKV&USAx9hov5Rp}ztFmL?cla=` zpddTAxhn+^uIoTE8f>~Zk4;6H$9$!$-oi)MAIWs2G@b2y4giH)6wuo?@Vj$O|VE}6Nev$K_zDg12Q|LMR4rQk~eQLR)9C(_~- zB=KOv!i4J)UQHzaD8Z%n0V+du{(X$lrK|0Nv~c$Yc2mp&MRe0nNajPSYZUYm8=aU% z+MlIBaf^q`l>pi$M5y3dTiv0fG$TA7x#Xi(;>kpqA+}{UL!w&eM##55599jJP%qto8jdl^qC}SZ-_Kq5 ze>XX#>UYKyg?CbA#-aky&_eIq9o@*K{LDuXE7Vc3WC6>mYo<4d=WoC?kT$ikh_WTw z3fYo(##plT%{r-e=aOIB8B7W#xSeG6C|oRrdE9Lh$C;Vl9tjj8K`*l1*zcfq@IW)7 zn@WVvA*eCZN{nO02NV73&r=6rqWjB|lQz(4ptEGn<0)Y_0J9ZAtG0PFPQ(Q0Rua;c zWbg!?1i({m(&i>8hGxoRH!I=rCh@Y#InhP^y~eH6l_)B^C3}(r1=87Jf%mDNQ9FV@ z_Ezl8Z!@2iy$|Kbv&9$lQc3#GTRl>Dz4JFhzQSyT1riT*=mC8TAZ%mSmC2epS zY5+}~LJe){UuTG^4s#&y$aoOJcdW79vJ$=PqN#gQUeNurMV60foo%OET?&?BN04&#HQ0Gn zNAgUX=b%rqhEPlRW}YGodcBY`mmqmQrTuQ_$8peISKO+i21{;IZ$&ZNjhD1{p2mIG)*6!(Ce zm|@?Fd5ztJ@&`-}{`n1gzYf39SqL!m=O$3mQb}IC_|!Tvr2h9Atk}sLT92l_SOugQ zL>}{vQPp3`wM3)EEC_wE-S^s1OQT}KIR5vJ{zct3uY^@rubbX{u8d&iIUyZd27Lo0 z!ovL|R!N@vYy&ad_y>_DkXe*)0;MeZZ;VM#sKV&{q!RD|t=`T1?~2`U^#y>Y!i`ru ze1*I5SF}b04vk$<+Ka)G9OK=T3x_XcjAJCT8xJf`3Ofx6cbylLuXdXRP~EQSJ3Oo# zR^~OAmzY=pg%XfQzZ-`q3Z;dHb67VFHN5HSQDL-^u`bqqa3LL?vsyS=4+k_47fWc7 zdeocmyFZ?|9vIWwv(Ax4(+A)*vsreis?q1jrEZOyHR<`IZV47KGakE=+er{=7lOpK zncEyBMU+5nNB6V~wDk+Ke_wB1)O{ZP-GiFaYY$^)4gqa_Mvm%e^RIelg^!SO`6x-1 zW=Z>xffRCsE2dYHpUKFzeLxggyXz4ooO!bve!f?6bGb+mwhsf{fD8aMP|uOQ7Tb0; zGdo!=ZoScT=d|J1BL5jA0Abx53G612W#_kboM(ExZ9@N~_~>gLbnGq>*(5g^$UOpc7YHD`~JEq_gV3Ri4FyN%3?=GgKbHB>bO^`95H%`INyYLsM~4VR)iBz_e4oVfvJunSxt;sBV#<{OfN zWikf-6{N7mIWwAk^s`~r{c^8;zjCU=xpL%{eH!?AaT*|7r)`-xe)GNn!}ZRglfTtm z+SQJC-E%1>1@m79b|9Els(D65URqY`QT;)=kQ}Itz-1kXW8To2UJ#oe^r#9DXtd(b zI`Ei_`<~?4aJ_&RtuO#Q^D+D3q~Bp!B+^o790C8I?h7DO#VgNVAELz*-;}fCnMr{0pLf3X+-U}YzA*bkxb#mx?=C88= zb0*6V#Y7XrT7jdKl5*N7GW7c|2Aj@ujpLg*jFFyEx+0UEI^8CrN{$*U$l zR%IF>Y+g_D&l37IEt1e}q(@IKRd{AUa zx%rqU(DX{Mm7K$3mwNZmMue9OnYx%niSqyG>nfn4SpW92NcYl6cZZ~ubcb{Z($d}K z(jg(Tq`=akbW1l#NlSMlARsL$-^#t-EBAlSH|Ok`v%mLw-kAB#oHOst`#fv*vW3M( zxf2JjuhMJTnJj3fp*}Df0+$Dhoj+B4FgA<|{Vr#vF11irxbWLkkl;BwToH!S*!RIC z>f_Ys?*&uwZlLP#9wIP;n1H z@-RIR%GB%Qxq1Dok~8O&?<;;k>=RAL8It6wbl2FO7gekMhd~$A8~*+&LB9R(zU_wZ zNW|0`tcOkisA0BWv#F33%g3zW?0O1D;OvqjvP3mMmJGl*ZXs7H@480xC{$_zw$hMi z7oVnUN;cV_0?iTlN6X+IMcL&tDBGXWDjO=4`Qd2_`pP(4jUL3m5&;#>1}j@{J=`$u z$nJf9v&q06|AJ0Brx2%u9W-BjX^1D0xwMT2GZ2dcH^kXAAp5Y(aHh!3dxH$SA-f9aW3~H*d)_3A?Gw8M z&C9*qJ04JZ&tulc*On87-kAQO+sw~oOwQOeKZP;p)@d-k?!pwzSwV*a-uVRdd$6}V zs7TFJHy~d(>>v&$4s^wIv%jSty& zK_s`Gf*3o(#EHqV`V`oyk)n})!$a~?#cBm56T9D>UuK!0cGsG2%+5$Pm(}kS1-qSX z@4hu@tjo{Y3GQ;sAeAeveydY(B#d`tq3~UP3Y6vuuMaI+(1lRU+IOl-XrtJ>;}0Yg ztUCMKZgTT1SK7$XeOufIyM#6k5Jv%o6Zi(x>4t^+H@$X9ko*oT<9|{p^bq5#HGm#! z1xDI1uaMNw0pE_GnS@B!0G0>6qz682nQfudaxkQul_|(zR_G0Ln<$vHu;+gh)FP`? zKNN6jmXsE}_U=ui%;~>PGX9uQIQcl-YQ$u2G1AT@c#?-7)dgz|OfJt{(7Z^Szwimf z=}UaCON^{pzy2RP6R7e3!3&SXR*gD^8<-h^T&XvQB@VkRQxYRLRfm zP?RJ*IJ|jvrPA>fbKk2F59IxAZBR0F@4>KdPyJ<7ktO-CkIt$$zF{tO_|!UHwNO|z znBGa`%8M6!KB7wwjGNJ6`z~@Mr?xq^&IRqv<)N^t2&RQSPsBF+l322fkZ@jbS?7t5 zu;WI|dw3Zp_ajnKgztK>5&VP$Zo$Q=1_Oni6!cFcw&8e#PDGgU4_B`|;^(_IQMbL_ zcy-#_cfBl2OR8*xe8!jGq_jQT{^GgAli`3^CR^!Uz|xn+3(kSZa>4Wt{P z$9#z;nTadE?jLAY?2aRHd-+U}ig4p?ERl-dTs$<^{z*mYq>NPzCm^xO_({qGdzakK z4(pT&B|r?{sLW__C7XiJ zu(oGw;|XbtH%hX}`s6=@hSGArD65?X6}juJY$l#t1QKRJ27@$ndZEPt3B_oAq4WBD z(e;IEz3kqMxe@!r!sArt51-e&S+<;306IWhpEpvaw;1Hx+UZ6oKmU4*3E`L}u6Eas z|5Jv07i63g3ll3jguT=hfWSSYfppChLG<*{!DCZiZ7i2hi;1w8@x<}O8JQV_TJ7ia z7fB$E%AZHnpFW_t0X$JiUnHnxj^N;SeX%u^KI`ub_C~0Py<$}-8fotc;e)Uj7!GmP zYt1UKn?p=0CRg}{BPSguro>w z1q*I&#yxt;RCaU}&GO!vf0x#M)|BW`t%!c1c|%5@fawutj@-M&xDWb4`g{tNMF_k- z#o5RX_>O#`sIlJa%h})YF*FpPgpFdnRHil3*h=>^xwQd9~bHH&0b*Y()R47V(uI zTSPO4%=aquYm^L)aAi>)KSY)?;ST09(TsRv74OAov8mol#^gIQsGR z)W}nbC{v2AMP4{>Lq&WFlSMG5YT8CE@s2x5BRsLpOS<{xe$TczwrCQeFkx^1qF_>{ z@x-hDERD-rtrL0xTj$CG`JvATW}_KgN9d8SsKo6-sbuXZkVi=n$djsz#Ag*clgR)K z%aII14*TQYq&*u`z;rP|pby;K^>bX69 zeNjm95`9HL6m^|QTOUXEP55U{O#@7bduTWa%L0dlV^8>fgqLtMU&+xca7PkxP>-rm zG&;J#&RX{(M91$%{P(>gTAd7JNo1tDmu-qpiB1gC)DufDUQnCoa|o zylj%6W|N9+fqB1N+}OSNUJ}qZX>nDQ z&fKrh#S8l`eeGSqIhP?fn_e}+W_&5~j=&QkR|iwO*4E-&tG>|qDr4r}yyq{l_@j#o z$u$*GLwAaZzFW{a#uOvQG&&oK9VHaKpR$HSGMjJtFcL<9aQX2RC=EVHSLd~uW8cOz zdp?|qS1{PXJ`gtG>Nw_DT9mNSp5TC|!CpPdD_N$AN8|pyPJ1K{+2R{dgpQ?JWu0Oz zscqe%$%VW`$vf%!xLHrP9ao%Kzb#hX;hQN&;M*{3R7T1Kh_Z$Lix^1+)V;U%VP(}d zUzIipc}_~uY^2+j_N&22*L2_P)bVoc6X-2m6IsVcYh(Guts5I!N>U08GzhQXAyy34vV(6bI7MAgcu#$!7M?K z9@f2#@w6#sr*RE^ce>icVPF1w4?Uq=?S7d`f z|EkfQyR-61R!x6>WKjr-gZw2!p`ls*&UU_XhJ&SJ;t>@e_?>QZfjsZPrqd$@Izp{a z?@A5FPHkfaZ*vwgwzwZVq|7I6HLsob)U#l|>^Mx8=nnHB*nyJ8a2!k;c$QdaAG&QN zFkyv_qXNU)MlBDu2>H1bmxnf-CYIXRMsuz))E>G%_Q6f=B2(reVtB0BsMIZ`@m!4n zU8_BCYuQy23v9iYzV@E|cGA1Xw~=5(TgzUwrSq*s+)Tp>eo;{*5NeF!JH6L% zMlH8axrt9RB{`#-6Z__0cVU^gG{Z+XjO=l8wNO zd)})Z9}jqV#W!7U+Ond~)D$`M->60bqRZq1?$6+El>|?+SJMx-D@2Dy2Nq&%}@xD`@4Q%&F$88>%d`Mpv?7i z`N|jS^7YHQp1M5=WSEBh_^GIc&O3Uc)|*8NhEK?pBKA;I!5PSQ)^Bk~Z1KW-jUxl_ zt=z|hNKp{F9zQnFr@A4;!AVZRG!zmR9MK>m_^L@Q5fbl6g?9^J+l%aT^51&4t#LBI z^hm;DV^Y)R#I=48LKeaSnxT2{laXx9%*{BNwDZ~^U2s*SZyGB?z^!KDI!GfOwRJ54 zli>#<)2DT-zdzBceqoA*@Q3aUVPLWz0ZbR-De`aWuHiWna7==s#)KGN*ocJn9B9Y$ z3J(WwE=x-MDO}VjL8-Qremk*aNCba=+O7Y!WnWZ>8il(pz!+5FhD zdb{~hbavu&WZ9?<&KTib0y~TWD2&lwO&Q%821tXO$r;?{F}I!}SV8zC#IrvhhfB#J z78syA!8$()E@FGe?FP)vcipU}-Ya6GNQ_E*K%H@B*rSfpy#Ni%nchU5Iu)G1P^lb2 zrWN|YY&kG6=ac`LmeOyG-GHId#aH&YTe0iL?H#XHt_3coH1H>z^7KAyV29gQN&tuD z&C`q8*QzVr|Lq5m@W<8hSo9?P;Pt8B&5>hd zu0?FBYmv;-N2q|);fyWx>Dj7{3{~+-M```m^B(^QQ;W=0s#uIdqFNu8R<}2;3hzv7 zhBVw&bHVUwE6NtB&jXPmoYKZg-10t`u@>)@(;FgBeDs$3lTjID6D6)TK6lIqA#+9G zrfkSXHCnL5AO^lECj0JND=IqEu(sFri4aVyJ@ z2h=@?^D7Y3pM1!}f-5D#>TX=)VXKF+gKW`|m5(U^3Mh`mEwW&tlP8J4Wd(I3)al_1 zF5Eom$uNX#VlM6!zT0s+?4_k^fuyh-8&sA?C%xw@+#DNQt$1?>%Ht;2cd4GF>JG`< zVmjc{qMeQl<3RW|GjW1?%BZ-lz)M|h6kbJtv7M5g4r2C*_8?YQaavK>z9j2KEtXs- zyxv++bOq&}LEN=p91ejwJa3x6SUh>gD52vmef|z1HCvA z_C85bxi|#xn791?V!gs~vI6bT<5tVj`}jo(&GE@E0+ihSyV>Q4c@sq`PqBi@o42f{ z{{FuFA34$wy6N8YC;&hSJ^;XYC&Ms2hXmoA#{?(pcpKww;@>J)rOl#4jQMD>ksD1D zEIg@RsbOPpB3tn4RHN`1r>XEk%+0H-hlgFPN_(_zv3BLz&g)7i37*+eWfv%5y((=v zgnep$T~K@I&%HnFfT(yoa}|5hy4Zya$Bj3<#xhZ7 znwKB~cAXJ)vh7su;Ev!z^N zU#Z?}$xA@7TQvV`7Tr)LZJ)Q63|{9Q*yLb~;1?;Yl8?yOox}Q(EQHI9F;pI1b)kB! z(x87=L!jtlLZJ9)&3B1{klD^4>7_0)xMgmW^`ybsiE?(HTX<#}<`lR*KRx?|la^}1 zy}It1?TFRv@p*zh@$7Le=nG%!Y;&o_fxUh~mZC094$fJ!Xd?Kj%KkiO#b-%_r!q=l z`$f*?Z$-u78o`U8twXtUj6y`8YatdKkpzW$=y2dc%MX4`EZQa^UnC@9S~|>kO= zS2q~SXdf~u#qLZ6@O3tZH4W7y%@*h(mM<#T%_l1Uq!KY`Z$*{dQ?E-}`dqrX;L5uf zU1Cz6Tu~#T2%ct}rx(vLQ-=EOE~HJlmLUPpL&Repg`VK1xh6p;SpBQt6sA7}GjaB1 zS;*B>HV)y$eLgtv_(_XI>qt=I#-vI2N?ntZV_m~MC*L*~I8Ivg$c@s>Cf6TYgG{V9 z;B1)2Iwvl}P~8x8U{{`aY)zc`=2*|=w%)<_GtHt^A?p!nZS0mo(cI80KxvkE@p04F zo49y`@YHAy`&{d}yv(G%sTuZ9;3{{;Zl)}~b#wdN*XZhq@t1XNAKTwh@DE)~j~xHj}P)iyQHq%4N+GK8tP-6IyFhOrWM#E0@k9F*UHt zTdTC$4l52}8a<{`Y|9rjG)~C!kVF3Z!uye^th}tAqrS(7HzT;V{lp3idEk1DcPGpP z76?&s6&qkeg4CT-bCfaEmzE6+hGmRr68J*L-k<}W?@HG|u!H)sv|l92%l@sO5>tHi zXQpHda`l4J3M+58)ak2E$8W~_-pLP^Sj(QaXzH`PD^SpFp;_qF^dmMhu{O#dc7x{U zuchwjjKz@Sx9n(ebu`)#sezAq{W3zLeZF|*CQWPk!7pn%vpDD+vn6Tvjup=0GPiyr zT{ba=B&WO`-9c2ur`PhE?wBcG^BpQ!ve{6_Ls)!sfSwm7*!PVT zJjjthm)YNa=*J}zvS%YN!7P$}f}r_vnn=X3s&2w8iX&+px9WW#)CDA{j3nna$|p_7 ztJ#WnHN2%yKP~0IOgDt1WU1uQ-RZhKw~@kgJilg0=W^gj!BA~?wAb?TgYa(RhfsI> zR>!w`VFNiwz8?j^8Iab7>8EpwFYNu(Lqm3IQwqz>GILSm>r;M`f69ImhN@pdoKZ6; zQOxW%`kfeh=BMv?Rzf%?xT|L*xA#zZkFKV5thzN}$`I;~XVU6zMZPsC8|_9mrmG4* zR41Yw7T33z!1xr9L@X!C{f^{SXmKGi)rwD}#ZGlik9 z#EtKmYmIN$T2YhDcsHkb@uGQCy7l)z3X*o$hB%94mw-RQgvEXaJK_yT;|z3J^;RE~ zu71%V>UD+VGDA`{?M>C7GgyR8WFY6g4Pwq!C}=z9p|?t=cyziM_bfzD+U()u+LkC` z|0~>d)da6SegHwfX8k>{AA zf3S<5)MpN^Rw)J?#Nu3dsX@}Z$)&71S7VtB&K@M_)|-*&IA(SfQE?fH#Mm-(mnCcN zNYdcTF|D(EXgP70jnQbVen@9I5v8u8i?XbE5g}YrCj?FD6#Q=JOQeAHdQkeQt-Q8| z+Sky8sw_RW&{C**2vbIym4TcYOJFrQPWHSh|A{xaH_#-{o71V9UrL#jI`(z6A!&}1 zvf85j_p$Ylr6Y+vRqj*_WYaoR7ZD%fsixsUN*fC&2Vbu!Gs}hTyfBfX!X2~Go&6H2 zf`dq}Luh_9@&;}m6AVP}Q7}3irg{g*yrJ-rRh1y@6-d$xx>FVnb6}_mjVmI{3!xZy zP{p>SIo`kGDF&r@)~E1;_)n&L)=C*F0!jK%?1a_f{ECt4%a9Fp9+c6n5Z1R}8?GQ( z;p0gwJG7shtw&3zH|)ns3hY+JT3<6pi|ul5ApLT`aj72$w*@c%9_0K6$eg9uJg z!36Nxi6QP2jK7rdi6I{*h+qp5M<)dCgz+KdlPrHci-qk>dosWzl%Ep<0L&2MNm@wX zG#UNBb+U}z)^8X98*m^l zjebvx-^|V$CKPzrbqMnU4bTl8qPp-H)5a&u#2=^ixjHR|n1C+tP3oI<)!vgPMfdYcNz^|`of0J)u;eApF)e`3)R@FO~ z)@=}@B?90y8LWf`@^*>xkHOcw8)xRgX5Z;y6T80-!0#oVrirO z0eiZ2M*{$ae=}hFyg&z9i1@P9AN3#EAAuBM^&i4SSY_@(dl>KPA6{np11?;Mricpb ziWd$5AaW1x#&QQ=hLA#@uOR+GDHefS%Y(IW3R@I)zcaIs?kJ@zRDZyKdCeEW;N39z z{T8Nq@8Fo(h!89&!GBu$We45_qZPqu{P#MxAn-Sh;D3(OFPHE_*z1j4*s&70N7@&< zBa!D3{->Qg^Ke)@b+9G0_s_?!$Q`9@4IQGlhW^Kh?mU}e6fYR%{)pDa{-#j>VNd>- zckc2UK5#sr1%k{Wwi`UKK-LBgkk1lwGc5cM33-zSh-wWP9ToZ=f>~WK z{j$3FYx{MzE!y0>+U|@m{?1Vt_|Ev^UoeL4|APOq!T4`x|JMfNcdL1UGXGyS_e?SV zCk_6Q_iv0oo=N%j7Up6ei z(RqQ^JuoOWglP^Pc1ZZVNe~odD<`4e8dB@*m*gh|?W9$w-Joo1q?@hDZgHwP0 c8~$t71se|mcH8@Pt0w+R&2Lt=@<3A@cuym@W^3GHOXIbErly91-i5$RwfIA9WD_NA8QQb3mP4S8> zdlFCP5bhI`fSa^w5Auz6xCsO9hmi-HJBj=3O6TkBoXIW_%qON7?6IRIsvG=fzb{dV z1SNnI8ap3Qvh5zMUlYi@M|4_+-hpRdvJ)36WHo2WVo{EE8a+K*wqhly4ww`wo&pcSQ##^(#J(+(gL znUpV{)%=!0|IzinMqMol?_fxMRB73C ziIHmQE4EtY7Ap9B&pJ%u5PsK0+Xxf}oU^7{q#~vX32p#Y%AcEv%wfABt{HfkRsWSd zIo^s>{p#P?RZ2%%Kxb<79_ez}59J03V!i3JxA1YSzFeJMv`}E&MJ_ghd3SDfIUv~% z9s&;@-a1cwcuA!HoJqBYcm@3<7|y)l+v`=OLUGCS<^`W;UQp+_(0G7bXp?K+60^8r zuq&`RQ_aBk_dLv^L8+~+0leioh21}cEqjo&)JTw!yW9tU< z1X-<<*SU4cyxnbGui$HN`#0PR)f#)-Ql?iqF!k_Bpj%GPyr+M9y=C|L)rjYx@YfgH z&yqJeX)p|S;~{7`9Eq$nRw@hp+sQkzbXE#8ZE3_zOT$rr_LC$a)>uy*Dz>HJfR<|y ziMXahMg1)g)(U z9$Twhb|zj(g@=Z>GRGVG1OwuCoYo%Vrq=dROZp;t`)Qk0D6QGbr2Qz?%<71L{F!%# zXLGV9Q+F2ghW-F>@4OoXY973P;+t}tMDCro?aKD#+}wZRl_+GaM}&;NykNRc$l&H| z$(rQ{Ibn|p#W&=74<*3tGRt*ff8yOR8bAXhbVGm6WFqE!FA^tGdh^gY zIhN)3$kwcD3{}YW#k!^c7AmRhFh}=r<8I11Wk_(!wt{P~Q$*PjV5CQm`7z?l(^i|A&5EZPg zp}lqh?tXB{?fuX4E38O|y{Hh9JBp!s271;f;RYb!0i=s`dC zKmk-gQueC5x>JAjlchAE@vs+4nzWiZp&rStb#CseIJJ%D#ed-b?cg4dzLnmvHmLd~ zw0N{y0)J<9W4XcDqgPi=h{NR10Mw9bqBnb}~BXdkRek>I*ImD$@GpG_usmUuS z%D=ETiNb@Rb%}q4-WYv`Yq$R(gMT@5e*73fk_9@Er%|ZO%z9VCcg852(`tD*76SdJ z|NYo3`KI?P{=Q3~AG8RzCGANqrQkF1(E-J5TMXDH`C`kQ1vlt>f% z`O;meaaQWiBDNig{8UsB#2ZJEfGE|V{>kHtN)^*9zOYMygtk?RBg(|Ykn3S9mTbI+`%5*Pe-IIt z6c#1vrQDf?w^Lpq`E*^ia$r;Ze&!p1(X{2_a|`oQMWOXnXYrHx>++0XefWC|=0J-u z3-61-s3h_0wIG`-2k(UCRv)j*Ii%&mt+z(_zyGI3+Ik)Qzse$M1zUwcs-Cwp za9e{w3-jN$12@7HjJhms8t5m83rfCPyS22=G1%6YHeD69jz9B=+oAjFz#8(4;P0M> z$=_lXWs`vOxF0LoFG@E2LA_^jvuAI6V{dGSz&)QEssImoUZT{APVc$S)6bw&3uofb z$9o~LxjiNf*B;eG>|aUdsxfhL2bM(2z--=l9D<~NyedBX1HuS=);KgsJxG@9sDtD2 zY`n_EXhP{SUT4$kerU);M6@x6%<#GSH_de&vx9NduRY%wIq=v^`-UB$XAe}mgUuZ#U9pI2tXy0<1ZJ|quz|+)j zR3FCie7cCcBpvd4CErafoa)Dx!Gfa}WI)cNak7BMR{OEL5+3r&6}-f0W5-~Pa-)VN z48!Rn|IwYiRMJq@whz(YL|e@q$tr6GD8WkYh?5QjY^sP1%WryX%olOa6*QMdsz@T9W21o6br1&>Ys$B4Z}Kv9OPxSQu6gMq2yJu5z{ zN}4~7`mtKOG*cx)sk8QXpv>9$L_?OC@ctJ=^9AX2CT*ge6qzkeLpyJYJ?JgH3cStI zj(TRbR;GW+Jk+#7O2>)M@|$$09=@HHUC02@W(B~9n;m(E z1+nWj#px5fRTXp3u>5WbbgDVr0C$UV#);#lj`SJ^Y|1;j&Aw)&iIuN*0B{f8Xj^@Q zjAm7tX>4S3+p8Rjx|b1+JP1a`0*Z?Clo5@9Gj6uFNE4k!T#7GWyrK`JLNh7GC0zOX z;0n*o6loU*5gCgaXniF;{Bj_1ns>hUpkTj%Xz2hcSrZK6vx-mPLcGL9S3HAZxfeR0 zq<_u~{?13#JPz(v_3wA)mr~)d7rbdr9eW>W{)yanR4S`Vz6Z280N@*wt@A16p80}2 zsg`r(f)p`jci!qBlUbn5dZ3D|NFj>I0aTFsmHKe@Jog&X0Uae)x7bq>SE*O;g(1nc zr|rEiLe=?KwOSBRwd7ZPcGc_+*7BI5jgzR#S-VSERyf9Ae_m2PnkTI+Ndd1nW_VNs|$eSDR|grhmlWs^#_-v_us6dwg12i}%arKHfmcBH7&zAQDS=Q z4M84H4{uI=Z%d3&jqSH-$uk<(^(~QpU{3`0m^(a^z!wOOQ7q<6(U$lpp~E_YGUz?;pjYzb$2b!kK^(mQt(GL&tWg(d~KL zL`6IIc}e6>TB|-CBU+}jU#99jq1?yZXh<-P_feVQjOeD0GzIMQX!7$e~FO;KC1m<7FZM?&iFNUxJOpsKTQMEKjNvMI!?Ar z^Sh{W;`lek{eTj&=`@k|HDqbu!J_c5xe7Ptg`X5DUt*^Pl&+klc_JCqrZ5_T=cldu zfhGjKuIBGic?y6OZly~?5Cs()ZMIxmYxH7=j9=+_GUz;+7?7J^ z?@wj+jmIeZGt9Qa)Hmh^4M@_fb8o&nS53HuglI z_#HC*dtQO~F9qA1#lW*s`uDH`P##!5|JqZ!P~d;ga>mm81o+Fa5MXlm*=GAasvvOg zjP+L_JZP_M`#($PfQ@wV7kiSyrnS>~B1 z%NLO5E7SJ78^Z6OOxeHZ#l*8Q_%F#a0LuzTdg@yZRkh^_J4;}M5q)>B;ViMNsgx>a zxK=nrPiuBxT{DhKJ!X8lTe^=nf*cq8pFsy>Qv3p2>=^50TkP0!WljZ531$TrDRwc73cqjs19fXZ3gHbe@_XspGoAQAhC)uXOLqt#Y^SNdc`sgJUE5-%^Y{ zGW*-`-mHKD$-zecLqB?}(k$&8HtA=NtsRSexnU)t4d-l&jAVJwF?A5QG&PB=%^dJr zdGk zjC}UlQ0+{uYPPVw-Kh`8DB4SArbh0Hx`?jo0v0;&it5mM6uXO9c1sSM8rhgQJL{a1 za{j_aeDQcjLdrjb^YVt3Fv?oT7z7?Xw<&%H*KhmFv$OghoUCQ7jXgLm;oB3nJlSs9 z3R#>_l$y19P08t2n{L&p;3^tFMIbR?abPI6P4Io&GE<#u--KGu&FYd{-vmUzVa2%7(Zvilf$q{xqvwz+ddtgX4I#h9^>}>GWpcKQ7B`HDZGKKvkJjPo z)!wEKGA>#_PI^TNJFFIR=o0D41S&$Kl@a@7Fe4iZX>~<&S4U+VRx<)^@k9$Tek<7Z%>hNaSB(QA@0~uHetObBlXk;GD zW|uax4$25@o>9h;`7u(~Hlq?OTN_I1 z5GfnLU>;=-EqRnwDK)-u1|dY3Q^DyMwo||0+sx&cW~%oA6r=^6AkB)(9);rfhVE1? zNcq8|;kI8}DyWlYuJsIV(mS?=TP7BF|?-_@GaNe9s;G?+69*Pk!oAj5G=w>BmPV}^ri^HLq{GUPOl%Dh!dn-tdI2c{Z_f%SYvcL@i>$M@w02mE6S-m&lPBcEKRj&{|rWD2QUG z^!1XK5Y0`O*=Zo~3-()z`6AU(;x1gQTbWF4tn5C4q`mYGehe2~jn2V+HPZsc zNKs5+xao-WVn_wmJPFUw0&_ij7#)~J-|!-A%~Dkx{U%tV{uj&m`7?Z@KW`8s<@bl5 z%s#pfUC`u*Oho7jTtOkb@x#Bs^#~JM?i3GS?^2Zqaq5jyP1N$B*TpypoMcqUmgxi3 z4lCXOZ_c(Z&W~R41^YI!Z*B@W8T}^ZKMV7Lj(h@W&Un#O#Z3Ml2JLfHNQA<7wSogU zlw|$0Ijh035#e$C+F8z2pE>ZqqSp@}hnX_qDHR2hiT`i4gEE%$_HjrHi^;Kqz?a}%ll#yoO# z93KVGe@~R~K<;N`=Fpix$%W}kS#(Da_A}*DwVdf+4b{WM!7g@e&+*y?MsFIOd#6C_-i*5= zqi>B|hAtx0=_Q_=uiVM2-7wO*DBMfwZ{d?+H?_IH2ED;$TDMc}qEB8XdPSK59o4!f zs_6Gx?gE?)19~UbM-03ev3~LKAv8RFK}0@eFfxio1i@+RYF*&R_40fpb|TPqK0@9) zdGW9CAp-`>!&{tkV3B{UAKTPs#h0q@g%&=w{>zYpYTwr)wkLPxpxWsgwuC6o-Ch3L z=6l<;xfl3z`~IdS0LQFZW+f%mi0(-wyGr~l3;7P>u(I+00&}UThz(F(5J=-5 zW+ST(Xedim8vpQkP@=jul)w82Qq;W`13$lCt_>LPJeOV7jSe8M3=c{jesOTb6e|HT z=RTT4&_o-$4%Y>Sik@Kp7&|epzM*S6Is3&Gu71)}&eXd?_zO+p7q_4dluM&f%W$o8z9G{1QK_4al&XuHdw+ znrYHHOJit=u;xU=-;jIi%~B@u@f%WVir(ov2|jOXGOj~(@C+6}1vPs@~m#Wc0+oE2`ZxSO?$x;1URrn*U!8y${T`kuDp zD+^1*k`+AtM>73SKK)CB%%W!vO_CSKtkMVq&-7RZ=Ce&8{blu0OLe^+8ke%B;6JoQ z*5ls0Pbz(Cz$mCXf+cC=hrZFVr`?Nci-{*w{kZBB!qS}CSrz++a-k7-%4ok6}Ij^DWErywQcU&sn;&@6Z`Us4t3R+w1EL9UatGYSlS0KmDY17s%y2{UYdl3F>IjUe6m>%k z^DpbRN+g~5>msc*H<4I2u~hh0WG!Ftj~x|~612a9yydj=4U4!AQA@A3A$=%OZ{Sqt zkxyW?F$H+%pWHjmkn+kH9EqvZ0ju zZ!=LT=%>8$0*N_thC_kRBbE73$)KT;Cn0Q z72tb;wexJy#^yXp);Sla$L14lT~7F;3{>wweV5iO7AW~NYUhpA!i5FmA?le%_CAB5 zXE?ci8Hk`)Dq|X?k?RGaL=c_;`Netr5s2F}wqE41+SICOhfqiPII13-{ z?;3Famsf6J=$1~vfb?2g5oD%ps+23L)WXr_td=UuMLGDXm@_B{42i=u3;G0M{uy`c zgO|}aB!g54;^${KcpXRNNWEax^(0<&-K=CG}P{0w#d$Po^!agha1WpBbC*)hO@P!!`j#)6DjC7K+W8 ztU>4QHOrVK@0HyX(ae0HDenUozZV{ZN>utQPQz1oC#H9!Z9^Gt4L0T<gLw`oGwI$4M<% zKLHk2cwY(2pWQzJgL!jO<(~zfSndoiyixz|OvJE#f$;ykos@c)atRLKvXu%0``>Yx zGX#P2f5y7m7Ti1)aA06a*kEAv|1%Io$%RR6*h2&6>B9P8j$wVn=CxL=kQZTG+X`EV z(x`>e!NgjV!O6Lhib9aP{5)=zh4pSXcTP;<6;h@eDcy)(!Vn-*S8m24?d9!#y?EM@ zKp&eB>8@Bxhd!TrE$YnUasJjh?ZqSZ{oH2&(~gFQ&o46P$gdI~X*s%c;M=O($8DEVleEL*{_-ii9kLnvH#jhiPNPN3jgC{XwZRl5=M!NdllvwVW{G5}d?W8GJ(zln z32CTaOK5#y=bb_bu$Hq z_AhI5WH&#T_L?MCAO)*VyntSIGU{dh4Uj9CrZkFygiFAf$y8 zydb>k&jiWWL=UHo)_WXenLS^O9Ykm@#$DWhbz!AGGA`JljH*K@ke74KbDExixt5&A zm7yTvX=_As2<^>!T{Ka~KTy3fuyOzcDwi!~x}1tz@^OR>6>U z(lzKQIyIc+M(=hnegM-S1EEQb_^@&5ods<;o|xH^l#6q1KByMiT13XaL;WP<$ z(bWdjZRgTtn*P(RSX^6A`Fd`B+OollkSI~ykBaP07hm`<#WF(}$*ZOmW&5;ef954C z#D{q9ng0HMQ)Q2&cE7`eZ&r3+&!~Xw2uRW^+~0gl2)VU;Bkohl*MI;XS1hs(Ve5VY z2?dZu`cb$(e~4(`VIU0M0FH(+qiBI>vaAsAH(-(_=Gd%6#{&U(Uc91KJgK8N~3HY5kL;`gol-1s> zUFM)%`a8>|UJN)~0M!co6fBmznZ-IhRv!G;8!+Mg!~!nedE;#X_i`eD2l!J%dD95? z$)Z{V8NmHrw_OYP?~;AjPn{xCvSCcAG{7LuB$iqcoYY&k3Juf`+e>CY+L~driJ#j#uSwVKQ6+QsnlSGOn5dhK5udrcOx|Ag2-wTxuC$&c9g8$s@d%lND?qlT{V1 zXU@%`4u!=ggW*o)nm>&q4%OgKTQ>d97=6h8U|4c2pu9TcvnANV;>$iUv?jDlf*npk zy_7~j&3MK?w+i%yjJh$b%8iPoW3Q~xuMPGXl-sTxg8bff%lhm4(FoRy7SLMXXx}lQZ5m@w! zt;G#?VS@^6bUCpONf-kT=?3f8^X-kP{v^A{o@$k^LB|s(jW|}P;$dd$wD6ao411R^ z4!f!AxBC!oXX%`rD0VeHG51h0Sgw~%31nE~Nw^yA;M8Ki|B0l*GGdwjJKeo* zT<57&u>jD@SR~e*A%u<;XA98>L|^U~?)^<2}zlH|}3@NQBpye^$6}xEZej z5K@Mn89(*@SdWMZwcJ}!IYZpKCf*S_Y8qp3;{Xc2$SZ9nq+6a$j9w9PSF3ZI@_PQB zFJ(h`%!VVPEmH?W7!ODUOYX>WHuRj^SZ|_fAj5qmb_- z;12kh%j22QTgrK_bJyPEimSoQ$N0gk%{z?x;=XJML)ee;W)Lk*NQuJ}>tCsf1<;XE z#n$78*i}Y8?v_?t;6W@~$9b~a6G44SH<7k7(JQR3n8TU(W6#PFd*<|omlX#nnLq8rR?;iC+N}zi(7@h!tO^F->?nCF3M{ zlt^7^890AN`W2Oc~9=OfT_nN2>OrLVTXZN0VH{4=BTA2T-9pM;*HYPU}a z@io4v48~nXnSI2JM`x)*x<>X;gUF_8+aTFZIca@L4@m=7Jub2nFA2319zf& zg*)yg7TFCc)rN?B!GH)6m|e1o4g>pHZ8iy2X-1^7AedjsI@B4x;i)>O45N3kGv2@A zvJ3dNUz;E>cR<*7TdHMqPZ>00QQcXia$>?1;Ez@7kC+aB1Mf+PSv@IZv$`itgYpwP zKDZhYv>kiybi{W)UZ_zm6e7ZWKHA))`l?YI%s1WU>doxpHjkwb!xm}&Ry-4%r54=&_>c2^12Da@{(U!(F{Xkqs*2;-=IT4Rv+UD{4T+1hIii!2g+Z0(T_NX^ zwQyWsF$VjoI@ZbbUZ(pM=t`BXIfyTeeu&!lIxlF~$p~GNyl6-TI$QIsZ!^-d70i9l zdVlSF^u2y4&3>I0MS|B3ikz51GCHMf^&}DG=3??B0OPPrsR)zN2BX4C*ey$a-Qr~0 zBk#y>Or{xF_D{;T2v1Lt@!)zCrFo2JXYw9haEu2^x~+Q98_ZL5rhJfY;P%@T|8}7o z&o%Fm9>)7%H1u>5z8)|szG>(8)e)-o^b)f5gyt@w-W(o#Px|2OFj*wk$?gel_2>+K zi2DG407-uAhmr-zN@EC56OfnzXea|TNBOr{mf%FF_YcvP<=?_YN6=Pu&2-gPaOYEb z&LgNB3lAIpB;(Jt!%hjHC$p)`D}HfrW9z2nuKzXNSYT@T!(E=GI1~Nk)sC@brHI`& zE#W8VSffazx@oq4FT15}5rK`@v|_2=LJ?@Z3(QfG|J*m{5|X&6ZoBkbgbS8e$u7#d zmqkI#ay9?KFX3l5A|HHQ*4mm&C&PtUzFC^v)grZ5pR@i0Ay3sVL*SB%#IAg$%^&)+ zPCKrruTg@ht=_|2wdJ)klrVA<8F|WhWz{#|=;@ITI?Zc|@;)L*9>Cdamyud)TQ2b- z2Sk^uDlP}{cEsEyw;=--%-{+Sal}eSG~_qEokZ|g zQr2;&;8uuN9TEfjssK%zxm@P>8Tb^V;Dh?|LZxk37D}$QmXThP96u60-r!aEuL+Wo zi>U^*!s`VxraK$DqP#SU1VE^_pTsipKxFS~xlF0S3d&z8cVKz~R(|q>fZ!?jSr3XX z_q#@HtL;a>r;aLqK<=_*o?Ma*n3W8t<7Uper2a&7ZGD)KTVgt>B<%)PPAn^ z4qrQ_aJh<4LR)dL0!uK84SLfxHAHEx))awfZYUleWS$$M-AcC>j5sl=Q)WsC?1sv~ zl<+Xn=P}QzGllpM;KN!l|4`fO4%qngK^AD;u~K6NB;?LLZs6QWL7NO_Vg*2(q~CEt zn~d>6@AmUh2^Q}e_-GEav(fDf!1u-8!a?PvRA1A+2?vQvAA^Z@)#&Y`P<_e{BxYaI zyj6u2(7o{m73~QIRqg5cC=MI~-`YZU8^3qSm9qd#h%;thgCxFN&56l{G+F4p}4(A+o|MwHX|TtCgmzlyloJC+y@4?Z%3gIpM;(xVWscnJAwd0``9I z-@@k)mi8|KZapiMWK?W5#MgB^!t$<;j$5c004$?%{pl?s(tCdu6c-GE^DSkulU`>0 zstei1w%(ldh8cN}DfVjlnHk20bVb~D_SJ_c_5Bek=^)GaUup#bT!xgVV_Y_wlNB!y zqQ$b7@g(zHgeRhes=_AURHq!=MCMfoS>i6 zY)-|S-7ifi*%uQ)3Z4aw$9eK{SifLR^YeYO+T>=wIRJ;xyjI|ROWPQD$j zX#@U@GT&H>?8vKp3p$ySnyrd;uW#t4Sml%XcTTQkn`gV#9^cs<1hS^eFl2G{6dRa* zUst+P)t{kk%(R%(R#eL>yE^E*TD>6Uc2}i+$Rg02W17Hfj*TbK_8iw+_r3#R6~e`a zC2PH(sIQVWCd_Q4LdjsN+%cycaLpI#&?B3d>!nM}&DGO2ZV}$UPZ~!1O1Jx}`_#-R zKNS#F@9)(Tn`I+REdEz>zovR(cMeOkI5}cjGt(>0E_c_X`xiJ*BY3QPfHV*fG>vg2*Qzm|DiZwO!~iO6@Syvi1VL zoG5G;nD=nbnb?2|#0G^>p8=Ywkkx};*xyTHNj;dyS@9bZnNA)J_AHampE-!w2}|9X z+JSh6+%eD3c$x{t+1OAlegv`9F1|KHP}qs&l13|4OS#&6B6z z^ANbdLIeJw2)k75dO6} zoD(+i(lvH10=bFE=z!EK7V0IX+{vs&W-C$XfY5J*VvT0D#6&O{{2ucq7I^Tp-(2FOiA*F=Kpaxy7KTWJhpmPwWq_1J|de`u?bnOgDRxdt#i| zJutnip`~JUmVU7mH)5pvBQO9YX7THb6NL(}vUL&h9pXjqhum2Z48qB-tQVj7;2tz% z25LI0KTKUD73UWM?05VRH8Bcq5jcL$ru0STn=&GDC;SjM-q7a?dFzG)G)om$s)!A; zOye=Lc9Mww8!FS{RO$3q5crM>Pu+{?H_89*M*Yt+!5U%;Wx#*pCOjB0i2rox|2K-< z-eLgL)BZ>06tN3vH|p9JX9^-M4p1cwR$%#58|7$GFpx*$RGOP!&wJ&&UC-VY0RlfER0k;_U5~7El~rlS??lTP_+#7* zxspWX#_&Vcp5OWyjJfSX%vuA6cipD|KwTC>9l>%0*e%!1{59;pxtdmTj9M4pUc2j7 z!3yItEx(SKfdH*ngDuZ?(=Eef$SceqV>TDUR( z$^r=gL({VMYI)?+%TdM0`8~hJkjvplyW@0Ru&46Rtm0+RCWy{#Z9MLlL8uuammu@E z>adg&#_s-^0_A`RekOu%Q^*CkL5T114MjD#p&sbTZnd}uXcaOTZ{CIkk&{t79b`nS z8FHABTMddX4rAV2=-TLZ6;ogUF*=xrBqBWiZ07b-n6=|l|4UTM)f_J?5+1r@H8jSrOd{B+X!b^(TP_(h%~}#nHu`Tm-{EH zX98+R^f-*fAhVSk{#fOwY|uNVexFOsStc^I1XX`|Q4=2J8l&|3FY#F@AiLrLa-lW) z?6M*^K@o}2d*ie_{&BD7H$&_0e4EOAF^vb=#-V4^m_0qzZV3=E`G_Nfcz{z2$wzxU zHu-u@&SdUT=ohXvRphXH)^Ad3jxQ94Mj!B4$l-;W=v!~E9)|CPajSzgbQVwUZk=gn z4A_rfT5OT68-af<(ZaYd@cw^UdgKM+Qi2Hv#>|wuQ%(cagA34HSrMUW=`r(Rj1FUU zK$azCFq45WPerA~g`i?YTE0+tNQs+9-|b<8Git5FYOAZ$xBCx^jm|>qDk*L0H#YPQ zGVL7g>$7D>fs+X^BrZKSL@Vv?RQJQ4G)V7ZTJ&*E8YF~&KsUxO z-wv4I1@Y3+uY)U3JgTEr${g;HdPZw? zcQ_07&ia@6HjLgPsK@1&`u;`@5=TsDm^U6INrNLnw-O;NIFQ?nlD_Pk1_@noIO%(;QDk$bDi#4fpD*n}JWky%MiP&pI;6JEb}k2}E*FIv z5RxMFf7BxP6>ASeXV#qR1V1N=zJeevsR87FA4|UOyW$fE~M@n81hm5l0o14%`m1;&MYA3 z_LXC7c}lXv%yV+3-qNZyb!u*0N0DWX+o39O5YF_}1S4AJs!j2h&KIF~M?W<+)~G9V z!XVR@C!3l}`oLO)e$`HY6A>{67Tr0 z=*4xZext7qoHD$Va>i&h<(MHygRT6tva*laM)j~}ew()|7qBi@C+IxQSX($b=Q{>kgG~z@DN|W$?s0d<)X@B3}`~WtGP#S8L0WCpdxPK|;B4PT?H!4GV82_)^#X-%Dc>R< z&sAFZ2u@zuA6$DdTDhhATj&=Dwms~n7^xJwXt3cuTPIacl}28s&D)=o4!d^v*UIcl z8`sfkc_TIYeu0BAc%Zr;J!+I9cjr!0qm;#%4CJTAtT# zyFgy9{HKn&h2p(b z)N|1xb-zvB2dI^g++Z}6I+rnVD;PdekqdbA%S3Kymejkds<;Pe$KvFHy0VBI zwDESLqZOh1RL4F1;)ws8dq4T-5f{RG`ehq0ocljDD(lS4c>F~T)U61@pFhX%Lq;gj zUZGE||8q{?rC-4SC>Z5A`=r}xc&vDrju)XIf%WK%20E|4w*+w5jkk+_n47)uTpU7PP^ZU^AcTt z&q4;kEZT%G99j7qkQUe$r z0-;@Ikdtm5hBMfe9LttF@u|A!pwh=~Ymy>m|3L%F^ zltIADq^NG9Uk)5!vr{?fqB7;d1~a!A`uTMX$q$>7Y|2xOU?{VJR51}wu{+F`ZC3UV zfR?U;sj5TjMw3ZPdCyh`UCOoE#^AXw@$@gFh-aTfeQ3`!tbL&J120Nz>q}I)u1HlZ zM4MgwDfYjV(3hV^erl(;_e1ezf+B$Rk1Z(zv7;wht#FeU=vB7eF@$NIQ3Hy3 z1UJOY9z#TMHT;b4r#Sc`6_1YxxJ=20xH2OZ_XzyI$5{ZM&9e=6HT?flfZ_YZzzX5z z_dlNmdBCaP5^@*%Xy;Eh-@UUyRGIJOe`!s3S znqr`Q;ALf%58heLlOLdMTJfc=*|~#OK=oL^OlR2f zD&k(WtEm0u4}H$FNYje{rzK+$+Dqpr8Z1wDHS!=_hqU1n?=b`>AqsRl4Dg=Ywqx!1 zZ{ZW=9|7aSTBEzRZjh!^oci{41XCLhKjHZMMsYNy-{-t2wWgJSt6G5(qfxix$Yae zjiNGj3D>%NOn*)5(_yAh7ijn5ZaBjK{Fujsw~lb>w>QCtJ@t2;>B=4hj)2V6!&#hc z)qx;`i8zb$*tjG)ri8YU8~-f(I*5UTkV-EwqjWBr2J?!cKy=w)71^ow$m9i2@5q=i6&P+wG3*k04^X)=6OE&OE9wG z4KCx|OA@vLmXodRqNl>6Vduiz#8`}`eH3ytaPiZb%F`-W zRj*jnfM1^0GC2FMQG)6AoN`8}r|?pglfakltkCt-CI=|5P(SP9=4HFCd&KPG@BSiF z`73yZK9`HyFKHEN0+`L^1~B|Hj~Z-!dy;@*+9kFvPECZ^X*?A!@7BxpAnu@^nY@+O zIQ_R0FVp}P;qcJ9A@7^;y1)Tq!Ib$Fet~53(9kV1BAM&t0a!z?^9SvZKTSwu1MFt3 zoVzVcI(l-V_!D?nTo&_~4sl=-{Lw7UCbS=Sat2Vkd~c@@3*M&Q9bnP1t8BXIUBJ5z0ILBm-<^V?nvFtdw^`N(&}`MzA}y zWnC#=D{2ahx|@@W_S9r&hEt)67&%E@@z)c;1#XeO3yAjeZ0dv;7~vFQ%~5!2bM9W4 zqrh>F-Sd8~J^f;S;+PeB*inMA8{Y>e{eq(?AYY^u%Gp~M@Y9;pSKH^2W~m)wrBB>> zr;f9Q{phkHo1rIs#~|WcAML>x{{Q0Y9it-)!>sMtwr$(CZQFLz=~Qgn>DXq+w(WFm z+h5+9S@X@zk6NeBs=v=!_pW`ez3=>ksvyyxii^@UNa@Sm2proW}>M#>+nBrm^+Za7ts z{ZAse>Ny4-1;f6wHV42m`}=7n6I(wFzhPl5?OJV-s2il2L&5zC?!UdEGuzWFH_8`i zt+WIVuol0VCmXe*Ienb1DV#0Q2Z||!+PQ>Y02Sd5PRJ|Q)JoZvd?r(W$|qZV=n<0F zn;77W#cOs&&6|pEjHo+Zaevki%*PNq=9&0k4Fpnc{t^~HBE}ZnFTG%-c8$|YvdQ%- z(Z1xR4dqo1L_LOrU#9}A7kpZNNQDM(7{dT}b8YMnLQ|6t=>u*B>-h!ze3B#l(CB3u6?b7)c)VY?Rtj)cn%`|48F9`Fk`FQ}u zMcPwducQanv}RF!{vws4PU*Iz^49B$I)SQ0s&800SN0DA{%m*O+{oRSpe7h)O?qqV zM_`?xkxUA!t|paiZ@UoG=#cIW>sYWo6iTQWZXq9WsgAVeqXbl6MvZ8XtChZQBsf1v zv`+|mP=8F&{z@Vp`fX02VC-Xt5?=s11P)c&-18|i70_5iq=OtUpMS>QQD>EFPVW00 zDNI_DC6u(>1lTZhjGw9+}^k8LtCSLufHGpo9HUXG{4aHK# z@n3{h{7~;(aXI%`R0LpPJ@SAhe{FiBW>ltjD6KV6M?DF_-Ve&YQ)RFd)0?rRe@|Tg zA7D>0gvN$mFukA57GGlCO}I{55nCtF(b6~QL0{BW%i@hf?%2zEGPwv_KWdKCtOn!c|d(xvSL4vH5o5uaNiTS53|0Q+pesMjZc7cvd(oui=T}n9V63w zIq7xO!9Bqp-uSRb0Ln9V_?K(?5BB%TZ5~~Y5Y~jlBVUxJHW@hzEaH<(fpRc*O?#>y z*N>EVt&v*Eq#G`IOaYi#*G6AOI8LS((wNi%5krXAY+244J?3@`fh2xGkxGM)V$&RY zmaY4k(0V~1T&}zsVob0Agj8Rl>xckGC5)KM42IG%L8etVk|AA>g6>vi0#dXEtBQ80 z1j9j4NADdo%jO)tm$Qr#DSI%BWrIuW+QjA`gBHn$ga?K1h5!u%7YXPhx4xo;uyc2dWUlyBaK~s8Z-xbT4-~6BKT?_h%*kBJUyA%1aBt+i`<(=c`5Z2!% zV>n&0T)YbEI76ZF8qIjw6Hx8!K0!)!c-~3Fhwy#p-2l1>7-ejnz+>M+3zLXRfd+h& z4^RrHC*(C7@S365RTohC!?F{Lma3a=IaFF2^{y39rxSJ{SB?hNnHz&L87+kPyb)D_ zbG9VPe4PxrdoDrC8OCXy0<5QQQSAaO^=Vt18BZHJL?T_W@6xlH0h-~vSXq0w8E2XU z<9tfUe}L+J>-=+OS-R?!6jMh;IL&3h=0P}ekyYY=Abe!DH^p^q96Wk+Ne{~$Jfpyc z9H1H!4K;zWnX4-IuwLVsG4BiKj0?f$F$1BA480r$EkE_izcU^viGr0WJ999;A2(gF zgxC2C2dMv?tyvud+{x%6j4ZNE`NH(Mxl1dAQk7JJva=f~fOvV$<^6+b7LIj%`Hc3D zrum-Dx7Ty>CY&kj`cb_KvY1zMo}r_Ky@l9M0?walCx1D)tjb=CUMLVB?*QipjXQ1v zfd9jdZ`|&!{GTYOj7%+M3@{MT^^Y;n^&cqc_6>Q`#sD1P7US9zoX`dxbcjRY}?0Wp6Qkc9S**9G3&PngMqlb|oPKBivg0^&vaFZ<@=^&E1Mmt-- zseirCb#&rN zDNi~sseZxm4XtqaT6rr^iO|*W=)AwxivZmS*W4`=( zzWUXEz3H0fdHnf)$+-jK1oNHfB)H7W1S8+Cu9qZ5bE|YDjA?xH1i_e#Y%CLBH_Z#c zkoCYqm{}nYZ|mF0;j-pPA~oauMd+C>vww({p&-2 zFCNzS=0z-?0{urP)eqYfX{ddeo27t!Zmaxjs6|`t#9K9!6ANKZat+gex%t?X`&D#U z(S#(n+m#=Q7f#|3L|YUQ%*oIQ2UD==H_kv?#l_S>LCIl*tDI-dxw$V>AFBaerN>g# zT4oM3(Ny=Fd$8+P*iHCZOw)&wEos~b)HpcQSlMe=&yU9{S*$b7PK4}gty`8=)h@ek zwW6B)eKxbkHfVL`n9-N{dEaU@;(AE`;Wd6ZpaIhx>)@avO;e}xR5}q2fCoMfDl9OK z!Y}AXd_Y$Fo+h>s8!59urnhR{! zKtlP+Wv6&cEF6m)I=(4BjT|P9&g?q)sD+3K-#zmc9FLaZ1qFyF*o84L?8yQ2m3A*7 z^QIN%`|Uhk)|Kstq5OY3=1{13L_$qYGy^dQkatk^n^h-ZGk%No?&^tYbkDIv3pET z)jLW4Ext4Q48Jso?~DRo==~`7o>`8V6fp&|{FDX>%|4LPv1@k~S-zrIz#slzj0b0A zE=FQ+X++6g!fT`prC2Y4;n^tzgFxVRz{Wb*n!B&$iK>RG(ulba#MY?xNwEJOs4p}a zi)MYYcgYIw`6rLP@lqMxQ@^oc$MELh0DEG8mBX^#k6(e>Lp=}3aW6xR#bi?*+%f+M zLS+vCsI0k!WJjo=OR&?PovgU+L)>24bFrp%R2*IM>qhyN=Xu3Frz4C=WWhcfse)#^ z+14^HH~h;=Sp$&Fl(I7Av`zn^>i%k zY>~)VN4ktDg|`E&s5KYORZ*Ra9;Y0|ltuy;gx?@qFY#J0FZo`DCapRCoTmat3LzQ=45}t`c5<~HDC^UFZ_vK z(|t$M&gYz_;@|f7fcqE31rJPEppLuO^mS9?p6Iv8Dvhu}(5!?rGk4~f37#2rixa$} zw-h%^H3tVr>QCyGPPV4n?=3e!38X5}OIkG6jsiM}>iDNRU|zn-pDG_u98yBBNHn)4 zZV+X`1jQO4_nz*UM4oVzbt{H`$qv?VYX;x9_M%k~I-AkUF{u7Ut0y|bQM81^V@<9( zhkjaOOe7?V&d>gk3w?D$uv&<_R(LKmHk?1pflz}01}Vb+pnkRuMgmj(5s$FvOp7!4X>{Awi`% za@7VHm83pZ?zauT=8H)>A#43by-S*WXKI%7$}dC=HFy7As73UigH|9;H@9X=#%Mb9 zXwfPjC89vaYySapqitB6r04rP0!q2#IX^!QSxyb*QdE6Y)A<_KHL@+2 zUm_xlm{!{@VF$5;{5-c>_k4hSB~uC~?G_B6eRufBq+5c&ak^L=T?LPq#RLm?Zn3A{L|(%lwD+ zeHqz2FkcXKYsB3m7#I?0DRnt?QE-%3%^slSOGl@xw7@?;dv$< z6s(P`_j#X}SC2D0hCRT4Kqh#8K&^kqAV-NtLgGP^&|Y%8d>P;@ilcnwjb(j_5Hna< zkgtobK#0w$-u6?c_>Q9g|PCl=%kxBt^0TM%<=A0&$^C&t@?Az z`BHV)N|qy_62i`g zjwANs6NuubHnIO(6YtH6MB;Za##_}5_wIc^rbr%$#IYoAV5R;f>jURhUhkycH}&A8 z-3M9|lR~ib-7hLaaou3xChu_LqWyR{izud0L7_hYCnUWv+!Jhx)A3vFsy=wuW@ax}tM z{ru=sauC#o!xz9Jh@!ZPG{S7yW)jdYhVTVrM6PA!4?tru@zms_f8bbZ!KllHA3xh`@na zP;D%}|5NV&w|12LC__a5iG1Pxpbb?2q4QOjFagg1)MM=LT>968eJvIUJ2>QF|9Cq% zFeuuh)%3k((Pq1F;%aVL%Xgt=>QdByXqj{KZbh{AnekU{vl;0ab~v2*sOxUmUGJ6$ z3s2tLyhTY|RG-4vKDp2D7svcvch{JP5Ov_+iMQd=lA>O6!sy#H`}Qz_T%|(&8?Oh> z*nn%O-JgkC{10xs`VWR8ReG2!%XXm=@$?eP@h@*LLd5IiD8hfc(j|UO!HBmXXnDTr znu=jJa5Kl=UL3)3Hy_FrfM`uZ#9dTAKw_9;$PM01-q9{AivVu_u~q==N|1pDQ;;^e=FKCLLGo-} z4=*>@>=rZI>edb?3)?JfBG!qb8$29>EYvgJih|80Ha+ilZWBEzZ?|s$hXQL>Yjf3a zsR=YU=ooV#{P#qCf?yXDlbwIUh|b>F%-&8rqp9{Al8K^SD5?0T3ejYc?~v^@$N>Ii z7%p=d zak@G@g{^rFO?X?jf)|rHfu%;)Eyfn7DqHE+3EBHT%0JHKrfEsjB=HoB*EK=IR*l{r_317M1r1zIUQ_d8 zgaQ5WcIBF>8T_x>;74?ww+@E^VrUfo%4ah1mDA*0SAL{AN;ntaiJXi!Hp^4)srhGI zZ<(q&kRUki&|vX7_*nlchgL**H&>xMi$0kY5%mHpaqv5qqfs&0m3fePJU|1B8?mX) zX)q z4-YEo$iN_20C?XtQnIg#+OFD!5d5v)YEt^fCb72bdaaN!|CX*Xx_P3< zdku9O&ZsszPe`YfUs_HTD+QHnCWWm@T}xFjKXJu==WRm2R!Js~!Q0$cR6xSf00lku z-Qh6`<5({fu5rV=!Qt6azj&nvx0gk>UfZ7WL{AW^=8LDdE@&ogjt4!Jt8`0x^ummK-Zgj- zEWxPUz*N3Ri7;%94iGfwcqArGE227y4bEeJJT?b|e8H4^AxAy)Z727xTc9(W@0Z;y zASw05v&l&6&&OR;?NM=#w*O=Y{f6jKzvVUfB#{6lz%@RkxCa3qH{qKpktzSaDYAKp>_0R2v`tDT7s2Z+UGvs^@sLjN9*@DeC3 zjb;{Al#H&brRH=AN;D49%s)_DURL<^p>n*CG~vs|Wt>r({aiV?aV@N+rd@@y{L<}c zz7^wKBlLvoHC%x9!2t#thkn5=<>lM{3RP;eF4YS2#a)9S7v)$oLh0rGJ%u&D5x?@rl zf@uRyk&!w33Z6c54fUJX%)S&9Ll9eL$LUf0sVFInx?RW^ZoQ-|Xefoy;={^?JTXOi z2NCoLs)U7NW6|r}GRaO3xrbiBPHcVibfr(8jNVJu>{ywtfyuJ!nQ1hXct@D=1Ha_ue^+nXxnAR3=$>dajAClF)d~A+8kOP0 zHFYoTf#?&_80V2wmXAKm1ZnMspgvIV^hgKd3WYFJV1aShkSNIeV0_ zbe5Tb)~SLj|6s%;Qv7}@;;&F*+NdSe3k&TvXHFN}Y#_mf(G%UyW$j9!IILT1Bz=LR zY@sX|RBVE6FuVYlI#XZ_5i`98b6h2zUF|ll0pPauOb8=k)w;yv;0?0{GmUa>S?1QI zoL)8*GR<=ufEa;y_7L5mGtp#B{6gjnu|#smpA8Uab0D!@p8X!0(h~X3z&$}U2iu@3 z*@$pK9QqA;HKHJAf=Mvs(B6}V^z@JKY6uqXJJ`wC|4G&vDl9~{^mLesNaI3Ex{6D* z0uU^lR&XW=rXt^$q)fihqmfzYHeSmY^CO6uDyu zPTE-VC`fc0To5+G#z3DRhIK9Lg8X62vnB4|MHZZ6n@&j!*38PW0^^?sM(F(TGBTb3awbK5TV>*xy2oNAqJw zNht^^#K*B}$^D%S*Uix#(t}5}SCN7yg;h0^!{X+X@hyC-dY8N|cVVAja08R+x=N5JkyAdBT4eTeK z-;A09{Bw~-H~fE>N(^y`pWkos&Jh0p#6B6i#jm7Od?f%ie;vJ&64G@zdOm#ExG#8Kzh1{@|^Rz>NiEaxP%6Im}WVT~BH!tfi zzpt-5^j;zjGB(nADN!{MSP~QytOpaQXRc80E7++ruvq3;NW6WN&h%uWUE&eaSZav4 zR~dS+0yw~y^wy}jOKA2KZGxOZ4B109z4io!y1aI3OrXi&n2+9)nrotQ%gWk^BdlyT zeSOq(HFF!PkA|@oa$s>zb(0BS(t>wl6ZvCwwqX*y`bCsT>z@fH=d@7YbbU)4GSa zOj4b}0|n>7iVGQ|?cdP~>oOe%1BO#q3xiO3yfZUWN`@M^xbAF+HPrINSrzd$>z(Zu zWZ=ZK$)kc!``<9JBXC*AiQv=q!&P$5T@jJQ6rsYQLiER2Me4stMyXgUr1D}}vVfg6 z1)2c6frx%hE_}Df5RG`Q0+ajHqG=0DOwrsZ-Zl0%KSmxgAx~wwZ(F2o;3^4uLt#(| z%?@X)c9e|yxp|o6-qs&tPX$*LUWvL!t#*p5v}GUI+h-0DJzaQvDm5* z{M4dS%N^!IXV24&aV;YMyH=x#aQgWd2|y8)furr)oq4zMPeGL53H`&lrC*PgW>ZCw ztQ+I3VuFWjQ*GfICfx5}S}d6B!YlqueXG)X?UPlyvo*XeizXunTdkRf713GwLDT$?A5^(WBwqf&ZH z_m`;nLaw;RtQD$}9dflD^d(&gkZye$m_4PW8if<=mY8sr!@{^Ya2Rqge*Y=4HRviM z7#E*kz?v>u7D;vQuExU!mrtC(39Aly%}WAptN@7RGL6*`qXw}HjUKfeyEGF#R`*R zh~)uIWkdp+U;(sF{*ffZVmHKO=<@LrUd9bgmfT%e8JGDl$R7;d@nYn2gNXmaSf&kT zT;>x&O*1d1C#SzGEIb|F?^jw0fh?Ri{9~0tt)tzO*lG%`h$5sxRqNd3&eM%Y)jEB0 zqHM;HIR=!*lGrOLyqB7*BhR%I|JtoC75@h0frl>QOLBebhbGi}J>Rm3x{Ot#@Rh>R zZam8C3hZYwXe%Ul!gx;T@yH;?NCPIUnar-c9_(KaVy?YJ$|potUB(V$Lgg#mnOw*$ z;)C&GuS%=#-M{=63?BbJWJ!d6)z8y@rRp+ZvfD1D#g@@WJ2H{ARh1vCV_^I6AqxXe z(#=n7$EV7gPE_G>@kjAZhA`)bHIA=CUM>0R_aX$B%)Z8r>Q(J;ZwnIK^h~t``te6h zEdvE+JxZ2@^f`T#>EVVeTIp$@L?s`GgtBImE106-8f@Bf#W~^dIkApRPi^;_Z?+>4 zG&#Li6C;^R_bo75nP!;0t02tMxuyYYtdj&i>?ijzg6g4jEx7y_-7wC6aD+VUZvd-w zE5a|^c6;a3`@^NPAbi6+7SGm8Q3HP#mF19BUVcxvLsOkYD+|dO4Kc}&L3LpVRb6cXIk zC@4`|X10cG1gsz~OcsI#gvi)UPqLM$BTF*1Q2jgvAX?zMSvPxNS7IgUW9fORZhu)~ zALgnxrNuqXqRHPC`5LPj^OF%pHcg`vN!gkqh6vuzW0FIP8AJSaTcFrA;w*B_T-0#j zBPB(&SO_dQ&>jZGSs-%llsR<60(U&3_iIIO&#cY~M}Z&*3lET1CLqKAXQM-OlsxK3 zQUI9@z-2=X=_|JSpsit^r$ic;=pRI>v~Pr~ZPu!_Y@$H~F6Jggwo4NFQdxa*EZssk zNL;UOGIlx|%j{RsF1{BU){~Tj$%?`OQ_k+p-;1@z_sdfL%Fp{eLqAZaurL+{(_1ij zS(ebc(S#&oV{HkHw1Kw3N<6~IDVUK8r49;Fz_5EPF7f6;8~HwmKq9lhdu(Sk6$cj# zo+tKOj$^csZ>TYy<64T`kVb2zk3=O-279H3>eOYY@tLQ3N6iNJya9*Yz~6o+*LlNF zcX-Xtt%9_yb+h>8D*K$>WF$K_8}qI1($q?_h$DLWYKMvz;wSlhvVSaa)n|zGx;@6L z0Qb^WDo%SAs#bZ`)|CpIu9~Fv0>)70dVC$q%5x=F8+XRS6R6`3h=bM^3ziRa03ucFs=@>qct`pAp!tA&o1f8WFsTW=7o zbgecleB3)7z3M_LL>5Mr*A7mh`k|Uz0Irh1FZ}dkNd~5I$7sKiEVedb`Erm4s<4i* z_PRDrboL=Zer|Ur6C)|qJHuy4852rfNX5M$X+J&mNV>DoRcteSQQL!HO^9wfFe_l2f!zXk-8t zoIOZ|4f>Wb*AC3WM6#x*t~ewVAQdd|)&R-Q87XgaE_hXm6}nKmp9*x*=n+*7ag4Q%468$YKu^McJeNo*DG`We zRz8N5W2md=m`dDDBQ)c%ZCUn+^;c>lTS=f7>fCYnke z`5*uK`e(_HM3ON&ev**{WRlV$IiUEz%EUe2>y=tnCc+{~!qu&WF}#>dg3ILPG8f~D zf}iv8&;EI&O*%vXhQ52F7}H;je4hxhZ}+FKAcDkg_e77UH@TlTe<@fB0={pMdnq_z z>y)XDCh530*(dmMWhuJs3N1{BhG~do{Js4NZ`mZ z`K-a)kDr=OeC98`#?9>vU-TCqi)Zr}Q;?J01Ggd+`G)6R&V}%xS5W~aqK&A2|&Vu!_; z`{gWmkwe4T;M91rqhl=i%yd-9P&hnx>$DtI^VNsVr=pR*OhHG^epX z96iW$C+Omxay&VcaftV~6TtVH>qP`n|Dt##R8-*>iCoFG8GzF2=|f0%A&=>IUluc? z*$14w7MA#o!|F$fGdjRkTWgq7fZNxF8)ZcAlE2EcvtH?d-7)b2Jk9%>3`p+JWI}X^ zb-jgKs;Rg6BUr}e-`3G7{H)(k>5Uob<#tF9=-xcy0o{H1gv_?|<#O6n4mhmNr7Sgf z(|g!>dIRo98P@U?JcoPprdI&c%WwZ{7QCqD6vff%yKAu5{hn|tic1+I$Rm}+FudwT z;P%yk3@|4STZEs<{ESUE__CoDy^pUKaooCkMdO(N;U`p%M6ZL01Mi<9a(E_k+0*$z{*h!*WKp?M%t$0Z&{bMVT z&TUAv&n9^xu%~RBqy#lbGOF0VIVq#F0ZQjdbi1?&fF%$ZIy72*m97v(_a!;fs1AgJ zP68)UIqKKzLh8TqvSBAMQWld{8dKb>{j7KM&1guh+de1)lhG%b&A>2kZ?XPD-%l(< zL4yFwI{p46xwqutAEJTukCteSaYk2-eY!OC46@lJ7}Rf%%n}FBBDRq^{D4%3356Ci zd-{ZVfbJf3VetSC-r}TKn(-N@bxYw!6y>}jHPRC~6dezIxuWPQ`Ym1^Py{6jb3nY* zIG(1fJM;`{bFBy?C|G9HIS+82I{vbXbxT8oZ6iljbCEvj(!4nr!&b^Ojzt~pRH1ht>S=;`KN zZ--`tauk@qsvkC@ri+K)(r>LR8Ex6chFEm)ziFhYA#3Rfnr1X6wf_n_I-sMjc5EA9 z1;kQ?sqp+NpOobu*PFnfy=)dPrxG>bV$JhDO~Sk+q9yAjG7byI1owOYKGt)-FgGwr zwbLJ;U@_3~)}Ee#sTr_#A_{xfvF5EYbaXPRym!jv#z)hONa?&lOV6sj^4M5zagMlO zl7(RyGKVR!(wGe_hDps=?{m>B_$N_?3<%HB$&o{;TE|%lDq}#b$x?BvebZN|*4U)E2HDTBY?mISw=b;fw2bw+K$ApGM8zjh^CZ(pEZ`GYgj;IW9jwOl&& zK_sAbL#+FaERcKP-lK=++mG;#mM8t9Cq%Eo2ZOL%*IR74iVCh%sy&qyeuU=FK2=5! z#Wr130&Y{fQc(tfTv4^KWIxqx2f%q5g4OxsfKxqnjkoAz4X;`S$$r+8hM7)w-cf>q z2A>83YGRQ8^ZHA*J;M!>PdjJvp0WoHs)`&%wT#uo3PWu!+)A;yD&ZpePXZ10G)>LU zLP?5aY44l$^DC=$7rG8#4e#`xIc=&6;#K;o9QDaqxx}G46o1R8bq3$x4uJoD)J#RE z@$ZTRX+-Riwm5SYs<*&rL-pUHU$Au)xRr9Q8Ay%c2~)dg(t{bB6cF?NM>oF$#WJ@& zJbsJ%>C`QtMr;-e6W3ST)ABb>|6|KyjrdhN4c15j96q=VS-oplOwX;q%JOv7zOCSjq^9V`(1#ASAhVxDq={@s`T0U!*u_B;TR`Rm zSNti7%oEsPbph2sH%uB|ATNm&F->XoY8)qtTMbJLY{ED`NT3$E!mQXCfK!i%8UCq7 zvOTyI+}M8{ZRE)rWGKQ|uYPjbZkRKCxeme-6Mr`m6pQ7R_5_ z@+@cik|E9~%IvAl8v{q^_~c{uhb>Rq(6F+4p!=ZJ74e?dnYOa%w!aru&ZUr-)Udrq%)P+6&5w$MgN<~;Ja zBv+dLOp5=58+1|g{Ipbp&^qPX zYB07!pi49fFliqmc%FgP!3I*FLYg zSoB0_82s2Wu%XULX?X1X!~u`8L;8Y`M~~`;ALDT_bJ%IF4*>YSTcaZ=%^u&)$?B`T zG!B7kzWUb#n6KwMUesv7+v7TlbVNekQB?!<^6yDThv(JFenczZ!geM44LINO`k?^+ zX*&1cYbN_PMBTMy?de7CAN(a4SDT(e4M5ze1GRNljR%KCip@EoT$!R&X4p;bQFcX% z^yKP6QL}1=*afVM$x*KnNt(vU--}8TBPc?z(&mEL3utg0nr&jB#YA`iu`;WIjg%O^ zG?=?7C-jVuMyYy!*^Z!?fj1uf_b1^>bJou}{JX+wR$-@WvYX|n-<$~ki*vBAk@!{c ze@{QM!R~_MAKCTrGXeR2jKTl6UNPyNg$H1-xS)hK^6iPcwTzBU6CNigwg%b&pFms& zJspNcnQ6q-r-`{dnR$ib$Xjq5>=E9RWN-0f4<*p+M!@0B;ofzqyyq|79n@vCCB8JzB6>9i$C(EF(S_AOe zWW|my6-jTfab0285B-}-iaQi~;J!iUu&z;m#+IeGXq(Ce+A~HPPaf&Af|BZ)m!EhE z8Lw*^uAakl$XW!asWSzy+P#O%k&|7+owaGOKd<8Bg>}EJll)T(KH*M32NgfjfUMCx z%Ntdbt0vPvCg_k}B+b|+soc=)=?lohv}eE>QddmeP;tm19yb>~^gN&#_(O_7wA?|| zF7$rxH;zmjyNZ~?!?Xjr<#LUGWaQsO$|FY*!LyV*RXxN!a#K{!m zaNw+m^`Kr#&N(%bx}G>k%E4NJ4e>({r)@yEWwS8FF@vJhVYP+JLR=Z~#R9}bU%yhr zjo-*V&C~h4L9r=(T7!(wum+%Ua_Tle!RJ4%9oI@=RL66@blrN;t4pZ)Lgg4O%}y>; zik@-!rEPG?ti#Ko=f;?<6{VtP0W>pDNMo3OB<-R$!IQ#A%6FUnK)-$xCE59nbG>+$ z(t(WBbcH+I(IK-#NEL^$9f0uPw)br9h%H-vbL)8U$IgAnv#G(^mS}k_y+&V<1szya za@Qj7eo)V2Bjz9j91cu_)Y6pHB6sG|VgG&^2TFsN;O&Ry=kdD8wd9X^#A55gE2A4C zDktxse_t-7@V*Cvy`Swz_cHYs3#+zah!7;2vstYYtWM{fW#XfIaPZj8J9Z}4?1vi8 zC0}P0X9hqIT5*wMF+YOS8iSp^tM{YNQ=$>&y!;=AQsL z$QcZxZ(}W%+)v{OnB6*S`vgPH?i(CKHbEUV(rqEVbgo?)C;F=LUoAF?VCp*!ODMfvG zXj?1%(v9oVbSt(j(s>Au{kqBmbV{kYt)7k)S5-sD0Oi$z9dM?7uDMzEGtQGPi0#?X zIKF2%wMzt@w_vq55)&U2X6o(b?Th8Rn@n;mQ&Us3^aT9FUCF2&kA_#*&bENY?P3p? z8apP2#Qd3J3Bq|Kh(lk{^5kr;{=&sD;dVN{>FbCv@W9E||NDU)rh`S)oRdrZIGDfyZ|AxJkI93pDIMTAKkM))y znJ(bB4~cFS)c#6&f)Ockn6 zEMA%P!7l|k9RG=jfQcz1M>R=KC=ccl;n>PWLs3vceCyVvxp;NiULh35?V-Lfl7U1} z2>4$Y{m|`5c1a>h(^r$zzV}m;SD$b9pOpSTnLKepxtzhKShh$J`eLTSeH=RYMjke_ z5Gq*aw0`?t-rC)o&|h%S1j|?r6B{3-t$bH2Rm}h{x6gGDQN`Ktt=`h9g7c1BlmWHw zt=cUWElVX@;U!}%Esx)p=B3@I!HzE_$Ki4TQ(f)^?IMoYOI{N(SkdYkb53d5?vr(U__{gB6`cOzX#u+tFPe1cD+{1+`By-{;EEmfN)06#{OEKjd zK0(>tln?HXxq7GH^sCVJ$A4)`OGJ7Uypr#F!Lj808f;^F34;t<%uyOW3LHgZ@Ox ztV38s>p)J5l1-leF0*hZv{MX47-K>=dhkgPG)NW7EXSgF_hN-3ZG#)lks3HqAX5QY zDK1zQM-(B@W$yg3NP;|h4Xh{p;Xe~lY#7(D=^Yf=Y}&Qsaf%IyRTASkjE9EnBP0n(nW?melyg|9ojORKkXoRk=m;B(gGe0uqWx^F z)C*K#%{QcY#QfjV!cy3|A^AsIc!B@?v8GRA5P(Qx5XJ?Bn8Tvy)946Q&8nDD69p$D zNuefEsLR<%J#Zw;YwLN8U8TO%_Kszx8FUI2M9ftJl9;>KYN#-(Z)d#)kFuV-pWQ8b ze1Cl-_mXJ1J(x;_F<`=b|D0j9$+5$PQ18F;%Zqh?@(1JfTJ?Eagxh4%+Rx`Wx|+S2&3! zclw^aaH2Y>R*n0QlC_7SZqp3-5f12IWShU&E;4RFCuZ~w%P0S^7lda{AJ~TNiKWLn zWm!OKUrnP`xD7{Dq>#Nc^d3HuL?GdL`AKm--AMw3*3|gXVl)*8o%t?PhIxO5X^_%+ zTEeKqMLa%lA)EtD7GUg0ei{~ho-c6KWNLrW_ZZu6 zxHQRWc6fV_`UfNjkki?EP1da?nTmsHEWy)p)``9ko^>Om@*_3u7X>JrVa@m4TWEdG zz`6m7wIu066V4Y+($78>XUU!ETCoYlW5}dS!vsb1`Rdg9!V0?S{Ps>{|?PYTbl zQ95(@ldh;yY~Dc^F)u(ONb-~HX$r(*Mos~Zz`4CdiX;>4a`yrUe`8Sydr=g{gHw6s zO-1HALN<`c=r@VuQ2&xg?=mb+l7#&_C_e~Lpp21!LWel7^6`lp#*Q1&ChrvCVAuTs zvyb&VKC%54Pf`mL(HTy1PGE1~!${U=v?>8@JIL$(EjQO42#G8Z7M74REZ<<7d@V^0 zJa8Z{<<%ZnLj5Pa{`!9(StEHrb+Df!+xBBZvi!FRnRS8;NKxH!oL55oQlLhIb@ks^ zo(~ zbDwjaz47&Re~;-8+GKC0+?XIrA2-5r=4E0$A`%q;Ygd}lM|zYka|EmrH(IaXW+v%s z_torQ1)|A6fNRM-Y-qnA#`wZ5o3%u#ffQ9ICXJiAAPO>N)OC@Y`Ug(^5ie#aP}c zRFQ31Mk4I|VVy&`*gBL^w5$+{{OlJ^x4cPfmeR{TfU`xLwkUCYSU@MzyU#YC>@{^f z^(ZcPL!IoZHQkZ-@|>i-ub)iWH)5vIbC4^$+m6rMBBuqDJ-<9M#vNGYEsfn(t1gC) z_(i^*ab8{Pq#fUS&m|uG^ro>GmzLjU@cQ0v4V!Fipk;cvl~+a2_8AF8<9Lq-!drvU z3A(=%Ac!=KzJBWJ%l|e8ACyDTl}DN!ZIzJ%sHM_>Ohzd-g2oOt5kV6>vt516H|+Hh zXG%w|F+m6=bG0Lc9SOB&UqQ|nGb@{NvqNnq(zLslc0-1`Fu0+J`dSe-uaUjPbP6p^ z#it?7r@_lV%vqa#Sg>EdC2RC1%f2d|L(^^_hhu{tp7HSiA{9qm}7iZwz72`~x+3;b&a8?Ywd z){ioJg|a%;V7wN@?+;IG2Q)I`_qm-lH+g~`@uB3Y=&A0m?!L6Lq*#mp>j}wU`%aC3 z=eFMF3!ur~vr>m`o;?XUAn9I`T{SolD`j&@BN}OV2n?Cd+dwX zH|KjX!n}mH5{#x9^+GN=f;F}@SZLbm^|`0o!@l%_E}k2!U+_+DIE7V#oo^xWW{&2b z0MZk^T%N?q%k1=WW zUl~P``=V4mMd?{Ottwka#TXm@y?J}A4!6AW^lXvXh>4G|jQe7F3J&!`k=}?4`Hg+X zK^lyYIL(qwiebeP1FO1gJ453E9D8Qgoh$I55IWF3CM` zY$DLPQ2>RL^j3+)Eh0mnjH>SM!s|4q9Dqx(fgfcHyB>1|%+#56IT?yVPJY>S#8t7b zMVM*b{0&4{S(J+4c=^djziLaTfZp96n$==vnB5v9y4hkHW@s;>p=9bi798$4f%s$MEyS7qGwfi9{gQwH#}?zGD^3tP7o z3OuB;fWGhM&4{kZk2fh-Ofo#N)HkHPrLnxKRW=69GSv>p&e6IBEUKDa9)bLz74D;m zo53Vpn%1K^eQb7(BwhFaYwIe&qFTN&LnYmTPudZr zPLi$KG;VxQ_&;7()1Qt`O z=9KPM+*_{9j_22?a{v!rh14YYcujBM%Bu4D(A17Q^2WH^_=I*7{gh?;BDI=j_r^C` zCEj$M3cq1)}z)+@S>|4;}Q-d#-Cme^SPp^rZM4QsQrnAl!Q{9md+Ow7M1CdY~QrRW(C zehi6FyiJ_!T+isKe=VJJ{`sOX9HC^*p@yzE-w*Ns7&FV)JYp~VN@=HxCs_Tu=u97x z*nt;1Luj{Qt|h-qF$G6tLaF{^6zY=m1?3?e-bhrdbtDy*qrfIa+a$YDV$&VRsG%5; zr<^v3#Ec|cZZ!*qrxt3rRu|V5t6k|2k)!2(p*P~`uog5>kHo{_7gTjfSQa zST@#JfysJjvRT#E8)P(9r3Ei%JS`_7ydEU5d&0dj*IvR>_jn=+@Fk6Km zYNXqazxhh9L}<~-wSh^D3rjQiRm0e2)M;RO0Nn9$^WgkJhvH`!{q|C?Dtb2rb zfz9``^_OLsbJQ&bHuM1NI0G^d&Qs~X_S=@QS% z-Tli$e-=mgNzr;ooos4th5se1?zg0sSq!Q9Ms-vqqd`{c9OzFj&?VLZdFdpHAdHgR z?UwzL=KDPq{^CmoD>212&wB^ zQ^&^k57kc<(k)C7^}a@v;qS5^-XA0P3?W<=rVxGgcsohT=uVSel)I>o`pZVAUb=Qk ziFJw~a8gUD8wq}|0Ia=p8LdmrGn%z{nq^k_VbE`?;Zyntj)0Q;p8G+1C~`4ltIQ|( zIhPR3DfbT^)fCB8OsSS-Ks8OP?Xt=Wox5p7VVmJ3+^VaAhy&YPfy;l>n;{jGg)Ksd z_^a>8^}rp=m#N2V>yx2Uo8`7%W2m_I`B$aa-z7n)Y=lti_t=>$ufu%S4eVqb^=L>C zxD{8W-7m-8hsXBcH2q!|x-;e~G);biRW`t*ko~7I4G^}9Gvis=A-@=ZE|ORdO;j2o z_G-$>-=WO*05KWR@PoLeBkRhQ&$mO6VQ%{bq}ZLSrkB}Xv3|VHdE=Ty?ymZTKz&BPWGxB(Sa}$!H5zX<%!~hr$Jf{46WQ z##KMvVG(0NM=Jw)KPTimZABKc9#5p)moCKFS_&F%SwU1~G?~T^^JRW=;gTq)4y0i( zEK16u+fzc#ve_Lt{KVASM?2~IH{ey;48FIw>(J$a)nx@V3!XVo7#32g z)sWV%>R+$HU1Ap59E29DI{6wuTaz;=U zQ@@2Apldtlr4vyv*aC*a$i<`Q3V4l{`vArBuzxRTU}L%1F3fWIpWS z-#zzo0kZo9diySH*o?x$L5RMcS}OViW^6pd6tTcuuhRXcj5sp~w_}hCX4dUp)x~L> zbl3eRrvzHnK8Y>Fio)|Id-hN$UoC{DN9{qE$s4rR&w%riXz|_7o|;?JZ^RZAj5!gn zMRuSifb7D3aTKka>*GgUpBdV0*U+t`&axlsu0p|ZPde!+tgr_?b=x^1huGMUSKV0tBg%?2c|9_6VIyS|#)0?>lW02oF{=GrzVxpX0%wTR_MNFi zH(M8x=$mqwqai5znn-ZO*7GML7BOiLy9=l59ZI}wP+b2uOoY1b#kVf1&}E;Dj`vH( zFW$1)uC+;A7A|Fm4}24T`oOlf(K*;5K`gj_V&P^LGns~37I4UzT#*|FO zGyzi2Qi-YHY38mVDpi&Y;FQFqOrplnto2N#^J+fn-d_2M<1kKN@lDolmyR0`h6V)j zytL~w7iglUNv{&~yNbm6rUxbQyb{Ts=%L$oVQ7`V8C_@mASpaJg>cl;#2K-xQs1uG z571!5yweTH;_@-vJIi+;O-738OZXIEK6wKM%P%UF(5b~;r8+4GyaIjGb6eMJdR8>L zzWUzRu^J=3o2ZPttV(81MD66>O23Ib{dw$AvL3o6vXA{?b;jN*kd>6 za(c0s?H{W!9Ixh99Ol&)jrdUj*L$~v9denAXI!O8f=xH?`WuT?bm7gr1HO7933fhB zC`IXQruKQ*G#f*u=0DQXUkL1;Xbhrc=Uq$gM9T{F$k@anvKwbBZfWySkfcS+6$Hrf`SXC(O2Ewwf;nR$v{dE)!K~`r4OLQ4b+3#F9__6|?n~ zblJMZx|Bq{_S%=h=he+=Rgx1u_RIa2KQ#`LF1V?GZf!~b)^x|~k@-e%=l9GJ3V8Sn z2o9m7Z+SWm(uP5*R>Sfw=YXtMDiC%V^!NU&O3(%j5{-XEp5DX< z(me60eifaBlrrWEkmLkN z<_G-)m;l@A9VErVl3>KbVmbr#Ao~TR1WmH>5DE}|B!q-Ao=9T^s8591%!UFaJ^VnT=4-)O?-$7wB+bR2onc$rcR4FLFe@xkYu#-e)%0 zIwJ!rdc@w4LHb9ws^|B{1Xx(0p-zrmq^I032p;e=?>wNn zeV9Ks{Td25Zxcdp>m3ymHWU4!#F@3fPUIcluyY0=Na7E`$pTa);A_}3fF(l^z)Z|% zVQ=o{cHFs4FffSu-J|LsV_>J6a%Po{Gjd#PfH{oFDb_RV7Mx+VzX!6?0fz5L{?PMm zj^1xnF0#W7%-G`w&4Wz@B|0w1MY6jC(sKewWGW7@C;mFfur}qK-Tw{e`FGg!gTtQx zxP8y(V316AU~oeD(61{^U=gRiuXE5y*9W}pcL8DO*mXN-O_Y>zSq=N70T5Y!5wq2rhAkH_zLAUiYYVmW`(``lMykkVFw$(ZAW)CrR6 zdq5I1plSKLpGw+50m(H&$ZS7wV@)0$3#9PV9k=Q&P`V~3_ \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +118,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +129,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +137,109 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index a9f778a..6689b85 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -54,7 +55,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -64,21 +65,6 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line @@ -86,17 +72,19 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal From 9c7de326c69e38a35878264fa008e292df461737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 27 Aug 2023 15:51:23 +0200 Subject: [PATCH 039/157] Update dependencies --- gradle.properties | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index ef5eeba..2f00e16 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ -quarkusVersion=3.2.4.Final +quarkusVersion=3.3.1 quarkusAmazonVersion=2.5.0 gitPluginVersion=3.0.0 -spotlessPluginVersion=6.20.0 +spotlessPluginVersion=6.21.0 sentryVersion=6.28.0 -tikaVersion=2.8.0 +tikaVersion=2.9.0 From ac71abc346e33bd95d1e6f0bb2374ac9461504f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 1 Sep 2023 12:14:39 +0200 Subject: [PATCH 040/157] Use one-shot gradle tasks --- .github/workflows/validation.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 75f71d9..2aa8950 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -21,7 +21,7 @@ jobs: cache: gradle - name: Run Spotless - run: ./gradlew spotlessCheck + run: ./gradlew --no-daemon spotlessCheck tests: name: Run tests @@ -39,9 +39,7 @@ jobs: cache: gradle - name: Run tests - run: | - ./gradlew compileMjml - ./gradlew test + run: ./gradlew --no-daemon compileMjml test env: MP_JWT_VERIFY_PUBLICKEY: ${{ vars.MP_JWT_VERIFY_PUBLICKEY }} SMALLRYE_JWT_SIGN_KEY: ${{ secrets.SMALLRYE_JWT_SIGN_KEY }} From 1ad07f77b4305db77ed329bb5f99bfc6acd81d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 3 Sep 2023 18:45:36 +0200 Subject: [PATCH 041/157] Fix email max length --- src/main/java/app/fyreplace/api/data/Email.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/app/fyreplace/api/data/Email.java b/src/main/java/app/fyreplace/api/data/Email.java index b3d619b..1c09e78 100644 --- a/src/main/java/app/fyreplace/api/data/Email.java +++ b/src/main/java/app/fyreplace/api/data/Email.java @@ -17,7 +17,7 @@ public class Email extends EntityBase { @JsonIgnore public User user; - @Column(length = 255, unique = true, nullable = false) + @Column(length = 254, unique = true, nullable = false) public String email; @Column(nullable = false) From 59f1e5707b39733c003f5cc2d94481d3ead98707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 3 Sep 2023 19:07:43 +0200 Subject: [PATCH 042/157] Use verbs for one-time actions --- .../app/fyreplace/api/endpoints/EmailsEndpoint.java | 2 +- .../api/testing/endpoints/emails/ActivateTests.java | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java index ee4fde9..8035623 100644 --- a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java @@ -121,7 +121,7 @@ public Response setMain(@PathParam("id") final UUID id) { } @POST - @Path("activation") + @Path("activate") @Authenticated @Transactional @APIResponse(responseCode = "200") diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java index 1586c6b..5326ea7 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java @@ -34,7 +34,7 @@ public final class ActivateTests extends TransactionalTests { public void activate() { given().contentType(ContentType.JSON) .body(new EmailActivation(newEmail.email, randomCode.code)) - .post("activation") + .post("activate") .then() .statusCode(200); assertEquals(0, RandomCode.count("id", randomCode.id)); @@ -45,7 +45,7 @@ public void activate() { public void activateWithInvalidEmail() { given().contentType(ContentType.JSON) .body(new EmailActivation("invalid", randomCode.code)) - .post("activation") + .post("activate") .then() .statusCode(404); assertEquals(1, RandomCode.count("id", randomCode.id)); @@ -57,7 +57,7 @@ public void activateWithOtherEmail() { final var otherUser = User.findByUsername("user_1"); given().contentType(ContentType.JSON) .body(new EmailActivation(otherUser.mainEmail.email, randomCode.code)) - .post("activation") + .post("activate") .then() .statusCode(404); assertEquals(1, RandomCode.count("id", randomCode.id)); @@ -68,7 +68,7 @@ public void activateWithOtherEmail() { public void activateWithInvalidCode() { given().contentType(ContentType.JSON) .body(new EmailActivation(newEmail.email, "invalid")) - .post("activation") + .post("activate") .then() .statusCode(404); assertEquals(1, RandomCode.count("id", randomCode.id)); @@ -77,7 +77,7 @@ public void activateWithInvalidCode() { @Test @TestSecurity(user = "user_0") public void activateWithEmptyInput() { - given().contentType(ContentType.JSON).post("activation").then().statusCode(400); + given().contentType(ContentType.JSON).post("activate").then().statusCode(400); assertEquals(1, RandomCode.count("id", randomCode.id)); } From 21287f9623a7b3660df3d3ef4c1bdb3316bcea14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 24 Sep 2023 11:57:13 +0200 Subject: [PATCH 043/157] Ensure generic test data is ready before tests --- .../java/app/fyreplace/api/testing/TransactionalTests.java | 4 ++-- .../fyreplace/api/testing/endpoints/emails/ActivateTests.java | 4 +++- .../fyreplace/api/testing/endpoints/emails/DeleteTests.java | 4 +++- .../app/fyreplace/api/testing/endpoints/emails/ListTests.java | 4 +++- .../fyreplace/api/testing/endpoints/emails/SetMainTests.java | 4 +++- .../fyreplace/api/testing/endpoints/tokens/CreateTests.java | 4 +++- .../app/fyreplace/api/testing/endpoints/users/BanTests.java | 4 +++- .../api/testing/endpoints/users/DeleteBlockTests.java | 4 +++- .../api/testing/endpoints/users/ListBlockedTests.java | 4 +++- 9 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/test/java/app/fyreplace/api/testing/TransactionalTests.java b/src/test/java/app/fyreplace/api/testing/TransactionalTests.java index 39da0bc..c49d2f7 100644 --- a/src/test/java/app/fyreplace/api/testing/TransactionalTests.java +++ b/src/test/java/app/fyreplace/api/testing/TransactionalTests.java @@ -19,12 +19,12 @@ public abstract class TransactionalTests { MockMailbox mailbox; @BeforeEach - public final void beforeEach_insertData() { + public void beforeEach() { seeder.insertData(); } @AfterEach - public final void afterEach_deleteData() { + public void afterEach() { seeder.deleteData(); mailbox.clear(); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java index 5326ea7..68bf185 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java @@ -83,7 +83,9 @@ public void activateWithEmptyInput() { @BeforeEach @Transactional - public void beforeEach_createEmail() { + @Override + public void beforeEach() { + super.beforeEach(); newEmail = new Email(); newEmail.user = User.findByUsername("user_0"); newEmail.email = "new_email@example.org"; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java index 4dbfe93..b9f919c 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java @@ -54,7 +54,9 @@ public void deleteNonExistentEmail() { @BeforeEach @Transactional - public void beforeEach_createEmail() { + @Override + public void beforeEach() { + super.beforeEach(); newEmail = new Email(); newEmail.user = User.findByUsername("user_0"); newEmail.email = "new_email@example.org"; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java index 1864a29..bc6abcf 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java @@ -56,7 +56,9 @@ public void listOutOfBounds() { @BeforeEach @Transactional - public void beforeEach_makeEmails() { + @Override + public void beforeEach() { + super.beforeEach(); final var user = User.findByUsername("user_0"); range(0, 100).forEach(i -> { final var email = new Email(); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java index 05a4854..6fc0057 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java @@ -67,7 +67,9 @@ public void setMainWithNonExistentEmail() { @BeforeEach @Transactional - public void beforeEach_createEmail() { + @Override + public void beforeEach() { + super.beforeEach(); secondaryEmail = new Email(); secondaryEmail.user = User.findByUsername("user_0"); secondaryEmail.email = "new_email@example.org"; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java index 5070d56..337cc60 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java @@ -130,7 +130,9 @@ public void createWithEmptyInput() { @BeforeEach @Transactional - public void beforeEach_createRandomCode() { + @Override + public void beforeEach() { + super.beforeEach(); normalUserRandomCode = makeRandomCode("user_0"); otherNormalUserRandomCode = makeRandomCode("user_1"); newUserRandomCode = makeRandomCode("user_inactive_0"); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/BanTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/BanTests.java index f06152d..5d12a41 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/BanTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/BanTests.java @@ -117,7 +117,9 @@ public void banAlreadyBanned() { @BeforeEach @Transactional - public void beforeEach_banUser() { + @Override + public void beforeEach() { + super.beforeEach(); var user = User.findByUsername("user_2"); user.isBanned = false; user.banCount = User.BanCount.ONCE; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java index 51160f6..8c25767 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java @@ -47,7 +47,9 @@ public void deleteBlockWithInvalidUser() { @BeforeEach @Transactional - public void beforeEach_createBlock() { + @Override + public void beforeEach() { + super.beforeEach(); final var user = User.findByUsername("user_0"); final var otherUser = User.findByUsername("user_1"); final var block = new Block(); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java index 1882942..5c17537 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java @@ -56,7 +56,9 @@ public void listOutOfBounds() { @BeforeEach @Transactional - public void beforeEach_createBlocks() { + @Override + public void beforeEach() { + super.beforeEach(); final var user = User.findByUsername("user_0"); range(10, 50).forEach(i -> { final var otherUser = User.findByUsername("user_" + i); From 8f4943c44e823f9024bc7e2d8d11cea9ace6a1c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 24 Sep 2023 12:01:47 +0200 Subject: [PATCH 044/157] Check for invalid page indexes --- .../java/app/fyreplace/api/endpoints/EmailsEndpoint.java | 3 ++- .../java/app/fyreplace/api/endpoints/UsersEndpoint.java | 3 ++- .../fyreplace/api/testing/endpoints/emails/ListTests.java | 8 +++++++- .../api/testing/endpoints/users/ListBlockedTests.java | 8 +++++++- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java index 8035623..9e437f8 100644 --- a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java @@ -14,6 +14,7 @@ import jakarta.transaction.Transactional; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; @@ -49,7 +50,7 @@ public final class EmailsEndpoint { @Authenticated @APIResponse(responseCode = "200") @APIResponse(responseCode = "401") - public List list(@QueryParam("page") final int page) { + public List list(@QueryParam("page") @PositiveOrZero final int page) { final var user = User.getFromSecurityContext(context); return Email.find("user", Sort.by("email"), user).page(page, pagingSize).list(); } diff --git a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java index 7052757..ccff4e4 100644 --- a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java @@ -18,6 +18,7 @@ import jakarta.transaction.Transactional; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; @@ -265,7 +266,7 @@ public void deleteMe() { @Authenticated @APIResponse(responseCode = "200") @APIResponse(responseCode = "401") - public List listBlocked(@QueryParam("page") final int page) { + public List listBlocked(@QueryParam("page") @PositiveOrZero final int page) { return Block.find("source", Sort.by("id"), User.getFromSecurityContext(context)) .page(page, pagingSize) .stream() diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java index bc6abcf..d0eb093 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java @@ -46,7 +46,13 @@ public void list() { @Test @TestSecurity(user = "user_0") public void listOutOfBounds() { - given().queryParam("page", 500) + given().queryParam("page", -1).get().then().statusCode(400); + } + + @Test + @TestSecurity(user = "user_0") + public void listTooFar() { + given().queryParam("page", 50) .get() .then() .statusCode(200) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java index 5c17537..ab3daa8 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java @@ -46,7 +46,13 @@ public void list() { @Test @TestSecurity(user = "user_0") public void listOutOfBounds() { - given().queryParam("page", 500) + given().queryParam("page", -1).get("blocked").then().statusCode(400); + } + + @Test + @TestSecurity(user = "user_0") + public void listTooFar() { + given().queryParam("page", 50) .get("blocked") .then() .statusCode(200) From 3c777fab4eb90a71bf199bae89b6858f28424761 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 24 Sep 2023 11:52:58 +0200 Subject: [PATCH 045/157] Allow getting users and email count --- .../api/endpoints/EmailsEndpoint.java | 8 ++++ .../api/endpoints/UsersEndpoint.java | 8 ++++ .../testing/endpoints/emails/CountTests.java | 45 +++++++++++++++++++ .../endpoints/users/CountBlockedTests.java | 43 ++++++++++++++++++ 4 files changed, 104 insertions(+) create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/emails/CountTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java diff --git a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java index 9e437f8..e386cd4 100644 --- a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java @@ -121,6 +121,14 @@ public Response setMain(@PathParam("id") final UUID id) { return Response.ok().build(); } + @GET + @Path("count") + @Authenticated + @APIResponse(responseCode = "200") + public long count() { + return Email.count("user", User.getFromSecurityContext(context)); + } + @POST @Path("activate") @Authenticated diff --git a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java index ccff4e4..1ceb0df 100644 --- a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java @@ -273,4 +273,12 @@ public List listBlocked(@QueryParam("page") @PositiveOrZero final .map(block -> block.target.getProfile()) .collect(Collectors.toList()); } + + @GET + @Path("blocked/count") + @Authenticated + @APIResponse(responseCode = "200") + public long countBlocked() { + return Block.count("source", User.getFromSecurityContext(context)); + } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CountTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CountTests.java new file mode 100644 index 0000000..aea13c5 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CountTests.java @@ -0,0 +1,45 @@ +package app.fyreplace.api.testing.endpoints.emails; + +import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; +import static java.util.stream.IntStream.range; +import static org.hamcrest.Matchers.equalTo; + +import app.fyreplace.api.data.Email; +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.EmailsEndpoint; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(EmailsEndpoint.class) +public class CountTests extends TransactionalTests { + @Test + @TestSecurity(user = "user_0") + public void count() { + given().get("count") + .then() + .statusCode(200) + .body(equalTo(String.valueOf(Email.count("user.username = 'user_0'")))); + } + + @BeforeEach + @Transactional + @Override + public void beforeEach() { + super.beforeEach(); + final var user = requireNonNull(User.findByUsername("user_0")); + range(0, 30).forEach(i -> { + final var email = new Email(); + email.user = user; + email.email = user.username + "_" + i + "@example.com"; + email.isVerified = i % 5 == 0; + email.persist(); + }); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java new file mode 100644 index 0000000..0b2cfa7 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java @@ -0,0 +1,43 @@ +package app.fyreplace.api.testing.endpoints.users; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; + +import app.fyreplace.api.data.Block; +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.UsersEndpoint; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(UsersEndpoint.class) +public class CountBlockedTests extends TransactionalTests { + @Test + @TestSecurity(user = "user_0") + public void countBlocked() { + given().get("blocked/count") + .then() + .statusCode(200) + .body(equalTo(String.valueOf(Block.count("source.username = 'user_0'")))); + } + + @BeforeEach + @Transactional + @Override + public void beforeEach() { + super.beforeEach(); + final var user = User.findByUsername("user_0"); + + for (final var otherUser : User.list("username > 'user_10'")) { + final var block = new Block(); + block.source = user; + block.target = otherUser; + block.persist(); + } + } +} From 6de9ddea16509cbbb04b96aaf20f8e19885cbdcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 30 Sep 2023 12:42:52 +0200 Subject: [PATCH 046/157] Build JVM images again --- .dockerignore | 1 + Dockerfile | 29 +++++++++++++++++++++-------- Makefile | 7 ++++++- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/.dockerignore b/.dockerignore index 433711f..23ace83 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,3 +9,4 @@ !settings.gradle !quarkus-sentry !src +!Makefile diff --git a/Dockerfile b/Dockerfile index 34dd03c..271c7df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,33 @@ -FROM ghcr.io/graalvm/jdk:ol9-java17 AS build - -RUN microdnf install -y git gcc glibc-devel libstdc++-devel zlib-devel +FROM node:lts AS build-emails +RUN apt-get update && apt-get install -y make WORKDIR /app COPY . ./ +RUN make emails + + +FROM eclipse-temurin:17-jdk AS build-code + +RUN apt-get update && apt-get install -y git +WORKDIR /app + +COPY --from=build-emails /app/ /app RUN git fetch --unshallow || echo "Nothing to do" -RUN ./gradlew compileMjml -RUN ./gradlew build -Dquarkus.package.type=native +RUN ./gradlew --no-daemon --exclude-task test build + -FROM oraclelinux:9-slim AS run +FROM eclipse-temurin:17-jre AS run ENV LANGUAGE="en_US:en" +ENV JAVA_OPTS="$JAVA_OPTS -Dquarkus.http.host=0.0.0.0" +ENV JAVA_OPTS="$JAVA_OPTS -Djava.util.logging.manager=org.jboss.logmanager.LogManager" -COPY --from=build --chown=nobody /app/build/*-runner /deployments/application +COPY --from=build-code --chown=nobody /app/build/quarkus-app/lib/ /deployments/lib +COPY --from=build-code --chown=nobody /app/build/quarkus-app/*.jar /deployments/ +COPY --from=build-code --chown=nobody /app/build/quarkus-app/app/ /deployments/app +COPY --from=build-code --chown=nobody /app/build/quarkus-app/quarkus/ /deployments/quarkus EXPOSE 8080 USER nobody -CMD /deployments/application -Dquarkus.http.host=0.0.0.0 +CMD java -jar /deployments/quarkus-run.jar diff --git a/Makefile b/Makefile index 7139049..7a63004 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,9 @@ -.PHONY: keygen-rsa +.PHONY: emails keygen-rsa + +emails: src/main/resources/templates/*/html.html + +src/main/resources/templates/%/html.html: src/main/resources/templates/%/html.html.mjml + npx mjml -c.minify=true $< -o $@ keygen-rsa: openssl genrsa > src/main/resources/keys/jwt.rsa 2048 From adf1718ef1dd3c1c504a2033c667fe0aedc22860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 6 Oct 2023 16:30:39 +0200 Subject: [PATCH 047/157] Simplify booleans --- .../app/fyreplace/api/endpoints/EmailsEndpoint.java | 2 +- .../app/fyreplace/api/endpoints/UsersEndpoint.java | 4 ++-- .../api/testing/endpoints/emails/SetMainTests.java | 12 ++++++------ .../testing/endpoints/users/CreateBlockTests.java | 10 +++++----- .../testing/endpoints/users/DeleteBlockTests.java | 8 ++++---- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java index e386cd4..3d1c18f 100644 --- a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java @@ -100,7 +100,7 @@ public void delete(@PathParam("id") final UUID id) { } @POST - @Path("{id}/isMain") + @Path("{id}/main") @Authenticated @Transactional @APIResponse(responseCode = "200") diff --git a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java index 1ceb0df..7ff68dd 100644 --- a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java @@ -110,7 +110,7 @@ public User retrieve(@PathParam("id") final UUID id) { } @PUT - @Path("{id}/isBlocked") + @Path("{id}/blocked") @Authenticated @Transactional @APIResponse(responseCode = "200") @@ -136,7 +136,7 @@ public Response createBlock(@PathParam("id") final UUID id) { } @DELETE - @Path("{id}/isBlocked") + @Path("{id}/blocked") @Authenticated @Transactional @APIResponse(responseCode = "204") diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java index 6fc0057..eb75481 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java @@ -25,7 +25,7 @@ public final class SetMainTests extends TransactionalTests { @TestSecurity(user = "user_0") public void setMain() { assertFalse(secondaryEmail.isMain()); - given().post(secondaryEmail.id + "/isMain").then().statusCode(200); + given().post(secondaryEmail.id + "/main").then().statusCode(200); secondaryEmail = Email.findById(secondaryEmail.id); assertTrue(secondaryEmail.isMain()); } @@ -34,8 +34,8 @@ public void setMain() { @TestSecurity(user = "user_0") public void setMainTwice() { assertFalse(secondaryEmail.isMain()); - given().post(secondaryEmail.id + "/isMain").then().statusCode(200); - given().post(secondaryEmail.id + "/isMain").then().statusCode(200); + given().post(secondaryEmail.id + "/main").then().statusCode(200); + given().post(secondaryEmail.id + "/main").then().statusCode(200); secondaryEmail = Email.findById(secondaryEmail.id); assertTrue(secondaryEmail.isMain()); } @@ -43,7 +43,7 @@ public void setMainTwice() { @Test @TestSecurity(user = "user_0") public void setMainWithUnverifiedEmail() { - given().post(unverifiedEmail.id + "/isMain").then().statusCode(403); + given().post(unverifiedEmail.id + "/main").then().statusCode(403); secondaryEmail = Email.findById(secondaryEmail.id); assertFalse(secondaryEmail.isMain()); } @@ -52,7 +52,7 @@ public void setMainWithUnverifiedEmail() { @TestSecurity(user = "user_0") public void setMainWithOtherEmail() { final var otherUser = User.findByUsername("user_1"); - given().post(otherUser.mainEmail.id + "/isMain").then().statusCode(404); + given().post(otherUser.mainEmail.id + "/main").then().statusCode(404); secondaryEmail = Email.findById(secondaryEmail.id); assertFalse(secondaryEmail.isMain()); } @@ -60,7 +60,7 @@ public void setMainWithOtherEmail() { @Test @TestSecurity(user = "user_0") public void setMainWithNonExistentEmail() { - given().post("invalid" + "/isMain").then().statusCode(404); + given().post("invalid" + "/main").then().statusCode(404); secondaryEmail = Email.findById(secondaryEmail.id); assertFalse(secondaryEmail.isMain()); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java index 5f8a03e..d9ae9b1 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java @@ -21,7 +21,7 @@ public void createBlock() { final var user = User.findByUsername("user_0"); final var otherUser = User.findByUsername("user_2"); assertEquals(0, Block.count("source = ?1 and target = ?2", user, otherUser)); - given().put(otherUser.id + "/isBlocked").then().statusCode(200); + given().put(otherUser.id + "/blocked").then().statusCode(200); assertEquals(1, Block.count("source = ?1 and target = ?2", user, otherUser)); } @@ -31,8 +31,8 @@ public void createBlockTwice() { final var user = User.findByUsername("user_0"); final var otherUser = User.findByUsername("user_1"); assertEquals(0, Block.count("source = ?1 and target = ?2", user, otherUser)); - given().put(otherUser.id + "/isBlocked").then().statusCode(200); - given().put(otherUser.id + "/isBlocked").then().statusCode(200); + given().put(otherUser.id + "/blocked").then().statusCode(200); + given().put(otherUser.id + "/blocked").then().statusCode(200); assertEquals(1, Block.count("source = ?1 and target = ?2", user, otherUser)); } @@ -41,7 +41,7 @@ public void createBlockTwice() { public void createBlockWithInvalidUser() { final var user = User.findByUsername("user_0"); final var blockCount = Block.count("source", user); - given().put("invalid/isBlocked").then().statusCode(404); + given().put("invalid/blocked").then().statusCode(404); assertEquals(blockCount, Block.count("source", user)); } @@ -49,7 +49,7 @@ public void createBlockWithInvalidUser() { @TestSecurity(user = "user_0") public void createBlockWithSelf() { final var user = User.findByUsername("user_0"); - given().put(user.id + "/isBlocked").then().statusCode(403); + given().put(user.id + "/blocked").then().statusCode(403); assertEquals(0, Block.count("source = ?1 and target = ?2", user, user)); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java index 8c25767..ee027ea 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java @@ -22,7 +22,7 @@ public final class DeleteBlockTests extends TransactionalTests { public void deleteBlock() { final var user = User.findByUsername("user_0"); final var otherUser = User.findByUsername("user_1"); - given().delete(otherUser.id + "/isBlocked").then().statusCode(204); + given().delete(otherUser.id + "/blocked").then().statusCode(204); assertEquals(0, Block.count("source = ?1 and target = ?2", user, otherUser)); } @@ -31,8 +31,8 @@ public void deleteBlock() { public void deleteBlockTwice() { final var user = User.findByUsername("user_0"); final var otherUser = User.findByUsername("user_1"); - given().delete(otherUser.id + "/isBlocked").then().statusCode(204); - given().delete(otherUser.id + "/isBlocked").then().statusCode(204); + given().delete(otherUser.id + "/blocked").then().statusCode(204); + given().delete(otherUser.id + "/blocked").then().statusCode(204); assertEquals(0, Block.count("source = ?1 and target = ?2", user, otherUser)); } @@ -41,7 +41,7 @@ public void deleteBlockTwice() { public void deleteBlockWithInvalidUser() { final var user = User.findByUsername("user_0"); final var blockCount = Block.count("source", user); - given().delete("invalid/isBlocked").then().statusCode(404); + given().delete("invalid/blocked").then().statusCode(404); assertEquals(blockCount, Block.count("source", user)); } From 4fca29bc275fbdd4ff1c06024292287ede63e24c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 6 Oct 2023 16:44:39 +0200 Subject: [PATCH 048/157] Fix test naming --- .../api/testing/endpoints/users/ListBlockedTests.java | 6 +++--- .../api/testing/endpoints/users/UpdateMeBioTests.java | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java index ab3daa8..0a154de 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java @@ -26,7 +26,7 @@ public final class ListBlockedTests extends TransactionalTests { @Test @TestSecurity(user = "user_0") - public void list() { + public void listBlocked() { final var user = User.findByUsername("user_0"); final var response = given().queryParam("page", 0) .get("blocked") @@ -45,13 +45,13 @@ public void list() { @Test @TestSecurity(user = "user_0") - public void listOutOfBounds() { + public void listBlockedOutOfBounds() { given().queryParam("page", -1).get("blocked").then().statusCode(400); } @Test @TestSecurity(user = "user_0") - public void listTooFar() { + public void listBlockedTooFar() { given().queryParam("page", 50) .get("blocked") .then() diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeBioTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeBioTests.java index a47a19a..4e92296 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeBioTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeBioTests.java @@ -21,7 +21,7 @@ public final class UpdateMeBioTests extends TransactionalTests { @ParameterizedTest @ValueSource(strings = {"Test", "Some random bio", ""}) @TestSecurity(user = "user_0") - public void updateBio(final String bio) { + public void updateMeBio(final String bio) { given().contentType(ContentType.TEXT).body(bio).put("me/bio").then().statusCode(200); final var user = User.findByUsername("user_0"); assertEquals(bio, user.bio); @@ -30,7 +30,7 @@ public void updateBio(final String bio) { @Test @TestSecurity(user = "user_0") @Transactional - public void updateBioWithBioTooLong() { + public void updateMeBioWithBioTooLong() { final var user = User.findByUsername("user_0"); final var bio = user.bio; given().contentType(ContentType.TEXT) @@ -43,7 +43,7 @@ public void updateBioWithBioTooLong() { } @Test - public void UpdateMeBioWithoutAuthentication() { + public void updateMeBioMeBioWithoutAuthentication() { given().contentType(ContentType.TEXT).body("Test").put("me/bio").then().statusCode(401); } } From f20a636f1a4b8f8d7387aaa17373aea670c14dc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 8 Sep 2023 11:13:32 +0200 Subject: [PATCH 049/157] Update dependencies --- build.gradle | 1 + gradle.properties | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 7eb0b35..c0fbf05 100644 --- a/build.gradle +++ b/build.gradle @@ -44,6 +44,7 @@ dependencies { testImplementation("io.quarkus:quarkus-test-h2") testImplementation("io.quarkus:quarkus-test-security") testImplementation("io.rest-assured:rest-assured") + testImplementation("org.apiguardian:apiguardian-api:+") } java { diff --git a/gradle.properties b/gradle.properties index 2f00e16..f722cb3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ -quarkusVersion=3.3.1 +quarkusVersion=3.4.1 quarkusAmazonVersion=2.5.0 +sentryVersion=6.29.0 +tikaVersion=2.9.0 gitPluginVersion=3.0.0 spotlessPluginVersion=6.21.0 -sentryVersion=6.28.0 -tikaVersion=2.9.0 From acfa89d0e6f8ee451be163f38c571e62ac501cab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 6 Oct 2023 13:02:39 +0200 Subject: [PATCH 050/157] Update Gradle --- gradle/wrapper/gradle-wrapper.jar | Bin 62076 -> 63721 bytes gradle/wrapper/gradle-wrapper.properties | 3 ++- gradlew | 8 ++++++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index c1962a79e29d3e0ab67b14947c167a862655af9b..7f93135c49b765f8051ef9d0a6055ff8e46073d8 100644 GIT binary patch delta 37652 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 delta 36122 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 Ac-xW1# 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 Q>rd#Eb%7 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c30b486..3fa8f86 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index aeb74cb..0adc8e1 100755 --- a/gradlew +++ b/gradlew @@ -83,7 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -130,10 +131,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. From 09c49c7325280a1763eb25f3b7539d4432c9805c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Tue, 15 Aug 2023 16:58:08 +0200 Subject: [PATCH 051/157] Add posts and comments --- .../api/data/AuthoredEntityBase.java | 42 +++ .../java/app/fyreplace/api/data/Chapter.java | 66 +++++ .../java/app/fyreplace/api/data/Comment.java | 45 ++++ .../fyreplace/api/data/CommentCreation.java | 6 + .../java/app/fyreplace/api/data/Email.java | 2 +- .../java/app/fyreplace/api/data/Post.java | 93 +++++++ .../fyreplace/api/data/PostPublication.java | 3 + .../app/fyreplace/api/data/Subscription.java | 32 +++ .../java/app/fyreplace/api/data/User.java | 48 +++- .../java/app/fyreplace/api/data/Vote.java | 28 ++ .../app/fyreplace/api/data/VoteCreation.java | 3 + .../fyreplace/api/data/dev/DataSeeder.java | 68 ++++- .../api/endpoints/ChaptersEndpoint.java | 211 +++++++++++++++ .../api/endpoints/CommentsEndpoint.java | 167 ++++++++++++ .../api/endpoints/EmailsEndpoint.java | 4 +- .../api/endpoints/PostsEndpoint.java | 242 ++++++++++++++++++ .../api/endpoints/TokensEndpoint.java | 2 +- .../api/endpoints/UsersEndpoint.java | 8 +- .../api/exceptions/ExceptionMappers.java | 5 + .../api/services/MimeTypeService.java | 8 + .../app/fyreplace/api/tasks/CleanupTasks.java | 2 +- src/main/resources/application.yaml | 4 + .../app/fyreplace/api/testing/ImageTests.java | 35 +++ .../api/testing/TransactionalTests.java | 2 + .../data/chapters/PositionBetweenTests.java | 37 +++ .../testing/data/posts/NormalizeTests.java | 75 ++++++ .../api/testing/endpoints/PostTestsBase.java | 22 ++ .../chapters/CreateChapterTests.java | 113 ++++++++ .../chapters/DeleteChapterTests.java | 123 +++++++++ .../chapters/UpdateChapterImageTests.java | 215 ++++++++++++++++ .../chapters/UpdateChapterPositionTests.java | 161 ++++++++++++ .../chapters/UpdateChapterTextTests.java | 164 ++++++++++++ .../endpoints/comments/AcknowledgeTests.java | 64 +++++ .../endpoints/comments/CountTests.java | 83 ++++++ .../endpoints/comments/CreateTests.java | 163 ++++++++++++ .../endpoints/comments/DeleteTests.java | 131 ++++++++++ .../testing/endpoints/comments/ListTests.java | 109 ++++++++ .../testing/endpoints/emails/CountTests.java | 2 +- .../testing/endpoints/emails/CreateTests.java | 2 +- .../testing/endpoints/emails/DeleteTests.java | 4 +- .../testing/endpoints/emails/ListTests.java | 2 +- .../endpoints/emails/SetMainTests.java | 14 +- .../testing/endpoints/posts/CountTests.java | 71 +++++ .../testing/endpoints/posts/CreateTests.java | 41 +++ .../testing/endpoints/posts/DeleteTests.java | 65 +++++ .../endpoints/posts/ListFeedTests.java | 85 ++++++ .../testing/endpoints/posts/ListTests.java | 107 ++++++++ .../testing/endpoints/posts/PublishTests.java | 117 +++++++++ .../endpoints/posts/RetrieveTests.java | 144 +++++++++++ .../endpoints/posts/SubscribeTests.java | 103 ++++++++ .../endpoints/posts/UnsubscribeTests.java | 97 +++++++ .../testing/endpoints/posts/VoteTests.java | 160 ++++++++++++ .../testing/endpoints/tokens/CreateTests.java | 8 +- .../users/{BanTests.java => BannedTests.java} | 42 +-- .../testing/endpoints/users/CreateTests.java | 6 +- .../endpoints/users/RetrieveMeTests.java | 2 +- .../endpoints/users/RetrieveTests.java | 6 +- .../endpoints/users/UpdateMeAvatarTests.java | 36 +-- .../cleanup/RemoveOldInactiveUsersTests.java | 4 +- 59 files changed, 3598 insertions(+), 106 deletions(-) create mode 100644 src/main/java/app/fyreplace/api/data/AuthoredEntityBase.java create mode 100644 src/main/java/app/fyreplace/api/data/Chapter.java create mode 100644 src/main/java/app/fyreplace/api/data/Comment.java create mode 100644 src/main/java/app/fyreplace/api/data/CommentCreation.java create mode 100644 src/main/java/app/fyreplace/api/data/Post.java create mode 100644 src/main/java/app/fyreplace/api/data/PostPublication.java create mode 100644 src/main/java/app/fyreplace/api/data/Subscription.java create mode 100644 src/main/java/app/fyreplace/api/data/Vote.java create mode 100644 src/main/java/app/fyreplace/api/data/VoteCreation.java create mode 100644 src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java create mode 100644 src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java create mode 100644 src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java create mode 100644 src/test/java/app/fyreplace/api/testing/ImageTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/data/chapters/PositionBetweenTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/data/posts/NormalizeTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/PostTestsBase.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/chapters/CreateChapterTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/chapters/DeleteChapterTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterImageTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterPositionTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterTextTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/comments/AcknowledgeTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/comments/CountTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/comments/CreateTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/comments/DeleteTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/comments/ListTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/posts/CountTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/posts/ListFeedTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/posts/PublishTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/posts/SubscribeTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/posts/UnsubscribeTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/posts/VoteTests.java rename src/test/java/app/fyreplace/api/testing/endpoints/users/{BanTests.java => BannedTests.java} (76%) diff --git a/src/main/java/app/fyreplace/api/data/AuthoredEntityBase.java b/src/main/java/app/fyreplace/api/data/AuthoredEntityBase.java new file mode 100644 index 0000000..3cd9ee8 --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/AuthoredEntityBase.java @@ -0,0 +1,42 @@ +package app.fyreplace.api.data; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nullable; +import jakarta.persistence.Column; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.Transient; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +@MappedSuperclass +public class AuthoredEntityBase extends TimestampedEntityBase { + @ManyToOne(optional = false) + @OnDelete(action = OnDeleteAction.CASCADE) + @JsonIgnore + public User author; + + @Column(nullable = false) + public boolean anonymous = false; + + @Transient + @JsonIgnore + @Nullable + public User currentUser; + + @SuppressWarnings("unused") + @JsonProperty("author") + @Nullable + public User.Profile getAuthorProfile() { + if (anonymous && (currentUser == null || !currentUser.id.equals(author.id))) { + return null; + } + + return author.getProfile(); + } + + public void setCurrentUser(@Nullable final User user) { + currentUser = user; + } +} diff --git a/src/main/java/app/fyreplace/api/data/Chapter.java b/src/main/java/app/fyreplace/api/data/Chapter.java new file mode 100644 index 0000000..96bc5e1 --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/Chapter.java @@ -0,0 +1,66 @@ +package app.fyreplace.api.data; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import jakarta.annotation.Nullable; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; +import jakarta.persistence.PostRemove; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +@Entity +@Table(name = "chapters", uniqueConstraints = @UniqueConstraint(columnNames = {"post_id", "position"})) +public class Chapter extends EntityBase { + @ManyToOne(optional = false) + @OnDelete(action = OnDeleteAction.CASCADE) + @JsonIgnore + public Post post; + + @Column(length = 50, nullable = false) + @JsonIgnore + public String position; + + @Column(length = 500, nullable = false) + public String text = ""; + + @OneToOne(cascade = CascadeType.PERSIST) + @OnDelete(action = OnDeleteAction.SET_NULL) + @JsonSerialize(using = StoredFile.Serializer.class) + @Schema(implementation = String.class) + public StoredFile image; + + @Column(nullable = false) + public int width = 0; + + @Column(nullable = false) + public int height = 0; + + @PostRemove + final void postRemove() { + if (image != null) { + image.delete(); + } + } + + public static String positionBetween(final @Nullable String before, final @Nullable String after) { + final var beforeLength = before == null ? 0 : before.length(); + final var afterLength = after == null ? 0 : after.length(); + + if (before == null && after == null) { + return "z"; + } else if (before != null && after != null && (before.equals(after) || before.compareTo(after) > 0)) { + throw new IllegalArgumentException(); + } else if (after == null || beforeLength > afterLength) { + return before + "z"; + } else { + return after.substring(0, afterLength - 1) + "az"; + } + } +} diff --git a/src/main/java/app/fyreplace/api/data/Comment.java b/src/main/java/app/fyreplace/api/data/Comment.java new file mode 100644 index 0000000..76d85aa --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/Comment.java @@ -0,0 +1,45 @@ +package app.fyreplace.api.data; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.quarkus.panache.common.Sort; +import jakarta.annotation.Nonnull; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +@Entity +@Table( + name = "comments", + indexes = {@Index(columnList = "post_id")}) +public class Comment extends AuthoredEntityBase implements Comparable { + @ManyToOne(optional = false) + @OnDelete(action = OnDeleteAction.CASCADE) + @JsonIgnore + public Post post; + + @Column(length = 1500, nullable = false) + public String text; + + @Column(nullable = false) + public boolean deleted; + + @Override + public int compareTo(@Nonnull final Comment other) { + final var dateComparison = dateCreated.compareTo(other.dateCreated); + return dateComparison != 0 ? dateComparison : id.compareTo(other.id); + } + + public void softDelete() { + text = ""; + deleted = true; + persist(); + } + + public static Sort sorting() { + return Sort.by("dateCreated", "id"); + } +} diff --git a/src/main/java/app/fyreplace/api/data/CommentCreation.java b/src/main/java/app/fyreplace/api/data/CommentCreation.java new file mode 100644 index 0000000..189cde5 --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/CommentCreation.java @@ -0,0 +1,6 @@ +package app.fyreplace.api.data; + +import jakarta.validation.constraints.NotBlank; +import org.hibernate.validator.constraints.Length; + +public record CommentCreation(@Length(min = 1, max = 1500) @NotBlank String text, boolean anonymous) {} diff --git a/src/main/java/app/fyreplace/api/data/Email.java b/src/main/java/app/fyreplace/api/data/Email.java index 1c09e78..a95593b 100644 --- a/src/main/java/app/fyreplace/api/data/Email.java +++ b/src/main/java/app/fyreplace/api/data/Email.java @@ -21,7 +21,7 @@ public class Email extends EntityBase { public String email; @Column(nullable = false) - public boolean isVerified = false; + public boolean verified = false; @JsonProperty("isMain") public boolean isMain() { diff --git a/src/main/java/app/fyreplace/api/data/Post.java b/src/main/java/app/fyreplace/api/data/Post.java new file mode 100644 index 0000000..5924c82 --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/Post.java @@ -0,0 +1,93 @@ +package app.fyreplace.api.data; + +import static java.util.Objects.requireNonNullElse; + +import app.fyreplace.api.exceptions.ForbiddenException; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.quarkus.panache.common.Sort; +import jakarta.annotation.Nullable; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import jakarta.ws.rs.NotFoundException; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import org.hibernate.annotations.Formula; + +@Entity +@Table(name = "posts") +public class Post extends AuthoredEntityBase { + @JsonIgnore + public Instant datePublished; + + @Column(nullable = false) + @JsonIgnore + public int life = 0; + + @SuppressWarnings("unused") + @Formula("(select count(*) from comments where comments.post_id = id)") + public long commentCount; + + @SuppressWarnings("unused") + @Formula("(select count(*) from votes where votes.post_id = id)") + public long voteCount; + + public static Duration shelfLife = Duration.ofDays(7); + + public Instant getDateCreated() { + return requireNonNullElse(datePublished, dateCreated); + } + + public List getChapters() { + return Chapter.find("post", Sort.by("position"), this).list(); + } + + @JsonIgnore + public boolean isOld() { + return datePublished != null && Instant.now().isAfter(datePublished.plus(shelfLife)); + } + + public void publish(final int life, final boolean anonymous) { + datePublished = Instant.now(); + this.life = life; + this.anonymous = anonymous; + persist(); + author.subscribeTo(this); + } + + public void normalize() { + final var chapters = getChapters(); + + for (final var chapter : chapters) { + chapter.position = chapter.position.replace("a", "0").replace("z", "9"); + chapter.persist(); + } + + for (var i = 0; i < chapters.size(); i++) { + final var chapter = chapters.get(i); + final var before = i - 1 >= 0 ? chapters.get(i - 1).position : null; + chapter.position = Chapter.positionBetween(before, null); + chapter.persist(); + } + } + + public static void validateAccess( + @Nullable final Post post, + @Nullable final User user, + @Nullable final Boolean mustBePublished, + final boolean mustBeAuthor) { + final Boolean postIsDraft = post != null && (post.datePublished == null); + final var userId = user != null ? user.id : null; + + if (post == null || (!post.author.id.equals(userId) && postIsDraft)) { + throw new NotFoundException(); + } else if (mustBePublished == postIsDraft) { + throw new ForbiddenException(postIsDraft ? "post_not_published" : "post_is_published"); + } else if (mustBeAuthor && !post.author.id.equals(userId)) { + throw new ForbiddenException("invalid_author"); + } + + post.setCurrentUser(user); + } +} diff --git a/src/main/java/app/fyreplace/api/data/PostPublication.java b/src/main/java/app/fyreplace/api/data/PostPublication.java new file mode 100644 index 0000000..55d4ab2 --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/PostPublication.java @@ -0,0 +1,3 @@ +package app.fyreplace.api.data; + +public record PostPublication(boolean anonymous) {} diff --git a/src/main/java/app/fyreplace/api/data/Subscription.java b/src/main/java/app/fyreplace/api/data/Subscription.java new file mode 100644 index 0000000..77bde59 --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/Subscription.java @@ -0,0 +1,32 @@ +package app.fyreplace.api.data; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import java.time.Instant; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import org.hibernate.annotations.SourceType; +import org.hibernate.annotations.UpdateTimestamp; + +@Entity +@Table(name = "subscriptions", uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "post_id"})) +public class Subscription extends EntityBase { + @ManyToOne(optional = false) + @OnDelete(action = OnDeleteAction.CASCADE) + public User user; + + @ManyToOne(optional = false) + @OnDelete(action = OnDeleteAction.CASCADE) + public Post post; + + @ManyToOne + @OnDelete(action = OnDeleteAction.CASCADE) + public Comment lastCommentSeen; + + @Column(nullable = false) + @UpdateTimestamp(source = SourceType.DB) + public Instant dateLastSeen; +} diff --git a/src/main/java/app/fyreplace/api/data/User.java b/src/main/java/app/fyreplace/api/data/User.java index e3b0e6e..c33b333 100644 --- a/src/main/java/app/fyreplace/api/data/User.java +++ b/src/main/java/app/fyreplace/api/data/User.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.quarkus.panache.common.Sort; import jakarta.annotation.Nullable; import jakarta.persistence.*; import jakarta.validation.constraints.NotBlank; @@ -73,7 +74,7 @@ public class User extends TimestampedEntityBase { @Column(nullable = false) @JsonIgnore - public boolean isActive = false; + public boolean active = false; @Column(nullable = false) public Rank rank = Rank.CITIZEN; @@ -88,7 +89,7 @@ public class User extends TimestampedEntityBase { public String bio = ""; @Column(nullable = false) - public boolean isBanned = false; + public boolean banned = false; @Column(nullable = false) @JsonIgnore @@ -117,22 +118,51 @@ final void postRemove() { } } - public static User findByUsername(final String username) { + public Subscription subscribeTo(final Post post) { + final var existing = Subscription.find("user = ?1 and post = ?2", this, post) + .firstResult(); + + if (existing != null) { + return existing; + } + + final var subscription = new Subscription(); + subscription.user = this; + subscription.post = post; + subscription.lastCommentSeen = + Comment.find("post", Sort.descending("dateCreated"), post).firstResult(); + subscription.persist(); + return subscription; + } + + public void unsubscribeFrom(final Post post) { + Subscription.delete("user = ?1 and post = ?2", this, post); + } + + public static @Nullable User findByUsername(final String username) { return findByUsername(username, null); } - public static User findByUsername(final String username, @Nullable final LockModeType lock) { + @SuppressWarnings("DataFlowIssue") + public static @Nullable User findByUsername(final String username, @Nullable final LockModeType lock) { return User.find("username", username).withLock(lock).firstResult(); } - public static User getFromSecurityContext(final SecurityContext context) { - return getFromSecurityContext(context, null); + public static @Nullable User getFromSecurityContext(final SecurityContext context) { + return getFromSecurityContext(context, null, true); + } + + public static @Nullable User getFromSecurityContext( + final SecurityContext context, @Nullable final LockModeType lock) { + return getFromSecurityContext(context, lock, true); } - public static User getFromSecurityContext(final SecurityContext context, final LockModeType lock) { - final var user = findByUsername(context.getUserPrincipal().getName(), lock); + public static @Nullable User getFromSecurityContext( + final SecurityContext context, @Nullable final LockModeType lock, final boolean required) { + final var principal = context.getUserPrincipal(); + final var user = findByUsername(principal != null ? principal.getName() : "", lock); - if (user == null) { + if (user == null && required) { throw new NotAuthorizedException("Bearer"); } diff --git a/src/main/java/app/fyreplace/api/data/Vote.java b/src/main/java/app/fyreplace/api/data/Vote.java new file mode 100644 index 0000000..27c22eb --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/Vote.java @@ -0,0 +1,28 @@ +package app.fyreplace.api.data; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +@Entity +@Table( + name = "votes", + uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "post_id"}), + indexes = {@Index(columnList = "post_id")}) +public class Vote extends EntityBase { + @ManyToOne(optional = false) + @OnDelete(action = OnDeleteAction.CASCADE) + public User user; + + @ManyToOne(optional = false) + @OnDelete(action = OnDeleteAction.CASCADE) + public Post post; + + @Column(nullable = false) + public boolean isSpread; +} diff --git a/src/main/java/app/fyreplace/api/data/VoteCreation.java b/src/main/java/app/fyreplace/api/data/VoteCreation.java new file mode 100644 index 0000000..9cd4bda --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/VoteCreation.java @@ -0,0 +1,3 @@ +package app.fyreplace.api.data; + +public record VoteCreation(boolean isSpread) {} diff --git a/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java b/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java index cfd5dc9..3d2a2c5 100644 --- a/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java +++ b/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java @@ -2,10 +2,14 @@ import static java.util.stream.IntStream.range; +import app.fyreplace.api.data.Chapter; +import app.fyreplace.api.data.Comment; import app.fyreplace.api.data.Email; +import app.fyreplace.api.data.Post; import app.fyreplace.api.data.RandomCode; import app.fyreplace.api.data.StoredFile; import app.fyreplace.api.data.User; +import io.quarkus.panache.common.Sort; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.ShutdownEvent; import io.quarkus.runtime.StartupEvent; @@ -15,11 +19,15 @@ import jakarta.transaction.Transactional; import org.eclipse.microprofile.config.inject.ConfigProperty; +@SuppressWarnings({"UnusedReturnValue", "unused"}) @ApplicationScoped public class DataSeeder { @ConfigProperty(name = "app.use-example-data") boolean useExampleData; + @ConfigProperty(name = "app.posts.starting-life") + int postsStartingLife; + public void onStartup(@Observes final StartupEvent event) { if (shouldUseExampleData()) { insertData(); @@ -34,8 +42,15 @@ public void onShutdown(@Observes final ShutdownEvent event) { @Transactional public void insertData() { - range(0, 100).forEach(i -> createUser("user_" + i, true)); + range(0, 20).forEach(i -> createUser("user_" + i, true)); range(0, 10).forEach(i -> createUser("user_inactive_" + i, false)); + final var user = User.findByUsername("user_0"); + range(0, 20).forEach(i -> createPost(user, "Post " + i, true, false)); + range(0, 20).forEach(i -> createPost(user, "Draft " + i, false, false)); + final var post = Post.find( + "author = ?1 and datePublished is not null", Sort.by("datePublished", "id"), user) + .firstResult(); + range(0, 10).forEach(i -> createComment(user, post, "Comment " + i, false)); } @Transactional @@ -43,26 +58,63 @@ public void deleteData() { Email.deleteAll(); RandomCode.deleteAll(); User.deleteAll(); + Post.deleteAll(); StoredFile.streamAll().forEach(StoredFile::delete); } - private boolean shouldUseExampleData() { - return useExampleData && ProfileManager.getLaunchMode() == LaunchMode.DEVELOPMENT; - } - - private void createUser(final String username, final boolean isActive) { + @Transactional(Transactional.TxType.REQUIRES_NEW) + public User createUser(final String username, final boolean active) { final var user = new User(); user.username = username; - user.isActive = isActive; + user.active = active; user.persist(); final var email = new Email(); email.user = user; email.email = username + "@example.org"; - email.isVerified = isActive; + email.verified = active; email.persist(); user.mainEmail = email; user.persist(); + return user; + } + + @Transactional(Transactional.TxType.REQUIRES_NEW) + public Post createPost(final User author, final String text, final boolean published, final boolean anonymous) { + final var post = new Post(); + post.author = author; + post.persist(); + String before = null; + + for (var i = 0; i < 3; i++) { + final var chapter = new Chapter(); + chapter.post = post; + chapter.position = Chapter.positionBetween(before, null); + chapter.text = text + ' ' + i; + chapter.persist(); + before = chapter.position; + } + + if (published) { + post.publish(postsStartingLife, anonymous); + } + + return post; + } + + @Transactional(Transactional.TxType.REQUIRES_NEW) + public Comment createComment(final User author, final Post post, final String text, final boolean anonymous) { + final var comment = new Comment(); + comment.author = author; + comment.anonymous = anonymous; + comment.post = post; + comment.text = text; + comment.persist(); + return comment; + } + + private boolean shouldUseExampleData() { + return useExampleData && ProfileManager.getLaunchMode() == LaunchMode.DEVELOPMENT; } } diff --git a/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java new file mode 100644 index 0000000..ea69703 --- /dev/null +++ b/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java @@ -0,0 +1,211 @@ +package app.fyreplace.api.endpoints; + +import static java.util.Objects.requireNonNullElse; + +import app.fyreplace.api.data.Chapter; +import app.fyreplace.api.data.Post; +import app.fyreplace.api.data.StoredFile; +import app.fyreplace.api.data.User; +import app.fyreplace.api.exceptions.ForbiddenException; +import app.fyreplace.api.services.MimeTypeService; +import app.fyreplace.api.services.mimetype.KnownMimeTypes; +import io.quarkus.panache.common.Sort; +import io.quarkus.security.Authenticated; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.core.SecurityContext; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.UUID; +import org.apache.tika.metadata.Metadata; +import org.apache.tika.metadata.Property; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.hibernate.validator.constraints.Length; + +@Path("posts/{id}/chapters") +public final class ChaptersEndpoint { + @ConfigProperty(name = "app.posts.max-chapter-count") + int postsMaxChapterCount; + + @Inject + MimeTypeService mimeTypeService; + + @Context + SecurityContext context; + + @POST + @Authenticated + @Transactional + @APIResponse( + responseCode = "201", + content = + @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Chapter.class))) + @APIResponse(responseCode = "401") + @APIResponse(responseCode = "403") + @APIResponse(responseCode = "404") + public Response createChapter(@PathParam("id") final UUID id) { + final var user = User.getFromSecurityContext(context); + final var post = Post.findById(id); + Post.validateAccess(post, user, false, true); + + if (Chapter.count("post", post) >= postsMaxChapterCount) { + throw new ForbiddenException("invalid_chapter_count"); + } + + final var lastExistingChapter = + Chapter.find("post", Sort.descending("position"), post).firstResult(); + final var chapter = new Chapter(); + chapter.post = post; + chapter.position = Chapter.positionBetween(lastExistingChapter.position, null); + chapter.persist(); + return Response.status(Status.CREATED).entity(chapter).build(); + } + + @DELETE + @Path("{position}") + @Authenticated + @Transactional + @APIResponse(responseCode = "204") + @APIResponse(responseCode = "401") + @APIResponse(responseCode = "403") + @APIResponse(responseCode = "404") + public void deleteChapter(@PathParam("id") final UUID id, @PathParam("position") final int position) { + final var user = User.getFromSecurityContext(context); + final var post = Post.findById(id); + Post.validateAccess(post, user, false, true); + getChapter(post, position).delete(); + } + + @PUT + @Path("{position}/position") + @Authenticated + @Transactional + @Consumes(MediaType.TEXT_PLAIN) + @APIResponse( + responseCode = "200", + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = Integer.class))) + @APIResponse(responseCode = "400") + @APIResponse(responseCode = "401") + @APIResponse(responseCode = "403") + @APIResponse(responseCode = "404") + public int updateChapterPosition( + @PathParam("id") final UUID id, @PathParam("position") final int position, @NotNull final Integer input) { + final var user = User.getFromSecurityContext(context); + final var post = Post.findById(id); + Post.validateAccess(post, user, false, true); + + if (position == input) { + return input; + } + + try { + final var chapters = post.getChapters(); + final var chapter = chapters.get(position); + final var before = input > position ? input : input - 1; + final var after = input > position ? input + 1 : input; + final var beforePosition = before >= 0 ? chapters.get(before).position : null; + final var afterPosition = after < chapters.size() ? chapters.get(after).position : null; + chapter.position = Chapter.positionBetween(beforePosition, afterPosition); + chapter.persist(); + return input; + } catch (final IndexOutOfBoundsException e) { + throw new NotFoundException(); + } + } + + @PUT + @Path("{position}/text") + @Authenticated + @Transactional + @Consumes(MediaType.TEXT_PLAIN) + @APIResponse( + responseCode = "200", + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) + @APIResponse(responseCode = "400") + @APIResponse(responseCode = "401") + @APIResponse(responseCode = "403") + @APIResponse(responseCode = "404") + public String updateChapterText( + @PathParam("id") final UUID id, + @PathParam("position") final int position, + @NotNull @Length(max = 500) String input) { + final var user = User.getFromSecurityContext(context); + final var post = Post.findById(id); + Post.validateAccess(post, user, false, true); + + try { + final var chapter = getChapter(post, position); + chapter.text = input; + chapter.persist(); + return input; + } catch (final IndexOutOfBoundsException e) { + throw new NotFoundException(); + } + } + + @PUT + @Path("{position}/image") + @Authenticated + @Transactional + @Consumes(MediaType.APPLICATION_OCTET_STREAM) + @APIResponse( + responseCode = "200", + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) + @APIResponse(responseCode = "400") + @APIResponse(responseCode = "401") + @APIResponse(responseCode = "403") + @APIResponse(responseCode = "404") + public String updateChapterImage( + @PathParam("id") final UUID id, @PathParam("position") final int position, final File input) + throws IOException { + mimeTypeService.validate(input, KnownMimeTypes.IMAGE); + final var user = User.getFromSecurityContext(context); + final var post = Post.findById(id); + Post.validateAccess(post, user, false, true); + final var data = Files.readAllBytes(input.toPath()); + + try { + final var chapter = getChapter(post, position); + final var metadata = mimeTypeService.getMetadata(input); + final var width = metadata.getInt(Metadata.IMAGE_WIDTH); + final var height = metadata.getInt(Metadata.IMAGE_LENGTH); + + if (chapter.image == null) { + chapter.image = new StoredFile("chapters/" + chapter.id, data); + } else { + chapter.image.store(data); + } + + chapter.width = requireNonNullElse(width, metadata.getInt(Property.internalInteger("Image Width"))); + chapter.height = requireNonNullElse(height, metadata.getInt(Property.internalInteger("Image Height"))); + chapter.persist(); + return chapter.image.toString(); + } catch (final IndexOutOfBoundsException e) { + throw new NotFoundException(); + } + } + + private Chapter getChapter(final Post post, final int position) { + try { + return post.getChapters().get(position); + } catch (final IndexOutOfBoundsException e) { + throw new NotFoundException(); + } + } +} diff --git a/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java new file mode 100644 index 0000000..b0dd780 --- /dev/null +++ b/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java @@ -0,0 +1,167 @@ +package app.fyreplace.api.endpoints; + +import app.fyreplace.api.data.Comment; +import app.fyreplace.api.data.CommentCreation; +import app.fyreplace.api.data.Post; +import app.fyreplace.api.data.Subscription; +import app.fyreplace.api.data.User; +import app.fyreplace.api.exceptions.ForbiddenException; +import io.quarkus.security.Authenticated; +import jakarta.annotation.Nullable; +import jakarta.transaction.Transactional; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.core.SecurityContext; +import java.util.UUID; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; + +@Path("posts/{id}/comments") +public final class CommentsEndpoint { + @ConfigProperty(name = "app.paging.size") + int pagingSize; + + @Context + SecurityContext context; + + @GET + @Authenticated + @APIResponse(responseCode = "200") + @APIResponse(responseCode = "404") + public Iterable list(@PathParam("id") final UUID id, @QueryParam("page") @PositiveOrZero final int page) { + final var user = User.getFromSecurityContext(context); + final var post = Post.findById(id); + Post.validateAccess(post, user, true, false); + + try (final var commentStream = + Comment.find("post", Comment.sorting(), post).page(page, pagingSize).stream()) { + return commentStream.peek(c -> c.setCurrentUser(user)).toList(); + } + } + + @GET + @Path("count") + @Authenticated + @APIResponse(responseCode = "200") + public long count(@PathParam("id") final UUID id, @QueryParam("read") @Nullable final Boolean read) { + final var user = User.getFromSecurityContext(context); + final var post = Post.findById(id); + Post.validateAccess(post, user, true, false); + + if (read == null) { + return Comment.count("post.id", post.id); + } + + final var subscription = Subscription.find("user = ?1 and post = ?2", user, post) + .firstResult(); + final var dateComparison = read ? '<' : '>'; + final var idComparison = read ? '=' : '>'; + return subscription != null && subscription.lastCommentSeen != null + ? Comment.count( + "post.id = ?1 and (dateCreated " + dateComparison + " ?2 or (dateCreated = ?2 and id " + + idComparison + " ?3))", + post.id, + subscription.lastCommentSeen.dateCreated, + subscription.lastCommentSeen.id) + : 0; + } + + @POST + @Authenticated + @Transactional + @APIResponse( + responseCode = "201", + content = + @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Comment.class))) + @APIResponse(responseCode = "404") + public Response create(@PathParam("id") final UUID id, @Valid @NotNull final CommentCreation input) { + final var user = User.getFromSecurityContext(context); + final var post = Post.findById(id); + Post.validateAccess(post, user, true, false); + + if (input.anonymous() && !user.id.equals(post.author.id)) { + throw new ForbiddenException("invalid_post_author"); + } + + final var comment = new Comment(); + comment.author = user; + comment.post = post; + comment.text = input.text(); + comment.anonymous = input.anonymous(); + comment.persist(); + comment.setCurrentUser(user); + return Response.status(Status.CREATED).entity(comment).build(); + } + + @DELETE + @Path("{position}") + @Authenticated + @Transactional + @APIResponse(responseCode = "204") + @APIResponse(responseCode = "404") + public void delete(@PathParam("id") final UUID id, @PathParam("position") @PositiveOrZero final int position) { + final var user = User.getFromSecurityContext(context); + final var post = Post.findById(id); + Post.validateAccess(post, user, true, false); + final var comment = getComment(post, position); + + if (!user.id.equals(comment.author.id)) { + throw new ForbiddenException("invalid_author"); + } + + comment.softDelete(); + } + + @POST + @Path("{position}/acknowledge") + @Authenticated + @Transactional + @APIResponse(responseCode = "200") + @APIResponse(responseCode = "404") + public Response acknowledge( + @PathParam("id") final UUID id, @PathParam("position") @PositiveOrZero final int position) { + final var user = User.getFromSecurityContext(context); + final var post = Post.findById(id); + Post.validateAccess(post, user, true, false); + final var comment = getComment(post, position); + final var subscription = Subscription.find("user = ?1 and post = ?2", user, post) + .firstResult(); + + if (subscription == null) { + return Response.ok().build(); + } + + if (subscription.lastCommentSeen == null || subscription.lastCommentSeen.compareTo(comment) < 0) { + subscription.lastCommentSeen = comment; + subscription.persist(); + } + + return Response.ok().build(); + } + + private Comment getComment(final Post post, final int position) { + final var comment = Comment.find("post", Comment.sorting(), post) + .range(position, position + 1) + .firstResult(); + + if (comment == null) { + throw new NotFoundException(); + } + + return comment; + } +} diff --git a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java index 3d1c18f..67227b0 100644 --- a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java @@ -112,7 +112,7 @@ public Response setMain(@PathParam("id") final UUID id) { if (email == null) { throw new NotFoundException(); - } else if (!email.isVerified) { + } else if (!email.verified) { throw new ForbiddenException("email_not_verified"); } @@ -146,7 +146,7 @@ public Response activate(@NotNull @Valid final EmailActivation input) { throw new NotFoundException(); } - randomCode.email.isVerified = true; + randomCode.email.verified = true; randomCode.email.persist(); randomCode.delete(); return Response.ok().build(); diff --git a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java new file mode 100644 index 0000000..8cf2837 --- /dev/null +++ b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java @@ -0,0 +1,242 @@ +package app.fyreplace.api.endpoints; + +import app.fyreplace.api.data.Chapter; +import app.fyreplace.api.data.Post; +import app.fyreplace.api.data.PostPublication; +import app.fyreplace.api.data.Subscription; +import app.fyreplace.api.data.User; +import app.fyreplace.api.data.Vote; +import app.fyreplace.api.data.VoteCreation; +import app.fyreplace.api.exceptions.ForbiddenException; +import io.quarkus.panache.common.Sort; +import io.quarkus.panache.common.Sort.Direction; +import io.quarkus.security.Authenticated; +import jakarta.persistence.LockModeType; +import jakarta.transaction.Transactional; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.core.SecurityContext; +import java.time.Instant; +import java.util.UUID; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; + +@Path("posts") +public final class PostsEndpoint { + @ConfigProperty(name = "app.paging.size") + int pagingSize; + + @ConfigProperty(name = "app.posts.starting-life") + int postsStartingLife; + + @Context + SecurityContext context; + + @GET + @Authenticated + @APIResponse(responseCode = "200") + @APIResponse(responseCode = "400") + public Iterable list( + @QueryParam("page") @PositiveOrZero final int page, + @QueryParam("ascending") final boolean ascending, + @QueryParam("type") @NotNull final PostListingType type) { + final var user = User.getFromSecurityContext(context); + final var direction = ascending ? Direction.Ascending : Direction.Descending; + final var basicSort = Sort.by("datePublished", "id").direction(direction); + + final var postStream = + switch (type) { + case SUBSCRIBED_TO -> Subscription.find( + "user", + Sort.by("dateLastSeen", "post.datePublished", "post.id") + .direction(direction), + user) + .page(page, pagingSize) + .stream() + .map(s -> s.post); + case PUBLISHED -> Post.find("author = ?1 and datePublished is not null", basicSort, user) + .page(page, pagingSize) + .stream(); + case DRAFTS -> Post.find("author = ?1 and datePublished is null", basicSort, user) + .page(page, pagingSize) + .stream(); + }; + + try (postStream) { + return postStream.peek(p -> p.setCurrentUser(user)).toList(); + } + } + + @POST + @Authenticated + @Transactional + @APIResponse( + responseCode = "201", + content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Post.class))) + @APIResponse(responseCode = "400") + @APIResponse(responseCode = "401") + public Response create() { + final var user = User.getFromSecurityContext(context); + final var post = new Post(); + post.author = user; + post.persist(); + post.setCurrentUser(user); + return Response.status(Status.CREATED).entity(post).build(); + } + + @GET + @Path("{id}") + @APIResponse( + responseCode = "200", + content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Post.class))) + @APIResponse(responseCode = "404") + public Post retrieve(@PathParam("id") final UUID id) { + final var user = User.getFromSecurityContext(context, null, false); + final var post = Post.findById(id); + Post.validateAccess(post, user, null, false); + return post; + } + + @DELETE + @Path("{id}") + @Authenticated + @Transactional + @APIResponse(responseCode = "204") + @APIResponse(responseCode = "401") + @APIResponse(responseCode = "403") + @APIResponse(responseCode = "404") + public void delete(@PathParam("id") final UUID id) { + final var user = User.getFromSecurityContext(context); + final var post = Post.findById(id); + Post.validateAccess(post, user, null, true); + post.delete(); + } + + @POST + @Path("{id}/publish") + @Authenticated + @Transactional + @APIResponse(responseCode = "200") + @APIResponse(responseCode = "401") + @APIResponse(responseCode = "403") + @APIResponse(responseCode = "404") + public Response publish(@PathParam("id") final UUID id, @Valid @NotNull final PostPublication input) { + final var user = User.getFromSecurityContext(context); + final var post = Post.findById(id); + Post.validateAccess(post, user, false, true); + + if (Chapter.count("post", post) == 0) { + throw new ForbiddenException("invalid_chapter_count"); + } + + post.publish(postsStartingLife, input.anonymous()); + return Response.ok().build(); + } + + @PUT + @Path("{id}/isSubscribed") + @Authenticated + @Transactional + @APIResponse(responseCode = "200") + @APIResponse(responseCode = "401") + @APIResponse(responseCode = "404") + public Response createSubscription(@PathParam("id") final UUID id) { + final var user = User.getFromSecurityContext(context); + final var post = Post.findById(id); + Post.validateAccess(post, user, true, false); + user.subscribeTo(post); + return Response.ok().build(); + } + + @DELETE + @Path("{id}/isSubscribed") + @Authenticated + @Transactional + @APIResponse(responseCode = "204") + @APIResponse(responseCode = "401") + @APIResponse(responseCode = "404") + public void deleteSubscription(@PathParam("id") final UUID id) { + final var user = User.getFromSecurityContext(context); + final var post = Post.findById(id); + Post.validateAccess(post, user, true, false); + user.unsubscribeFrom(post); + } + + @POST + @Path("{id}/vote") + @Authenticated + @Transactional + @APIResponse(responseCode = "200") + @APIResponse(responseCode = "404") + public Response vote(@PathParam("id") final UUID id, @Valid @NotNull final VoteCreation input) { + final var user = User.getFromSecurityContext(context); + final var post = Post.findById(id, LockModeType.PESSIMISTIC_WRITE); + Post.validateAccess(post, user, true, false); + + if (post.isOld()) { + throw new ForbiddenException("post_too_old"); + } else if (user.id.equals(post.author.id)) { + throw new ForbiddenException("invalid_author"); + } + + final var vote = new Vote(); + vote.user = user; + vote.post = post; + vote.isSpread = input.isSpread(); + vote.persist(); + post.life += vote.isSpread ? 1 : -1; + post.persist(); + return Response.ok().build(); + } + + @GET + @Path("count") + @Authenticated + @APIResponse(responseCode = "200") + public long count(@QueryParam("type") @NotNull final PostListingType type) { + final var user = User.getFromSecurityContext(context); + return switch (type) { + case SUBSCRIBED_TO -> Subscription.count("user", user); + case PUBLISHED -> Post.count("author = ?1 and datePublished is not null", user); + case DRAFTS -> Post.count("author = ?1 and datePublished is null", user); + }; + } + + @GET + @Path("feed") + @Authenticated + @APIResponse(responseCode = "200") + public Iterable listFeed() { + final var user = User.getFromSecurityContext(context); + + try (final var postStream = Post.find( + "author != ?1 and datePublished > ?2 and life > 0 and id not in (select post.id from Vote where user = ?1)", + Sort.by("life", "datePublished", "id"), + user, + Instant.now().minus(Post.shelfLife)) + .range(0, 2) + .stream()) { + return postStream.peek(p -> p.setCurrentUser(user)).toList(); + } + } + + public enum PostListingType { + SUBSCRIBED_TO, + PUBLISHED, + DRAFTS + } +} diff --git a/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java index de402d9..4bb174b 100644 --- a/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java @@ -52,7 +52,7 @@ public Response create(@Valid @NotNull final TokenCreation input) { throw new NotFoundException(); } - randomCode.email.isVerified = true; + randomCode.email.verified = true; randomCode.email.persist(); randomCode.delete(); return Response.status(Status.CREATED).entity(jwtService.makeJwt(email)).build(); diff --git a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java index 7ff68dd..bd646d8 100644 --- a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java @@ -154,19 +154,19 @@ public void deleteBlock(@PathParam("id") final UUID id) { } @PUT - @Path("{id}/isBanned") + @Path("{id}/banned") @RolesAllowed({"ADMINISTRATOR", "MODERATOR"}) @Transactional @APIResponse(responseCode = "200") @APIResponse(responseCode = "401") @APIResponse(responseCode = "403") @APIResponse(responseCode = "404") - public Response ban(@PathParam("id") final UUID id) { + public Response updateBanned(@PathParam("id") final UUID id) { final var user = User.findById(id, LockModeType.PESSIMISTIC_WRITE); if (user == null) { throw new NotFoundException(); - } else if (!user.isBanned) { + } else if (!user.banned) { if (user.banCount == User.BanCount.NEVER) { user.dateBanEnd = Instant.now().plus(Duration.ofDays(7)); user.banCount = User.BanCount.ONCE; @@ -174,7 +174,7 @@ public Response ban(@PathParam("id") final UUID id) { user.banCount = User.BanCount.ONE_TOO_MANY; } - user.isBanned = true; + user.banned = true; user.persist(); } diff --git a/src/main/java/app/fyreplace/api/exceptions/ExceptionMappers.java b/src/main/java/app/fyreplace/api/exceptions/ExceptionMappers.java index 7ec2778..fc68791 100644 --- a/src/main/java/app/fyreplace/api/exceptions/ExceptionMappers.java +++ b/src/main/java/app/fyreplace/api/exceptions/ExceptionMappers.java @@ -18,4 +18,9 @@ public Response handleConflictException(final ConflictException exception) { public Response handleUnsupportedMediaTypeException(final UnsupportedMediaTypeException exception) { return Responses.makeFrom(exception); } + + @ServerExceptionMapper + public Response handleNumberFormatException(final NumberFormatException exception) { + return Response.status(Response.Status.BAD_REQUEST).build(); + } } diff --git a/src/main/java/app/fyreplace/api/services/MimeTypeService.java b/src/main/java/app/fyreplace/api/services/MimeTypeService.java index f47b293..d18dff5 100644 --- a/src/main/java/app/fyreplace/api/services/MimeTypeService.java +++ b/src/main/java/app/fyreplace/api/services/MimeTypeService.java @@ -6,6 +6,7 @@ import java.io.File; import java.io.IOException; import org.apache.tika.Tika; +import org.apache.tika.metadata.Metadata; @ApplicationScoped public final class MimeTypeService { @@ -17,4 +18,11 @@ public void validate(final File file, final KnownMimeTypes types) throws IOExcep throw new UnsupportedMediaTypeException("invalid_media_type"); } } + + public Metadata getMetadata(final File file) throws IOException { + final var tika = new Tika(); + final var metadata = new Metadata(); + tika.parse(file, metadata); + return metadata; + } } diff --git a/src/main/java/app/fyreplace/api/tasks/CleanupTasks.java b/src/main/java/app/fyreplace/api/tasks/CleanupTasks.java index ede9392..ae85df9 100644 --- a/src/main/java/app/fyreplace/api/tasks/CleanupTasks.java +++ b/src/main/java/app/fyreplace/api/tasks/CleanupTasks.java @@ -13,7 +13,7 @@ public final class CleanupTasks { @Scheduled(cron = "0 0 * * * ?") @Transactional public void removeOldInactiveUsers() { - User.delete("isActive = false and dateCreated < ?1", oneDayAgo()); + User.delete("active = false and dateCreated < ?1", oneDayAgo()); } @Scheduled(cron = "0 5 * * * ?") diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index da01808..9e455de 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -55,6 +55,10 @@ app: bucket: "" custom-domain: "" + posts: + max-chapter-count: 10 + starting-life: 4 + # Dev profile "%dev": diff --git a/src/test/java/app/fyreplace/api/testing/ImageTests.java b/src/test/java/app/fyreplace/api/testing/ImageTests.java new file mode 100644 index 0000000..f5001cf --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/ImageTests.java @@ -0,0 +1,35 @@ +package app.fyreplace.api.testing; + +import io.quarkus.test.common.http.TestHTTPResource; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +public abstract class ImageTests extends TransactionalTests { + @TestHTTPResource("image.jpeg") + URL jpeg; + + @TestHTTPResource("image.png") + URL png; + + @TestHTTPResource("image.webp") + URL webp; + + @TestHTTPResource("image.gif") + URL gif; + + @TestHTTPResource("image.txt") + URL text; + + protected InputStream openStream(final String fileType) throws IOException { + return (switch (fileType) { + case "jpeg" -> jpeg; + case "png" -> png; + case "webp" -> webp; + case "gif" -> gif; + case "text" -> text; + default -> throw new IllegalArgumentException("Unknown file type: " + fileType); + }) + .openStream(); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/TransactionalTests.java b/src/test/java/app/fyreplace/api/testing/TransactionalTests.java index c49d2f7..b0ff8c0 100644 --- a/src/test/java/app/fyreplace/api/testing/TransactionalTests.java +++ b/src/test/java/app/fyreplace/api/testing/TransactionalTests.java @@ -18,6 +18,8 @@ public abstract class TransactionalTests { @Inject MockMailbox mailbox; + protected static final String fakeId = "00000000-0000-0000-0000-000000000000"; + @BeforeEach public void beforeEach() { seeder.insertData(); diff --git a/src/test/java/app/fyreplace/api/testing/data/chapters/PositionBetweenTests.java b/src/test/java/app/fyreplace/api/testing/data/chapters/PositionBetweenTests.java new file mode 100644 index 0000000..aca8075 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/data/chapters/PositionBetweenTests.java @@ -0,0 +1,37 @@ +package app.fyreplace.api.testing.data.chapters; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import app.fyreplace.api.data.Chapter; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; + +@QuarkusTest +public final class PositionBetweenTests extends TransactionalTests { + @Test + public void positionBetweenNullAndNull() { + assertEquals("z", Chapter.positionBetween(null, null)); + } + + @Test + public void positionBetweenNullAndSomething() { + assertEquals("az", Chapter.positionBetween(null, "z")); + } + + @Test + public void positionBetweenSomethingAndNull() { + assertEquals("zz", Chapter.positionBetween("z", null)); + } + + @Test + public void positionBetweenSmallAndLarge() { + assertEquals("zaz", Chapter.positionBetween("z", "zz")); + } + + @Test + public void positionBetweenLargeAndSmall() { + assertThrows(IllegalArgumentException.class, () -> Chapter.positionBetween("zz", "z")); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/data/posts/NormalizeTests.java b/src/test/java/app/fyreplace/api/testing/data/posts/NormalizeTests.java new file mode 100644 index 0000000..edc7346 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/data/posts/NormalizeTests.java @@ -0,0 +1,75 @@ +package app.fyreplace.api.testing.data.posts; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import app.fyreplace.api.data.Chapter; +import app.fyreplace.api.data.Post; +import app.fyreplace.api.data.User; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.transaction.Transactional; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@QuarkusTest +public final class NormalizeTests extends TransactionalTests { + private Post post; + + private Post emptyPost; + + @Test + @Transactional + public void normalize() { + post.normalize(); + assertPostNormalized(post); + } + + @Test + @Transactional + public void normalizeTwice() { + post.normalize(); + post.normalize(); + assertPostNormalized(post); + } + + @Test + @Transactional + public void normalizeEmpty() { + emptyPost.normalize(); + assertEquals(0, emptyPost.getChapters().size()); + assertPostNormalized(emptyPost); + } + + @BeforeEach + @Transactional + @Override + public void beforeEach() { + super.beforeEach(); + final var user = User.findByUsername("user_0"); + final var chapterPositions = List.of("azaz", "azz", "zzazaz"); + + post = new Post(); + post.author = user; + post.persist(); + + for (final var position : chapterPositions) { + final var chapter = new Chapter(); + chapter.post = post; + chapter.position = position; + chapter.persist(); + } + + emptyPost = new Post(); + emptyPost.author = user; + emptyPost.persist(); + } + + private void assertPostNormalized(final Post post) { + final var chapters = post.getChapters(); + + for (var i = 0; i < chapters.size(); i++) { + assertEquals("z".repeat(i + 1), chapters.get(i).position); + } + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/PostTestsBase.java b/src/test/java/app/fyreplace/api/testing/endpoints/PostTestsBase.java new file mode 100644 index 0000000..5c004ee --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/PostTestsBase.java @@ -0,0 +1,22 @@ +package app.fyreplace.api.testing.endpoints; + +import app.fyreplace.api.data.Post; +import app.fyreplace.api.testing.ImageTests; +import io.quarkus.panache.common.Sort; +import org.junit.jupiter.api.BeforeEach; + +public abstract class PostTestsBase extends ImageTests { + public Post post; + + public Post draft; + + @BeforeEach + @Override + public void beforeEach() { + super.beforeEach(); + post = Post.find("author.username = 'user_0' and datePublished is not null", Sort.by("datePublished", "id")) + .firstResult(); + draft = Post.find("author.username = 'user_0' and datePublished is null", Sort.by("datePublished", "id")) + .firstResult(); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/CreateChapterTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/CreateChapterTests.java new file mode 100644 index 0000000..22d6aa4 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/CreateChapterTests.java @@ -0,0 +1,113 @@ +package app.fyreplace.api.testing.endpoints.chapters; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.isA; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import app.fyreplace.api.data.Chapter; +import app.fyreplace.api.endpoints.ChaptersEndpoint; +import app.fyreplace.api.testing.endpoints.PostTestsBase; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.panache.common.Sort; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(ChaptersEndpoint.class) +public final class CreateChapterTests extends PostTestsBase { + @ConfigProperty(name = "app.posts.max-chapter-count") + int postsMaxChapterCount; + + @Test + @TestSecurity(user = "user_0") + public void createChapterInOwnPost() { + final var chapterCount = post.getChapters().size(); + given().pathParam("id", post.id).post().then().statusCode(403); + assertEquals(chapterCount, Chapter.count("post", post)); + } + + @Test + @TestSecurity(user = "user_0") + public void createChapterInOwnDraft() { + final var chapterCount = draft.getChapters().size(); + final var response = given().pathParam("id", draft.id) + .post() + .then() + .statusCode(201) + .body("id", isA(String.class)) + .body("text", equalTo("")) + .body("image", nullValue()) + .body("width", equalTo(0)) + .body("height", equalTo(0)); + assertEquals(chapterCount + 1, Chapter.count("post", draft)); + final var lastChapter = Chapter.find("post", Sort.descending("position"), draft) + .firstResult(); + response.body("id", equalTo(lastChapter.id.toString())); + } + + @Test + @TestSecurity(user = "user_0") + public void createChapterInOwnDraftOverMaximum() { + QuarkusTransaction.requiringNew().run(() -> { + final var chapters = draft.getChapters(); + final var newChapters = postsMaxChapterCount - chapters.size(); + String before = chapters.get(chapters.size() - 1).position; + + for (var i = 0; i < newChapters; i++) { + final var chapter = new Chapter(); + chapter.post = draft; + chapter.position = Chapter.positionBetween(before, null); + ; + chapter.persist(); + before = chapter.position; + } + }); + + final var chapterCount = draft.getChapters().size(); + given().pathParam("id", draft.id).post().then().statusCode(403); + assertEquals(chapterCount, Chapter.count("post", draft)); + } + + @Test + @TestSecurity(user = "user_1") + public void createChapterInOtherPost() { + final var chapterCount = post.getChapters().size(); + given().pathParam("id", post.id).post().then().statusCode(403); + assertEquals(chapterCount, Chapter.count("post", post)); + } + + @Test + @TestSecurity(user = "user_1") + public void createChapterInOtherDraft() { + final var chapterCount = draft.getChapters().size(); + given().pathParam("id", draft.id).post().then().statusCode(404); + assertEquals(chapterCount, Chapter.count("post", draft)); + } + + @Test + public void createChapterInPostUnauthenticated() { + final var chapterCount = post.getChapters().size(); + given().pathParam("id", post.id).post().then().statusCode(401); + assertEquals(chapterCount, Chapter.count("post", post)); + } + + @Test + public void createChapterInDraftUnauthenticated() { + final var chapterCount = draft.getChapters().size(); + given().pathParam("id", draft.id).post().then().statusCode(401); + assertEquals(chapterCount, Chapter.count("post", draft)); + } + + @Test + @TestSecurity(user = "user_0") + public void createChapterInNonExistentPost() { + final var chapterCount = Chapter.count(); + given().pathParam("id", fakeId).post().then().statusCode(404); + assertEquals(chapterCount, Chapter.count()); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/DeleteChapterTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/DeleteChapterTests.java new file mode 100644 index 0000000..ec5942c --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/DeleteChapterTests.java @@ -0,0 +1,123 @@ +package app.fyreplace.api.testing.endpoints.chapters; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import app.fyreplace.api.data.Chapter; +import app.fyreplace.api.endpoints.ChaptersEndpoint; +import app.fyreplace.api.testing.endpoints.PostTestsBase; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import jakarta.transaction.Transactional; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@QuarkusTest +@TestHTTPEndpoint(ChaptersEndpoint.class) +public final class DeleteChapterTests extends PostTestsBase { + @ParameterizedTest + @ValueSource(ints = {0, 1, 2}) + @TestSecurity(user = "user_0") + public void deleteChapterInOwnPost(final int position) { + final var chapterCount = Chapter.count("post", post); + final var chapterId = post.getChapters().get(position).id; + given().pathParam("id", post.id).delete(String.valueOf(position)).then().statusCode(403); + assertEquals(chapterCount, Chapter.count("post", post)); + assertEquals(1, Chapter.count("id", chapterId)); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2}) + @TestSecurity(user = "user_0") + public void deleteChapterInOwnDraft(final int position) { + final var chapterCount = Chapter.count("post", draft); + final var chapterId = draft.getChapters().get(position).id; + given().pathParam("id", draft.id) + .delete(String.valueOf(position)) + .then() + .statusCode(204); + assertEquals(chapterCount - 1, Chapter.count("post", draft)); + assertEquals(0, Chapter.count("id", chapterId)); + } + + @ParameterizedTest + @ValueSource(strings = {"-1", "12", "1.5", "null"}) + @TestSecurity(user = "user_0") + public void deleteChapterInOwnDraftOutOfBounds(final String position) { + final var chapterCount = Chapter.count("post", draft); + given().pathParam("id", draft.id) + .delete(String.valueOf(position)) + .then() + .statusCode(404); + assertEquals(chapterCount, Chapter.count("post", draft)); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2}) + @TestSecurity(user = "user_1") + public void deleteChapterInOtherPost(final int position) { + final var chapterCount = Chapter.count("post", post); + final var chapterId = post.getChapters().get(position).id; + given().pathParam("id", post.id).delete(String.valueOf(position)).then().statusCode(403); + assertEquals(chapterCount, Chapter.count("post", post)); + assertEquals(1, Chapter.count("id", chapterId)); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2}) + @TestSecurity(user = "user_1") + public void deleteChapterInOtherDraft(final int position) { + final var chapterCount = Chapter.count("post", draft); + final var chapterId = draft.getChapters().get(position).id; + given().pathParam("id", draft.id) + .delete(String.valueOf(position)) + .then() + .statusCode(404); + assertEquals(chapterCount, Chapter.count("post", draft)); + assertEquals(1, Chapter.count("id", chapterId)); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2}) + public void deleteChapterInPostUnauthenticated(final int position) { + final var chapterCount = Chapter.count("post", post); + final var chapterId = post.getChapters().get(position).id; + given().pathParam("id", post.id).delete(String.valueOf(position)).then().statusCode(401); + assertEquals(chapterCount, Chapter.count("post", post)); + assertEquals(1, Chapter.count("id", chapterId)); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2}) + public void deleteChapterInDraftUnauthenticated(final int position) { + final var chapterCount = Chapter.count("post", draft); + final var chapterId = draft.getChapters().get(position).id; + given().pathParam("id", draft.id) + .delete(String.valueOf(position)) + .then() + .statusCode(401); + assertEquals(chapterCount, Chapter.count("post", draft)); + assertEquals(1, Chapter.count("id", chapterId)); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2}) + @TestSecurity(user = "user_0") + public void deleteChapterInNonExistentPost(final int position) { + final var chapterCount = Chapter.count(); + given().pathParam("id", fakeId).delete(String.valueOf(position)).then().statusCode(404); + assertEquals(chapterCount, Chapter.count()); + } + + @ParameterizedTest + @ValueSource(strings = {"fake", "null"}) + @TestSecurity(user = "user_0") + @Transactional + public void deleteNonExistentChapter(final String position) { + given().pathParam("id", draft.id) + .delete(String.valueOf(position)) + .then() + .statusCode(404); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterImageTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterImageTests.java new file mode 100644 index 0000000..4af8b8d --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterImageTests.java @@ -0,0 +1,215 @@ +package app.fyreplace.api.testing.endpoints.chapters; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import app.fyreplace.api.data.Chapter; +import app.fyreplace.api.endpoints.ChaptersEndpoint; +import app.fyreplace.api.testing.endpoints.PostTestsBase; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import java.io.IOException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@QuarkusTest +@TestHTTPEndpoint(ChaptersEndpoint.class) +public final class UpdateChapterImageTests extends PostTestsBase { + @Test + @TestSecurity(user = "user_0") + public void updateChapterImageInOwnPost() throws IOException { + final var position = 0; + + try (final var stream = openStream("jpeg")) { + given().contentType(ContentType.BINARY) + .body(stream.readAllBytes()) + .pathParam("id", post.id) + .put(position + "/image") + .then() + .statusCode(403); + } + + final var chapter = post.getChapters().get(position); + assertHasNoImage(chapter); + } + + @ParameterizedTest + @ValueSource(strings = {"jpeg", "png", "webp"}) + @TestSecurity(user = "user_0") + public void updateChapterImageInOwnDraft(final String fileType) throws IOException { + final var position = 0; + + try (final var stream = openStream(fileType)) { + given().contentType(ContentType.BINARY) + .body(stream.readAllBytes()) + .pathParam("id", draft.id) + .put(position + "/image") + .then() + .statusCode(200); + } + + final var chapter = draft.getChapters().get(position); + assertHasImage(chapter); + } + + @ParameterizedTest + @ValueSource(strings = {"-1", "12"}) + @TestSecurity(user = "user_0") + public void updateChapterImageInOwnDraftOutOfBounds(final String position) throws IOException { + try (final var stream = openStream("jpeg")) { + given().contentType(ContentType.BINARY) + .body(stream.readAllBytes()) + .pathParam("id", draft.id) + .put(position + "/image") + .then() + .statusCode(404); + } + } + + @ParameterizedTest + @ValueSource(strings = {"gif", "text"}) + @TestSecurity(user = "user_0") + public void updateChapterImageInOwnDraftWithInvalidType(final String fileType) throws IOException { + final var position = 0; + + try (final var stream = openStream(fileType)) { + given().contentType(ContentType.BINARY) + .body(stream.readAllBytes()) + .pathParam("id", draft.id) + .put(position + "/image") + .then() + .statusCode(415); + } + + final var chapter = draft.getChapters().get(position); + assertHasNoImage(chapter); + } + + @Test + @TestSecurity(user = "user_0") + public void updateChapterImageInOwnDraftWithoutInput() throws IOException { + final var position = 0; + given().pathParam("id", draft.id).put(position + "/image").then().statusCode(415); + final var chapter = draft.getChapters().get(position); + assertHasNoImage(chapter); + } + + @Test + @TestSecurity(user = "user_1") + public void updateChapterImageInOtherPost() throws IOException { + final var position = 0; + + try (final var stream = openStream("jpeg")) { + given().contentType(ContentType.BINARY) + .body(stream.readAllBytes()) + .pathParam("id", post.id) + .put(position + "/image") + .then() + .statusCode(403); + } + + final var chapter = post.getChapters().get(position); + assertHasNoImage(chapter); + } + + @Test + @TestSecurity(user = "user_1") + public void updateChapterImageInOtherDraft() throws IOException { + final var position = 0; + + try (final var stream = openStream("jpeg")) { + given().contentType(ContentType.BINARY) + .body(stream.readAllBytes()) + .pathParam("id", draft.id) + .put(position + "/image") + .then() + .statusCode(404); + } + + final var chapter = post.getChapters().get(position); + assertHasNoImage(chapter); + } + + @Test + public void updateChapterImageInPostUnauthenticated() throws IOException { + final var position = 0; + + try (final var stream = openStream("jpeg")) { + given().contentType(ContentType.BINARY) + .body(stream.readAllBytes()) + .pathParam("id", post.id) + .put(position + "/image") + .then() + .statusCode(401); + } + + final var chapter = post.getChapters().get(position); + assertHasNoImage(chapter); + } + + @Test + public void updateChapterImageInDraftUnauthenticated() throws IOException { + final var position = 0; + + try (final var stream = openStream("jpeg")) { + given().contentType(ContentType.BINARY) + .body(stream.readAllBytes()) + .pathParam("id", draft.id) + .put(position + "/image") + .then() + .statusCode(401); + } + + final var chapter = draft.getChapters().get(position); + assertHasNoImage(chapter); + } + + @Test + @TestSecurity(user = "user_0") + public void updateChapterTextInNonExistentPost() throws IOException { + final var position = 0; + + try (final var stream = openStream("jpeg")) { + given().contentType(ContentType.BINARY) + .body(stream.readAllBytes()) + .pathParam("id", fakeId) + .put(position + "/image") + .then() + .statusCode(404); + } + + final var chapter = draft.getChapters().get(position); + assertHasNoImage(chapter); + } + + @ParameterizedTest + @ValueSource(strings = {"fake", "null"}) + @TestSecurity(user = "user_0") + public void updateNonExistentChapterText(final String position) throws IOException { + try (final var stream = openStream("jpeg")) { + given().contentType(ContentType.BINARY) + .body(stream.readAllBytes()) + .pathParam("id", draft.id) + .put(position + "/image") + .then() + .statusCode(404); + } + } + + private void assertHasImage(final Chapter chapter) { + assertNotNull(chapter.image); + assertEquals(256, chapter.width); + assertEquals(256, chapter.height); + } + + private void assertHasNoImage(final Chapter chapter) { + assertNull(chapter.image); + assertEquals(0, chapter.width); + assertEquals(0, chapter.height); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterPositionTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterPositionTests.java new file mode 100644 index 0000000..120c72b --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterPositionTests.java @@ -0,0 +1,161 @@ +package app.fyreplace.api.testing.endpoints.chapters; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import app.fyreplace.api.data.Chapter; +import app.fyreplace.api.endpoints.ChaptersEndpoint; +import app.fyreplace.api.testing.endpoints.PostTestsBase; +import io.quarkus.panache.common.Sort; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import jakarta.transaction.Transactional; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@QuarkusTest +@TestHTTPEndpoint(ChaptersEndpoint.class) +public final class UpdateChapterPositionTests extends PostTestsBase { + @ParameterizedTest + @ValueSource(ints = {0, 1, 2}) + @TestSecurity(user = "user_0") + @Transactional + public void updateChapterPositionInOwnPost(final int to) { + final var from = 1; + final var chapter = post.getChapters().get(from); + final var position = chapter.position; + given().body(to).pathParam("id", post.id).put(from + "/position").then().statusCode(403); + chapter.refresh(); + assertEquals(position, chapter.position); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2}) + @TestSecurity(user = "user_0") + public void updateChapterPositionInOwnDraft(final int to) { + final var from = 1; + final var chapter = draft.getChapters().get(from); + given().body(to) + .pathParam("id", draft.id) + .put(from + "/position") + .then() + .statusCode(200); + final var chapters = + Chapter.find("post", Sort.by("position"), draft).list(); + assertEquals(chapter.id, chapters.get(to).id); + } + + @ParameterizedTest + @ValueSource(strings = {"-1", "12"}) + @TestSecurity(user = "user_0") + @Transactional + public void updateChapterPositionInOwnDraftOutOfBounds(final String to) { + final var from = 1; + final var chapter = draft.getChapters().get(from); + final var position = chapter.position; + given().body(to) + .pathParam("id", draft.id) + .put(from + "/position") + .then() + .statusCode(404); + chapter.refresh(); + assertEquals(position, chapter.position); + } + + @ParameterizedTest + @ValueSource(strings = {"1.5", "null"}) + @TestSecurity(user = "user_0") + @Transactional + public void updateChapterPositionInOwnDraftWithInvalidInput(final String to) { + final var from = 1; + final var chapter = draft.getChapters().get(from); + final var position = chapter.position; + given().body(to) + .pathParam("id", draft.id) + .put(from + "/position") + .then() + .statusCode(400); + chapter.refresh(); + assertEquals(position, chapter.position); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2}) + @TestSecurity(user = "user_1") + @Transactional + public void updateChapterPositionInOtherPost(final int to) { + final var from = 1; + final var chapter = post.getChapters().get(from); + final var oldPosition = chapter.position; + given().body(to).pathParam("id", post.id).put(from + "/position").then().statusCode(403); + chapter.refresh(); + assertEquals(oldPosition, chapter.position); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2}) + @TestSecurity(user = "user_1") + @Transactional + public void updateChapterPositionInOtherDraft(final int to) { + final var from = 1; + final var chapter = draft.getChapters().get(from); + final var oldPosition = chapter.position; + given().body(to) + .pathParam("id", draft.id) + .put(from + "/position") + .then() + .statusCode(404); + chapter.refresh(); + assertEquals(oldPosition, chapter.position); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2}) + @Transactional + public void updateChapterPositionInPostUnauthenticated(final int to) { + final var from = 1; + final var chapter = post.getChapters().get(from); + final var oldPosition = chapter.position; + given().body(to).pathParam("id", post.id).put(from + "/position").then().statusCode(401); + chapter.refresh(); + assertEquals(oldPosition, chapter.position); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2}) + @Transactional + public void updateChapterPositionInDraftUnauthenticated(final int to) { + final var from = 1; + final var chapter = draft.getChapters().get(from); + final var oldPosition = chapter.position; + given().body(to) + .pathParam("id", draft.id) + .put(from + "/position") + .then() + .statusCode(401); + chapter.refresh(); + assertEquals(oldPosition, chapter.position); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2}) + @TestSecurity(user = "user_0") + @Transactional + public void updateChapterPositionInNonExistentPost(final int to) { + final var from = 1; + final var chapter = post.getChapters().get(from); + final var oldPosition = chapter.position; + given().body(to).pathParam("id", fakeId).put(from + "/position").then().statusCode(404); + chapter.refresh(); + assertEquals(oldPosition, chapter.position); + } + + @ParameterizedTest + @ValueSource(strings = {"fake", "null"}) + @TestSecurity(user = "user_0") + @Transactional + public void updateNonExistentChapterPosition(final String from) { + given().body(1).pathParam("id", draft.id).put(from + "/position").then().statusCode(404); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterTextTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterTextTests.java new file mode 100644 index 0000000..b7534eb --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterTextTests.java @@ -0,0 +1,164 @@ +package app.fyreplace.api.testing.endpoints.chapters; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import app.fyreplace.api.endpoints.ChaptersEndpoint; +import app.fyreplace.api.testing.endpoints.PostTestsBase; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@QuarkusTest +@TestHTTPEndpoint(ChaptersEndpoint.class) +public final class UpdateChapterTextTests extends PostTestsBase { + @Test + @TestSecurity(user = "user_0") + @Transactional + public void updateChapterTextInOwnPost() { + final var position = 0; + final var chapter = post.getChapters().get(position); + final var oldText = chapter.text; + given().body("Hello") + .pathParam("id", post.id) + .put(position + "/text") + .then() + .statusCode(403); + chapter.refresh(); + assertEquals(oldText, chapter.text); + } + + @ParameterizedTest + @ValueSource(strings = {"Hello", ""}) + @TestSecurity(user = "user_0") + public void updateChapterTextInOwnDraft(final String text) { + final var position = 0; + given().body(text) + .pathParam("id", draft.id) + .put(position + "/text") + .then() + .statusCode(200); + final var chapter = draft.getChapters().get(position); + assertEquals(text, chapter.text); + } + + @ParameterizedTest + @ValueSource(strings = {"-1", "12"}) + @TestSecurity(user = "user_0") + public void updateChapterTextInOwnDraftOutOfBounds(final String position) { + given().body("Hello") + .pathParam("id", draft.id) + .put(position + "/text") + .then() + .statusCode(404); + } + + @Test + @TestSecurity(user = "user_0") + @Transactional + public void updateChapterTextInOwnDraftWithInvalidInput() { + final var position = 0; + final var chapter = draft.getChapters().get(position); + final var oldText = chapter.text; + given().body("a".repeat(501)) + .pathParam("id", draft.id) + .put(position + "/text") + .then() + .statusCode(400); + chapter.refresh(); + assertEquals(oldText, chapter.text); + } + + @Test + @TestSecurity(user = "user_1") + @Transactional + public void updateChapterTextInOtherPost() { + final var position = 0; + final var chapter = post.getChapters().get(position); + final var oldText = chapter.text; + given().body("Hello") + .pathParam("id", post.id) + .put(position + "/text") + .then() + .statusCode(403); + chapter.refresh(); + assertEquals(oldText, chapter.text); + } + + @Test + @TestSecurity(user = "user_1") + @Transactional + public void updateChapterTextInOtherDraft() { + final var position = 0; + final var chapter = draft.getChapters().get(position); + final var oldText = chapter.text; + given().body("Hello") + .pathParam("id", draft.id) + .put(position + "/text") + .then() + .statusCode(404); + chapter.refresh(); + assertEquals(oldText, chapter.text); + } + + @Test + @Transactional + public void updateChapterTextInPostUnauthenticated() { + final var position = 0; + final var chapter = post.getChapters().get(position); + final var oldText = chapter.text; + given().body("Hello") + .pathParam("id", post.id) + .put(position + "/text") + .then() + .statusCode(401); + chapter.refresh(); + assertEquals(oldText, chapter.text); + } + + @Test + @Transactional + public void updateChapterTextInDraftUnauthenticated() { + final var position = 0; + final var chapter = draft.getChapters().get(position); + final var oldText = chapter.text; + given().body("Hello") + .pathParam("id", draft.id) + .put(position + "/text") + .then() + .statusCode(401); + chapter.refresh(); + assertEquals(oldText, chapter.text); + } + + @Test + @TestSecurity(user = "user_0") + @Transactional + public void updateChapterTextInNonExistentPost() { + final var position = 0; + final var chapter = draft.getChapters().get(position); + final var oldText = chapter.text; + given().body("Hello") + .pathParam("id", fakeId) + .put(position + "/text") + .then() + .statusCode(404); + chapter.refresh(); + assertEquals(oldText, chapter.text); + } + + @ParameterizedTest + @ValueSource(strings = {"fake", "null"}) + @TestSecurity(user = "user_0") + public void updateNonExistentChapterText(final String from) { + given().body("Hello") + .pathParam("id", draft.id) + .put(from + "/text") + .then() + .statusCode(404); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/AcknowledgeTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/AcknowledgeTests.java new file mode 100644 index 0000000..d5e18b4 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/AcknowledgeTests.java @@ -0,0 +1,64 @@ +package app.fyreplace.api.testing.endpoints.comments; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import app.fyreplace.api.data.Comment; +import app.fyreplace.api.data.Subscription; +import app.fyreplace.api.endpoints.CommentsEndpoint; +import app.fyreplace.api.testing.endpoints.PostTestsBase; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(CommentsEndpoint.class) +public class AcknowledgeTests extends PostTestsBase { + @Test + @TestSecurity(user = "user_0") + public void acknowledge() { + final var position = 5; + given().pathParam("id", post.id).post(position + "/acknowledge").then().statusCode(200); + final var subscription = Subscription.find("user.username = 'user_0' and post = ?1", post) + .firstResult(); + final var comment = + Comment.list("post", Comment.sorting(), post).get(position); + assertEquals(subscription.lastCommentSeen.id, comment.id); + } + + @Test + @TestSecurity(user = "user_0") + public void acknowledgePastComment() { + final var currentPosition = 5; + final var position = 3; + QuarkusTransaction.requiringNew().run(() -> { + final var subscription = Subscription.find("user.username = 'user_0' and post = ?1", post) + .firstResult(); + subscription.lastCommentSeen = + Comment.list("post", Comment.sorting(), post).get(currentPosition); + subscription.persist(); + }); + given().pathParam("id", post.id).post(position + "/acknowledge").then().statusCode(200); + final var subscription = Subscription.find("user.username = 'user_0' and post = ?1", post) + .firstResult(); + final var comment = + Comment.list("post", Comment.sorting(), post).get(currentPosition); + assertEquals(subscription.lastCommentSeen.id, comment.id); + } + + @Test + @TestSecurity(user = "user_0") + public void acknowledgeOutOfBounds() { + final var position = -1; + given().pathParam("id", post.id).post(position + "/acknowledge").then().statusCode(400); + } + + @Test + @TestSecurity(user = "user_0") + public void acknowledgeTooFar() { + final var position = 12; + given().pathParam("id", post.id).post(position + "/acknowledge").then().statusCode(404); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/CountTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/CountTests.java new file mode 100644 index 0000000..2a5d819 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/CountTests.java @@ -0,0 +1,83 @@ +package app.fyreplace.api.testing.endpoints.comments; + +import static io.restassured.RestAssured.given; +import static java.util.stream.IntStream.range; +import static org.hamcrest.Matchers.equalTo; + +import app.fyreplace.api.data.Comment; +import app.fyreplace.api.data.Post; +import app.fyreplace.api.data.Subscription; +import app.fyreplace.api.data.User; +import app.fyreplace.api.data.dev.DataSeeder; +import app.fyreplace.api.endpoints.CommentsEndpoint; +import app.fyreplace.api.testing.endpoints.PostTestsBase; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(CommentsEndpoint.class) +public class CountTests extends PostTestsBase { + @Inject + DataSeeder dataSeeder; + + private static final int readCommentCount = 10; + + private static final int notReadCommentCount = 6; + + @Test + @TestSecurity(user = "user_0") + public void count() { + given().pathParam("id", post.id) + .get("count") + .then() + .statusCode(200) + .body(equalTo(String.valueOf(readCommentCount + notReadCommentCount))); + } + + @Test + @TestSecurity(user = "user_0") + public void countRead() { + given().pathParam("id", post.id) + .queryParam("read", true) + .get("count") + .then() + .statusCode(200) + .body(equalTo(String.valueOf(readCommentCount))); + } + + @Test + @TestSecurity(user = "user_0") + public void countNotRead() { + given().pathParam("id", post.id) + .queryParam("read", false) + .get("count") + .then() + .statusCode(200) + .body(equalTo(String.valueOf(notReadCommentCount))); + } + + @BeforeEach + @Transactional + @Override + public void beforeEach() { + super.beforeEach(); + Comment.deleteAll(); + final var user = User.findByUsername("user_1"); + range(0, readCommentCount).forEach(i -> dataSeeder.createComment(user, post, "Comment " + i, false)); + final var subscription = Subscription.find("user.username = 'user_0' and post = ?1", post) + .firstResult(); + subscription.lastCommentSeen = Comment.find( + "post", Comment.sorting().descending(), post) + .firstResult(); + subscription.persist(); + range(0, notReadCommentCount).forEach(i -> dataSeeder.createComment(user, post, "Comment " + i, false)); + range(0, 10) + .forEach(i -> dataSeeder.createComment( + user, Post.find("id != ?1", post.id).firstResult(), "Comment " + i, false)); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/CreateTests.java new file mode 100644 index 0000000..33a827b --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/CreateTests.java @@ -0,0 +1,163 @@ +package app.fyreplace.api.testing.endpoints.comments; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import app.fyreplace.api.data.Comment; +import app.fyreplace.api.data.CommentCreation; +import app.fyreplace.api.data.Post; +import app.fyreplace.api.endpoints.CommentsEndpoint; +import app.fyreplace.api.testing.endpoints.PostTestsBase; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(CommentsEndpoint.class) +public class CreateTests extends PostTestsBase { + @Test + @TestSecurity(user = "user_0") + public void createOnOwnPost() { + final var commentCount = Comment.count("post", post); + final var input = new CommentCreation("Text", false); + final var response = given().contentType(ContentType.JSON) + .body(input) + .pathParam("id", post.id) + .post() + .then() + .statusCode(201) + .body("text", equalTo(input.text())) + .body("anonymous", equalTo(input.anonymous())); + assertEquals(commentCount + 1, Comment.count("post", post)); + final var comment = getLastComment(post); + response.body("id", equalTo(comment.id.toString())); + assertEquals(input.text(), comment.text); + assertEquals("user_0", comment.author.username); + } + + @Test + @TestSecurity(user = "user_0") + public void createAnonymouslyOnOwnPost() { + final var commentCount = Comment.count("post", post); + final var input = new CommentCreation("Text", true); + final var response = given().contentType(ContentType.JSON) + .body(input) + .pathParam("id", post.id) + .post() + .then() + .statusCode(201) + .body("text", equalTo(input.text())) + .body("anonymous", equalTo(input.anonymous())); + assertEquals(commentCount + 1, Comment.count("post", post)); + final var comment = getLastComment(post); + response.body("id", equalTo(comment.id.toString())); + assertEquals(input.text(), comment.text); + assertEquals("user_0", comment.author.username); + } + + @Test + @TestSecurity(user = "user_0") + public void createOnOwnDraft() { + final var input = new CommentCreation("Text", false); + given().contentType(ContentType.JSON) + .body(input) + .pathParam("id", draft.id) + .post() + .then() + .statusCode(403); + assertEquals(0, Comment.count("post", draft)); + } + + @Test + @TestSecurity(user = "user_1") + public void createOnOtherPost() { + final var commentCount = Comment.count("post", post); + final var input = new CommentCreation("Text", false); + final var response = given().contentType(ContentType.JSON) + .body(input) + .pathParam("id", post.id) + .post() + .then() + .statusCode(201) + .body("text", equalTo(input.text())); + assertEquals(commentCount + 1, Comment.count("post", post)); + final var comment = getLastComment(post); + response.body("id", equalTo(comment.id.toString())); + assertEquals(input.text(), comment.text); + assertEquals("user_1", comment.author.username); + } + + @Test + @TestSecurity(user = "user_1") + public void createAnonymouslyOnOtherPost() { + final var commentCount = Comment.count("post", post); + final var input = new CommentCreation("Text", true); + given().contentType(ContentType.JSON) + .body(input) + .pathParam("id", post.id) + .post() + .then() + .statusCode(403); + assertEquals(commentCount, Comment.count("post", post)); + } + + @Test + @TestSecurity(user = "user_1") + public void createOnOtherDraft() { + final var input = new CommentCreation("Text", false); + given().contentType(ContentType.JSON) + .body(input) + .pathParam("id", draft.id) + .post() + .then() + .statusCode(404); + assertEquals(0, Comment.count("post", draft)); + } + + @Test + @TestSecurity(user = "user_1") + public void createOnNonExistentPost() { + final var commentCount = Comment.count(); + final var input = new CommentCreation("Text", false); + given().contentType(ContentType.JSON) + .body(input) + .pathParam("id", fakeId) + .post() + .then() + .statusCode(404); + assertEquals(commentCount, Comment.count()); + } + + @Test + @TestSecurity(user = "user_1") + public void createWithEmptyInput() { + final var commentCount = Comment.count("post", post); + given().contentType(ContentType.JSON) + .pathParam("id", post.id) + .post() + .then() + .statusCode(400); + assertEquals(commentCount, Comment.count("post", post)); + } + + @Test + public void createUnauthenticated() { + final var commentCount = Comment.count("post", post); + final var input = new CommentCreation("Text", false); + given().contentType(ContentType.JSON) + .body(input) + .pathParam("id", post.id) + .post() + .then() + .statusCode(401); + assertEquals(commentCount, Comment.count("post", post)); + } + + private Comment getLastComment(final Post post) { + return Comment.find("post", Comment.sorting().descending(), post) + .firstResult(); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/DeleteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/DeleteTests.java new file mode 100644 index 0000000..4410f1b --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/DeleteTests.java @@ -0,0 +1,131 @@ +package app.fyreplace.api.testing.endpoints.comments; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import app.fyreplace.api.data.Comment; +import app.fyreplace.api.data.User; +import app.fyreplace.api.data.dev.DataSeeder; +import app.fyreplace.api.endpoints.CommentsEndpoint; +import app.fyreplace.api.testing.endpoints.PostTestsBase; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(CommentsEndpoint.class) +public class DeleteTests extends PostTestsBase { + @Inject + DataSeeder dataSeeder; + + @Test + @TestSecurity(user = "user_0") + @Transactional + public void deleteOwnCommentOnOwnPost() { + final var commentCount = Comment.count("post", post); + final var position = 0; + given().pathParam("id", post.id).delete(String.valueOf(position)).then().statusCode(204); + assertEquals(commentCount, Comment.count("post", post)); + final var comment = getComment(position); + comment.refresh(); + assertTrue(comment.deleted); + assertEquals("", comment.text); + } + + @Test + @TestSecurity(user = "user_1") + @Transactional + public void deleteOwnCommentOnOtherPost() { + final var commentCount = Comment.count("post", post); + final var position = 1; + given().pathParam("id", post.id).delete(String.valueOf(position)).then().statusCode(204); + assertEquals(commentCount, Comment.count("post", post)); + final var comment = getComment(position); + comment.refresh(); + assertTrue(comment.deleted); + assertEquals("", comment.text); + } + + @Test + @TestSecurity(user = "user_0") + @Transactional + public void deleteOtherCommentOnOwnPost() { + final var commentCount = Comment.count("post", post); + final var position = 1; + given().pathParam("id", post.id).delete(String.valueOf(position)).then().statusCode(403); + assertEquals(commentCount, Comment.count("post", post)); + final var comment = getComment(position); + comment.refresh(); + assertFalse(comment.deleted); + } + + @Test + @TestSecurity(user = "user_1") + @Transactional + public void deleteOtherCommentOnOtherPost() { + final var commentCount = Comment.count("post", post); + final var position = 0; + given().pathParam("id", post.id).delete(String.valueOf(position)).then().statusCode(403); + assertEquals(commentCount, Comment.count("post", post)); + final var comment = getComment(position); + comment.refresh(); + assertFalse(comment.deleted); + } + + @Test + @TestSecurity(user = "user_1") + public void deleteOutOfBounds() { + final var commentCount = Comment.count("post", post); + final var position = -1; + given().pathParam("id", post.id).delete(String.valueOf(position)).then().statusCode(400); + assertEquals(commentCount, Comment.count("post", post)); + } + + @Test + @TestSecurity(user = "user_1") + public void deleteTooFar() { + final var commentCount = Comment.count("post", post); + final var position = 50; + given().pathParam("id", post.id).delete(String.valueOf(position)).then().statusCode(404); + assertEquals(commentCount, Comment.count("post", post)); + } + + @Test + @TestSecurity(user = "user_0") + public void deleteOnNonExistentPost() { + final var commentCount = Comment.count(); + given().pathParam("id", fakeId).delete(String.valueOf(0)).then().statusCode(404); + assertEquals(commentCount, Comment.count()); + } + + @Test + public void deleteUnauthenticated() { + final var commentCount = Comment.count("post", post); + given().pathParam("id", post.id).delete(String.valueOf(0)).then().statusCode(401); + assertEquals(commentCount, Comment.count("post", post)); + } + + @BeforeEach + @Transactional + @Override + public void beforeEach() { + super.beforeEach(); + Comment.deleteAll(); + final var user = User.findByUsername("user_0"); + final var otherUser = User.findByUsername("user_1"); + dataSeeder.createComment(user, post, "Comment from user 0", false); + dataSeeder.createComment(otherUser, post, "Comment from user 1", false); + } + + private Comment getComment(final int position) { + return Comment.find("post", Comment.sorting(), post) + .range(position, position + 1) + .firstResult(); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/ListTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/ListTests.java new file mode 100644 index 0000000..b58be8b --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/ListTests.java @@ -0,0 +1,109 @@ +package app.fyreplace.api.testing.endpoints.comments; + +import static io.restassured.RestAssured.given; +import static java.util.stream.IntStream.range; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.in; +import static org.hamcrest.Matchers.nullValue; + +import app.fyreplace.api.data.Comment; +import app.fyreplace.api.data.User; +import app.fyreplace.api.data.dev.DataSeeder; +import app.fyreplace.api.endpoints.CommentsEndpoint; +import app.fyreplace.api.testing.endpoints.PostTestsBase; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import java.util.ArrayList; +import java.util.List; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(CommentsEndpoint.class) +public class ListTests extends PostTestsBase { + @ConfigProperty(name = "app.paging.size") + int pagingSize; + + @Inject + DataSeeder dataSeeder; + + private final List commentIds = new ArrayList<>(); + + @Test + @TestSecurity(user = "user_0") + public void listInOwnPost() { + final var response = given().pathParam("id", post.id) + .queryParam("page", 0) + .get() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("size()", equalTo(pagingSize)) + .body("[0].id", in(commentIds)) + .body("[0].author.username", equalTo("user_0")) + .body("[0].anonymous", equalTo(true)); + + range(1, pagingSize).forEach(i -> response.body("[" + i + "].id", in(commentIds)) + .body("[" + i + "].author.username", equalTo("user_1"))); + } + + @Test + @TestSecurity(user = "user_1") + public void listInOtherPost() { + final var response = given().pathParam("id", post.id) + .queryParam("page", 0) + .get() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("size()", equalTo(pagingSize)) + .body("[0].id", in(commentIds)) + .body("[0].author", nullValue()) + .body("[0].anonymous", equalTo(true)); + + range(1, pagingSize).forEach(i -> response.body("[" + i + "].id", in(commentIds)) + .body("[" + i + "].author.username", equalTo("user_1"))); + } + + @Test + @TestSecurity(user = "user_0") + public void listOutOfBounds() { + final var page = -1; + given().pathParam("id", post.id).queryParam("page", page).get().then().statusCode(400); + } + + @Test + @TestSecurity(user = "user_0") + public void listTooFar() { + final var page = 50; + given().pathParam("id", post.id) + .queryParam("page", page) + .get() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body(equalTo("[]")); + } + + @BeforeEach + @Transactional + @Override + public void beforeEach() { + super.beforeEach(); + Comment.deleteAll(); + final var user0 = User.findByUsername("user_0"); + final var user1 = User.findByUsername("user_1"); + commentIds.add( + dataSeeder.createComment(user0, post, "Comment 0", true).id.toString()); + range(1, pagingSize) + .forEach(i -> commentIds.add(dataSeeder + .createComment(user1, post, "Comment " + i, false) + .id + .toString())); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CountTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CountTests.java index aea13c5..5a7feb4 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CountTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CountTests.java @@ -38,7 +38,7 @@ public void beforeEach() { final var email = new Email(); email.user = user; email.email = user.username + "_" + i + "@example.com"; - email.isVerified = i % 5 == 0; + email.verified = i % 5 == 0; email.persist(); }); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java index 9d1f401..32c934a 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java @@ -30,7 +30,7 @@ public void create() { .contentType(ContentType.JSON) .statusCode(201) .body("email", equalTo(email)) - .body("isVerified", equalTo(false)) + .body("verified", equalTo(false)) .body("isMain", equalTo(false)); assertEquals(emailCount + 1, Email.count()); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java index b9f919c..f426a8d 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java @@ -48,7 +48,7 @@ public void deleteOtherEmail() { @TestSecurity(user = "user_0") public void deleteNonExistentEmail() { final var emailCount = Email.count(); - given().delete("nope").then().statusCode(404); + given().delete(fakeId).then().statusCode(404); assertEquals(emailCount, Email.count()); } @@ -60,7 +60,7 @@ public void beforeEach() { newEmail = new Email(); newEmail.user = User.findByUsername("user_0"); newEmail.email = "new_email@example.org"; - newEmail.isVerified = true; + newEmail.verified = true; newEmail.persist(); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java index d0eb093..10610cd 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java @@ -70,7 +70,7 @@ public void beforeEach() { final var email = new Email(); email.user = user; email.email = user.username + "_" + i + "@example.com"; - email.isVerified = i % 5 == 0; + email.verified = i % 5 == 0; email.persist(); }); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java index eb75481..4eb45d9 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java @@ -8,6 +8,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.EmailsEndpoint; import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -19,7 +20,6 @@ @TestHTTPEndpoint(EmailsEndpoint.class) public final class SetMainTests extends TransactionalTests { private Email secondaryEmail; - private Email unverifiedEmail; @Test @TestSecurity(user = "user_0") @@ -43,7 +43,8 @@ public void setMainTwice() { @Test @TestSecurity(user = "user_0") public void setMainWithUnverifiedEmail() { - given().post(unverifiedEmail.id + "/main").then().statusCode(403); + QuarkusTransaction.requiringNew().run(() -> Email.update("verified = false where id = ?1", secondaryEmail.id)); + given().post(secondaryEmail.id + "/main").then().statusCode(403); secondaryEmail = Email.findById(secondaryEmail.id); assertFalse(secondaryEmail.isMain()); } @@ -60,7 +61,7 @@ public void setMainWithOtherEmail() { @Test @TestSecurity(user = "user_0") public void setMainWithNonExistentEmail() { - given().post("invalid" + "/main").then().statusCode(404); + given().post(fakeId + "/main").then().statusCode(404); secondaryEmail = Email.findById(secondaryEmail.id); assertFalse(secondaryEmail.isMain()); } @@ -73,12 +74,7 @@ public void beforeEach() { secondaryEmail = new Email(); secondaryEmail.user = User.findByUsername("user_0"); secondaryEmail.email = "new_email@example.org"; - secondaryEmail.isVerified = true; + secondaryEmail.verified = true; secondaryEmail.persist(); - unverifiedEmail = new Email(); - unverifiedEmail.user = secondaryEmail.user; - unverifiedEmail.email = "unverified@example.org"; - unverifiedEmail.isVerified = false; - unverifiedEmail.persist(); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/CountTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/CountTests.java new file mode 100644 index 0000000..e36bf07 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/CountTests.java @@ -0,0 +1,71 @@ +package app.fyreplace.api.testing.endpoints.posts; + +import static io.restassured.RestAssured.given; +import static java.util.stream.IntStream.range; +import static org.hamcrest.Matchers.equalTo; + +import app.fyreplace.api.data.Post; +import app.fyreplace.api.data.User; +import app.fyreplace.api.data.dev.DataSeeder; +import app.fyreplace.api.endpoints.PostsEndpoint; +import app.fyreplace.api.testing.endpoints.PostTestsBase; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(PostsEndpoint.class) +public class CountTests extends PostTestsBase { + @Inject + DataSeeder dataSeeder; + + private static final int publishedPostCount = 10; + + private static final int draftPostCount = 6; + + @Test + @TestSecurity(user = "user_0") + public void countSubscribedTo() { + given().queryParam("type", PostsEndpoint.PostListingType.SUBSCRIBED_TO) + .get("count") + .then() + .statusCode(200) + .body(equalTo(String.valueOf(publishedPostCount))); + } + + @Test + @TestSecurity(user = "user_0") + public void countPublished() { + given().queryParam("type", PostsEndpoint.PostListingType.PUBLISHED) + .get("count") + .then() + .statusCode(200) + .body(equalTo(String.valueOf(publishedPostCount))); + } + + @Test + @TestSecurity(user = "user_0") + public void countDrafts() { + given().queryParam("type", PostsEndpoint.PostListingType.DRAFTS) + .get("count") + .then() + .statusCode(200) + .body(equalTo(String.valueOf(draftPostCount))); + } + + @BeforeEach + @Transactional + @Override + public void beforeEach() { + super.beforeEach(); + Post.deleteAll(); + final var user = User.findByUsername("user_0"); + range(0, publishedPostCount).forEach(i -> dataSeeder.createPost(user, "Post " + i, true, false)); + range(0, draftPostCount).forEach(i -> dataSeeder.createPost(user, "Post " + i, false, false)); + range(0, 10).forEach(i -> dataSeeder.createPost(User.findByUsername("user_1"), "Post " + i, false, false)); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateTests.java new file mode 100644 index 0000000..42767fb --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateTests.java @@ -0,0 +1,41 @@ +package app.fyreplace.api.testing.endpoints.posts; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.isA; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import app.fyreplace.api.data.Post; +import app.fyreplace.api.endpoints.PostsEndpoint; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(PostsEndpoint.class) +public final class CreateTests extends TransactionalTests { + @Test + @TestSecurity(user = "user_0") + public void create() { + final var postCount = Post.count("author.username", "user_0"); + given().post() + .then() + .statusCode(201) + .body("id", isA(String.class)) + .body("dateCreated", notNullValue()) + .body("datePublished", nullValue()) + .body("author.username", equalTo("user_0")) + .body("anonymous", equalTo(false)) + .body("chapters.size()", equalTo(0)); + assertEquals(postCount + 1, Post.count("author.username", "user_0")); + } + + @Test + public void createUnauthenticated() { + given().post().then().statusCode(401); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteTests.java new file mode 100644 index 0000000..414b4fd --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteTests.java @@ -0,0 +1,65 @@ +package app.fyreplace.api.testing.endpoints.posts; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import app.fyreplace.api.data.Post; +import app.fyreplace.api.endpoints.PostsEndpoint; +import app.fyreplace.api.testing.endpoints.PostTestsBase; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@QuarkusTest +@TestHTTPEndpoint(PostsEndpoint.class) +public final class DeleteTests extends PostTestsBase { + @Test + @TestSecurity(user = "user_0") + public void deleteOwnPost() { + given().delete(post.id.toString()).then().statusCode(204); + assertEquals(0, Post.count("id", post.id)); + } + + @Test + @TestSecurity(user = "user_0") + public void deleteOwnDraft() { + given().delete(draft.id.toString()).then().statusCode(204); + assertEquals(0, Post.count("id", draft.id)); + } + + @Test + @TestSecurity(user = "user_1") + public void deleteOtherPost() { + given().delete(post.id.toString()).then().statusCode(403); + assertEquals(1, Post.count("id", post.id)); + } + + @Test + @TestSecurity(user = "user_1") + public void deleteOtherDraft() { + given().delete(draft.id.toString()).then().statusCode(404); + assertEquals(1, Post.count("id", draft.id)); + } + + @Test + public void deletePostUnauthenticated() { + given().delete(post.id.toString()).then().statusCode(401); + assertEquals(1, Post.count("id", post.id)); + } + + @Test + public void deleteDraftUnauthenticated() { + given().delete(draft.id.toString()).then().statusCode(401); + assertEquals(1, Post.count("id", draft.id)); + } + + @ParameterizedTest + @ValueSource(strings = {"fake", "00000000-0000-0000-0000-000000000000"}) + @TestSecurity(user = "user_0") + public void deleteNonExistent(final String id) { + given().delete(id).then().statusCode(404); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListFeedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListFeedTests.java new file mode 100644 index 0000000..974adbb --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListFeedTests.java @@ -0,0 +1,85 @@ +package app.fyreplace.api.testing.endpoints.posts; + +import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; +import static java.util.stream.IntStream.range; +import static org.hamcrest.CoreMatchers.equalTo; + +import app.fyreplace.api.data.Post; +import app.fyreplace.api.data.User; +import app.fyreplace.api.data.Vote; +import app.fyreplace.api.data.dev.DataSeeder; +import app.fyreplace.api.endpoints.PostsEndpoint; +import app.fyreplace.api.testing.endpoints.PostTestsBase; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(PostsEndpoint.class) +public final class ListFeedTests extends PostTestsBase { + @Inject + DataSeeder dataSeeder; + + @Test + @TestSecurity(user = "user_0") + public void listFeedWithOtherPosts() { + final var user = requireNonNull(User.findByUsername("user_1")); + range(0, 5).forEach(i -> dataSeeder.createPost(user, "Post " + i, true, false)); + final var response = given().get("feed").then().statusCode(200).body("size()", equalTo(3)); + range(0, 3).forEach(i -> response.body("[" + i + "].author.id", equalTo(user.id.toString()))); + } + + @Test + @TestSecurity(user = "user_0") + public void listFeedWithOtherDrafts() { + final var user = requireNonNull(User.findByUsername("user_1")); + range(0, 5).forEach(i -> dataSeeder.createPost(user, "Post " + i, false, false)); + given().get("feed").then().statusCode(200).body("size()", equalTo(0)); + } + + @Test + @TestSecurity(user = "user_0") + public void listFeedWithOwnPosts() { + final var user = requireNonNull(User.findByUsername("user_0")); + range(0, 5).forEach(i -> dataSeeder.createPost(user, "Post " + i, true, false)); + given().get("feed").then().statusCode(200).body("size()", equalTo(0)); + } + + @Test + @TestSecurity(user = "user_0") + public void listFeedWithOwnDrafts() { + final var user = requireNonNull(User.findByUsername("user_0")); + range(0, 5).forEach(i -> dataSeeder.createPost(user, "Post " + i, false, false)); + given().get("feed").then().statusCode(200).body("size()", equalTo(0)); + } + + @Test + @TestSecurity(user = "user_0") + public void listFeedWithAlreadyVotedPosts() { + final var user = requireNonNull(User.findByUsername("user_0")); + final var otherUser = requireNonNull(User.findByUsername("user_1")); + QuarkusTransaction.requiringNew().run(() -> range(0, 5).forEach(i -> { + final var post = dataSeeder.createPost(otherUser, "Post " + i, true, false); + final var vote = new Vote(); + vote.user = user; + vote.post = post; + vote.persist(); + })); + + given().get("feed").then().statusCode(200).body("size()", equalTo(0)); + } + + @BeforeEach + @Transactional + @Override + public void beforeEach() { + super.beforeEach(); + Post.deleteAll(); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java new file mode 100644 index 0000000..e1ae1c4 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java @@ -0,0 +1,107 @@ +package app.fyreplace.api.testing.endpoints.posts; + +import static io.restassured.RestAssured.given; +import static java.util.stream.IntStream.range; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.in; + +import app.fyreplace.api.data.Post; +import app.fyreplace.api.data.User; +import app.fyreplace.api.data.dev.DataSeeder; +import app.fyreplace.api.endpoints.PostsEndpoint; +import app.fyreplace.api.endpoints.PostsEndpoint.PostListingType; +import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import java.util.ArrayList; +import java.util.List; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(PostsEndpoint.class) +public final class ListTests extends TransactionalTests { + @ConfigProperty(name = "app.paging.size") + int pagingSize; + + @Inject + DataSeeder dataSeeder; + + private final List subscribedToPostIds = new ArrayList<>(); + + @Test + @TestSecurity(user = "user_0") + public void listSubscribedTo() { + QuarkusTransaction.requiringNew().run(this::makeSubscribedToPosts); + final var response = given().queryParam("page", 0) + .queryParam("ascending", false) + .queryParam("type", PostListingType.SUBSCRIBED_TO) + .get() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("size()", equalTo(pagingSize)); + + range(0, pagingSize).forEach(i -> response.body("[" + i + "].id", in(subscribedToPostIds))); + } + + @Test + @TestSecurity(user = "user_0") + public void listPublished() { + final var publishedPostIds = Post.stream( + "author = ?1 and datePublished is not null", User.findByUsername("user_0")) + .map(post -> post.id.toString()) + .toList(); + final var response = given().queryParam("page", 0) + .queryParam("ascending", false) + .queryParam("type", PostListingType.PUBLISHED) + .get() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("size()", equalTo(pagingSize)); + + range(0, pagingSize).forEach(i -> response.body("[" + i + "].id", in(publishedPostIds))); + } + + @Test + @TestSecurity(user = "user_0") + public void listDrafts() { + final var draftIds = Post.stream("author = ?1 and datePublished is null", User.findByUsername("user_0")) + .map(post -> post.id.toString()) + .toList(); + final var response = given().queryParam("page", 0) + .queryParam("ascending", false) + .queryParam("type", PostListingType.DRAFTS) + .get() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("size()", equalTo(pagingSize)); + + range(0, pagingSize).forEach(i -> response.body("[" + i + "].id", in(draftIds))); + } + + @Transactional + public void makeSubscribedToPosts() { + final var user0 = User.findByUsername("user_0"); + final var user1 = User.findByUsername("user_1"); + final var user2 = User.findByUsername("user_2"); + + range(0, 20).forEach(i -> dataSeeder.createPost(user1, "Post " + i, true, false)); + range(0, 20).forEach(i -> dataSeeder.createPost(user2, "Post " + i, true, false)); + + subscribedToPostIds.clear(); + + Post.stream("author", user1).forEach(post -> { + user0.subscribeTo(post); + subscribedToPostIds.add(post.id.toString()); + }); + Post.stream("author", user0).forEach(post -> subscribedToPostIds.add(post.id.toString())); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/PublishTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/PublishTests.java new file mode 100644 index 0000000..b0d08af --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/PublishTests.java @@ -0,0 +1,117 @@ +package app.fyreplace.api.testing.endpoints.posts; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import app.fyreplace.api.data.Chapter; +import app.fyreplace.api.data.Post; +import app.fyreplace.api.data.PostPublication; +import app.fyreplace.api.endpoints.PostsEndpoint; +import app.fyreplace.api.testing.endpoints.PostTestsBase; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@QuarkusTest +@TestHTTPEndpoint(PostsEndpoint.class) +public final class PublishTests extends PostTestsBase { + @Test + @TestSecurity(user = "user_0") + public void publishOwnPost() { + given().contentType(ContentType.JSON) + .body(new PostPublication(false)) + .post(post.id + "/publish") + .then() + .statusCode(403); + } + + @Test + @TestSecurity(user = "user_0") + public void publishOwnDraft() { + given().contentType(ContentType.JSON) + .body(new PostPublication(true)) + .post(draft.id + "/publish") + .then() + .statusCode(200); + assertEquals(0, Post.count("id = ?1 and datePublished is null and anonymous = false", draft.id)); + } + + @Test + @TestSecurity(user = "user_0") + public void publishAnonymouslyOwnDraft() { + given().contentType(ContentType.JSON) + .body(new PostPublication(false)) + .post(draft.id + "/publish") + .then() + .statusCode(200); + assertEquals(0, Post.count("id = ?1 and datePublished is null and anonymous = true", draft.id)); + } + + @Test + @TestSecurity(user = "user_0") + public void publishOwnDraftWithoutChapters() { + QuarkusTransaction.requiringNew().run(() -> Chapter.delete("post", draft)); + given().contentType(ContentType.JSON) + .body(new PostPublication(false)) + .post(draft.id + "/publish") + .then() + .statusCode(403); + assertEquals(1, Post.count("id = ?1 and datePublished is null", draft.id)); + } + + @Test + @TestSecurity(user = "user_1") + public void publishOtherPost() { + given().contentType(ContentType.JSON) + .body(new PostPublication(false)) + .post(post.id + "/publish") + .then() + .statusCode(403); + } + + @Test + @TestSecurity(user = "user_1") + public void publishOtherDraft() { + given().contentType(ContentType.JSON) + .body(new PostPublication(false)) + .post(draft.id + "/publish") + .then() + .statusCode(404); + assertEquals(1, Post.count("id = ?1 and datePublished is null", draft.id)); + } + + @Test + public void publishPostUnauthenticated() { + given().contentType(ContentType.JSON) + .body(new PostPublication(false)) + .post(post.id + "/publish") + .then() + .statusCode(401); + } + + @Test + public void publishDraftUnauthenticated() { + given().contentType(ContentType.JSON) + .body(new PostPublication(false)) + .post(draft.id + "/publish") + .then() + .statusCode(401); + assertEquals(1, Post.count("id = ?1 and datePublished is null", draft.id)); + } + + @ParameterizedTest + @ValueSource(strings = {"fake", "00000000-0000-0000-0000-000000000000"}) + @TestSecurity(user = "user_0") + public void publishNonExistent(final String id) { + given().contentType(ContentType.JSON) + .body(new PostPublication(false)) + .post(id + "/publish") + .then() + .statusCode(404); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java new file mode 100644 index 0000000..519ffc2 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java @@ -0,0 +1,144 @@ +package app.fyreplace.api.testing.endpoints.posts; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; + +import app.fyreplace.api.data.Comment; +import app.fyreplace.api.data.Post; +import app.fyreplace.api.data.Vote; +import app.fyreplace.api.data.dev.DataSeeder; +import app.fyreplace.api.endpoints.PostsEndpoint; +import app.fyreplace.api.testing.endpoints.PostTestsBase; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import jakarta.inject.Inject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@QuarkusTest +@TestHTTPEndpoint(PostsEndpoint.class) +public final class RetrieveTests extends PostTestsBase { + @Inject + DataSeeder dataSeeder; + + private Post anonymousPost; + + @Test + @TestSecurity(user = "user_0") + public void retrieveOwnPost() { + given().get(post.id.toString()) + .then() + .statusCode(200) + .body("id", equalTo(post.id.toString())) + .body("dateCreated", equalTo(post.datePublished.toString())) + .body("author.username", equalTo("user_0")) + .body("anonymous", equalTo(false)) + .body("chapters.size()", equalTo(post.getChapters().size())) + .body("commentCount", equalTo((int) Comment.count("post", post))) + .body("voteCount", equalTo((int) Vote.count("post", post))); + } + + @Test + @TestSecurity(user = "user_0") + public void retrieveOwnAnonymousPost() { + given().get(anonymousPost.id.toString()) + .then() + .statusCode(200) + .body("id", equalTo(anonymousPost.id.toString())) + .body("dateCreated", equalTo(anonymousPost.datePublished.toString())) + .body("author.username", equalTo("user_0")) + .body("anonymous", equalTo(true)) + .body("chapters.size()", equalTo(anonymousPost.getChapters().size())); + } + + @Test + @TestSecurity(user = "user_0") + public void retrieveOwnDraft() { + given().get(draft.id.toString()) + .then() + .statusCode(200) + .body("id", equalTo(draft.id.toString())) + .body("dateCreated", equalTo(draft.dateCreated.toString())) + .body("author.username", equalTo("user_0")) + .body("anonymous", equalTo(draft.anonymous)) + .body("chapters.size()", equalTo(draft.getChapters().size())); + } + + @Test + @TestSecurity(user = "user_1") + public void retrieveOtherPost() { + given().get(post.id.toString()) + .then() + .statusCode(200) + .body("id", equalTo(post.id.toString())) + .body("dateCreated", equalTo(post.datePublished.toString())) + .body("author.username", equalTo("user_0")) + .body("anonymous", equalTo(false)) + .body("chapters.size()", equalTo(post.getChapters().size())); + } + + @Test + @TestSecurity(user = "user_1") + public void retrieveOtherAnonymousPost() { + given().get(anonymousPost.id.toString()) + .then() + .statusCode(200) + .body("id", equalTo(anonymousPost.id.toString())) + .body("dateCreated", equalTo(anonymousPost.datePublished.toString())) + .body("author", nullValue()) + .body("anonymous", equalTo(true)) + .body("chapters.size()", equalTo(anonymousPost.getChapters().size())); + } + + @Test + @TestSecurity(user = "user_1") + public void retrieveOtherDraft() { + given().get(draft.id.toString()).then().statusCode(404); + } + + @Test + public void retrievePostUnauthenticated() { + given().get(post.id.toString()) + .then() + .statusCode(200) + .body("id", equalTo(post.id.toString())) + .body("dateCreated", equalTo(post.datePublished.toString())) + .body("author.username", equalTo("user_0")) + .body("anonymous", equalTo(false)) + .body("chapters.size()", equalTo(post.getChapters().size())); + } + + @Test + public void retrieveAnonymousPostUnauthenticated() { + given().get(anonymousPost.id.toString()) + .then() + .statusCode(200) + .body("id", equalTo(anonymousPost.id.toString())) + .body("dateCreated", equalTo(anonymousPost.datePublished.toString())) + .body("author", nullValue()) + .body("anonymous", equalTo(true)) + .body("chapters.size()", equalTo(anonymousPost.getChapters().size())); + } + + @Test + public void retrieveDraftUnauthenticated() { + given().get(draft.id.toString()).then().statusCode(404); + } + + @ParameterizedTest + @ValueSource(strings = {"fake", "00000000-0000-0000-0000-000000000000"}) + public void retrieveNonExistent(final String id) { + given().get(id).then().statusCode(404); + } + + @BeforeEach + @Override + public void beforeEach() { + super.beforeEach(); + anonymousPost = dataSeeder.createPost(post.author, "Anonymous Post", true, true); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/SubscribeTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/SubscribeTests.java new file mode 100644 index 0000000..d7ccede --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/SubscribeTests.java @@ -0,0 +1,103 @@ +package app.fyreplace.api.testing.endpoints.posts; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import app.fyreplace.api.data.Comment; +import app.fyreplace.api.data.Subscription; +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.PostsEndpoint; +import app.fyreplace.api.testing.endpoints.PostTestsBase; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@QuarkusTest +@TestHTTPEndpoint(PostsEndpoint.class) +public final class SubscribeTests extends PostTestsBase { + @Test + @TestSecurity(user = "user_1") + public void subscribeToOtherPost() { + final var user = User.findByUsername("user_1"); + assertEquals(0, Subscription.count("user = ?1 and post = ?2", user, post)); + given().put(post.id + "/isSubscribed").then().statusCode(200); + final var subscription = Subscription.find("user = ?1 and post = ?2", user, post) + .firstResult(); + assertNotNull(subscription); + final var comment = Comment.find("post", Comment.sorting().descending(), post) + .firstResult(); + assertEquals(comment.id, subscription.lastCommentSeen.id); + } + + @Test + @TestSecurity(user = "user_1") + public void subscribeToOtherPostTwice() { + final var user = User.findByUsername("user_1"); + given().put(post.id + "/isSubscribed").then().statusCode(200); + given().put(post.id + "/isSubscribed").then().statusCode(200); + final var subscription = Subscription.find("user = ?1 and post = ?2", user, post) + .firstResult(); + assertNotNull(subscription); + final var comment = Comment.find("post", Comment.sorting().descending(), post) + .firstResult(); + assertEquals(comment.id, subscription.lastCommentSeen.id); + } + + @Test + @TestSecurity(user = "user_0") + public void subscribeToOwnPost() { + final var user = User.findByUsername("user_0"); + assertEquals(1, Subscription.count("user = ?1 and post = ?2", user, post)); + given().put(post.id + "/isSubscribed").then().statusCode(200); + final var subscription = Subscription.find("user = ?1 and post = ?2", user, post) + .firstResult(); + assertNotNull(subscription); + assertNull(subscription.lastCommentSeen); + } + + @Test + @TestSecurity(user = "user_1") + public void subscribeToOtherDraft() { + final var user = User.findByUsername("user_1"); + assertEquals(0, Subscription.count("user = ?1 and post = ?2", user, draft)); + given().put(draft.id + "/isSubscribed").then().statusCode(404); + assertEquals(0, Subscription.count("user = ?1 and post = ?2", user, draft)); + } + + @Test + @TestSecurity(user = "user_0") + public void subscribeToOwnDraft() { + final var user = User.findByUsername("user_0"); + assertEquals(0, Subscription.count("user = ?1 and post = ?2", user, draft)); + given().put(draft.id + "/isSubscribed").then().statusCode(403); + assertEquals(0, Subscription.count("user = ?1 and post = ?2", user, draft)); + } + + @Test + public void subscribeToPostUnauthenticated() { + final var subscriptionCount = Subscription.count(); + given().put(post.id + "/isSubscribed").then().statusCode(401); + assertEquals(subscriptionCount, Subscription.count()); + } + + @Test + public void subscribeToDraftUnauthenticated() { + final var subscriptionCount = Subscription.count(); + given().put(draft.id + "/isSubscribed").then().statusCode(401); + assertEquals(subscriptionCount, Subscription.count()); + } + + @ParameterizedTest + @ValueSource(strings = {"fake", "00000000-0000-0000-0000-000000000000"}) + @TestSecurity(user = "user_0") + public void subscribeToNonExistent(final String id) { + final var subscriptionCount = Subscription.count(); + given().put(id + "/isSubscribed").then().statusCode(404); + assertEquals(subscriptionCount, Subscription.count()); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/UnsubscribeTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/UnsubscribeTests.java new file mode 100644 index 0000000..6f7106b --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/UnsubscribeTests.java @@ -0,0 +1,97 @@ +package app.fyreplace.api.testing.endpoints.posts; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import app.fyreplace.api.data.Subscription; +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.PostsEndpoint; +import app.fyreplace.api.testing.endpoints.PostTestsBase; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@QuarkusTest +@TestHTTPEndpoint(PostsEndpoint.class) +public final class UnsubscribeTests extends PostTestsBase { + @Test + @TestSecurity(user = "user_1") + public void unsubscribeFromOtherPost() { + final var user = User.findByUsername("user_1"); + assertEquals(1, Subscription.count("user = ?1 and post = ?2", user, post)); + given().delete(post.id + "/isSubscribed").then().statusCode(204); + assertEquals(0, Subscription.count("user = ?1 and post = ?2", user, post)); + } + + @Test + @TestSecurity(user = "user_1") + public void unsubscribeFromOtherPostTwice() { + final var user = User.findByUsername("user_1"); + given().delete(post.id + "/isSubscribed").then().statusCode(204); + given().delete(post.id + "/isSubscribed").then().statusCode(204); + assertEquals(0, Subscription.count("user = ?1 and post = ?2", user, post)); + } + + @Test + @TestSecurity(user = "user_0") + public void unsubscribeFromOwnPost() { + final var user = User.findByUsername("user_0"); + assertEquals(1, Subscription.count("user = ?1 and post = ?2", user, post)); + given().delete(post.id + "/isSubscribed").then().statusCode(204); + assertEquals(0, Subscription.count("user = ?1 and post = ?2", user, post)); + } + + @Test + @TestSecurity(user = "user_1") + public void unsubscribeFromOtherDraft() { + final var user = User.findByUsername("user_1"); + assertEquals(0, Subscription.count("user = ?1 and post = ?2", user, draft)); + given().delete(draft.id + "/isSubscribed").then().statusCode(404); + assertEquals(0, Subscription.count("user = ?1 and post = ?2", user, draft)); + } + + @Test + @TestSecurity(user = "user_0") + public void unsubscribeFromOwnDraft() { + final var user = User.findByUsername("user_0"); + assertEquals(0, Subscription.count("user = ?1 and post = ?2", user, draft)); + given().delete(draft.id + "/isSubscribed").then().statusCode(403); + assertEquals(0, Subscription.count("user = ?1 and post = ?2", user, draft)); + } + + @Test + public void unsubscribeFromPostUnauthenticated() { + final var subscriptionCount = Subscription.count(); + given().delete(post.id + "/isSubscribed").then().statusCode(401); + assertEquals(subscriptionCount, Subscription.count()); + } + + @Test + public void unsubscribeFromDraftUnauthenticated() { + final var subscriptionCount = Subscription.count(); + given().delete(draft.id + "/isSubscribed").then().statusCode(401); + assertEquals(subscriptionCount, Subscription.count()); + } + + @ParameterizedTest + @ValueSource(strings = {"fake", "00000000-0000-0000-0000-000000000000"}) + @TestSecurity(user = "user_0") + public void unsubscribeFromNonExistent(final String id) { + final var subscriptionCount = Subscription.count(); + given().delete(id + "/isSubscribed").then().statusCode(404); + assertEquals(subscriptionCount, Subscription.count()); + } + + @BeforeEach + @Transactional + @Override + public void beforeEach() { + super.beforeEach(); + User.findByUsername("user_1").subscribeTo(post); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/VoteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/VoteTests.java new file mode 100644 index 0000000..c92ec4e --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/VoteTests.java @@ -0,0 +1,160 @@ +package app.fyreplace.api.testing.endpoints.posts; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import app.fyreplace.api.data.Post; +import app.fyreplace.api.data.Vote; +import app.fyreplace.api.data.VoteCreation; +import app.fyreplace.api.endpoints.PostsEndpoint; +import app.fyreplace.api.testing.endpoints.PostTestsBase; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import jakarta.transaction.Transactional; +import java.time.Duration; +import java.time.Instant; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(PostsEndpoint.class) +public final class VoteTests extends PostTestsBase { + @Test + @TestSecurity(user = "user_1") + @Transactional + public void voteWithSpread() { + final var voteCount = Vote.count(); + final var postLife = post.life; + given().contentType(ContentType.JSON) + .body(new VoteCreation(true)) + .post(post.id + "/vote") + .then() + .statusCode(200); + assertEquals(voteCount + 1, Vote.count()); + final var vote = + Vote.find("post = ?1 and user.username = 'user_1'", post).firstResult(); + assertTrue(vote.isSpread); + assertEquals(postLife + 1, vote.post.life); + } + + @Test + @TestSecurity(user = "user_1") + @Transactional + public void voteWithoutSpread() { + final var voteCount = Vote.count(); + final var postLife = post.life; + given().contentType(ContentType.JSON) + .body(new VoteCreation(false)) + .post(post.id + "/vote") + .then() + .statusCode(200); + assertEquals(voteCount + 1, Vote.count()); + final var vote = + Vote.find("post = ?1 and user.username = 'user_1'", post).firstResult(); + assertFalse(vote.isSpread); + assertEquals(postLife - 1, vote.post.life); + } + + @Test + @TestSecurity(user = "user_1") + public void voteOnOldPost() { + QuarkusTransaction.requiringNew() + .run(() -> Post.update( + "datePublished = ?1 where id = ?2", + Instant.now().minus(Post.shelfLife.plus(Duration.ofDays(1))), + post.id)); + final var voteCount = Vote.count(); + final var postLife = post.life; + given().contentType(ContentType.JSON) + .body(new VoteCreation(false)) + .post(post.id + "/vote") + .then() + .statusCode(403); + assertEquals(voteCount, Vote.count()); + assertEquals(1, Post.count("id = ?1 and life = ?2", post.id, postLife)); + } + + @Test + @TestSecurity(user = "user_1") + public void voteOnOldDraft() { + QuarkusTransaction.requiringNew() + .run(() -> Post.update( + "dateCreated = ?1 where id = ?2", + Instant.now().minus(Post.shelfLife.plus(Duration.ofDays(1))), + draft.id)); + final var voteCount = Vote.count(); + final var postLife = draft.life; + given().contentType(ContentType.JSON) + .body(new VoteCreation(false)) + .post(draft.id + "/vote") + .then() + .statusCode(404); + assertEquals(voteCount, Vote.count()); + assertEquals(1, Post.count("id = ?1 and life = ?2", draft.id, postLife)); + } + + @Test + @TestSecurity(user = "user_0") + public void voteOnOwnPost() { + final var voteCount = Vote.count(); + final var postLife = post.life; + given().contentType(ContentType.JSON) + .body(new VoteCreation(false)) + .post(post.id + "/vote") + .then() + .statusCode(403); + assertEquals(voteCount, Vote.count()); + assertEquals(1, Post.count("id = ?1 and life = ?2", post.id, postLife)); + } + + @Test + @TestSecurity(user = "user_0") + public void voteOnOwnDraft() { + final var voteCount = Vote.count(); + final var postLife = draft.life; + given().contentType(ContentType.JSON) + .body(new VoteCreation(false)) + .post(draft.id + "/vote") + .then() + .statusCode(403); + assertEquals(voteCount, Vote.count()); + assertEquals(1, Post.count("id = ?1 and life = ?2", draft.id, postLife)); + } + + @Test + @TestSecurity(user = "user_1") + public void voteOnNonExistentPost() { + final var voteCount = Vote.count(); + given().contentType(ContentType.JSON) + .body(new VoteCreation(false)) + .post(fakeId + "/vote") + .then() + .statusCode(404); + assertEquals(voteCount, Vote.count()); + } + + @Test + @TestSecurity(user = "user_1") + public void voteWithEmptyInput() { + final var voteCount = Vote.count(); + final var postLife = post.life; + given().contentType(ContentType.JSON).post(post.id + "/vote").then().statusCode(400); + assertEquals(voteCount, Vote.count()); + assertEquals(1, Post.count("id = ?1 and life = ?2", post.id, postLife)); + } + + @Test + public void voteUnauthenticated() { + final var voteCount = Vote.count(); + given().contentType(ContentType.JSON) + .body(new VoteCreation(false)) + .post(fakeId + "/vote") + .then() + .statusCode(401); + assertEquals(voteCount, Vote.count()); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java index 337cc60..2e7bf4b 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java @@ -47,7 +47,7 @@ public void createWithUsername() { @Transactional public void createWithNewUsername() { final var randomCodeCount = RandomCode.count(); - assertFalse(newUserRandomCode.email.isVerified); + assertFalse(newUserRandomCode.email.verified); given().contentType(ContentType.JSON) .body(new TokenCreation(newUserRandomCode.email.user.username, newUserRandomCode.code)) .post() @@ -57,7 +57,7 @@ public void createWithNewUsername() { .body(isA(String.class)); assertEquals(randomCodeCount - 1, RandomCode.count()); final var email = Email.find("id", newUserRandomCode.email.id).firstResult(); - assertTrue(email.isVerified); + assertTrue(email.verified); } @Test @@ -76,7 +76,7 @@ public void createWithEmail() { @Test public void createWithNewEmail() { final var randomCodeCount = RandomCode.count(); - assertFalse(newUserRandomCode.email.isVerified); + assertFalse(newUserRandomCode.email.verified); given().contentType(ContentType.JSON) .body(new TokenCreation(newUserRandomCode.email.email, newUserRandomCode.code)) .post() @@ -86,7 +86,7 @@ public void createWithNewEmail() { .body(isA(String.class)); assertEquals(randomCodeCount - 1, RandomCode.count()); final var email = Email.find("id", newUserRandomCode.email.id).firstResult(); - assertTrue(email.isVerified); + assertTrue(email.verified); } @Test diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/BanTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java similarity index 76% rename from src/test/java/app/fyreplace/api/testing/endpoints/users/BanTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java index 5d12a41..e2ab0c4 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/BanTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java @@ -17,15 +17,15 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class BanTests extends TransactionalTests { +public final class BannedTests extends TransactionalTests { @Test @TestSecurity(user = "user_0", roles = "ADMINISTRATOR") @Transactional public void banWithAdministrator() { final var user = User.findByUsername("user_1"); - given().put(user.id + "/isBanned").then().statusCode(200); + given().put(user.id + "/banned").then().statusCode(200); user.refresh(); - assertTrue(user.isBanned); + assertTrue(user.banned); assertEquals(User.BanCount.ONCE, user.banCount); } @@ -34,9 +34,9 @@ public void banWithAdministrator() { @Transactional public void banWithModerator() { final var user = User.findByUsername("user_1"); - given().put(user.id + "/isBanned").then().statusCode(200); + given().put(user.id + "/banned").then().statusCode(200); user.refresh(); - assertTrue(user.isBanned); + assertTrue(user.banned); assertEquals(User.BanCount.ONCE, user.banCount); } @@ -45,9 +45,9 @@ public void banWithModerator() { @Transactional public void banWithUser() { final var user = User.findByUsername("user_1"); - given().put(user.id + "/isBanned").then().statusCode(403); + given().put(user.id + "/banned").then().statusCode(403); user.refresh(); - assertFalse(user.isBanned); + assertFalse(user.banned); assertEquals(User.BanCount.NEVER, user.banCount); } @@ -55,9 +55,9 @@ public void banWithUser() { @Transactional public void banUnauthenticated() { final var user = User.findByUsername("user_1"); - given().put(user.id + "/isBanned").then().statusCode(401); + given().put(user.id + "/banned").then().statusCode(401); user.refresh(); - assertFalse(user.isBanned); + assertFalse(user.banned); assertEquals(User.BanCount.NEVER, user.banCount); } @@ -66,9 +66,9 @@ public void banUnauthenticated() { @Transactional public void banTwiceWithAdministrator() { final var user = User.findByUsername("user_2"); - given().put(user.id + "/isBanned").then().statusCode(200); + given().put(user.id + "/banned").then().statusCode(200); user.refresh(); - assertTrue(user.isBanned); + assertTrue(user.banned); assertEquals(User.BanCount.ONE_TOO_MANY, user.banCount); } @@ -77,9 +77,9 @@ public void banTwiceWithAdministrator() { @Transactional public void banTwiceWithModerator() { final var user = User.findByUsername("user_2"); - given().put(user.id + "/isBanned").then().statusCode(200); + given().put(user.id + "/banned").then().statusCode(200); user.refresh(); - assertTrue(user.isBanned); + assertTrue(user.banned); assertEquals(User.BanCount.ONE_TOO_MANY, user.banCount); } @@ -88,9 +88,9 @@ public void banTwiceWithModerator() { @Transactional public void banTwiceWithUser() { final var user = User.findByUsername("user_2"); - given().put(user.id + "/isBanned").then().statusCode(403); + given().put(user.id + "/banned").then().statusCode(403); user.refresh(); - assertFalse(user.isBanned); + assertFalse(user.banned); assertEquals(User.BanCount.ONCE, user.banCount); } @@ -98,9 +98,9 @@ public void banTwiceWithUser() { @Transactional public void banTwiceUnauthenticated() { final var user = User.findByUsername("user_2"); - given().put(user.id + "/isBanned").then().statusCode(401); + given().put(user.id + "/banned").then().statusCode(401); user.refresh(); - assertFalse(user.isBanned); + assertFalse(user.banned); assertEquals(User.BanCount.ONCE, user.banCount); } @@ -109,9 +109,9 @@ public void banTwiceUnauthenticated() { @Transactional public void banAlreadyBanned() { final var user = User.findByUsername("user_3"); - given().put(user.id + "/isBanned").then().statusCode(200); + given().put(user.id + "/banned").then().statusCode(200); user.refresh(); - assertTrue(user.isBanned); + assertTrue(user.banned); assertEquals(User.BanCount.ONCE, user.banCount); } @@ -121,12 +121,12 @@ public void banAlreadyBanned() { public void beforeEach() { super.beforeEach(); var user = User.findByUsername("user_2"); - user.isBanned = false; + user.banned = false; user.banCount = User.BanCount.ONCE; user.persist(); user = User.findByUsername("user_3"); - user.isBanned = true; + user.banned = true; user.banCount = User.BanCount.ONCE; user.persist(); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java index 43c554e..eb5f1b8 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java @@ -36,16 +36,16 @@ public void create() { .body("dateCreated", notNullValue()) .body("username", equalTo("new_user")) .body("rank", equalTo(User.Rank.CITIZEN.name())) - .body("isBanned", equalTo(false)) + .body("banned", equalTo(false)) .body("avatar", nullValue()) .body("bio", equalTo("")) - .body("isBanned", equalTo(false)); + .body("banned", equalTo(false)); assertEquals(userCount + 1, User.count()); assertEquals(emailCount + 1, Email.count()); final var user = User.findByUsername("new_user"); assertNotNull(user); assertEquals("new@example.org", user.mainEmail.email); - assertFalse(user.mainEmail.isVerified); + assertFalse(user.mainEmail.verified); final var mails = getMailsSentTo(user.mainEmail); assertSingleEmail(UserActivationEmail.class, mails); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java index e169aaf..e02d2d8 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java @@ -31,7 +31,7 @@ public void retrieveMe() { .body("rank", equalTo(User.Rank.CITIZEN.name())) .body("avatar", nullValue()) .body("bio", equalTo("")) - .body("isBanned", equalTo(false)); + .body("banned", equalTo(false)); } @Test diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java index 66bf603..fb5c475 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java @@ -18,7 +18,7 @@ @TestHTTPEndpoint(UsersEndpoint.class) public final class RetrieveTests extends TransactionalTests { @ParameterizedTest - @ValueSource(strings = {"user_0", "user_12", "user_50"}) + @ValueSource(strings = {"user_0", "user_12"}) public void retrieve(final String username) { final var user = User.findByUsername(username); given().get(user.id.toString()) @@ -31,11 +31,11 @@ public void retrieve(final String username) { .body("rank", equalTo(User.Rank.CITIZEN.name())) .body("avatar", nullValue()) .body("bio", equalTo("")) - .body("isBanned", equalTo(false)); + .body("banned", equalTo(false)); } @ParameterizedTest - @ValueSource(strings = {"nope", "fake", "@", "admin"}) + @ValueSource(strings = {"nope", "fake", "@", "admin", "00000000-0000-0000-0000-000000000000"}) public void retrieveNonExistent(final String userId) { given().get(userId).then().statusCode(404); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java index 322e9c8..35f90a1 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java @@ -9,43 +9,26 @@ import app.fyreplace.api.data.StoredFile; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.ImageTests; import io.quarkus.test.common.http.TestHTTPEndpoint; -import io.quarkus.test.common.http.TestHTTPResource; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; import io.restassured.http.ContentType; import java.io.IOException; -import java.net.URL; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class UpdateMeAvatarTests extends TransactionalTests { - @TestHTTPResource("image.jpeg") - URL jpeg; - - @TestHTTPResource("image.png") - URL png; - - @TestHTTPResource("image.webp") - URL webp; - - @TestHTTPResource("image.gif") - URL gif; - - @TestHTTPResource("image.txt") - URL text; - +public final class UpdateMeAvatarTests extends ImageTests { @ParameterizedTest @ValueSource(strings = {"jpeg", "png", "webp"}) @TestSecurity(user = "user_0") public void updateMeAvatar(final String fileType) throws IOException { final var remoteFileCount = StoredFile.count(); - try (final var stream = getUrl(fileType).openStream()) { + try (final var stream = openStream(fileType)) { given().contentType(ContentType.BINARY) .body(stream.readAllBytes()) .put("me/avatar") @@ -66,7 +49,7 @@ public void updateMeAvatar(final String fileType) throws IOException { public void updateMeAvatarWithInvalidType(final String fileType) throws IOException { final var remoteFileCount = StoredFile.count(); - try (final var stream = getUrl(fileType).openStream()) { + try (final var stream = openStream(fileType)) { given().contentType(ContentType.BINARY) .body(stream.readAllBytes()) .put("me/avatar") @@ -88,15 +71,4 @@ public void updateMeAvatarWithoutInput() { final var user = User.findByUsername("user_0"); assertNull(user.avatar); } - - private URL getUrl(final String fileType) { - return switch (fileType) { - case "jpeg" -> jpeg; - case "png" -> png; - case "webp" -> webp; - case "gif" -> gif; - case "text" -> text; - default -> null; - }; - } } diff --git a/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldInactiveUsersTests.java b/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldInactiveUsersTests.java index a5b0a6b..b5763df 100644 --- a/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldInactiveUsersTests.java +++ b/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldInactiveUsersTests.java @@ -21,8 +21,8 @@ public final class RemoveOldInactiveUsersTests extends TransactionalTests { @Transactional public void removeOldInactiveUsers() { final var totalUserCount = User.count(); - final var inactiveUserCount = User.count("isActive = false"); - User.update("dateCreated = ?1 where isActive = false", Instant.now().minus(Duration.ofDays(2))); + final var inactiveUserCount = User.count("active = false"); + User.update("dateCreated = ?1 where active = false", Instant.now().minus(Duration.ofDays(2))); cleanupTasks.removeOldInactiveUsers(); assertEquals(totalUserCount - inactiveUserCount, User.count()); } From 85e597541e43780bd7ba9bc6b51c6cf40b039bc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 25 Aug 2023 16:17:27 +0200 Subject: [PATCH 052/157] Let OpenAPI infer types --- .../java/app/fyreplace/api/endpoints/PostsEndpoint.java | 4 +--- .../java/app/fyreplace/api/endpoints/UsersEndpoint.java | 8 ++------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java index 8cf2837..b01f106 100644 --- a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java @@ -100,9 +100,7 @@ public Response create() { @GET @Path("{id}") - @APIResponse( - responseCode = "200", - content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Post.class))) + @APIResponse(responseCode = "200") @APIResponse(responseCode = "404") public Post retrieve(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context, null, false); diff --git a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java index bd646d8..40217ce 100644 --- a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java @@ -95,9 +95,7 @@ public Response create(@Valid @NotNull final UserCreation input) { @GET @Path("{id}") - @APIResponse( - responseCode = "200", - content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = User.class))) + @APIResponse(responseCode = "200") @APIResponse(responseCode = "404") public User retrieve(@PathParam("id") final UUID id) { final var user = User.findById(id); @@ -184,9 +182,7 @@ public Response updateBanned(@PathParam("id") final UUID id) { @GET @Path("me") @Authenticated - @APIResponse( - responseCode = "200", - content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = User.class))) + @APIResponse(responseCode = "200") @APIResponse(responseCode = "401") public User retrieveMe() { return retrieve(User.getFromSecurityContext(context).id); From c060f7cc3a63130323e66420322570f3d407ce37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 25 Aug 2023 18:01:42 +0200 Subject: [PATCH 053/157] Code cleanup --- .../fyreplace/api/data/EmailActivation.java | 2 +- .../app/fyreplace/api/data/EmailCreation.java | 2 +- .../fyreplace/api/data/NewTokenCreation.java | 2 +- .../app/fyreplace/api/data/StoredFile.java | 16 +++++++++-- .../java/app/fyreplace/api/data/User.java | 28 ++++++++----------- .../api/endpoints/ChaptersEndpoint.java | 10 ------- .../api/endpoints/DevUsersEndpoint.java | 10 ++++++- .../api/endpoints/EmailsEndpoint.java | 10 ------- .../api/endpoints/PostsEndpoint.java | 7 ----- .../api/endpoints/TokensEndpoint.java | 2 +- .../api/endpoints/UsersEndpoint.java | 19 ++----------- .../api/exceptions/ConflictException.java | 2 +- .../UnsupportedMediaTypeException.java | 2 +- .../fyreplace/api/services/LocaleService.java | 6 ++-- .../storage/local/LocalStorageService.java | 5 +++- .../services/storage/s3/S3StorageService.java | 1 + .../api/testing/TransactionalTests.java | 6 ++-- .../chapters/CreateChapterTests.java | 1 - .../chapters/UpdateChapterImageTests.java | 2 +- .../endpoints/emails/ActivateTests.java | 3 +- .../testing/endpoints/emails/ListTests.java | 14 ++++------ .../endpoints/emails/SetMainTests.java | 3 +- .../endpoints/posts/UnsubscribeTests.java | 3 +- .../endpoints/tokens/CreateNewTests.java | 9 +++--- .../testing/endpoints/tokens/CreateTests.java | 3 +- .../testing/endpoints/users/BannedTests.java | 23 +++++++-------- .../endpoints/users/CreateBlockTests.java | 3 +- .../endpoints/users/DeleteBlockTests.java | 9 +++--- .../endpoints/users/DeleteMeAvatarTests.java | 2 +- .../endpoints/users/ListBlockedTests.java | 6 ++-- .../endpoints/users/RetrieveMeTests.java | 3 +- .../endpoints/users/RetrieveTests.java | 3 +- .../endpoints/users/UpdateMeBioTests.java | 5 ++-- .../cleanup/RemoveOldRandomCodesTests.java | 3 +- 34 files changed, 106 insertions(+), 119 deletions(-) diff --git a/src/main/java/app/fyreplace/api/data/EmailActivation.java b/src/main/java/app/fyreplace/api/data/EmailActivation.java index 37f949e..9b9efe7 100644 --- a/src/main/java/app/fyreplace/api/data/EmailActivation.java +++ b/src/main/java/app/fyreplace/api/data/EmailActivation.java @@ -2,4 +2,4 @@ import jakarta.validation.constraints.NotBlank; -public final record EmailActivation(@NotBlank String email, @NotBlank String code) {} +public record EmailActivation(@NotBlank String email, @NotBlank String code) {} diff --git a/src/main/java/app/fyreplace/api/data/EmailCreation.java b/src/main/java/app/fyreplace/api/data/EmailCreation.java index 39a4bcc..39f46c2 100644 --- a/src/main/java/app/fyreplace/api/data/EmailCreation.java +++ b/src/main/java/app/fyreplace/api/data/EmailCreation.java @@ -4,4 +4,4 @@ import jakarta.validation.constraints.NotBlank; import org.hibernate.validator.constraints.Length; -public final record EmailCreation(@Length(min = 3, max = 254) @NotBlank @Email String email) {} +public record EmailCreation(@Length(min = 3, max = 254) @NotBlank @Email String email) {} diff --git a/src/main/java/app/fyreplace/api/data/NewTokenCreation.java b/src/main/java/app/fyreplace/api/data/NewTokenCreation.java index e2bc529..f153da3 100644 --- a/src/main/java/app/fyreplace/api/data/NewTokenCreation.java +++ b/src/main/java/app/fyreplace/api/data/NewTokenCreation.java @@ -2,4 +2,4 @@ import jakarta.validation.constraints.NotBlank; -public final record NewTokenCreation(@NotBlank String identifier) {} +public record NewTokenCreation(@NotBlank String identifier) {} diff --git a/src/main/java/app/fyreplace/api/data/StoredFile.java b/src/main/java/app/fyreplace/api/data/StoredFile.java index 771269f..8dbe25d 100644 --- a/src/main/java/app/fyreplace/api/data/StoredFile.java +++ b/src/main/java/app/fyreplace/api/data/StoredFile.java @@ -18,8 +18,7 @@ @Table(name = "remote_files") public class StoredFile extends EntityBase { @Transient - final StorageService storageService = - Arc.container().instance(StorageService.class).get(); + private StorageService storageService; @Column(unique = true, nullable = false) public String path; @@ -28,13 +27,16 @@ public class StoredFile extends EntityBase { @Nullable private byte[] data; + @SuppressWarnings("unused") public StoredFile() { data = null; + initStorageService(); } - public StoredFile(final String path, final byte[] data) { + public StoredFile(final String path, @Nullable final byte[] data) { this.path = path; this.data = data; + initStorageService(); } @Override @@ -48,6 +50,7 @@ public void store(final byte[] data) throws IOException { } } + @SuppressWarnings("unused") @PostPersist final void postPersist() throws IOException { if (data != null) { @@ -56,11 +59,18 @@ final void postPersist() throws IOException { } } + @SuppressWarnings("unused") @PreRemove final void preDestroy() throws IOException { storageService.remove(path); } + private void initStorageService() { + try (final var service = Arc.container().instance(StorageService.class)) { + storageService = service.get(); + } + } + public static final class Serializer extends JsonSerializer { @Override public void serialize( diff --git a/src/main/java/app/fyreplace/api/data/User.java b/src/main/java/app/fyreplace/api/data/User.java index c33b333..7fa75e1 100644 --- a/src/main/java/app/fyreplace/api/data/User.java +++ b/src/main/java/app/fyreplace/api/data/User.java @@ -5,8 +5,6 @@ import io.quarkus.panache.common.Sort; import jakarta.annotation.Nullable; import jakarta.persistence.*; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import jakarta.ws.rs.NotAuthorizedException; import jakarta.ws.rs.core.SecurityContext; import java.time.Instant; @@ -22,7 +20,7 @@ @Entity @Table(name = "users") public class User extends TimestampedEntityBase { - public static final Set forbiddenUsernames = new HashSet(Arrays.asList( + public static final Set forbiddenUsernames = new HashSet<>(Arrays.asList( "admin", "admins", "administrator", @@ -111,6 +109,7 @@ public Profile getProfile() { return new Profile(id, username, avatar != null ? avatar.toString() : null); } + @SuppressWarnings("unused") @PostRemove final void postRemove() { if (avatar != null) { @@ -118,12 +117,11 @@ final void postRemove() { } } - public Subscription subscribeTo(final Post post) { - final var existing = Subscription.find("user = ?1 and post = ?2", this, post) - .firstResult(); + public void subscribeTo(final Post post) { + final var existing = Subscription.count("user = ?1 and post = ?2", this, post); - if (existing != null) { - return existing; + if (existing > 0) { + return; } final var subscription = new Subscription(); @@ -132,7 +130,6 @@ public Subscription subscribeTo(final Post post) { subscription.lastCommentSeen = Comment.find("post", Sort.descending("dateCreated"), post).firstResult(); subscription.persist(); - return subscription; } public void unsubscribeFrom(final Post post) { @@ -148,12 +145,11 @@ public void unsubscribeFrom(final Post post) { return User.find("username", username).withLock(lock).firstResult(); } - public static @Nullable User getFromSecurityContext(final SecurityContext context) { - return getFromSecurityContext(context, null, true); + public static User getFromSecurityContext(final SecurityContext context) { + return getFromSecurityContext(context, null); } - public static @Nullable User getFromSecurityContext( - final SecurityContext context, @Nullable final LockModeType lock) { + public static User getFromSecurityContext(final SecurityContext context, @Nullable final LockModeType lock) { return getFromSecurityContext(context, lock, true); } @@ -172,14 +168,14 @@ public void unsubscribeFrom(final Post post) { public enum Rank { CITIZEN, MODERATOR, - ADMINISTRATOR; + ADMINISTRATOR } public enum BanCount { NEVER, ONCE, - ONE_TOO_MANY; + ONE_TOO_MANY } - public static final record Profile(@NotNull UUID id, @NotBlank String username, String avatar) {} + public record Profile(UUID id, String username, String avatar) {} } diff --git a/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java index ea69703..2f49eff 100644 --- a/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java @@ -56,8 +56,6 @@ public final class ChaptersEndpoint { responseCode = "201", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Chapter.class))) - @APIResponse(responseCode = "401") - @APIResponse(responseCode = "403") @APIResponse(responseCode = "404") public Response createChapter(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context); @@ -82,8 +80,6 @@ public Response createChapter(@PathParam("id") final UUID id) { @Authenticated @Transactional @APIResponse(responseCode = "204") - @APIResponse(responseCode = "401") - @APIResponse(responseCode = "403") @APIResponse(responseCode = "404") public void deleteChapter(@PathParam("id") final UUID id, @PathParam("position") final int position) { final var user = User.getFromSecurityContext(context); @@ -101,8 +97,6 @@ public void deleteChapter(@PathParam("id") final UUID id, @PathParam("position") responseCode = "200", content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = Integer.class))) @APIResponse(responseCode = "400") - @APIResponse(responseCode = "401") - @APIResponse(responseCode = "403") @APIResponse(responseCode = "404") public int updateChapterPosition( @PathParam("id") final UUID id, @PathParam("position") final int position, @NotNull final Integer input) { @@ -138,8 +132,6 @@ public int updateChapterPosition( responseCode = "200", content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) @APIResponse(responseCode = "400") - @APIResponse(responseCode = "401") - @APIResponse(responseCode = "403") @APIResponse(responseCode = "404") public String updateChapterText( @PathParam("id") final UUID id, @@ -168,8 +160,6 @@ public String updateChapterText( responseCode = "200", content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) @APIResponse(responseCode = "400") - @APIResponse(responseCode = "401") - @APIResponse(responseCode = "403") @APIResponse(responseCode = "404") public String updateChapterImage( @PathParam("id") final UUID id, @PathParam("position") final int position, final File input) diff --git a/src/main/java/app/fyreplace/api/endpoints/DevUsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/DevUsersEndpoint.java index d5e6169..1ce01e2 100644 --- a/src/main/java/app/fyreplace/api/endpoints/DevUsersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/DevUsersEndpoint.java @@ -4,6 +4,7 @@ import app.fyreplace.api.services.JwtService; import jakarta.inject.Inject; import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.core.MediaType; @@ -21,7 +22,14 @@ public final class DevUsersEndpoint { @APIResponse( responseCode = "200", content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) + @APIResponse(responseCode = "404") public String retrieveToken(@PathParam("username") final String username) { - return jwtService.makeJwt(User.findByUsername(username)); + final var user = User.findByUsername(username); + + if (user == null) { + throw new NotFoundException(); + } + + return jwtService.makeJwt(user); } } diff --git a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java index 67227b0..ccc491e 100644 --- a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java @@ -15,7 +15,6 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.PositiveOrZero; -import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; import jakarta.ws.rs.NotFoundException; @@ -48,8 +47,6 @@ public final class EmailsEndpoint { @GET @Authenticated - @APIResponse(responseCode = "200") - @APIResponse(responseCode = "401") public List list(@QueryParam("page") @PositiveOrZero final int page) { final var user = User.getFromSecurityContext(context); return Email.find("user", Sort.by("email"), user).page(page, pagingSize).list(); @@ -58,12 +55,9 @@ public List list(@QueryParam("page") @PositiveOrZero final int page) { @POST @Authenticated @Transactional - @Consumes(MediaType.APPLICATION_JSON) @APIResponse( responseCode = "201", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Email.class))) - @APIResponse(responseCode = "400") - @APIResponse(responseCode = "401") @APIResponse(responseCode = "409") public Response create(@Valid @NotNull final EmailCreation input) { if (Email.count("email", input.email()) > 0) { @@ -83,8 +77,6 @@ public Response create(@Valid @NotNull final EmailCreation input) { @Authenticated @Transactional @APIResponse(responseCode = "204") - @APIResponse(responseCode = "401") - @APIResponse(responseCode = "403") @APIResponse(responseCode = "404") public void delete(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context); @@ -104,7 +96,6 @@ public void delete(@PathParam("id") final UUID id) { @Authenticated @Transactional @APIResponse(responseCode = "200") - @APIResponse(responseCode = "403") @APIResponse(responseCode = "404") public Response setMain(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context); @@ -135,7 +126,6 @@ public long count() { @Transactional @APIResponse(responseCode = "200") @APIResponse(responseCode = "400") - @APIResponse(responseCode = "401") @APIResponse(responseCode = "404") public Response activate(@NotNull @Valid final EmailActivation input) { var email = Email.find("email", input.email()).firstResult(); diff --git a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java index b01f106..d98e12d 100644 --- a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java @@ -88,7 +88,6 @@ public Iterable list( responseCode = "201", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Post.class))) @APIResponse(responseCode = "400") - @APIResponse(responseCode = "401") public Response create() { final var user = User.getFromSecurityContext(context); final var post = new Post(); @@ -114,8 +113,6 @@ public Post retrieve(@PathParam("id") final UUID id) { @Authenticated @Transactional @APIResponse(responseCode = "204") - @APIResponse(responseCode = "401") - @APIResponse(responseCode = "403") @APIResponse(responseCode = "404") public void delete(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context); @@ -129,8 +126,6 @@ public void delete(@PathParam("id") final UUID id) { @Authenticated @Transactional @APIResponse(responseCode = "200") - @APIResponse(responseCode = "401") - @APIResponse(responseCode = "403") @APIResponse(responseCode = "404") public Response publish(@PathParam("id") final UUID id, @Valid @NotNull final PostPublication input) { final var user = User.getFromSecurityContext(context); @@ -150,7 +145,6 @@ public Response publish(@PathParam("id") final UUID id, @Valid @NotNull final Po @Authenticated @Transactional @APIResponse(responseCode = "200") - @APIResponse(responseCode = "401") @APIResponse(responseCode = "404") public Response createSubscription(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context); @@ -165,7 +159,6 @@ public Response createSubscription(@PathParam("id") final UUID id) { @Authenticated @Transactional @APIResponse(responseCode = "204") - @APIResponse(responseCode = "401") @APIResponse(responseCode = "404") public void deleteSubscription(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context); diff --git a/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java index 4bb174b..7345c4b 100644 --- a/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java @@ -64,7 +64,7 @@ public Response create(@Valid @NotNull final TokenCreation input) { @APIResponse( responseCode = "200", content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) - @APIResponse(responseCode = "401") + @APIResponse(responseCode = "404") public String retrieveNew() { return jwtService.makeJwt(User.getFromSecurityContext(context)); } diff --git a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java index 40217ce..08eb399 100644 --- a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java @@ -38,9 +38,7 @@ import java.nio.file.Files; import java.time.Duration; import java.time.Instant; -import java.util.List; import java.util.UUID; -import java.util.stream.Collectors; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.openapi.annotations.media.Content; import org.eclipse.microprofile.openapi.annotations.media.Schema; @@ -112,8 +110,6 @@ public User retrieve(@PathParam("id") final UUID id) { @Authenticated @Transactional @APIResponse(responseCode = "200") - @APIResponse(responseCode = "401") - @APIResponse(responseCode = "403") @APIResponse(responseCode = "404") public Response createBlock(@PathParam("id") final UUID id) { final var source = User.getFromSecurityContext(context); @@ -138,7 +134,6 @@ public Response createBlock(@PathParam("id") final UUID id) { @Authenticated @Transactional @APIResponse(responseCode = "204") - @APIResponse(responseCode = "401") @APIResponse(responseCode = "404") public void deleteBlock(@PathParam("id") final UUID id) { final var source = User.getFromSecurityContext(context); @@ -156,8 +151,6 @@ public void deleteBlock(@PathParam("id") final UUID id) { @RolesAllowed({"ADMINISTRATOR", "MODERATOR"}) @Transactional @APIResponse(responseCode = "200") - @APIResponse(responseCode = "401") - @APIResponse(responseCode = "403") @APIResponse(responseCode = "404") public Response updateBanned(@PathParam("id") final UUID id) { final var user = User.findById(id, LockModeType.PESSIMISTIC_WRITE); @@ -183,7 +176,6 @@ public Response updateBanned(@PathParam("id") final UUID id) { @Path("me") @Authenticated @APIResponse(responseCode = "200") - @APIResponse(responseCode = "401") public User retrieveMe() { return retrieve(User.getFromSecurityContext(context).id); } @@ -197,7 +189,6 @@ public User retrieveMe() { responseCode = "200", content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) @APIResponse(responseCode = "400") - @APIResponse(responseCode = "401") public String updateMeBio(@NotNull @Length(max = 3000) final String input) { final var user = User.getFromSecurityContext(context, LockModeType.PESSIMISTIC_READ); user.bio = input; @@ -213,7 +204,6 @@ public String updateMeBio(@NotNull @Length(max = 3000) final String input) { @APIResponse( responseCode = "200", content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) - @APIResponse(responseCode = "401") @APIResponse(responseCode = "413") @APIResponse(responseCode = "415") public String updateMeAvatar(final File input) throws IOException { @@ -236,7 +226,6 @@ public String updateMeAvatar(final File input) throws IOException { @Authenticated @Transactional @APIResponse(responseCode = "204") - @APIResponse(responseCode = "401") public void deleteMeAvatar() { final var user = User.getFromSecurityContext(context, LockModeType.PESSIMISTIC_WRITE); @@ -252,22 +241,20 @@ public void deleteMeAvatar() { @Authenticated @Transactional @APIResponse(responseCode = "204") - @APIResponse(responseCode = "401") public void deleteMe() { - User.delete("username", context.getUserPrincipal().getName()); + User.getFromSecurityContext(context).delete(); } @GET @Path("blocked") @Authenticated @APIResponse(responseCode = "200") - @APIResponse(responseCode = "401") - public List listBlocked(@QueryParam("page") @PositiveOrZero final int page) { + public Iterable listBlocked(@QueryParam("page") @PositiveOrZero final int page) { return Block.find("source", Sort.by("id"), User.getFromSecurityContext(context)) .page(page, pagingSize) .stream() .map(block -> block.target.getProfile()) - .collect(Collectors.toList()); + .toList(); } @GET diff --git a/src/main/java/app/fyreplace/api/exceptions/ConflictException.java b/src/main/java/app/fyreplace/api/exceptions/ConflictException.java index 246cd06..b4cd713 100644 --- a/src/main/java/app/fyreplace/api/exceptions/ConflictException.java +++ b/src/main/java/app/fyreplace/api/exceptions/ConflictException.java @@ -4,7 +4,7 @@ import jakarta.ws.rs.core.Response; public final class ConflictException extends ClientErrorException implements ExplainableException { - private String explanationValue; + private final String explanationValue; public ConflictException(final String explanationValue) { super(Response.Status.CONFLICT); diff --git a/src/main/java/app/fyreplace/api/exceptions/UnsupportedMediaTypeException.java b/src/main/java/app/fyreplace/api/exceptions/UnsupportedMediaTypeException.java index e835051..1481652 100644 --- a/src/main/java/app/fyreplace/api/exceptions/UnsupportedMediaTypeException.java +++ b/src/main/java/app/fyreplace/api/exceptions/UnsupportedMediaTypeException.java @@ -4,7 +4,7 @@ import jakarta.ws.rs.core.Response; public final class UnsupportedMediaTypeException extends ClientErrorException implements ExplainableException { - private String explanationValue; + private final String explanationValue; public UnsupportedMediaTypeException(final String explanationValue) { super(Response.Status.UNSUPPORTED_MEDIA_TYPE); diff --git a/src/main/java/app/fyreplace/api/services/LocaleService.java b/src/main/java/app/fyreplace/api/services/LocaleService.java index 4befee4..53f48e3 100644 --- a/src/main/java/app/fyreplace/api/services/LocaleService.java +++ b/src/main/java/app/fyreplace/api/services/LocaleService.java @@ -6,6 +6,7 @@ import jakarta.ws.rs.core.HttpHeaders; import java.util.Locale; import java.util.MissingResourceException; +import java.util.Objects; import java.util.ResourceBundle; @Dependent @@ -17,13 +18,12 @@ public ResourceBundle getResourceBundle(final String name) { final var path = "i18n." + name; return headers.getAcceptableLanguages().stream() .map(locale -> getResourceBundleOrNull(path, locale)) - .filter(bundle -> bundle != null) + .filter(Objects::nonNull) .findFirst() .orElse(ResourceBundle.getBundle(path)); } - @Nullable - private ResourceBundle getResourceBundleOrNull(final String name, final Locale locale) { + private @Nullable ResourceBundle getResourceBundleOrNull(final String name, final Locale locale) { try { return ResourceBundle.getBundle(name, locale); } catch (final MissingResourceException e) { diff --git a/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java b/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java index 5d85708..8795cb2 100644 --- a/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java +++ b/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java @@ -11,6 +11,7 @@ import java.net.URI; import java.nio.file.Paths; +@SuppressWarnings("unused") @ApplicationScoped @Unremovable @IfBuildProperty(name = "app.storage.type", stringValue = "local") @@ -18,6 +19,7 @@ public final class LocalStorageService implements StorageService { @Inject LocalStorageConfig config; + @SuppressWarnings("ResultOfMethodCallIgnored") @Override public void store(final String path, final byte[] data) throws IOException { final var file = getFile(path); @@ -28,8 +30,9 @@ public void store(final String path, final byte[] data) throws IOException { } } + @SuppressWarnings("ResultOfMethodCallIgnored") @Override - public void remove(final String path) throws IOException { + public void remove(final String path) { getFile(path).delete(); } diff --git a/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java b/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java index da44f3c..9618995 100644 --- a/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java +++ b/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java @@ -11,6 +11,7 @@ import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; +@SuppressWarnings("unused") @ApplicationScoped @Unremovable @IfBuildProperty(name = "app.storage.type", stringValue = "s3") diff --git a/src/test/java/app/fyreplace/api/testing/TransactionalTests.java b/src/test/java/app/fyreplace/api/testing/TransactionalTests.java index b0ff8c0..43bee3a 100644 --- a/src/test/java/app/fyreplace/api/testing/TransactionalTests.java +++ b/src/test/java/app/fyreplace/api/testing/TransactionalTests.java @@ -13,7 +13,7 @@ @QuarkusTestResource(DatabaseTestResource.class) public abstract class TransactionalTests { @Inject - DataSeeder seeder; + DataSeeder dataSeeder; @Inject MockMailbox mailbox; @@ -22,12 +22,12 @@ public abstract class TransactionalTests { @BeforeEach public void beforeEach() { - seeder.insertData(); + dataSeeder.insertData(); } @AfterEach public void afterEach() { - seeder.deleteData(); + dataSeeder.deleteData(); mailbox.clear(); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/CreateChapterTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/CreateChapterTests.java index 22d6aa4..b42c557 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/CreateChapterTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/CreateChapterTests.java @@ -62,7 +62,6 @@ public void createChapterInOwnDraftOverMaximum() { final var chapter = new Chapter(); chapter.post = draft; chapter.position = Chapter.positionBetween(before, null); - ; chapter.persist(); before = chapter.position; } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterImageTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterImageTests.java index 4af8b8d..59a52ba 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterImageTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterImageTests.java @@ -92,7 +92,7 @@ public void updateChapterImageInOwnDraftWithInvalidType(final String fileType) t @Test @TestSecurity(user = "user_0") - public void updateChapterImageInOwnDraftWithoutInput() throws IOException { + public void updateChapterImageInOwnDraftWithoutInput() { final var position = 0; given().pathParam("id", draft.id).put(position + "/image").then().statusCode(415); final var chapter = draft.getChapters().get(position); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java index 68bf185..e411fd4 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java @@ -1,6 +1,7 @@ package app.fyreplace.api.testing.endpoints.emails; import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertEquals; import app.fyreplace.api.data.Email; @@ -54,7 +55,7 @@ public void activateWithInvalidEmail() { @Test @TestSecurity(user = "user_0") public void activateWithOtherEmail() { - final var otherUser = User.findByUsername("user_1"); + final var otherUser = requireNonNull(User.findByUsername("user_1")); given().contentType(ContentType.JSON) .body(new EmailActivation(otherUser.mainEmail.email, randomCode.code)) .post("activate") diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java index 10610cd..909bd2a 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java @@ -1,6 +1,7 @@ package app.fyreplace.api.testing.endpoints.emails; import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; import static java.util.stream.IntStream.range; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.in; @@ -35,12 +36,9 @@ public void list() { .contentType(ContentType.JSON) .body("size()", equalTo(pagingSize)); - range(0, pagingSize) - .forEach(i -> response.body( - "[" + i + "].email", - in(Email.stream("user", user) - .map(email -> email.email) - .toList()))); + final var emails = + Email.stream("user", user).map(email -> email.email).toList(); + range(0, pagingSize).forEach(i -> response.body("[" + i + "].email", in(emails))); } @Test @@ -65,8 +63,8 @@ public void listTooFar() { @Override public void beforeEach() { super.beforeEach(); - final var user = User.findByUsername("user_0"); - range(0, 100).forEach(i -> { + final var user = requireNonNull(User.findByUsername("user_0")); + range(0, 30).forEach(i -> { final var email = new Email(); email.user = user; email.email = user.username + "_" + i + "@example.com"; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java index 4eb45d9..c55a0a0 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java @@ -1,6 +1,7 @@ package app.fyreplace.api.testing.endpoints.emails; import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -52,7 +53,7 @@ public void setMainWithUnverifiedEmail() { @Test @TestSecurity(user = "user_0") public void setMainWithOtherEmail() { - final var otherUser = User.findByUsername("user_1"); + final var otherUser = requireNonNull(User.findByUsername("user_1")); given().post(otherUser.mainEmail.id + "/main").then().statusCode(404); secondaryEmail = Email.findById(secondaryEmail.id); assertFalse(secondaryEmail.isMain()); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/UnsubscribeTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/UnsubscribeTests.java index 6f7106b..52bbfff 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/UnsubscribeTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/UnsubscribeTests.java @@ -1,6 +1,7 @@ package app.fyreplace.api.testing.endpoints.posts; import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertEquals; import app.fyreplace.api.data.Subscription; @@ -92,6 +93,6 @@ public void unsubscribeFromNonExistent(final String id) { @Override public void beforeEach() { super.beforeEach(); - User.findByUsername("user_1").subscribeTo(post); + requireNonNull(User.findByUsername("user_1")).subscribeTo(post); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTests.java index 02a6034..20d8ccd 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTests.java @@ -2,6 +2,7 @@ import static app.fyreplace.api.testing.Assertions.assertSingleEmail; import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertEquals; import app.fyreplace.api.data.NewTokenCreation; @@ -19,7 +20,7 @@ public final class CreateNewTests extends TransactionalTests { @Test public void createNewWithUsername() { - final var user = User.findByUsername("user_0"); + final var user = requireNonNull(User.findByUsername("user_0")); given().contentType(ContentType.JSON) .body(new NewTokenCreation(user.username)) .post("new") @@ -30,7 +31,7 @@ public void createNewWithUsername() { @Test public void createNewWithEmail() { - final var user = User.findByUsername("user_0"); + final var user = requireNonNull(User.findByUsername("user_0")); given().contentType(ContentType.JSON) .body(new NewTokenCreation(user.mainEmail.email)) .post("new") @@ -41,7 +42,7 @@ public void createNewWithEmail() { @Test public void createNewWithInvalidIdentifier() { - final var user = User.findByUsername("user_0"); + final var user = requireNonNull(User.findByUsername("user_0")); given().contentType(ContentType.JSON) .body(new NewTokenCreation("invalid")) .post("new") @@ -52,7 +53,7 @@ public void createNewWithInvalidIdentifier() { @Test public void createNewWithEmptyInput() { - final var user = User.findByUsername("user_0"); + final var user = requireNonNull(User.findByUsername("user_0")); given().contentType(ContentType.JSON).post("new").then().statusCode(400); assertEquals(0, getMailsSentTo(user.mainEmail).size()); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java index 2e7bf4b..80a1581 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java @@ -1,6 +1,7 @@ package app.fyreplace.api.testing.endpoints.tokens; import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; import static org.hamcrest.Matchers.isA; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -140,7 +141,7 @@ public void beforeEach() { private RandomCode makeRandomCode(final String username) { final var code = new RandomCode(); - code.email = User.findByUsername(username).mainEmail; + code.email = requireNonNull(User.findByUsername(username)).mainEmail; code.code = randomService.generateCode(); code.persist(); return code; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java index e2ab0c4..8764df4 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java @@ -1,6 +1,7 @@ package app.fyreplace.api.testing.endpoints.users; import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -22,7 +23,7 @@ public final class BannedTests extends TransactionalTests { @TestSecurity(user = "user_0", roles = "ADMINISTRATOR") @Transactional public void banWithAdministrator() { - final var user = User.findByUsername("user_1"); + final var user = requireNonNull(User.findByUsername("user_1")); given().put(user.id + "/banned").then().statusCode(200); user.refresh(); assertTrue(user.banned); @@ -33,7 +34,7 @@ public void banWithAdministrator() { @TestSecurity(user = "user_0", roles = "MODERATOR") @Transactional public void banWithModerator() { - final var user = User.findByUsername("user_1"); + final var user = requireNonNull(User.findByUsername("user_1")); given().put(user.id + "/banned").then().statusCode(200); user.refresh(); assertTrue(user.banned); @@ -44,7 +45,7 @@ public void banWithModerator() { @TestSecurity(user = "user_0") @Transactional public void banWithUser() { - final var user = User.findByUsername("user_1"); + final var user = requireNonNull(User.findByUsername("user_1")); given().put(user.id + "/banned").then().statusCode(403); user.refresh(); assertFalse(user.banned); @@ -54,7 +55,7 @@ public void banWithUser() { @Test @Transactional public void banUnauthenticated() { - final var user = User.findByUsername("user_1"); + final var user = requireNonNull(User.findByUsername("user_1")); given().put(user.id + "/banned").then().statusCode(401); user.refresh(); assertFalse(user.banned); @@ -65,7 +66,7 @@ public void banUnauthenticated() { @TestSecurity(user = "user_0", roles = "ADMINISTRATOR") @Transactional public void banTwiceWithAdministrator() { - final var user = User.findByUsername("user_2"); + final var user = requireNonNull(User.findByUsername("user_2")); given().put(user.id + "/banned").then().statusCode(200); user.refresh(); assertTrue(user.banned); @@ -76,7 +77,7 @@ public void banTwiceWithAdministrator() { @TestSecurity(user = "user_0", roles = "MODERATOR") @Transactional public void banTwiceWithModerator() { - final var user = User.findByUsername("user_2"); + final var user = requireNonNull(User.findByUsername("user_2")); given().put(user.id + "/banned").then().statusCode(200); user.refresh(); assertTrue(user.banned); @@ -87,7 +88,7 @@ public void banTwiceWithModerator() { @TestSecurity(user = "user_0") @Transactional public void banTwiceWithUser() { - final var user = User.findByUsername("user_2"); + final var user = requireNonNull(User.findByUsername("user_2")); given().put(user.id + "/banned").then().statusCode(403); user.refresh(); assertFalse(user.banned); @@ -97,7 +98,7 @@ public void banTwiceWithUser() { @Test @Transactional public void banTwiceUnauthenticated() { - final var user = User.findByUsername("user_2"); + final var user = requireNonNull(User.findByUsername("user_2")); given().put(user.id + "/banned").then().statusCode(401); user.refresh(); assertFalse(user.banned); @@ -108,7 +109,7 @@ public void banTwiceUnauthenticated() { @TestSecurity(user = "user_0", roles = "ADMINISTRATOR") @Transactional public void banAlreadyBanned() { - final var user = User.findByUsername("user_3"); + final var user = requireNonNull(User.findByUsername("user_3")); given().put(user.id + "/banned").then().statusCode(200); user.refresh(); assertTrue(user.banned); @@ -120,12 +121,12 @@ public void banAlreadyBanned() { @Override public void beforeEach() { super.beforeEach(); - var user = User.findByUsername("user_2"); + var user = requireNonNull(User.findByUsername("user_2")); user.banned = false; user.banCount = User.BanCount.ONCE; user.persist(); - user = User.findByUsername("user_3"); + user = requireNonNull(User.findByUsername("user_3")); user.banned = true; user.banCount = User.BanCount.ONCE; user.persist(); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java index d9ae9b1..0d6b794 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java @@ -1,6 +1,7 @@ package app.fyreplace.api.testing.endpoints.users; import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertEquals; import app.fyreplace.api.data.Block; @@ -48,7 +49,7 @@ public void createBlockWithInvalidUser() { @Test @TestSecurity(user = "user_0") public void createBlockWithSelf() { - final var user = User.findByUsername("user_0"); + final var user = requireNonNull(User.findByUsername("user_0")); given().put(user.id + "/blocked").then().statusCode(403); assertEquals(0, Block.count("source = ?1 and target = ?2", user, user)); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java index ee027ea..1722e21 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java @@ -1,6 +1,7 @@ package app.fyreplace.api.testing.endpoints.users; import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertEquals; import app.fyreplace.api.data.Block; @@ -20,8 +21,8 @@ public final class DeleteBlockTests extends TransactionalTests { @Test @TestSecurity(user = "user_0") public void deleteBlock() { - final var user = User.findByUsername("user_0"); - final var otherUser = User.findByUsername("user_1"); + final var user = requireNonNull(User.findByUsername("user_0")); + final var otherUser = requireNonNull(User.findByUsername("user_1")); given().delete(otherUser.id + "/blocked").then().statusCode(204); assertEquals(0, Block.count("source = ?1 and target = ?2", user, otherUser)); } @@ -29,8 +30,8 @@ public void deleteBlock() { @Test @TestSecurity(user = "user_0") public void deleteBlockTwice() { - final var user = User.findByUsername("user_0"); - final var otherUser = User.findByUsername("user_1"); + final var user = requireNonNull(User.findByUsername("user_0")); + final var otherUser = requireNonNull(User.findByUsername("user_1")); given().delete(otherUser.id + "/blocked").then().statusCode(204); given().delete(otherUser.id + "/blocked").then().statusCode(204); assertEquals(0, Block.count("source = ?1 and target = ?2", user, otherUser)); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeAvatarTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeAvatarTests.java index 73cda79..ce06a4d 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeAvatarTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeAvatarTests.java @@ -39,7 +39,7 @@ public void deleteMeAvatar() throws IOException { @Test @TestSecurity(user = "user_0") - public void deleteMeAvatarWithoutAvatar() throws IOException { + public void deleteMeAvatarWithoutAvatar() { final var remoteFileCount = StoredFile.count(); given().delete("me/avatar").then().statusCode(204); assertEquals(remoteFileCount, StoredFile.count()); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java index 0a154de..05df6c0 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java @@ -66,12 +66,12 @@ public void listBlockedTooFar() { public void beforeEach() { super.beforeEach(); final var user = User.findByUsername("user_0"); - range(10, 50).forEach(i -> { - final var otherUser = User.findByUsername("user_" + i); + + for (final var otherUser : User.list("username > 'user_10'")) { final var block = new Block(); block.source = user; block.target = otherUser; block.persist(); - }); + } } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java index e02d2d8..ffc349a 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java @@ -1,6 +1,7 @@ package app.fyreplace.api.testing.endpoints.users; import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; @@ -20,7 +21,7 @@ public final class RetrieveMeTests extends TransactionalTests { @Test @TestSecurity(user = "user_10") public void retrieveMe() { - final var user = User.findByUsername("user_10"); + final var user = requireNonNull(User.findByUsername("user_10")); given().get("/me") .then() .contentType(ContentType.JSON) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java index fb5c475..be32f56 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java @@ -1,6 +1,7 @@ package app.fyreplace.api.testing.endpoints.users; import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; @@ -20,7 +21,7 @@ public final class RetrieveTests extends TransactionalTests { @ParameterizedTest @ValueSource(strings = {"user_0", "user_12"}) public void retrieve(final String username) { - final var user = User.findByUsername(username); + final var user = requireNonNull(User.findByUsername(username)); given().get(user.id.toString()) .then() .contentType(ContentType.JSON) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeBioTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeBioTests.java index 4e92296..2338ebb 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeBioTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeBioTests.java @@ -1,6 +1,7 @@ package app.fyreplace.api.testing.endpoints.users; import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertEquals; import app.fyreplace.api.data.User; @@ -23,7 +24,7 @@ public final class UpdateMeBioTests extends TransactionalTests { @TestSecurity(user = "user_0") public void updateMeBio(final String bio) { given().contentType(ContentType.TEXT).body(bio).put("me/bio").then().statusCode(200); - final var user = User.findByUsername("user_0"); + final var user = requireNonNull(User.findByUsername("user_0")); assertEquals(bio, user.bio); } @@ -31,7 +32,7 @@ public void updateMeBio(final String bio) { @TestSecurity(user = "user_0") @Transactional public void updateMeBioWithBioTooLong() { - final var user = User.findByUsername("user_0"); + final var user = requireNonNull(User.findByUsername("user_0")); final var bio = user.bio; given().contentType(ContentType.TEXT) .body("a".repeat(3001)) diff --git a/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldRandomCodesTests.java b/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldRandomCodesTests.java index 88bb043..7585bec 100644 --- a/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldRandomCodesTests.java +++ b/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldRandomCodesTests.java @@ -1,5 +1,6 @@ package app.fyreplace.api.testing.tasks.cleanup; +import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertEquals; import app.fyreplace.api.data.RandomCode; @@ -26,7 +27,7 @@ public final class RemoveOldRandomCodesTests extends TransactionalTests { @Transactional public void removeOldRandomCodes() { final var randomCode = new RandomCode(); - randomCode.email = User.findByUsername("user_0").mainEmail; + randomCode.email = requireNonNull(User.findByUsername("user_0")).mainEmail; randomCode.code = randomService.generateCode(); randomCode.persist(); final var randomCodeCount = RandomCode.count(); From 107b703744e023bdddcc4dedc34a0fa470d8c9d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 1 Sep 2023 12:16:59 +0200 Subject: [PATCH 054/157] Eagerly fetch rows in many-to-many tables --- src/main/java/app/fyreplace/api/data/Block.java | 5 +++-- src/main/java/app/fyreplace/api/data/Subscription.java | 5 +++-- src/main/java/app/fyreplace/api/data/Vote.java | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/app/fyreplace/api/data/Block.java b/src/main/java/app/fyreplace/api/data/Block.java index 070562a..33be551 100644 --- a/src/main/java/app/fyreplace/api/data/Block.java +++ b/src/main/java/app/fyreplace/api/data/Block.java @@ -1,6 +1,7 @@ package app.fyreplace.api.data; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; @@ -10,11 +11,11 @@ @Entity @Table(name = "blocks", uniqueConstraints = @UniqueConstraint(columnNames = {"source_id", "target_id"})) public class Block extends EntityBase { - @ManyToOne(optional = false) + @ManyToOne(optional = false, fetch = FetchType.EAGER) @OnDelete(action = OnDeleteAction.CASCADE) public User source; - @ManyToOne(optional = false) + @ManyToOne(optional = false, fetch = FetchType.EAGER) @OnDelete(action = OnDeleteAction.CASCADE) public User target; } diff --git a/src/main/java/app/fyreplace/api/data/Subscription.java b/src/main/java/app/fyreplace/api/data/Subscription.java index 77bde59..c6c42e0 100644 --- a/src/main/java/app/fyreplace/api/data/Subscription.java +++ b/src/main/java/app/fyreplace/api/data/Subscription.java @@ -2,6 +2,7 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; @@ -14,11 +15,11 @@ @Entity @Table(name = "subscriptions", uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "post_id"})) public class Subscription extends EntityBase { - @ManyToOne(optional = false) + @ManyToOne(optional = false, fetch = FetchType.EAGER) @OnDelete(action = OnDeleteAction.CASCADE) public User user; - @ManyToOne(optional = false) + @ManyToOne(optional = false, fetch = FetchType.EAGER) @OnDelete(action = OnDeleteAction.CASCADE) public Post post; diff --git a/src/main/java/app/fyreplace/api/data/Vote.java b/src/main/java/app/fyreplace/api/data/Vote.java index 27c22eb..c04fd8d 100644 --- a/src/main/java/app/fyreplace/api/data/Vote.java +++ b/src/main/java/app/fyreplace/api/data/Vote.java @@ -2,6 +2,7 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.Index; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; @@ -15,11 +16,11 @@ uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "post_id"}), indexes = {@Index(columnList = "post_id")}) public class Vote extends EntityBase { - @ManyToOne(optional = false) + @ManyToOne(optional = false, fetch = FetchType.EAGER) @OnDelete(action = OnDeleteAction.CASCADE) public User user; - @ManyToOne(optional = false) + @ManyToOne(optional = false, fetch = FetchType.EAGER) @OnDelete(action = OnDeleteAction.CASCADE) public Post post; From f7b6dc435e953aa29d9627b20acb39e680b48d90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 1 Sep 2023 16:18:07 +0200 Subject: [PATCH 055/157] Always close streams --- .../api/endpoints/UsersEndpoint.java | 11 +-- .../testing/endpoints/emails/ListTests.java | 7 +- .../testing/endpoints/posts/ListTests.java | 68 +++++++++++-------- .../endpoints/users/ListBlockedTests.java | 10 ++- 4 files changed, 52 insertions(+), 44 deletions(-) diff --git a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java index 08eb399..b3ea105 100644 --- a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java @@ -250,11 +250,12 @@ public void deleteMe() { @Authenticated @APIResponse(responseCode = "200") public Iterable listBlocked(@QueryParam("page") @PositiveOrZero final int page) { - return Block.find("source", Sort.by("id"), User.getFromSecurityContext(context)) - .page(page, pagingSize) - .stream() - .map(block -> block.target.getProfile()) - .toList(); + final var user = User.getFromSecurityContext(context); + final var blocks = Block.find("source", Sort.by("id"), user); + + try (final var stream = blocks.page(page, pagingSize).stream()) { + return stream.map(block -> block.target.getProfile()).toList(); + } } @GET diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java index 909bd2a..98f0a3c 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java @@ -36,9 +36,10 @@ public void list() { .contentType(ContentType.JSON) .body("size()", equalTo(pagingSize)); - final var emails = - Email.stream("user", user).map(email -> email.email).toList(); - range(0, pagingSize).forEach(i -> response.body("[" + i + "].email", in(emails))); + try (final var stream = Email.stream("user", user)) { + final var emails = stream.map(email -> email.email).toList(); + range(0, pagingSize).forEach(i -> response.body("[" + i + "].email", in(emails))); + } } @Test diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java index e1ae1c4..4b17b08 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java @@ -53,38 +53,41 @@ public void listSubscribedTo() { @Test @TestSecurity(user = "user_0") public void listPublished() { - final var publishedPostIds = Post.stream( - "author = ?1 and datePublished is not null", User.findByUsername("user_0")) - .map(post -> post.id.toString()) - .toList(); - final var response = given().queryParam("page", 0) - .queryParam("ascending", false) - .queryParam("type", PostListingType.PUBLISHED) - .get() - .then() - .statusCode(200) - .contentType(ContentType.JSON) - .body("size()", equalTo(pagingSize)); + final var user = User.findByUsername("user_0"); - range(0, pagingSize).forEach(i -> response.body("[" + i + "].id", in(publishedPostIds))); + try (final var stream = Post.stream("author = ?1 and datePublished is not null", user)) { + final var publishedPostIds = stream.map(post -> post.id.toString()).toList(); + final var response = given().queryParam("page", 0) + .queryParam("ascending", false) + .queryParam("type", PostListingType.PUBLISHED) + .get() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("size()", equalTo(pagingSize)); + + range(0, pagingSize).forEach(i -> response.body("[" + i + "].id", in(publishedPostIds))); + } } @Test @TestSecurity(user = "user_0") public void listDrafts() { - final var draftIds = Post.stream("author = ?1 and datePublished is null", User.findByUsername("user_0")) - .map(post -> post.id.toString()) - .toList(); - final var response = given().queryParam("page", 0) - .queryParam("ascending", false) - .queryParam("type", PostListingType.DRAFTS) - .get() - .then() - .statusCode(200) - .contentType(ContentType.JSON) - .body("size()", equalTo(pagingSize)); + final var user = User.findByUsername("user_0"); - range(0, pagingSize).forEach(i -> response.body("[" + i + "].id", in(draftIds))); + try (final var stream = Post.stream("author = ?1 and datePublished is null", user)) { + final var draftIds = stream.map(post -> post.id.toString()).toList(); + final var response = given().queryParam("page", 0) + .queryParam("ascending", false) + .queryParam("type", PostListingType.DRAFTS) + .get() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("size()", equalTo(pagingSize)); + + range(0, pagingSize).forEach(i -> response.body("[" + i + "].id", in(draftIds))); + } } @Transactional @@ -98,10 +101,15 @@ public void makeSubscribedToPosts() { subscribedToPostIds.clear(); - Post.stream("author", user1).forEach(post -> { - user0.subscribeTo(post); - subscribedToPostIds.add(post.id.toString()); - }); - Post.stream("author", user0).forEach(post -> subscribedToPostIds.add(post.id.toString())); + try (final var stream = Post.stream("author", user1)) { + stream.forEach(post -> { + user0.subscribeTo(post); + subscribedToPostIds.add(post.id.toString()); + }); + } + + try (final var stream = Post.stream("author", user0)) { + stream.forEach(post -> subscribedToPostIds.add(post.id.toString())); + } } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java index 05df6c0..3d7d351 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java @@ -35,12 +35,10 @@ public void listBlocked() { .contentType(ContentType.JSON) .body("size()", equalTo(pagingSize)); - range(0, pagingSize) - .forEach(i -> response.body( - "[" + i + "].username", - in(Block.stream("source", user) - .map(block -> block.target.username) - .toList()))); + try (final var stream = Block.stream("source", user)) { + final var blocks = stream.map(block -> block.target.username).toList(); + range(0, pagingSize).forEach(i -> response.body("[" + i + "].username", in(blocks))); + } } @Test From 37fdb357c0d615f0847414532e330253088cc04e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 6 Oct 2023 11:52:05 +0200 Subject: [PATCH 056/157] Ensure random UUIDs --- src/main/java/app/fyreplace/api/data/EntityBase.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/app/fyreplace/api/data/EntityBase.java b/src/main/java/app/fyreplace/api/data/EntityBase.java index 4874c2d..6619988 100644 --- a/src/main/java/app/fyreplace/api/data/EntityBase.java +++ b/src/main/java/app/fyreplace/api/data/EntityBase.java @@ -5,11 +5,13 @@ import jakarta.persistence.Id; import jakarta.persistence.MappedSuperclass; import java.util.UUID; +import org.hibernate.annotations.UuidGenerator; @MappedSuperclass public abstract class EntityBase extends PanacheEntityBase { @Id @GeneratedValue + @UuidGenerator(style = UuidGenerator.Style.RANDOM) public UUID id; public void refresh() { From 550d3f90d3c2fabfa35e78cf4d63fa25e8c48436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 6 Oct 2023 11:55:03 +0200 Subject: [PATCH 057/157] Take user blocking into account --- .../java/app/fyreplace/api/data/Post.java | 2 + .../java/app/fyreplace/api/data/User.java | 28 +++- .../api/endpoints/CommentsEndpoint.java | 54 ++++---- .../api/endpoints/PostsEndpoint.java | 9 +- .../api/endpoints/UsersEndpoint.java | 9 +- .../api/testing/endpoints/PostTestsBase.java | 8 ++ .../endpoints/comments/CountTests.java | 32 +++-- .../endpoints/comments/CreateTests.java | 32 +++++ .../testing/endpoints/comments/ListTests.java | 23 ++++ .../posts/CreateSubscriptionTests.java | 127 ++++++++++++++++++ .../posts/DeleteSubscriptionTests.java | 124 +++++++++++++++++ .../endpoints/posts/ListFeedTests.java | 26 ++++ .../endpoints/posts/RetrieveTests.java | 39 +++--- .../endpoints/posts/SubscribeTests.java | 103 -------------- .../endpoints/posts/UnsubscribeTests.java | 98 -------------- .../testing/endpoints/posts/VoteTests.java | 31 +++++ .../endpoints/users/CountBlockedTests.java | 5 +- .../endpoints/users/CreateBlockTests.java | 25 ++-- .../endpoints/users/DeleteBlockTests.java | 17 +-- .../endpoints/users/ListBlockedTests.java | 5 +- 20 files changed, 508 insertions(+), 289 deletions(-) create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateSubscriptionTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteSubscriptionTests.java delete mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/posts/SubscribeTests.java delete mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/posts/UnsubscribeTests.java diff --git a/src/main/java/app/fyreplace/api/data/Post.java b/src/main/java/app/fyreplace/api/data/Post.java index 5924c82..976b043 100644 --- a/src/main/java/app/fyreplace/api/data/Post.java +++ b/src/main/java/app/fyreplace/api/data/Post.java @@ -86,6 +86,8 @@ public static void validateAccess( throw new ForbiddenException(postIsDraft ? "post_not_published" : "post_is_published"); } else if (mustBeAuthor && !post.author.id.equals(userId)) { throw new ForbiddenException("invalid_author"); + } else if (!post.anonymous && user != null && post.author.isBlocking(user)) { + throw new ForbiddenException("user_is_blocked"); } post.setCurrentUser(user); diff --git a/src/main/java/app/fyreplace/api/data/User.java b/src/main/java/app/fyreplace/api/data/User.java index 7fa75e1..d18dbcc 100644 --- a/src/main/java/app/fyreplace/api/data/User.java +++ b/src/main/java/app/fyreplace/api/data/User.java @@ -117,10 +117,28 @@ final void postRemove() { } } - public void subscribeTo(final Post post) { - final var existing = Subscription.count("user = ?1 and post = ?2", this, post); + public void block(final User user) { + if (isBlocking(user)) { + return; + } + + final var block = new Block(); + block.source = this; + block.target = user; + block.persist(); + Subscription.delete("user = ?1 and post.author = ?2", user, this); + } + + public void unblock(final User user) { + Block.delete("source = ?1 and target = ?2", this, user); + } + + public boolean isBlocking(final User user) { + return Block.count("source = ?1 and target = ?2", this, user) > 0; + } - if (existing > 0) { + public void subscribeTo(final Post post) { + if (isSubscribedTo(post)) { return; } @@ -136,6 +154,10 @@ public void unsubscribeFrom(final Post post) { Subscription.delete("user = ?1 and post = ?2", this, post); } + public boolean isSubscribedTo(final Post post) { + return Subscription.count("user = ?1 and post = ?2", this, post) > 0; + } + public static @Nullable User findByUsername(final String username) { return findByUsername(username, null); } diff --git a/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java index b0dd780..613130d 100644 --- a/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java @@ -53,33 +53,6 @@ public Iterable list(@PathParam("id") final UUID id, @QueryParam("page" } } - @GET - @Path("count") - @Authenticated - @APIResponse(responseCode = "200") - public long count(@PathParam("id") final UUID id, @QueryParam("read") @Nullable final Boolean read) { - final var user = User.getFromSecurityContext(context); - final var post = Post.findById(id); - Post.validateAccess(post, user, true, false); - - if (read == null) { - return Comment.count("post.id", post.id); - } - - final var subscription = Subscription.find("user = ?1 and post = ?2", user, post) - .firstResult(); - final var dateComparison = read ? '<' : '>'; - final var idComparison = read ? '=' : '>'; - return subscription != null && subscription.lastCommentSeen != null - ? Comment.count( - "post.id = ?1 and (dateCreated " + dateComparison + " ?2 or (dateCreated = ?2 and id " - + idComparison + " ?3))", - post.id, - subscription.lastCommentSeen.dateCreated, - subscription.lastCommentSeen.id) - : 0; - } - @POST @Authenticated @Transactional @@ -153,6 +126,33 @@ public Response acknowledge( return Response.ok().build(); } + @GET + @Path("count") + @Authenticated + @APIResponse(responseCode = "200") + public long count(@PathParam("id") final UUID id, @QueryParam("read") @Nullable final Boolean read) { + final var user = User.getFromSecurityContext(context); + final var post = Post.findById(id); + Post.validateAccess(post, user, true, false); + + if (read == null) { + return Comment.count("post.id", post.id); + } + + final var subscription = Subscription.find("user = ?1 and post = ?2", user, post) + .firstResult(); + final var dateComparison = read ? '<' : '>'; + final var idComparison = read ? '=' : '>'; + return subscription != null && subscription.lastCommentSeen != null + ? Comment.count( + "post.id = ?1 and (dateCreated " + dateComparison + " ?2 or (dateCreated = ?2 and id " + + idComparison + " ?3))", + post.id, + subscription.lastCommentSeen.dateCreated, + subscription.lastCommentSeen.id) + : 0; + } + private Comment getComment(final Post post, final int position) { final var comment = Comment.find("post", Comment.sorting(), post) .range(position, position + 1) diff --git a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java index d98e12d..41510fc 100644 --- a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java @@ -141,7 +141,7 @@ public Response publish(@PathParam("id") final UUID id, @Valid @NotNull final Po } @PUT - @Path("{id}/isSubscribed") + @Path("{id}/subscribed") @Authenticated @Transactional @APIResponse(responseCode = "200") @@ -155,7 +155,7 @@ public Response createSubscription(@PathParam("id") final UUID id) { } @DELETE - @Path("{id}/isSubscribed") + @Path("{id}/subscribed") @Authenticated @Transactional @APIResponse(responseCode = "204") @@ -215,7 +215,10 @@ public Iterable listFeed() { final var user = User.getFromSecurityContext(context); try (final var postStream = Post.find( - "author != ?1 and datePublished > ?2 and life > 0 and id not in (select post.id from Vote where user = ?1)", + "author != ?1 and datePublished > ?2 and life > 0" + + "and id not in (select post.id from Vote where user = ?1)" + + "and author.id not in (select target.id from Block where source = ?1)" + + "and author.id not in (select source.id from Block where target = ?1)", Sort.by("life", "datePublished", "id"), user, Instant.now().minus(Post.shelfLife)) diff --git a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java index b3ea105..2a521c9 100644 --- a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java @@ -119,11 +119,8 @@ public Response createBlock(@PathParam("id") final UUID id) { throw new NotFoundException(); } else if (source.id.equals(target.id)) { throw new ForbiddenException("user_is_self"); - } else if (Block.count("source = ?1 and target = ?2", source, target) == 0) { - final var block = new Block(); - block.source = source; - block.target = target; - block.persist(); + } else { + source.block(target); } return Response.ok().build(); @@ -143,7 +140,7 @@ public void deleteBlock(@PathParam("id") final UUID id) { throw new NotFoundException(); } - Block.delete("source = ?1 and target = ?2", source, target); + source.unblock(target); } @PUT diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/PostTestsBase.java b/src/test/java/app/fyreplace/api/testing/endpoints/PostTestsBase.java index 5c004ee..0b58c1d 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/PostTestsBase.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/PostTestsBase.java @@ -1,15 +1,22 @@ package app.fyreplace.api.testing.endpoints; import app.fyreplace.api.data.Post; +import app.fyreplace.api.data.dev.DataSeeder; import app.fyreplace.api.testing.ImageTests; import io.quarkus.panache.common.Sort; +import jakarta.inject.Inject; import org.junit.jupiter.api.BeforeEach; public abstract class PostTestsBase extends ImageTests { + @Inject + DataSeeder dataSeeder; + public Post post; public Post draft; + public Post anonymousPost; + @BeforeEach @Override public void beforeEach() { @@ -18,5 +25,6 @@ public void beforeEach() { .firstResult(); draft = Post.find("author.username = 'user_0' and datePublished is null", Sort.by("datePublished", "id")) .firstResult(); + anonymousPost = dataSeeder.createPost(post.author, "Anonymous Post", true, true); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/CountTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/CountTests.java index 2a5d819..a7859c1 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/comments/CountTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/CountTests.java @@ -1,6 +1,7 @@ package app.fyreplace.api.testing.endpoints.comments; import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; import static java.util.stream.IntStream.range; import static org.hamcrest.Matchers.equalTo; @@ -11,6 +12,7 @@ import app.fyreplace.api.data.dev.DataSeeder; import app.fyreplace.api.endpoints.CommentsEndpoint; import app.fyreplace.api.testing.endpoints.PostTestsBase; +import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -30,7 +32,7 @@ public class CountTests extends PostTestsBase { private static final int notReadCommentCount = 6; @Test - @TestSecurity(user = "user_0") + @TestSecurity(user = "user_1") public void count() { given().pathParam("id", post.id) .get("count") @@ -40,7 +42,7 @@ public void count() { } @Test - @TestSecurity(user = "user_0") + @TestSecurity(user = "user_1") public void countRead() { given().pathParam("id", post.id) .queryParam("read", true) @@ -51,7 +53,7 @@ public void countRead() { } @Test - @TestSecurity(user = "user_0") + @TestSecurity(user = "user_1") public void countNotRead() { given().pathParam("id", post.id) .queryParam("read", false) @@ -61,23 +63,37 @@ public void countNotRead() { .body(equalTo(String.valueOf(notReadCommentCount))); } + @Test + @TestSecurity(user = "user_1") + public void countWhenBlocked() { + QuarkusTransaction.requiringNew().run(() -> { + final var user = requireNonNull(User.findByUsername("user_0")); + final var otherUser = requireNonNull(User.findByUsername("user_1")); + user.block(otherUser); + }); + + given().pathParam("id", post.id).get("count").then().statusCode(403); + } + @BeforeEach @Transactional @Override public void beforeEach() { super.beforeEach(); Comment.deleteAll(); - final var user = User.findByUsername("user_1"); - range(0, readCommentCount).forEach(i -> dataSeeder.createComment(user, post, "Comment " + i, false)); - final var subscription = Subscription.find("user.username = 'user_0' and post = ?1", post) + final var user1 = User.findByUsername("user_1"); + final var user2 = User.findByUsername("user_2"); + range(0, readCommentCount).forEach(i -> dataSeeder.createComment(user2, post, "Comment " + i, false)); + user1.subscribeTo(post); + final var subscription = Subscription.find("user = ?1 and post = ?2", user1, post) .firstResult(); subscription.lastCommentSeen = Comment.find( "post", Comment.sorting().descending(), post) .firstResult(); subscription.persist(); - range(0, notReadCommentCount).forEach(i -> dataSeeder.createComment(user, post, "Comment " + i, false)); + range(0, notReadCommentCount).forEach(i -> dataSeeder.createComment(user2, post, "Comment " + i, false)); range(0, 10) .forEach(i -> dataSeeder.createComment( - user, Post.find("id != ?1", post.id).firstResult(), "Comment " + i, false)); + user2, Post.find("id != ?1", post.id).firstResult(), "Comment " + i, false)); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/CreateTests.java index 33a827b..6a9e22c 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/comments/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/CreateTests.java @@ -7,8 +7,10 @@ import app.fyreplace.api.data.Comment; import app.fyreplace.api.data.CommentCreation; import app.fyreplace.api.data.Post; +import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.CommentsEndpoint; import app.fyreplace.api.testing.endpoints.PostTestsBase; +import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -104,6 +106,36 @@ public void createAnonymouslyOnOtherPost() { assertEquals(commentCount, Comment.count("post", post)); } + @Test + @TestSecurity(user = "user_1") + public void createOnOtherPostWhenBlocked() { + QuarkusTransaction.requiringNew().run(() -> post.author.block(User.findByUsername("user_1"))); + final var commentCount = Comment.count("post", post); + final var input = new CommentCreation("Text", false); + given().contentType(ContentType.JSON) + .body(input) + .pathParam("id", post.id) + .post() + .then() + .statusCode(403); + assertEquals(commentCount, Comment.count("post", post)); + } + + @Test + @TestSecurity(user = "user_1") + public void createOnOtherAnonymousPostWhenBlocked() { + QuarkusTransaction.requiringNew().run(() -> anonymousPost.author.block(User.findByUsername("user_1"))); + final var commentCount = Comment.count("post", anonymousPost); + final var input = new CommentCreation("Text", false); + given().contentType(ContentType.JSON) + .body(input) + .pathParam("id", anonymousPost.id) + .post() + .then() + .statusCode(201); + assertEquals(commentCount + 1, Comment.count("post", anonymousPost)); + } + @Test @TestSecurity(user = "user_1") public void createOnOtherDraft() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/ListTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/ListTests.java index b58be8b..e350f8a 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/comments/ListTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/ListTests.java @@ -1,6 +1,7 @@ package app.fyreplace.api.testing.endpoints.comments; import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; import static java.util.stream.IntStream.range; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.in; @@ -11,6 +12,7 @@ import app.fyreplace.api.data.dev.DataSeeder; import app.fyreplace.api.endpoints.CommentsEndpoint; import app.fyreplace.api.testing.endpoints.PostTestsBase; +import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -70,6 +72,27 @@ public void listInOtherPost() { .body("[" + i + "].author.username", equalTo("user_1"))); } + @Test + @TestSecurity(user = "user_1") + public void listInOtherPostWhenBlocked() { + final var user = requireNonNull(User.findByUsername("user_1")); + QuarkusTransaction.requiringNew().run(() -> post.author.block(user)); + given().pathParam("id", post.id).queryParam("page", 0).get().then().statusCode(403); + } + + @Test + @TestSecurity(user = "user_1") + public void listInOtherAnonymousPostWhenBlocked() { + final var user = requireNonNull(User.findByUsername("user_1")); + QuarkusTransaction.requiringNew().run(() -> anonymousPost.author.block(user)); + given().pathParam("id", anonymousPost.id) + .queryParam("page", 0) + .get() + .then() + .statusCode(200) + .contentType(ContentType.JSON); + } + @Test @TestSecurity(user = "user_0") public void listOutOfBounds() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateSubscriptionTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateSubscriptionTests.java new file mode 100644 index 0000000..9675522 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateSubscriptionTests.java @@ -0,0 +1,127 @@ +package app.fyreplace.api.testing.endpoints.posts; + +import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import app.fyreplace.api.data.Comment; +import app.fyreplace.api.data.Subscription; +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.PostsEndpoint; +import app.fyreplace.api.testing.endpoints.PostTestsBase; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@QuarkusTest +@TestHTTPEndpoint(PostsEndpoint.class) +public final class CreateSubscriptionTests extends PostTestsBase { + @Test + @TestSecurity(user = "user_1") + public void createSubscriptionWithOtherPost() { + final var user = requireNonNull(User.findByUsername("user_1")); + assertFalse(user.isSubscribedTo(post)); + given().put(post.id + "/subscribed").then().statusCode(200); + final var subscription = Subscription.find("user = ?1 and post = ?2", user, post) + .firstResult(); + assertNotNull(subscription); + final var comment = Comment.find("post", Comment.sorting().descending(), post) + .firstResult(); + assertEquals(comment.id, subscription.lastCommentSeen.id); + } + + @Test + @TestSecurity(user = "user_1") + public void createSubscriptionWithOtherPostTwice() { + final var user = User.findByUsername("user_1"); + given().put(post.id + "/subscribed").then().statusCode(200); + given().put(post.id + "/subscribed").then().statusCode(200); + final var subscription = Subscription.find("user = ?1 and post = ?2", user, post) + .firstResult(); + assertNotNull(subscription); + final var comment = Comment.find("post", Comment.sorting().descending(), post) + .firstResult(); + assertEquals(comment.id, subscription.lastCommentSeen.id); + } + + @Test + @TestSecurity(user = "user_0") + public void createSubscriptionWithOwnPost() { + final var user = requireNonNull(User.findByUsername("user_0")); + assertTrue(user.isSubscribedTo(post)); + given().put(post.id + "/subscribed").then().statusCode(200); + final var subscription = Subscription.find("user = ?1 and post = ?2", user, post) + .firstResult(); + assertNotNull(subscription); + assertNull(subscription.lastCommentSeen); + } + + @Test + @TestSecurity(user = "user_1") + public void createSubscriptionWithOtherDraft() { + final var user = requireNonNull(User.findByUsername("user_1")); + assertFalse(user.isSubscribedTo(draft)); + given().put(draft.id + "/subscribed").then().statusCode(404); + assertFalse(user.isSubscribedTo(draft)); + } + + @Test + @TestSecurity(user = "user_0") + public void createSubscriptionWithOwnDraft() { + final var user = requireNonNull(User.findByUsername("user_0")); + assertFalse(user.isSubscribedTo(draft)); + given().put(draft.id + "/subscribed").then().statusCode(403); + assertFalse(user.isSubscribedTo(draft)); + } + + @Test + @TestSecurity(user = "user_1") + public void createSubscriptionWithOtherPostWhenBlocked() { + final var user = requireNonNull(User.findByUsername("user_1")); + QuarkusTransaction.requiringNew().run(() -> post.author.block(user)); + assertFalse(user.isSubscribedTo(post)); + given().put(post.id + "/subscribed").then().statusCode(403); + assertFalse(user.isSubscribedTo(post)); + } + + @Test + @TestSecurity(user = "user_1") + public void createSubscriptionWithOtherAnonymousPostWhenBlocked() { + final var user = requireNonNull(User.findByUsername("user_1")); + QuarkusTransaction.requiringNew().run(() -> anonymousPost.author.block(user)); + assertFalse(user.isSubscribedTo(anonymousPost)); + given().put(anonymousPost.id + "/subscribed").then().statusCode(200); + assertTrue(user.isSubscribedTo(anonymousPost)); + } + + @Test + public void createSubscriptionWithPostUnauthenticated() { + final var subscriptionCount = Subscription.count(); + given().put(post.id + "/subscribed").then().statusCode(401); + assertEquals(subscriptionCount, Subscription.count()); + } + + @Test + public void createSubscriptionWithDraftUnauthenticated() { + final var subscriptionCount = Subscription.count(); + given().put(draft.id + "/subscribed").then().statusCode(401); + assertEquals(subscriptionCount, Subscription.count()); + } + + @ParameterizedTest + @ValueSource(strings = {"fake", "00000000-0000-0000-0000-000000000000"}) + @TestSecurity(user = "user_0") + public void createSubscriptionWithNonExistent(final String id) { + final var subscriptionCount = Subscription.count(); + given().put(id + "/subscribed").then().statusCode(404); + assertEquals(subscriptionCount, Subscription.count()); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteSubscriptionTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteSubscriptionTests.java new file mode 100644 index 0000000..6a231cb --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteSubscriptionTests.java @@ -0,0 +1,124 @@ +package app.fyreplace.api.testing.endpoints.posts; + +import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import app.fyreplace.api.data.Subscription; +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.PostsEndpoint; +import app.fyreplace.api.testing.endpoints.PostTestsBase; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@QuarkusTest +@TestHTTPEndpoint(PostsEndpoint.class) +public final class DeleteSubscriptionTests extends PostTestsBase { + @Test + @TestSecurity(user = "user_1") + public void deleteSubscriptionWithOtherPost() { + final var user = requireNonNull(User.findByUsername("user_1")); + assertTrue(user.isSubscribedTo(post)); + given().delete(post.id + "/subscribed").then().statusCode(204); + assertFalse(user.isSubscribedTo(post)); + } + + @Test + @TestSecurity(user = "user_1") + public void deleteSubscriptionWithOtherPostTwice() { + final var user = requireNonNull(User.findByUsername("user_1")); + given().delete(post.id + "/subscribed").then().statusCode(204); + given().delete(post.id + "/subscribed").then().statusCode(204); + assertFalse(user.isSubscribedTo(post)); + } + + @Test + @TestSecurity(user = "user_0") + public void deleteSubscriptionWithOwnPost() { + final var user = requireNonNull(User.findByUsername("user_0")); + assertTrue(user.isSubscribedTo(post)); + given().delete(post.id + "/subscribed").then().statusCode(204); + assertFalse(user.isSubscribedTo(post)); + } + + @Test + @TestSecurity(user = "user_1") + public void deleteSubscriptionWithOtherDraft() { + final var user = requireNonNull(User.findByUsername("user_1")); + assertFalse(user.isSubscribedTo(draft)); + given().delete(draft.id + "/subscribed").then().statusCode(404); + assertFalse(user.isSubscribedTo(draft)); + } + + @Test + @TestSecurity(user = "user_0") + public void deleteSubscriptionWithOwnDraft() { + final var user = requireNonNull(User.findByUsername("user_0")); + assertFalse(user.isSubscribedTo(draft)); + given().delete(draft.id + "/subscribed").then().statusCode(403); + assertFalse(user.isSubscribedTo(draft)); + } + + @Test + @TestSecurity(user = "user_1") + public void deleteToOtherPostWhenBlocked() { + final var user = requireNonNull(User.findByUsername("user_1")); + QuarkusTransaction.requiringNew().run(() -> post.author.block(user)); + assertFalse(user.isSubscribedTo(post)); + given().delete(post.id + "/subscribed").then().statusCode(403); + assertFalse(user.isSubscribedTo(post)); + } + + @Test + @TestSecurity(user = "user_1") + public void deleteToOtherAnonymousPostWhenBlocked() { + final var user = requireNonNull(User.findByUsername("user_1")); + QuarkusTransaction.requiringNew().run(() -> { + anonymousPost.author.block(user); + user.subscribeTo(anonymousPost); + }); + assertTrue(user.isSubscribedTo(anonymousPost)); + given().delete(anonymousPost.id + "/subscribed").then().statusCode(204); + assertFalse(user.isSubscribedTo(anonymousPost)); + } + + @Test + public void deleteSubscriptionWithPostUnauthenticated() { + final var subscriptionCount = Subscription.count(); + given().delete(post.id + "/subscribed").then().statusCode(401); + assertEquals(subscriptionCount, Subscription.count()); + } + + @Test + public void deleteSubscriptionWithDraftUnauthenticated() { + final var subscriptionCount = Subscription.count(); + given().delete(draft.id + "/subscribed").then().statusCode(401); + assertEquals(subscriptionCount, Subscription.count()); + } + + @ParameterizedTest + @ValueSource(strings = {"fake", "00000000-0000-0000-0000-000000000000"}) + @TestSecurity(user = "user_0") + public void deleteSubscriptionWithNonExistent(final String id) { + final var subscriptionCount = Subscription.count(); + given().delete(id + "/subscribed").then().statusCode(404); + assertEquals(subscriptionCount, Subscription.count()); + } + + @BeforeEach + @Transactional + @Override + public void beforeEach() { + super.beforeEach(); + requireNonNull(User.findByUsername("user_1")).subscribeTo(post); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListFeedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListFeedTests.java index 974adbb..54675cd 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListFeedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListFeedTests.java @@ -5,6 +5,7 @@ import static java.util.stream.IntStream.range; import static org.hamcrest.CoreMatchers.equalTo; +import app.fyreplace.api.data.Block; import app.fyreplace.api.data.Post; import app.fyreplace.api.data.User; import app.fyreplace.api.data.Vote; @@ -75,11 +76,36 @@ public void listFeedWithAlreadyVotedPosts() { given().get("feed").then().statusCode(200).body("size()", equalTo(0)); } + @Test + @TestSecurity(user = "user_0") + public void listFeedWithPostsFromBlockedUser() { + final var user = requireNonNull(User.findByUsername("user_0")); + final var otherUser = requireNonNull(User.findByUsername("user_1")); + QuarkusTransaction.requiringNew().run(() -> user.block(otherUser)); + QuarkusTransaction.requiringNew() + .run(() -> range(0, 5).forEach(i -> dataSeeder.createPost(otherUser, "Post " + i, true, false))); + + given().get("feed").then().statusCode(200).body("size()", equalTo(0)); + } + + @Test + @TestSecurity(user = "user_0") + public void listFeedWithPostsFromBlockingUser() { + final var user = requireNonNull(User.findByUsername("user_0")); + final var otherUser = requireNonNull(User.findByUsername("user_1")); + QuarkusTransaction.requiringNew().run(() -> user.block(otherUser)); + QuarkusTransaction.requiringNew() + .run(() -> range(0, 5).forEach(i -> dataSeeder.createPost(otherUser, "Post " + i, true, false))); + + given().get("feed").then().statusCode(200).body("size()", equalTo(0)); + } + @BeforeEach @Transactional @Override public void beforeEach() { super.beforeEach(); Post.deleteAll(); + Block.deleteAll(); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java index 519ffc2..bec1225 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java @@ -5,16 +5,14 @@ import static org.hamcrest.Matchers.nullValue; import app.fyreplace.api.data.Comment; -import app.fyreplace.api.data.Post; +import app.fyreplace.api.data.User; import app.fyreplace.api.data.Vote; -import app.fyreplace.api.data.dev.DataSeeder; import app.fyreplace.api.endpoints.PostsEndpoint; import app.fyreplace.api.testing.endpoints.PostTestsBase; +import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; -import jakarta.inject.Inject; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -22,11 +20,6 @@ @QuarkusTest @TestHTTPEndpoint(PostsEndpoint.class) public final class RetrieveTests extends PostTestsBase { - @Inject - DataSeeder dataSeeder; - - private Post anonymousPost; - @Test @TestSecurity(user = "user_0") public void retrieveOwnPost() { @@ -100,6 +93,27 @@ public void retrieveOtherDraft() { given().get(draft.id.toString()).then().statusCode(404); } + @Test + @TestSecurity(user = "user_1") + public void retrieveOtherPostWhenBlocked() { + QuarkusTransaction.requiringNew().run(() -> post.author.block(User.findByUsername("user_1"))); + given().get(post.id.toString()).then().statusCode(403); + } + + @Test + @TestSecurity(user = "user_1") + public void retrieveOtherAnonymousPostWhenBlocked() { + QuarkusTransaction.requiringNew().run(() -> post.author.block(User.findByUsername("user_1"))); + given().get(anonymousPost.id.toString()) + .then() + .statusCode(200) + .body("id", equalTo(anonymousPost.id.toString())) + .body("dateCreated", equalTo(anonymousPost.datePublished.toString())) + .body("author", nullValue()) + .body("anonymous", equalTo(true)) + .body("chapters.size()", equalTo(anonymousPost.getChapters().size())); + } + @Test public void retrievePostUnauthenticated() { given().get(post.id.toString()) @@ -134,11 +148,4 @@ public void retrieveDraftUnauthenticated() { public void retrieveNonExistent(final String id) { given().get(id).then().statusCode(404); } - - @BeforeEach - @Override - public void beforeEach() { - super.beforeEach(); - anonymousPost = dataSeeder.createPost(post.author, "Anonymous Post", true, true); - } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/SubscribeTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/SubscribeTests.java deleted file mode 100644 index d7ccede..0000000 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/SubscribeTests.java +++ /dev/null @@ -1,103 +0,0 @@ -package app.fyreplace.api.testing.endpoints.posts; - -import static io.restassured.RestAssured.given; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; - -import app.fyreplace.api.data.Comment; -import app.fyreplace.api.data.Subscription; -import app.fyreplace.api.data.User; -import app.fyreplace.api.endpoints.PostsEndpoint; -import app.fyreplace.api.testing.endpoints.PostTestsBase; -import io.quarkus.test.common.http.TestHTTPEndpoint; -import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.security.TestSecurity; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -@QuarkusTest -@TestHTTPEndpoint(PostsEndpoint.class) -public final class SubscribeTests extends PostTestsBase { - @Test - @TestSecurity(user = "user_1") - public void subscribeToOtherPost() { - final var user = User.findByUsername("user_1"); - assertEquals(0, Subscription.count("user = ?1 and post = ?2", user, post)); - given().put(post.id + "/isSubscribed").then().statusCode(200); - final var subscription = Subscription.find("user = ?1 and post = ?2", user, post) - .firstResult(); - assertNotNull(subscription); - final var comment = Comment.find("post", Comment.sorting().descending(), post) - .firstResult(); - assertEquals(comment.id, subscription.lastCommentSeen.id); - } - - @Test - @TestSecurity(user = "user_1") - public void subscribeToOtherPostTwice() { - final var user = User.findByUsername("user_1"); - given().put(post.id + "/isSubscribed").then().statusCode(200); - given().put(post.id + "/isSubscribed").then().statusCode(200); - final var subscription = Subscription.find("user = ?1 and post = ?2", user, post) - .firstResult(); - assertNotNull(subscription); - final var comment = Comment.find("post", Comment.sorting().descending(), post) - .firstResult(); - assertEquals(comment.id, subscription.lastCommentSeen.id); - } - - @Test - @TestSecurity(user = "user_0") - public void subscribeToOwnPost() { - final var user = User.findByUsername("user_0"); - assertEquals(1, Subscription.count("user = ?1 and post = ?2", user, post)); - given().put(post.id + "/isSubscribed").then().statusCode(200); - final var subscription = Subscription.find("user = ?1 and post = ?2", user, post) - .firstResult(); - assertNotNull(subscription); - assertNull(subscription.lastCommentSeen); - } - - @Test - @TestSecurity(user = "user_1") - public void subscribeToOtherDraft() { - final var user = User.findByUsername("user_1"); - assertEquals(0, Subscription.count("user = ?1 and post = ?2", user, draft)); - given().put(draft.id + "/isSubscribed").then().statusCode(404); - assertEquals(0, Subscription.count("user = ?1 and post = ?2", user, draft)); - } - - @Test - @TestSecurity(user = "user_0") - public void subscribeToOwnDraft() { - final var user = User.findByUsername("user_0"); - assertEquals(0, Subscription.count("user = ?1 and post = ?2", user, draft)); - given().put(draft.id + "/isSubscribed").then().statusCode(403); - assertEquals(0, Subscription.count("user = ?1 and post = ?2", user, draft)); - } - - @Test - public void subscribeToPostUnauthenticated() { - final var subscriptionCount = Subscription.count(); - given().put(post.id + "/isSubscribed").then().statusCode(401); - assertEquals(subscriptionCount, Subscription.count()); - } - - @Test - public void subscribeToDraftUnauthenticated() { - final var subscriptionCount = Subscription.count(); - given().put(draft.id + "/isSubscribed").then().statusCode(401); - assertEquals(subscriptionCount, Subscription.count()); - } - - @ParameterizedTest - @ValueSource(strings = {"fake", "00000000-0000-0000-0000-000000000000"}) - @TestSecurity(user = "user_0") - public void subscribeToNonExistent(final String id) { - final var subscriptionCount = Subscription.count(); - given().put(id + "/isSubscribed").then().statusCode(404); - assertEquals(subscriptionCount, Subscription.count()); - } -} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/UnsubscribeTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/UnsubscribeTests.java deleted file mode 100644 index 52bbfff..0000000 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/UnsubscribeTests.java +++ /dev/null @@ -1,98 +0,0 @@ -package app.fyreplace.api.testing.endpoints.posts; - -import static io.restassured.RestAssured.given; -import static java.util.Objects.requireNonNull; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import app.fyreplace.api.data.Subscription; -import app.fyreplace.api.data.User; -import app.fyreplace.api.endpoints.PostsEndpoint; -import app.fyreplace.api.testing.endpoints.PostTestsBase; -import io.quarkus.test.common.http.TestHTTPEndpoint; -import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.security.TestSecurity; -import jakarta.transaction.Transactional; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -@QuarkusTest -@TestHTTPEndpoint(PostsEndpoint.class) -public final class UnsubscribeTests extends PostTestsBase { - @Test - @TestSecurity(user = "user_1") - public void unsubscribeFromOtherPost() { - final var user = User.findByUsername("user_1"); - assertEquals(1, Subscription.count("user = ?1 and post = ?2", user, post)); - given().delete(post.id + "/isSubscribed").then().statusCode(204); - assertEquals(0, Subscription.count("user = ?1 and post = ?2", user, post)); - } - - @Test - @TestSecurity(user = "user_1") - public void unsubscribeFromOtherPostTwice() { - final var user = User.findByUsername("user_1"); - given().delete(post.id + "/isSubscribed").then().statusCode(204); - given().delete(post.id + "/isSubscribed").then().statusCode(204); - assertEquals(0, Subscription.count("user = ?1 and post = ?2", user, post)); - } - - @Test - @TestSecurity(user = "user_0") - public void unsubscribeFromOwnPost() { - final var user = User.findByUsername("user_0"); - assertEquals(1, Subscription.count("user = ?1 and post = ?2", user, post)); - given().delete(post.id + "/isSubscribed").then().statusCode(204); - assertEquals(0, Subscription.count("user = ?1 and post = ?2", user, post)); - } - - @Test - @TestSecurity(user = "user_1") - public void unsubscribeFromOtherDraft() { - final var user = User.findByUsername("user_1"); - assertEquals(0, Subscription.count("user = ?1 and post = ?2", user, draft)); - given().delete(draft.id + "/isSubscribed").then().statusCode(404); - assertEquals(0, Subscription.count("user = ?1 and post = ?2", user, draft)); - } - - @Test - @TestSecurity(user = "user_0") - public void unsubscribeFromOwnDraft() { - final var user = User.findByUsername("user_0"); - assertEquals(0, Subscription.count("user = ?1 and post = ?2", user, draft)); - given().delete(draft.id + "/isSubscribed").then().statusCode(403); - assertEquals(0, Subscription.count("user = ?1 and post = ?2", user, draft)); - } - - @Test - public void unsubscribeFromPostUnauthenticated() { - final var subscriptionCount = Subscription.count(); - given().delete(post.id + "/isSubscribed").then().statusCode(401); - assertEquals(subscriptionCount, Subscription.count()); - } - - @Test - public void unsubscribeFromDraftUnauthenticated() { - final var subscriptionCount = Subscription.count(); - given().delete(draft.id + "/isSubscribed").then().statusCode(401); - assertEquals(subscriptionCount, Subscription.count()); - } - - @ParameterizedTest - @ValueSource(strings = {"fake", "00000000-0000-0000-0000-000000000000"}) - @TestSecurity(user = "user_0") - public void unsubscribeFromNonExistent(final String id) { - final var subscriptionCount = Subscription.count(); - given().delete(id + "/isSubscribed").then().statusCode(404); - assertEquals(subscriptionCount, Subscription.count()); - } - - @BeforeEach - @Transactional - @Override - public void beforeEach() { - super.beforeEach(); - requireNonNull(User.findByUsername("user_1")).subscribeTo(post); - } -} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/VoteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/VoteTests.java index c92ec4e..6197fdf 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/VoteTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/VoteTests.java @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import app.fyreplace.api.data.Post; +import app.fyreplace.api.data.User; import app.fyreplace.api.data.Vote; import app.fyreplace.api.data.VoteCreation; import app.fyreplace.api.endpoints.PostsEndpoint; @@ -125,6 +126,36 @@ public void voteOnOwnDraft() { assertEquals(1, Post.count("id = ?1 and life = ?2", draft.id, postLife)); } + @Test + @TestSecurity(user = "user_1") + public void voteOnOtherPostWhenBlocked() { + final var user = User.findByUsername("user_1"); + QuarkusTransaction.requiringNew().run(() -> post.author.block(user)); + final var voteCount = Vote.count(); + final var postLife = post.life; + given().contentType(ContentType.JSON) + .body(new VoteCreation(false)) + .post(post.id + "/vote") + .then() + .statusCode(403); + assertEquals(voteCount, Vote.count()); + assertEquals(1, Post.count("id = ?1 and life = ?2", post.id, postLife)); + } + + @Test + @TestSecurity(user = "user_1") + public void voteOnOtherAnonymousPostWhenBlocked() { + final var user = User.findByUsername("user_1"); + QuarkusTransaction.requiringNew().run(() -> anonymousPost.author.block(user)); + final var voteCount = Vote.count(); + given().contentType(ContentType.JSON) + .body(new VoteCreation(true)) + .post(anonymousPost.id + "/vote") + .then() + .statusCode(200); + assertEquals(voteCount + 1, Vote.count()); + } + @Test @TestSecurity(user = "user_1") public void voteOnNonExistentPost() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java index 0b2cfa7..32bddb1 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java @@ -34,10 +34,7 @@ public void beforeEach() { final var user = User.findByUsername("user_0"); for (final var otherUser : User.list("username > 'user_10'")) { - final var block = new Block(); - block.source = user; - block.target = otherUser; - block.persist(); + user.block(otherUser); } } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java index 0d6b794..5867215 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java @@ -3,11 +3,15 @@ import static io.restassured.RestAssured.given; import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import app.fyreplace.api.data.Block; +import app.fyreplace.api.data.Post; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; import app.fyreplace.api.testing.TransactionalTests; +import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -19,22 +23,25 @@ public final class CreateBlockTests extends TransactionalTests { @Test @TestSecurity(user = "user_0") public void createBlock() { - final var user = User.findByUsername("user_0"); - final var otherUser = User.findByUsername("user_2"); - assertEquals(0, Block.count("source = ?1 and target = ?2", user, otherUser)); + final var user = requireNonNull(User.findByUsername("user_0")); + final var otherUser = requireNonNull(User.findByUsername("user_1")); + final var post = Post.find("author", user).firstResult(); + QuarkusTransaction.requiringNew().run(() -> otherUser.subscribeTo(post)); + assertFalse(user.isBlocking(otherUser)); given().put(otherUser.id + "/blocked").then().statusCode(200); - assertEquals(1, Block.count("source = ?1 and target = ?2", user, otherUser)); + assertTrue(user.isBlocking(otherUser)); + assertFalse(otherUser.isSubscribedTo(post)); } @Test @TestSecurity(user = "user_0") public void createBlockTwice() { - final var user = User.findByUsername("user_0"); - final var otherUser = User.findByUsername("user_1"); - assertEquals(0, Block.count("source = ?1 and target = ?2", user, otherUser)); + final var user = requireNonNull(User.findByUsername("user_0")); + final var otherUser = requireNonNull(User.findByUsername("user_1")); + assertFalse(user.isBlocking(otherUser)); given().put(otherUser.id + "/blocked").then().statusCode(200); given().put(otherUser.id + "/blocked").then().statusCode(200); - assertEquals(1, Block.count("source = ?1 and target = ?2", user, otherUser)); + assertTrue(user.isBlocking(otherUser)); } @Test @@ -51,6 +58,6 @@ public void createBlockWithInvalidUser() { public void createBlockWithSelf() { final var user = requireNonNull(User.findByUsername("user_0")); given().put(user.id + "/blocked").then().statusCode(403); - assertEquals(0, Block.count("source = ?1 and target = ?2", user, user)); + assertFalse(user.isBlocking(user)); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java index 1722e21..ddd26d5 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java @@ -3,6 +3,8 @@ import static io.restassured.RestAssured.given; import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import app.fyreplace.api.data.Block; import app.fyreplace.api.data.User; @@ -23,8 +25,9 @@ public final class DeleteBlockTests extends TransactionalTests { public void deleteBlock() { final var user = requireNonNull(User.findByUsername("user_0")); final var otherUser = requireNonNull(User.findByUsername("user_1")); + assertTrue(user.isBlocking(otherUser)); given().delete(otherUser.id + "/blocked").then().statusCode(204); - assertEquals(0, Block.count("source = ?1 and target = ?2", user, otherUser)); + assertFalse(user.isBlocking(otherUser)); } @Test @@ -32,9 +35,10 @@ public void deleteBlock() { public void deleteBlockTwice() { final var user = requireNonNull(User.findByUsername("user_0")); final var otherUser = requireNonNull(User.findByUsername("user_1")); + assertTrue(user.isBlocking(otherUser)); given().delete(otherUser.id + "/blocked").then().statusCode(204); given().delete(otherUser.id + "/blocked").then().statusCode(204); - assertEquals(0, Block.count("source = ?1 and target = ?2", user, otherUser)); + assertFalse(user.isBlocking(otherUser)); } @Test @@ -51,11 +55,8 @@ public void deleteBlockWithInvalidUser() { @Override public void beforeEach() { super.beforeEach(); - final var user = User.findByUsername("user_0"); - final var otherUser = User.findByUsername("user_1"); - final var block = new Block(); - block.source = user; - block.target = otherUser; - block.persist(); + final var user = requireNonNull(User.findByUsername("user_0")); + final var otherUser = requireNonNull(User.findByUsername("user_1")); + user.block(otherUser); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java index 3d7d351..34f9c63 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java @@ -66,10 +66,7 @@ public void beforeEach() { final var user = User.findByUsername("user_0"); for (final var otherUser : User.list("username > 'user_10'")) { - final var block = new Block(); - block.source = user; - block.target = otherUser; - block.persist(); + user.block(otherUser); } } } From ade6f726599dfb7af4a8db9eed4272c4fb19b636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 7 Oct 2023 13:15:42 +0200 Subject: [PATCH 058/157] Ensure test setups are always transactional --- .../java/app/fyreplace/api/testing/endpoints/PostTestsBase.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/PostTestsBase.java b/src/test/java/app/fyreplace/api/testing/endpoints/PostTestsBase.java index 0b58c1d..df4f2b2 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/PostTestsBase.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/PostTestsBase.java @@ -5,6 +5,7 @@ import app.fyreplace.api.testing.ImageTests; import io.quarkus.panache.common.Sort; import jakarta.inject.Inject; +import jakarta.transaction.Transactional; import org.junit.jupiter.api.BeforeEach; public abstract class PostTestsBase extends ImageTests { @@ -18,6 +19,7 @@ public abstract class PostTestsBase extends ImageTests { public Post anonymousPost; @BeforeEach + @Transactional @Override public void beforeEach() { super.beforeEach(); From bbf14d1992aaca56d231e27a352ce57022028eb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 7 Oct 2023 13:17:28 +0200 Subject: [PATCH 059/157] Complete post retrieval tests --- .../endpoints/posts/RetrieveTests.java | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java index bec1225..e8769e7 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java @@ -45,7 +45,9 @@ public void retrieveOwnAnonymousPost() { .body("dateCreated", equalTo(anonymousPost.datePublished.toString())) .body("author.username", equalTo("user_0")) .body("anonymous", equalTo(true)) - .body("chapters.size()", equalTo(anonymousPost.getChapters().size())); + .body("chapters.size()", equalTo(anonymousPost.getChapters().size())) + .body("commentCount", equalTo((int) Comment.count("post", anonymousPost))) + .body("voteCount", equalTo((int) Vote.count("post", anonymousPost))); } @Test @@ -71,7 +73,9 @@ public void retrieveOtherPost() { .body("dateCreated", equalTo(post.datePublished.toString())) .body("author.username", equalTo("user_0")) .body("anonymous", equalTo(false)) - .body("chapters.size()", equalTo(post.getChapters().size())); + .body("chapters.size()", equalTo(post.getChapters().size())) + .body("commentCount", equalTo((int) Comment.count("post", post))) + .body("voteCount", equalTo((int) Vote.count("post", post))); } @Test @@ -84,7 +88,9 @@ public void retrieveOtherAnonymousPost() { .body("dateCreated", equalTo(anonymousPost.datePublished.toString())) .body("author", nullValue()) .body("anonymous", equalTo(true)) - .body("chapters.size()", equalTo(anonymousPost.getChapters().size())); + .body("chapters.size()", equalTo(anonymousPost.getChapters().size())) + .body("commentCount", equalTo((int) Comment.count("post", anonymousPost))) + .body("voteCount", equalTo((int) Vote.count("post", anonymousPost))); } @Test @@ -111,7 +117,9 @@ public void retrieveOtherAnonymousPostWhenBlocked() { .body("dateCreated", equalTo(anonymousPost.datePublished.toString())) .body("author", nullValue()) .body("anonymous", equalTo(true)) - .body("chapters.size()", equalTo(anonymousPost.getChapters().size())); + .body("chapters.size()", equalTo(anonymousPost.getChapters().size())) + .body("commentCount", equalTo((int) Comment.count("post", anonymousPost))) + .body("voteCount", equalTo((int) Vote.count("post", anonymousPost))); } @Test @@ -123,7 +131,9 @@ public void retrievePostUnauthenticated() { .body("dateCreated", equalTo(post.datePublished.toString())) .body("author.username", equalTo("user_0")) .body("anonymous", equalTo(false)) - .body("chapters.size()", equalTo(post.getChapters().size())); + .body("chapters.size()", equalTo(post.getChapters().size())) + .body("commentCount", equalTo((int) Comment.count("post", post))) + .body("voteCount", equalTo((int) Vote.count("post", post))); } @Test @@ -135,7 +145,9 @@ public void retrieveAnonymousPostUnauthenticated() { .body("dateCreated", equalTo(anonymousPost.datePublished.toString())) .body("author", nullValue()) .body("anonymous", equalTo(true)) - .body("chapters.size()", equalTo(anonymousPost.getChapters().size())); + .body("chapters.size()", equalTo(anonymousPost.getChapters().size())) + .body("commentCount", equalTo((int) Comment.count("post", anonymousPost))) + .body("voteCount", equalTo((int) Vote.count("post", anonymousPost))); } @Test From 2d7281f9172b9ea95c3d127fd40eb7dcf743dcb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 7 Oct 2023 13:39:19 +0200 Subject: [PATCH 060/157] Simplify publication date handling --- .../java/app/fyreplace/api/data/Comment.java | 5 ----- src/main/java/app/fyreplace/api/data/Post.java | 16 +++++----------- .../api/data/TimestampedEntityBase.java | 5 +++++ .../app/fyreplace/api/data/dev/DataSeeder.java | 4 +--- .../fyreplace/api/endpoints/PostsEndpoint.java | 16 ++++++++-------- .../api/testing/endpoints/PostTestsBase.java | 5 ++--- .../api/testing/endpoints/posts/CreateTests.java | 3 +-- .../api/testing/endpoints/posts/ListTests.java | 4 ++-- .../testing/endpoints/posts/PublishTests.java | 10 +++++----- .../testing/endpoints/posts/RetrieveTests.java | 14 +++++++------- .../api/testing/endpoints/posts/VoteTests.java | 2 +- 11 files changed, 37 insertions(+), 47 deletions(-) diff --git a/src/main/java/app/fyreplace/api/data/Comment.java b/src/main/java/app/fyreplace/api/data/Comment.java index 76d85aa..b4c4b3f 100644 --- a/src/main/java/app/fyreplace/api/data/Comment.java +++ b/src/main/java/app/fyreplace/api/data/Comment.java @@ -1,7 +1,6 @@ package app.fyreplace.api.data; import com.fasterxml.jackson.annotation.JsonIgnore; -import io.quarkus.panache.common.Sort; import jakarta.annotation.Nonnull; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -38,8 +37,4 @@ public void softDelete() { deleted = true; persist(); } - - public static Sort sorting() { - return Sort.by("dateCreated", "id"); - } } diff --git a/src/main/java/app/fyreplace/api/data/Post.java b/src/main/java/app/fyreplace/api/data/Post.java index 976b043..3a6996c 100644 --- a/src/main/java/app/fyreplace/api/data/Post.java +++ b/src/main/java/app/fyreplace/api/data/Post.java @@ -1,7 +1,5 @@ package app.fyreplace.api.data; -import static java.util.Objects.requireNonNullElse; - import app.fyreplace.api.exceptions.ForbiddenException; import com.fasterxml.jackson.annotation.JsonIgnore; import io.quarkus.panache.common.Sort; @@ -18,8 +16,7 @@ @Entity @Table(name = "posts") public class Post extends AuthoredEntityBase { - @JsonIgnore - public Instant datePublished; + public boolean published = false; @Column(nullable = false) @JsonIgnore @@ -35,21 +32,18 @@ public class Post extends AuthoredEntityBase { public static Duration shelfLife = Duration.ofDays(7); - public Instant getDateCreated() { - return requireNonNullElse(datePublished, dateCreated); - } - public List getChapters() { return Chapter.find("post", Sort.by("position"), this).list(); } @JsonIgnore public boolean isOld() { - return datePublished != null && Instant.now().isAfter(datePublished.plus(shelfLife)); + return published && Instant.now().isAfter(dateCreated.plus(shelfLife)); } public void publish(final int life, final boolean anonymous) { - datePublished = Instant.now(); + dateCreated = Instant.now(); + published = true; this.life = life; this.anonymous = anonymous; persist(); @@ -77,7 +71,7 @@ public static void validateAccess( @Nullable final User user, @Nullable final Boolean mustBePublished, final boolean mustBeAuthor) { - final Boolean postIsDraft = post != null && (post.datePublished == null); + final Boolean postIsDraft = post != null && (!post.published); final var userId = user != null ? user.id : null; if (post == null || (!post.author.id.equals(userId) && postIsDraft)) { diff --git a/src/main/java/app/fyreplace/api/data/TimestampedEntityBase.java b/src/main/java/app/fyreplace/api/data/TimestampedEntityBase.java index ce4357e..969d12f 100644 --- a/src/main/java/app/fyreplace/api/data/TimestampedEntityBase.java +++ b/src/main/java/app/fyreplace/api/data/TimestampedEntityBase.java @@ -1,5 +1,6 @@ package app.fyreplace.api.data; +import io.quarkus.panache.common.Sort; import jakarta.persistence.Column; import jakarta.persistence.MappedSuperclass; import java.time.Instant; @@ -11,4 +12,8 @@ public abstract class TimestampedEntityBase extends EntityBase { @Column(nullable = false) @CreationTimestamp(source = SourceType.DB) public Instant dateCreated; + + public static Sort sorting() { + return Sort.by("dateCreated", "id"); + } } diff --git a/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java b/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java index 3d2a2c5..aa0f550 100644 --- a/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java +++ b/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java @@ -9,7 +9,6 @@ import app.fyreplace.api.data.RandomCode; import app.fyreplace.api.data.StoredFile; import app.fyreplace.api.data.User; -import io.quarkus.panache.common.Sort; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.ShutdownEvent; import io.quarkus.runtime.StartupEvent; @@ -47,8 +46,7 @@ public void insertData() { final var user = User.findByUsername("user_0"); range(0, 20).forEach(i -> createPost(user, "Post " + i, true, false)); range(0, 20).forEach(i -> createPost(user, "Draft " + i, false, false)); - final var post = Post.find( - "author = ?1 and datePublished is not null", Sort.by("datePublished", "id"), user) + final var post = Post.find("author = ?1 and published = true", Post.sorting(), user) .firstResult(); range(0, 10).forEach(i -> createComment(user, post, "Comment " + i, false)); } diff --git a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java index 41510fc..182492e 100644 --- a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java @@ -56,22 +56,22 @@ public Iterable list( @QueryParam("type") @NotNull final PostListingType type) { final var user = User.getFromSecurityContext(context); final var direction = ascending ? Direction.Ascending : Direction.Descending; - final var basicSort = Sort.by("datePublished", "id").direction(direction); + final var basicSort = Post.sorting().direction(direction); final var postStream = switch (type) { case SUBSCRIBED_TO -> Subscription.find( "user", - Sort.by("dateLastSeen", "post.datePublished", "post.id") + Sort.by("dateLastSeen", "post.dateCreated", "post.id") .direction(direction), user) .page(page, pagingSize) .stream() .map(s -> s.post); - case PUBLISHED -> Post.find("author = ?1 and datePublished is not null", basicSort, user) + case PUBLISHED -> Post.find("author = ?1 and published = true", basicSort, user) .page(page, pagingSize) .stream(); - case DRAFTS -> Post.find("author = ?1 and datePublished is null", basicSort, user) + case DRAFTS -> Post.find("author = ?1 and published = false", basicSort, user) .page(page, pagingSize) .stream(); }; @@ -202,8 +202,8 @@ public long count(@QueryParam("type") @NotNull final PostListingType type) { final var user = User.getFromSecurityContext(context); return switch (type) { case SUBSCRIBED_TO -> Subscription.count("user", user); - case PUBLISHED -> Post.count("author = ?1 and datePublished is not null", user); - case DRAFTS -> Post.count("author = ?1 and datePublished is null", user); + case PUBLISHED -> Post.count("author = ?1 and published = true", user); + case DRAFTS -> Post.count("author = ?1 and published = false", user); }; } @@ -215,11 +215,11 @@ public Iterable listFeed() { final var user = User.getFromSecurityContext(context); try (final var postStream = Post.find( - "author != ?1 and datePublished > ?2 and life > 0" + "author != ?1 and dateCreated > ?2 and published = true and life > 0" + "and id not in (select post.id from Vote where user = ?1)" + "and author.id not in (select target.id from Block where source = ?1)" + "and author.id not in (select source.id from Block where target = ?1)", - Sort.by("life", "datePublished", "id"), + Sort.by("life", "dateCreated", "id"), user, Instant.now().minus(Post.shelfLife)) .range(0, 2) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/PostTestsBase.java b/src/test/java/app/fyreplace/api/testing/endpoints/PostTestsBase.java index df4f2b2..4202345 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/PostTestsBase.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/PostTestsBase.java @@ -3,7 +3,6 @@ import app.fyreplace.api.data.Post; import app.fyreplace.api.data.dev.DataSeeder; import app.fyreplace.api.testing.ImageTests; -import io.quarkus.panache.common.Sort; import jakarta.inject.Inject; import jakarta.transaction.Transactional; import org.junit.jupiter.api.BeforeEach; @@ -23,9 +22,9 @@ public abstract class PostTestsBase extends ImageTests { @Override public void beforeEach() { super.beforeEach(); - post = Post.find("author.username = 'user_0' and datePublished is not null", Sort.by("datePublished", "id")) + post = Post.find("author.username = 'user_0' and published = true", Post.sorting()) .firstResult(); - draft = Post.find("author.username = 'user_0' and datePublished is null", Sort.by("datePublished", "id")) + draft = Post.find("author.username = 'user_0' and published = false", Post.sorting()) .firstResult(); anonymousPost = dataSeeder.createPost(post.author, "Anonymous Post", true, true); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateTests.java index 42767fb..5fe787a 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateTests.java @@ -4,7 +4,6 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.isA; import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertEquals; import app.fyreplace.api.data.Post; @@ -27,7 +26,7 @@ public void create() { .statusCode(201) .body("id", isA(String.class)) .body("dateCreated", notNullValue()) - .body("datePublished", nullValue()) + .body("published", equalTo(false)) .body("author.username", equalTo("user_0")) .body("anonymous", equalTo(false)) .body("chapters.size()", equalTo(0)); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java index 4b17b08..5ab798f 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java @@ -55,7 +55,7 @@ public void listSubscribedTo() { public void listPublished() { final var user = User.findByUsername("user_0"); - try (final var stream = Post.stream("author = ?1 and datePublished is not null", user)) { + try (final var stream = Post.stream("author = ?1 and published = true", user)) { final var publishedPostIds = stream.map(post -> post.id.toString()).toList(); final var response = given().queryParam("page", 0) .queryParam("ascending", false) @@ -75,7 +75,7 @@ public void listPublished() { public void listDrafts() { final var user = User.findByUsername("user_0"); - try (final var stream = Post.stream("author = ?1 and datePublished is null", user)) { + try (final var stream = Post.stream("author = ?1 and published = false", user)) { final var draftIds = stream.map(post -> post.id.toString()).toList(); final var response = given().queryParam("page", 0) .queryParam("ascending", false) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/PublishTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/PublishTests.java index b0d08af..63fe45f 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/PublishTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/PublishTests.java @@ -38,7 +38,7 @@ public void publishOwnDraft() { .post(draft.id + "/publish") .then() .statusCode(200); - assertEquals(0, Post.count("id = ?1 and datePublished is null and anonymous = false", draft.id)); + assertEquals(0, Post.count("id = ?1 and published = false and anonymous = false", draft.id)); } @Test @@ -49,7 +49,7 @@ public void publishAnonymouslyOwnDraft() { .post(draft.id + "/publish") .then() .statusCode(200); - assertEquals(0, Post.count("id = ?1 and datePublished is null and anonymous = true", draft.id)); + assertEquals(0, Post.count("id = ?1 and published = false and anonymous = true", draft.id)); } @Test @@ -61,7 +61,7 @@ public void publishOwnDraftWithoutChapters() { .post(draft.id + "/publish") .then() .statusCode(403); - assertEquals(1, Post.count("id = ?1 and datePublished is null", draft.id)); + assertEquals(1, Post.count("id = ?1 and published = false", draft.id)); } @Test @@ -82,7 +82,7 @@ public void publishOtherDraft() { .post(draft.id + "/publish") .then() .statusCode(404); - assertEquals(1, Post.count("id = ?1 and datePublished is null", draft.id)); + assertEquals(1, Post.count("id = ?1 and published = false", draft.id)); } @Test @@ -101,7 +101,7 @@ public void publishDraftUnauthenticated() { .post(draft.id + "/publish") .then() .statusCode(401); - assertEquals(1, Post.count("id = ?1 and datePublished is null", draft.id)); + assertEquals(1, Post.count("id = ?1 and published = false", draft.id)); } @ParameterizedTest diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java index e8769e7..3f95e15 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java @@ -27,7 +27,7 @@ public void retrieveOwnPost() { .then() .statusCode(200) .body("id", equalTo(post.id.toString())) - .body("dateCreated", equalTo(post.datePublished.toString())) + .body("dateCreated", equalTo(post.dateCreated.toString())) .body("author.username", equalTo("user_0")) .body("anonymous", equalTo(false)) .body("chapters.size()", equalTo(post.getChapters().size())) @@ -42,7 +42,7 @@ public void retrieveOwnAnonymousPost() { .then() .statusCode(200) .body("id", equalTo(anonymousPost.id.toString())) - .body("dateCreated", equalTo(anonymousPost.datePublished.toString())) + .body("dateCreated", equalTo(anonymousPost.dateCreated.toString())) .body("author.username", equalTo("user_0")) .body("anonymous", equalTo(true)) .body("chapters.size()", equalTo(anonymousPost.getChapters().size())) @@ -70,7 +70,7 @@ public void retrieveOtherPost() { .then() .statusCode(200) .body("id", equalTo(post.id.toString())) - .body("dateCreated", equalTo(post.datePublished.toString())) + .body("dateCreated", equalTo(post.dateCreated.toString())) .body("author.username", equalTo("user_0")) .body("anonymous", equalTo(false)) .body("chapters.size()", equalTo(post.getChapters().size())) @@ -85,7 +85,7 @@ public void retrieveOtherAnonymousPost() { .then() .statusCode(200) .body("id", equalTo(anonymousPost.id.toString())) - .body("dateCreated", equalTo(anonymousPost.datePublished.toString())) + .body("dateCreated", equalTo(anonymousPost.dateCreated.toString())) .body("author", nullValue()) .body("anonymous", equalTo(true)) .body("chapters.size()", equalTo(anonymousPost.getChapters().size())) @@ -114,7 +114,7 @@ public void retrieveOtherAnonymousPostWhenBlocked() { .then() .statusCode(200) .body("id", equalTo(anonymousPost.id.toString())) - .body("dateCreated", equalTo(anonymousPost.datePublished.toString())) + .body("dateCreated", equalTo(anonymousPost.dateCreated.toString())) .body("author", nullValue()) .body("anonymous", equalTo(true)) .body("chapters.size()", equalTo(anonymousPost.getChapters().size())) @@ -128,7 +128,7 @@ public void retrievePostUnauthenticated() { .then() .statusCode(200) .body("id", equalTo(post.id.toString())) - .body("dateCreated", equalTo(post.datePublished.toString())) + .body("dateCreated", equalTo(post.dateCreated.toString())) .body("author.username", equalTo("user_0")) .body("anonymous", equalTo(false)) .body("chapters.size()", equalTo(post.getChapters().size())) @@ -142,7 +142,7 @@ public void retrieveAnonymousPostUnauthenticated() { .then() .statusCode(200) .body("id", equalTo(anonymousPost.id.toString())) - .body("dateCreated", equalTo(anonymousPost.datePublished.toString())) + .body("dateCreated", equalTo(anonymousPost.dateCreated.toString())) .body("author", nullValue()) .body("anonymous", equalTo(true)) .body("chapters.size()", equalTo(anonymousPost.getChapters().size())) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/VoteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/VoteTests.java index 6197fdf..a18defe 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/VoteTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/VoteTests.java @@ -65,7 +65,7 @@ public void voteWithoutSpread() { public void voteOnOldPost() { QuarkusTransaction.requiringNew() .run(() -> Post.update( - "datePublished = ?1 where id = ?2", + "dateCreated = ?1 where id = ?2", Instant.now().minus(Post.shelfLife.plus(Duration.ofDays(1))), post.id)); final var voteCount = Vote.count(); From 78bd2e042f09b657602323f9126d55883028a960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 7 Oct 2023 13:43:31 +0200 Subject: [PATCH 061/157] Use byte arrays for file uploads --- .../fyreplace/api/endpoints/ChaptersEndpoint.java | 9 +++------ .../fyreplace/api/endpoints/UsersEndpoint.java | 9 +++------ .../fyreplace/api/services/MimeTypeService.java | 15 +++++++++------ 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java index 2f49eff..9241060 100644 --- a/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java @@ -26,9 +26,7 @@ import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; import jakarta.ws.rs.core.SecurityContext; -import java.io.File; import java.io.IOException; -import java.nio.file.Files; import java.util.UUID; import org.apache.tika.metadata.Metadata; import org.apache.tika.metadata.Property; @@ -162,13 +160,12 @@ public String updateChapterText( @APIResponse(responseCode = "400") @APIResponse(responseCode = "404") public String updateChapterImage( - @PathParam("id") final UUID id, @PathParam("position") final int position, final File input) + @PathParam("id") final UUID id, @PathParam("position") final int position, final byte[] input) throws IOException { mimeTypeService.validate(input, KnownMimeTypes.IMAGE); final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); Post.validateAccess(post, user, false, true); - final var data = Files.readAllBytes(input.toPath()); try { final var chapter = getChapter(post, position); @@ -177,9 +174,9 @@ public String updateChapterImage( final var height = metadata.getInt(Metadata.IMAGE_LENGTH); if (chapter.image == null) { - chapter.image = new StoredFile("chapters/" + chapter.id, data); + chapter.image = new StoredFile("chapters/" + chapter.id, input); } else { - chapter.image.store(data); + chapter.image.store(input); } chapter.width = requireNonNullElse(width, metadata.getInt(Property.internalInteger("Image Width"))); diff --git a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java index 2a521c9..6fb1cdd 100644 --- a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java @@ -33,9 +33,7 @@ import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; import jakarta.ws.rs.core.SecurityContext; -import java.io.File; import java.io.IOException; -import java.nio.file.Files; import java.time.Duration; import java.time.Instant; import java.util.UUID; @@ -203,15 +201,14 @@ public String updateMeBio(@NotNull @Length(max = 3000) final String input) { content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) @APIResponse(responseCode = "413") @APIResponse(responseCode = "415") - public String updateMeAvatar(final File input) throws IOException { + public String updateMeAvatar(final byte[] input) throws IOException { mimeTypeService.validate(input, KnownMimeTypes.IMAGE); final var user = User.getFromSecurityContext(context, LockModeType.PESSIMISTIC_WRITE); - final var data = Files.readAllBytes(input.toPath()); if (user.avatar == null) { - user.avatar = new StoredFile("avatars/" + user.id, data); + user.avatar = new StoredFile("avatars/" + user.id, input); } else { - user.avatar.store(data); + user.avatar.store(input); } user.persist(); diff --git a/src/main/java/app/fyreplace/api/services/MimeTypeService.java b/src/main/java/app/fyreplace/api/services/MimeTypeService.java index d18dff5..e62e5a4 100644 --- a/src/main/java/app/fyreplace/api/services/MimeTypeService.java +++ b/src/main/java/app/fyreplace/api/services/MimeTypeService.java @@ -3,26 +3,29 @@ import app.fyreplace.api.exceptions.UnsupportedMediaTypeException; import app.fyreplace.api.services.mimetype.KnownMimeTypes; import jakarta.enterprise.context.ApplicationScoped; -import java.io.File; +import java.io.ByteArrayInputStream; import java.io.IOException; import org.apache.tika.Tika; import org.apache.tika.metadata.Metadata; @ApplicationScoped public final class MimeTypeService { - public void validate(final File file, final KnownMimeTypes types) throws IOException { + public void validate(final byte[] data, final KnownMimeTypes types) { final var tika = new Tika(); - final var mimeType = tika.detect(file); + final var mimeType = tika.detect(data); if (mimeType == null || !types.types.contains(mimeType)) { throw new UnsupportedMediaTypeException("invalid_media_type"); } } - public Metadata getMetadata(final File file) throws IOException { + public Metadata getMetadata(final byte[] data) throws IOException { final var tika = new Tika(); final var metadata = new Metadata(); - tika.parse(file, metadata); - return metadata; + + try (final var stream = new ByteArrayInputStream(data); + final var ignored = tika.parse(stream, metadata)) { + return metadata; + } } } From 4aebecc074dfb034a33ded593f08fe61ded96308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 8 Oct 2023 16:16:32 +0200 Subject: [PATCH 062/157] Fix test classes naming --- .../api/testing/{ImageTests.java => ImageTestsBase.java} | 2 +- .../{TransactionalTests.java => TransactionalTestsBase.java} | 2 +- .../api/testing/data/chapters/PositionBetweenTests.java | 4 ++-- .../app/fyreplace/api/testing/data/posts/NormalizeTests.java | 4 ++-- .../app/fyreplace/api/testing/endpoints/PostTestsBase.java | 4 ++-- .../fyreplace/api/testing/endpoints/emails/ActivateTests.java | 4 ++-- .../fyreplace/api/testing/endpoints/emails/CountTests.java | 4 ++-- .../fyreplace/api/testing/endpoints/emails/CreateTests.java | 4 ++-- .../fyreplace/api/testing/endpoints/emails/DeleteTests.java | 4 ++-- .../app/fyreplace/api/testing/endpoints/emails/ListTests.java | 4 ++-- .../fyreplace/api/testing/endpoints/emails/SetMainTests.java | 4 ++-- .../fyreplace/api/testing/endpoints/posts/CreateTests.java | 4 ++-- .../app/fyreplace/api/testing/endpoints/posts/ListTests.java | 4 ++-- .../api/testing/endpoints/tokens/CreateNewTests.java | 4 ++-- .../fyreplace/api/testing/endpoints/tokens/CreateTests.java | 4 ++-- .../api/testing/endpoints/tokens/RetrieveNewTests.java | 4 ++-- .../fyreplace/api/testing/endpoints/users/BannedTests.java | 4 ++-- .../api/testing/endpoints/users/CountBlockedTests.java | 4 ++-- .../api/testing/endpoints/users/CreateBlockTests.java | 4 ++-- .../fyreplace/api/testing/endpoints/users/CreateTests.java | 4 ++-- .../api/testing/endpoints/users/DeleteBlockTests.java | 4 ++-- .../api/testing/endpoints/users/DeleteMeAvatarTests.java | 4 ++-- .../fyreplace/api/testing/endpoints/users/DeleteMeTests.java | 4 ++-- .../api/testing/endpoints/users/ListBlockedTests.java | 4 ++-- .../api/testing/endpoints/users/RetrieveMeTests.java | 4 ++-- .../fyreplace/api/testing/endpoints/users/RetrieveTests.java | 4 ++-- .../api/testing/endpoints/users/UpdateMeAvatarTests.java | 4 ++-- .../api/testing/endpoints/users/UpdateMeBioTests.java | 4 ++-- .../api/testing/endpoints/users/dev/RetrieveTokenTests.java | 4 ++-- .../testing/tasks/cleanup/RemoveOldInactiveUsersTests.java | 4 ++-- .../api/testing/tasks/cleanup/RemoveOldRandomCodesTests.java | 4 ++-- 31 files changed, 60 insertions(+), 60 deletions(-) rename src/test/java/app/fyreplace/api/testing/{ImageTests.java => ImageTestsBase.java} (92%) rename src/test/java/app/fyreplace/api/testing/{TransactionalTests.java => TransactionalTestsBase.java} (95%) diff --git a/src/test/java/app/fyreplace/api/testing/ImageTests.java b/src/test/java/app/fyreplace/api/testing/ImageTestsBase.java similarity index 92% rename from src/test/java/app/fyreplace/api/testing/ImageTests.java rename to src/test/java/app/fyreplace/api/testing/ImageTestsBase.java index f5001cf..b037e04 100644 --- a/src/test/java/app/fyreplace/api/testing/ImageTests.java +++ b/src/test/java/app/fyreplace/api/testing/ImageTestsBase.java @@ -5,7 +5,7 @@ import java.io.InputStream; import java.net.URL; -public abstract class ImageTests extends TransactionalTests { +public abstract class ImageTestsBase extends TransactionalTestsBase { @TestHTTPResource("image.jpeg") URL jpeg; diff --git a/src/test/java/app/fyreplace/api/testing/TransactionalTests.java b/src/test/java/app/fyreplace/api/testing/TransactionalTestsBase.java similarity index 95% rename from src/test/java/app/fyreplace/api/testing/TransactionalTests.java rename to src/test/java/app/fyreplace/api/testing/TransactionalTestsBase.java index 43bee3a..e7aa2a1 100644 --- a/src/test/java/app/fyreplace/api/testing/TransactionalTests.java +++ b/src/test/java/app/fyreplace/api/testing/TransactionalTestsBase.java @@ -11,7 +11,7 @@ import org.junit.jupiter.api.BeforeEach; @QuarkusTestResource(DatabaseTestResource.class) -public abstract class TransactionalTests { +public abstract class TransactionalTestsBase { @Inject DataSeeder dataSeeder; diff --git a/src/test/java/app/fyreplace/api/testing/data/chapters/PositionBetweenTests.java b/src/test/java/app/fyreplace/api/testing/data/chapters/PositionBetweenTests.java index aca8075..51492e5 100644 --- a/src/test/java/app/fyreplace/api/testing/data/chapters/PositionBetweenTests.java +++ b/src/test/java/app/fyreplace/api/testing/data/chapters/PositionBetweenTests.java @@ -4,12 +4,12 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import app.fyreplace.api.data.Chapter; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.test.junit.QuarkusTest; import org.junit.jupiter.api.Test; @QuarkusTest -public final class PositionBetweenTests extends TransactionalTests { +public final class PositionBetweenTests extends TransactionalTestsBase { @Test public void positionBetweenNullAndNull() { assertEquals("z", Chapter.positionBetween(null, null)); diff --git a/src/test/java/app/fyreplace/api/testing/data/posts/NormalizeTests.java b/src/test/java/app/fyreplace/api/testing/data/posts/NormalizeTests.java index edc7346..4162f31 100644 --- a/src/test/java/app/fyreplace/api/testing/data/posts/NormalizeTests.java +++ b/src/test/java/app/fyreplace/api/testing/data/posts/NormalizeTests.java @@ -5,7 +5,7 @@ import app.fyreplace.api.data.Chapter; import app.fyreplace.api.data.Post; import app.fyreplace.api.data.User; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.test.junit.QuarkusTest; import jakarta.transaction.Transactional; import java.util.List; @@ -13,7 +13,7 @@ import org.junit.jupiter.api.Test; @QuarkusTest -public final class NormalizeTests extends TransactionalTests { +public final class NormalizeTests extends TransactionalTestsBase { private Post post; private Post emptyPost; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/PostTestsBase.java b/src/test/java/app/fyreplace/api/testing/endpoints/PostTestsBase.java index 4202345..eb3a8c9 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/PostTestsBase.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/PostTestsBase.java @@ -2,12 +2,12 @@ import app.fyreplace.api.data.Post; import app.fyreplace.api.data.dev.DataSeeder; -import app.fyreplace.api.testing.ImageTests; +import app.fyreplace.api.testing.ImageTestsBase; import jakarta.inject.Inject; import jakarta.transaction.Transactional; import org.junit.jupiter.api.BeforeEach; -public abstract class PostTestsBase extends ImageTests { +public abstract class PostTestsBase extends ImageTestsBase { @Inject DataSeeder dataSeeder; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java index e411fd4..2c48745 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java @@ -10,7 +10,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.EmailsEndpoint; import app.fyreplace.api.services.RandomService; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -22,7 +22,7 @@ @QuarkusTest @TestHTTPEndpoint(EmailsEndpoint.class) -public final class ActivateTests extends TransactionalTests { +public final class ActivateTests extends TransactionalTestsBase { @Inject RandomService randomService; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CountTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CountTests.java index 5a7feb4..24c442a 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CountTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CountTests.java @@ -8,7 +8,7 @@ import app.fyreplace.api.data.Email; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.EmailsEndpoint; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -18,7 +18,7 @@ @QuarkusTest @TestHTTPEndpoint(EmailsEndpoint.class) -public class CountTests extends TransactionalTests { +public class CountTests extends TransactionalTestsBase { @Test @TestSecurity(user = "user_0") public void count() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java index 32c934a..2ba4488 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java @@ -8,7 +8,7 @@ import app.fyreplace.api.data.EmailCreation; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.EmailsEndpoint; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -17,7 +17,7 @@ @QuarkusTest @TestHTTPEndpoint(EmailsEndpoint.class) -public final class CreateTests extends TransactionalTests { +public final class CreateTests extends TransactionalTestsBase { @Test @TestSecurity(user = "user_0") public void create() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java index f426a8d..fe3de91 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java @@ -6,7 +6,7 @@ import app.fyreplace.api.data.Email; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.EmailsEndpoint; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -16,7 +16,7 @@ @QuarkusTest @TestHTTPEndpoint(EmailsEndpoint.class) -public final class DeleteTests extends TransactionalTests { +public final class DeleteTests extends TransactionalTestsBase { private Email newEmail; @Test diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java index 98f0a3c..269dbad 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java @@ -9,7 +9,7 @@ import app.fyreplace.api.data.Email; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.EmailsEndpoint; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -21,7 +21,7 @@ @QuarkusTest @TestHTTPEndpoint(EmailsEndpoint.class) -public final class ListTests extends TransactionalTests { +public final class ListTests extends TransactionalTestsBase { @ConfigProperty(name = "app.paging.size") int pagingSize; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java index c55a0a0..4331788 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java @@ -8,7 +8,7 @@ import app.fyreplace.api.data.Email; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.EmailsEndpoint; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; @@ -19,7 +19,7 @@ @QuarkusTest @TestHTTPEndpoint(EmailsEndpoint.class) -public final class SetMainTests extends TransactionalTests { +public final class SetMainTests extends TransactionalTestsBase { private Email secondaryEmail; @Test diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateTests.java index 5fe787a..b07603d 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateTests.java @@ -8,7 +8,7 @@ import app.fyreplace.api.data.Post; import app.fyreplace.api.endpoints.PostsEndpoint; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -16,7 +16,7 @@ @QuarkusTest @TestHTTPEndpoint(PostsEndpoint.class) -public final class CreateTests extends TransactionalTests { +public final class CreateTests extends TransactionalTestsBase { @Test @TestSecurity(user = "user_0") public void create() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java index 5ab798f..a868430 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java @@ -10,7 +10,7 @@ import app.fyreplace.api.data.dev.DataSeeder; import app.fyreplace.api.endpoints.PostsEndpoint; import app.fyreplace.api.endpoints.PostsEndpoint.PostListingType; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; @@ -25,7 +25,7 @@ @QuarkusTest @TestHTTPEndpoint(PostsEndpoint.class) -public final class ListTests extends TransactionalTests { +public final class ListTests extends TransactionalTestsBase { @ConfigProperty(name = "app.paging.size") int pagingSize; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTests.java index 20d8ccd..ad0df91 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTests.java @@ -9,7 +9,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.emails.UserConnectionEmail; import app.fyreplace.api.endpoints.TokensEndpoint; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.restassured.http.ContentType; @@ -17,7 +17,7 @@ @QuarkusTest @TestHTTPEndpoint(TokensEndpoint.class) -public final class CreateNewTests extends TransactionalTests { +public final class CreateNewTests extends TransactionalTestsBase { @Test public void createNewWithUsername() { final var user = requireNonNull(User.findByUsername("user_0")); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java index 80a1581..881be87 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java @@ -13,7 +13,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.TokensEndpoint; import app.fyreplace.api.services.RandomService; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.restassured.http.ContentType; @@ -24,7 +24,7 @@ @QuarkusTest @TestHTTPEndpoint(TokensEndpoint.class) -public final class CreateTests extends TransactionalTests { +public final class CreateTests extends TransactionalTestsBase { @Inject RandomService randomService; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/RetrieveNewTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/RetrieveNewTests.java index 97d6cc5..4d4efac 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/RetrieveNewTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/RetrieveNewTests.java @@ -4,7 +4,7 @@ import static org.hamcrest.Matchers.isA; import app.fyreplace.api.endpoints.TokensEndpoint; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -13,7 +13,7 @@ @QuarkusTest @TestHTTPEndpoint(TokensEndpoint.class) -public final class RetrieveNewTests extends TransactionalTests { +public final class RetrieveNewTests extends TransactionalTestsBase { @Test @TestSecurity(user = "user_0") public void retrieveNew() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java index 8764df4..90b7d88 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java @@ -8,7 +8,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -18,7 +18,7 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class BannedTests extends TransactionalTests { +public final class BannedTests extends TransactionalTestsBase { @Test @TestSecurity(user = "user_0", roles = "ADMINISTRATOR") @Transactional diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java index 32bddb1..699047a 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java @@ -6,7 +6,7 @@ import app.fyreplace.api.data.Block; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -16,7 +16,7 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public class CountBlockedTests extends TransactionalTests { +public class CountBlockedTests extends TransactionalTestsBase { @Test @TestSecurity(user = "user_0") public void countBlocked() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java index 5867215..3ce3ae4 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java @@ -10,7 +10,7 @@ import app.fyreplace.api.data.Post; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; @@ -19,7 +19,7 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class CreateBlockTests extends TransactionalTests { +public final class CreateBlockTests extends TransactionalTestsBase { @Test @TestSecurity(user = "user_0") public void createBlock() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java index eb5f1b8..dcc9b30 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java @@ -14,7 +14,7 @@ import app.fyreplace.api.data.UserCreation; import app.fyreplace.api.emails.UserActivationEmail; import app.fyreplace.api.endpoints.UsersEndpoint; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.restassured.http.ContentType; @@ -22,7 +22,7 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class CreateTests extends TransactionalTests { +public final class CreateTests extends TransactionalTestsBase { @Test public void create() { final var userCount = User.count(); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java index ddd26d5..b8ef461 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java @@ -9,7 +9,7 @@ import app.fyreplace.api.data.Block; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -19,7 +19,7 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class DeleteBlockTests extends TransactionalTests { +public final class DeleteBlockTests extends TransactionalTestsBase { @Test @TestSecurity(user = "user_0") public void deleteBlock() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeAvatarTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeAvatarTests.java index ce06a4d..a899fa2 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeAvatarTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeAvatarTests.java @@ -5,7 +5,7 @@ import app.fyreplace.api.data.StoredFile; import app.fyreplace.api.endpoints.UsersEndpoint; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.common.http.TestHTTPResource; import io.quarkus.test.junit.QuarkusTest; @@ -17,7 +17,7 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class DeleteMeAvatarTests extends TransactionalTests { +public final class DeleteMeAvatarTests extends TransactionalTestsBase { @TestHTTPResource("image.jpeg") URL jpeg; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeTests.java index f4681dd..abcefe0 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeTests.java @@ -6,7 +6,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -14,7 +14,7 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class DeleteMeTests extends TransactionalTests { +public final class DeleteMeTests extends TransactionalTestsBase { @Test @TestSecurity(user = "user_0") public void deleteMe() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java index 34f9c63..60005be 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java @@ -8,7 +8,7 @@ import app.fyreplace.api.data.Block; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -20,7 +20,7 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class ListBlockedTests extends TransactionalTests { +public final class ListBlockedTests extends TransactionalTestsBase { @ConfigProperty(name = "app.paging.size") int pagingSize; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java index ffc349a..5775848 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java @@ -8,7 +8,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -17,7 +17,7 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class RetrieveMeTests extends TransactionalTests { +public final class RetrieveMeTests extends TransactionalTestsBase { @Test @TestSecurity(user = "user_10") public void retrieveMe() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java index be32f56..880aa33 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java @@ -8,7 +8,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.restassured.http.ContentType; @@ -17,7 +17,7 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class RetrieveTests extends TransactionalTests { +public final class RetrieveTests extends TransactionalTestsBase { @ParameterizedTest @ValueSource(strings = {"user_0", "user_12"}) public void retrieve(final String username) { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java index 35f90a1..8cd52ab 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java @@ -9,7 +9,7 @@ import app.fyreplace.api.data.StoredFile; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; -import app.fyreplace.api.testing.ImageTests; +import app.fyreplace.api.testing.ImageTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -21,7 +21,7 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class UpdateMeAvatarTests extends ImageTests { +public final class UpdateMeAvatarTests extends ImageTestsBase { @ParameterizedTest @ValueSource(strings = {"jpeg", "png", "webp"}) @TestSecurity(user = "user_0") diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeBioTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeBioTests.java index 2338ebb..8901fed 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeBioTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeBioTests.java @@ -6,7 +6,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -18,7 +18,7 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class UpdateMeBioTests extends TransactionalTests { +public final class UpdateMeBioTests extends TransactionalTestsBase { @ParameterizedTest @ValueSource(strings = {"Test", "Some random bio", ""}) @TestSecurity(user = "user_0") diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/dev/RetrieveTokenTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/dev/RetrieveTokenTests.java index 4dab984..cdb552d 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/dev/RetrieveTokenTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/dev/RetrieveTokenTests.java @@ -3,14 +3,14 @@ import static io.restassured.RestAssured.given; import app.fyreplace.api.endpoints.DevUsersEndpoint; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import org.junit.jupiter.api.Test; @QuarkusTest @TestHTTPEndpoint(DevUsersEndpoint.class) -public final class RetrieveTokenTests extends TransactionalTests { +public final class RetrieveTokenTests extends TransactionalTestsBase { @Test public void retrieveToken() { given().get("user_0/token").then().statusCode(401); diff --git a/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldInactiveUsersTests.java b/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldInactiveUsersTests.java index b5763df..28faf08 100644 --- a/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldInactiveUsersTests.java +++ b/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldInactiveUsersTests.java @@ -4,7 +4,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.tasks.CleanupTasks; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; import jakarta.transaction.Transactional; @@ -13,7 +13,7 @@ import org.junit.jupiter.api.Test; @QuarkusTest -public final class RemoveOldInactiveUsersTests extends TransactionalTests { +public final class RemoveOldInactiveUsersTests extends TransactionalTestsBase { @Inject CleanupTasks cleanupTasks; diff --git a/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldRandomCodesTests.java b/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldRandomCodesTests.java index 7585bec..85250b9 100644 --- a/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldRandomCodesTests.java +++ b/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldRandomCodesTests.java @@ -7,7 +7,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.services.RandomService; import app.fyreplace.api.tasks.CleanupTasks; -import app.fyreplace.api.testing.TransactionalTests; +import app.fyreplace.api.testing.TransactionalTestsBase; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; import jakarta.transaction.Transactional; @@ -16,7 +16,7 @@ import org.junit.jupiter.api.Test; @QuarkusTest -public final class RemoveOldRandomCodesTests extends TransactionalTests { +public final class RemoveOldRandomCodesTests extends TransactionalTestsBase { @Inject RandomService randomService; From 661e6ee9a4d62f0b5180a691a0313e9b409bf07c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 8 Oct 2023 16:21:51 +0200 Subject: [PATCH 063/157] Delete everything after every test --- .../java/app/fyreplace/api/data/dev/DataSeeder.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java b/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java index aa0f550..3f107c8 100644 --- a/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java +++ b/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java @@ -2,13 +2,16 @@ import static java.util.stream.IntStream.range; +import app.fyreplace.api.data.Block; import app.fyreplace.api.data.Chapter; import app.fyreplace.api.data.Comment; import app.fyreplace.api.data.Email; import app.fyreplace.api.data.Post; import app.fyreplace.api.data.RandomCode; import app.fyreplace.api.data.StoredFile; +import app.fyreplace.api.data.Subscription; import app.fyreplace.api.data.User; +import app.fyreplace.api.data.Vote; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.ShutdownEvent; import io.quarkus.runtime.StartupEvent; @@ -55,9 +58,17 @@ public void insertData() { public void deleteData() { Email.deleteAll(); RandomCode.deleteAll(); + Block.deleteAll(); User.deleteAll(); + Subscription.deleteAll(); + Vote.deleteAll(); + Chapter.deleteAll(); Post.deleteAll(); - StoredFile.streamAll().forEach(StoredFile::delete); + Comment.deleteAll(); + + try (final var stream = StoredFile.streamAll()) { + stream.forEach(StoredFile::delete); + } } @Transactional(Transactional.TxType.REQUIRES_NEW) From 5c9b4a198f4f7d3a494a37e1b9dde68696837ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 8 Oct 2023 18:41:28 +0200 Subject: [PATCH 064/157] Ignore inactive users --- .../api/endpoints/UsersEndpoint.java | 33 +++++++++---------- .../testing/endpoints/users/BannedTests.java | 18 +++++----- .../endpoints/users/CreateBlockTests.java | 10 ++++++ .../endpoints/users/DeleteBlockTests.java | 10 ++++++ .../endpoints/users/RetrieveTests.java | 9 ++++- 5 files changed, 52 insertions(+), 28 deletions(-) diff --git a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java index 6fb1cdd..7c18c0c 100644 --- a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java @@ -12,6 +12,7 @@ import app.fyreplace.api.services.mimetype.KnownMimeTypes; import io.quarkus.panache.common.Sort; import io.quarkus.security.Authenticated; +import jakarta.annotation.Nullable; import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; import jakarta.persistence.LockModeType; @@ -95,12 +96,8 @@ public Response create(@Valid @NotNull final UserCreation input) { @APIResponse(responseCode = "404") public User retrieve(@PathParam("id") final UUID id) { final var user = User.findById(id); - - if (user == null) { - throw new NotFoundException(); - } else { - return user; - } + validateUser(user); + return user; } @PUT @@ -112,10 +109,9 @@ public User retrieve(@PathParam("id") final UUID id) { public Response createBlock(@PathParam("id") final UUID id) { final var source = User.getFromSecurityContext(context); final var target = User.findById(id); + validateUser(target); - if (target == null) { - throw new NotFoundException(); - } else if (source.id.equals(target.id)) { + if (source.id.equals(target.id)) { throw new ForbiddenException("user_is_self"); } else { source.block(target); @@ -133,11 +129,7 @@ public Response createBlock(@PathParam("id") final UUID id) { public void deleteBlock(@PathParam("id") final UUID id) { final var source = User.getFromSecurityContext(context); final var target = User.findById(id); - - if (target == null) { - throw new NotFoundException(); - } - + validateUser(target); source.unblock(target); } @@ -149,10 +141,9 @@ public void deleteBlock(@PathParam("id") final UUID id) { @APIResponse(responseCode = "404") public Response updateBanned(@PathParam("id") final UUID id) { final var user = User.findById(id, LockModeType.PESSIMISTIC_WRITE); + validateUser(user); - if (user == null) { - throw new NotFoundException(); - } else if (!user.banned) { + if (!user.banned) { if (user.banCount == User.BanCount.NEVER) { user.dateBanEnd = Instant.now().plus(Duration.ofDays(7)); user.banCount = User.BanCount.ONCE; @@ -188,7 +179,7 @@ public String updateMeBio(@NotNull @Length(max = 3000) final String input) { final var user = User.getFromSecurityContext(context, LockModeType.PESSIMISTIC_READ); user.bio = input; user.persist(); - return user.bio; + return input; } @PUT @@ -259,4 +250,10 @@ public Iterable listBlocked(@QueryParam("page") @PositiveOrZero fi public long countBlocked() { return Block.count("source", User.getFromSecurityContext(context)); } + + private void validateUser(@Nullable User user) { + if (user == null || !user.active) { + throw new NotFoundException(); + } + } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java index 90b7d88..5592d12 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java @@ -22,7 +22,7 @@ public final class BannedTests extends TransactionalTestsBase { @Test @TestSecurity(user = "user_0", roles = "ADMINISTRATOR") @Transactional - public void banWithAdministrator() { + public void updateBannedWithAdministrator() { final var user = requireNonNull(User.findByUsername("user_1")); given().put(user.id + "/banned").then().statusCode(200); user.refresh(); @@ -33,7 +33,7 @@ public void banWithAdministrator() { @Test @TestSecurity(user = "user_0", roles = "MODERATOR") @Transactional - public void banWithModerator() { + public void updateBannedWithModerator() { final var user = requireNonNull(User.findByUsername("user_1")); given().put(user.id + "/banned").then().statusCode(200); user.refresh(); @@ -44,7 +44,7 @@ public void banWithModerator() { @Test @TestSecurity(user = "user_0") @Transactional - public void banWithUser() { + public void updateBannedWithUser() { final var user = requireNonNull(User.findByUsername("user_1")); given().put(user.id + "/banned").then().statusCode(403); user.refresh(); @@ -54,7 +54,7 @@ public void banWithUser() { @Test @Transactional - public void banUnauthenticated() { + public void updateBannedUnauthenticated() { final var user = requireNonNull(User.findByUsername("user_1")); given().put(user.id + "/banned").then().statusCode(401); user.refresh(); @@ -65,7 +65,7 @@ public void banUnauthenticated() { @Test @TestSecurity(user = "user_0", roles = "ADMINISTRATOR") @Transactional - public void banTwiceWithAdministrator() { + public void updateBannedTwiceWithAdministrator() { final var user = requireNonNull(User.findByUsername("user_2")); given().put(user.id + "/banned").then().statusCode(200); user.refresh(); @@ -76,7 +76,7 @@ public void banTwiceWithAdministrator() { @Test @TestSecurity(user = "user_0", roles = "MODERATOR") @Transactional - public void banTwiceWithModerator() { + public void updateBannedTwiceWithModerator() { final var user = requireNonNull(User.findByUsername("user_2")); given().put(user.id + "/banned").then().statusCode(200); user.refresh(); @@ -87,7 +87,7 @@ public void banTwiceWithModerator() { @Test @TestSecurity(user = "user_0") @Transactional - public void banTwiceWithUser() { + public void updateBannedTwiceWithUser() { final var user = requireNonNull(User.findByUsername("user_2")); given().put(user.id + "/banned").then().statusCode(403); user.refresh(); @@ -97,7 +97,7 @@ public void banTwiceWithUser() { @Test @Transactional - public void banTwiceUnauthenticated() { + public void updateBannedTwiceUnauthenticated() { final var user = requireNonNull(User.findByUsername("user_2")); given().put(user.id + "/banned").then().statusCode(401); user.refresh(); @@ -108,7 +108,7 @@ public void banTwiceUnauthenticated() { @Test @TestSecurity(user = "user_0", roles = "ADMINISTRATOR") @Transactional - public void banAlreadyBanned() { + public void updateBannedAlreadyBanned() { final var user = requireNonNull(User.findByUsername("user_3")); given().put(user.id + "/banned").then().statusCode(200); user.refresh(); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java index 3ce3ae4..0903814 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java @@ -44,6 +44,16 @@ public void createBlockTwice() { assertTrue(user.isBlocking(otherUser)); } + @Test + @TestSecurity(user = "user_0") + public void createBlockWithInactiveUser() { + final var user = User.findByUsername("user_0"); + final var otherUser = requireNonNull(User.findByUsername("user_inactive_1")); + final var blockCount = Block.count("source", user); + given().put(otherUser.id + "/blocked").then().statusCode(404); + assertEquals(blockCount, Block.count("source", user)); + } + @Test @TestSecurity(user = "user_0") public void createBlockWithInvalidUser() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java index b8ef461..147b8ff 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java @@ -41,6 +41,16 @@ public void deleteBlockTwice() { assertFalse(user.isBlocking(otherUser)); } + @Test + @TestSecurity(user = "user_0") + public void deleteBlockWithInactiveUser() { + final var user = User.findByUsername("user_0"); + final var otherUser = requireNonNull(User.findByUsername("user_inactive_1")); + final var blockCount = Block.count("source", user); + given().delete(otherUser.id + "/blocked").then().statusCode(404); + assertEquals(blockCount, Block.count("source", user)); + } + @Test @TestSecurity(user = "user_0") public void deleteBlockWithInvalidUser() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java index 880aa33..64e188a 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java @@ -19,7 +19,7 @@ @TestHTTPEndpoint(UsersEndpoint.class) public final class RetrieveTests extends TransactionalTestsBase { @ParameterizedTest - @ValueSource(strings = {"user_0", "user_12"}) + @ValueSource(strings = {"user_0", "user_1", "user_2"}) public void retrieve(final String username) { final var user = requireNonNull(User.findByUsername(username)); given().get(user.id.toString()) @@ -35,6 +35,13 @@ public void retrieve(final String username) { .body("banned", equalTo(false)); } + @ParameterizedTest + @ValueSource(strings = {"user_inactive_0", "user_inactive_1", "user_inactive_2"}) + public void retrieveInactive(final String username) { + final var user = requireNonNull(User.findByUsername(username)); + given().get(user.id.toString()).then().statusCode(404); + } + @ParameterizedTest @ValueSource(strings = {"nope", "fake", "@", "admin", "00000000-0000-0000-0000-000000000000"}) public void retrieveNonExistent(final String userId) { From 85724dfc03c8c806d766f6c765d4bff1dfa5c973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 8 Oct 2023 18:43:01 +0200 Subject: [PATCH 065/157] Run tests without base DataSeeder data --- .../api/testing/CommentTestsBase.java | 21 +++++++++++ .../fyreplace/api/testing/ImageTestsBase.java | 35 ------------------ .../{endpoints => }/PostTestsBase.java | 24 ++++++++---- .../api/testing/TransactionalTestsBase.java | 37 +++++++++++++++++-- .../fyreplace/api/testing/UserTestsBase.java | 25 +++++++++++++ .../data/chapters/PositionBetweenTests.java | 4 +- .../testing/data/posts/NormalizeTests.java | 4 +- .../chapters/CreateChapterTests.java | 2 +- .../chapters/DeleteChapterTests.java | 2 +- .../chapters/UpdateChapterImageTests.java | 2 +- .../chapters/UpdateChapterPositionTests.java | 2 +- .../chapters/UpdateChapterTextTests.java | 2 +- .../endpoints/comments/AcknowledgeTests.java | 4 +- .../endpoints/comments/CountTests.java | 4 +- .../endpoints/comments/CreateTests.java | 4 +- .../endpoints/comments/DeleteTests.java | 4 +- .../testing/endpoints/comments/ListTests.java | 4 +- .../endpoints/emails/ActivateTests.java | 4 +- .../testing/endpoints/emails/CountTests.java | 4 +- .../testing/endpoints/emails/CreateTests.java | 4 +- .../testing/endpoints/emails/DeleteTests.java | 4 +- .../testing/endpoints/emails/ListTests.java | 4 +- .../endpoints/emails/SetMainTests.java | 4 +- .../testing/endpoints/posts/CountTests.java | 2 +- .../posts/CreateSubscriptionTests.java | 4 +- .../testing/endpoints/posts/CreateTests.java | 4 +- .../posts/DeleteSubscriptionTests.java | 2 +- .../testing/endpoints/posts/DeleteTests.java | 2 +- .../endpoints/posts/ListFeedTests.java | 2 +- .../testing/endpoints/posts/ListTests.java | 14 ++++++- .../testing/endpoints/posts/PublishTests.java | 2 +- .../endpoints/posts/RetrieveTests.java | 2 +- .../testing/endpoints/posts/VoteTests.java | 2 +- .../endpoints/tokens/CreateNewTests.java | 4 +- .../testing/endpoints/tokens/CreateTests.java | 4 +- .../endpoints/tokens/RetrieveNewTests.java | 4 +- .../testing/endpoints/users/BannedTests.java | 9 ++++- .../endpoints/users/CountBlockedTests.java | 4 +- .../endpoints/users/CreateBlockTests.java | 6 +-- .../testing/endpoints/users/CreateTests.java | 4 +- .../endpoints/users/DeleteBlockTests.java | 4 +- .../endpoints/users/DeleteMeAvatarTests.java | 4 +- .../endpoints/users/DeleteMeTests.java | 4 +- .../endpoints/users/ListBlockedTests.java | 11 ++++-- .../endpoints/users/RetrieveMeTests.java | 8 ++-- .../endpoints/users/RetrieveTests.java | 4 +- .../endpoints/users/UpdateMeAvatarTests.java | 4 +- .../endpoints/users/UpdateMeBioTests.java | 4 +- .../users/dev/RetrieveTokenTests.java | 4 +- .../cleanup/RemoveOldInactiveUsersTests.java | 4 +- .../cleanup/RemoveOldRandomCodesTests.java | 4 +- 51 files changed, 198 insertions(+), 132 deletions(-) create mode 100644 src/test/java/app/fyreplace/api/testing/CommentTestsBase.java delete mode 100644 src/test/java/app/fyreplace/api/testing/ImageTestsBase.java rename src/test/java/app/fyreplace/api/testing/{endpoints => }/PostTestsBase.java (54%) create mode 100644 src/test/java/app/fyreplace/api/testing/UserTestsBase.java diff --git a/src/test/java/app/fyreplace/api/testing/CommentTestsBase.java b/src/test/java/app/fyreplace/api/testing/CommentTestsBase.java new file mode 100644 index 0000000..4187604 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/CommentTestsBase.java @@ -0,0 +1,21 @@ +package app.fyreplace.api.testing; + +import static java.util.stream.IntStream.range; + +import app.fyreplace.api.data.Post; +import app.fyreplace.api.data.User; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; + +public class CommentTestsBase extends PostTestsBase { + @BeforeEach + @Transactional + @Override + public void beforeEach() { + super.beforeEach(); + final var user = User.findByUsername("user_0"); + final var post = Post.find("author = ?1 and published = true", Post.sorting(), user) + .firstResult(); + range(0, 10).forEach(i -> dataSeeder.createComment(user, post, "Comment " + i, false)); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/ImageTestsBase.java b/src/test/java/app/fyreplace/api/testing/ImageTestsBase.java deleted file mode 100644 index b037e04..0000000 --- a/src/test/java/app/fyreplace/api/testing/ImageTestsBase.java +++ /dev/null @@ -1,35 +0,0 @@ -package app.fyreplace.api.testing; - -import io.quarkus.test.common.http.TestHTTPResource; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; - -public abstract class ImageTestsBase extends TransactionalTestsBase { - @TestHTTPResource("image.jpeg") - URL jpeg; - - @TestHTTPResource("image.png") - URL png; - - @TestHTTPResource("image.webp") - URL webp; - - @TestHTTPResource("image.gif") - URL gif; - - @TestHTTPResource("image.txt") - URL text; - - protected InputStream openStream(final String fileType) throws IOException { - return (switch (fileType) { - case "jpeg" -> jpeg; - case "png" -> png; - case "webp" -> webp; - case "gif" -> gif; - case "text" -> text; - default -> throw new IllegalArgumentException("Unknown file type: " + fileType); - }) - .openStream(); - } -} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/PostTestsBase.java b/src/test/java/app/fyreplace/api/testing/PostTestsBase.java similarity index 54% rename from src/test/java/app/fyreplace/api/testing/endpoints/PostTestsBase.java rename to src/test/java/app/fyreplace/api/testing/PostTestsBase.java index eb3a8c9..2a5df93 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/PostTestsBase.java +++ b/src/test/java/app/fyreplace/api/testing/PostTestsBase.java @@ -1,16 +1,13 @@ -package app.fyreplace.api.testing.endpoints; +package app.fyreplace.api.testing; + +import static java.util.stream.IntStream.range; import app.fyreplace.api.data.Post; -import app.fyreplace.api.data.dev.DataSeeder; -import app.fyreplace.api.testing.ImageTestsBase; -import jakarta.inject.Inject; +import app.fyreplace.api.data.User; import jakarta.transaction.Transactional; import org.junit.jupiter.api.BeforeEach; -public abstract class PostTestsBase extends ImageTestsBase { - @Inject - DataSeeder dataSeeder; - +public abstract class PostTestsBase extends UserTestsBase { public Post post; public Post draft; @@ -22,10 +19,21 @@ public abstract class PostTestsBase extends ImageTestsBase { @Override public void beforeEach() { super.beforeEach(); + final var user = User.findByUsername("user_0"); + range(0, getPostCount()).forEach(i -> dataSeeder.createPost(user, "Post " + i, true, false)); + range(0, getDraftCount()).forEach(i -> dataSeeder.createPost(user, "Draft " + i, false, false)); post = Post.find("author.username = 'user_0' and published = true", Post.sorting()) .firstResult(); draft = Post.find("author.username = 'user_0' and published = false", Post.sorting()) .firstResult(); anonymousPost = dataSeeder.createPost(post.author, "Anonymous Post", true, true); } + + public int getPostCount() { + return 3; + } + + public int getDraftCount() { + return 3; + } } diff --git a/src/test/java/app/fyreplace/api/testing/TransactionalTestsBase.java b/src/test/java/app/fyreplace/api/testing/TransactionalTestsBase.java index e7aa2a1..747f19a 100644 --- a/src/test/java/app/fyreplace/api/testing/TransactionalTestsBase.java +++ b/src/test/java/app/fyreplace/api/testing/TransactionalTestsBase.java @@ -5,7 +5,11 @@ import io.quarkus.mailer.Mail; import io.quarkus.mailer.MockMailbox; import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.http.TestHTTPResource; import jakarta.inject.Inject; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; import java.util.List; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -13,17 +17,30 @@ @QuarkusTestResource(DatabaseTestResource.class) public abstract class TransactionalTestsBase { @Inject - DataSeeder dataSeeder; + public DataSeeder dataSeeder; @Inject MockMailbox mailbox; + @TestHTTPResource("image.jpeg") + URL jpeg; + + @TestHTTPResource("image.png") + URL png; + + @TestHTTPResource("image.webp") + URL webp; + + @TestHTTPResource("image.gif") + URL gif; + + @TestHTTPResource("image.txt") + URL text; + protected static final String fakeId = "00000000-0000-0000-0000-000000000000"; @BeforeEach - public void beforeEach() { - dataSeeder.insertData(); - } + public void beforeEach() {} @AfterEach public void afterEach() { @@ -34,4 +51,16 @@ public void afterEach() { protected List getMailsSentTo(final Email email) { return mailbox.getMailsSentTo(email.email); } + + protected InputStream openStream(final String fileType) throws IOException { + return (switch (fileType) { + case "jpeg" -> jpeg; + case "png" -> png; + case "webp" -> webp; + case "gif" -> gif; + case "text" -> text; + default -> throw new IllegalArgumentException("Unknown file type: " + fileType); + }) + .openStream(); + } } diff --git a/src/test/java/app/fyreplace/api/testing/UserTestsBase.java b/src/test/java/app/fyreplace/api/testing/UserTestsBase.java new file mode 100644 index 0000000..a530ae2 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/UserTestsBase.java @@ -0,0 +1,25 @@ +package app.fyreplace.api.testing; + +import static java.util.stream.IntStream.range; + +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; + +public class UserTestsBase extends TransactionalTestsBase { + @BeforeEach + @Transactional + @Override + public void beforeEach() { + super.beforeEach(); + range(0, getActiveUserCount()).forEach(i -> dataSeeder.createUser("user_" + i, true)); + range(0, getInactiveUserCount()).forEach(i -> dataSeeder.createUser("user_inactive_" + i, false)); + } + + public int getActiveUserCount() { + return 3; + } + + public int getInactiveUserCount() { + return 3; + } +} diff --git a/src/test/java/app/fyreplace/api/testing/data/chapters/PositionBetweenTests.java b/src/test/java/app/fyreplace/api/testing/data/chapters/PositionBetweenTests.java index 51492e5..be9ace3 100644 --- a/src/test/java/app/fyreplace/api/testing/data/chapters/PositionBetweenTests.java +++ b/src/test/java/app/fyreplace/api/testing/data/chapters/PositionBetweenTests.java @@ -4,12 +4,12 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import app.fyreplace.api.data.Chapter; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.PostTestsBase; import io.quarkus.test.junit.QuarkusTest; import org.junit.jupiter.api.Test; @QuarkusTest -public final class PositionBetweenTests extends TransactionalTestsBase { +public final class PositionBetweenTests extends PostTestsBase { @Test public void positionBetweenNullAndNull() { assertEquals("z", Chapter.positionBetween(null, null)); diff --git a/src/test/java/app/fyreplace/api/testing/data/posts/NormalizeTests.java b/src/test/java/app/fyreplace/api/testing/data/posts/NormalizeTests.java index 4162f31..5780cbe 100644 --- a/src/test/java/app/fyreplace/api/testing/data/posts/NormalizeTests.java +++ b/src/test/java/app/fyreplace/api/testing/data/posts/NormalizeTests.java @@ -5,7 +5,7 @@ import app.fyreplace.api.data.Chapter; import app.fyreplace.api.data.Post; import app.fyreplace.api.data.User; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.PostTestsBase; import io.quarkus.test.junit.QuarkusTest; import jakarta.transaction.Transactional; import java.util.List; @@ -13,7 +13,7 @@ import org.junit.jupiter.api.Test; @QuarkusTest -public final class NormalizeTests extends TransactionalTestsBase { +public final class NormalizeTests extends PostTestsBase { private Post post; private Post emptyPost; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/CreateChapterTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/CreateChapterTests.java index b42c557..a7b7802 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/CreateChapterTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/CreateChapterTests.java @@ -8,7 +8,7 @@ import app.fyreplace.api.data.Chapter; import app.fyreplace.api.endpoints.ChaptersEndpoint; -import app.fyreplace.api.testing.endpoints.PostTestsBase; +import app.fyreplace.api.testing.PostTestsBase; import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.panache.common.Sort; import io.quarkus.test.common.http.TestHTTPEndpoint; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/DeleteChapterTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/DeleteChapterTests.java index ec5942c..e287c0d 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/DeleteChapterTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/DeleteChapterTests.java @@ -5,7 +5,7 @@ import app.fyreplace.api.data.Chapter; import app.fyreplace.api.endpoints.ChaptersEndpoint; -import app.fyreplace.api.testing.endpoints.PostTestsBase; +import app.fyreplace.api.testing.PostTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterImageTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterImageTests.java index 59a52ba..ff8777a 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterImageTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterImageTests.java @@ -7,7 +7,7 @@ import app.fyreplace.api.data.Chapter; import app.fyreplace.api.endpoints.ChaptersEndpoint; -import app.fyreplace.api.testing.endpoints.PostTestsBase; +import app.fyreplace.api.testing.PostTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterPositionTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterPositionTests.java index 120c72b..94ca3ce 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterPositionTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterPositionTests.java @@ -5,7 +5,7 @@ import app.fyreplace.api.data.Chapter; import app.fyreplace.api.endpoints.ChaptersEndpoint; -import app.fyreplace.api.testing.endpoints.PostTestsBase; +import app.fyreplace.api.testing.PostTestsBase; import io.quarkus.panache.common.Sort; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterTextTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterTextTests.java index b7534eb..560289c 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterTextTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterTextTests.java @@ -4,7 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import app.fyreplace.api.endpoints.ChaptersEndpoint; -import app.fyreplace.api.testing.endpoints.PostTestsBase; +import app.fyreplace.api.testing.PostTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/AcknowledgeTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/AcknowledgeTests.java index d5e18b4..13ccc4d 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/comments/AcknowledgeTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/AcknowledgeTests.java @@ -6,7 +6,7 @@ import app.fyreplace.api.data.Comment; import app.fyreplace.api.data.Subscription; import app.fyreplace.api.endpoints.CommentsEndpoint; -import app.fyreplace.api.testing.endpoints.PostTestsBase; +import app.fyreplace.api.testing.CommentTestsBase; import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; @@ -15,7 +15,7 @@ @QuarkusTest @TestHTTPEndpoint(CommentsEndpoint.class) -public class AcknowledgeTests extends PostTestsBase { +public class AcknowledgeTests extends CommentTestsBase { @Test @TestSecurity(user = "user_0") public void acknowledge() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/CountTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/CountTests.java index a7859c1..4e04da2 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/comments/CountTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/CountTests.java @@ -11,7 +11,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.data.dev.DataSeeder; import app.fyreplace.api.endpoints.CommentsEndpoint; -import app.fyreplace.api.testing.endpoints.PostTestsBase; +import app.fyreplace.api.testing.CommentTestsBase; import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; @@ -23,7 +23,7 @@ @QuarkusTest @TestHTTPEndpoint(CommentsEndpoint.class) -public class CountTests extends PostTestsBase { +public class CountTests extends CommentTestsBase { @Inject DataSeeder dataSeeder; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/CreateTests.java index 6a9e22c..be3ffdf 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/comments/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/CreateTests.java @@ -9,7 +9,7 @@ import app.fyreplace.api.data.Post; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.CommentsEndpoint; -import app.fyreplace.api.testing.endpoints.PostTestsBase; +import app.fyreplace.api.testing.CommentTestsBase; import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; @@ -19,7 +19,7 @@ @QuarkusTest @TestHTTPEndpoint(CommentsEndpoint.class) -public class CreateTests extends PostTestsBase { +public class CreateTests extends CommentTestsBase { @Test @TestSecurity(user = "user_0") public void createOnOwnPost() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/DeleteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/DeleteTests.java index 4410f1b..b9b35b4 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/comments/DeleteTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/DeleteTests.java @@ -9,7 +9,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.data.dev.DataSeeder; import app.fyreplace.api.endpoints.CommentsEndpoint; -import app.fyreplace.api.testing.endpoints.PostTestsBase; +import app.fyreplace.api.testing.CommentTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -20,7 +20,7 @@ @QuarkusTest @TestHTTPEndpoint(CommentsEndpoint.class) -public class DeleteTests extends PostTestsBase { +public class DeleteTests extends CommentTestsBase { @Inject DataSeeder dataSeeder; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/ListTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/ListTests.java index e350f8a..9beb757 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/comments/ListTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/ListTests.java @@ -11,7 +11,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.data.dev.DataSeeder; import app.fyreplace.api.endpoints.CommentsEndpoint; -import app.fyreplace.api.testing.endpoints.PostTestsBase; +import app.fyreplace.api.testing.CommentTestsBase; import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; @@ -27,7 +27,7 @@ @QuarkusTest @TestHTTPEndpoint(CommentsEndpoint.class) -public class ListTests extends PostTestsBase { +public class ListTests extends CommentTestsBase { @ConfigProperty(name = "app.paging.size") int pagingSize; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java index 2c48745..3d08e1e 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java @@ -10,7 +10,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.EmailsEndpoint; import app.fyreplace.api.services.RandomService; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.UserTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -22,7 +22,7 @@ @QuarkusTest @TestHTTPEndpoint(EmailsEndpoint.class) -public final class ActivateTests extends TransactionalTestsBase { +public final class ActivateTests extends UserTestsBase { @Inject RandomService randomService; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CountTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CountTests.java index 24c442a..02842cf 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CountTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CountTests.java @@ -8,7 +8,7 @@ import app.fyreplace.api.data.Email; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.EmailsEndpoint; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.UserTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -18,7 +18,7 @@ @QuarkusTest @TestHTTPEndpoint(EmailsEndpoint.class) -public class CountTests extends TransactionalTestsBase { +public class CountTests extends UserTestsBase { @Test @TestSecurity(user = "user_0") public void count() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java index 2ba4488..ed13a80 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java @@ -8,7 +8,7 @@ import app.fyreplace.api.data.EmailCreation; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.EmailsEndpoint; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.UserTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -17,7 +17,7 @@ @QuarkusTest @TestHTTPEndpoint(EmailsEndpoint.class) -public final class CreateTests extends TransactionalTestsBase { +public final class CreateTests extends UserTestsBase { @Test @TestSecurity(user = "user_0") public void create() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java index fe3de91..f1b0996 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java @@ -6,7 +6,7 @@ import app.fyreplace.api.data.Email; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.EmailsEndpoint; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.UserTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -16,7 +16,7 @@ @QuarkusTest @TestHTTPEndpoint(EmailsEndpoint.class) -public final class DeleteTests extends TransactionalTestsBase { +public final class DeleteTests extends UserTestsBase { private Email newEmail; @Test diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java index 269dbad..35598ce 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java @@ -9,7 +9,7 @@ import app.fyreplace.api.data.Email; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.EmailsEndpoint; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.UserTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -21,7 +21,7 @@ @QuarkusTest @TestHTTPEndpoint(EmailsEndpoint.class) -public final class ListTests extends TransactionalTestsBase { +public final class ListTests extends UserTestsBase { @ConfigProperty(name = "app.paging.size") int pagingSize; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java index 4331788..dd21c63 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java @@ -8,7 +8,7 @@ import app.fyreplace.api.data.Email; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.EmailsEndpoint; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.UserTestsBase; import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; @@ -19,7 +19,7 @@ @QuarkusTest @TestHTTPEndpoint(EmailsEndpoint.class) -public final class SetMainTests extends TransactionalTestsBase { +public final class SetMainTests extends UserTestsBase { private Email secondaryEmail; @Test diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/CountTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/CountTests.java index e36bf07..32dda63 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/CountTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/CountTests.java @@ -8,7 +8,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.data.dev.DataSeeder; import app.fyreplace.api.endpoints.PostsEndpoint; -import app.fyreplace.api.testing.endpoints.PostTestsBase; +import app.fyreplace.api.testing.PostTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateSubscriptionTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateSubscriptionTests.java index 9675522..a8763a3 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateSubscriptionTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateSubscriptionTests.java @@ -12,7 +12,7 @@ import app.fyreplace.api.data.Subscription; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.PostsEndpoint; -import app.fyreplace.api.testing.endpoints.PostTestsBase; +import app.fyreplace.api.testing.CommentTestsBase; import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; @@ -23,7 +23,7 @@ @QuarkusTest @TestHTTPEndpoint(PostsEndpoint.class) -public final class CreateSubscriptionTests extends PostTestsBase { +public final class CreateSubscriptionTests extends CommentTestsBase { @Test @TestSecurity(user = "user_1") public void createSubscriptionWithOtherPost() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateTests.java index b07603d..f01176e 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateTests.java @@ -8,7 +8,7 @@ import app.fyreplace.api.data.Post; import app.fyreplace.api.endpoints.PostsEndpoint; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.PostTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -16,7 +16,7 @@ @QuarkusTest @TestHTTPEndpoint(PostsEndpoint.class) -public final class CreateTests extends TransactionalTestsBase { +public final class CreateTests extends PostTestsBase { @Test @TestSecurity(user = "user_0") public void create() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteSubscriptionTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteSubscriptionTests.java index 6a231cb..0b1cbfb 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteSubscriptionTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteSubscriptionTests.java @@ -9,7 +9,7 @@ import app.fyreplace.api.data.Subscription; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.PostsEndpoint; -import app.fyreplace.api.testing.endpoints.PostTestsBase; +import app.fyreplace.api.testing.PostTestsBase; import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteTests.java index 414b4fd..6c6660d 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteTests.java @@ -5,7 +5,7 @@ import app.fyreplace.api.data.Post; import app.fyreplace.api.endpoints.PostsEndpoint; -import app.fyreplace.api.testing.endpoints.PostTestsBase; +import app.fyreplace.api.testing.PostTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListFeedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListFeedTests.java index 54675cd..575f353 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListFeedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListFeedTests.java @@ -11,7 +11,7 @@ import app.fyreplace.api.data.Vote; import app.fyreplace.api.data.dev.DataSeeder; import app.fyreplace.api.endpoints.PostsEndpoint; -import app.fyreplace.api.testing.endpoints.PostTestsBase; +import app.fyreplace.api.testing.PostTestsBase; import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java index a868430..a9106da 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java @@ -10,7 +10,7 @@ import app.fyreplace.api.data.dev.DataSeeder; import app.fyreplace.api.endpoints.PostsEndpoint; import app.fyreplace.api.endpoints.PostsEndpoint.PostListingType; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.PostTestsBase; import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; @@ -25,7 +25,7 @@ @QuarkusTest @TestHTTPEndpoint(PostsEndpoint.class) -public final class ListTests extends TransactionalTestsBase { +public final class ListTests extends PostTestsBase { @ConfigProperty(name = "app.paging.size") int pagingSize; @@ -112,4 +112,14 @@ public void makeSubscribedToPosts() { stream.forEach(post -> subscribedToPostIds.add(post.id.toString())); } } + + @Override + public int getPostCount() { + return 20; + } + + @Override + public int getDraftCount() { + return 20; + } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/PublishTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/PublishTests.java index 63fe45f..ba065de 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/PublishTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/PublishTests.java @@ -7,7 +7,7 @@ import app.fyreplace.api.data.Post; import app.fyreplace.api.data.PostPublication; import app.fyreplace.api.endpoints.PostsEndpoint; -import app.fyreplace.api.testing.endpoints.PostTestsBase; +import app.fyreplace.api.testing.PostTestsBase; import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java index 3f95e15..f6831c8 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java @@ -8,7 +8,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.data.Vote; import app.fyreplace.api.endpoints.PostsEndpoint; -import app.fyreplace.api.testing.endpoints.PostTestsBase; +import app.fyreplace.api.testing.PostTestsBase; import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/VoteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/VoteTests.java index a18defe..620f314 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/VoteTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/VoteTests.java @@ -10,7 +10,7 @@ import app.fyreplace.api.data.Vote; import app.fyreplace.api.data.VoteCreation; import app.fyreplace.api.endpoints.PostsEndpoint; -import app.fyreplace.api.testing.endpoints.PostTestsBase; +import app.fyreplace.api.testing.PostTestsBase; import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTests.java index ad0df91..fb87f1b 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTests.java @@ -9,7 +9,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.emails.UserConnectionEmail; import app.fyreplace.api.endpoints.TokensEndpoint; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.UserTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.restassured.http.ContentType; @@ -17,7 +17,7 @@ @QuarkusTest @TestHTTPEndpoint(TokensEndpoint.class) -public final class CreateNewTests extends TransactionalTestsBase { +public final class CreateNewTests extends UserTestsBase { @Test public void createNewWithUsername() { final var user = requireNonNull(User.findByUsername("user_0")); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java index 881be87..43ad705 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java @@ -13,7 +13,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.TokensEndpoint; import app.fyreplace.api.services.RandomService; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.UserTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.restassured.http.ContentType; @@ -24,7 +24,7 @@ @QuarkusTest @TestHTTPEndpoint(TokensEndpoint.class) -public final class CreateTests extends TransactionalTestsBase { +public final class CreateTests extends UserTestsBase { @Inject RandomService randomService; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/RetrieveNewTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/RetrieveNewTests.java index 4d4efac..ae844ae 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/RetrieveNewTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/RetrieveNewTests.java @@ -4,7 +4,7 @@ import static org.hamcrest.Matchers.isA; import app.fyreplace.api.endpoints.TokensEndpoint; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.UserTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -13,7 +13,7 @@ @QuarkusTest @TestHTTPEndpoint(TokensEndpoint.class) -public final class RetrieveNewTests extends TransactionalTestsBase { +public final class RetrieveNewTests extends UserTestsBase { @Test @TestSecurity(user = "user_0") public void retrieveNew() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java index 5592d12..97a28ba 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java @@ -8,7 +8,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.UserTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -18,7 +18,7 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class BannedTests extends TransactionalTestsBase { +public final class BannedTests extends UserTestsBase { @Test @TestSecurity(user = "user_0", roles = "ADMINISTRATOR") @Transactional @@ -131,4 +131,9 @@ public void beforeEach() { user.banCount = User.BanCount.ONCE; user.persist(); } + + @Override + public int getActiveUserCount() { + return 20; + } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java index 699047a..3909ceb 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java @@ -6,7 +6,7 @@ import app.fyreplace.api.data.Block; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.UserTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -16,7 +16,7 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public class CountBlockedTests extends TransactionalTestsBase { +public class CountBlockedTests extends UserTestsBase { @Test @TestSecurity(user = "user_0") public void countBlocked() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java index 0903814..d700518 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java @@ -7,10 +7,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import app.fyreplace.api.data.Block; -import app.fyreplace.api.data.Post; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.PostTestsBase; import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; @@ -19,13 +18,12 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class CreateBlockTests extends TransactionalTestsBase { +public final class CreateBlockTests extends PostTestsBase { @Test @TestSecurity(user = "user_0") public void createBlock() { final var user = requireNonNull(User.findByUsername("user_0")); final var otherUser = requireNonNull(User.findByUsername("user_1")); - final var post = Post.find("author", user).firstResult(); QuarkusTransaction.requiringNew().run(() -> otherUser.subscribeTo(post)); assertFalse(user.isBlocking(otherUser)); given().put(otherUser.id + "/blocked").then().statusCode(200); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java index dcc9b30..ca92004 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java @@ -14,7 +14,7 @@ import app.fyreplace.api.data.UserCreation; import app.fyreplace.api.emails.UserActivationEmail; import app.fyreplace.api.endpoints.UsersEndpoint; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.UserTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.restassured.http.ContentType; @@ -22,7 +22,7 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class CreateTests extends TransactionalTestsBase { +public final class CreateTests extends UserTestsBase { @Test public void create() { final var userCount = User.count(); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java index 147b8ff..215350c 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java @@ -9,7 +9,7 @@ import app.fyreplace.api.data.Block; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.UserTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -19,7 +19,7 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class DeleteBlockTests extends TransactionalTestsBase { +public final class DeleteBlockTests extends UserTestsBase { @Test @TestSecurity(user = "user_0") public void deleteBlock() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeAvatarTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeAvatarTests.java index a899fa2..7ba2932 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeAvatarTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeAvatarTests.java @@ -5,7 +5,7 @@ import app.fyreplace.api.data.StoredFile; import app.fyreplace.api.endpoints.UsersEndpoint; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.UserTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.common.http.TestHTTPResource; import io.quarkus.test.junit.QuarkusTest; @@ -17,7 +17,7 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class DeleteMeAvatarTests extends TransactionalTestsBase { +public final class DeleteMeAvatarTests extends UserTestsBase { @TestHTTPResource("image.jpeg") URL jpeg; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeTests.java index abcefe0..7918131 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeTests.java @@ -6,7 +6,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.UserTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -14,7 +14,7 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class DeleteMeTests extends TransactionalTestsBase { +public final class DeleteMeTests extends UserTestsBase { @Test @TestSecurity(user = "user_0") public void deleteMe() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java index 60005be..c7e6e72 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java @@ -8,7 +8,7 @@ import app.fyreplace.api.data.Block; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.UserTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -20,7 +20,7 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class ListBlockedTests extends TransactionalTestsBase { +public final class ListBlockedTests extends UserTestsBase { @ConfigProperty(name = "app.paging.size") int pagingSize; @@ -65,8 +65,13 @@ public void beforeEach() { super.beforeEach(); final var user = User.findByUsername("user_0"); - for (final var otherUser : User.list("username > 'user_10'")) { + for (final var otherUser : User.list("username > 'user_10' and active = true")) { user.block(otherUser); } } + + @Override + public int getActiveUserCount() { + return 20; + } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java index 5775848..d235d99 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java @@ -8,7 +8,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.UserTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -17,11 +17,11 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class RetrieveMeTests extends TransactionalTestsBase { +public final class RetrieveMeTests extends UserTestsBase { @Test - @TestSecurity(user = "user_10") + @TestSecurity(user = "user_2") public void retrieveMe() { - final var user = requireNonNull(User.findByUsername("user_10")); + final var user = requireNonNull(User.findByUsername("user_2")); given().get("/me") .then() .contentType(ContentType.JSON) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java index 64e188a..58366cc 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java @@ -8,7 +8,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.UserTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.restassured.http.ContentType; @@ -17,7 +17,7 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class RetrieveTests extends TransactionalTestsBase { +public final class RetrieveTests extends UserTestsBase { @ParameterizedTest @ValueSource(strings = {"user_0", "user_1", "user_2"}) public void retrieve(final String username) { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java index 8cd52ab..3444c9f 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java @@ -9,7 +9,7 @@ import app.fyreplace.api.data.StoredFile; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; -import app.fyreplace.api.testing.ImageTestsBase; +import app.fyreplace.api.testing.UserTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -21,7 +21,7 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class UpdateMeAvatarTests extends ImageTestsBase { +public final class UpdateMeAvatarTests extends UserTestsBase { @ParameterizedTest @ValueSource(strings = {"jpeg", "png", "webp"}) @TestSecurity(user = "user_0") diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeBioTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeBioTests.java index 8901fed..c7166e7 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeBioTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeBioTests.java @@ -6,7 +6,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.UserTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -18,7 +18,7 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class UpdateMeBioTests extends TransactionalTestsBase { +public final class UpdateMeBioTests extends UserTestsBase { @ParameterizedTest @ValueSource(strings = {"Test", "Some random bio", ""}) @TestSecurity(user = "user_0") diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/dev/RetrieveTokenTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/dev/RetrieveTokenTests.java index cdb552d..d4b0d51 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/dev/RetrieveTokenTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/dev/RetrieveTokenTests.java @@ -3,14 +3,14 @@ import static io.restassured.RestAssured.given; import app.fyreplace.api.endpoints.DevUsersEndpoint; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.UserTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import org.junit.jupiter.api.Test; @QuarkusTest @TestHTTPEndpoint(DevUsersEndpoint.class) -public final class RetrieveTokenTests extends TransactionalTestsBase { +public final class RetrieveTokenTests extends UserTestsBase { @Test public void retrieveToken() { given().get("user_0/token").then().statusCode(401); diff --git a/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldInactiveUsersTests.java b/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldInactiveUsersTests.java index 28faf08..99ada7d 100644 --- a/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldInactiveUsersTests.java +++ b/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldInactiveUsersTests.java @@ -4,7 +4,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.tasks.CleanupTasks; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.UserTestsBase; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; import jakarta.transaction.Transactional; @@ -13,7 +13,7 @@ import org.junit.jupiter.api.Test; @QuarkusTest -public final class RemoveOldInactiveUsersTests extends TransactionalTestsBase { +public final class RemoveOldInactiveUsersTests extends UserTestsBase { @Inject CleanupTasks cleanupTasks; diff --git a/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldRandomCodesTests.java b/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldRandomCodesTests.java index 85250b9..15b074d 100644 --- a/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldRandomCodesTests.java +++ b/src/test/java/app/fyreplace/api/testing/tasks/cleanup/RemoveOldRandomCodesTests.java @@ -7,7 +7,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.services.RandomService; import app.fyreplace.api.tasks.CleanupTasks; -import app.fyreplace.api.testing.TransactionalTestsBase; +import app.fyreplace.api.testing.UserTestsBase; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; import jakarta.transaction.Transactional; @@ -16,7 +16,7 @@ import org.junit.jupiter.api.Test; @QuarkusTest -public final class RemoveOldRandomCodesTests extends TransactionalTestsBase { +public final class RemoveOldRandomCodesTests extends UserTestsBase { @Inject RandomService randomService; From ef7d318afb7f36457aca3e8574afc46f043ce571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Thu, 12 Oct 2023 12:36:58 +0200 Subject: [PATCH 066/157] Update Gradle --- gradlew | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/gradlew b/gradlew index 0adc8e1..1aa94a4 100755 --- a/gradlew +++ b/gradlew @@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -202,11 +202,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ From 348a0b3bf8db03c094265fc213c0fa7853414859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 13 Oct 2023 11:34:59 +0200 Subject: [PATCH 067/157] Code cleanup --- src/main/java/app/fyreplace/api/data/Chapter.java | 1 + src/main/java/app/fyreplace/api/data/Post.java | 2 +- src/main/java/app/fyreplace/api/data/User.java | 3 +-- .../app/fyreplace/api/endpoints/CommentsEndpoint.java | 4 ++-- .../app/fyreplace/api/endpoints/PostsEndpoint.java | 10 +++++----- .../endpoints/chapters/UpdateChapterPositionTests.java | 3 +-- .../api/testing/endpoints/posts/CreateTests.java | 6 ++++-- .../api/testing/endpoints/posts/PublishTests.java | 5 +++++ 8 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/main/java/app/fyreplace/api/data/Chapter.java b/src/main/java/app/fyreplace/api/data/Chapter.java index 96bc5e1..a7423aa 100644 --- a/src/main/java/app/fyreplace/api/data/Chapter.java +++ b/src/main/java/app/fyreplace/api/data/Chapter.java @@ -42,6 +42,7 @@ public class Chapter extends EntityBase { @Column(nullable = false) public int height = 0; + @SuppressWarnings("unused") @PostRemove final void postRemove() { if (image != null) { diff --git a/src/main/java/app/fyreplace/api/data/Post.java b/src/main/java/app/fyreplace/api/data/Post.java index 3a6996c..1717ce3 100644 --- a/src/main/java/app/fyreplace/api/data/Post.java +++ b/src/main/java/app/fyreplace/api/data/Post.java @@ -33,7 +33,7 @@ public class Post extends AuthoredEntityBase { public static Duration shelfLife = Duration.ofDays(7); public List getChapters() { - return Chapter.find("post", Sort.by("position"), this).list(); + return Chapter.list("post", Sort.by("position"), this); } @JsonIgnore diff --git a/src/main/java/app/fyreplace/api/data/User.java b/src/main/java/app/fyreplace/api/data/User.java index d18dbcc..a5d2def 100644 --- a/src/main/java/app/fyreplace/api/data/User.java +++ b/src/main/java/app/fyreplace/api/data/User.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import io.quarkus.panache.common.Sort; import jakarta.annotation.Nullable; import jakarta.persistence.*; import jakarta.ws.rs.NotAuthorizedException; @@ -146,7 +145,7 @@ public void subscribeTo(final Post post) { subscription.user = this; subscription.post = post; subscription.lastCommentSeen = - Comment.find("post", Sort.descending("dateCreated"), post).firstResult(); + Comment.find("post", Comment.sorting().descending(), post).firstResult(); subscription.persist(); } diff --git a/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java index 613130d..d409a7d 100644 --- a/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java @@ -47,9 +47,9 @@ public Iterable list(@PathParam("id") final UUID id, @QueryParam("page" final var post = Post.findById(id); Post.validateAccess(post, user, true, false); - try (final var commentStream = + try (final var stream = Comment.find("post", Comment.sorting(), post).page(page, pagingSize).stream()) { - return commentStream.peek(c -> c.setCurrentUser(user)).toList(); + return stream.peek(c -> c.setCurrentUser(user)).toList(); } } diff --git a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java index 182492e..6699156 100644 --- a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java @@ -58,7 +58,7 @@ public Iterable list( final var direction = ascending ? Direction.Ascending : Direction.Descending; final var basicSort = Post.sorting().direction(direction); - final var postStream = + final var stream = switch (type) { case SUBSCRIBED_TO -> Subscription.find( "user", @@ -76,8 +76,8 @@ public Iterable list( .stream(); }; - try (postStream) { - return postStream.peek(p -> p.setCurrentUser(user)).toList(); + try (stream) { + return stream.peek(p -> p.setCurrentUser(user)).toList(); } } @@ -214,7 +214,7 @@ public long count(@QueryParam("type") @NotNull final PostListingType type) { public Iterable listFeed() { final var user = User.getFromSecurityContext(context); - try (final var postStream = Post.find( + try (final var stream = Post.find( "author != ?1 and dateCreated > ?2 and published = true and life > 0" + "and id not in (select post.id from Vote where user = ?1)" + "and author.id not in (select target.id from Block where source = ?1)" @@ -224,7 +224,7 @@ public Iterable listFeed() { Instant.now().minus(Post.shelfLife)) .range(0, 2) .stream()) { - return postStream.peek(p -> p.setCurrentUser(user)).toList(); + return stream.peek(p -> p.setCurrentUser(user)).toList(); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterPositionTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterPositionTests.java index 94ca3ce..4e1b58d 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterPositionTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterPositionTests.java @@ -41,8 +41,7 @@ public void updateChapterPositionInOwnDraft(final int to) { .put(from + "/position") .then() .statusCode(200); - final var chapters = - Chapter.find("post", Sort.by("position"), draft).list(); + final var chapters = Chapter.list("post", Sort.by("position"), draft); assertEquals(chapter.id, chapters.get(to).id); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateTests.java index f01176e..59e80a7 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateTests.java @@ -20,7 +20,7 @@ public final class CreateTests extends PostTestsBase { @Test @TestSecurity(user = "user_0") public void create() { - final var postCount = Post.count("author.username", "user_0"); + final var postCount = Post.count("author.username = 'user_0'"); given().post() .then() .statusCode(201) @@ -30,11 +30,13 @@ public void create() { .body("author.username", equalTo("user_0")) .body("anonymous", equalTo(false)) .body("chapters.size()", equalTo(0)); - assertEquals(postCount + 1, Post.count("author.username", "user_0")); + assertEquals(postCount + 1, Post.count("author.username = 'user_0'")); } @Test public void createUnauthenticated() { + final var postCount = Post.count("author.username = 'user_0'"); given().post().then().statusCode(401); + assertEquals(postCount, Post.count()); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/PublishTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/PublishTests.java index ba065de..dddf331 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/PublishTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/PublishTests.java @@ -6,6 +6,7 @@ import app.fyreplace.api.data.Chapter; import app.fyreplace.api.data.Post; import app.fyreplace.api.data.PostPublication; +import app.fyreplace.api.data.Subscription; import app.fyreplace.api.endpoints.PostsEndpoint; import app.fyreplace.api.testing.PostTestsBase; import io.quarkus.narayana.jta.QuarkusTransaction; @@ -33,23 +34,27 @@ public void publishOwnPost() { @Test @TestSecurity(user = "user_0") public void publishOwnDraft() { + final var subscriptionCount = Subscription.count("user.username = 'user_0'"); given().contentType(ContentType.JSON) .body(new PostPublication(true)) .post(draft.id + "/publish") .then() .statusCode(200); assertEquals(0, Post.count("id = ?1 and published = false and anonymous = false", draft.id)); + assertEquals(subscriptionCount + 1, Subscription.count("user.username = 'user_0'")); } @Test @TestSecurity(user = "user_0") public void publishAnonymouslyOwnDraft() { + final var subscriptionCount = Subscription.count("user.username = 'user_0'"); given().contentType(ContentType.JSON) .body(new PostPublication(false)) .post(draft.id + "/publish") .then() .statusCode(200); assertEquals(0, Post.count("id = ?1 and published = false and anonymous = true", draft.id)); + assertEquals(subscriptionCount + 1, Subscription.count("user.username = 'user_0'")); } @Test From 09705522ffa135051e35a051590b53ce36f3ca7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 14 Oct 2023 12:08:14 +0200 Subject: [PATCH 068/157] Update dependencies --- build.gradle | 1 + gradle.properties | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index c0fbf05..5697b00 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,7 @@ dependencies { implementation("org.apache.tika:tika-core") implementation("org.apache.tika:tika-parsers-standard-package") implementation("software.amazon.awssdk:url-connection-client") + implementation("software.amazon.awssdk.crt:aws-crt") implementation(project(":quarkus-sentry:deployment")) implementation(project(":quarkus-sentry:runtime")) testImplementation("io.quarkus:quarkus-junit5") diff --git a/gradle.properties b/gradle.properties index f722cb3..2d0025b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ -quarkusVersion=3.4.1 -quarkusAmazonVersion=2.5.0 -sentryVersion=6.29.0 +quarkusVersion=3.4.3 +quarkusAmazonVersion=2.5.2 +sentryVersion=6.31.0 tikaVersion=2.9.0 gitPluginVersion=3.0.0 -spotlessPluginVersion=6.21.0 +spotlessPluginVersion=6.22.0 From cb4dee1739573ee93ace18db9412fe0dcc2d5b82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 14 Oct 2023 13:44:47 +0200 Subject: [PATCH 069/157] Allow public reads on bucket --- .../services/storage/s3/S3StorageService.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java b/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java index 9618995..43922f0 100644 --- a/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java +++ b/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java @@ -1,12 +1,17 @@ package app.fyreplace.api.services.storage.s3; import app.fyreplace.api.services.StorageService; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import io.quarkus.arc.Unremovable; import io.quarkus.arc.properties.IfBuildProperty; +import io.quarkus.runtime.StartupEvent; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; import java.net.URI; import java.net.URISyntaxException; +import java.util.List; import org.jboss.logging.Logger; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; @@ -24,6 +29,25 @@ public final class S3StorageService implements StorageService { @Inject S3Client client; + @Inject + ObjectMapper objectMapper; + + public void onStartup(@Observes final StartupEvent event) { + client.putBucketPolicy(b -> { + final var statement = new Policy.Statement(); + statement.Effect = "Allow"; + statement.Principal = "*"; + statement.Action = "s3:GetObject"; + statement.Resource = "arn:aws:s3:::" + config.bucket() + "/*"; + + try { + b.bucket(config.bucket()).policy(objectMapper.writeValueAsString(new Policy(List.of(statement)))); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + }); + } + @Override public void store(final String path, final byte[] data) { client.putObject(b -> b.bucket(config.bucket()).key(path), RequestBody.fromBytes(data)); From c943414385023c45e8e1615fe4e2cf4990e1d0e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 14 Oct 2023 13:49:36 +0200 Subject: [PATCH 070/157] Use custom S3 endpoint when possible --- .env-example | 4 ++-- .../api/services/storage/s3/S3StorageConfig.java | 3 ++- .../api/services/storage/s3/S3StorageService.java | 8 +++++++- src/main/resources/application.yaml | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.env-example b/.env-example index 929330b..59fc68f 100644 --- a/.env-example +++ b/.env-example @@ -2,7 +2,7 @@ QUARKUS_DATASOURCE_USERNAME=fyreplace QUARKUS_DATASOURCE_PASSWORD=fyreplace QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://localhost/fyreplace -QUARKUS_S3_ENDPOINT_OVERRIDE=https://fra1.digitaloceanspaces.com +QUARKUS_S3_ENDPOINT_OVERRIDE=https://storage.example.org QUARKUS_S3_AWS_REGION=eu-west-1 QUARKUS_S3_AWS_CREDENTIALS_TYPE=static QUARKUS_S3_AWS_CREDENTIALS_STATIC_PROVIDER_ACCESS_KEY_ID=key-id @@ -19,4 +19,4 @@ APP_URL=https://fyreplace.example.org APP_STORAGE_TYPE=s3 APP_STORAGE_S3_BUCKET=fyreplace -APP_STORAGE_S3_CUSTOM_DOMAIN=storage.fyreplace.example.org +APP_STORAGE_S3_CUSTOM_ENDPOINT=https://cdn.storage.example.org diff --git a/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageConfig.java b/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageConfig.java index 7bbcb79..4d67262 100644 --- a/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageConfig.java +++ b/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageConfig.java @@ -2,10 +2,11 @@ import io.smallrye.config.ConfigMapping; import java.net.URI; +import java.util.Optional; @ConfigMapping(prefix = "app.storage.s3") public interface S3StorageConfig { String bucket(); - URI customDomain(); + Optional customEndpoint(); } diff --git a/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java b/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java index 43922f0..cad7c80 100644 --- a/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java +++ b/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java @@ -62,7 +62,13 @@ public void remove(final String path) { public URI getUri(final String path) { try { return client.utilities() - .getUrl(b -> b.bucket(config.bucket()).key(path)) + .getUrl(b -> { + b.bucket(config.bucket()).key(path); + + if (config.customEndpoint().isPresent()) { + b.endpoint(config.customEndpoint().get()); + } + }) .toURI(); } catch (final URISyntaxException e) { logger.error("Failed to get URI for S3 object", e); diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 9e455de..660f8d7 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -53,7 +53,7 @@ app: s3: bucket: "" - custom-domain: "" + custom-endpoint: "" posts: max-chapter-count: 10 From 89159469a58d86026f0bd472119c9df30259d1bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 14 Oct 2023 14:39:25 +0200 Subject: [PATCH 071/157] Add missing file --- .../api/services/storage/s3/Policy.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/main/java/app/fyreplace/api/services/storage/s3/Policy.java diff --git a/src/main/java/app/fyreplace/api/services/storage/s3/Policy.java b/src/main/java/app/fyreplace/api/services/storage/s3/Policy.java new file mode 100644 index 0000000..0be891b --- /dev/null +++ b/src/main/java/app/fyreplace/api/services/storage/s3/Policy.java @@ -0,0 +1,23 @@ +package app.fyreplace.api.services.storage.s3; + +import java.util.List; + +public class Policy { + public String Version = "2012-10-17"; + + public List Statement; + + public Policy(final List statement) { + Statement = statement; + } + + public static class Statement { + public String Effect; + + public String Principal; + + public String Action; + + public String Resource; + } +} From d53a6db9ecb565565f0b75e10f8898c0c50565a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 15 Oct 2023 12:10:40 +0200 Subject: [PATCH 072/157] Add subscriptions endpoint --- .../app/fyreplace/api/data/Subscription.java | 59 +++++++++++++++- .../api/endpoints/PostsEndpoint.java | 3 +- .../api/endpoints/SubscriptionsEndpoint.java | 69 +++++++++++++++++++ .../api/testing/SubscriptionTestsBase.java | 24 +++++++ .../testing/endpoints/posts/DeleteTests.java | 2 +- .../subscriptions/ClearUnreadTests.java | 25 +++++++ .../endpoints/subscriptions/DeleteTests.java | 43 ++++++++++++ .../subscriptions/ListUnreadTests.java | 38 ++++++++++ 8 files changed, 257 insertions(+), 6 deletions(-) create mode 100644 src/main/java/app/fyreplace/api/endpoints/SubscriptionsEndpoint.java create mode 100644 src/test/java/app/fyreplace/api/testing/SubscriptionTestsBase.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ClearUnreadTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/DeleteTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ListUnreadTests.java diff --git a/src/main/java/app/fyreplace/api/data/Subscription.java b/src/main/java/app/fyreplace/api/data/Subscription.java index c6c42e0..aa4ee9e 100644 --- a/src/main/java/app/fyreplace/api/data/Subscription.java +++ b/src/main/java/app/fyreplace/api/data/Subscription.java @@ -1,5 +1,7 @@ package app.fyreplace.api.data; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.quarkus.panache.common.Sort; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -7,6 +9,7 @@ import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; import java.time.Instant; +import org.hibernate.annotations.Formula; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; import org.hibernate.annotations.SourceType; @@ -17,17 +20,67 @@ public class Subscription extends EntityBase { @ManyToOne(optional = false, fetch = FetchType.EAGER) @OnDelete(action = OnDeleteAction.CASCADE) + @JsonIgnore public User user; @ManyToOne(optional = false, fetch = FetchType.EAGER) @OnDelete(action = OnDeleteAction.CASCADE) public Post post; + @SuppressWarnings("unused") + @Column(nullable = false) + @UpdateTimestamp(source = SourceType.DB) + @JsonIgnore + public Instant dateUpdated; + @ManyToOne @OnDelete(action = OnDeleteAction.CASCADE) + @JsonIgnore public Comment lastCommentSeen; - @Column(nullable = false) - @UpdateTimestamp(source = SourceType.DB) - public Instant dateLastSeen; + @SuppressWarnings("unused") + @Formula( + """ + ( + select count(*) from comments + where comments.post_id = post_id + and ( + case + when last_comment_seen_id is null then true + else ( + comments.date_created > ( + select comments.date_created from comments + where comments.id = last_comment_seen_id + ) + ) + end + ) + ) + """) + public long unreadCommentCount; + + public void markAsRead() { + lastCommentSeen = + Comment.find("post", Post.sorting().descending(), post).firstResult(); + persist(); + } + + public static Sort sorting() { + return Sort.by("dateUpdated", "id"); + } + + public static void markAsRead(final User user) { + update( + """ + update Subscription as s set + lastCommentSeen = ( + select id from Comment as c + where c.post = s.post + order by c.dateCreated desc, c.id desc + limit 1 + ) + where user = ?1 + """, + user); + } } diff --git a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java index 6699156..8a4d6f2 100644 --- a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java @@ -62,8 +62,7 @@ public Iterable list( switch (type) { case SUBSCRIBED_TO -> Subscription.find( "user", - Sort.by("dateLastSeen", "post.dateCreated", "post.id") - .direction(direction), + Sort.by("post.dateCreated", "post.id").direction(direction), user) .page(page, pagingSize) .stream() diff --git a/src/main/java/app/fyreplace/api/endpoints/SubscriptionsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/SubscriptionsEndpoint.java new file mode 100644 index 0000000..d94a683 --- /dev/null +++ b/src/main/java/app/fyreplace/api/endpoints/SubscriptionsEndpoint.java @@ -0,0 +1,69 @@ +package app.fyreplace.api.endpoints; + +import app.fyreplace.api.data.Subscription; +import app.fyreplace.api.data.User; +import io.quarkus.security.Authenticated; +import jakarta.transaction.Transactional; +import jakarta.validation.constraints.PositiveOrZero; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.SecurityContext; +import java.util.UUID; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; + +@Path("subscriptions") +public class SubscriptionsEndpoint { + @ConfigProperty(name = "app.paging.size") + int pagingSize; + + @Context + SecurityContext context; + + @DELETE + @Path("{id}") + @Authenticated + @Transactional + @APIResponse(responseCode = "204") + @APIResponse(responseCode = "404") + public void delete(@PathParam("id") final UUID id) { + final var user = User.getFromSecurityContext(context); + final var subscription = Subscription.findById(id); + + if (subscription == null || !subscription.user.id.equals(user.id)) { + throw new NotFoundException(); + } + + subscription.markAsRead(); + } + + @GET + @Path("unread") + @Authenticated + @APIResponse(responseCode = "200") + @APIResponse(responseCode = "400") + public Iterable listUnread(@QueryParam("page") @PositiveOrZero final int page) { + final var user = User.getFromSecurityContext(context); + + try (final var stream = + Subscription.find("user = ?1 and unreadCommentCount > 0", Subscription.sorting(), user) + .page(page, pagingSize) + .stream()) { + return stream.peek(s -> s.post.setCurrentUser(user)).toList(); + } + } + + @DELETE + @Path("unread") + @Authenticated + @Transactional + @APIResponse(responseCode = "204") + public void clearUnread() { + Subscription.markAsRead(User.getFromSecurityContext(context)); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/SubscriptionTestsBase.java b/src/test/java/app/fyreplace/api/testing/SubscriptionTestsBase.java new file mode 100644 index 0000000..9c04173 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/SubscriptionTestsBase.java @@ -0,0 +1,24 @@ +package app.fyreplace.api.testing; + +import app.fyreplace.api.data.Post; +import app.fyreplace.api.data.User; +import io.quarkus.narayana.jta.QuarkusTransaction; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; + +public class SubscriptionTestsBase extends PostTestsBase { + @BeforeEach + @Transactional + @Override + public void beforeEach() { + super.beforeEach(); + final var user = User.findByUsername("user_0"); + final var otherUser = User.findByUsername("user_1"); + + try (final var stream = Post.stream("author = ?1 and published = true", user)) { + stream.forEach(post -> dataSeeder.createComment(otherUser, post, "Comment", false)); + } + + QuarkusTransaction.requiringNew().run(() -> dataSeeder.createComment(otherUser, post, "Comment", false)); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteTests.java index 6c6660d..07fa480 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteTests.java @@ -59,7 +59,7 @@ public void deleteDraftUnauthenticated() { @ParameterizedTest @ValueSource(strings = {"fake", "00000000-0000-0000-0000-000000000000"}) @TestSecurity(user = "user_0") - public void deleteNonExistent(final String id) { + public void deleteNonExistentPost(final String id) { given().delete(id).then().statusCode(404); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ClearUnreadTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ClearUnreadTests.java new file mode 100644 index 0000000..6de2377 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ClearUnreadTests.java @@ -0,0 +1,25 @@ +package app.fyreplace.api.testing.endpoints.subscriptions; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import app.fyreplace.api.data.Subscription; +import app.fyreplace.api.endpoints.SubscriptionsEndpoint; +import app.fyreplace.api.testing.SubscriptionTestsBase; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(SubscriptionsEndpoint.class) +public class ClearUnreadTests extends SubscriptionTestsBase { + @Test + @TestSecurity(user = "user_0") + public void clearUnread() { + assertNotEquals(0, Subscription.count("user.username = 'user_0' and unreadCommentCount > 0")); + given().delete("unread").then().statusCode(204); + assertEquals(0, Subscription.count("user.username = 'user_0' and unreadCommentCount > 0")); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/DeleteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/DeleteTests.java new file mode 100644 index 0000000..edf6a85 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/DeleteTests.java @@ -0,0 +1,43 @@ +package app.fyreplace.api.testing.endpoints.subscriptions; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import app.fyreplace.api.data.Subscription; +import app.fyreplace.api.endpoints.SubscriptionsEndpoint; +import app.fyreplace.api.testing.SubscriptionTestsBase; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(SubscriptionsEndpoint.class) +public class DeleteTests extends SubscriptionTestsBase { + @Test + @TestSecurity(user = "user_0") + public void delete() { + final var subscriptions = + Subscription.list("user.username = 'user_0' and unreadCommentCount > 0"); + given().delete(subscriptions.get(0).id.toString()).then().statusCode(204); + assertEquals( + subscriptions.size() - 1, Subscription.count("user.username = 'user_0' and unreadCommentCount > 0")); + } + + @Test + @TestSecurity(user = "user_1") + public void deleteOtherSubscription() { + final var subscriptions = + Subscription.list("user.username = 'user_0' and unreadCommentCount > 0"); + given().delete(subscriptions.get(0).id.toString()).then().statusCode(404); + assertEquals(subscriptions.size(), Subscription.count("user.username = 'user_0' and unreadCommentCount > 0")); + } + + @Test + @TestSecurity(user = "user_0") + public void deleteNonExistent() { + final var subscriptionCount = Subscription.count("user.username = 'user_0' and unreadCommentCount > 0"); + given().delete(fakeId).then().statusCode(404); + assertEquals(subscriptionCount, Subscription.count("user.username = 'user_0' and unreadCommentCount > 0")); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ListUnreadTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ListUnreadTests.java new file mode 100644 index 0000000..3e69338 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ListUnreadTests.java @@ -0,0 +1,38 @@ +package app.fyreplace.api.testing.endpoints.subscriptions; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; + +import app.fyreplace.api.endpoints.SubscriptionsEndpoint; +import app.fyreplace.api.testing.SubscriptionTestsBase; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(SubscriptionsEndpoint.class) +public class ListUnreadTests extends SubscriptionTestsBase { + @ConfigProperty(name = "app.paging.size") + int pagingSize; + + @Test + @TestSecurity(user = "user_0") + public void listUnread() { + given().get("unread") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("size()", equalTo(pagingSize)) + .body("[0].post.id", equalTo(post.id.toString())) + .body("[0].unreadCommentCount", equalTo(2)) + .body("[1].unreadCommentCount", equalTo(1)); + } + + @Override + public int getPostCount() { + return 20; + } +} From ef1fcd8484be8acfac272c68f2c6821fc0067f2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 15 Oct 2023 16:08:29 +0200 Subject: [PATCH 073/157] Use record for S3 policy --- .../api/services/storage/s3/Policy.java | 20 +++---------------- .../services/storage/s3/S3StorageService.java | 10 ++++------ 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/src/main/java/app/fyreplace/api/services/storage/s3/Policy.java b/src/main/java/app/fyreplace/api/services/storage/s3/Policy.java index 0be891b..1cd2652 100644 --- a/src/main/java/app/fyreplace/api/services/storage/s3/Policy.java +++ b/src/main/java/app/fyreplace/api/services/storage/s3/Policy.java @@ -2,22 +2,8 @@ import java.util.List; -public class Policy { - public String Version = "2012-10-17"; +public record Policy(String Version, List Statement) { + public static final String currentVersion = "2012-10-17"; - public List Statement; - - public Policy(final List statement) { - Statement = statement; - } - - public static class Statement { - public String Effect; - - public String Principal; - - public String Action; - - public String Resource; - } + public record Statement(String Effect, String Principal, String Action, String Resource) {} } diff --git a/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java b/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java index cad7c80..7ae874f 100644 --- a/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java +++ b/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java @@ -34,14 +34,12 @@ public final class S3StorageService implements StorageService { public void onStartup(@Observes final StartupEvent event) { client.putBucketPolicy(b -> { - final var statement = new Policy.Statement(); - statement.Effect = "Allow"; - statement.Principal = "*"; - statement.Action = "s3:GetObject"; - statement.Resource = "arn:aws:s3:::" + config.bucket() + "/*"; + final var statement = + new Policy.Statement("Allow", "*", "s3:GetObject", "arn:aws:s3:::" + config.bucket() + "/*"); try { - b.bucket(config.bucket()).policy(objectMapper.writeValueAsString(new Policy(List.of(statement)))); + b.bucket(config.bucket()) + .policy(objectMapper.writeValueAsString(new Policy(Policy.currentVersion, List.of(statement)))); } catch (JsonProcessingException e) { throw new RuntimeException(e); } From 8a5ca0a07efee42efda64a3c89534c86cef05d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 15 Oct 2023 16:08:55 +0200 Subject: [PATCH 074/157] Use URI instead of deprecated URL --- .../app/fyreplace/api/emails/EmailBase.java | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/main/java/app/fyreplace/api/emails/EmailBase.java b/src/main/java/app/fyreplace/api/emails/EmailBase.java index b05fd8c..52fdfaf 100644 --- a/src/main/java/app/fyreplace/api/emails/EmailBase.java +++ b/src/main/java/app/fyreplace/api/emails/EmailBase.java @@ -9,12 +9,10 @@ import io.quarkus.qute.TemplateInstance; import io.smallrye.common.annotation.Blocking; import jakarta.inject.Inject; -import java.net.MalformedURLException; -import java.net.URL; +import java.net.URI; import java.util.List; import java.util.ResourceBundle; import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.jboss.logging.Logger; public abstract class EmailBase extends Mail { @ConfigProperty(name = "app.url") @@ -33,8 +31,6 @@ public abstract class EmailBase extends Mail { private Email email; - private final Logger logger = Logger.getLogger(this.getClass()); - protected abstract String action(); protected abstract TemplateInstance textTemplate(); @@ -64,13 +60,10 @@ protected String getRandomCode() { } protected String getLink() { - try { - final var url = new URL(new URL(appUrl), "?action=" + action()); - return String.format("%s#%s:%s", url, email.user.username, getRandomCode()); - } catch (final MalformedURLException e) { - logger.error("Could not generate link", e); - return null; - } + return URI.create(appUrl) + .resolve("?action=" + action()) + .resolve('#' + email.user.username + ':' + getRandomCode()) + .toString(); } protected ResourceBundle getResourceBundle() { From 5c08f073fdf464ebaea1d01f522d851eceafe172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 15 Oct 2023 18:37:03 +0200 Subject: [PATCH 075/157] Allow for artificial idempotency --- build.gradle | 5 ++-- .../cache/DuplicateRequestKeyGenerator.java | 24 +++++++++++++++++++ .../api/endpoints/ChaptersEndpoint.java | 7 +++++- .../api/endpoints/CommentsEndpoint.java | 7 +++++- .../api/endpoints/DevUsersEndpoint.java | 3 +++ .../api/endpoints/EmailsEndpoint.java | 8 ++++++- .../api/endpoints/PostsEndpoint.java | 9 ++++++- .../api/endpoints/TokensEndpoint.java | 4 ++++ .../api/endpoints/UsersEndpoint.java | 7 +++++- 9 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 src/main/java/app/fyreplace/api/cache/DuplicateRequestKeyGenerator.java diff --git a/build.gradle b/build.gradle index 5697b00..99ae602 100644 --- a/build.gradle +++ b/build.gradle @@ -18,11 +18,12 @@ dependencies { implementation(enforcedPlatform("io.quarkiverse.amazonservices:quarkus-amazon-services-bom:${quarkusAmazonVersion}")) implementation(enforcedPlatform("org.apache.tika:tika-bom:${tikaVersion}")) implementation("io.quarkus:quarkus-arc") + implementation("io.quarkus:quarkus-cache") implementation("io.quarkus:quarkus-config-yaml") implementation("io.quarkus:quarkus-hibernate-orm-panache") implementation("io.quarkus:quarkus-hibernate-validator") - implementation("io.quarkus:quarkus-jdbc-postgresql") implementation("io.quarkus:quarkus-jdbc-h2") + implementation("io.quarkus:quarkus-jdbc-postgresql") implementation("io.quarkus:quarkus-mailer") implementation("io.quarkus:quarkus-resteasy-reactive") implementation("io.quarkus:quarkus-resteasy-reactive-jackson") @@ -30,9 +31,9 @@ dependencies { implementation("io.quarkus:quarkus-scheduler") implementation("io.quarkus:quarkus-security") implementation("io.quarkus:quarkus-smallrye-health") - implementation("io.quarkus:quarkus-smallrye-openapi") implementation("io.quarkus:quarkus-smallrye-jwt") implementation("io.quarkus:quarkus-smallrye-jwt-build") + implementation("io.quarkus:quarkus-smallrye-openapi") implementation("io.quarkiverse.amazonservices:quarkus-amazon-s3") implementation("org.jboss.logmanager:log4j2-jboss-logmanager") implementation("org.apache.tika:tika-core") diff --git a/src/main/java/app/fyreplace/api/cache/DuplicateRequestKeyGenerator.java b/src/main/java/app/fyreplace/api/cache/DuplicateRequestKeyGenerator.java new file mode 100644 index 0000000..e7768e5 --- /dev/null +++ b/src/main/java/app/fyreplace/api/cache/DuplicateRequestKeyGenerator.java @@ -0,0 +1,24 @@ +package app.fyreplace.api.cache; + +import static java.util.Objects.requireNonNullElse; + +import io.quarkus.cache.CacheKeyGenerator; +import io.quarkus.cache.CompositeCacheKey; +import jakarta.annotation.Nullable; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.HttpHeaders; +import java.lang.reflect.Method; +import java.util.UUID; + +@ApplicationScoped +public class DuplicateRequestKeyGenerator implements CacheKeyGenerator { + @Context + HttpHeaders headers; + + @Override + public @Nullable Object generate(final Method method, final Object... methodParams) { + final var requestId = requireNonNullElse(headers.getHeaderString("X-Request-Id"), UUID.randomUUID()); + return new CompositeCacheKey(headers.getHeaderString("Authorization"), requestId); + } +} diff --git a/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java index 9241060..ae749d3 100644 --- a/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java @@ -2,6 +2,7 @@ import static java.util.Objects.requireNonNullElse; +import app.fyreplace.api.cache.DuplicateRequestKeyGenerator; import app.fyreplace.api.data.Chapter; import app.fyreplace.api.data.Post; import app.fyreplace.api.data.StoredFile; @@ -9,6 +10,7 @@ import app.fyreplace.api.exceptions.ForbiddenException; import app.fyreplace.api.services.MimeTypeService; import app.fyreplace.api.services.mimetype.KnownMimeTypes; +import io.quarkus.cache.CacheResult; import io.quarkus.panache.common.Sort; import io.quarkus.security.Authenticated; import jakarta.inject.Inject; @@ -55,6 +57,7 @@ public final class ChaptersEndpoint { content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Chapter.class))) @APIResponse(responseCode = "404") + @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response createChapter(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); @@ -79,11 +82,13 @@ public Response createChapter(@PathParam("id") final UUID id) { @Transactional @APIResponse(responseCode = "204") @APIResponse(responseCode = "404") - public void deleteChapter(@PathParam("id") final UUID id, @PathParam("position") final int position) { + @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) + public Response deleteChapter(@PathParam("id") final UUID id, @PathParam("position") final int position) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); Post.validateAccess(post, user, false, true); getChapter(post, position).delete(); + return Response.noContent().build(); } @PUT diff --git a/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java index d409a7d..9006bb4 100644 --- a/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java @@ -1,11 +1,13 @@ package app.fyreplace.api.endpoints; +import app.fyreplace.api.cache.DuplicateRequestKeyGenerator; import app.fyreplace.api.data.Comment; import app.fyreplace.api.data.CommentCreation; import app.fyreplace.api.data.Post; import app.fyreplace.api.data.Subscription; import app.fyreplace.api.data.User; import app.fyreplace.api.exceptions.ForbiddenException; +import io.quarkus.cache.CacheResult; import io.quarkus.security.Authenticated; import jakarta.annotation.Nullable; import jakarta.transaction.Transactional; @@ -61,6 +63,7 @@ public Iterable list(@PathParam("id") final UUID id, @QueryParam("page" content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Comment.class))) @APIResponse(responseCode = "404") + @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response create(@PathParam("id") final UUID id, @Valid @NotNull final CommentCreation input) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); @@ -86,7 +89,8 @@ public Response create(@PathParam("id") final UUID id, @Valid @NotNull final Com @Transactional @APIResponse(responseCode = "204") @APIResponse(responseCode = "404") - public void delete(@PathParam("id") final UUID id, @PathParam("position") @PositiveOrZero final int position) { + @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) + public Response delete(@PathParam("id") final UUID id, @PathParam("position") @PositiveOrZero final int position) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); Post.validateAccess(post, user, true, false); @@ -97,6 +101,7 @@ public void delete(@PathParam("id") final UUID id, @PathParam("position") @Posit } comment.softDelete(); + return Response.noContent().build(); } @POST diff --git a/src/main/java/app/fyreplace/api/endpoints/DevUsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/DevUsersEndpoint.java index 1ce01e2..9b20ff7 100644 --- a/src/main/java/app/fyreplace/api/endpoints/DevUsersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/DevUsersEndpoint.java @@ -1,7 +1,9 @@ package app.fyreplace.api.endpoints; +import app.fyreplace.api.cache.DuplicateRequestKeyGenerator; import app.fyreplace.api.data.User; import app.fyreplace.api.services.JwtService; +import io.quarkus.cache.CacheResult; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.NotFoundException; @@ -23,6 +25,7 @@ public final class DevUsersEndpoint { responseCode = "200", content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) @APIResponse(responseCode = "404") + @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public String retrieveToken(@PathParam("username") final String username) { final var user = User.findByUsername(username); diff --git a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java index ccc491e..86b94fb 100644 --- a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java @@ -1,5 +1,6 @@ package app.fyreplace.api.endpoints; +import app.fyreplace.api.cache.DuplicateRequestKeyGenerator; import app.fyreplace.api.data.Email; import app.fyreplace.api.data.EmailActivation; import app.fyreplace.api.data.EmailCreation; @@ -8,6 +9,7 @@ import app.fyreplace.api.emails.EmailVerificationEmail; import app.fyreplace.api.exceptions.ConflictException; import app.fyreplace.api.exceptions.ForbiddenException; +import io.quarkus.cache.CacheResult; import io.quarkus.panache.common.Sort; import io.quarkus.security.Authenticated; import jakarta.inject.Inject; @@ -59,6 +61,7 @@ public List list(@QueryParam("page") @PositiveOrZero final int page) { responseCode = "201", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Email.class))) @APIResponse(responseCode = "409") + @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response create(@Valid @NotNull final EmailCreation input) { if (Email.count("email", input.email()) > 0) { throw new ConflictException("email_taken"); @@ -78,7 +81,8 @@ public Response create(@Valid @NotNull final EmailCreation input) { @Transactional @APIResponse(responseCode = "204") @APIResponse(responseCode = "404") - public void delete(@PathParam("id") final UUID id) { + @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) + public Response delete(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context); final var email = Email.find("user = ?1 and id = ?2", user, id).firstResult(); @@ -89,6 +93,7 @@ public void delete(@PathParam("id") final UUID id) { } email.delete(); + return Response.noContent().build(); } @POST @@ -127,6 +132,7 @@ public long count() { @APIResponse(responseCode = "200") @APIResponse(responseCode = "400") @APIResponse(responseCode = "404") + @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response activate(@NotNull @Valid final EmailActivation input) { var email = Email.find("email", input.email()).firstResult(); final var randomCode = RandomCode.find("email = ?1 and code = ?2", email, input.code()) diff --git a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java index 8a4d6f2..6f6949b 100644 --- a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java @@ -1,5 +1,6 @@ package app.fyreplace.api.endpoints; +import app.fyreplace.api.cache.DuplicateRequestKeyGenerator; import app.fyreplace.api.data.Chapter; import app.fyreplace.api.data.Post; import app.fyreplace.api.data.PostPublication; @@ -8,6 +9,7 @@ import app.fyreplace.api.data.Vote; import app.fyreplace.api.data.VoteCreation; import app.fyreplace.api.exceptions.ForbiddenException; +import io.quarkus.cache.CacheResult; import io.quarkus.panache.common.Sort; import io.quarkus.panache.common.Sort.Direction; import io.quarkus.security.Authenticated; @@ -87,6 +89,7 @@ public Iterable list( responseCode = "201", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Post.class))) @APIResponse(responseCode = "400") + @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response create() { final var user = User.getFromSecurityContext(context); final var post = new Post(); @@ -113,11 +116,13 @@ public Post retrieve(@PathParam("id") final UUID id) { @Transactional @APIResponse(responseCode = "204") @APIResponse(responseCode = "404") - public void delete(@PathParam("id") final UUID id) { + @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) + public Response delete(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); Post.validateAccess(post, user, null, true); post.delete(); + return Response.noContent().build(); } @POST @@ -126,6 +131,7 @@ public void delete(@PathParam("id") final UUID id) { @Transactional @APIResponse(responseCode = "200") @APIResponse(responseCode = "404") + @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response publish(@PathParam("id") final UUID id, @Valid @NotNull final PostPublication input) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); @@ -172,6 +178,7 @@ public void deleteSubscription(@PathParam("id") final UUID id) { @Transactional @APIResponse(responseCode = "200") @APIResponse(responseCode = "404") + @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response vote(@PathParam("id") final UUID id, @Valid @NotNull final VoteCreation input) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id, LockModeType.PESSIMISTIC_WRITE); diff --git a/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java index 7345c4b..aec3d6d 100644 --- a/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java @@ -1,5 +1,6 @@ package app.fyreplace.api.endpoints; +import app.fyreplace.api.cache.DuplicateRequestKeyGenerator; import app.fyreplace.api.data.Email; import app.fyreplace.api.data.NewTokenCreation; import app.fyreplace.api.data.RandomCode; @@ -7,6 +8,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.emails.UserConnectionEmail; import app.fyreplace.api.services.JwtService; +import io.quarkus.cache.CacheResult; import io.quarkus.security.Authenticated; import jakarta.inject.Inject; import jakarta.transaction.Transactional; @@ -43,6 +45,7 @@ public final class TokensEndpoint { content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) @APIResponse(responseCode = "400") @APIResponse(responseCode = "404") + @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response create(@Valid @NotNull final TokenCreation input) { final var email = getEmail(input.identifier()); final var randomCode = RandomCode.find("email = ?1 and code = ?2", email, input.code()) @@ -75,6 +78,7 @@ public String retrieveNew() { @APIResponse(responseCode = "200") @APIResponse(responseCode = "400") @APIResponse(responseCode = "404") + @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response createNew(@NotNull @Valid final NewTokenCreation input) { final var email = getEmail(input.identifier()); userConnectionEmail.sendTo(email); diff --git a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java index 7c18c0c..95621ad 100644 --- a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java @@ -1,5 +1,6 @@ package app.fyreplace.api.endpoints; +import app.fyreplace.api.cache.DuplicateRequestKeyGenerator; import app.fyreplace.api.data.Block; import app.fyreplace.api.data.Email; import app.fyreplace.api.data.StoredFile; @@ -10,6 +11,7 @@ import app.fyreplace.api.exceptions.ForbiddenException; import app.fyreplace.api.services.MimeTypeService; import app.fyreplace.api.services.mimetype.KnownMimeTypes; +import io.quarkus.cache.CacheResult; import io.quarkus.panache.common.Sort; import io.quarkus.security.Authenticated; import jakarta.annotation.Nullable; @@ -66,6 +68,7 @@ public final class UsersEndpoint { @APIResponse(responseCode = "400") @APIResponse(responseCode = "403") @APIResponse(responseCode = "409") + @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response create(@Valid @NotNull final UserCreation input) { if (User.forbiddenUsernames.contains(input.username())) { throw new ForbiddenException("username_forbidden"); @@ -226,8 +229,10 @@ public void deleteMeAvatar() { @Authenticated @Transactional @APIResponse(responseCode = "204") - public void deleteMe() { + @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) + public Response deleteMe() { User.getFromSecurityContext(context).delete(); + return Response.noContent().build(); } @GET From 2269b942fc6f3c1a014db185f4f3b1590a0b430c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Tue, 17 Oct 2023 12:44:10 +0200 Subject: [PATCH 076/157] Use PUT correctly --- .../app/fyreplace/api/data/BlockUpdate.java | 3 + .../api/data/SubscriptionUpdate.java | 3 + .../java/app/fyreplace/api/data/Vote.java | 2 +- .../app/fyreplace/api/data/VoteCreation.java | 2 +- .../api/endpoints/PostsEndpoint.java | 32 ++++--- .../api/endpoints/UsersEndpoint.java | 21 ++--- ...java => UpdateSubscribedToFalseTests.java} | 86 +++++++++++++----- ....java => UpdateSubscribedToTrueTests.java} | 90 ++++++++++++++----- .../testing/endpoints/posts/VoteTests.java | 4 +- ...ts.java => UpdateBlockedToFalseTests.java} | 42 ++++++--- ...sts.java => UpdateBlockedToTrueTests.java} | 50 ++++++++--- 11 files changed, 235 insertions(+), 100 deletions(-) create mode 100644 src/main/java/app/fyreplace/api/data/BlockUpdate.java create mode 100644 src/main/java/app/fyreplace/api/data/SubscriptionUpdate.java rename src/test/java/app/fyreplace/api/testing/endpoints/posts/{DeleteSubscriptionTests.java => UpdateSubscribedToFalseTests.java} (55%) rename src/test/java/app/fyreplace/api/testing/endpoints/posts/{CreateSubscriptionTests.java => UpdateSubscribedToTrueTests.java} (57%) rename src/test/java/app/fyreplace/api/testing/endpoints/users/{DeleteBlockTests.java => UpdateBlockedToFalseTests.java} (63%) rename src/test/java/app/fyreplace/api/testing/endpoints/users/{CreateBlockTests.java => UpdateBlockedToTrueTests.java} (59%) diff --git a/src/main/java/app/fyreplace/api/data/BlockUpdate.java b/src/main/java/app/fyreplace/api/data/BlockUpdate.java new file mode 100644 index 0000000..d57e851 --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/BlockUpdate.java @@ -0,0 +1,3 @@ +package app.fyreplace.api.data; + +public record BlockUpdate(boolean blocked) {} diff --git a/src/main/java/app/fyreplace/api/data/SubscriptionUpdate.java b/src/main/java/app/fyreplace/api/data/SubscriptionUpdate.java new file mode 100644 index 0000000..4060857 --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/SubscriptionUpdate.java @@ -0,0 +1,3 @@ +package app.fyreplace.api.data; + +public record SubscriptionUpdate(boolean subscribed) {} diff --git a/src/main/java/app/fyreplace/api/data/Vote.java b/src/main/java/app/fyreplace/api/data/Vote.java index c04fd8d..1af74a5 100644 --- a/src/main/java/app/fyreplace/api/data/Vote.java +++ b/src/main/java/app/fyreplace/api/data/Vote.java @@ -25,5 +25,5 @@ public class Vote extends EntityBase { public Post post; @Column(nullable = false) - public boolean isSpread; + public boolean spread; } diff --git a/src/main/java/app/fyreplace/api/data/VoteCreation.java b/src/main/java/app/fyreplace/api/data/VoteCreation.java index 9cd4bda..d04b915 100644 --- a/src/main/java/app/fyreplace/api/data/VoteCreation.java +++ b/src/main/java/app/fyreplace/api/data/VoteCreation.java @@ -1,3 +1,3 @@ package app.fyreplace.api.data; -public record VoteCreation(boolean isSpread) {} +public record VoteCreation(boolean spread) {} diff --git a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java index 6f6949b..d0af9ab 100644 --- a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java @@ -5,6 +5,7 @@ import app.fyreplace.api.data.Post; import app.fyreplace.api.data.PostPublication; import app.fyreplace.api.data.Subscription; +import app.fyreplace.api.data.SubscriptionUpdate; import app.fyreplace.api.data.User; import app.fyreplace.api.data.Vote; import app.fyreplace.api.data.VoteCreation; @@ -130,6 +131,7 @@ public Response delete(@PathParam("id") final UUID id) { @Authenticated @Transactional @APIResponse(responseCode = "200") + @APIResponse(responseCode = "400") @APIResponse(responseCode = "404") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response publish(@PathParam("id") final UUID id, @Valid @NotNull final PostPublication input) { @@ -150,26 +152,20 @@ public Response publish(@PathParam("id") final UUID id, @Valid @NotNull final Po @Authenticated @Transactional @APIResponse(responseCode = "200") + @APIResponse(responseCode = "400") @APIResponse(responseCode = "404") - public Response createSubscription(@PathParam("id") final UUID id) { + public Response updateSubscribed(@PathParam("id") final UUID id, @Valid @NotNull final SubscriptionUpdate input) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); Post.validateAccess(post, user, true, false); - user.subscribeTo(post); - return Response.ok().build(); - } - @DELETE - @Path("{id}/subscribed") - @Authenticated - @Transactional - @APIResponse(responseCode = "204") - @APIResponse(responseCode = "404") - public void deleteSubscription(@PathParam("id") final UUID id) { - final var user = User.getFromSecurityContext(context); - final var post = Post.findById(id); - Post.validateAccess(post, user, true, false); - user.unsubscribeFrom(post); + if (input.subscribed()) { + user.subscribeTo(post); + } else { + user.unsubscribeFrom(post); + } + + return Response.ok().build(); } @POST @@ -177,6 +173,7 @@ public void deleteSubscription(@PathParam("id") final UUID id) { @Authenticated @Transactional @APIResponse(responseCode = "200") + @APIResponse(responseCode = "400") @APIResponse(responseCode = "404") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response vote(@PathParam("id") final UUID id, @Valid @NotNull final VoteCreation input) { @@ -193,9 +190,9 @@ public Response vote(@PathParam("id") final UUID id, @Valid @NotNull final VoteC final var vote = new Vote(); vote.user = user; vote.post = post; - vote.isSpread = input.isSpread(); + vote.spread = input.spread(); vote.persist(); - post.life += vote.isSpread ? 1 : -1; + post.life += vote.spread ? 1 : -1; post.persist(); return Response.ok().build(); } @@ -204,6 +201,7 @@ public Response vote(@PathParam("id") final UUID id, @Valid @NotNull final VoteC @Path("count") @Authenticated @APIResponse(responseCode = "200") + @APIResponse(responseCode = "400") public long count(@QueryParam("type") @NotNull final PostListingType type) { final var user = User.getFromSecurityContext(context); return switch (type) { diff --git a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java index 95621ad..2c57bdf 100644 --- a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java @@ -2,6 +2,7 @@ import app.fyreplace.api.cache.DuplicateRequestKeyGenerator; import app.fyreplace.api.data.Block; +import app.fyreplace.api.data.BlockUpdate; import app.fyreplace.api.data.Email; import app.fyreplace.api.data.StoredFile; import app.fyreplace.api.data.User; @@ -108,34 +109,24 @@ public User retrieve(@PathParam("id") final UUID id) { @Authenticated @Transactional @APIResponse(responseCode = "200") + @APIResponse(responseCode = "400") @APIResponse(responseCode = "404") - public Response createBlock(@PathParam("id") final UUID id) { + public Response updateBlocked(@PathParam("id") final UUID id, @Valid @NotNull final BlockUpdate input) { final var source = User.getFromSecurityContext(context); final var target = User.findById(id); validateUser(target); if (source.id.equals(target.id)) { throw new ForbiddenException("user_is_self"); - } else { + } else if (input.blocked()) { source.block(target); + } else { + source.unblock(target); } return Response.ok().build(); } - @DELETE - @Path("{id}/blocked") - @Authenticated - @Transactional - @APIResponse(responseCode = "204") - @APIResponse(responseCode = "404") - public void deleteBlock(@PathParam("id") final UUID id) { - final var source = User.getFromSecurityContext(context); - final var target = User.findById(id); - validateUser(target); - source.unblock(target); - } - @PUT @Path("{id}/banned") @RolesAllowed({"ADMINISTRATOR", "MODERATOR"}) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteSubscriptionTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateSubscribedToFalseTests.java similarity index 55% rename from src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteSubscriptionTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateSubscribedToFalseTests.java index 0b1cbfb..ff08531 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteSubscriptionTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateSubscribedToFalseTests.java @@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import app.fyreplace.api.data.Subscription; +import app.fyreplace.api.data.SubscriptionUpdate; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.PostsEndpoint; import app.fyreplace.api.testing.PostTestsBase; @@ -14,6 +15,7 @@ import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; import jakarta.transaction.Transactional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -22,49 +24,73 @@ @QuarkusTest @TestHTTPEndpoint(PostsEndpoint.class) -public final class DeleteSubscriptionTests extends PostTestsBase { +public final class UpdateSubscribedToFalseTests extends PostTestsBase { @Test @TestSecurity(user = "user_1") - public void deleteSubscriptionWithOtherPost() { + public void updateSubscribedWithOtherPost() { final var user = requireNonNull(User.findByUsername("user_1")); assertTrue(user.isSubscribedTo(post)); - given().delete(post.id + "/subscribed").then().statusCode(204); + given().contentType(ContentType.JSON) + .body(new SubscriptionUpdate(false)) + .put(post.id + "/subscribed") + .then() + .statusCode(200); assertFalse(user.isSubscribedTo(post)); } @Test @TestSecurity(user = "user_1") - public void deleteSubscriptionWithOtherPostTwice() { + public void updateSubscribedWithOtherPostTwice() { final var user = requireNonNull(User.findByUsername("user_1")); - given().delete(post.id + "/subscribed").then().statusCode(204); - given().delete(post.id + "/subscribed").then().statusCode(204); + given().contentType(ContentType.JSON) + .body(new SubscriptionUpdate(false)) + .put(post.id + "/subscribed") + .then() + .statusCode(200); + given().contentType(ContentType.JSON) + .body(new SubscriptionUpdate(false)) + .put(post.id + "/subscribed") + .then() + .statusCode(200); assertFalse(user.isSubscribedTo(post)); } @Test @TestSecurity(user = "user_0") - public void deleteSubscriptionWithOwnPost() { + public void updateSubscribedWithOwnPost() { final var user = requireNonNull(User.findByUsername("user_0")); assertTrue(user.isSubscribedTo(post)); - given().delete(post.id + "/subscribed").then().statusCode(204); + given().contentType(ContentType.JSON) + .body(new SubscriptionUpdate(false)) + .put(post.id + "/subscribed") + .then() + .statusCode(200); assertFalse(user.isSubscribedTo(post)); } @Test @TestSecurity(user = "user_1") - public void deleteSubscriptionWithOtherDraft() { + public void updateSubscribedWithOtherDraft() { final var user = requireNonNull(User.findByUsername("user_1")); assertFalse(user.isSubscribedTo(draft)); - given().delete(draft.id + "/subscribed").then().statusCode(404); + given().contentType(ContentType.JSON) + .body(new SubscriptionUpdate(false)) + .put(draft.id + "/subscribed") + .then() + .statusCode(404); assertFalse(user.isSubscribedTo(draft)); } @Test @TestSecurity(user = "user_0") - public void deleteSubscriptionWithOwnDraft() { + public void updateSubscribedWithOwnDraft() { final var user = requireNonNull(User.findByUsername("user_0")); assertFalse(user.isSubscribedTo(draft)); - given().delete(draft.id + "/subscribed").then().statusCode(403); + given().contentType(ContentType.JSON) + .body(new SubscriptionUpdate(false)) + .put(draft.id + "/subscribed") + .then() + .statusCode(403); assertFalse(user.isSubscribedTo(draft)); } @@ -74,7 +100,11 @@ public void deleteToOtherPostWhenBlocked() { final var user = requireNonNull(User.findByUsername("user_1")); QuarkusTransaction.requiringNew().run(() -> post.author.block(user)); assertFalse(user.isSubscribedTo(post)); - given().delete(post.id + "/subscribed").then().statusCode(403); + given().contentType(ContentType.JSON) + .body(new SubscriptionUpdate(false)) + .put(post.id + "/subscribed") + .then() + .statusCode(403); assertFalse(user.isSubscribedTo(post)); } @@ -87,30 +117,46 @@ public void deleteToOtherAnonymousPostWhenBlocked() { user.subscribeTo(anonymousPost); }); assertTrue(user.isSubscribedTo(anonymousPost)); - given().delete(anonymousPost.id + "/subscribed").then().statusCode(204); + given().contentType(ContentType.JSON) + .body(new SubscriptionUpdate(false)) + .put(anonymousPost.id + "/subscribed") + .then() + .statusCode(200); assertFalse(user.isSubscribedTo(anonymousPost)); } @Test - public void deleteSubscriptionWithPostUnauthenticated() { + public void updateSubscribedWithPostUnauthenticated() { final var subscriptionCount = Subscription.count(); - given().delete(post.id + "/subscribed").then().statusCode(401); + given().contentType(ContentType.JSON) + .body(new SubscriptionUpdate(false)) + .put(post.id + "/subscribed") + .then() + .statusCode(401); assertEquals(subscriptionCount, Subscription.count()); } @Test - public void deleteSubscriptionWithDraftUnauthenticated() { + public void updateSubscribedWithDraftUnauthenticated() { final var subscriptionCount = Subscription.count(); - given().delete(draft.id + "/subscribed").then().statusCode(401); + given().contentType(ContentType.JSON) + .body(new SubscriptionUpdate(false)) + .put(draft.id + "/subscribed") + .then() + .statusCode(401); assertEquals(subscriptionCount, Subscription.count()); } @ParameterizedTest @ValueSource(strings = {"fake", "00000000-0000-0000-0000-000000000000"}) @TestSecurity(user = "user_0") - public void deleteSubscriptionWithNonExistent(final String id) { + public void updateSubscribedWithNonExistent(final String id) { final var subscriptionCount = Subscription.count(); - given().delete(id + "/subscribed").then().statusCode(404); + given().contentType(ContentType.JSON) + .body(new SubscriptionUpdate(false)) + .put(id + "/subscribed") + .then() + .statusCode(404); assertEquals(subscriptionCount, Subscription.count()); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateSubscriptionTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateSubscribedToTrueTests.java similarity index 57% rename from src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateSubscriptionTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateSubscribedToTrueTests.java index a8763a3..d6c2e59 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateSubscriptionTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateSubscribedToTrueTests.java @@ -10,6 +10,7 @@ import app.fyreplace.api.data.Comment; import app.fyreplace.api.data.Subscription; +import app.fyreplace.api.data.SubscriptionUpdate; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.PostsEndpoint; import app.fyreplace.api.testing.CommentTestsBase; @@ -17,19 +18,24 @@ import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @QuarkusTest @TestHTTPEndpoint(PostsEndpoint.class) -public final class CreateSubscriptionTests extends CommentTestsBase { +public final class UpdateSubscribedToTrueTests extends CommentTestsBase { @Test @TestSecurity(user = "user_1") - public void createSubscriptionWithOtherPost() { + public void updateSubscribedWithOtherPost() { final var user = requireNonNull(User.findByUsername("user_1")); assertFalse(user.isSubscribedTo(post)); - given().put(post.id + "/subscribed").then().statusCode(200); + given().contentType(ContentType.JSON) + .body(new SubscriptionUpdate(true)) + .put(post.id + "/subscribed") + .then() + .statusCode(200); final var subscription = Subscription.find("user = ?1 and post = ?2", user, post) .firstResult(); assertNotNull(subscription); @@ -40,10 +46,18 @@ public void createSubscriptionWithOtherPost() { @Test @TestSecurity(user = "user_1") - public void createSubscriptionWithOtherPostTwice() { + public void updateSubscribedWithOtherPostTwice() { final var user = User.findByUsername("user_1"); - given().put(post.id + "/subscribed").then().statusCode(200); - given().put(post.id + "/subscribed").then().statusCode(200); + given().contentType(ContentType.JSON) + .body(new SubscriptionUpdate(true)) + .put(post.id + "/subscribed") + .then() + .statusCode(200); + given().contentType(ContentType.JSON) + .body(new SubscriptionUpdate(true)) + .put(post.id + "/subscribed") + .then() + .statusCode(200); final var subscription = Subscription.find("user = ?1 and post = ?2", user, post) .firstResult(); assertNotNull(subscription); @@ -54,10 +68,14 @@ public void createSubscriptionWithOtherPostTwice() { @Test @TestSecurity(user = "user_0") - public void createSubscriptionWithOwnPost() { + public void updateSubscribedWithOwnPost() { final var user = requireNonNull(User.findByUsername("user_0")); assertTrue(user.isSubscribedTo(post)); - given().put(post.id + "/subscribed").then().statusCode(200); + given().contentType(ContentType.JSON) + .body(new SubscriptionUpdate(true)) + .put(post.id + "/subscribed") + .then() + .statusCode(200); final var subscription = Subscription.find("user = ?1 and post = ?2", user, post) .firstResult(); assertNotNull(subscription); @@ -66,62 +84,90 @@ public void createSubscriptionWithOwnPost() { @Test @TestSecurity(user = "user_1") - public void createSubscriptionWithOtherDraft() { + public void updateSubscribedWithOtherDraft() { final var user = requireNonNull(User.findByUsername("user_1")); assertFalse(user.isSubscribedTo(draft)); - given().put(draft.id + "/subscribed").then().statusCode(404); + given().contentType(ContentType.JSON) + .body(new SubscriptionUpdate(true)) + .put(draft.id + "/subscribed") + .then() + .statusCode(404); assertFalse(user.isSubscribedTo(draft)); } @Test @TestSecurity(user = "user_0") - public void createSubscriptionWithOwnDraft() { + public void updateSubscribedWithOwnDraft() { final var user = requireNonNull(User.findByUsername("user_0")); assertFalse(user.isSubscribedTo(draft)); - given().put(draft.id + "/subscribed").then().statusCode(403); + given().contentType(ContentType.JSON) + .body(new SubscriptionUpdate(true)) + .put(draft.id + "/subscribed") + .then() + .statusCode(403); assertFalse(user.isSubscribedTo(draft)); } @Test @TestSecurity(user = "user_1") - public void createSubscriptionWithOtherPostWhenBlocked() { + public void updateSubscribedWithOtherPostWhenBlocked() { final var user = requireNonNull(User.findByUsername("user_1")); QuarkusTransaction.requiringNew().run(() -> post.author.block(user)); assertFalse(user.isSubscribedTo(post)); - given().put(post.id + "/subscribed").then().statusCode(403); + given().contentType(ContentType.JSON) + .body(new SubscriptionUpdate(true)) + .put(post.id + "/subscribed") + .then() + .statusCode(403); assertFalse(user.isSubscribedTo(post)); } @Test @TestSecurity(user = "user_1") - public void createSubscriptionWithOtherAnonymousPostWhenBlocked() { + public void updateSubscribedWithOtherAnonymousPostWhenBlocked() { final var user = requireNonNull(User.findByUsername("user_1")); QuarkusTransaction.requiringNew().run(() -> anonymousPost.author.block(user)); assertFalse(user.isSubscribedTo(anonymousPost)); - given().put(anonymousPost.id + "/subscribed").then().statusCode(200); + given().contentType(ContentType.JSON) + .body(new SubscriptionUpdate(true)) + .put(anonymousPost.id + "/subscribed") + .then() + .statusCode(200); assertTrue(user.isSubscribedTo(anonymousPost)); } @Test - public void createSubscriptionWithPostUnauthenticated() { + public void updateSubscribedWithPostUnauthenticated() { final var subscriptionCount = Subscription.count(); - given().put(post.id + "/subscribed").then().statusCode(401); + given().contentType(ContentType.JSON) + .body(new SubscriptionUpdate(true)) + .put(post.id + "/subscribed") + .then() + .statusCode(401); assertEquals(subscriptionCount, Subscription.count()); } @Test - public void createSubscriptionWithDraftUnauthenticated() { + public void updateSubscribedWithDraftUnauthenticated() { final var subscriptionCount = Subscription.count(); - given().put(draft.id + "/subscribed").then().statusCode(401); + given().contentType(ContentType.JSON) + .body(new SubscriptionUpdate(true)) + .put(draft.id + "/subscribed") + .then() + .statusCode(401); assertEquals(subscriptionCount, Subscription.count()); } @ParameterizedTest @ValueSource(strings = {"fake", "00000000-0000-0000-0000-000000000000"}) @TestSecurity(user = "user_0") - public void createSubscriptionWithNonExistent(final String id) { + public void updateSubscribedWithNonExistent(final String id) { final var subscriptionCount = Subscription.count(); - given().put(id + "/subscribed").then().statusCode(404); + given().contentType(ContentType.JSON) + .body(new SubscriptionUpdate(true)) + .put(id + "/subscribed") + .then() + .statusCode(404); assertEquals(subscriptionCount, Subscription.count()); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/VoteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/VoteTests.java index 620f314..aa7c101 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/VoteTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/VoteTests.java @@ -38,7 +38,7 @@ public void voteWithSpread() { assertEquals(voteCount + 1, Vote.count()); final var vote = Vote.find("post = ?1 and user.username = 'user_1'", post).firstResult(); - assertTrue(vote.isSpread); + assertTrue(vote.spread); assertEquals(postLife + 1, vote.post.life); } @@ -56,7 +56,7 @@ public void voteWithoutSpread() { assertEquals(voteCount + 1, Vote.count()); final var vote = Vote.find("post = ?1 and user.username = 'user_1'", post).firstResult(); - assertFalse(vote.isSpread); + assertFalse(vote.spread); assertEquals(postLife - 1, vote.post.life); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBlockedToFalseTests.java similarity index 63% rename from src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBlockedToFalseTests.java index 215350c..2735c19 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteBlockTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBlockedToFalseTests.java @@ -7,56 +7,78 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import app.fyreplace.api.data.Block; +import app.fyreplace.api.data.BlockUpdate; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; import app.fyreplace.api.testing.UserTestsBase; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; import jakarta.transaction.Transactional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class DeleteBlockTests extends UserTestsBase { +public final class UpdateBlockedToFalseTests extends UserTestsBase { @Test @TestSecurity(user = "user_0") - public void deleteBlock() { + public void updateBlocked() { final var user = requireNonNull(User.findByUsername("user_0")); final var otherUser = requireNonNull(User.findByUsername("user_1")); assertTrue(user.isBlocking(otherUser)); - given().delete(otherUser.id + "/blocked").then().statusCode(204); + given().contentType(ContentType.JSON) + .body(new BlockUpdate(false)) + .put(otherUser.id + "/blocked") + .then() + .statusCode(200); assertFalse(user.isBlocking(otherUser)); } @Test @TestSecurity(user = "user_0") - public void deleteBlockTwice() { + public void updateBlockedTwice() { final var user = requireNonNull(User.findByUsername("user_0")); final var otherUser = requireNonNull(User.findByUsername("user_1")); assertTrue(user.isBlocking(otherUser)); - given().delete(otherUser.id + "/blocked").then().statusCode(204); - given().delete(otherUser.id + "/blocked").then().statusCode(204); + given().contentType(ContentType.JSON) + .body(new BlockUpdate(false)) + .put(otherUser.id + "/blocked") + .then() + .statusCode(200); + given().contentType(ContentType.JSON) + .body(new BlockUpdate(false)) + .put(otherUser.id + "/blocked") + .then() + .statusCode(200); assertFalse(user.isBlocking(otherUser)); } @Test @TestSecurity(user = "user_0") - public void deleteBlockWithInactiveUser() { + public void updateBlockedWithInactiveUser() { final var user = User.findByUsername("user_0"); final var otherUser = requireNonNull(User.findByUsername("user_inactive_1")); final var blockCount = Block.count("source", user); - given().delete(otherUser.id + "/blocked").then().statusCode(404); + given().contentType(ContentType.JSON) + .body(new BlockUpdate(false)) + .put(otherUser.id + "/blocked") + .then() + .statusCode(404); assertEquals(blockCount, Block.count("source", user)); } @Test @TestSecurity(user = "user_0") - public void deleteBlockWithInvalidUser() { + public void updateBlockedWithInvalidUser() { final var user = User.findByUsername("user_0"); final var blockCount = Block.count("source", user); - given().delete("invalid/blocked").then().statusCode(404); + given().contentType(ContentType.JSON) + .body(new BlockUpdate(false)) + .put("invalid/blocked") + .then() + .statusCode(404); assertEquals(blockCount, Block.count("source", user)); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBlockedToTrueTests.java similarity index 59% rename from src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBlockedToTrueTests.java index d700518..5efa83e 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateBlockTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBlockedToTrueTests.java @@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import app.fyreplace.api.data.Block; +import app.fyreplace.api.data.BlockUpdate; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; import app.fyreplace.api.testing.PostTestsBase; @@ -14,58 +15,83 @@ import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; import org.junit.jupiter.api.Test; @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class CreateBlockTests extends PostTestsBase { +public final class UpdateBlockedToTrueTests extends PostTestsBase { @Test @TestSecurity(user = "user_0") - public void createBlock() { + public void updateBlocked() { final var user = requireNonNull(User.findByUsername("user_0")); final var otherUser = requireNonNull(User.findByUsername("user_1")); QuarkusTransaction.requiringNew().run(() -> otherUser.subscribeTo(post)); assertFalse(user.isBlocking(otherUser)); - given().put(otherUser.id + "/blocked").then().statusCode(200); + given().contentType(ContentType.JSON) + .body(new BlockUpdate(true)) + .put(otherUser.id + "/blocked") + .then() + .statusCode(200); assertTrue(user.isBlocking(otherUser)); assertFalse(otherUser.isSubscribedTo(post)); } @Test @TestSecurity(user = "user_0") - public void createBlockTwice() { + public void updateBlockedTwice() { final var user = requireNonNull(User.findByUsername("user_0")); final var otherUser = requireNonNull(User.findByUsername("user_1")); assertFalse(user.isBlocking(otherUser)); - given().put(otherUser.id + "/blocked").then().statusCode(200); - given().put(otherUser.id + "/blocked").then().statusCode(200); + given().contentType(ContentType.JSON) + .body(new BlockUpdate(true)) + .put(otherUser.id + "/blocked") + .then() + .statusCode(200); + given().contentType(ContentType.JSON) + .body(new BlockUpdate(true)) + .put(otherUser.id + "/blocked") + .then() + .statusCode(200); assertTrue(user.isBlocking(otherUser)); } @Test @TestSecurity(user = "user_0") - public void createBlockWithInactiveUser() { + public void updateBlockedWithInactiveUser() { final var user = User.findByUsername("user_0"); final var otherUser = requireNonNull(User.findByUsername("user_inactive_1")); final var blockCount = Block.count("source", user); - given().put(otherUser.id + "/blocked").then().statusCode(404); + given().contentType(ContentType.JSON) + .body(new BlockUpdate(true)) + .put(otherUser.id + "/blocked") + .then() + .statusCode(404); assertEquals(blockCount, Block.count("source", user)); } @Test @TestSecurity(user = "user_0") - public void createBlockWithInvalidUser() { + public void updateBlockedWithInvalidUser() { final var user = User.findByUsername("user_0"); final var blockCount = Block.count("source", user); - given().put("invalid/blocked").then().statusCode(404); + given().contentType(ContentType.JSON) + .body(new BlockUpdate(true)) + .put("invalid/blocked") + .then() + .statusCode(404); assertEquals(blockCount, Block.count("source", user)); } @Test @TestSecurity(user = "user_0") - public void createBlockWithSelf() { + public void updateBlockedWithSelf() { final var user = requireNonNull(User.findByUsername("user_0")); - given().put(user.id + "/blocked").then().statusCode(403); + given().contentType(ContentType.JSON) + .body(new BlockUpdate(true)) + .put(user.id + "/blocked") + .then() + .statusCode(403); assertFalse(user.isBlocking(user)); } } From af48e22b30c40dd5ead6179e09728a2df52547d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 21 Oct 2023 11:16:43 +0200 Subject: [PATCH 077/157] Add push notifications --- .../api/data/PushNotificationToken.java | 33 +++++++++++++ .../data/PushNotificationTokenCreation.java | 6 +++ .../fyreplace/api/data/dev/DataSeeder.java | 2 + .../api/endpoints/CommentsEndpoint.java | 7 +++ .../PushNotificationTokensEndpoint.java | 40 ++++++++++++++++ .../PushNotificationDispatcher.java | 7 +++ .../pushnotificationtokens/UpdateTests.java | 48 +++++++++++++++++++ 7 files changed, 143 insertions(+) create mode 100644 src/main/java/app/fyreplace/api/data/PushNotificationToken.java create mode 100644 src/main/java/app/fyreplace/api/data/PushNotificationTokenCreation.java create mode 100644 src/main/java/app/fyreplace/api/endpoints/PushNotificationTokensEndpoint.java create mode 100644 src/main/java/app/fyreplace/api/pushnotifications/PushNotificationDispatcher.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/pushnotificationtokens/UpdateTests.java diff --git a/src/main/java/app/fyreplace/api/data/PushNotificationToken.java b/src/main/java/app/fyreplace/api/data/PushNotificationToken.java new file mode 100644 index 0000000..c6aca49 --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/PushNotificationToken.java @@ -0,0 +1,33 @@ +package app.fyreplace.api.data; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +@Entity +@Table(name = "push_notification_tokens") +public class PushNotificationToken extends EntityBase { + @ManyToOne(optional = false) + @OnDelete(action = OnDeleteAction.CASCADE) + @JsonIgnore + public User user; + + @Column(nullable = false) + public Service service; + + @Column(nullable = false) + public String token; + + public enum Service { + APPLE, + FIREBASE, + HUAWEI, + SAMSUNG, + WEB, + WINDOWS + } +} diff --git a/src/main/java/app/fyreplace/api/data/PushNotificationTokenCreation.java b/src/main/java/app/fyreplace/api/data/PushNotificationTokenCreation.java new file mode 100644 index 0000000..1a4914f --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/PushNotificationTokenCreation.java @@ -0,0 +1,6 @@ +package app.fyreplace.api.data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record PushNotificationTokenCreation(@NotNull PushNotificationToken.Service service, @NotBlank String token) {} diff --git a/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java b/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java index 3f107c8..f2c7de0 100644 --- a/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java +++ b/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java @@ -7,6 +7,7 @@ import app.fyreplace.api.data.Comment; import app.fyreplace.api.data.Email; import app.fyreplace.api.data.Post; +import app.fyreplace.api.data.PushNotificationToken; import app.fyreplace.api.data.RandomCode; import app.fyreplace.api.data.StoredFile; import app.fyreplace.api.data.Subscription; @@ -59,6 +60,7 @@ public void deleteData() { Email.deleteAll(); RandomCode.deleteAll(); Block.deleteAll(); + PushNotificationToken.deleteAll(); User.deleteAll(); Subscription.deleteAll(); Vote.deleteAll(); diff --git a/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java index 9006bb4..11219ac 100644 --- a/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java @@ -7,9 +7,12 @@ import app.fyreplace.api.data.Subscription; import app.fyreplace.api.data.User; import app.fyreplace.api.exceptions.ForbiddenException; +import app.fyreplace.api.pushnotifications.PushNotificationDispatcher; import io.quarkus.cache.CacheResult; import io.quarkus.security.Authenticated; import jakarta.annotation.Nullable; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; import jakarta.transaction.Transactional; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; @@ -37,6 +40,9 @@ public final class CommentsEndpoint { @ConfigProperty(name = "app.paging.size") int pagingSize; + @Inject + Instance pushNotificationDispatchers; + @Context SecurityContext context; @@ -80,6 +86,7 @@ public Response create(@PathParam("id") final UUID id, @Valid @NotNull final Com comment.anonymous = input.anonymous(); comment.persist(); comment.setCurrentUser(user); + pushNotificationDispatchers.forEach(d -> d.dispatch(comment)); return Response.status(Status.CREATED).entity(comment).build(); } diff --git a/src/main/java/app/fyreplace/api/endpoints/PushNotificationTokensEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/PushNotificationTokensEndpoint.java new file mode 100644 index 0000000..7277f10 --- /dev/null +++ b/src/main/java/app/fyreplace/api/endpoints/PushNotificationTokensEndpoint.java @@ -0,0 +1,40 @@ +package app.fyreplace.api.endpoints; + +import app.fyreplace.api.data.PushNotificationToken; +import app.fyreplace.api.data.PushNotificationTokenCreation; +import app.fyreplace.api.data.User; +import io.quarkus.security.Authenticated; +import jakarta.transaction.Transactional; +import jakarta.validation.Valid; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.SecurityContext; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; + +@Path("push-notification-tokens") +public class PushNotificationTokensEndpoint { + @Context + SecurityContext context; + + @PUT + @Authenticated + @Transactional + @APIResponse(responseCode = "200") + @APIResponse(responseCode = "400") + public PushNotificationToken update(@Valid final PushNotificationTokenCreation input) { + final var user = User.getFromSecurityContext(context); + var token = PushNotificationToken.find("user = ?1 and token = ?2", user, input.token()) + .firstResult(); + + if (token == null) { + token = new PushNotificationToken(); + token.user = user; + token.service = input.service(); + token.token = input.token(); + token.persist(); + } + + return token; + } +} diff --git a/src/main/java/app/fyreplace/api/pushnotifications/PushNotificationDispatcher.java b/src/main/java/app/fyreplace/api/pushnotifications/PushNotificationDispatcher.java new file mode 100644 index 0000000..342f63a --- /dev/null +++ b/src/main/java/app/fyreplace/api/pushnotifications/PushNotificationDispatcher.java @@ -0,0 +1,7 @@ +package app.fyreplace.api.pushnotifications; + +import app.fyreplace.api.data.Comment; + +public interface PushNotificationDispatcher { + void dispatch(final Comment comment); +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/pushnotificationtokens/UpdateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/pushnotificationtokens/UpdateTests.java new file mode 100644 index 0000000..10bca32 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/pushnotificationtokens/UpdateTests.java @@ -0,0 +1,48 @@ +package app.fyreplace.api.testing.endpoints.pushnotificationtokens; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import app.fyreplace.api.data.PushNotificationToken; +import app.fyreplace.api.data.PushNotificationTokenCreation; +import app.fyreplace.api.endpoints.PushNotificationTokensEndpoint; +import app.fyreplace.api.testing.UserTestsBase; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(PushNotificationTokensEndpoint.class) +public class UpdateTests extends UserTestsBase { + @Test + @TestSecurity(user = "user_0") + public void update() { + final var tokenCount = PushNotificationToken.count(); + given().contentType(ContentType.JSON) + .body(new PushNotificationTokenCreation(PushNotificationToken.Service.WEB, "token")) + .put() + .then() + .statusCode(200); + assertEquals(tokenCount + 1, PushNotificationToken.count()); + final var token = PushNotificationToken.find( + "user.username = 'user_0' and token = 'token'") + .firstResult(); + assertEquals(PushNotificationToken.Service.WEB, token.service); + } + + @Test + @TestSecurity(user = "user_0") + public void updateTwice() { + final var tokenCount = PushNotificationToken.count(); + final var input = new PushNotificationTokenCreation(PushNotificationToken.Service.WEB, "token"); + given().contentType(ContentType.JSON).body(input).put().then().statusCode(200); + given().contentType(ContentType.JSON).body(input).put().then().statusCode(200); + assertEquals(tokenCount + 1, PushNotificationToken.count()); + final var token = PushNotificationToken.find( + "user.username = 'user_0' and token = 'token'") + .firstResult(); + assertEquals(PushNotificationToken.Service.WEB, token.service); + } +} From a758e3917bbf8a22b1eb311c13f66718b6e96f87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 21 Oct 2023 12:01:55 +0200 Subject: [PATCH 078/157] Use Sentry Gradle plugin --- build.gradle | 9 +++++++++ gradle.properties | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 99ae602..789a6f5 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,7 @@ plugins { id "io.quarkus" id "com.palantir.git-version" version "${gitPluginVersion}" id "com.diffplug.spotless" version "${spotlessPluginVersion}" + id "io.sentry.jvm.gradle" version "${sentryPluginVersion}" } group = "app.fyreplace" @@ -49,6 +50,14 @@ dependencies { testImplementation("org.apiguardian:apiguardian-api:+") } +sentry { + includeSourceContext = true + + autoInstallation { + enabled.set(true) + } +} + java { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 diff --git a/gradle.properties b/gradle.properties index 2d0025b..3c500db 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,7 @@ quarkusVersion=3.4.3 quarkusAmazonVersion=2.5.2 -sentryVersion=6.31.0 +sentryVersion=6.32.0 tikaVersion=2.9.0 gitPluginVersion=3.0.0 spotlessPluginVersion=6.22.0 +sentryPluginVersion=3.14.0 From 117675e48770ca888ace0e6a8d4f7d168a6b3b8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 21 Oct 2023 19:27:31 +0200 Subject: [PATCH 079/157] Format code --- .../api/sentry/processors/SentryProcessor.java | 6 +++--- .../sentry/beans}/SentrySpanProcessorProducer.java | 3 ++- .../fyreplace/api/exceptions/ExceptionMappers.java | 1 + .../java/app/fyreplace/api/services/JwtService.java | 12 ++++++------ .../api/services/storage/s3/S3StorageService.java | 7 ++----- 5 files changed, 14 insertions(+), 15 deletions(-) rename quarkus-sentry/{deployment/src/main/java/app/fyreplace/api/sentry => runtime/src/main/java/app/fyreplace/api/sentry/beans}/SentrySpanProcessorProducer.java (80%) diff --git a/quarkus-sentry/deployment/src/main/java/app/fyreplace/api/sentry/processors/SentryProcessor.java b/quarkus-sentry/deployment/src/main/java/app/fyreplace/api/sentry/processors/SentryProcessor.java index 6af1684..12f66fd 100644 --- a/quarkus-sentry/deployment/src/main/java/app/fyreplace/api/sentry/processors/SentryProcessor.java +++ b/quarkus-sentry/deployment/src/main/java/app/fyreplace/api/sentry/processors/SentryProcessor.java @@ -1,6 +1,6 @@ package app.fyreplace.api.sentry.processors; -import app.fyreplace.api.sentry.SentrySpanProcessorProducer; +import app.fyreplace.api.sentry.beans.SentrySpanProcessorProducer; import app.fyreplace.api.sentry.config.SentryConfig; import app.fyreplace.api.sentry.recorders.SentryRecorder; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; @@ -10,7 +10,7 @@ import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.LogHandlerBuildItem; -final class SentryProcessor { +public final class SentryProcessor { private static final String FEATURE = "sentry"; @BuildStep @@ -25,7 +25,7 @@ LogHandlerBuildItem addSentryHandler(final SentryConfig config, final SentryReco } @BuildStep - AdditionalBeanBuildItem addAdditionalBeans() { + AdditionalBeanBuildItem addSentrySpanProcessorProducer() { return AdditionalBeanBuildItem.builder() .addBeanClass(SentrySpanProcessorProducer.class) .build(); diff --git a/quarkus-sentry/deployment/src/main/java/app/fyreplace/api/sentry/SentrySpanProcessorProducer.java b/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/beans/SentrySpanProcessorProducer.java similarity index 80% rename from quarkus-sentry/deployment/src/main/java/app/fyreplace/api/sentry/SentrySpanProcessorProducer.java rename to quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/beans/SentrySpanProcessorProducer.java index 4093764..94a737e 100644 --- a/quarkus-sentry/deployment/src/main/java/app/fyreplace/api/sentry/SentrySpanProcessorProducer.java +++ b/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/beans/SentrySpanProcessorProducer.java @@ -1,9 +1,10 @@ -package app.fyreplace.api.sentry; +package app.fyreplace.api.sentry.beans; import io.sentry.opentelemetry.SentrySpanProcessor; import jakarta.enterprise.context.ApplicationScoped; public final class SentrySpanProcessorProducer { + @SuppressWarnings("unused") @ApplicationScoped public SentrySpanProcessor produceSentrySpanProcessor() { return new SentrySpanProcessor(); diff --git a/src/main/java/app/fyreplace/api/exceptions/ExceptionMappers.java b/src/main/java/app/fyreplace/api/exceptions/ExceptionMappers.java index fc68791..e6e208d 100644 --- a/src/main/java/app/fyreplace/api/exceptions/ExceptionMappers.java +++ b/src/main/java/app/fyreplace/api/exceptions/ExceptionMappers.java @@ -3,6 +3,7 @@ import jakarta.ws.rs.core.Response; import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +@SuppressWarnings("unused") public final class ExceptionMappers { @ServerExceptionMapper public Response handleForbiddenException(final ForbiddenException exception) { diff --git a/src/main/java/app/fyreplace/api/services/JwtService.java b/src/main/java/app/fyreplace/api/services/JwtService.java index e1ef930..4da500c 100644 --- a/src/main/java/app/fyreplace/api/services/JwtService.java +++ b/src/main/java/app/fyreplace/api/services/JwtService.java @@ -13,14 +13,14 @@ public final class JwtService { String appUrl; public String makeJwt(final User user) { - return makeJwt(user.mainEmail); - } - - public String makeJwt(final Email email) { return Jwt.issuer(appUrl) - .subject(email.user.username) - .groups(email.user.getGroups()) + .subject(user.username) + .groups(user.getGroups()) .expiresIn(Duration.ofDays(3)) .sign(); } + + public String makeJwt(final Email email) { + return makeJwt(email.user); + } } diff --git a/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java b/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java index 7ae874f..8db18e8 100644 --- a/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java +++ b/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java @@ -40,7 +40,7 @@ public void onStartup(@Observes final StartupEvent event) { try { b.bucket(config.bucket()) .policy(objectMapper.writeValueAsString(new Policy(Policy.currentVersion, List.of(statement)))); - } catch (JsonProcessingException e) { + } catch (final JsonProcessingException e) { throw new RuntimeException(e); } }); @@ -62,10 +62,7 @@ public URI getUri(final String path) { return client.utilities() .getUrl(b -> { b.bucket(config.bucket()).key(path); - - if (config.customEndpoint().isPresent()) { - b.endpoint(config.customEndpoint().get()); - } + config.customEndpoint().ifPresent(b::endpoint); }) .toURI(); } catch (final URISyntaxException e) { From 1d2aebc19bb67c2d6b05d2c5f36a5073f1e195b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 21 Oct 2023 19:30:51 +0200 Subject: [PATCH 080/157] Give Sentry more context --- quarkus-sentry/runtime/build.gradle | 1 + .../api/sentry/filters/SentryFilter.java | 44 +++++++++++++++++++ .../services/jakarta.ws.rs.ext.Providers | 1 + .../fyreplace/api/filters/SentryFilter.java | 28 ++++++++++++ 4 files changed, 74 insertions(+) create mode 100644 quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/filters/SentryFilter.java create mode 100644 quarkus-sentry/runtime/src/main/resources/META-INF/services/jakarta.ws.rs.ext.Providers create mode 100644 src/main/java/app/fyreplace/api/filters/SentryFilter.java diff --git a/quarkus-sentry/runtime/build.gradle b/quarkus-sentry/runtime/build.gradle index 09d5c48..b530433 100644 --- a/quarkus-sentry/runtime/build.gradle +++ b/quarkus-sentry/runtime/build.gradle @@ -9,6 +9,7 @@ repositories { dependencies { implementation("io.quarkus:quarkus-core") implementation("io.quarkus:quarkus-arc") + implementation("io.quarkus:quarkus-resteasy-reactive") implementation("io.sentry:sentry-jul") implementation("io.opentelemetry.instrumentation:opentelemetry-jdbc") } diff --git a/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/filters/SentryFilter.java b/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/filters/SentryFilter.java new file mode 100644 index 0000000..5486f6c --- /dev/null +++ b/quarkus-sentry/runtime/src/main/java/app/fyreplace/api/sentry/filters/SentryFilter.java @@ -0,0 +1,44 @@ +package app.fyreplace.api.sentry.filters; + +import io.sentry.Sentry; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.container.DynamicFeature; +import jakarta.ws.rs.container.ResourceInfo; +import jakarta.ws.rs.core.FeatureContext; +import jakarta.ws.rs.ext.Provider; +import java.util.Map; +import java.util.stream.Collectors; + +@SuppressWarnings("unused") +@Provider +public final class SentryFilter implements ContainerRequestFilter, DynamicFeature { + @Override + public void filter(final ContainerRequestContext context) { + final var method = context.getMethod(); + final var uriInfo = context.getUriInfo(); + final var sentryRequest = new io.sentry.protocol.Request(); + + sentryRequest.setApiTarget("rest"); + sentryRequest.setMethod(method); + sentryRequest.setUrl(uriInfo.getRequestUri().toString()); + sentryRequest.setQueryString(uriInfo.getQueryParameters().entrySet().stream() + .map(entry -> entry.getKey() + '=' + String.join(",", entry.getValue())) + .collect(Collectors.joining("&"))); + sentryRequest.setHeaders(context.getHeaders().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> String.join(";", entry.getValue())))); + sentryRequest.setCookies(context.getCookies().entrySet().stream() + .map(entry -> entry.getKey() + '=' + entry.getValue().getValue()) + .collect(Collectors.joining(";"))); + + Sentry.configureScope(scope -> { + scope.setTransaction(method + ' ' + uriInfo.getPath()); + scope.setRequest(sentryRequest); + }); + } + + @Override + public void configure(final ResourceInfo resourceInfo, final FeatureContext context) { + context.register(this); + } +} diff --git a/quarkus-sentry/runtime/src/main/resources/META-INF/services/jakarta.ws.rs.ext.Providers b/quarkus-sentry/runtime/src/main/resources/META-INF/services/jakarta.ws.rs.ext.Providers new file mode 100644 index 0000000..6e3c6f8 --- /dev/null +++ b/quarkus-sentry/runtime/src/main/resources/META-INF/services/jakarta.ws.rs.ext.Providers @@ -0,0 +1 @@ +app.fyreplace.api.sentry.filters.SentryFilter diff --git a/src/main/java/app/fyreplace/api/filters/SentryFilter.java b/src/main/java/app/fyreplace/api/filters/SentryFilter.java new file mode 100644 index 0000000..7121790 --- /dev/null +++ b/src/main/java/app/fyreplace/api/filters/SentryFilter.java @@ -0,0 +1,28 @@ +package app.fyreplace.api.filters; + +import app.fyreplace.api.data.User; +import io.sentry.Sentry; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.ext.Provider; + +@SuppressWarnings("unused") +@Provider +public final class SentryFilter implements ContainerRequestFilter { + @Override + public void filter(final ContainerRequestContext context) { + final var user = User.getFromSecurityContext(context.getSecurityContext(), null, false); + + Sentry.configureScope(scope -> { + if (user != null) { + final var sentryUser = new io.sentry.protocol.User(); + sentryUser.setId(user.id.toString()); + sentryUser.setUsername(user.username); + sentryUser.setEmail(user.mainEmail != null ? user.mainEmail.email : null); + scope.setUser(sentryUser); + } else { + scope.setUser(null); + } + }); + } +} From 4bbea8cb898f5d1e291375bb7ce5302fd21d0e9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 22 Oct 2023 11:55:14 +0200 Subject: [PATCH 081/157] Improve stored files naming --- .../app/fyreplace/api/data/StoredFile.java | 21 ++++++++++++------- .../api/endpoints/ChaptersEndpoint.java | 2 +- .../api/endpoints/UsersEndpoint.java | 2 +- .../api/services/MimeTypeService.java | 19 ++++++++++++++--- 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/main/java/app/fyreplace/api/data/StoredFile.java b/src/main/java/app/fyreplace/api/data/StoredFile.java index 8dbe25d..0fdc240 100644 --- a/src/main/java/app/fyreplace/api/data/StoredFile.java +++ b/src/main/java/app/fyreplace/api/data/StoredFile.java @@ -1,5 +1,6 @@ package app.fyreplace.api.data; +import app.fyreplace.api.services.MimeTypeService; import app.fyreplace.api.services.StorageService; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; @@ -13,6 +14,7 @@ import jakarta.persistence.Table; import jakarta.persistence.Transient; import java.io.IOException; +import java.nio.file.Paths; @Entity @Table(name = "remote_files") @@ -20,6 +22,9 @@ public class StoredFile extends EntityBase { @Transient private StorageService storageService; + @Transient + private MimeTypeService mimeTypeService; + @Column(unique = true, nullable = false) public String path; @@ -30,13 +35,13 @@ public class StoredFile extends EntityBase { @SuppressWarnings("unused") public StoredFile() { data = null; - initStorageService(); + initServices(); } - public StoredFile(final String path, @Nullable final byte[] data) { - this.path = path; + public StoredFile(final String directory, final String name, @Nullable final byte[] data) { + initServices(); + this.path = Paths.get(directory, name) + mimeTypeService.getExtension(data); this.data = data; - initStorageService(); } @Override @@ -65,9 +70,11 @@ final void preDestroy() throws IOException { storageService.remove(path); } - private void initStorageService() { - try (final var service = Arc.container().instance(StorageService.class)) { - storageService = service.get(); + private void initServices() { + try (final var storage = Arc.container().instance(StorageService.class); + final var mimeType = Arc.container().instance(MimeTypeService.class)) { + storageService = storage.get(); + mimeTypeService = mimeType.get(); } } diff --git a/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java index ae749d3..fd6a4fe 100644 --- a/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java @@ -179,7 +179,7 @@ public String updateChapterImage( final var height = metadata.getInt(Metadata.IMAGE_LENGTH); if (chapter.image == null) { - chapter.image = new StoredFile("chapters/" + chapter.id, input); + chapter.image = new StoredFile("chapters", chapter.id.toString(), input); } else { chapter.image.store(input); } diff --git a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java index 2c57bdf..4a9e46d 100644 --- a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java @@ -191,7 +191,7 @@ public String updateMeAvatar(final byte[] input) throws IOException { final var user = User.getFromSecurityContext(context, LockModeType.PESSIMISTIC_WRITE); if (user.avatar == null) { - user.avatar = new StoredFile("avatars/" + user.id, input); + user.avatar = new StoredFile("avatars", user.username, input); } else { user.avatar.store(input); } diff --git a/src/main/java/app/fyreplace/api/services/MimeTypeService.java b/src/main/java/app/fyreplace/api/services/MimeTypeService.java index e62e5a4..d584c2a 100644 --- a/src/main/java/app/fyreplace/api/services/MimeTypeService.java +++ b/src/main/java/app/fyreplace/api/services/MimeTypeService.java @@ -7,14 +7,27 @@ import java.io.IOException; import org.apache.tika.Tika; import org.apache.tika.metadata.Metadata; +import org.apache.tika.mime.MimeTypeException; +import org.apache.tika.mime.MimeTypes; @ApplicationScoped public final class MimeTypeService { + public String getMimeType(final byte[] data) { + return new Tika().detect(data); + } + + public String getExtension(final byte[] data) { + try { + return MimeTypes.getDefaultMimeTypes().forName(getMimeType(data)).getExtension(); + } catch (final MimeTypeException e) { + return ".unknown"; + } + } + public void validate(final byte[] data, final KnownMimeTypes types) { - final var tika = new Tika(); - final var mimeType = tika.detect(data); + final var mimeType = getMimeType(data); - if (mimeType == null || !types.types.contains(mimeType)) { + if (!types.types.contains(mimeType)) { throw new UnsupportedMediaTypeException("invalid_media_type"); } } From c6a84b949ac83b00672988126ed7e87f91c189de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 22 Oct 2023 11:59:17 +0200 Subject: [PATCH 082/157] Add content type to stored files --- .../api/services/storage/s3/S3StorageService.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java b/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java index 8db18e8..8506ac5 100644 --- a/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java +++ b/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java @@ -1,5 +1,6 @@ package app.fyreplace.api.services.storage.s3; +import app.fyreplace.api.services.MimeTypeService; import app.fyreplace.api.services.StorageService; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -32,6 +33,9 @@ public final class S3StorageService implements StorageService { @Inject ObjectMapper objectMapper; + @Inject + MimeTypeService mimeTypeService; + public void onStartup(@Observes final StartupEvent event) { client.putBucketPolicy(b -> { final var statement = @@ -48,7 +52,9 @@ public void onStartup(@Observes final StartupEvent event) { @Override public void store(final String path, final byte[] data) { - client.putObject(b -> b.bucket(config.bucket()).key(path), RequestBody.fromBytes(data)); + client.putObject( + b -> b.bucket(config.bucket()).key(path).contentType(mimeTypeService.getMimeType(data)), + RequestBody.fromBytes(data)); } @Override From e4256c6e9f2359ea5c7f2938320cec91b021f777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 22 Oct 2023 14:51:56 +0200 Subject: [PATCH 083/157] Make local stored files downloadable --- .env-example | 3 +- .../app/fyreplace/api/emails/EmailBase.java | 6 ++-- .../api/endpoints/StoredFilesEndpoint.java | 31 +++++++++++++++++++ .../api/services/StorageService.java | 2 ++ .../storage/local/LocalStorageService.java | 18 ++++++++++- .../services/storage/s3/S3StorageService.java | 12 +++++++ src/main/resources/application.yaml | 3 ++ 7 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java diff --git a/.env-example b/.env-example index 59fc68f..d62ce1b 100644 --- a/.env-example +++ b/.env-example @@ -15,7 +15,8 @@ QUARKUS_SENTRY_TRACES_SAMPLE_RATE=1.0 MP_JWT_VERIFY_PUBLICKEY=public-key-content SMALLRYE_JWT_SIGN_KEY=private-key-content -APP_URL=https://fyreplace.example.org +APP_URL=https://api.fyreplace.example.org +APP_FRONT_URL=https://fyreplace.example.org APP_STORAGE_TYPE=s3 APP_STORAGE_S3_BUCKET=fyreplace diff --git a/src/main/java/app/fyreplace/api/emails/EmailBase.java b/src/main/java/app/fyreplace/api/emails/EmailBase.java index 52fdfaf..eff9ca4 100644 --- a/src/main/java/app/fyreplace/api/emails/EmailBase.java +++ b/src/main/java/app/fyreplace/api/emails/EmailBase.java @@ -15,8 +15,8 @@ import org.eclipse.microprofile.config.inject.ConfigProperty; public abstract class EmailBase extends Mail { - @ConfigProperty(name = "app.url") - String appUrl; + @ConfigProperty(name = "app.front.url") + String appFrontUrl; @Inject Mailer mailer; @@ -60,7 +60,7 @@ protected String getRandomCode() { } protected String getLink() { - return URI.create(appUrl) + return URI.create(appFrontUrl) .resolve("?action=" + action()) .resolve('#' + email.user.username + ':' + getRandomCode()) .toString(); diff --git a/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java new file mode 100644 index 0000000..25d6ccc --- /dev/null +++ b/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java @@ -0,0 +1,31 @@ +package app.fyreplace.api.endpoints; + +import app.fyreplace.api.services.MimeTypeService; +import app.fyreplace.api.services.StorageService; +import io.quarkus.arc.properties.IfBuildProperty; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Response; +import java.io.IOException; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; + +@Path("stored-files") +@IfBuildProperty(name = "app.storage.type", stringValue = "local") +public class StoredFilesEndpoint { + @Inject + StorageService storageService; + + @Inject + MimeTypeService mimeTypeService; + + @GET + @Path("{path:.*}") + @APIResponse(responseCode = "200") + @APIResponse(responseCode = "404") + public Response retrieve(@PathParam("path") final String path) throws IOException { + final var data = storageService.fetch(path); + return Response.ok(data).type(mimeTypeService.getMimeType(data)).build(); + } +} diff --git a/src/main/java/app/fyreplace/api/services/StorageService.java b/src/main/java/app/fyreplace/api/services/StorageService.java index 4c205bc..4ba3830 100644 --- a/src/main/java/app/fyreplace/api/services/StorageService.java +++ b/src/main/java/app/fyreplace/api/services/StorageService.java @@ -4,6 +4,8 @@ import java.net.URI; public interface StorageService { + byte[] fetch(final String path) throws IOException; + void store(final String path, final byte[] data) throws IOException; void remove(final String path) throws IOException; diff --git a/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java b/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java index 8795cb2..27b4782 100644 --- a/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java +++ b/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java @@ -5,20 +5,36 @@ import io.quarkus.arc.properties.IfBuildProperty; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import jakarta.ws.rs.NotFoundException; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.net.URI; import java.nio.file.Paths; +import org.eclipse.microprofile.config.inject.ConfigProperty; @SuppressWarnings("unused") @ApplicationScoped @Unremovable @IfBuildProperty(name = "app.storage.type", stringValue = "local") public final class LocalStorageService implements StorageService { + @ConfigProperty(name = "app.url") + String appUrl; + @Inject LocalStorageConfig config; + @Override + public byte[] fetch(final String path) throws IOException { + try (final var reader = new FileInputStream(getFile(path))) { + return reader.readAllBytes(); + } catch (final FileNotFoundException e) { + throw new NotFoundException(); + } + } + @SuppressWarnings("ResultOfMethodCallIgnored") @Override public void store(final String path, final byte[] data) throws IOException { @@ -38,7 +54,7 @@ public void remove(final String path) { @Override public URI getUri(final String path) { - return getFile(path).toURI(); + return URI.create(appUrl).resolve(Paths.get("stored-files", path).toString()); } private File getFile(final String path) { diff --git a/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java b/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java index 8506ac5..e75990e 100644 --- a/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java +++ b/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java @@ -10,12 +10,15 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; +import jakarta.ws.rs.NotFoundException; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.List; import org.jboss.logging.Logger; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.NoSuchKeyException; @SuppressWarnings("unused") @ApplicationScoped @@ -50,6 +53,15 @@ public void onStartup(@Observes final StartupEvent event) { }); } + @Override + public byte[] fetch(final String path) throws IOException { + try { + return client.getObject(b -> b.bucket(config.bucket()).key(path)).readAllBytes(); + } catch (final NoSuchKeyException e) { + throw new NotFoundException(); + } + } + @Override public void store(final String path, final byte[] data) { client.putObject( diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 660f8d7..32e2f0c 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -42,6 +42,9 @@ app: name: Fyreplace use-example-data: false + front: + url: "" + paging: size: 12 From db20807b214fd14225bbb162f294fe2fbf7588db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 22 Oct 2023 17:48:30 +0200 Subject: [PATCH 084/157] Do things the Jakarta way --- .../api/exceptions/ExceptionMappers.java | 27 ------------------- .../mappers/ConflictExceptionMapper.java | 8 ++++++ .../mappers/ExplainableExceptionMapper.java | 15 +++++++++++ .../mappers/ForbiddenExceptionMapper.java | 8 ++++++ .../mappers/NumberFormatExceptionMapper.java | 14 ++++++++++ .../UnsupportedMediaTypeExceptionMapper.java | 9 +++++++ 6 files changed, 54 insertions(+), 27 deletions(-) delete mode 100644 src/main/java/app/fyreplace/api/exceptions/ExceptionMappers.java create mode 100644 src/main/java/app/fyreplace/api/exceptions/mappers/ConflictExceptionMapper.java create mode 100644 src/main/java/app/fyreplace/api/exceptions/mappers/ExplainableExceptionMapper.java create mode 100644 src/main/java/app/fyreplace/api/exceptions/mappers/ForbiddenExceptionMapper.java create mode 100644 src/main/java/app/fyreplace/api/exceptions/mappers/NumberFormatExceptionMapper.java create mode 100644 src/main/java/app/fyreplace/api/exceptions/mappers/UnsupportedMediaTypeExceptionMapper.java diff --git a/src/main/java/app/fyreplace/api/exceptions/ExceptionMappers.java b/src/main/java/app/fyreplace/api/exceptions/ExceptionMappers.java deleted file mode 100644 index e6e208d..0000000 --- a/src/main/java/app/fyreplace/api/exceptions/ExceptionMappers.java +++ /dev/null @@ -1,27 +0,0 @@ -package app.fyreplace.api.exceptions; - -import jakarta.ws.rs.core.Response; -import org.jboss.resteasy.reactive.server.ServerExceptionMapper; - -@SuppressWarnings("unused") -public final class ExceptionMappers { - @ServerExceptionMapper - public Response handleForbiddenException(final ForbiddenException exception) { - return Responses.makeFrom(exception); - } - - @ServerExceptionMapper - public Response handleConflictException(final ConflictException exception) { - return Responses.makeFrom(exception); - } - - @ServerExceptionMapper - public Response handleUnsupportedMediaTypeException(final UnsupportedMediaTypeException exception) { - return Responses.makeFrom(exception); - } - - @ServerExceptionMapper - public Response handleNumberFormatException(final NumberFormatException exception) { - return Response.status(Response.Status.BAD_REQUEST).build(); - } -} diff --git a/src/main/java/app/fyreplace/api/exceptions/mappers/ConflictExceptionMapper.java b/src/main/java/app/fyreplace/api/exceptions/mappers/ConflictExceptionMapper.java new file mode 100644 index 0000000..8d81c71 --- /dev/null +++ b/src/main/java/app/fyreplace/api/exceptions/mappers/ConflictExceptionMapper.java @@ -0,0 +1,8 @@ +package app.fyreplace.api.exceptions.mappers; + +import app.fyreplace.api.exceptions.ConflictException; +import jakarta.ws.rs.ext.Provider; + +@SuppressWarnings("unused") +@Provider +public final class ConflictExceptionMapper extends ExplainableExceptionMapper {} diff --git a/src/main/java/app/fyreplace/api/exceptions/mappers/ExplainableExceptionMapper.java b/src/main/java/app/fyreplace/api/exceptions/mappers/ExplainableExceptionMapper.java new file mode 100644 index 0000000..80312b4 --- /dev/null +++ b/src/main/java/app/fyreplace/api/exceptions/mappers/ExplainableExceptionMapper.java @@ -0,0 +1,15 @@ +package app.fyreplace.api.exceptions.mappers; + +import app.fyreplace.api.exceptions.ExplainableException; +import app.fyreplace.api.exceptions.Responses; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; + +public abstract class ExplainableExceptionMapper + implements ExceptionMapper { + @Override + public Response toResponse(final E exception) { + return Responses.makeFrom(exception); + } +} diff --git a/src/main/java/app/fyreplace/api/exceptions/mappers/ForbiddenExceptionMapper.java b/src/main/java/app/fyreplace/api/exceptions/mappers/ForbiddenExceptionMapper.java new file mode 100644 index 0000000..558ed83 --- /dev/null +++ b/src/main/java/app/fyreplace/api/exceptions/mappers/ForbiddenExceptionMapper.java @@ -0,0 +1,8 @@ +package app.fyreplace.api.exceptions.mappers; + +import app.fyreplace.api.exceptions.ForbiddenException; +import jakarta.ws.rs.ext.Provider; + +@SuppressWarnings("unused") +@Provider +public final class ForbiddenExceptionMapper extends ExplainableExceptionMapper {} diff --git a/src/main/java/app/fyreplace/api/exceptions/mappers/NumberFormatExceptionMapper.java b/src/main/java/app/fyreplace/api/exceptions/mappers/NumberFormatExceptionMapper.java new file mode 100644 index 0000000..cfe32f9 --- /dev/null +++ b/src/main/java/app/fyreplace/api/exceptions/mappers/NumberFormatExceptionMapper.java @@ -0,0 +1,14 @@ +package app.fyreplace.api.exceptions.mappers; + +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +@SuppressWarnings("unused") +@Provider +public final class NumberFormatExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(final NumberFormatException exception) { + return Response.status(Response.Status.BAD_REQUEST).build(); + } +} diff --git a/src/main/java/app/fyreplace/api/exceptions/mappers/UnsupportedMediaTypeExceptionMapper.java b/src/main/java/app/fyreplace/api/exceptions/mappers/UnsupportedMediaTypeExceptionMapper.java new file mode 100644 index 0000000..aa78bbf --- /dev/null +++ b/src/main/java/app/fyreplace/api/exceptions/mappers/UnsupportedMediaTypeExceptionMapper.java @@ -0,0 +1,9 @@ +package app.fyreplace.api.exceptions.mappers; + +import app.fyreplace.api.exceptions.UnsupportedMediaTypeException; +import jakarta.ws.rs.ext.Provider; + +@SuppressWarnings("unused") +@Provider +public final class UnsupportedMediaTypeExceptionMapper + extends ExplainableExceptionMapper {} From 83520874c6057246cb47ff27b140be0ad8cc8e4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 22 Oct 2023 17:56:06 +0200 Subject: [PATCH 085/157] Fix tests --- .github/workflows/validation.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 2aa8950..a8c28fd 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -43,5 +43,6 @@ jobs: env: MP_JWT_VERIFY_PUBLICKEY: ${{ vars.MP_JWT_VERIFY_PUBLICKEY }} SMALLRYE_JWT_SIGN_KEY: ${{ secrets.SMALLRYE_JWT_SIGN_KEY }} - APP_URL: https://fyreplace.example.org + APP_URL: https://api.fyreplace.example.org + APP_FRONT_URL: https://fyreplace.example.org APP_STORAGE_LOCAL_PATH: /tmp/fyreplace/storage From bacd7f5e1bfd53fcc0239feaad9e943e90bdfa6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 22 Oct 2023 18:15:56 +0200 Subject: [PATCH 086/157] Restrict filesystem access to stored files --- .../services/storage/local/LocalStorageConfig.java | 3 ++- .../services/storage/local/LocalStorageService.java | 12 +++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageConfig.java b/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageConfig.java index e82fbe1..6a43a2a 100644 --- a/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageConfig.java +++ b/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageConfig.java @@ -1,8 +1,9 @@ package app.fyreplace.api.services.storage.local; import io.smallrye.config.ConfigMapping; +import java.nio.file.Path; @ConfigMapping(prefix = "app.storage.local") public interface LocalStorageConfig { - String path(); + Path path(); } diff --git a/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java b/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java index 27b4782..9a0789a 100644 --- a/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java +++ b/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java @@ -48,7 +48,7 @@ public void store(final String path, final byte[] data) throws IOException { @SuppressWarnings("ResultOfMethodCallIgnored") @Override - public void remove(final String path) { + public void remove(final String path) throws IOException { getFile(path).delete(); } @@ -57,7 +57,13 @@ public URI getUri(final String path) { return URI.create(appUrl).resolve(Paths.get("stored-files", path).toString()); } - private File getFile(final String path) { - return Paths.get(config.path(), path).toFile(); + private File getFile(final String path) throws IOException { + final var file = config.path().resolve(path).toFile(); + + if (!file.getCanonicalPath().startsWith(config.path().toFile().getCanonicalPath())) { + throw new NotFoundException(); + } + + return file; } } From cd3dc739503dfcd0e7c2c1468e2125cc4dfa1f58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 22 Oct 2023 18:20:09 +0200 Subject: [PATCH 087/157] Update workflows dependencies --- .github/workflows/scanning.yml | 2 +- .github/workflows/validation.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/scanning.yml b/.github/workflows/scanning.yml index 80b8007..fc66f28 100644 --- a/.github/workflows/scanning.yml +++ b/.github/workflows/scanning.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up CodeQL uses: github/codeql-action/init@v2 diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index a8c28fd..20c3a10 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - uses: actions/setup-java@v3 with: @@ -30,7 +30,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - uses: actions/setup-java@v3 with: From 4346718fb43ef20cefc58bd925fb005978020fd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 22 Oct 2023 18:40:21 +0200 Subject: [PATCH 088/157] Deduplicate endpoint path --- .../api/services/storage/local/LocalStorageService.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java b/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java index 9a0789a..35308d1 100644 --- a/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java +++ b/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java @@ -1,11 +1,13 @@ package app.fyreplace.api.services.storage.local; +import app.fyreplace.api.endpoints.StoredFilesEndpoint; import app.fyreplace.api.services.StorageService; import io.quarkus.arc.Unremovable; import io.quarkus.arc.properties.IfBuildProperty; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.Path; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -54,7 +56,8 @@ public void remove(final String path) throws IOException { @Override public URI getUri(final String path) { - return URI.create(appUrl).resolve(Paths.get("stored-files", path).toString()); + final var pathBase = StoredFilesEndpoint.class.getAnnotation(Path.class).value(); + return URI.create(appUrl).resolve(Paths.get(pathBase, path).toString()); } private File getFile(final String path) throws IOException { From e7ff8e489f2c0b1915f6789419cec287329ba1d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 22 Oct 2023 18:41:08 +0200 Subject: [PATCH 089/157] Don't check paths using simple strings --- .../api/services/storage/local/LocalStorageService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java b/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java index 35308d1..d3cac40 100644 --- a/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java +++ b/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java @@ -50,7 +50,7 @@ public void store(final String path, final byte[] data) throws IOException { @SuppressWarnings("ResultOfMethodCallIgnored") @Override - public void remove(final String path) throws IOException { + public void remove(final String path) { getFile(path).delete(); } @@ -60,10 +60,10 @@ public URI getUri(final String path) { return URI.create(appUrl).resolve(Paths.get(pathBase, path).toString()); } - private File getFile(final String path) throws IOException { + private File getFile(final String path) { final var file = config.path().resolve(path).toFile(); - if (!file.getCanonicalPath().startsWith(config.path().toFile().getCanonicalPath())) { + if (!file.toPath().normalize().startsWith(config.path().normalize())) { throw new NotFoundException(); } From 08b194a5255b94c00367ca17cbea771b0dc2c32a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 22 Oct 2023 19:33:38 +0200 Subject: [PATCH 090/157] Fix HTML emails --- Makefile | 2 +- .../app/fyreplace/api/emails/EmailBase.java | 3 +++ .../api/emails/EmailVerificationEmail.java | 4 ++-- .../api/emails/UserActivationEmail.java | 4 ++-- .../api/emails/UserConnectionEmail.java | 4 ++-- src/main/resources/META-INF/branding/logo.png | Bin 7293 -> 28 bytes .../META-INF/resources/images/logo.png | Bin 0 -> 7293 bytes .../UserActivationEmail/html.html.mjml | 2 +- .../resources/templates/emails/_attributes.mjml | 4 ++-- src/main/resources/templates/emails/_logo.mjml | 4 ++-- 10 files changed, 15 insertions(+), 12 deletions(-) mode change 100644 => 120000 src/main/resources/META-INF/branding/logo.png create mode 100644 src/main/resources/META-INF/resources/images/logo.png diff --git a/Makefile b/Makefile index 7a63004..55c2c63 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ emails: src/main/resources/templates/*/html.html -src/main/resources/templates/%/html.html: src/main/resources/templates/%/html.html.mjml +src/main/resources/templates/%/html.html: src/main/resources/templates/%/html.html.mjml src/main/resources/templates/emails/*.mjml npx mjml -c.minify=true $< -o $@ keygen-rsa: diff --git a/src/main/java/app/fyreplace/api/emails/EmailBase.java b/src/main/java/app/fyreplace/api/emails/EmailBase.java index eff9ca4..19a619e 100644 --- a/src/main/java/app/fyreplace/api/emails/EmailBase.java +++ b/src/main/java/app/fyreplace/api/emails/EmailBase.java @@ -15,6 +15,9 @@ import org.eclipse.microprofile.config.inject.ConfigProperty; public abstract class EmailBase extends Mail { + @ConfigProperty(name = "app.url") + String appUrl; + @ConfigProperty(name = "app.front.url") String appFrontUrl; diff --git a/src/main/java/app/fyreplace/api/emails/EmailVerificationEmail.java b/src/main/java/app/fyreplace/api/emails/EmailVerificationEmail.java index e5b55ba..e8db744 100644 --- a/src/main/java/app/fyreplace/api/emails/EmailVerificationEmail.java +++ b/src/main/java/app/fyreplace/api/emails/EmailVerificationEmail.java @@ -19,13 +19,13 @@ protected TemplateInstance textTemplate() { @Override protected TemplateInstance htmlTemplate() { - return Templates.html(getResourceBundle(), getRandomCode(), getLink()); + return Templates.html(getResourceBundle(), appUrl, getRandomCode(), getLink()); } @CheckedTemplate public static class Templates { public static native TemplateInstance text(ResourceBundle res, String code, String link); - public static native TemplateInstance html(ResourceBundle res, String code, String link); + public static native TemplateInstance html(ResourceBundle res, String appUrl, String code, String link); } } diff --git a/src/main/java/app/fyreplace/api/emails/UserActivationEmail.java b/src/main/java/app/fyreplace/api/emails/UserActivationEmail.java index 3b10503..8bcb748 100644 --- a/src/main/java/app/fyreplace/api/emails/UserActivationEmail.java +++ b/src/main/java/app/fyreplace/api/emails/UserActivationEmail.java @@ -23,13 +23,13 @@ protected TemplateInstance textTemplate() { @Override protected TemplateInstance htmlTemplate() { - return Templates.html(getResourceBundle(), appName, getRandomCode(), getLink()); + return Templates.html(getResourceBundle(), appUrl, appName, getRandomCode(), getLink()); } @CheckedTemplate public static class Templates { public static native TemplateInstance text(ResourceBundle res, String appName, String code, String link); - public static native TemplateInstance html(ResourceBundle res, String appName, String code, String link); + public static native TemplateInstance html(ResourceBundle res, String appUrl, String appName, String code, String link); } } diff --git a/src/main/java/app/fyreplace/api/emails/UserConnectionEmail.java b/src/main/java/app/fyreplace/api/emails/UserConnectionEmail.java index b4904ce..b9ce5d8 100644 --- a/src/main/java/app/fyreplace/api/emails/UserConnectionEmail.java +++ b/src/main/java/app/fyreplace/api/emails/UserConnectionEmail.java @@ -19,13 +19,13 @@ protected TemplateInstance textTemplate() { @Override protected TemplateInstance htmlTemplate() { - return Templates.html(getResourceBundle(), getRandomCode(), getLink()); + return Templates.html(getResourceBundle(), appUrl, getRandomCode(), getLink()); } @CheckedTemplate public static class Templates { public static native TemplateInstance text(ResourceBundle res, String code, String link); - public static native TemplateInstance html(ResourceBundle res, String code, String link); + public static native TemplateInstance html(ResourceBundle res, String appUrl, String code, String link); } } diff --git a/src/main/resources/META-INF/branding/logo.png b/src/main/resources/META-INF/branding/logo.png deleted file mode 100644 index f339748c3a4e1b08e19128a4aa3b6770a12f3ae8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7293 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 WZ^!8avq$HNf$u@@mfo>``M&@KkE)LV diff --git a/src/main/resources/META-INF/branding/logo.png b/src/main/resources/META-INF/branding/logo.png new file mode 120000 index 0000000..308db3f --- /dev/null +++ b/src/main/resources/META-INF/branding/logo.png @@ -0,0 +1 @@ +../resources/images/logo.png \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/images/logo.png b/src/main/resources/META-INF/resources/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f339748c3a4e1b08e19128a4aa3b6770a12f3ae8 GIT binary patch literal 7293 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 WZ^!8avq$HNf$u@@mfo>``M&@KkE)LV literal 0 HcmV?d00001 diff --git a/src/main/resources/templates/UserActivationEmail/html.html.mjml b/src/main/resources/templates/UserActivationEmail/html.html.mjml index 04aef09..f51f97b 100644 --- a/src/main/resources/templates/UserActivationEmail/html.html.mjml +++ b/src/main/resources/templates/UserActivationEmail/html.html.mjml @@ -4,7 +4,7 @@ - {res.getString("title")} + {res.getString("title").replace("$1", appName)} {res.getString("codeDescription")} {code} {res.getString("linkDescription")} diff --git a/src/main/resources/templates/emails/_attributes.mjml b/src/main/resources/templates/emails/_attributes.mjml index 63598c0..a99f4d4 100644 --- a/src/main/resources/templates/emails/_attributes.mjml +++ b/src/main/resources/templates/emails/_attributes.mjml @@ -1,6 +1,6 @@ - - + + \ No newline at end of file diff --git a/src/main/resources/templates/emails/_logo.mjml b/src/main/resources/templates/emails/_logo.mjml index 4d18b60..0702fbb 100644 --- a/src/main/resources/templates/emails/_logo.mjml +++ b/src/main/resources/templates/emails/_logo.mjml @@ -1,2 +1,2 @@ - - \ No newline at end of file + + \ No newline at end of file From b9cdd64b1d7455fb34c8be80198dd26ae21bd6e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 22 Oct 2023 19:46:01 +0200 Subject: [PATCH 091/157] Simplify rank checking --- .../api/endpoints/UsersEndpoint.java | 2 +- .../testing/endpoints/users/BannedTests.java | 45 +++++++------------ 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java index 4a9e46d..f28d9f3 100644 --- a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java @@ -129,7 +129,7 @@ public Response updateBlocked(@PathParam("id") final UUID id, @Valid @NotNull fi @PUT @Path("{id}/banned") - @RolesAllowed({"ADMINISTRATOR", "MODERATOR"}) + @RolesAllowed("MODERATOR") @Transactional @APIResponse(responseCode = "200") @APIResponse(responseCode = "404") diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java index 97a28ba..41d1846 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java @@ -19,21 +19,10 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) public final class BannedTests extends UserTestsBase { - @Test - @TestSecurity(user = "user_0", roles = "ADMINISTRATOR") - @Transactional - public void updateBannedWithAdministrator() { - final var user = requireNonNull(User.findByUsername("user_1")); - given().put(user.id + "/banned").then().statusCode(200); - user.refresh(); - assertTrue(user.banned); - assertEquals(User.BanCount.ONCE, user.banCount); - } - @Test @TestSecurity(user = "user_0", roles = "MODERATOR") @Transactional - public void updateBannedWithModerator() { + public void updateBannedAsModerator() { final var user = requireNonNull(User.findByUsername("user_1")); given().put(user.id + "/banned").then().statusCode(200); user.refresh(); @@ -44,7 +33,7 @@ public void updateBannedWithModerator() { @Test @TestSecurity(user = "user_0") @Transactional - public void updateBannedWithUser() { + public void updateBannedAsUser() { final var user = requireNonNull(User.findByUsername("user_1")); given().put(user.id + "/banned").then().statusCode(403); user.refresh(); @@ -62,21 +51,10 @@ public void updateBannedUnauthenticated() { assertEquals(User.BanCount.NEVER, user.banCount); } - @Test - @TestSecurity(user = "user_0", roles = "ADMINISTRATOR") - @Transactional - public void updateBannedTwiceWithAdministrator() { - final var user = requireNonNull(User.findByUsername("user_2")); - given().put(user.id + "/banned").then().statusCode(200); - user.refresh(); - assertTrue(user.banned); - assertEquals(User.BanCount.ONE_TOO_MANY, user.banCount); - } - @Test @TestSecurity(user = "user_0", roles = "MODERATOR") @Transactional - public void updateBannedTwiceWithModerator() { + public void updateBannedTwiceAsModerator() { final var user = requireNonNull(User.findByUsername("user_2")); given().put(user.id + "/banned").then().statusCode(200); user.refresh(); @@ -87,7 +65,7 @@ public void updateBannedTwiceWithModerator() { @Test @TestSecurity(user = "user_0") @Transactional - public void updateBannedTwiceWithUser() { + public void updateBannedTwiceAsUser() { final var user = requireNonNull(User.findByUsername("user_2")); given().put(user.id + "/banned").then().statusCode(403); user.refresh(); @@ -106,9 +84,9 @@ public void updateBannedTwiceUnauthenticated() { } @Test - @TestSecurity(user = "user_0", roles = "ADMINISTRATOR") + @TestSecurity(user = "user_0", roles = "MODERATOR") @Transactional - public void updateBannedAlreadyBanned() { + public void updateBannedAlreadyBannedAsModerator() { final var user = requireNonNull(User.findByUsername("user_3")); given().put(user.id + "/banned").then().statusCode(200); user.refresh(); @@ -116,6 +94,17 @@ public void updateBannedAlreadyBanned() { assertEquals(User.BanCount.ONCE, user.banCount); } + @Test + @TestSecurity(user = "user_0") + @Transactional + public void updateBannedAlreadyBannedAsUser() { + final var user = requireNonNull(User.findByUsername("user_3")); + given().put(user.id + "/banned").then().statusCode(403); + user.refresh(); + assertTrue(user.banned); + assertEquals(User.BanCount.ONCE, user.banCount); + } + @BeforeEach @Transactional @Override From 1bf4ec7b937427e50cb593a0e1ae00171469fc9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 22 Oct 2023 19:50:32 +0200 Subject: [PATCH 092/157] Format code --- .../java/app/fyreplace/api/emails/UserActivationEmail.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/app/fyreplace/api/emails/UserActivationEmail.java b/src/main/java/app/fyreplace/api/emails/UserActivationEmail.java index 8bcb748..5a8f63a 100644 --- a/src/main/java/app/fyreplace/api/emails/UserActivationEmail.java +++ b/src/main/java/app/fyreplace/api/emails/UserActivationEmail.java @@ -30,6 +30,7 @@ protected TemplateInstance htmlTemplate() { public static class Templates { public static native TemplateInstance text(ResourceBundle res, String appName, String code, String link); - public static native TemplateInstance html(ResourceBundle res, String appUrl, String appName, String code, String link); + public static native TemplateInstance html( + ResourceBundle res, String appUrl, String appName, String code, String link); } } From b5e01d8cb24d1a84a0b9ebf29fc1af861e897524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 27 Oct 2023 11:42:11 +0200 Subject: [PATCH 093/157] Format code --- .../app/fyreplace/api/endpoints/CommentsEndpoint.java | 1 + .../app/fyreplace/api/endpoints/EmailsEndpoint.java | 1 + .../api/endpoints/PushNotificationTokensEndpoint.java | 2 +- .../fyreplace/api/endpoints/StoredFilesEndpoint.java | 2 +- .../fyreplace/api/endpoints/SubscriptionsEndpoint.java | 2 +- .../testing/endpoints/comments/AcknowledgeTests.java | 2 +- .../api/testing/endpoints/comments/CountTests.java | 2 +- .../api/testing/endpoints/comments/CreateTests.java | 2 +- .../api/testing/endpoints/comments/DeleteTests.java | 2 +- .../api/testing/endpoints/comments/ListTests.java | 2 +- .../{users/dev => dev/users}/RetrieveTokenTests.java | 2 +- .../api/testing/endpoints/emails/CountTests.java | 2 +- .../api/testing/endpoints/posts/CountTests.java | 2 +- .../endpoints/posts/UpdateSubscribedToFalseTests.java | 2 +- .../endpoints/posts/UpdateSubscribedToTrueTests.java | 2 +- .../endpoints/pushnotificationtokens/UpdateTests.java | 2 +- .../endpoints/subscriptions/ClearUnreadTests.java | 2 +- .../testing/endpoints/subscriptions/DeleteTests.java | 2 +- .../endpoints/subscriptions/ListUnreadTests.java | 2 +- .../api/testing/endpoints/users/CountBlockedTests.java | 2 +- .../users/{BannedTests.java => UpdateBannedTests.java} | 2 +- .../endpoints/users/UpdateBlockedToFalseTests.java | 10 ++++------ .../endpoints/users/UpdateBlockedToTrueTests.java | 10 ++++------ 23 files changed, 29 insertions(+), 31 deletions(-) rename src/test/java/app/fyreplace/api/testing/endpoints/{users/dev => dev/users}/RetrieveTokenTests.java (90%) rename src/test/java/app/fyreplace/api/testing/endpoints/users/{BannedTests.java => UpdateBannedTests.java} (98%) diff --git a/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java index 11219ac..b68f960 100644 --- a/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java @@ -142,6 +142,7 @@ public Response acknowledge( @Path("count") @Authenticated @APIResponse(responseCode = "200") + @APIResponse(responseCode = "404") public long count(@PathParam("id") final UUID id, @QueryParam("read") @Nullable final Boolean read) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); diff --git a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java index 86b94fb..ab2dda6 100644 --- a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java @@ -49,6 +49,7 @@ public final class EmailsEndpoint { @GET @Authenticated + @APIResponse(responseCode = "200") public List list(@QueryParam("page") @PositiveOrZero final int page) { final var user = User.getFromSecurityContext(context); return Email.find("user", Sort.by("email"), user).page(page, pagingSize).list(); diff --git a/src/main/java/app/fyreplace/api/endpoints/PushNotificationTokensEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/PushNotificationTokensEndpoint.java index 7277f10..9b250c2 100644 --- a/src/main/java/app/fyreplace/api/endpoints/PushNotificationTokensEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/PushNotificationTokensEndpoint.java @@ -13,7 +13,7 @@ import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; @Path("push-notification-tokens") -public class PushNotificationTokensEndpoint { +public final class PushNotificationTokensEndpoint { @Context SecurityContext context; diff --git a/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java index 25d6ccc..3a91b4f 100644 --- a/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java @@ -13,7 +13,7 @@ @Path("stored-files") @IfBuildProperty(name = "app.storage.type", stringValue = "local") -public class StoredFilesEndpoint { +public final class StoredFilesEndpoint { @Inject StorageService storageService; diff --git a/src/main/java/app/fyreplace/api/endpoints/SubscriptionsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/SubscriptionsEndpoint.java index d94a683..cc69d1f 100644 --- a/src/main/java/app/fyreplace/api/endpoints/SubscriptionsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/SubscriptionsEndpoint.java @@ -18,7 +18,7 @@ import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; @Path("subscriptions") -public class SubscriptionsEndpoint { +public final class SubscriptionsEndpoint { @ConfigProperty(name = "app.paging.size") int pagingSize; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/AcknowledgeTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/AcknowledgeTests.java index 13ccc4d..818d420 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/comments/AcknowledgeTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/AcknowledgeTests.java @@ -15,7 +15,7 @@ @QuarkusTest @TestHTTPEndpoint(CommentsEndpoint.class) -public class AcknowledgeTests extends CommentTestsBase { +public final class AcknowledgeTests extends CommentTestsBase { @Test @TestSecurity(user = "user_0") public void acknowledge() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/CountTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/CountTests.java index 4e04da2..56b32c2 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/comments/CountTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/CountTests.java @@ -23,7 +23,7 @@ @QuarkusTest @TestHTTPEndpoint(CommentsEndpoint.class) -public class CountTests extends CommentTestsBase { +public final class CountTests extends CommentTestsBase { @Inject DataSeeder dataSeeder; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/CreateTests.java index be3ffdf..72bfb4f 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/comments/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/CreateTests.java @@ -19,7 +19,7 @@ @QuarkusTest @TestHTTPEndpoint(CommentsEndpoint.class) -public class CreateTests extends CommentTestsBase { +public final class CreateTests extends CommentTestsBase { @Test @TestSecurity(user = "user_0") public void createOnOwnPost() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/DeleteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/DeleteTests.java index b9b35b4..ab4b79e 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/comments/DeleteTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/DeleteTests.java @@ -20,7 +20,7 @@ @QuarkusTest @TestHTTPEndpoint(CommentsEndpoint.class) -public class DeleteTests extends CommentTestsBase { +public final class DeleteTests extends CommentTestsBase { @Inject DataSeeder dataSeeder; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/ListTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/ListTests.java index 9beb757..f5535dc 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/comments/ListTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/ListTests.java @@ -27,7 +27,7 @@ @QuarkusTest @TestHTTPEndpoint(CommentsEndpoint.class) -public class ListTests extends CommentTestsBase { +public final class ListTests extends CommentTestsBase { @ConfigProperty(name = "app.paging.size") int pagingSize; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/dev/RetrieveTokenTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/dev/users/RetrieveTokenTests.java similarity index 90% rename from src/test/java/app/fyreplace/api/testing/endpoints/users/dev/RetrieveTokenTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/dev/users/RetrieveTokenTests.java index d4b0d51..d9ea000 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/dev/RetrieveTokenTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/dev/users/RetrieveTokenTests.java @@ -1,4 +1,4 @@ -package app.fyreplace.api.testing.endpoints.users.dev; +package app.fyreplace.api.testing.endpoints.dev.users; import static io.restassured.RestAssured.given; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CountTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CountTests.java index 02842cf..73a45a6 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CountTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CountTests.java @@ -18,7 +18,7 @@ @QuarkusTest @TestHTTPEndpoint(EmailsEndpoint.class) -public class CountTests extends UserTestsBase { +public final class CountTests extends UserTestsBase { @Test @TestSecurity(user = "user_0") public void count() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/CountTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/CountTests.java index 32dda63..e226c17 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/CountTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/CountTests.java @@ -19,7 +19,7 @@ @QuarkusTest @TestHTTPEndpoint(PostsEndpoint.class) -public class CountTests extends PostTestsBase { +public final class CountTests extends PostTestsBase { @Inject DataSeeder dataSeeder; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateSubscribedToFalseTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateSubscribedToFalseTests.java index ff08531..696ed3b 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateSubscribedToFalseTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateSubscribedToFalseTests.java @@ -150,7 +150,7 @@ public void updateSubscribedWithDraftUnauthenticated() { @ParameterizedTest @ValueSource(strings = {"fake", "00000000-0000-0000-0000-000000000000"}) @TestSecurity(user = "user_0") - public void updateSubscribedWithNonExistent(final String id) { + public void updateSubscribedWithNonExistentPost(final String id) { final var subscriptionCount = Subscription.count(); given().contentType(ContentType.JSON) .body(new SubscriptionUpdate(false)) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateSubscribedToTrueTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateSubscribedToTrueTests.java index d6c2e59..51a6efc 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateSubscribedToTrueTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateSubscribedToTrueTests.java @@ -161,7 +161,7 @@ public void updateSubscribedWithDraftUnauthenticated() { @ParameterizedTest @ValueSource(strings = {"fake", "00000000-0000-0000-0000-000000000000"}) @TestSecurity(user = "user_0") - public void updateSubscribedWithNonExistent(final String id) { + public void updateSubscribedWithNonExistentPost(final String id) { final var subscriptionCount = Subscription.count(); given().contentType(ContentType.JSON) .body(new SubscriptionUpdate(true)) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/pushnotificationtokens/UpdateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/pushnotificationtokens/UpdateTests.java index 10bca32..13f25b5 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/pushnotificationtokens/UpdateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/pushnotificationtokens/UpdateTests.java @@ -15,7 +15,7 @@ @QuarkusTest @TestHTTPEndpoint(PushNotificationTokensEndpoint.class) -public class UpdateTests extends UserTestsBase { +public final class UpdateTests extends UserTestsBase { @Test @TestSecurity(user = "user_0") public void update() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ClearUnreadTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ClearUnreadTests.java index 6de2377..bbcc333 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ClearUnreadTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ClearUnreadTests.java @@ -14,7 +14,7 @@ @QuarkusTest @TestHTTPEndpoint(SubscriptionsEndpoint.class) -public class ClearUnreadTests extends SubscriptionTestsBase { +public final class ClearUnreadTests extends SubscriptionTestsBase { @Test @TestSecurity(user = "user_0") public void clearUnread() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/DeleteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/DeleteTests.java index edf6a85..b056a12 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/DeleteTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/DeleteTests.java @@ -13,7 +13,7 @@ @QuarkusTest @TestHTTPEndpoint(SubscriptionsEndpoint.class) -public class DeleteTests extends SubscriptionTestsBase { +public final class DeleteTests extends SubscriptionTestsBase { @Test @TestSecurity(user = "user_0") public void delete() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ListUnreadTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ListUnreadTests.java index 3e69338..2bc5d11 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ListUnreadTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ListUnreadTests.java @@ -14,7 +14,7 @@ @QuarkusTest @TestHTTPEndpoint(SubscriptionsEndpoint.class) -public class ListUnreadTests extends SubscriptionTestsBase { +public final class ListUnreadTests extends SubscriptionTestsBase { @ConfigProperty(name = "app.paging.size") int pagingSize; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java index 3909ceb..b9569c2 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java @@ -16,7 +16,7 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public class CountBlockedTests extends UserTestsBase { +public final class CountBlockedTests extends UserTestsBase { @Test @TestSecurity(user = "user_0") public void countBlocked() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBannedTests.java similarity index 98% rename from src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBannedTests.java index 41d1846..8f3dfc8 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/BannedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBannedTests.java @@ -18,7 +18,7 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class BannedTests extends UserTestsBase { +public final class UpdateBannedTests extends UserTestsBase { @Test @TestSecurity(user = "user_0", roles = "MODERATOR") @Transactional diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBlockedToFalseTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBlockedToFalseTests.java index 2735c19..d53ddb8 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBlockedToFalseTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBlockedToFalseTests.java @@ -58,28 +58,26 @@ public void updateBlockedTwice() { @Test @TestSecurity(user = "user_0") public void updateBlockedWithInactiveUser() { - final var user = User.findByUsername("user_0"); final var otherUser = requireNonNull(User.findByUsername("user_inactive_1")); - final var blockCount = Block.count("source", user); + final var blockCount = Block.count(); given().contentType(ContentType.JSON) .body(new BlockUpdate(false)) .put(otherUser.id + "/blocked") .then() .statusCode(404); - assertEquals(blockCount, Block.count("source", user)); + assertEquals(blockCount, Block.count()); } @Test @TestSecurity(user = "user_0") public void updateBlockedWithInvalidUser() { - final var user = User.findByUsername("user_0"); - final var blockCount = Block.count("source", user); + final var blockCount = Block.count(); given().contentType(ContentType.JSON) .body(new BlockUpdate(false)) .put("invalid/blocked") .then() .statusCode(404); - assertEquals(blockCount, Block.count("source", user)); + assertEquals(blockCount, Block.count()); } @BeforeEach diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBlockedToTrueTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBlockedToTrueTests.java index 5efa83e..540ef1e 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBlockedToTrueTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBlockedToTrueTests.java @@ -59,28 +59,26 @@ public void updateBlockedTwice() { @Test @TestSecurity(user = "user_0") public void updateBlockedWithInactiveUser() { - final var user = User.findByUsername("user_0"); final var otherUser = requireNonNull(User.findByUsername("user_inactive_1")); - final var blockCount = Block.count("source", user); + final var blockCount = Block.count(); given().contentType(ContentType.JSON) .body(new BlockUpdate(true)) .put(otherUser.id + "/blocked") .then() .statusCode(404); - assertEquals(blockCount, Block.count("source", user)); + assertEquals(blockCount, Block.count()); } @Test @TestSecurity(user = "user_0") public void updateBlockedWithInvalidUser() { - final var user = User.findByUsername("user_0"); - final var blockCount = Block.count("source", user); + final var blockCount = Block.count(); given().contentType(ContentType.JSON) .body(new BlockUpdate(true)) .put("invalid/blocked") .then() .statusCode(404); - assertEquals(blockCount, Block.count("source", user)); + assertEquals(blockCount, Block.count()); } @Test From fbcd84f420693a812d78d902137bc382748cb7da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 27 Oct 2023 12:32:48 +0200 Subject: [PATCH 094/157] Update dependencies --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 3c500db..dfe0e98 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ -quarkusVersion=3.4.3 +quarkusVersion=3.5.0 quarkusAmazonVersion=2.5.2 sentryVersion=6.32.0 -tikaVersion=2.9.0 +tikaVersion=2.9.1 gitPluginVersion=3.0.0 spotlessPluginVersion=6.22.0 sentryPluginVersion=3.14.0 From 2b5cba02747d32a6b77bd3a4d503f65c4653a27a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 27 Oct 2023 13:10:07 +0200 Subject: [PATCH 095/157] Add reports --- build.gradle | 1 + gradle.properties | 1 + .../java/app/fyreplace/api/data/Comment.java | 2 +- .../app/fyreplace/api/data/EntityBase.java | 2 + .../java/app/fyreplace/api/data/Post.java | 6 +- .../java/app/fyreplace/api/data/Report.java | 42 ++++++ .../app/fyreplace/api/data/ReportUpdate.java | 3 + .../app/fyreplace/api/data/Reportable.java | 27 ++++ .../java/app/fyreplace/api/data/User.java | 2 +- .../fyreplace/api/data/dev/DataSeeder.java | 2 + .../api/endpoints/CommentsEndpoint.java | 42 +++++- .../api/endpoints/PostsEndpoint.java | 51 +++++-- .../api/endpoints/ReportsEndpoint.java | 23 ++++ .../api/endpoints/UsersEndpoint.java | 23 ++++ .../endpoints/comments/DeleteTests.java | 2 +- .../comments/UpdateReportedToFalseTests.java | 124 +++++++++++++++++ .../comments/UpdateReportedToTrueTests.java | 121 ++++++++++++++++ .../posts/UpdateReportedToFalseTests.java | 129 ++++++++++++++++++ .../posts/UpdateReportedToTrueTests.java | 117 ++++++++++++++++ .../testing/endpoints/reports/ListTests.java | 59 ++++++++ .../users/UpdateReportedToFalseTests.java | 116 ++++++++++++++++ .../users/UpdateReportedToTrueTests.java | 104 ++++++++++++++ 22 files changed, 972 insertions(+), 27 deletions(-) create mode 100644 src/main/java/app/fyreplace/api/data/Report.java create mode 100644 src/main/java/app/fyreplace/api/data/ReportUpdate.java create mode 100644 src/main/java/app/fyreplace/api/data/Reportable.java create mode 100644 src/main/java/app/fyreplace/api/endpoints/ReportsEndpoint.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/comments/UpdateReportedToFalseTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/comments/UpdateReportedToTrueTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateReportedToFalseTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateReportedToTrueTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/reports/ListTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateReportedToFalseTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateReportedToTrueTests.java diff --git a/build.gradle b/build.gradle index 789a6f5..c48bab7 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,7 @@ plugins { id "io.quarkus" id "com.palantir.git-version" version "${gitPluginVersion}" id "com.diffplug.spotless" version "${spotlessPluginVersion}" + id "io.freefair.lombok" version "${lombokPluginVersion}" id "io.sentry.jvm.gradle" version "${sentryPluginVersion}" } diff --git a/gradle.properties b/gradle.properties index dfe0e98..39edb39 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,4 +4,5 @@ sentryVersion=6.32.0 tikaVersion=2.9.1 gitPluginVersion=3.0.0 spotlessPluginVersion=6.22.0 +lombokPluginVersion=8.4 sentryPluginVersion=3.14.0 diff --git a/src/main/java/app/fyreplace/api/data/Comment.java b/src/main/java/app/fyreplace/api/data/Comment.java index b4c4b3f..ea3ca7a 100644 --- a/src/main/java/app/fyreplace/api/data/Comment.java +++ b/src/main/java/app/fyreplace/api/data/Comment.java @@ -14,7 +14,7 @@ @Table( name = "comments", indexes = {@Index(columnList = "post_id")}) -public class Comment extends AuthoredEntityBase implements Comparable { +public class Comment extends AuthoredEntityBase implements Comparable, Reportable { @ManyToOne(optional = false) @OnDelete(action = OnDeleteAction.CASCADE) @JsonIgnore diff --git a/src/main/java/app/fyreplace/api/data/EntityBase.java b/src/main/java/app/fyreplace/api/data/EntityBase.java index 6619988..74f23fe 100644 --- a/src/main/java/app/fyreplace/api/data/EntityBase.java +++ b/src/main/java/app/fyreplace/api/data/EntityBase.java @@ -5,8 +5,10 @@ import jakarta.persistence.Id; import jakarta.persistence.MappedSuperclass; import java.util.UUID; +import lombok.Getter; import org.hibernate.annotations.UuidGenerator; +@Getter @MappedSuperclass public abstract class EntityBase extends PanacheEntityBase { @Id diff --git a/src/main/java/app/fyreplace/api/data/Post.java b/src/main/java/app/fyreplace/api/data/Post.java index 1717ce3..dc121c5 100644 --- a/src/main/java/app/fyreplace/api/data/Post.java +++ b/src/main/java/app/fyreplace/api/data/Post.java @@ -15,7 +15,7 @@ @Entity @Table(name = "posts") -public class Post extends AuthoredEntityBase { +public class Post extends AuthoredEntityBase implements Reportable { public boolean published = false; @Column(nullable = false) @@ -70,7 +70,7 @@ public static void validateAccess( @Nullable final Post post, @Nullable final User user, @Nullable final Boolean mustBePublished, - final boolean mustBeAuthor) { + @Nullable final Boolean mustBeAuthor) { final Boolean postIsDraft = post != null && (!post.published); final var userId = user != null ? user.id : null; @@ -78,7 +78,7 @@ public static void validateAccess( throw new NotFoundException(); } else if (mustBePublished == postIsDraft) { throw new ForbiddenException(postIsDraft ? "post_not_published" : "post_is_published"); - } else if (mustBeAuthor && !post.author.id.equals(userId)) { + } else if (mustBeAuthor != null && mustBeAuthor != post.author.id.equals(userId)) { throw new ForbiddenException("invalid_author"); } else if (!post.anonymous && user != null && post.author.isBlocking(user)) { throw new ForbiddenException("user_is_blocked"); diff --git a/src/main/java/app/fyreplace/api/data/Report.java b/src/main/java/app/fyreplace/api/data/Report.java new file mode 100644 index 0000000..5e67566 --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/Report.java @@ -0,0 +1,42 @@ +package app.fyreplace.api.data; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import java.util.UUID; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +@Entity +@Table( + name = "reports", + uniqueConstraints = {@UniqueConstraint(columnNames = {"source_id", "targetModel", "targetId"})}) +public class Report extends TimestampedEntityBase { + @ManyToOne(optional = false) + @OnDelete(action = OnDeleteAction.CASCADE) + @JsonIgnore + public User source; + + @Column(nullable = false) + @JsonIgnore + public Class targetModel; + + @Column(nullable = false) + public UUID targetId; + + @SuppressWarnings("unused") + @JsonProperty("source") + public User.Profile getSourceProfile() { + return source.getProfile(); + } + + @SuppressWarnings("unused") + @JsonProperty("targetModel") + public String getTargetModelSimpleName() { + return targetModel.getSimpleName(); + } +} diff --git a/src/main/java/app/fyreplace/api/data/ReportUpdate.java b/src/main/java/app/fyreplace/api/data/ReportUpdate.java new file mode 100644 index 0000000..2672e8f --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/ReportUpdate.java @@ -0,0 +1,3 @@ +package app.fyreplace.api.data; + +public record ReportUpdate(boolean reported) {} diff --git a/src/main/java/app/fyreplace/api/data/Reportable.java b/src/main/java/app/fyreplace/api/data/Reportable.java new file mode 100644 index 0000000..47153a4 --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/Reportable.java @@ -0,0 +1,27 @@ +package app.fyreplace.api.data; + +import java.util.UUID; + +public interface Reportable { + UUID getId(); + + default void reportBy(final User user) { + if (isReportedBy(user)) { + return; + } + + final Report report = new Report(); + report.source = user; + report.targetModel = getClass(); + report.targetId = getId(); + report.persist(); + } + + default void absolveBy(final User user) { + Report.delete("source = ?1 and targetModel = ?2 and targetId = ?3", user, getClass(), getId()); + } + + default boolean isReportedBy(final User user) { + return Report.count("source = ?1 and targetModel = ?2 and targetId = ?3", user, getClass(), getId()) > 0; + } +} diff --git a/src/main/java/app/fyreplace/api/data/User.java b/src/main/java/app/fyreplace/api/data/User.java index a5d2def..569ae99 100644 --- a/src/main/java/app/fyreplace/api/data/User.java +++ b/src/main/java/app/fyreplace/api/data/User.java @@ -18,7 +18,7 @@ @Entity @Table(name = "users") -public class User extends TimestampedEntityBase { +public class User extends TimestampedEntityBase implements Reportable { public static final Set forbiddenUsernames = new HashSet<>(Arrays.asList( "admin", "admins", diff --git a/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java b/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java index f2c7de0..8e0e221 100644 --- a/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java +++ b/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java @@ -9,6 +9,7 @@ import app.fyreplace.api.data.Post; import app.fyreplace.api.data.PushNotificationToken; import app.fyreplace.api.data.RandomCode; +import app.fyreplace.api.data.Report; import app.fyreplace.api.data.StoredFile; import app.fyreplace.api.data.Subscription; import app.fyreplace.api.data.User; @@ -61,6 +62,7 @@ public void deleteData() { RandomCode.deleteAll(); Block.deleteAll(); PushNotificationToken.deleteAll(); + Report.deleteAll(); User.deleteAll(); Subscription.deleteAll(); Vote.deleteAll(); diff --git a/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java index b68f960..efdcb3f 100644 --- a/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java @@ -4,6 +4,7 @@ import app.fyreplace.api.data.Comment; import app.fyreplace.api.data.CommentCreation; import app.fyreplace.api.data.Post; +import app.fyreplace.api.data.ReportUpdate; import app.fyreplace.api.data.Subscription; import app.fyreplace.api.data.User; import app.fyreplace.api.exceptions.ForbiddenException; @@ -21,6 +22,7 @@ import jakarta.ws.rs.GET; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.QueryParam; @@ -53,7 +55,7 @@ public final class CommentsEndpoint { public Iterable list(@PathParam("id") final UUID id, @QueryParam("page") @PositiveOrZero final int page) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); - Post.validateAccess(post, user, true, false); + Post.validateAccess(post, user, true, null); try (final var stream = Comment.find("post", Comment.sorting(), post).page(page, pagingSize).stream()) { @@ -73,7 +75,7 @@ public Iterable list(@PathParam("id") final UUID id, @QueryParam("page" public Response create(@PathParam("id") final UUID id, @Valid @NotNull final CommentCreation input) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); - Post.validateAccess(post, user, true, false); + Post.validateAccess(post, user, true, null); if (input.anonymous() && !user.id.equals(post.author.id)) { throw new ForbiddenException("invalid_post_author"); @@ -100,7 +102,7 @@ public Response create(@PathParam("id") final UUID id, @Valid @NotNull final Com public Response delete(@PathParam("id") final UUID id, @PathParam("position") @PositiveOrZero final int position) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); - Post.validateAccess(post, user, true, false); + Post.validateAccess(post, user, true, null); final var comment = getComment(post, position); if (!user.id.equals(comment.author.id)) { @@ -111,6 +113,34 @@ public Response delete(@PathParam("id") final UUID id, @PathParam("position") @P return Response.noContent().build(); } + @PUT + @Path("{position}/reported") + @Authenticated + @Transactional + @APIResponse(responseCode = "200") + @APIResponse(responseCode = "404") + public Response updateReported( + @PathParam("id") final UUID id, + @PathParam("position") @PositiveOrZero final int position, + @NotNull @Valid final ReportUpdate input) { + final var user = User.getFromSecurityContext(context); + final var post = Post.findById(id); + Post.validateAccess(post, user, true, null); + final var comment = getComment(post, position); + + if (user.id.equals(comment.author.id)) { + throw new ForbiddenException("invalid_author"); + } + + if (input.reported()) { + comment.reportBy(user); + } else { + comment.absolveBy(user); + } + + return Response.ok().build(); + } + @POST @Path("{position}/acknowledge") @Authenticated @@ -121,7 +151,7 @@ public Response acknowledge( @PathParam("id") final UUID id, @PathParam("position") @PositiveOrZero final int position) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); - Post.validateAccess(post, user, true, false); + Post.validateAccess(post, user, true, null); final var comment = getComment(post, position); final var subscription = Subscription.find("user = ?1 and post = ?2", user, post) .firstResult(); @@ -146,7 +176,7 @@ public Response acknowledge( public long count(@PathParam("id") final UUID id, @QueryParam("read") @Nullable final Boolean read) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); - Post.validateAccess(post, user, true, false); + Post.validateAccess(post, user, true, null); if (read == null) { return Comment.count("post.id", post.id); @@ -168,7 +198,7 @@ public long count(@PathParam("id") final UUID id, @QueryParam("read") @Nullable private Comment getComment(final Post post, final int position) { final var comment = Comment.find("post", Comment.sorting(), post) - .range(position, position + 1) + .range(position, position) .firstResult(); if (comment == null) { diff --git a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java index d0af9ab..cdca4d9 100644 --- a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java @@ -4,6 +4,7 @@ import app.fyreplace.api.data.Chapter; import app.fyreplace.api.data.Post; import app.fyreplace.api.data.PostPublication; +import app.fyreplace.api.data.ReportUpdate; import app.fyreplace.api.data.Subscription; import app.fyreplace.api.data.SubscriptionUpdate; import app.fyreplace.api.data.User; @@ -107,7 +108,7 @@ public Response create() { public Post retrieve(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context, null, false); final var post = Post.findById(id); - Post.validateAccess(post, user, null, false); + Post.validateAccess(post, user, null, null); return post; } @@ -126,48 +127,68 @@ public Response delete(@PathParam("id") final UUID id) { return Response.noContent().build(); } - @POST - @Path("{id}/publish") + @PUT + @Path("{id}/subscribed") @Authenticated @Transactional @APIResponse(responseCode = "200") @APIResponse(responseCode = "400") @APIResponse(responseCode = "404") - @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) - public Response publish(@PathParam("id") final UUID id, @Valid @NotNull final PostPublication input) { + public Response updateSubscribed(@PathParam("id") final UUID id, @Valid @NotNull final SubscriptionUpdate input) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); - Post.validateAccess(post, user, false, true); + Post.validateAccess(post, user, true, null); - if (Chapter.count("post", post) == 0) { - throw new ForbiddenException("invalid_chapter_count"); + if (input.subscribed()) { + user.subscribeTo(post); + } else { + user.unsubscribeFrom(post); } - post.publish(postsStartingLife, input.anonymous()); return Response.ok().build(); } @PUT - @Path("{id}/subscribed") + @Path("{id}/reported") @Authenticated @Transactional @APIResponse(responseCode = "200") - @APIResponse(responseCode = "400") @APIResponse(responseCode = "404") - public Response updateSubscribed(@PathParam("id") final UUID id, @Valid @NotNull final SubscriptionUpdate input) { + public Response updateReported(@PathParam("id") final UUID id, @NotNull @Valid final ReportUpdate input) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); Post.validateAccess(post, user, true, false); - if (input.subscribed()) { - user.subscribeTo(post); + if (input.reported()) { + post.reportBy(user); } else { - user.unsubscribeFrom(post); + post.absolveBy(user); } return Response.ok().build(); } + @POST + @Path("{id}/publish") + @Authenticated + @Transactional + @APIResponse(responseCode = "200") + @APIResponse(responseCode = "400") + @APIResponse(responseCode = "404") + @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) + public Response publish(@PathParam("id") final UUID id, @Valid @NotNull final PostPublication input) { + final var user = User.getFromSecurityContext(context); + final var post = Post.findById(id); + Post.validateAccess(post, user, false, true); + + if (Chapter.count("post", post) == 0) { + throw new ForbiddenException("invalid_chapter_count"); + } + + post.publish(postsStartingLife, input.anonymous()); + return Response.ok().build(); + } + @POST @Path("{id}/vote") @Authenticated diff --git a/src/main/java/app/fyreplace/api/endpoints/ReportsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/ReportsEndpoint.java new file mode 100644 index 0000000..5b0261c --- /dev/null +++ b/src/main/java/app/fyreplace/api/endpoints/ReportsEndpoint.java @@ -0,0 +1,23 @@ +package app.fyreplace.api.endpoints; + +import app.fyreplace.api.data.Report; +import jakarta.annotation.security.RolesAllowed; +import jakarta.validation.constraints.PositiveOrZero; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; + +@Path("reports") +public final class ReportsEndpoint { + @ConfigProperty(name = "app.paging.size") + int pagingSize; + + @GET + @RolesAllowed("MODERATOR") + @APIResponse(responseCode = "200") + public Iterable list(@QueryParam("page") @PositiveOrZero final int page) { + return Report.findAll(Report.sorting()).page(page, pagingSize).list(); + } +} diff --git a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java index f28d9f3..a33a180 100644 --- a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java @@ -4,6 +4,7 @@ import app.fyreplace.api.data.Block; import app.fyreplace.api.data.BlockUpdate; import app.fyreplace.api.data.Email; +import app.fyreplace.api.data.ReportUpdate; import app.fyreplace.api.data.StoredFile; import app.fyreplace.api.data.User; import app.fyreplace.api.data.UserCreation; @@ -152,6 +153,28 @@ public Response updateBanned(@PathParam("id") final UUID id) { return Response.ok().build(); } + @PUT + @Path("{id}/reported") + @Authenticated + @Transactional + @APIResponse(responseCode = "200") + @APIResponse(responseCode = "404") + public Response updateReported(@PathParam("id") final UUID id, @NotNull @Valid final ReportUpdate input) { + final var source = User.getFromSecurityContext(context); + final var target = User.findById(id); + validateUser(target); + + if (source.id.equals(target.id)) { + throw new ForbiddenException("user_is_self"); + } else if (input.reported()) { + target.reportBy(source); + } else { + target.absolveBy(source); + } + + return Response.ok().build(); + } + @GET @Path("me") @Authenticated diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/DeleteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/DeleteTests.java index ab4b79e..ffb8b25 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/comments/DeleteTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/DeleteTests.java @@ -125,7 +125,7 @@ public void beforeEach() { private Comment getComment(final int position) { return Comment.find("post", Comment.sorting(), post) - .range(position, position + 1) + .range(position, position) .firstResult(); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/UpdateReportedToFalseTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/UpdateReportedToFalseTests.java new file mode 100644 index 0000000..a460cd9 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/UpdateReportedToFalseTests.java @@ -0,0 +1,124 @@ +package app.fyreplace.api.testing.endpoints.comments; + +import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import app.fyreplace.api.data.Comment; +import app.fyreplace.api.data.Report; +import app.fyreplace.api.data.ReportUpdate; +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.CommentsEndpoint; +import app.fyreplace.api.testing.CommentTestsBase; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(CommentsEndpoint.class) +public final class UpdateReportedToFalseTests extends CommentTestsBase { + private Comment comment; + + @Test + @TestSecurity(user = "user_0") + public void updateReportWithOwnComment() { + final var user = User.findByUsername("user_0"); + assertFalse(comment.isReportedBy(user)); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(false)) + .pathParam("id", post.id) + .put("0/reported") + .then() + .statusCode(403); + assertFalse(comment.isReportedBy(user)); + } + + @Test + @TestSecurity(user = "user_1") + public void updateReportWithOtherComment() { + final var user = User.findByUsername("user_1"); + assertTrue(comment.isReportedBy(user)); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(false)) + .pathParam("id", post.id) + .put("0/reported") + .then() + .statusCode(200); + assertFalse(comment.isReportedBy(user)); + } + + @Test + @TestSecurity(user = "user_1") + public void updateReportWithOtherCommentTwice() { + final var user = User.findByUsername("user_1"); + assertTrue(comment.isReportedBy(user)); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(false)) + .pathParam("id", post.id) + .put("0/reported") + .then() + .statusCode(200); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(false)) + .pathParam("id", post.id) + .put("0/reported") + .then() + .statusCode(200); + assertFalse(comment.isReportedBy(user)); + } + + @Test + @TestSecurity(user = "user_0") + public void updateReportOutOfBounds() { + final var reportCount = Report.count(); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(false)) + .pathParam("id", post.id) + .put("-1/reported") + .then() + .statusCode(400); + assertEquals(reportCount, Report.count()); + } + + @Test + @TestSecurity(user = "user_0") + public void updateReportTooFar() { + final var reportCount = Report.count(); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(false)) + .pathParam("id", post.id) + .put("50/reported") + .then() + .statusCode(404); + assertEquals(reportCount, Report.count()); + } + + @Test + public void updateReportUnauthenticated() { + final var reportCount = Report.count(); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(false)) + .pathParam("id", post.id) + .put("0/reported") + .then() + .statusCode(401); + assertEquals(reportCount, Report.count()); + } + + @BeforeEach + @Transactional + @Override + public void beforeEach() { + super.beforeEach(); + final var user = requireNonNull(User.findByUsername("user_1")); + comment = Comment.find("post = ?1 and author.username = 'user_0'", Comment.sorting(), post) + .firstResult(); + comment.reportBy(user); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/UpdateReportedToTrueTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/UpdateReportedToTrueTests.java new file mode 100644 index 0000000..a610e24 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/UpdateReportedToTrueTests.java @@ -0,0 +1,121 @@ +package app.fyreplace.api.testing.endpoints.comments; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import app.fyreplace.api.data.Comment; +import app.fyreplace.api.data.Report; +import app.fyreplace.api.data.ReportUpdate; +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.CommentsEndpoint; +import app.fyreplace.api.testing.CommentTestsBase; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(CommentsEndpoint.class) +public final class UpdateReportedToTrueTests extends CommentTestsBase { + private Comment comment; + + @Test + @TestSecurity(user = "user_0") + public void updateReportWithOwnComment() { + final var user = User.findByUsername("user_0"); + assertFalse(comment.isReportedBy(user)); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(true)) + .pathParam("id", post.id) + .put("0/reported") + .then() + .statusCode(403); + assertFalse(comment.isReportedBy(user)); + } + + @Test + @TestSecurity(user = "user_1") + public void updateReportWithOtherComment() { + final var user = User.findByUsername("user_1"); + assertFalse(comment.isReportedBy(user)); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(true)) + .pathParam("id", post.id) + .put("0/reported") + .then() + .statusCode(200); + assertTrue(comment.isReportedBy(user)); + } + + @Test + @TestSecurity(user = "user_1") + public void updateReportWithOtherCommentTwice() { + final var user = User.findByUsername("user_1"); + assertFalse(comment.isReportedBy(user)); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(true)) + .pathParam("id", post.id) + .put("0/reported") + .then() + .statusCode(200); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(true)) + .pathParam("id", post.id) + .put("0/reported") + .then() + .statusCode(200); + assertTrue(comment.isReportedBy(user)); + } + + @Test + @TestSecurity(user = "user_0") + public void updateReportOutOfBounds() { + final var reportCount = Report.count(); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(true)) + .pathParam("id", post.id) + .put("-1/reported") + .then() + .statusCode(400); + assertEquals(reportCount, Report.count()); + } + + @Test + @TestSecurity(user = "user_0") + public void updateReportTooFar() { + final var reportCount = Report.count(); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(true)) + .pathParam("id", post.id) + .put("50/reported") + .then() + .statusCode(404); + assertEquals(reportCount, Report.count()); + } + + @Test + public void updateReportUnauthenticated() { + final var reportCount = Report.count(); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(true)) + .pathParam("id", post.id) + .put("0/reported") + .then() + .statusCode(401); + assertEquals(reportCount, Report.count()); + } + + @BeforeEach + @Transactional + @Override + public void beforeEach() { + super.beforeEach(); + comment = Comment.find("post = ?1 and author.username = 'user_0'", Comment.sorting(), post) + .firstResult(); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateReportedToFalseTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateReportedToFalseTests.java new file mode 100644 index 0000000..77ccbeb --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateReportedToFalseTests.java @@ -0,0 +1,129 @@ +package app.fyreplace.api.testing.endpoints.posts; + +import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import app.fyreplace.api.data.Report; +import app.fyreplace.api.data.ReportUpdate; +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.PostsEndpoint; +import app.fyreplace.api.testing.PostTestsBase; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@QuarkusTest +@TestHTTPEndpoint(PostsEndpoint.class) +public final class UpdateReportedToFalseTests extends PostTestsBase { + @Test + @TestSecurity(user = "user_0") + public void updateReportWithOwnPost() { + final var user = User.findByUsername("user_0"); + assertFalse(post.isReportedBy(user)); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(false)) + .put(post.id + "/reported") + .then() + .statusCode(403); + assertFalse(post.isReportedBy(user)); + } + + @Test + @TestSecurity(user = "user_1") + public void updateReportWithOtherPost() { + final var user = User.findByUsername("user_1"); + assertTrue(post.isReportedBy(user)); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(false)) + .put(post.id + "/reported") + .then() + .statusCode(200); + assertFalse(post.isReportedBy(user)); + } + + @Test + @TestSecurity(user = "user_1") + public void updateReportWithOtherPostTwice() { + final var user = User.findByUsername("user_1"); + assertTrue(post.isReportedBy(user)); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(false)) + .put(post.id + "/reported") + .then() + .statusCode(200); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(false)) + .put(post.id + "/reported") + .then() + .statusCode(200); + assertFalse(post.isReportedBy(user)); + } + + @Test + @TestSecurity(user = "user_0") + public void updateReportWithOwnDraft() { + final var user = User.findByUsername("user_0"); + assertFalse(draft.isReportedBy(user)); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(false)) + .put(draft.id + "/reported") + .then() + .statusCode(403); + assertFalse(draft.isReportedBy(user)); + } + + @Test + @TestSecurity(user = "user_1") + public void updateReportWithOtherDraft() { + final var user = User.findByUsername("user_1"); + assertFalse(draft.isReportedBy(user)); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(false)) + .put(draft.id + "/reported") + .then() + .statusCode(404); + assertFalse(draft.isReportedBy(user)); + } + + @ParameterizedTest + @ValueSource(strings = {"fake", "00000000-0000-0000-0000-000000000000"}) + @TestSecurity(user = "user_0") + public void updateReportWithNonExistentPost(final String id) { + final var reportCount = Report.count(); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(false)) + .put(id + "/reported") + .then() + .statusCode(404); + assertEquals(reportCount, Report.count()); + } + + @Test + public void updateReportUnauthenticated() { + final var reportCount = Report.count(); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(false)) + .put(post.id + "/reported") + .then() + .statusCode(401); + assertEquals(reportCount, Report.count()); + } + + @BeforeEach + @Transactional + @Override + public void beforeEach() { + super.beforeEach(); + final var user = requireNonNull(User.findByUsername("user_1")); + post.reportBy(user); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateReportedToTrueTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateReportedToTrueTests.java new file mode 100644 index 0000000..d6f0985 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateReportedToTrueTests.java @@ -0,0 +1,117 @@ +package app.fyreplace.api.testing.endpoints.posts; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import app.fyreplace.api.data.Report; +import app.fyreplace.api.data.ReportUpdate; +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.PostsEndpoint; +import app.fyreplace.api.testing.PostTestsBase; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@QuarkusTest +@TestHTTPEndpoint(PostsEndpoint.class) +public final class UpdateReportedToTrueTests extends PostTestsBase { + @Test + @TestSecurity(user = "user_0") + public void updateReportWithOwnPost() { + final var user = User.findByUsername("user_0"); + assertFalse(post.isReportedBy(user)); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(true)) + .put(post.id + "/reported") + .then() + .statusCode(403); + assertFalse(post.isReportedBy(user)); + } + + @Test + @TestSecurity(user = "user_1") + public void updateReportWithOtherPost() { + final var user = User.findByUsername("user_1"); + assertFalse(post.isReportedBy(user)); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(true)) + .put(post.id + "/reported") + .then() + .statusCode(200); + assertTrue(post.isReportedBy(user)); + } + + @Test + @TestSecurity(user = "user_1") + public void updateReportWithOtherPostTwice() { + final var user = User.findByUsername("user_1"); + assertFalse(post.isReportedBy(user)); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(true)) + .put(post.id + "/reported") + .then() + .statusCode(200); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(true)) + .put(post.id + "/reported") + .then() + .statusCode(200); + assertTrue(post.isReportedBy(user)); + } + + @Test + @TestSecurity(user = "user_0") + public void updateReportWithOwnDraft() { + final var user = User.findByUsername("user_0"); + assertFalse(draft.isReportedBy(user)); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(true)) + .put(draft.id + "/reported") + .then() + .statusCode(403); + assertFalse(draft.isReportedBy(user)); + } + + @Test + @TestSecurity(user = "user_1") + public void updateReportWithOtherDraft() { + final var user = User.findByUsername("user_1"); + assertFalse(draft.isReportedBy(user)); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(true)) + .put(draft.id + "/reported") + .then() + .statusCode(404); + assertFalse(draft.isReportedBy(user)); + } + + @ParameterizedTest + @ValueSource(strings = {"fake", "00000000-0000-0000-0000-000000000000"}) + @TestSecurity(user = "user_0") + public void updateReportWithNonExistentPost(final String id) { + final var reportCount = Report.count(); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(true)) + .put(id + "/reported") + .then() + .statusCode(404); + assertEquals(reportCount, Report.count()); + } + + @Test + public void updateReportUnauthenticated() { + final var reportCount = Report.count(); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(true)) + .put(post.id + "/reported") + .then() + .statusCode(401); + assertEquals(reportCount, Report.count()); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/reports/ListTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/reports/ListTests.java new file mode 100644 index 0000000..a702a48 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/reports/ListTests.java @@ -0,0 +1,59 @@ +package app.fyreplace.api.testing.endpoints.reports; + +import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; +import static java.util.stream.IntStream.range; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.in; + +import app.fyreplace.api.data.Report; +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.ReportsEndpoint; +import app.fyreplace.api.testing.UserTestsBase; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import jakarta.transaction.Transactional; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(ReportsEndpoint.class) +public final class ListTests extends UserTestsBase { + @ConfigProperty(name = "app.paging.size") + int pagingSize; + + @Test + @TestSecurity(user = "user_0") + public void listAsCitizen() { + given().get().then().statusCode(403); + } + + @Test + @TestSecurity(user = "user_0", roles = "MODERATOR") + public void listAsModerator() { + final var response = given().get().then().statusCode(200).body("size()", equalTo(pagingSize)); + range(0, pagingSize).forEach(i -> { + try (final var stream = Report.streamAll().map(r -> r.targetId.toString())) { + response.body("[" + i + "].targetModel", equalTo(User.class.getSimpleName())) + .body("[" + i + "].targetId", in(stream.toList())); + } + }); + } + + @BeforeEach + @Transactional + @Override + public void beforeEach() { + super.beforeEach(); + final var source = requireNonNull(User.findByUsername("user_0")); + range(1, 16) + .forEach(i -> requireNonNull(User.findByUsername("user_" + i)).reportBy(source)); + } + + @Override + public int getActiveUserCount() { + return 20; + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateReportedToFalseTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateReportedToFalseTests.java new file mode 100644 index 0000000..17c367f --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateReportedToFalseTests.java @@ -0,0 +1,116 @@ +package app.fyreplace.api.testing.endpoints.users; + +import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import app.fyreplace.api.data.Report; +import app.fyreplace.api.data.ReportUpdate; +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.UsersEndpoint; +import app.fyreplace.api.testing.UserTestsBase; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(UsersEndpoint.class) +public final class UpdateReportedToFalseTests extends UserTestsBase { + @Test + @TestSecurity(user = "user_0") + public void updateReport() { + final var source = requireNonNull(User.findByUsername("user_0")); + final var target = requireNonNull(User.findByUsername("user_1")); + assertTrue(target.isReportedBy(source)); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(false)) + .put(target.id + "/reported") + .then() + .statusCode(200); + assertFalse(target.isReportedBy(source)); + } + + @Test + @TestSecurity(user = "user_0") + public void updateReportTwice() { + final var source = requireNonNull(User.findByUsername("user_0")); + final var target = requireNonNull(User.findByUsername("user_1")); + assertTrue(target.isReportedBy(source)); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(false)) + .put(target.id + "/reported") + .then() + .statusCode(200); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(false)) + .put(target.id + "/reported") + .then() + .statusCode(200); + assertFalse(target.isReportedBy(source)); + } + + @Test + @TestSecurity(user = "user_0") + public void updateReportWithInactiveUser() { + final var reportCount = Report.count(); + final var target = requireNonNull(User.findByUsername("user_inactive_1")); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(false)) + .put(target.id + "/reported") + .then() + .statusCode(404); + assertEquals(reportCount, Report.count()); + } + + @Test + @TestSecurity(user = "user_0") + public void updateReportWithInvalidUser() { + final var reportCount = Report.count(); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(false)) + .put("invalid/reported") + .then() + .statusCode(404); + assertEquals(reportCount, Report.count()); + } + + @Test + @TestSecurity(user = "user_0") + public void updateReportWithSelf() { + final var user = requireNonNull(User.findByUsername("user_0")); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(false)) + .put(user.id + "/reported") + .then() + .statusCode(403); + assertFalse(user.isReportedBy(user)); + } + + @Test + public void updateReportUnauthenticated() { + final var reportCount = Report.count(); + final var user = requireNonNull(User.findByUsername("user_0")); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(false)) + .put(user.id + "/reported") + .then() + .statusCode(401); + assertEquals(reportCount, Report.count()); + } + + @BeforeEach + @Transactional + @Override + public void beforeEach() { + super.beforeEach(); + final var source = requireNonNull(User.findByUsername("user_0")); + final var target = requireNonNull(User.findByUsername("user_1")); + target.reportBy(source); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateReportedToTrueTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateReportedToTrueTests.java new file mode 100644 index 0000000..a542882 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateReportedToTrueTests.java @@ -0,0 +1,104 @@ +package app.fyreplace.api.testing.endpoints.users; + +import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import app.fyreplace.api.data.Report; +import app.fyreplace.api.data.ReportUpdate; +import app.fyreplace.api.data.User; +import app.fyreplace.api.endpoints.UsersEndpoint; +import app.fyreplace.api.testing.UserTestsBase; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(UsersEndpoint.class) +public final class UpdateReportedToTrueTests extends UserTestsBase { + @Test + @TestSecurity(user = "user_0") + public void updateReport() { + final var source = requireNonNull(User.findByUsername("user_0")); + final var target = requireNonNull(User.findByUsername("user_1")); + assertFalse(target.isReportedBy(source)); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(true)) + .put(target.id + "/reported") + .then() + .statusCode(200); + assertTrue(target.isReportedBy(source)); + } + + @Test + @TestSecurity(user = "user_0") + public void updateReportTwice() { + final var source = requireNonNull(User.findByUsername("user_0")); + final var target = requireNonNull(User.findByUsername("user_1")); + assertFalse(target.isReportedBy(source)); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(true)) + .put(target.id + "/reported") + .then() + .statusCode(200); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(true)) + .put(target.id + "/reported") + .then() + .statusCode(200); + assertTrue(target.isReportedBy(source)); + } + + @Test + @TestSecurity(user = "user_0") + public void updateReportWithInactiveUser() { + final var reportCount = Report.count(); + final var target = requireNonNull(User.findByUsername("user_inactive_1")); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(true)) + .put(target.id + "/reported") + .then() + .statusCode(404); + assertEquals(reportCount, Report.count()); + } + + @Test + @TestSecurity(user = "user_0") + public void updateReportWithInvalidUser() { + final var reportCount = Report.count(); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(true)) + .put("invalid/reported") + .then() + .statusCode(404); + assertEquals(reportCount, Report.count()); + } + + @Test + @TestSecurity(user = "user_0") + public void updateReportWithSelf() { + final var user = requireNonNull(User.findByUsername("user_0")); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(true)) + .put(user.id + "/reported") + .then() + .statusCode(403); + assertFalse(user.isReportedBy(user)); + } + + @Test + public void updateReportUnauthenticated() { + final var reportCount = Report.count(); + final var user = requireNonNull(User.findByUsername("user_0")); + given().contentType(ContentType.JSON) + .body(new ReportUpdate(true)) + .put(user.id + "/reported") + .then() + .statusCode(401); + assertEquals(reportCount, Report.count()); + } +} From d3ce425b23544f6e61ba32272f182516318e99ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 28 Oct 2023 19:04:11 +0200 Subject: [PATCH 096/157] Adjust database schema --- .../java/app/fyreplace/api/data/PushNotificationToken.java | 3 +++ src/main/java/app/fyreplace/api/data/StoredFile.java | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/app/fyreplace/api/data/PushNotificationToken.java b/src/main/java/app/fyreplace/api/data/PushNotificationToken.java index c6aca49..e5bff34 100644 --- a/src/main/java/app/fyreplace/api/data/PushNotificationToken.java +++ b/src/main/java/app/fyreplace/api/data/PushNotificationToken.java @@ -3,6 +3,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import org.hibernate.annotations.OnDelete; @@ -17,6 +19,7 @@ public class PushNotificationToken extends EntityBase { public User user; @Column(nullable = false) + @Enumerated(EnumType.STRING) public Service service; @Column(nullable = false) diff --git a/src/main/java/app/fyreplace/api/data/StoredFile.java b/src/main/java/app/fyreplace/api/data/StoredFile.java index 0fdc240..67482c9 100644 --- a/src/main/java/app/fyreplace/api/data/StoredFile.java +++ b/src/main/java/app/fyreplace/api/data/StoredFile.java @@ -17,7 +17,7 @@ import java.nio.file.Paths; @Entity -@Table(name = "remote_files") +@Table(name = "stored_files") public class StoredFile extends EntityBase { @Transient private StorageService storageService; From 0657e24c719cd9ecd0a14a7fc63b9c5f81fa56b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 29 Oct 2023 13:38:52 +0100 Subject: [PATCH 097/157] Activate users --- src/main/java/app/fyreplace/api/data/RandomCode.java | 6 ++++++ .../java/app/fyreplace/api/endpoints/EmailsEndpoint.java | 4 +--- .../java/app/fyreplace/api/endpoints/TokensEndpoint.java | 6 +++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/app/fyreplace/api/data/RandomCode.java b/src/main/java/app/fyreplace/api/data/RandomCode.java index c17d348..f2ee2bc 100644 --- a/src/main/java/app/fyreplace/api/data/RandomCode.java +++ b/src/main/java/app/fyreplace/api/data/RandomCode.java @@ -21,4 +21,10 @@ public class RandomCode extends TimestampedEntityBase { public String toString() { return code; } + + public void validateEmail() { + email.verified = true; + email.persist(); + delete(); + } } diff --git a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java index ab2dda6..a037147 100644 --- a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java @@ -143,9 +143,7 @@ public Response activate(@NotNull @Valid final EmailActivation input) { throw new NotFoundException(); } - randomCode.email.verified = true; - randomCode.email.persist(); - randomCode.delete(); + randomCode.validateEmail(); return Response.ok().build(); } } diff --git a/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java index aec3d6d..34523a8 100644 --- a/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java @@ -55,9 +55,9 @@ public Response create(@Valid @NotNull final TokenCreation input) { throw new NotFoundException(); } - randomCode.email.verified = true; - randomCode.email.persist(); - randomCode.delete(); + randomCode.validateEmail(); + randomCode.email.user.active = true; + randomCode.email.user.persist(); return Response.status(Status.CREATED).entity(jwtService.makeJwt(email)).build(); } From b5b750b24b73456a47afc77f9b6ca6c7b256c6ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 29 Oct 2023 15:59:49 +0100 Subject: [PATCH 098/157] Remove useless error message --- .../exceptions/UnsupportedMediaTypeException.java | 12 ++---------- .../mappers/UnsupportedMediaTypeExceptionMapper.java | 10 ++++++++-- .../app/fyreplace/api/services/MimeTypeService.java | 2 +- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/main/java/app/fyreplace/api/exceptions/UnsupportedMediaTypeException.java b/src/main/java/app/fyreplace/api/exceptions/UnsupportedMediaTypeException.java index 1481652..54770f8 100644 --- a/src/main/java/app/fyreplace/api/exceptions/UnsupportedMediaTypeException.java +++ b/src/main/java/app/fyreplace/api/exceptions/UnsupportedMediaTypeException.java @@ -3,16 +3,8 @@ import jakarta.ws.rs.ClientErrorException; import jakarta.ws.rs.core.Response; -public final class UnsupportedMediaTypeException extends ClientErrorException implements ExplainableException { - private final String explanationValue; - - public UnsupportedMediaTypeException(final String explanationValue) { +public final class UnsupportedMediaTypeException extends ClientErrorException { + public UnsupportedMediaTypeException() { super(Response.Status.UNSUPPORTED_MEDIA_TYPE); - this.explanationValue = explanationValue; - } - - @Override - public Object getExplanationValue() { - return explanationValue; } } diff --git a/src/main/java/app/fyreplace/api/exceptions/mappers/UnsupportedMediaTypeExceptionMapper.java b/src/main/java/app/fyreplace/api/exceptions/mappers/UnsupportedMediaTypeExceptionMapper.java index aa78bbf..5d0f145 100644 --- a/src/main/java/app/fyreplace/api/exceptions/mappers/UnsupportedMediaTypeExceptionMapper.java +++ b/src/main/java/app/fyreplace/api/exceptions/mappers/UnsupportedMediaTypeExceptionMapper.java @@ -1,9 +1,15 @@ package app.fyreplace.api.exceptions.mappers; import app.fyreplace.api.exceptions.UnsupportedMediaTypeException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; import jakarta.ws.rs.ext.Provider; @SuppressWarnings("unused") @Provider -public final class UnsupportedMediaTypeExceptionMapper - extends ExplainableExceptionMapper {} +public final class UnsupportedMediaTypeExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(final UnsupportedMediaTypeException exception) { + return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE).build(); + } +} diff --git a/src/main/java/app/fyreplace/api/services/MimeTypeService.java b/src/main/java/app/fyreplace/api/services/MimeTypeService.java index d584c2a..dae1243 100644 --- a/src/main/java/app/fyreplace/api/services/MimeTypeService.java +++ b/src/main/java/app/fyreplace/api/services/MimeTypeService.java @@ -28,7 +28,7 @@ public void validate(final byte[] data, final KnownMimeTypes types) { final var mimeType = getMimeType(data); if (!types.types.contains(mimeType)) { - throw new UnsupportedMediaTypeException("invalid_media_type"); + throw new UnsupportedMediaTypeException(); } } From c92447ff3d48a47f2935800d743a23f5f91e789e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 29 Oct 2023 16:02:37 +0100 Subject: [PATCH 099/157] Format code --- src/main/java/app/fyreplace/api/emails/UserConnectionEmail.java | 2 +- src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/app/fyreplace/api/emails/UserConnectionEmail.java b/src/main/java/app/fyreplace/api/emails/UserConnectionEmail.java index b9ce5d8..86d4130 100644 --- a/src/main/java/app/fyreplace/api/emails/UserConnectionEmail.java +++ b/src/main/java/app/fyreplace/api/emails/UserConnectionEmail.java @@ -6,7 +6,7 @@ import java.util.ResourceBundle; @Dependent -public class UserConnectionEmail extends EmailBase { +public final class UserConnectionEmail extends EmailBase { @Override protected String action() { return "connect"; diff --git a/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java index 34523a8..5c51d56 100644 --- a/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java @@ -67,7 +67,6 @@ public Response create(@Valid @NotNull final TokenCreation input) { @APIResponse( responseCode = "200", content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) - @APIResponse(responseCode = "404") public String retrieveNew() { return jwtService.makeJwt(User.getFromSecurityContext(context)); } From 43e25c83e96b83bd6986ef7ee6ffb9a9e63df1c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 29 Oct 2023 17:07:06 +0100 Subject: [PATCH 100/157] Remove boolean prefix for OpenAPI --- src/main/java/app/fyreplace/api/data/Email.java | 2 +- .../app/fyreplace/api/testing/endpoints/emails/CreateTests.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/app/fyreplace/api/data/Email.java b/src/main/java/app/fyreplace/api/data/Email.java index a95593b..15a4e66 100644 --- a/src/main/java/app/fyreplace/api/data/Email.java +++ b/src/main/java/app/fyreplace/api/data/Email.java @@ -23,7 +23,7 @@ public class Email extends EntityBase { @Column(nullable = false) public boolean verified = false; - @JsonProperty("isMain") + @JsonProperty("main") public boolean isMain() { return id.equals(user.mainEmail.id); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java index ed13a80..6b9c904 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java @@ -31,7 +31,7 @@ public void create() { .statusCode(201) .body("email", equalTo(email)) .body("verified", equalTo(false)) - .body("isMain", equalTo(false)); + .body("main", equalTo(false)); assertEquals(emailCount + 1, Email.count()); } From 10e7abf71588194d986e4f325844e46bb2ebf164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 29 Oct 2023 17:31:56 +0100 Subject: [PATCH 101/157] Lift old bans --- .../fyreplace/api/tasks/ModerationTasks.java | 19 +++++ .../WelcomeBackBannedUsersTests.java | 80 +++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 src/main/java/app/fyreplace/api/tasks/ModerationTasks.java create mode 100644 src/test/java/app/fyreplace/api/testing/tasks/moderation/WelcomeBackBannedUsersTests.java diff --git a/src/main/java/app/fyreplace/api/tasks/ModerationTasks.java b/src/main/java/app/fyreplace/api/tasks/ModerationTasks.java new file mode 100644 index 0000000..d2f5db8 --- /dev/null +++ b/src/main/java/app/fyreplace/api/tasks/ModerationTasks.java @@ -0,0 +1,19 @@ +package app.fyreplace.api.tasks; + +import app.fyreplace.api.data.User; +import io.quarkus.scheduler.Scheduled; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.transaction.Transactional; + +@ApplicationScoped +public final class ModerationTasks { + @Scheduled(cron = "0 15 * * * ?") + @Transactional + public void welcomeBackBannedUsers() { + User.update( + """ + banned = false, dateBanEnd = null + where banned and dateBanEnd < current_timestamp + """); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/tasks/moderation/WelcomeBackBannedUsersTests.java b/src/test/java/app/fyreplace/api/testing/tasks/moderation/WelcomeBackBannedUsersTests.java new file mode 100644 index 0000000..c2889ac --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/tasks/moderation/WelcomeBackBannedUsersTests.java @@ -0,0 +1,80 @@ +package app.fyreplace.api.testing.tasks.moderation; + +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import app.fyreplace.api.data.User; +import app.fyreplace.api.tasks.ModerationTasks; +import app.fyreplace.api.testing.UserTestsBase; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import java.time.Duration; +import java.time.Instant; +import org.junit.jupiter.api.Test; + +@QuarkusTest +public final class WelcomeBackBannedUsersTests extends UserTestsBase { + @Inject + ModerationTasks moderationTasks; + + @Test + public void welcomeBackUserBannedNever() { + moderationTasks.welcomeBackBannedUsers(); + final var user = requireNonNull(User.findByUsername("user_0")); + assertFalse(user.banned); + assertEquals(User.BanCount.NEVER, user.banCount); + assertNull(user.dateBanEnd); + } + + @Test + public void welcomeBackUserBannedOnceLongAgo() { + QuarkusTransaction.requiringNew().run(() -> { + final var user = requireNonNull(User.findByUsername("user_0")); + user.banned = true; + user.banCount = User.BanCount.ONCE; + user.dateBanEnd = Instant.now().minus(Duration.ofDays(1)); + user.persist(); + }); + moderationTasks.welcomeBackBannedUsers(); + final var user = requireNonNull(User.findByUsername("user_0")); + assertFalse(user.banned); + assertEquals(User.BanCount.ONCE, user.banCount); + assertNull(user.dateBanEnd); + } + + @Test + public void welcomeBackUserBannedOnceRecently() { + QuarkusTransaction.requiringNew().run(() -> { + final var user = requireNonNull(User.findByUsername("user_0")); + user.banned = true; + user.banCount = User.BanCount.ONCE; + user.dateBanEnd = Instant.now().plus(Duration.ofDays(1)); + user.persist(); + }); + moderationTasks.welcomeBackBannedUsers(); + final var user = requireNonNull(User.findByUsername("user_0")); + assertTrue(user.banned); + assertEquals(User.BanCount.ONCE, user.banCount); + assertNotNull(user.dateBanEnd); + } + + @Test + public void welcomeBackUserBannedForever() { + QuarkusTransaction.requiringNew().run(() -> { + final var user = requireNonNull(User.findByUsername("user_0")); + user.banned = true; + user.banCount = User.BanCount.ONE_TOO_MANY; + user.persist(); + }); + moderationTasks.welcomeBackBannedUsers(); + final var user = requireNonNull(User.findByUsername("user_0")); + assertTrue(user.banned); + assertEquals(User.BanCount.ONE_TOO_MANY, user.banCount); + assertNull(user.dateBanEnd); + } +} From 36d4c184b1a31762869c5c748c8c81233473e338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 29 Oct 2023 16:09:07 +0100 Subject: [PATCH 102/157] Soft delete users and posts --- .../api/data/AuthoredEntityBase.java | 2 +- .../java/app/fyreplace/api/data/Comment.java | 16 ++-- .../java/app/fyreplace/api/data/Post.java | 11 ++- .../api/data/SoftDeletableEntityBase.java | 31 ++++++++ .../java/app/fyreplace/api/data/User.java | 17 ++++- .../api/endpoints/CommentsEndpoint.java | 2 +- .../api/endpoints/PostsEndpoint.java | 32 +++++--- .../api/endpoints/UsersEndpoint.java | 7 +- .../api/exceptions/GoneException.java | 10 +++ .../app/fyreplace/api/tasks/CleanupTasks.java | 34 ++++++++- .../api/testing/CommentTestsBase.java | 3 - .../testing/endpoints/posts/DeleteTests.java | 2 +- .../endpoints/posts/RetrieveTests.java | 8 ++ .../endpoints/users/DeleteMeTests.java | 8 +- .../endpoints/users/RetrieveTests.java | 11 +++ .../ScrubSoftDeletedEntitiesTests.java | 74 +++++++++++++++++++ 16 files changed, 236 insertions(+), 32 deletions(-) create mode 100644 src/main/java/app/fyreplace/api/data/SoftDeletableEntityBase.java create mode 100644 src/main/java/app/fyreplace/api/exceptions/GoneException.java create mode 100644 src/test/java/app/fyreplace/api/testing/tasks/cleanup/ScrubSoftDeletedEntitiesTests.java diff --git a/src/main/java/app/fyreplace/api/data/AuthoredEntityBase.java b/src/main/java/app/fyreplace/api/data/AuthoredEntityBase.java index 3cd9ee8..4bcf1a0 100644 --- a/src/main/java/app/fyreplace/api/data/AuthoredEntityBase.java +++ b/src/main/java/app/fyreplace/api/data/AuthoredEntityBase.java @@ -11,7 +11,7 @@ import org.hibernate.annotations.OnDeleteAction; @MappedSuperclass -public class AuthoredEntityBase extends TimestampedEntityBase { +public abstract class AuthoredEntityBase extends SoftDeletableEntityBase { @ManyToOne(optional = false) @OnDelete(action = OnDeleteAction.CASCADE) @JsonIgnore diff --git a/src/main/java/app/fyreplace/api/data/Comment.java b/src/main/java/app/fyreplace/api/data/Comment.java index ea3ca7a..f3e6ffa 100644 --- a/src/main/java/app/fyreplace/api/data/Comment.java +++ b/src/main/java/app/fyreplace/api/data/Comment.java @@ -1,6 +1,7 @@ package app.fyreplace.api.data; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.annotation.Nonnull; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -23,18 +24,21 @@ public class Comment extends AuthoredEntityBase implements Comparable, @Column(length = 1500, nullable = false) public String text; - @Column(nullable = false) - public boolean deleted; - @Override public int compareTo(@Nonnull final Comment other) { final var dateComparison = dateCreated.compareTo(other.dateCreated); return dateComparison != 0 ? dateComparison : id.compareTo(other.id); } - public void softDelete() { + @Override + public void scrub() { + super.scrub(); text = ""; - deleted = true; - persist(); + } + + @SuppressWarnings("unused") + @JsonProperty("deleted") + public boolean isDeleted() { + return deleted; } } diff --git a/src/main/java/app/fyreplace/api/data/Post.java b/src/main/java/app/fyreplace/api/data/Post.java index dc121c5..8409ff2 100644 --- a/src/main/java/app/fyreplace/api/data/Post.java +++ b/src/main/java/app/fyreplace/api/data/Post.java @@ -1,6 +1,7 @@ package app.fyreplace.api.data; import app.fyreplace.api.exceptions.ForbiddenException; +import app.fyreplace.api.exceptions.GoneException; import com.fasterxml.jackson.annotation.JsonIgnore; import io.quarkus.panache.common.Sort; import jakarta.annotation.Nullable; @@ -32,6 +33,12 @@ public class Post extends AuthoredEntityBase implements Reportable { public static Duration shelfLife = Duration.ofDays(7); + @Override + public void scrub() { + super.scrub(); + getChapters().forEach(Chapter::delete); + } + public List getChapters() { return Chapter.list("post", Sort.by("position"), this); } @@ -71,11 +78,13 @@ public static void validateAccess( @Nullable final User user, @Nullable final Boolean mustBePublished, @Nullable final Boolean mustBeAuthor) { - final Boolean postIsDraft = post != null && (!post.published); + final Boolean postIsDraft = post != null && !post.published; final var userId = user != null ? user.id : null; if (post == null || (!post.author.id.equals(userId) && postIsDraft)) { throw new NotFoundException(); + } else if (post.deleted) { + throw new GoneException(); } else if (mustBePublished == postIsDraft) { throw new ForbiddenException(postIsDraft ? "post_not_published" : "post_is_published"); } else if (mustBeAuthor != null && mustBeAuthor != post.author.id.equals(userId)) { diff --git a/src/main/java/app/fyreplace/api/data/SoftDeletableEntityBase.java b/src/main/java/app/fyreplace/api/data/SoftDeletableEntityBase.java new file mode 100644 index 0000000..8bc4c95 --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/SoftDeletableEntityBase.java @@ -0,0 +1,31 @@ +package app.fyreplace.api.data; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; +import org.hibernate.annotations.Filter; +import org.hibernate.annotations.FilterDef; + +@MappedSuperclass +@FilterDef(name = "existing") +@Filter(name = "existing", condition = "deleted = false") +public abstract class SoftDeletableEntityBase extends TimestampedEntityBase { + @Column(nullable = false) + @JsonIgnore + public boolean deleted; + + @Column(nullable = false) + @JsonIgnore + public boolean scrubbed; + + public void softDelete() { + deleted = true; + persist(); + } + + public void scrub() { + deleted = true; + scrubbed = true; + persist(); + } +} diff --git a/src/main/java/app/fyreplace/api/data/User.java b/src/main/java/app/fyreplace/api/data/User.java index 569ae99..79b261b 100644 --- a/src/main/java/app/fyreplace/api/data/User.java +++ b/src/main/java/app/fyreplace/api/data/User.java @@ -18,7 +18,7 @@ @Entity @Table(name = "users") -public class User extends TimestampedEntityBase implements Reportable { +public class User extends SoftDeletableEntityBase implements Reportable { public static final Set forbiddenUsernames = new HashSet<>(Arrays.asList( "admin", "admins", @@ -61,7 +61,7 @@ public class User extends TimestampedEntityBase implements Reportable { "void", "voids")); - @Column(length = 100, unique = true, nullable = false) + @Column(length = 100, unique = true) public String username; @ManyToOne @@ -95,6 +95,19 @@ public class User extends TimestampedEntityBase implements Reportable { @JsonIgnore public Instant dateBanEnd; + @Override + public void scrub() { + super.scrub(); + Email.delete("user", this); + username = null; + mainEmail = null; + bio = ""; + + if (avatar != null) { + avatar.delete(); + } + } + @JsonIgnore public Set getGroups() { return Arrays.stream(Rank.values()) diff --git a/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java index efdcb3f..c3d63ab 100644 --- a/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java @@ -109,7 +109,7 @@ public Response delete(@PathParam("id") final UUID id, @PathParam("position") @P throw new ForbiddenException("invalid_author"); } - comment.softDelete(); + comment.scrub(); return Response.noContent().build(); } diff --git a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java index cdca4d9..f6d4671 100644 --- a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java @@ -65,16 +65,18 @@ public Iterable list( final var stream = switch (type) { case SUBSCRIBED_TO -> Subscription.find( - "user", + "user = ?1 and post.deleted = false", Sort.by("post.dateCreated", "post.id").direction(direction), user) .page(page, pagingSize) .stream() .map(s -> s.post); case PUBLISHED -> Post.find("author = ?1 and published = true", basicSort, user) + .filter("existing") .page(page, pagingSize) .stream(); case DRAFTS -> Post.find("author = ?1 and published = false", basicSort, user) + .filter("existing") .page(page, pagingSize) .stream(); }; @@ -123,7 +125,13 @@ public Response delete(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); Post.validateAccess(post, user, null, true); - post.delete(); + + if (post.published) { + post.softDelete(); + } else { + post.delete(); + } + return Response.noContent().build(); } @@ -226,9 +234,9 @@ public Response vote(@PathParam("id") final UUID id, @Valid @NotNull final VoteC public long count(@QueryParam("type") @NotNull final PostListingType type) { final var user = User.getFromSecurityContext(context); return switch (type) { - case SUBSCRIBED_TO -> Subscription.count("user", user); - case PUBLISHED -> Post.count("author = ?1 and published = true", user); - case DRAFTS -> Post.count("author = ?1 and published = false", user); + case SUBSCRIBED_TO -> Subscription.count("user = ?1 and post.deleted = false", user); + case PUBLISHED -> Post.count("author = ?1 and published = true and deleted = false", user); + case DRAFTS -> Post.count("author = ?1 and published = false and deleted = false", user); }; } @@ -240,13 +248,19 @@ public Iterable listFeed() { final var user = User.getFromSecurityContext(context); try (final var stream = Post.find( - "author != ?1 and dateCreated > ?2 and published = true and life > 0" - + "and id not in (select post.id from Vote where user = ?1)" - + "and author.id not in (select target.id from Block where source = ?1)" - + "and author.id not in (select source.id from Block where target = ?1)", + """ + author != ?1 + and dateCreated > ?2 + and published = true + and life > 0 + and id not in (select post.id from Vote where user = ?1) + and author.id not in (select target.id from Block where source = ?1) + and author.id not in (select source.id from Block where target = ?1) + """, Sort.by("life", "dateCreated", "id"), user, Instant.now().minus(Post.shelfLife)) + .filter("existing") .range(0, 2) .stream()) { return stream.peek(p -> p.setCurrentUser(user)).toList(); diff --git a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java index a33a180..3de53f6 100644 --- a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java @@ -11,6 +11,7 @@ import app.fyreplace.api.emails.UserActivationEmail; import app.fyreplace.api.exceptions.ConflictException; import app.fyreplace.api.exceptions.ForbiddenException; +import app.fyreplace.api.exceptions.GoneException; import app.fyreplace.api.services.MimeTypeService; import app.fyreplace.api.services.mimetype.KnownMimeTypes; import io.quarkus.cache.CacheResult; @@ -245,7 +246,7 @@ public void deleteMeAvatar() { @APIResponse(responseCode = "204") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response deleteMe() { - User.getFromSecurityContext(context).delete(); + User.getFromSecurityContext(context).softDelete(); return Response.noContent().build(); } @@ -257,7 +258,7 @@ public Iterable listBlocked(@QueryParam("page") @PositiveOrZero fi final var user = User.getFromSecurityContext(context); final var blocks = Block.find("source", Sort.by("id"), user); - try (final var stream = blocks.page(page, pagingSize).stream()) { + try (final var stream = blocks.filter("existing").page(page, pagingSize).stream()) { return stream.map(block -> block.target.getProfile()).toList(); } } @@ -273,6 +274,8 @@ public long countBlocked() { private void validateUser(@Nullable User user) { if (user == null || !user.active) { throw new NotFoundException(); + } else if (user.deleted) { + throw new GoneException(); } } } diff --git a/src/main/java/app/fyreplace/api/exceptions/GoneException.java b/src/main/java/app/fyreplace/api/exceptions/GoneException.java new file mode 100644 index 0000000..3e4ec6a --- /dev/null +++ b/src/main/java/app/fyreplace/api/exceptions/GoneException.java @@ -0,0 +1,10 @@ +package app.fyreplace.api.exceptions; + +import jakarta.ws.rs.ClientErrorException; +import jakarta.ws.rs.core.Response; + +public final class GoneException extends ClientErrorException { + public GoneException() { + super(Response.Status.GONE); + } +} diff --git a/src/main/java/app/fyreplace/api/tasks/CleanupTasks.java b/src/main/java/app/fyreplace/api/tasks/CleanupTasks.java index ae85df9..b85a297 100644 --- a/src/main/java/app/fyreplace/api/tasks/CleanupTasks.java +++ b/src/main/java/app/fyreplace/api/tasks/CleanupTasks.java @@ -1,5 +1,7 @@ package app.fyreplace.api.tasks; +import app.fyreplace.api.data.Comment; +import app.fyreplace.api.data.Post; import app.fyreplace.api.data.RandomCode; import app.fyreplace.api.data.User; import io.quarkus.scheduler.Scheduled; @@ -7,16 +9,36 @@ import jakarta.transaction.Transactional; import java.time.Duration; import java.time.Instant; +import java.util.Arrays; +import java.util.stream.Collectors; +import java.util.stream.Stream; @ApplicationScoped public final class CleanupTasks { - @Scheduled(cron = "0 0 * * * ?") + + @Scheduled(cron = "0 0 0 * * ?") + @Transactional + public void scrubSoftDeletedEntities() { + try (final var stream = User.stream(scrubConditions())) { + stream.forEach(User::scrub); + } + + try (final var stream = Post.stream(scrubConditions("author"))) { + stream.forEach(Post::scrub); + } + + try (final var stream = Comment.stream(scrubConditions("author", "post"))) { + stream.forEach(Comment::scrub); + } + } + + @Scheduled(cron = "0 5 * * * ?") @Transactional public void removeOldInactiveUsers() { User.delete("active = false and dateCreated < ?1", oneDayAgo()); } - @Scheduled(cron = "0 5 * * * ?") + @Scheduled(cron = "0 10 * * * ?") @Transactional public void removeOldRandomCodes() { RandomCode.delete("dateCreated < ?1", oneDayAgo()); @@ -25,4 +47,12 @@ public void removeOldRandomCodes() { private Instant oneDayAgo() { return Instant.now().minus(Duration.ofDays(1)); } + + private String scrubConditions(final String... fields) { + return "scrubbed = false and (" + + Stream.concat(Stream.of(""), Arrays.stream(fields).map(field -> field + '.')) + .map(fieldDot -> fieldDot + "deleted = true") + .collect(Collectors.joining(" or ")) + + ')'; + } } diff --git a/src/test/java/app/fyreplace/api/testing/CommentTestsBase.java b/src/test/java/app/fyreplace/api/testing/CommentTestsBase.java index 4187604..3c73e5f 100644 --- a/src/test/java/app/fyreplace/api/testing/CommentTestsBase.java +++ b/src/test/java/app/fyreplace/api/testing/CommentTestsBase.java @@ -2,7 +2,6 @@ import static java.util.stream.IntStream.range; -import app.fyreplace.api.data.Post; import app.fyreplace.api.data.User; import jakarta.transaction.Transactional; import org.junit.jupiter.api.BeforeEach; @@ -14,8 +13,6 @@ public class CommentTestsBase extends PostTestsBase { public void beforeEach() { super.beforeEach(); final var user = User.findByUsername("user_0"); - final var post = Post.find("author = ?1 and published = true", Post.sorting(), user) - .firstResult(); range(0, 10).forEach(i -> dataSeeder.createComment(user, post, "Comment " + i, false)); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteTests.java index 07fa480..ca3ea53 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteTests.java @@ -20,7 +20,7 @@ public final class DeleteTests extends PostTestsBase { @TestSecurity(user = "user_0") public void deleteOwnPost() { given().delete(post.id.toString()).then().statusCode(204); - assertEquals(0, Post.count("id", post.id)); + assertEquals(1, Post.count("id = ?1 and deleted = true", post.id)); } @Test diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java index f6831c8..fe52767 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java @@ -5,6 +5,7 @@ import static org.hamcrest.Matchers.nullValue; import app.fyreplace.api.data.Comment; +import app.fyreplace.api.data.Post; import app.fyreplace.api.data.User; import app.fyreplace.api.data.Vote; import app.fyreplace.api.endpoints.PostsEndpoint; @@ -155,6 +156,13 @@ public void retrieveDraftUnauthenticated() { given().get(draft.id.toString()).then().statusCode(404); } + @Test + public void retrieveDeletedPost() { + QuarkusTransaction.requiringNew() + .run(() -> Post.findById(this.post.id).softDelete()); + given().get(post.id.toString()).then().statusCode(410); + } + @ParameterizedTest @ValueSource(strings = {"fake", "00000000-0000-0000-0000-000000000000"}) public void retrieveNonExistent(final String id) { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeTests.java index 7918131..2373d87 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeTests.java @@ -1,8 +1,9 @@ package app.fyreplace.api.testing.endpoints.users; import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; @@ -18,10 +19,9 @@ public final class DeleteMeTests extends UserTestsBase { @Test @TestSecurity(user = "user_0") public void deleteMe() { - final var userCount = User.count(); given().delete("me").then().statusCode(204); - assertEquals(userCount - 1, User.count()); - assertNull(User.findByUsername("user_0")); + final var user = requireNonNull(User.findByUsername("user_0")); + assertTrue(user.deleted); } @Test diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java index 58366cc..7d3c797 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java @@ -9,9 +9,12 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.UsersEndpoint; import app.fyreplace.api.testing.UserTestsBase; +import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.restassured.http.ContentType; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -42,6 +45,14 @@ public void retrieveInactive(final String username) { given().get(user.id.toString()).then().statusCode(404); } + @Test + @Transactional + public void retrieveDeleted() { + final var user = requireNonNull(User.findByUsername("user_0")); + QuarkusTransaction.requiringNew().run(() -> User.findById(user.id).softDelete()); + given().get(user.id.toString()).then().statusCode(410); + } + @ParameterizedTest @ValueSource(strings = {"nope", "fake", "@", "admin", "00000000-0000-0000-0000-000000000000"}) public void retrieveNonExistent(final String userId) { diff --git a/src/test/java/app/fyreplace/api/testing/tasks/cleanup/ScrubSoftDeletedEntitiesTests.java b/src/test/java/app/fyreplace/api/testing/tasks/cleanup/ScrubSoftDeletedEntitiesTests.java new file mode 100644 index 0000000..40836be --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/tasks/cleanup/ScrubSoftDeletedEntitiesTests.java @@ -0,0 +1,74 @@ +package app.fyreplace.api.testing.tasks.cleanup; + +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import app.fyreplace.api.data.Chapter; +import app.fyreplace.api.data.Comment; +import app.fyreplace.api.data.Email; +import app.fyreplace.api.data.Post; +import app.fyreplace.api.data.User; +import app.fyreplace.api.tasks.CleanupTasks; +import app.fyreplace.api.testing.CommentTestsBase; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.Test; + +@QuarkusTest +public final class ScrubSoftDeletedEntitiesTests extends CommentTestsBase { + @Inject + CleanupTasks cleanupTasks; + + @Test + @Transactional + public void removeSoftDeletedUser() { + final var userCount = User.count(); + final var user = requireNonNull(User.findByUsername("user_0")); + QuarkusTransaction.requiringNew().run(() -> User.findById(user.id).softDelete()); + cleanupTasks.scrubSoftDeletedEntities(); + assertEquals(userCount, User.count()); + user.refresh(); + assertTrue(user.deleted); + assertTrue(user.scrubbed); + assertNull(user.username); + assertNull(user.mainEmail); + assertNull(user.avatar); + assertEquals("", user.bio); + assertEquals(0, Email.count("user", user)); + assertEquals(0, Post.count("author = ?1 and deleted = false", user)); + assertEquals(0, Comment.count("author = ?1 and deleted = false", user)); + } + + @Test + @Transactional + public void removeSoftDeletedPost() { + final var postCount = Post.count(); + QuarkusTransaction.requiringNew().run(() -> Post.findById(post.id).softDelete()); + cleanupTasks.scrubSoftDeletedEntities(); + assertEquals(postCount, Post.count()); + final var post = Post.findById(this.post.id); + assertTrue(post.deleted); + assertTrue(post.scrubbed); + assertEquals(0, Chapter.count("post", post)); + assertEquals(0, Comment.count("post = ?1 and deleted = false", post)); + } + + @Test + @Transactional + public void removeSoftDeletedComment() { + final var commentCount = Comment.count(); + final var comment = Comment.find("post", post).firstResult(); + QuarkusTransaction.requiringNew() + .run(() -> Comment.findById(comment.id).softDelete()); + cleanupTasks.scrubSoftDeletedEntities(); + assertEquals(commentCount, Comment.count()); + comment.refresh(); + assertTrue(comment.deleted); + assertTrue(comment.scrubbed); + assertEquals("", comment.text); + } +} From 7e4bb3cc821d10ba4141e5b80324b7d23ff413c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Wed, 1 Nov 2023 17:55:33 +0100 Subject: [PATCH 103/157] Give Gradle more resources --- gradle.properties | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gradle.properties b/gradle.properties index 39edb39..3e11d7e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,5 @@ +org.gradle.jvmargs=-Xmx1024M + quarkusVersion=3.5.0 quarkusAmazonVersion=2.5.2 sentryVersion=6.32.0 From b6912503ef7818553a57afb061b3cdd3b00f3cfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Wed, 1 Nov 2023 18:03:40 +0100 Subject: [PATCH 104/157] Add liquibase migrations --- build.gradle | 7 +- src/main/resources/application.yaml | 80 +-- src/main/resources/db/changeLog.yaml | 803 +++++++++++++++++++++++++++ 3 files changed, 836 insertions(+), 54 deletions(-) create mode 100644 src/main/resources/db/changeLog.yaml diff --git a/build.gradle b/build.gradle index c48bab7..2631961 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,7 @@ dependencies { implementation("io.quarkus:quarkus-hibernate-validator") implementation("io.quarkus:quarkus-jdbc-h2") implementation("io.quarkus:quarkus-jdbc-postgresql") + implementation("io.quarkus:quarkus-liquibase") implementation("io.quarkus:quarkus-mailer") implementation("io.quarkus:quarkus-resteasy-reactive") implementation("io.quarkus:quarkus-resteasy-reactive-jackson") @@ -37,7 +38,6 @@ dependencies { implementation("io.quarkus:quarkus-smallrye-jwt-build") implementation("io.quarkus:quarkus-smallrye-openapi") implementation("io.quarkiverse.amazonservices:quarkus-amazon-s3") - implementation("org.jboss.logmanager:log4j2-jboss-logmanager") implementation("org.apache.tika:tika-core") implementation("org.apache.tika:tika-parsers-standard-package") implementation("software.amazon.awssdk:url-connection-client") @@ -95,6 +95,11 @@ spotless { palantirJavaFormat() target "**/src/*/java/**/*.java" } + + yaml { + jackson() + target "**/src/*/resources/**/*.yaml" + } } compileJava.dependsOn "spotlessApply" diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 32e2f0c..d12ab11 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,105 +1,79 @@ -# Main profile - +--- quarkus: datasource: - db-kind: postgresql - + db-kind: "postgresql" hibernate-orm: - physical-naming-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy - + physical-naming-strategy: "org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy" + liquibase: + change-log: "db/changeLog.yaml" + migrate-at-start: true qute: content-types: - mjml: text/plain - + mjml: "text/plain" http: auth: permission: dev: - paths: /dev/* - policy: deny - + paths: "/dev/*" + policy: "deny" cors: - ~: true - + "~": true limits: - max-body-size: 1M - + max-body-size: "1M" mailer: port: 587 - start-tls: REQUIRED - + start-tls: "REQUIRED" s3: devservices: enabled: false - mp: jwt: verify: - issuer: ${app.url} - + issuer: "${app.url}" app: url: "" - name: Fyreplace + name: "Fyreplace" use-example-data: false - front: url: "" - paging: size: 12 - storage: - type: local - + type: "local" local: path: "" - s3: bucket: "" custom-endpoint: "" - posts: max-chapter-count: 10 starting-life: 4 - -# Dev profile - -"%dev": +'%dev': quarkus: hibernate-orm: database: generation: - ~: drop-and-create - + "~": "drop-and-create" + liquibase: + enabled: false http: auth: permission: dev: - paths: /dev/* - policy: permit - + paths: "/dev/*" + policy: "permit" app: use-example-data: true - -# Test profile - -"%test": +'%test': quarkus: datasource: - db-kind: h2 + db-kind: "h2" jdbc: - user: h2 - password: h2 - url: jdbc:h2:mem:fyreplace - - hibernate-orm: - database: - generation: - ~: drop-and-create - + user: "h2" + password: "h2" + url: "jdbc:h2:mem:fyreplace" http: cors: - origins: /.*/ - + origins: "/.*/" scheduler: enabled: false diff --git a/src/main/resources/db/changeLog.yaml b/src/main/resources/db/changeLog.yaml new file mode 100644 index 0000000..36581c9 --- /dev/null +++ b/src/main/resources/db/changeLog.yaml @@ -0,0 +1,803 @@ +--- +databaseChangeLog: +- changeSet: + id: "initial-0001" + author: "generated" + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: "stored_files_pkey" + name: "id" + type: "UUID" + - column: + constraints: + nullable: false + name: "path" + type: "VARCHAR(255)" + tableName: "stored_files" +- changeSet: + id: "initial-0002" + author: "generated" + changes: + - addUniqueConstraint: + columnNames: "path" + constraintName: "stored_files_path_key" + tableName: "stored_files" +- changeSet: + id: "initial-0003" + author: "generated" + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + name: "active" + type: "BOOLEAN" + - column: + constraints: + nullable: false + name: "ban_count" + type: "SMALLINT" + - column: + constraints: + nullable: false + name: "banned" + type: "BOOLEAN" + - column: + constraints: + nullable: false + name: "deleted" + type: "BOOLEAN" + - column: + constraints: + nullable: false + name: "rank" + type: "SMALLINT" + - column: + constraints: + nullable: false + name: "scrubbed" + type: "BOOLEAN" + - column: + name: "date_ban_end" + type: "TIMESTAMP WITH TIME ZONE" + - column: + constraints: + nullable: false + name: "date_created" + type: "TIMESTAMP WITH TIME ZONE" + - column: + name: "avatar_id" + type: "UUID" + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: "users_pkey" + name: "id" + type: "UUID" + - column: + name: "main_email_id" + type: "UUID" + - column: + name: "username" + type: "VARCHAR(100)" + - column: + constraints: + nullable: false + name: "bio" + type: "VARCHAR(3000)" + tableName: "users" +- changeSet: + id: "initial-0004" + author: "generated" + changes: + - addUniqueConstraint: + columnNames: "avatar_id" + constraintName: "users_avatar_id_key" + tableName: "users" +- changeSet: + id: "initial-0005" + author: "generated" + changes: + - addUniqueConstraint: + columnNames: "username" + constraintName: "users_username_key" + tableName: "users" +- changeSet: + id: "initial-0006" + author: "generated" + changes: + - addForeignKeyConstraint: + baseColumnNames: "avatar_id" + baseTableName: "users" + constraintName: "fk3yu3huc4hacx6fgh0v6qim7e4" + deferrable: false + initiallyDeferred: false + onDelete: "SET NULL" + onUpdate: "NO ACTION" + referencedColumnNames: "id" + referencedTableName: "stored_files" + validate: true +- changeSet: + id: "initial-0007" + author: "generated" + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + name: "verified" + type: "BOOLEAN" + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: "emails_pkey" + name: "id" + type: "UUID" + - column: + constraints: + nullable: false + name: "user_id" + type: "UUID" + - column: + constraints: + nullable: false + name: "email" + type: "VARCHAR(254)" + tableName: "emails" +- changeSet: + id: "initial-0008" + author: "generated" + changes: + - addUniqueConstraint: + columnNames: "email" + constraintName: "emails_email_key" + tableName: "emails" +- changeSet: + id: "initial-0009" + author: "generated" + changes: + - addForeignKeyConstraint: + baseColumnNames: "main_email_id" + baseTableName: "users" + constraintName: "fkh69m5hbvj9ehf1bgouu5ovhq8" + deferrable: false + initiallyDeferred: false + onDelete: "SET NULL" + onUpdate: "NO ACTION" + referencedColumnNames: "id" + referencedTableName: "emails" + validate: true +- changeSet: + id: "initial-0010" + author: "generated" + changes: + - addForeignKeyConstraint: + baseColumnNames: "user_id" + baseTableName: "emails" + constraintName: "fk41wb6kvdemvj1602iltrfr1uo" + deferrable: false + initiallyDeferred: false + onDelete: "CASCADE" + onUpdate: "NO ACTION" + referencedColumnNames: "id" + referencedTableName: "users" + validate: true +- changeSet: + id: "initial-0011" + author: "generated" + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + name: "date_created" + type: "TIMESTAMP WITH TIME ZONE" + - column: + constraints: + nullable: false + name: "email_id" + type: "UUID" + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: "random_codes_pkey" + name: "id" + type: "UUID" + - column: + constraints: + nullable: false + name: "code" + type: "VARCHAR(255)" + tableName: "random_codes" +- changeSet: + id: "initial-0012" + author: "generated" + changes: + - addForeignKeyConstraint: + baseColumnNames: "email_id" + baseTableName: "random_codes" + constraintName: "fk7in6mdb1ax6jdd2v8p4cd4o3g" + deferrable: false + initiallyDeferred: false + onDelete: "CASCADE" + onUpdate: "NO ACTION" + referencedColumnNames: "id" + referencedTableName: "emails" + validate: true +- changeSet: + id: "initial-0013" + author: "generated" + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: "blocks_pkey" + name: "id" + type: "UUID" + - column: + constraints: + nullable: false + name: "source_id" + type: "UUID" + - column: + constraints: + nullable: false + name: "target_id" + type: "UUID" + tableName: "blocks" +- changeSet: + id: "initial-0014" + author: "generated" + changes: + - addUniqueConstraint: + columnNames: "source_id, target_id" + constraintName: "ukou6l1m15jrlhofu7hn5ueytw" + tableName: "blocks" +- changeSet: + id: "initial-0015" + author: "generated" + changes: + - addForeignKeyConstraint: + baseColumnNames: "source_id" + baseTableName: "blocks" + constraintName: "fk9wmpvu0ydl4ay1qynekibi9q4" + deferrable: false + initiallyDeferred: false + onDelete: "CASCADE" + onUpdate: "NO ACTION" + referencedColumnNames: "id" + referencedTableName: "users" + validate: true +- changeSet: + id: "initial-0016" + author: "generated" + changes: + - addForeignKeyConstraint: + baseColumnNames: "target_id" + baseTableName: "blocks" + constraintName: "fk4xwbx7o70g208mb0rpskdwnct" + deferrable: false + initiallyDeferred: false + onDelete: "CASCADE" + onUpdate: "NO ACTION" + referencedColumnNames: "id" + referencedTableName: "users" + validate: true +- changeSet: + id: "initial-0017" + author: "generated" + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: "push_notification_tokens_pkey" + name: "id" + type: "UUID" + - column: + constraints: + nullable: false + name: "user_id" + type: "UUID" + - column: + constraints: + nullable: false + name: "service" + type: "VARCHAR(255)" + - column: + constraints: + nullable: false + name: "token" + type: "VARCHAR(255)" + tableName: "push_notification_tokens" +- changeSet: + id: "initial-0018" + author: "generated" + changes: + - addForeignKeyConstraint: + baseColumnNames: "user_id" + baseTableName: "push_notification_tokens" + constraintName: "fkg63ygyp1y6pvmypol9d7gp40d" + deferrable: false + initiallyDeferred: false + onDelete: "CASCADE" + onUpdate: "NO ACTION" + referencedColumnNames: "id" + referencedTableName: "users" + validate: true +- changeSet: + id: "initial-0019" + author: "generated" + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + name: "date_created" + type: "TIMESTAMP WITH TIME ZONE" + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: "reports_pkey" + name: "id" + type: "UUID" + - column: + constraints: + nullable: false + name: "source_id" + type: "UUID" + - column: + constraints: + nullable: false + name: "target_id" + type: "UUID" + - column: + constraints: + nullable: false + name: "target_model" + type: "VARCHAR(255)" + tableName: "reports" +- changeSet: + id: "initial-0020" + author: "generated" + changes: + - addUniqueConstraint: + columnNames: "source_id, target_model, target_id" + constraintName: "uka65rlbm4g84192wjr0gidyos1" + tableName: "reports" +- changeSet: + id: "initial-0021" + author: "generated" + changes: + - addForeignKeyConstraint: + baseColumnNames: "source_id" + baseTableName: "reports" + constraintName: "fkmscflduaciisepdvojksfy53e" + deferrable: false + initiallyDeferred: false + onDelete: "CASCADE" + onUpdate: "NO ACTION" + referencedColumnNames: "id" + referencedTableName: "users" + validate: true +- changeSet: + id: "initial-0022" + author: "generated" + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + name: "anonymous" + type: "BOOLEAN" + - column: + constraints: + nullable: false + name: "deleted" + type: "BOOLEAN" + - column: + constraints: + nullable: false + name: "life" + type: "INTEGER" + - column: + constraints: + nullable: false + name: "published" + type: "BOOLEAN" + - column: + constraints: + nullable: false + name: "scrubbed" + type: "BOOLEAN" + - column: + constraints: + nullable: false + name: "date_created" + type: "TIMESTAMP WITH TIME ZONE" + - column: + constraints: + nullable: false + name: "author_id" + type: "UUID" + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: "posts_pkey" + name: "id" + type: "UUID" + tableName: "posts" +- changeSet: + id: "initial-0023" + author: "generated" + changes: + - addForeignKeyConstraint: + baseColumnNames: "author_id" + baseTableName: "posts" + constraintName: "fk6xvn0811tkyo3nfjk2xvqx6ns" + deferrable: false + initiallyDeferred: false + onDelete: "CASCADE" + onUpdate: "NO ACTION" + referencedColumnNames: "id" + referencedTableName: "users" + validate: true +- changeSet: + id: "initial-0024" + author: "generated" + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + name: "height" + type: "INTEGER" + - column: + constraints: + nullable: false + name: "width" + type: "INTEGER" + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: "chapters_pkey" + name: "id" + type: "UUID" + - column: + name: "image_id" + type: "UUID" + - column: + constraints: + nullable: false + name: "post_id" + type: "UUID" + - column: + constraints: + nullable: false + name: "position" + type: "VARCHAR(50)" + - column: + constraints: + nullable: false + name: "text" + type: "VARCHAR(500)" + tableName: "chapters" +- changeSet: + id: "initial-0025" + author: "generated" + changes: + - addUniqueConstraint: + columnNames: "image_id" + constraintName: "chapters_image_id_key" + tableName: "chapters" +- changeSet: + id: "initial-0026" + author: "generated" + changes: + - addUniqueConstraint: + columnNames: "post_id, position" + constraintName: "ukqj1py5nfact1vlvjgtflawowy" + tableName: "chapters" +- changeSet: + id: "initial-0027" + author: "generated" + changes: + - addForeignKeyConstraint: + baseColumnNames: "image_id" + baseTableName: "chapters" + constraintName: "fkl426htf69f75849igwddqmpar" + deferrable: false + initiallyDeferred: false + onDelete: "SET NULL" + onUpdate: "NO ACTION" + referencedColumnNames: "id" + referencedTableName: "stored_files" + validate: true +- changeSet: + id: "initial-0028" + author: "generated" + changes: + - addForeignKeyConstraint: + baseColumnNames: "post_id" + baseTableName: "chapters" + constraintName: "fkgelawcoy6fpvefbjkxf42503d" + deferrable: false + initiallyDeferred: false + onDelete: "CASCADE" + onUpdate: "NO ACTION" + referencedColumnNames: "id" + referencedTableName: "posts" + validate: true +- changeSet: + id: "initial-0029" + author: "generated" + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + name: "anonymous" + type: "BOOLEAN" + - column: + constraints: + nullable: false + name: "deleted" + type: "BOOLEAN" + - column: + constraints: + nullable: false + name: "scrubbed" + type: "BOOLEAN" + - column: + constraints: + nullable: false + name: "date_created" + type: "TIMESTAMP WITH TIME ZONE" + - column: + constraints: + nullable: false + name: "author_id" + type: "UUID" + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: "comments_pkey" + name: "id" + type: "UUID" + - column: + constraints: + nullable: false + name: "post_id" + type: "UUID" + - column: + constraints: + nullable: false + name: "text" + type: "VARCHAR(1500)" + tableName: "comments" +- changeSet: + id: "initial-0030" + author: "generated" + changes: + - createIndex: + columns: + - column: + name: "post_id" + indexName: "idx2ocgo3lfadb3wq0tx8wyt7sj2" + tableName: "comments" +- changeSet: + id: "initial-0031" + author: "generated" + changes: + - addForeignKeyConstraint: + baseColumnNames: "author_id" + baseTableName: "comments" + constraintName: "fkn2na60ukhs76ibtpt9burkm27" + deferrable: false + initiallyDeferred: false + onDelete: "CASCADE" + onUpdate: "NO ACTION" + referencedColumnNames: "id" + referencedTableName: "users" + validate: true +- changeSet: + id: "initial-0032" + author: "generated" + changes: + - addForeignKeyConstraint: + baseColumnNames: "post_id" + baseTableName: "comments" + constraintName: "fkh4c7lvsc298whoyd4w9ta25cr" + deferrable: false + initiallyDeferred: false + onDelete: "CASCADE" + onUpdate: "NO ACTION" + referencedColumnNames: "id" + referencedTableName: "posts" + validate: true +- changeSet: + id: "initial-0033" + author: "generated" + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + name: "date_updated" + type: "TIMESTAMP WITH TIME ZONE" + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: "subscriptions_pkey" + name: "id" + type: "UUID" + - column: + name: "last_comment_seen_id" + type: "UUID" + - column: + constraints: + nullable: false + name: "post_id" + type: "UUID" + - column: + constraints: + nullable: false + name: "user_id" + type: "UUID" + tableName: "subscriptions" +- changeSet: + id: "initial-0034" + author: "generated" + changes: + - addUniqueConstraint: + columnNames: "user_id, post_id" + constraintName: "ukjvj9d1ro9lw17oe2pyl9bonm4" + tableName: "subscriptions" +- changeSet: + id: "initial-0035" + author: "generated" + changes: + - addForeignKeyConstraint: + baseColumnNames: "last_comment_seen_id" + baseTableName: "subscriptions" + constraintName: "fk283jtphhwtlrrj52uv6v63wb" + deferrable: false + initiallyDeferred: false + onDelete: "CASCADE" + onUpdate: "NO ACTION" + referencedColumnNames: "id" + referencedTableName: "comments" + validate: true +- changeSet: + id: "initial-0036" + author: "generated" + changes: + - addForeignKeyConstraint: + baseColumnNames: "post_id" + baseTableName: "subscriptions" + constraintName: "fkahxduy73eys83pydr9d9jdt34" + deferrable: false + initiallyDeferred: false + onDelete: "CASCADE" + onUpdate: "NO ACTION" + referencedColumnNames: "id" + referencedTableName: "posts" + validate: true +- changeSet: + id: "initial-0037" + author: "generated" + changes: + - addForeignKeyConstraint: + baseColumnNames: "user_id" + baseTableName: "subscriptions" + constraintName: "fkhro52ohfqfbay9774bev0qinr" + deferrable: false + initiallyDeferred: false + onDelete: "CASCADE" + onUpdate: "NO ACTION" + referencedColumnNames: "id" + referencedTableName: "users" + validate: true +- changeSet: + id: "initial-0038" + author: "generated" + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + name: "spread" + type: "BOOLEAN" + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: "votes_pkey" + name: "id" + type: "UUID" + - column: + constraints: + nullable: false + name: "post_id" + type: "UUID" + - column: + constraints: + nullable: false + name: "user_id" + type: "UUID" + tableName: "votes" +- changeSet: + id: "initial-0039" + author: "generated" + changes: + - createIndex: + columns: + - column: + name: "post_id" + indexName: "idxsnowcffjecrw34fxm6h5fyah4" + tableName: "votes" +- changeSet: + id: "initial-0040" + author: "generated" + changes: + - addUniqueConstraint: + columnNames: "user_id, post_id" + constraintName: "ukpa0qu72klq223r3f3mpgf9ele" + tableName: "votes" +- changeSet: + id: "initial-0041" + author: "generated" + changes: + - addForeignKeyConstraint: + baseColumnNames: "post_id" + baseTableName: "votes" + constraintName: "fk1m2jqtro85c13ya5kv0kvkc97" + deferrable: false + initiallyDeferred: false + onDelete: "CASCADE" + onUpdate: "NO ACTION" + referencedColumnNames: "id" + referencedTableName: "posts" + validate: true +- changeSet: + id: "initial-0042" + author: "generated" + changes: + - addForeignKeyConstraint: + baseColumnNames: "user_id" + baseTableName: "votes" + constraintName: "fkli4uj3ic2vypf5pialchj925e" + deferrable: false + initiallyDeferred: false + onDelete: "CASCADE" + onUpdate: "NO ACTION" + referencedColumnNames: "id" + referencedTableName: "users" + validate: true From 341d057805f23f0d847e94d2ec91ab6d535e8dbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 3 Nov 2023 18:49:33 +0100 Subject: [PATCH 105/157] Simplify config --- src/main/resources/application.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index d12ab11..8a7b375 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -16,8 +16,7 @@ quarkus: dev: paths: "/dev/*" policy: "deny" - cors: - "~": true + cors: true limits: max-body-size: "1M" mailer: @@ -52,8 +51,7 @@ app: quarkus: hibernate-orm: database: - generation: - "~": "drop-and-create" + generation: "drop-and-create" liquibase: enabled: false http: From 42c07ca81c45393774fb7f02ccd0d54f9bc5a7ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 5 Nov 2023 00:19:28 +0100 Subject: [PATCH 106/157] Put email in email links --- src/main/java/app/fyreplace/api/emails/EmailBase.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/app/fyreplace/api/emails/EmailBase.java b/src/main/java/app/fyreplace/api/emails/EmailBase.java index 19a619e..bedc277 100644 --- a/src/main/java/app/fyreplace/api/emails/EmailBase.java +++ b/src/main/java/app/fyreplace/api/emails/EmailBase.java @@ -65,7 +65,7 @@ protected String getRandomCode() { protected String getLink() { return URI.create(appFrontUrl) .resolve("?action=" + action()) - .resolve('#' + email.user.username + ':' + getRandomCode()) + .resolve('#' + email.email + ':' + getRandomCode()) .toString(); } From 197697829f69ef23b101c7cb1fce964bf09cb8b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Mon, 6 Nov 2023 23:29:00 +0100 Subject: [PATCH 107/157] Add passwords --- build.gradle | 1 + .../java/app/fyreplace/api/data/Password.java | 19 +++ .../app/fyreplace/api/data/TokenCreation.java | 2 +- .../fyreplace/api/data/dev/DataSeeder.java | 2 + ...DevUsersEndpoint.java => DevEndpoint.java} | 17 +- .../api/endpoints/EmailsEndpoint.java | 2 +- .../api/endpoints/TokensEndpoint.java | 21 ++- .../fyreplace/api/services/JwtService.java | 5 - src/main/resources/db/changeLog.yaml | 145 ++++++++++++------ .../dev/RetrievePasswordHashTests.java | 20 +++ .../endpoints/dev/RetrieveUserTokenTests.java | 20 +++ .../dev/users/RetrieveTokenTests.java | 18 --- .../testing/endpoints/tokens/CreateTests.java | 25 ++- 13 files changed, 213 insertions(+), 84 deletions(-) create mode 100644 src/main/java/app/fyreplace/api/data/Password.java rename src/main/java/app/fyreplace/api/endpoints/{DevUsersEndpoint.java => DevEndpoint.java} (70%) create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/dev/RetrievePasswordHashTests.java create mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/dev/RetrieveUserTokenTests.java delete mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/dev/users/RetrieveTokenTests.java diff --git a/build.gradle b/build.gradle index 2631961..97a2eeb 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,7 @@ dependencies { implementation("io.quarkus:quarkus-arc") implementation("io.quarkus:quarkus-cache") implementation("io.quarkus:quarkus-config-yaml") + implementation("io.quarkus:quarkus-elytron-security-jdbc") implementation("io.quarkus:quarkus-hibernate-orm-panache") implementation("io.quarkus:quarkus-hibernate-validator") implementation("io.quarkus:quarkus-jdbc-h2") diff --git a/src/main/java/app/fyreplace/api/data/Password.java b/src/main/java/app/fyreplace/api/data/Password.java new file mode 100644 index 0000000..3e07ecf --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/Password.java @@ -0,0 +1,19 @@ +package app.fyreplace.api.data; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +@Entity +@Table(name = "passwords") +public class Password extends EntityBase { + @OneToOne(optional = false) + @OnDelete(action = OnDeleteAction.CASCADE) + public User user; + + @Column(nullable = false) + public String password; +} diff --git a/src/main/java/app/fyreplace/api/data/TokenCreation.java b/src/main/java/app/fyreplace/api/data/TokenCreation.java index 23f6e8d..a2e5450 100644 --- a/src/main/java/app/fyreplace/api/data/TokenCreation.java +++ b/src/main/java/app/fyreplace/api/data/TokenCreation.java @@ -2,4 +2,4 @@ import jakarta.validation.constraints.NotBlank; -public record TokenCreation(@NotBlank String identifier, @NotBlank String code) {} +public record TokenCreation(@NotBlank String identifier, @NotBlank String secret) {} diff --git a/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java b/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java index 8e0e221..fe23a2e 100644 --- a/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java +++ b/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java @@ -6,6 +6,7 @@ import app.fyreplace.api.data.Chapter; import app.fyreplace.api.data.Comment; import app.fyreplace.api.data.Email; +import app.fyreplace.api.data.Password; import app.fyreplace.api.data.Post; import app.fyreplace.api.data.PushNotificationToken; import app.fyreplace.api.data.RandomCode; @@ -59,6 +60,7 @@ public void insertData() { @Transactional public void deleteData() { Email.deleteAll(); + Password.deleteAll(); RandomCode.deleteAll(); Block.deleteAll(); PushNotificationToken.deleteAll(); diff --git a/src/main/java/app/fyreplace/api/endpoints/DevUsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/DevEndpoint.java similarity index 70% rename from src/main/java/app/fyreplace/api/endpoints/DevUsersEndpoint.java rename to src/main/java/app/fyreplace/api/endpoints/DevEndpoint.java index 9b20ff7..609e61f 100644 --- a/src/main/java/app/fyreplace/api/endpoints/DevUsersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/DevEndpoint.java @@ -4,6 +4,7 @@ import app.fyreplace.api.data.User; import app.fyreplace.api.services.JwtService; import io.quarkus.cache.CacheResult; +import io.quarkus.elytron.security.common.BcryptUtil; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.NotFoundException; @@ -14,13 +15,13 @@ import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; -@Path("dev/users") -public final class DevUsersEndpoint { +@Path("dev") +public final class DevEndpoint { @Inject JwtService jwtService; @GET - @Path("{username}/token") + @Path("users/{username}/token") @APIResponse( responseCode = "200", content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) @@ -35,4 +36,14 @@ public String retrieveToken(@PathParam("username") final String username) { return jwtService.makeJwt(user); } + + @GET + @Path("passwords/{password}/hash") + @APIResponse( + responseCode = "200", + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) + @APIResponse(responseCode = "404") + public String retrievePassword(@PathParam("password") final String password) { + return BcryptUtil.bcryptHash(password); + } } diff --git a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java index a037147..bb4da47 100644 --- a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java @@ -135,7 +135,7 @@ public long count() { @APIResponse(responseCode = "404") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response activate(@NotNull @Valid final EmailActivation input) { - var email = Email.find("email", input.email()).firstResult(); + final var email = Email.find("email", input.email()).firstResult(); final var randomCode = RandomCode.find("email = ?1 and code = ?2", email, input.code()) .firstResult(); diff --git a/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java index 5c51d56..5bcbddf 100644 --- a/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java @@ -3,12 +3,14 @@ import app.fyreplace.api.cache.DuplicateRequestKeyGenerator; import app.fyreplace.api.data.Email; import app.fyreplace.api.data.NewTokenCreation; +import app.fyreplace.api.data.Password; import app.fyreplace.api.data.RandomCode; import app.fyreplace.api.data.TokenCreation; import app.fyreplace.api.data.User; import app.fyreplace.api.emails.UserConnectionEmail; import app.fyreplace.api.services.JwtService; import io.quarkus.cache.CacheResult; +import io.quarkus.elytron.security.common.BcryptUtil; import io.quarkus.security.Authenticated; import jakarta.inject.Inject; import jakarta.transaction.Transactional; @@ -48,17 +50,24 @@ public final class TokensEndpoint { @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response create(@Valid @NotNull final TokenCreation input) { final var email = getEmail(input.identifier()); - final var randomCode = RandomCode.find("email = ?1 and code = ?2", email, input.code()) + final var randomCode = RandomCode.find("email = ?1 and code = ?2", email, input.secret()) .firstResult(); + final var password = Password.find("user", email.user).firstResult(); - if (randomCode == null) { + if (randomCode != null) { + randomCode.validateEmail(); + } else if (password != null && BcryptUtil.matches(input.secret(), password.password)) { + email.verified = true; + email.persist(); + } else { throw new NotFoundException(); } - randomCode.validateEmail(); - randomCode.email.user.active = true; - randomCode.email.user.persist(); - return Response.status(Status.CREATED).entity(jwtService.makeJwt(email)).build(); + email.user.active = true; + email.user.persist(); + return Response.status(Status.CREATED) + .entity(jwtService.makeJwt(email.user)) + .build(); } @GET diff --git a/src/main/java/app/fyreplace/api/services/JwtService.java b/src/main/java/app/fyreplace/api/services/JwtService.java index 4da500c..8d8556b 100644 --- a/src/main/java/app/fyreplace/api/services/JwtService.java +++ b/src/main/java/app/fyreplace/api/services/JwtService.java @@ -1,6 +1,5 @@ package app.fyreplace.api.services; -import app.fyreplace.api.data.Email; import app.fyreplace.api.data.User; import io.smallrye.jwt.build.Jwt; import jakarta.enterprise.context.ApplicationScoped; @@ -19,8 +18,4 @@ public String makeJwt(final User user) { .expiresIn(Duration.ofDays(3)) .sign(); } - - public String makeJwt(final Email email) { - return makeJwt(email.user); - } } diff --git a/src/main/resources/db/changeLog.yaml b/src/main/resources/db/changeLog.yaml index 36581c9..5d2d218 100644 --- a/src/main/resources/db/changeLog.yaml +++ b/src/main/resources/db/changeLog.yaml @@ -1,7 +1,7 @@ --- databaseChangeLog: - changeSet: - id: "initial-0001" + id: "stored_files-0001" author: "generated" changes: - createTable: @@ -20,7 +20,7 @@ databaseChangeLog: type: "VARCHAR(255)" tableName: "stored_files" - changeSet: - id: "initial-0002" + id: "stored_files-0002" author: "generated" changes: - addUniqueConstraint: @@ -28,7 +28,7 @@ databaseChangeLog: constraintName: "stored_files_path_key" tableName: "stored_files" - changeSet: - id: "initial-0003" + id: "users-0001" author: "generated" changes: - createTable: @@ -94,7 +94,7 @@ databaseChangeLog: type: "VARCHAR(3000)" tableName: "users" - changeSet: - id: "initial-0004" + id: "users-0002" author: "generated" changes: - addUniqueConstraint: @@ -102,7 +102,7 @@ databaseChangeLog: constraintName: "users_avatar_id_key" tableName: "users" - changeSet: - id: "initial-0005" + id: "users-0003" author: "generated" changes: - addUniqueConstraint: @@ -110,7 +110,7 @@ databaseChangeLog: constraintName: "users_username_key" tableName: "users" - changeSet: - id: "initial-0006" + id: "users-0004" author: "generated" changes: - addForeignKeyConstraint: @@ -125,7 +125,54 @@ databaseChangeLog: referencedTableName: "stored_files" validate: true - changeSet: - id: "initial-0007" + id: "passwords-0001" + author: "generated" + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: "passwords_pkey" + name: "id" + type: "UUID" + - column: + constraints: + nullable: false + name: "user_id" + type: "UUID" + - column: + constraints: + nullable: false + name: "password" + type: "VARCHAR(255)" + tableName: "passwords" +- changeSet: + id: "passwords-0002" + author: "generated" + changes: + - addUniqueConstraint: + columnNames: "user_id" + constraintName: "passwords_user_id_key" + tableName: "passwords" +- changeSet: + id: "passwords-0003" + author: "generated" + changes: + - addForeignKeyConstraint: + baseColumnNames: "user_id" + baseTableName: "passwords" + constraintName: "fkqiupw3oqiukdfyc45xvoky044" + deferrable: false + initiallyDeferred: false + onDelete: "CASCADE" + onUpdate: "NO ACTION" + referencedColumnNames: "id" + referencedTableName: "users" + validate: true +- changeSet: + id: "emails-0001" author: "generated" changes: - createTable: @@ -154,15 +201,7 @@ databaseChangeLog: type: "VARCHAR(254)" tableName: "emails" - changeSet: - id: "initial-0008" - author: "generated" - changes: - - addUniqueConstraint: - columnNames: "email" - constraintName: "emails_email_key" - tableName: "emails" -- changeSet: - id: "initial-0009" + id: "users-0005" author: "generated" changes: - addForeignKeyConstraint: @@ -177,7 +216,15 @@ databaseChangeLog: referencedTableName: "emails" validate: true - changeSet: - id: "initial-0010" + id: "emails-0002" + author: "generated" + changes: + - addUniqueConstraint: + columnNames: "email" + constraintName: "emails_email_key" + tableName: "emails" +- changeSet: + id: "emails-003" author: "generated" changes: - addForeignKeyConstraint: @@ -192,7 +239,7 @@ databaseChangeLog: referencedTableName: "users" validate: true - changeSet: - id: "initial-0011" + id: "random_codes-0001" author: "generated" changes: - createTable: @@ -221,7 +268,7 @@ databaseChangeLog: type: "VARCHAR(255)" tableName: "random_codes" - changeSet: - id: "initial-0012" + id: "random_codes-0002" author: "generated" changes: - addForeignKeyConstraint: @@ -236,7 +283,7 @@ databaseChangeLog: referencedTableName: "emails" validate: true - changeSet: - id: "initial-0013" + id: "blocks-0001" author: "generated" changes: - createTable: @@ -260,7 +307,7 @@ databaseChangeLog: type: "UUID" tableName: "blocks" - changeSet: - id: "initial-0014" + id: "blocks-0002" author: "generated" changes: - addUniqueConstraint: @@ -268,7 +315,7 @@ databaseChangeLog: constraintName: "ukou6l1m15jrlhofu7hn5ueytw" tableName: "blocks" - changeSet: - id: "initial-0015" + id: "blocks-0003" author: "generated" changes: - addForeignKeyConstraint: @@ -283,7 +330,7 @@ databaseChangeLog: referencedTableName: "users" validate: true - changeSet: - id: "initial-0016" + id: "blocks-0004" author: "generated" changes: - addForeignKeyConstraint: @@ -298,7 +345,7 @@ databaseChangeLog: referencedTableName: "users" validate: true - changeSet: - id: "initial-0017" + id: "push_notification_tokens-0001" author: "generated" changes: - createTable: @@ -327,7 +374,7 @@ databaseChangeLog: type: "VARCHAR(255)" tableName: "push_notification_tokens" - changeSet: - id: "initial-0018" + id: "push_notification_tokens-0002" author: "generated" changes: - addForeignKeyConstraint: @@ -342,7 +389,7 @@ databaseChangeLog: referencedTableName: "users" validate: true - changeSet: - id: "initial-0019" + id: "reports-0001" author: "generated" changes: - createTable: @@ -376,7 +423,7 @@ databaseChangeLog: type: "VARCHAR(255)" tableName: "reports" - changeSet: - id: "initial-0020" + id: "reports-0002" author: "generated" changes: - addUniqueConstraint: @@ -384,7 +431,7 @@ databaseChangeLog: constraintName: "uka65rlbm4g84192wjr0gidyos1" tableName: "reports" - changeSet: - id: "initial-0021" + id: "reports-0003" author: "generated" changes: - addForeignKeyConstraint: @@ -399,7 +446,7 @@ databaseChangeLog: referencedTableName: "users" validate: true - changeSet: - id: "initial-0022" + id: "posts-0001" author: "generated" changes: - createTable: @@ -448,7 +495,7 @@ databaseChangeLog: type: "UUID" tableName: "posts" - changeSet: - id: "initial-0023" + id: "posts-0002" author: "generated" changes: - addForeignKeyConstraint: @@ -463,7 +510,7 @@ databaseChangeLog: referencedTableName: "users" validate: true - changeSet: - id: "initial-0024" + id: "chapters-0001" author: "generated" changes: - createTable: @@ -505,7 +552,7 @@ databaseChangeLog: type: "VARCHAR(500)" tableName: "chapters" - changeSet: - id: "initial-0025" + id: "chapters-0002" author: "generated" changes: - addUniqueConstraint: @@ -513,7 +560,7 @@ databaseChangeLog: constraintName: "chapters_image_id_key" tableName: "chapters" - changeSet: - id: "initial-0026" + id: "chapters-0003" author: "generated" changes: - addUniqueConstraint: @@ -521,7 +568,7 @@ databaseChangeLog: constraintName: "ukqj1py5nfact1vlvjgtflawowy" tableName: "chapters" - changeSet: - id: "initial-0027" + id: "chapters-0004" author: "generated" changes: - addForeignKeyConstraint: @@ -536,7 +583,7 @@ databaseChangeLog: referencedTableName: "stored_files" validate: true - changeSet: - id: "initial-0028" + id: "chapters-0005" author: "generated" changes: - addForeignKeyConstraint: @@ -551,7 +598,7 @@ databaseChangeLog: referencedTableName: "posts" validate: true - changeSet: - id: "initial-0029" + id: "comments-0001" author: "generated" changes: - createTable: @@ -600,7 +647,7 @@ databaseChangeLog: type: "VARCHAR(1500)" tableName: "comments" - changeSet: - id: "initial-0030" + id: "comments-0002" author: "generated" changes: - createIndex: @@ -610,7 +657,7 @@ databaseChangeLog: indexName: "idx2ocgo3lfadb3wq0tx8wyt7sj2" tableName: "comments" - changeSet: - id: "initial-0031" + id: "comments-0003" author: "generated" changes: - addForeignKeyConstraint: @@ -625,7 +672,7 @@ databaseChangeLog: referencedTableName: "users" validate: true - changeSet: - id: "initial-0032" + id: "comments-0004" author: "generated" changes: - addForeignKeyConstraint: @@ -640,7 +687,7 @@ databaseChangeLog: referencedTableName: "posts" validate: true - changeSet: - id: "initial-0033" + id: "subscriptions-0001" author: "generated" changes: - createTable: @@ -672,7 +719,7 @@ databaseChangeLog: type: "UUID" tableName: "subscriptions" - changeSet: - id: "initial-0034" + id: "subscriptions-0002" author: "generated" changes: - addUniqueConstraint: @@ -680,7 +727,7 @@ databaseChangeLog: constraintName: "ukjvj9d1ro9lw17oe2pyl9bonm4" tableName: "subscriptions" - changeSet: - id: "initial-0035" + id: "subscriptions-0003" author: "generated" changes: - addForeignKeyConstraint: @@ -695,7 +742,7 @@ databaseChangeLog: referencedTableName: "comments" validate: true - changeSet: - id: "initial-0036" + id: "subscriptions-0004" author: "generated" changes: - addForeignKeyConstraint: @@ -710,7 +757,7 @@ databaseChangeLog: referencedTableName: "posts" validate: true - changeSet: - id: "initial-0037" + id: "subscriptions-0005" author: "generated" changes: - addForeignKeyConstraint: @@ -725,7 +772,7 @@ databaseChangeLog: referencedTableName: "users" validate: true - changeSet: - id: "initial-0038" + id: "votes-0001" author: "generated" changes: - createTable: @@ -754,7 +801,7 @@ databaseChangeLog: type: "UUID" tableName: "votes" - changeSet: - id: "initial-0039" + id: "votes-0002" author: "generated" changes: - createIndex: @@ -764,7 +811,7 @@ databaseChangeLog: indexName: "idxsnowcffjecrw34fxm6h5fyah4" tableName: "votes" - changeSet: - id: "initial-0040" + id: "votes-0003" author: "generated" changes: - addUniqueConstraint: @@ -772,7 +819,7 @@ databaseChangeLog: constraintName: "ukpa0qu72klq223r3f3mpgf9ele" tableName: "votes" - changeSet: - id: "initial-0041" + id: "votes-0004" author: "generated" changes: - addForeignKeyConstraint: @@ -787,7 +834,7 @@ databaseChangeLog: referencedTableName: "posts" validate: true - changeSet: - id: "initial-0042" + id: "votes-0005" author: "generated" changes: - addForeignKeyConstraint: diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/dev/RetrievePasswordHashTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/dev/RetrievePasswordHashTests.java new file mode 100644 index 0000000..75a04b5 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/dev/RetrievePasswordHashTests.java @@ -0,0 +1,20 @@ +package app.fyreplace.api.testing.endpoints.dev; + +import static io.restassured.RestAssured.given; + +import app.fyreplace.api.endpoints.DevEndpoint; +import app.fyreplace.api.testing.UserTestsBase; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(DevEndpoint.class) +public final class RetrievePasswordHashTests extends UserTestsBase { + @Test + @TestSecurity(user = "user_0") + public void retrieveToken() { + given().get("passwords/hello/hash").then().statusCode(403); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/dev/RetrieveUserTokenTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/dev/RetrieveUserTokenTests.java new file mode 100644 index 0000000..679b7d4 --- /dev/null +++ b/src/test/java/app/fyreplace/api/testing/endpoints/dev/RetrieveUserTokenTests.java @@ -0,0 +1,20 @@ +package app.fyreplace.api.testing.endpoints.dev; + +import static io.restassured.RestAssured.given; + +import app.fyreplace.api.endpoints.DevEndpoint; +import app.fyreplace.api.testing.UserTestsBase; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(DevEndpoint.class) +public final class RetrieveUserTokenTests extends UserTestsBase { + @Test + @TestSecurity(user = "user_0") + public void retrieveToken() { + given().get("user_0/token").then().statusCode(403); + } +} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/dev/users/RetrieveTokenTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/dev/users/RetrieveTokenTests.java deleted file mode 100644 index d9ea000..0000000 --- a/src/test/java/app/fyreplace/api/testing/endpoints/dev/users/RetrieveTokenTests.java +++ /dev/null @@ -1,18 +0,0 @@ -package app.fyreplace.api.testing.endpoints.dev.users; - -import static io.restassured.RestAssured.given; - -import app.fyreplace.api.endpoints.DevUsersEndpoint; -import app.fyreplace.api.testing.UserTestsBase; -import io.quarkus.test.common.http.TestHTTPEndpoint; -import io.quarkus.test.junit.QuarkusTest; -import org.junit.jupiter.api.Test; - -@QuarkusTest -@TestHTTPEndpoint(DevUsersEndpoint.class) -public final class RetrieveTokenTests extends UserTestsBase { - @Test - public void retrieveToken() { - given().get("user_0/token").then().statusCode(401); - } -} diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java index 43ad705..bd2bee6 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java @@ -8,12 +8,14 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import app.fyreplace.api.data.Email; +import app.fyreplace.api.data.Password; import app.fyreplace.api.data.RandomCode; import app.fyreplace.api.data.TokenCreation; import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.TokensEndpoint; import app.fyreplace.api.services.RandomService; import app.fyreplace.api.testing.UserTestsBase; +import io.quarkus.elytron.security.common.BcryptUtil; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.restassured.http.ContentType; @@ -31,6 +33,7 @@ public final class CreateTests extends UserTestsBase { private RandomCode normalUserRandomCode; private RandomCode otherNormalUserRandomCode; private RandomCode newUserRandomCode; + private Password password; @Test public void createWithUsername() { @@ -90,6 +93,22 @@ public void createWithNewEmail() { assertTrue(email.verified); } + @Test + public void createWithPassword() { + final var randomCodeCount = RandomCode.count(); + assertFalse(password.user.mainEmail.verified); + given().contentType(ContentType.JSON) + .body(new TokenCreation(password.user.mainEmail.email, "password")) + .post() + .then() + .statusCode(201) + .contentType(ContentType.TEXT) + .body(isA(String.class)); + assertEquals(randomCodeCount, RandomCode.count()); + final var email = Email.find("id", password.user.mainEmail.id).firstResult(); + assertTrue(email.verified); + } + @Test public void createWithInvalidUsername() { given().contentType(ContentType.JSON) @@ -100,7 +119,7 @@ public void createWithInvalidUsername() { } @Test - public void createWithInvalidCode() { + public void createWithInvalidSecret() { given().contentType(ContentType.JSON) .body(new TokenCreation(normalUserRandomCode.email.user.username, "bad")) .post() @@ -137,6 +156,10 @@ public void beforeEach() { normalUserRandomCode = makeRandomCode("user_0"); otherNormalUserRandomCode = makeRandomCode("user_1"); newUserRandomCode = makeRandomCode("user_inactive_0"); + password = new Password(); + password.user = User.findByUsername("user_inactive_1"); + password.password = BcryptUtil.bcryptHash("password"); + password.persist(); } private RandomCode makeRandomCode(final String username) { From f939e4141ad2c5da4db2d7d189cc5ca50c529899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Thu, 9 Nov 2023 19:07:20 +0100 Subject: [PATCH 108/157] Simplify post listing --- .../api/endpoints/PostsEndpoint.java | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java index f6d4671..8eea9f9 100644 --- a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java @@ -60,28 +60,16 @@ public Iterable list( @QueryParam("type") @NotNull final PostListingType type) { final var user = User.getFromSecurityContext(context); final var direction = ascending ? Direction.Ascending : Direction.Descending; - final var basicSort = Post.sorting().direction(direction); - - final var stream = + final var sorting = Post.sorting().direction(direction); + final var query = switch (type) { - case SUBSCRIBED_TO -> Subscription.find( - "user = ?1 and post.deleted = false", - Sort.by("post.dateCreated", "post.id").direction(direction), - user) - .page(page, pagingSize) - .stream() - .map(s -> s.post); - case PUBLISHED -> Post.find("author = ?1 and published = true", basicSort, user) - .filter("existing") - .page(page, pagingSize) - .stream(); - case DRAFTS -> Post.find("author = ?1 and published = false", basicSort, user) - .filter("existing") - .page(page, pagingSize) - .stream(); + case SUBSCRIBED_TO -> "from Post p where (select count(*) from Subscription where user = ?1 and post.id = p.id) > 0"; + case PUBLISHED -> "author = ?1 and published = true"; + case DRAFTS -> "author = ?1 and published = false"; }; - try (stream) { + try (final var stream = + Post.find(query, sorting, user).filter("existing").page(page, pagingSize).stream()) { return stream.peek(p -> p.setCurrentUser(user)).toList(); } } From fe88249f389ca1ae7c106baba323fde72fc37f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 25 Nov 2023 17:10:00 +0100 Subject: [PATCH 109/157] Update dependencies --- gradle.properties | 10 +++++----- gradle/wrapper/gradle-wrapper.properties | 2 +- src/main/java/app/fyreplace/api/emails/EmailBase.java | 2 -- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/gradle.properties b/gradle.properties index 3e11d7e..c1048d0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,10 +1,10 @@ org.gradle.jvmargs=-Xmx1024M -quarkusVersion=3.5.0 -quarkusAmazonVersion=2.5.2 -sentryVersion=6.32.0 +quarkusVersion=3.6.3 +quarkusAmazonVersion=2.7.2 +sentryVersion=7.0.0 tikaVersion=2.9.1 gitPluginVersion=3.0.0 -spotlessPluginVersion=6.22.0 +spotlessPluginVersion=6.23.3 lombokPluginVersion=8.4 -sentryPluginVersion=3.14.0 +sentryPluginVersion=4.0.0 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3fa8f86..1af9e09 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/java/app/fyreplace/api/emails/EmailBase.java b/src/main/java/app/fyreplace/api/emails/EmailBase.java index bedc277..0dc6c36 100644 --- a/src/main/java/app/fyreplace/api/emails/EmailBase.java +++ b/src/main/java/app/fyreplace/api/emails/EmailBase.java @@ -7,7 +7,6 @@ import io.quarkus.mailer.Mail; import io.quarkus.mailer.Mailer; import io.quarkus.qute.TemplateInstance; -import io.smallrye.common.annotation.Blocking; import jakarta.inject.Inject; import java.net.URI; import java.util.List; @@ -40,7 +39,6 @@ public abstract class EmailBase extends Mail { protected abstract TemplateInstance htmlTemplate(); - @Blocking public void sendTo(final Email email) { this.email = email; mailer.send(this.setSubject(getResourceBundle().getString("subject")) From 7a83f2e2311a68e3809012bfea93beb4b1fa49c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 17 Dec 2023 12:21:25 +0100 Subject: [PATCH 110/157] Update Java --- .github/workflows/validation.yml | 4 ++-- Dockerfile | 4 ++-- build.gradle | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 20c3a10..49fe1b9 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/setup-java@v3 with: distribution: temurin - java-version: '17' + java-version: '21' cache: gradle - name: Run Spotless @@ -35,7 +35,7 @@ jobs: - uses: actions/setup-java@v3 with: distribution: temurin - java-version: '17' + java-version: '21' cache: gradle - name: Run tests diff --git a/Dockerfile b/Dockerfile index 271c7df..e5ab407 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ COPY . ./ RUN make emails -FROM eclipse-temurin:17-jdk AS build-code +FROM eclipse-temurin:21-jdk AS build-code RUN apt-get update && apt-get install -y git WORKDIR /app @@ -17,7 +17,7 @@ RUN git fetch --unshallow || echo "Nothing to do" RUN ./gradlew --no-daemon --exclude-task test build -FROM eclipse-temurin:17-jre AS run +FROM eclipse-temurin:21-jre AS run ENV LANGUAGE="en_US:en" ENV JAVA_OPTS="$JAVA_OPTS -Dquarkus.http.host=0.0.0.0" diff --git a/build.gradle b/build.gradle index 97a2eeb..a0efc87 100644 --- a/build.gradle +++ b/build.gradle @@ -61,8 +61,8 @@ sentry { } java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } test { From 6145edc5b7764d7a5f16b5bc90e56e2b7091252a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 19 Jan 2024 12:16:45 +0100 Subject: [PATCH 111/157] Make Chapters comparable --- src/main/java/app/fyreplace/api/data/Chapter.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/app/fyreplace/api/data/Chapter.java b/src/main/java/app/fyreplace/api/data/Chapter.java index a7423aa..dd657bc 100644 --- a/src/main/java/app/fyreplace/api/data/Chapter.java +++ b/src/main/java/app/fyreplace/api/data/Chapter.java @@ -17,7 +17,7 @@ @Entity @Table(name = "chapters", uniqueConstraints = @UniqueConstraint(columnNames = {"post_id", "position"})) -public class Chapter extends EntityBase { +public class Chapter extends EntityBase implements Comparable { @ManyToOne(optional = false) @OnDelete(action = OnDeleteAction.CASCADE) @JsonIgnore @@ -42,6 +42,11 @@ public class Chapter extends EntityBase { @Column(nullable = false) public int height = 0; + @Override + public int compareTo(final Chapter other) { + return position.compareTo(other.position); + } + @SuppressWarnings("unused") @PostRemove final void postRemove() { From 953c27aec0f9c49d976fd40dd5fb53b8ae99aaa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 19 Jan 2024 12:16:56 +0100 Subject: [PATCH 112/157] Update dependencies --- gradle.properties | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gradle.properties b/gradle.properties index c1048d0..8dd3321 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,10 +1,10 @@ org.gradle.jvmargs=-Xmx1024M -quarkusVersion=3.6.3 -quarkusAmazonVersion=2.7.2 -sentryVersion=7.0.0 +quarkusVersion=3.6.6 +quarkusAmazonVersion=2.10.1 +sentryVersion=7.2.0 tikaVersion=2.9.1 gitPluginVersion=3.0.0 -spotlessPluginVersion=6.23.3 +spotlessPluginVersion=6.24.0 lombokPluginVersion=8.4 -sentryPluginVersion=4.0.0 +sentryPluginVersion=4.2.0 From d6e56b9080c4fab7440990a8deb088b594bbf5c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 27 Jan 2024 17:50:22 +0100 Subject: [PATCH 113/157] Fix email generation --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 55c2c63..68284ea 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: emails keygen-rsa -emails: src/main/resources/templates/*/html.html +emails: src/main/resources/templates/*Email/html.html src/main/resources/templates/%/html.html: src/main/resources/templates/%/html.html.mjml src/main/resources/templates/emails/*.mjml npx mjml -c.minify=true $< -o $@ From 52cd39d398b0c49ddf1f5db770a9ddfe70454fd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 28 Jan 2024 11:41:05 +0100 Subject: [PATCH 114/157] Actually fix email generation --- Makefile | 4 ++-- .../templates/EmailVerificationEmail/html.html.mjml | 6 +++--- .../resources/templates/UserActivationEmail/html.html.mjml | 6 +++--- .../resources/templates/UserConnectionEmail/html.html.mjml | 6 +++--- src/main/resources/templates/{emails => }/_attributes.mjml | 0 .../resources/templates/{emails => }/_link_end_notice.mjml | 0 src/main/resources/templates/{emails => }/_logo.mjml | 0 7 files changed, 11 insertions(+), 11 deletions(-) rename src/main/resources/templates/{emails => }/_attributes.mjml (100%) rename src/main/resources/templates/{emails => }/_link_end_notice.mjml (100%) rename src/main/resources/templates/{emails => }/_logo.mjml (100%) diff --git a/Makefile b/Makefile index 68284ea..4abdc2e 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ .PHONY: emails keygen-rsa -emails: src/main/resources/templates/*Email/html.html +emails: src/main/resources/templates/*/html.html -src/main/resources/templates/%/html.html: src/main/resources/templates/%/html.html.mjml src/main/resources/templates/emails/*.mjml +src/main/resources/templates/%/html.html: src/main/resources/templates/%/html.html.mjml src/main/resources/templates/*.mjml npx mjml -c.minify=true $< -o $@ keygen-rsa: diff --git a/src/main/resources/templates/EmailVerificationEmail/html.html.mjml b/src/main/resources/templates/EmailVerificationEmail/html.html.mjml index 5140c85..fa16230 100644 --- a/src/main/resources/templates/EmailVerificationEmail/html.html.mjml +++ b/src/main/resources/templates/EmailVerificationEmail/html.html.mjml @@ -1,14 +1,14 @@ - + - + {res.getString("codeDescription")} {code} {res.getString("linkDescription")} {res.getString("button")} - + diff --git a/src/main/resources/templates/UserActivationEmail/html.html.mjml b/src/main/resources/templates/UserActivationEmail/html.html.mjml index f51f97b..f78224c 100644 --- a/src/main/resources/templates/UserActivationEmail/html.html.mjml +++ b/src/main/resources/templates/UserActivationEmail/html.html.mjml @@ -1,15 +1,15 @@ - + - + {res.getString("title").replace("$1", appName)} {res.getString("codeDescription")} {code} {res.getString("linkDescription")} {res.getString("button")} - + diff --git a/src/main/resources/templates/UserConnectionEmail/html.html.mjml b/src/main/resources/templates/UserConnectionEmail/html.html.mjml index 5140c85..fa16230 100644 --- a/src/main/resources/templates/UserConnectionEmail/html.html.mjml +++ b/src/main/resources/templates/UserConnectionEmail/html.html.mjml @@ -1,14 +1,14 @@ - + - + {res.getString("codeDescription")} {code} {res.getString("linkDescription")} {res.getString("button")} - + diff --git a/src/main/resources/templates/emails/_attributes.mjml b/src/main/resources/templates/_attributes.mjml similarity index 100% rename from src/main/resources/templates/emails/_attributes.mjml rename to src/main/resources/templates/_attributes.mjml diff --git a/src/main/resources/templates/emails/_link_end_notice.mjml b/src/main/resources/templates/_link_end_notice.mjml similarity index 100% rename from src/main/resources/templates/emails/_link_end_notice.mjml rename to src/main/resources/templates/_link_end_notice.mjml diff --git a/src/main/resources/templates/emails/_logo.mjml b/src/main/resources/templates/_logo.mjml similarity index 100% rename from src/main/resources/templates/emails/_logo.mjml rename to src/main/resources/templates/_logo.mjml From 6ecbe76c9c1a09a865fb591314d5515471094850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 28 Jan 2024 11:41:18 +0100 Subject: [PATCH 115/157] Update dependencies --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 8dd3321..8698538 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,10 +1,10 @@ org.gradle.jvmargs=-Xmx1024M -quarkusVersion=3.6.6 +quarkusVersion=3.6.7 quarkusAmazonVersion=2.10.1 sentryVersion=7.2.0 tikaVersion=2.9.1 gitPluginVersion=3.0.0 -spotlessPluginVersion=6.24.0 +spotlessPluginVersion=6.25.0 lombokPluginVersion=8.4 sentryPluginVersion=4.2.0 From ca1b77269b143454c5f0542e2c96a2e42b8ed4f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 28 Jan 2024 12:41:28 +0100 Subject: [PATCH 116/157] Remove Sentry Gradle plugin --- build.gradle | 11 ++--------- gradle.properties | 1 - 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/build.gradle b/build.gradle index a0efc87..3ef3bbf 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,6 @@ plugins { id "com.palantir.git-version" version "${gitPluginVersion}" id "com.diffplug.spotless" version "${spotlessPluginVersion}" id "io.freefair.lombok" version "${lombokPluginVersion}" - id "io.sentry.jvm.gradle" version "${sentryPluginVersion}" } group = "app.fyreplace" @@ -18,6 +17,7 @@ repositories { dependencies { implementation(enforcedPlatform("io.quarkus:quarkus-bom:${quarkusVersion}")) implementation(enforcedPlatform("io.quarkiverse.amazonservices:quarkus-amazon-services-bom:${quarkusAmazonVersion}")) + implementation(enforcedPlatform("io.sentry:sentry-bom:${sentryVersion}")) implementation(enforcedPlatform("org.apache.tika:tika-bom:${tikaVersion}")) implementation("io.quarkus:quarkus-arc") implementation("io.quarkus:quarkus-cache") @@ -39,6 +39,7 @@ dependencies { implementation("io.quarkus:quarkus-smallrye-jwt-build") implementation("io.quarkus:quarkus-smallrye-openapi") implementation("io.quarkiverse.amazonservices:quarkus-amazon-s3") + implementation("io.sentry:sentry-jul") implementation("org.apache.tika:tika-core") implementation("org.apache.tika:tika-parsers-standard-package") implementation("software.amazon.awssdk:url-connection-client") @@ -52,14 +53,6 @@ dependencies { testImplementation("org.apiguardian:apiguardian-api:+") } -sentry { - includeSourceContext = true - - autoInstallation { - enabled.set(true) - } -} - java { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 diff --git a/gradle.properties b/gradle.properties index 8698538..2319552 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,4 +7,3 @@ tikaVersion=2.9.1 gitPluginVersion=3.0.0 spotlessPluginVersion=6.25.0 lombokPluginVersion=8.4 -sentryPluginVersion=4.2.0 From cf8ef7b6f634d8664365f9c1f759c3b7fe1af929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 28 Jan 2024 13:58:33 +0100 Subject: [PATCH 117/157] Finally fix email generation --- Makefile | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 4abdc2e..d79085e 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,10 @@ .PHONY: emails keygen-rsa -emails: src/main/resources/templates/*/html.html - -src/main/resources/templates/%/html.html: src/main/resources/templates/%/html.html.mjml src/main/resources/templates/*.mjml - npx mjml -c.minify=true $< -o $@ +emails: + for email_path in src/main/resources/templates/*/html.html.mjml; \ + do \ + npx mjml -c.minify=true $$email_path -o $${email_path%.mjml}; \ + done keygen-rsa: openssl genrsa > src/main/resources/keys/jwt.rsa 2048 From 1bca2d724abeaa0c108583d204d6fd8fa0475a6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 28 Jan 2024 14:56:38 +0100 Subject: [PATCH 118/157] Provide build-time var --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index e5ab407..f3b3dd7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,9 @@ RUN make emails FROM eclipse-temurin:21-jdk AS build-code +ARG APP_STORAGE_TYPE +ENV APP_STORAGE_TYPE=$APP_STORAGE_TYPE + RUN apt-get update && apt-get install -y git WORKDIR /app From 0d03c65b074ef1b9bc586ac2041c490d8178f451 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 17 Feb 2024 13:04:32 +0100 Subject: [PATCH 119/157] Add Windows-compatible email build script --- .github/workflows/validation.yml | 5 ++++- build.gradle | 11 ----------- make.bat | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 12 deletions(-) create mode 100644 make.bat diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 49fe1b9..b9511eb 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -38,8 +38,11 @@ jobs: java-version: '21' cache: gradle + - name: Build emails + run: make emails + - name: Run tests - run: ./gradlew --no-daemon compileMjml test + run: ./gradlew --no-daemon test env: MP_JWT_VERIFY_PUBLICKEY: ${{ vars.MP_JWT_VERIFY_PUBLICKEY }} SMALLRYE_JWT_SIGN_KEY: ${{ secrets.SMALLRYE_JWT_SIGN_KEY }} diff --git a/build.gradle b/build.gradle index 3ef3bbf..d38c411 100644 --- a/build.gradle +++ b/build.gradle @@ -71,17 +71,6 @@ compileTestJava { options.encoding = "UTF-8" } -tasks.register("compileMjml") { - doLast { - fileTree("src/main/resources/templates").include("**/*.html.mjml").each { file -> - exec { - workingDir "$projectDir" - commandLine "npx", "mjml", "-r", file.getPath(), "-o", "${file.getPath().replace(".mjml", "")}" - } - } - } -} - spotless { java { importOrder() diff --git a/make.bat b/make.bat new file mode 100644 index 0000000..9693699 --- /dev/null +++ b/make.bat @@ -0,0 +1,16 @@ +@ECHO off +SETLOCAL EnableDelayedExpansion + +GOTO %1 + +:emails + FOR /R src\main\resources\templates %%G IN (*html.html.mjml) DO ( + npx mjml -c.minify=true "%%G" -o "%%~dpnG" + ) + GOTO :eof + +:keygen-rsa + openssl genrsa > src\main\resources\keys\jwt.rsa 2048 + openssl pkcs8 -topk8 -nocrypt -inform PEM -in src\main\resources\keys\jwt.rsa -outform PEM > src\main\resources\keys\jwt.rsa.pem + openssl rsa -in src\main\resources\keys\jwt.rsa -pubout > src\main\resources\keys\jwt.rsa.pub + GOTO :eof From a9f94a55d58bca81a2b76f11e7ba7da47bd11b0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 17 Feb 2024 13:56:44 +0100 Subject: [PATCH 120/157] Update dependencies --- build.gradle | 4 ++-- gradle.properties | 7 +++---- gradle/wrapper/gradle-wrapper.jar | Bin 63721 -> 43462 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew.bat | 20 ++++++++++---------- quarkus-sentry/build.gradle | 2 +- 6 files changed, 17 insertions(+), 18 deletions(-) diff --git a/build.gradle b/build.gradle index d38c411..d5ec2cf 100644 --- a/build.gradle +++ b/build.gradle @@ -15,8 +15,8 @@ repositories { } dependencies { - implementation(enforcedPlatform("io.quarkus:quarkus-bom:${quarkusVersion}")) - implementation(enforcedPlatform("io.quarkiverse.amazonservices:quarkus-amazon-services-bom:${quarkusAmazonVersion}")) + implementation(enforcedPlatform("io.quarkus.platform:quarkus-bom:${quarkusVersion}")) + implementation(enforcedPlatform("io.quarkus.platform:quarkus-amazon-services-bom:${quarkusVersion}")) implementation(enforcedPlatform("io.sentry:sentry-bom:${sentryVersion}")) implementation(enforcedPlatform("org.apache.tika:tika-bom:${tikaVersion}")) implementation("io.quarkus:quarkus-arc") diff --git a/gradle.properties b/gradle.properties index 2319552..52de7fe 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,8 @@ org.gradle.jvmargs=-Xmx1024M -quarkusVersion=3.6.7 -quarkusAmazonVersion=2.10.1 -sentryVersion=7.2.0 +quarkusVersion=3.7.3 +sentryVersion=7.4.0 tikaVersion=2.9.1 gitPluginVersion=3.0.0 spotlessPluginVersion=6.25.0 -lombokPluginVersion=8.4 +lombokPluginVersion=8.6 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135c49b765f8051ef9d0a6055ff8e46073d8..d64cd4917707c1f8861d8cb53dd15194d4248596 100644 GIT binary patch literal 43462 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 literal 63721 zcmb5Wb9gP!wgnp7wrv|bwr$&XvSZt}Z6`anZSUAlc9NHKf9JdJ;NJVr`=eI(_pMp0 zy1VAAG3FfAOI`{X1O)&90s;U4K;XLp008~hCjbEC_fbYfS%6kTR+JtXK>nW$ZR+`W ze|#J8f4A@M|F5BpfUJb5h>|j$jOe}0oE!`Zf6fM>CR?!y@zU(cL8NsKk`a z6tx5mAkdjD;J=LcJ;;Aw8p!v#ouk>mUDZF@ zK>yvw%+bKu+T{Nk@LZ;zkYy0HBKw06_IWcMHo*0HKpTsEFZhn5qCHH9j z)|XpN&{`!0a>Vl+PmdQc)Yg4A(AG-z!+@Q#eHr&g<9D?7E)_aEB?s_rx>UE9TUq|? z;(ggJt>9l?C|zoO@5)tu?EV0x_7T17q4fF-q3{yZ^ipUbKcRZ4Qftd!xO(#UGhb2y>?*@{xq%`(-`2T^vc=#< zx!+@4pRdk&*1ht2OWk^Z5IAQ0YTAXLkL{(D*$gENaD)7A%^XXrCchN&z2x+*>o2FwPFjWpeaL=!tzv#JOW#( z$B)Nel<+$bkH1KZv3&-}=SiG~w2sbDbAWarg%5>YbC|}*d9hBjBkR(@tyM0T)FO$# zPtRXukGPnOd)~z=?avu+4Co@wF}1T)-uh5jI<1$HLtyDrVak{gw`mcH@Q-@wg{v^c zRzu}hMKFHV<8w}o*yg6p@Sq%=gkd~;`_VGTS?L@yVu`xuGy+dH6YOwcP6ZE`_0rK% zAx5!FjDuss`FQ3eF|mhrWkjux(Pny^k$u_)dyCSEbAsecHsq#8B3n3kDU(zW5yE|( zgc>sFQywFj5}U*qtF9Y(bi*;>B7WJykcAXF86@)z|0-Vm@jt!EPoLA6>r)?@DIobIZ5Sx zsc@OC{b|3%vaMbyeM|O^UxEYlEMHK4r)V-{r)_yz`w1*xV0|lh-LQOP`OP`Pk1aW( z8DSlGN>Ts|n*xj+%If~+E_BxK)~5T#w6Q1WEKt{!Xtbd`J;`2a>8boRo;7u2M&iOop4qcy<)z023=oghSFV zST;?S;ye+dRQe>ygiJ6HCv4;~3DHtJ({fWeE~$H@mKn@Oh6Z(_sO>01JwH5oA4nvK zr5Sr^g+LC zLt(i&ecdmqsIJGNOSUyUpglvhhrY8lGkzO=0USEKNL%8zHshS>Qziu|`eyWP^5xL4 zRP122_dCJl>hZc~?58w~>`P_s18VoU|7(|Eit0-lZRgLTZKNq5{k zE?V=`7=R&ro(X%LTS*f+#H-mGo_j3dm@F_krAYegDLk6UV{`UKE;{YSsn$ z(yz{v1@p|p!0>g04!eRSrSVb>MQYPr8_MA|MpoGzqyd*$@4j|)cD_%^Hrd>SorF>@ zBX+V<@vEB5PRLGR(uP9&U&5=(HVc?6B58NJT_igiAH*q~Wb`dDZpJSKfy5#Aag4IX zj~uv74EQ_Q_1qaXWI!7Vf@ZrdUhZFE;L&P_Xr8l@GMkhc#=plV0+g(ki>+7fO%?Jb zl+bTy7q{w^pTb{>(Xf2q1BVdq?#f=!geqssXp z4pMu*q;iiHmA*IjOj4`4S&|8@gSw*^{|PT}Aw~}ZXU`6=vZB=GGeMm}V6W46|pU&58~P+?LUs%n@J}CSrICkeng6YJ^M? zS(W?K4nOtoBe4tvBXs@@`i?4G$S2W&;$z8VBSM;Mn9 zxcaEiQ9=vS|bIJ>*tf9AH~m&U%2+Dim<)E=}KORp+cZ^!@wI`h1NVBXu{@%hB2Cq(dXx_aQ9x3mr*fwL5!ZryQqi|KFJuzvP zK1)nrKZ7U+B{1ZmJub?4)Ln^J6k!i0t~VO#=q1{?T)%OV?MN}k5M{}vjyZu#M0_*u z8jwZKJ#Df~1jcLXZL7bnCEhB6IzQZ-GcoQJ!16I*39iazoVGugcKA{lhiHg4Ta2fD zk1Utyc5%QzZ$s3;p0N+N8VX{sd!~l*Ta3|t>lhI&G`sr6L~G5Lul`>m z{!^INm?J|&7X=;{XveF!(b*=?9NAp4y&r&N3(GKcW4rS(Ejk|Lzs1PrxPI_owB-`H zg3(Rruh^&)`TKA6+_!n>RdI6pw>Vt1_j&+bKIaMTYLiqhZ#y_=J8`TK{Jd<7l9&sY z^^`hmi7^14s16B6)1O;vJWOF$=$B5ONW;;2&|pUvJlmeUS&F;DbSHCrEb0QBDR|my zIs+pE0Y^`qJTyH-_mP=)Y+u^LHcuZhsM3+P||?+W#V!_6E-8boP#R-*na4!o-Q1 zVthtYhK{mDhF(&7Okzo9dTi03X(AE{8cH$JIg%MEQca`S zy@8{Fjft~~BdzWC(di#X{ny;!yYGK9b@=b|zcKZ{vv4D8i+`ilOPl;PJl{!&5-0!w z^fOl#|}vVg%=n)@_e1BrP)`A zKPgs`O0EO}Y2KWLuo`iGaKu1k#YR6BMySxQf2V++Wo{6EHmK>A~Q5o73yM z-RbxC7Qdh0Cz!nG+7BRZE>~FLI-?&W_rJUl-8FDIaXoNBL)@1hwKa^wOr1($*5h~T zF;%f^%<$p8Y_yu(JEg=c_O!aZ#)Gjh$n(hfJAp$C2he555W5zdrBqjFmo|VY+el;o z=*D_w|GXG|p0**hQ7~9-n|y5k%B}TAF0iarDM!q-jYbR^us(>&y;n^2l0C%@2B}KM zyeRT9)oMt97Agvc4sEKUEy%MpXr2vz*lb zh*L}}iG>-pqDRw7ud{=FvTD?}xjD)w{`KzjNom-$jS^;iw0+7nXSnt1R@G|VqoRhE%12nm+PH?9`(4rM0kfrZzIK9JU=^$YNyLvAIoxl#Q)xxDz!^0@zZ zSCs$nfcxK_vRYM34O<1}QHZ|hp4`ioX3x8(UV(FU$J@o%tw3t4k1QPmlEpZa2IujG&(roX_q*%e`Hq|);0;@k z0z=fZiFckp#JzW0p+2A+D$PC~IsakhJJkG(c;CqAgFfU0Z`u$PzG~-9I1oPHrCw&)@s^Dc~^)#HPW0Ra}J^=|h7Fs*<8|b13ZzG6MP*Q1dkoZ6&A^!}|hbjM{2HpqlSXv_UUg1U4gn z3Q)2VjU^ti1myodv+tjhSZp%D978m~p& z43uZUrraHs80Mq&vcetqfQpQP?m!CFj)44t8Z}k`E798wxg&~aCm+DBoI+nKq}&j^ zlPY3W$)K;KtEajks1`G?-@me7C>{PiiBu+41#yU_c(dITaqE?IQ(DBu+c^Ux!>pCj zLC|HJGU*v+!it1(;3e`6igkH(VA)-S+k(*yqxMgUah3$@C zz`7hEM47xr>j8^g`%*f=6S5n>z%Bt_Fg{Tvmr+MIsCx=0gsu_sF`q2hlkEmisz#Fy zj_0;zUWr;Gz}$BS%Y`meb(=$d%@Crs(OoJ|}m#<7=-A~PQbyN$x%2iXP2@e*nO0b7AwfH8cCUa*Wfu@b)D_>I*%uE4O3 z(lfnB`-Xf*LfC)E}e?%X2kK7DItK6Tf<+M^mX0Ijf_!IP>7c8IZX%8_#0060P{QMuV^B9i<^E`_Qf0pv9(P%_s8D`qvDE9LK9u-jB}J2S`(mCO&XHTS04Z5Ez*vl^T%!^$~EH8M-UdwhegL>3IQ*)(MtuH2Xt1p!fS4o~*rR?WLxlA!sjc2(O znjJn~wQ!Fp9s2e^IWP1C<4%sFF}T4omr}7+4asciyo3DntTgWIzhQpQirM$9{EbQd z3jz9vS@{aOqTQHI|l#aUV@2Q^Wko4T0T04Me4!2nsdrA8QY1%fnAYb~d2GDz@lAtfcHq(P7 zaMBAGo}+NcE-K*@9y;Vt3*(aCaMKXBB*BJcD_Qnxpt75r?GeAQ}*|>pYJE=uZb73 zC>sv)18)q#EGrTG6io*}JLuB_jP3AU1Uiu$D7r|2_zlIGb9 zjhst#ni)Y`$)!fc#reM*$~iaYoz~_Cy7J3ZTiPm)E?%`fbk`3Tu-F#`{i!l5pNEn5 zO-Tw-=TojYhzT{J=?SZj=Z8#|eoF>434b-DXiUsignxXNaR3 zm_}4iWU$gt2Mw5NvZ5(VpF`?X*f2UZDs1TEa1oZCif?Jdgr{>O~7}-$|BZ7I(IKW`{f;@|IZFX*R8&iT= zoWstN8&R;}@2Ka%d3vrLtR|O??ben;k8QbS-WB0VgiCz;<$pBmIZdN!aalyCSEm)crpS9dcD^Y@XT1a3+zpi-`D}e#HV<} z$Y(G&o~PvL-xSVD5D?JqF3?B9rxGWeb=oEGJ3vRp5xfBPlngh1O$yI95EL+T8{GC@ z98i1H9KhZGFl|;`)_=QpM6H?eDPpw~^(aFQWwyXZ8_EEE4#@QeT_URray*mEOGsGc z6|sdXtq!hVZo=d#+9^@lm&L5|q&-GDCyUx#YQiccq;spOBe3V+VKdjJA=IL=Zn%P} zNk=_8u}VhzFf{UYZV0`lUwcD&)9AFx0@Fc6LD9A6Rd1=ga>Mi0)_QxM2ddCVRmZ0d z+J=uXc(?5JLX3=)e)Jm$HS2yF`44IKhwRnm2*669_J=2LlwuF5$1tAo@ROSU@-y+;Foy2IEl2^V1N;fk~YR z?&EP8#t&m0B=?aJeuz~lHjAzRBX>&x=A;gIvb>MD{XEV zV%l-+9N-)i;YH%nKP?>f`=?#`>B(`*t`aiPLoQM(a6(qs4p5KFjDBN?8JGrf3z8>= zi7sD)c)Nm~x{e<^jy4nTx${P~cwz_*a>%0_;ULou3kHCAD7EYkw@l$8TN#LO9jC( z1BeFW`k+bu5e8Ns^a8dPcjEVHM;r6UX+cN=Uy7HU)j-myRU0wHd$A1fNI~`4;I~`zC)3ul#8#^rXVSO*m}Ag>c%_;nj=Nv$rCZ z*~L@C@OZg%Q^m)lc-kcX&a*a5`y&DaRxh6O*dfhLfF+fU5wKs(1v*!TkZidw*)YBP za@r`3+^IHRFeO%!ai%rxy;R;;V^Fr=OJlpBX;(b*3+SIw}7= zIq$*Thr(Zft-RlY)D3e8V;BmD&HOfX+E$H#Y@B3?UL5L~_fA-@*IB-!gItK7PIgG9 zgWuGZK_nuZjHVT_Fv(XxtU%)58;W39vzTI2n&)&4Dmq7&JX6G>XFaAR{7_3QB6zsT z?$L8c*WdN~nZGiscY%5KljQARN;`w$gho=p006z;n(qIQ*Zu<``TMO3n0{ARL@gYh zoRwS*|Niw~cR!?hE{m*y@F`1)vx-JRfqET=dJ5_(076st(=lFfjtKHoYg`k3oNmo_ zNbQEw8&sO5jAYmkD|Zaz_yUb0rC})U!rCHOl}JhbYIDLzLvrZVw0~JO`d*6f;X&?V=#T@ND*cv^I;`sFeq4 z##H5;gpZTb^0Hz@3C*~u0AqqNZ-r%rN3KD~%Gw`0XsIq$(^MEb<~H(2*5G^<2(*aI z%7}WB+TRlMIrEK#s0 z93xn*Ohb=kWFc)BNHG4I(~RPn-R8#0lqyBBz5OM6o5|>x9LK@%HaM}}Y5goCQRt2C z{j*2TtT4ne!Z}vh89mjwiSXG=%DURar~=kGNNaO_+Nkb+tRi~Rkf!7a$*QlavziD( z83s4GmQ^Wf*0Bd04f#0HX@ua_d8 z23~z*53ePD6@xwZ(vdl0DLc=>cPIOPOdca&MyR^jhhKrdQO?_jJh`xV3GKz&2lvP8 zEOwW6L*ufvK;TN{=S&R@pzV^U=QNk^Ec}5H z+2~JvEVA{`uMAr)?Kf|aW>33`)UL@bnfIUQc~L;TsTQ6>r-<^rB8uoNOJ>HWgqMI8 zSW}pZmp_;z_2O5_RD|fGyTxaxk53Hg_3Khc<8AUzV|ZeK{fp|Ne933=1&_^Dbv5^u zB9n=*)k*tjHDRJ@$bp9mrh}qFn*s}npMl5BMDC%Hs0M0g-hW~P*3CNG06G!MOPEQ_ zi}Qs-6M8aMt;sL$vlmVBR^+Ry<64jrm1EI1%#j?c?4b*7>)a{aDw#TfTYKq+SjEFA z(aJ&z_0?0JB83D-i3Vh+o|XV4UP+YJ$9Boid2^M2en@APw&wx7vU~t$r2V`F|7Qfo z>WKgI@eNBZ-+Og<{u2ZiG%>YvH2L3fNpV9J;WLJoBZda)01Rn;o@){01{7E#ke(7U zHK>S#qZ(N=aoae*4X!0A{)nu0R_sKpi1{)u>GVjC+b5Jyl6#AoQ-1_3UDovNSo`T> z?c-@7XX*2GMy?k?{g)7?Sv;SJkmxYPJPs!&QqB12ejq`Lee^-cDveVWL^CTUldb(G zjDGe(O4P=S{4fF=#~oAu>LG>wrU^z_?3yt24FOx>}{^lCGh8?vtvY$^hbZ)9I0E3r3NOlb9I?F-Yc=r$*~l`4N^xzlV~N zl~#oc>U)Yjl0BxV>O*Kr@lKT{Z09OXt2GlvE38nfs+DD7exl|&vT;)>VFXJVZp9Np zDK}aO;R3~ag$X*|hRVY3OPax|PG`@_ESc8E!mHRByJbZQRS38V2F__7MW~sgh!a>98Q2%lUNFO=^xU52|?D=IK#QjwBky-C>zOWlsiiM&1n z;!&1((Xn1$9K}xabq~222gYvx3hnZPg}VMF_GV~5ocE=-v>V=T&RsLBo&`)DOyIj* zLV{h)JU_y*7SdRtDajP_Y+rBkNN*1_TXiKwHH2&p51d(#zv~s#HwbNy?<+(=9WBvo zw2hkk2Dj%kTFhY+$T+W-b7@qD!bkfN#Z2ng@Pd=i3-i?xYfs5Z*1hO?kd7Sp^9`;Y zM2jeGg<-nJD1er@Pc_cSY7wo5dzQX44=%6rn}P_SRbpzsA{6B+!$3B0#;}qwO37G^ zL(V_5JK`XT?OHVk|{_$vQ|oNEpab*BO4F zUTNQ7RUhnRsU`TK#~`)$icsvKh~(pl=3p6m98@k3P#~upd=k*u20SNcb{l^1rUa)>qO997)pYRWMncC8A&&MHlbW?7i^7M`+B$hH~Y|J zd>FYOGQ;j>Zc2e7R{KK7)0>>nn_jYJy&o@sK!4G>-rLKM8Hv)f;hi1D2fAc$+six2 zyVZ@wZ6x|fJ!4KrpCJY=!Mq0;)X)OoS~{Lkh6u8J`eK%u0WtKh6B>GW_)PVc zl}-k`p09qwGtZ@VbYJC!>29V?Dr>>vk?)o(x?!z*9DJ||9qG-&G~#kXxbw{KKYy}J zQKa-dPt~M~E}V?PhW0R26xdA%1T*%ra6SguGu50YHngOTIv)@N|YttEXo#OZfgtP7;H?EeZZxo<}3YlYxtBq znJ!WFR^tmGf0Py}N?kZ(#=VtpC@%xJkDmfcCoBTxq zr_|5gP?u1@vJZbxPZ|G0AW4=tpb84gM2DpJU||(b8kMOV1S3|(yuwZJ&rIiFW(U;5 zUtAW`O6F6Zy+eZ1EDuP~AAHlSY-+A_eI5Gx)%*uro5tljy}kCZU*_d7)oJ>oQSZ3* zneTn`{gnNC&uJd)0aMBzAg021?YJ~b(fmkwZAd696a=0NzBAqBN54KuNDwa*no(^O z6p05bioXUR^uXjpTol*ppHp%1v9e)vkoUAUJyBx3lw0UO39b0?^{}yb!$yca(@DUn zCquRF?t=Zb9`Ed3AI6|L{eX~ijVH`VzSMheKoP7LSSf4g>md>`yi!TkoG5P>Ofp+n z(v~rW+(5L96L{vBb^g51B=(o)?%%xhvT*A5btOpw(TKh^g^4c zw>0%X!_0`{iN%RbVk+A^f{w-4-SSf*fu@FhruNL##F~sF24O~u zyYF<3el2b$$wZ_|uW#@Ak+VAGk#e|kS8nL1g>2B-SNMjMp^8;-FfeofY2fphFHO!{ z*!o4oTb{4e;S<|JEs<1_hPsmAlVNk?_5-Fp5KKU&d#FiNW~Y+pVFk@Cua1I{T+1|+ zHx6rFMor)7L)krbilqsWwy@T+g3DiH5MyVf8Wy}XbEaoFIDr~y;@r&I>FMW{ z?Q+(IgyebZ)-i4jNoXQhq4Muy9Fv+OxU;9_Jmn+<`mEC#%2Q_2bpcgzcinygNI!&^ z=V$)o2&Yz04~+&pPWWn`rrWxJ&}8khR)6B(--!9Q zubo}h+1T)>a@c)H^i``@<^j?|r4*{;tQf78(xn0g39IoZw0(CwY1f<%F>kEaJ zp9u|IeMY5mRdAlw*+gSN^5$Q)ShM<~E=(c8QM+T-Qk)FyKz#Sw0EJ*edYcuOtO#~Cx^(M7w5 z3)rl#L)rF|(Vun2LkFr!rg8Q@=r>9p>(t3Gf_auiJ2Xx9HmxYTa|=MH_SUlYL`mz9 zTTS$`%;D-|Jt}AP1&k7PcnfFNTH0A-*FmxstjBDiZX?}%u%Yq94$fUT&z6od+(Uk> zuqsld#G(b$G8tus=M!N#oPd|PVFX)?M?tCD0tS%2IGTfh}3YA3f&UM)W$_GNV8 zQo+a(ml2Km4o6O%gKTCSDNq+#zCTIQ1*`TIJh~k6Gp;htHBFnne))rlFdGqwC6dx2+La1&Mnko*352k0y z+tQcwndQlX`nc6nb$A9?<-o|r*%aWXV#=6PQic0Ok_D;q>wbv&j7cKc!w4~KF#-{6 z(S%6Za)WpGIWf7jZ3svNG5OLs0>vCL9{V7cgO%zevIVMH{WgP*^D9ws&OqA{yr|m| zKD4*07dGXshJHd#e%x%J+qmS^lS|0Bp?{drv;{@{l9ArPO&?Q5=?OO9=}h$oVe#3b z3Yofj&Cb}WC$PxmRRS)H%&$1-)z7jELS}!u!zQ?A^Y{Tv4QVt*vd@uj-^t2fYRzQj zfxGR>-q|o$3sGn^#VzZ!QQx?h9`njeJry}@x?|k0-GTTA4y3t2E`3DZ!A~D?GiJup z)8%PK2^9OVRlP(24P^4_<|D=H^7}WlWu#LgsdHzB%cPy|f8dD3|A^mh4WXxhLTVu_ z@abE{6Saz|Y{rXYPd4$tfPYo}ef(oQWZ=4Bct-=_9`#Qgp4ma$n$`tOwq#&E18$B; z@Bp)bn3&rEi0>fWWZ@7k5WazfoX`SCO4jQWwVuo+$PmSZn^Hz?O(-tW@*DGxuf)V1 zO_xm&;NVCaHD4dqt(-MlszI3F-p?0!-e$fbiCeuaw66h^TTDLWuaV<@C-`=Xe5WL) zwooG7h>4&*)p3pKMS3O!4>-4jQUN}iAMQ)2*70?hP~)TzzR?-f@?Aqy$$1Iy8VGG$ zMM?8;j!pUX7QQD$gRc_#+=raAS577ga-w?jd`vCiN5lu)dEUkkUPl9!?{$IJNxQys z*E4e$eF&n&+AMRQR2gcaFEjAy*r)G!s(P6D&TfoApMFC_*Ftx0|D0@E-=B7tezU@d zZ{hGiN;YLIoSeRS;9o%dEua4b%4R3;$SugDjP$x;Z!M!@QibuSBb)HY!3zJ7M;^jw zlx6AD50FD&p3JyP*>o+t9YWW8(7P2t!VQQ21pHJOcG_SXQD;(5aX#M6x##5H_Re>6lPyDCjxr*R(+HE%c&QN+b^tbT zXBJk?p)zhJj#I?&Y2n&~XiytG9!1ox;bw5Rbj~)7c(MFBb4>IiRATdhg zmiEFlj@S_hwYYI(ki{}&<;_7(Z0Qkfq>am z&LtL=2qc7rWguk3BtE4zL41@#S;NN*-jWw|7Kx7H7~_%7fPt;TIX}Ubo>;Rmj94V> zNB1=;-9AR7s`Pxn}t_6^3ahlq53e&!Lh85uG zec0vJY_6e`tg7LgfrJ3k!DjR)Bi#L@DHIrZ`sK=<5O0Ip!fxGf*OgGSpP@Hbbe&$9 z;ZI}8lEoC2_7;%L2=w?tb%1oL0V+=Z`7b=P&lNGY;yVBazXRYu;+cQDKvm*7NCxu&i;zub zAJh#11%?w>E2rf2e~C4+rAb-&$^vsdACs7 z@|Ra!OfVM(ke{vyiqh7puf&Yp6cd6{DptUteYfIRWG3pI+5< zBVBI_xkBAc<(pcb$!Y%dTW(b;B;2pOI-(QCsLv@U-D1XJ z(Gk8Q3l7Ws46Aktuj>|s{$6zA&xCPuXL-kB`CgYMs}4IeyG*P51IDwW?8UNQd+$i~ zlxOPtSi5L|gJcF@DwmJA5Ju8HEJ>o{{upwIpb!f{2(vLNBw`7xMbvcw<^{Fj@E~1( z?w`iIMieunS#>nXlmUcSMU+D3rX28f?s7z;X=se6bo8;5vM|O^(D6{A9*ChnGH!RG zP##3>LDC3jZPE4PH32AxrqPk|yIIrq~`aL-=}`okhNu9aT%q z1b)7iJ)CN=V#Ly84N_r7U^SH2FGdE5FpTO2 z630TF$P>GNMu8`rOytb(lB2};`;P4YNwW1<5d3Q~AX#P0aX}R2b2)`rgkp#zTxcGj zAV^cvFbhP|JgWrq_e`~exr~sIR$6p5V?o4Wym3kQ3HA+;Pr$bQ0(PmADVO%MKL!^q z?zAM8j1l4jrq|5X+V!8S*2Wl@=7*pPgciTVK6kS1Ge zMsd_u6DFK$jTnvVtE;qa+8(1sGBu~n&F%dh(&c(Zs4Fc#A=gG^^%^AyH}1^?|8quj zl@Z47h$){PlELJgYZCIHHL= z{U8O>Tw4x3<1{?$8>k-P<}1y9DmAZP_;(3Y*{Sk^H^A=_iSJ@+s5ktgwTXz_2$~W9>VVZsfwCm@s0sQ zeB50_yu@uS+e7QoPvdCwDz{prjo(AFwR%C?z`EL{1`|coJHQTk^nX=tvs1<0arUOJ z!^`*x&&BvTYmemyZ)2p~{%eYX=JVR?DYr(rNgqRMA5E1PR1Iw=prk=L2ldy3r3Vg@27IZx43+ywyzr-X*p*d@tZV+!U#~$-q=8c zgdSuh#r?b4GhEGNai)ayHQpk>5(%j5c@C1K3(W1pb~HeHpaqijJZa-e6vq_8t-^M^ zBJxq|MqZc?pjXPIH}70a5vt!IUh;l}<>VX<-Qcv^u@5(@@M2CHSe_hD$VG-eiV^V( zj7*9T0?di?P$FaD6oo?)<)QT>Npf6Og!GO^GmPV(Km0!=+dE&bk#SNI+C9RGQ|{~O*VC+tXK3!n`5 zHfl6>lwf_aEVV3`0T!aHNZLsj$paS$=LL(?b!Czaa5bbSuZ6#$_@LK<(7yrrl+80| z{tOFd=|ta2Z`^ssozD9BINn45NxUeCQis?-BKmU*Kt=FY-NJ+)8S1ecuFtN-M?&42 zl2$G>u!iNhAk*HoJ^4v^9#ORYp5t^wDj6|lx~5w45#E5wVqI1JQ~9l?nPp1YINf++ zMAdSif~_ETv@Er(EFBI^@L4BULFW>)NI+ejHFP*T}UhWNN`I)RRS8za? z*@`1>9ZB}An%aT5K=_2iQmfE;GcBVHLF!$`I99o5GO`O%O_zLr9AG18>&^HkG(;=V z%}c!OBQ~?MX(9h~tajX{=x)+!cbM7$YzTlmsPOdp2L-?GoW`@{lY9U3f;OUo*BwRB z8A+nv(br0-SH#VxGy#ZrgnGD(=@;HME;yd46EgWJ`EL%oXc&lFpc@Y}^>G(W>h_v_ zlN!`idhX+OjL+~T?19sroAFVGfa5tX-D49w$1g2g_-T|EpHL6}K_aX4$K=LTvwtlF zL*z}j{f+Uoe7{-px3_5iKPA<_7W=>Izkk)!l9ez2w%vi(?Y;i8AxRNLSOGDzNoqoI zP!1uAl}r=_871(G?y`i&)-7{u=%nxk7CZ_Qh#!|ITec zwQn`33GTUM`;D2POWnkqngqJhJRlM>CTONzTG}>^Q0wUunQyn|TAiHzyX2_%ATx%P z%7gW)%4rA9^)M<_%k@`Y?RbC<29sWU&5;@|9thf2#zf8z12$hRcZ!CSb>kUp=4N#y zl3hE#y6>kkA8VY2`W`g5Ip?2qC_BY$>R`iGQLhz2-S>x(RuWv)SPaGdl^)gGw7tjR zH@;jwk!jIaCgSg_*9iF|a);sRUTq30(8I(obh^|}S~}P4U^BIGYqcz;MPpC~Y@k_m zaw4WG1_vz2GdCAX!$_a%GHK**@IrHSkGoN>)e}>yzUTm52on`hYot7cB=oA-h1u|R ztH$11t?54Qg2L+i33FPFKKRm1aOjKST{l1*(nps`>sv%VqeVMWjl5+Gh+9);hIP8? zA@$?}Sc z3qIRpba+y5yf{R6G(u8Z^vkg0Fu&D-7?1s=QZU`Ub{-!Y`I?AGf1VNuc^L3v>)>i# z{DV9W$)>34wnzAXUiV^ZpYKw>UElrN_5Xj6{r_3| z$X5PK`e5$7>~9Dj7gK5ash(dvs`vwfk}&RD`>04;j62zoXESkFBklYaKm5seyiX(P zqQ-;XxlV*yg?Dhlx%xt!b0N3GHp@(p$A;8|%# zZ5m2KL|{on4nr>2_s9Yh=r5ScQ0;aMF)G$-9-Ca6%wA`Pa)i?NGFA|#Yi?{X-4ZO_ z^}%7%vkzvUHa$-^Y#aA+aiR5sa%S|Ebyn`EV<3Pc?ax_f>@sBZF1S;7y$CXd5t5=WGsTKBk8$OfH4v|0?0I=Yp}7c=WBSCg!{0n)XmiU;lfx)**zZaYqmDJelxk$)nZyx5`x$6R|fz(;u zEje5Dtm|a%zK!!tk3{i9$I2b{vXNFy%Bf{50X!x{98+BsDr_u9i>G5%*sqEX|06J0 z^IY{UcEbj6LDwuMh7cH`H@9sVt1l1#8kEQ(LyT@&+K}(ReE`ux8gb0r6L_#bDUo^P z3Ka2lRo52Hdtl_%+pwVs14=q`{d^L58PsU@AMf(hENumaxM{7iAT5sYmWh@hQCO^ zK&}ijo=`VqZ#a3vE?`7QW0ZREL17ZvDfdqKGD?0D4fg{7v%|Yj&_jcKJAB)>=*RS* zto8p6@k%;&^ZF>hvXm&$PCuEp{uqw3VPG$9VMdW5$w-fy2CNNT>E;>ejBgy-m_6`& z97L1p{%srn@O_JQgFpa_#f(_)eb#YS>o>q3(*uB;uZb605(iqM$=NK{nHY=+X2*G) zO3-_Xh%aG}fHWe*==58zBwp%&`mge<8uq8;xIxOd=P%9EK!34^E9sk|(Zq1QSz-JVeP12Fp)-`F|KY$LPwUE?rku zY@OJ)Z9A!ojfzfeyJ9;zv2EM7ZQB)AR5xGa-tMn^bl)FmoIiVyJ@!~@%{}qXXD&Ns zPnfe5U+&ohKefILu_1mPfLGuapX@btta5C#gPB2cjk5m4T}Nfi+Vfka!Yd(L?-c~5 z#ZK4VeQEXNPc4r$K00Fg>g#_W!YZ)cJ?JTS<&68_$#cZT-ME`}tcwqg3#``3M3UPvn+pi}(VNNx6y zFIMVb6OwYU(2`at$gHba*qrMVUl8xk5z-z~fb@Q3Y_+aXuEKH}L+>eW__!IAd@V}L zkw#s%H0v2k5-=vh$^vPCuAi22Luu3uKTf6fPo?*nvj$9(u)4$6tvF-%IM+3pt*cgs z_?wW}J7VAA{_~!?))?s6{M=KPpVhg4fNuU*|3THp@_(q!b*hdl{fjRVFWtu^1dV(f z6iOux9hi&+UK=|%M*~|aqFK{Urfl!TA}UWY#`w(0P!KMe1Si{8|o))Gy6d7;!JQYhgMYmXl?3FfOM2nQGN@~Ap6(G z3+d_5y@=nkpKAhRqf{qQ~k7Z$v&l&@m7Ppt#FSNzKPZM z8LhihcE6i=<(#87E|Wr~HKvVWhkll4iSK$^mUHaxgy8*K$_Zj;zJ`L$naPj+^3zTi z-3NTaaKnD5FPY-~?Tq6QHnmDDRxu0mh0D|zD~Y=vv_qig5r-cIbCpxlju&8Sya)@{ zsmv6XUSi)@(?PvItkiZEeN*)AE~I_?#+Ja-r8$(XiXei2d@Hi7Rx8+rZZb?ZLa{;@*EHeRQ-YDadz~M*YCM4&F-r;E#M+@CSJMJ0oU|PQ^ z=E!HBJDMQ2TN*Y(Ag(ynAL8%^v;=~q?s4plA_hig&5Z0x_^Oab!T)@6kRN$)qEJ6E zNuQjg|G7iwU(N8pI@_6==0CL;lRh1dQF#wePhmu@hADFd3B5KIH#dx(2A zp~K&;Xw}F_N6CU~0)QpQk7s$a+LcTOj1%=WXI(U=Dv!6 z{#<#-)2+gCyyv=Jw?Ab#PVkxPDeH|sAxyG`|Ys}A$PW4TdBv%zDz z^?lwrxWR<%Vzc8Sgt|?FL6ej_*e&rhqJZ3Y>k=X(^dytycR;XDU16}Pc9Vn0>_@H+ zQ;a`GSMEG64=JRAOg%~L)x*w{2re6DVprNp+FcNra4VdNjiaF0M^*>CdPkt(m150rCue?FVdL0nFL$V%5y6N z%eLr5%YN7D06k5ji5*p4v$UMM)G??Q%RB27IvH7vYr_^3>1D-M66#MN8tWGw>WED} z5AhlsanO=STFYFs)Il_0i)l)f<8qn|$DW7ZXhf5xI;m+7M5-%P63XFQrG9>DMqHc} zsgNU9nR`b}E^mL5=@7<1_R~j@q_2U^3h|+`7YH-?C=vme1C3m`Fe0HC>pjt6f_XMh zy~-i-8R46QNYneL4t@)<0VU7({aUO?aH`z4V2+kxgH5pYD5)wCh75JqQY)jIPN=U6 z+qi8cGiOtXG2tXm;_CfpH9ESCz#i5B(42}rBJJF$jh<1sbpj^8&L;gzGHb8M{of+} zzF^8VgML2O9nxBW7AvdEt90vp+#kZxWf@A)o9f9}vKJy9NDBjBW zSt=Hcs=YWCwnfY1UYx*+msp{g!w0HC<_SM!VL1(I2PE?CS}r(eh?{I)mQixmo5^p# zV?2R!R@3GV6hwTCrfHiK#3Orj>I!GS2kYhk1S;aFBD_}u2v;0HYFq}Iz1Z(I4oca4 zxquja8$+8JW_EagDHf$a1OTk5S97umGSDaj)gH=fLs9>_=XvVj^Xj9a#gLdk=&3tl zfmK9MNnIX9v{?%xdw7568 zNrZ|roYs(vC4pHB5RJ8>)^*OuyNC>x7ad)tB_}3SgQ96+-JT^Qi<`xi=)_=$Skwv~ zdqeT9Pa`LYvCAn&rMa2aCDV(TMI#PA5g#RtV|CWpgDYRA^|55LLN^uNh*gOU>Z=a06qJ;$C9z8;n-Pq=qZnc1zUwJ@t)L;&NN+E5m zRkQ(SeM8=l-aoAKGKD>!@?mWTW&~)uF2PYUJ;tB^my`r9n|Ly~0c%diYzqs9W#FTjy?h&X3TnH zXqA{QI82sdjPO->f=^K^f>N`+B`q9&rN0bOXO79S&a9XX8zund(kW7O76f4dcWhIu zER`XSMSFbSL>b;Rp#`CuGJ&p$s~G|76){d?xSA5wVg##_O0DrmyEYppyBr%fyWbbv zp`K84JwRNP$d-pJ!Qk|(RMr?*!wi1if-9G#0p>>1QXKXWFy)eB3ai)l3601q8!9JC zvU#ZWWDNKq9g6fYs?JQ)Q4C_cgTy3FhgKb8s&m)DdmL5zhNK#8wWg!J*7G7Qhe9VU zha?^AQTDpYcuN!B+#1dE*X{<#!M%zfUQbj=zLE{dW0XeQ7-oIsGY6RbkP2re@Q{}r_$iiH0xU%iN*ST`A)-EH6eaZB$GA#v)cLi z*MpA(3bYk$oBDKAzu^kJoSUsDd|856DApz={3u8sbQV@JnRkp2nC|)m;#T=DvIL-O zI4vh;g7824l}*`_p@MT4+d`JZ2%6NQh=N9bmgJ#q!hK@_<`HQq3}Z8Ij>3%~<*= zcv=!oT#5xmeGI92lqm9sGVE%#X$ls;St|F#u!?5Y7syhx6q#MVRa&lBmmn%$C0QzU z);*ldgwwCmzM3uglr}!Z2G+?& zf%Dpo&mD%2ZcNFiN-Z0f;c_Q;A%f@>26f?{d1kxIJD}LxsQkB47SAdwinfMILZdN3 zfj^HmTzS3Ku5BxY>ANutS8WPQ-G>v4^_Qndy==P3pDm+Xc?>rUHl-4+^%Sp5atOja z2oP}ftw-rqnb}+khR3CrRg^ibi6?QYk1*i^;kQGirQ=uB9Sd1NTfT-Rbv;hqnY4neE5H1YUrjS2m+2&@uXiAo- zrKUX|Ohg7(6F(AoP~tj;NZlV#xsfo-5reuQHB$&EIAhyZk;bL;k9ouDmJNBAun;H& zn;Of1z_Qj`x&M;5X;{s~iGzBQTY^kv-k{ksbE*Dl%Qf%N@hQCfY~iUw!=F-*$cpf2 z3wix|aLBV0b;W@z^%7S{>9Z^T^fLOI68_;l@+Qzaxo`nAI8emTV@rRhEKZ z?*z_{oGdI~R*#<2{bkz$G~^Qef}$*4OYTgtL$e9q!FY7EqxJ2`zk6SQc}M(k(_MaV zSLJnTXw&@djco1~a(vhBl^&w=$fa9{Sru>7g8SHahv$&Bl(D@(Zwxo_3r=;VH|uc5 zi1Ny)J!<(KN-EcQ(xlw%PNwK8U>4$9nVOhj(y0l9X^vP1TA>r_7WtSExIOsz`nDOP zs}d>Vxb2Vo2e5x8p(n~Y5ggAyvib>d)6?)|E@{FIz?G3PVGLf7-;BxaP;c?7ddH$z zA+{~k^V=bZuXafOv!RPsE1GrR3J2TH9uB=Z67gok+u`V#}BR86hB1xl}H4v`F+mRfr zYhortD%@IGfh!JB(NUNSDh+qDz?4ztEgCz&bIG-Wg7w-ua4ChgQR_c+z8dT3<1?uX z*G(DKy_LTl*Ea!%v!RhpCXW1WJO6F`bgS-SB;Xw9#! z<*K}=#wVu9$`Yo|e!z-CPYH!nj7s9dEPr-E`DXUBu0n!xX~&|%#G=BeM?X@shQQMf zMvr2!y7p_gD5-!Lnm|a@z8Of^EKboZsTMk%5VsJEm>VsJ4W7Kv{<|#4f-qDE$D-W>gWT%z-!qXnDHhOvLk=?^a1*|0j z{pW{M0{#1VcR5;F!!fIlLVNh_Gj zbnW(_j?0c2q$EHIi@fSMR{OUKBcLr{Y&$hrM8XhPByyZaXy|dd&{hYQRJ9@Fn%h3p7*VQolBIV@Eq`=y%5BU~3RPa^$a?ixp^cCg z+}Q*X+CW9~TL29@OOng(#OAOd!)e$d%sr}^KBJ-?-X&|4HTmtemxmp?cT3uA?md4% zT8yZ0U;6Rg6JHy3fJae{6TMGS?ZUX6+gGTT{Q{)SI85$5FD{g-eR%O0KMpWPY`4@O zx!hen1*8^E(*}{m^V_?}(b5k3hYo=T+$&M32+B`}81~KKZhY;2H{7O-M@vbCzuX0n zW-&HXeyr1%I3$@ns-V1~Lb@wIpkmx|8I~ob1Of7i6BTNysEwI}=!nU%q7(V_^+d*G z7G;07m(CRTJup!`cdYi93r^+LY+`M*>aMuHJm(A8_O8C#A*$!Xvddgpjx5)?_EB*q zgE8o5O>e~9IiSC@WtZpF{4Bj2J5eZ>uUzY%TgWF7wdDE!fSQIAWCP)V{;HsU3ap?4 znRsiiDbtN7i9hapO;(|Ew>Ip2TZSvK9Z^N21%J?OiA_&eP1{(Pu_=%JjKy|HOardq ze?zK^K zA%sjF64*Wufad%H<) z^|t>e*h+Z1#l=5wHexzt9HNDNXgM=-OPWKd^5p!~%SIl>Fo&7BvNpbf8{NXmH)o{r zO=aBJ;meX1^{O%q;kqdw*5k!Y7%t_30 zy{nGRVc&5qt?dBwLs+^Sfp;f`YVMSB#C>z^a9@fpZ!xb|b-JEz1LBX7ci)V@W+kvQ89KWA0T~Lj$aCcfW#nD5bt&Y_< z-q{4ZXDqVg?|0o)j1%l0^_it0WF*LCn-+)c!2y5yS7aZIN$>0LqNnkujV*YVes(v$ zY@_-!Q;!ZyJ}Bg|G-~w@or&u0RO?vlt5*9~yeoPV_UWrO2J54b4#{D(D>jF(R88u2 zo#B^@iF_%S>{iXSol8jpmsZuJ?+;epg>k=$d`?GSegAVp3n$`GVDvK${N*#L_1`44 z{w0fL{2%)0|E+qgZtjX}itZz^KJt4Y;*8uSK}Ft38+3>j|K(PxIXXR-t4VopXo#9# zt|F{LWr-?34y`$nLBVV_*UEgA6AUI65dYIbqpNq9cl&uLJ0~L}<=ESlOm?Y-S@L*d z<7vt}`)TW#f%Rp$Q}6@3=j$7Tze@_uZO@aMn<|si{?S}~maII`VTjs&?}jQ4_cut9$)PEqMukwoXobzaKx^MV z2fQwl+;LSZ$qy%Tys0oo^K=jOw$!YwCv^ei4NBVauL)tN%=wz9M{uf{IB(BxK|lT*pFkmNK_1tV`nb%jH=a0~VNq2RCKY(rG7jz!-D^k)Ec)yS%17pE#o6&eY+ z^qN(hQT$}5F(=4lgNQhlxj?nB4N6ntUY6(?+R#B?W3hY_a*)hnr4PA|vJ<6p`K3Z5Hy z{{8(|ux~NLUW=!?9Qe&WXMTAkQnLXg(g=I@(VG3{HE13OaUT|DljyWXPs2FE@?`iU z4GQlM&Q=T<4&v@Fe<+TuXiZQT3G~vZ&^POfmI1K2h6t4eD}Gk5XFGpbj1n_g*{qmD6Xy z`6Vv|lLZtLmrnv*{Q%xxtcWVj3K4M%$bdBk_a&ar{{GWyu#ljM;dII;*jP;QH z#+^o-A4np{@|Mz+LphTD0`FTyxYq#wY)*&Ls5o{0z9yg2K+K7ZN>j1>N&;r+Z`vI| zDzG1LJZ+sE?m?>x{5LJx^)g&pGEpY=fQ-4}{x=ru;}FL$inHemOg%|R*ZXPodU}Kh zFEd5#+8rGq$Y<_?k-}r5zgQ3jRV=ooHiF|@z_#D4pKVEmn5CGV(9VKCyG|sT9nc=U zEoT67R`C->KY8Wp-fEcjjFm^;Cg(ls|*ABVHq8clBE(;~K^b+S>6uj70g? z&{XQ5U&!Z$SO7zfP+y^8XBbiu*Cv-yJG|l-oe*!s5$@Lh_KpxYL2sx`B|V=dETN>5K+C+CU~a_3cI8{vbu$TNVdGf15*>D zz@f{zIlorkY>TRh7mKuAlN9A0>N>SV`X)+bEHms=mfYTMWt_AJtz_h+JMmrgH?mZt zm=lfdF`t^J*XLg7v+iS)XZROygK=CS@CvUaJo&w2W!Wb@aa?~Drtf`JV^cCMjngVZ zv&xaIBEo8EYWuML+vxCpjjY^s1-ahXJzAV6hTw%ZIy!FjI}aJ+{rE&u#>rs)vzuxz z+$5z=7W?zH2>Eb32dvgHYZtCAf!=OLY-pb4>Ae79rd68E2LkVPj-|jFeyqtBCCwiW zkB@kO_(3wFq)7qwV}bA=zD!*@UhT`geq}ITo%@O(Z5Y80nEX~;0-8kO{oB6|(4fQh z);73T!>3@{ZobPwRv*W?7m0Ml9GmJBCJd&6E?hdj9lV= z4flNfsc(J*DyPv?RCOx!MSvk(M952PJ-G|JeVxWVjN~SNS6n-_Ge3Q;TGE;EQvZg86%wZ`MB zSMQua(i*R8a75!6$QRO^(o7sGoomb+Y{OMy;m~Oa`;P9Yqo>?bJAhqXxLr7_3g_n>f#UVtxG!^F#1+y@os6x(sg z^28bsQ@8rw%Gxk-stAEPRbv^}5sLe=VMbkc@Jjimqjvmd!3E7+QnL>|(^3!R} zD-l1l7*Amu@j+PWLGHXXaFG0Ct2Q=}5YNUxEQHCAU7gA$sSC<5OGylNnQUa>>l%sM zyu}z6i&({U@x^hln**o6r2s-(C-L50tQvz|zHTqW!ir?w&V23tuYEDJVV#5pE|OJu z7^R!A$iM$YCe?8n67l*J-okwfZ+ZTkGvZ)tVPfR;|3gyFjF)8V zyXXN=!*bpyRg9#~Bg1+UDYCt0 ztp4&?t1X0q>uz;ann$OrZs{5*r`(oNvw=$7O#rD|Wuv*wIi)4b zGtq4%BX+kkagv3F9Id6~-c+1&?zny%w5j&nk9SQfo0k4LhdSU_kWGW7axkfpgR`8* z!?UTG*Zi_baA1^0eda8S|@&F z{)Rad0kiLjB|=}XFJhD(S3ssKlveFFmkN{Vl^_nb!o5M!RC=m)V&v2%e?ZoRC@h3> zJ(?pvToFd`*Zc@HFPL#=otWKwtuuQ_dT-Hr{S%pQX<6dqVJ8;f(o)4~VM_kEQkMR+ zs1SCVi~k>M`u1u2xc}>#D!V&6nOOh-E$O&SzYrjJdZpaDv1!R-QGA141WjQe2s0J~ zQ;AXG)F+K#K8_5HVqRoRM%^EduqOnS(j2)|ctA6Q^=|s_WJYU;Z%5bHp08HPL`YF2 zR)Ad1z{zh`=sDs^&V}J z%$Z$!jd7BY5AkT?j`eqMs%!Gm@T8)4w3GYEX~IwgE~`d|@T{WYHkudy(47brgHXx& zBL1yFG6!!!VOSmDxBpefy2{L_u5yTwja&HA!mYA#wg#bc-m%~8aRR|~AvMnind@zs zy>wkShe5&*un^zvSOdlVu%kHsEo>@puMQ`b1}(|)l~E{5)f7gC=E$fP(FC2=F<^|A zxeIm?{EE!3sO!Gr7e{w)Dx(uU#3WrFZ>ibmKSQ1tY?*-Nh1TDHLe+k*;{Rp!Bmd_m zb#^kh`Y*8l|9Cz2e{;RL%_lg{#^Ar+NH|3z*Zye>!alpt{z;4dFAw^^H!6ING*EFc z_yqhr8d!;%nHX9AKhFQZBGrSzfzYCi%C!(Q5*~hX>)0N`vbhZ@N|i;_972WSx*>LH z87?en(;2_`{_JHF`Sv6Wlps;dCcj+8IJ8ca6`DsOQCMb3n# z3)_w%FuJ3>fjeOOtWyq)ag|PmgQbC-s}KRHG~enBcIwqIiGW8R8jFeBNY9|YswRY5 zjGUxdGgUD26wOpwM#8a!Nuqg68*dG@VM~SbOroL_On0N6QdT9?)NeB3@0FCC?Z|E0 z6TPZj(AsPtwCw>*{eDEE}Gby>0q{*lI+g2e&(YQrsY&uGM{O~}(oM@YWmb*F zA0^rr5~UD^qmNljq$F#ARXRZ1igP`MQx4aS6*MS;Ot(1L5jF2NJ;de!NujUYg$dr# z=TEL_zTj2@>ZZN(NYCeVX2==~=aT)R30gETO{G&GM4XN<+!&W&(WcDP%oL8PyIVUC zs5AvMgh6qr-2?^unB@mXK*Dbil^y-GTC+>&N5HkzXtozVf93m~xOUHn8`HpX=$_v2 z61H;Z1qK9o;>->tb8y%#4H)765W4E>TQ1o0PFj)uTOPEvv&}%(_mG0ISmyhnQV33Z$#&yd{ zc{>8V8XK$3u8}04CmAQ#I@XvtmB*s4t8va?-IY4@CN>;)mLb_4!&P3XSw4pA_NzDb zORn!blT-aHk1%Jpi>T~oGLuh{DB)JIGZ9KOsciWs2N7mM1JWM+lna4vkDL?Q)z_Ct z`!mi0jtr+4*L&N7jk&LodVO#6?_qRGVaucqVB8*us6i3BTa^^EI0x%EREQSXV@f!lak6Wf1cNZ8>*artIJ(ADO*=<-an`3zB4d*oO*8D1K!f z*A@P1bZCNtU=p!742MrAj%&5v%Xp_dSX@4YCw%F|%Dk=u|1BOmo)HsVz)nD5USa zR~??e61sO(;PR)iaxK{M%QM_rIua9C^4ppVS$qCT9j2%?*em?`4Z;4@>I(c%M&#cH z>4}*;ej<4cKkbCAjjDsyKS8rIm90O)Jjgyxj5^venBx&7B!xLmzxW3jhj7sR(^3Fz z84EY|p1NauwXUr;FfZjdaAfh%ivyp+^!jBjJuAaKa!yCq=?T_)R!>16?{~p)FQ3LDoMyG%hL#pR!f@P%*;#90rs_y z@9}@r1BmM-SJ#DeuqCQk=J?ixDSwL*wh|G#us;dd{H}3*-Y7Tv5m=bQJMcH+_S`zVtf;!0kt*(zwJ zs+kedTm!A}cMiM!qv(c$o5K%}Yd0|nOd0iLjus&;s0Acvoi-PFrWm?+q9f^FslxGi z6ywB`QpL$rJzWDg(4)C4+!2cLE}UPCTBLa*_=c#*$b2PWrRN46$y~yST3a2$7hEH= zNjux+wna^AzQ=KEa_5#9Ph=G1{S0#hh1L3hQ`@HrVnCx{!fw_a0N5xV(iPdKZ-HOM za)LdgK}1ww*C_>V7hbQnTzjURJL`S%`6nTHcgS+dB6b_;PY1FsrdE8(2K6FN>37!62j_cBlui{jO^$dPkGHV>pXvW0EiOA zqW`YaSUBWg_v^Y5tPJfWLcLpsA8T zG)!x>pKMpt!lv3&KV!-um= zKCir6`bEL_LCFx4Z5bAFXW$g3Cq`?Q%)3q0r852XI*Der*JNuKUZ`C{cCuu8R8nkt z%pnF>R$uY8L+D!V{s^9>IC+bmt<05h**>49R*#vpM*4i0qRB2uPbg8{{s#9yC;Z18 zD7|4m<9qneQ84uX|J&f-g8a|nFKFt34@Bt{CU`v(SYbbn95Q67*)_Esl_;v291s=9 z+#2F2apZU4Tq=x+?V}CjwD(P=U~d<=mfEFuyPB`Ey82V9G#Sk8H_Ob_RnP3s?)S_3 zr%}Pb?;lt_)Nf>@zX~D~TBr;-LS<1I##8z`;0ZCvI_QbXNh8Iv)$LS=*gHr;}dgb=w5$3k2la1keIm|=7<-JD>)U%=Avl0Vj@+&vxn zt-)`vJxJr88D&!}2^{GPXc^nmRf#}nb$4MMkBA21GzB`-Or`-3lq^O^svO7Vs~FdM zv`NvzyG+0T!P8l_&8gH|pzE{N(gv_tgDU7SWeiI-iHC#0Ai%Ixn4&nt{5y3(GQs)i z&uA;~_0shP$0Wh0VooIeyC|lak__#KVJfxa7*mYmZ22@(<^W}FdKjd*U1CqSjNKW% z*z$5$=t^+;Ui=MoDW~A7;)Mj%ibX1_p4gu>RC}Z_pl`U*{_z@+HN?AF{_W z?M_X@o%w8fgFIJ$fIzBeK=v#*`mtY$HC3tqw7q^GCT!P$I%=2N4FY7j9nG8aIm$c9 zeKTxVKN!UJ{#W)zxW|Q^K!3s;(*7Gbn;e@pQBCDS(I|Y0euK#dSQ_W^)sv5pa%<^o zyu}3d?Lx`)3-n5Sy9r#`I{+t6x%I%G(iewGbvor&I^{lhu-!#}*Q3^itvY(^UWXgvthH52zLy&T+B)Pw;5>4D6>74 zO_EBS)>l!zLTVkX@NDqyN2cXTwsUVao7$HcqV2%t$YzdAC&T)dwzExa3*kt9d(}al zA~M}=%2NVNUjZiO7c>04YH)sRelXJYpWSn^aC$|Ji|E13a^-v2MB!Nc*b+=KY7MCm zqIteKfNkONq}uM;PB?vvgQvfKLPMB8u5+Am=d#>g+o&Ysb>dX9EC8q?D$pJH!MTAqa=DS5$cb+;hEvjwVfF{4;M{5U&^_+r zvZdu_rildI!*|*A$TzJ&apQWV@p{!W`=?t(o0{?9y&vM)V)ycGSlI3`;ps(vf2PUq zX745#`cmT*ra7XECC0gKkpu2eyhFEUb?;4@X7weEnLjXj_F~?OzL1U1L0|s6M+kIhmi%`n5vvDALMagi4`wMc=JV{XiO+^ z?s9i7;GgrRW{Mx)d7rj)?(;|b-`iBNPqdwtt%32se@?w4<^KU&585_kZ=`Wy^oLu9 z?DQAh5z%q;UkP48jgMFHTf#mj?#z|=w= z(q6~17Vn}P)J3M?O)x))%a5+>TFW3No~TgP;f}K$#icBh;rSS+R|}l鯊%1Et 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⁢ zv5o9#E7!G0XmdzfsS{efPNi`1b44~SZ4Z8fuX!I}#8g+(wxzQwUT#Xb2(tbY1+EUhGKoT@KEU9Ktl>_0 z%bjDJg;#*gtJZv!-Zs`?^}v5eKmnbjqlvnSzE@_SP|LG_PJ6CYU+6zY6>92%E+ z=j@TZf-iW4(%U{lnYxQA;7Q!b;^brF8n0D>)`q5>|WDDXLrqYU_tKN2>=#@~OE7grMnNh?UOz-O~6 z6%rHy{#h9K0AT+lDC7q4{hw^|q6*Ry;;L%Q@)Ga}$60_q%D)rv(CtS$CQbpq9|y1e zRSrN4;$Jyl{m5bZw`$8TGvb}(LpY{-cQ)fcyJv7l3S52TLXVDsphtv&aPuDk1OzCA z4A^QtC(!11`IsNx_HnSy?>EKpHJWT^wmS~hc^p^zIIh@9f6U@I2 zC=Mve{j2^)mS#U$e{@Q?SO6%LDsXz@SY+=cK_QMmXBIU)j!$ajc-zLx3V60EXJ!qC zi<%2x8Q24YN+&8U@CIlN zrZkcT9yh%LrlGS9`G)KdP(@9Eo-AQz@8GEFWcb7U=a0H^ZVbLmz{+&M7W(nXJ4sN8 zJLR7eeK(K8`2-}j(T7JsO`L!+CvbueT%izanm-^A1Dn{`1Nw`9P?cq;7no+XfC`K(GO9?O^5zNIt4M+M8LM0=7Gz8UA@Z0N+lg+cX)NfazRu z5D)~HA^(u%w^cz+@2@_#S|u>GpB+j4KzQ^&Wcl9f z&hG#bCA(Yk0D&t&aJE^xME^&E-&xGHhXn%}psEIj641H+Nl-}boj;)Zt*t(4wZ5DN z@GXF$bL=&pBq-#vkTkh>7hl%K5|3 z{`Vn9b$iR-SoGENp}bn4;fR3>9sA%X2@1L3aE9yTra;Wb#_`xWwLSLdfu+PAu+o3| zGVnpzPr=ch{uuoHjtw7+_!L_2;knQ!DuDl0R`|%jr+}jFzXtrHIKc323?JO{l&;VF z*L1+}JU7%QJOg|5|Tc|D8fN zJORAg=_vsy{ak|o);@)Yh8Lkcg@$FG3k@ep36BRa^>~UmnRPziS>Z=`Jb2x*Q#`%A zU*i3&Vg?TluO@X0O;r2Jl6LKLUOVhSqg1*qOt^|8*c7 zo(298@+r$k_wQNGHv{|$tW(T8L+4_`FQ{kEW5Jgg{yf7ey4ss_(SNKfz(N9lx&a;< je(UuV8hP?p&}TPdm1I$XmG#(RzlD&B2izSj9sl%y5~4qc diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1af9e09..a80b22c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew.bat b/gradlew.bat index 6689b85..7101f8e 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/quarkus-sentry/build.gradle b/quarkus-sentry/build.gradle index 90d0581..6c0ddcf 100644 --- a/quarkus-sentry/build.gradle +++ b/quarkus-sentry/build.gradle @@ -7,7 +7,7 @@ subprojects { group = "app.fyreplace" version = gitVersion() dependencies { - implementation(enforcedPlatform("io.quarkus:quarkus-bom:${quarkusVersion}")) + implementation(enforcedPlatform("io.quarkus.platform:quarkus-bom:${quarkusVersion}")) implementation(enforcedPlatform("io.sentry:sentry-bom:${sentryVersion}")) implementation("io.quarkus:quarkus-opentelemetry") implementation("io.sentry:sentry-opentelemetry-core") From b7547ac3343f1045fc6983807bc951a5fad537e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 24 Feb 2024 12:51:44 +0100 Subject: [PATCH 121/157] Make path building Windows-compatible --- src/main/java/app/fyreplace/api/emails/EmailBase.java | 9 +++++---- .../api/services/storage/local/LocalStorageService.java | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/app/fyreplace/api/emails/EmailBase.java b/src/main/java/app/fyreplace/api/emails/EmailBase.java index 0dc6c36..5ab93de 100644 --- a/src/main/java/app/fyreplace/api/emails/EmailBase.java +++ b/src/main/java/app/fyreplace/api/emails/EmailBase.java @@ -8,7 +8,7 @@ import io.quarkus.mailer.Mailer; import io.quarkus.qute.TemplateInstance; import jakarta.inject.Inject; -import java.net.URI; +import jakarta.ws.rs.core.UriBuilder; import java.util.List; import java.util.ResourceBundle; import org.eclipse.microprofile.config.inject.ConfigProperty; @@ -61,9 +61,10 @@ protected String getRandomCode() { } protected String getLink() { - return URI.create(appFrontUrl) - .resolve("?action=" + action()) - .resolve('#' + email.email + ':' + getRandomCode()) + return UriBuilder.fromUri(appFrontUrl) + .queryParam("action", action()) + .fragment(email.email + ':' + getRandomCode()) + .build() .toString(); } diff --git a/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java b/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java index d3cac40..68ba574 100644 --- a/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java +++ b/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java @@ -8,13 +8,13 @@ import jakarta.inject.Inject; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.UriBuilder; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.net.URI; -import java.nio.file.Paths; import org.eclipse.microprofile.config.inject.ConfigProperty; @SuppressWarnings("unused") @@ -57,7 +57,7 @@ public void remove(final String path) { @Override public URI getUri(final String path) { final var pathBase = StoredFilesEndpoint.class.getAnnotation(Path.class).value(); - return URI.create(appUrl).resolve(Paths.get(pathBase, path).toString()); + return UriBuilder.fromUri(appUrl).path(pathBase).path(path).build(); } private File getFile(final String path) { From ef1da872a1484406f1ad794735f4d19fcd474762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 24 Feb 2024 12:55:22 +0100 Subject: [PATCH 122/157] Update workflow dependencies --- .github/workflows/validation.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index b9511eb..9301598 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -14,7 +14,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: temurin java-version: '21' @@ -32,7 +32,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: temurin java-version: '21' From 1ab6b3438ec4475603621acdcb57f7997e114f81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 9 Mar 2024 13:20:54 +0100 Subject: [PATCH 123/157] Update dependencies --- gradle.properties | 4 ++-- quarkus-sentry/deployment/build.gradle | 4 +++- quarkus-sentry/runtime/build.gradle | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 52de7fe..10b42be 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ org.gradle.jvmargs=-Xmx1024M -quarkusVersion=3.7.3 -sentryVersion=7.4.0 +quarkusVersion=3.8.2 +sentryVersion=7.5.0 tikaVersion=2.9.1 gitPluginVersion=3.0.0 spotlessPluginVersion=6.25.0 diff --git a/quarkus-sentry/deployment/build.gradle b/quarkus-sentry/deployment/build.gradle index b85a7b0..95a4393 100644 --- a/quarkus-sentry/deployment/build.gradle +++ b/quarkus-sentry/deployment/build.gradle @@ -6,7 +6,9 @@ repositories { } dependencies { - implementation("io.quarkus:quarkus-core-deployment") implementation("io.quarkus:quarkus-arc-deployment") + implementation("io.quarkus:quarkus-core-deployment") + implementation("io.quarkus:quarkus-opentelemetry-deployment") + implementation("io.quarkus:quarkus-resteasy-reactive-deployment") implementation(project(":quarkus-sentry:runtime")) } diff --git a/quarkus-sentry/runtime/build.gradle b/quarkus-sentry/runtime/build.gradle index b530433..7ebd194 100644 --- a/quarkus-sentry/runtime/build.gradle +++ b/quarkus-sentry/runtime/build.gradle @@ -7,8 +7,8 @@ repositories { } dependencies { - implementation("io.quarkus:quarkus-core") implementation("io.quarkus:quarkus-arc") + implementation("io.quarkus:quarkus-core") implementation("io.quarkus:quarkus-resteasy-reactive") implementation("io.sentry:sentry-jul") implementation("io.opentelemetry.instrumentation:opentelemetry-jdbc") From 0b31c90ce52f45cc4e2ea39cda130fcec8602cce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 28 Apr 2024 18:16:25 +0200 Subject: [PATCH 124/157] Update dependencies --- build.gradle | 6 +++--- gradle.properties | 6 +++--- src/main/resources/application.yaml | 3 +++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index d5ec2cf..cb8fb38 100644 --- a/build.gradle +++ b/build.gradle @@ -29,9 +29,9 @@ dependencies { implementation("io.quarkus:quarkus-jdbc-postgresql") implementation("io.quarkus:quarkus-liquibase") implementation("io.quarkus:quarkus-mailer") - implementation("io.quarkus:quarkus-resteasy-reactive") - implementation("io.quarkus:quarkus-resteasy-reactive-jackson") - implementation("io.quarkus:quarkus-resteasy-reactive-qute") + implementation("io.quarkus:quarkus-rest") + implementation("io.quarkus:quarkus-rest-jackson") + implementation("io.quarkus:quarkus-rest-qute") implementation("io.quarkus:quarkus-scheduler") implementation("io.quarkus:quarkus-security") implementation("io.quarkus:quarkus-smallrye-health") diff --git a/gradle.properties b/gradle.properties index 10b42be..46edc6a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,8 +1,8 @@ org.gradle.jvmargs=-Xmx1024M -quarkusVersion=3.8.2 -sentryVersion=7.5.0 -tikaVersion=2.9.1 +quarkusVersion=3.9.5 +sentryVersion=7.8.0 +tikaVersion=2.9.2 gitPluginVersion=3.0.0 spotlessPluginVersion=6.25.0 lombokPluginVersion=8.6 diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 8a7b375..bc8bcf8 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -19,6 +19,9 @@ quarkus: cors: true limits: max-body-size: "1M" + otel: + security-events: + enabled: true mailer: port: 587 start-tls: "REQUIRED" From 59343af39d9beccb2df10c19c7e99689698201fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 3 May 2024 23:12:27 +0200 Subject: [PATCH 125/157] Format code --- src/main/java/app/fyreplace/api/data/User.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/app/fyreplace/api/data/User.java b/src/main/java/app/fyreplace/api/data/User.java index 79b261b..7ca5379 100644 --- a/src/main/java/app/fyreplace/api/data/User.java +++ b/src/main/java/app/fyreplace/api/data/User.java @@ -3,7 +3,14 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import jakarta.annotation.Nullable; -import jakarta.persistence.*; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.LockModeType; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; +import jakarta.persistence.PostRemove; +import jakarta.persistence.Table; import jakarta.ws.rs.NotAuthorizedException; import jakarta.ws.rs.core.SecurityContext; import java.time.Instant; @@ -174,7 +181,6 @@ public boolean isSubscribedTo(final Post post) { return findByUsername(username, null); } - @SuppressWarnings("DataFlowIssue") public static @Nullable User findByUsername(final String username, @Nullable final LockModeType lock) { return User.find("username", username).withLock(lock).firstResult(); } From f2ffaea539653e6053f364a75d490c9c18212f46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Tue, 7 May 2024 16:38:48 +0200 Subject: [PATCH 126/157] Use Java 21 --- src/main/java/app/fyreplace/api/data/Post.java | 2 +- .../app/fyreplace/api/testing/Assertions.java | 2 +- .../endpoints/chapters/CreateChapterTests.java | 2 +- .../chapters/UpdateChapterImageTests.java | 18 +++++++++--------- .../chapters/UpdateChapterTextTests.java | 16 ++++++++-------- .../testing/endpoints/comments/CountTests.java | 2 +- .../testing/endpoints/emails/CreateTests.java | 3 ++- .../testing/endpoints/emails/DeleteTests.java | 5 +++-- .../api/testing/endpoints/posts/ListTests.java | 7 ++++--- .../endpoints/subscriptions/DeleteTests.java | 4 ++-- .../endpoints/users/CountBlockedTests.java | 3 ++- .../endpoints/users/ListBlockedTests.java | 3 ++- .../endpoints/users/UpdateMeAvatarTests.java | 7 ++++--- 13 files changed, 40 insertions(+), 34 deletions(-) diff --git a/src/main/java/app/fyreplace/api/data/Post.java b/src/main/java/app/fyreplace/api/data/Post.java index 8409ff2..87591ee 100644 --- a/src/main/java/app/fyreplace/api/data/Post.java +++ b/src/main/java/app/fyreplace/api/data/Post.java @@ -31,7 +31,7 @@ public class Post extends AuthoredEntityBase implements Reportable { @Formula("(select count(*) from votes where votes.post_id = id)") public long voteCount; - public static Duration shelfLife = Duration.ofDays(7); + public static final Duration shelfLife = Duration.ofDays(7); @Override public void scrub() { diff --git a/src/test/java/app/fyreplace/api/testing/Assertions.java b/src/test/java/app/fyreplace/api/testing/Assertions.java index 993966f..0fce6b1 100644 --- a/src/test/java/app/fyreplace/api/testing/Assertions.java +++ b/src/test/java/app/fyreplace/api/testing/Assertions.java @@ -9,6 +9,6 @@ public final class Assertions { public static void assertSingleEmail(final Class emailClass, final List mails) { assertEquals(1, mails.size()); - assertInstanceOf(emailClass, mails.get(0)); + assertInstanceOf(emailClass, mails.getFirst()); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/CreateChapterTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/CreateChapterTests.java index a7b7802..f76c421 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/CreateChapterTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/CreateChapterTests.java @@ -56,7 +56,7 @@ public void createChapterInOwnDraftOverMaximum() { QuarkusTransaction.requiringNew().run(() -> { final var chapters = draft.getChapters(); final var newChapters = postsMaxChapterCount - chapters.size(); - String before = chapters.get(chapters.size() - 1).position; + String before = chapters.getLast().position; for (var i = 0; i < newChapters; i++) { final var chapter = new Chapter(); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterImageTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterImageTests.java index ff8777a..836714d 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterImageTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterImageTests.java @@ -34,7 +34,7 @@ public void updateChapterImageInOwnPost() throws IOException { .statusCode(403); } - final var chapter = post.getChapters().get(position); + final var chapter = post.getChapters().getFirst(); assertHasNoImage(chapter); } @@ -53,7 +53,7 @@ public void updateChapterImageInOwnDraft(final String fileType) throws IOExcepti .statusCode(200); } - final var chapter = draft.getChapters().get(position); + final var chapter = draft.getChapters().getFirst(); assertHasImage(chapter); } @@ -86,7 +86,7 @@ public void updateChapterImageInOwnDraftWithInvalidType(final String fileType) t .statusCode(415); } - final var chapter = draft.getChapters().get(position); + final var chapter = draft.getChapters().getFirst(); assertHasNoImage(chapter); } @@ -95,7 +95,7 @@ public void updateChapterImageInOwnDraftWithInvalidType(final String fileType) t public void updateChapterImageInOwnDraftWithoutInput() { final var position = 0; given().pathParam("id", draft.id).put(position + "/image").then().statusCode(415); - final var chapter = draft.getChapters().get(position); + final var chapter = draft.getChapters().getFirst(); assertHasNoImage(chapter); } @@ -113,7 +113,7 @@ public void updateChapterImageInOtherPost() throws IOException { .statusCode(403); } - final var chapter = post.getChapters().get(position); + final var chapter = post.getChapters().getFirst(); assertHasNoImage(chapter); } @@ -131,7 +131,7 @@ public void updateChapterImageInOtherDraft() throws IOException { .statusCode(404); } - final var chapter = post.getChapters().get(position); + final var chapter = post.getChapters().getFirst(); assertHasNoImage(chapter); } @@ -148,7 +148,7 @@ public void updateChapterImageInPostUnauthenticated() throws IOException { .statusCode(401); } - final var chapter = post.getChapters().get(position); + final var chapter = post.getChapters().getFirst(); assertHasNoImage(chapter); } @@ -165,7 +165,7 @@ public void updateChapterImageInDraftUnauthenticated() throws IOException { .statusCode(401); } - final var chapter = draft.getChapters().get(position); + final var chapter = draft.getChapters().getFirst(); assertHasNoImage(chapter); } @@ -183,7 +183,7 @@ public void updateChapterTextInNonExistentPost() throws IOException { .statusCode(404); } - final var chapter = draft.getChapters().get(position); + final var chapter = draft.getChapters().getFirst(); assertHasNoImage(chapter); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterTextTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterTextTests.java index 560289c..31b4470 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterTextTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterTextTests.java @@ -21,7 +21,7 @@ public final class UpdateChapterTextTests extends PostTestsBase { @Transactional public void updateChapterTextInOwnPost() { final var position = 0; - final var chapter = post.getChapters().get(position); + final var chapter = post.getChapters().getFirst(); final var oldText = chapter.text; given().body("Hello") .pathParam("id", post.id) @@ -42,7 +42,7 @@ public void updateChapterTextInOwnDraft(final String text) { .put(position + "/text") .then() .statusCode(200); - final var chapter = draft.getChapters().get(position); + final var chapter = draft.getChapters().getFirst(); assertEquals(text, chapter.text); } @@ -62,7 +62,7 @@ public void updateChapterTextInOwnDraftOutOfBounds(final String position) { @Transactional public void updateChapterTextInOwnDraftWithInvalidInput() { final var position = 0; - final var chapter = draft.getChapters().get(position); + final var chapter = draft.getChapters().getFirst(); final var oldText = chapter.text; given().body("a".repeat(501)) .pathParam("id", draft.id) @@ -78,7 +78,7 @@ public void updateChapterTextInOwnDraftWithInvalidInput() { @Transactional public void updateChapterTextInOtherPost() { final var position = 0; - final var chapter = post.getChapters().get(position); + final var chapter = post.getChapters().getFirst(); final var oldText = chapter.text; given().body("Hello") .pathParam("id", post.id) @@ -94,7 +94,7 @@ public void updateChapterTextInOtherPost() { @Transactional public void updateChapterTextInOtherDraft() { final var position = 0; - final var chapter = draft.getChapters().get(position); + final var chapter = draft.getChapters().getFirst(); final var oldText = chapter.text; given().body("Hello") .pathParam("id", draft.id) @@ -109,7 +109,7 @@ public void updateChapterTextInOtherDraft() { @Transactional public void updateChapterTextInPostUnauthenticated() { final var position = 0; - final var chapter = post.getChapters().get(position); + final var chapter = post.getChapters().getFirst(); final var oldText = chapter.text; given().body("Hello") .pathParam("id", post.id) @@ -124,7 +124,7 @@ public void updateChapterTextInPostUnauthenticated() { @Transactional public void updateChapterTextInDraftUnauthenticated() { final var position = 0; - final var chapter = draft.getChapters().get(position); + final var chapter = draft.getChapters().getFirst(); final var oldText = chapter.text; given().body("Hello") .pathParam("id", draft.id) @@ -140,7 +140,7 @@ public void updateChapterTextInDraftUnauthenticated() { @Transactional public void updateChapterTextInNonExistentPost() { final var position = 0; - final var chapter = draft.getChapters().get(position); + final var chapter = draft.getChapters().getFirst(); final var oldText = chapter.text; given().body("Hello") .pathParam("id", fakeId) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/CountTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/CountTests.java index 56b32c2..78da7b2 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/comments/CountTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/CountTests.java @@ -84,7 +84,7 @@ public void beforeEach() { final var user1 = User.findByUsername("user_1"); final var user2 = User.findByUsername("user_2"); range(0, readCommentCount).forEach(i -> dataSeeder.createComment(user2, post, "Comment " + i, false)); - user1.subscribeTo(post); + requireNonNull(user1).subscribeTo(post); final var subscription = Subscription.find("user = ?1 and post = ?2", user1, post) .firstResult(); subscription.lastCommentSeen = Comment.find( diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java index 6b9c904..b4bff51 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java @@ -1,6 +1,7 @@ package app.fyreplace.api.testing.endpoints.emails; import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; import static org.hamcrest.Matchers.equalTo; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -64,7 +65,7 @@ public void createWithEmptyEmail() { @Test @TestSecurity(user = "user_0") public void createWithExistingEmail() { - final var existingUser = User.findByUsername("user_1"); + final var existingUser = requireNonNull(User.findByUsername("user_1")); final var emailCount = Email.count(); given().contentType(ContentType.JSON) .body(new EmailCreation(existingUser.mainEmail.email)) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java index f1b0996..06afbc5 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java @@ -1,6 +1,7 @@ package app.fyreplace.api.testing.endpoints.emails; import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertEquals; import app.fyreplace.api.data.Email; @@ -29,7 +30,7 @@ public void delete() { @Test @TestSecurity(user = "user_0") public void deleteMainEmail() { - final var user = User.findByUsername("user_0"); + final var user = requireNonNull(User.findByUsername("user_0")); final var emailCount = Email.count(); given().delete(user.mainEmail.id.toString()).then().statusCode(403); assertEquals(emailCount, Email.count()); @@ -38,7 +39,7 @@ public void deleteMainEmail() { @Test @TestSecurity(user = "user_0") public void deleteOtherEmail() { - final var otherUser = User.findByUsername("user_1"); + final var otherUser = requireNonNull(User.findByUsername("user_1")); final var emailCount = Email.count(); given().delete(otherUser.mainEmail.id.toString()).then().statusCode(404); assertEquals(emailCount, Email.count()); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java index a9106da..8e8ff5d 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java @@ -1,6 +1,7 @@ package app.fyreplace.api.testing.endpoints.posts; import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; import static java.util.stream.IntStream.range; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.in; @@ -92,9 +93,9 @@ public void listDrafts() { @Transactional public void makeSubscribedToPosts() { - final var user0 = User.findByUsername("user_0"); - final var user1 = User.findByUsername("user_1"); - final var user2 = User.findByUsername("user_2"); + final var user0 = requireNonNull(User.findByUsername("user_0")); + final var user1 = requireNonNull(User.findByUsername("user_1")); + final var user2 = requireNonNull(User.findByUsername("user_2")); range(0, 20).forEach(i -> dataSeeder.createPost(user1, "Post " + i, true, false)); range(0, 20).forEach(i -> dataSeeder.createPost(user2, "Post " + i, true, false)); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/DeleteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/DeleteTests.java index b056a12..3f60de5 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/DeleteTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/DeleteTests.java @@ -19,7 +19,7 @@ public final class DeleteTests extends SubscriptionTestsBase { public void delete() { final var subscriptions = Subscription.list("user.username = 'user_0' and unreadCommentCount > 0"); - given().delete(subscriptions.get(0).id.toString()).then().statusCode(204); + given().delete(subscriptions.getFirst().id.toString()).then().statusCode(204); assertEquals( subscriptions.size() - 1, Subscription.count("user.username = 'user_0' and unreadCommentCount > 0")); } @@ -29,7 +29,7 @@ public void delete() { public void deleteOtherSubscription() { final var subscriptions = Subscription.list("user.username = 'user_0' and unreadCommentCount > 0"); - given().delete(subscriptions.get(0).id.toString()).then().statusCode(404); + given().delete(subscriptions.getFirst().id.toString()).then().statusCode(404); assertEquals(subscriptions.size(), Subscription.count("user.username = 'user_0' and unreadCommentCount > 0")); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java index b9569c2..5805d47 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java @@ -1,6 +1,7 @@ package app.fyreplace.api.testing.endpoints.users; import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; import static org.hamcrest.Matchers.equalTo; import app.fyreplace.api.data.Block; @@ -31,7 +32,7 @@ public void countBlocked() { @Override public void beforeEach() { super.beforeEach(); - final var user = User.findByUsername("user_0"); + final var user = requireNonNull(User.findByUsername("user_0")); for (final var otherUser : User.list("username > 'user_10'")) { user.block(otherUser); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java index c7e6e72..dc281a0 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java @@ -1,6 +1,7 @@ package app.fyreplace.api.testing.endpoints.users; import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; import static java.util.stream.IntStream.range; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.in; @@ -63,7 +64,7 @@ public void listBlockedTooFar() { @Override public void beforeEach() { super.beforeEach(); - final var user = User.findByUsername("user_0"); + final var user = requireNonNull(User.findByUsername("user_0")); for (final var otherUser : User.list("username > 'user_10' and active = true")) { user.block(otherUser); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java index 3444c9f..7adddfb 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java @@ -1,6 +1,7 @@ package app.fyreplace.api.testing.endpoints.users; import static io.restassured.RestAssured.given; +import static java.util.Objects.requireNonNull; import static org.hamcrest.Matchers.isA; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -39,7 +40,7 @@ public void updateMeAvatar(final String fileType) throws IOException { } assertEquals(remoteFileCount + 1, StoredFile.count()); - final var user = User.findByUsername("user_0"); + final var user = requireNonNull(User.findByUsername("user_0")); assertNotNull(user.avatar); } @@ -58,7 +59,7 @@ public void updateMeAvatarWithInvalidType(final String fileType) throws IOExcept } assertEquals(remoteFileCount, StoredFile.count()); - final var user = User.findByUsername("user_0"); + final var user = requireNonNull(User.findByUsername("user_0")); assertNull(user.avatar); } @@ -68,7 +69,7 @@ public void updateMeAvatarWithoutInput() { final var remoteFileCount = StoredFile.count(); given().contentType(ContentType.BINARY).put("me/avatar").then().statusCode(415); assertEquals(remoteFileCount, StoredFile.count()); - final var user = User.findByUsername("user_0"); + final var user = requireNonNull(User.findByUsername("user_0")); assertNull(user.avatar); } } From 7859a0c4573b5f458c18acac1f982759e028b092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 4 May 2024 15:37:52 +0200 Subject: [PATCH 127/157] Delete subscriptions when necessary --- .../java/app/fyreplace/api/data/Post.java | 6 +++++ .../java/app/fyreplace/api/data/User.java | 6 +++++ .../endpoints/subscriptions/DeleteTests.java | 22 +++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/src/main/java/app/fyreplace/api/data/Post.java b/src/main/java/app/fyreplace/api/data/Post.java index 87591ee..545899d 100644 --- a/src/main/java/app/fyreplace/api/data/Post.java +++ b/src/main/java/app/fyreplace/api/data/Post.java @@ -33,6 +33,12 @@ public class Post extends AuthoredEntityBase implements Reportable { public static final Duration shelfLife = Duration.ofDays(7); + @Override + public void softDelete() { + super.softDelete(); + Subscription.delete("post", this); + } + @Override public void scrub() { super.scrub(); diff --git a/src/main/java/app/fyreplace/api/data/User.java b/src/main/java/app/fyreplace/api/data/User.java index 7ca5379..d81ad60 100644 --- a/src/main/java/app/fyreplace/api/data/User.java +++ b/src/main/java/app/fyreplace/api/data/User.java @@ -128,6 +128,12 @@ public Profile getProfile() { return new Profile(id, username, avatar != null ? avatar.toString() : null); } + @Override + public void softDelete() { + super.softDelete(); + Subscription.delete("user", this); + } + @SuppressWarnings("unused") @PostRemove final void postRemove() { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/DeleteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/DeleteTests.java index 3f60de5..66b0818 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/DeleteTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/DeleteTests.java @@ -3,9 +3,12 @@ import static io.restassured.RestAssured.given; import static org.junit.jupiter.api.Assertions.assertEquals; +import app.fyreplace.api.data.Post; import app.fyreplace.api.data.Subscription; +import app.fyreplace.api.data.User; import app.fyreplace.api.endpoints.SubscriptionsEndpoint; import app.fyreplace.api.testing.SubscriptionTestsBase; +import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; @@ -24,6 +27,25 @@ public void delete() { subscriptions.size() - 1, Subscription.count("user.username = 'user_0' and unreadCommentCount > 0")); } + @Test + public void deletePost() { + final var subscriptions = + Subscription.list("user.username = 'user_0' and unreadCommentCount > 0"); + QuarkusTransaction.requiringNew() + .run(() -> Post.findById(subscriptions.getFirst().post.id).softDelete()); + assertEquals( + 0, Subscription.count("post.id = ?1 and unreadCommentCount > 0", subscriptions.getFirst().post.id)); + } + + @Test + public void deleteUser() { + final var subscriptions = + Subscription.list("user.username = 'user_0' and unreadCommentCount > 0"); + QuarkusTransaction.requiringNew() + .run(() -> User.findById(subscriptions.getFirst().user.id).softDelete()); + assertEquals(0, Subscription.count("user.username = 'user_0' and unreadCommentCount > 0")); + } + @Test @TestSecurity(user = "user_1") public void deleteOtherSubscription() { From e4aa9167b265ccc1942f021c4b692015032552ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 25 May 2024 22:09:02 +0200 Subject: [PATCH 128/157] Send user ID in JWT --- src/main/java/app/fyreplace/api/services/JwtService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/app/fyreplace/api/services/JwtService.java b/src/main/java/app/fyreplace/api/services/JwtService.java index 8d8556b..9d98a41 100644 --- a/src/main/java/app/fyreplace/api/services/JwtService.java +++ b/src/main/java/app/fyreplace/api/services/JwtService.java @@ -13,7 +13,8 @@ public final class JwtService { public String makeJwt(final User user) { return Jwt.issuer(appUrl) - .subject(user.username) + .subject(user.id.toString()) + .upn(user.username) .groups(user.getGroups()) .expiresIn(Duration.ofDays(3)) .sign(); From 8521815ba05703dc647fa4fc8e83a666ccb25b53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Mon, 13 May 2024 13:14:36 +0200 Subject: [PATCH 129/157] Avoid duplicate database request --- .../java/app/fyreplace/api/data/User.java | 13 +++++++++ .../fyreplace/api/filters/SentryFilter.java | 28 ------------------- 2 files changed, 13 insertions(+), 28 deletions(-) delete mode 100644 src/main/java/app/fyreplace/api/filters/SentryFilter.java diff --git a/src/main/java/app/fyreplace/api/data/User.java b/src/main/java/app/fyreplace/api/data/User.java index d81ad60..78a6170 100644 --- a/src/main/java/app/fyreplace/api/data/User.java +++ b/src/main/java/app/fyreplace/api/data/User.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.sentry.Sentry; import jakarta.annotation.Nullable; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; @@ -208,6 +209,18 @@ public static User getFromSecurityContext(final SecurityContext context, @Nullab throw new NotAuthorizedException("Bearer"); } + Sentry.configureScope(scope -> { + if (user != null) { + final var sentryUser = new io.sentry.protocol.User(); + sentryUser.setId(user.id.toString()); + sentryUser.setUsername(user.username); + sentryUser.setEmail(user.mainEmail != null ? user.mainEmail.email : null); + scope.setUser(sentryUser); + } else { + scope.setUser(null); + } + }); + return user; } diff --git a/src/main/java/app/fyreplace/api/filters/SentryFilter.java b/src/main/java/app/fyreplace/api/filters/SentryFilter.java deleted file mode 100644 index 7121790..0000000 --- a/src/main/java/app/fyreplace/api/filters/SentryFilter.java +++ /dev/null @@ -1,28 +0,0 @@ -package app.fyreplace.api.filters; - -import app.fyreplace.api.data.User; -import io.sentry.Sentry; -import jakarta.ws.rs.container.ContainerRequestContext; -import jakarta.ws.rs.container.ContainerRequestFilter; -import jakarta.ws.rs.ext.Provider; - -@SuppressWarnings("unused") -@Provider -public final class SentryFilter implements ContainerRequestFilter { - @Override - public void filter(final ContainerRequestContext context) { - final var user = User.getFromSecurityContext(context.getSecurityContext(), null, false); - - Sentry.configureScope(scope -> { - if (user != null) { - final var sentryUser = new io.sentry.protocol.User(); - sentryUser.setId(user.id.toString()); - sentryUser.setUsername(user.username); - sentryUser.setEmail(user.mainEmail != null ? user.mainEmail.email : null); - scope.setUser(sentryUser); - } else { - scope.setUser(null); - } - }); - } -} From fcc72ee7b0f8d507da8ddbc4175120b062b56d10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Mon, 13 May 2024 16:28:08 +0200 Subject: [PATCH 130/157] Add in-memory storage --- .github/workflows/validation.yml | 1 - .../app/fyreplace/api/data/StoredFile.java | 9 +++++- .../api/endpoints/StoredFilesEndpoint.java | 16 +++++++--- .../api/services/StorageService.java | 3 +- .../storage/LocalStorageServiceBase.java | 19 ++++++++++++ .../storage/file/FileStorageConfig.java | 9 ++++++ .../FileStorageService.java} | 24 +++----------- .../storage/local/LocalStorageConfig.java | 9 ------ .../storage/memory/MemoryStorageService.java | 31 +++++++++++++++++++ .../services/storage/s3/S3StorageService.java | 22 +++++-------- src/main/resources/application.yaml | 9 ++++-- 11 files changed, 99 insertions(+), 53 deletions(-) create mode 100644 src/main/java/app/fyreplace/api/services/storage/LocalStorageServiceBase.java create mode 100644 src/main/java/app/fyreplace/api/services/storage/file/FileStorageConfig.java rename src/main/java/app/fyreplace/api/services/storage/{local/LocalStorageService.java => file/FileStorageService.java} (66%) delete mode 100644 src/main/java/app/fyreplace/api/services/storage/local/LocalStorageConfig.java create mode 100644 src/main/java/app/fyreplace/api/services/storage/memory/MemoryStorageService.java diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 9301598..065faf6 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -48,4 +48,3 @@ jobs: SMALLRYE_JWT_SIGN_KEY: ${{ secrets.SMALLRYE_JWT_SIGN_KEY }} APP_URL: https://api.fyreplace.example.org APP_FRONT_URL: https://fyreplace.example.org - APP_STORAGE_LOCAL_PATH: /tmp/fyreplace/storage diff --git a/src/main/java/app/fyreplace/api/data/StoredFile.java b/src/main/java/app/fyreplace/api/data/StoredFile.java index 67482c9..0ab7c2d 100644 --- a/src/main/java/app/fyreplace/api/data/StoredFile.java +++ b/src/main/java/app/fyreplace/api/data/StoredFile.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import io.quarkus.arc.Arc; +import io.sentry.Sentry; import jakarta.annotation.Nullable; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -14,6 +15,7 @@ import jakarta.persistence.Table; import jakarta.persistence.Transient; import java.io.IOException; +import java.net.URISyntaxException; import java.nio.file.Paths; @Entity @@ -46,7 +48,12 @@ public StoredFile(final String directory, final String name, @Nullable final byt @Override public String toString() { - return storageService.getUri(path).toString(); + try { + return storageService.getUri(path).toString(); + } catch (final URISyntaxException e) { + Sentry.captureException(e); + return ""; + } } public void store(final byte[] data) throws IOException { diff --git a/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java index 3a91b4f..75dbebe 100644 --- a/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java @@ -2,9 +2,10 @@ import app.fyreplace.api.services.MimeTypeService; import app.fyreplace.api.services.StorageService; -import io.quarkus.arc.properties.IfBuildProperty; +import io.quarkus.arc.properties.UnlessBuildProperty; import jakarta.inject.Inject; import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.core.Response; @@ -12,7 +13,7 @@ import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; @Path("stored-files") -@IfBuildProperty(name = "app.storage.type", stringValue = "local") +@UnlessBuildProperty(name = "app.storage.type", stringValue = "s3") public final class StoredFilesEndpoint { @Inject StorageService storageService; @@ -24,8 +25,13 @@ public final class StoredFilesEndpoint { @Path("{path:.*}") @APIResponse(responseCode = "200") @APIResponse(responseCode = "404") - public Response retrieve(@PathParam("path") final String path) throws IOException { - final var data = storageService.fetch(path); - return Response.ok(data).type(mimeTypeService.getMimeType(data)).build(); + public Response retrieve(@PathParam("path") final String path) { + try { + final byte[] data; + data = storageService.fetch(path); + return Response.ok(data).type(mimeTypeService.getMimeType(data)).build(); + } catch (final IOException e) { + throw new NotFoundException(); + } } } diff --git a/src/main/java/app/fyreplace/api/services/StorageService.java b/src/main/java/app/fyreplace/api/services/StorageService.java index 4ba3830..2e72bb9 100644 --- a/src/main/java/app/fyreplace/api/services/StorageService.java +++ b/src/main/java/app/fyreplace/api/services/StorageService.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.net.URI; +import java.net.URISyntaxException; public interface StorageService { byte[] fetch(final String path) throws IOException; @@ -10,5 +11,5 @@ public interface StorageService { void remove(final String path) throws IOException; - URI getUri(final String path); + URI getUri(final String path) throws URISyntaxException; } diff --git a/src/main/java/app/fyreplace/api/services/storage/LocalStorageServiceBase.java b/src/main/java/app/fyreplace/api/services/storage/LocalStorageServiceBase.java new file mode 100644 index 0000000..40f7f5d --- /dev/null +++ b/src/main/java/app/fyreplace/api/services/storage/LocalStorageServiceBase.java @@ -0,0 +1,19 @@ +package app.fyreplace.api.services.storage; + +import app.fyreplace.api.endpoints.StoredFilesEndpoint; +import app.fyreplace.api.services.StorageService; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.UriBuilder; +import java.net.URI; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +public abstract class LocalStorageServiceBase implements StorageService { + @ConfigProperty(name = "app.url") + String appUrl; + + @Override + public URI getUri(final String path) { + final var pathBase = StoredFilesEndpoint.class.getAnnotation(Path.class).value(); + return UriBuilder.fromUri(appUrl).path(pathBase).path(path).build(); + } +} diff --git a/src/main/java/app/fyreplace/api/services/storage/file/FileStorageConfig.java b/src/main/java/app/fyreplace/api/services/storage/file/FileStorageConfig.java new file mode 100644 index 0000000..c920555 --- /dev/null +++ b/src/main/java/app/fyreplace/api/services/storage/file/FileStorageConfig.java @@ -0,0 +1,9 @@ +package app.fyreplace.api.services.storage.file; + +import io.smallrye.config.ConfigMapping; +import java.nio.file.Path; + +@ConfigMapping(prefix = "app.storage.file") +public interface FileStorageConfig { + Path path(); +} diff --git a/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java b/src/main/java/app/fyreplace/api/services/storage/file/FileStorageService.java similarity index 66% rename from src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java rename to src/main/java/app/fyreplace/api/services/storage/file/FileStorageService.java index 68ba574..a7f4419 100644 --- a/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageService.java +++ b/src/main/java/app/fyreplace/api/services/storage/file/FileStorageService.java @@ -1,32 +1,24 @@ -package app.fyreplace.api.services.storage.local; +package app.fyreplace.api.services.storage.file; -import app.fyreplace.api.endpoints.StoredFilesEndpoint; -import app.fyreplace.api.services.StorageService; +import app.fyreplace.api.services.storage.LocalStorageServiceBase; import io.quarkus.arc.Unremovable; import io.quarkus.arc.properties.IfBuildProperty; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.NotFoundException; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.core.UriBuilder; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; -import java.net.URI; -import org.eclipse.microprofile.config.inject.ConfigProperty; @SuppressWarnings("unused") @ApplicationScoped @Unremovable -@IfBuildProperty(name = "app.storage.type", stringValue = "local") -public final class LocalStorageService implements StorageService { - @ConfigProperty(name = "app.url") - String appUrl; - +@IfBuildProperty(name = "app.storage.type", stringValue = "file") +public final class FileStorageService extends LocalStorageServiceBase { @Inject - LocalStorageConfig config; + FileStorageConfig config; @Override public byte[] fetch(final String path) throws IOException { @@ -54,12 +46,6 @@ public void remove(final String path) { getFile(path).delete(); } - @Override - public URI getUri(final String path) { - final var pathBase = StoredFilesEndpoint.class.getAnnotation(Path.class).value(); - return UriBuilder.fromUri(appUrl).path(pathBase).path(path).build(); - } - private File getFile(final String path) { final var file = config.path().resolve(path).toFile(); diff --git a/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageConfig.java b/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageConfig.java deleted file mode 100644 index 6a43a2a..0000000 --- a/src/main/java/app/fyreplace/api/services/storage/local/LocalStorageConfig.java +++ /dev/null @@ -1,9 +0,0 @@ -package app.fyreplace.api.services.storage.local; - -import io.smallrye.config.ConfigMapping; -import java.nio.file.Path; - -@ConfigMapping(prefix = "app.storage.local") -public interface LocalStorageConfig { - Path path(); -} diff --git a/src/main/java/app/fyreplace/api/services/storage/memory/MemoryStorageService.java b/src/main/java/app/fyreplace/api/services/storage/memory/MemoryStorageService.java new file mode 100644 index 0000000..63b0134 --- /dev/null +++ b/src/main/java/app/fyreplace/api/services/storage/memory/MemoryStorageService.java @@ -0,0 +1,31 @@ +package app.fyreplace.api.services.storage.memory; + +import app.fyreplace.api.services.storage.LocalStorageServiceBase; +import io.quarkus.arc.Unremovable; +import io.quarkus.arc.properties.IfBuildProperty; +import jakarta.enterprise.context.ApplicationScoped; +import java.util.HashMap; +import java.util.Map; + +@SuppressWarnings("unused") +@ApplicationScoped +@Unremovable +@IfBuildProperty(name = "app.storage.type", stringValue = "memory") +public final class MemoryStorageService extends LocalStorageServiceBase { + private final Map storage = new HashMap<>(); + + @Override + public byte[] fetch(final String path) { + return storage.get(path); + } + + @Override + public void store(final String path, final byte[] data) { + storage.put(path, data); + } + + @Override + public void remove(final String path) { + storage.remove(path); + } +} diff --git a/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java b/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java index e75990e..ccf5e8c 100644 --- a/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java +++ b/src/main/java/app/fyreplace/api/services/storage/s3/S3StorageService.java @@ -15,7 +15,6 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.List; -import org.jboss.logging.Logger; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.NoSuchKeyException; @@ -25,8 +24,6 @@ @Unremovable @IfBuildProperty(name = "app.storage.type", stringValue = "s3") public final class S3StorageService implements StorageService { - private static final Logger logger = Logger.getLogger(S3StorageService.class); - @Inject S3StorageConfig config; @@ -75,17 +72,12 @@ public void remove(final String path) { } @Override - public URI getUri(final String path) { - try { - return client.utilities() - .getUrl(b -> { - b.bucket(config.bucket()).key(path); - config.customEndpoint().ifPresent(b::endpoint); - }) - .toURI(); - } catch (final URISyntaxException e) { - logger.error("Failed to get URI for S3 object", e); - return null; - } + public URI getUri(final String path) throws URISyntaxException { + return client.utilities() + .getUrl(b -> { + b.bucket(config.bucket()).key(path); + config.customEndpoint().ifPresent(b::endpoint); + }) + .toURI(); } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index bc8bcf8..59a555c 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -41,8 +41,8 @@ app: paging: size: 12 storage: - type: "local" - local: + type: "file" + file: path: "" s3: bucket: "" @@ -65,6 +65,8 @@ app: policy: "permit" app: use-example-data: true + storage: + type: "memory" '%test': quarkus: datasource: @@ -78,3 +80,6 @@ app: origins: "/.*/" scheduler: enabled: false + app: + storage: + type: "memory" From c459b614b13de1f32a1cb401f7a437fb34c37dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Mon, 13 May 2024 16:36:03 +0200 Subject: [PATCH 131/157] Postpone push notifications --- .../api/data/PushNotificationToken.java | 36 -------------- .../data/PushNotificationTokenCreation.java | 6 --- .../fyreplace/api/data/dev/DataSeeder.java | 2 - .../api/endpoints/CommentsEndpoint.java | 7 --- .../PushNotificationTokensEndpoint.java | 40 ---------------- .../PushNotificationDispatcher.java | 7 --- src/main/resources/db/changeLog.yaml | 44 ----------------- .../pushnotificationtokens/UpdateTests.java | 48 ------------------- 8 files changed, 190 deletions(-) delete mode 100644 src/main/java/app/fyreplace/api/data/PushNotificationToken.java delete mode 100644 src/main/java/app/fyreplace/api/data/PushNotificationTokenCreation.java delete mode 100644 src/main/java/app/fyreplace/api/endpoints/PushNotificationTokensEndpoint.java delete mode 100644 src/main/java/app/fyreplace/api/pushnotifications/PushNotificationDispatcher.java delete mode 100644 src/test/java/app/fyreplace/api/testing/endpoints/pushnotificationtokens/UpdateTests.java diff --git a/src/main/java/app/fyreplace/api/data/PushNotificationToken.java b/src/main/java/app/fyreplace/api/data/PushNotificationToken.java deleted file mode 100644 index e5bff34..0000000 --- a/src/main/java/app/fyreplace/api/data/PushNotificationToken.java +++ /dev/null @@ -1,36 +0,0 @@ -package app.fyreplace.api.data; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import org.hibernate.annotations.OnDelete; -import org.hibernate.annotations.OnDeleteAction; - -@Entity -@Table(name = "push_notification_tokens") -public class PushNotificationToken extends EntityBase { - @ManyToOne(optional = false) - @OnDelete(action = OnDeleteAction.CASCADE) - @JsonIgnore - public User user; - - @Column(nullable = false) - @Enumerated(EnumType.STRING) - public Service service; - - @Column(nullable = false) - public String token; - - public enum Service { - APPLE, - FIREBASE, - HUAWEI, - SAMSUNG, - WEB, - WINDOWS - } -} diff --git a/src/main/java/app/fyreplace/api/data/PushNotificationTokenCreation.java b/src/main/java/app/fyreplace/api/data/PushNotificationTokenCreation.java deleted file mode 100644 index 1a4914f..0000000 --- a/src/main/java/app/fyreplace/api/data/PushNotificationTokenCreation.java +++ /dev/null @@ -1,6 +0,0 @@ -package app.fyreplace.api.data; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - -public record PushNotificationTokenCreation(@NotNull PushNotificationToken.Service service, @NotBlank String token) {} diff --git a/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java b/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java index fe23a2e..baeb67e 100644 --- a/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java +++ b/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java @@ -8,7 +8,6 @@ import app.fyreplace.api.data.Email; import app.fyreplace.api.data.Password; import app.fyreplace.api.data.Post; -import app.fyreplace.api.data.PushNotificationToken; import app.fyreplace.api.data.RandomCode; import app.fyreplace.api.data.Report; import app.fyreplace.api.data.StoredFile; @@ -63,7 +62,6 @@ public void deleteData() { Password.deleteAll(); RandomCode.deleteAll(); Block.deleteAll(); - PushNotificationToken.deleteAll(); Report.deleteAll(); User.deleteAll(); Subscription.deleteAll(); diff --git a/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java index c3d63ab..83fcbb1 100644 --- a/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java @@ -8,12 +8,9 @@ import app.fyreplace.api.data.Subscription; import app.fyreplace.api.data.User; import app.fyreplace.api.exceptions.ForbiddenException; -import app.fyreplace.api.pushnotifications.PushNotificationDispatcher; import io.quarkus.cache.CacheResult; import io.quarkus.security.Authenticated; import jakarta.annotation.Nullable; -import jakarta.enterprise.inject.Instance; -import jakarta.inject.Inject; import jakarta.transaction.Transactional; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; @@ -42,9 +39,6 @@ public final class CommentsEndpoint { @ConfigProperty(name = "app.paging.size") int pagingSize; - @Inject - Instance pushNotificationDispatchers; - @Context SecurityContext context; @@ -88,7 +82,6 @@ public Response create(@PathParam("id") final UUID id, @Valid @NotNull final Com comment.anonymous = input.anonymous(); comment.persist(); comment.setCurrentUser(user); - pushNotificationDispatchers.forEach(d -> d.dispatch(comment)); return Response.status(Status.CREATED).entity(comment).build(); } diff --git a/src/main/java/app/fyreplace/api/endpoints/PushNotificationTokensEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/PushNotificationTokensEndpoint.java deleted file mode 100644 index 9b250c2..0000000 --- a/src/main/java/app/fyreplace/api/endpoints/PushNotificationTokensEndpoint.java +++ /dev/null @@ -1,40 +0,0 @@ -package app.fyreplace.api.endpoints; - -import app.fyreplace.api.data.PushNotificationToken; -import app.fyreplace.api.data.PushNotificationTokenCreation; -import app.fyreplace.api.data.User; -import io.quarkus.security.Authenticated; -import jakarta.transaction.Transactional; -import jakarta.validation.Valid; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.SecurityContext; -import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; - -@Path("push-notification-tokens") -public final class PushNotificationTokensEndpoint { - @Context - SecurityContext context; - - @PUT - @Authenticated - @Transactional - @APIResponse(responseCode = "200") - @APIResponse(responseCode = "400") - public PushNotificationToken update(@Valid final PushNotificationTokenCreation input) { - final var user = User.getFromSecurityContext(context); - var token = PushNotificationToken.find("user = ?1 and token = ?2", user, input.token()) - .firstResult(); - - if (token == null) { - token = new PushNotificationToken(); - token.user = user; - token.service = input.service(); - token.token = input.token(); - token.persist(); - } - - return token; - } -} diff --git a/src/main/java/app/fyreplace/api/pushnotifications/PushNotificationDispatcher.java b/src/main/java/app/fyreplace/api/pushnotifications/PushNotificationDispatcher.java deleted file mode 100644 index 342f63a..0000000 --- a/src/main/java/app/fyreplace/api/pushnotifications/PushNotificationDispatcher.java +++ /dev/null @@ -1,7 +0,0 @@ -package app.fyreplace.api.pushnotifications; - -import app.fyreplace.api.data.Comment; - -public interface PushNotificationDispatcher { - void dispatch(final Comment comment); -} diff --git a/src/main/resources/db/changeLog.yaml b/src/main/resources/db/changeLog.yaml index 5d2d218..104c53a 100644 --- a/src/main/resources/db/changeLog.yaml +++ b/src/main/resources/db/changeLog.yaml @@ -344,50 +344,6 @@ databaseChangeLog: referencedColumnNames: "id" referencedTableName: "users" validate: true -- changeSet: - id: "push_notification_tokens-0001" - author: "generated" - changes: - - createTable: - columns: - - column: - constraints: - nullable: false - primaryKey: true - primaryKeyName: "push_notification_tokens_pkey" - name: "id" - type: "UUID" - - column: - constraints: - nullable: false - name: "user_id" - type: "UUID" - - column: - constraints: - nullable: false - name: "service" - type: "VARCHAR(255)" - - column: - constraints: - nullable: false - name: "token" - type: "VARCHAR(255)" - tableName: "push_notification_tokens" -- changeSet: - id: "push_notification_tokens-0002" - author: "generated" - changes: - - addForeignKeyConstraint: - baseColumnNames: "user_id" - baseTableName: "push_notification_tokens" - constraintName: "fkg63ygyp1y6pvmypol9d7gp40d" - deferrable: false - initiallyDeferred: false - onDelete: "CASCADE" - onUpdate: "NO ACTION" - referencedColumnNames: "id" - referencedTableName: "users" - validate: true - changeSet: id: "reports-0001" author: "generated" diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/pushnotificationtokens/UpdateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/pushnotificationtokens/UpdateTests.java deleted file mode 100644 index 13f25b5..0000000 --- a/src/test/java/app/fyreplace/api/testing/endpoints/pushnotificationtokens/UpdateTests.java +++ /dev/null @@ -1,48 +0,0 @@ -package app.fyreplace.api.testing.endpoints.pushnotificationtokens; - -import static io.restassured.RestAssured.given; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import app.fyreplace.api.data.PushNotificationToken; -import app.fyreplace.api.data.PushNotificationTokenCreation; -import app.fyreplace.api.endpoints.PushNotificationTokensEndpoint; -import app.fyreplace.api.testing.UserTestsBase; -import io.quarkus.test.common.http.TestHTTPEndpoint; -import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.security.TestSecurity; -import io.restassured.http.ContentType; -import org.junit.jupiter.api.Test; - -@QuarkusTest -@TestHTTPEndpoint(PushNotificationTokensEndpoint.class) -public final class UpdateTests extends UserTestsBase { - @Test - @TestSecurity(user = "user_0") - public void update() { - final var tokenCount = PushNotificationToken.count(); - given().contentType(ContentType.JSON) - .body(new PushNotificationTokenCreation(PushNotificationToken.Service.WEB, "token")) - .put() - .then() - .statusCode(200); - assertEquals(tokenCount + 1, PushNotificationToken.count()); - final var token = PushNotificationToken.find( - "user.username = 'user_0' and token = 'token'") - .firstResult(); - assertEquals(PushNotificationToken.Service.WEB, token.service); - } - - @Test - @TestSecurity(user = "user_0") - public void updateTwice() { - final var tokenCount = PushNotificationToken.count(); - final var input = new PushNotificationTokenCreation(PushNotificationToken.Service.WEB, "token"); - given().contentType(ContentType.JSON).body(input).put().then().statusCode(200); - given().contentType(ContentType.JSON).body(input).put().then().statusCode(200); - assertEquals(tokenCount + 1, PushNotificationToken.count()); - final var token = PushNotificationToken.find( - "user.username = 'user_0' and token = 'token'") - .firstResult(); - assertEquals(PushNotificationToken.Service.WEB, token.service); - } -} From 9b16a04c4d3df88dd0112e2ce4fcc4f25d43cc65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Tue, 28 May 2024 11:11:43 +0200 Subject: [PATCH 132/157] Always allow using stored files endpoints --- .../api/endpoints/StoredFilesEndpoint.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java index 75dbebe..c8eeac0 100644 --- a/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java @@ -2,19 +2,25 @@ import app.fyreplace.api.services.MimeTypeService; import app.fyreplace.api.services.StorageService; -import io.quarkus.arc.properties.UnlessBuildProperty; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.RedirectionException; import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; @Path("stored-files") -@UnlessBuildProperty(name = "app.storage.type", stringValue = "s3") public final class StoredFilesEndpoint { + @ConfigProperty(name = "app.url") + String appUrl; + @Inject StorageService storageService; @@ -24,8 +30,16 @@ public final class StoredFilesEndpoint { @GET @Path("{path:.*}") @APIResponse(responseCode = "200") + @APIResponse(responseCode = "303") @APIResponse(responseCode = "404") - public Response retrieve(@PathParam("path") final String path) { + public Response retrieve(@PathParam("path") final String path) throws URISyntaxException { + final var appUri = new URI(appUrl); + final var requestUri = storageService.getUri(path); + + if (!appUri.getHost().equals(requestUri.getHost())) { + throw new RedirectionException(Status.SEE_OTHER, requestUri); + } + try { final byte[] data; data = storageService.fetch(path); From 25a47df8b1f15e47cbc5f919219563deab783282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Tue, 28 May 2024 11:21:56 +0200 Subject: [PATCH 133/157] Use PUT for idempotent request --- .../api/endpoints/EmailsEndpoint.java | 5 ++-- ...SetMainTests.java => UpdateMainTests.java} | 24 +++++++++---------- 2 files changed, 15 insertions(+), 14 deletions(-) rename src/test/java/app/fyreplace/api/testing/endpoints/emails/{SetMainTests.java => UpdateMainTests.java} (76%) diff --git a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java index bb4da47..9f384df 100644 --- a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java @@ -21,6 +21,7 @@ import jakarta.ws.rs.GET; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.QueryParam; @@ -97,13 +98,13 @@ public Response delete(@PathParam("id") final UUID id) { return Response.noContent().build(); } - @POST + @PUT @Path("{id}/main") @Authenticated @Transactional @APIResponse(responseCode = "200") @APIResponse(responseCode = "404") - public Response setMain(@PathParam("id") final UUID id) { + public Response updateMain(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context); final var email = Email.find("user = ?1 and id = ?2", user, id).firstResult(); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/UpdateMainTests.java similarity index 76% rename from src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/emails/UpdateMainTests.java index dd21c63..dc17621 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/SetMainTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/UpdateMainTests.java @@ -19,50 +19,50 @@ @QuarkusTest @TestHTTPEndpoint(EmailsEndpoint.class) -public final class SetMainTests extends UserTestsBase { +public final class UpdateMainTests extends UserTestsBase { private Email secondaryEmail; @Test @TestSecurity(user = "user_0") - public void setMain() { + public void updateMain() { assertFalse(secondaryEmail.isMain()); - given().post(secondaryEmail.id + "/main").then().statusCode(200); + given().put(secondaryEmail.id + "/main").then().statusCode(200); secondaryEmail = Email.findById(secondaryEmail.id); assertTrue(secondaryEmail.isMain()); } @Test @TestSecurity(user = "user_0") - public void setMainTwice() { + public void updateMainTwice() { assertFalse(secondaryEmail.isMain()); - given().post(secondaryEmail.id + "/main").then().statusCode(200); - given().post(secondaryEmail.id + "/main").then().statusCode(200); + given().put(secondaryEmail.id + "/main").then().statusCode(200); + given().put(secondaryEmail.id + "/main").then().statusCode(200); secondaryEmail = Email.findById(secondaryEmail.id); assertTrue(secondaryEmail.isMain()); } @Test @TestSecurity(user = "user_0") - public void setMainWithUnverifiedEmail() { + public void updateMainWithUnverifiedEmail() { QuarkusTransaction.requiringNew().run(() -> Email.update("verified = false where id = ?1", secondaryEmail.id)); - given().post(secondaryEmail.id + "/main").then().statusCode(403); + given().put(secondaryEmail.id + "/main").then().statusCode(403); secondaryEmail = Email.findById(secondaryEmail.id); assertFalse(secondaryEmail.isMain()); } @Test @TestSecurity(user = "user_0") - public void setMainWithOtherEmail() { + public void updateMainWithOtherEmail() { final var otherUser = requireNonNull(User.findByUsername("user_1")); - given().post(otherUser.mainEmail.id + "/main").then().statusCode(404); + given().put(otherUser.mainEmail.id + "/main").then().statusCode(404); secondaryEmail = Email.findById(secondaryEmail.id); assertFalse(secondaryEmail.isMain()); } @Test @TestSecurity(user = "user_0") - public void setMainWithNonExistentEmail() { - given().post(fakeId + "/main").then().statusCode(404); + public void updateMainWithNonExistentEmail() { + given().put(fakeId + "/main").then().statusCode(404); secondaryEmail = Email.findById(secondaryEmail.id); assertFalse(secondaryEmail.isMain()); } From f13041646b943051510ec4c1a0e508e776a7ed61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Tue, 28 May 2024 11:42:56 +0200 Subject: [PATCH 134/157] Format code --- src/main/java/app/fyreplace/api/endpoints/DevEndpoint.java | 2 +- src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/app/fyreplace/api/endpoints/DevEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/DevEndpoint.java index 609e61f..d6c3ba2 100644 --- a/src/main/java/app/fyreplace/api/endpoints/DevEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/DevEndpoint.java @@ -43,7 +43,7 @@ public String retrieveToken(@PathParam("username") final String username) { responseCode = "200", content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) @APIResponse(responseCode = "404") - public String retrievePassword(@PathParam("password") final String password) { + public String retrievePasswordHash(@PathParam("password") final String password) { return BcryptUtil.bcryptHash(password); } } diff --git a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java index 9f384df..026d35e 100644 --- a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java @@ -30,7 +30,6 @@ import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; import jakarta.ws.rs.core.SecurityContext; -import java.util.List; import java.util.UUID; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.openapi.annotations.media.Content; @@ -51,7 +50,7 @@ public final class EmailsEndpoint { @GET @Authenticated @APIResponse(responseCode = "200") - public List list(@QueryParam("page") @PositiveOrZero final int page) { + public Iterable list(@QueryParam("page") @PositiveOrZero final int page) { final var user = User.getFromSecurityContext(context); return Email.find("user", Sort.by("email"), user).page(page, pagingSize).list(); } From 5dc027c9c384491ddfbaaed6e6abcec924aaf60e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Tue, 28 May 2024 17:50:19 +0200 Subject: [PATCH 135/157] Provide "random" user tint --- src/main/java/app/fyreplace/api/data/User.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/main/java/app/fyreplace/api/data/User.java b/src/main/java/app/fyreplace/api/data/User.java index 78a6170..1142af3 100644 --- a/src/main/java/app/fyreplace/api/data/User.java +++ b/src/main/java/app/fyreplace/api/data/User.java @@ -14,12 +14,14 @@ import jakarta.persistence.Table; import jakarta.ws.rs.NotAuthorizedException; import jakarta.ws.rs.core.SecurityContext; +import java.awt.Color; import java.time.Instant; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; +import java.util.zip.CRC32; import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; @@ -126,7 +128,18 @@ public Set getGroups() { @JsonIgnore public Profile getProfile() { - return new Profile(id, username, avatar != null ? avatar.toString() : null); + return new Profile(id, username, avatar != null ? avatar.toString() : null, getTint()); + } + + public String getTint() { + final var crc = new CRC32(); + crc.update(username.getBytes()); + final var hue = (float) (crc.getValue() / Math.pow(2, 32)); + final var h = hue * 6; + final var variance = (h - (float) Math.floor(h)) * 0.25f; + final var brightness = (int) h % 2 == 0 ? 1f - variance : 0.75f + variance; + final var color = Color.HSBtoRGB(hue, 0.5f, brightness); + return "#%02X%02X%02X".formatted((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF); } @Override @@ -236,5 +249,5 @@ public enum BanCount { ONE_TOO_MANY } - public record Profile(UUID id, String username, String avatar) {} + public record Profile(UUID id, String username, String avatar, String tint) {} } From fc27cd682bd29fc7392be81ec92b8a3be90f1b3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Mon, 27 May 2024 21:16:23 +0200 Subject: [PATCH 136/157] Produce better OpenAPI schema --- .../api/data/AuthoredEntityBase.java | 2 + .../java/app/fyreplace/api/data/Block.java | 3 ++ .../java/app/fyreplace/api/data/Comment.java | 2 + .../java/app/fyreplace/api/data/Email.java | 4 ++ .../app/fyreplace/api/data/EntityBase.java | 2 + .../java/app/fyreplace/api/data/Post.java | 5 +++ .../java/app/fyreplace/api/data/Report.java | 4 ++ .../app/fyreplace/api/data/Reportable.java | 2 + .../app/fyreplace/api/data/StoredFile.java | 2 + .../app/fyreplace/api/data/Subscription.java | 3 ++ .../api/data/TimestampedEntityBase.java | 2 + .../java/app/fyreplace/api/data/User.java | 13 +++++- .../api/endpoints/ChaptersEndpoint.java | 22 ++++++---- .../api/endpoints/CommentsEndpoint.java | 23 +++++----- .../fyreplace/api/endpoints/DevEndpoint.java | 6 ++- .../api/endpoints/EmailsEndpoint.java | 21 ++++----- .../api/endpoints/PostsEndpoint.java | 43 ++++++++++--------- .../api/endpoints/ReportsEndpoint.java | 2 +- .../api/endpoints/StoredFilesEndpoint.java | 6 +-- .../api/endpoints/SubscriptionsEndpoint.java | 10 ++--- .../api/endpoints/TokensEndpoint.java | 12 +++--- .../api/endpoints/UsersEndpoint.java | 43 ++++++++++--------- .../fyreplace/api/filters/OpenAPIFilter.java | 18 ++++++++ 23 files changed, 161 insertions(+), 89 deletions(-) create mode 100644 src/main/java/app/fyreplace/api/filters/OpenAPIFilter.java diff --git a/src/main/java/app/fyreplace/api/data/AuthoredEntityBase.java b/src/main/java/app/fyreplace/api/data/AuthoredEntityBase.java index 4bcf1a0..eb43dbd 100644 --- a/src/main/java/app/fyreplace/api/data/AuthoredEntityBase.java +++ b/src/main/java/app/fyreplace/api/data/AuthoredEntityBase.java @@ -7,6 +7,7 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.MappedSuperclass; import jakarta.persistence.Transient; +import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; @@ -18,6 +19,7 @@ public abstract class AuthoredEntityBase extends SoftDeletableEntityBase { public User author; @Column(nullable = false) + @Schema(required = true) public boolean anonymous = false; @Transient diff --git a/src/main/java/app/fyreplace/api/data/Block.java b/src/main/java/app/fyreplace/api/data/Block.java index 33be551..0c62c8f 100644 --- a/src/main/java/app/fyreplace/api/data/Block.java +++ b/src/main/java/app/fyreplace/api/data/Block.java @@ -5,6 +5,7 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; +import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; @@ -13,9 +14,11 @@ public class Block extends EntityBase { @ManyToOne(optional = false, fetch = FetchType.EAGER) @OnDelete(action = OnDeleteAction.CASCADE) + @Schema(required = true) public User source; @ManyToOne(optional = false, fetch = FetchType.EAGER) @OnDelete(action = OnDeleteAction.CASCADE) + @Schema(required = true) public User target; } diff --git a/src/main/java/app/fyreplace/api/data/Comment.java b/src/main/java/app/fyreplace/api/data/Comment.java index f3e6ffa..67300c6 100644 --- a/src/main/java/app/fyreplace/api/data/Comment.java +++ b/src/main/java/app/fyreplace/api/data/Comment.java @@ -8,6 +8,7 @@ import jakarta.persistence.Index; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; @@ -22,6 +23,7 @@ public class Comment extends AuthoredEntityBase implements Comparable, public Post post; @Column(length = 1500, nullable = false) + @Schema(required = true) public String text; @Override diff --git a/src/main/java/app/fyreplace/api/data/Email.java b/src/main/java/app/fyreplace/api/data/Email.java index 15a4e66..5dc1922 100644 --- a/src/main/java/app/fyreplace/api/data/Email.java +++ b/src/main/java/app/fyreplace/api/data/Email.java @@ -6,6 +6,7 @@ import jakarta.persistence.Entity; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; @@ -18,12 +19,15 @@ public class Email extends EntityBase { public User user; @Column(length = 254, unique = true, nullable = false) + @Schema(required = true) public String email; @Column(nullable = false) + @Schema(required = true) public boolean verified = false; @JsonProperty("main") + @Schema(required = true) public boolean isMain() { return id.equals(user.mainEmail.id); } diff --git a/src/main/java/app/fyreplace/api/data/EntityBase.java b/src/main/java/app/fyreplace/api/data/EntityBase.java index 74f23fe..cc8c66f 100644 --- a/src/main/java/app/fyreplace/api/data/EntityBase.java +++ b/src/main/java/app/fyreplace/api/data/EntityBase.java @@ -6,6 +6,7 @@ import jakarta.persistence.MappedSuperclass; import java.util.UUID; import lombok.Getter; +import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.hibernate.annotations.UuidGenerator; @Getter @@ -14,6 +15,7 @@ public abstract class EntityBase extends PanacheEntityBase { @Id @GeneratedValue @UuidGenerator(style = UuidGenerator.Style.RANDOM) + @Schema(required = true) public UUID id; public void refresh() { diff --git a/src/main/java/app/fyreplace/api/data/Post.java b/src/main/java/app/fyreplace/api/data/Post.java index 545899d..6ca9ff2 100644 --- a/src/main/java/app/fyreplace/api/data/Post.java +++ b/src/main/java/app/fyreplace/api/data/Post.java @@ -12,11 +12,14 @@ import java.time.Duration; import java.time.Instant; import java.util.List; +import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.hibernate.annotations.Formula; @Entity @Table(name = "posts") public class Post extends AuthoredEntityBase implements Reportable { + @Column(nullable = false) + @Schema(required = true) public boolean published = false; @Column(nullable = false) @@ -25,10 +28,12 @@ public class Post extends AuthoredEntityBase implements Reportable { @SuppressWarnings("unused") @Formula("(select count(*) from comments where comments.post_id = id)") + @Schema(required = true) public long commentCount; @SuppressWarnings("unused") @Formula("(select count(*) from votes where votes.post_id = id)") + @Schema(required = true) public long voteCount; public static final Duration shelfLife = Duration.ofDays(7); diff --git a/src/main/java/app/fyreplace/api/data/Report.java b/src/main/java/app/fyreplace/api/data/Report.java index 5e67566..1ae388d 100644 --- a/src/main/java/app/fyreplace/api/data/Report.java +++ b/src/main/java/app/fyreplace/api/data/Report.java @@ -8,6 +8,7 @@ import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; import java.util.UUID; +import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; @@ -26,16 +27,19 @@ public class Report extends TimestampedEntityBase { public Class targetModel; @Column(nullable = false) + @Schema(required = true) public UUID targetId; @SuppressWarnings("unused") @JsonProperty("source") + @Schema(required = true) public User.Profile getSourceProfile() { return source.getProfile(); } @SuppressWarnings("unused") @JsonProperty("targetModel") + @Schema(required = true) public String getTargetModelSimpleName() { return targetModel.getSimpleName(); } diff --git a/src/main/java/app/fyreplace/api/data/Reportable.java b/src/main/java/app/fyreplace/api/data/Reportable.java index 47153a4..392e903 100644 --- a/src/main/java/app/fyreplace/api/data/Reportable.java +++ b/src/main/java/app/fyreplace/api/data/Reportable.java @@ -1,8 +1,10 @@ package app.fyreplace.api.data; import java.util.UUID; +import org.eclipse.microprofile.openapi.annotations.media.Schema; public interface Reportable { + @Schema(required = true) UUID getId(); default void reportBy(final User user) { diff --git a/src/main/java/app/fyreplace/api/data/StoredFile.java b/src/main/java/app/fyreplace/api/data/StoredFile.java index 0ab7c2d..ef72eb8 100644 --- a/src/main/java/app/fyreplace/api/data/StoredFile.java +++ b/src/main/java/app/fyreplace/api/data/StoredFile.java @@ -17,6 +17,7 @@ import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Paths; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @Entity @Table(name = "stored_files") @@ -28,6 +29,7 @@ public class StoredFile extends EntityBase { private MimeTypeService mimeTypeService; @Column(unique = true, nullable = false) + @Schema(required = true) public String path; @Transient diff --git a/src/main/java/app/fyreplace/api/data/Subscription.java b/src/main/java/app/fyreplace/api/data/Subscription.java index aa4ee9e..7cc9f00 100644 --- a/src/main/java/app/fyreplace/api/data/Subscription.java +++ b/src/main/java/app/fyreplace/api/data/Subscription.java @@ -9,6 +9,7 @@ import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; import java.time.Instant; +import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.hibernate.annotations.Formula; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; @@ -25,6 +26,7 @@ public class Subscription extends EntityBase { @ManyToOne(optional = false, fetch = FetchType.EAGER) @OnDelete(action = OnDeleteAction.CASCADE) + @Schema(required = true) public Post post; @SuppressWarnings("unused") @@ -57,6 +59,7 @@ select count(*) from comments ) ) """) + @Schema(required = true) public long unreadCommentCount; public void markAsRead() { diff --git a/src/main/java/app/fyreplace/api/data/TimestampedEntityBase.java b/src/main/java/app/fyreplace/api/data/TimestampedEntityBase.java index 969d12f..5bb5d45 100644 --- a/src/main/java/app/fyreplace/api/data/TimestampedEntityBase.java +++ b/src/main/java/app/fyreplace/api/data/TimestampedEntityBase.java @@ -4,6 +4,7 @@ import jakarta.persistence.Column; import jakarta.persistence.MappedSuperclass; import java.time.Instant; +import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.SourceType; @@ -11,6 +12,7 @@ public abstract class TimestampedEntityBase extends EntityBase { @Column(nullable = false) @CreationTimestamp(source = SourceType.DB) + @Schema(required = true) public Instant dateCreated; public static Sort sorting() { diff --git a/src/main/java/app/fyreplace/api/data/User.java b/src/main/java/app/fyreplace/api/data/User.java index 1142af3..00679be 100644 --- a/src/main/java/app/fyreplace/api/data/User.java +++ b/src/main/java/app/fyreplace/api/data/User.java @@ -72,6 +72,7 @@ public class User extends SoftDeletableEntityBase implements Reportable { "voids")); @Column(length = 100, unique = true) + @Schema(required = true) public String username; @ManyToOne @@ -84,18 +85,21 @@ public class User extends SoftDeletableEntityBase implements Reportable { public boolean active = false; @Column(nullable = false) + @Schema(required = true) public Rank rank = Rank.CITIZEN; @OneToOne(cascade = CascadeType.PERSIST) @OnDelete(action = OnDeleteAction.SET_NULL) @JsonSerialize(using = StoredFile.Serializer.class) - @Schema(implementation = String.class) + @Schema(required = true, implementation = String.class) public StoredFile avatar; @Column(length = 3000, nullable = false) + @Schema(required = true) public String bio = ""; @Column(nullable = false) + @Schema(required = true) public boolean banned = false; @Column(nullable = false) @@ -131,6 +135,7 @@ public Profile getProfile() { return new Profile(id, username, avatar != null ? avatar.toString() : null, getTint()); } + @Schema(required = true) public String getTint() { final var crc = new CRC32(); crc.update(username.getBytes()); @@ -249,5 +254,9 @@ public enum BanCount { ONE_TOO_MANY } - public record Profile(UUID id, String username, String avatar, String tint) {} + public record Profile( + @Schema(required = true) UUID id, + @Schema(required = true) String username, + @Schema(required = true) String avatar, + @Schema(required = true) String tint) {} } diff --git a/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java index fd6a4fe..fa24ea0 100644 --- a/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java @@ -54,9 +54,10 @@ public final class ChaptersEndpoint { @Transactional @APIResponse( responseCode = "201", + description = "Created", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Chapter.class))) - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "404", description = "Not found") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response createChapter(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context); @@ -80,8 +81,8 @@ public Response createChapter(@PathParam("id") final UUID id) { @Path("{position}") @Authenticated @Transactional - @APIResponse(responseCode = "204") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "204", description = "No content") + @APIResponse(responseCode = "404", description = "Not found") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response deleteChapter(@PathParam("id") final UUID id, @PathParam("position") final int position) { final var user = User.getFromSecurityContext(context); @@ -98,9 +99,10 @@ public Response deleteChapter(@PathParam("id") final UUID id, @PathParam("positi @Consumes(MediaType.TEXT_PLAIN) @APIResponse( responseCode = "200", + description = "OK", content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = Integer.class))) - @APIResponse(responseCode = "400") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "400", description = "Bad request") + @APIResponse(responseCode = "404", description = "Not found") public int updateChapterPosition( @PathParam("id") final UUID id, @PathParam("position") final int position, @NotNull final Integer input) { final var user = User.getFromSecurityContext(context); @@ -133,9 +135,10 @@ public int updateChapterPosition( @Consumes(MediaType.TEXT_PLAIN) @APIResponse( responseCode = "200", + description = "OK", content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) - @APIResponse(responseCode = "400") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "400", description = "Bad request") + @APIResponse(responseCode = "404", description = "Not found") public String updateChapterText( @PathParam("id") final UUID id, @PathParam("position") final int position, @@ -161,9 +164,10 @@ public String updateChapterText( @Consumes(MediaType.APPLICATION_OCTET_STREAM) @APIResponse( responseCode = "200", + description = "OK", content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) - @APIResponse(responseCode = "400") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "400", description = "Bad request") + @APIResponse(responseCode = "404", description = "Not found") public String updateChapterImage( @PathParam("id") final UUID id, @PathParam("position") final int position, final byte[] input) throws IOException { diff --git a/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java index 83fcbb1..5e8430c 100644 --- a/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java @@ -44,8 +44,8 @@ public final class CommentsEndpoint { @GET @Authenticated - @APIResponse(responseCode = "200") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "200", description = "OK") + @APIResponse(responseCode = "404", description = "Not Found") public Iterable list(@PathParam("id") final UUID id, @QueryParam("page") @PositiveOrZero final int page) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); @@ -62,9 +62,10 @@ public Iterable list(@PathParam("id") final UUID id, @QueryParam("page" @Transactional @APIResponse( responseCode = "201", + description = "Created", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Comment.class))) - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "404", description = "Not Found") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response create(@PathParam("id") final UUID id, @Valid @NotNull final CommentCreation input) { final var user = User.getFromSecurityContext(context); @@ -89,8 +90,8 @@ public Response create(@PathParam("id") final UUID id, @Valid @NotNull final Com @Path("{position}") @Authenticated @Transactional - @APIResponse(responseCode = "204") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "204", description = "No Content") + @APIResponse(responseCode = "404", description = "Not Found") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response delete(@PathParam("id") final UUID id, @PathParam("position") @PositiveOrZero final int position) { final var user = User.getFromSecurityContext(context); @@ -110,8 +111,8 @@ public Response delete(@PathParam("id") final UUID id, @PathParam("position") @P @Path("{position}/reported") @Authenticated @Transactional - @APIResponse(responseCode = "200") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "200", description = "OK") + @APIResponse(responseCode = "404", description = "Not Found") public Response updateReported( @PathParam("id") final UUID id, @PathParam("position") @PositiveOrZero final int position, @@ -138,8 +139,8 @@ public Response updateReported( @Path("{position}/acknowledge") @Authenticated @Transactional - @APIResponse(responseCode = "200") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "200", description = "OK") + @APIResponse(responseCode = "404", description = "Not Found") public Response acknowledge( @PathParam("id") final UUID id, @PathParam("position") @PositiveOrZero final int position) { final var user = User.getFromSecurityContext(context); @@ -164,8 +165,8 @@ public Response acknowledge( @GET @Path("count") @Authenticated - @APIResponse(responseCode = "200") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "200", description = "OK") + @APIResponse(responseCode = "404", description = "Not Found") public long count(@PathParam("id") final UUID id, @QueryParam("read") @Nullable final Boolean read) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); diff --git a/src/main/java/app/fyreplace/api/endpoints/DevEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/DevEndpoint.java index d6c3ba2..da0a629 100644 --- a/src/main/java/app/fyreplace/api/endpoints/DevEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/DevEndpoint.java @@ -24,8 +24,9 @@ public final class DevEndpoint { @Path("users/{username}/token") @APIResponse( responseCode = "200", + description = "OK", content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "404", description = "Not Found") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public String retrieveToken(@PathParam("username") final String username) { final var user = User.findByUsername(username); @@ -41,8 +42,9 @@ public String retrieveToken(@PathParam("username") final String username) { @Path("passwords/{password}/hash") @APIResponse( responseCode = "200", + description = "OK", content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "404", description = "Not Found") public String retrievePasswordHash(@PathParam("password") final String password) { return BcryptUtil.bcryptHash(password); } diff --git a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java index 026d35e..527f9ff 100644 --- a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java @@ -49,7 +49,7 @@ public final class EmailsEndpoint { @GET @Authenticated - @APIResponse(responseCode = "200") + @APIResponse(responseCode = "200", description = "OK") public Iterable list(@QueryParam("page") @PositiveOrZero final int page) { final var user = User.getFromSecurityContext(context); return Email.find("user", Sort.by("email"), user).page(page, pagingSize).list(); @@ -60,8 +60,9 @@ public Iterable list(@QueryParam("page") @PositiveOrZero final int page) @Transactional @APIResponse( responseCode = "201", + description = "Created", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Email.class))) - @APIResponse(responseCode = "409") + @APIResponse(responseCode = "409", description = "Conflict") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response create(@Valid @NotNull final EmailCreation input) { if (Email.count("email", input.email()) > 0) { @@ -80,8 +81,8 @@ public Response create(@Valid @NotNull final EmailCreation input) { @Path("{id}") @Authenticated @Transactional - @APIResponse(responseCode = "204") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "204", description = "No Content") + @APIResponse(responseCode = "404", description = "Not Found") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response delete(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context); @@ -101,8 +102,8 @@ public Response delete(@PathParam("id") final UUID id) { @Path("{id}/main") @Authenticated @Transactional - @APIResponse(responseCode = "200") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "200", description = "OK") + @APIResponse(responseCode = "404", description = "Not Found") public Response updateMain(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context); final var email = Email.find("user = ?1 and id = ?2", user, id).firstResult(); @@ -121,7 +122,7 @@ public Response updateMain(@PathParam("id") final UUID id) { @GET @Path("count") @Authenticated - @APIResponse(responseCode = "200") + @APIResponse(responseCode = "200", description = "OK") public long count() { return Email.count("user", User.getFromSecurityContext(context)); } @@ -130,9 +131,9 @@ public long count() { @Path("activate") @Authenticated @Transactional - @APIResponse(responseCode = "200") - @APIResponse(responseCode = "400") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "200", description = "OK") + @APIResponse(responseCode = "400", description = "Bad Request") + @APIResponse(responseCode = "404", description = "Not Found") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response activate(@NotNull @Valid final EmailActivation input) { final var email = Email.find("email", input.email()).firstResult(); diff --git a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java index 8eea9f9..a121008 100644 --- a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java @@ -52,8 +52,8 @@ public final class PostsEndpoint { @GET @Authenticated - @APIResponse(responseCode = "200") - @APIResponse(responseCode = "400") + @APIResponse(responseCode = "200", description = "OK") + @APIResponse(responseCode = "400", description = "Bad Request") public Iterable list( @QueryParam("page") @PositiveOrZero final int page, @QueryParam("ascending") final boolean ascending, @@ -79,8 +79,9 @@ public Iterable list( @Transactional @APIResponse( responseCode = "201", + description = "Created", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Post.class))) - @APIResponse(responseCode = "400") + @APIResponse(responseCode = "400", description = "Bad Request") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response create() { final var user = User.getFromSecurityContext(context); @@ -93,8 +94,8 @@ public Response create() { @GET @Path("{id}") - @APIResponse(responseCode = "200") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "200", description = "OK") + @APIResponse(responseCode = "404", description = "Not Found") public Post retrieve(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context, null, false); final var post = Post.findById(id); @@ -106,8 +107,8 @@ public Post retrieve(@PathParam("id") final UUID id) { @Path("{id}") @Authenticated @Transactional - @APIResponse(responseCode = "204") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "204", description = "No Content") + @APIResponse(responseCode = "404", description = "Not Found") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response delete(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context); @@ -127,9 +128,9 @@ public Response delete(@PathParam("id") final UUID id) { @Path("{id}/subscribed") @Authenticated @Transactional - @APIResponse(responseCode = "200") - @APIResponse(responseCode = "400") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "200", description = "OK") + @APIResponse(responseCode = "400", description = "Bad Request") + @APIResponse(responseCode = "404", description = "Not Found") public Response updateSubscribed(@PathParam("id") final UUID id, @Valid @NotNull final SubscriptionUpdate input) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); @@ -148,8 +149,8 @@ public Response updateSubscribed(@PathParam("id") final UUID id, @Valid @NotNull @Path("{id}/reported") @Authenticated @Transactional - @APIResponse(responseCode = "200") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "200", description = "OK") + @APIResponse(responseCode = "404", description = "Not Found") public Response updateReported(@PathParam("id") final UUID id, @NotNull @Valid final ReportUpdate input) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); @@ -168,9 +169,9 @@ public Response updateReported(@PathParam("id") final UUID id, @NotNull @Valid f @Path("{id}/publish") @Authenticated @Transactional - @APIResponse(responseCode = "200") - @APIResponse(responseCode = "400") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "200", description = "OK") + @APIResponse(responseCode = "400", description = "Bad Request") + @APIResponse(responseCode = "404", description = "Not Found") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response publish(@PathParam("id") final UUID id, @Valid @NotNull final PostPublication input) { final var user = User.getFromSecurityContext(context); @@ -189,9 +190,9 @@ public Response publish(@PathParam("id") final UUID id, @Valid @NotNull final Po @Path("{id}/vote") @Authenticated @Transactional - @APIResponse(responseCode = "200") - @APIResponse(responseCode = "400") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "200", description = "OK") + @APIResponse(responseCode = "400", description = "Bad Request") + @APIResponse(responseCode = "404", description = "Not Found") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response vote(@PathParam("id") final UUID id, @Valid @NotNull final VoteCreation input) { final var user = User.getFromSecurityContext(context); @@ -217,8 +218,8 @@ public Response vote(@PathParam("id") final UUID id, @Valid @NotNull final VoteC @GET @Path("count") @Authenticated - @APIResponse(responseCode = "200") - @APIResponse(responseCode = "400") + @APIResponse(responseCode = "200", description = "OK") + @APIResponse(responseCode = "400", description = "Bad Request") public long count(@QueryParam("type") @NotNull final PostListingType type) { final var user = User.getFromSecurityContext(context); return switch (type) { @@ -231,7 +232,7 @@ public long count(@QueryParam("type") @NotNull final PostListingType type) { @GET @Path("feed") @Authenticated - @APIResponse(responseCode = "200") + @APIResponse(responseCode = "200", description = "OK") public Iterable listFeed() { final var user = User.getFromSecurityContext(context); diff --git a/src/main/java/app/fyreplace/api/endpoints/ReportsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/ReportsEndpoint.java index 5b0261c..ec26531 100644 --- a/src/main/java/app/fyreplace/api/endpoints/ReportsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/ReportsEndpoint.java @@ -16,7 +16,7 @@ public final class ReportsEndpoint { @GET @RolesAllowed("MODERATOR") - @APIResponse(responseCode = "200") + @APIResponse(responseCode = "200", description = "OK") public Iterable list(@QueryParam("page") @PositiveOrZero final int page) { return Report.findAll(Report.sorting()).page(page, pagingSize).list(); } diff --git a/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java index c8eeac0..3bc5b09 100644 --- a/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java @@ -29,9 +29,9 @@ public final class StoredFilesEndpoint { @GET @Path("{path:.*}") - @APIResponse(responseCode = "200") - @APIResponse(responseCode = "303") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "200", description = "OK") + @APIResponse(responseCode = "303", description = "See Other") + @APIResponse(responseCode = "404", description = "Not Found") public Response retrieve(@PathParam("path") final String path) throws URISyntaxException { final var appUri = new URI(appUrl); final var requestUri = storageService.getUri(path); diff --git a/src/main/java/app/fyreplace/api/endpoints/SubscriptionsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/SubscriptionsEndpoint.java index cc69d1f..c0f36f5 100644 --- a/src/main/java/app/fyreplace/api/endpoints/SubscriptionsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/SubscriptionsEndpoint.java @@ -29,8 +29,8 @@ public final class SubscriptionsEndpoint { @Path("{id}") @Authenticated @Transactional - @APIResponse(responseCode = "204") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "204", description = "No Content") + @APIResponse(responseCode = "404", description = "Not Found") public void delete(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context); final var subscription = Subscription.findById(id); @@ -45,8 +45,8 @@ public void delete(@PathParam("id") final UUID id) { @GET @Path("unread") @Authenticated - @APIResponse(responseCode = "200") - @APIResponse(responseCode = "400") + @APIResponse(responseCode = "200", description = "OK") + @APIResponse(responseCode = "400", description = "Bad Request") public Iterable listUnread(@QueryParam("page") @PositiveOrZero final int page) { final var user = User.getFromSecurityContext(context); @@ -62,7 +62,7 @@ public Iterable listUnread(@QueryParam("page") @PositiveOrZero fin @Path("unread") @Authenticated @Transactional - @APIResponse(responseCode = "204") + @APIResponse(responseCode = "204", description = "No Content") public void clearUnread() { Subscription.markAsRead(User.getFromSecurityContext(context)); } diff --git a/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java index 5bcbddf..8b426c4 100644 --- a/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java @@ -44,9 +44,10 @@ public final class TokensEndpoint { @Transactional @APIResponse( responseCode = "201", + description = "Created", content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) - @APIResponse(responseCode = "400") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "400", description = "Bad Request") + @APIResponse(responseCode = "404", description = "Not Found") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response create(@Valid @NotNull final TokenCreation input) { final var email = getEmail(input.identifier()); @@ -75,6 +76,7 @@ public Response create(@Valid @NotNull final TokenCreation input) { @Authenticated @APIResponse( responseCode = "200", + description = "OK", content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) public String retrieveNew() { return jwtService.makeJwt(User.getFromSecurityContext(context)); @@ -83,9 +85,9 @@ public String retrieveNew() { @POST @Path("new") @Transactional - @APIResponse(responseCode = "200") - @APIResponse(responseCode = "400") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "200", description = "OK") + @APIResponse(responseCode = "400", description = "Bad Request") + @APIResponse(responseCode = "404", description = "Not Found") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response createNew(@NotNull @Valid final NewTokenCreation input) { final var email = getEmail(input.identifier()); diff --git a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java index 3de53f6..547f1a8 100644 --- a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java @@ -67,10 +67,11 @@ public final class UsersEndpoint { @Transactional @APIResponse( responseCode = "201", + description = "Created", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = User.class))) - @APIResponse(responseCode = "400") - @APIResponse(responseCode = "403") - @APIResponse(responseCode = "409") + @APIResponse(responseCode = "400", description = "Bad Request") + @APIResponse(responseCode = "403", description = "Not Allowed") + @APIResponse(responseCode = "409", description = "Conflict") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response create(@Valid @NotNull final UserCreation input) { if (User.forbiddenUsernames.contains(input.username())) { @@ -98,8 +99,8 @@ public Response create(@Valid @NotNull final UserCreation input) { @GET @Path("{id}") - @APIResponse(responseCode = "200") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "200", description = "OK") + @APIResponse(responseCode = "404", description = "Not Found") public User retrieve(@PathParam("id") final UUID id) { final var user = User.findById(id); validateUser(user); @@ -110,9 +111,9 @@ public User retrieve(@PathParam("id") final UUID id) { @Path("{id}/blocked") @Authenticated @Transactional - @APIResponse(responseCode = "200") - @APIResponse(responseCode = "400") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "200", description = "OK") + @APIResponse(responseCode = "400", description = "Bad Request") + @APIResponse(responseCode = "404", description = "Not Found") public Response updateBlocked(@PathParam("id") final UUID id, @Valid @NotNull final BlockUpdate input) { final var source = User.getFromSecurityContext(context); final var target = User.findById(id); @@ -133,8 +134,8 @@ public Response updateBlocked(@PathParam("id") final UUID id, @Valid @NotNull fi @Path("{id}/banned") @RolesAllowed("MODERATOR") @Transactional - @APIResponse(responseCode = "200") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "200", description = "OK") + @APIResponse(responseCode = "404", description = "Not Found") public Response updateBanned(@PathParam("id") final UUID id) { final var user = User.findById(id, LockModeType.PESSIMISTIC_WRITE); validateUser(user); @@ -158,8 +159,8 @@ public Response updateBanned(@PathParam("id") final UUID id) { @Path("{id}/reported") @Authenticated @Transactional - @APIResponse(responseCode = "200") - @APIResponse(responseCode = "404") + @APIResponse(responseCode = "200", description = "OK") + @APIResponse(responseCode = "404", description = "Not Found") public Response updateReported(@PathParam("id") final UUID id, @NotNull @Valid final ReportUpdate input) { final var source = User.getFromSecurityContext(context); final var target = User.findById(id); @@ -179,7 +180,7 @@ public Response updateReported(@PathParam("id") final UUID id, @NotNull @Valid f @GET @Path("me") @Authenticated - @APIResponse(responseCode = "200") + @APIResponse(responseCode = "200", description = "OK") public User retrieveMe() { return retrieve(User.getFromSecurityContext(context).id); } @@ -191,8 +192,9 @@ public User retrieveMe() { @Consumes(MediaType.TEXT_PLAIN) @APIResponse( responseCode = "200", + description = "OK", content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) - @APIResponse(responseCode = "400") + @APIResponse(responseCode = "400", description = "Bad Request") public String updateMeBio(@NotNull @Length(max = 3000) final String input) { final var user = User.getFromSecurityContext(context, LockModeType.PESSIMISTIC_READ); user.bio = input; @@ -207,9 +209,10 @@ public String updateMeBio(@NotNull @Length(max = 3000) final String input) { @Consumes(MediaType.APPLICATION_OCTET_STREAM) @APIResponse( responseCode = "200", + description = "OK", content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) - @APIResponse(responseCode = "413") - @APIResponse(responseCode = "415") + @APIResponse(responseCode = "413", description = "Payload Too Large") + @APIResponse(responseCode = "415", description = "Unsupported Media Type") public String updateMeAvatar(final byte[] input) throws IOException { mimeTypeService.validate(input, KnownMimeTypes.IMAGE); final var user = User.getFromSecurityContext(context, LockModeType.PESSIMISTIC_WRITE); @@ -228,7 +231,7 @@ public String updateMeAvatar(final byte[] input) throws IOException { @Path("me/avatar") @Authenticated @Transactional - @APIResponse(responseCode = "204") + @APIResponse(responseCode = "204", description = "No Content") public void deleteMeAvatar() { final var user = User.getFromSecurityContext(context, LockModeType.PESSIMISTIC_WRITE); @@ -243,7 +246,7 @@ public void deleteMeAvatar() { @Path("me") @Authenticated @Transactional - @APIResponse(responseCode = "204") + @APIResponse(responseCode = "204", description = "No Content") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) public Response deleteMe() { User.getFromSecurityContext(context).softDelete(); @@ -253,7 +256,7 @@ public Response deleteMe() { @GET @Path("blocked") @Authenticated - @APIResponse(responseCode = "200") + @APIResponse(responseCode = "200", description = "OK") public Iterable listBlocked(@QueryParam("page") @PositiveOrZero final int page) { final var user = User.getFromSecurityContext(context); final var blocks = Block.find("source", Sort.by("id"), user); @@ -266,7 +269,7 @@ public Iterable listBlocked(@QueryParam("page") @PositiveOrZero fi @GET @Path("blocked/count") @Authenticated - @APIResponse(responseCode = "200") + @APIResponse(responseCode = "200", description = "OK") public long countBlocked() { return Block.count("source", User.getFromSecurityContext(context)); } diff --git a/src/main/java/app/fyreplace/api/filters/OpenAPIFilter.java b/src/main/java/app/fyreplace/api/filters/OpenAPIFilter.java new file mode 100644 index 0000000..b9667a4 --- /dev/null +++ b/src/main/java/app/fyreplace/api/filters/OpenAPIFilter.java @@ -0,0 +1,18 @@ +package app.fyreplace.api.filters; + +import io.quarkus.smallrye.openapi.OpenApiFilter; +import org.eclipse.microprofile.openapi.OASFactory; +import org.eclipse.microprofile.openapi.OASFilter; +import org.eclipse.microprofile.openapi.models.Operation; + +@SuppressWarnings("unused") +@OpenApiFilter(OpenApiFilter.RunStage.BUILD) +public final class OpenAPIFilter implements OASFilter { + @Override + public Operation filterOperation(final Operation operation) { + operation + .getResponses() + .addAPIResponse("default", OASFactory.createAPIResponse().description("Unexpected error")); + return operation; + } +} From b5428daf351186500433a1707383d62e46da19f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 9 Jun 2024 00:03:49 +0200 Subject: [PATCH 137/157] Produce even better OpenAPI schema --- .../api/endpoints/ChaptersEndpoint.java | 6 ++-- .../api/endpoints/CommentsEndpoint.java | 14 ++++---- .../fyreplace/api/endpoints/DevEndpoint.java | 21 +++-------- .../api/endpoints/EmailsEndpoint.java | 12 +++---- .../api/endpoints/PostsEndpoint.java | 20 +++++------ .../api/endpoints/ReportsEndpoint.java | 2 +- .../api/endpoints/StoredFilesEndpoint.java | 2 +- .../api/endpoints/SubscriptionsEndpoint.java | 6 ++-- .../api/endpoints/TokensEndpoint.java | 6 ++-- .../api/endpoints/UsersEndpoint.java | 36 +++++++++---------- src/main/resources/application.yaml | 8 ++++- .../chapters/CreateChapterTests.java | 4 +-- ...geTests.java => SetChapterImageTests.java} | 24 ++++++------- ...ests.java => SetChapterPositionTests.java} | 22 ++++++------ ...extTests.java => SetChapterTextTests.java} | 22 ++++++------ ...ests.java => AcknowledgeCommentTests.java} | 8 ++--- ...ountTests.java => CountCommentsTests.java} | 10 +++--- ...eateTests.java => CreateCommentTests.java} | 24 ++++++------- ...leteTests.java => DeleteCommentTests.java} | 10 +++--- ...{ListTests.java => ListCommentsTests.java} | 14 ++++---- .../comments/UpdateReportedToFalseTests.java | 12 +++---- .../comments/UpdateReportedToTrueTests.java | 12 +++---- .../dev/RetrievePasswordHashTests.java | 2 +- .../endpoints/dev/RetrieveUserTokenTests.java | 2 +- ...vateTests.java => ActivateEmailTests.java} | 12 +++---- ...{CountTests.java => CountEmailsTests.java} | 4 +-- ...CreateTests.java => CreateEmailTests.java} | 12 +++---- ...DeleteTests.java => DeleteEmailTests.java} | 4 +-- .../{ListTests.java => ListEmailsTests.java} | 8 ++--- .../endpoints/emails/UpdateMainTests.java | 10 +++--- .../{CountTests.java => CountPostsTests.java} | 6 ++-- ...{CreateTests.java => CreatePostTests.java} | 6 ++-- ...{DeleteTests.java => DeletePostTests.java} | 6 ++-- .../{RetrieveTests.java => GetPostTests.java} | 28 +++++++-------- ...FeedTests.java => ListPostsFeedTests.java} | 16 ++++----- .../{ListTests.java => ListPostsTests.java} | 26 +++++++------- ...ublishTests.java => PublishPostTests.java} | 10 +++--- ....java => SetPostReportedToFalseTests.java} | 16 ++++----- ...s.java => SetPostReportedToTrueTests.java} | 16 ++++----- ...ava => SetPostSubscribedToFalseTests.java} | 22 ++++++------ ...java => SetPostSubscribedToTrueTests.java} | 22 ++++++------ .../{VoteTests.java => VotePostTests.java} | 24 ++++++------- .../{ListTests.java => ListReportsTests.java} | 6 ++-- ...ava => ClearUnreadSubscriptionsTests.java} | 4 +-- ...ests.java => DeleteSubscriptionTests.java} | 6 ++-- ...java => ListUnreadSubscriptionsTests.java} | 4 +-- ...NewTests.java => CreateNewTokenTests.java} | 10 +++--- ...CreateTests.java => CreateTokenTests.java} | 22 ++++++------ ...eveNewTests.java => GetNewTokenTests.java} | 6 ++-- ...Tests.java => CountBlockedUsersTests.java} | 4 +-- ...{CreateTests.java => CreateUserTests.java} | 22 ++++++------ ...java => DeleteCurrentUserAvatarTests.java} | 12 +++---- ...Tests.java => DeleteCurrentUserTests.java} | 10 +++--- ...eMeTests.java => GetCurrentUserTests.java} | 10 +++--- .../{RetrieveTests.java => GetUserTests.java} | 10 +++--- ...dTests.java => ListBlockedUsersTests.java} | 8 ++--- ...ts.java => SetCurrentUserAvatarTests.java} | 14 ++++---- ...Tests.java => SetCurrentUserBioTests.java} | 22 ++++++++---- ...nnedTests.java => SetUserBannedTests.java} | 18 +++++----- ...s.java => SetUserBlockedToFalseTests.java} | 22 +++++++++--- ...ts.java => SetUserBlockedToTrueTests.java} | 12 +++---- ....java => SetUserReportedToFalseTests.java} | 14 ++++---- ...s.java => SetUserReportedToTrueTests.java} | 14 ++++---- 63 files changed, 407 insertions(+), 390 deletions(-) rename src/test/java/app/fyreplace/api/testing/endpoints/chapters/{UpdateChapterImageTests.java => SetChapterImageTests.java} (87%) rename src/test/java/app/fyreplace/api/testing/endpoints/chapters/{UpdateChapterPositionTests.java => SetChapterPositionTests.java} (86%) rename src/test/java/app/fyreplace/api/testing/endpoints/chapters/{UpdateChapterTextTests.java => SetChapterTextTests.java} (87%) rename src/test/java/app/fyreplace/api/testing/endpoints/comments/{AcknowledgeTests.java => AcknowledgeCommentTests.java} (92%) rename src/test/java/app/fyreplace/api/testing/endpoints/comments/{CountTests.java => CountCommentsTests.java} (93%) rename src/test/java/app/fyreplace/api/testing/endpoints/comments/{CreateTests.java => CreateCommentTests.java} (91%) rename src/test/java/app/fyreplace/api/testing/endpoints/comments/{DeleteTests.java => DeleteCommentTests.java} (94%) rename src/test/java/app/fyreplace/api/testing/endpoints/comments/{ListTests.java => ListCommentsTests.java} (92%) rename src/test/java/app/fyreplace/api/testing/endpoints/emails/{ActivateTests.java => ActivateEmailTests.java} (91%) rename src/test/java/app/fyreplace/api/testing/endpoints/emails/{CountTests.java => CountEmailsTests.java} (93%) rename src/test/java/app/fyreplace/api/testing/endpoints/emails/{CreateTests.java => CreateEmailTests.java} (91%) rename src/test/java/app/fyreplace/api/testing/endpoints/emails/{DeleteTests.java => DeleteEmailTests.java} (95%) rename src/test/java/app/fyreplace/api/testing/endpoints/emails/{ListTests.java => ListEmailsTests.java} (93%) rename src/test/java/app/fyreplace/api/testing/endpoints/posts/{CountTests.java => CountPostsTests.java} (94%) rename src/test/java/app/fyreplace/api/testing/endpoints/posts/{CreateTests.java => CreatePostTests.java} (90%) rename src/test/java/app/fyreplace/api/testing/endpoints/posts/{DeleteTests.java => DeletePostTests.java} (92%) rename src/test/java/app/fyreplace/api/testing/endpoints/posts/{RetrieveTests.java => GetPostTests.java} (91%) rename src/test/java/app/fyreplace/api/testing/endpoints/posts/{ListFeedTests.java => ListPostsFeedTests.java} (90%) rename src/test/java/app/fyreplace/api/testing/endpoints/posts/{ListTests.java => ListPostsTests.java} (97%) rename src/test/java/app/fyreplace/api/testing/endpoints/posts/{PublishTests.java => PublishPostTests.java} (93%) rename src/test/java/app/fyreplace/api/testing/endpoints/posts/{UpdateReportedToFalseTests.java => SetPostReportedToFalseTests.java} (90%) rename src/test/java/app/fyreplace/api/testing/endpoints/posts/{UpdateReportedToTrueTests.java => SetPostReportedToTrueTests.java} (90%) rename src/test/java/app/fyreplace/api/testing/endpoints/posts/{UpdateSubscribedToFalseTests.java => SetPostSubscribedToFalseTests.java} (90%) rename src/test/java/app/fyreplace/api/testing/endpoints/posts/{UpdateSubscribedToTrueTests.java => SetPostSubscribedToTrueTests.java} (90%) rename src/test/java/app/fyreplace/api/testing/endpoints/posts/{VoteTests.java => VotePostTests.java} (92%) rename src/test/java/app/fyreplace/api/testing/endpoints/reports/{ListTests.java => ListReportsTests.java} (92%) rename src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/{ClearUnreadTests.java => ClearUnreadSubscriptionsTests.java} (88%) rename src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/{DeleteTests.java => DeleteSubscriptionTests.java} (94%) rename src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/{ListUnreadTests.java => ListUnreadSubscriptionsTests.java} (90%) rename src/test/java/app/fyreplace/api/testing/endpoints/tokens/{CreateNewTests.java => CreateNewTokenTests.java} (88%) rename src/test/java/app/fyreplace/api/testing/endpoints/tokens/{CreateTests.java => CreateTokenTests.java} (92%) rename src/test/java/app/fyreplace/api/testing/endpoints/tokens/{RetrieveNewTests.java => GetNewTokenTests.java} (83%) rename src/test/java/app/fyreplace/api/testing/endpoints/users/{CountBlockedTests.java => CountBlockedUsersTests.java} (92%) rename src/test/java/app/fyreplace/api/testing/endpoints/users/{CreateTests.java => CreateUserTests.java} (90%) rename src/test/java/app/fyreplace/api/testing/endpoints/users/{DeleteMeAvatarTests.java => DeleteCurrentUserAvatarTests.java} (77%) rename src/test/java/app/fyreplace/api/testing/endpoints/users/{DeleteMeTests.java => DeleteCurrentUserTests.java} (76%) rename src/test/java/app/fyreplace/api/testing/endpoints/users/{RetrieveMeTests.java => GetCurrentUserTests.java} (84%) rename src/test/java/app/fyreplace/api/testing/endpoints/users/{RetrieveTests.java => GetUserTests.java} (89%) rename src/test/java/app/fyreplace/api/testing/endpoints/users/{ListBlockedTests.java => ListBlockedUsersTests.java} (92%) rename src/test/java/app/fyreplace/api/testing/endpoints/users/{UpdateMeAvatarTests.java => SetCurrentUserAvatarTests.java} (83%) rename src/test/java/app/fyreplace/api/testing/endpoints/users/{UpdateMeBioTests.java => SetCurrentUserBioTests.java} (70%) rename src/test/java/app/fyreplace/api/testing/endpoints/users/{UpdateBannedTests.java => SetUserBannedTests.java} (89%) rename src/test/java/app/fyreplace/api/testing/endpoints/users/{UpdateBlockedToFalseTests.java => SetUserBlockedToFalseTests.java} (83%) rename src/test/java/app/fyreplace/api/testing/endpoints/users/{UpdateBlockedToTrueTests.java => SetUserBlockedToTrueTests.java} (92%) rename src/test/java/app/fyreplace/api/testing/endpoints/users/{UpdateReportedToFalseTests.java => SetUserReportedToFalseTests.java} (92%) rename src/test/java/app/fyreplace/api/testing/endpoints/users/{UpdateReportedToTrueTests.java => SetUserReportedToTrueTests.java} (91%) diff --git a/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java index fa24ea0..8d7b194 100644 --- a/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java @@ -103,7 +103,7 @@ public Response deleteChapter(@PathParam("id") final UUID id, @PathParam("positi content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = Integer.class))) @APIResponse(responseCode = "400", description = "Bad request") @APIResponse(responseCode = "404", description = "Not found") - public int updateChapterPosition( + public int setChapterPosition( @PathParam("id") final UUID id, @PathParam("position") final int position, @NotNull final Integer input) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); @@ -139,7 +139,7 @@ public int updateChapterPosition( content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) @APIResponse(responseCode = "400", description = "Bad request") @APIResponse(responseCode = "404", description = "Not found") - public String updateChapterText( + public String setChapterText( @PathParam("id") final UUID id, @PathParam("position") final int position, @NotNull @Length(max = 500) String input) { @@ -168,7 +168,7 @@ public String updateChapterText( content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) @APIResponse(responseCode = "400", description = "Bad request") @APIResponse(responseCode = "404", description = "Not found") - public String updateChapterImage( + public String setChapterImage( @PathParam("id") final UUID id, @PathParam("position") final int position, final byte[] input) throws IOException { mimeTypeService.validate(input, KnownMimeTypes.IMAGE); diff --git a/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java index 5e8430c..e2ca4f9 100644 --- a/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/CommentsEndpoint.java @@ -46,7 +46,8 @@ public final class CommentsEndpoint { @Authenticated @APIResponse(responseCode = "200", description = "OK") @APIResponse(responseCode = "404", description = "Not Found") - public Iterable list(@PathParam("id") final UUID id, @QueryParam("page") @PositiveOrZero final int page) { + public Iterable listComments( + @PathParam("id") final UUID id, @QueryParam("page") @PositiveOrZero final int page) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); Post.validateAccess(post, user, true, null); @@ -67,7 +68,7 @@ public Iterable list(@PathParam("id") final UUID id, @QueryParam("page" @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Comment.class))) @APIResponse(responseCode = "404", description = "Not Found") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) - public Response create(@PathParam("id") final UUID id, @Valid @NotNull final CommentCreation input) { + public Response createComment(@PathParam("id") final UUID id, @Valid @NotNull final CommentCreation input) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); Post.validateAccess(post, user, true, null); @@ -93,7 +94,8 @@ public Response create(@PathParam("id") final UUID id, @Valid @NotNull final Com @APIResponse(responseCode = "204", description = "No Content") @APIResponse(responseCode = "404", description = "Not Found") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) - public Response delete(@PathParam("id") final UUID id, @PathParam("position") @PositiveOrZero final int position) { + public Response deleteComment( + @PathParam("id") final UUID id, @PathParam("position") @PositiveOrZero final int position) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); Post.validateAccess(post, user, true, null); @@ -113,7 +115,7 @@ public Response delete(@PathParam("id") final UUID id, @PathParam("position") @P @Transactional @APIResponse(responseCode = "200", description = "OK") @APIResponse(responseCode = "404", description = "Not Found") - public Response updateReported( + public Response setCommentReported( @PathParam("id") final UUID id, @PathParam("position") @PositiveOrZero final int position, @NotNull @Valid final ReportUpdate input) { @@ -141,7 +143,7 @@ public Response updateReported( @Transactional @APIResponse(responseCode = "200", description = "OK") @APIResponse(responseCode = "404", description = "Not Found") - public Response acknowledge( + public Response acknowledgeComment( @PathParam("id") final UUID id, @PathParam("position") @PositiveOrZero final int position) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); @@ -167,7 +169,7 @@ public Response acknowledge( @Authenticated @APIResponse(responseCode = "200", description = "OK") @APIResponse(responseCode = "404", description = "Not Found") - public long count(@PathParam("id") final UUID id, @QueryParam("read") @Nullable final Boolean read) { + public long countComments(@PathParam("id") final UUID id, @QueryParam("read") @Nullable final Boolean read) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); Post.validateAccess(post, user, true, null); diff --git a/src/main/java/app/fyreplace/api/endpoints/DevEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/DevEndpoint.java index da0a629..d6521fa 100644 --- a/src/main/java/app/fyreplace/api/endpoints/DevEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/DevEndpoint.java @@ -10,10 +10,7 @@ import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.core.MediaType; -import org.eclipse.microprofile.openapi.annotations.media.Content; -import org.eclipse.microprofile.openapi.annotations.media.Schema; -import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.Operation; @Path("dev") public final class DevEndpoint { @@ -22,13 +19,9 @@ public final class DevEndpoint { @GET @Path("users/{username}/token") - @APIResponse( - responseCode = "200", - description = "OK", - content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) - @APIResponse(responseCode = "404", description = "Not Found") + @Operation(hidden = true) @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) - public String retrieveToken(@PathParam("username") final String username) { + public String getUserToken(@PathParam("username") final String username) { final var user = User.findByUsername(username); if (user == null) { @@ -40,12 +33,8 @@ public String retrieveToken(@PathParam("username") final String username) { @GET @Path("passwords/{password}/hash") - @APIResponse( - responseCode = "200", - description = "OK", - content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) - @APIResponse(responseCode = "404", description = "Not Found") - public String retrievePasswordHash(@PathParam("password") final String password) { + @Operation(hidden = true) + public String getPasswordHash(@PathParam("password") final String password) { return BcryptUtil.bcryptHash(password); } } diff --git a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java index 527f9ff..2987b85 100644 --- a/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/EmailsEndpoint.java @@ -50,7 +50,7 @@ public final class EmailsEndpoint { @GET @Authenticated @APIResponse(responseCode = "200", description = "OK") - public Iterable list(@QueryParam("page") @PositiveOrZero final int page) { + public Iterable listEmails(@QueryParam("page") @PositiveOrZero final int page) { final var user = User.getFromSecurityContext(context); return Email.find("user", Sort.by("email"), user).page(page, pagingSize).list(); } @@ -64,7 +64,7 @@ public Iterable list(@QueryParam("page") @PositiveOrZero final int page) content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Email.class))) @APIResponse(responseCode = "409", description = "Conflict") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) - public Response create(@Valid @NotNull final EmailCreation input) { + public Response createEmail(@Valid @NotNull final EmailCreation input) { if (Email.count("email", input.email()) > 0) { throw new ConflictException("email_taken"); } @@ -84,7 +84,7 @@ public Response create(@Valid @NotNull final EmailCreation input) { @APIResponse(responseCode = "204", description = "No Content") @APIResponse(responseCode = "404", description = "Not Found") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) - public Response delete(@PathParam("id") final UUID id) { + public Response deleteEmail(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context); final var email = Email.find("user = ?1 and id = ?2", user, id).firstResult(); @@ -104,7 +104,7 @@ public Response delete(@PathParam("id") final UUID id) { @Transactional @APIResponse(responseCode = "200", description = "OK") @APIResponse(responseCode = "404", description = "Not Found") - public Response updateMain(@PathParam("id") final UUID id) { + public Response setMainEmail(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context); final var email = Email.find("user = ?1 and id = ?2", user, id).firstResult(); @@ -123,7 +123,7 @@ public Response updateMain(@PathParam("id") final UUID id) { @Path("count") @Authenticated @APIResponse(responseCode = "200", description = "OK") - public long count() { + public long countEmails() { return Email.count("user", User.getFromSecurityContext(context)); } @@ -135,7 +135,7 @@ public long count() { @APIResponse(responseCode = "400", description = "Bad Request") @APIResponse(responseCode = "404", description = "Not Found") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) - public Response activate(@NotNull @Valid final EmailActivation input) { + public Response activateEmail(@NotNull @Valid final EmailActivation input) { final var email = Email.find("email", input.email()).firstResult(); final var randomCode = RandomCode.find("email = ?1 and code = ?2", email, input.code()) .firstResult(); diff --git a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java index a121008..ae216be 100644 --- a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java @@ -54,7 +54,7 @@ public final class PostsEndpoint { @Authenticated @APIResponse(responseCode = "200", description = "OK") @APIResponse(responseCode = "400", description = "Bad Request") - public Iterable list( + public Iterable listPosts( @QueryParam("page") @PositiveOrZero final int page, @QueryParam("ascending") final boolean ascending, @QueryParam("type") @NotNull final PostListingType type) { @@ -83,7 +83,7 @@ public Iterable list( content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Post.class))) @APIResponse(responseCode = "400", description = "Bad Request") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) - public Response create() { + public Response createPost() { final var user = User.getFromSecurityContext(context); final var post = new Post(); post.author = user; @@ -96,7 +96,7 @@ public Response create() { @Path("{id}") @APIResponse(responseCode = "200", description = "OK") @APIResponse(responseCode = "404", description = "Not Found") - public Post retrieve(@PathParam("id") final UUID id) { + public Post getPost(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context, null, false); final var post = Post.findById(id); Post.validateAccess(post, user, null, null); @@ -110,7 +110,7 @@ public Post retrieve(@PathParam("id") final UUID id) { @APIResponse(responseCode = "204", description = "No Content") @APIResponse(responseCode = "404", description = "Not Found") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) - public Response delete(@PathParam("id") final UUID id) { + public Response deletePost(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); Post.validateAccess(post, user, null, true); @@ -131,7 +131,7 @@ public Response delete(@PathParam("id") final UUID id) { @APIResponse(responseCode = "200", description = "OK") @APIResponse(responseCode = "400", description = "Bad Request") @APIResponse(responseCode = "404", description = "Not Found") - public Response updateSubscribed(@PathParam("id") final UUID id, @Valid @NotNull final SubscriptionUpdate input) { + public Response setPostSubscribed(@PathParam("id") final UUID id, @Valid @NotNull final SubscriptionUpdate input) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); Post.validateAccess(post, user, true, null); @@ -151,7 +151,7 @@ public Response updateSubscribed(@PathParam("id") final UUID id, @Valid @NotNull @Transactional @APIResponse(responseCode = "200", description = "OK") @APIResponse(responseCode = "404", description = "Not Found") - public Response updateReported(@PathParam("id") final UUID id, @NotNull @Valid final ReportUpdate input) { + public Response setPostReported(@PathParam("id") final UUID id, @NotNull @Valid final ReportUpdate input) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); Post.validateAccess(post, user, true, false); @@ -173,7 +173,7 @@ public Response updateReported(@PathParam("id") final UUID id, @NotNull @Valid f @APIResponse(responseCode = "400", description = "Bad Request") @APIResponse(responseCode = "404", description = "Not Found") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) - public Response publish(@PathParam("id") final UUID id, @Valid @NotNull final PostPublication input) { + public Response publishPost(@PathParam("id") final UUID id, @Valid @NotNull final PostPublication input) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); Post.validateAccess(post, user, false, true); @@ -194,7 +194,7 @@ public Response publish(@PathParam("id") final UUID id, @Valid @NotNull final Po @APIResponse(responseCode = "400", description = "Bad Request") @APIResponse(responseCode = "404", description = "Not Found") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) - public Response vote(@PathParam("id") final UUID id, @Valid @NotNull final VoteCreation input) { + public Response votePost(@PathParam("id") final UUID id, @Valid @NotNull final VoteCreation input) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id, LockModeType.PESSIMISTIC_WRITE); Post.validateAccess(post, user, true, false); @@ -220,7 +220,7 @@ public Response vote(@PathParam("id") final UUID id, @Valid @NotNull final VoteC @Authenticated @APIResponse(responseCode = "200", description = "OK") @APIResponse(responseCode = "400", description = "Bad Request") - public long count(@QueryParam("type") @NotNull final PostListingType type) { + public long countPosts(@QueryParam("type") @NotNull final PostListingType type) { final var user = User.getFromSecurityContext(context); return switch (type) { case SUBSCRIBED_TO -> Subscription.count("user = ?1 and post.deleted = false", user); @@ -233,7 +233,7 @@ public long count(@QueryParam("type") @NotNull final PostListingType type) { @Path("feed") @Authenticated @APIResponse(responseCode = "200", description = "OK") - public Iterable listFeed() { + public Iterable listPostsFeed() { final var user = User.getFromSecurityContext(context); try (final var stream = Post.find( diff --git a/src/main/java/app/fyreplace/api/endpoints/ReportsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/ReportsEndpoint.java index ec26531..4428b94 100644 --- a/src/main/java/app/fyreplace/api/endpoints/ReportsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/ReportsEndpoint.java @@ -17,7 +17,7 @@ public final class ReportsEndpoint { @GET @RolesAllowed("MODERATOR") @APIResponse(responseCode = "200", description = "OK") - public Iterable list(@QueryParam("page") @PositiveOrZero final int page) { + public Iterable listReports(@QueryParam("page") @PositiveOrZero final int page) { return Report.findAll(Report.sorting()).page(page, pagingSize).list(); } } diff --git a/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java index 3bc5b09..8e8595b 100644 --- a/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java @@ -32,7 +32,7 @@ public final class StoredFilesEndpoint { @APIResponse(responseCode = "200", description = "OK") @APIResponse(responseCode = "303", description = "See Other") @APIResponse(responseCode = "404", description = "Not Found") - public Response retrieve(@PathParam("path") final String path) throws URISyntaxException { + public Response getStoredFile(@PathParam("path") final String path) throws URISyntaxException { final var appUri = new URI(appUrl); final var requestUri = storageService.getUri(path); diff --git a/src/main/java/app/fyreplace/api/endpoints/SubscriptionsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/SubscriptionsEndpoint.java index c0f36f5..dba30ef 100644 --- a/src/main/java/app/fyreplace/api/endpoints/SubscriptionsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/SubscriptionsEndpoint.java @@ -31,7 +31,7 @@ public final class SubscriptionsEndpoint { @Transactional @APIResponse(responseCode = "204", description = "No Content") @APIResponse(responseCode = "404", description = "Not Found") - public void delete(@PathParam("id") final UUID id) { + public void deleteSubscription(@PathParam("id") final UUID id) { final var user = User.getFromSecurityContext(context); final var subscription = Subscription.findById(id); @@ -47,7 +47,7 @@ public void delete(@PathParam("id") final UUID id) { @Authenticated @APIResponse(responseCode = "200", description = "OK") @APIResponse(responseCode = "400", description = "Bad Request") - public Iterable listUnread(@QueryParam("page") @PositiveOrZero final int page) { + public Iterable listUnreadSubscriptions(@QueryParam("page") @PositiveOrZero final int page) { final var user = User.getFromSecurityContext(context); try (final var stream = @@ -63,7 +63,7 @@ public Iterable listUnread(@QueryParam("page") @PositiveOrZero fin @Authenticated @Transactional @APIResponse(responseCode = "204", description = "No Content") - public void clearUnread() { + public void clearUnreadSubscriptions() { Subscription.markAsRead(User.getFromSecurityContext(context)); } } diff --git a/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java index 8b426c4..4e311fa 100644 --- a/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java @@ -49,7 +49,7 @@ public final class TokensEndpoint { @APIResponse(responseCode = "400", description = "Bad Request") @APIResponse(responseCode = "404", description = "Not Found") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) - public Response create(@Valid @NotNull final TokenCreation input) { + public Response createToken(@Valid @NotNull final TokenCreation input) { final var email = getEmail(input.identifier()); final var randomCode = RandomCode.find("email = ?1 and code = ?2", email, input.secret()) .firstResult(); @@ -78,7 +78,7 @@ public Response create(@Valid @NotNull final TokenCreation input) { responseCode = "200", description = "OK", content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) - public String retrieveNew() { + public String getNewToken() { return jwtService.makeJwt(User.getFromSecurityContext(context)); } @@ -89,7 +89,7 @@ public String retrieveNew() { @APIResponse(responseCode = "400", description = "Bad Request") @APIResponse(responseCode = "404", description = "Not Found") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) - public Response createNew(@NotNull @Valid final NewTokenCreation input) { + public Response createNewToken(@NotNull @Valid final NewTokenCreation input) { final var email = getEmail(input.identifier()); userConnectionEmail.sendTo(email); return Response.ok().build(); diff --git a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java index 547f1a8..7d05cc0 100644 --- a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java @@ -73,7 +73,7 @@ public final class UsersEndpoint { @APIResponse(responseCode = "403", description = "Not Allowed") @APIResponse(responseCode = "409", description = "Conflict") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) - public Response create(@Valid @NotNull final UserCreation input) { + public Response createUser(@Valid @NotNull final UserCreation input) { if (User.forbiddenUsernames.contains(input.username())) { throw new ForbiddenException("username_forbidden"); } else if (User.count("username", input.username()) > 0) { @@ -101,7 +101,7 @@ public Response create(@Valid @NotNull final UserCreation input) { @Path("{id}") @APIResponse(responseCode = "200", description = "OK") @APIResponse(responseCode = "404", description = "Not Found") - public User retrieve(@PathParam("id") final UUID id) { + public User getUser(@PathParam("id") final UUID id) { final var user = User.findById(id); validateUser(user); return user; @@ -114,7 +114,7 @@ public User retrieve(@PathParam("id") final UUID id) { @APIResponse(responseCode = "200", description = "OK") @APIResponse(responseCode = "400", description = "Bad Request") @APIResponse(responseCode = "404", description = "Not Found") - public Response updateBlocked(@PathParam("id") final UUID id, @Valid @NotNull final BlockUpdate input) { + public Response setUserBlocked(@PathParam("id") final UUID id, @Valid @NotNull final BlockUpdate input) { final var source = User.getFromSecurityContext(context); final var target = User.findById(id); validateUser(target); @@ -136,7 +136,7 @@ public Response updateBlocked(@PathParam("id") final UUID id, @Valid @NotNull fi @Transactional @APIResponse(responseCode = "200", description = "OK") @APIResponse(responseCode = "404", description = "Not Found") - public Response updateBanned(@PathParam("id") final UUID id) { + public Response setUserBanned(@PathParam("id") final UUID id) { final var user = User.findById(id, LockModeType.PESSIMISTIC_WRITE); validateUser(user); @@ -161,7 +161,7 @@ public Response updateBanned(@PathParam("id") final UUID id) { @Transactional @APIResponse(responseCode = "200", description = "OK") @APIResponse(responseCode = "404", description = "Not Found") - public Response updateReported(@PathParam("id") final UUID id, @NotNull @Valid final ReportUpdate input) { + public Response setUserReported(@PathParam("id") final UUID id, @NotNull @Valid final ReportUpdate input) { final var source = User.getFromSecurityContext(context); final var target = User.findById(id); validateUser(target); @@ -178,15 +178,15 @@ public Response updateReported(@PathParam("id") final UUID id, @NotNull @Valid f } @GET - @Path("me") + @Path("current") @Authenticated @APIResponse(responseCode = "200", description = "OK") - public User retrieveMe() { - return retrieve(User.getFromSecurityContext(context).id); + public User getCurrentUser() { + return getUser(User.getFromSecurityContext(context).id); } @PUT - @Path("me/bio") + @Path("current/bio") @Authenticated @Transactional @Consumes(MediaType.TEXT_PLAIN) @@ -195,7 +195,7 @@ public User retrieveMe() { description = "OK", content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) @APIResponse(responseCode = "400", description = "Bad Request") - public String updateMeBio(@NotNull @Length(max = 3000) final String input) { + public String setCurrentUserBio(@NotNull @Length(max = 3000) final String input) { final var user = User.getFromSecurityContext(context, LockModeType.PESSIMISTIC_READ); user.bio = input; user.persist(); @@ -203,7 +203,7 @@ public String updateMeBio(@NotNull @Length(max = 3000) final String input) { } @PUT - @Path("me/avatar") + @Path("current/avatar") @Authenticated @Transactional @Consumes(MediaType.APPLICATION_OCTET_STREAM) @@ -213,7 +213,7 @@ public String updateMeBio(@NotNull @Length(max = 3000) final String input) { content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) @APIResponse(responseCode = "413", description = "Payload Too Large") @APIResponse(responseCode = "415", description = "Unsupported Media Type") - public String updateMeAvatar(final byte[] input) throws IOException { + public String setCurrentUserAvatar(final byte[] input) throws IOException { mimeTypeService.validate(input, KnownMimeTypes.IMAGE); final var user = User.getFromSecurityContext(context, LockModeType.PESSIMISTIC_WRITE); @@ -228,11 +228,11 @@ public String updateMeAvatar(final byte[] input) throws IOException { } @DELETE - @Path("me/avatar") + @Path("current/avatar") @Authenticated @Transactional @APIResponse(responseCode = "204", description = "No Content") - public void deleteMeAvatar() { + public void deleteCurrentUserAvatar() { final var user = User.getFromSecurityContext(context, LockModeType.PESSIMISTIC_WRITE); if (user.avatar != null) { @@ -243,12 +243,12 @@ public void deleteMeAvatar() { } @DELETE - @Path("me") + @Path("current") @Authenticated @Transactional @APIResponse(responseCode = "204", description = "No Content") @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class) - public Response deleteMe() { + public Response deleteCurrentUser() { User.getFromSecurityContext(context).softDelete(); return Response.noContent().build(); } @@ -257,7 +257,7 @@ public Response deleteMe() { @Path("blocked") @Authenticated @APIResponse(responseCode = "200", description = "OK") - public Iterable listBlocked(@QueryParam("page") @PositiveOrZero final int page) { + public Iterable listBlockedUsers(@QueryParam("page") @PositiveOrZero final int page) { final var user = User.getFromSecurityContext(context); final var blocks = Block.find("source", Sort.by("id"), user); @@ -270,7 +270,7 @@ public Iterable listBlocked(@QueryParam("page") @PositiveOrZero fi @Path("blocked/count") @Authenticated @APIResponse(responseCode = "200", description = "OK") - public long countBlocked() { + public long countBlockedUsers() { return Block.count("source", User.getFromSecurityContext(context)); } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 59a555c..4f3b099 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -24,14 +24,20 @@ quarkus: enabled: true mailer: port: 587 - start-tls: "REQUIRED" + start-tls: "required" s3: devservices: enabled: false + smallrye-openapi: + info-title: "${app.name} API" mp: jwt: verify: issuer: "${app.url}" + openapi: + extensions: + smallrye: + operationIdStrategy: "method" app: url: "" name: "Fyreplace" diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/CreateChapterTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/CreateChapterTests.java index f76c421..ff2f8e9 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/CreateChapterTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/CreateChapterTests.java @@ -89,14 +89,14 @@ public void createChapterInOtherDraft() { } @Test - public void createChapterInPostUnauthenticated() { + public void createChapterInPostWhileUnauthenticated() { final var chapterCount = post.getChapters().size(); given().pathParam("id", post.id).post().then().statusCode(401); assertEquals(chapterCount, Chapter.count("post", post)); } @Test - public void createChapterInDraftUnauthenticated() { + public void createChapterInDraftWhileUnauthenticated() { final var chapterCount = draft.getChapters().size(); given().pathParam("id", draft.id).post().then().statusCode(401); assertEquals(chapterCount, Chapter.count("post", draft)); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterImageTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/SetChapterImageTests.java similarity index 87% rename from src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterImageTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/chapters/SetChapterImageTests.java index 836714d..e307084 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterImageTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/SetChapterImageTests.java @@ -19,10 +19,10 @@ @QuarkusTest @TestHTTPEndpoint(ChaptersEndpoint.class) -public final class UpdateChapterImageTests extends PostTestsBase { +public final class SetChapterImageTests extends PostTestsBase { @Test @TestSecurity(user = "user_0") - public void updateChapterImageInOwnPost() throws IOException { + public void setChapterImageInOwnPost() throws IOException { final var position = 0; try (final var stream = openStream("jpeg")) { @@ -41,7 +41,7 @@ public void updateChapterImageInOwnPost() throws IOException { @ParameterizedTest @ValueSource(strings = {"jpeg", "png", "webp"}) @TestSecurity(user = "user_0") - public void updateChapterImageInOwnDraft(final String fileType) throws IOException { + public void setChapterImageInOwnDraft(final String fileType) throws IOException { final var position = 0; try (final var stream = openStream(fileType)) { @@ -60,7 +60,7 @@ public void updateChapterImageInOwnDraft(final String fileType) throws IOExcepti @ParameterizedTest @ValueSource(strings = {"-1", "12"}) @TestSecurity(user = "user_0") - public void updateChapterImageInOwnDraftOutOfBounds(final String position) throws IOException { + public void setChapterImageInOwnDraftOutOfBounds(final String position) throws IOException { try (final var stream = openStream("jpeg")) { given().contentType(ContentType.BINARY) .body(stream.readAllBytes()) @@ -74,7 +74,7 @@ public void updateChapterImageInOwnDraftOutOfBounds(final String position) throw @ParameterizedTest @ValueSource(strings = {"gif", "text"}) @TestSecurity(user = "user_0") - public void updateChapterImageInOwnDraftWithInvalidType(final String fileType) throws IOException { + public void setChapterImageInOwnDraftWithInvalidType(final String fileType) throws IOException { final var position = 0; try (final var stream = openStream(fileType)) { @@ -92,7 +92,7 @@ public void updateChapterImageInOwnDraftWithInvalidType(final String fileType) t @Test @TestSecurity(user = "user_0") - public void updateChapterImageInOwnDraftWithoutInput() { + public void setChapterImageInOwnDraftWithoutInput() { final var position = 0; given().pathParam("id", draft.id).put(position + "/image").then().statusCode(415); final var chapter = draft.getChapters().getFirst(); @@ -101,7 +101,7 @@ public void updateChapterImageInOwnDraftWithoutInput() { @Test @TestSecurity(user = "user_1") - public void updateChapterImageInOtherPost() throws IOException { + public void setChapterImageInOtherPost() throws IOException { final var position = 0; try (final var stream = openStream("jpeg")) { @@ -119,7 +119,7 @@ public void updateChapterImageInOtherPost() throws IOException { @Test @TestSecurity(user = "user_1") - public void updateChapterImageInOtherDraft() throws IOException { + public void setChapterImageInOtherDraft() throws IOException { final var position = 0; try (final var stream = openStream("jpeg")) { @@ -136,7 +136,7 @@ public void updateChapterImageInOtherDraft() throws IOException { } @Test - public void updateChapterImageInPostUnauthenticated() throws IOException { + public void setChapterImageInPostWhileUnauthenticated() throws IOException { final var position = 0; try (final var stream = openStream("jpeg")) { @@ -153,7 +153,7 @@ public void updateChapterImageInPostUnauthenticated() throws IOException { } @Test - public void updateChapterImageInDraftUnauthenticated() throws IOException { + public void setChapterImageInDraftWhileUnauthenticated() throws IOException { final var position = 0; try (final var stream = openStream("jpeg")) { @@ -171,7 +171,7 @@ public void updateChapterImageInDraftUnauthenticated() throws IOException { @Test @TestSecurity(user = "user_0") - public void updateChapterTextInNonExistentPost() throws IOException { + public void setChapterTextInNonExistentPost() throws IOException { final var position = 0; try (final var stream = openStream("jpeg")) { @@ -190,7 +190,7 @@ public void updateChapterTextInNonExistentPost() throws IOException { @ParameterizedTest @ValueSource(strings = {"fake", "null"}) @TestSecurity(user = "user_0") - public void updateNonExistentChapterText(final String position) throws IOException { + public void setNonExistentChapterText(final String position) throws IOException { try (final var stream = openStream("jpeg")) { given().contentType(ContentType.BINARY) .body(stream.readAllBytes()) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterPositionTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/SetChapterPositionTests.java similarity index 86% rename from src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterPositionTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/chapters/SetChapterPositionTests.java index 4e1b58d..97b7198 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterPositionTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/SetChapterPositionTests.java @@ -16,12 +16,12 @@ @QuarkusTest @TestHTTPEndpoint(ChaptersEndpoint.class) -public final class UpdateChapterPositionTests extends PostTestsBase { +public final class SetChapterPositionTests extends PostTestsBase { @ParameterizedTest @ValueSource(ints = {0, 1, 2}) @TestSecurity(user = "user_0") @Transactional - public void updateChapterPositionInOwnPost(final int to) { + public void setChapterPositionInOwnPost(final int to) { final var from = 1; final var chapter = post.getChapters().get(from); final var position = chapter.position; @@ -33,7 +33,7 @@ public void updateChapterPositionInOwnPost(final int to) { @ParameterizedTest @ValueSource(ints = {0, 1, 2}) @TestSecurity(user = "user_0") - public void updateChapterPositionInOwnDraft(final int to) { + public void setChapterPositionInOwnDraft(final int to) { final var from = 1; final var chapter = draft.getChapters().get(from); given().body(to) @@ -49,7 +49,7 @@ public void updateChapterPositionInOwnDraft(final int to) { @ValueSource(strings = {"-1", "12"}) @TestSecurity(user = "user_0") @Transactional - public void updateChapterPositionInOwnDraftOutOfBounds(final String to) { + public void setChapterPositionInOwnDraftOutOfBounds(final String to) { final var from = 1; final var chapter = draft.getChapters().get(from); final var position = chapter.position; @@ -66,7 +66,7 @@ public void updateChapterPositionInOwnDraftOutOfBounds(final String to) { @ValueSource(strings = {"1.5", "null"}) @TestSecurity(user = "user_0") @Transactional - public void updateChapterPositionInOwnDraftWithInvalidInput(final String to) { + public void setChapterPositionInOwnDraftWithInvalidInput(final String to) { final var from = 1; final var chapter = draft.getChapters().get(from); final var position = chapter.position; @@ -83,7 +83,7 @@ public void updateChapterPositionInOwnDraftWithInvalidInput(final String to) { @ValueSource(ints = {0, 1, 2}) @TestSecurity(user = "user_1") @Transactional - public void updateChapterPositionInOtherPost(final int to) { + public void setChapterPositionInOtherPost(final int to) { final var from = 1; final var chapter = post.getChapters().get(from); final var oldPosition = chapter.position; @@ -96,7 +96,7 @@ public void updateChapterPositionInOtherPost(final int to) { @ValueSource(ints = {0, 1, 2}) @TestSecurity(user = "user_1") @Transactional - public void updateChapterPositionInOtherDraft(final int to) { + public void setChapterPositionInOtherDraft(final int to) { final var from = 1; final var chapter = draft.getChapters().get(from); final var oldPosition = chapter.position; @@ -112,7 +112,7 @@ public void updateChapterPositionInOtherDraft(final int to) { @ParameterizedTest @ValueSource(ints = {0, 1, 2}) @Transactional - public void updateChapterPositionInPostUnauthenticated(final int to) { + public void setChapterPositionInPostUnauthenticated(final int to) { final var from = 1; final var chapter = post.getChapters().get(from); final var oldPosition = chapter.position; @@ -124,7 +124,7 @@ public void updateChapterPositionInPostUnauthenticated(final int to) { @ParameterizedTest @ValueSource(ints = {0, 1, 2}) @Transactional - public void updateChapterPositionInDraftUnauthenticated(final int to) { + public void setChapterPositionInDraftUnauthenticated(final int to) { final var from = 1; final var chapter = draft.getChapters().get(from); final var oldPosition = chapter.position; @@ -141,7 +141,7 @@ public void updateChapterPositionInDraftUnauthenticated(final int to) { @ValueSource(ints = {0, 1, 2}) @TestSecurity(user = "user_0") @Transactional - public void updateChapterPositionInNonExistentPost(final int to) { + public void setChapterPositionInNonExistentPost(final int to) { final var from = 1; final var chapter = post.getChapters().get(from); final var oldPosition = chapter.position; @@ -154,7 +154,7 @@ public void updateChapterPositionInNonExistentPost(final int to) { @ValueSource(strings = {"fake", "null"}) @TestSecurity(user = "user_0") @Transactional - public void updateNonExistentChapterPosition(final String from) { + public void setNonExistentChapterPosition(final String from) { given().body(1).pathParam("id", draft.id).put(from + "/position").then().statusCode(404); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterTextTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/SetChapterTextTests.java similarity index 87% rename from src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterTextTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/chapters/SetChapterTextTests.java index 31b4470..31d19dc 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/UpdateChapterTextTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/SetChapterTextTests.java @@ -15,11 +15,11 @@ @QuarkusTest @TestHTTPEndpoint(ChaptersEndpoint.class) -public final class UpdateChapterTextTests extends PostTestsBase { +public final class SetChapterTextTests extends PostTestsBase { @Test @TestSecurity(user = "user_0") @Transactional - public void updateChapterTextInOwnPost() { + public void setChapterTextInOwnPost() { final var position = 0; final var chapter = post.getChapters().getFirst(); final var oldText = chapter.text; @@ -35,7 +35,7 @@ public void updateChapterTextInOwnPost() { @ParameterizedTest @ValueSource(strings = {"Hello", ""}) @TestSecurity(user = "user_0") - public void updateChapterTextInOwnDraft(final String text) { + public void setChapterTextInOwnDraft(final String text) { final var position = 0; given().body(text) .pathParam("id", draft.id) @@ -49,7 +49,7 @@ public void updateChapterTextInOwnDraft(final String text) { @ParameterizedTest @ValueSource(strings = {"-1", "12"}) @TestSecurity(user = "user_0") - public void updateChapterTextInOwnDraftOutOfBounds(final String position) { + public void setChapterTextInOwnDraftOutOfBounds(final String position) { given().body("Hello") .pathParam("id", draft.id) .put(position + "/text") @@ -60,7 +60,7 @@ public void updateChapterTextInOwnDraftOutOfBounds(final String position) { @Test @TestSecurity(user = "user_0") @Transactional - public void updateChapterTextInOwnDraftWithInvalidInput() { + public void setChapterTextInOwnDraftWithInvalidInput() { final var position = 0; final var chapter = draft.getChapters().getFirst(); final var oldText = chapter.text; @@ -76,7 +76,7 @@ public void updateChapterTextInOwnDraftWithInvalidInput() { @Test @TestSecurity(user = "user_1") @Transactional - public void updateChapterTextInOtherPost() { + public void setChapterTextInOtherPost() { final var position = 0; final var chapter = post.getChapters().getFirst(); final var oldText = chapter.text; @@ -92,7 +92,7 @@ public void updateChapterTextInOtherPost() { @Test @TestSecurity(user = "user_1") @Transactional - public void updateChapterTextInOtherDraft() { + public void setChapterTextInOtherDraft() { final var position = 0; final var chapter = draft.getChapters().getFirst(); final var oldText = chapter.text; @@ -107,7 +107,7 @@ public void updateChapterTextInOtherDraft() { @Test @Transactional - public void updateChapterTextInPostUnauthenticated() { + public void setChapterTextInPostWhileUnauthenticated() { final var position = 0; final var chapter = post.getChapters().getFirst(); final var oldText = chapter.text; @@ -122,7 +122,7 @@ public void updateChapterTextInPostUnauthenticated() { @Test @Transactional - public void updateChapterTextInDraftUnauthenticated() { + public void setChapterTextInDraftWhileUnauthenticated() { final var position = 0; final var chapter = draft.getChapters().getFirst(); final var oldText = chapter.text; @@ -138,7 +138,7 @@ public void updateChapterTextInDraftUnauthenticated() { @Test @TestSecurity(user = "user_0") @Transactional - public void updateChapterTextInNonExistentPost() { + public void setChapterTextInNonExistentPost() { final var position = 0; final var chapter = draft.getChapters().getFirst(); final var oldText = chapter.text; @@ -154,7 +154,7 @@ public void updateChapterTextInNonExistentPost() { @ParameterizedTest @ValueSource(strings = {"fake", "null"}) @TestSecurity(user = "user_0") - public void updateNonExistentChapterText(final String from) { + public void setNonExistentChapterText(final String from) { given().body("Hello") .pathParam("id", draft.id) .put(from + "/text") diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/AcknowledgeTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/AcknowledgeCommentTests.java similarity index 92% rename from src/test/java/app/fyreplace/api/testing/endpoints/comments/AcknowledgeTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/comments/AcknowledgeCommentTests.java index 818d420..799f908 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/comments/AcknowledgeTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/AcknowledgeCommentTests.java @@ -15,10 +15,10 @@ @QuarkusTest @TestHTTPEndpoint(CommentsEndpoint.class) -public final class AcknowledgeTests extends CommentTestsBase { +public final class AcknowledgeCommentTests extends CommentTestsBase { @Test @TestSecurity(user = "user_0") - public void acknowledge() { + public void acknowledgeComment() { final var position = 5; given().pathParam("id", post.id).post(position + "/acknowledge").then().statusCode(200); final var subscription = Subscription.find("user.username = 'user_0' and post = ?1", post) @@ -50,14 +50,14 @@ public void acknowledgePastComment() { @Test @TestSecurity(user = "user_0") - public void acknowledgeOutOfBounds() { + public void acknowledgeCommentOutOfBounds() { final var position = -1; given().pathParam("id", post.id).post(position + "/acknowledge").then().statusCode(400); } @Test @TestSecurity(user = "user_0") - public void acknowledgeTooFar() { + public void acknowledgeCommentTooFar() { final var position = 12; given().pathParam("id", post.id).post(position + "/acknowledge").then().statusCode(404); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/CountTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/CountCommentsTests.java similarity index 93% rename from src/test/java/app/fyreplace/api/testing/endpoints/comments/CountTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/comments/CountCommentsTests.java index 78da7b2..91c6fe8 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/comments/CountTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/CountCommentsTests.java @@ -23,7 +23,7 @@ @QuarkusTest @TestHTTPEndpoint(CommentsEndpoint.class) -public final class CountTests extends CommentTestsBase { +public final class CountCommentsTests extends CommentTestsBase { @Inject DataSeeder dataSeeder; @@ -33,7 +33,7 @@ public final class CountTests extends CommentTestsBase { @Test @TestSecurity(user = "user_1") - public void count() { + public void countComments() { given().pathParam("id", post.id) .get("count") .then() @@ -43,7 +43,7 @@ public void count() { @Test @TestSecurity(user = "user_1") - public void countRead() { + public void countReadComments() { given().pathParam("id", post.id) .queryParam("read", true) .get("count") @@ -54,7 +54,7 @@ public void countRead() { @Test @TestSecurity(user = "user_1") - public void countNotRead() { + public void countNotReadComments() { given().pathParam("id", post.id) .queryParam("read", false) .get("count") @@ -65,7 +65,7 @@ public void countNotRead() { @Test @TestSecurity(user = "user_1") - public void countWhenBlocked() { + public void countCommentsWhenBlocked() { QuarkusTransaction.requiringNew().run(() -> { final var user = requireNonNull(User.findByUsername("user_0")); final var otherUser = requireNonNull(User.findByUsername("user_1")); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/CreateCommentTests.java similarity index 91% rename from src/test/java/app/fyreplace/api/testing/endpoints/comments/CreateTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/comments/CreateCommentTests.java index 72bfb4f..1560e3b 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/comments/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/CreateCommentTests.java @@ -19,10 +19,10 @@ @QuarkusTest @TestHTTPEndpoint(CommentsEndpoint.class) -public final class CreateTests extends CommentTestsBase { +public final class CreateCommentTests extends CommentTestsBase { @Test @TestSecurity(user = "user_0") - public void createOnOwnPost() { + public void createCommentOnOwnPost() { final var commentCount = Comment.count("post", post); final var input = new CommentCreation("Text", false); final var response = given().contentType(ContentType.JSON) @@ -42,7 +42,7 @@ public void createOnOwnPost() { @Test @TestSecurity(user = "user_0") - public void createAnonymouslyOnOwnPost() { + public void createCommentAnonymouslyOnOwnPost() { final var commentCount = Comment.count("post", post); final var input = new CommentCreation("Text", true); final var response = given().contentType(ContentType.JSON) @@ -62,7 +62,7 @@ public void createAnonymouslyOnOwnPost() { @Test @TestSecurity(user = "user_0") - public void createOnOwnDraft() { + public void createCommentOnOwnDraft() { final var input = new CommentCreation("Text", false); given().contentType(ContentType.JSON) .body(input) @@ -75,7 +75,7 @@ public void createOnOwnDraft() { @Test @TestSecurity(user = "user_1") - public void createOnOtherPost() { + public void createCommentOnOtherPost() { final var commentCount = Comment.count("post", post); final var input = new CommentCreation("Text", false); final var response = given().contentType(ContentType.JSON) @@ -94,7 +94,7 @@ public void createOnOtherPost() { @Test @TestSecurity(user = "user_1") - public void createAnonymouslyOnOtherPost() { + public void createCommentAnonymouslyOnOtherPost() { final var commentCount = Comment.count("post", post); final var input = new CommentCreation("Text", true); given().contentType(ContentType.JSON) @@ -108,7 +108,7 @@ public void createAnonymouslyOnOtherPost() { @Test @TestSecurity(user = "user_1") - public void createOnOtherPostWhenBlocked() { + public void createCommentOnOtherPostWhenBlocked() { QuarkusTransaction.requiringNew().run(() -> post.author.block(User.findByUsername("user_1"))); final var commentCount = Comment.count("post", post); final var input = new CommentCreation("Text", false); @@ -123,7 +123,7 @@ public void createOnOtherPostWhenBlocked() { @Test @TestSecurity(user = "user_1") - public void createOnOtherAnonymousPostWhenBlocked() { + public void createCommentOnOtherAnonymousPostWhenBlocked() { QuarkusTransaction.requiringNew().run(() -> anonymousPost.author.block(User.findByUsername("user_1"))); final var commentCount = Comment.count("post", anonymousPost); final var input = new CommentCreation("Text", false); @@ -138,7 +138,7 @@ public void createOnOtherAnonymousPostWhenBlocked() { @Test @TestSecurity(user = "user_1") - public void createOnOtherDraft() { + public void createCommentOnOtherDraft() { final var input = new CommentCreation("Text", false); given().contentType(ContentType.JSON) .body(input) @@ -151,7 +151,7 @@ public void createOnOtherDraft() { @Test @TestSecurity(user = "user_1") - public void createOnNonExistentPost() { + public void createCommentOnNonExistentPost() { final var commentCount = Comment.count(); final var input = new CommentCreation("Text", false); given().contentType(ContentType.JSON) @@ -165,7 +165,7 @@ public void createOnNonExistentPost() { @Test @TestSecurity(user = "user_1") - public void createWithEmptyInput() { + public void createCommentWithEmptyInput() { final var commentCount = Comment.count("post", post); given().contentType(ContentType.JSON) .pathParam("id", post.id) @@ -176,7 +176,7 @@ public void createWithEmptyInput() { } @Test - public void createUnauthenticated() { + public void createCommentWhileUnauthenticated() { final var commentCount = Comment.count("post", post); final var input = new CommentCreation("Text", false); given().contentType(ContentType.JSON) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/DeleteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/DeleteCommentTests.java similarity index 94% rename from src/test/java/app/fyreplace/api/testing/endpoints/comments/DeleteTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/comments/DeleteCommentTests.java index ffb8b25..0acf192 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/comments/DeleteTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/DeleteCommentTests.java @@ -20,7 +20,7 @@ @QuarkusTest @TestHTTPEndpoint(CommentsEndpoint.class) -public final class DeleteTests extends CommentTestsBase { +public final class DeleteCommentTests extends CommentTestsBase { @Inject DataSeeder dataSeeder; @@ -80,7 +80,7 @@ public void deleteOtherCommentOnOtherPost() { @Test @TestSecurity(user = "user_1") - public void deleteOutOfBounds() { + public void deleteCommentOutOfBounds() { final var commentCount = Comment.count("post", post); final var position = -1; given().pathParam("id", post.id).delete(String.valueOf(position)).then().statusCode(400); @@ -89,7 +89,7 @@ public void deleteOutOfBounds() { @Test @TestSecurity(user = "user_1") - public void deleteTooFar() { + public void deleteCommentTooFar() { final var commentCount = Comment.count("post", post); final var position = 50; given().pathParam("id", post.id).delete(String.valueOf(position)).then().statusCode(404); @@ -98,14 +98,14 @@ public void deleteTooFar() { @Test @TestSecurity(user = "user_0") - public void deleteOnNonExistentPost() { + public void deleteCommentOnNonExistentPost() { final var commentCount = Comment.count(); given().pathParam("id", fakeId).delete(String.valueOf(0)).then().statusCode(404); assertEquals(commentCount, Comment.count()); } @Test - public void deleteUnauthenticated() { + public void deleteCommentWhileUnauthenticated() { final var commentCount = Comment.count("post", post); given().pathParam("id", post.id).delete(String.valueOf(0)).then().statusCode(401); assertEquals(commentCount, Comment.count("post", post)); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/ListTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/ListCommentsTests.java similarity index 92% rename from src/test/java/app/fyreplace/api/testing/endpoints/comments/ListTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/comments/ListCommentsTests.java index f5535dc..d46a916 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/comments/ListTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/ListCommentsTests.java @@ -27,7 +27,7 @@ @QuarkusTest @TestHTTPEndpoint(CommentsEndpoint.class) -public final class ListTests extends CommentTestsBase { +public final class ListCommentsTests extends CommentTestsBase { @ConfigProperty(name = "app.paging.size") int pagingSize; @@ -38,7 +38,7 @@ public final class ListTests extends CommentTestsBase { @Test @TestSecurity(user = "user_0") - public void listInOwnPost() { + public void listCommentsInOwnPost() { final var response = given().pathParam("id", post.id) .queryParam("page", 0) .get() @@ -56,7 +56,7 @@ public void listInOwnPost() { @Test @TestSecurity(user = "user_1") - public void listInOtherPost() { + public void listCommentsInOtherPost() { final var response = given().pathParam("id", post.id) .queryParam("page", 0) .get() @@ -74,7 +74,7 @@ public void listInOtherPost() { @Test @TestSecurity(user = "user_1") - public void listInOtherPostWhenBlocked() { + public void listCommentsInOtherPostWhenBlocked() { final var user = requireNonNull(User.findByUsername("user_1")); QuarkusTransaction.requiringNew().run(() -> post.author.block(user)); given().pathParam("id", post.id).queryParam("page", 0).get().then().statusCode(403); @@ -82,7 +82,7 @@ public void listInOtherPostWhenBlocked() { @Test @TestSecurity(user = "user_1") - public void listInOtherAnonymousPostWhenBlocked() { + public void listCommentsInOtherAnonymousPostWhenBlocked() { final var user = requireNonNull(User.findByUsername("user_1")); QuarkusTransaction.requiringNew().run(() -> anonymousPost.author.block(user)); given().pathParam("id", anonymousPost.id) @@ -95,14 +95,14 @@ public void listInOtherAnonymousPostWhenBlocked() { @Test @TestSecurity(user = "user_0") - public void listOutOfBounds() { + public void listCommentsOutOfBounds() { final var page = -1; given().pathParam("id", post.id).queryParam("page", page).get().then().statusCode(400); } @Test @TestSecurity(user = "user_0") - public void listTooFar() { + public void listCommentsTooFar() { final var page = 50; given().pathParam("id", post.id) .queryParam("page", page) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/UpdateReportedToFalseTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/UpdateReportedToFalseTests.java index a460cd9..0f30608 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/comments/UpdateReportedToFalseTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/UpdateReportedToFalseTests.java @@ -27,7 +27,7 @@ public final class UpdateReportedToFalseTests extends CommentTestsBase { @Test @TestSecurity(user = "user_0") - public void updateReportWithOwnComment() { + public void setReportWithOwnComment() { final var user = User.findByUsername("user_0"); assertFalse(comment.isReportedBy(user)); given().contentType(ContentType.JSON) @@ -41,7 +41,7 @@ public void updateReportWithOwnComment() { @Test @TestSecurity(user = "user_1") - public void updateReportWithOtherComment() { + public void setReportWithOtherComment() { final var user = User.findByUsername("user_1"); assertTrue(comment.isReportedBy(user)); given().contentType(ContentType.JSON) @@ -55,7 +55,7 @@ public void updateReportWithOtherComment() { @Test @TestSecurity(user = "user_1") - public void updateReportWithOtherCommentTwice() { + public void setReportWithOtherCommentTwice() { final var user = User.findByUsername("user_1"); assertTrue(comment.isReportedBy(user)); given().contentType(ContentType.JSON) @@ -75,7 +75,7 @@ public void updateReportWithOtherCommentTwice() { @Test @TestSecurity(user = "user_0") - public void updateReportOutOfBounds() { + public void setReportOutOfBounds() { final var reportCount = Report.count(); given().contentType(ContentType.JSON) .body(new ReportUpdate(false)) @@ -88,7 +88,7 @@ public void updateReportOutOfBounds() { @Test @TestSecurity(user = "user_0") - public void updateReportTooFar() { + public void setReportTooFar() { final var reportCount = Report.count(); given().contentType(ContentType.JSON) .body(new ReportUpdate(false)) @@ -100,7 +100,7 @@ public void updateReportTooFar() { } @Test - public void updateReportUnauthenticated() { + public void setReportWhileUnauthenticated() { final var reportCount = Report.count(); given().contentType(ContentType.JSON) .body(new ReportUpdate(false)) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/comments/UpdateReportedToTrueTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/comments/UpdateReportedToTrueTests.java index a610e24..207cb38 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/comments/UpdateReportedToTrueTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/comments/UpdateReportedToTrueTests.java @@ -26,7 +26,7 @@ public final class UpdateReportedToTrueTests extends CommentTestsBase { @Test @TestSecurity(user = "user_0") - public void updateReportWithOwnComment() { + public void setReportWithOwnComment() { final var user = User.findByUsername("user_0"); assertFalse(comment.isReportedBy(user)); given().contentType(ContentType.JSON) @@ -40,7 +40,7 @@ public void updateReportWithOwnComment() { @Test @TestSecurity(user = "user_1") - public void updateReportWithOtherComment() { + public void setReportWithOtherComment() { final var user = User.findByUsername("user_1"); assertFalse(comment.isReportedBy(user)); given().contentType(ContentType.JSON) @@ -54,7 +54,7 @@ public void updateReportWithOtherComment() { @Test @TestSecurity(user = "user_1") - public void updateReportWithOtherCommentTwice() { + public void setReportWithOtherCommentTwice() { final var user = User.findByUsername("user_1"); assertFalse(comment.isReportedBy(user)); given().contentType(ContentType.JSON) @@ -74,7 +74,7 @@ public void updateReportWithOtherCommentTwice() { @Test @TestSecurity(user = "user_0") - public void updateReportOutOfBounds() { + public void setReportOutOfBounds() { final var reportCount = Report.count(); given().contentType(ContentType.JSON) .body(new ReportUpdate(true)) @@ -87,7 +87,7 @@ public void updateReportOutOfBounds() { @Test @TestSecurity(user = "user_0") - public void updateReportTooFar() { + public void setReportTooFar() { final var reportCount = Report.count(); given().contentType(ContentType.JSON) .body(new ReportUpdate(true)) @@ -99,7 +99,7 @@ public void updateReportTooFar() { } @Test - public void updateReportUnauthenticated() { + public void setReportWhileUnauthenticated() { final var reportCount = Report.count(); given().contentType(ContentType.JSON) .body(new ReportUpdate(true)) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/dev/RetrievePasswordHashTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/dev/RetrievePasswordHashTests.java index 75a04b5..acb16e5 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/dev/RetrievePasswordHashTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/dev/RetrievePasswordHashTests.java @@ -14,7 +14,7 @@ public final class RetrievePasswordHashTests extends UserTestsBase { @Test @TestSecurity(user = "user_0") - public void retrieveToken() { + public void getPasswordHash() { given().get("passwords/hello/hash").then().statusCode(403); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/dev/RetrieveUserTokenTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/dev/RetrieveUserTokenTests.java index 679b7d4..4a90956 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/dev/RetrieveUserTokenTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/dev/RetrieveUserTokenTests.java @@ -14,7 +14,7 @@ public final class RetrieveUserTokenTests extends UserTestsBase { @Test @TestSecurity(user = "user_0") - public void retrieveToken() { + public void getUserToken() { given().get("user_0/token").then().statusCode(403); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateEmailTests.java similarity index 91% rename from src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateEmailTests.java index 3d08e1e..4c267cb 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ActivateEmailTests.java @@ -22,7 +22,7 @@ @QuarkusTest @TestHTTPEndpoint(EmailsEndpoint.class) -public final class ActivateTests extends UserTestsBase { +public final class ActivateEmailTests extends UserTestsBase { @Inject RandomService randomService; @@ -32,7 +32,7 @@ public final class ActivateTests extends UserTestsBase { @Test @TestSecurity(user = "user_0") - public void activate() { + public void activateEmail() { given().contentType(ContentType.JSON) .body(new EmailActivation(newEmail.email, randomCode.code)) .post("activate") @@ -43,7 +43,7 @@ public void activate() { @Test @TestSecurity(user = "user_0") - public void activateWithInvalidEmail() { + public void activateEmailWithInvalidEmail() { given().contentType(ContentType.JSON) .body(new EmailActivation("invalid", randomCode.code)) .post("activate") @@ -54,7 +54,7 @@ public void activateWithInvalidEmail() { @Test @TestSecurity(user = "user_0") - public void activateWithOtherEmail() { + public void activateEmailWithOtherEmail() { final var otherUser = requireNonNull(User.findByUsername("user_1")); given().contentType(ContentType.JSON) .body(new EmailActivation(otherUser.mainEmail.email, randomCode.code)) @@ -66,7 +66,7 @@ public void activateWithOtherEmail() { @Test @TestSecurity(user = "user_0") - public void activateWithInvalidCode() { + public void activateEmailWithInvalidCode() { given().contentType(ContentType.JSON) .body(new EmailActivation(newEmail.email, "invalid")) .post("activate") @@ -77,7 +77,7 @@ public void activateWithInvalidCode() { @Test @TestSecurity(user = "user_0") - public void activateWithEmptyInput() { + public void activateEmailWithEmptyInput() { given().contentType(ContentType.JSON).post("activate").then().statusCode(400); assertEquals(1, RandomCode.count("id", randomCode.id)); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CountTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CountEmailsTests.java similarity index 93% rename from src/test/java/app/fyreplace/api/testing/endpoints/emails/CountTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/emails/CountEmailsTests.java index 73a45a6..33d2e4b 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CountTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CountEmailsTests.java @@ -18,10 +18,10 @@ @QuarkusTest @TestHTTPEndpoint(EmailsEndpoint.class) -public final class CountTests extends UserTestsBase { +public final class CountEmailsTests extends UserTestsBase { @Test @TestSecurity(user = "user_0") - public void count() { + public void countEmails() { given().get("count") .then() .statusCode(200) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateEmailTests.java similarity index 91% rename from src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateEmailTests.java index b4bff51..5cf843b 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/CreateEmailTests.java @@ -18,10 +18,10 @@ @QuarkusTest @TestHTTPEndpoint(EmailsEndpoint.class) -public final class CreateTests extends UserTestsBase { +public final class CreateEmailTests extends UserTestsBase { @Test @TestSecurity(user = "user_0") - public void create() { + public void createEmail() { final var email = "some_new_email@example.org"; final var emailCount = Email.count(); given().contentType(ContentType.JSON) @@ -38,7 +38,7 @@ public void create() { @Test @TestSecurity(user = "user_0") - public void createWithInvalidEmail() { + public void createEmailWithInvalidEmail() { final var emailCount = Email.count(); given().contentType(ContentType.JSON) .body(new EmailCreation("invalid")) @@ -51,7 +51,7 @@ public void createWithInvalidEmail() { @Test @TestSecurity(user = "user_0") - public void createWithEmptyEmail() { + public void createEmailWithEmptyEmail() { final var emailCount = Email.count(); given().contentType(ContentType.JSON) .body(new EmailCreation("")) @@ -64,7 +64,7 @@ public void createWithEmptyEmail() { @Test @TestSecurity(user = "user_0") - public void createWithExistingEmail() { + public void createEmailWithExistingEmail() { final var existingUser = requireNonNull(User.findByUsername("user_1")); final var emailCount = Email.count(); given().contentType(ContentType.JSON) @@ -78,7 +78,7 @@ public void createWithExistingEmail() { @Test @TestSecurity(user = "user_0") - public void createWithEmptyInput() { + public void createEmailWithEmptyInput() { final var emailCount = Email.count(); given().contentType(ContentType.JSON) .post() diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteEmailTests.java similarity index 95% rename from src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteEmailTests.java index 06afbc5..ea1537c 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/DeleteEmailTests.java @@ -17,12 +17,12 @@ @QuarkusTest @TestHTTPEndpoint(EmailsEndpoint.class) -public final class DeleteTests extends UserTestsBase { +public final class DeleteEmailTests extends UserTestsBase { private Email newEmail; @Test @TestSecurity(user = "user_0") - public void delete() { + public void deleteEmail() { given().delete(newEmail.id.toString()).then().statusCode(204); assertEquals(0, Email.count("id", newEmail.id)); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListEmailsTests.java similarity index 93% rename from src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/emails/ListEmailsTests.java index 35598ce..6b6d8ec 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/ListEmailsTests.java @@ -21,13 +21,13 @@ @QuarkusTest @TestHTTPEndpoint(EmailsEndpoint.class) -public final class ListTests extends UserTestsBase { +public final class ListEmailsTests extends UserTestsBase { @ConfigProperty(name = "app.paging.size") int pagingSize; @Test @TestSecurity(user = "user_0") - public void list() { + public void listEmails() { final var user = User.findByUsername("user_0"); final var response = given().queryParam("page", 0) .get() @@ -44,13 +44,13 @@ public void list() { @Test @TestSecurity(user = "user_0") - public void listOutOfBounds() { + public void listEmailsOutOfBounds() { given().queryParam("page", -1).get().then().statusCode(400); } @Test @TestSecurity(user = "user_0") - public void listTooFar() { + public void listEmailsTooFar() { given().queryParam("page", 50) .get() .then() diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/emails/UpdateMainTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/emails/UpdateMainTests.java index dc17621..980d790 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/emails/UpdateMainTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/emails/UpdateMainTests.java @@ -24,7 +24,7 @@ public final class UpdateMainTests extends UserTestsBase { @Test @TestSecurity(user = "user_0") - public void updateMain() { + public void setMain() { assertFalse(secondaryEmail.isMain()); given().put(secondaryEmail.id + "/main").then().statusCode(200); secondaryEmail = Email.findById(secondaryEmail.id); @@ -33,7 +33,7 @@ public void updateMain() { @Test @TestSecurity(user = "user_0") - public void updateMainTwice() { + public void setMainTwice() { assertFalse(secondaryEmail.isMain()); given().put(secondaryEmail.id + "/main").then().statusCode(200); given().put(secondaryEmail.id + "/main").then().statusCode(200); @@ -43,7 +43,7 @@ public void updateMainTwice() { @Test @TestSecurity(user = "user_0") - public void updateMainWithUnverifiedEmail() { + public void setMainWithUnverifiedEmail() { QuarkusTransaction.requiringNew().run(() -> Email.update("verified = false where id = ?1", secondaryEmail.id)); given().put(secondaryEmail.id + "/main").then().statusCode(403); secondaryEmail = Email.findById(secondaryEmail.id); @@ -52,7 +52,7 @@ public void updateMainWithUnverifiedEmail() { @Test @TestSecurity(user = "user_0") - public void updateMainWithOtherEmail() { + public void setMainWithOtherEmail() { final var otherUser = requireNonNull(User.findByUsername("user_1")); given().put(otherUser.mainEmail.id + "/main").then().statusCode(404); secondaryEmail = Email.findById(secondaryEmail.id); @@ -61,7 +61,7 @@ public void updateMainWithOtherEmail() { @Test @TestSecurity(user = "user_0") - public void updateMainWithNonExistentEmail() { + public void setMainWithNonExistentEmail() { given().put(fakeId + "/main").then().statusCode(404); secondaryEmail = Email.findById(secondaryEmail.id); assertFalse(secondaryEmail.isMain()); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/CountTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/CountPostsTests.java similarity index 94% rename from src/test/java/app/fyreplace/api/testing/endpoints/posts/CountTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/posts/CountPostsTests.java index e226c17..4749b2f 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/CountTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/CountPostsTests.java @@ -19,7 +19,7 @@ @QuarkusTest @TestHTTPEndpoint(PostsEndpoint.class) -public final class CountTests extends PostTestsBase { +public final class CountPostsTests extends PostTestsBase { @Inject DataSeeder dataSeeder; @@ -29,7 +29,7 @@ public final class CountTests extends PostTestsBase { @Test @TestSecurity(user = "user_0") - public void countSubscribedTo() { + public void countSubscribedPosts() { given().queryParam("type", PostsEndpoint.PostListingType.SUBSCRIBED_TO) .get("count") .then() @@ -39,7 +39,7 @@ public void countSubscribedTo() { @Test @TestSecurity(user = "user_0") - public void countPublished() { + public void countPublishedPosts() { given().queryParam("type", PostsEndpoint.PostListingType.PUBLISHED) .get("count") .then() diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreatePostTests.java similarity index 90% rename from src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/posts/CreatePostTests.java index 59e80a7..a5148e9 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/CreatePostTests.java @@ -16,10 +16,10 @@ @QuarkusTest @TestHTTPEndpoint(PostsEndpoint.class) -public final class CreateTests extends PostTestsBase { +public final class CreatePostTests extends PostTestsBase { @Test @TestSecurity(user = "user_0") - public void create() { + public void createPost() { final var postCount = Post.count("author.username = 'user_0'"); given().post() .then() @@ -34,7 +34,7 @@ public void create() { } @Test - public void createUnauthenticated() { + public void createPostWhileWhileUnauthenticated() { final var postCount = Post.count("author.username = 'user_0'"); given().post().then().statusCode(401); assertEquals(postCount, Post.count()); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeletePostTests.java similarity index 92% rename from src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/posts/DeletePostTests.java index ca3ea53..e48c043 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeleteTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/DeletePostTests.java @@ -15,7 +15,7 @@ @QuarkusTest @TestHTTPEndpoint(PostsEndpoint.class) -public final class DeleteTests extends PostTestsBase { +public final class DeletePostTests extends PostTestsBase { @Test @TestSecurity(user = "user_0") public void deleteOwnPost() { @@ -45,13 +45,13 @@ public void deleteOtherDraft() { } @Test - public void deletePostUnauthenticated() { + public void deletePostWhileWhileUnauthenticated() { given().delete(post.id.toString()).then().statusCode(401); assertEquals(1, Post.count("id", post.id)); } @Test - public void deleteDraftUnauthenticated() { + public void deleteDraftWhileWhileUnauthenticated() { given().delete(draft.id.toString()).then().statusCode(401); assertEquals(1, Post.count("id", draft.id)); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/GetPostTests.java similarity index 91% rename from src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/posts/GetPostTests.java index fe52767..a77858c 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/RetrieveTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/GetPostTests.java @@ -20,10 +20,10 @@ @QuarkusTest @TestHTTPEndpoint(PostsEndpoint.class) -public final class RetrieveTests extends PostTestsBase { +public final class GetPostTests extends PostTestsBase { @Test @TestSecurity(user = "user_0") - public void retrieveOwnPost() { + public void getOwnPost() { given().get(post.id.toString()) .then() .statusCode(200) @@ -38,7 +38,7 @@ public void retrieveOwnPost() { @Test @TestSecurity(user = "user_0") - public void retrieveOwnAnonymousPost() { + public void getOwnAnonymousPost() { given().get(anonymousPost.id.toString()) .then() .statusCode(200) @@ -53,7 +53,7 @@ public void retrieveOwnAnonymousPost() { @Test @TestSecurity(user = "user_0") - public void retrieveOwnDraft() { + public void getOwnDraft() { given().get(draft.id.toString()) .then() .statusCode(200) @@ -66,7 +66,7 @@ public void retrieveOwnDraft() { @Test @TestSecurity(user = "user_1") - public void retrieveOtherPost() { + public void getOtherPost() { given().get(post.id.toString()) .then() .statusCode(200) @@ -81,7 +81,7 @@ public void retrieveOtherPost() { @Test @TestSecurity(user = "user_1") - public void retrieveOtherAnonymousPost() { + public void getOtherAnonymousPost() { given().get(anonymousPost.id.toString()) .then() .statusCode(200) @@ -96,20 +96,20 @@ public void retrieveOtherAnonymousPost() { @Test @TestSecurity(user = "user_1") - public void retrieveOtherDraft() { + public void getOtherDraft() { given().get(draft.id.toString()).then().statusCode(404); } @Test @TestSecurity(user = "user_1") - public void retrieveOtherPostWhenBlocked() { + public void getOtherPostWhenBlocked() { QuarkusTransaction.requiringNew().run(() -> post.author.block(User.findByUsername("user_1"))); given().get(post.id.toString()).then().statusCode(403); } @Test @TestSecurity(user = "user_1") - public void retrieveOtherAnonymousPostWhenBlocked() { + public void getOtherAnonymousPostWhenBlocked() { QuarkusTransaction.requiringNew().run(() -> post.author.block(User.findByUsername("user_1"))); given().get(anonymousPost.id.toString()) .then() @@ -124,7 +124,7 @@ public void retrieveOtherAnonymousPostWhenBlocked() { } @Test - public void retrievePostUnauthenticated() { + public void getPostWhileWhileUnauthenticated() { given().get(post.id.toString()) .then() .statusCode(200) @@ -138,7 +138,7 @@ public void retrievePostUnauthenticated() { } @Test - public void retrieveAnonymousPostUnauthenticated() { + public void getAnonymousPostWhileWhileUnauthenticated() { given().get(anonymousPost.id.toString()) .then() .statusCode(200) @@ -152,12 +152,12 @@ public void retrieveAnonymousPostUnauthenticated() { } @Test - public void retrieveDraftUnauthenticated() { + public void getDraftWhileWhileUnauthenticated() { given().get(draft.id.toString()).then().statusCode(404); } @Test - public void retrieveDeletedPost() { + public void getDeletedPost() { QuarkusTransaction.requiringNew() .run(() -> Post.findById(this.post.id).softDelete()); given().get(post.id.toString()).then().statusCode(410); @@ -165,7 +165,7 @@ public void retrieveDeletedPost() { @ParameterizedTest @ValueSource(strings = {"fake", "00000000-0000-0000-0000-000000000000"}) - public void retrieveNonExistent(final String id) { + public void getNonExistentPost(final String id) { given().get(id).then().statusCode(404); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListFeedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListPostsFeedTests.java similarity index 90% rename from src/test/java/app/fyreplace/api/testing/endpoints/posts/ListFeedTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/posts/ListPostsFeedTests.java index 575f353..abb3ce9 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListFeedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListPostsFeedTests.java @@ -23,13 +23,13 @@ @QuarkusTest @TestHTTPEndpoint(PostsEndpoint.class) -public final class ListFeedTests extends PostTestsBase { +public final class ListPostsFeedTests extends PostTestsBase { @Inject DataSeeder dataSeeder; @Test @TestSecurity(user = "user_0") - public void listFeedWithOtherPosts() { + public void listPostsFeedWithOtherPosts() { final var user = requireNonNull(User.findByUsername("user_1")); range(0, 5).forEach(i -> dataSeeder.createPost(user, "Post " + i, true, false)); final var response = given().get("feed").then().statusCode(200).body("size()", equalTo(3)); @@ -38,7 +38,7 @@ public void listFeedWithOtherPosts() { @Test @TestSecurity(user = "user_0") - public void listFeedWithOtherDrafts() { + public void listPostsFeedWithOtherDrafts() { final var user = requireNonNull(User.findByUsername("user_1")); range(0, 5).forEach(i -> dataSeeder.createPost(user, "Post " + i, false, false)); given().get("feed").then().statusCode(200).body("size()", equalTo(0)); @@ -46,7 +46,7 @@ public void listFeedWithOtherDrafts() { @Test @TestSecurity(user = "user_0") - public void listFeedWithOwnPosts() { + public void listPostsFeedWithOwnPosts() { final var user = requireNonNull(User.findByUsername("user_0")); range(0, 5).forEach(i -> dataSeeder.createPost(user, "Post " + i, true, false)); given().get("feed").then().statusCode(200).body("size()", equalTo(0)); @@ -54,7 +54,7 @@ public void listFeedWithOwnPosts() { @Test @TestSecurity(user = "user_0") - public void listFeedWithOwnDrafts() { + public void listPostsFeedWithOwnDrafts() { final var user = requireNonNull(User.findByUsername("user_0")); range(0, 5).forEach(i -> dataSeeder.createPost(user, "Post " + i, false, false)); given().get("feed").then().statusCode(200).body("size()", equalTo(0)); @@ -62,7 +62,7 @@ public void listFeedWithOwnDrafts() { @Test @TestSecurity(user = "user_0") - public void listFeedWithAlreadyVotedPosts() { + public void listPostsFeedWithAlreadyVotedPosts() { final var user = requireNonNull(User.findByUsername("user_0")); final var otherUser = requireNonNull(User.findByUsername("user_1")); QuarkusTransaction.requiringNew().run(() -> range(0, 5).forEach(i -> { @@ -78,7 +78,7 @@ public void listFeedWithAlreadyVotedPosts() { @Test @TestSecurity(user = "user_0") - public void listFeedWithPostsFromBlockedUser() { + public void listPostsFeedWithPostsFromBlockedUser() { final var user = requireNonNull(User.findByUsername("user_0")); final var otherUser = requireNonNull(User.findByUsername("user_1")); QuarkusTransaction.requiringNew().run(() -> user.block(otherUser)); @@ -90,7 +90,7 @@ public void listFeedWithPostsFromBlockedUser() { @Test @TestSecurity(user = "user_0") - public void listFeedWithPostsFromBlockingUser() { + public void listPostsFeedWithPostsFromBlockingUser() { final var user = requireNonNull(User.findByUsername("user_0")); final var otherUser = requireNonNull(User.findByUsername("user_1")); QuarkusTransaction.requiringNew().run(() -> user.block(otherUser)); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListPostsTests.java similarity index 97% rename from src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/posts/ListPostsTests.java index 8e8ff5d..8004a8b 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListPostsTests.java @@ -26,7 +26,7 @@ @QuarkusTest @TestHTTPEndpoint(PostsEndpoint.class) -public final class ListTests extends PostTestsBase { +public final class ListPostsTests extends PostTestsBase { @ConfigProperty(name = "app.paging.size") int pagingSize; @@ -37,7 +37,7 @@ public final class ListTests extends PostTestsBase { @Test @TestSecurity(user = "user_0") - public void listSubscribedTo() { + public void listSubscribedPosts() { QuarkusTransaction.requiringNew().run(this::makeSubscribedToPosts); final var response = given().queryParam("page", 0) .queryParam("ascending", false) @@ -53,7 +53,7 @@ public void listSubscribedTo() { @Test @TestSecurity(user = "user_0") - public void listPublished() { + public void listPublishedPosts() { final var user = User.findByUsername("user_0"); try (final var stream = Post.stream("author = ?1 and published = true", user)) { @@ -91,6 +91,16 @@ public void listDrafts() { } } + @Override + public int getPostCount() { + return 20; + } + + @Override + public int getDraftCount() { + return 20; + } + @Transactional public void makeSubscribedToPosts() { final var user0 = requireNonNull(User.findByUsername("user_0")); @@ -113,14 +123,4 @@ public void makeSubscribedToPosts() { stream.forEach(post -> subscribedToPostIds.add(post.id.toString())); } } - - @Override - public int getPostCount() { - return 20; - } - - @Override - public int getDraftCount() { - return 20; - } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/PublishTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/PublishPostTests.java similarity index 93% rename from src/test/java/app/fyreplace/api/testing/endpoints/posts/PublishTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/posts/PublishPostTests.java index dddf331..02209c6 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/PublishTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/PublishPostTests.java @@ -20,7 +20,7 @@ @QuarkusTest @TestHTTPEndpoint(PostsEndpoint.class) -public final class PublishTests extends PostTestsBase { +public final class PublishPostTests extends PostTestsBase { @Test @TestSecurity(user = "user_0") public void publishOwnPost() { @@ -46,7 +46,7 @@ public void publishOwnDraft() { @Test @TestSecurity(user = "user_0") - public void publishAnonymouslyOwnDraft() { + public void publishOwnDraftAnonymously() { final var subscriptionCount = Subscription.count("user.username = 'user_0'"); given().contentType(ContentType.JSON) .body(new PostPublication(false)) @@ -91,7 +91,7 @@ public void publishOtherDraft() { } @Test - public void publishPostUnauthenticated() { + public void publishPostWhileWhileUnauthenticated() { given().contentType(ContentType.JSON) .body(new PostPublication(false)) .post(post.id + "/publish") @@ -100,7 +100,7 @@ public void publishPostUnauthenticated() { } @Test - public void publishDraftUnauthenticated() { + public void publishDraftWhileWhileUnauthenticated() { given().contentType(ContentType.JSON) .body(new PostPublication(false)) .post(draft.id + "/publish") @@ -112,7 +112,7 @@ public void publishDraftUnauthenticated() { @ParameterizedTest @ValueSource(strings = {"fake", "00000000-0000-0000-0000-000000000000"}) @TestSecurity(user = "user_0") - public void publishNonExistent(final String id) { + public void publishNonExistentPost(final String id) { given().contentType(ContentType.JSON) .body(new PostPublication(false)) .post(id + "/publish") diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateReportedToFalseTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/SetPostReportedToFalseTests.java similarity index 90% rename from src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateReportedToFalseTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/posts/SetPostReportedToFalseTests.java index 77ccbeb..28edfe6 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateReportedToFalseTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/SetPostReportedToFalseTests.java @@ -23,10 +23,10 @@ @QuarkusTest @TestHTTPEndpoint(PostsEndpoint.class) -public final class UpdateReportedToFalseTests extends PostTestsBase { +public final class SetPostReportedToFalseTests extends PostTestsBase { @Test @TestSecurity(user = "user_0") - public void updateReportWithOwnPost() { + public void setOwnPostReported() { final var user = User.findByUsername("user_0"); assertFalse(post.isReportedBy(user)); given().contentType(ContentType.JSON) @@ -39,7 +39,7 @@ public void updateReportWithOwnPost() { @Test @TestSecurity(user = "user_1") - public void updateReportWithOtherPost() { + public void setOtherPostReported() { final var user = User.findByUsername("user_1"); assertTrue(post.isReportedBy(user)); given().contentType(ContentType.JSON) @@ -52,7 +52,7 @@ public void updateReportWithOtherPost() { @Test @TestSecurity(user = "user_1") - public void updateReportWithOtherPostTwice() { + public void setOtherPostReportedTwice() { final var user = User.findByUsername("user_1"); assertTrue(post.isReportedBy(user)); given().contentType(ContentType.JSON) @@ -70,7 +70,7 @@ public void updateReportWithOtherPostTwice() { @Test @TestSecurity(user = "user_0") - public void updateReportWithOwnDraft() { + public void setOwnDraftReported() { final var user = User.findByUsername("user_0"); assertFalse(draft.isReportedBy(user)); given().contentType(ContentType.JSON) @@ -83,7 +83,7 @@ public void updateReportWithOwnDraft() { @Test @TestSecurity(user = "user_1") - public void updateReportWithOtherDraft() { + public void setOtherDraftReported() { final var user = User.findByUsername("user_1"); assertFalse(draft.isReportedBy(user)); given().contentType(ContentType.JSON) @@ -97,7 +97,7 @@ public void updateReportWithOtherDraft() { @ParameterizedTest @ValueSource(strings = {"fake", "00000000-0000-0000-0000-000000000000"}) @TestSecurity(user = "user_0") - public void updateReportWithNonExistentPost(final String id) { + public void setNonExistentPostReported(final String id) { final var reportCount = Report.count(); given().contentType(ContentType.JSON) .body(new ReportUpdate(false)) @@ -108,7 +108,7 @@ public void updateReportWithNonExistentPost(final String id) { } @Test - public void updateReportUnauthenticated() { + public void setPostReportedWhileWhileUnauthenticated() { final var reportCount = Report.count(); given().contentType(ContentType.JSON) .body(new ReportUpdate(false)) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateReportedToTrueTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/SetPostReportedToTrueTests.java similarity index 90% rename from src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateReportedToTrueTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/posts/SetPostReportedToTrueTests.java index d6f0985..8d88399 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateReportedToTrueTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/SetPostReportedToTrueTests.java @@ -20,10 +20,10 @@ @QuarkusTest @TestHTTPEndpoint(PostsEndpoint.class) -public final class UpdateReportedToTrueTests extends PostTestsBase { +public final class SetPostReportedToTrueTests extends PostTestsBase { @Test @TestSecurity(user = "user_0") - public void updateReportWithOwnPost() { + public void setOwnPostReported() { final var user = User.findByUsername("user_0"); assertFalse(post.isReportedBy(user)); given().contentType(ContentType.JSON) @@ -36,7 +36,7 @@ public void updateReportWithOwnPost() { @Test @TestSecurity(user = "user_1") - public void updateReportWithOtherPost() { + public void setOtherPostReported() { final var user = User.findByUsername("user_1"); assertFalse(post.isReportedBy(user)); given().contentType(ContentType.JSON) @@ -49,7 +49,7 @@ public void updateReportWithOtherPost() { @Test @TestSecurity(user = "user_1") - public void updateReportWithOtherPostTwice() { + public void setOtherPostReportedTwice() { final var user = User.findByUsername("user_1"); assertFalse(post.isReportedBy(user)); given().contentType(ContentType.JSON) @@ -67,7 +67,7 @@ public void updateReportWithOtherPostTwice() { @Test @TestSecurity(user = "user_0") - public void updateReportWithOwnDraft() { + public void setOwnDraftReported() { final var user = User.findByUsername("user_0"); assertFalse(draft.isReportedBy(user)); given().contentType(ContentType.JSON) @@ -80,7 +80,7 @@ public void updateReportWithOwnDraft() { @Test @TestSecurity(user = "user_1") - public void updateReportWithOtherDraft() { + public void setOtherDraftReported() { final var user = User.findByUsername("user_1"); assertFalse(draft.isReportedBy(user)); given().contentType(ContentType.JSON) @@ -94,7 +94,7 @@ public void updateReportWithOtherDraft() { @ParameterizedTest @ValueSource(strings = {"fake", "00000000-0000-0000-0000-000000000000"}) @TestSecurity(user = "user_0") - public void updateReportWithNonExistentPost(final String id) { + public void setNonExistentPostReported(final String id) { final var reportCount = Report.count(); given().contentType(ContentType.JSON) .body(new ReportUpdate(true)) @@ -105,7 +105,7 @@ public void updateReportWithNonExistentPost(final String id) { } @Test - public void updateReportUnauthenticated() { + public void setPostReportedWhileWhileUnauthenticated() { final var reportCount = Report.count(); given().contentType(ContentType.JSON) .body(new ReportUpdate(true)) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateSubscribedToFalseTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/SetPostSubscribedToFalseTests.java similarity index 90% rename from src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateSubscribedToFalseTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/posts/SetPostSubscribedToFalseTests.java index 696ed3b..6becfce 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateSubscribedToFalseTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/SetPostSubscribedToFalseTests.java @@ -24,10 +24,10 @@ @QuarkusTest @TestHTTPEndpoint(PostsEndpoint.class) -public final class UpdateSubscribedToFalseTests extends PostTestsBase { +public final class SetPostSubscribedToFalseTests extends PostTestsBase { @Test @TestSecurity(user = "user_1") - public void updateSubscribedWithOtherPost() { + public void setOtherPostSubscribed() { final var user = requireNonNull(User.findByUsername("user_1")); assertTrue(user.isSubscribedTo(post)); given().contentType(ContentType.JSON) @@ -40,7 +40,7 @@ public void updateSubscribedWithOtherPost() { @Test @TestSecurity(user = "user_1") - public void updateSubscribedWithOtherPostTwice() { + public void setOtherPostSubscribedTwice() { final var user = requireNonNull(User.findByUsername("user_1")); given().contentType(ContentType.JSON) .body(new SubscriptionUpdate(false)) @@ -57,7 +57,7 @@ public void updateSubscribedWithOtherPostTwice() { @Test @TestSecurity(user = "user_0") - public void updateSubscribedWithOwnPost() { + public void setOwnPostSubscribed() { final var user = requireNonNull(User.findByUsername("user_0")); assertTrue(user.isSubscribedTo(post)); given().contentType(ContentType.JSON) @@ -70,7 +70,7 @@ public void updateSubscribedWithOwnPost() { @Test @TestSecurity(user = "user_1") - public void updateSubscribedWithOtherDraft() { + public void setOtherDraftSubscribed() { final var user = requireNonNull(User.findByUsername("user_1")); assertFalse(user.isSubscribedTo(draft)); given().contentType(ContentType.JSON) @@ -83,7 +83,7 @@ public void updateSubscribedWithOtherDraft() { @Test @TestSecurity(user = "user_0") - public void updateSubscribedWithOwnDraft() { + public void setOwnDraftSubscribed() { final var user = requireNonNull(User.findByUsername("user_0")); assertFalse(user.isSubscribedTo(draft)); given().contentType(ContentType.JSON) @@ -96,7 +96,7 @@ public void updateSubscribedWithOwnDraft() { @Test @TestSecurity(user = "user_1") - public void deleteToOtherPostWhenBlocked() { + public void setOtherPostSubscribedWhenBlocked() { final var user = requireNonNull(User.findByUsername("user_1")); QuarkusTransaction.requiringNew().run(() -> post.author.block(user)); assertFalse(user.isSubscribedTo(post)); @@ -110,7 +110,7 @@ public void deleteToOtherPostWhenBlocked() { @Test @TestSecurity(user = "user_1") - public void deleteToOtherAnonymousPostWhenBlocked() { + public void setOtherAnonymousPostSubscribedWhenBlocked() { final var user = requireNonNull(User.findByUsername("user_1")); QuarkusTransaction.requiringNew().run(() -> { anonymousPost.author.block(user); @@ -126,7 +126,7 @@ public void deleteToOtherAnonymousPostWhenBlocked() { } @Test - public void updateSubscribedWithPostUnauthenticated() { + public void setPostSubscribedWhileWhileUnauthenticated() { final var subscriptionCount = Subscription.count(); given().contentType(ContentType.JSON) .body(new SubscriptionUpdate(false)) @@ -137,7 +137,7 @@ public void updateSubscribedWithPostUnauthenticated() { } @Test - public void updateSubscribedWithDraftUnauthenticated() { + public void setDraftSubscribedWhileWhileUnauthenticated() { final var subscriptionCount = Subscription.count(); given().contentType(ContentType.JSON) .body(new SubscriptionUpdate(false)) @@ -150,7 +150,7 @@ public void updateSubscribedWithDraftUnauthenticated() { @ParameterizedTest @ValueSource(strings = {"fake", "00000000-0000-0000-0000-000000000000"}) @TestSecurity(user = "user_0") - public void updateSubscribedWithNonExistentPost(final String id) { + public void setNonExistentPostSubscribed(final String id) { final var subscriptionCount = Subscription.count(); given().contentType(ContentType.JSON) .body(new SubscriptionUpdate(false)) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateSubscribedToTrueTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/SetPostSubscribedToTrueTests.java similarity index 90% rename from src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateSubscribedToTrueTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/posts/SetPostSubscribedToTrueTests.java index 51a6efc..80d8e52 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/UpdateSubscribedToTrueTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/SetPostSubscribedToTrueTests.java @@ -25,10 +25,10 @@ @QuarkusTest @TestHTTPEndpoint(PostsEndpoint.class) -public final class UpdateSubscribedToTrueTests extends CommentTestsBase { +public final class SetPostSubscribedToTrueTests extends CommentTestsBase { @Test @TestSecurity(user = "user_1") - public void updateSubscribedWithOtherPost() { + public void setOtherPostSubscribed() { final var user = requireNonNull(User.findByUsername("user_1")); assertFalse(user.isSubscribedTo(post)); given().contentType(ContentType.JSON) @@ -46,7 +46,7 @@ public void updateSubscribedWithOtherPost() { @Test @TestSecurity(user = "user_1") - public void updateSubscribedWithOtherPostTwice() { + public void setOtherPostSubscribedTwice() { final var user = User.findByUsername("user_1"); given().contentType(ContentType.JSON) .body(new SubscriptionUpdate(true)) @@ -68,7 +68,7 @@ public void updateSubscribedWithOtherPostTwice() { @Test @TestSecurity(user = "user_0") - public void updateSubscribedWithOwnPost() { + public void setOwnPostSubscribed() { final var user = requireNonNull(User.findByUsername("user_0")); assertTrue(user.isSubscribedTo(post)); given().contentType(ContentType.JSON) @@ -84,7 +84,7 @@ public void updateSubscribedWithOwnPost() { @Test @TestSecurity(user = "user_1") - public void updateSubscribedWithOtherDraft() { + public void setOtherDraftSubscribed() { final var user = requireNonNull(User.findByUsername("user_1")); assertFalse(user.isSubscribedTo(draft)); given().contentType(ContentType.JSON) @@ -97,7 +97,7 @@ public void updateSubscribedWithOtherDraft() { @Test @TestSecurity(user = "user_0") - public void updateSubscribedWithOwnDraft() { + public void setOwnDraftSubscribed() { final var user = requireNonNull(User.findByUsername("user_0")); assertFalse(user.isSubscribedTo(draft)); given().contentType(ContentType.JSON) @@ -110,7 +110,7 @@ public void updateSubscribedWithOwnDraft() { @Test @TestSecurity(user = "user_1") - public void updateSubscribedWithOtherPostWhenBlocked() { + public void setOtherPostSubscribedWhenBlocked() { final var user = requireNonNull(User.findByUsername("user_1")); QuarkusTransaction.requiringNew().run(() -> post.author.block(user)); assertFalse(user.isSubscribedTo(post)); @@ -124,7 +124,7 @@ public void updateSubscribedWithOtherPostWhenBlocked() { @Test @TestSecurity(user = "user_1") - public void updateSubscribedWithOtherAnonymousPostWhenBlocked() { + public void setOtherAnonymousPostSubscribedWhenBlocked() { final var user = requireNonNull(User.findByUsername("user_1")); QuarkusTransaction.requiringNew().run(() -> anonymousPost.author.block(user)); assertFalse(user.isSubscribedTo(anonymousPost)); @@ -137,7 +137,7 @@ public void updateSubscribedWithOtherAnonymousPostWhenBlocked() { } @Test - public void updateSubscribedWithPostUnauthenticated() { + public void setPostSubscribedWhileWhileUnauthenticated() { final var subscriptionCount = Subscription.count(); given().contentType(ContentType.JSON) .body(new SubscriptionUpdate(true)) @@ -148,7 +148,7 @@ public void updateSubscribedWithPostUnauthenticated() { } @Test - public void updateSubscribedWithDraftUnauthenticated() { + public void setDraftSubscribedWhileWhileUnauthenticated() { final var subscriptionCount = Subscription.count(); given().contentType(ContentType.JSON) .body(new SubscriptionUpdate(true)) @@ -161,7 +161,7 @@ public void updateSubscribedWithDraftUnauthenticated() { @ParameterizedTest @ValueSource(strings = {"fake", "00000000-0000-0000-0000-000000000000"}) @TestSecurity(user = "user_0") - public void updateSubscribedWithNonExistentPost(final String id) { + public void setNonExistentPostSubscribed(final String id) { final var subscriptionCount = Subscription.count(); given().contentType(ContentType.JSON) .body(new SubscriptionUpdate(true)) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/VoteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/VotePostTests.java similarity index 92% rename from src/test/java/app/fyreplace/api/testing/endpoints/posts/VoteTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/posts/VotePostTests.java index aa7c101..3c78be3 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/VoteTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/VotePostTests.java @@ -23,11 +23,11 @@ @QuarkusTest @TestHTTPEndpoint(PostsEndpoint.class) -public final class VoteTests extends PostTestsBase { +public final class VotePostTests extends PostTestsBase { @Test @TestSecurity(user = "user_1") @Transactional - public void voteWithSpread() { + public void votePostWithSpread() { final var voteCount = Vote.count(); final var postLife = post.life; given().contentType(ContentType.JSON) @@ -45,7 +45,7 @@ public void voteWithSpread() { @Test @TestSecurity(user = "user_1") @Transactional - public void voteWithoutSpread() { + public void votePostWithoutSpread() { final var voteCount = Vote.count(); final var postLife = post.life; given().contentType(ContentType.JSON) @@ -62,7 +62,7 @@ public void voteWithoutSpread() { @Test @TestSecurity(user = "user_1") - public void voteOnOldPost() { + public void voteOldPost() { QuarkusTransaction.requiringNew() .run(() -> Post.update( "dateCreated = ?1 where id = ?2", @@ -81,7 +81,7 @@ public void voteOnOldPost() { @Test @TestSecurity(user = "user_1") - public void voteOnOldDraft() { + public void voteOldDraft() { QuarkusTransaction.requiringNew() .run(() -> Post.update( "dateCreated = ?1 where id = ?2", @@ -100,7 +100,7 @@ public void voteOnOldDraft() { @Test @TestSecurity(user = "user_0") - public void voteOnOwnPost() { + public void voteOwnPost() { final var voteCount = Vote.count(); final var postLife = post.life; given().contentType(ContentType.JSON) @@ -114,7 +114,7 @@ public void voteOnOwnPost() { @Test @TestSecurity(user = "user_0") - public void voteOnOwnDraft() { + public void voteOwnDraft() { final var voteCount = Vote.count(); final var postLife = draft.life; given().contentType(ContentType.JSON) @@ -128,7 +128,7 @@ public void voteOnOwnDraft() { @Test @TestSecurity(user = "user_1") - public void voteOnOtherPostWhenBlocked() { + public void voteOtherPostWhenBlocked() { final var user = User.findByUsername("user_1"); QuarkusTransaction.requiringNew().run(() -> post.author.block(user)); final var voteCount = Vote.count(); @@ -144,7 +144,7 @@ public void voteOnOtherPostWhenBlocked() { @Test @TestSecurity(user = "user_1") - public void voteOnOtherAnonymousPostWhenBlocked() { + public void voteOtherAnonymousPostWhenBlocked() { final var user = User.findByUsername("user_1"); QuarkusTransaction.requiringNew().run(() -> anonymousPost.author.block(user)); final var voteCount = Vote.count(); @@ -158,7 +158,7 @@ public void voteOnOtherAnonymousPostWhenBlocked() { @Test @TestSecurity(user = "user_1") - public void voteOnNonExistentPost() { + public void voteNonExistentPost() { final var voteCount = Vote.count(); given().contentType(ContentType.JSON) .body(new VoteCreation(false)) @@ -170,7 +170,7 @@ public void voteOnNonExistentPost() { @Test @TestSecurity(user = "user_1") - public void voteWithEmptyInput() { + public void votePostWithEmptyInput() { final var voteCount = Vote.count(); final var postLife = post.life; given().contentType(ContentType.JSON).post(post.id + "/vote").then().statusCode(400); @@ -179,7 +179,7 @@ public void voteWithEmptyInput() { } @Test - public void voteUnauthenticated() { + public void votePostWhileWhileUnauthenticated() { final var voteCount = Vote.count(); given().contentType(ContentType.JSON) .body(new VoteCreation(false)) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/reports/ListTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/reports/ListReportsTests.java similarity index 92% rename from src/test/java/app/fyreplace/api/testing/endpoints/reports/ListTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/reports/ListReportsTests.java index a702a48..834ca74 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/reports/ListTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/reports/ListReportsTests.java @@ -20,19 +20,19 @@ @QuarkusTest @TestHTTPEndpoint(ReportsEndpoint.class) -public final class ListTests extends UserTestsBase { +public final class ListReportsTests extends UserTestsBase { @ConfigProperty(name = "app.paging.size") int pagingSize; @Test @TestSecurity(user = "user_0") - public void listAsCitizen() { + public void listReportsAsCitizen() { given().get().then().statusCode(403); } @Test @TestSecurity(user = "user_0", roles = "MODERATOR") - public void listAsModerator() { + public void listReportsAsModerator() { final var response = given().get().then().statusCode(200).body("size()", equalTo(pagingSize)); range(0, pagingSize).forEach(i -> { try (final var stream = Report.streamAll().map(r -> r.targetId.toString())) { diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ClearUnreadTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ClearUnreadSubscriptionsTests.java similarity index 88% rename from src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ClearUnreadTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ClearUnreadSubscriptionsTests.java index bbcc333..ad8214b 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ClearUnreadTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ClearUnreadSubscriptionsTests.java @@ -14,10 +14,10 @@ @QuarkusTest @TestHTTPEndpoint(SubscriptionsEndpoint.class) -public final class ClearUnreadTests extends SubscriptionTestsBase { +public final class ClearUnreadSubscriptionsTests extends SubscriptionTestsBase { @Test @TestSecurity(user = "user_0") - public void clearUnread() { + public void clearUnreadSubscriptions() { assertNotEquals(0, Subscription.count("user.username = 'user_0' and unreadCommentCount > 0")); given().delete("unread").then().statusCode(204); assertEquals(0, Subscription.count("user.username = 'user_0' and unreadCommentCount > 0")); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/DeleteTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/DeleteSubscriptionTests.java similarity index 94% rename from src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/DeleteTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/DeleteSubscriptionTests.java index 66b0818..fab15bf 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/DeleteTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/DeleteSubscriptionTests.java @@ -16,10 +16,10 @@ @QuarkusTest @TestHTTPEndpoint(SubscriptionsEndpoint.class) -public final class DeleteTests extends SubscriptionTestsBase { +public final class DeleteSubscriptionTests extends SubscriptionTestsBase { @Test @TestSecurity(user = "user_0") - public void delete() { + public void deleteSubscription() { final var subscriptions = Subscription.list("user.username = 'user_0' and unreadCommentCount > 0"); given().delete(subscriptions.getFirst().id.toString()).then().statusCode(204); @@ -57,7 +57,7 @@ public void deleteOtherSubscription() { @Test @TestSecurity(user = "user_0") - public void deleteNonExistent() { + public void deleteNonExistentSubscription() { final var subscriptionCount = Subscription.count("user.username = 'user_0' and unreadCommentCount > 0"); given().delete(fakeId).then().statusCode(404); assertEquals(subscriptionCount, Subscription.count("user.username = 'user_0' and unreadCommentCount > 0")); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ListUnreadTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ListUnreadSubscriptionsTests.java similarity index 90% rename from src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ListUnreadTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ListUnreadSubscriptionsTests.java index 2bc5d11..d73308e 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ListUnreadTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/subscriptions/ListUnreadSubscriptionsTests.java @@ -14,13 +14,13 @@ @QuarkusTest @TestHTTPEndpoint(SubscriptionsEndpoint.class) -public final class ListUnreadTests extends SubscriptionTestsBase { +public final class ListUnreadSubscriptionsTests extends SubscriptionTestsBase { @ConfigProperty(name = "app.paging.size") int pagingSize; @Test @TestSecurity(user = "user_0") - public void listUnread() { + public void listUnreadSubscriptions() { given().get("unread") .then() .statusCode(200) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTokenTests.java similarity index 88% rename from src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTokenTests.java index fb87f1b..987413f 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateNewTokenTests.java @@ -17,9 +17,9 @@ @QuarkusTest @TestHTTPEndpoint(TokensEndpoint.class) -public final class CreateNewTests extends UserTestsBase { +public final class CreateNewTokenTests extends UserTestsBase { @Test - public void createNewWithUsername() { + public void createNewTokenWithUsername() { final var user = requireNonNull(User.findByUsername("user_0")); given().contentType(ContentType.JSON) .body(new NewTokenCreation(user.username)) @@ -30,7 +30,7 @@ public void createNewWithUsername() { } @Test - public void createNewWithEmail() { + public void createNewTokenWithEmail() { final var user = requireNonNull(User.findByUsername("user_0")); given().contentType(ContentType.JSON) .body(new NewTokenCreation(user.mainEmail.email)) @@ -41,7 +41,7 @@ public void createNewWithEmail() { } @Test - public void createNewWithInvalidIdentifier() { + public void createNewTokenWithInvalidIdentifier() { final var user = requireNonNull(User.findByUsername("user_0")); given().contentType(ContentType.JSON) .body(new NewTokenCreation("invalid")) @@ -52,7 +52,7 @@ public void createNewWithInvalidIdentifier() { } @Test - public void createNewWithEmptyInput() { + public void createNewTokenWithEmptyInput() { final var user = requireNonNull(User.findByUsername("user_0")); given().contentType(ContentType.JSON).post("new").then().statusCode(400); assertEquals(0, getMailsSentTo(user.mainEmail).size()); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTokenTests.java similarity index 92% rename from src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTokenTests.java index bd2bee6..0f707c2 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/CreateTokenTests.java @@ -26,7 +26,7 @@ @QuarkusTest @TestHTTPEndpoint(TokensEndpoint.class) -public final class CreateTests extends UserTestsBase { +public final class CreateTokenTests extends UserTestsBase { @Inject RandomService randomService; @@ -36,7 +36,7 @@ public final class CreateTests extends UserTestsBase { private Password password; @Test - public void createWithUsername() { + public void createTokenWithUsername() { final var randomCodeCount = RandomCode.count(); given().contentType(ContentType.JSON) .body(new TokenCreation(normalUserRandomCode.email.user.username, normalUserRandomCode.code)) @@ -49,7 +49,7 @@ public void createWithUsername() { @Test @Transactional - public void createWithNewUsername() { + public void createTokenWithNewUsername() { final var randomCodeCount = RandomCode.count(); assertFalse(newUserRandomCode.email.verified); given().contentType(ContentType.JSON) @@ -65,7 +65,7 @@ public void createWithNewUsername() { } @Test - public void createWithEmail() { + public void createTokenWithEmail() { final var randomCodeCount = RandomCode.count(); given().contentType(ContentType.JSON) .body(new TokenCreation(normalUserRandomCode.email.email, normalUserRandomCode.code)) @@ -78,7 +78,7 @@ public void createWithEmail() { } @Test - public void createWithNewEmail() { + public void createTokenWithNewEmail() { final var randomCodeCount = RandomCode.count(); assertFalse(newUserRandomCode.email.verified); given().contentType(ContentType.JSON) @@ -94,7 +94,7 @@ public void createWithNewEmail() { } @Test - public void createWithPassword() { + public void createTokenWithPassword() { final var randomCodeCount = RandomCode.count(); assertFalse(password.user.mainEmail.verified); given().contentType(ContentType.JSON) @@ -110,7 +110,7 @@ public void createWithPassword() { } @Test - public void createWithInvalidUsername() { + public void createTokenWithInvalidUsername() { given().contentType(ContentType.JSON) .body(new TokenCreation("bad", normalUserRandomCode.code)) .post() @@ -119,7 +119,7 @@ public void createWithInvalidUsername() { } @Test - public void createWithInvalidSecret() { + public void createTokenWithInvalidSecret() { given().contentType(ContentType.JSON) .body(new TokenCreation(normalUserRandomCode.email.user.username, "bad")) .post() @@ -128,14 +128,14 @@ public void createWithInvalidSecret() { } @Test - public void createTwice() { + public void createTokenTwice() { final var input = new TokenCreation(normalUserRandomCode.email.user.username, normalUserRandomCode.code); given().contentType(ContentType.JSON).body(input).post().then().statusCode(201); given().contentType(ContentType.JSON).body(input).post().then().statusCode(404); } @Test - public void createWithOtherCode() { + public void createTokenWithOtherCode() { given().contentType(ContentType.JSON) .body(new TokenCreation(normalUserRandomCode.email.user.username, otherNormalUserRandomCode.code)) .post() @@ -144,7 +144,7 @@ public void createWithOtherCode() { } @Test - public void createWithEmptyInput() { + public void createTokenWithEmptyInput() { given().contentType(ContentType.JSON).post().then().statusCode(400); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/RetrieveNewTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/GetNewTokenTests.java similarity index 83% rename from src/test/java/app/fyreplace/api/testing/endpoints/tokens/RetrieveNewTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/tokens/GetNewTokenTests.java index ae844ae..63eb6c9 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/tokens/RetrieveNewTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/tokens/GetNewTokenTests.java @@ -13,15 +13,15 @@ @QuarkusTest @TestHTTPEndpoint(TokensEndpoint.class) -public final class RetrieveNewTests extends UserTestsBase { +public final class GetNewTokenTests extends UserTestsBase { @Test @TestSecurity(user = "user_0") - public void retrieveNew() { + public void getNewToken() { given().get("new").then().statusCode(200).contentType(ContentType.TEXT).body(isA(String.class)); } @Test - public void retrieveNewUnauthenticated() { + public void getNewTokenWhileUnauthenticated() { given().get("new").then().statusCode(401); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedUsersTests.java similarity index 92% rename from src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedUsersTests.java index 5805d47..64f3bdb 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/CountBlockedUsersTests.java @@ -17,10 +17,10 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class CountBlockedTests extends UserTestsBase { +public final class CountBlockedUsersTests extends UserTestsBase { @Test @TestSecurity(user = "user_0") - public void countBlocked() { + public void countBlockedUsers() { given().get("blocked/count") .then() .statusCode(200) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateUserTests.java similarity index 90% rename from src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/users/CreateUserTests.java index ca92004..aadbfd3 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/CreateUserTests.java @@ -22,9 +22,9 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class CreateTests extends UserTestsBase { +public final class CreateUserTests extends UserTestsBase { @Test - public void create() { + public void createUser() { final var userCount = User.count(); final var emailCount = Email.count(); given().contentType(ContentType.JSON) @@ -51,7 +51,7 @@ public void create() { } @Test - public void createWithInvalidUsername() { + public void createUserWithInvalidUsername() { final var userCount = User.count(); given().contentType(ContentType.JSON) .body(new UserCreation("new@example.org", "no spaces allowed")) @@ -63,7 +63,7 @@ public void createWithInvalidUsername() { } @Test - public void createWithUsernameTooShort() { + public void createUserWithUsernameTooShort() { final var userCount = User.count(); given().contentType(ContentType.JSON) .body(new UserCreation("new@example.org", "a")) @@ -74,7 +74,7 @@ public void createWithUsernameTooShort() { } @Test - public void createWithUsernameTooLong() { + public void createUserWithUsernameTooLong() { final var userCount = User.count(); given().contentType(ContentType.JSON) .body(new UserCreation("new@example.org", "a".repeat(150))) @@ -85,7 +85,7 @@ public void createWithUsernameTooLong() { } @Test - public void createWithForbiddenUsername() { + public void createUserWithForbiddenUsername() { final var userCount = User.count(); given().contentType(ContentType.JSON) .body(new UserCreation("new@example.org", "admin")) @@ -96,7 +96,7 @@ public void createWithForbiddenUsername() { } @Test - public void createWithInvalidEmail() { + public void createUserWithInvalidEmail() { final var userCount = User.count(); given().contentType(ContentType.JSON) .body(new UserCreation("not-an-email", "new_user")) @@ -107,7 +107,7 @@ public void createWithInvalidEmail() { } @Test - public void createWithEmptyEmail() { + public void createUserWithEmptyEmail() { final var userCount = User.count(); given().contentType(ContentType.JSON) .body(new UserCreation("", "new_user")) @@ -118,7 +118,7 @@ public void createWithEmptyEmail() { } @Test - public void createWithExistingUsername() { + public void createUserWithExistingUsername() { final var userCount = User.count(); final var existingUser = User.findAll().firstResult(); given().contentType(ContentType.JSON) @@ -130,7 +130,7 @@ public void createWithExistingUsername() { } @Test - public void createWithExistingEmail() { + public void createUserWithExistingEmail() { final var userCount = User.count(); final var existingEmail = Email.findAll().firstResult(); given().contentType(ContentType.JSON) @@ -142,7 +142,7 @@ public void createWithExistingEmail() { } @Test - public void createWithEmptyInput() { + public void createUserWithEmptyInput() { final var userCount = User.count(); given().contentType(ContentType.JSON).post().then().statusCode(400); assertEquals(userCount, User.count()); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeAvatarTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteCurrentUserAvatarTests.java similarity index 77% rename from src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeAvatarTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteCurrentUserAvatarTests.java index 7ba2932..7f5ede8 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeAvatarTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteCurrentUserAvatarTests.java @@ -17,31 +17,31 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class DeleteMeAvatarTests extends UserTestsBase { +public final class DeleteCurrentUserAvatarTests extends UserTestsBase { @TestHTTPResource("image.jpeg") URL jpeg; @Test @TestSecurity(user = "user_0") - public void deleteMeAvatar() throws IOException { + public void deleteCurrentUserAvatar() throws IOException { try (final var stream = jpeg.openStream()) { given().contentType(ContentType.BINARY) .body(stream.readAllBytes()) - .put("me/avatar") + .put("current/avatar") .then() .statusCode(200); } final var remoteFileCount = StoredFile.count(); - given().delete("me/avatar").then().statusCode(204); + given().delete("current/avatar").then().statusCode(204); assertEquals(remoteFileCount - 1, StoredFile.count()); } @Test @TestSecurity(user = "user_0") - public void deleteMeAvatarWithoutAvatar() { + public void deleteCurrentUserAvatarWithoutAvatar() { final var remoteFileCount = StoredFile.count(); - given().delete("me/avatar").then().statusCode(204); + given().delete("current/avatar").then().statusCode(204); assertEquals(remoteFileCount, StoredFile.count()); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteCurrentUserTests.java similarity index 76% rename from src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteCurrentUserTests.java index 2373d87..da550f0 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteMeTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/DeleteCurrentUserTests.java @@ -15,19 +15,19 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class DeleteMeTests extends UserTestsBase { +public final class DeleteCurrentUserTests extends UserTestsBase { @Test @TestSecurity(user = "user_0") - public void deleteMe() { - given().delete("me").then().statusCode(204); + public void deleteCurrentUser() { + given().delete("current").then().statusCode(204); final var user = requireNonNull(User.findByUsername("user_0")); assertTrue(user.deleted); } @Test - public void deleteMeWithoutAuthentication() { + public void deleteCurrentUserWhileUnauthenticated() { final var userCount = User.count(); - given().delete("me").then().statusCode(401); + given().delete("current").then().statusCode(401); assertEquals(userCount, User.count()); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/GetCurrentUserTests.java similarity index 84% rename from src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/users/GetCurrentUserTests.java index d235d99..cdaad4d 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveMeTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/GetCurrentUserTests.java @@ -17,12 +17,12 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class RetrieveMeTests extends UserTestsBase { +public final class GetCurrentUserTests extends UserTestsBase { @Test @TestSecurity(user = "user_2") - public void retrieveMe() { + public void getCurrentUser() { final var user = requireNonNull(User.findByUsername("user_2")); - given().get("/me") + given().get("/current") .then() .contentType(ContentType.JSON) .statusCode(200) @@ -36,7 +36,7 @@ public void retrieveMe() { } @Test - public void retrieveMeUnauthenticated() { - given().get("/me").then().statusCode(401); + public void getCurrentUserWhileUnauthenticated() { + given().get("/current").then().statusCode(401); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/GetUserTests.java similarity index 89% rename from src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/users/GetUserTests.java index 7d3c797..8119ee8 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/RetrieveTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/GetUserTests.java @@ -20,10 +20,10 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class RetrieveTests extends UserTestsBase { +public final class GetUserTests extends UserTestsBase { @ParameterizedTest @ValueSource(strings = {"user_0", "user_1", "user_2"}) - public void retrieve(final String username) { + public void getUser(final String username) { final var user = requireNonNull(User.findByUsername(username)); given().get(user.id.toString()) .then() @@ -40,14 +40,14 @@ public void retrieve(final String username) { @ParameterizedTest @ValueSource(strings = {"user_inactive_0", "user_inactive_1", "user_inactive_2"}) - public void retrieveInactive(final String username) { + public void getInactiveUser(final String username) { final var user = requireNonNull(User.findByUsername(username)); given().get(user.id.toString()).then().statusCode(404); } @Test @Transactional - public void retrieveDeleted() { + public void getDeletedUser() { final var user = requireNonNull(User.findByUsername("user_0")); QuarkusTransaction.requiringNew().run(() -> User.findById(user.id).softDelete()); given().get(user.id.toString()).then().statusCode(410); @@ -55,7 +55,7 @@ public void retrieveDeleted() { @ParameterizedTest @ValueSource(strings = {"nope", "fake", "@", "admin", "00000000-0000-0000-0000-000000000000"}) - public void retrieveNonExistent(final String userId) { + public void getNonExistentUser(final String userId) { given().get(userId).then().statusCode(404); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedUsersTests.java similarity index 92% rename from src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedUsersTests.java index dc281a0..5721805 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/ListBlockedUsersTests.java @@ -21,13 +21,13 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class ListBlockedTests extends UserTestsBase { +public final class ListBlockedUsersTests extends UserTestsBase { @ConfigProperty(name = "app.paging.size") int pagingSize; @Test @TestSecurity(user = "user_0") - public void listBlocked() { + public void listBlockedUsers() { final var user = User.findByUsername("user_0"); final var response = given().queryParam("page", 0) .get("blocked") @@ -44,13 +44,13 @@ public void listBlocked() { @Test @TestSecurity(user = "user_0") - public void listBlockedOutOfBounds() { + public void listBlockedUsersOutOfBounds() { given().queryParam("page", -1).get("blocked").then().statusCode(400); } @Test @TestSecurity(user = "user_0") - public void listBlockedTooFar() { + public void listBlockedUsersTooFar() { given().queryParam("page", 50) .get("blocked") .then() diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/SetCurrentUserAvatarTests.java similarity index 83% rename from src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/users/SetCurrentUserAvatarTests.java index 7adddfb..8d9f10a 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeAvatarTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/SetCurrentUserAvatarTests.java @@ -22,17 +22,17 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class UpdateMeAvatarTests extends UserTestsBase { +public final class SetCurrentUserAvatarTests extends UserTestsBase { @ParameterizedTest @ValueSource(strings = {"jpeg", "png", "webp"}) @TestSecurity(user = "user_0") - public void updateMeAvatar(final String fileType) throws IOException { + public void setCurrentUserAvatar(final String fileType) throws IOException { final var remoteFileCount = StoredFile.count(); try (final var stream = openStream(fileType)) { given().contentType(ContentType.BINARY) .body(stream.readAllBytes()) - .put("me/avatar") + .put("current/avatar") .then() .contentType(ContentType.TEXT) .statusCode(200) @@ -47,13 +47,13 @@ public void updateMeAvatar(final String fileType) throws IOException { @ParameterizedTest @ValueSource(strings = {"gif", "text"}) @TestSecurity(user = "user_0") - public void updateMeAvatarWithInvalidType(final String fileType) throws IOException { + public void setCurrentUserAvatarWithInvalidType(final String fileType) throws IOException { final var remoteFileCount = StoredFile.count(); try (final var stream = openStream(fileType)) { given().contentType(ContentType.BINARY) .body(stream.readAllBytes()) - .put("me/avatar") + .put("current/avatar") .then() .statusCode(415); } @@ -65,9 +65,9 @@ public void updateMeAvatarWithInvalidType(final String fileType) throws IOExcept @Test @TestSecurity(user = "user_0") - public void updateMeAvatarWithoutInput() { + public void setCurrentUserAvatarWithoutInput() { final var remoteFileCount = StoredFile.count(); - given().contentType(ContentType.BINARY).put("me/avatar").then().statusCode(415); + given().contentType(ContentType.BINARY).put("current/avatar").then().statusCode(415); assertEquals(remoteFileCount, StoredFile.count()); final var user = requireNonNull(User.findByUsername("user_0")); assertNull(user.avatar); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeBioTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/SetCurrentUserBioTests.java similarity index 70% rename from src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeBioTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/users/SetCurrentUserBioTests.java index c7166e7..ce7b7fe 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateMeBioTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/SetCurrentUserBioTests.java @@ -18,12 +18,16 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class UpdateMeBioTests extends UserTestsBase { +public final class SetCurrentUserBioTests extends UserTestsBase { @ParameterizedTest @ValueSource(strings = {"Test", "Some random bio", ""}) @TestSecurity(user = "user_0") - public void updateMeBio(final String bio) { - given().contentType(ContentType.TEXT).body(bio).put("me/bio").then().statusCode(200); + public void setCurrentUserBio(final String bio) { + given().contentType(ContentType.TEXT) + .body(bio) + .put("current/bio") + .then() + .statusCode(200); final var user = requireNonNull(User.findByUsername("user_0")); assertEquals(bio, user.bio); } @@ -31,12 +35,12 @@ public void updateMeBio(final String bio) { @Test @TestSecurity(user = "user_0") @Transactional - public void updateMeBioWithBioTooLong() { + public void setCurrentUserBioTooLong() { final var user = requireNonNull(User.findByUsername("user_0")); final var bio = user.bio; given().contentType(ContentType.TEXT) .body("a".repeat(3001)) - .put("me/bio") + .put("current/bio") .then() .statusCode(400); user.refresh(); @@ -44,7 +48,11 @@ public void updateMeBioWithBioTooLong() { } @Test - public void updateMeBioMeBioWithoutAuthentication() { - given().contentType(ContentType.TEXT).body("Test").put("me/bio").then().statusCode(401); + public void setCurrentUserBioWhileUnauthenticated() { + given().contentType(ContentType.TEXT) + .body("Test") + .put("current/bio") + .then() + .statusCode(401); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBannedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/SetUserBannedTests.java similarity index 89% rename from src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBannedTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/users/SetUserBannedTests.java index 8f3dfc8..d619646 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBannedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/SetUserBannedTests.java @@ -18,11 +18,11 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class UpdateBannedTests extends UserTestsBase { +public final class SetUserBannedTests extends UserTestsBase { @Test @TestSecurity(user = "user_0", roles = "MODERATOR") @Transactional - public void updateBannedAsModerator() { + public void setUserBannedAsModerator() { final var user = requireNonNull(User.findByUsername("user_1")); given().put(user.id + "/banned").then().statusCode(200); user.refresh(); @@ -33,7 +33,7 @@ public void updateBannedAsModerator() { @Test @TestSecurity(user = "user_0") @Transactional - public void updateBannedAsUser() { + public void setUserBannedAsUser() { final var user = requireNonNull(User.findByUsername("user_1")); given().put(user.id + "/banned").then().statusCode(403); user.refresh(); @@ -43,7 +43,7 @@ public void updateBannedAsUser() { @Test @Transactional - public void updateBannedUnauthenticated() { + public void setUserBannedWhileUnauthenticated() { final var user = requireNonNull(User.findByUsername("user_1")); given().put(user.id + "/banned").then().statusCode(401); user.refresh(); @@ -54,7 +54,7 @@ public void updateBannedUnauthenticated() { @Test @TestSecurity(user = "user_0", roles = "MODERATOR") @Transactional - public void updateBannedTwiceAsModerator() { + public void setUserBannedTwiceAsModerator() { final var user = requireNonNull(User.findByUsername("user_2")); given().put(user.id + "/banned").then().statusCode(200); user.refresh(); @@ -65,7 +65,7 @@ public void updateBannedTwiceAsModerator() { @Test @TestSecurity(user = "user_0") @Transactional - public void updateBannedTwiceAsUser() { + public void setUserBannedTwiceAsUser() { final var user = requireNonNull(User.findByUsername("user_2")); given().put(user.id + "/banned").then().statusCode(403); user.refresh(); @@ -75,7 +75,7 @@ public void updateBannedTwiceAsUser() { @Test @Transactional - public void updateBannedTwiceUnauthenticated() { + public void setUserBannedTwiceWhileUnauthenticated() { final var user = requireNonNull(User.findByUsername("user_2")); given().put(user.id + "/banned").then().statusCode(401); user.refresh(); @@ -86,7 +86,7 @@ public void updateBannedTwiceUnauthenticated() { @Test @TestSecurity(user = "user_0", roles = "MODERATOR") @Transactional - public void updateBannedAlreadyBannedAsModerator() { + public void setUserBannedAlreadyBannedAsModerator() { final var user = requireNonNull(User.findByUsername("user_3")); given().put(user.id + "/banned").then().statusCode(200); user.refresh(); @@ -97,7 +97,7 @@ public void updateBannedAlreadyBannedAsModerator() { @Test @TestSecurity(user = "user_0") @Transactional - public void updateBannedAlreadyBannedAsUser() { + public void setUserBannedAlreadyBannedAsUser() { final var user = requireNonNull(User.findByUsername("user_3")); given().put(user.id + "/banned").then().statusCode(403); user.refresh(); diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBlockedToFalseTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/SetUserBlockedToFalseTests.java similarity index 83% rename from src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBlockedToFalseTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/users/SetUserBlockedToFalseTests.java index d53ddb8..efc4eb4 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBlockedToFalseTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/SetUserBlockedToFalseTests.java @@ -21,10 +21,10 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class UpdateBlockedToFalseTests extends UserTestsBase { +public final class SetUserBlockedToFalseTests extends UserTestsBase { @Test @TestSecurity(user = "user_0") - public void updateBlocked() { + public void setUserBlocked() { final var user = requireNonNull(User.findByUsername("user_0")); final var otherUser = requireNonNull(User.findByUsername("user_1")); assertTrue(user.isBlocking(otherUser)); @@ -38,7 +38,7 @@ public void updateBlocked() { @Test @TestSecurity(user = "user_0") - public void updateBlockedTwice() { + public void setUserBlockedTwice() { final var user = requireNonNull(User.findByUsername("user_0")); final var otherUser = requireNonNull(User.findByUsername("user_1")); assertTrue(user.isBlocking(otherUser)); @@ -57,7 +57,7 @@ public void updateBlockedTwice() { @Test @TestSecurity(user = "user_0") - public void updateBlockedWithInactiveUser() { + public void setInactiveUserBlocked() { final var otherUser = requireNonNull(User.findByUsername("user_inactive_1")); final var blockCount = Block.count(); given().contentType(ContentType.JSON) @@ -70,7 +70,7 @@ public void updateBlockedWithInactiveUser() { @Test @TestSecurity(user = "user_0") - public void updateBlockedWithInvalidUser() { + public void setInvalidUserUserBlocked() { final var blockCount = Block.count(); given().contentType(ContentType.JSON) .body(new BlockUpdate(false)) @@ -80,6 +80,18 @@ public void updateBlockedWithInvalidUser() { assertEquals(blockCount, Block.count()); } + @Test + @TestSecurity(user = "user_0") + public void setCurrentUserBlocked() { + final var user = requireNonNull(User.findByUsername("user_0")); + given().contentType(ContentType.JSON) + .body(new BlockUpdate(false)) + .put(user.id + "/blocked") + .then() + .statusCode(403); + assertFalse(user.isBlocking(user)); + } + @BeforeEach @Transactional @Override diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBlockedToTrueTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/SetUserBlockedToTrueTests.java similarity index 92% rename from src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBlockedToTrueTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/users/SetUserBlockedToTrueTests.java index 540ef1e..c79870a 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateBlockedToTrueTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/SetUserBlockedToTrueTests.java @@ -20,10 +20,10 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class UpdateBlockedToTrueTests extends PostTestsBase { +public final class SetUserBlockedToTrueTests extends PostTestsBase { @Test @TestSecurity(user = "user_0") - public void updateBlocked() { + public void setUserBlocked() { final var user = requireNonNull(User.findByUsername("user_0")); final var otherUser = requireNonNull(User.findByUsername("user_1")); QuarkusTransaction.requiringNew().run(() -> otherUser.subscribeTo(post)); @@ -39,7 +39,7 @@ public void updateBlocked() { @Test @TestSecurity(user = "user_0") - public void updateBlockedTwice() { + public void setUserBlockedTwice() { final var user = requireNonNull(User.findByUsername("user_0")); final var otherUser = requireNonNull(User.findByUsername("user_1")); assertFalse(user.isBlocking(otherUser)); @@ -58,7 +58,7 @@ public void updateBlockedTwice() { @Test @TestSecurity(user = "user_0") - public void updateBlockedWithInactiveUser() { + public void setInactiveUserBlocked() { final var otherUser = requireNonNull(User.findByUsername("user_inactive_1")); final var blockCount = Block.count(); given().contentType(ContentType.JSON) @@ -71,7 +71,7 @@ public void updateBlockedWithInactiveUser() { @Test @TestSecurity(user = "user_0") - public void updateBlockedWithInvalidUser() { + public void setInvalidUserBlocked() { final var blockCount = Block.count(); given().contentType(ContentType.JSON) .body(new BlockUpdate(true)) @@ -83,7 +83,7 @@ public void updateBlockedWithInvalidUser() { @Test @TestSecurity(user = "user_0") - public void updateBlockedWithSelf() { + public void setCurrentUserBlocked() { final var user = requireNonNull(User.findByUsername("user_0")); given().contentType(ContentType.JSON) .body(new BlockUpdate(true)) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateReportedToFalseTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/SetUserReportedToFalseTests.java similarity index 92% rename from src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateReportedToFalseTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/users/SetUserReportedToFalseTests.java index 17c367f..c8e7038 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateReportedToFalseTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/SetUserReportedToFalseTests.java @@ -21,10 +21,10 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class UpdateReportedToFalseTests extends UserTestsBase { +public final class SetUserReportedToFalseTests extends UserTestsBase { @Test @TestSecurity(user = "user_0") - public void updateReport() { + public void setUserReported() { final var source = requireNonNull(User.findByUsername("user_0")); final var target = requireNonNull(User.findByUsername("user_1")); assertTrue(target.isReportedBy(source)); @@ -38,7 +38,7 @@ public void updateReport() { @Test @TestSecurity(user = "user_0") - public void updateReportTwice() { + public void setUserReportedTwice() { final var source = requireNonNull(User.findByUsername("user_0")); final var target = requireNonNull(User.findByUsername("user_1")); assertTrue(target.isReportedBy(source)); @@ -57,7 +57,7 @@ public void updateReportTwice() { @Test @TestSecurity(user = "user_0") - public void updateReportWithInactiveUser() { + public void setInactiveUserReported() { final var reportCount = Report.count(); final var target = requireNonNull(User.findByUsername("user_inactive_1")); given().contentType(ContentType.JSON) @@ -70,7 +70,7 @@ public void updateReportWithInactiveUser() { @Test @TestSecurity(user = "user_0") - public void updateReportWithInvalidUser() { + public void setInvalidUserReported() { final var reportCount = Report.count(); given().contentType(ContentType.JSON) .body(new ReportUpdate(false)) @@ -82,7 +82,7 @@ public void updateReportWithInvalidUser() { @Test @TestSecurity(user = "user_0") - public void updateReportWithSelf() { + public void setCurrentUserReported() { final var user = requireNonNull(User.findByUsername("user_0")); given().contentType(ContentType.JSON) .body(new ReportUpdate(false)) @@ -93,7 +93,7 @@ public void updateReportWithSelf() { } @Test - public void updateReportUnauthenticated() { + public void setUserReportedWhileUnauthenticated() { final var reportCount = Report.count(); final var user = requireNonNull(User.findByUsername("user_0")); given().contentType(ContentType.JSON) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateReportedToTrueTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/users/SetUserReportedToTrueTests.java similarity index 91% rename from src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateReportedToTrueTests.java rename to src/test/java/app/fyreplace/api/testing/endpoints/users/SetUserReportedToTrueTests.java index a542882..f34a10c 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/users/UpdateReportedToTrueTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/users/SetUserReportedToTrueTests.java @@ -19,10 +19,10 @@ @QuarkusTest @TestHTTPEndpoint(UsersEndpoint.class) -public final class UpdateReportedToTrueTests extends UserTestsBase { +public final class SetUserReportedToTrueTests extends UserTestsBase { @Test @TestSecurity(user = "user_0") - public void updateReport() { + public void setUserReported() { final var source = requireNonNull(User.findByUsername("user_0")); final var target = requireNonNull(User.findByUsername("user_1")); assertFalse(target.isReportedBy(source)); @@ -36,7 +36,7 @@ public void updateReport() { @Test @TestSecurity(user = "user_0") - public void updateReportTwice() { + public void setUserReportedTwice() { final var source = requireNonNull(User.findByUsername("user_0")); final var target = requireNonNull(User.findByUsername("user_1")); assertFalse(target.isReportedBy(source)); @@ -55,7 +55,7 @@ public void updateReportTwice() { @Test @TestSecurity(user = "user_0") - public void updateReportWithInactiveUser() { + public void setInactiveUserReported() { final var reportCount = Report.count(); final var target = requireNonNull(User.findByUsername("user_inactive_1")); given().contentType(ContentType.JSON) @@ -68,7 +68,7 @@ public void updateReportWithInactiveUser() { @Test @TestSecurity(user = "user_0") - public void updateReportWithInvalidUser() { + public void setInvalidUserReported() { final var reportCount = Report.count(); given().contentType(ContentType.JSON) .body(new ReportUpdate(true)) @@ -80,7 +80,7 @@ public void updateReportWithInvalidUser() { @Test @TestSecurity(user = "user_0") - public void updateReportWithSelf() { + public void setCurrentUserReported() { final var user = requireNonNull(User.findByUsername("user_0")); given().contentType(ContentType.JSON) .body(new ReportUpdate(true)) @@ -91,7 +91,7 @@ public void updateReportWithSelf() { } @Test - public void updateReportUnauthenticated() { + public void setUserReportedWhileUnauthenticated() { final var reportCount = Report.count(); final var user = requireNonNull(User.findByUsername("user_0")); given().contentType(ContentType.JSON) From de025e651a383bdd7a1793a3ec0f175eec6d4426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sun, 9 Jun 2024 18:08:49 +0200 Subject: [PATCH 138/157] Update dependencies --- .gitattributes | 2 + gradle.properties | 6 +- gradle/wrapper/gradle-wrapper.jar | Bin 43462 -> 43453 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 2 +- gradlew.bat | 184 +++++++++++------------ 6 files changed, 99 insertions(+), 97 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..eb5b2ee --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto +*.bat eol=crlf diff --git a/gradle.properties b/gradle.properties index 46edc6a..207e672 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,8 +1,8 @@ org.gradle.jvmargs=-Xmx1024M -quarkusVersion=3.9.5 -sentryVersion=7.8.0 +quarkusVersion=3.11.3 +sentryVersion=7.10.0 tikaVersion=2.9.2 -gitPluginVersion=3.0.0 +gitPluginVersion=3.1.0 spotlessPluginVersion=6.25.0 lombokPluginVersion=8.6 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd4917707c1f8861d8cb53dd15194d4248596..e6441136f3d4ba8a0da8d277868979cfbc8ad796 100644 GIT binary patch delta 34118 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 delta 34176 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 SGW6ptSs!X7{rG*o_g?|vpSEZK diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a80b22c..a441313 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a4..b740cf1 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. diff --git a/gradlew.bat b/gradlew.bat index 7101f8e..25da30d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,92 +1,92 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From 361264f981d06b0d34e4fbd76b2ca12792ce3e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Thu, 20 Jun 2024 18:56:29 +0200 Subject: [PATCH 139/157] Code cleanup --- .../app/fyreplace/api/testing/SubscriptionTestsBase.java | 2 +- .../testing/endpoints/chapters/DeleteChapterTests.java | 4 ++-- .../endpoints/chapters/SetChapterPositionTests.java | 4 ++-- .../api/testing/endpoints/posts/ListPostsFeedTests.java | 8 ++------ 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/test/java/app/fyreplace/api/testing/SubscriptionTestsBase.java b/src/test/java/app/fyreplace/api/testing/SubscriptionTestsBase.java index 9c04173..3a6de80 100644 --- a/src/test/java/app/fyreplace/api/testing/SubscriptionTestsBase.java +++ b/src/test/java/app/fyreplace/api/testing/SubscriptionTestsBase.java @@ -19,6 +19,6 @@ public void beforeEach() { stream.forEach(post -> dataSeeder.createComment(otherUser, post, "Comment", false)); } - QuarkusTransaction.requiringNew().run(() -> dataSeeder.createComment(otherUser, post, "Comment", false)); + dataSeeder.createComment(otherUser, post, "Comment", false); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/DeleteChapterTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/DeleteChapterTests.java index e287c0d..ff1f618 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/DeleteChapterTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/DeleteChapterTests.java @@ -80,7 +80,7 @@ public void deleteChapterInOtherDraft(final int position) { @ParameterizedTest @ValueSource(ints = {0, 1, 2}) - public void deleteChapterInPostUnauthenticated(final int position) { + public void deleteChapterInPostWhileUnauthenticated(final int position) { final var chapterCount = Chapter.count("post", post); final var chapterId = post.getChapters().get(position).id; given().pathParam("id", post.id).delete(String.valueOf(position)).then().statusCode(401); @@ -90,7 +90,7 @@ public void deleteChapterInPostUnauthenticated(final int position) { @ParameterizedTest @ValueSource(ints = {0, 1, 2}) - public void deleteChapterInDraftUnauthenticated(final int position) { + public void deleteChapterInDraftWhileUnauthenticated(final int position) { final var chapterCount = Chapter.count("post", draft); final var chapterId = draft.getChapters().get(position).id; given().pathParam("id", draft.id) diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/SetChapterPositionTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/SetChapterPositionTests.java index 97b7198..73f61eb 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/SetChapterPositionTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/SetChapterPositionTests.java @@ -112,7 +112,7 @@ public void setChapterPositionInOtherDraft(final int to) { @ParameterizedTest @ValueSource(ints = {0, 1, 2}) @Transactional - public void setChapterPositionInPostUnauthenticated(final int to) { + public void setChapterPositionInPostWhileUnauthenticated(final int to) { final var from = 1; final var chapter = post.getChapters().get(from); final var oldPosition = chapter.position; @@ -124,7 +124,7 @@ public void setChapterPositionInPostUnauthenticated(final int to) { @ParameterizedTest @ValueSource(ints = {0, 1, 2}) @Transactional - public void setChapterPositionInDraftUnauthenticated(final int to) { + public void setChapterPositionInDraftWhileUnauthenticated(final int to) { final var from = 1; final var chapter = draft.getChapters().get(from); final var oldPosition = chapter.position; diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListPostsFeedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListPostsFeedTests.java index abb3ce9..92a0264 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListPostsFeedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListPostsFeedTests.java @@ -82,9 +82,7 @@ public void listPostsFeedWithPostsFromBlockedUser() { final var user = requireNonNull(User.findByUsername("user_0")); final var otherUser = requireNonNull(User.findByUsername("user_1")); QuarkusTransaction.requiringNew().run(() -> user.block(otherUser)); - QuarkusTransaction.requiringNew() - .run(() -> range(0, 5).forEach(i -> dataSeeder.createPost(otherUser, "Post " + i, true, false))); - + range(0, 5).forEach(i -> dataSeeder.createPost(otherUser, "Post " + i, true, false)); given().get("feed").then().statusCode(200).body("size()", equalTo(0)); } @@ -94,9 +92,7 @@ public void listPostsFeedWithPostsFromBlockingUser() { final var user = requireNonNull(User.findByUsername("user_0")); final var otherUser = requireNonNull(User.findByUsername("user_1")); QuarkusTransaction.requiringNew().run(() -> user.block(otherUser)); - QuarkusTransaction.requiringNew() - .run(() -> range(0, 5).forEach(i -> dataSeeder.createPost(otherUser, "Post " + i, true, false))); - + range(0, 5).forEach(i -> dataSeeder.createPost(otherUser, "Post " + i, true, false)); given().get("feed").then().statusCode(200).body("size()", equalTo(0)); } From eca823aa0dd16ad1bc7b3678415d46cc0313b57c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Thu, 20 Jun 2024 18:59:48 +0200 Subject: [PATCH 140/157] Format code --- .../java/app/fyreplace/api/testing/SubscriptionTestsBase.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/app/fyreplace/api/testing/SubscriptionTestsBase.java b/src/test/java/app/fyreplace/api/testing/SubscriptionTestsBase.java index 3a6de80..5d73fe8 100644 --- a/src/test/java/app/fyreplace/api/testing/SubscriptionTestsBase.java +++ b/src/test/java/app/fyreplace/api/testing/SubscriptionTestsBase.java @@ -2,7 +2,6 @@ import app.fyreplace.api.data.Post; import app.fyreplace.api.data.User; -import io.quarkus.narayana.jta.QuarkusTransaction; import jakarta.transaction.Transactional; import org.junit.jupiter.api.BeforeEach; From 89f8a6fbe2c996d06e62c718ded698f394d799d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Thu, 20 Jun 2024 18:57:08 +0200 Subject: [PATCH 141/157] Allow anonymous feeds --- .../api/endpoints/PostsEndpoint.java | 38 ++++++++++--------- .../endpoints/posts/ListPostsFeedTests.java | 8 ++++ 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java index ae216be..9c17d01 100644 --- a/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/PostsEndpoint.java @@ -33,6 +33,8 @@ import jakarta.ws.rs.core.Response.Status; import jakarta.ws.rs.core.SecurityContext; import java.time.Instant; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.openapi.annotations.media.Content; @@ -231,27 +233,27 @@ public long countPosts(@QueryParam("type") @NotNull final PostListingType type) @GET @Path("feed") - @Authenticated @APIResponse(responseCode = "200", description = "OK") public Iterable listPostsFeed() { - final var user = User.getFromSecurityContext(context); + final var user = User.getFromSecurityContext(context, null, false); + final var conditions = new ArrayList<>(List.of("dateCreated > ?1", "published = true", "life > 0")); + final var sorting = Sort.by("life", "dateCreated", "id"); + final var deadline = Instant.now().minus(Post.shelfLife); + + if (user != null) { + conditions.addAll(List.of( + "author != ?2", + "id not in (select post.id from Vote where user = ?2)", + "author.id not in (select target.id from Block where source = ?2)", + "author.id not in (select source.id from Block where target = ?2)")); + } + + final var conditionsString = String.join(" and ", conditions); + final var posts = user != null + ? Post.find(conditionsString, sorting, deadline, user) + : Post.find(conditionsString, sorting, deadline); - try (final var stream = Post.find( - """ - author != ?1 - and dateCreated > ?2 - and published = true - and life > 0 - and id not in (select post.id from Vote where user = ?1) - and author.id not in (select target.id from Block where source = ?1) - and author.id not in (select source.id from Block where target = ?1) - """, - Sort.by("life", "dateCreated", "id"), - user, - Instant.now().minus(Post.shelfLife)) - .filter("existing") - .range(0, 2) - .stream()) { + try (final var stream = posts.filter("existing").range(0, 2).stream()) { return stream.peek(p -> p.setCurrentUser(user)).toList(); } } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListPostsFeedTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListPostsFeedTests.java index 92a0264..42fd053 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListPostsFeedTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/posts/ListPostsFeedTests.java @@ -96,6 +96,14 @@ public void listPostsFeedWithPostsFromBlockingUser() { given().get("feed").then().statusCode(200).body("size()", equalTo(0)); } + @Test + public void listPostsFeedWhileUnauthenticated() { + final var user = requireNonNull(User.findByUsername("user_1")); + range(0, 5).forEach(i -> dataSeeder.createPost(user, "Post " + i, true, false)); + final var response = given().get("feed").then().statusCode(200).body("size()", equalTo(3)); + range(0, 3).forEach(i -> response.body("[" + i + "].author.id", equalTo(user.id.toString()))); + } + @BeforeEach @Transactional @Override From 970faea4dbab717f169434d5b8b9005dfa023285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 29 Jun 2024 18:22:55 +0200 Subject: [PATCH 142/157] Fix incorrect OpenAPI request body type --- .../api/endpoints/ChaptersEndpoint.java | 17 +++++------------ .../fyreplace/api/endpoints/TokensEndpoint.java | 5 +---- .../fyreplace/api/endpoints/UsersEndpoint.java | 13 ++----------- 3 files changed, 8 insertions(+), 27 deletions(-) diff --git a/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java index 8d7b194..d7bd369 100644 --- a/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java @@ -96,11 +96,12 @@ public Response deleteChapter(@PathParam("id") final UUID id, @PathParam("positi @Path("{position}/position") @Authenticated @Transactional - @Consumes(MediaType.TEXT_PLAIN) + @Consumes(MediaType.APPLICATION_JSON) @APIResponse( responseCode = "200", description = "OK", - content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = Integer.class))) + content = + @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Integer.class))) @APIResponse(responseCode = "400", description = "Bad request") @APIResponse(responseCode = "404", description = "Not found") public int setChapterPosition( @@ -132,11 +133,7 @@ public int setChapterPosition( @Path("{position}/text") @Authenticated @Transactional - @Consumes(MediaType.TEXT_PLAIN) - @APIResponse( - responseCode = "200", - description = "OK", - content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) + @APIResponse(responseCode = "200", description = "OK") @APIResponse(responseCode = "400", description = "Bad request") @APIResponse(responseCode = "404", description = "Not found") public String setChapterText( @@ -161,11 +158,7 @@ public String setChapterText( @Path("{position}/image") @Authenticated @Transactional - @Consumes(MediaType.APPLICATION_OCTET_STREAM) - @APIResponse( - responseCode = "200", - description = "OK", - content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) + @APIResponse(responseCode = "200", description = "OK") @APIResponse(responseCode = "400", description = "Bad request") @APIResponse(responseCode = "404", description = "Not found") public String setChapterImage( diff --git a/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java index 4e311fa..49215d5 100644 --- a/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java @@ -74,10 +74,7 @@ public Response createToken(@Valid @NotNull final TokenCreation input) { @GET @Path("new") @Authenticated - @APIResponse( - responseCode = "200", - description = "OK", - content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) + @APIResponse(responseCode = "200", description = "OK") public String getNewToken() { return jwtService.makeJwt(User.getFromSecurityContext(context)); } diff --git a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java index 7d05cc0..53e305e 100644 --- a/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/UsersEndpoint.java @@ -25,7 +25,6 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.PositiveOrZero; -import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; import jakarta.ws.rs.NotFoundException; @@ -189,11 +188,7 @@ public User getCurrentUser() { @Path("current/bio") @Authenticated @Transactional - @Consumes(MediaType.TEXT_PLAIN) - @APIResponse( - responseCode = "200", - description = "OK", - content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) + @APIResponse(responseCode = "200", description = "OK") @APIResponse(responseCode = "400", description = "Bad Request") public String setCurrentUserBio(@NotNull @Length(max = 3000) final String input) { final var user = User.getFromSecurityContext(context, LockModeType.PESSIMISTIC_READ); @@ -206,11 +201,7 @@ public String setCurrentUserBio(@NotNull @Length(max = 3000) final String input) @Path("current/avatar") @Authenticated @Transactional - @Consumes(MediaType.APPLICATION_OCTET_STREAM) - @APIResponse( - responseCode = "200", - description = "OK", - content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class))) + @APIResponse(responseCode = "200", description = "OK") @APIResponse(responseCode = "413", description = "Payload Too Large") @APIResponse(responseCode = "415", description = "Unsupported Media Type") public String setCurrentUserAvatar(final byte[] input) throws IOException { From 27c5b6ba7a7275a6bbedbce4668cca26b0b77a33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Sat, 29 Jun 2024 18:50:00 +0200 Subject: [PATCH 143/157] Better handle chapter position inputs --- .../api/data/ChapterPositionUpdate.java | 5 ++ .../api/endpoints/ChaptersEndpoint.java | 25 ++++---- .../chapters/SetChapterPositionTests.java | 58 ++++++++++++++----- 3 files changed, 61 insertions(+), 27 deletions(-) create mode 100644 src/main/java/app/fyreplace/api/data/ChapterPositionUpdate.java diff --git a/src/main/java/app/fyreplace/api/data/ChapterPositionUpdate.java b/src/main/java/app/fyreplace/api/data/ChapterPositionUpdate.java new file mode 100644 index 0000000..ffdd2cf --- /dev/null +++ b/src/main/java/app/fyreplace/api/data/ChapterPositionUpdate.java @@ -0,0 +1,5 @@ +package app.fyreplace.api.data; + +import jakarta.validation.constraints.Min; + +public record ChapterPositionUpdate(@Min(0) int position) {} diff --git a/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java index d7bd369..5ea0cb7 100644 --- a/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java +++ b/src/main/java/app/fyreplace/api/endpoints/ChaptersEndpoint.java @@ -4,6 +4,7 @@ import app.fyreplace.api.cache.DuplicateRequestKeyGenerator; import app.fyreplace.api.data.Chapter; +import app.fyreplace.api.data.ChapterPositionUpdate; import app.fyreplace.api.data.Post; import app.fyreplace.api.data.StoredFile; import app.fyreplace.api.data.User; @@ -16,7 +17,6 @@ import jakarta.inject.Inject; import jakarta.transaction.Transactional; import jakarta.validation.constraints.NotNull; -import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.POST; @@ -96,34 +96,31 @@ public Response deleteChapter(@PathParam("id") final UUID id, @PathParam("positi @Path("{position}/position") @Authenticated @Transactional - @Consumes(MediaType.APPLICATION_JSON) - @APIResponse( - responseCode = "200", - description = "OK", - content = - @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Integer.class))) + @APIResponse(responseCode = "200", description = "OK") @APIResponse(responseCode = "400", description = "Bad request") @APIResponse(responseCode = "404", description = "Not found") - public int setChapterPosition( - @PathParam("id") final UUID id, @PathParam("position") final int position, @NotNull final Integer input) { + public Response setChapterPosition( + @PathParam("id") final UUID id, + @PathParam("position") final int position, + @NotNull final ChapterPositionUpdate input) { final var user = User.getFromSecurityContext(context); final var post = Post.findById(id); Post.validateAccess(post, user, false, true); - if (position == input) { - return input; + if (position == input.position()) { + return Response.ok().build(); } try { final var chapters = post.getChapters(); final var chapter = chapters.get(position); - final var before = input > position ? input : input - 1; - final var after = input > position ? input + 1 : input; + final var before = input.position() > position ? input.position() : input.position() - 1; + final var after = input.position() > position ? input.position() + 1 : input.position(); final var beforePosition = before >= 0 ? chapters.get(before).position : null; final var afterPosition = after < chapters.size() ? chapters.get(after).position : null; chapter.position = Chapter.positionBetween(beforePosition, afterPosition); chapter.persist(); - return input; + return Response.ok().build(); } catch (final IndexOutOfBoundsException e) { throw new NotFoundException(); } diff --git a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/SetChapterPositionTests.java b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/SetChapterPositionTests.java index 73f61eb..6b787d9 100644 --- a/src/test/java/app/fyreplace/api/testing/endpoints/chapters/SetChapterPositionTests.java +++ b/src/test/java/app/fyreplace/api/testing/endpoints/chapters/SetChapterPositionTests.java @@ -4,12 +4,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import app.fyreplace.api.data.Chapter; +import app.fyreplace.api.data.ChapterPositionUpdate; import app.fyreplace.api.endpoints.ChaptersEndpoint; import app.fyreplace.api.testing.PostTestsBase; import io.quarkus.panache.common.Sort; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; import jakarta.transaction.Transactional; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -25,7 +27,12 @@ public void setChapterPositionInOwnPost(final int to) { final var from = 1; final var chapter = post.getChapters().get(from); final var position = chapter.position; - given().body(to).pathParam("id", post.id).put(from + "/position").then().statusCode(403); + given().contentType(ContentType.JSON) + .body(new ChapterPositionUpdate(to)) + .pathParam("id", post.id) + .put(from + "/position") + .then() + .statusCode(403); chapter.refresh(); assertEquals(position, chapter.position); } @@ -36,7 +43,8 @@ public void setChapterPositionInOwnPost(final int to) { public void setChapterPositionInOwnDraft(final int to) { final var from = 1; final var chapter = draft.getChapters().get(from); - given().body(to) + given().contentType(ContentType.JSON) + .body(new ChapterPositionUpdate(to)) .pathParam("id", draft.id) .put(from + "/position") .then() @@ -46,14 +54,15 @@ public void setChapterPositionInOwnDraft(final int to) { } @ParameterizedTest - @ValueSource(strings = {"-1", "12"}) + @ValueSource(ints = {-1, 12}) @TestSecurity(user = "user_0") @Transactional - public void setChapterPositionInOwnDraftOutOfBounds(final String to) { + public void setChapterPositionInOwnDraftOutOfBounds(final int to) { final var from = 1; final var chapter = draft.getChapters().get(from); final var position = chapter.position; - given().body(to) + given().contentType(ContentType.JSON) + .body(new ChapterPositionUpdate(to)) .pathParam("id", draft.id) .put(from + "/position") .then() @@ -63,14 +72,15 @@ public void setChapterPositionInOwnDraftOutOfBounds(final String to) { } @ParameterizedTest - @ValueSource(strings = {"1.5", "null"}) + @ValueSource(strings = {"{ position: 1.5 }", "null"}) @TestSecurity(user = "user_0") @Transactional public void setChapterPositionInOwnDraftWithInvalidInput(final String to) { final var from = 1; final var chapter = draft.getChapters().get(from); final var position = chapter.position; - given().body(to) + given().contentType(ContentType.JSON) + .body(to) .pathParam("id", draft.id) .put(from + "/position") .then() @@ -87,7 +97,12 @@ public void setChapterPositionInOtherPost(final int to) { final var from = 1; final var chapter = post.getChapters().get(from); final var oldPosition = chapter.position; - given().body(to).pathParam("id", post.id).put(from + "/position").then().statusCode(403); + given().contentType(ContentType.JSON) + .body(new ChapterPositionUpdate(to)) + .pathParam("id", post.id) + .put(from + "/position") + .then() + .statusCode(403); chapter.refresh(); assertEquals(oldPosition, chapter.position); } @@ -100,7 +115,8 @@ public void setChapterPositionInOtherDraft(final int to) { final var from = 1; final var chapter = draft.getChapters().get(from); final var oldPosition = chapter.position; - given().body(to) + given().contentType(ContentType.JSON) + .body(new ChapterPositionUpdate(to)) .pathParam("id", draft.id) .put(from + "/position") .then() @@ -116,7 +132,12 @@ public void setChapterPositionInPostWhileUnauthenticated(final int to) { final var from = 1; final var chapter = post.getChapters().get(from); final var oldPosition = chapter.position; - given().body(to).pathParam("id", post.id).put(from + "/position").then().statusCode(401); + given().contentType(ContentType.JSON) + .body(new ChapterPositionUpdate(to)) + .pathParam("id", post.id) + .put(from + "/position") + .then() + .statusCode(401); chapter.refresh(); assertEquals(oldPosition, chapter.position); } @@ -128,7 +149,8 @@ public void setChapterPositionInDraftWhileUnauthenticated(final int to) { final var from = 1; final var chapter = draft.getChapters().get(from); final var oldPosition = chapter.position; - given().body(to) + given().contentType(ContentType.JSON) + .body(new ChapterPositionUpdate(to)) .pathParam("id", draft.id) .put(from + "/position") .then() @@ -145,7 +167,12 @@ public void setChapterPositionInNonExistentPost(final int to) { final var from = 1; final var chapter = post.getChapters().get(from); final var oldPosition = chapter.position; - given().body(to).pathParam("id", fakeId).put(from + "/position").then().statusCode(404); + given().contentType(ContentType.JSON) + .body(new ChapterPositionUpdate(to)) + .pathParam("id", fakeId) + .put(from + "/position") + .then() + .statusCode(404); chapter.refresh(); assertEquals(oldPosition, chapter.position); } @@ -155,6 +182,11 @@ public void setChapterPositionInNonExistentPost(final int to) { @TestSecurity(user = "user_0") @Transactional public void setNonExistentChapterPosition(final String from) { - given().body(1).pathParam("id", draft.id).put(from + "/position").then().statusCode(404); + given().contentType(ContentType.JSON) + .body(new ChapterPositionUpdate(1)) + .pathParam("id", draft.id) + .put(from + "/position") + .then() + .statusCode(404); } } From 663e3f811853cdb258c56b65d99a20f1794f6c6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Mon, 1 Jul 2024 13:18:54 +0200 Subject: [PATCH 144/157] Update dependencies --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 207e672..706e3a5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ org.gradle.jvmargs=-Xmx1024M -quarkusVersion=3.11.3 +quarkusVersion=3.12.0 sentryVersion=7.10.0 tikaVersion=2.9.2 gitPluginVersion=3.1.0 From abf238e793fe9ea6d5ddc93750e3510564f61001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Tue, 2 Jul 2024 17:43:31 +0200 Subject: [PATCH 145/157] Align username length with old API --- src/main/java/app/fyreplace/api/data/User.java | 2 +- src/main/java/app/fyreplace/api/data/UserCreation.java | 2 +- src/main/resources/db/changeLog.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/app/fyreplace/api/data/User.java b/src/main/java/app/fyreplace/api/data/User.java index 00679be..e4a7967 100644 --- a/src/main/java/app/fyreplace/api/data/User.java +++ b/src/main/java/app/fyreplace/api/data/User.java @@ -71,7 +71,7 @@ public class User extends SoftDeletableEntityBase implements Reportable { "void", "voids")); - @Column(length = 100, unique = true) + @Column(length = 50, unique = true) @Schema(required = true) public String username; diff --git a/src/main/java/app/fyreplace/api/data/UserCreation.java b/src/main/java/app/fyreplace/api/data/UserCreation.java index f88a2d6..5baab24 100644 --- a/src/main/java/app/fyreplace/api/data/UserCreation.java +++ b/src/main/java/app/fyreplace/api/data/UserCreation.java @@ -7,4 +7,4 @@ public record UserCreation( @NotBlank @Length(min = 3, max = 254) @Email String email, - @NotBlank @Length(min = 3, max = 100) @Regex(pattern = "^[\\w.@+-]+\\Z") String username) {} + @NotBlank @Length(min = 3, max = 50) @Regex(pattern = "^[\\w.@+-]+\\Z") String username) {} diff --git a/src/main/resources/db/changeLog.yaml b/src/main/resources/db/changeLog.yaml index 104c53a..eeb20e2 100644 --- a/src/main/resources/db/changeLog.yaml +++ b/src/main/resources/db/changeLog.yaml @@ -86,7 +86,7 @@ databaseChangeLog: type: "UUID" - column: name: "username" - type: "VARCHAR(100)" + type: "VARCHAR(50)" - column: constraints: nullable: false From 57ac3672456cc10d4e8cdefc1aa9c208a462a3d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Thu, 4 Jul 2024 14:01:12 +0200 Subject: [PATCH 146/157] Add schema.org info in emails --- .../EmailVerificationEmail/html.html.mjml | 30 ++++++++-------- .../UserActivationEmail/html.html.mjml | 34 +++++++++++-------- .../UserConnectionEmail/html.html.mjml | 30 ++++++++-------- src/main/resources/templates/_attributes.mjml | 23 ++++++++++--- .../resources/templates/_link_end_notice.mjml | 2 +- src/main/resources/templates/_logo.mjml | 2 +- 6 files changed, 71 insertions(+), 50 deletions(-) diff --git a/src/main/resources/templates/EmailVerificationEmail/html.html.mjml b/src/main/resources/templates/EmailVerificationEmail/html.html.mjml index fa16230..bd929a5 100644 --- a/src/main/resources/templates/EmailVerificationEmail/html.html.mjml +++ b/src/main/resources/templates/EmailVerificationEmail/html.html.mjml @@ -1,15 +1,17 @@ - - - - - - {res.getString("codeDescription")} - {code} - {res.getString("linkDescription")} - {res.getString("button")} - - - - - \ No newline at end of file + + + + + + {res.getString("codeDescription")} + {code} + {res.getString("linkDescription")} + + {res.getString("button")} + + + + + + diff --git a/src/main/resources/templates/UserActivationEmail/html.html.mjml b/src/main/resources/templates/UserActivationEmail/html.html.mjml index f78224c..f75d75f 100644 --- a/src/main/resources/templates/UserActivationEmail/html.html.mjml +++ b/src/main/resources/templates/UserActivationEmail/html.html.mjml @@ -1,16 +1,20 @@ - - - - - - {res.getString("title").replace("$1", appName)} - {res.getString("codeDescription")} - {code} - {res.getString("linkDescription")} - {res.getString("button")} - - - - - \ No newline at end of file + + + + + + {res.getString("title").replace("$1", + appName)} + + {res.getString("codeDescription")} + {code} + {res.getString("linkDescription")} + + {res.getString("button")} + + + + + + diff --git a/src/main/resources/templates/UserConnectionEmail/html.html.mjml b/src/main/resources/templates/UserConnectionEmail/html.html.mjml index fa16230..bd929a5 100644 --- a/src/main/resources/templates/UserConnectionEmail/html.html.mjml +++ b/src/main/resources/templates/UserConnectionEmail/html.html.mjml @@ -1,15 +1,17 @@ - - - - - - {res.getString("codeDescription")} - {code} - {res.getString("linkDescription")} - {res.getString("button")} - - - - - \ No newline at end of file + + + + + + {res.getString("codeDescription")} + {code} + {res.getString("linkDescription")} + + {res.getString("button")} + + + + + + diff --git a/src/main/resources/templates/_attributes.mjml b/src/main/resources/templates/_attributes.mjml index a99f4d4..9407127 100644 --- a/src/main/resources/templates/_attributes.mjml +++ b/src/main/resources/templates/_attributes.mjml @@ -1,6 +1,19 @@ - - - - - \ No newline at end of file + + + + + + + + diff --git a/src/main/resources/templates/_link_end_notice.mjml b/src/main/resources/templates/_link_end_notice.mjml index ffbca77..18542f3 100644 --- a/src/main/resources/templates/_link_end_notice.mjml +++ b/src/main/resources/templates/_link_end_notice.mjml @@ -1 +1 @@ -{res.getString("linkEndNotice")} \ No newline at end of file +{res.getString("linkEndNotice")} diff --git a/src/main/resources/templates/_logo.mjml b/src/main/resources/templates/_logo.mjml index 0702fbb..59407d4 100644 --- a/src/main/resources/templates/_logo.mjml +++ b/src/main/resources/templates/_logo.mjml @@ -1,2 +1,2 @@ - \ No newline at end of file + From c88e981a32253bb3c9ebc1e1033d687856377263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Thu, 4 Jul 2024 23:48:31 +0200 Subject: [PATCH 147/157] Add more metadata in emails --- .env-example | 1 + .github/workflows/validation.yml | 1 + .../app/fyreplace/api/data/RandomCode.java | 3 ++ .../java/app/fyreplace/api/data/User.java | 3 ++ .../app/fyreplace/api/emails/EmailBase.java | 44 ++++++++++++++++-- .../api/emails/EmailVerificationEmail.java | 9 ++-- .../api/emails/UserActivationEmail.java | 14 ++---- .../api/emails/UserConnectionEmail.java | 9 ++-- .../app/fyreplace/api/tasks/CleanupTasks.java | 9 +--- src/main/resources/META-INF/branding/logo.png | Bin 28 -> 8364 bytes .../resources/images/logo-maskable.png | Bin 0 -> 5534 bytes .../META-INF/resources/images/logo.png | Bin 7293 -> 8364 bytes src/main/resources/application.yaml | 2 + .../EmailVerificationEmail/html.html.mjml | 12 ++--- .../templates/EmailVerificationEmail/text.txt | 10 ++-- .../UserActivationEmail/html.html.mjml | 16 +++---- .../templates/UserActivationEmail/text.txt | 12 ++--- .../UserConnectionEmail/html.html.mjml | 12 ++--- .../templates/UserConnectionEmail/text.txt | 10 ++-- src/main/resources/templates/_attributes.mjml | 19 -------- src/main/resources/templates/_head.mjml | 33 +++++++++++++ .../resources/templates/_link_end_notice.mjml | 2 +- src/main/resources/templates/_logo.mjml | 2 +- 23 files changed, 136 insertions(+), 87 deletions(-) mode change 120000 => 100644 src/main/resources/META-INF/branding/logo.png create mode 100644 src/main/resources/META-INF/resources/images/logo-maskable.png delete mode 100644 src/main/resources/templates/_attributes.mjml create mode 100644 src/main/resources/templates/_head.mjml diff --git a/.env-example b/.env-example index d62ce1b..9bdf8db 100644 --- a/.env-example +++ b/.env-example @@ -17,6 +17,7 @@ SMALLRYE_JWT_SIGN_KEY=private-key-content APP_URL=https://api.fyreplace.example.org APP_FRONT_URL=https://fyreplace.example.org +APP_WEBSITE_URL=https://www.fyreplace.example.org APP_STORAGE_TYPE=s3 APP_STORAGE_S3_BUCKET=fyreplace diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 065faf6..b3d81d6 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -48,3 +48,4 @@ jobs: SMALLRYE_JWT_SIGN_KEY: ${{ secrets.SMALLRYE_JWT_SIGN_KEY }} APP_URL: https://api.fyreplace.example.org APP_FRONT_URL: https://fyreplace.example.org + APP_WEBSITE_URL: https://www.fyreplace.example.org diff --git a/src/main/java/app/fyreplace/api/data/RandomCode.java b/src/main/java/app/fyreplace/api/data/RandomCode.java index f2ee2bc..ea24ce4 100644 --- a/src/main/java/app/fyreplace/api/data/RandomCode.java +++ b/src/main/java/app/fyreplace/api/data/RandomCode.java @@ -4,12 +4,15 @@ import jakarta.persistence.Entity; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import java.time.Duration; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; @Entity @Table(name = "random_codes") public class RandomCode extends TimestampedEntityBase { + public static Duration lifetime = Duration.ofDays(1); + @ManyToOne(optional = false) @OnDelete(action = OnDeleteAction.CASCADE) public Email email; diff --git a/src/main/java/app/fyreplace/api/data/User.java b/src/main/java/app/fyreplace/api/data/User.java index e4a7967..579e88c 100644 --- a/src/main/java/app/fyreplace/api/data/User.java +++ b/src/main/java/app/fyreplace/api/data/User.java @@ -15,6 +15,7 @@ import jakarta.ws.rs.NotAuthorizedException; import jakarta.ws.rs.core.SecurityContext; import java.awt.Color; +import java.time.Duration; import java.time.Instant; import java.util.Arrays; import java.util.HashSet; @@ -71,6 +72,8 @@ public class User extends SoftDeletableEntityBase implements Reportable { "void", "voids")); + public static Duration lifetime = Duration.ofDays(1); + @Column(length = 50, unique = true) @Schema(required = true) public String username; diff --git a/src/main/java/app/fyreplace/api/emails/EmailBase.java b/src/main/java/app/fyreplace/api/emails/EmailBase.java index 5ab93de..b6890ed 100644 --- a/src/main/java/app/fyreplace/api/emails/EmailBase.java +++ b/src/main/java/app/fyreplace/api/emails/EmailBase.java @@ -9,7 +9,12 @@ import io.quarkus.qute.TemplateInstance; import jakarta.inject.Inject; import jakarta.ws.rs.core.UriBuilder; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.ResourceBundle; import org.eclipse.microprofile.config.inject.ConfigProperty; @@ -17,9 +22,15 @@ public abstract class EmailBase extends Mail { @ConfigProperty(name = "app.url") String appUrl; + @ConfigProperty(name = "app.name") + String appName; + @ConfigProperty(name = "app.front.url") String appFrontUrl; + @ConfigProperty(name = "app.website.url") + String appWebsiteUrl; + @Inject Mailer mailer; @@ -29,7 +40,7 @@ public abstract class EmailBase extends Mail { @Inject LocaleService localeService; - private String code; + private RandomCode code; private Email email; @@ -41,13 +52,16 @@ public abstract class EmailBase extends Mail { public void sendTo(final Email email) { this.email = email; + var formatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss Z"); + var date = getTemplateCommonData().expiration.atZone(ZoneId.systemDefault()); mailer.send(this.setSubject(getResourceBundle().getString("subject")) + .setHeaders(Map.of("Expires", Collections.singletonList(formatter.format(date)))) .setText(textTemplate().render()) .setHtml(htmlTemplate().render()) .setTo(List.of(email.email))); } - protected String getRandomCode() { + protected RandomCode getRandomCode() { if (code != null) { return code; } @@ -56,7 +70,7 @@ protected String getRandomCode() { randomCode.email = email; randomCode.code = randomService.generateCode(); randomCode.persist(); - code = randomCode.toString(); + code = randomCode; return code; } @@ -71,4 +85,28 @@ protected String getLink() { protected ResourceBundle getResourceBundle() { return localeService.getResourceBundle(this.getClass().getSimpleName()); } + + protected TemplateCommonData getTemplateCommonData() { + return new TemplateCommonData(getResourceBundle(), appUrl, appName, appWebsiteUrl, getRandomCode(), getLink()); + } + + public static final class TemplateCommonData { + public final ResourceBundle res; + public final String appUrl; + public final String appName; + public final String websiteUrl; + public final RandomCode code; + public final String link; + public final Instant expiration = Instant.now().plus(RandomCode.lifetime); + + private TemplateCommonData( + ResourceBundle res, String appUrl, String appName, String websiteUrl, RandomCode code, String link) { + this.res = res; + this.appUrl = appUrl; + this.appName = appName; + this.websiteUrl = websiteUrl; + this.code = code; + this.link = link; + } + } } diff --git a/src/main/java/app/fyreplace/api/emails/EmailVerificationEmail.java b/src/main/java/app/fyreplace/api/emails/EmailVerificationEmail.java index e8db744..dd28193 100644 --- a/src/main/java/app/fyreplace/api/emails/EmailVerificationEmail.java +++ b/src/main/java/app/fyreplace/api/emails/EmailVerificationEmail.java @@ -3,7 +3,6 @@ import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.TemplateInstance; import jakarta.enterprise.context.Dependent; -import java.util.ResourceBundle; @Dependent public final class EmailVerificationEmail extends EmailBase { @@ -14,18 +13,18 @@ protected String action() { @Override protected TemplateInstance textTemplate() { - return Templates.text(getResourceBundle(), getRandomCode(), getLink()); + return Templates.text(getTemplateCommonData()); } @Override protected TemplateInstance htmlTemplate() { - return Templates.html(getResourceBundle(), appUrl, getRandomCode(), getLink()); + return Templates.html(getTemplateCommonData()); } @CheckedTemplate public static class Templates { - public static native TemplateInstance text(ResourceBundle res, String code, String link); + public static native TemplateInstance text(TemplateCommonData d); - public static native TemplateInstance html(ResourceBundle res, String appUrl, String code, String link); + public static native TemplateInstance html(TemplateCommonData d); } } diff --git a/src/main/java/app/fyreplace/api/emails/UserActivationEmail.java b/src/main/java/app/fyreplace/api/emails/UserActivationEmail.java index 5a8f63a..e276290 100644 --- a/src/main/java/app/fyreplace/api/emails/UserActivationEmail.java +++ b/src/main/java/app/fyreplace/api/emails/UserActivationEmail.java @@ -3,14 +3,9 @@ import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.TemplateInstance; import jakarta.enterprise.context.Dependent; -import java.util.ResourceBundle; -import org.eclipse.microprofile.config.inject.ConfigProperty; @Dependent public final class UserActivationEmail extends EmailBase { - @ConfigProperty(name = "app.name") - String appName; - @Override protected String action() { return "connect"; @@ -18,19 +13,18 @@ protected String action() { @Override protected TemplateInstance textTemplate() { - return Templates.text(getResourceBundle(), appName, getRandomCode(), getLink()); + return Templates.text(getTemplateCommonData()); } @Override protected TemplateInstance htmlTemplate() { - return Templates.html(getResourceBundle(), appUrl, appName, getRandomCode(), getLink()); + return Templates.html(getTemplateCommonData()); } @CheckedTemplate public static class Templates { - public static native TemplateInstance text(ResourceBundle res, String appName, String code, String link); + public static native TemplateInstance text(TemplateCommonData d); - public static native TemplateInstance html( - ResourceBundle res, String appUrl, String appName, String code, String link); + public static native TemplateInstance html(TemplateCommonData d); } } diff --git a/src/main/java/app/fyreplace/api/emails/UserConnectionEmail.java b/src/main/java/app/fyreplace/api/emails/UserConnectionEmail.java index 86d4130..c9a8ed4 100644 --- a/src/main/java/app/fyreplace/api/emails/UserConnectionEmail.java +++ b/src/main/java/app/fyreplace/api/emails/UserConnectionEmail.java @@ -3,7 +3,6 @@ import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.TemplateInstance; import jakarta.enterprise.context.Dependent; -import java.util.ResourceBundle; @Dependent public final class UserConnectionEmail extends EmailBase { @@ -14,18 +13,18 @@ protected String action() { @Override protected TemplateInstance textTemplate() { - return Templates.text(getResourceBundle(), getRandomCode(), getLink()); + return Templates.text(getTemplateCommonData()); } @Override protected TemplateInstance htmlTemplate() { - return Templates.html(getResourceBundle(), appUrl, getRandomCode(), getLink()); + return Templates.html(getTemplateCommonData()); } @CheckedTemplate public static class Templates { - public static native TemplateInstance text(ResourceBundle res, String code, String link); + public static native TemplateInstance text(TemplateCommonData d); - public static native TemplateInstance html(ResourceBundle res, String appUrl, String code, String link); + public static native TemplateInstance html(TemplateCommonData d); } } diff --git a/src/main/java/app/fyreplace/api/tasks/CleanupTasks.java b/src/main/java/app/fyreplace/api/tasks/CleanupTasks.java index b85a297..1a666a5 100644 --- a/src/main/java/app/fyreplace/api/tasks/CleanupTasks.java +++ b/src/main/java/app/fyreplace/api/tasks/CleanupTasks.java @@ -7,7 +7,6 @@ import io.quarkus.scheduler.Scheduled; import jakarta.enterprise.context.ApplicationScoped; import jakarta.transaction.Transactional; -import java.time.Duration; import java.time.Instant; import java.util.Arrays; import java.util.stream.Collectors; @@ -35,17 +34,13 @@ public void scrubSoftDeletedEntities() { @Scheduled(cron = "0 5 * * * ?") @Transactional public void removeOldInactiveUsers() { - User.delete("active = false and dateCreated < ?1", oneDayAgo()); + User.delete("active = false and dateCreated < ?1", Instant.now().minus(User.lifetime)); } @Scheduled(cron = "0 10 * * * ?") @Transactional public void removeOldRandomCodes() { - RandomCode.delete("dateCreated < ?1", oneDayAgo()); - } - - private Instant oneDayAgo() { - return Instant.now().minus(Duration.ofDays(1)); + RandomCode.delete("dateCreated < ?1", Instant.now().minus(RandomCode.lifetime)); } private String scrubConditions(final String... fields) { diff --git a/src/main/resources/META-INF/branding/logo.png b/src/main/resources/META-INF/branding/logo.png deleted file mode 120000 index 308db3f..0000000 --- a/src/main/resources/META-INF/branding/logo.png +++ /dev/null @@ -1 +0,0 @@ -../resources/images/logo.png \ No newline at end of file diff --git a/src/main/resources/META-INF/branding/logo.png b/src/main/resources/META-INF/branding/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..eb1ee675df0dc5e1f0b8c95648d46e51bb6223b7 GIT binary patch literal 8364 zcmbt)XIPV4m-Y@Ih*DGz0v1r|Ql*HLQ0xg!gNn3(h2E6jiE;!*iY7oP(ximYJ5mBD z3epi0I%1)B0s$!@^ElV}-nr&`XZ$lyeyr@e?zQ%M*1q>nUfeLa#(Ch_0RR9e;`(J2 z08G$VCa|9s66-$XZAh@**S&TbFn)j1NCX@Jr|uyx>)i4gUL5m%XJH*Ex3XN@t%|_> zt@G&YZ5`4n(`#>XedJzaF0!6_WXH>+{W#~=W01~8ny!pH&3yKv^IrldaSG4R9Jpuq za>KOTK;ojS)ZNs|r#y!~ymH_0XPolPxrKeS@zrsezNOKca{BdDot!M!pHSZ$niO=jXDAmV0L|P}M!bOdYY6+BKnF^9WD&@_?jplBybh5q1B1G>zQ~*q~_z#xW2;LtSL#vkW8z~@s=Np*p8vWK1G$N zPu?yIv$fb!EXviOa}(?4UP_C!p55o2mkVC`b$-pLroHa&xSotRofh?NZ5B)9suJHq zBCBPMtevCou0=D!PR>tKehQn62O+Oge~>$LOUIkvKUT{(q)x_^p05ay++0|RrEYV( z-tbiG?-tUUcwujgcGy5tdqva@`?TN*Sl8_23PgW4e7eSsT)lY!H1*!);VIBqRhL5! z>tgHfKk!=!H8WZxl8MnV!g5=-_zQi#-P3~9@)&7E>tTdoUlwE05Vz~uTazYj;htR9 zSiVkeIm&Qxm|r1p&qaFw8hd1j?G~**6}{6s*;Lu9tOQF7a{IFUuxT&}E1l4+RDC53 zOK#Aoe%Dwc0|8$&qa%&Y@#*{%_zx1$c2W(;-@@vAE|MlEsU$zmB8agG?8_vrgAf!cw<(@22Y9+j&l25d?g>XCDF%Z!HWwU%IP1OzWKd{8HBaC<5#E{(ZMz zEe(%AlWoKH)FbVKt)tfKoeeObDQvEbL#Tx_uW zQ(c@UhpWkHXt*xhN)CO{<|s>7vGu&}?@RemP)3Qs8M*6WUu*82wbZYr4ZD#OUH1ch z$5MB0i$oOTZ0C+TBd+FLR^VL|zg5oASErB5cf&nWW422?mWg`?weZD_UN`b5%ST>e z`S@e%Bn# z+zzpj-fW51a)*8q6>NK?z?d#;5*jUwkZ_vFKY$-x7O*lHnj!ahHit80=`o-Vrqg_e|#u92rQCz zxyRInX;`xT$XvN)SARt{!SjYMJyD%QnOPy|jaI#*!_G^@UdyXM+A<*-&$+#vAGXjH z?A1T;5;a$UX@fG5MM0a#nFnNno>kb|H+*)bX6o!nS;P45cuc{xZ~CW3y**1Lb?|$> z_EuH7xTSei&BW0$d4OZU7onF(K2!9a%3^7->ReF~FHoaAoIXj7AaAQ6Dut-?&d!yB z36@-D?hWY#L$;2x7NLaKtqr;|wcNgvdzR<*mTsf{XQMr?q;C9Y5}b;b)2C8T7kx~& z$5&5M_~;KBPCOphus839&mf4jmtqW&*1AzePlM!mJIO0TTs%S+O5z0@lfUb6is!eh zYy1K#QS0~Q24Xti(S@OE$&u_>Ir19U*8(k#XXrq@ljh<_k&Q}?DRIR6V)}5M`(L|rzo{30yUunm^NjuVTuS##s z|Bj_zL8oi!Cs$Z15SD^x~$jXk1281B~3lSHa{=VO^{ykK*vCgD@sqw2Tk<)fqQUtX@JCTT# zqOkL-3d!c(f>kF5KG!lrQc_K$ZoSa=@_;XY8#fMR-Jou7pbKwscY_H7N?A>yC!g~w>=N75@)(w?%ZCxW#LKt#qrGU zIkDgWh`+Mace_Uq;f||3ESNB-(v4IRfj)UNCmp!|s(Va2tT`y)GFDvT<57R}etp40 z4$k=cF4Y+mNy;8C_XDNRc2PVa$PpK*B}Vj$^&89sl&KS>rQf@#q;D8fW7I`I>asi-}znaXdSCKoL9!V#iN}t zkDPx_Ag?u(Fxh#`HjXYww9Jx%G7`RQFeSQ#4wZ2Sm}ABBE{iw6*wAEZlq21`=JENK z*}FMe+R|V|W~D`!dGlmfXPtY}V}4S7cDH4z{*j#lZ&`<9O0~oo5^ws0jA!h`Jd!!h zZTiE)!TH>0W@fPXnjPZmM-TC$AGSF<_`+GrPep7^7}BG|Xg+s9K&nOs0XI6VW^=W< zIpqu}pLg*~&+w`e(#DUD1>MKqd^Qtb(($WRwyoz`HRH*-CWE+>B$avk=)ex>Ia@pb zcT^&dTP`RPP8qb!I$=(oa*Ip+N- z{@erpse0Dk18Ev)agV#Tq?czrem>op8}H)g`_ev^@Mhg@I;nbE*+m=Vj5z!ql{%>! z-oraLy0v;SvevE7%6#c*%D}r#ZfQz~ua&s4C`kfcR(xiU6`Fm-oYpken z$Xw~Md#ApbdZboyv6q~NR~NGJ|Jts~r0Sx{6QJ=>vxX=DFaQogR2vff`}GI+KQ|~#z2;pW+vt%kv7V`O?Tk(f6utj~j%^53#-^Gp5z08T-0{>we` zV1eZV$3jZ~NoP@cbdK^Js1;hq{}IGs0O#QT&n`U4MI}c(in_wbOuQ=Q!Yef^|7fSO zmGCKXok)tnv8kR_CNGHmKl}%L@n=JQvO4|!z_UHr$b5x z>sWkJX~7wfH|Qm#+;K&yRCg*ZXXKvoc`sQquN_mf|T~W7?uAu*<8!JOhn3KG{b++)8}=0b)X(c8U;OvV9__jxcHN{8n6)(mj@zjnm(i3dY(X8 zi4D-%t?XnoQ@r2cdJ0%x^?x1}Nxc6&$@?*gsJe5FIg(C9^>zYTm5Ly&4x+MwG1G|P&+ za`rSd8vo0SIM0{Us}(15};Mbd=BU2OQ-n^wI$6dpHt2DA(F`;ESUAblB!n21nnatgoA;E!oZE49Io6 zD53VxpUuek)R3xFt}8PoUU2l`?2M=kS6!YU-cX?3Vq2)vkDFdjAl2=oi&cg2 z$}8~&!Yk{S(-wK}A4FE&|GtJ@C^`~ZQe0@h?qogKTg#ZU(T2_tY}@NIYtPHY8Ka`b z35#lwfpfD&Nxz(Pso3GYJhm>*Bx{Fth~N~3`K^~QX>&IX9G|y8m{N2o>0Z|WI%(qB zfiD-jyWh-5-wN%l-qHG2EjeiyTg=p0oWdZwWvdhPPskgp`LvL?(Xn=O>}T8O!k?*2 zeiVywxc@fRYrezhMJ$wurD)X&^oj?(_jx)7A6-4dqvyUDPu`Sj5{qA}s1hyN76;k} z1?_yh^$NRj>e;aeEmJ=h26uYF#=R<-`>Wna}V zvU>E>-Z`nofYOwj&#oiE6~7O$0#ehW)_fFg@U1~jqO6FKrm#DWQPmXAP#h2$vQXaE zYZVkArV7#>Wq^b=)x;;6% zS}be$rw|NduM6we9Ud5%T$|?Zure{43*Xn0eY-veE3VdZ>E|kfJ^~reUcuEh-aKE* zd^8ijMH7+p3Lh%>uSKgYQn!Lwdu)H|A-g_DY}B?V$u*R@HhgTA8yC$GwiQ!tlsq|g zxHv!-HyQ8t!#235iWJaRx2T}7MnXir2%~T3(W4sj3DfNdK$6fg$FDIvrYev_`g?a_X(KWoo~A}E zu~n9PG@Ix(Nt2+)-I)IH%;BMEwl$JS%jCOuO-tl4xGaTtZBwsQL)ks~ycD1x2XX9x z`G>CLqLxgHWsqkEYuk$#cPh8$cF7z5f<93&!BWTi*z!9z_ySEdmzg3ZM_Ao@?`7}V zU@UoS=@;86P2@`XbvvPwMVT1ygQanHI63Ci{B_15{es6wX)H z)X}i1oB4Rd@nD-J0h4%CG@`Vs?+mn|D~)vsZv(GLaTv62*go(qFoH#oUs-5RrR+F zb{D*}R}C011X}Avpahik(0?*J1iCxiB$_0>&hM<54kYR6l@p5a?Px^cV?iHnns=-j zWDCEvG9v}$qBpEUh)F^XtB4ki^vZhMI5pLbYPnmee($ZF+u<74TvQXs;mQ zKT&fol0z~T8lv3Ni%Bc9S*ckQ&6!_gys9M&+}x2{_p*oQu^Sc%=iDuP=S8)O2(pPd zfz8gY(2^m0igTZAw83z#_>heKVw~yb_0|WmM!zWolp!T{0F4b19@{KGz)7!AlE7}flJxb?v~7VKsXc~8@_mRJO96}i9ZnzQ zP81m;>->VE-lnjeYJG7a|B(ILH70G-Do2P9AZs@QmdYSS7M4H)p1O0anCV6Vw3-j4 zz+upv%Sh*ev>K(rKHuGXaurzRbR4U$7zpcmK0Z$IUaFF9J+H=uIsM33bvE3&0BAIx zg%JPf7d!FMP8fO9-4V6f!vi?#h9f`^#5a5Q_)fwaZ%Ay_gB9KJzQcf{<$!keP4LTb zaWqF5@N+FL9{>$GSGEd6ft5r|$+FT9`9xIow)oDTQY)7lGvQJIdhz-qqX;>&pxo`gNkw8R69{2{>Mj{GMXy*R+ci;Kp8cKo#|h1CIW&V3U!| z)`rJ0tDmO>?+}3e4F_50QKyl2XF6*br6R{Y4g%);mu$uuS76^x$GmV1PEndC90A3X zJZ6Ps7pj!o?1W!ZWK$7DIf^n^CwY;|UxZN{Y)Ys^5nK%`MpGr>OU+d*$FeO!U+cq{=7C53FST&Afw5N;n@WFyK>4j;akGY*(uwfUam&po+fjjB zhT*CEn0nZ7cU1GG8{G8!xXUPfpLX|zHDf1iEcR(Pjm0~o1MX>^u(M>rl-hLOLVe9E z0(Ki?!E`O_`)_4{9-+W;cU8oaVgZ!aT$C+asd;xc+rMw<0|_3U#h8tf5;*`mJ$dyJyTl;ow9^LCQiZ?jK@aE10_}-NaM3iFS+uaP$1X3gr0;30Yu+}D-&`+W<3`xb zY6HJ-DKQJxt0d(>0;TA-9HEfzg^U^q2J2G><@FVo;*5GU?AH}~bgDN3%j|guj3ouQ z-83oLconpFprBrhJ%gr<*t@O5)RQ&Dh>Pgbs2Oj#GR}r^qu$m+w7nJac0>ZTfawNV zWwtmY*_{N?^Y@|R-8R?EhxXXYC6=@YeZkbk(jU35oFDM^oNXqO?fvMOiQsudpf-_g z{b`sILbYy7OHPIsz&N|VYSsv3uYP4jl-^%VVx5($U|kZCsinS)q23%j(5R%URko?- z)l*NNNefKJl51$8Di6m7y%6wQ<4h~^`K<-&bv5}3{O@c#OhbGs+dfv{sy%nK^g4(9 z_%Jd?T5&_TB5B8dJCwocb;A zkF;Nbsj;S7xQW&66BtW_L$tV<6Bi_scAdie<&(!tAAv1c@KguKZ5c271jpRt z#Te;kuwOI^##h&9EnURSF$k0H;p(7Tl3zz(B!mKyfvEHIC%n zdfm|)bu^E)5g5>0p9#pg{*H$yZu>#Q(CL=WKpltqauGQcC+11~Y<;5XIQkBnUKM^C zXx!41R(1I*g_=(l8?v|pwtKye)@)T5b19QgctOw=U3NhwUJA5k;tJEEhWf;psI}-^ zrXH25$10g|!qkv!8X0fACIR!q#)#Cz-mdAkL%aZ?2|#YusFuOxHZe4?Z( z&K?APs;*s~JBNt-Sor~vysn|TyRXB`q-&?dCJ%v{;q2o{XZvnN#xq`<4!&?%-zMLknRqEWmU8EOXE?>0)d^X}F~RnNtf$TML58vk`< z)X1{Ey&*}dbyUP(bO>P$1U<-gaLIwJ{ydzYzVEp5_)Q*|)vG)Ey%2e{x)^F5Swlq> zTI;B@%p3_qgef!0EeB*_H<@hQzEecHV#0#?eqdkA;$}`cAtf#mj-ryu3s|fLR^xQ5 z6ED*?xkjWadnzu5N*5W|pW7_G*UUK?f*&u`U;+&?Cr*@gMcqJ5_c9 z0I>VyFAiq_KpcD(2V}kv@hT_E!9m*3#mNB>eSeB5q(=atnZbON8omeeq>4WL4=zU<%6xvFLzeLdRhK!6p0 zvHDK=g3O`8w`T^9+<;aa@08897ys>t>4F8gv)q&2O@AGmU1JDaP@>s5+p1$tONd#U z*-!L0yP=Z6M$}_~f`)pFA>YOm6LvtWCqqitZ-({6MqA5`z?AJp z7)T%DFXCnk@P)R@-sV&Yhov3UW>R@CrzeSRw+Cr ze=#FE_`A5V25m3vxJwLn5d6lME>k$)jO`aGad#2cQ;WG%8HUu_E?1xP|f264W67Y6f6%3DpswxdEmjpUUK2Azt@L%Zh1 zMdOq|n{=FDa8HeHdZL%g6(w9PRrEE1=v+%g#kzf`H*@-Z*e951lN&q#R101!jnA;; ze2yK=Ev|a86uvO7I=P}<;$XJJN%aCwcz~8c$Tl7dXMek^!q-Wd~6ic zm}p^?I^{+Pwo~_*H50W{Abzx*kt!4Q#Mc`;3Vj(O3crd~IX z#d;$1Vo~!WDg@E%bEF9w8T(AsD!qO#zc;X2;9x?)zgS^CWjG0fYx<#7Z#yLjsZQYF z-`18kOK)7FMtXZERb+jg!It;IGq^aO9Bz*3vbS(K%+`aG-N)<{eOC;siYG`%g z;G?}QCgx*zgSazx_-&m~E1$LcSlS=lj|=4B_}uQ11bqE`o!_T>3)YkG&XI$+0^|Qe zPp_Pu_@EYpxwPDJvoLDoXY^3bf^AS$)EYh4RuS^NX7!oSgOx*YSgWrss7~un@_5zw zY)so{W${A7^(N$pgR=InTBp)G#>X=}lahKHhH3A#d`v%G%Wg4z-l-qK3|g5oYxS9E zb_&~mV7b*L)y)W`D#2m&Hf|t7tkg*}iM3`#Rc_-rp@8{gfL%z3K?sKfGeooE z$UK2xfSnB@5&Z>T{mp0mgen7`-D+1%t+Z6b0cZWcl;vV6qSrhmK9cEm<4wdWk9KeA zNb{AhCja^McdGZlH2dPYk6h$0y#6t3WB#Zg=DMdWwWk^{`ZPt5KOQ{Ug}eJ?!L+d zSGJ;}BD(KOuQeHD9jnUB7VH^Y|BL+f&pypmh2*1A8EO3}Ukr66s{sD!T9&%8B5q9=%G0s&zIW^%R~OfA%9> zh^P))dD*f*aIvpXPX75!%0+olsd@ucI@MJuT^i+GLe~?*_n62#0?xTVl6bf8{9zMc zirw8Td?`F;T{Cp$K>i>_?3>A860J`mxsUD{DK;k;osotXf=((faJlAN+%5W$I+uTe z{qx#?l`z*S#E6)3i~D7Kt zP^qyEcy{CZ)_|2rR|g3S|IV1s zXUe^U9vG?y2xHVBqLW(ZKpYY3t|N9?0nE$TiyeY5pozX_Fa-s-GrFHpp7CUko*R#` z%$#GVUu?@%=gQ4g>QN>h9?gs4aaUUkk>KtoP-sWCl;KdX^7mhd^)SO4VMTwe&h@Ji zYM+6KU5?Nl8dy~>_DAAdN}LIn&st8RJ5Lgwn(a#zU3v@=w=uH79ZsRwFrg|FnvF7& zEHludOi1m|-wSz};G1J51EfUF)DWRI;hzPTi@ZL~`xEOX{+qY^Gz(&8%)s57iA_A5 zF(kxAUy6<7dw<0suDj#CUN&4lhxa^u8X`9F<d%sJ!LGzWltODqY=RW4g zUgcosxmbxo2!QJ5-9vv+I=mz5CXLv7k)0BSv|YU~vaz2L+VHkqCq_FxKPa=E z`vAZ12Ea0=x>GHZY%rT%NySpSQ(c8PvQXddbQzvQsN24%7I~usIe;*z*!b4IUMe}L z0NfASeo(*YQBI)=zUs0*6KXU~7PwXLE^SKfJ<=0%yQGEwh4x(~>3~rJ_{|JkRb9id zZQ>sk$kZwp25luJ8k*KP1$bWCR7DS_6vy~g?HNlwmklaH`uCe|wd0bAb`$Sq1`!6R zttPUc>nLxQJB`eVc;l#f-e>=8t3Dks$WD~5n$EjWf3$_)P|tY|g9W}53s8eVTZhxP z)oQ<=DjqW*GE&0yDHH9xc2OI{+d{Drg6NwO;bS0GHlXd;@Zrsldp$PlF#?ZKVW#El zB_o1S;_>dDhUBkSbkg}Afjos^QPOn1JD-Mw3op9_2i)iDppH3AV@R*_ZR;EhndVjmU-d^n= zY?FYr;olkg^I!oJW?e^H%qL^Rc|6)k%M&BS^X{}DIlt`>PskC~!qK%|MMrvozzWCC z3)2Rs@B1xuvhpA<-8z&zQI6|!seqcnzZLd>0ZdV~#Fn#R@=yHuZ%{3OxWMqhi0%6` zJccdx2?XPKspMAzzDEx5cqTr%nvaooe*%Ej^;u1L_&*Bb_FhbhBd&lmqo$lEe&kou z4(t^Weyeg_K%2F3f7=WlKl9yY*PT`n3%k~Upg3vw|5~&W>VtD3K;JU2&Q$D3=!zy0 zYOg3kjyu8L{e68f?p4T4b+OR8=tsdt>5$}|Azr>1vFfqs@Jv)PFmSoQu}`jOL9$3( z++C~(_+?I3>Vw<1V#PDtXR#jTz`&7!M|4Kzx4*&!gaVPWO_p-u6l_|_V20qcvA zGz7VyHt%-ag5@s?&V(cu^JGvCz}FRg>*`Y@$6QJii;vYq+|J&cycvSE3foEmC!h5d zaYneO*ow`MfwOFHk=mYdZ;YhrU-!5C7w|-<2nBI>;;mcjecFCKk;Ircii|3g?FERj zkvnF`j)OqW=S_!%RYMm-vL-*XhYFrK)!)9AwWqbLeYvnj>(iW&8Qoa2-8g3=R?5&X z#HWNABE;o8_OvQnBsTJ779dlCZ=cu@%8B&`eatsD94+o7OD!6qtRu&_ky28nOWAY8urI6tmxk@Uk& z8%SB@5rmX<9X}o25#Yl+c1Gq+yK#g@*mn9zwe3)|ZezSlfcxiO`;;;axCvMa? zy830EyT)2xYZE9DNpDunZ52fPwAjtwK5JOPKehACH!~IVSI*QZr?`y7+joP}q}#GR zOy)ZpZs8onjecKs!}X6VkS;0vH@YAX77Djm1(np$Lc#(vyWriGy~)3Yvv`&qJb9Tl zH>ZcVrzS1UdK*q1wuScCpfEeZt0%|w?HA-5^0(UBRl{lz4U#`uyF z6AY%LL=&@FJZXNgr2WmU5sId~TIyI2?Us-{+W=ApV`_{#YOOvwG%|#-Hy57f!*JO{ yEVq$p4;VJDgG#;YngII#ZFK)X`}`LoDfSELu?(f3`oIfU;N;J44i!ILzWpB{h7;BR literal 0 HcmV?d00001 diff --git a/src/main/resources/META-INF/resources/images/logo.png b/src/main/resources/META-INF/resources/images/logo.png index f339748c3a4e1b08e19128a4aa3b6770a12f3ae8..eb1ee675df0dc5e1f0b8c95648d46e51bb6223b7 100644 GIT binary patch literal 8364 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 WZ^!8avq$HNf$u@@mfo>``M&@KkE)LV diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 4f3b099..0aca24a 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -44,6 +44,8 @@ app: use-example-data: false front: url: "" + website: + url: "" paging: size: 12 storage: diff --git a/src/main/resources/templates/EmailVerificationEmail/html.html.mjml b/src/main/resources/templates/EmailVerificationEmail/html.html.mjml index bd929a5..7e1fff9 100644 --- a/src/main/resources/templates/EmailVerificationEmail/html.html.mjml +++ b/src/main/resources/templates/EmailVerificationEmail/html.html.mjml @@ -1,14 +1,14 @@ - + - {res.getString("codeDescription")} - {code} - {res.getString("linkDescription")} - - {res.getString("button")} + {d.res.getString("codeDescription")} + {d.code.code} + {d.res.getString("linkDescription")} + + {d.res.getString("button")} diff --git a/src/main/resources/templates/EmailVerificationEmail/text.txt b/src/main/resources/templates/EmailVerificationEmail/text.txt index 46f6d33..f18f4a2 100644 --- a/src/main/resources/templates/EmailVerificationEmail/text.txt +++ b/src/main/resources/templates/EmailVerificationEmail/text.txt @@ -1,7 +1,7 @@ -{res.getString("codeDescription")} -{code} +{d.res.getString("codeDescription")} +{d.code.code} -{res.getString("linkDescription")} -{link} +{d.res.getString("linkDescription")} +{d.link} -{res.getString("linkEndNotice")} +{d.res.getString("linkEndNotice")} diff --git a/src/main/resources/templates/UserActivationEmail/html.html.mjml b/src/main/resources/templates/UserActivationEmail/html.html.mjml index f75d75f..2d5a9f2 100644 --- a/src/main/resources/templates/UserActivationEmail/html.html.mjml +++ b/src/main/resources/templates/UserActivationEmail/html.html.mjml @@ -1,17 +1,17 @@ - + - {res.getString("title").replace("$1", - appName)} + {d.res.getString("title").replace("$1", + d.appName)} - {res.getString("codeDescription")} - {code} - {res.getString("linkDescription")} - - {res.getString("button")} + {d.res.getString("codeDescription")} + {d.code.code} + {d.res.getString("linkDescription")} + + {d.res.getString("button")} diff --git a/src/main/resources/templates/UserActivationEmail/text.txt b/src/main/resources/templates/UserActivationEmail/text.txt index 47f8c36..d9cbe52 100644 --- a/src/main/resources/templates/UserActivationEmail/text.txt +++ b/src/main/resources/templates/UserActivationEmail/text.txt @@ -1,9 +1,9 @@ -{res.getString("title").replace("$1", appName)} +{d.res.getString("title").replace("$1", d.appName)} -{res.getString("codeDescription")} -{code} +{d.res.getString("codeDescription")} +{d.code.code} -{res.getString("linkDescription")} -{link} +{d.res.getString("linkDescription")} +{d.link} -{res.getString("linkEndNotice")} +{d.res.getString("linkEndNotice")} diff --git a/src/main/resources/templates/UserConnectionEmail/html.html.mjml b/src/main/resources/templates/UserConnectionEmail/html.html.mjml index bd929a5..7e1fff9 100644 --- a/src/main/resources/templates/UserConnectionEmail/html.html.mjml +++ b/src/main/resources/templates/UserConnectionEmail/html.html.mjml @@ -1,14 +1,14 @@ - + - {res.getString("codeDescription")} - {code} - {res.getString("linkDescription")} - - {res.getString("button")} + {d.res.getString("codeDescription")} + {d.code.code} + {d.res.getString("linkDescription")} + + {d.res.getString("button")} diff --git a/src/main/resources/templates/UserConnectionEmail/text.txt b/src/main/resources/templates/UserConnectionEmail/text.txt index 46f6d33..f18f4a2 100644 --- a/src/main/resources/templates/UserConnectionEmail/text.txt +++ b/src/main/resources/templates/UserConnectionEmail/text.txt @@ -1,7 +1,7 @@ -{res.getString("codeDescription")} -{code} +{d.res.getString("codeDescription")} +{d.code.code} -{res.getString("linkDescription")} -{link} +{d.res.getString("linkDescription")} +{d.link} -{res.getString("linkEndNotice")} +{d.res.getString("linkEndNotice")} diff --git a/src/main/resources/templates/_attributes.mjml b/src/main/resources/templates/_attributes.mjml deleted file mode 100644 index 9407127..0000000 --- a/src/main/resources/templates/_attributes.mjml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - diff --git a/src/main/resources/templates/_head.mjml b/src/main/resources/templates/_head.mjml new file mode 100644 index 0000000..6e270aa --- /dev/null +++ b/src/main/resources/templates/_head.mjml @@ -0,0 +1,33 @@ + + + + + + + + + diff --git a/src/main/resources/templates/_link_end_notice.mjml b/src/main/resources/templates/_link_end_notice.mjml index 18542f3..8eb5fd6 100644 --- a/src/main/resources/templates/_link_end_notice.mjml +++ b/src/main/resources/templates/_link_end_notice.mjml @@ -1 +1 @@ -{res.getString("linkEndNotice")} +{d.res.getString("linkEndNotice")} diff --git a/src/main/resources/templates/_logo.mjml b/src/main/resources/templates/_logo.mjml index 59407d4..0cb5194 100644 --- a/src/main/resources/templates/_logo.mjml +++ b/src/main/resources/templates/_logo.mjml @@ -1,2 +1,2 @@ - + From ac8123da9a9e4f6cb176f2132ccb9591f2193d83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 5 Jul 2024 12:56:05 +0200 Subject: [PATCH 148/157] Build container --- .github/workflows/publishing.yml | 116 +++++++++++++++++++++++++++++++ .github/workflows/validation.yml | 51 -------------- 2 files changed, 116 insertions(+), 51 deletions(-) create mode 100644 .github/workflows/publishing.yml delete mode 100644 .github/workflows/validation.yml diff --git a/.github/workflows/publishing.yml b/.github/workflows/publishing.yml new file mode 100644 index 0000000..4bd56be --- /dev/null +++ b/.github/workflows/publishing.yml @@ -0,0 +1,116 @@ +name: Publishing + +on: + push: + branches: + - develop + - release/* + - hotfix/* + tags: + - v*.*.* + +env: + REGISTRY: ghcr.io + +jobs: + formatting: + name: Check formatting + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + cache: gradle + + - name: Run Spotless + run: ./gradlew --no-daemon spotlessCheck + + test: + name: Test + runs-on: ubuntu-latest + environment: test + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + cache: gradle + + - name: Build emails + run: make emails + + - name: Run tests + run: ./gradlew --no-daemon test + env: + MP_JWT_VERIFY_PUBLICKEY: ${{ vars.MP_JWT_VERIFY_PUBLICKEY }} + SMALLRYE_JWT_SIGN_KEY: ${{ secrets.SMALLRYE_JWT_SIGN_KEY }} + APP_URL: https://api.fyreplace.example.org + APP_FRONT_URL: https://fyreplace.example.org + APP_WEBSITE_URL: https://www.fyreplace.example.org + + build: + name: Build + needs: test + runs-on: ubuntu-latest + environment: container + permissions: + contents: read + packages: write + attestations: write + id-token: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: true + + - name: Login to registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ github.repository }} + tags: | + type=sha + type=ref,event=branch + type=edge,branch=develop + type=semver,pattern={{version}} + + - name: Setup Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push Docker image + id: push + uses: docker/build-push-action@v6 + with: + build-args: | + APP_STORAGE_TYPE=s3 + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v1 + with: + subject-name: ${{ env.REGISTRY }}/${{ github.repository}} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml deleted file mode 100644 index b3d81d6..0000000 --- a/.github/workflows/validation.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Validation - -on: - push: - branches: - - develop - -jobs: - formatting: - name: Check formatting - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: '21' - cache: gradle - - - name: Run Spotless - run: ./gradlew --no-daemon spotlessCheck - - tests: - name: Run tests - runs-on: ubuntu-latest - environment: test - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: '21' - cache: gradle - - - name: Build emails - run: make emails - - - name: Run tests - run: ./gradlew --no-daemon test - env: - MP_JWT_VERIFY_PUBLICKEY: ${{ vars.MP_JWT_VERIFY_PUBLICKEY }} - SMALLRYE_JWT_SIGN_KEY: ${{ secrets.SMALLRYE_JWT_SIGN_KEY }} - APP_URL: https://api.fyreplace.example.org - APP_FRONT_URL: https://fyreplace.example.org - APP_WEBSITE_URL: https://www.fyreplace.example.org From 19d3fba35f1abc7479cfa331857380bacc4cb644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Mon, 8 Jul 2024 11:32:10 +0200 Subject: [PATCH 149/157] Update dependencies --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 706e3a5..975ed2b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ org.gradle.jvmargs=-Xmx1024M -quarkusVersion=3.12.0 -sentryVersion=7.10.0 +quarkusVersion=3.12.1 +sentryVersion=7.11.0 tikaVersion=2.9.2 gitPluginVersion=3.1.0 spotlessPluginVersion=6.25.0 From c2a81fc05ea55a89b453f20a5f4dadbfb5e35642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Mon, 15 Jul 2024 11:47:20 +0200 Subject: [PATCH 150/157] Modernize Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f3b3dd7..fe9aad6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,4 +33,4 @@ COPY --from=build-code --chown=nobody /app/build/quarkus-app/quarkus/ /deploymen EXPOSE 8080 USER nobody -CMD java -jar /deployments/quarkus-run.jar +CMD ["java", "-jar", "/deployments/quarkus-run.jar"] From a21ebb4ebdfd7637b0a2a83eaab8c4e84de2ca17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Mon, 15 Jul 2024 12:08:40 +0200 Subject: [PATCH 151/157] Update dependencies --- .gitattributes | 3 ++- build.gradle | 4 ++-- gradle.properties | 4 ++-- gradle/wrapper/gradle-wrapper.jar | Bin 43453 -> 43504 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 5 ++++- gradlew.bat | 2 ++ quarkus-sentry/build.gradle | 2 +- settings.gradle | 4 ++-- 9 files changed, 16 insertions(+), 10 deletions(-) diff --git a/.gitattributes b/.gitattributes index eb5b2ee..4a915cf 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ * text=auto -*.bat eol=crlf +gradlew eol=lf +gradlew.bat eol=crlf diff --git a/build.gradle b/build.gradle index cb8fb38..d44804c 100644 --- a/build.gradle +++ b/build.gradle @@ -15,8 +15,8 @@ repositories { } dependencies { - implementation(enforcedPlatform("io.quarkus.platform:quarkus-bom:${quarkusVersion}")) - implementation(enforcedPlatform("io.quarkus.platform:quarkus-amazon-services-bom:${quarkusVersion}")) + implementation(enforcedPlatform("io.quarkus.platform:quarkus-bom:${quarkusPlatformVersion}")) + implementation(enforcedPlatform("io.quarkus.platform:quarkus-amazon-services-bom:${quarkusPlatformVersion}")) implementation(enforcedPlatform("io.sentry:sentry-bom:${sentryVersion}")) implementation(enforcedPlatform("org.apache.tika:tika-bom:${tikaVersion}")) implementation("io.quarkus:quarkus-arc") diff --git a/gradle.properties b/gradle.properties index 975ed2b..eeb2297 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ org.gradle.jvmargs=-Xmx1024M - -quarkusVersion=3.12.1 +quarkusPlatformVersion=3.12.2 +quarkusPluginVersion=3.12.2 sentryVersion=7.11.0 tikaVersion=2.9.2 gitPluginVersion=3.1.0 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e6441136f3d4ba8a0da8d277868979cfbc8ad796..2c3521197d7c4586c843d1d3e9090525f1898cde 100644 GIT binary patch delta 8703 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 Ojj7^59?Sdh$^Qpp*~F>< delta 8662 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a441313..09523c0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index b740cf1..f5feea6 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 25da30d..9d21a21 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## diff --git a/quarkus-sentry/build.gradle b/quarkus-sentry/build.gradle index 6c0ddcf..ddec448 100644 --- a/quarkus-sentry/build.gradle +++ b/quarkus-sentry/build.gradle @@ -7,7 +7,7 @@ subprojects { group = "app.fyreplace" version = gitVersion() dependencies { - implementation(enforcedPlatform("io.quarkus.platform:quarkus-bom:${quarkusVersion}")) + implementation(enforcedPlatform("io.quarkus.platform:quarkus-bom:${quarkusPlatformVersion}")) implementation(enforcedPlatform("io.sentry:sentry-bom:${sentryVersion}")) implementation("io.quarkus:quarkus-opentelemetry") implementation("io.sentry:sentry-opentelemetry-core") diff --git a/settings.gradle b/settings.gradle index 23e1939..b3a0511 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,8 +6,8 @@ pluginManagement { } plugins { - id "io.quarkus" version "${quarkusVersion}" - id "io.quarkus.extension" version "${quarkusVersion}" + id "io.quarkus" version "${quarkusPluginVersion}" + id "io.quarkus.extension" version "${quarkusPluginVersion}" } } From 51ed9c12fbc26c458ceaac74ab12832359fe8109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Tue, 16 Jul 2024 17:34:52 +0200 Subject: [PATCH 152/157] Remove useless .dockerignore --- .dockerignore | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 23ace83..0000000 --- a/.dockerignore +++ /dev/null @@ -1,12 +0,0 @@ -* -!.git -!.gitignore -!gradle -!gradlew -!gradlew.bat -!gradle.properties -!build.gradle -!settings.gradle -!quarkus-sentry -!src -!Makefile From f67944058a15c5f58d0e79e0335bb51d9ef4482d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Tue, 16 Jul 2024 18:02:28 +0200 Subject: [PATCH 153/157] Update dependencies --- .github/workflows/scanning.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/scanning.yml b/.github/workflows/scanning.yml index fc66f28..832270a 100644 --- a/.github/workflows/scanning.yml +++ b/.github/workflows/scanning.yml @@ -17,10 +17,10 @@ jobs: uses: actions/checkout@v4 - name: Set up CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 - name: Run autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 - name: Run analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From 18902e3c34c481f6bc3223a7844921542f8477c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Tue, 16 Jul 2024 18:07:26 +0200 Subject: [PATCH 154/157] Add email configuration example --- .env-example | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.env-example b/.env-example index 9bdf8db..30175eb 100644 --- a/.env-example +++ b/.env-example @@ -8,6 +8,11 @@ QUARKUS_S3_AWS_CREDENTIALS_TYPE=static QUARKUS_S3_AWS_CREDENTIALS_STATIC_PROVIDER_ACCESS_KEY_ID=key-id QUARKUS_S3_AWS_CREDENTIALS_STATIC_PROVIDER_SECRET_ACCESS_KEY=secret-key +QUARKUS_MAILER_HOST=smtp.example.org +QUARKUS_MAILER_FROM=noreply@example.org +QUARKUS_MAILER_USERNAME=postmaster@example.org +QUARKUS_MAILER_PASSWORD=password + QUARKUS_SENTRY_DSN=https://sentry.example.org QUARKUS_SENTRY_ENVIRONMENT=local QUARKUS_SENTRY_TRACES_SAMPLE_RATE=1.0 From d4d3032abdace907da5012320e8274ac5e6d9712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Tue, 16 Jul 2024 18:46:11 +0200 Subject: [PATCH 155/157] Add badge to README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b9ceab7..be53a1e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Fyreplace API +[![Shipping](https://github.com/fyreplace/fyreplace-api-quarkus/actions/workflows/publishing.yml/badge.svg)](https://github.com/fyreplace/fyreplace-api-quarkus/actions/workflows/publishing.yml) + Future API for [Fyreplace](https://fyreplace.net). ## License From 3feeaf565543c1a928bc412bd6294b2388eff22d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Tue, 16 Jul 2024 20:41:07 +0200 Subject: [PATCH 156/157] Try building native image again --- Dockerfile | 18 ++++++------------ build.gradle | 2 -- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/Dockerfile b/Dockerfile index fe9aad6..1dcee24 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,30 +7,24 @@ COPY . ./ RUN make emails -FROM eclipse-temurin:21-jdk AS build-code +FROM ghcr.io/graalvm/native-image-community:21-muslib AS build-code ARG APP_STORAGE_TYPE ENV APP_STORAGE_TYPE=$APP_STORAGE_TYPE -RUN apt-get update && apt-get install -y git +RUN microdnf install -y git findutils WORKDIR /app COPY --from=build-emails /app/ /app RUN git fetch --unshallow || echo "Nothing to do" -RUN ./gradlew --no-daemon --exclude-task test build +RUN ./gradlew --no-daemon --exclude-task test build -Dquarkus.package.jar.enabled=false -Dquarkus.native.enabled=true -Dquarkus.native.additional-build-args=--static,--libc=musl -FROM eclipse-temurin:21-jre AS run +FROM scratch AS run ENV LANGUAGE="en_US:en" -ENV JAVA_OPTS="$JAVA_OPTS -Dquarkus.http.host=0.0.0.0" -ENV JAVA_OPTS="$JAVA_OPTS -Djava.util.logging.manager=org.jboss.logmanager.LogManager" -COPY --from=build-code --chown=nobody /app/build/quarkus-app/lib/ /deployments/lib -COPY --from=build-code --chown=nobody /app/build/quarkus-app/*.jar /deployments/ -COPY --from=build-code --chown=nobody /app/build/quarkus-app/app/ /deployments/app -COPY --from=build-code --chown=nobody /app/build/quarkus-app/quarkus/ /deployments/quarkus +COPY --from=build-code /app/build/*-runner /application EXPOSE 8080 -USER nobody -CMD ["java", "-jar", "/deployments/quarkus-run.jar"] +CMD ["/application", "-Dquarkus.http.host=0.0.0.0", "-Djava.util.logging.manager=org.jboss.logmanager.LogManager"] diff --git a/build.gradle b/build.gradle index d44804c..000e2dd 100644 --- a/build.gradle +++ b/build.gradle @@ -84,5 +84,3 @@ spotless { target "**/src/*/resources/**/*.yaml" } } - -compileJava.dependsOn "spotlessApply" From 8096adbde2c71edd18020ed0c8e9aae033f8ca3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Wed, 17 Jul 2024 12:35:12 +0200 Subject: [PATCH 157/157] Rollback to JVM image --- Dockerfile | 18 ++++++++++++------ Dockerfile.native | 30 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 Dockerfile.native diff --git a/Dockerfile b/Dockerfile index 1dcee24..fe9aad6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,24 +7,30 @@ COPY . ./ RUN make emails -FROM ghcr.io/graalvm/native-image-community:21-muslib AS build-code +FROM eclipse-temurin:21-jdk AS build-code ARG APP_STORAGE_TYPE ENV APP_STORAGE_TYPE=$APP_STORAGE_TYPE -RUN microdnf install -y git findutils +RUN apt-get update && apt-get install -y git WORKDIR /app COPY --from=build-emails /app/ /app RUN git fetch --unshallow || echo "Nothing to do" -RUN ./gradlew --no-daemon --exclude-task test build -Dquarkus.package.jar.enabled=false -Dquarkus.native.enabled=true -Dquarkus.native.additional-build-args=--static,--libc=musl +RUN ./gradlew --no-daemon --exclude-task test build -FROM scratch AS run +FROM eclipse-temurin:21-jre AS run ENV LANGUAGE="en_US:en" +ENV JAVA_OPTS="$JAVA_OPTS -Dquarkus.http.host=0.0.0.0" +ENV JAVA_OPTS="$JAVA_OPTS -Djava.util.logging.manager=org.jboss.logmanager.LogManager" -COPY --from=build-code /app/build/*-runner /application +COPY --from=build-code --chown=nobody /app/build/quarkus-app/lib/ /deployments/lib +COPY --from=build-code --chown=nobody /app/build/quarkus-app/*.jar /deployments/ +COPY --from=build-code --chown=nobody /app/build/quarkus-app/app/ /deployments/app +COPY --from=build-code --chown=nobody /app/build/quarkus-app/quarkus/ /deployments/quarkus EXPOSE 8080 -CMD ["/application", "-Dquarkus.http.host=0.0.0.0", "-Djava.util.logging.manager=org.jboss.logmanager.LogManager"] +USER nobody +CMD ["java", "-jar", "/deployments/quarkus-run.jar"] diff --git a/Dockerfile.native b/Dockerfile.native new file mode 100644 index 0000000..1dcee24 --- /dev/null +++ b/Dockerfile.native @@ -0,0 +1,30 @@ +FROM node:lts AS build-emails + +RUN apt-get update && apt-get install -y make +WORKDIR /app + +COPY . ./ +RUN make emails + + +FROM ghcr.io/graalvm/native-image-community:21-muslib AS build-code + +ARG APP_STORAGE_TYPE +ENV APP_STORAGE_TYPE=$APP_STORAGE_TYPE + +RUN microdnf install -y git findutils +WORKDIR /app + +COPY --from=build-emails /app/ /app +RUN git fetch --unshallow || echo "Nothing to do" +RUN ./gradlew --no-daemon --exclude-task test build -Dquarkus.package.jar.enabled=false -Dquarkus.native.enabled=true -Dquarkus.native.additional-build-args=--static,--libc=musl + + +FROM scratch AS run + +ENV LANGUAGE="en_US:en" + +COPY --from=build-code /app/build/*-runner /application + +EXPOSE 8080 +CMD ["/application", "-Dquarkus.http.host=0.0.0.0", "-Djava.util.logging.manager=org.jboss.logmanager.LogManager"]