Dancer2-2.3.0000755000765000024 015267764222 12405 5ustar00jasonstaff000000000000LICENSE100644000765000024 4643515267764222 13527 0ustar00jasonstaff000000000000Dancer2-2.3.0This software is copyright (c) 2024 by Alexis Sukrieh. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. Terms of the Perl programming language system itself a) the GNU General Public License as published by the Free Software Foundation; either version 1, or (at your option) any later version, or b) the "Artistic License" --- The GNU General Public License, Version 1, February 1989 --- This software is Copyright (c) 2024 by Alexis Sukrieh. This is free software, licensed under: The GNU General Public License, Version 1, February 1989 GNU GENERAL PUBLIC LICENSE Version 1, February 1989 Copyright (C) 1989 Free Software Foundation, Inc. 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The license agreements of most software companies try to keep users at the mercy of those companies. By contrast, our General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. The General Public License applies to the Free Software Foundation's software and to any other program whose authors commit to using it. You can use it for your programs, too. When we speak of free software, we are referring to freedom, not price. Specifically, the General Public License is designed to make sure that you have the freedom to give away or sell copies of free software, 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. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of a such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must tell them their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any work containing the Program or a portion of it, either verbatim or with modifications. Each licensee is addressed as "you". 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the notices that refer to this General Public License and to the absence of any warranty; and give any other recipients of the Program a copy of this General Public License along with the Program. You may charge a fee for the physical act of transferring a copy. 2. You may modify your copy or copies of the Program or any portion of it, and copy and distribute such modifications under the terms of Paragraph 1 above, provided that you also do the following: a) cause the modified files to carry prominent notices stating that you changed the files and the date of any change; and b) cause the whole of any work that you distribute or publish, that in whole or in part contains the Program or any part thereof, either with or without modifications, to be licensed at no charge to all third parties under the terms of this General Public License (except that you may choose to grant warranty protection to some or all third parties, at your option). c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the simplest and most usual way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this General Public License. d) You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. Mere aggregation of another independent work with the Program (or its derivative) on a volume of a storage or distribution medium does not bring the other work under the scope of these terms. 3. You may copy and distribute the Program (or a portion or derivative of it, under Paragraph 2) in object code or executable form under the terms of Paragraphs 1 and 2 above provided that you also do one of the following: a) accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Paragraphs 1 and 2 above; or, b) accompany it with a written offer, valid for at least three years, to give any third party free (except for a nominal charge for the cost of distribution) a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Paragraphs 1 and 2 above; or, c) accompany it with the information you received as to where the corresponding source code may be obtained. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form alone.) Source code for a work means the preferred form of the work for making modifications to it. For an executable file, complete source code means all the source code for all modules it contains; but, as a special exception, it need not include source code for modules which are standard libraries that accompany the operating system on which the executable file runs, or for standard header files or definitions files that accompany that operating system. 4. You may not copy, modify, sublicense, distribute or transfer the Program except as expressly provided under this General Public License. Any attempt otherwise to copy, modify, sublicense, distribute or transfer the Program is void, and will automatically terminate your rights to use the Program under this License. However, parties who have received copies, or rights to use copies, from you under this General Public License will not have their licenses terminated so long as such parties remain in full compliance. 5. By copying, distributing or modifying the Program (or any work based on the Program) you indicate your acceptance of this license to do so, and all its terms and conditions. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. 7. The Free Software Foundation may publish revised and/or new versions of the 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 a version number of the license which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the license, you may choose any version ever published by the Free Software Foundation. 8. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 9. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. 10. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE 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. END OF TERMS AND CONDITIONS Appendix: 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 humanity, 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 convey 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) 19yy This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 1, 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) 19xx name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (a program to direct compilers to make passes at assemblers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice That's all there is to it! --- The Perl Artistic License 1.0 --- This software is Copyright (c) 2024 by Alexis Sukrieh. This is free software, licensed under: The Perl Artistic License 1.0 The "Artistic License" Preamble The intent of this document is to state the conditions under which a Package may be copied, such that the Copyright Holder maintains some semblance of artistic control over the development of the package, while giving the users of the package the right to use and distribute the Package in a more-or-less customary fashion, plus the right to make reasonable modifications. Definitions: "Package" refers to the collection of files distributed by the Copyright Holder, and derivatives of that collection of files created through textual modification. "Standard Version" refers to such a Package if it has not been modified, or has been modified in accordance with the wishes of the Copyright Holder as specified below. "Copyright Holder" is whoever is named in the copyright or copyrights for the package. "You" is you, if you're thinking about copying or distributing this Package. "Reasonable copying fee" is whatever you can justify on the basis of media cost, duplication charges, time of people involved, and so on. (You will not be required to justify it to the Copyright Holder, but only to the computing community at large as a market that must bear the fee.) "Freely Available" means that no fee is charged for the item itself, though there may be fees involved in handling the item. It also means that recipients of the item may redistribute it under the same conditions they received it. 1. You may make and give away verbatim copies of the source form of the Standard Version of this Package without restriction, provided that you duplicate all of the original copyright notices and associated disclaimers. 2. You may apply bug fixes, portability fixes and other modifications derived from the Public Domain or from the Copyright Holder. A Package modified in such a way shall still be considered the Standard Version. 3. You may otherwise modify your copy of this Package in any way, provided that you insert a prominent notice in each changed file stating how and when you changed that file, and provided that you do at least ONE of the following: a) place your modifications in the Public Domain or otherwise make them Freely Available, such as by posting said modifications to Usenet or an equivalent medium, or placing the modifications on a major archive site such as uunet.uu.net, or by allowing the Copyright Holder to include your modifications in the Standard Version of the Package. b) use the modified Package only within your corporation or organization. c) rename any non-standard executables so the names do not conflict with standard executables, which must also be provided, and provide a separate manual page for each non-standard executable that clearly documents how it differs from the Standard Version. d) make other distribution arrangements with the Copyright Holder. 4. You may distribute the programs of this Package in object code or executable form, provided that you do at least ONE of the following: a) distribute a Standard Version of the executables and library files, together with instructions (in the manual page or equivalent) on where to get the Standard Version. b) accompany the distribution with the machine-readable source of the Package with your modifications. c) give non-standard executables non-standard names, and clearly document the differences in manual pages (or equivalent), together with instructions on where to get the Standard Version. d) make other distribution arrangements with the Copyright Holder. 5. You may charge a reasonable copying fee for any distribution of this Package. You may charge any fee you choose for support of this Package. You may not charge a fee for this Package itself. However, you may distribute this Package in aggregate with other (possibly commercial) programs as part of a larger (possibly commercial) software distribution provided that you do not advertise this Package as a product of your own. You may embed this Package's interpreter within an executable of yours (by linking); this shall be construed as a mere form of aggregation, provided that the complete Standard Version of the interpreter is so embedded. 6. The scripts and library files supplied as input to or produced as output from the programs of this Package do not automatically fall under the copyright of this Package, but belong to whoever generated them, and may be sold commercially, and may be aggregated with this Package. If such scripts or library files are aggregated with this Package via the so-called "undump" or "unexec" methods of producing a binary executable image, then distribution of such an image shall neither be construed as a distribution of this Package nor shall it fall under the restrictions of Paragraphs 3 and 4, provided that you do not represent such an executable image as a Standard Version of this Package. 7. C subroutines (or comparably compiled subroutines in other languages) supplied by you and linked into this Package in order to emulate subroutines and variables of the language defined by this Package shall not be considered part of this Package, but are the equivalent of input as in Paragraph 6, provided these subroutines do not change the language in any way that would cause it to fail the regression tests for the language. 8. Aggregation of this Package with a commercial distribution is always permitted provided that the use of this Package is embedded; that is, when no overt attempt is made to make this Package's interfaces visible to the end user of the commercial distribution. Such use shall not be construed as a distribution of this Package. 9. The name of the Copyright Holder may not be used to endorse or promote products derived from this software without specific prior written permission. 10. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. The End AUTHORS100644000765000024 13115267764222 13511 0ustar00jasonstaff000000000000Dancer2-2.3.0See perldoc Dancer2.pm, section AUTHORS, for a list of core developers and contributors. Changes100644000765000024 26041615267764222 14032 0ustar00jasonstaff000000000000Dancer2-2.3.02.3.0 2026-10-26 19:55:58-04:00 America/New_York [ SECURITY ] * PR #1866: JSON serializer should not leak file/line detail (Russell @veryrusty Jenkins) [ BUG FIXES ] * GH #1794: Add support for HTTP QUERY method. Old style plugins that register query will die at load, and apps with their own sub query will get a redefine warning. (Mohammad S Anwar) * PR #1845: Fix Windows-only test failures (David Precious) * GH #1861: Fix numbers being serialized to JSON as strings. The UTF-8 scan in Dancer2::Serializer::JSON gave numeric values a string form, which JSON::XS and JSON::PP before 2.97 then encoded as quoted strings. (David Precious) * GH #1863: Fix the JSON serializer modifying the data it is given. Hash keys needing UTF-8 decoding were deleted from the caller's hash, losing those entries, and arrayrefs were decoded in place. (David Precious) [ ENHANCEMENTS ] * None [ DOCUMENTATION ] * None [ DEPRECATED ] * None [ MISC ] * None 2.2.2 2026-09-28 19:45:16-04:00 America/New_York [ SECURITY ] * None [ BUG FIXES ] * GH #1837: Fix class-method serializer crash (David Precious) * GH #1843: Fix cpantesters, Debian build issues in gen.t (David Precious) [ ENHANCEMENTS ] * None [ DOCUMENTATION ] * PR #1844: Update Patch's name (Nova Patch) [ DEPRECATED ] * None [ MISC ] * None 2.2.1 2026-09-18 21:12:29-04:00 America/New_York [ SECURITY ] * PR #1838: Serializer::YAML refuses blessed objects and code in request bodies; raise min YAML version to 1.30 (David Precious) * PR #1839: Serializer::YAML also zeroes $YAML::UseCode, which otherwise reopens the code eval (David Precious) [ BUG FIXES ] * GH #1840: Specify min version of CLI::Osprey needed (Jason A. Crome) [ ENHANCEMENTS ] * None [ DOCUMENTATION ] * None [ DEPRECATED ] * None [ MISC ] * None 2.2.0 2026-09-16 07:58:19-04:00 America/New_York [ SECURITY ] * PR #1836: Fix path traversal in Dancer2::Handler::File, which served files from outside public_dir; see GHSA-6xw8-v24c-m783 (David Precious) * PR #1836: A halting on_hook_exception handler no longer lets the route the hook refused run anyway; see GHSA-v527-r4px-7vx7 (David Precious) * GH #1822: Strip CR and LF from response header names, as was already done for header values (David Precious) * GH #1823: AutoPage no longer serves a layout as a page on case-insensitive filesystems (David Precious) [ BUG FIXES ] * GH #1781: Fix directory detection heuristic (Jason A. Crome) * GH #1784: Fix UTF-8 handling in Serializer::JSON for readonly values (Russell @veryrusty Jenkins) * GH #1790: Fix infinite recursion into blessed objects in JSON Serializer (Russell @veryrusty Jenkins) * PR #1791: Send correct error codes in send_file (Anton Lundin) * PR #1797: Fix path()/dirname() DSL keywords dropping their first argument (Mike Weisenborn) * PR #1801: Fix failing CI (Jason A. Crome) * PR #1804: Make session ID generation always use Crypt::URandom and harden validate_id against invalid session IDs (David Precious) * GH #1824: Encode each response content assignment on its own merits, not just the first (David Precious) * GH #1825: Serializer::Mutable now ignores content type parameters such as charset when choosing a format (David Precious) * GH #1826: uri_for_route accepts a route parameter of 0, and refuses an empty one with a clearer message (David Precious) * GH #1827: Hooks are compiled exactly once however many times to_app is called (David Precious) * GH #1828: A NUL byte in a static file request no longer warns once per request (David Precious) * GH #1829: dancer2 gen -g (and -r) no longer dies after writing the application (David Precious) * GH #1830: dancer2 gen names the application directory after the dashed distribution name (David Precious) * GH #1831: dancer2 gen appends a relative, matchable pattern to MANIFEST.SKIP (David Precious) [ ENHANCEMENTS ] * None [ DOCUMENTATION ] * GH #1832: Document Serializer::Mutable's actual header precedence in each direction (David Precious) * GH #1833: Remove %D from the documented log_format characters; it was never implemented (David Precious) [ DEPRECATED ] * PR #1821: Remove Data::Dumper serializer from Dancer2 core, along with from_dumper/to_dumper keywords, tests (David Precious) [ MISC ] * GH #1834: Rename share/.gitignore so git stops applying it to this distribution's own share/ tree (David Precious) * GH #1835: Add a characterization test suite under t/unit, t/integration and t/e2e (David Precious) * With thanks to Curtis "Ovid" Poe, whose generated test suite from PAAD for Dancer2 found the defects fixed above 2.1.0 2026-03-11 21:29:26-04:00 America/New_York [ BUG FIXES ] * GH #686: Fix: to_json is double encoding UTF8 (Sawyer X) * GH #863: Fix case insensitive system confusion (Sawyer X) * GH #1124: Fix: charset config option is mostly ignored (Sawyer X) * GH #1143: Fix utf8 in URL (Sawyer X, Sorin Pop) * GH #1449, 1630: Make plugin DSL keyword app-specific (Sawyer X) * GH #1772: t/dsl/send_file.t fails with content_type-related errors (Sawyer X) * GH #1773: t/dsl/send_as.t throws json-related warnings (Jason A. Crome) * GH #1774: t/hooks.t throws void warnings (Jason A. Crome) * GH #1777: Properly unlink file uploads on Windows (Sawyer X) [ ENHANCEMENTS ] * GH #763: Strict config mode; warn on unknown config keys with opt-out (Sawyer X) * GH #763: Default strict config to off, but scaffold new apps with stict config enabled (Jason A. Crome) * GH #1073: Get multiple session cookie values at once with clear method (Sawyer X) * GH #1264: Move to Path::Tiny (Sawyer X) * GH #1323: Allow fully qualified namespaces for all engines (Russell @veryrusty Jenkins) * GH #1594: Use Unicode::UTF8 if available (Sawyer X) * GH #1664: Stop sending double server headers (Sawyer X) * GH #1709: `send_as` should use the full serializer, including hooks (Sawyer X) * PR #1757: Remove api_version, improve dispatching loop (Sawyer X) * PR #1758: Move MIME ownership to app (Sawyer X) * PR #1767: Turn off strict_config for noisy tests (Jason A. Crome) * PR #1780: Add package name capability to logger output (Mikko Koivunalho) [ DOCUMENTATION ] * GH #1431: Better document behavior of views setting (Jason A. Crome) * PR #1749, #1750: Fix broken manual and tutorial links (Gil Magno, Jason A. Crome) * PR #1753: Fix structure of config docs (Mikko Koivunalho) * PR #1762: Remove keyword logger from DSL document (Mikko Koivunalho) [ DEPRECATED ] * None [ MISC ] * PR #1776: Remove "Powered by..." from the error page (Jason A. Crome) 2.0.1 2025-10-22 18:13:37-04:00 America/New_York [ BUG FIXES ] * None [ ENHANCEMENTS ] * None [ DOCUMENTATION ] * GH #1745: Fix cookbook link in README (icyavocado) * PR #1748: Various documentation fixes (Jason A. Crome) [ DEPRECATED ] * None [ MISC ] * None 2.0.0 2025-09-15 17:49:26-04:00 America/New_York [ BUG FIXES ] * GH #1701: Split cookie values on & only (Yanick Champoux) [ ENHANCEMENTS ] * GH #530: Make data censoring configurable (Yanick Champoux, David Precious) * GH #850: Scaffold tutorial app; allow multiple apps to be scaffolded in core Dancer2 (Jason A. Crome) * GH #1512: Log hook entries as they are executed (Yanick Champoux) * GH #1615: Remove Dancer2::Template::Simple from Dancer2 core (Jason A. Crome) * PR #1637: New, extendable configuration system (Mikko Koivunalho) * GH #1723: Enable use of a different Template Toolkit base class (Andy Beverley) * PR #1727: Don't create CPAN package files when generating new apps (Jason A. Crome) * PR #1731: Retire Template::Tiny fork, use CPAN's (Jason A. Crome, Karen Etheridge, Damien Krotkine, Yanick Champoux) * PR #1736: Allow config system to bootstrap itself (Yanick Champoux) * GH #1737: Add on_hook_exception for errors during hook processing (Andy Beverley) * PR #1739: Add source to on_hook_exception (Andy Beverley) * PR #1742: Refactor CLI for future expansion (Jason A. Crome) [ DOCUMENTATION ] * GH #1342: Document skipping private methods in pod coverage tests (Jason A. Crome) * PR #1721: New Dancer2 docs, reorganization of all documentation; from TPRF grant (Jason A. Crome) * PR #1741: Cover items missed in earlier documentation branch (Jason A. Crome) [ DEPRECATED ] * None [ MISC ] * None 1.1.2 2024-11-25 08:34:51-05:00 America/New_York [ BUG FIXES ] * None [ ENHANCEMENTS ] * None [ DOCUMENTATION ] * None [ DEPRECATED ] * None [ MISC ] * PR #1715: Update deprecated download-artifact (dependabot) * GH #1716: Remove Scope::Upper from list of recommended deps (Russell @veryrusty Jenkins) * PR #1718: Add Module::Pluggable as a requirement, excluding broken versions (Russell @veryrusty Jenkins) * PR #1719: Force install of Module::Pluggable so CI can successfully run (Jason A. Crome, Russell @veryrusty Jenkins) 1.1.1 2024-07-18 19:48:00-04:00 America/New_York [ BUG FIXES ] * GH #1712: Fix use of send_as in templates (Andy Beverley) [ ENHANCEMENTS ] * None [ DOCUMENTATION ] * PR #1706: Document missing logging hooks and log format option; fix typo in logging test (Jason A. Crome) [ DEPRECATED ] * None [ MISC ] * None 1.1.0 2023-12-11 20:28:26-05:00 America/New_York [ BUG FIXES ] * None [ ENHANCEMENTS ] * GH #33: Named routes; add uri_for_route keyword (Sawyer X) [ DOCUMENTATION ] * None [ DEPRECATED ] * None [ MISC ] * None 1.0.0 2023-10-09 10:10:10-04:00 America/New_York [ BUG FIXES ] * GH #1663: Allow overriding of prefix in add_route (GeekRuthie) * GH #1675: Stringify VERSION_FROM correctly in Makefile.PL (Jason A. Crome) * GH #1677: Don't deserialize multipart form data on post (Emil Perhinschi) * GH #1694: Update JS assets in Dancer2 app skel (Jason A. Crome) [ ENHANCEMENTS ] * PR #1682: Bump minimum version of Perl to 5.14 (Jason A. Crome) [ DOCUMENTATION ] * GH #1580: Document the purpose of the .dancer file (Jason A. Crome) * GH #1669: Show correct usage of Dancer2::Core::Error (GeekRuthie) * GH #1674: Fix POD for input_handle() (mauke) * GH #1414: Add documentation resources to the doc map (Jason A. Crome, Yanick Champoux) * PR #1684: Remove shumphrey from core developers (Steven Humphrey) * GH #1685: Document the versioning scheme and Dancer2 release process (Jason A. Crome) * PR #1688: Fixed various bugs/issues in Dancer2 Pod (Jason A. Crome) * PR #1691: Update the contribution guidelines (Jason A. Crome) * PR #1692: Change README extension .mkdn -> .md (Jason A. Crome) [ DEPRECATED ] * GH #1645: Deprecated Dancer2::Test (Jason A. Crome) * GH #1646: Deprecated keyword: push_header (Jason A. Crome) * GH #1647: Deprecated keyword: header (Jason A. Crome) * GH #1648: Deprecated keyword: headers (Jason A. Crome) * GH #1649: Deprecated keyword: context (Jason A. Crome) * GH #1650: Deprecated: splat/capture named placeholders (Jason A. Crome) * GH #1651: Deprecated core Request instance method: request->dispatch_path (Jason A. Crome) * GH #1652: Deprecated keyword in plugins: plugin_setting (Jason A. Crome) * GH #1653: Deprecated keyword in plugins: dancer_app (Jason A. Crome) * GH #1654: Deprecated keyword in plugins: request (Jason A. Crome) * GH #1655: Deprecated keyword in plugins: var (Jason A. Crome) * GH #1656: Deprecated keyword in plugins: hook (Jason A. Crome) [ MISC ] * GH #1659: Rename `master` branch to be `main` (Yanick Champoux) 0.400001 2023-02-05 18:41:48-05:00 America/New_York [ BUG FIXES ] * PR #1247: Fix edge case in plugin compat (Sawyer X) * GH #1621: Fix recursion error in TT after longjump (Andy Beverley, Russell @veryrusty Jenkins) * PR #1667: Remove failing module from GitHub Actions config (Jason A. Crome) [ ENHANCEMENTS ] * GH #769, PR #829, #1662: Rename show_errors as show_stacktrace (Nuno Ramos Carvalho, Sawyer X) * GH #1594: Use Unicode::UTF8 if available (Sawyer X) [ DOCUMENTATION ] * GH #1657: Fix the Dancer2::DeprecationPolicy abstract (Jason A. Crome) * GH #1661: Add 'gen' command to 'dancer2' runs (Steve Bertrand) * PR #1671: Fix broken links in POD; avoid passive voice (Tom Hukins) 0.400000 2022-03-13 22:16:13-04:00 America/New_York [ BUG FIXES ] * PR #1634: Fix CI push setting to run tests on branches with / in the name (Stefan Hornburg - Racke) * PR #1641: Fix uninitialized warnings from parsing routes with mixed regex/splats (Russell @veryrusty Jenkins) [ ENHANCEMENTS ] * PR #1627: Set minimum Perl to 5.10.1 (Peter Mottram - SysPete) * PR #1643: Set minimum Perl to 5.12.5 (Jason A. Crome) [ DOCUMENTATION ] * PR #1633: Fix YAML example in the tutorial (Tina Müller) * PR #1644: Add deprecation policy to docs; link git guide (Jason A. Crome) 0.301004 2021-06-06 13:30:28-04:00 America/New_York [ BUG FIXES ] * GH #1624: Fix missing gen subcommand in tutorial (racke) * PR #1626: Add File::Which to Dancer2 dependencies (Jason A. Crome) [ ENHANCEMENTS ] * None [ DOCUMENTATION ] * None 0.301003 2021-06-03 09:24:33-04:00 America/New_York [ BUG FIXES ] * GH #1611: Redirect '/' doesn't always work as expected (Russell @veryrusty Jenkins, Christopher Gurnee) * PR #1620: Quiet spammy failing CI builds (Jason A. Crome) * PR #1623: Copy Dockerfile from the right spot (Jason A. Crome) [ ENHANCEMENTS ] * PR #1613: Add git features to Dancer2 CLI (Jason A. Crome) * PR #1614: Generate Dockerfile when creating new app (Jason A. Crome) [ DOCUMENTATION ] * PR #1563: Fix typos in perlcritic.rc notes (Achyut Kumar Panda) * PR #1609: Document and test for missing DSL keywords (racke, Jason A. Crome) * PR #1618: Provide a consistent list of community resources (Jason A. Crome) * PR #1619: Clarify Dancer2::Template::Simple's role in life (Jason A. Crome) 0.301002 2021-04-18 15:29:32-04:00 America/New_York [ BUG FIXES ] * None [ ENHANCEMENTS ] * GH #1593: Log files used to build config (Nick Tonkin) * PR #1608: Migrate CLI to CLI::Osprey (Jason A. Crome) # PR #1610: Replace File::Find with Path::Tiny in CLI (Jason A. Crome) [ DOCUMENTATION ] * PR #1597: Update Core/Hook.pm Pod (Paul Clements) 0.301001 2021-03-17 08:52:34-04:00 America/New_York [ BUG FIXES ] * PR #1607: Fix broken tests following App::Cmd removal (Jason Crome) [ ENHANCEMENTS ] * PR #1606: Clean up various build prereqs (Graham Knop) [ DOCUMENTATION ] * None 0.301000 2021-03-15 18:58:17-04:00 America/New_York [ BUG FIXES ] * PR #1586: Run version checks against MetaCPAN (Jason A. Crome) * PR #1604: Remove App::Cmd dependency; have script/dancer2 bail if it's not installed (David Precious) [ ENHANCEMENTS ] * GH #1064: Add DSL keyword request_data (Mickey Nasriachi) * PR #1581: Disable scheduled GitHub action on Forks (Gabor Szabo) * PR #1590: Improve install/documentation of XS modules (Jason A. Crome) * PR #1591: Add more plugins to GitHub Actions CI (Gabor Szabo) [ DOCUMENTATION ] * GH #1582: Small fixes in Migration POD (Sergiy Borodych) * PR #1595: Add cookbook recipe for enabling/disabling routes at runtime (Nick Tonkin) * PR #1599: Punctuation correction in docs (Nick Tonkin) 0.300005 2021-01-26 15:57:41-05:00 America/New_York [ BUG FIXES ] * GH #1546: Add MIME type for all files served from public (Russell @veryrusty Jenkins) * GH #1555: Remove all leftovers of warnings config setting (Sergiy Borodych) * GH #1557: Remove HTTP::XSCookies workaround (Alexander Pankoff) * GH #1564: Add script_name to redirects beginning with / (Nigel Gregoire) * GH #1567: Fix CSS so errors do not display ourside of
 HTML
      element (Elliot Holden)
    * GH #1568: Serializer::Mutable doesn't auto-load other serializers
      (Russell @veryrusty Jenkins)
    * GH #1579: Fix missing push_header method in Response::Delayed 
      (Paul Clements)
    
    [ ENHANCEMENTS ]
    * GH #1552: Update jQuery (Sergiy Borodych)
    * GH #1558: Test to make sure uploads aren't discarded after the
      forward keyword is used (Alexander Pankoff)
    * GH #1571: Add GitHub Actions support (Gabor Szabo)
    * GH #1572: Install Dancer2::Session::Cookie in order to run the test 
      t/issues/gh-811.t (racke)

    [ DOCUMENTATION ]
    * GH #1490: Document Dancer2::Core::App->template() (Steve Dondley)
    * GH #1551: Fix pod for cookie same site attribute (Sergiy Borodych)
    * GH #1562: Fix links, missing code in Tutorial (cloveistaken)

0.300004  2020-05-26 20:52:34-04:00 America/New_York

    [ BUG FIXES ]
    * GH #1509: Request instantiation fails throw 400 Bad Response (Russell
      @veryrusty Jenkins). This resolves GH #1056, 1482, 1496, 1507, 1508,
      and 1510.
    
    [ ENHANCEMENTS ]
    * GH #1510: Test for proper multi-part form handing (ice-lenor, Sawyer X)
    * GH #1547: Cookie SameSite support (Russell @veryrusty Jenkins)

    [ DOCUMENTATION ]
    * None

0.300003  2020-04-09 10:39:55-04:00 America/New_York

    [ BUG FIXES ]
    * None
    
    [ ENHANCEMENTS ]
    * None

    [ DOCUMENTATION ]
    * GH #1543: Various tutorial cleanups (Peter Mottram - SysPete)

0.300002  2020-04-07 11:44:59-04:00 America/New_York

    [ BUG FIXES ]
    * GH #1541: Fix broken test that is skipped under CI (Peter Mottram - 
      SysPete)
    
    [ ENHANCEMENTS ]
    * None

    [ DOCUMENTATION ]
    * None

0.300001  2020-04-06 12:14:47-04:00 America/New_York

    [ BUG FIXES ]
    * GH #1461: Relax redirect to match RFC_7231 (James Raspass)
    * GH #1499: Don't double decode date strings (simbabque)
    * GH #1536: Fix storing objects in YAML sessions (Tom Hukins)

    [ ENHANCEMENTS ]
    * GH #1540: Improve configuration handling (Tom Hukins)

    [ DOCUMENTATION ]
    * GH #1425: Revamped Tutorial (Steve Dondley)
    * GH #1521: Documentation on configuring the adress to listen on: 
      "server" does not seem to work, but "host" does (Ma_Sys.ma)
    * GH #1530: Fix missing space in docs (ferki)
    * GH #1535: Fix example code in SessionFactory::File (Sergiy Borodych)
    * GH #1538: Defined or should not be an assignment (Timothy Alexis 
      Vass)

0.300000  2019-12-23 23:55:09-06:00 America/Chicago

    [ BUG FIXES ]
    * None

    [ ENHANCEMENTS ]
    * GH #1127, GH #1476: Route parameters with types (Peter Mottram - 
      SysPete)

    [ DOCUMENTATION ]
    * None 

0.208002  2019-12-14 16:08:46-05:00 America/New_York

    [ BUG FIXES ]
    * GH#1527: Update travis dist to 'trusty' (Sergiy Borodych)

    [ ENHANCEMENTS ]
    * GH #1525: Remove use of Return::MultiLevel, and implement stack frame 
      jumping manually (Graham Knop)

    [ DOCUMENTATION ]
    * GH #1505: Fix Flaskr link (Mohammad S Anwar)
    * GH #1506, 1520: Explain what add_route() does with args (Tom Hukins)
    * GH #1519: Fix Template Toolkit config docs (Tom Hukins)
    * GH #1522: Fix itetare typo (Stefan Hornburg - Racke)
    * GH #1523: Fix typo in Template Toolkit documentation (Mike Katasonov)
    * GH #1524: Fix error in configuration documentation (Tom Hukins)
    * GH #1526: Mention that TT2 config start_tag/end_tag need escaping 
      (Chris White)
    * GH #1528: Note that"Engines" key must be merged in config.yml (Chris
      White)

0.208001  2019-08-04 21:06:25-04:00 America/New_York

    [ BUG FIXES ]
    * GH #1515: Add Types::Standard to cpanfile (Russell @veryrusty Jenkins)

    [ ENHANCEMENTS ]
    * None

    [ DOCUMENTATION ]
    * GH #1513: Fix Dancer2::Test typo (Utkarsh Gupta)

0.208000  2019-06-19 10:21:16-04:00 America/New_York

    [ BUG FIXES ]
    * PR #1493: Fix body not being sent on forward (Johannes Piehler)
    * PR #1498: Load missing Encode in logger role (simbabque)
    * PR #1501: Set :raw when copying files to new project (xenu)
    * GH #1502: Update jquery (racke)

    [ ENHANCEMENTS ]
    * GH #1320: Implement prepare_app keyword (Sawyer X)

    [ DOCUMENTATION ]
    * Tidy up Cookbook POD. (Mohammad S Anwar)

0.207000  2018-11-14 17:24:25-05:00 America/New_York

    [ BUG FIXES ]
    * GH #1427: Allow layout_dir to be configured by set keyword (Russell
      @veryrusty Jenkins)
    * GH #1456: Engine logging respects minimum level filtering (Daniel Perrett)
    * PR #1479: Remove arbitrary Perl 5.10 requirement from tests (Dan Book)
    * PR #1480: Correct dynamic HTTP::XSCookies requirement (Dan Book)
    * PR #1486: Install dzil deps for use by Appveyor (Dan Book)

    [ ENHANCEMENTS ]
    * GH #1418: Send plain text content with send_as() (Steve Dondley)
    * PR #1457: Serializer mutable with custom mapping. Also resolves issues
      #795, #973, and #901 (Russell @veryrusty Jenkins, Yanick Champoux, 
      Daniel Böhmer, Steven Humphrey)
    * PR #1459: Add no default middleware feature. Also resolves #1410 
      (Russell @veryrusty Jenkins)
    * GH #1469: Code of Conduct enhancements (MaxPerl)

    [ DOCUMENTATION ]
    * GH #1166: Add behind_proxy docs to Deployment manual (Nuno Ramos 
      Carvalho)
    * GH #1417: Add "set engines" documentation (Deirdre Moran)
    * PR #1450: Add calculator example (Gabor Szabo)
    * PR #1452: Fix Pod formatting for CPAN (simbabque)
    * PR #1454: Fix typos in docs (Gil Magno)
    * PR #1464: Can't set environment with 'set' keyword (Ben Kaufman)
    * PR #1470: Use session for flash and explain in detail (simbabque)
    * PR #1472: Migration, tutorial, other doc fixes (Jason A. Crome)
    * PR #1473: Show support resources after generating new app (Jason A.
      Crome)
    * PR #1474: Use the correct URL for HAProxy (Jason A. Crome)
    * PR #1475: Add manual section for security concerns (Jason A. Crome)
    * PR #1487: Clarify deprecation of Dancer2::Test (Steve Dondley)

0.206000  2018-04-19 22:09:46-04:00 America/New_York

    [ BUG FIXES ]
    * GH #1090, #1406: Replace HTTP::Body with HTTP::Entity::Parser in
      Dancer2::Core::Request. (Russell @veryrusty Jenkins)
    * GH #1292: Fix multiple attribute definitions within Plugins
      (Nigel Gregoire)
    * GH #1304: Fix the order by which config files are loaded, independently
      of their filename extension (Alberto Simões, Russell @veryrusty Jenkins)
    * GH #1400: Fix infinite recursion with exceptions that use circular
      references. (Andre Walker)
    * GH #1430: Fix `dancer2 gen` from source directory when Dancer2 not
      installed. (Tina @perlpunk Müller - Tina)
    * GH #1434: Add `validate_id` method to verify a session id before
      requesting the session engine fetch it from its data store.
      (Russell @veryrusty Jenkins)
    * GH #1435, #1438: Allow XS crush_cookie methods to return an arrayref
      of values. (Russell @veryrusty Jenkins)
    * GH #1443: Update copyright year (Joseph Frazer)
    * GH #1445: Use latest HTTP::Headers::Fast (Russell @veryrusty Jenkins)
    * PR #1447: Fix missing build requires (Mohammad S Anwar)
    
    [ ENHANCEMENTS ]
    * PR #1354: TemplateToolkit template engine will log (at debug level)
      if a template is not found. (Kiel R Stirling, Russell @veryrusty Jenkins)
    * GH #1432: Support Content-Disposition of inline in 
      send_file() (Dave Webb)
    * PR #1433: Verbose testing in AppVeyor (Graham Knop)

    [ DOCUMENTATION ]
    * GH #1314: Documentation tweaks (David Precious)
    * GH #1317: Document serializer configuration (sdeseille)
    * GH #1386: Add Hello World example (Gabor Szabo)
    * PR #1408: List project development resources (Steve Dondley)
    * PR #1426: Move performance improvement information from Migration guide
      to Deployment (Pedro Melo)

0.206000_02 2018-04-09 21:48:24-04:00 America/New_York (TRIAL RELEASE)

    [ BUG FIXES ]
    * GH #1090, #1406: Replace HTTP::Body with HTTP::Entity::Parser in
      Dancer2::Core::Request. (Russell @veryrusty Jenkins)
    * GH #1304: Fix the order by which config files are loaded, independently
      of their filename extension (Alberto Simões, Russell @veryrusty Jenkins)
    * GH #1400: Fix infinite recursion with exceptions that use circular
      references. (Andre Walker)
    * GH #1430: Fix `dancer2 gen` from source directory when Dancer2 not
      installed. (Tina @perlpunk Müller - Tina)
    * GH #1434: Add `validate_id` method to verify a session id before
      requesting the session engine fetch it from its data store.
      (Russell @veryrusty Jenkins)
    * GH #1435, #1438: Allow XS crush_cookie methods to return an arrayref
      of values. (Russell @veryrusty Jenkins)
    * GH #1443: Update copyright year (Joseph Frazer)
    * GH #1445: Use latest HTTP::Headers::Fast (Russell @veryrusty Jenkins)
    
    [ ENHANCEMENTS ]
    * PR #1354: TemplateToolkit template engine will log (at debug level)
      if a template is not found. (Kiel R Stirling, Russell @veryrusty Jenkins)
    * GH #1432: Support Content-Disposition of inline in 
      send_file() (Dave Webb)
    * PR #1433: Verbose testing in AppVeyor (Graham Knop)

    [ DOCUMENTATION ]
    * GH #1317: Document serializer configuration (sdeseille)
    * PR #1426: Move performance improvement information from Migration guide
      to Deployment (Pedro Melo)

0.205002  2017-10-17 16:08:25-05:00 America/Chicago

    [ BUG FIXES ]
    * GH #1362: Make cookies http_only by default (David Precious)
    * GH #1366: Use proper shebang on dancer script and make EU::MM do the job
    * GH #1373: Unset Dancer environment vars before testing (Alberto Simões)
    * GH #1380: Consider class of error displayed when using show_errors
      (Nick Tonkin).
    * GH #1383: Remove Deflater from default app skeleton (Pierre Vigier)
    * GH #1385: Fix links inside the documentation (Alberto Simões)
    * GH #1390: Honour no_server_tokens config in error responses (Russell
      @veryrusty Jenkins)

    [ DOCUMENTATION ]
    * GH #1285: Add "Default Template Variables" section to manual (simbabque)
    * GH #1312: Fix docs for Dancer2::Core::Route->match, which takes a request
      object (simbabque).
    * GH #1368: Don't allow XSS in tutorial (simbabque)
    * GH #1383: Remove full URL on links to third party modules (Alberto Simoes)
    * GH #1395: Customize TT behavior via subclassing (simbabque).

0.205001  2017-07-11 08:03:21-05:00 America/Chicago

    [ BUG FIXES ]
    * GH #1332: Add check for old version of HTTP::XSCookies (Peter Mottram -
      SysPete)
    * GH #1336: Fix warnings on 5.10 and below. (Sawyer X)
    * GH #1347: Add Perl versions 5.22-5.26 and appveyor to Travis-CI
      configuration (Dave Jacoby)

    [ ENHANCEMENTS ]
    * GH #1281: Use Ref::Util in Core for all reference checks (Mickey 
      Nasriachi)
    * GH #1338: Add message explaining how to run newly-created application
      (Jonathan Cast)

    [ DOCUMENTATION ]
    * GH #1334: Fix prefix example in Cookbook (Abdullah Diab)
    * GH #1335: Add missing word in request->host docs (Glenn Fowler)
    * GH #1337: Fix link in SEE ALSO section of Dancer2::Core::Types (Stefan
      Hornburg - Racke)
    * GH #1341: Clarify plugin documentation (Stefan Hornburg - Racke)
    * GH #1345, #1351, #1356: Fix password check code example in tutorial
      (Jonathan Cast)
    * GH #1355: Fix typo (Gregor Herrmann)

0.205000  2017-03-10 15:37:52-06:00 America/Chicago

    [ BUG FIXES ]
    * GH #1325: Support multi-value cookies when using HTTP::XSCookies.
      (James Raspass)
    * GH #1303: Read configuration options when send_as() creates a new 
      serializer (Paul Williams)
    * GH #1290: Properly check buffer length in _read_to_end() (Marketa 
      Wachtlova)
    * GH #1322: Deprecate broken request->dispatch_path in favor of 
      request->path. Warn the developer of the deprecation (Russell 
      @veryrusty Jenkins).

    [ ENHANCEMENTS ]
    * GH #1326: Speed up by using Type::Tiny, again. (Pete SysPete Mottram)
    * GH #1318: Add support for the SameSite cookie attribute. (James Raspass)
    * GH #1283: Skeleton now provides an example of setting the appdir.
      (Jason Lewis)
    * GH #1315: Adjust dist.ini to set "build_requires" for 
      ExtUtils::MakeMaker. (Atoomic)
    * GH #1331: Preliminary prepare_app() work (Sawyer X)

    [ DOCUMENTATION ]
    * GH #1324: Fix broken link to send_file. (Fabrice Gabolde)
    * GH #1311: Typo and link fixes. (Breno G. de Oliveira - @garu)
    * GH #1310: Document query string parameters in uri_for. (Michael J South)
    * GH #1329: Remove dead code from file upload example (Stefan Hornburg - 
      Racke)
    * GH #1256: Additions to migration manual (Daniel Perrett)
    * GH #1330: Add middleware examples to scaffolder (David - sbts)

0.204004  2017-01-26 18:29:34+01:00 Europe/Amsterdam

    [ BUG FIXES ]
    * GH #1307: Fix breakage of Template::Toolkit, caused by
      previous release. (Peter SysPete Mottram)

0.204003  2017-01-25 15:21:40-06:00 America/Chicago

    [ BUG FIXES ]
    * GH #1299: Fix missing CPANTS prereqs (Mohammad S. Anwar)

    [ ENHANCEMENTS ]
    * GH #1249: Improve consistency with Template::Toolkit,
      using correct case for 'include_path', 'stop_tag', 'end_tag',
      and 'start_tag', removing ANYCASE option.
      (Klaus Ita)
    * Call route exception hook before logging an error, allowing devs to
      raise their own errors bedore D2 logging takes over. (Andy Beverley)

    [ DOCUMENTATION ]
    * Add another example of the delayed asynchronous mechanism
      (Ed @mohawk2 J., Sawyer X)
    * GH #1291: Document 'change_session_id' in Dancer2::Core::App.
      (Peter SysPete Mottram)
    * Fix typo in Dancer2::Core::Response (Gregorr Herrmann)
    * Document Dancer2::Plugin::RootURIFor (Mario Zieschang)

0.204002  2016-12-21 15:40:02-06:00 America/Chicago

    [ BUG FIXES ]
    * GH #975: Fix "public_dir" configuration to work, just like
      DANCER_PUBLIC. (Sawyer X)

    [ ENHANCEMENTS ]
    * You can now call '$self->find_plugin(...)' within a plugin
      in order to find a plugin, in order to use its DSL in your
      custom plugin. (Sawyer X)

    [ DOCUMENTATION ]
    * GH #1282: Typo in Cookbook. (Kurt Edmiston)
    * GH #1214: Update Migration document. (Sawyer X)
    * GH #1286: Clarify hook behavior when disabling layout (biafra)
    * GH #1280: Update documentation to use specific parameter 
                keywords (Hunter McMillen)

0.204001  2016-10-17 08:29:00-05:00 America/Chicago

    [ BUG FIXES ]
    * Restore 5.8 support (fix test which required captures).
      (Russell @veryrusty Jenkins)
    * PR #1271: fix wrong regex check against $_ (Mickey Nasriachi)

    [ ENHANCEMENTS ]
    * GH #1262: Add 'encode_json' and 'decode_json' DSL, which are
      recommended instead of 'to_json' and 'from_json'.
      (Dennis @episodeiv lichtenthäler)

    [ DOCUMENTATION ]
    * Fix some typos.(Dennis @episodeiv lichtenthäler)
    * GH #1031: Remove D2::Core::Context remnants from docs.
      (Sawyer X)

    [ PACKAGING ]
    * GH #1273: Do not require Test::Perl::Critic to install.
      (Dennis lichtenthäler)

0.204000  2016-10-10 20:56:51-05:00 America/Chicago

    [ BUG FIXES ]
    * GH #1255: Fix hook overriding in plugin. (Yves Orton)
    * GH #1191: Named capture prior to dispatch breaks dispatch.
      (Yves Orton)
    * GH #1235: Clean up descriptions for HTTP codes 303 and 305.
      (Yanick Champoux)
    * Remove duplicate (and errornous) 451 error message.
      (Sawyer X)
    * GH #1116, #1245: Ensure cached Hash::MultiValue parameters are cloned
      into the new request. (Russell @veryrusty Jenkins)

    [ ENHANCEMENTS ]
    * You can now provide a $EVAL_SHIM to Dancer2::Core::App in order
      to have custom code run on eval{} calls. One example of this
      is to handle proper counting of stack frames when you want to
      unwind/unroll the stack for custom error reporting.
      (Yves Orton)
    * Added a cpanfile to allow installing local dependencies with 
      carton. (Mickey Nasriachi)
    * GH #1260: Specify optional charset to send_file and send_as
      (Russell @veryrusty Jenkins)
    * PR #1162: Change skeleton template tags so skeletons can generate
      applications that use Template Toolkit default tags (Jason Lewis)
    * GH #1149: Fix config loading inconsistencies, support local config
      files in addition to standard Dancer conf files (Jonathan Scott Duff)
    * PR #1269: Stash decoded body_parameters separately from those 
      in Plack::Request (Russell @veryrusty Jenkins)
    * GH #1253: Static middleware should send 304 Not Modified to enable
      intermediate level caching. (Russell @veryrusty Jenkins)

    [ DOCUMENTATION ]
    * GH #608: Remove extra general COPYRIGHT notice in Tutorial.
      (Sawyer X)
    * Simplify upload example. (Alberto Simões, Sawyer X)

0.203001  2016-09-03 20:59:47-05:00 America/Chicago

    [ BUG FIXES ]
    * GH #1237: Specify minimum version of List::Util required for pair*
      functionals. (Russell @veryrusty Jenkins)

    [ ENHANCEMENTS ]
    * PR #1242: Replace Class::Load with Module::Runtime (Russell 
      Jenkins - @veryrusty)

0.203000  2016-08-24 22:09:56-05:00 America/Chicago

    [ BUG FIXES ]
    * GH #1232: Force deserialization of body data even when an existing 
      Plack::Request object has already parsed request body. Don't double
      decode deserialized data. (Russell Jenkins - @veryrusty)

    [ ENHANCEMENTS ]
    * GH #1195: Add change_session_id() method - both as a good security 
      practice and to comply with other established security standards.
      (Peter Mottram)
    * GH #1234: Add convenience functions to access Dancer's HTTP_CODES
      table. (Yanick Champoux)

    [ DOCUMENTATION ]
    * Fix Typo (Stefan Hornburg - Racke)
    * Document $session->data (Stefan Hornburg - Racke)
    
0.202000  2016-08-13 13:50:30-05:00 America/Chicago

    [ BUG FIXES ]
    * Fix memory leak in plugins. (Sawyer X)
    * GH #1180, #1220: Revert (most of) GH #1120. Change back to using
      MooX::Types::MooseLike until issues around Type::Tiny are resolved.
      Peter (@SysPete) Mottram
    * GH #1192: Decode body|query|request_parameters (Peter Mottram)
    * GH #1224: Plugins defined with :PluginKeyword attribute are now 
      exported. (Yanick Champoux)
    * GH #1226: Plugins can now call the DSL of the app via $self->dsl
      (Sawyer X)

    [ ENHANCEMENTS ]
    * PR #1223: Add YAML::XS to Recommends (Peter Mottram)
    * PR #1117: If installed, use HTTP::XSCookies and all cookie operations 
      will be faster (Peter Mottram)
    * PR #1228: Allow register_plugin() to pass @_ properly (Sawyer X)
    * PR #1231: Plugins can now call the syntax of plugins they loaded 
      (Sawyer X)

    [ DOCUMENTATION ]
    * PR #1151: Note that config is immutable after first read (Peter Mottram)
    * PR #1222: Update list of files generated by `dancer2 -a`, make name of 
      sample app consistent (Daniel Perrett)

0.201000  2016-07-22 08:26:18-05:00 America/Chicago

    [ BUG FIXES ]
    * GH #1216: Make DSL work in edge-case of plugins calling DSL before the
      app class loaded Dancer2. (Sawyer X)
    * GH #1210: Show proper module/line number in log output (Masaaki Saito)

    [ ENHANCEMENTS ]
    * GH #900: Switch from to_json to encode/encode_json (Nuno Ramos Carvalho)
    * GH #1196: Move serializer from JSON to JSON::MaybeXS (Nuno Ramos Carvalho)
    * GH #1215: Remove unused DANCER2_SHARE_DIR env variable (Jason A. Crome)

    [ DOCUMENTATION ]
    * PR #1213: Clarify params merging docs and related examples
      (Daniel Perrett)
    * Add Peter Mottram (@SysPete) to list of core developers. (Russell Jenkins)
    * PR #1208: Introduce appdir before it's used; simplify description of what
      a view is (James E Keenan)
    * GH #1218: By request, remove David Golden from list of core developers. 
      Created "emeritus" section to honor the contributions of former core 
      developers. Thanks, xdg!

0.200003  2016-07-11 17:17:57+02:00 Europe/Amsterdam

    [ BUG FIXES ]
    * PR #1198: Session::YAML should not accept bad session cookie value 
      from client (Peter Mottram)
    * Require minimum version of YAML of 0.86 (to satisfy GH #899) and a 
      maximum version of YAML 1.15. YAML 1.16 causes test failures as
      reported by CPAN Testers.
    * Remove session test data from builds. (Peter Mottram)

    [ ENHANCEMENTS ]
    * Require minimum version of ExtUtils::MakeMaker of 7.1101 to support 
      a range of prereq version numbers (rjbs, Jason Crome, Sawyer X)
    * GH #1188: Add error message to open_file (exercism-1)
    * Support showing private variables in templates under
      Template::Toolkit. (Alberto Simões)

    [ DOCUMENTATION ]
    * GH #1193: Spelling correction (Gregor Herrmann)
    * Fix typo of config option in Pod. (Nuno Carvalho)
    * Fix POD syntax error. (Nuno Carvalho)
    * Fix Manual error. (James E Keenan)
    * Move documentation index to dancer2. (Alan Berndt)
    * GH #1209: Clean up examples for 'set views' and 'set public_dir'
      in Dancer2::Manual (James E Keenan)

0.200002  2016-06-22 16:39:13+02:00 Europe/Amsterdam

    [ BUG FIXES ]
    * Using `var` with a `forward`ed request now works.
      (Sawyer X, Jason Crome)


0.200001  2016-06-16 15:51:04+02:00 Europe/Amsterdam

    [ BUG FIXES ]
    * GH #1175: Plugins are not required to be in the Dancer2::Plugin
      namespace. (Russell @veryrusty Jenkins)
    * GH #1176, #1177: Remove Test::Deep as a test dependency.
      (Nuno Carvalho, Peter Mottram)
    * GH #1185: Fails on 5.25.1. (Tony Cook)

    [ DOCUMENTATION ]
    * GH #1178: Update D2::Manual with links to new plugin architecture.
      (Joel Berger, Jason A. Crome)
    * GH #1184: Use 'before_template_render' rather than the special case
      'before_template' in D2::Manual and D2::Tutorial (Philippe Bricout)

    [ ENHANCEMENTS ]
    * GH #1018: Additional plugin hook tests (Ruben Amortegui)

0.200000  2016-05-31 15:05:46+02:00 Europe/Amsterdam

    [ BUG FIXES ]
    * GH #1174: Update plugin tests to stop deprecation warnings
      (Peter Mottram)
    * GH #1173: Reword error when serialization / deserialization fails
      to be more generic (Russell @veryrusty Jenkins)

    [ ENHANCEMENTS ]
    * Introduce an improved variation of the Dancer2::Plugin::SendAs
      into core. You can now override the serializer (or lack thereof)
      at any point in time for a response by calling `send_as`. You
      can also send the options of `send_file` (like the Content-Type)
      and the charset for the app is also respected.
      (Russell @veryrusty Jenkins)

0.166001_04 2016-05-27 14:54:53+02:00 Europe/Amsterdam (TRIAL RELEASE)

    [ BUG FIXES ]
    * GH #1171: Ensure request query parameter parsing is independent of
      Plack version (Russell Jenkins)

0.166001_03 2016-05-27 13:23:52+02:00 Europe/Amsterdam (TRIAL RELEASE)

    [ BUG FIXES ]
    * GH #1165, #1167: Copy is_behind_proxy attribute into new request
      on forward. (Russell Jenkins)

    [ ENHANCEMENTS ]
    * GH #1120: Move from MooX::Types::MooseLike to Type::Tiny for
      performance. (Peter Mottram)
    * GH #1145, #1164: Replace Class::Load with Module::Runtime
      (Sawyer X)
    * GH #1159, #1163: Make template keyword global.
      (Sawyer X, Russell Jenkins)

    [ DOCUMENTATION ]
    * GH #1158: List both static and shared modules in Apache's deploy
      instructions. (Varadinsky)

0.166001_02 2016-04-29 16:42:54+02:00 Europe/Amsterdam (TRIAL RELEASE)


    [ BUG FIXES ]
    * GH #1160: Engines receive correct log callback on build
      (Peter Mottram)
    * GH #1148: Ensure request body parameter parsing is independent of
      Plack version (Russell Jenkins)

0.166001_01 2016-04-19 21:50:35+02:00 Europe/Amsterdam (TRIAL RELEASE)

    [ BUG FIXES ]
    * GH #1102: Handle multiple '..' in file path utilities.
      (Oleg A. Mamontov, Peter Mottram)
    * GH #1114: Fix missing prereqs as reported by CPANTS.
      (Mohammad S Anwar)
    * GH #1128: Shh warning if optional megasplat is not present.
      (David Precious)
    * GH #1139: Fix incorrect Content-Length header added by AutoPage
      handler (Michael Kröll, Russell Jenkins)
    * GH #1144: Change tt tags to span in skel (Jason Lewis)
    * GH #1046: "no_server_tokens" configuration option doesn't work.
      (Sawyer X)
    # GH #1155, #1157: Fix megasplat value splitting when there are empty
      trailing path segments. (Tatsuhiko Miyagawa, Russell Jenkins)
      NOTE: Paths matching a megasplat that end with a '/' will now include
      an empty string as the last value. For the route pattern '/foo/**',
      the path '/foo/bar', the megasplat gives ['bar'], whereas '/foo/bar/'
      now gives ['bar','']. Joining the array of megasplat values will now
      always be the string matched against for the megasplit.

    [ DOCUMENTATION ]
    * GH #1119: Improve the deployment documentation. (Andrew Beverley)
    * GH #1123: Document import of utf8 pragma. (Victor Adam)
    * GH #1132: Fix spelling mistakes in POD (Gregor Herrmann)
    * GH #1134: Fix spelling errors detected by codespell (James McCoy)
    * GH #1153: Fix POD rendering error. (Sawyer X)

    [ ENHANCEMENTS ]
    * GH #1129: engine.logger.* hooks are called around logging a message.
      (Russell @veryrusty Jenkins)
    * GH #1146: Cleaner display of error context (Vernon Lyon)
    * GH #1085: Add consistent keywords for accessing headers;
      'request_header' for request, 'response_header', 'response_headers'
      and 'push_response_header' for response. (Russell @veryrusty Jenkins)
    * GH #1010: New Dancer2::Plugin architecture, includes support for
      plugins using other plugins. (Yanick Champoux, Russell Jenkins,
      Sawyer X, Damien Krotkine, Stefan @racke Hornburg, Peter Mottram)
      Note: Considerable effort has gone into working with the authors
      of existing plugins to ensure their plugins are compatible with both
      the 'old' and the new reworked plugin architecture. Please upgrade
      your plugins to a recent release.
      (Special thanks to Peter @SysPete Mottram)

0.166001  2016-01-22 07:54:46+01:00 Europe/Amsterdam

    [ BUG FIXES ]
    * GH #1105, #1106, #1108: Autopage + Template Toolkit broke in last
      release. (Kaitlyn Parkhurst @symkat, Russell Jenkins)

0.166000  2016-01-12 19:01:51+01:00 Europe/Amsterdam

    [ BUG FIXES ]
    * GH #1013, #1092: Remove race condition caused by caching available
      engines. (Sawyer X, Menno Blom, Russell Jenkins)
    * GH #1089: Exact macthing of route regex comments for tokens/splats.
      (Sawyer X)
    * GH #1079, #1082: Allow routes to return '0' as response content,
      and serializer hooks are called when default response content is
      to be returned. (Alberto Simões, Russell Jenkins)
    * GH #1093, 1095: Use a dynamic TT2 INCLUDE_PATH to allow relative
      views with relative includes; fixing regression introduced by #1037.
      (Russell Jenkins)
    * GH #1096, #1097: Return compatibility on Perl 5.8.x!
      (Peter Mottram - @SysPete)

    [ DOCUMENTATION ]
    * GH #1076: Typo in Dancer2::Core::Hook POD. (Jonathan Scott Duff)

    [ ENHANCEMENTS ]
    * GH #1074: Add sample session engine config to skeleton app.
      (Peter Mottram - @SysPete)
    * GH #1088: Return route objects when defining new routes.
      (Sawyer X)

0.165000  2015-12-17 09:19:13+01:00 Europe/Amsterdam

    [ BUG FIXES ]
    * Revert session_name change, as this would invalidate all existing
      changes. We will need to rethink this change.
      (Stefan @racke Hornburg, Sawyer X)

0.164000  2015-12-16 23:42:24+01:00 Europe/Amsterdam

    [ DOCUMENTATION ]
    * Update core team members and contributors list. (Russell Jenkins)
    * GH #1066: Fix typo in Cookbook. (gertvanoss)
    * Correct typo. It's "query_parameters", not "request_parameters".
      Thanks to mst for letting me know and making sure I fix it!
      (Sawyer X)

    [ BUG FIXES ]
    * GH #1040: Forward with a post body no longer tries to re-read body
      filehandle. (Bas Bloemsaat)
    * GH #1042: Add Diggest::SHA as explicit prequisite for installs on
      perl < v5.9.3. (Russell Jenkins)
    * GH #1071, #1070: HTML escape the message in the default error page.
      (Peter Mottram)
    * GH #1062, #1063: Command line interface didn't support
      "-s SKELETON_DIRECTORY" in any order.
      (Nuno Carvalho)
    * GH #1052, #1053: Always call before_serializer hook when serializer
      is set.
      (Mickey Nasriachi)
    * GH #1034: Correctly use different session cookie name for Dancer2.
      (Jason A. Crome)
    * GH #1060: Remove trailing slashes when providing skeleton
      directory.
      (Gabor Szabo)

    [ ENHANCEMENTS ]
    * Use Plack 1.0035 to make sure you only have HTTP::Headers::Fast
      in the Plack::Request object internally.
    * GH #951 #1037: Dancer2::Template::TemplateToolkit no longer sets TT2
      INCLUDE_PATH directive, allowing `views` setting to be non-absolute
      paths. (Russell Jenkins)
    * GH #1032 #1043: Add .dancer file to new app scaffolding.
      (Jason A. Crome)
    * GH #1045: Small cleanups to Request class. (Russell Jenkins)
    * GH #1033: strict && warnings in Dancer2::CLI. (Mohammad S Anwar)
    * GH #1052, #1053: Allow before_serializer hook to change the content
      using @_.
      (Mickey Nasriachi)
    * GH #1060: Ignore .git directory when using an external skeleton
      directory.
      (Gabor Szabo)
    * GH #1060: Support more asset file extensions. (Gabor Szabo)
    * GH #1072: Add request->is_options(). (Theo van Hoesel)

0.163000  2015-10-15 12:47:57+02:00 Europe/Amsterdam

    [ DOCUMENTATION ]
    * GH: #1030: Fix pod references pointing to Dancer package
      (Mohammad S Anwar, Russell Jenkins)

0.162000_01 2015-10-13 17:05:09+02:00 Europe/Amsterdam (TRIAL RELEASE)

    [ BUG FIXES ]
    * GH #996: Fix warning with optional arguments. (Bas Bloemsaat)
    * GH #1001: Do not trigger an internal error on 404. (Russell Jenkins)
    * GH #1008,#976: Hack to quiet warning while plugins
      architecture is being rewritten. (Russell Jenkins)
    * Use Safe::Isa when calling their functions in the respected eval.
      (Sawyer X)

    [ ENHANCEMENTS ]
    * GH #738, #740, #988: route_parameters, query_parameters, and
      body_parameters keywords added, providing Hash::MultiValue objects!
      (Sawyer X)
    * #941, #999: delayed() keyword now has "on_error" option for controlling
      errors.
      (Sawyer X)
    * dancer2 app now support -s switch to supply an app skeleton
      (Nuno Carvalho)
    * "perl_version" token in templates now uses $^V, not $]. (Sawyer X)
    * GH #966: Remove Dist::Zilla::Plugin::AutoPrereqs. (Vernon)
    * GH #992: Deprecate creating route named placeholders ":captures"
      and ":splat". (Sawyer X)
    * Bump Moo requirement to 2.000000. (Alberto Simões)
    * GH #1012: Add :nopragmas import flag. (Sawyer X)

    [ DOCUMENTATION ]
    * GH #974: Use correct classname. (Sawyer X)
    * GH #958: Fix manual example with loading additional routes. (Sawyer X)
    * GH #960: Fix a few links. (Sawyer X)
    * Document you can install Scope::Upper for greater speed. (Sawyer X)
    * GH #1000: Correct POD name for Dancer2::Manual::Deployment.
      (Jason A. Crome)
    * GH #1017: Fix instructions on running app.psgi. Highlight
      beginner-friendly application running instructions. (Jason Crome)
    * GH #920, #1020: Remove deprecated functionality from example plugin.
      (Jason Crome)
    * GH #1002: Correct execute_hook() call in plugins documentation.
      (Jason Crome)
    * Expand on auto-reloading options using Plack Shotgun loader.
      (Jason Crome, @girlwithglasses)
    * GH #1024: Document the need to define static_handler when changing
      the public_dir option. (Sébastien Deseille)

0.162000  2015-09-06 13:08:05+02:00 Europe/Amsterdam

    [ BUG FIXES ]
    * Not exactly bug fix, but now captures() always returns hashref.
      (Sawyer X)
    * GH #931: Using params() keyword, route parameters now override body
      parameters which override query parameters. (Sawyer X)

    [ ENHANCEMENTS ]
    * Small speed bump: use eval{} instead of Try::Tiny. (Sawyer X)

    [ DOCUMENTATION ]
    * Replace File::Slurp with File::Slurper in tutorial.
      (Nick Tonkin)

0.161000_01 2015-08-28 15:29:00+02:00 Europe/Amsterdam

    [ BUG FIXES ]
    * GH #947, #948: Escape file paths in regex patterns. (A. Sinan Unur)
    * GH #944: Setting response content in before hook when a serializer
      is set no longer triggers an error.
      (Russell Jenkins, Dmitrii Tcyganov)
    * GH #965: Remove non-existant role from Response::Delayed.
      (Vernon, Russell Jenkins)
    * GH #971: Route options matching no longer uses each iterator.
      (Tina Müller)
    * GH #959: Custom error template rendering fixed. (Russell Jenkins)
    * GH #961: Render custom error templates in before hooks. (Russell Jenkins)
    * GH #978: Tests - fix response regex after html_encode (Vernon)
    * GH #972: Exceptions thrown by serializers no longer masked.
      (Russell Jenkins)

    [ DOCUMENTATION ]
    * GH #967: Fix upload example. (Alberto Simões)
    * GH #881: Add cookie timeout example. (Andy Beverley)
    * GH #963: Document all available template tokens. (Sawyer X)

    [ ENHANCEMENTS ]
    * Optimize the s*#t out of basic routing. Faster than Dancer 1 now.
      (Sawyer X)
    * Only load HTTP::Server::PSGI when asked to start a development
      server not under Plack. (Sawyer X, Mickey Nasriachi)
    * GH #949: Produce cleaner, non-verbose test output (Vernon)
    * GH #950: Decode characters in param keys (Patrick Zimmermann)
    * GH #914: Include stack trace on default error page when
      show_errors is true. (Vernon)
    * GH #980, #981: halt keyword sets response content if provided,
      as Dancer 1 does. (Achilles Kars)
    * GH #909, #957, #983: HTML5 templates in generated apps and
      default error template (Gabor Szabo, Kadir, Vernon)
    * GH #972, #719, #969, #644, #647: Streamline serializer helpers.
      to_json/from_json now faster. (Russell Jenkins)

0.161000  2015-07-08 14:57:16+02:00 Europe/Amsterdam

    [ BUG FIXES ]
    * GH #915, #930: Check existence of optional extension headers when
      behind proxy. (Andy Beverley, Pedro Melo, Russell Jenkins)
    * GH #926, #940: Set session directory default to $apprdir/session.
      (Russell Jenkins)
    * GH #936, #939: Use the error_template configuration on a 404.
      (Russell Jenkins)
    * GH #844, #937: Non-hash serialized params do not cause a crash. (Sawyer X)
    * GH #943: Pass @_ to UNIVERSAL's VERSION so it validates version number.
      (Sawyer X)
    * GH #934: Cleanup internals in the old Dispatcher. (Russell Jenkins)

    [ DOCUMENTATION ]
    * Sanitize Changes
    * GH #938: Fix POD link to params keyword. (Ludovic Tolhurst-Cleaver)
    * GH #935: Provide more details and considerations when using
      behind_proxy. (Andy Beverley)

    [ ENHANCEMENT ]
    * GH #933: use note in tests to produce cleaner non-verbose output (Vernon)
    * Remove unnecessary dependencies: build chain should be smaller. (Sawyer X)
    * No need for Module::Build. (Sawyer X)
    * GH #911: Dancer2 request object is now a subclass of Plack::Request.
      It's also much faster now. (Sawyer X)

0.160003  2015-06-06 11:09:00+02:00 Europe/Amsterdam

    [ BUG FIXES ]
    * GH #921, #922: Plack >= 1.0035. (Russell Jenkins, Alberto Simões)

    [ ENHANCEMENT ]
    * #922: Use HTTP::Headers::Fast in request and response objects
    (Russell Jenkins)

0.160002  2015-06-04 13:03:38+02:00 Europe/Amsterdam

    [ BUG FIXES ]
    * GH #920: Sanitize session IDs in file-based sessions.
      (Russell Jenkins, Andrew Beverley)

    [ DOCUMENTATION ]
    * GH #908: Cleanup Dancer references in DBIC section of cookbook
      (Julien Fiegehenn)
    * GH #910: Misc spelling and grammar fixes (Gregor Herrmann)
    * GH #916: Fix test example. (Peter Mottram - @SysPete)
    * GH #912, #913: Fix documentation on when stacks are printed.
      (Andrew Solomon)

0.160001  2015-05-14 20:40:10+02:00 Europe/Amsterdam

    [ BUG FIXES ]
    * GH #893, #895: Catch config parse errors when Config::Any doesn't throw
      them. (Russell Jenkins)
    * GH #899: Minimum YAML version supported is v0.86 (Shlomi Fish)
    * GH #906: send_file - missing import and fix logic error for streaming
      by default (Russell Jenkins)

    [ DOCUMENTATION ]
    * GH #897: Remove docs for unimplemented 'load' keyword (Fayland Lam)

    [ ENHANCEMENT ]
    * GH #894, #898: Add status and headers methods to ::Response::Delayed
      (Yanick Champoux, Charlie Gonzalez)

0.160000  2015-04-27 00:12:55+02:00 Europe/Amsterdam

    [ BUG FIXES ]
    * GH #868: Fix incorrect access name in $error->throw. (cdmalon)
    * GH #879, #883: Fix version numbering in packaging and tests.
      (Russell Jenkins)
    * File serving (send_file) won't call serializer. (Russell Jenkins)
    * GH #892, #510: Workaround for multiple plugins with hooks.
      (Russell Jenkins, Alberto Simões)
    * GH #558: Remove "prefix" inconsistency with possibly missing postfixed
      forward slash. (Sawyer X)

    [ DOCUMENTATION ]
    * GH #816, #874 Document session engine changes in migration documentation.
      (Chenchen Zhao)
    * GH #866, #870: Clarify that you cannot forward to a static file, why,
      and two different ways of accomplishing it without forward.
      (Sakshee Vijayvargia)
    * GH #878: Rework example for optional named matching due to operator
      precedence. (Andrew Solomon)
    * GH #844: Document Simple session backend is the default. (Sawyer X)

    [ ENHANCEMENT ]
    * GH #869: Streaming file serving (send_file). (Russell Jenkins)
    * GH #793: "prefix" now supports the path definition spec. (Sawyer X)
    * GH #817, #845: Route spec under a prefix doesn't need to start with
      a slash (but must without a prefix).
      (Sawyer X, Russell Jenkins)
    * GH #871: Use Safe.pm instead of eval with Dancer2::Serializer::Dumper.
      (David Zurborg)
    * GH #880: Reduce and cleanup different logging calls in order to handle
      the stack frames traceback for logging classes. (Russell Jenkins)
    * GH #857, #875: When failing to render in Template::Toolkit, make the
      error reflect it's a TT error, not an internal one.
      (valerycodes)

0.159003  2015-03-23 14:57:15+01:00 Europe/Amsterdam

    [ BUG FIXES ]
    * Fixed another memory leak with compiled hooks. (Sawyer X)
    * Fixed a memory leak with conditionally applied static middleware
      (Russell Jenkins)

    [ DOCUMENTATION ]
    * GH #854, #858: Fix after_template_render hook example. (Adam Weinberger)
    * GH #861: Improve documentation of 'forward'. (Andy Beverley)

0.159002  2015-03-03 19:21:21+01:00 Europe/Amsterdam

    [ BUG FIXES ]
    * GH #856: Memory leak when throwing exception from a hook. (Sawyer X)

0.159001  2015-02-25 15:31:35+01:00 Europe/Amsterdam

    [ BUG FIXES ]
    * GH #855: Ensure Dancer2::Test is compatible with Pod::Simple 3.30.
      (Russell Jenkins)

    [ DOCUMENTATION ]
    * Add an example for delayed (async) streaming response. (Sawyer X)
    * Small link fix. (Sawyer X)

0.159000  2015-02-24 04:51:20+01:00 Europe/Amsterdam

    [ BUG FIXES ]
    * GH #762: Delay app cleanup until errors are rendered. (Russell Jenkins)
    * GH #835: Correct Logic error in Logger if no request exists.
               (Lennart Hengstmengel)
    * GH #839: Correct "no_server_tokens" definition in production.yml.
               (Nikita K)
    * GH #853, #852: Handle malformed (contentless) cookies. (pants)
    * GH #840, #842: Ensure session data available to template engines.
                     (Russell Jenkins)
    * GH #565, #847, #849: Fix HTTP Status template logic and documentation.
                           (Daniel Muey, Russell Jenkins, Dávid Kovács)
    * GH #843: Add missing attributes to Moo class used in tests. (Graham Knop)

    [ ENHANCEMENT ]
    * GH #836: Support delayed (asynchronous) responses!
               ("Delayed responses" in Dancer2::Manual for more information.)
               (Sawyer X)
    * GH #824: Use Plack::MIME by default, MIME::Types as failback if available.
               (Alberto Simões)
    * GH #792, #848: Keywords can now use prototypes.
                     (Russell Jenkins, Sawyer X)

    [ DOCUMENTATION ]
    * GH #837, #838, #841: Major documentation restructure. (Snigdha Dagar)
      (Check eb9416e9 and a78e27d7 for more details.)
    * GH #823: Cleanup Manual and Cookbook docs. (Omar M. Othman)
    * GH #828: Provide README.mkdn. (Nuno Carvalho)
    * GH #830: Fix typo in Session::YAML pod. (Vince W)
    * GH #831,#832: Fix broken link in Session::YAML pod. (Vince W)

0.158000  2015-01-01 18:08:04+01:00 Europe/Amsterdam

    ** Happy new year! **

    [ ENHANCEMENT ]
    * GH #778: Avoid hard-coded static page location. (Dávid Kovács)
    * Speed up big uploads significantly. (Rick Myers)
    * GH #821: Use Import::Into to import pragmas. (Russell Jenkins)
    * GH #782: Fix utf8 pragma import. (Maxim Vuets)
    * GH #786: Perlbrew fix. (Dávid Kovács)
    * GH #622: Refactoring. (James Raspass)

    [ DOCUMENTATION ]
    * GH #713: Change order of statements in Cookbook to not imply that
      D2::P::Ajax::ajax() calls have priority. (Sawyer X)

0.157001  2014-12-21 20:40:13+01:00 Europe/Amsterdam

    [ ENHANCEMENT ]
    * GH #814, #815: Rename "app.pl" to "app.psgi". (Sawyer X)

0.157000  2014-12-14 18:23:33+01:00 Europe/Amsterdam

    [ BUG FIXES ]
    * GH #799: Set current request earlier so log formats using requests
      will work. (Sawyer X)
    * GH #650: Provide default environment to app for templating.
      (Dávid Kovács, Chi Trinh)
    * GH #800: Better portability code, for different Windows situations.
      (Christian Walde)
    * Less littering of the test directories with session files. (Sawyer X)

    [ ENHANCEMENT ]
    * GH #810: strict && warnings in the app.pl. (Sawyer X)
    * Use to_app keyword in skeleton. (Sawyer X)
    * GH #801: Under production, server tokens are disabled. (Sawyer X)
    * GH #588, #779: Remove LWP::UserAgent in favor of HTTP::Tiny.
      (Dávid Kovács, simbabque, Sawyer X)
    * Remove all usages of Test::TCP in favor of Plack::Test. (Sawyer X)

    [ DOCUMENTATION ]
    * GH #802: Remove indication of warnings configuration option
      and add explanation in migration document. (Sawyer X)
    * GH #806: Link in main docs to the migration document. (Gabor Szabo)
    * GH #807: Update migration document with more session data,
      changes to app.pl, and Template::Toolkit configuration. (Gabor Szabo)
    * GH #813: Update migration document with information on encoding and
      usage of Plack::Request internally. (Gabor Szabo, Sawyer X)

0.156001  2014-12-08 23:03:43+01:00 Europe/Amsterdam

    [ DOCUMENTATION ]
    * Documentations suggested serializers aren't consistent. We fixed it
      so we make sure docs reflect that. (Sawyer X)

0.156000  2014-12-07 18:04:14+01:00 Europe/Amsterdam

    [ BUG FIXES ]
    * Do not try to deserialize empty content.
      (Lennart Hengstmengel, Sawyer X)
    * Do not call serialization hooks when no serialization took place.
      (Sawyer X)
    * Be more cautious on undef output from serializer.
      (Daniel Böhmer, Sawyer X)

    [ ENHANCEMENTS ]
    * Add cpanfile when scaffolding a new app.
      (Dávid Kovács, Sawyer X)
    * Response "content" attribute no longer stringifies. This should help
      reduce warnings, odd debugging problems, etc. (Sawyer X)
    * DSL "uri_for" no longer returns URI object. Instead just the URI.
      (Sawyer X)

    [ DOCUMENTATION ]
    * GH #777: Fix doc for mentioning public dir.
      (Dávid Kovács, Sawyer X)
    * GH #787: Document all environment variables. (Sawyer X)

0.155004  2014-12-04 11:51:23+01:00 Europe/Amsterdam

    [ BUG FIXES ]
    * Guard against content length being empty strings. This is really
      bizarre case but saw it once. (Sawyer X)

0.155003  2014-12-03 22:32:12+01:00 Europe/Amsterdam

    [ BUG FIXES ]
    * GH #798: More test fixes on Windows. (A. Sinan Unur)

0.155002  2014-12-02 22:59:32+01:00 Europe/Amsterdam

    [ BUG FIXES ]
    * Fix test on Windows. (A. Sinan Unur)

0.155001  2014-11-28 17:42:24+01:00 Europe/Amsterdam

    [ BUG FIXES ]
    * Small typo in test. (Dávid Kovács)

0.155000  2014-11-28 01:18:39+01:00 Europe/Amsterdam

    [ BUG FIXES ]
    * GH #773, #775: AutoPage handler no longer renders layouts.
      (Dávid Kovács, Sawyer X)
    * GH #770: Prevent crazy race condition between the logger engine and
      other engines. This means engines now call "log_cb" to log.
      (Sawyer X)
    * App now has default name to caller package. (Sawyer X)
    * Serializers will not try to serialize empty content. (Sawyer X)
    * Lots of cleanups in Core::Request in favor of Plack::Request.
      (Sawyer X)

    [ ENHANCEMENTS ]
    * Layouts directory can be configured using 'layout_dir'.
      (Sawyer X)
    * GH #648, #760: Logger format now supports 'h', 'u', 'U', 'h', 'i'.
      They are documented but weren't really available.
      (Lennart Hengstmengel)
    * Serializers having errors will not fail if there is no logger.
      (Sawyer X)
    * Create a request object with a single argument of $env, like
      Plack::Request. (Sawyer X)

    [ DOCUMENTATION ]
    * Remove documented hack for static content because we use the middleware
      now anyway. (Sawyer X)
    * Document further the difference between splat and megasplat.
      (Dávid Kovács)

0.154000  2014-11-17 15:36:31+01:00 Europe/Amsterdam

    [ BUG FIXES ]
    * GH #744: Serialize anything, not just references. (Sawyer X)
    * GH #744: Serialize regardless of content_type of serializer. (Sawyer X)
    * GH #764: Catch template render errors. (Russell Jenkins, Steven Humphrey)
    * Calling uri_for(undef) doesn't crash. (Sawyer X)
    * GH #732: Correct name for 403 (Forbidden, not Unauthorized).
      (Theo van Hoesel, Sawyer X, Mickey Nasriachi, Omar M. Othman)
    * GH #753: Syntax of parameterized types. (Russell Jenkins)
    * GH #734: Failing tests on Windows. (Russell Jenkins, Sawyer X)

    [ ENHANCEMENTS ]
    * GH #664, #684, #715: Handler::File replaced for static files with
      Plack::Middleware::Static, allowing files to be served *before* routes.
      This means hooks do not apply to static files anymore!
      (Russell Jenkins, DavsX)
    * Engines now have "logger" attribute to log errors. It sends the
      Dancer2::Logger:: object, if one exists. (Sawyer X)
    * Serializers do not need to implement "loaded" method. (Sawyer X)
    * GH #733: In core: response_xxx removed in favor of generic
      standard_response. (Sawyer X, Mickey Nasriachi, Omar M. Othman)
    * GH #514, #642, #729: Allow mixing named params, splat, and
      megasplat. (Russell Jenkins, Johan Spade, Dávid Kovács)
    * GH #596: no_server_tokens works, as well as DANCER_NO_SERVER_TOKENS.
      (Omar M. Othman, Sawyer X, Mickey Nasriachi)
    * GH #639: Validate engine types in configuration.
      (Sawyer X, Omar M. Othman, Mickey Nasriachi, Russell Jenkins)
    * GH #663, #741: Remove "accept_type" attribute and other references.
      (Mickey Nasriachi, Theo van Hoesel)
    * GH #748: Provide forwarded_host, forwarded_protocol. (Sawyer X)
    * GH #748: Do not provide a default env, require env for a request.
      (Sawyer X)
    * GH #742: Update test skeleton to use to_app. (Dávid Kovács)
    * GH #636: Use Plack::Test in more tests. (Dávid Kovács)

    [ DOCUMENTATION ]
    * GH #656: Dancer2::Manual::Testing as a guide for testing Dancer2
      applications. (Sawyer X)
    * Improved documentation of route matching. (Russell Jenkins)
    * Migration document update relating to enhancements.
      (Sawyer X, Mickey Nasriachi)
    * GH #731: Document changes in session.
      (racke, Sawyer X, Mickey Nasriachi, Omar M. Othman)
    * Document "id" attribute in Request object. (Sawyer X)
    * Correct Cookbook examples on command line scripts. (Sawyer X)

0.153002  2014-10-30 09:23:52+01:00 Europe/Amsterdam

    [ BUG FIXES ]
    * GH #734: More failing tests. (Sawyer X)

0.153001  2014-10-27 12:39:54+01:00 Europe/Amsterdam

    [ BUG FIXES ]
    * GH #734: Failing tests on Windows. (Sawyer X)

    [ DOCUMENTATION ]
    * GH #724: Plack::Test example in Dancer2::Test. (Jakob Voss)

0.153000  2014-10-23 23:45:36+02:00 Europe/Amsterdam

    [ BUG FIXES ]
    * GH #634, #687: Fix file logger defaults.
      (Russell Jenkins, Dávid Kovács, Sawyer X)
    * GH #730: Make App use app-level config for behind_proxy. (Sawyer X)
    * GH #727: Disable regex metachars when calculating app location in tests
      (Gregor Herrmann)
    * GH #681, #682, #712: Clear session engine within destroy_session.
      (DavX, Russell Jenkins)
    * Ignore :tests in importing, don't suggest :script. (Sawyer X)

    [ ENHANCEMENT ]
    * Internal: Move the implementation of send_file from DSL to App.
      (Russell Jenkins)

    [ DOCUMENTATION ]
    * GH #728: Typos in Policy document. (Olaf Alders, Sawyer X)

0.152000  2014-10-14 04:30:59+02:00 Europe/Amsterdam

    [ BUG FIXES ]
    * GH #723: Redispatched requests lose data. (Sawyer X)

    [ ENHANCEMENT ]
    * Provide 'content' keyword to set the response content. (Sawyer x)
    * GH #616, #155, #615: Session engines are now lazy! (Russell Jenkins)
    * Dancer2 response objects can be created from arrays or from
      Plack::Response objects. (Sawyer X)
    * GH #718: Clean up App's Template engine. (Russell Jenkins)
    * Adding class-based tests. (Sawyer X)

    [ DOCUMENTATION ]
    * Add a policy document under Dancer2::Policy. (Sawyer X)
    * Document log_format instead of logger_format. (Sawyer X)

0.151000  2014-10-08 21:49:06+02:00 Europe/Amsterdam

    [ ENHANCEMENT ]
    * Apps are now a proper independent PSGI application. Forwarding
      and passing requests between apps will still work if you use the
      'Dancer2->psgi_app' method without providing a class, but it might
      still be phased out in the future.
      (Sawyer X)

    [ DOCUMENTATION ]
    * Migration document! (Snigdha Dagar)
    * GH #667: Fix typo in cookbook pod. (Lindsey Beesley)
    * GH #649, #670: Document core logger. (simbabque)
    * GH #689: Git guide markdown fixes. (Paul Cochrane)
    * GH #690, #691, #694, #696, #698, #699, #700, #702, #703,
         #704, #705, #706, #707, #708, #710: Doc cleanups.
      (Paul Cochrane)
    * GH #688: Improve testing documentation. (Paul Chochrane)
    * GH #692: Document serving static files using
      Plack::Middleware::Static. (Dávid Kovács @DavsX)
    * GH #695: Correct Dancer2::Logger::Capture, add test example.
      (Dávid Kovács @DavsX)
    * GH #716: Correct document on proxy procotol forwarding
      in Apache. (Andy Beverley)

0.150000  2014-08-17 01:35:16CEST+0200 Europe/Amsterdam

    [ DOCUMENTATION ]
    * GH #657: Update multi-app example in cookbook to include route
      merging. (Bas Bloemsaat)
    * GH #643: Improve session factory docs by mentioning Dancer2::Config.
      (Andy Jack)

    [ BUG FIXES ]
    * Postponed hooks are no longer sent to all Apps.
      (Sawyer X, Mickey Nasriachi)
    * 404 File Not Found Application reworked to stay up to date with
      postponed hooks merging in multiple apps. (Russell Jenkins)
    * GH #610, #662: Removed two circular references memory leaks!
      (Russell Jenkins)
    * GH #633: Log an error when a hook dies. (DavsX)

    [ ENHANCEMENT ]
    * Allow settings apps in the psgi_app() call by name or regex.
      (Sawyer X)
    * GH #651: silly typo in clearer method name (DavsX).

0.149000_02 2014-08-10 13:50:39CEST+0200 Europe/Amsterdam

    [ ENHANCEMENT ]
    * GH #641: Adding a shim layer to prevent available hooks (and
      thus plugins) from breaking.
    * Each App can now define its own configuration. The Runner's
      application-specific configure has been untangled.
      (Russell @veryrusty Jenkins, Sawyer X, Mickey Nasriachi)
    * Multiple Dancer App support. You can now create a App-specific
      PSGI application using MyApp->psgi_app.
      (Russell @veryrusty Jenkins, Sawyer X, Mickey Nasriachi)
    * Add routes and hooks to an existing app on import.
      (Russell @veryrusty Jenkins, Stevan Humphrey, Stefan racke
      Hornburg, Jean Stebens, Chunzi, Sawyer X, Mickey Nasriachi)
    * Allow DSL class to be specified in configuration file.
      (Stevan Humphrey)
    * forward() now returns a new request which is then just runs
      the dispatching loop again. (Sawyer X, Mickey Nasriachi)

    [ BUG FIXES ]
    * GH #336: Set log level correctly.
      (Andrew Solomon, Pedro Bruno)
    * GH #627, #607: Remove potential context issues with returning
      undef explicitly. (Javier Rojas)
    * GH #646: Fix whitespacing for tests. (DavsX)

0.149000_01 2014-07-23 21:31:21CEST+0200 Europe/Amsterdam

    *************************** NOTICE ***************************
    * This very is a major upgrade                               *
    * We untangled the context, DSL implementation a bit         *
    * Please check your code, including your plugins, thoroughly *
    * Thank you                                                  *

    [ ENHANCEMENTS ]
    * GH #589: Removing Dancer2::Core::Context global context variable.
      Finally in.
      (Sawyer X, Mickey Nasriachi, Russell @veryrusty Jenkins)

    [ BUG FIXES ]
    * GH #606, #605: Fix for setting public directory.
      (Ivan Kocienski, Russell Jenkins, Stefan @racke Hornburg)
    * GH #618, #620: Fix jQuery link generated by CLI skeleton.
      (Michał Wojciechowski)
    * GH #589: Major memory leak fix by removal of Dancer2::Core::Context.

    [ ENHANCEMENTS ]
    * GH #620: Bump jQuery to 1.11.1. (Michał Wojciechowski)

0.143000  2014-07-05 21:39:28CEST+0200 Europe/Amsterdam

    [ BUG FIXES ]
    * GH #538, #539: Coerce propogated exceptions to strings within Error object.
      (Steven Humphrey)
    * GH #531: Generate valid HTML when show_errors is true from Error objects.
      (Steven Humphrey)
    * GH #603: Update skeleton test to use Plack::Test. (Sawyer X)

    [ ENHANCEMENTS ]
    * Provide psgi_app in top-level Dancer.pm to make it easier to change it.
      (Sawyer X)

0.142000  2014-06-24 15:16:42CEST+0200 Europe/Amsterdam

    [ BUG FIXES ]
    * GH #550, #555: Allow the content type to be set when using send_file
      as per the documentation. (Russell Jenkins, Steven Humphrey)

    [ ENHANCEMENTS ]
    * GH #512, #520, #602: Pass all settings into JSON serializer engine.
      (Jakob Voss, Russell Jenkins)
    * GH #532: Serialize runtime errors such as those produced by die if a
      serializer exists. (Steven Humphrey)

0.141000  2014-06-08 22:27:03CEST+0200 Europe/Amsterdam

    * No functional changes.

0.140900_01 2014-06-07 23:32:56IDT+0300 Asia/Jerusalem

    [ BUG FIXES ]
    * GH #447: Setting the apphandler now triggers the Dancer Runner
      configuration change, which works. (Sawyer X)
    * GH #578: Remove the default engine configurations. (Sawyer X)
    * GH #567: Check for proper module names in loading engines. Might help
      with taint mode. (Sawyer X)
    * GH #585, #595: Return 405 Method Not Allowed instead of 500.
      (Omar M. Othman)
    * GH #570, #579: Ensure keywords pass, send_error and send_file
      exit immediately when executed. (Russell Jenkins)

    [ ENHANCEMENTS ]
    * GH #587: Serializer::Mutable alive! (Pedro Bruno)

    [ DOCUMENTATION ]
    * Fix doc for params(). Ported from Dancer#1025 (Stefan Hornburg)

0.140001  2014-05-01 10:49:25CEST+0200 Europe/Amsterdam

    [ BUG FIXES ]
    * Bugfix for extracting multiple cookies within a request.
      (Cymon, Russell Jenkins)
    * Require minimum version of Plack to make sure we can add the Head
      middleware. Not exactly a bug, but not a feature. (Sawyer X)

    [ DOCUMENTATION ]
    * Correct reference to HTTP::Server::Simple::PSGI. (Russell Jenkins)

0.140000  2014-04-28 23:14:31CEST+0200 Europe/Amsterdam

    [ ENHANCEMENTS ]
    * Replace Config role with better ConfigReader role.
      (Mickey Nasriachi, Stefan Hornburg, Sawyer X)
    * Move App-related attributes (engines) to App instead of config role.
      (Mickey Nasriachi, Stefan Hornburg, Sawyer X)
    * Untangle Runner-Server (removing Server entirely).
      (Mickey Nasriachi, Stefan Hornburg, Sawyer X)
    * Replace HTTP::Server::Simple::PSGI with HTTP::Server::PSGI.
      (Mickey Nasriachi, Stefan Hornburg, Sawyer X)
    * GH #527: Build request cookie objects from request headers, not env.
      (Russell Jenkins)
    * GH #569: Transform cookie using the HTTP_COOKIE header, per PSGI spec.
      (Russell Jenkins)
    * GH #559, #544: Use Plack middleware for HEAD request content removal.
      (Russell Jenkins)
    * GH #513, #483: Deserialize body content for DELETE requests.
      (Russell Jenkins, Yanick Champoux, Sawyer X)

0.13      2014-04-13 19:19:44CEST+0200 Europe/Amsterdam

    [ ENHANCEMENTS ]
    * GH #562: Change YAML::Any to YAML (Steven Humphrey, Russell Jenkins).

    [ BUG FIXES ]
    * GH #524: Double encoding for YAML sessions.
    * GH #557: Switch to using YAML::Old.
    * GH #548: Deserializer test failure.

0.12      2014-04-07 22:42:12 Europe/Amsterdam

    [ ENHANCEMENTS ]
    * GH#518: Bump jQuery to 1.10.2 (Grzegorz Rożniecki).
    * GH#535: Support OPTIONS and PATCH requests in Server::Standalone.
      (Russell Jenkins)
    * GH#553: Dancer2 CLI: specify directory to write app skeleton
      (Jean Stebens)
    * GH#543: Additional HTTP Methods for Ajax plugin (Jean Stebens).

    [ DOCUMENTATION ]
    * RT#91428: POD encoding set to UTF-8 in main .pm (Gregor Herrmann).
    * GH#517: Miscellaneous documentation fixes (Cesare Gargano).
    * GH#518: "Getting started" demo page fixes (Grzegorz Rożniecki).
    * GH#522: s/PerlHandler/PerlResponseHandler/ in Apache2 sample configuration
      (Grzegorz Rożniecki)
    * GH#521: Remove duplicated POD and clean up list details (Shlomi Fish)
    * GH#526: Cleanup POD formating and code snippets in manual.
      (Grzegorz Rożniecki)

    [ BUG FIXES ]
    * GH#528,529: Force PSGI server in dispatch scripts for CGI or fcgi
      deployments (Erik Smit, Alberto Simões)
    * GH#550,GH#551: Update all headers in Handler::File
      (Sawyer X, Stefan @racke Hornburg)
    * GH#540: Fix hook execution when default scalar was used in hook code.
      (baynes, Russell Jenkins)
    * GH#552: Rework test suite to use Plack::Test
      (Sawyer X, Stefan @racke Hornburg)
    * GH#560: Return value of hooks do not alter response content.
      (Jean Stebens)

0.11      2013-12-15 14:19:22 Europe/Amsterdam

    [ ENHANCEMENTS ]
    * GH#481: Don't pollute @INC automatically when Dancer2 is imported, each
      runner is now responsible of including the local ./lib dir if needed.
    * GH#469, 418: Dancer2::Plugin provides a ':no_dsl' flag for modern Plugins
      (Pedro Melo)
    * GH#485: Keywords 'redirect' and 'forward' exit immediately when executed in
      a route/hook. New dependency on Return::MultiLevel (Russell Jenkins).
    * GH#495: Use accessor and predicates instead of direct access.
      Addresses GH#493 too. (Russell Jenkins)
    * GH#502,GH#472: Rework halt to use with_return from Return::MultiLevel.
      (Russell Jenkins)
    * GH#479,GH#480,GH#508: Pass parameters to params() in the DSL.
      (Slava Goltser, unickuity, Russell Jenkins)
    * GH#505: Fix empty HTTP_REFERER in Dancer::Core::Request (Menno Blom).
    * GH#503: Multiple reverse proxy support (Menno Blom).
    * GH#371,GH#506: CLI tool rewrite (using App::Cmd, supports plugins, etc.).
      (Ivan Kruglov, Samit Badle, Sawyer X)
    * GH#498: Add some missing items in MANIFEST.SKIP (Gabor Szabo, Sawyer X).

    [ DOCUMENTATION ]
    * GH#489: Remove link to Dancer2::Deployment pod which does not exist
      (Sweet-kid)
    * GH#511: s/Deflator/Deflater/; (Cesare Gargano)
    * GH#491: Updated config paths for template_toolkit in cookbook.
      (Mark A. Stratman)
    * GH#494: Update session config details (Dancer2::Config),
      namespace fixup in Dancer2::Core::cookie.
      (Russell Jenkins)
    * GH#470: Fix Plack::Builder mount usage (Pedro Melo).
    * GH#507: Fix plenty of typos (David Steinbrunner).
    * GH#477: Document problem with Plack Shotgun on Windows (Ahmad M. Zawawi).
    * GH#504: Add link to Dancer2::Plugin::Sixpack (Menno Blom).
    * GH#490: Document Dancer2 should be FatPackable (Sawyer X).
    * GH#452: Make a complete authors section, clean it up (Pau Amma).
    * More fixes to main documentation (Pau Amma).

0.10      2013-09-28 15:26:41 Europe/Paris

    [ DOCUMENTATION ]
    * GH#431: Miscellaneous documentation fixes (Gideon D'souza)
    * Small POD corrections (Ashvini V)

    [ ENHANCEMENTS ]
    * GH#482: Show the startup banner when the worker starts by default
      (Alexis Sukrieh).
    * GH#481: Include local lib dir in @INC by defaults (Alexis Sukrieh).
    * GH#423: Remove ':tests' from Dancer.pm import (Alberto Simões).
    * GH#422: Get rid of core_debug method (Alberto Simões).
    * GH#421: Support Plugin::Ajax content_type (Russell Jenkins).
    * GH#428: Make default errors CSS path relocatable (Russell Jenkins).
    * GH#427, GH#443: Replace global warnings with lexical (Russell Jenkins).
    * GH#374: Don't create an app from app.psgi (Alberto Simões).
    * Cleanup Core::Request, Core::Request::Upload (Mickey Nasriachi).
    * GH#445: Test Template::Simple (Alexis Sukrieh, Russell Jenkins).
    * GH#449: Test Session hooks (Gideon D'souza)
    * GH#434,440: Imutable attributes (Mickey Nasriachi).
    * GH#435: Allow send_error to serialize error (Russell Jenkins).
    * Add more tests to session id rw (Pedro Melo).
    * Whitespace cleanup (Ivan Bessarabov).

    [ BUG FIXES ]
    * GH#424,425: Fix logger tests for different timezones, and close
                  logfile before deleting it: Windows dixit.
                  (Gideon D'souza, Russell Jenkins)

0.09      2013-09-02 00:12:58 Asia/Jerusalem

    [ ENHANCEMENTS ]
    * Rewite DSL keyword engine (Mickey Nasriachi)
    * Require minimum Role::Tiny 1.003000 (Alberto Simões)
    * GH#382: Move Request attributes to params, and fix serializers
              behavior (Russell Jenkins)
    * GH#406: Replace Dancer2::ModuleLoader with Class::Load
              (Alberto Simões, Sawyer X)
    * GH#329: Remove 'load_app' DSL keyword. Remove reference to
              'load' as well. (Sawyer X)
    * GH#412: Autopages are now called properly with correct MIME.
              (Alberto Simões)

    [ DOCUMENTATION ]
    * GH#390: minor cookbook documentation fixes (Russell Jenkins)
    * GH#392: remove support to auto_reload and suggest alternative
      in Dancer2::Cookbook (Ahmad M. Zawawi)
    * GH#397,407: Miscellaneous documentation fixes (Andrew Solomon)
    * Documentation cleanups (Alex Beamish)

    [ BUG FIXES ]
    * When compiling route regex object with prefix, add the closing anchor
      (Mickey Nasriachi)
    * GH#386: honor log level defined in config file (Alberto Simões)
    * GH#396,409: Miscellaneous bug fixes (Russell Jenkins)
    * GH#403: Fix forward behavior (Russell Jenkins)

0.08      2013-08-18 15:22:45 Asia/Jerusalem

    [ ENHANCEMENTS ]
    * GH#352: Define content_type as a property for serializers. (Franck Cuny)
    * Cleanup duplicate HTTP status code between Core::Error and Core::HTTP
      (Russel Jenkins)
    * GH#363: Move core methods to Dancer2::Core (Alberto Simões)
    * GH#362: Serializers documentation and test cleanup. (Franck Cuny)
    * Refactoring of the engine method. (Franck Cuny)
    * Misc. code cleanup. (Russel Jenkins)
    * GH#280: Remove the unused ':syntax' importing tag (Sawyer X)
    * Display startup info only if environment is "development" (Franck Cuny)
    * Move postponed_hooks to server from runner (Sawyer X)
    * Provide easier access to global runner (Sawyer X)
    * Bunch of code cleanups which also includes speed boost (Sawyer X)
    * More immutability in the runner class and config role (Sawyer X)

    [ BUG FIXES ]
    * GH#85, GH#354: Fix autopages, especially in subdirs
      (Stefan Hornburg, Alberto Simões)
    * GH#365: Fix serializer settings (Steven Humphrey)
    * GH#333: callerstack for logger was too short (Alberto Simões)
    * GH#369: Move request deserialization from Dispatcher to Content & Request
      (Russell Jenkins)

    [ DOCUMENTATION ]
    * GH#192: Documentation the current usage of middlewares using
              Plack::Builder (Sawyer X)
    * GH#195, GH#197, GH#372: Multiple apps with Plack::Builder (Sawyer X)
    * GH#348: Documentation of Role::Logger (Franck Cuny)
    * GH#350: Move part of README.md to GitGuide.md (Franck Cuny)
    * GH#353: Documentation of Role::Serializer (Alberto Simões, Franck Cuny)
    * Misc. minor documentation tweak (Alberto Simões, Franck Cuny)

0.07      2013-08-04 01:14:59 Asia/Jerusalem

    [ ENHANCEMENTS ]
    * GH#344, GH#284: Now forward() calls preserve sessions (cym0n, Alberto Simões)
    * Separation of engines from triggers and configuration (Sawyer X, Franck Cuny)
    * GH#347: Remove old compatibility option 'log_path' (Franck Cuny)
    * GH#156, GH#250, GH#349: Remove unused module (Alberto Simões, mokko)
    * GH#331: Hook cleanups and documentation. (Franck Cuny)
    * GH#335: Serializing cleanup. (Franck Cuny)
    * GH#332: Clean up multiple definitions of core_debug (Franck Cuny)
    * GH#338: Clean up route builder (Mickey Nasriachi)
    * Clean up of the dzil configuration (Alberto Simões)

    [ BUG FIXES ]
    * GH#334: Fix for GH#86, to display custom 500 page/template on
      internal server errors (Russell Jenkins)
    * GH#346: Fix tests on 5.8.9 (Albert Simões)

    [ DOCUMENTATION ]
    * GH#345: Documentation reorganization (Alberto Simões, Franck Cuny)

0.06 2013-07-30 (Sawyer X)

    [ ENHANCEMENTS ]
    * Clean up of the dzil configuration (Alberto Simões,Franck Cuny, Russel Jenkins)
    * GH#327: Add support for 'info' log level (Russell Jenkins)
    * Remove 'for_versions' usage from tests (Alberto Simões)

    [ BUG FIXES ]
    * GH#326, GH#232: don't end up with empty views and layout (Franck Cuny)
    * GH#325: don't die or complain when two routes have the same path (Franck Cuny)
    * GH#320: fix plugin_setting deprecation warning (David Golden)

    [ DOCUMENTATION ]
    * POD cleanup (Sawyer X, Franck Cuny)

0.05      2013-07-20 18:51:53 Europe/Paris

    [ DEPRECATION ]

    * Dancer2::Plugin drops support for Dancer 1 (issue #207)
      a DEPRECATION notice is issued when a plugin uses the old syntax
      (Alexis Sukrieh, Mokko, David Golden)
    * Drop support for 'use Dancer2 :moose' (Franck Cuny)

    [ ENHANCEMENTS ]
    * Add support for HTTP_X_FORWARDED_PROTO (Yanick Champoux)
    * Don't inflate custom types (Graham Knop)
    * Encode UTF8 params in Dancer2::Test (Vincent Bachelier)
    * Make Dancer2::Core::Request more lazy (Franck Cuny)
    * Don't use rootdir for app location (David Golden)
    * Improve File logger (David Golden)
    * Drop body when status is 1x or [23]04 (Franck Cuny)
    * Add support for HTTP_X_FORWARDED_PROTO (Yanick Champoux)
    * Prevent duplicate routes from being created (Franck Cuny)
    * Add support for route options (Franck Cuny)
    * Add support for prefix with route defined with regex (Franck Cuny)
    * Methods to return path of views and layout in the Template role
      (Franck Cuny, Yanick Champoux).
    * GH#31, GH#221: Config merging support (Russell Jenkins)

    [ BUG FIXES ]
    * GH#272: test function 'route_doesnt_exist' was not handling test comment
      properly. (Jeff Boes, Yanick Champoux)
    * GH#228: handle UTF-8 correctly in JSON serializer (Steven Humphrey)
    * GH#270: handle correctly serializer's options (Keith Broughton)
    * GH#274: `dancer -v' returns the correct version (Dinis Rebolo)
    * GH#286: for HEAD request, drop response's body (Franck Cuny)
    * GH#293: fix defaults tests for a newly generated app (Franck Cuny)
    * GH#216: check 'show_errors' when returning an internal error (Franck Cuny)
    * GH#246: Add serialization of log messages (Stefan Hornburg)
    * GH#268: Dancer2::Core::Response->status accepts stringy HTTP codes
      (Franck Cuny)
    * GH#308: Add support for ENV{DANCER_CONFDIR} and ENV{DANCER_ENVDIR}
      (Franck Cuny)
    * GH#210: Don't print startup banner if startup_info is set to 0
      (Maurice Mengel, Franck Cuny)
    * plugin_setting does not trigger a DEPRECATION warning anymore
      (Report by Alberto Simões, fix by Alexis Sukrieh)
    * GH#251: Support for on-the-fly changes of layouts/views (Franck Cuny)
    * GH#302: Avoid double encoding in Handler::File (Russell Jenkins)

    [ DOCUMENTATION ]
    * Lots of documentation cleanup (Mokko, David Precious)
    * Documenting Dancer2::Handler::AutoPage (Sabiha Imran, Sawyer X)
    * Documenting Dancer2::Core::Dispatcher (Babitha Balachandran)
    * Documenting Dancer2::Manual::DSL (David Precious, Franck Cuny)
    * Various typo (Shlomi Fish, Colin Kuskie, Stefan Hornburg, Rick Yakubowski)
    * Documenting some internals (Colin Kuskie)
    * Documenting Dancer2::Core::MIME (Babitha B.)
    * Documenting Manual::Developers (Maurice Mengel)
    * Documenting Dancer2::Core::Response (Colin Kuskie)

0.04 - 2013-04-22 (Alexis Sukrieh)

    [ BUG FIXES ]
    * Fix "Internal Sever Error" when sending a file with send_file
      (Dinis Rebolo)
    * Allow the setting of the 'views' directory, like stated in documentation
      (Alexander Karelas)

    [ ENHANCEMENTS ]
    * Implement Dancer2::Test file uploads (Steven Humphrey)
    * Give Dancer2::Test the ability to handle multiselect inputs
      (Steven Humphrey)
    * Make Cookie objects stringify to their value. (David Precious)
    * New routines for Dancer2::Test to check pod coverage in apps routes
      (Dinis Rebolo)
    * New script dancer2 to bootstrap an application (mokko)
    * Fix tests when running under Windows environments (Russell Jenkins)
    * Serializing modify the response's content type (Yanick Champoux)

    [ DOCUMENTATION ]
    * Make introduction more fluid in Dancer2's POD. (mokko)

    [ PACKAGING ]
    * Remove prereq Digest::SHA (mokko)
    * Dancer::P::Bcrypt recommends Dancer::P::Passphrase (Blabos de Blebe)

0.03 - 2013-03-07 (Alexis Sukrieh)

    [ ENHANCEMENTS ]
    * Don't create a session when just checking if a value exists
      (David Golden)
    * Only flush sessions if they are dirty
      (David Golden)
    * Allow the default template file extension to be changed.
      (David Precious)
    * Add on_plugin_import function to Dancer2::Plugin (David Golden)
      (Fix for issue #284)

    [BUG FIXES]
    * Dancer2::ModuleLoader now use Module::Runtime at its core
      (issue #166, Yanick Champoux)

    [ DOCUMENTATION ]
    * changed <% to [% in documentations (Alexander Karelas)
    * Improve Dancer2::Plugin documentation (David Golden)

0.02 - 2013-02-24   (Alexis Sukrieh)

    [ DOCUMENTATION ]
    * No more "TODO" tokens in the documentations
    * More documentation for Core classes
    (Alexis Sukrieh)

    [ ENHANCEMENTS ]
    * Removed the "api_version" code that is useless and was breaking some
      tests.
    (Alexis Sukrieh)

0.01

    [ ENHANCEMENTS ]
    * Dancer::Test takes a hash instead of an array for better backward
      compatibility with Dancer 1.
      (Celogeek)
    * Session revamp: better decoupling between Session and SessionFactory,
      support for session destruction and session values deletion. Everythin
      regarding session settings is now configurable.
      (David Golden).
    * Add route_exists and route_doesnt_exist in Dancer::Test (Mokko)
    * session cookie duration can be expressed with human readable strings
    * instead of numeric values (Alexis Sukrieh, issue #157).

    [ BUG FIXES ]
    * The engine configuration is now passed down to
      Dancer::Template::Implementation::ForkedTiny (Damien Krotkine).
    * Dancer App lookup now try to detect the dir "bin" and "lib" or ".dancer"
      file. (Celogeek)
    * Issues #125 and #126
      Support for configuration bits for session objects, possible to change the
      cookie name instead of the hard-coded value 'dancer.session'.
      (Reported by David Golden, fixed by Alexis Sukrieh).

    [ DOCUMENTATION ]
    * Add more POD in Dancer::Test (Mokko)

1.9999_02

    * Fix tests for previous release, tests cannot assume we're under Dancer 2
      when the version is 1.9999
      (Alexis Sukrieh)

1.9999_01

    * First DEVELEOPER release of Dancer 2
      complete rewrite of Dancer with a Moo backend.
      (Alexis Sukrieh, David Precious, Damien Krotkine, SawyerX, Yanick Champoux
      and others, plus Matt S. Trout as a reviewer).
t000755000765000024          015267764222 12571 5ustar00jasonstaff000000000000Dancer2-2.3.0app.t100644000765000024      1625015267764222 13722 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict;
use warnings;
use Test::More;
use Test::Fatal;
use Dancer2;
use Dancer2::Core::App;
use Dancer2::Core::Dispatcher;
use Dancer2::Core::Hook;
use Path::Tiny qw< path >;

# our app/dispatcher object
my $app = Dancer2::Core::App->new( name => 'main', );
$app->setting( show_stacktrace => 1 ); # enable show stacktrace
my $dispatcher = Dancer2::Core::Dispatcher->new( apps => [$app] );

# first basic tests
isa_ok $app, 'Dancer2::Core::App';

# some routes to play with
my @routes = (
    {   method => 'get',
        regexp => '/',
        code   => sub {'/'},
    },
    {   method => 'get',
        regexp => '/blog',
        code   => sub {'/blog'},
    },
);

# testing with and without prefixes
for my $p ( '/', '/mywebsite' ) {
    for my $r (@routes) {
        $app->prefix($p);
        $app->add_route(%$r);
    }
}

is $app->environment, 'development';

my $routes_regexps = $app->routes_regexps_for('get');
is( scalar(@$routes_regexps), 4, "route regexps are OK" );

for my $path ( '/', '/blog', '/mywebsite/', '/mywebsite/blog', ) {
    my $env = {
        REQUEST_METHOD => 'GET',
        PATH_INFO      => $path
    };

    my $expected = {
        '/'               => '/',
        '/blog'           => '/blog',
        '/mywebsite/'     => '/',
        '/mywebsite/blog' => '/blog',
    };

    my $resp = $dispatcher->dispatch($env);
    is $resp->[0], 200, 'got a 200';
    is $resp->[2][0], $expected->{$path}, 'got expected route';
}

note "testing lexical prefixes";

# clear the prefix in $app (and by the way, makes sure it works when prefix is
# undef).
$app->prefix(undef);

# nested prefixes bitches!
$app->lexical_prefix(
    '/foo' => sub {
        $app->add_route(
            method => 'get',
            regexp => '/',
            code   => sub {'/foo/'}
        );

        $app->add_route(
            method => 'get',
            regexp => '/second',
            code   => sub {'/foo/second'}
        );

        $app->lexical_prefix(
            '/bar' => sub {
                $app->add_route(
                    method => 'get',
                    regexp => '/',
                    code   => sub {'/foo/bar'}
                );
                $app->add_route(
                    method => 'get',
                    regexp => '/second',
                    code   => sub {'/foo/bar/second'}
                );
            }
        );
    },
);

# to make sure the lexical prefix did not crash anything
$app->add_route(
    method => 'get',
    regexp => '/root',
    code   => sub {'/root'}
);

# make sure a meaningless lexical prefix is ignored
$app->lexical_prefix(
    '/' => sub {
        $app->add_route(
            method => 'get',
            regexp => '/somewhere',
            code   => sub {'/somewhere'},
        );
    }
);

for
  my $path ( '/foo/', '/foo/second', '/foo/bar/second', '/root', '/somewhere' )
{
    my $env = {
        REQUEST_METHOD => 'GET',
        PATH_INFO      => $path,
    };

    my $resp = $dispatcher->dispatch($env);
    is $resp->[0], 200, 'got a 200';
    is $resp->[2][0], $path, 'got expected route';
}

note 'Check to ensure that add_route can override a prefix, even with undef. (gh-1663)';

my @more_routes = (
    {   method => 'get',
        regexp => '/prefix_test',
        code   => sub {'/prefix_test'},
    },
    {   method => 'get',
        regexp => '/prefix_override_test',
        prefix => '/prefixtest2',
        code   => sub {'/prefix_override_test'},
    },
    {   method => 'get',
        regexp => '/noprefix_test',
        code   => sub {'/noprefix_test'},
   prefix => undef,
    },
);
$app->prefix('/prefixtest');

for my $r (@more_routes) {
   $app->add_route(%$r);
}

my $expected_retvals = {
   '/prefix_test' => 404,
   '/prefixtest/prefix_test' => 200,
   '/prefixtest2/prefix_test' => 404,
   '/prefix_override_test' => 404,
   '/prefixtest/prefix_override_test' => 404,
   '/prefixtest2/prefix_override_test' => 200,
   '/noprefix_test' => 200,
   '/prefixtest/noprefix_test' => 404,
   '/prefixtest2/noprefix_test' => 404,
};

my $expected = {
   '/prefix_test' => undef,
   '/prefixtest/prefix_test' => '/prefix_test',
   '/prefixtest2/prefix_test' => undef,
   '/prefix_override_test' => undef,
   '/prefixtest/prefix_override_test' => undef,
   '/prefixtest2/prefix_override_test' => '/prefix_override_test',
   '/noprefix_test' => '/noprefix_test',
   '/prefixtest/noprefix_test' => undef,
   '/prefixtest2/noprefix_test' => undef,
};

for my $path ( sort keys %$expected_retvals ) {
   my $env = {
       SERVER_PORT => 5000,
       SERVER_NAME => 'test.local',
       REQUEST_METHOD => 'GET',
       PATH_INFO      => $path
   };

   my $resp = $dispatcher->dispatch($env);
   is $resp->[0], $expected_retvals->{$path}, "got expected return value on $path";
   next if $expected_retvals->{$path} == 404;
   is $resp->[2][0], $expected->{$path}, 'got expected route';
}

note "test a failure in the callback of a lexical prefix";
like(
    exception {
        $app->lexical_prefix( '/test' => sub { Failure->game_over() } );
    },
    qr{Unable to run the callback for prefix '/test': Can't locate object method "game_over" via package "Failure"},
    "caught an exception in the lexical prefix callback",
);

$app->add_hook(
    Dancer2::Core::Hook->new(
        name => 'before',
        code => sub {1},
    )
);

$app->add_hook(
    Dancer2::Core::Hook->new(
        name => 'before',
        code => sub { Foo->failure; },
    )
);

$app->compile_hooks;
my $env = {
    REQUEST_METHOD => 'GET',
    PATH_INFO      => '/',
};

like(
    $dispatcher->dispatch($env)->[2][0],
    qr/Exception caught in 'core.app.before_request' filter: Hook error: Can't locate object method "failure"/,
    'before filter nonexistent method failure',
);

$app->replace_hook( 'core.app.before_request', [ sub {1} ] );
$app->compile_hooks;
$env = {
    REQUEST_METHOD => 'GET',
    PATH_INFO      => '/',
};

# test duplicate routes when the path is a regex
$app = Dancer2::Core::App->new( name => 'main' );
my $regexp_route = {
    method => 'get', 'regexp' => qr!/(\d+)!, code => sub {1}
};
$app->add_route(%$regexp_route);

# try to get an invalid engine
eval {$app->engine('foo')};
like(
    $@,
    qr/^Engine 'foo' is not supported/,
    "Engine 'foo' does not exist",
);

my $tmpl_engine = $app->engine('template');
ok $tmpl_engine, "Template engine is defined";

ok !$app->has_serializer_engine, "Serializer engine does not exist";

is_deeply(
    $app->_get_config_for_engine('NonExistent'),
    {},
    'Empty configuration for nonexistent engine',
);

# TODO: not such an intelligent check, these ones...
# set configuration for an engine
$app->config->{'engines'}{'template'}{'Tiny'}{'hello'} = 'world';
$app->config->{'engines'}{'template'}{'Some::Other::Template::Namespace'}{'hello'} = 'world';

is_deeply(
    $app->_get_config_for_engine( template => 'Tiny', $app->config ),
    { hello => 'world' },
    '_get_config_for_engine can find the right configuration',
);

is_deeply(
    $app->_get_config_for_engine( template => '+Some::Other::Template::Namespace', $app->config ),
    { hello => 'world' },
    '_get_config_for_engine can find the right configuration',
);

is(
    path( $app->caller ),
    path( qw< t app.t > ),
    'Correct caller for app',
);

done_testing;
cpanfile100644000765000024      1102015267764222 14204 0ustar00jasonstaff000000000000Dancer2-2.3.0requires 'perl', 5.014000;
requires 'Attribute::Handlers';
requires 'Carp';
requires 'Clone';
requires 'Config::Any';
requires 'Crypt::URandom', '0.36';
requires 'Data::Censor' => '0.04';
requires 'Digest::SHA';
requires 'Encode';
requires 'Exporter', '5.57';
requires 'Exporter::Tiny';
requires 'File::Copy';
requires 'File::Path';
requires 'File::Share';
requires 'File::Temp';
requires 'Hash::Merge::Simple';
requires 'Hash::MultiValue';
requires 'HTTP::Date';
requires 'HTTP::Headers::Fast', '0.21';
requires 'HTTP::Tiny';
requires 'Import::Into';
requires 'JSON::MaybeXS';
requires 'List::Util', '1.29';   # 1.29 has the pair* functions
requires 'MIME::Base64', '3.13'; # 3.13 has the URL safe variants
requires 'Module::Runtime';
requires 'Moo', '2.000000';
requires 'Moo::Role';
requires 'parent';
requires 'Path::Tiny';
requires 'Plack', '1.0040';
requires 'Plack::Middleware::FixMissingBodyInRedirect';
requires 'Plack::Middleware::RemoveRedundantBody';
requires 'POSIX';
requires 'Ref::Util';
requires 'Safe::Isa';
requires 'Sub::Quote';
requires 'Template';
requires 'Template::Tiny', '1.16';
requires 'Test::Builder';
requires 'Test::More';
requires 'Types::Standard';
requires 'Type::Tiny', '1.000006';
requires 'URI::Escape';
requires 'CLI::Osprey','0.09';
requires 'File::Which';
requires 'Sub::Util';

requires 'Role::Tiny', '2.000000';
conflicts 'Role::Tiny', '== 2.000007';

# Module::Pluggable 6.1 and 6.2 fail their test suite when run as root,
# such as under docker in a github action
requires 'Module::Pluggable';
conflicts 'Module::Pluggable', '== 6.1';
conflicts 'Module::Pluggable', '== 6.2';

# Dancer2::Serializer::YAML hands untrusted request bodies to YAML::Load, so
# the floor here is a security constraint, decided as follows:
#
#   1.25  introduced $YAML::LoadBlessed. The serializer sets it to 0 itself so
#         the protection does not depend on YAML.pm's ambient default -- but
#         below 1.25 the variable does not exist and setting it is a silent
#         no-op, leaving !!perl/hash:Some::Class able to instantiate an
#         arbitrary blessed object from a request body.
#   1.26  fixed a parsing regression introduced in 1.25.
#   1.28  "only enable loading globs when $LoadCode is set" -- an upstream
#         security fix in the same load path we expose to untrusted input, and
#         unlike LoadBlessed it is behavioural, so we cannot reproduce it from
#         our side on older versions. This is the real security floor.
#   1.30  changed the $YAML::LoadBlessed default to 0. We override it
#         explicitly either way, so this adds nothing functionally -- but it is
#         from January 2020 and costs nothing, and it keeps the safe behaviour
#         if our explicit setting is ever lost in a refactor.
#
# The serializer also zeroes $YAML::UseCode, because the loader ORs it into
# its code-loading decision (load_code($YAML::LoadCode || $YAML::UseCode)) and
# an ambient $YAML::UseCode = 1 would otherwise defeat the LoadCode setting.
# UseCode has existed since long before any of the above, so it changes
# nothing about the 1.30 floor.
#
# 1.30 also supersedes two older constraints, no longer declared separately:
# a floor of 0.86 (https://github.com/PerlDancer/Dancer2/issues/899), and an
# exclusion of the broken 1.16 (cpantesters reports
# 25911c10-4199-11e6-8d7d-86c55bc2a771 and 284ac158-419a-11e6-9a35-e3e15bc2a771).
requires 'YAML', '1.30';

recommends 'CGI::Deurl::XS';
recommends 'Class::XSAccessor';
recommends 'Cpanel::JSON::XS';
recommends 'HTTP::XSCookies', '0.000015';
recommends 'HTTP::XSHeaders';
recommends 'MooX::TypeTiny';
recommends 'Pod::Simple::Search';
recommends 'Pod::Simple::SimpleTree';
recommends 'Type::Tiny::XS';
recommends 'URL::Encode::XS';
recommends 'YAML::XS';
recommends 'Unicode::UTF8';

suggests 'Fcntl';
suggests 'MIME::Types';

test_requires 'Capture::Tiny', '0.12';
test_requires 'HTTP::Cookies';
test_requires 'HTTP::Headers';
test_requires 'Pod::Simple::SimpleTree';
test_requires 'Template';
test_requires 'Test::Builder';
test_requires 'Test::EOL';
test_requires 'Test::Fatal';
test_requires 'Test::More';
test_requires 'Test::More', '0.92';
test_requires 'Test::Exception';

author_requires 'Test::NoTabs';
author_requires 'Test::Pod';
author_requires 'AnyEvent';
author_requires 'CBOR::XS';
author_requires 'Class::Method::Modifiers';
author_requires 'Dist::Zilla::Plugin::Test::UnusedVars';
author_requires 'Perl::Tidy';
author_requires 'Test::Memory::Cycle';
author_requires 'Test::MockTime';
author_requires 'Test::Perl::Critic';
author_requires 'Test::Whitespaces';
author_requires 'YAML::XS';
time.t100644000765000024       413415267764222 14056 0ustar00jasonstaff000000000000Dancer2-2.3.0/t# time.t

use strict;
use warnings;
use Test::More;

my $mocked_epoch = 1355676244;    # "Sun, 16-Dec-2012 16:44:04 GMT"

# The order is important!
eval { require Test::MockTime; 1; }
    or plan skip_all => 'Test::MockTime not present';

Test::MockTime::set_fixed_time($mocked_epoch);
require Dancer2::Core::Time;

my @tests = (
    [ "1h"      => 3600  => "Sun, 16-Dec-2012 17:44:04 GMT" ],
    [ "1 hour"  => 3600  => "Sun, 16-Dec-2012 17:44:04 GMT" ],
    [ "+1 hour" => 3600  => "Sun, 16-Dec-2012 17:44:04 GMT" ],
    [ "-1h"     => -3600 => "Sun, 16-Dec-2012 15:44:04 GMT" ],
    [ "1 hours" => 3600  => "Sun, 16-Dec-2012 17:44:04 GMT" ],

    [ "1d"    => ( 3600 * 24 ) => "Mon, 17-Dec-2012 16:44:04 GMT" ],
    [ "1 day" => ( 3600 * 24 ) => "Mon, 17-Dec-2012 16:44:04 GMT" ],


);

foreach my $test (@tests) {
    my ( $expr, $secs, $gmt_string ) = @$test;

    subtest "Expression: \"$expr\"" => sub {
        my $t = Dancer2::Core::Time->new( expression => $expr );
        is $t->seconds, $secs, "\"$expr\" is $secs seconds";
        is $t->epoch, ( $t->seconds + $mocked_epoch ),
          "... its epoch is " . $t->epoch;
        is $t->gmt_string, $gmt_string,
          "... and its GMT string is $gmt_string";
    };
}

subtest "Forcing another epoch in the object should work" => sub {
    my $t = Dancer2::Core::Time->new( epoch => 1, expression => "1h" );
    is $t->seconds, 3600, "...1h is still 3600 seconds";
    is $t->epoch,   1,    "... epoch is 1";
    is $t->gmt_string, 'Thu, 01-Jan-1970 00:00:01 GMT',
      "... and is expressed as Thu, 01-Jan-1970 00:00:01 GMT";
};

subtest "unparsable strings should be kept" => sub {
    for my $t (
        [ "something silly", "something silly", "something silly" ],
        [ "+2 something",    "+2 something",    "+2 something" ],
      )
    {
        my ( $expr, $secs, $gmt ) = @$t;
        my $t = Dancer2::Core::Time->new( expression => $expr );
        is $t->seconds,    $secs, "\"$expr\" is $secs seconds";
        is $t->epoch,      $expr, "... its epoch is $expr";
        is $t->gmt_string, $gmt,  "... and its GMT string is $gmt";
    }
};

done_testing;
mime.t100644000765000024       230115267764222 14041 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict;
use warnings;

use Test::More tests => 12;

BEGIN {
    use_ok 'Dancer2::Core::MIME';
}

my $mime = Dancer2::Core::MIME->new();

is_deeply( $mime->custom_types, {}, 'user defined mime_types are empty' );

$mime->add_type( foo => 'text/foo' );
is_deeply( $mime->custom_types, { foo => 'text/foo' }, 'text/foo is saved' );
is( $mime->for_name('foo'), 'text/foo', 'mime type foo is found' );

$mime->add_alias( bar => 'foo' );
is( $mime->for_name('bar'), 'text/foo', 'mime type bar is found' );

is( $mime->for_file('foo.bar'),
    'text/foo', 'mime type for extension .bar is found'
);

is( $mime->for_file('foobar'),
    $mime->default, 'mime type for no extension is the default'
);

is( $mime->add_alias( xpto => 'BAR' ),
    'text/foo', 'mime gets correctly lowercased for user types'
);

is $mime->add_alias( xpto => 'SVG' ) => 'image/svg+xml',
  'mime gets correctly lowercased for system types';

is $mime->add_alias( zbr => 'baz' ) => $mime->default,
  'alias of unknown mime type gets default mime type';

is $mime->name_or_type("text/zbr") => "text/zbr",
  'name_or_type does not change if it seems a mime type string';

is $mime->name_or_type("svg") => "image/svg+xml", 'name_or_type knows svg';
vars.t100644000765000024       112615267764222 14071 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict;
use warnings;
use Test::More import => ['!pass'];
use Plack::Test;
use HTTP::Request::Common;
use Ref::Util qw;

plan tests => 3;

{
    use Dancer2;

    hook before => sub {
        var( "xpto" => "foo" );
        vars->{zbr} = 'ugh';
    };

    get '/bar' => sub {
        var("xpto");
    };

    get '/baz' => sub {
        vars->{zbr};
    };
}

my $app = __PACKAGE__->to_app;
ok( is_coderef($app), 'Got app' );

test_psgi $app, sub {
    my $cb = shift;

    is( $cb->( GET '/bar' )->content, 'foo', 'foo' );
    is( $cb->( GET '/baz' )->content, 'ugh', 'ugh' );
};
META.yml100644000765000024      1712615267764222 13766 0ustar00jasonstaff000000000000Dancer2-2.3.0---
abstract: 'Lightweight yet powerful web application framework'
author:
  - 'Dancer Core Developers'
build_requires:
  Capture::Tiny: '0.12'
  ExtUtils::MakeMaker: '0'
  File::Spec: '0'
  HTTP::Cookies: '0'
  HTTP::Headers: '0'
  IO::Handle: '0'
  IPC::Open3: '0'
  Pod::Simple::SimpleTree: '0'
  Template: '0'
  Test::Builder: '0'
  Test::EOL: '0'
  Test::Exception: '0'
  Test::Fatal: '0'
  Test::More: '0.92'
configure_requires:
  CPAN::Meta::Requirements: '2.120620'
  ExtUtils::MakeMaker: '0'
  File::ShareDir::Install: '0.06'
  Module::Metadata: '0'
conflicts:
  Module::Pluggable: '== 6.2'
  Role::Tiny: '== 2.000007'
dynamic_config: 1
generated_by: 'Dist::Zilla version 6.037, CPAN::Meta::Converter version 2.150013'
license: perl
meta-spec:
  url: http://module-build.sourceforge.net/META-spec-v1.4.html
  version: '1.4'
name: Dancer2
provides:
  Dancer2:
    file: lib/Dancer2.pm
    version: v2.3.0
  Dancer2::CLI:
    file: lib/Dancer2/CLI.pm
    version: v2.3.0
  Dancer2::CLI::Gen:
    file: lib/Dancer2/CLI/Gen.pm
    version: v2.3.0
  Dancer2::CLI::Version:
    file: lib/Dancer2/CLI/Version.pm
    version: v2.3.0
  Dancer2::ConfigReader:
    file: lib/Dancer2/ConfigReader.pm
    version: v2.3.0
  Dancer2::ConfigReader::Config::Any:
    file: lib/Dancer2/ConfigReader/Config/Any.pm
    version: v2.3.0
  Dancer2::ConfigUtils:
    file: lib/Dancer2/ConfigUtils.pm
    version: v2.3.0
  Dancer2::Core:
    file: lib/Dancer2/Core.pm
    version: v2.3.0
  Dancer2::Core::App:
    file: lib/Dancer2/Core/App.pm
    version: v2.3.0
  Dancer2::Core::Cookie:
    file: lib/Dancer2/Core/Cookie.pm
    version: v2.3.0
  Dancer2::Core::DSL:
    file: lib/Dancer2/Core/DSL.pm
    version: v2.3.0
  Dancer2::Core::Dispatcher:
    file: lib/Dancer2/Core/Dispatcher.pm
    version: v2.3.0
  Dancer2::Core::Error:
    file: lib/Dancer2/Core/Error.pm
    version: v2.3.0
  Dancer2::Core::Factory:
    file: lib/Dancer2/Core/Factory.pm
    version: v2.3.0
  Dancer2::Core::HTTP:
    file: lib/Dancer2/Core/HTTP.pm
    version: v2.3.0
  Dancer2::Core::Hook:
    file: lib/Dancer2/Core/Hook.pm
    version: v2.3.0
  Dancer2::Core::MIME:
    file: lib/Dancer2/Core/MIME.pm
    version: v2.3.0
  Dancer2::Core::Request:
    file: lib/Dancer2/Core/Request.pm
    version: v2.3.0
  Dancer2::Core::Request::Upload:
    file: lib/Dancer2/Core/Request/Upload.pm
    version: v2.3.0
  Dancer2::Core::Response:
    file: lib/Dancer2/Core/Response.pm
    version: v2.3.0
  Dancer2::Core::Response::Delayed:
    file: lib/Dancer2/Core/Response/Delayed.pm
    version: v2.3.0
  Dancer2::Core::Role::ConfigReader:
    file: lib/Dancer2/Core/Role/ConfigReader.pm
    version: v2.3.0
  Dancer2::Core::Role::DSL:
    file: lib/Dancer2/Core/Role/DSL.pm
    version: v2.3.0
  Dancer2::Core::Role::Engine:
    file: lib/Dancer2/Core/Role/Engine.pm
    version: v2.3.0
  Dancer2::Core::Role::Handler:
    file: lib/Dancer2/Core/Role/Handler.pm
    version: v2.3.0
  Dancer2::Core::Role::HasConfig:
    file: lib/Dancer2/Core/Role/HasConfig.pm
    version: v2.3.0
  Dancer2::Core::Role::HasEnvironment:
    file: lib/Dancer2/Core/Role/HasEnvironment.pm
    version: v2.3.0
  Dancer2::Core::Role::HasLocation:
    file: lib/Dancer2/Core/Role/HasLocation.pm
    version: v2.3.0
  Dancer2::Core::Role::Hookable:
    file: lib/Dancer2/Core/Role/Hookable.pm
    version: v2.3.0
  Dancer2::Core::Role::Logger:
    file: lib/Dancer2/Core/Role/Logger.pm
    version: v2.3.0
  Dancer2::Core::Role::Serializer:
    file: lib/Dancer2/Core/Role/Serializer.pm
    version: v2.3.0
  Dancer2::Core::Role::SessionFactory:
    file: lib/Dancer2/Core/Role/SessionFactory.pm
    version: v2.3.0
  Dancer2::Core::Role::SessionFactory::File:
    file: lib/Dancer2/Core/Role/SessionFactory/File.pm
    version: v2.3.0
  Dancer2::Core::Role::StandardResponses:
    file: lib/Dancer2/Core/Role/StandardResponses.pm
    version: v2.3.0
  Dancer2::Core::Role::Template:
    file: lib/Dancer2/Core/Role/Template.pm
    version: v2.3.0
  Dancer2::Core::Route:
    file: lib/Dancer2/Core/Route.pm
    version: v2.3.0
  Dancer2::Core::Runner:
    file: lib/Dancer2/Core/Runner.pm
    version: v2.3.0
  Dancer2::Core::Session:
    file: lib/Dancer2/Core/Session.pm
    version: v2.3.0
  Dancer2::Core::Time:
    file: lib/Dancer2/Core/Time.pm
    version: v2.3.0
  Dancer2::Core::Types:
    file: lib/Dancer2/Core/Types.pm
    version: v2.3.0
  Dancer2::FileUtils:
    file: lib/Dancer2/FileUtils.pm
    version: v2.3.0
  Dancer2::Handler::AutoPage:
    file: lib/Dancer2/Handler/AutoPage.pm
    version: v2.3.0
  Dancer2::Handler::File:
    file: lib/Dancer2/Handler/File.pm
    version: v2.3.0
  Dancer2::Logger::Capture:
    file: lib/Dancer2/Logger/Capture.pm
    version: v2.3.0
  Dancer2::Logger::Capture::Trap:
    file: lib/Dancer2/Logger/Capture/Trap.pm
    version: v2.3.0
  Dancer2::Logger::Console:
    file: lib/Dancer2/Logger/Console.pm
    version: v2.3.0
  Dancer2::Logger::Diag:
    file: lib/Dancer2/Logger/Diag.pm
    version: v2.3.0
  Dancer2::Logger::File:
    file: lib/Dancer2/Logger/File.pm
    version: v2.3.0
  Dancer2::Logger::Note:
    file: lib/Dancer2/Logger/Note.pm
    version: v2.3.0
  Dancer2::Logger::Null:
    file: lib/Dancer2/Logger/Null.pm
    version: v2.3.0
  Dancer2::Plugin:
    file: lib/Dancer2/Plugin.pm
    version: v2.3.0
  Dancer2::Serializer::JSON:
    file: lib/Dancer2/Serializer/JSON.pm
    version: v2.3.0
  Dancer2::Serializer::Mutable:
    file: lib/Dancer2/Serializer/Mutable.pm
    version: v2.3.0
  Dancer2::Serializer::YAML:
    file: lib/Dancer2/Serializer/YAML.pm
    version: v2.3.0
  Dancer2::Session::Simple:
    file: lib/Dancer2/Session/Simple.pm
    version: v2.3.0
  Dancer2::Session::YAML:
    file: lib/Dancer2/Session/YAML.pm
    version: v2.3.0
  Dancer2::Template::TemplateToolkit:
    file: lib/Dancer2/Template/TemplateToolkit.pm
    version: v2.3.0
  Dancer2::Template::Tiny:
    file: lib/Dancer2/Template/Tiny.pm
    version: v2.3.0
  Dancer2::Test:
    file: lib/Dancer2/Test.pm
    version: v2.3.0
recommends:
  CGI::Deurl::XS: '0'
  Class::XSAccessor: '0'
  Cpanel::JSON::XS: '0'
  HTTP::XSCookies: '0.000015'
  HTTP::XSHeaders: '0'
  MooX::TypeTiny: '0'
  Pod::Simple::Search: '0'
  Pod::Simple::SimpleTree: '0'
  Type::Tiny::XS: '0'
  URL::Encode::XS: '0'
  Unicode::UTF8: '0'
  YAML::XS: '0'
requires:
  Attribute::Handlers: '0'
  CLI::Osprey: '0.09'
  Carp: '0'
  Clone: '0'
  Config::Any: '0'
  Crypt::URandom: '0.36'
  Data::Censor: '0.04'
  Digest::SHA: '0'
  Encode: '0'
  Exporter: '5.57'
  Exporter::Tiny: '0'
  File::Copy: '0'
  File::Path: '0'
  File::Share: '0'
  File::Temp: '0'
  File::Which: '0'
  HTTP::Date: '0'
  HTTP::Headers::Fast: '0.21'
  HTTP::Tiny: '0'
  Hash::Merge::Simple: '0'
  Hash::MultiValue: '0'
  Import::Into: '0'
  JSON::MaybeXS: '0'
  List::Util: '1.29'
  MIME::Base64: '3.13'
  Module::Pluggable: '0'
  Module::Runtime: '0'
  Moo: '2.000000'
  Moo::Role: '0'
  POSIX: '0'
  Path::Tiny: '0'
  Plack: '1.0040'
  Plack::Middleware::FixMissingBodyInRedirect: '0'
  Plack::Middleware::RemoveRedundantBody: '0'
  Ref::Util: '0'
  Role::Tiny: '2.000000'
  Safe::Isa: '0'
  Sub::Quote: '0'
  Sub::Util: '0'
  Template: '0'
  Template::Tiny: '1.16'
  Test::Builder: '0'
  Test::More: '0.92'
  Type::Tiny: '1.000006'
  Types::Standard: '0'
  URI::Escape: '0'
  YAML: '1.30'
  parent: '0'
  perl: '5.014'
resources:
  IRC: irc://irc.perl.org/#dancer
  WebIRC: https://chat.mibbit.com/#dancer@irc.perl.org
  bugtracker: https://github.com/PerlDancer/Dancer2/issues
  homepage: http://perldancer.org/
  repository: git://github.com/PerlDancer/Dancer2.git
version: 2.3.0
x_generated_by_perl: v5.44.0
x_serialization_backend: 'YAML::Tiny version 1.76'
x_spdx_expression: 'Artistic-1.0-Perl OR GPL-1.0-or-later'
MANIFEST100644000765000024      3233715267764222 13647 0ustar00jasonstaff000000000000Dancer2-2.3.0# This file was automatically generated by Dist::Zilla::Plugin::Manifest v6.037
AUTHORS
CODE_OF_CONDUCT.md
CONTRIBUTING.md
Changes
LICENSE
MANIFEST
META.json
META.yml
Makefile.PL
README.md
Releasing-Dancer2.md
SECURITY.md
cpanfile
examples/single/hello_world.psgi
examples/single/simple_calculator.psgi
lib/Dancer2.pm
lib/Dancer2/CLI.pm
lib/Dancer2/CLI/Gen.pm
lib/Dancer2/CLI/Version.pm
lib/Dancer2/ConfigReader.pm
lib/Dancer2/ConfigReader/Config/Any.pm
lib/Dancer2/ConfigUtils.pm
lib/Dancer2/Core.pm
lib/Dancer2/Core/App.pm
lib/Dancer2/Core/Cookie.pm
lib/Dancer2/Core/DSL.pm
lib/Dancer2/Core/Dispatcher.pm
lib/Dancer2/Core/Error.pm
lib/Dancer2/Core/Factory.pm
lib/Dancer2/Core/HTTP.pm
lib/Dancer2/Core/Hook.pm
lib/Dancer2/Core/MIME.pm
lib/Dancer2/Core/Request.pm
lib/Dancer2/Core/Request/Upload.pm
lib/Dancer2/Core/Response.pm
lib/Dancer2/Core/Response/Delayed.pm
lib/Dancer2/Core/Role/ConfigReader.pm
lib/Dancer2/Core/Role/DSL.pm
lib/Dancer2/Core/Role/Engine.pm
lib/Dancer2/Core/Role/Handler.pm
lib/Dancer2/Core/Role/HasConfig.pm
lib/Dancer2/Core/Role/HasEnvironment.pm
lib/Dancer2/Core/Role/HasLocation.pm
lib/Dancer2/Core/Role/Hookable.pm
lib/Dancer2/Core/Role/Logger.pm
lib/Dancer2/Core/Role/Serializer.pm
lib/Dancer2/Core/Role/SessionFactory.pm
lib/Dancer2/Core/Role/SessionFactory/File.pm
lib/Dancer2/Core/Role/StandardResponses.pm
lib/Dancer2/Core/Role/Template.pm
lib/Dancer2/Core/Route.pm
lib/Dancer2/Core/Runner.pm
lib/Dancer2/Core/Session.pm
lib/Dancer2/Core/Time.pm
lib/Dancer2/Core/Types.pm
lib/Dancer2/DeprecationPolicy.pod
lib/Dancer2/FileUtils.pm
lib/Dancer2/Handler/AutoPage.pm
lib/Dancer2/Handler/File.pm
lib/Dancer2/Logger/Capture.pm
lib/Dancer2/Logger/Capture/Trap.pm
lib/Dancer2/Logger/Console.pm
lib/Dancer2/Logger/Diag.pm
lib/Dancer2/Logger/File.pm
lib/Dancer2/Logger/Note.pm
lib/Dancer2/Logger/Null.pm
lib/Dancer2/Manual.pod
lib/Dancer2/Manual/Config.pod
lib/Dancer2/Manual/Cookbook.pod
lib/Dancer2/Manual/Deployment.pod
lib/Dancer2/Manual/Extending.pod
lib/Dancer2/Manual/Keywords.pod
lib/Dancer2/Manual/Migration.pod
lib/Dancer2/Manual/Plugins.pod
lib/Dancer2/Manual/QuickStart.pod
lib/Dancer2/Manual/Testing.pod
lib/Dancer2/Manual/Tutorial.pod
lib/Dancer2/Plugin.pm
lib/Dancer2/Policy.pod
lib/Dancer2/Serializer/JSON.pm
lib/Dancer2/Serializer/Mutable.pm
lib/Dancer2/Serializer/YAML.pm
lib/Dancer2/Session/Simple.pm
lib/Dancer2/Session/YAML.pm
lib/Dancer2/Template/TemplateToolkit.pm
lib/Dancer2/Template/Tiny.pm
lib/Dancer2/Test.pm
script/dancer2
share/docker/Dockerfile
share/gitignore
share/skel/default/.dancer
share/skel/default/MANIFEST.SKIP
share/skel/default/Makefile.PL
share/skel/default/bin/+app.psgi
share/skel/default/config.yml
share/skel/default/cpanfile
share/skel/default/environments/development.yml
share/skel/default/environments/production.yml
share/skel/default/lib/AppFile.pm
share/skel/default/public/+dispatch.cgi
share/skel/default/public/+dispatch.fcgi
share/skel/default/public/404.html
share/skel/default/public/500.html
share/skel/default/public/css/error.css
share/skel/default/public/css/style.css
share/skel/default/public/favicon.ico
share/skel/default/public/images/perldancer-bg.jpg
share/skel/default/public/images/perldancer.jpg
share/skel/default/t/001_base.t
share/skel/default/t/002_index_route.t
share/skel/default/views/index.tt
share/skel/default/views/layouts/main.tt
share/skel/tutorial/bin/+app.psgi
share/skel/tutorial/config.yml
share/skel/tutorial/cpanfile
share/skel/tutorial/db/blog.db
share/skel/tutorial/db/entries.sql
share/skel/tutorial/db/users.sql
share/skel/tutorial/lib/AppFile.pm
share/skel/tutorial/lib/AppFile/Schema.pm
share/skel/tutorial/lib/AppFile/Schema/Result/Entry.pm
share/skel/tutorial/lib/AppFile/Schema/Result/User.pm
share/skel/tutorial/public/404.html
share/skel/tutorial/public/500.html
share/skel/tutorial/public/css/error.css
share/skel/tutorial/public/css/style.css
share/skel/tutorial/public/dispatch.cgi
share/skel/tutorial/public/dispatch.fcgi
share/skel/tutorial/public/favicon.ico
share/skel/tutorial/t/001_base.t
share/skel/tutorial/t/002_index_route.t
share/skel/tutorial/t/003_login.t
share/skel/tutorial/t/004_blog.t
share/skel/tutorial/t/db/test.db
share/skel/tutorial/views/create_update.tt
share/skel/tutorial/views/delete.tt
share/skel/tutorial/views/entry.tt
share/skel/tutorial/views/index.tt
share/skel/tutorial/views/layouts/main.tt
share/skel/tutorial/views/login.tt
t/00-compile.t
t/00-report-prereqs.dd
t/00-report-prereqs.t
t/app.t
t/app/t1/bin/app.psgi
t/app/t1/config.yml
t/app/t1/lib/App1.pm
t/app/t1/lib/Sub/App2.pm
t/app/t2/.dancer
t/app/t2/config.yml
t/app/t2/lib/App3.pm
t/app/t3/lib/App4.pm
t/app/t3/lib/config.yml
t/app/t_config_file_extended/bin/app.psgi
t/app/t_config_file_extended/config.yml
t/app/t_config_file_extended/lib/App1.pm
t/app_alone.t
t/author-distmeta.t
t/author-no-tabs.t
t/author-pod-syntax.t
t/auto_page.t
t/caller.t
t/charset_server.t
t/classes/Dancer2-Core-Factory/new.t
t/classes/Dancer2-Core-Hook/new.t
t/classes/Dancer2-Core-Request/new.t
t/classes/Dancer2-Core-Request/serializers.t
t/classes/Dancer2-Core-Response-Delayed/after_hooks.t
t/classes/Dancer2-Core-Response-Delayed/new.t
t/classes/Dancer2-Core-Response/new_from.t
t/classes/Dancer2-Core-Role-Engine/with.t
t/classes/Dancer2-Core-Role-Handler/with.t
t/classes/Dancer2-Core-Role-HasConfig/with.t
t/classes/Dancer2-Core-Role-HasEnvironment/with.t
t/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/bin/.exists
t/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/blib/lib/fakescript.pl
t/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/lib/fake/inner/dir/.exists
t/classes/Dancer2-Core-Role-HasLocation/FakeDancerFile/.dancer
t/classes/Dancer2-Core-Role-HasLocation/FakeDancerFile/fakescript.pl
t/classes/Dancer2-Core-Role-HasLocation/FakeDistRoot/bin/.exists
t/classes/Dancer2-Core-Role-HasLocation/FakeDistRoot/configonly/config.yml
t/classes/Dancer2-Core-Role-HasLocation/FakeDistRoot/configonly/fakescript.pl
t/classes/Dancer2-Core-Role-HasLocation/FakeDistRoot/lib/.exists
t/classes/Dancer2-Core-Role-HasLocation/FakeDistRoot/t/config.yml
t/classes/Dancer2-Core-Role-HasLocation/FakeDistRoot/t/fakescript.pl
t/classes/Dancer2-Core-Role-HasLocation/FakeDistRoot/t/public/.exists
t/classes/Dancer2-Core-Role-HasLocation/FakeDistRoot/t/views/.exists
t/classes/Dancer2-Core-Role-HasLocation/FakeDistRoot/viewsonly/fakescript.pl
t/classes/Dancer2-Core-Role-HasLocation/FakeDistRoot/viewsonly/views/.exists
t/classes/Dancer2-Core-Role-HasLocation/with.t
t/classes/Dancer2-Core-Role-Serializer/with.t
t/classes/Dancer2-Core-Role-StandardResponses/with.t
t/classes/Dancer2-Core-Route/base.t
t/classes/Dancer2-Core-Route/deprecated_param_keys.t
t/classes/Dancer2-Core-Route/match.t
t/classes/Dancer2-Core-Runner/environment.t
t/classes/Dancer2-Core-Runner/new.t
t/classes/Dancer2-Core-Runner/psgi_app.t
t/classes/Dancer2-Core/camelize.t
t/classes/Dancer2/import-pragmas.t
t/classes/Dancer2/import.t
t/config.t
t/config.yml
t/config/config.yml
t/config/environments/failure.yml
t/config/environments/merging.yml
t/config/environments/production.yml
t/config/environments/staging.json
t/config2/config.yml
t/config2/config_local.yml
t/config2/environments/lconfig.yml
t/config2/environments/lconfig_local.yml
t/config_file_extended.t
t/config_many.t
t/config_many_failure.t
t/config_multiapp.t
t/config_reader.t
t/config_settings.t
t/config_utils.t
t/context-in-before.t
t/cookie.t
t/corpus/pretty/505.tt
t/corpus/pretty/relative.tt
t/corpus/pretty_public/404.html
t/corpus/pretty_public/510.html
t/corpus/static/1x1.png
t/corpus/static/empty.foo
t/corpus/static/index.html
t/custom_dsl.t
t/dancer-test.t
t/dancer-test/config.yml
t/deserialize.t
t/disp_named_capture.t
t/dispatcher.t
t/dsl/any.t
t/dsl/app.t
t/dsl/content.t
t/dsl/delayed.t
t/dsl/error_template.t
t/dsl/extend.t
t/dsl/extend_config/config.yml
t/dsl/halt.t
t/dsl/halt_with_param.t
t/dsl/json.t
t/dsl/mime.t
t/dsl/parameters.t
t/dsl/pass.t
t/dsl/path.t
t/dsl/pod.t
t/dsl/request.t
t/dsl/request_data.t
t/dsl/route_retvals.t
t/dsl/send_as.t
t/dsl/send_file.t
t/dsl/splat.t
t/dsl/to_app.t
t/dsl/uri_for.t
t/dsl/uri_for_route.t
t/dsl/yaml.t
t/dsl_path_dirname.t
t/e2e/cli/gen.t
t/engine.t
t/error.t
t/examples/hello_world.t
t/examples/simple_calculator.t
t/factory.t
t/file_utils.t
t/forward.t
t/forward_before_hook.t
t/forward_hmv_params.t
t/forward_test_tcp.t
t/hooks.t
t/http_methods.t
t/http_status.t
t/integration/dispatch/flow.t
t/integration/dispatch/multiapp.t
t/integration/error/fallback.t
t/integration/error/rendering.t
t/integration/handler/file.t
t/integration/hooks/chain.t
t/integration/hooks/engine.t
t/integration/serializer/json.t
t/integration/serializer/mutable.t
t/integration/serializer/yaml.t
t/integration/session/lifecycle.t
t/integration/template/autopage.t
t/integration/template/rendering.t
t/issues/config.yml
t/issues/gh-1013/gh-1013.t
t/issues/gh-1013/views/t.tt
t/issues/gh-1046/config.yml
t/issues/gh-1046/gh-1046.t
t/issues/gh-1070.t
t/issues/gh-1098.t
t/issues/gh-1216/gh-1216.t
t/issues/gh-1216/lib/App.pm
t/issues/gh-1216/lib/App/Extra.pm
t/issues/gh-1216/lib/Dancer2/Plugin/Null.pm
t/issues/gh-1226/gh-1226.t
t/issues/gh-1226/lib/App.pm
t/issues/gh-1226/lib/App/Extra.pm
t/issues/gh-1226/lib/Dancer2/Plugin/Test/AccessDSL.pm
t/issues/gh-1230/gh-1230.t
t/issues/gh-1230/lib/App.pm
t/issues/gh-1230/lib/App/Extra.pm
t/issues/gh-1230/lib/Dancer2/Plugin/Test/AccessDSL.pm
t/issues/gh-1230/lib/Dancer2/Plugin/Test/AccessPluginDSL.pm
t/issues/gh-1232.t
t/issues/gh-1289.t
t/issues/gh-1449/TestPlugin.pm
t/issues/gh-1449/gh-1449.t
t/issues/gh-1564.t
t/issues/gh-1621/gh-1621.t
t/issues/gh-1621/views/redirect.tt
t/issues/gh-1664/gh-1664.t
t/issues/gh-1712/gh-1712.t
t/issues/gh-1712/views/exec.tt
t/issues/gh-1861.t
t/issues/gh-1863.t
t/issues/gh-596.t
t/issues/gh-634.t
t/issues/gh-639/fails/.dancer
t/issues/gh-639/fails/config.yml
t/issues/gh-639/fails/issue.t
t/issues/gh-639/succeeds/.dancer
t/issues/gh-639/succeeds/config.yml
t/issues/gh-639/succeeds/issue.t
t/issues/gh-650/gh-650.t
t/issues/gh-650/views/environment_setting.tt
t/issues/gh-723.t
t/issues/gh-730.t
t/issues/gh-762.t
t/issues/gh-762/views/404.tt
t/issues/gh-794.t
t/issues/gh-797.t
t/issues/gh-799.t
t/issues/gh-811.t
t/issues/gh-931.t
t/issues/gh-936.t
t/issues/gh-936/views/error.tt
t/issues/gh-944.t
t/issues/gh-975/config.yml
t/issues/gh-975/gh-975.t
t/issues/gh-975/test_public_dir/test.txt
t/issues/memleak/config.yml
t/issues/memleak/die_in_hooks.t
t/issues/vars-in-forward.t
t/lib/App1.pm
t/lib/App2.pm
t/lib/Dancer2/ConfigReader/Additional.pm
t/lib/Dancer2/ConfigReader/File/Extended.pm
t/lib/Dancer2/ConfigReader/Recursive.pm
t/lib/Dancer2/ConfigReader/TestDummy.pm
t/lib/Dancer2/Plugin/Bar.pm
t/lib/Dancer2/Plugin/DancerPlugin.pm
t/lib/Dancer2/Plugin/DefineKeywords.pm
t/lib/Dancer2/Plugin/EmptyPlugin.pm
t/lib/Dancer2/Plugin/Foo.pm
t/lib/Dancer2/Plugin/FooPlugin.pm
t/lib/Dancer2/Plugin/Hookee.pm
t/lib/Dancer2/Plugin/OnPluginImport.pm
t/lib/Dancer2/Plugin/PluginWithImport.pm
t/lib/Dancer2/Plugin/Polite.pm
t/lib/Dancer2/Session/SimpleNoChangeId.pm
t/lib/Dancer2/Template/TemplateToolkitFoo.pm
t/lib/Foo.pm
t/lib/MyDancerDSL.pm
t/lib/PoC/Plugin/Polite.pm
t/lib/SubApp1.pm
t/lib/SubApp2.pm
t/lib/TemplateFoo.pm
t/lib/TestApp.pm
t/lib/TestPod.pm
t/lib/TestTypeLibrary.pm
t/lib/poc.pm
t/lib/poc2.pm
t/log_die_before_hook.t
t/log_levels.t
t/logger.t
t/logger_console.t
t/memory_cycles.t
t/mime.t
t/multi_apps.t
t/multi_apps_forward.t
t/multiapp_template_hooks.t
t/multipart_content.t
t/named_apps.t
t/named_routes.t
t/no_default_middleware.t
t/plugin2/app_dsl_cb/app_dsl_cb.t
t/plugin2/app_dsl_cb/lib/App.pm
t/plugin2/app_dsl_cb/lib/App/TestPlugin.pm
t/plugin2/basic-2.t
t/plugin2/basic.t
t/plugin2/define-keywords.t
t/plugin2/find_plugin.t
t/plugin2/from-config.t
t/plugin2/hooks.t
t/plugin2/inside-plugin.t
t/plugin2/keywords-hooks-namespace.t
t/plugin2/memory_cycles.t
t/plugin2/no-app-munging.t
t/plugin2/no-clobbering.t
t/plugin2/no-config.t
t/plugin2/with-plugins.t
t/plugin_import.t
t/plugin_multiple_apps.t
t/plugin_register.t
t/plugin_syntax.t
t/prepare_app.t
t/psgi_app.t
t/psgi_app_forward_and_pass.t
t/public/file.txt
t/redirect.t
t/request.t
t/request_make_forward_to.t
t/request_multipart_formdata.t
t/request_upload.t
t/response.t
t/roles/hook.t
t/route-pod-coverage/route-pod-coverage.t
t/route_query.t
t/scope_problems/config.yml
t/scope_problems/dispatcher_internal_request.t
t/scope_problems/keywords_before_template_hook.t
t/scope_problems/session_is_cleared.t
t/scope_problems/views/500.tt
t/scope_problems/with_return_dies.t
t/serializer.t
t/serializer_helpers.t
t/serializer_json.t
t/serializer_mutable.t
t/serializer_mutable_custom.t
t/session_bad_client_cookie.t
t/session_config.t
t/session_engines.t
t/session_forward.t
t/session_hooks.t
t/session_hooks_no_change_id.t
t/session_in_template.t
t/session_lifecycle.t
t/session_object.t
t/session_yaml_object.t
t/shared_engines.t
t/static_content.t
t/strict_config.t
t/template.t
t/template_default_tokens.t
t/template_ext.t
t/template_name.t
t/time.t
t/types.t
t/unit/configreader/merging.t
t/unit/configreader/readers.t
t/unit/configreader/strict.t
t/unit/core/cookie.t
t/unit/core/request.t
t/unit/core/response.t
t/unit/core/route.t
t/unit/logger/format.t
t/unit/logger/levels.t
t/utf8_strict.t
t/utf8_url.t
t/vars.t
t/views/auto_page.tt
t/views/beforetemplate.tt
t/views/folder/page.tt
t/views/index.tt
t/views/layouts/main.tt
t/views/session_in_template.tt
t/views/template_simple_index.tt
t/views/tokens.tt
xt/perlcritic.rc
xt/perltidy.rc
xt/whitespace.t
README.md100644000765000024      2026215267764222 13767 0ustar00jasonstaff000000000000Dancer2-2.3.0

Perl Dancer logo

Dancer2 is a lightweight yet powerful web application framework written in Perl.
Tutorial · Manual · Discussion Forums · Public Wiki · Mailing List


Dancer2 is the evolution of [Dancer](https://metacpan.org/pod/Dancer) and is based on on [Moo](https://metacpan.org/pod/Moo), a lightweight object framework for Perl. Dancer2 can optionally use XS modules for speed, but at its core remains fatpackable (via [App::FatPacker](https://metacpan.org/pod/App%3A%3AFatPacker)), allowing you to easily deploy Dancer2 applications in environments that do not support custom installations of CPAN modules. Dancer2 is easy and fun: use Dancer2; get '/' => sub { "Hello World" }; dance; ## Documentation Index You have questions. We have answers. - Dancer2 Tutorial Want to learn by example? The [Dancer2 tutorial](https://metacpan.org/pod/Dancer2%3A%3AManual%3A%3ATutorial) will take you from installation to a working application. - Quick Start Want to get going faster? [Quick Start](https://metacpan.org/dist/Dancer2/view/lib/Dancer2/Manual/QuickStart.pod) will help you install Dancer2 and bootstrap a new application quickly. - Manual Want to gain understanding of Dancer2 so you can use it best? The [Dancer2::Manual](https://metacpan.org/pod/Dancer2%3A%3AManual) is a comprehensive guide to the framework. - Keyword Guide Looking for a list of all the keywords? The [DSL guide](https://metacpan.org/pod/Dancer2%3A%3AManual%3A%3AKeywords) documents the entire Dancer2 DSL. - Configuration Need to fine tune your application? The [configuration guide](https://metacpan.org/pod/Dancer2%3A%3AConfig) is a complete reference to all configuration options. - Deployment Ready to get your application off the ground? [Deploying Dancer2 Applications](https://metacpan.org/pod/Dancer2%3A%3AManual%3A%3ADeployment) helps you deploy your application to a real-world host. - Cookbook How do I...? Our [cookbook](https://metacpan.org/dist/Dancer2/view/lib/Dancer2/Manual/Cookbook.pod) comes with various recipes in many tasty flavors! - Plugins Looking for add-on functionality for your application? The [plugin guide](https://metacpan.org/pod/Dancer2%3A%3APlugins) contains our curated list of recommended plugins. For information on how to author a plugin, see [the plugin author's guide](https://metacpan.org/pod/Dancer2%3A%3APlugin#Writing-the-plugin). - Dancer2 Migration Guide Starting from Dancer 1? Jump over to [the migration guide](https://metacpan.org/pod/Dancer2%3A%3AManual%3A%3AMigration) to learn how to make the smoothest transition to Dancer2. ### Other Documentation - Core and Community Policy, and Standards of Conduct The [Dancer core and community policy, and standards of conduct](https://metacpan.org/pod/Dancer2%3A%3APolicy) defines what constitutes acceptable behavior in our community, what behavior is considered abusive and unacceptable, and what steps will be taken to remediate inappropriate and abusive behavior. By participating in any public forum for Dancer or its community, you are agreeing to the terms of this policy. - GitHub Wiki Our [wiki](https://github.com/PerlDancer/Dancer2/wiki) has community-contributed documentation, as well as other information that doesn't quite fit within this manual. - Contributing The [contribution guidelines](https://github.com/PerlDancer/Dancer2/blob/main/CONTRIBUTING.md) describe how to set up your development environment to contribute to the development of Dancer2, Dancer2's Git workflow, submission guidelines, and various coding standards. - Deprecation Policy The [deprecation policy](https://metacpan.org/pod/Dancer2%3A%3ADeprecationPolicy) defines the process for removing old, broken, unused, or outdated code from the Dancer2 codebase. This policy is critical for guiding and shaping future development of Dancer2. # Security Reports If you need to report a security vulnerability in Dancer2, send all pertinent information to [dancer-security@dancer.pm](mailto:dancer-security@dancer.pm), or report it via the GitHub security tool. These reports will be addressed in the earliest possible timeframe. # Support You are welcome to join our mailing list. For subscription information, mail address and archives see [http://lists.preshweb.co.uk/mailman/listinfo/dancer-users](http://lists.preshweb.co.uk/mailman/listinfo/dancer-users). We are also on IRC: #dancer on irc.perl.org. # Authors ## Dancer Core Team Alberto Simões Alexis Sukrieh D Ruth Holloway (GeekRuthie) Damien Krotkine David Precious Franck Cuny Jason A. Crome Mickey Nasriachi Peter Mottram (SysPete) Russell Jenkins Sawyer X Stefan Hornburg (Racke) Yanick Champoux ## Core Team Emeritus David Golden Steven Humphrey ## Contributors A. Sinan Unur Abdullah Diab Achyut Kumar Panda Ahmad M. Zawawi Alex Beamish Alexander Karelas Alexander Pankoff Alexandr Ciornii Andrew Beverley Andrew Grangaard Andrew Inishev Andrew Solomon Andy Jack Ashvini V B10m Bas Bloemsaat baynes Ben Hutton Ben Kaufman biafra Blabos de Blebe Breno G. de Oliveira cdmalon Celogeek Cesare Gargano Charlie Gonzalez chenchen000 Chi Trinh Christian Walde Christopher White cloveistaken Colin Kuskie cym0n Dale Gallagher Dan Book (Grinnz) Daniel Böhmer Daniel Muey Daniel Perrett Dave Jacoby Dave Webb David (sbts) David Steinbrunner David Zurborg Davs Deirdre Moran Dennis Lichtenthäler Dinis Rebolo dtcyganov Elliot Holden Emil Perhinschi Erik Smit Fayland Lam ferki Gabor Szabo GeekRuthie geistteufel Gideon D'souza Gil Magno Glenn Fowler Graham Knop Gregor Herrmann Grzegorz Rożniecki Hobbestigrou Hunter McMillen ice-lenor Ivan Bessarabov Ivan Kruglov JaHIY Jakob Voss James Aitken James Raspass James McCoy Jason Lewis Javier Rojas Jean Stebens Jens Rehsack Joel Berger Johannes Piehler Jonathan Cast Jonathan Scott Duff Joseph Frazer Julien Fiegehenn (simbabque) Julio Fraire Kaitlyn Parkhurst (SYMKAT) kbeyazli Keith Broughton lbeesley Lennart Hengstmengel Ludovic Tolhurst-Cleaver Mario Zieschang Mark A. Stratman Marketa Wachtlova Masaaki Saito Mateu X Hunter Matt Phillips Matt S Trout mauke Maurice MaxPerl Ma_Sys.ma Menno Blom Michael Kröll Michał Wojciechowski Mike Katasonov Mohammad S Anwar mokko Nick Tonkin Nigel Gregoire Nikita K Nova Patch Nuno Carvalho Olaf Alders Olivier Mengué Omar M. Othman pants Patrick Zimmermann Pau Amma Paul Clements Paul Cochrane Paul Williams Pedro Bruno Pedro Melo Philippe Bricout Ricardo Signes Rick Yakubowski Ruben Amortegui Sakshee Vijay (sakshee3) Sam Kington Samit Badle Sebastien Deseille (sdeseille) Sergiy Borodych Shlomi Fish Slava Goltser Snigdha Steve Bertrand Steve Dondley Steven Humphrey Tatsuhiko Miyagawa Timothy Alexis Vass Tina Müller Tom Hukins Upasana Shukla Utkarsh Gupta Vernon Lyon Victor Adam Vince Willems Vincent Bachelier xenu Yves Orton # Author Dancer Core Developers # Copyright and License This software is copyright (c) 2024 by Alexis Sukrieh. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. error.t100644000765000024 2216415267764222 14274 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use lib 't/lib'; use Test::More import => ['!pass']; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; use List::Util qw; use Module::Runtime qw/ require_module /; use Dancer2::Core::App; use Dancer2::Core::Response; use Dancer2::Core::Request; use Dancer2::Core::Error; use JSON::MaybeXS qw/JSON/; # Error serialization my $env = { 'psgi.url_scheme' => 'http', REQUEST_METHOD => 'GET', SCRIPT_NAME => '/foo', PATH_INFO => '/bar/baz', REQUEST_URI => '/foo/bar/baz', QUERY_STRING => 'foo=42&bar=12&bar=13&bar=14', SERVER_NAME => 'localhost', SERVER_PORT => 5000, SERVER_PROTOCOL => 'HTTP/1.1', REMOTE_ADDR => '127.0.0.1', HTTP_COOKIE => 'dancer.session=1234; fbs_102="access_token=xxxxxxxxxx%7Cffffff"', HTTP_X_FORWARDED_FOR => '127.0.0.2', REMOTE_HOST => 'localhost', HTTP_USER_AGENT => 'Mozilla', REMOTE_USER => 'sukria', }; my $app = Dancer2::Core::App->new( name => 'main' ); my $request = $app->build_request($env); $app->set_request($request); subtest 'basic defaults of Error object' => sub { my $err = Dancer2::Core::Error->new( app => $app ); is $err->status, 500, 'code'; is $err->title, 'Error 500 - Internal Server Error', 'title'; is $err->message, '', 'message'; like $err->content, qr!http://localhost:5000/foo/css!, "error content contains css path relative to uri_base"; }; subtest "send_error in route" => sub { { package App; use Dancer2; set serializer => 'JSON'; get '/error' => sub { send_error "This is a custom error message"; return "send_error returns so this content is not processed"; }; } my $app = App->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; my $r = $cb->( GET '/error' ); is( $r->code, 500, 'send_error sets the status to 500' ); like( $r->content, qr{This is a custom error message}, 'Error message looks good', ); is( $r->content_type, 'application/json', 'Response has appropriate content type after serialization', ); }; }; subtest "send_error with custom stuff" => sub { { package App; use Dancer2; get '/error/:x' => sub { my $x = param('x'); send_error "Error $x", "5$x"; }; } my $app = App->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; my $r = $cb->( GET '/error/42' ); is( $r->code, 542, 'send_error sets the status to 542' ); like( $r->content, qr{Error 42}, 'Error message looks good' ); }; }; subtest 'Response->error()' => sub { my $resp = Dancer2::Core::Response->new; isa_ok $resp->error( message => 'oops', status => 418 ), 'Dancer2::Core::Error'; is $resp->status => 418, 'response code is 418'; like $resp->content => qr/oops/, 'response content overriden by error'; like $resp->content => qr/teapot/, 'error code title is present'; ok $resp->is_halted, 'response is halted'; }; subtest 'Throwing an error with a response' => sub { my $resp = Dancer2::Core::Response->new; my $err = eval { Dancer2::Core::Error->new( exception => 'our exception', show_stacktrace => 1 )->throw($resp) }; isa_ok($err, 'Dancer2::Core::Response', "Error->throw() accepts a response"); }; subtest 'Error with show_stacktrace: 0' => sub { my $err = Dancer2::Core::Error->new( exception => 'our exception', show_stacktrace => 0 )->throw; unlike $err->content => qr/our exception/; }; subtest 'Error with show_stacktrace: 1' => sub { my $err = Dancer2::Core::Error->new( exception => 'our exception', show_stacktrace => 1 )->throw; like $err->content => qr/our exception/; }; subtest 'App dies with serialized error' => sub { { package AppDies; use Dancer2; set serializer => 'JSON'; get '/die' => sub { die "oh no\n"; # I should serialize }; } my $app = AppDies->to_app; isa_ok( $app, 'CODE', 'Got app' ); test_psgi $app, sub { my $cb = shift; my $r = $cb->( GET '/die' ); is( $r->code, 500, '/die returns 500' ); my $out = eval { JSON->new->utf8(0)->decode($r->decoded_content) }; ok(!$@, 'JSON decoding serializer error produces no errors'); isa_ok($out, 'HASH', 'Error deserializes to a hash'); like($out->{exception}, qr/^oh no/, 'Get expected error message'); }; }; subtest 'Error with exception object' => sub { local $@; eval { MyTestException->throw('a test exception object') }; my $exception = $@; my $err = Dancer2::Core::Error->new( app => Dancer2::Core::App->new(), exception => $exception, show_stacktrace => 1, )->throw; like $err->content, qr/a test exception object/, 'Error content contains exception message'; }; subtest 'Errors without server tokens' => sub { { package AppNoServerTokens; use Dancer2; set serializer => 'JSON'; set no_server_tokens => 1; get '/ohno' => sub { die "oh no"; }; } my $test = Plack::Test->create( AppNoServerTokens->to_app ); my $r = $test->request( GET '/ohno' ); is( $r->code, 500, "/ohno returned 500 response"); is( $r->header('server'), undef, "No server header when no_server_tokens => 1" ); }; subtest 'Errors with show_stacktrace and circular references' => sub { { package App::ShowErrorsCircRef; use Dancer2; set show_stacktrace => 1; set something_with_config => {something => config}; set password => '===VERY-UNIQUE-STRING==='; set innocent_thing => '===VERY-INNOCENT-STRING==='; set template => 'tiny'; # Trigger an error that makes Dancer2::Core::Error::_censor enter an # infinite loop get '/ohno' => sub { template q{I don't exist}; }; } my $test = Plack::Test->create( App::ShowErrorsCircRef->to_app ); my $r = $test->request( GET '/ohno' ); is( $r->code, 500, "/ohno returned 500 response"); like( $r->content, qr{Stack}, 'it includes a stack trace' ); my @password_values = ($r->content =~ /\bpassword\b(.+)\n/g); my $is_password_hidden = all { /Hidden \(looks potentially sensitive\)/ } @password_values; ok($is_password_hidden, "password was hidden in stacktrace"); cmp_ok(@password_values, '>', 1, 'password key appears more than once in the stacktrace'); unlike($r->content, qr{===VERY-UNIQUE-STRING===}, 'password value does not appear in the stacktrace'); like($r->content, qr{===VERY-INNOCENT-STRING===}, 'Values for other keys (non-sensitive) appear in the stacktrace'); }; subtest censor => sub { sub MyApp::Censor::censor { $_[0]->{hush} = 'NOT TELLING'; return 1; } my $app = Dancer2::Core::App->new( name => 'main' ); $app->setting( password => 'potato' ); # oh my, we're leaking a password subtest 'core censor()' => sub { my $error = Dancer2::Core::Error->new( app => $app ); unlike $error->environment => qr/potato/, 'the password is censored'; like $error->environment => qr/^.*password.*Hidden.*$/m, 'we say it is hidden'; }; subtest 'custom censor' => sub { subtest 'via function string' => sub { my $app = Dancer2::Core::App->new( name => 'main' ); my $error = Dancer2::Core::Error->new( app => $app ); $app->setting( hush => 'potato' ); $app->setting( error_censor => 'MyApp::Censor::censor' ); unlike $error->environment => qr/potato/, 'the password is censored'; like $error->environment => qr/^ .* hush .* NOT \s TELLING .* $/xm, 'we say it is hidden'; }; subtest 'via class hashref' => sub { my $app = Dancer2::Core::App->new( name => 'main' ); $app->setting( 'error_censor' => { 'Data::Censor' => { sensitive_fields => ['hush'], replacement => 'NOT TELLING', } }); my $error = Dancer2::Core::Error->new( app => $app ); $app->setting( hush => 'potato' ); unlike $error->environment => qr/potato/, 'the password is censored'; like $error->environment => qr/^ .* hush .* NOT \s TELLING .* $/xm, 'we say it is hidden'; }; } }; done_testing; { # Simple test exception class package MyTestException; use overload '""' => \&as_str; sub new { return bless {}; } sub throw { my ( $class, $error ) = @_; my $self = ref($class) ? $class : $class->new; $self->{error} = $error; die $self; } sub as_str { return $_[0]->{error} } } hooks.t100644000765000024 2256615267764222 14274 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Path::Tiny (); use Plack::Test; use HTTP::Request::Common; use Capture::Tiny 0.12 'capture_stderr'; use JSON::MaybeXS; eval { require Template; 1; } or plan skip_all => 'Template::Toolkit not present'; my $tests_flags = {}; { package App::WithSerializer; use Dancer2; use Ref::Util qw; set serializer => 'JSON'; my @hooks = qw( before_request after_request before_serializer after_serializer ); for my $hook (@hooks) { hook $hook => sub { $tests_flags->{$hook} ||= 0; $tests_flags->{$hook}++; }; } get '/' => sub { +{ "ok" => 1 } }; hook 'before_serializer' => sub { my ($data) = @_; # don't shift, want to alias.. if ( is_arrayref($data) ) { push( @{$data}, ( added_in_hook => 1 ) ); } elsif ( is_hashref($data) ) { $data->{'added_in_hook'} = 1; } else { $_[0] = +{ 'added_in_hook' => 1 }; } }; get '/forward' => sub { Test::More::note 'About to forward!'; forward '/' }; get '/redirect' => sub { redirect '/' }; get '/json' => sub { +[ foo => 42 ] }; get '/nothing' => sub { return }; } { package App::WithFile; use Dancer2; my @hooks = qw< before_file_render after_file_render >; for my $hook (@hooks) { hook $hook => sub { $tests_flags->{$hook} ||= 0; $tests_flags->{$hook}++; }; } get '/send_file' => sub { send_file( Path::Tiny::path(__FILE__)->absolute->stringify, system_path => 1 ); }; } { package App::WithTemplate; use Dancer2; set template => 'tiny'; my @hooks = qw( before_template_render after_template_render ); for my $hook (@hooks) { hook $hook => sub { $tests_flags->{$hook} ||= 0; $tests_flags->{$hook}++; }; } get '/template' => sub { template \"PLOP"; }; } { package App::WithIntercept; use Dancer2; get '/intercepted' => sub {'not intercepted'}; hook before => sub { response->content('halted by before'); halt; }; } { package App::WithError; use Dancer2; my @hooks = qw( on_route_exception ); for my $hook (@hooks) { hook $hook => sub { $tests_flags->{$hook} ||= 0; $tests_flags->{$hook}++; }; } get '/route_exception' => sub {die 'this is a route exception'}; hook after => sub { # GH#540 - ensure setting default scalar does not # interfere with hook execution (aliasing) $_ = 42; }; hook on_route_exception => sub { my ($app, $error) = @_; ::is ref($app), 'Dancer2::Core::App'; ::like $error, qr/this is a route exception/; }; hook init_error => sub { my ($error) = @_; ::is ref($error), 'Dancer2::Core::Error'; }; hook before_error => sub { my ($error) = @_; ::is ref($error), 'Dancer2::Core::Error'; }; hook after_error => sub { my ($response) = @_; ::is ref($response), 'Dancer2::Core::Response'; ::ok !$response->is_halted; ::like $response->content, qr/Internal Server Error/; }; } # 3 test apps for the exception hook - see comments below { package App::HookException; use Dancer2; get '/hook_exception' => sub { return 1 }; hook after => sub { die 'this is an exception in the after hook'; }; hook on_hook_exception => sub { my ($app, $error, $hook_name) = @_; ::is ref($app), 'Dancer2::Core::App'; ::like $error, qr/this is an exception in the after hook/; ::is $hook_name, 'core.app.after_request'; $app->response->content("Setting response from exception hook"); $app->response->halt; }; } { package App::HookExceptionDouble; use Dancer2; get '/hook_exception_double' => sub { return 1 }; hook after => sub { die 'this is an exception in the after hook'; }; hook on_hook_exception => sub { my ($app, $error) = @_; $app->response->content("Setting response from exception hook and then dying"); $app->response->halt; die 'this is an exception in the exception hook'; }; } { package App::HookExceptionRecursive; use Dancer2; get '/hook_exception_recursive' => sub { return 1 }; hook after => sub { die 'this is an exception in the after hook'; }; hook on_hook_exception => sub { my ($app, $error) = @_; die 'this is an exception in the hook exception causing recursion'; }; } subtest 'Request hooks' => sub { my $test = Plack::Test->create( App::WithSerializer->to_app ); $test->request( GET '/' ); is( $tests_flags->{before_request}, 1, "before_request was called" ); is( $tests_flags->{after_request}, 1, "after_request was called" ); is( $tests_flags->{before_serializer}, 1, "before_serializer was called" ); is( $tests_flags->{after_serializer}, 1, "after_serializer was called" ); is( $tests_flags->{before_file_render}, undef, "before_file_render undef" ); note 'after hook called once per request'; # Get current value of the 'after_request' tests flag. my $current = $tests_flags->{after_request}; $test->request( GET '/redirect' ); is( $tests_flags->{after_request}, ++$current, "after_request called after redirect", ); note 'Serializer hooks'; $test->request( GET '/forward' ); is( $tests_flags->{after_request}, ++$current, "after_request called only once after forward", ); my $res = $test->request( GET '/json' ); is( $res->content, '["foo",42,"added_in_hook",1]', 'Response serialized' ); is( $tests_flags->{before_serializer}, 4, 'before_serializer was called' ); is( $tests_flags->{after_serializer}, 4, 'after_serializer was called' ); is( $tests_flags->{before_file_render}, undef, "before_file_render undef" ); $res = $test->request( GET '/nothing' ); is( $res->content, '{"added_in_hook":1}', 'Before hook modified content' ); is( $tests_flags->{before_serializer}, 5, 'before_serializer was called with no content' ); is( $tests_flags->{after_serializer}, 5, 'after_serializer was called after content changes in hook' ); }; subtest 'file render hooks' => sub { my $test = Plack::Test->create( App::WithFile->to_app ); $test->request( GET '/send_file' ); is( $tests_flags->{before_file_render}, 1, "before_file_render was called" ); is( $tests_flags->{after_file_render}, 1, "after_file_render was called" ); }; subtest 'template render hook' => sub { my $test = Plack::Test->create( App::WithTemplate->to_app ); $test->request( GET '/template' ); is( $tests_flags->{before_template_render}, 1, "before_template_render was called", ); is( $tests_flags->{after_template_render}, 1, "after_template_render was called", ); }; subtest 'before can halt' => sub { my $test = Plack::Test->create( App::WithIntercept->to_app ); my $resp = $test->request( GET '/intercepted' ); is( $resp->content, 'halted by before' ); }; subtest 'route_exception' => sub { my $test = Plack::Test->create( App::WithError->to_app ); capture_stderr { $test->request( GET '/route_exception' ) }; }; # A basic test for a hook that is called in the event of an exception in a hook subtest 'hook_exception' => sub { my $test = Plack::Test->create( App::HookException->to_app ); my $resp = $test->request( GET '/hook_exception' ); is( $resp->content, 'Setting response from exception hook' ); }; # A more advanced test that checks that an exception can take place within the # exception hook, but only if it sets the response content and performs a halt subtest 'hook_exception_double' => sub { my $test = Plack::Test->create( App::HookExceptionDouble->to_app ); my $resp = $test->request( GET '/hook_exception_double'); is( $resp->content, 'Setting response from exception hook and then dying' ); }; # Similar to the above, but without any handling for the second exception which # would otherwise result in a recursive situation subtest 'hook_exception_recursive' => sub { my $test = Plack::Test->create( App::HookExceptionRecursive->to_app ); my $resp = $test->request( GET '/hook_exception_recursive'); like( $resp->content, qr/Internal Server Error/ ); }; subtest 'hook entries logging' => sub { $::hook_counter = 0; package App::HookEntries { use Sub::Util qw/ set_subname /; use Dancer2; set log => 'core'; set logger => 'capture'; $::trap = engine('logger')->trapper; get '/' => sub { 'hello there' }; hook 'before_request' => set_subname my_before => sub { $::hook_counter++; }; sub my_after { $::hook_counter++ } hook 'after_request' => \&my_after; } my $app = App::HookEntries->to_app; my $test = Plack::Test->create( $app ); $test->request( GET '/' ); is $::hook_counter => 2, "we hit both hooks"; my @logs = map {$_->{message}} @{$::trap->read}; for my $hook ( qw/ my_before my_after / ) { ok scalar( grep { /$hook/ } @logs ), "App::HookEntries::$hook" } }; done_testing; types.t100644000765000024 1303015267764222 14277 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More tests => 51; use Test::Fatal; use Dancer2::Core::Types; ok( exception { Str->(undef) }, 'Str does not accept undef value', ); is( exception { Str->('something') }, undef, 'Str', ); like( exception { Str->( { foo => 'something' } ) }, qr{Reference.+foo.+something.+did not pass type constraint.+Str}, 'Str', ); is( exception { Num->(34) }, undef, 'Num', ); ok( exception { Num->(undef) }, 'Num does not accept undef value', ); like( exception { Num->('not a number') }, qr{not a number.+did not pass type constraint.+Num}, 'Num fail', ); is( exception { Bool->(1) }, undef, 'Bool true value', ); is( exception { Bool->(0) }, undef, 'Bool false value', ); is( exception { Bool->(undef) }, undef, 'Bool does accepts undef value', ); like( exception { Bool->('2') }, qr{2.+did not pass type constraint.+Bool}, 'Bool fail', ); is( exception { RegexpRef->(qr{.*}) }, undef, 'Regexp', ); like( exception { RegexpRef->('/.*/') }, qr{\Q/.*/\E.+did not pass type constraint.+RegexpRef}, 'Regexp fail', ); ok( exception { RegexpRef->(undef) }, 'Regexp does not accept undef value', ); is( exception { HashRef->( { goo => 'le' } ) }, undef, 'HashRef', ); like( exception { HashRef->('/.*/') }, qr{\Q/.*/\E.+did not pass type constraint.+HashRef}, 'HashRef fail', ); ok( exception { HashRef->(undef) }, 'HashRef does not accept undef value', ); is( exception { ArrayRef->( [ 1, 2, 3, 4 ] ) }, undef, 'ArrayRef', ); like( exception { ArrayRef->('/.*/') }, qr{\Q/.*/\E.+did not pass type constraint.+ArrayRef}, 'ArrayRef fail', ); ok( exception { ArrayRef->(undef) }, 'ArrayRef does not accept undef value', ); is( exception { CodeRef->( sub {44} ); }, undef, 'CodeRef', ); like( exception { CodeRef->('/.*/') }, qr{\Q/.*/\E.+did not pass type constraint.+CodeRef}, 'CodeRef fail', ); ok( exception { CodeRef->(undef) }, 'CodeRef does not accept undef value', ); { package InstanceChecker::zad7; use Moo; use Dancer2::Core::Types; has foo => ( is => 'ro', isa => InstanceOf ['Foo'] ); } is( exception { InstanceChecker::zad7->new( foo => bless {}, 'Foo' ) }, undef, 'InstanceOf', ); like( exception { InstanceChecker::zad7->new( foo => bless {}, 'Bar' ) }, qr{Reference bless.+Bar.+not isa Foo}, 'InstanceOf fail', ); ok( exception { InstanceOf('Foo')->(undef) }, 'InstanceOf does not accept undef value', ); is( exception { Dancer2Prefix->('/foo') }, undef, 'Dancer2Prefix', ); like( exception { Dancer2Prefix->('bar/something') }, qr{bar/something.+did not pass type constraint.+Dancer2Prefix}, 'Dancer2Prefix fail', ); # see Dancer2Prefix definition, undef is a valid value like( exception { Dancer2Prefix->(undef) }, qr/Undef.+did not pass type constraint.+Dancer2Prefix/, 'Dancer2Prefix does not accept undef value', ); is( exception { Dancer2AppName->('Foo') }, undef, 'Dancer2AppName', ); is( exception { Dancer2AppName->('Foo::Bar') }, undef, 'Dancer2AppName', ); is( exception { Dancer2AppName->('Foo::Bar::Baz') }, undef, 'Dancer2AppName', ); like( exception { Dancer2AppName->('Foo:Bar') }, qr{Foo:Bar is not a Dancer2AppName}, 'Dancer2AppName fails with single colons', ); like( exception { Dancer2AppName->('Foo:::Bar') }, qr{Foo:::Bar is not a Dancer2AppName}, 'Dancer2AppName fails with tripe colons', ); like( exception { Dancer2AppName->('7Foo') }, qr{7Foo is not a Dancer2AppName}, 'Dancer2AppName fails with beginning number', ); like( exception { Dancer2AppName->('Foo::45Bar') }, qr{Foo::45Bar is not a Dancer2AppName}, 'Dancer2AppName fails with beginning number', ); like( exception { Dancer2AppName->('-F') }, qr{-F is not a Dancer2AppName}, 'Dancer2AppName fails with special character', ); like( exception { Dancer2AppName->('Foo::-') }, qr{Foo::- is not a Dancer2AppName}, 'Dancer2AppName fails with special character', ); like( exception { Dancer2AppName->('Foo^') }, qr{\QFoo^\E is not a Dancer2AppName}, 'Dancer2AppName fails with special character', ); ok( exception { Dancer2AppName->(undef) }, 'Dancer2AppName does not accept undef value', ); like( exception { Dancer2AppName->('') }, qr{Empty string is not a Dancer2AppName}, 'Dancer2AppName fails an empty string value', ); is( exception { Dancer2Method->('post') }, undef, 'Dancer2Method', ); like( exception { Dancer2Method->('POST') }, qr{POST.+did not pass type constraint.+Dancer2Method}, 'Dancer2Method fail', ); ok( exception { Dancer2Method->(undef) }, 'Dancer2Method does not accept undef value', ); is( exception { Dancer2HTTPMethod->('POST') }, undef, 'Dancer2HTTPMethod', ); like( exception { Dancer2HTTPMethod->('post') }, qr{post.+did not pass type constraint.+Dancer2HTTPMethod}, 'Dancer2HTTPMethod fail', ); ok( exception { Dancer2HTTPMethod->(undef) }, 'Dancer2Method does not accept undef value', ); use Dancer2::Core::Error; use Dancer2::Core::Hook; ok( exception { Hook->(undef) }, 'Hook does not accept undef value' ); ok(exception { Hook->(Dancer2::Core::Error->new) }, 'Hook does not Core::Error as value'); is( exception { Hook->(Dancer2::Core::Hook->new(name => 'test', code => sub { })) }, undef, 'Hook', ); is(exception { ReadableFilePath->('t') }, undef, 'ReadableFilePath'); like( exception { ReadableFilePath->('nosuchdirectory') }, qr/Value "nosuchdirectory" did not pass type constraint "ReadableFilePath"/, 'ReadableFilePath' ); META.json100644000765000024 3040515267764222 14131 0ustar00jasonstaff000000000000Dancer2-2.3.0{ "abstract" : "Lightweight yet powerful web application framework", "author" : [ "Dancer Core Developers" ], "dynamic_config" : 1, "generated_by" : "Dist::Zilla version 6.037, CPAN::Meta::Converter version 2.150013", "license" : [ "perl_5" ], "meta-spec" : { "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", "version" : 2 }, "name" : "Dancer2", "prereqs" : { "configure" : { "requires" : { "CPAN::Meta::Requirements" : "2.120620", "ExtUtils::MakeMaker" : "0", "File::ShareDir::Install" : "0.06", "Module::Metadata" : "0" } }, "develop" : { "requires" : { "AnyEvent" : "0", "CBOR::XS" : "0", "Class::Method::Modifiers" : "0", "Dist::Zilla::Plugin::Test::UnusedVars" : "0", "Perl::Tidy" : "0", "Test::CPAN::Meta" : "0", "Test::Memory::Cycle" : "0", "Test::MockTime" : "0", "Test::More" : "0.88", "Test::NoTabs" : "0", "Test::Perl::Critic" : "0", "Test::Pod" : "1.41", "Test::Whitespaces" : "0", "YAML::XS" : "0" } }, "runtime" : { "conflicts" : { "Module::Pluggable" : "== 6.2", "Role::Tiny" : "== 2.000007" }, "recommends" : { "CGI::Deurl::XS" : "0", "Class::XSAccessor" : "0", "Cpanel::JSON::XS" : "0", "HTTP::XSCookies" : "0.000015", "HTTP::XSHeaders" : "0", "MooX::TypeTiny" : "0", "Pod::Simple::Search" : "0", "Pod::Simple::SimpleTree" : "0", "Type::Tiny::XS" : "0", "URL::Encode::XS" : "0", "Unicode::UTF8" : "0", "YAML::XS" : "0" }, "requires" : { "Attribute::Handlers" : "0", "CLI::Osprey" : "0.09", "Carp" : "0", "Clone" : "0", "Config::Any" : "0", "Crypt::URandom" : "0.36", "Data::Censor" : "0.04", "Digest::SHA" : "0", "Encode" : "0", "Exporter" : "5.57", "Exporter::Tiny" : "0", "File::Copy" : "0", "File::Path" : "0", "File::Share" : "0", "File::Temp" : "0", "File::Which" : "0", "HTTP::Date" : "0", "HTTP::Headers::Fast" : "0.21", "HTTP::Tiny" : "0", "Hash::Merge::Simple" : "0", "Hash::MultiValue" : "0", "Import::Into" : "0", "JSON::MaybeXS" : "0", "List::Util" : "1.29", "MIME::Base64" : "3.13", "Module::Pluggable" : "0", "Module::Runtime" : "0", "Moo" : "2.000000", "Moo::Role" : "0", "POSIX" : "0", "Path::Tiny" : "0", "Plack" : "1.0040", "Plack::Middleware::FixMissingBodyInRedirect" : "0", "Plack::Middleware::RemoveRedundantBody" : "0", "Ref::Util" : "0", "Role::Tiny" : "2.000000", "Safe::Isa" : "0", "Sub::Quote" : "0", "Sub::Util" : "0", "Template" : "0", "Template::Tiny" : "1.16", "Test::Builder" : "0", "Test::More" : "0.92", "Type::Tiny" : "1.000006", "Types::Standard" : "0", "URI::Escape" : "0", "YAML" : "1.30", "parent" : "0", "perl" : "5.014" }, "suggests" : { "Fcntl" : "0", "MIME::Types" : "0" } }, "test" : { "recommends" : { "CPAN::Meta" : "2.120900" }, "requires" : { "Capture::Tiny" : "0.12", "ExtUtils::MakeMaker" : "0", "File::Spec" : "0", "HTTP::Cookies" : "0", "HTTP::Headers" : "0", "IO::Handle" : "0", "IPC::Open3" : "0", "Pod::Simple::SimpleTree" : "0", "Template" : "0", "Test::Builder" : "0", "Test::EOL" : "0", "Test::Exception" : "0", "Test::Fatal" : "0", "Test::More" : "0.92" } } }, "provides" : { "Dancer2" : { "file" : "lib/Dancer2.pm", "version" : "v2.3.0" }, "Dancer2::CLI" : { "file" : "lib/Dancer2/CLI.pm", "version" : "v2.3.0" }, "Dancer2::CLI::Gen" : { "file" : "lib/Dancer2/CLI/Gen.pm", "version" : "v2.3.0" }, "Dancer2::CLI::Version" : { "file" : "lib/Dancer2/CLI/Version.pm", "version" : "v2.3.0" }, "Dancer2::ConfigReader" : { "file" : "lib/Dancer2/ConfigReader.pm", "version" : "v2.3.0" }, "Dancer2::ConfigReader::Config::Any" : { "file" : "lib/Dancer2/ConfigReader/Config/Any.pm", "version" : "v2.3.0" }, "Dancer2::ConfigUtils" : { "file" : "lib/Dancer2/ConfigUtils.pm", "version" : "v2.3.0" }, "Dancer2::Core" : { "file" : "lib/Dancer2/Core.pm", "version" : "v2.3.0" }, "Dancer2::Core::App" : { "file" : "lib/Dancer2/Core/App.pm", "version" : "v2.3.0" }, "Dancer2::Core::Cookie" : { "file" : "lib/Dancer2/Core/Cookie.pm", "version" : "v2.3.0" }, "Dancer2::Core::DSL" : { "file" : "lib/Dancer2/Core/DSL.pm", "version" : "v2.3.0" }, "Dancer2::Core::Dispatcher" : { "file" : "lib/Dancer2/Core/Dispatcher.pm", "version" : "v2.3.0" }, "Dancer2::Core::Error" : { "file" : "lib/Dancer2/Core/Error.pm", "version" : "v2.3.0" }, "Dancer2::Core::Factory" : { "file" : "lib/Dancer2/Core/Factory.pm", "version" : "v2.3.0" }, "Dancer2::Core::HTTP" : { "file" : "lib/Dancer2/Core/HTTP.pm", "version" : "v2.3.0" }, "Dancer2::Core::Hook" : { "file" : "lib/Dancer2/Core/Hook.pm", "version" : "v2.3.0" }, "Dancer2::Core::MIME" : { "file" : "lib/Dancer2/Core/MIME.pm", "version" : "v2.3.0" }, "Dancer2::Core::Request" : { "file" : "lib/Dancer2/Core/Request.pm", "version" : "v2.3.0" }, "Dancer2::Core::Request::Upload" : { "file" : "lib/Dancer2/Core/Request/Upload.pm", "version" : "v2.3.0" }, "Dancer2::Core::Response" : { "file" : "lib/Dancer2/Core/Response.pm", "version" : "v2.3.0" }, "Dancer2::Core::Response::Delayed" : { "file" : "lib/Dancer2/Core/Response/Delayed.pm", "version" : "v2.3.0" }, "Dancer2::Core::Role::ConfigReader" : { "file" : "lib/Dancer2/Core/Role/ConfigReader.pm", "version" : "v2.3.0" }, "Dancer2::Core::Role::DSL" : { "file" : "lib/Dancer2/Core/Role/DSL.pm", "version" : "v2.3.0" }, "Dancer2::Core::Role::Engine" : { "file" : "lib/Dancer2/Core/Role/Engine.pm", "version" : "v2.3.0" }, "Dancer2::Core::Role::Handler" : { "file" : "lib/Dancer2/Core/Role/Handler.pm", "version" : "v2.3.0" }, "Dancer2::Core::Role::HasConfig" : { "file" : "lib/Dancer2/Core/Role/HasConfig.pm", "version" : "v2.3.0" }, "Dancer2::Core::Role::HasEnvironment" : { "file" : "lib/Dancer2/Core/Role/HasEnvironment.pm", "version" : "v2.3.0" }, "Dancer2::Core::Role::HasLocation" : { "file" : "lib/Dancer2/Core/Role/HasLocation.pm", "version" : "v2.3.0" }, "Dancer2::Core::Role::Hookable" : { "file" : "lib/Dancer2/Core/Role/Hookable.pm", "version" : "v2.3.0" }, "Dancer2::Core::Role::Logger" : { "file" : "lib/Dancer2/Core/Role/Logger.pm", "version" : "v2.3.0" }, "Dancer2::Core::Role::Serializer" : { "file" : "lib/Dancer2/Core/Role/Serializer.pm", "version" : "v2.3.0" }, "Dancer2::Core::Role::SessionFactory" : { "file" : "lib/Dancer2/Core/Role/SessionFactory.pm", "version" : "v2.3.0" }, "Dancer2::Core::Role::SessionFactory::File" : { "file" : "lib/Dancer2/Core/Role/SessionFactory/File.pm", "version" : "v2.3.0" }, "Dancer2::Core::Role::StandardResponses" : { "file" : "lib/Dancer2/Core/Role/StandardResponses.pm", "version" : "v2.3.0" }, "Dancer2::Core::Role::Template" : { "file" : "lib/Dancer2/Core/Role/Template.pm", "version" : "v2.3.0" }, "Dancer2::Core::Route" : { "file" : "lib/Dancer2/Core/Route.pm", "version" : "v2.3.0" }, "Dancer2::Core::Runner" : { "file" : "lib/Dancer2/Core/Runner.pm", "version" : "v2.3.0" }, "Dancer2::Core::Session" : { "file" : "lib/Dancer2/Core/Session.pm", "version" : "v2.3.0" }, "Dancer2::Core::Time" : { "file" : "lib/Dancer2/Core/Time.pm", "version" : "v2.3.0" }, "Dancer2::Core::Types" : { "file" : "lib/Dancer2/Core/Types.pm", "version" : "v2.3.0" }, "Dancer2::FileUtils" : { "file" : "lib/Dancer2/FileUtils.pm", "version" : "v2.3.0" }, "Dancer2::Handler::AutoPage" : { "file" : "lib/Dancer2/Handler/AutoPage.pm", "version" : "v2.3.0" }, "Dancer2::Handler::File" : { "file" : "lib/Dancer2/Handler/File.pm", "version" : "v2.3.0" }, "Dancer2::Logger::Capture" : { "file" : "lib/Dancer2/Logger/Capture.pm", "version" : "v2.3.0" }, "Dancer2::Logger::Capture::Trap" : { "file" : "lib/Dancer2/Logger/Capture/Trap.pm", "version" : "v2.3.0" }, "Dancer2::Logger::Console" : { "file" : "lib/Dancer2/Logger/Console.pm", "version" : "v2.3.0" }, "Dancer2::Logger::Diag" : { "file" : "lib/Dancer2/Logger/Diag.pm", "version" : "v2.3.0" }, "Dancer2::Logger::File" : { "file" : "lib/Dancer2/Logger/File.pm", "version" : "v2.3.0" }, "Dancer2::Logger::Note" : { "file" : "lib/Dancer2/Logger/Note.pm", "version" : "v2.3.0" }, "Dancer2::Logger::Null" : { "file" : "lib/Dancer2/Logger/Null.pm", "version" : "v2.3.0" }, "Dancer2::Plugin" : { "file" : "lib/Dancer2/Plugin.pm", "version" : "v2.3.0" }, "Dancer2::Serializer::JSON" : { "file" : "lib/Dancer2/Serializer/JSON.pm", "version" : "v2.3.0" }, "Dancer2::Serializer::Mutable" : { "file" : "lib/Dancer2/Serializer/Mutable.pm", "version" : "v2.3.0" }, "Dancer2::Serializer::YAML" : { "file" : "lib/Dancer2/Serializer/YAML.pm", "version" : "v2.3.0" }, "Dancer2::Session::Simple" : { "file" : "lib/Dancer2/Session/Simple.pm", "version" : "v2.3.0" }, "Dancer2::Session::YAML" : { "file" : "lib/Dancer2/Session/YAML.pm", "version" : "v2.3.0" }, "Dancer2::Template::TemplateToolkit" : { "file" : "lib/Dancer2/Template/TemplateToolkit.pm", "version" : "v2.3.0" }, "Dancer2::Template::Tiny" : { "file" : "lib/Dancer2/Template/Tiny.pm", "version" : "v2.3.0" }, "Dancer2::Test" : { "file" : "lib/Dancer2/Test.pm", "version" : "v2.3.0" } }, "release_status" : "stable", "resources" : { "bugtracker" : { "web" : "https://github.com/PerlDancer/Dancer2/issues" }, "homepage" : "http://perldancer.org/", "repository" : { "type" : "git", "url" : "git://github.com/PerlDancer/Dancer2.git", "web" : "https://github.com/PerlDancer/Dancer2" }, "x_IRC" : "irc://irc.perl.org/#dancer", "x_WebIRC" : "https://chat.mibbit.com/#dancer@irc.perl.org" }, "version" : "2.3.0", "x_generated_by_perl" : "v5.44.0", "x_serialization_backend" : "Cpanel::JSON::XS version 4.43", "x_spdx_expression" : "Artistic-1.0-Perl OR GPL-1.0-or-later" } engine.t100644000765000024 263615267764222 14372 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Test::Fatal; use Dancer2::Core::App; use Dancer2::Template::Tiny; { my $f = Dancer2::Template::Tiny->new(); isa_ok( $f, 'Dancer2::Template::Tiny' ); ok( $f->does('Dancer2::Core::Role::Engine'), 'Consumed Role::Engine', ); ok( $f->does('Dancer2::Core::Role::Template'), 'Consumed Role::Template', ); is( $f->name, 'Tiny', 'Correct engine name' ); } # checks for validity of engine names my $app = Dancer2::Core::App->new(); isa_ok( $app, 'Dancer2::Core::App' ); { no warnings qw; *Dancer2::Core::Factory::create = sub { $_[1] }; } foreach my $engine_type ( qw ) { note($engine_type); my $engine; my $build_method = "_build_${engine_type}_engine"; is( exception { $engine = $app->$build_method( undef, { $engine_type => 'Fake43Thing' } ); }, undef, "Built $engine_type successfully with proper name", ); like( exception { $engine = $app->$build_method( undef, { $engine_type => '7&&afail' } ); }, qr/^Cannot load $engine_type engine '7&&afail': illegal module name/, "Failed creating $engine_type with illegal name", ); is( $engine, $engine_type, 'Correct response from override' ); } done_testing; caller.t100644000765000024 76415267764222 14347 0ustar00jasonstaff000000000000Dancer2-2.3.0/t#!perl use strict; use warnings; use Test::More tests => 2; use Plack::Test; use HTTP::Request::Common; use Path::Tiny qw< path >; { package App; use Dancer2; get '/' => sub { app->caller }; } my $app = App->to_app; test_psgi $app, sub { my $cb = shift; my $res = $cb->( GET '/' ); is( $res->code, 200, '[GET /] Successful' ); is( path( $res->content )->stringify, path(qw)->stringify, 'Correct App name from caller', ); }; config.t100644000765000024 471315267764222 14370 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Test::Fatal; use Carp 'croak'; # use Dancer2::Core::Runner; use Path::Tiny qw< path >; use Dancer2::ConfigReader; # undefine ENV vars used as defaults for app environment in these tests local $ENV{DANCER_ENVIRONMENT}; local $ENV{PLACK_ENV}; # my $runner = Dancer2::Core::Runner->new(); my $location = path( __FILE__ )->parent->child('config')->stringify; my $location2 = path( __FILE__ )->parent->child('config2')->stringify; { my $cfgr = Dancer2::ConfigReader->new( environment => 'my_env', location => $location, default_config => { content_type => 'text/html', charset => 'UTF-8', }, ); is( $cfgr->config->{'application'}->{'some_feature'}, 'foo', 'Ok config' ); is( $cfgr->config->{'charset'}, 'utf-8', 'Ok default config' ); } { # note "bad YAML file: environments/failure.yml"; like( exception { Dancer2::ConfigReader->new( environment => 'failure', location => $location, default_config => { }, )->config; }, qr{Unable to parse the configuration file}, 'Configuration file parsing failure', ); } { my $cfgr = Dancer2::ConfigReader->new( environment => 'any_env', location => $location, default_config => { }, ); my $cfg = $cfgr->config; isnt( $cfg, undef, 'OK config read' ); } { my $cfgr = Dancer2::ConfigReader->new( environment => 'merging', location => $location, default_config => { }, ); # note "config merging"; # Check the 'application' top-level key; its the only key that # is currently a HoH in the test configurations is_deeply $cfgr->config->{application}, { some_feature => 'bar', another_setting => 'baz', }, "full merging of configuration hashes"; } { my $cfgr = Dancer2::ConfigReader->new( environment => 'lconfig', location => $location2, default_config => { }, ); is_deeply $cfgr->config->{application}, { feature_1 => 'foo', feature_2 => 'alpha', feature_3 => 'replacement', feature_4 => 'blat', feature_5 => 'beta', feature_6 => 'bar', feature_7 => 'baz', feature_8 => 'goober', }, "full merging of local configuration hashes"; } done_testing; cookie.t100644000765000024 1663215267764222 14417 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::Fatal; use Test::More; BEGIN { # Freeze time at Tue, 15-Jun-2010 00:00:00 GMT *CORE::GLOBAL::time = sub { return 1276560000 } } use Dancer2::Core::Cookie; use Dancer2::Core::Request; if ( Dancer2::Core::Cookie::_USE_XS ) { subtest 'with HTTP::XSCookies' => \&all_tests; no warnings 'redefine'; *Dancer2::Core::Cookie::to_header = \&Dancer2::Core::Cookie::pp_to_header; } else { diag "If you want extra speed, install HTTP::XSCookies"; } subtest 'w/o HTTP::XSCookies' => \&all_tests; sub all_tests { my $cookie = Dancer2::Core::Cookie->new( name => "foo" ); subtest "Constructor" => sub { isa_ok $cookie => 'Dancer2::Core::Cookie'; can_ok $cookie => 'to_header'; }; subtest "Setting values" => sub { is $cookie->value("foo") => "foo", "Can set value"; is $cookie->value => "foo", "Set value stuck"; is $cookie . "bar", "foobar", "Stringifies to desired value"; ok $cookie->value( [qw(a b c)] ), "can set multiple values"; is $cookie->value => 'a', "get first value in scalar context"; is_deeply [ $cookie->value ] => [qw(a b c)], "get all values in list context";; is_deeply [ $cookie->values ] => [qw(a b c)], "values returns all values"; is scalar $cookie->values => 3, "values returns arrayref in scalar context"; ok $cookie->value( { x => 1, y => 2 } ), "can set values with a hashref"; like $cookie->value => qr/^[xy]$/; # hashes doesn't store order... is_deeply [ sort $cookie->value ] => [ sort ( 1, 2, 'x', 'y' ) ]; }; subtest "accessors and defaults" => sub { is $cookie->name => 'foo', "name is as expected"; is $cookie->name("bar") => "bar", "can change name"; is $cookie->name => 'bar', "name change stuck"; ok !$cookie->domain, "no domain set by default"; is $cookie->domain("dancer.org") => "dancer.org", "setting domain returns new value"; is $cookie->domain => "dancer.org", "new domain valjue stuck"; is $cookie->domain("") => "", "can clear domain"; ok !$cookie->domain, "no domain set now"; is $cookie->path => '/', "by default, path is /"; ok $cookie->has_path, "has_path"; is $cookie->path("/foo") => "/foo", "setting path returns new value"; ok $cookie->has_path, "has_path"; is $cookie->path => "/foo", "new path stuck"; ok !$cookie->secure, "no cookie secure flag by default"; is $cookie->secure(1) => 1, "enabling \$cookie->secure returns new value"; is $cookie->secure => 1, "\$cookie->secure flag is enabled"; is $cookie->secure(0) => 0, "disabling \$cookie->secure returns new value"; ok !$cookie->secure, "\$cookie->secure flag is disabled"; ok $cookie->http_only, "http_only by default"; is $cookie->http_only(0) => 0, "disabling \$cookie->http_only returns new value"; ok !$cookie->http_only, "\$cookie->http_only is now disabled"; like exception { $cookie->same_site('foo') }, qr/Value "foo" did not pass type constraint "Enum\["Strict","Lax","None"\]/; }; my %times = ( "+2" => "Tue, 15-Jun-2010 00:00:02 GMT", "+2h" => "Tue, 15-Jun-2010 02:00:00 GMT", "-2h" => "Mon, 14-Jun-2010 22:00:00 GMT", "1 hour" => "Tue, 15-Jun-2010 01:00:00 GMT", "3 weeks 4 days 2 hours 99 min 0 secs" => "Sat, 10-Jul-2010 03:39:00 GMT", "2 months" => "Sat, 14-Aug-2010 00:00:00 GMT", "12 years" => "Sun, 12-Jun-2022 00:00:00 GMT", 1288817656 => "Wed, 03-Nov-2010 20:54:16 GMT", 1288731256 => "Tue, 02-Nov-2010 20:54:16 GMT", 1288644856 => "Mon, 01-Nov-2010 20:54:16 GMT", 1288558456 => "Sun, 31-Oct-2010 20:54:16 GMT", 1288472056 => "Sat, 30-Oct-2010 20:54:16 GMT", 1288385656 => "Fri, 29-Oct-2010 20:54:16 GMT", 1288299256 => "Thu, 28-Oct-2010 20:54:16 GMT", 1288212856 => "Wed, 27-Oct-2010 20:54:16 GMT", # Anything not understood is passed through "basset hounds got long ears" => "basset hounds got long ears", "+2 something" => "+2 something", ); subtest "expiration strings" => sub { my $min = 60; my $hour = 60 * $min; my $day = 24 * $hour; my $week = 7 * $day; my $mon = 30 * $day; my $year = 365 * $day; ok !$cookie->expires; for my $exp ( keys %times ) { my $want = $times{$exp}; $cookie->expires($exp); is $cookie->expires => $want, "expiry $exp => $want";; } }; subtest "to header" => sub { my @cake = ( { cookie => { name => 'bar', value => 'foo', expires => '+2h', secure => 1 }, expected => sprintf( "bar=foo; Expires=%s; HttpOnly; Path=/; Secure", $times{'+2h'}, ), }, { cookie => { name => 'bar', value => 'foo', domain => 'dancer.org', path => '/dance', http_only => 1 }, expected => "bar=foo; Domain=dancer.org; HttpOnly; Path=/dance", }, { cookie => { name => 'bar', value => 'foo', }, expected => "bar=foo; HttpOnly; Path=/", }, { cookie => { name => 'bar', value => 'foo', http_only => 0, }, expected => "bar=foo; Path=/", }, { cookie => { name => 'bar', value => 'foo', http_only => '0', }, expected => "bar=foo; Path=/", }, { cookie => { name => 'same-site', value => 'strict', same_site => 'Strict', }, expected => 'same-site=strict; HttpOnly; Path=/; SameSite=Strict', }, { cookie => { name => 'same-site', value => 'lax', same_site => 'Lax', }, expected => 'same-site=lax; HttpOnly; Path=/; SameSite=Lax', }, ); for my $cook (@cake) { my $c = Dancer2::Core::Cookie->new(%{$cook->{cookie}}); # name=value; sorted fields my @a = split /; /, $c->to_header; is join("; ", shift @a, sort @a), $cook->{expected}; } }; subtest 'multi-value' => sub { my $c = Dancer2::Core::Cookie->new( name => 'foo', value => [qw/bar baz/] ); is $c->to_header, 'foo=bar&baz; Path=/; HttpOnly'; my $r = Dancer2::Core::Request->new( env => { HTTP_COOKIE => 'foo=bar&baz' } ); is_deeply [ $r->cookies->{foo}->value ], [qw/bar baz/]; }; } done_testing; logger.t100644000765000024 536615267764222 14407 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse Test::More; use strict; use warnings; use Path::Tiny (); BEGIN { # Freeze time at Tue, 15-Jun-2010 00:00:00 GMT *CORE::GLOBAL::time = sub { return 1276560000 } } my $_logs = []; { package Dancer2::Logger::Test; use Moo; with 'Dancer2::Core::Role::Logger'; sub log { my ( $self, $level, $message ) = @_; push @$_logs, $self->format_message( $level, $message ); } } my $logger = Dancer2::Logger::Test->new( app_name => 'test' ); is $logger->log_level, 'debug'; $logger->debug("foo"); # Hard to make caller(6) work when we deal with the logger directly, # so do not check for a specific filename. like $_logs->[0], qr{debug \@2010-06-1\d \d\d:\d\d:00> foo in }; subtest 'log level and capture' => sub { use Dancer2::Logger::Capture; use Dancer2; # NOTE: this will read the config.yml under t/ that defines log level as info set logger => 'capture'; warning "Danger! Warning!"; info "Tango, Foxtrot"; debug "I like pie."; my $trap = dancer_app->engine('logger')->trapper; my $msg = $trap->read; delete $msg->[0]{'formatted'}; delete $msg->[1]{'formatted'}; is_deeply $msg, [ { level => "warning", message => "Danger! Warning!", }, { level => "info", message => "Tango, Foxtrot", }, ]; # each call to read cleans the trap is_deeply $trap->read, []; }; subtest 'logger engine hooks' => sub { # before hook can change log level or message. hook 'engine.logger.before' => sub { my $logger = shift; # @_ = ( $level, @message_args ) $_[0] = 'panic'; # eg. log all messages at the 'panic' level }; my $str = "Thou shalt not pass"; warning $str; my $trap = dancer_app->engine('logger')->trapper; my $msg = $trap->read; delete $msg->[0]{'formatted'}; is_deeply $msg, [ { level => "panic", message => $str, }, ]; }; subtest 'logger file' => sub { use Dancer2; use File::Temp qw/tempdir/; my $dir = tempdir( CLEANUP => 1 ); set engines => { logger => { File => { log_dir => $dir, file_name => 'test', } } }; # XXX this sucks, we need to set the engine *before* the logger # - Franck, 2013/08/03 set logger => 'file'; warning "Danger! Warning!"; open my $log_file, '<', Path::Tiny::path($dir, 'test')->stringify; my $txt = <$log_file>; like $txt, qr/Danger! Warning!/; }; # Explicitly close the logger file handle for those systems that # do not allow "open" files to be unlinked (Windows). GH#424. my $log_engine = engine('logger'); close $log_engine->fh; done_testing; SECURITY.md100644000765000024 44015267764222 14235 0ustar00jasonstaff000000000000Dancer2-2.3.0If you need to report a security vulnerability in Dancer, send all pertinent information to [dancer-security@lists.perldancer.org](mailto:dancer-security@lists.perldancer.org), or report it via the GitHub security tool. These reports will be addressed in the earliest possible timeframe. request.t100644000765000024 2216515267764222 14634 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Dancer2::Core::App; use Dancer2::Core::Request; diag "If you want extra speed, install URL::Encode::XS" if !$Dancer2::Core::Request::XS_URL_DECODE; diag "If you want extra speed, install CGI::Deurl::XS" if !$Dancer2::Core::Request::XS_PARSE_QUERY_STRING; sub run_test { my $env = { 'psgi.url_scheme' => 'http', REQUEST_METHOD => 'GET', SCRIPT_NAME => '/foo', PATH_INFO => '/bar/baz', REQUEST_URI => '/foo/bar/baz', QUERY_STRING => 'foo=42&bar=12&bar=13&bar=14', SERVER_NAME => 'localhost', SERVER_PORT => 5000, SERVER_PROTOCOL => 'HTTP/1.1', REMOTE_ADDR => '127.0.0.1', HTTP_X_FORWARDED_FOR => '127.0.0.2', HTTP_X_FORWARDED_HOST => 'secure.frontend', HTTP_X_FORWARDED_PROTOCOL => 'https', REMOTE_HOST => 'localhost', HTTP_USER_AGENT => 'Mozilla', REMOTE_USER => 'sukria', HTTP_COOKIE => 'cookie.a=foo=bar; cookie.b=1234abcd; no.value.cookie', }; my $req = Dancer2::Core::Request->new( env => $env ); note "tests for accessors"; is $req->agent, 'Mozilla'; is $req->user_agent, 'Mozilla'; is $req->remote_address, '127.0.0.1'; is $req->address, '127.0.0.1'; is $req->forwarded_for_address, '127.0.0.2'; is $req->remote_host, 'localhost'; is $req->protocol, 'HTTP/1.1'; is $req->port, 5000; is $req->request_uri, '/foo/bar/baz'; is $req->uri, '/foo/bar/baz'; is $req->user, 'sukria'; is $req->script_name, '/foo'; is $req->scheme, 'http'; is $req->referer, undef; ok( !$req->secure ); is $req->method, 'GET'; is $req->request_method, 'GET'; ok( $req->is_get ); ok( !$req->is_post ); ok( !$req->is_put ); ok( !$req->is_delete ); ok( !$req->is_patch ); ok( !$req->is_head ); is $req->id, 1; is $req->to_string, '[#1] GET /bar/baz'; note "tests params"; is_deeply { $req->params }, { foo => 42, bar => [ 12, 13, 14 ] }; note "tests cookies"; is( keys %{ $req->cookies }, 2, "multiple cookies extracted" ); my $forward = Dancer2::Core::App->new( request => $req ) ->make_forward_to('/somewhere'); is $forward->path_info, '/somewhere'; is $forward->method, 'GET'; note "tests for uri_for"; is $req->base, 'http://localhost:5000/foo'; is $req->uri_for( 'bar', { baz => 'baz' } ), 'http://localhost:5000/foo/bar?baz=baz'; is $req->uri_for('/bar'), 'http://localhost:5000/foo/bar'; is $req->uri_for( '/bar', undef, 1 ), 'http://localhost:5000/foo/bar', 'uri_for returns a URI (with $dont_escape)'; is $req->request_uri, '/foo/bar/baz'; is $req->path_info, '/bar/baz'; { local $env->{SCRIPT_NAME} = ''; is $req->uri_for('/foo'), 'http://localhost:5000/foo'; } { local $env->{SERVER_NAME} = 0; is $req->base, 'http://0:5000/foo'; local $env->{HTTP_HOST} = 'oddhostname:5000'; is $req->base, 'http://oddhostname:5000/foo'; } note "testing behind proxy"; { my $req = Dancer2::Core::Request->new( env => $env, is_behind_proxy => 1 ); is $req->secure, 1; is $req->host, $env->{HTTP_X_FORWARDED_HOST}; is $req->scheme, 'https'; } note "testing behind proxy when optional headers are not set"; { # local modifications to env: local $env->{HTTP_HOST} = 'oddhostname:5000'; delete local $env->{'HTTP_X_FORWARDED_FOR'}; delete local $env->{'HTTP_X_FORWARDED_HOST'}; delete local $env->{'HTTP_X_FORWARDED_PROTOCOL'}; my $req = Dancer2::Core::Request->new( env => $env, is_behind_proxy => 1 ); is ! $req->secure, 1; is $req->host, 'oddhostname:5000'; is $req->scheme, 'http'; } note "testing path and uri_base"; { # Base env used for path and uri_base tests my $base = { 'psgi.url_scheme' => 'http', REQUEST_METHOD => 'GET', QUERY_STRING => '', SERVER_NAME => 'localhost', SERVER_PORT => 5000, SERVER_PROTOCOL => 'HTTP/1.1', }; # PATH_INFO not set my $env = { %$base, SCRIPT_NAME => '/foo', PATH_INFO => '', REQUEST_URI => '/foo', }; my $req = Dancer2::Core::Request->new( env => $env ); is( $req->path, '/', 'path corrent when empty PATH_INFO' ); is( $req->uri_base, 'http://localhost:5000/foo', 'uri_base correct when empty PATH_INFO' ); # SCRIPT_NAME not set $env = { %$base, SCRIPT_NAME => '', PATH_INFO => '/foo', REQUEST_URI => '/foo', }; $req = Dancer2::Core::Request->new( env => $env ); is( $req->path, '/foo', 'path corrent when empty SCRIPT_NAME' ); is( $req->uri_base, 'http://localhost:5000', 'uri_base handles empty SCRIPT_NAME' ); # Both SCRIPT_NAME and PATH_INFO set # PSGI spec does not allow SCRIPT_NAME='/', PATH_INFO='/some/path' $env = { %$base, SCRIPT_NAME => '/foo', PATH_INFO => '/bar/baz/', REQUEST_URI => '/foo/bar/baz/', }; $req = Dancer2::Core::Request->new( env => $env ); is( $req->path, '/bar/baz/', 'path corrent when both PATH_INFO and SCRIPT_NAME set' ); is( $req->uri_base, 'http://localhost:5000/foo', 'uri_base correct when both PATH_INFO and SCRIPT_NAME set', ); # Neither SCRIPT_NAME or PATH_INFO set $env = { %$base, SCRIPT_NAME => '', PATH_INFO => '', REQUEST_URI => '/foo/', }; $req = Dancer2::Core::Request->new( env => $env ); is( $req->path, '/', 'path corrent when calculated from REQUEST_URI' ); is( $req->uri_base, 'http://localhost:5000', 'uri_base correct when calculated from REQUEST_URI', ); } note "testing forward"; $env = { 'REQUEST_METHOD' => 'GET', 'REQUEST_URI' => '/', 'PATH_INFO' => '/', 'QUERY_STRING' => 'foo=bar&number=42', }; $req = Dancer2::Core::Request->new( env => $env ); is $req->path, '/', 'path is /'; is $req->method, 'GET', 'method is get'; is_deeply scalar( $req->params ), { foo => 'bar', number => 42 }, 'params are parsed'; $req = Dancer2::Core::App->new( request => $req ) ->make_forward_to('/new/path'); is $req->path, '/new/path', 'path is changed'; is $req->method, 'GET', 'method is unchanged'; is_deeply scalar( $req->params ), { foo => 'bar', number => 42 }, 'params are not touched'; $req = Dancer2::Core::App->new( request => $req ) ->make_forward_to( '/new/path', undef, { method => 'POST' }, ); is $req->path, '/new/path', 'path is changed'; is $req->method, 'POST', 'method is changed'; is_deeply scalar( $req->params ), { foo => 'bar', number => 42 }, 'params are not touched'; note "testing unicode params"; $env = { 'REQUEST_METHOD' => 'GET', 'REQUEST_URI' => '/', 'PATH_INFO' => '/', 'QUERY_STRING' => "M%C3%BCller=L%C3%BCdenscheid", }; $req = Dancer2::Core::Request->new( env => $env ); is_deeply scalar( $req->params ), { "M\N{U+00FC}ller", "L\N{U+00FC}denscheid" }, 'multi byte unicode chars work in param keys and values'; { note "testing private _decode not to mangle hash"; my @warnings; local $SIG{__WARN__} = sub { push @warnings, @_; }; my $h = { zzz => undef, }; for ( 'aaa' .. 'fff' ) { $h->{$_} = $_; } my $i = Dancer2::Core::Request->_decode($h); is_deeply( $i, $h, 'hash not mangled' ); ok( !@warnings, 'no warnings were issued' ); } } note "Run test with XS_URL_DECODE" if $Dancer2::Core::Request::XS_URL_DECODE; note "Run test with XS_PARSE_QUERY_STRING" if $Dancer2::Core::Request::XS_PARSE_QUERY_STRING; run_test(); if ($Dancer2::Core::Request::XS_PARSE_QUERY_STRING) { note "Run test without XS_PARSE_QUERY_STRING"; $Dancer2::Core::Request::XS_PARSE_QUERY_STRING = 0; $Dancer2::Core::Request::_id = 0; run_test(); } if ($Dancer2::Core::Request::XS_URL_DECODE) { note "Run test without XS_URL_DECODE"; $Dancer2::Core::Request::XS_URL_DECODE = 0; $Dancer2::Core::Request::_id = 0; run_test(); } done_testing; forward.t100644000765000024 1000215267764222 14573 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More import => ['!pass']; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; use Dancer2; set behind_proxy => 1; get '/' => sub { 'home:' . join( ',', params ); }; get '/bounce/' => sub { return forward '/'; }; get '/bounce/:withparams/' => sub { return forward '/'; }; get '/bounce2/adding_params/' => sub { return forward '/', { withparams => 'foo' }; }; post '/simple_post_route/' => sub { 'post:' . join( ',', params ); }; get '/go_to_post/' => sub { return forward '/simple_post_route/', { foo => 'bar' }, { method => 'post' }; }; get '/proxy/' => sub { return uri_for('/'); }; get '/forward_with_proxy/' => sub { forward '/proxy/'; }; # NOT SUPPORTED IN DANCER2 # In dancer2, vars are alive for only one request flow, a forward initiate a # new request flow, then the vars HashRef is destroyed. # # get '/b' => sub { vars->{test} = 1; forward '/a'; }; # get '/a' => sub { return "test is " . var('test'); }; my $app = __PACKAGE__->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; is( $cb->( GET '/' )->code, 200, '[GET /] Correct code' ); is( $cb->( GET '/' )->content, 'home:', '[GET /] Correct content' ); is( $cb->( GET '/bounce/' )->code, 200, '[GET /bounce] Correct code' ); is( $cb->( GET '/bounce/' )->content, 'home:', '[GET /bounce] Correct content', ); is( $cb->( GET '/bounce/thesethings/' )->code, 200, '[GET /bounce/thesethings/] Correct code', ); is( $cb->( GET '/bounce/thesethings/' )->content, 'home:withparams,thesethings', '[GET /bounce/thesethings/] Correct content', ); is( $cb->( GET '/bounce2/adding_params/' )->code, 200, '[GET /bounce2/adding_params/] Correct code', ); is( $cb->( GET '/bounce2/adding_params/' )->content, 'home:withparams,foo', '[GET /bounce2/adding_params/] Correct content', ); is( $cb->( GET '/go_to_post/' )->code, 200, '[GET /go_to_post/] Correct code', ); is( $cb->( GET '/go_to_post/' )->content, 'post:foo,bar', '[GET /go_to_post/] Correct content', ); # NOT SUPPORTED # response_status_is [ GET => '/b' ] => 200; # response_content_is [ GET => '/b' ] => 'test is 1'; { my $res = $cb->( GET '/bounce/' ); is( $res->headers->content_length, 5, '[GET /bounce/] Correct content length', ); is( $res->headers->content_type, 'text/html', '[GET /bounce/] Correct content type', ); is( $res->headers->content_type_charset, 'UTF-8', '[GET /bounce/] Default content type charset', ); } # checking post post '/' => sub {'post-home'}; post '/bounce/' => sub { forward('/') }; is( $cb->( POST '/' )->code, 200, '[POST /] Correct code' ); is( $cb->( POST '/' )->content, 'post-home', '[POST /] Correct content' ); is( $cb->( POST '/bounce/' )->code, 200, '[POST /bounce/] Correct code', ); is( $cb->( POST '/bounce/' )->content, 'post-home', '[POST /bounce/] Correct content', ); { my $res = $cb->( POST '/bounce/' ); is( $res->headers->content_length, 9, '[POST /bounce/] Correct content length', ); is( $res->headers->content_type, 'text/html', '[POST /bounce/] Correct content type', ); is( $res->headers->content_type_charset, 'UTF-8', '[POST /bounce/] Default content type charset', ); } is( $cb->( GET '/forward_with_proxy/', 'X-Forwarded-Proto' => 'https' )->content, 'https://localhost/', '[GET /forward_with_proxy/] maintained is_behind_proxy', ); }; done_testing; factory.t100644000765000024 102015267764222 14556 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Test::Fatal; use Dancer2::Core; use Dancer2::Core::Factory; is Dancer2::Core::camelize('foo_bar_baz'), 'FooBarBaz'; is Dancer2::Core::camelize('FooBarBaz'), 'FooBarBaz'; like( exception { my $l = Dancer2::Core::Factory->create( unknown => 'stuff' ) }, qr{Unable to load class for Unknown component Stuff:}, 'Failure to load nonexistent class', ); my $l = Dancer2::Core::Factory->create( logger => 'console' ); isa_ok $l, 'Dancer2::Logger::Console'; done_testing; dsl000755000765000024 015267764222 13353 5ustar00jasonstaff000000000000Dancer2-2.3.0/tpod.t100644000765000024 234215267764222 14463 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/dsluse strict; use warnings; use Test::More; use Dancer2::Core::DSL; use Pod::Simple::SimpleTree; use Ref::Util qw< is_arrayref >; { package App; use Dancer2; } my $dsl_keywords = Dancer2::Core::DSL->new(app => App->to_app)->dsl_keywords; isa_ok($dsl_keywords, 'HASH', 'Check whether keywords are present'); my $podpa = Pod::Simple::SimpleTree->new->parse_file('lib/Dancer2/Manual/Keywords.pod')->root; for my $entry (@$podpa) { if (ref($entry) eq 'ARRAY') { if ($entry->[0] eq 'head2') { # get the bare keyword and compare with the authoritative list my $title = $entry->[2]; my $keyword = is_arrayref($title) ? $title->[2] : $title; if (exists $dsl_keywords->{$keyword}) { $dsl_keywords->{$keyword}->{found} = $entry->[1]->{startline}; } } } } # go through the authoritative list and test we have a corresponding POD entry my %missing; for my $keyword ( sort keys %$dsl_keywords ) { exists $dsl_keywords->{$keyword}->{found} or $missing{$keyword}++; ok( exists $dsl_keywords->{$keyword}->{found}, "Keyword \"$keyword\" is documented in Dancer2::Manual::Keywords" ); } done_testing; print "$_\n" for keys %missing; app.t100644000765000024 53415267764222 14442 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/dsluse strict; use warnings; use Test::More tests => 2; use Plack::Test; use HTTP::Request::Common; { package App; use Dancer2; get '/' => sub { my $app = app; ::isa_ok( $app, 'Dancer2::Core::App' ); ::is( $app->name, 'App', 'Correct app name' ); }; } Plack::Test->create( App->to_app )->request( GET '/' ); any.t100644000765000024 221015267764222 14462 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/dsluse strict; use warnings; use Test::More tests => 2; use Plack::Test; use HTTP::Request::Common; { package App; use Dancer2; any [ 'get', 'post' ] => '/test' => sub { request->method }; any '/all' => sub { request->method }; } my $test = Plack::Test->create( App->to_app ); subtest 'any with params' => sub { my @success = qw; my @fails = qw; foreach my $method (@success) { my $req = HTTP::Request->new( $method => '/test' ); is( $test->request($req)->content, $method, "Method $method works", ); } foreach my $method (@fails) { my $req = HTTP::Request->new( $method => '/test' ); ok( ! $test->request($req)->is_success, "Method $method doesn't exist", ); } }; subtest 'any without params' => sub { foreach my $method ( qw ) { my $req = HTTP::Request->new( $method => '/all' ); is( $test->request($req)->content, $method, "Method $method works", ); } }; Makefile.PL100644000765000024 1617415267764222 14471 0ustar00jasonstaff000000000000Dancer2-2.3.0# This file was automatically generated by Dist::Zilla::Plugin::MakeMaker v6.037 use strict; use warnings; use 5.014; use ExtUtils::MakeMaker; use File::ShareDir::Install; $File::ShareDir::Install::INCLUDE_DOTFILES = 1; $File::ShareDir::Install::INCLUDE_DOTDIRS = 1; install_share dist => "share"; my %WriteMakefileArgs = ( "ABSTRACT" => "Lightweight yet powerful web application framework", "AUTHOR" => "Dancer Core Developers", "CONFIGURE_REQUIRES" => { "CPAN::Meta::Requirements" => "2.120620", "ExtUtils::MakeMaker" => 0, "File::ShareDir::Install" => "0.06", "Module::Metadata" => 0 }, "DISTNAME" => "Dancer2", "EXE_FILES" => [ "script/dancer2" ], "LICENSE" => "perl", "MIN_PERL_VERSION" => "5.014", "NAME" => "Dancer2", "PREREQ_PM" => { "Attribute::Handlers" => 0, "CLI::Osprey" => "0.09", "Carp" => 0, "Clone" => 0, "Config::Any" => 0, "Crypt::URandom" => "0.36", "Data::Censor" => "0.04", "Digest::SHA" => 0, "Encode" => 0, "Exporter" => "5.57", "Exporter::Tiny" => 0, "File::Copy" => 0, "File::Path" => 0, "File::Share" => 0, "File::Temp" => 0, "File::Which" => 0, "HTTP::Date" => 0, "HTTP::Headers::Fast" => "0.21", "HTTP::Tiny" => 0, "Hash::Merge::Simple" => 0, "Hash::MultiValue" => 0, "Import::Into" => 0, "JSON::MaybeXS" => 0, "List::Util" => "1.29", "MIME::Base64" => "3.13", "Module::Pluggable" => 0, "Module::Runtime" => 0, "Moo" => "2.000000", "Moo::Role" => 0, "POSIX" => 0, "Path::Tiny" => 0, "Plack" => "1.0040", "Plack::Middleware::FixMissingBodyInRedirect" => 0, "Plack::Middleware::RemoveRedundantBody" => 0, "Ref::Util" => 0, "Role::Tiny" => "2.000000", "Safe::Isa" => 0, "Sub::Quote" => 0, "Sub::Util" => 0, "Template" => 0, "Template::Tiny" => "1.16", "Test::Builder" => 0, "Test::More" => "0.92", "Type::Tiny" => "1.000006", "Types::Standard" => 0, "URI::Escape" => 0, "YAML" => "1.30", "parent" => 0 }, "TEST_REQUIRES" => { "Capture::Tiny" => "0.12", "ExtUtils::MakeMaker" => 0, "File::Spec" => 0, "HTTP::Cookies" => 0, "HTTP::Headers" => 0, "IO::Handle" => 0, "IPC::Open3" => 0, "Pod::Simple::SimpleTree" => 0, "Template" => 0, "Test::Builder" => 0, "Test::EOL" => 0, "Test::Exception" => 0, "Test::Fatal" => 0, "Test::More" => "0.92" }, "VERSION" => "2.3.0", "test" => { "TESTS" => "t/*.t t/classes/Dancer2-Core-Factory/*.t t/classes/Dancer2-Core-Hook/*.t t/classes/Dancer2-Core-Request/*.t t/classes/Dancer2-Core-Response-Delayed/*.t t/classes/Dancer2-Core-Response/*.t t/classes/Dancer2-Core-Role-Engine/*.t t/classes/Dancer2-Core-Role-Handler/*.t t/classes/Dancer2-Core-Role-HasConfig/*.t t/classes/Dancer2-Core-Role-HasEnvironment/*.t t/classes/Dancer2-Core-Role-HasLocation/*.t t/classes/Dancer2-Core-Role-Serializer/*.t t/classes/Dancer2-Core-Role-StandardResponses/*.t t/classes/Dancer2-Core-Route/*.t t/classes/Dancer2-Core-Runner/*.t t/classes/Dancer2-Core/*.t t/classes/Dancer2/*.t t/dsl/*.t t/e2e/cli/*.t t/examples/*.t t/integration/dispatch/*.t t/integration/error/*.t t/integration/handler/*.t t/integration/hooks/*.t t/integration/serializer/*.t t/integration/session/*.t t/integration/template/*.t t/issues/*.t t/issues/gh-1013/*.t t/issues/gh-1046/*.t t/issues/gh-1216/*.t t/issues/gh-1226/*.t t/issues/gh-1230/*.t t/issues/gh-1449/*.t t/issues/gh-1621/*.t t/issues/gh-1664/*.t t/issues/gh-1712/*.t t/issues/gh-639/fails/*.t t/issues/gh-639/succeeds/*.t t/issues/gh-650/*.t t/issues/gh-975/*.t t/issues/memleak/*.t t/plugin2/*.t t/plugin2/app_dsl_cb/*.t t/roles/*.t t/route-pod-coverage/*.t t/scope_problems/*.t t/unit/configreader/*.t t/unit/core/*.t t/unit/logger/*.t" } ); my %FallbackPrereqs = ( "Attribute::Handlers" => 0, "CLI::Osprey" => "0.09", "Capture::Tiny" => "0.12", "Carp" => 0, "Clone" => 0, "Config::Any" => 0, "Crypt::URandom" => "0.36", "Data::Censor" => "0.04", "Digest::SHA" => 0, "Encode" => 0, "Exporter" => "5.57", "Exporter::Tiny" => 0, "ExtUtils::MakeMaker" => 0, "File::Copy" => 0, "File::Path" => 0, "File::Share" => 0, "File::Spec" => 0, "File::Temp" => 0, "File::Which" => 0, "HTTP::Cookies" => 0, "HTTP::Date" => 0, "HTTP::Headers" => 0, "HTTP::Headers::Fast" => "0.21", "HTTP::Tiny" => 0, "Hash::Merge::Simple" => 0, "Hash::MultiValue" => 0, "IO::Handle" => 0, "IPC::Open3" => 0, "Import::Into" => 0, "JSON::MaybeXS" => 0, "List::Util" => "1.29", "MIME::Base64" => "3.13", "Module::Pluggable" => 0, "Module::Runtime" => 0, "Moo" => "2.000000", "Moo::Role" => 0, "POSIX" => 0, "Path::Tiny" => 0, "Plack" => "1.0040", "Plack::Middleware::FixMissingBodyInRedirect" => 0, "Plack::Middleware::RemoveRedundantBody" => 0, "Pod::Simple::SimpleTree" => 0, "Ref::Util" => 0, "Role::Tiny" => "2.000000", "Safe::Isa" => 0, "Sub::Quote" => 0, "Sub::Util" => 0, "Template" => 0, "Template::Tiny" => "1.16", "Test::Builder" => 0, "Test::EOL" => 0, "Test::Exception" => 0, "Test::Fatal" => 0, "Test::More" => "0.92", "Type::Tiny" => "1.000006", "Types::Standard" => 0, "URI::Escape" => 0, "YAML" => "1.30", "parent" => 0 ); # inserted by Dist::Zilla::Plugin::DynamicPrereqs 0.040 if (has_module('HTTP::XSCookies')) { requires('HTTP::XSCookies', '0.000007') } unless ( eval { ExtUtils::MakeMaker->VERSION(6.63_03) } ) { delete $WriteMakefileArgs{TEST_REQUIRES}; delete $WriteMakefileArgs{BUILD_REQUIRES}; $WriteMakefileArgs{PREREQ_PM} = \%FallbackPrereqs; } delete $WriteMakefileArgs{CONFIGURE_REQUIRES} unless eval { ExtUtils::MakeMaker->VERSION(6.52) }; WriteMakefile(%WriteMakefileArgs); { package MY; use File::ShareDir::Install qw(postamble); } # inserted by Dist::Zilla::Plugin::DynamicPrereqs 0.040 sub _add_prereq { my ($mm_key, $module, $version_or_range) = @_; $version_or_range ||= 0; warn "$module already exists in $mm_key (at version $WriteMakefileArgs{$mm_key}{$module}) -- need to do a sane metamerge!" if exists $WriteMakefileArgs{$mm_key}{$module} and $WriteMakefileArgs{$mm_key}{$module} ne '0' and $WriteMakefileArgs{$mm_key}{$module} ne $version_or_range; warn "$module already exists in FallbackPrereqs (at version $FallbackPrereqs{$module}) -- need to do a sane metamerge!" if exists $FallbackPrereqs{$module} and $FallbackPrereqs{$module} ne '0' and $FallbackPrereqs{$module} ne $version_or_range; $WriteMakefileArgs{$mm_key}{$module} = $FallbackPrereqs{$module} = $version_or_range; return; } sub has_module { my ($module, $version_or_range) = @_; require Module::Metadata; my $mmd = Module::Metadata->new_from_module($module); return undef if not $mmd; return $mmd->version($module) if not defined $version_or_range; require CPAN::Meta::Requirements; my $req = CPAN::Meta::Requirements->new; $req->add_string_requirement($module => $version_or_range); return 1 if $req->accepts_module($module => $mmd->version($module)); return 0; } sub requires { goto &runtime_requires } sub runtime_requires { my ($module, $version_or_range) = @_; _add_prereq(PREREQ_PM => $module, $version_or_range); } utf8_url.t100644000765000024 104315267764222 14664 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse utf8; use strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; use Encode; my $utf8 = 'ľš�ťžýáí'; { package MyApp; use Dancer2; use utf8; get '/ľš�ťžýáí' => sub { return 'ľš�ťžýáí'; }; } my $app = Dancer2->psgi_app; test_psgi $app, sub { my $cb = shift; my $res = $cb->( GET '/ľš�ťžýáí' ); is $res->code, 200, 'route succeeded'; is Encode::decode('utf8', $res->content), 'ľš�ťžýáí', 'Correct content'; }; done_testing(); response.t100644000765000024 257215267764222 14762 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More import => ['!pass']; use Dancer2; use Dancer2::Core::Response; my $r = Dancer2::Core::Response->new( content => "hello" ); is $r->status, 200; is $r->content, 'hello'; note "content_type"; $r = Dancer2::Core::Response->new( headers => [ 'Content-Type' => 'text/html' ], content => 'foo', ); is_deeply $r->to_psgi, [ 200, [ 'Content-Length' => 3, 'Content-Type' => 'text/html', ], ['foo'] ]; isa_ok $r->headers, 'HTTP::Headers'; is $r->content_type, 'text/html'; $r->content_type('text/plain'); is $r->content_type, 'text/plain'; ok( !$r->is_forwarded ); $r->forward('http://perldancer.org'); ok( $r->is_forwarded ); is $r->header('X-Foo'), undef; $r->header( 'X-Foo' => 42 ); is $r->header('X-Foo'), 42; $r->header( 'X-Foo' => 432 ); is $r->header('X-Foo'), 432; $r->push_header( 'X-Foo' => 777 ); is $r->header('X-Foo'), '432, 777'; $r->header( 'X-Bar' => 234 ); is $r->header('X-Bar'), '234'; is scalar( @{ $r->headers_to_array } ), 10; # stringify HTTP status $r = Dancer2::Core::Response->new( content => "foo", status => "Not Found" ); is $r->status, 404; $r = Dancer2::Core::Response->new( content => "foo", status => "not_modified" ); is $r->status, 304; # test setting content as "0" $r = Dancer2::Core::Response->new( content => "foo" ); $r->content("0"); is $r->content, "0"; done_testing; redirect.t100644000765000024 652015267764222 14722 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; subtest 'basic redirects' => sub { { package App1; use Dancer2; get '/' => sub {'home'}; get '/bounce' => sub { redirect '/' }; get '/redirect' => sub { response_header 'X-Foo' => 'foo'; redirect '/'; }; get '/redirect_querystring' => sub { redirect '/login?failed=1' }; get '/redirect_uriescaped' => sub { redirect '?foo=bar+%26+baz' }; } my $app = App1->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; { my $res = $cb->( GET '/' ); is( $res->code, 200, '[GET /] Correct code' ); is( $res->content, 'home', '[GET /] Correct content' ); is( $res->headers->content_type, 'text/html', '[GET /] Correct content-type', ); is( $cb->( GET '/bounce' )->code, 302, '[GET /bounce] Correct code', ); } { my $res = $cb->( GET '/redirect' ); is( $res->code, 302, '[GET /redirect] Correct code' ); is( $res->headers->header('Location'), '/', 'Correct Location header', ); is( $res->headers->header('X-Foo'), 'foo', 'Correct X-Foo header', ); } { my $res = $cb->( GET '/redirect_querystring' ); is( $res->code, 302, '[GET /redirect_querystring] Correct code' ); is( $res->headers->header('Location'), '/login?failed=1', 'Correct Location header', ); } { my $res = $cb->( GET '/redirect_uriescaped' ); is( $res->code, 302, '[GET /redirect_uriescaped] Correct code' ); is( $res->headers->header('Location'), '?foo=bar+%26+baz', 'Correct Location header', ); } }; }; # redirect absolute subtest 'absolute and relative redirects' => sub { { package App2; use Dancer2; get '/absolute_with_host' => sub { redirect "http://foo.com/somewhere"; }; get '/absolute' => sub { redirect "/absolute"; }; get '/relative' => sub { redirect "somewhere/else"; }; } my $app = App2->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; { my $res = $cb->( GET '/absolute_with_host' ); is( $res->headers->header('Location'), 'http://foo.com/somewhere', 'Correct Location header', ); } { my $res = $cb->( GET '/absolute' ); is( $res->headers->header('Location'), '/absolute', 'Correct Location header', ); } { my $res = $cb->( GET '/relative' ); is( $res->headers->header('Location'), 'somewhere/else', 'Correct Location header', ); } }; }; done_testing; config.yml100644000765000024 12415267764222 14676 0ustar00jasonstaff000000000000Dancer2-2.3.0/tlog: "info" logger: "Note" strict_config: 0 plugins: "FooPlugin": plugin: 42 psgi_app.t100644000765000024 436315267764222 14726 0ustar00jasonstaff000000000000Dancer2-2.3.0/t#!perl use strict; use warnings; use Test::More tests => 25; use Plack::Test; use HTTP::Request::Common; { package App1; use Dancer2; get '/1' => sub {1}; } { package App2; use Dancer2; get '/2' => sub {2}; } { package App3; use Dancer2; get '/3' => sub {3}; } sub is_available { my ( $cb, @apps ) = @_; foreach my $app (@apps) { is( $cb->( GET "/$app" )->content, $app, "App$app available" ); } } sub isnt_available { my ( $cb, @apps ) = @_; foreach my $app (@apps) { is( $cb->( GET "/$app" )->code, 404, "App$app is not available", ); } } note 'All Apps'; { my $app = Dancer2->psgi_app; isa_ok( $app, 'CODE', 'Got PSGI app' ); test_psgi $app, sub { my $cb = shift; is_available( $cb, 1, 2, 3 ); }; } note 'Specific Apps by parameters'; { my @apps = @{ Dancer2->runner->apps }[ 0, 2 ]; is( scalar @apps, 2, 'Took two apps from the Runner' ); my $app = Dancer2->psgi_app(\@apps); isa_ok( $app, 'CODE', 'Got PSGI app' ); test_psgi $app, sub { my $cb = shift; is_available( $cb, 1, 3 ); isnt_available( $cb, 2 ); }; } note 'Specific Apps via App objects'; { my $app = App2->psgi_app; isa_ok( $app, 'CODE', 'Got PSGI app' ); test_psgi $app, sub { my $cb = shift; is_available( $cb, 2 ); isnt_available( $cb, 1, 3 ); }; }; note 'Specific apps by App names'; { my $app = Dancer2->psgi_app( [ 'App1', 'App3' ] ); isa_ok( $app, 'CODE', 'Got PSGI app' ); test_psgi $app, sub { my $cb = shift; isnt_available( $cb, 2 ); is_available( $cb, 1, 3 ); }; } note 'Specific apps by App names with regular expression, v1'; { my $app = Dancer2->psgi_app( [ qr/^App1$/, qr/^App3$/ ] ); isa_ok( $app, 'CODE', 'Got PSGI app' ); test_psgi $app, sub { my $cb = shift; isnt_available( $cb, 2 ); is_available( $cb, 1, 3 ); }; } note 'Specific apps by App names with regular expression, v2'; { my $app = Dancer2->psgi_app( [ qr/^App(2|3)$/ ] ); isa_ok( $app, 'CODE', 'Got PSGI app' ); test_psgi $app, sub { my $cb = shift; isnt_available( $cb, 1 ); is_available( $cb, 2, 3 ); }; } template.t100644000765000024 1273515267764222 14761 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Dancer2::Core::Hook; use Plack::Test; use HTTP::Request::Common; use Path::Tiny qw< path >; use lib 't/lib'; eval { require Template; Template->import(); 1 } or plan skip_all => 'Template::Toolkit probably missing.'; use_ok('Dancer2::Template::TemplateToolkit'); use_ok('Dancer2::Template::TemplateToolkitFoo'); my $views = path( __FILE__ )->parent->child('views')->absolute->stringify; my $tt = Dancer2::Template::TemplateToolkit->new( views => $views, layout => 'main.tt', layout_dir => 'layouts', ); isa_ok $tt, 'Dancer2::Template::TemplateToolkit'; ok $tt->does('Dancer2::Core::Role::Template'); $tt->add_hook( Dancer2::Core::Hook->new( name => 'engine.template.before_render', code => sub { my $tokens = shift; $tokens->{before_template_render} = 1; }, ) ); $tt->add_hook( Dancer2::Core::Hook->new( name => 'engine.template.before_layout_render', code => sub { my $tokens = shift; my $content = shift; $tokens->{before_layout_render} = 1; $$content .= "\ncontent added in before_layout_render"; }, ) ); $tt->add_hook( Dancer2::Core::Hook->new( name => 'engine.template.after_layout_render', code => sub { my $content = shift; $$content .= "\ncontent added in after_layout_render"; }, ) ); $tt->add_hook( Dancer2::Core::Hook->new( name => 'engine.template.after_render', code => sub { my $content = shift; $$content .= 'content added in after_template_render'; }, ) ); { package Bar; use Dancer2; # set template engine for first app Dancer2->runner->apps->[0]->set_template_engine($tt); get '/' => sub { template index => { var => 42 } }; # Call template as a global keyword my $global= template( index => { var => 21 } ); get '/global' => sub { $global }; } subtest 'template hooks' => sub { my $space = " "; my $result = "layout top var = 42 before_layout_render = 1 --- [index] var = 42 before_layout_render =$space before_template_render = 1 content added in after_template_render content added in before_layout_render --- layout bottom content added in after_layout_render"; my $test = Plack::Test->create( Bar->to_app ); my $res = $test->request( GET '/' ); is $res->content, $result, '[GET /] Correct content with template hooks'; $result =~ s/42/21/g; $res = $test->request( GET '/global' ); is $res->content, $result, '[GET /global] Correct content with template hooks'; }; # Test that a custom class can be used for Template::Toolkit my $tt_custom = Dancer2::Template::TemplateToolkitFoo->new; isa_ok $tt_custom, 'Dancer2::Template::TemplateToolkit'; isa_ok $tt_custom, 'Dancer2::Template::TemplateToolkitFoo'; { package CustomFoo; use Dancer2; Dancer2->runner->apps->[1]->set_template_engine($tt_custom); get '/' => sub { template 'index' }; } subtest 'custom template render' => sub { my $test = Plack::Test->create( CustomFoo->to_app ); my $res = $test->request( GET '/' ); is $res->content, 'Custom Render Template', 'Custom Render Template'; }; { package Foo; use Dancer2; set views => '/this/is/our/path'; get '/default_views' => sub { set 'views' }; get '/set_views_via_settings' => sub { set views => '/other/path' }; get '/get_views_via_settings' => sub { set 'views' }; get '/default_layout_dir' => sub { app->template_engine->layout_dir }; get '/set_layout_dir_via_settings' => sub { set layout_dir => 'alt_layout' }; get '/get_layout_dir_via_settings' => sub { set 'layout_dir' }; } subtest "modify views - absolute paths" => sub { my $test = Plack::Test->create( Foo->to_app ); is( $test->request( GET '/default_views' )->content, '/this/is/our/path', '[GET /default_views] Correct content', ); # trigger a test via a route $test->request( GET '/set_views_via_settings' ); is( $test->request( GET '/get_views_via_settings' )->content, '/other/path', '[GET /get_views_via_settings] Correct content', ); }; subtest "modify layout_dir" => sub { my $test = Plack::Test->create( Foo->to_app ); is( $test->request( GET '/default_layout_dir' )->content, 'layouts', '[GET /default_layout_dir] Correct layout dir', ); # trigger a test via a route $test->request( GET '/set_layout_dir_via_settings' ); is( $test->request( GET '/get_layout_dir_via_settings' )->content, 'alt_layout', '[GET /get_layout_dir_via_settings] Correct content', ); }; { package Baz; use Dancer2; set template => 'template_toolkit'; get '/set_views/**' => sub { my ($view) = splat; set views => join('/', @$view ); }; get '/:file' => sub { template param('file'); }; } subtest "modify views propagates to TT2 via dynamic INCLUDE_PATH" => sub { my $test = Plack::Test->create( Baz->to_app ); my $res = $test->request( GET '/index' ); is $res->code, 200, 'got template from views'; # Change views - this is an existing test corpus.. $test->request( GET '/set_views/t/corpus/pretty' ); # Get another template that is known to exist in the test corpus $res = $test->request( GET '/relative.tt' ); is $res->code, 200, 'got template from other view'; }; done_testing; lib000755000765000024 015267764222 13337 5ustar00jasonstaff000000000000Dancer2-2.3.0/tpoc.pm100644000765000024 43215267764222 14575 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/libpackage poc; use Dancer2; our $VERSION = '0.1'; use Dancer2::Plugin::Foo; set plugins => { Foo => { one => 1, two => 2, size => 4, }, }; get '/' => sub { return 'hello there'; }; get '/truncate' => sub { truncate_txt "hello there" }; true; Foo.pm100644000765000024 12615267764222 14537 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/libpackage t::lib::Foo; use Dancer2; get '/in_foo' => sub { session('test'); }; 1; mime.t100644000765000024 177115267764222 14635 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/dsluse strict; use warnings; use Test::More tests => 2; use Plack::Test; use HTTP::Request::Common; { package App::MIME; use Dancer2; # set some MIME aliases... mime->add_type( foo => 'text/foo' ); mime->add_alias( f => 'foo' ); # Set default mime type set 'default_mime_type' => 'text/bar'; # test static corpus set static_handler => 1; set public_dir => 't/corpus/static'; # added type get '/foo' => sub { send_file 'empty.foo'; }; } my $app = Plack::Test->create( App::MIME->to_app ); subtest 'send_file content type' => sub { my $res = $app->request( GET '/foo' ); ok( $res->is_success, 'Successful request' ); is( $res->content_type, 'text/foo', '.. and correct mime type'); }; # Ref: #1546 subtest 'static handler content type' => sub { my $res = $app->request( GET '/empty.foo' ); ok( $res->is_success, 'Successful request via static handler' ); is( $res->content_type, 'text/foo', '.. and correct mime type'); }; halt.t100644000765000024 333515267764222 14634 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/dsluse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; subtest 'halt within routes' => sub { { package App; use Dancer2; get '/' => sub { 'hello' }; get '/halt' => sub { response_header 'X-Foo' => 'foo'; halt; }; get '/shortcircuit' => sub { app->response->content('halted'); halt; redirect '/'; # won't get executed as halt returns immediately. }; } my $app = App->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; { my $res = $cb->( GET '/shortcircuit' ); is( $res->code, 200, '[/shortcircuit] Correct status' ); is( $res->content, 'halted', '[/shortcircuit] Correct content' ); } { my $res = $cb->( GET '/halt' ); is( $res->headers->header('X-Foo'), 'foo', '[/halt] Correct X-Foo header', ); } }; }; subtest 'halt in before hook' => sub { { package App; use Dancer2; hook before => sub { response->content('I was halted'); halt if request->path eq '/shortcircuit'; }; } my $app = App->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; my $res = $cb->( GET '/shortcircuit' ); is( $res->code, 200, '[/shortcircuit] Correct code with before hook' ); is( $res->content, 'I was halted', '[/shortcircuit] Correct content with before hook', ); }; }; done_testing; path.t100644000765000024 634715267764222 14646 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/dsluse strict; use warnings; use Test::More tests => 5; use Plack::Test; use Plack::Request; use Plack::Builder; use HTTP::Request::Common; { package App; use Dancer2; get '/' => sub { my $dancer_req = request; my $env = $dancer_req->env; my $plack_req = Plack::Request->new($env); ::like( $env->{'PATH_INFO'}, qr{^/?$}, 'PATH_INFO empty or /', ); ::is( $dancer_req->path_info, $env->{'PATH_INFO'}, 'D2 path_info matches $env', ); ::is( $dancer_req->path_info, $plack_req->path_info, 'D2 path_info matches Plack path_info', ); ::is( $dancer_req->path, '/', 'D2 path is /' ); ::is( $plack_req->path, '/', 'Plack path is /' ); return $dancer_req->script_name; }; get '/endpoint' => sub { my $dancer_req = request; my $env = $dancer_req->env; my $plack_req = Plack::Request->new($env); ::is( $env->{'PATH_INFO'}, '/endpoint', 'PATH_INFO /endpoint', ); ::is( $dancer_req->path_info, $env->{'PATH_INFO'}, 'D2 path_info matches $env', ); ::is( $dancer_req->path_info, $plack_req->path_info, 'D2 path_info matches Plack path_info', ); ::is( $dancer_req->path, '/endpoint', 'D2 path is /' ); ::is( $plack_req->path, '/endpoint', 'Plack path is /' ); return $dancer_req->script_name; }; } subtest '/' => sub { my $test = Plack::Test->create( App->to_app ); my $res = $test->request( GET '/' ); ok( $res->is_success, 'Result successful' ); is( $res->content, '', 'script_name is empty' ); }; subtest '/endpoint' => sub { my $test = Plack::Test->create( App->to_app ); my $res = $test->request( GET '/endpoint' ); ok( $res->is_success, 'Result successful' ); is( $res->content, '', 'script_name is empty' ); }; subtest '/mounted/' => sub { my $app = builder { mount '/' => sub { [200,[],['OK']] }; mount '/mounted' => App->to_app; }; my $test = Plack::Test->create($app); my $res = $test->request( GET '/mounted/' ); ok( $res->is_success, 'Result successful' ); is( $res->content, '/mounted', 'script_name is /mounted' ); }; subtest '/mounted/endpoint' => sub { my $app = builder { mount '/' => sub { [200,[],['OK']] }; mount '/mounted' => App->to_app; }; my $test = Plack::Test->create($app); my $res = $test->request( GET '/mounted/endpoint' ); ok( $res->is_success, 'Result successful' ); is( $res->content, '/mounted', 'script_name is /mounted' ); }; # Tests behaviour when SCRIPT_NAME is also the beginning of PATH_INFO # See the discussion in #1288. subtest '/endpoint/endpoint' => sub { my $app = builder { mount '/' => sub { [200,[],['OK']] }; mount '/endpoint' => App->to_app; }; my $test = Plack::Test->create($app); my $res = $test->request( GET '/endpoint/endpoint' ); ok( $res->is_success, 'Result successful' ); is( $res->content, '/endpoint', 'script_name is /endpoint' ); }; json.t100644000765000024 67215267764222 14636 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/dsluse strict; use warnings; use Test::More tests => 2; use Plack::Test; use HTTP::Request::Common; { package App; use Dancer2; get '/' => sub { my $app = app; my %test = (foo => 'bar'); ::is( encode_json(\%test), '{"foo":"bar"}', 'encode_json works' ); ::is_deeply( decode_json(encode_json(\%test)), \%test, 'decode_json works' ); }; } Plack::Test->create( App->to_app )->request( GET '/' ); pass.t100644000765000024 174315267764222 14653 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/dsluse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; subtest 'pass within routes' => sub { { package App; use Dancer2; get '/' => sub { 'hello' }; get '/**' => sub { response_header 'X-Pass' => 'pass'; pass; redirect '/'; # won't get executed as pass returns immediately. }; get '/pass' => sub { return "the baton"; }; } my $app = App->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; { my $res = $cb->( GET '/pass' ); is( $res->code, 200, '[/pass] Correct status' ); is( $res->content, 'the baton', '[/pass] Correct content' ); is( $res->headers->header('X-Pass'), 'pass', '[/pass] Correct X-Pass header', ); } }; }; done_testing; yaml.t100644000765000024 65415267764222 14627 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/dsluse strict; use warnings; use Test::More tests => 2; use Plack::Test; use HTTP::Request::Common; { package App; use Dancer2; get '/' => sub { my $app = app; my %test = (foo => 'bar'); ::is( to_yaml(\%test), "---\nfoo: bar\n", 'to_yaml works' ); ::is_deeply( from_yaml(to_yaml(\%test)), \%test, 'from_yaml works' ); }; } Plack::Test->create( App->to_app )->request( GET '/' ); app_alone.t100644000765000024 63415267764222 15037 0ustar00jasonstaff000000000000Dancer2-2.3.0/t#!perl use strict; use warnings; use Test::More tests => 3; use Plack::Test; use HTTP::Request::Common; { package MyApp; use Dancer2; get '/' => sub {'OK'}; } my $app = MyApp->to_app; isa_ok( $app, 'CODE' ); test_psgi $app, sub { my $cb = shift; is( $cb->( GET '/' )->code, 200, '[GET /] Correct status' ); is( $cb->( GET '/' )->content, 'OK', '[GET /] Correct content' ); }; auto_page.t100644000765000024 462515267764222 15071 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; { package AutoPageTest; use Dancer2; set auto_page => 1; set views => 't/views'; set layout => 'main'; set charset => 'UTF-8'; } my @engines = ('tiny'); eval {require Template; Template->import(); push @engines, 'template_toolkit';}; for my $tt_engine ( @engines ) { # Change template engine and run tests AutoPageTest::set( template => $tt_engine ); subtest "autopage with template $tt_engine" => \&run_tests; } sub run_tests { my $test = Plack::Test->create( AutoPageTest->to_app ); { my $r = $test->request( GET '/auto_page' ); is( $r->code, 200, 'Autopage found the page' ); # ö is U+00F6 or c3 b6 when encoded as bytes like( $r->content, qr/---\nHey! This is Auto Page w\x{c3}\x{b6}rking/, '...with proper content', ); is( $r->headers->content_type, 'text/html', 'auto page has correct content type header', ); is( $r->headers->content_type_charset, 'UTF-8', 'auto page has correct charset in content type header', ); is( $r->headers->content_length, 98, # auto_page.tt+layouts/main.tt processed. ö has two bytes in UTF-8 'auto page has correct content length header', ); } { my $r = $test->request( GET '/folder/page' ); is( $r->code, 200, 'Autopage found the page under a folder' ); like( $r->content, qr/---\nPage under folder/, '...with proper content', ); } { my $r = $test->request( GET '/non_existent_page' ); is( $r->code, 404, 'Autopage doesn\'t try to render nonexistent pages' ); } { my $r = $test->request( GET '/layouts/main'); is( $r->code, 404, 'Layouts are not served' ); } { my $r = $test->request( GET '/file.txt' ); is( $r->code, 200, 'found file on public with autopage' ); is( $r->content, "this is a public file\n", '[GET /file.txt] Correct content', ); like( $r->headers->content_type, qr{text/plain}, 'public served file has correct content type header', ); } } done_testing; App2.pm100644000765000024 25715267764222 14623 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/libpackage t::lib::App2; use strict; use warnings; use Dancer2; use lib '.'; use t::lib::DancerPlugin; install_hooks; get '/app2' => sub { session 'before_plugin'; }; 1; poc2.pm100644000765000024 65415267764222 14665 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/libpackage poc2; use strict; use warnings; use Dancer2; set logger => 'Capture'; BEGIN { set plugins => { Polite => { smiley => '8-D', }, }; } use PoC::Plugin::Polite ':app'; hook 'smileys' => sub { send_error "Not in sudoers file. This incident will be reported"; }; get '/' => sub { add_smileys( 'make me a sandwich.' ); }; get '/sudo' => sub { hooked_smileys( 'make me a sandwich.' ); }; 1; App1.pm100644000765000024 25715267764222 14622 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/libpackage t::lib::App1; use strict; use warnings; use Dancer2; use lib '.'; use t::lib::DancerPlugin; install_hooks; get '/app1' => sub { session 'before_plugin'; }; 1; splat.t100644000765000024 120515267764222 15021 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/dsluse strict; use warnings; use Test::More tests => 4; use Plack::Test; use HTTP::Request::Common; my @splat; { package App; use Dancer2; get '/*/*/*' => sub { my $params = params(); ::is_deeply( $params, { splat => [ qw ], foo => 42 }, 'Correct params', ); @splat = splat; }; } my $test = Plack::Test->create( App->to_app ); my $res = $test->request( GET '/foo/bar/baz?foo=42' ); is_deeply( [@splat], [qw(foo bar baz)], 'splat behaves as expected' ); is( $res->code, 200, 'got a 200' ); is_deeply( $res->content, 3, 'got expected response' ); named_apps.t100644000765000024 136315267764222 15230 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More import => ['!pass']; use Plack::Test; use HTTP::Request::Common; { package Foo; use Dancer2; hook before => sub { vars->{foo} = 'foo' }; post '/foo' => sub { return vars->{foo} . 'foo' . vars->{baz}; }; } { package Bar; use Dancer2 appname => 'Foo'; # Add routes and hooks to Foo. hook before => sub { vars->{baz} = 'baz' }; post '/bar' => sub { return vars->{foo} . 'bar' . vars->{baz}; } } my $app = Dancer2->psgi_app; test_psgi $app, sub { my $cb = shift; for my $path ( qw/foo bar/ ) { my $res = $cb->( POST "/$path" ); is $res->content, "foo${path}baz", "Got app content path $path"; } }; done_testing; file_utils.t100644000765000024 336615267764222 15265 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use utf8; use Test::More tests => 14; use Test::Fatal; use Path::Tiny (); use File::Temp 0.22; use Dancer2::FileUtils qw/read_file_content path_or_empty path/; sub write_file { my ( $file, $content ) = @_; open my $fh, '>', $file or die "cannot write file $file : $!"; binmode $fh, ':encoding(utf-8)'; print $fh $content; close $fh; } sub hexe { my $s = shift; $s =~ s/([\x00-\x1F])/sprintf('%#x',ord($1))/eg; return $s; } like( exception { Dancer2::FileUtils::open_file( '<', '/slfkjsdlkfjsdlf' ) }, qr/^Error open.+No such file or directory/, 'Failure opening nonexistent file', ); my $content = Dancer2::FileUtils::read_file_content(); is $content, undef; my $p = Dancer2::FileUtils::dirname('/somewhere'); is $p, '/'; my $tmp = File::Temp->new(); my $two = "²�"; write_file( $tmp, "one$/$two" ); $content = read_file_content($tmp); is hexe($content), hexe("one$/$two"); my @content = read_file_content($tmp); is hexe( $content[0] ), hexe("one$/"); is $content[1], "$two"; # returns UNDEF on non-existant path my $path = 'bla/blah'; if ( !-e $path ) { is( path_or_empty($path), '', 'path_or_empty on non-existent path', ); } my $tmpdir = File::Temp->newdir; is( path_or_empty($tmpdir), Path::Tiny::path("$tmpdir")->stringify, 'path_or_empty on an existing path' ); note "escape_filename"; { my $names = [ [ undef => 'undef' ], [ 'abcdef' => 'abcdef' ], [ 'ab++ef' => 'ab+2b+2bef' ], [ 'a/../b.txt' => 'a+2f+2e+2e+2fb+2etxt' ], [ "test\0\0" => 'test+00+00' ], [ 'test☠☠☠' => 'test+2620+2620+2620' ], ]; for my $case ( @$names ) { is Dancer2::FileUtils::escape_filename( $case->[0] ), $case->[1]; } } log_levels.t100644000765000024 656215267764222 15262 0ustar00jasonstaff000000000000Dancer2-2.3.0/t#!perl use strict; use warnings; use Test::More tests => 8; use Capture::Tiny 0.12 'capture_stderr'; use Plack::Test; use HTTP::Request::Common; { package App; use Dancer2; set logger => 'console'; set log => 'debug'; get '/debug' => sub { debug "debug msg\n"; warning "warning msg\n"; error "error msg\n"; set log => 'warning'; return 'debug'; }; get '/warning' => sub { debug "debug msg\n"; warning "warning msg\n"; error "error msg\n"; return 'warning'; }; get '/engine-warning' => sub { # Ensure that the logger and warining level is going to be used by the engines, not just the application code # Also ensure that the current log level, not the log level when the serialiser is created, is what counts. set log => 'debug'; set serializer => 'JSON'; set template => 'Tiny'; set session => 'Simple'; set log => 'warning'; foreach my $engine (qw(serializer session template)) { app->engine($engine)->log_cb->($_ => "$engine $_ msg\n") for qw(debug warning error); } return ["engine-warning"]; }; } my $app = App->to_app; test_psgi $app, sub { my $cb = shift; my $res; { my $stderr = capture_stderr { $res = $cb->( GET '/debug' ) }; is( $res->code, 200, 'Successful response' ); is( $res->content, 'debug', 'Correct content' ); like( $stderr, qr/ ^ # a debug line \[App:\d+\] \s debug [^\n]+ \n # a warning line \[App:\d+\] \s warning [^\n]+ \n # followed by an error line \[App:\d+\] \s error [^\n]+ \n $ /x, 'Log levels work', ); } { my $stderr = capture_stderr { $res = $cb->( GET '/warning' ) }; is( $res->code, 200, 'Successful response' ); is( $res->content, 'warning', 'Correct content' ); like( $stderr, qr/ ^ # a warning line \[App:\d+\] \s warning [^\n]+ \n # followed by an error line \[App:\d+\] \s error [^\n]+ \n $ /x, 'Log levels work', ); } { my $stderr = capture_stderr { $res = $cb->( GET '/engine-warning' ) }; is( $res->code, 200, 'Successful response' ); like( $stderr, qr/ ^ # serializer engine should output warning and error only \[App:\d+\] \s warning [^\n]+? serializer \s warning [^\n]+ \n \[App:\d+\] \s error [^\n]+? serializer \s error [^\n]+ \n # session engine should output warning and error only \[App:\d+\] \s warning [^\n]+? session \s warning [^\n]+ \n \[App:\d+\] \s error [^\n]+? session \s error [^\n]+ \n # template engine should output warning and error only \[App:\d+\] \s warning [^\n]+? template \s warning [^\n]+ \n \[App:\d+\] \s error [^\n]+? template \s error [^\n]+ \n $ /x, 'Log levels work', ); } }; serializer.t100644000765000024 134615267764222 15273 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More tests => 4; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; { package MyApp; use Dancer2; set serializer => 'JSON'; get '/json' => sub { +{ bar => 'baz' } }; } my $app = MyApp->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; { # Response with implicit call to the serializer my $res = $cb->( GET '/json' ); is( $res->code, 200, '[/json] Correct status' ); is( $res->content, '{"bar":"baz"}', '[/json] Correct content' ); is( $res->headers->content_type, 'application/json', '[/json] Correct content-type headers', ); } }; dispatcher.t100644000765000024 1440115267764222 15264 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More import => ['!pass']; use Carp 'croak'; use Ref::Util qw; use Dancer2; use Dancer2::Core::App; use Dancer2::Core::Route; use Dancer2::Core::Dispatcher; use Dancer2::Core::Hook; use Dancer2::Core::Response; set logger => 'Null'; # init our test fixture my $buffer = {}; my $app = Dancer2::Core::App->new( name => 'main' ); $app->setting( logger => engine('logger') ); $app->setting( show_stacktrace => 1 ); # a simple / route my $simple_route = $app->add_route( method => 'get', regexp => '/', code => sub {"home"}, ); # an error route my $error_route = $app->add_route( method => 'get', regexp => '/error', code => sub { Fail->fail }, ); # A chain of two route for /user/$foo my $user_name_route = $app->add_route( method => 'get', regexp => '/user/:name', code => sub { my $app = shift; $buffer->{user} = $app->request->params->{'name'}; $app->response->has_passed(1); }, ); my $user_splat_route = $app->add_route( method => 'get', regexp => '/user/*?', code => sub { my $app = shift; "Hello " . $app->request->params->{'name'}; }, ); # a route with a 204 response my $removed_content_route = $app->add_route( method => 'get', regexp => '/twoohfour', code => sub { my $app = shift; $app->response->status(204); "This content should be removed"; }, ); my $route_from_request; # simulates a redirect with halt $app->add_hook( Dancer2::Core::Hook->new( name => 'before', code => sub { my $app = shift; $route_from_request = $app->request->route; if ( $app->request->path_info eq '/haltme' ) { $app->response->header( Location => 'http://perldancer.org' ); $app->response->status(302); $app->response->is_halted(1); } }, ) ); my $was_in_second_filter = 0; $app->add_hook( Dancer2::Core::Hook->new( name => 'before', code => sub { my $app = shift; if ( $app->request->path_info eq '/haltme' ) { $was_in_second_filter = 1; # should not happen because first filter halted the flow } }, ) ); my $halt_route = $app->add_route( method => 'get', regexp => '/haltme', code => sub {"should not get there"}, ); # the tests my @tests = ( { env => { REQUEST_METHOD => 'GET', PATH_INFO => '/', }, expected => [ 200, [ 'Content-Length' => 4, 'Content-Type' => 'text/html; charset=utf-8', ], ["home"], $simple_route, ] }, { env => { REQUEST_METHOD => 'GET', PATH_INFO => '/user/Johnny', }, expected => [ 200, [ 'Content-Length' => 12, 'Content-Type' => 'text/html; charset=utf-8', ], ["Hello Johnny"], $user_splat_route, # the second, after the first pass()es ] }, { env => { REQUEST_METHOD => 'GET', PATH_INFO => '/twoohfour', }, expected => [ 204, [ 'Content-Type' => 'text/html; charset=utf-8' ], [], $removed_content_route, ] }, { env => { REQUEST_METHOD => 'GET', PATH_INFO => '/haltme', }, expected => [ 302, [ 'Location' => 'http://perldancer.org', 'Content-Length' => '305', 'Content-Type' => 'text/html; charset=utf-8', ], qr/This item has moved/, $halt_route, ] }, # NOT SUPPORTED YET # { env => { # REQUEST_METHOD => 'GET', # PATH_INFO => '/admin', # }, # expected => [200, [], ["home"]] # }, ); $app->compile_hooks; plan tests => 20; my $dispatcher = Dancer2::Core::Dispatcher->new( apps => [$app] ); my $counter = 0; foreach my $test (@tests) { my $env = $test->{env}; my $expected = $test->{expected}; my $path = $env->{'PATH_INFO'}; $route_from_request = undef; diag sprintf "Dispatch test %d, for %s %s", $counter, $test->{env}{REQUEST_METHOD}, $test->{env}{PATH_INFO}; my $resp = $dispatcher->dispatch($env); is( $resp->[0], $expected->[0], "[$path] Return code ok" ); my %got_headers = @{ $resp->[1] }; my %exp_headers = @{ $expected->[1] }; is_deeply( \%got_headers, \%exp_headers, "[$path] Correct headers" ); if ( is_regexpref( $expected->[2] ) ) { like $resp->[2][0] => $expected->[2], "[$path] Contents ok. (test $counter)"; } else { is_deeply $resp->[2] => $expected->[2], "[$path] Contents ok. (test $counter)"; } is( $route_from_request, # squirreled away by before hook, $expected->[3], "Expected route is stored in request (test $counter)", ); $counter++; } foreach my $test ( { env => { REQUEST_METHOD => 'GET', PATH_INFO => '/error', 'psgi.uri_scheme' => 'http', SERVER_NAME => 'localhost', SERVER_PORT => 5000, SERVER_PROTOCOL => 'HTTP/1.1', }, expected => [ 500, [ 'Content-Length', "Content-Type", 'text/html' ], qr!Internal Server Error.*Can't locate object method "fail" via package "Fail" \(perhaps you forgot to load "Fail"\?\) at t[\\/]dispatcher\.t line \d+\.!s ] } ) { my $env = $test->{env}; my $expected = $test->{expected}; my $psgi_response = $dispatcher->dispatch($env); my $resp = Dancer2::Core::Response->new( status => $psgi_response->[0], headers => $psgi_response->[1], content => $psgi_response->[2][0], ); is $resp->status => $expected->[0], "Return code ok."; ok( $resp->header('Content-Length') >= 140, "Length ok." ); like $resp->content, $expected->[2], "contents ok"; } is $was_in_second_filter, 0, "didn't enter the second filter, because of halt"; custom_dsl.t100644000765000024 105015267764222 15266 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More import => ['!pass']; use Plack::Test; use HTTP::Request::Common; use FindBin qw($Bin); use lib "$Bin/lib"; use Dancer2 dsl => 'MyDancerDSL'; envoie '/' => sub { request->method; }; prend '/' => sub { proto { ::ok('in proto') }; # no sub! request->method; }; my $test = Plack::Test->create( __PACKAGE__->to_app ); is( $test->request( GET '/' )->content, 'GET', '[GET /] Correct content' ); is( $test->request( POST '/' )->content, 'POST', '[POST /] Correct content' ); done_testing(); multi_apps.t100644000765000024 177715267764222 15307 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Plack::Builder; use Plack::Test; use HTTP::Request::Common; { package MyTestWiki; use Dancer2; get '/' => sub { __PACKAGE__ }; get '/wiki' => sub {'WIKI'}; package MyTestForum; use Dancer2; get '/' => sub { __PACKAGE__ }; get '/forum' => sub {'FORUM'}; } { my $app = builder { mount '/wiki' => MyTestWiki->to_app; mount '/forum' => MyTestForum->to_app; }; isa_ok( $app, 'CODE', 'Got app' ); test_psgi $app, sub { my $cb = shift; is( $cb->( GET '/wiki' )->content, 'MyTestWiki', "Got wiki root" ); is( $cb->( GET '/forum' )->content, 'MyTestForum', "Got forum root" ); }; } { my $app = Dancer2->psgi_app; isa_ok( $app, 'CODE', 'Got app' ); test_psgi $app, sub { my $cb = shift; is( $cb->( GET '/wiki' )->content, 'WIKI', 'Got /wiki path' ); is( $cb->( GET '/forum' )->content, 'FORUM', 'Got /forum path' ); }; } done_testing; roles000755000765000024 015267764222 13715 5ustar00jasonstaff000000000000Dancer2-2.3.0/thook.t100644000765000024 263115267764222 15204 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/rolesuse strict; use warnings; use Test::More tests => 8; use Test::Fatal; use Dancer2::Core::Hook; my $h = Dancer2::Core::Hook->new( name => 'before_template', code => sub {'BT'} ); is $h->name, 'before_template_render'; is $h->code->(), 'BT'; { package Foo; use Moo; with 'Dancer2::Core::Role::Hookable'; sub hook_aliases { +{} } sub supported_hooks {'foobar'} } my $f = Foo->new; like( exception { $f->execute_hook() }, qr{execute_hook needs a hook name}, 'execute_hook needs a hook name', ); my $count = 0; my $some_hook = Dancer2::Core::Hook->new( name => 'foobar', code => sub { $count++; } ); ok( !exception { $f->add_hook($some_hook) }, 'Supported hook can be installed', ); like( exception { $f->add_hook( Dancer2::Core::Hook->new( name => 'unknown_hook', code => sub { $count++; } ) ); }, qr{Unsupported hook 'unknown_hook'}, 'Unsupported hook cannot be installed', ); $f->execute_hook('foobar'); is $count, 1; like( exception { $f->replace_hook( 'doesnotexist', [] ) }, qr{Hook 'doesnotexist' must be installed first}, 'Nonexistent hook fails', ); my $new_hooks = [ sub { $count-- }, sub { $count-- }, sub { $count-- } ]; $f->replace_hook( 'foobar', $new_hooks ); $f->execute_hook('foobar'); is $count, -2, 'replaced hooks were installed and executed'; to_app.t100644000765000024 103615267764222 15162 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/dsluse strict; use warnings; use Plack::Test; use HTTP::Request::Common; use Test::More tests => 2; { package App1; use Dancer2; get '/' => sub {'App1'}; my $app = to_app; ::test_psgi $app, sub { my $cb = shift; ::is( $cb->( ::GET '/' )->content, 'App1', 'Got first App' ); }; } { package App2; use Dancer2; get '/' => sub {'App2'}; my $app = to_app; ::test_psgi $app, sub { my $cb = shift; ::is( $cb->( ::GET '/' )->content, 'App2', 'Got second App' ); }; } extend.t100644000765000024 203515267764222 15167 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/dsl# define a sample DSL extension that will be used in the rest of these test # This extends Dancer2::Core::DSL but provides an extra keyword # # Each test below creates a new package so it can load Dancer2 BEGIN { package Dancer2::Test::ExtendedDSL; use Moo; extends 'Dancer2::Core::DSL'; sub BUILD { my ( $self ) = @_; $self->register(foo => 1); } sub foo { return $_[1]; } } package main; use Test::More tests => 5; package test1; use Test::More; use Dancer2 dsl => 'Dancer2::Test::ExtendedDSL'; ok(defined &foo, 'use line dsl can foo'); is(foo('bar'), 'bar', 'use line Foo returns bar'); package test2; use Test::More; ok(!defined &foo, 'intermediate package has no polluted namespace'); package test3; use Test::More; use FindBin; use Path::Tiny qw< path >; BEGIN { $ENV{DANCER_CONFDIR} = path( $FindBin::Bin, 'extend_config' )->stringify; } use Dancer2; ok(defined &foo, 'config specified DSL can foo'); is(foo('baz'), 'baz', 'config specified Foo returns baz'); done_testing; xt000755000765000024 015267764222 12761 5ustar00jasonstaff000000000000Dancer2-2.3.0perltidy.rc100644000765000024 224215267764222 15303 0ustar00jasonstaff000000000000Dancer2-2.3.0/xt-l=79 # Max line width is 79 cols -i=4 # Indent level is 4 cols -ci=4 # Continuation indent is 4 cols -se # Errors to STDERR -vt=2 # Maximal vertical tightness -cti=0 # No extra indentation for closing brackets -pt=1 # Medium parenthesis tightness -bt=1 # Medium brace tightness -sbt=1 # Medium square bracket tightness -bbt=1 # Medium block brace tightness -nsfs # No space before semicolons -nolq # Don't outdent long quoted strings --break-at-old-comma-breakpoints -wbb="% + - * / x != == >= <= =~ < > | & **= += *= &= <<= &&= -= /= |= >>= ||= .= %= ^= x=" # Break before all operators # extras/overrides/deviations from PBP --maximum-line-length=79 # be less generous --warning-output # Show warnings --maximum-consecutive-blank-lines=2 # default is 1 --nohanging-side-comments # troublesome for commented out code -isbc # block comments may only be indented if they have some space characters before the # -ci=2 # Continuation indent is 2 cols # we use version control, so just rewrite the file # -b # -- should not be active for dzil plugin ## for the up-tight folk :) #-pt=2 # High parenthesis tightness #-bt=2 # High brace tightness #-sbt=2 # High square bracket tightness script000755000765000024 015267764222 13632 5ustar00jasonstaff000000000000Dancer2-2.3.0dancer2100755000765000024 564115267764222 15244 0ustar00jasonstaff000000000000Dancer2-2.3.0/script#!/usr/bin/env perl # PODNAME: dancer2 # ABSTRACT: Dancer2 command line interface use strict; use warnings; use Dancer2::CLI; Dancer2::CLI->new_with_options->run; __END__ =pod =encoding UTF-8 =head1 NAME dancer2 - Dancer2 command line interface =head1 VERSION version 2.3.0 =head1 SYNOPSIS dancer2 [options...] =head1 DESCRIPTION (If you're looking for the main L module documentation via CLI, try C.) Dancer2 is the new generation lightweight web-framework for Perl. This tool provides nice, easily-extendable CLI interface for it. =head2 Documentation Index Documentation on Dancer2 is split into several manpages. Below is a complete outline on where to go for help. =over 4 =item * Dancer2 Tutorial If you are new to the Dancer approach, you should start by reading our L. =item * Dancer2 Manual L is the reference for Dancer2. Here you will find information on the concepts of Dancer2 application development and a comprehensive reference to the Dancer2 domain specific language. =item * Dancer2 Keywords The keywords for Dancer2 can be found under L. =item * Dancer2 Deployment For configuration examples of different deployment solutions involving Dancer2 and Plack, refer to L. =item * Dancer2 Cookbook Specific examples of code for real-life problems and some 'tricks' for applications in Dancer can be found in L =item * Dancer2 Config For configuration file details refer to L. It is a complete list of all configuration options. =item * Dancer2 Plugins Refer to L for a partial list of available Dancer2 plugins. Note that although we try to keep this list up to date we expect plugin authors to tell us about new modules. =item * Dancer2 Migration guide L provides the most up-to-date instruction on how to convert a Dancer (1) based application to Dancer2. =back =head1 NAME dancer2 - Dancer2 command line interface =head1 COMMANDS =over =item * gen Create a new Dancer2 application. =item * version Display version of Dancer2 currently installed. =back To get detailed description of each individual command run: dancer2 --help The latest list of available commands can be displayed by: dancer2 =head1 AUTHOR Dancer Core Developers =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2021 by Alexis Sukrieh. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =head1 AUTHOR Dancer Core Developers =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2026 by Alexis Sukrieh. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut lib000755000765000024 015267764222 13074 5ustar00jasonstaff000000000000Dancer2-2.3.0Dancer2.pm100644000765000024 2471215267764222 15076 0ustar00jasonstaff000000000000Dancer2-2.3.0/libpackage Dancer2; $Dancer2::VERSION = '2.3.0'; # ABSTRACT: Lightweight yet powerful web application framework use 5.12.0; use strict; use warnings; use List::Util 'first'; use Module::Runtime 'use_module'; use Import::Into; use Dancer2::Core; use Dancer2::Core::App; use Dancer2::Core::Runner; our $AUTHORITY = 'SUKRIA'; sub VERSION { shift->SUPER::VERSION(@_) || '0.000000_000' } our $runner; sub runner {$runner} sub psgi_app { shift->runner->psgi_app(@_) } sub import { my ($class, @args) = @_; my ($caller, $script) = caller; my @final_args; my $clean_import; foreach my $arg (@args) { # ignore, no longer necessary # in the future these will warn as deprecated grep +($arg eq $_), qw<:script :syntax :tests> and next; if ($arg eq ':nopragmas') { $clean_import++; next; } if (substr($arg, 0, 1) eq '!') { push @final_args, $arg, 1; } else { push @final_args, $arg; } } $clean_import or $_->import::into($caller) for qw; scalar @final_args % 2 and die q{parameters must be key/value pairs or '!keyword'}; my %final_args = @final_args; my $appname = delete $final_args{appname}; $appname ||= $caller; # never instantiated the runner, should do it now if (not defined $runner) { $runner = Dancer2::Core::Runner->new(); } # Search through registered apps, creating a new app object # if we do not find one with the same name. my $app; ($app) = first { $_->name eq $appname } @{$runner->apps}; if (!$app) { # populating with the server's postponed hooks in advance $app = Dancer2::Core::App->new( name => $appname, caller => $script, environment => $runner->environment, postponed_hooks => $runner->postponed_hooks->{$appname} || {}, ); # register the app within the runner instance $runner->register_application($app); } _set_import_method_to_caller($caller); # use config dsl class, must extend Dancer2::Core::DSL my $config_dsl = $app->setting('dsl_class') || 'Dancer2::Core::DSL'; $final_args{dsl} ||= $config_dsl; # load the DSL, defaulting to Dancer2::Core::DSL my $dsl = use_module($final_args{dsl})->new(app => $app); $dsl->export_symbols_to($caller, \%final_args); } sub _set_import_method_to_caller { my ($caller) = @_; my $import = sub { my ($self, %options) = @_; my $with = $options{with}; for my $key (keys %$with) { $self->dancer_app->setting($key => $with->{$key}); } }; { ## no critic no strict 'refs'; no warnings 'redefine'; *{"${caller}::import"} = $import; } } 1; __END__ =pod =encoding UTF-8 =head1 NAME Dancer2 - Lightweight yet powerful web application framework =head1 VERSION version 2.3.0 =head1 DESCRIPTION (If you're looking for the L command documentation via CLI, try C.) Dancer2 is the new generation of L, the lightweight web framework for Perl. Dancer2 is a complete rewrite based on L. Dancer2 can optionally use XS modules for speed, but at its core remains fatpackable (via L), enabling you to easily deploy Dancer2 applications on hosts that do not support custom CPAN modules. Creating web applications with Dancer2 is easy and fun: #!/usr/bin/env perl package HelloWorld; use Dancer2; get '/' => sub { return "Hello, world!"; }; true; HelloWorld->to_app; This is the main module for the Dancer2 distribution. It contains logic for creating a new Dancer2 application. =head2 Documentation Index You have questions. We have answers. =over 4 =item * Dancer2 Tutorial Want to learn by example? The L will take you from installation to a working application. item * Quick Start Want to get going faster? L will help you install Dancer2 and bootstrap a new application quickly. =item * Dancer2 Manual Want to gain understanding of Dancer2 so you can use it best? The L is a comprehensive guide to the framework. =item * Dancer2 Keywords Looking for list of all the keywords? The L documents the entire Dancer2 DSL. =item * Dancer2 Config Need to fine tune your application? The L is the complete reference to all configuration options. =item * Dancer2 Deployment Ready to get your application off the ground? L helps you deploy your application to a real-world host. =item * Dancer2 Cookbook How do I...? Our L comes with various recipes in many tasty flavors! =item * Dancer2 Plugins Looking for add-on functionality for your application? The L contains our curated list of recommended plugins. For information on how to author a plugin, see L. =item * Dancer2 Migration guide Starting from Dancer 1? Jump over to the L to learn how to make the smoothest transition to Dancer2. =back =head3 Other Documentation =over =item * Core and Community Policy, and Standards of Conduct The L defines what constitutes acceptable behavior in our community, what behavior is considered abusive and unacceptable, and what steps will be taken to remediate inappropriate and abusive behavior. By participating in any public forum for Dancer or its community, you are agreeing to the terms of this policy. =item * GitHub Wiki Our L has community-contributed documentation, as well as other information that doesn't quite fit within this manual. =item * Contributing The L describe how to set up your development environment to contribute to the development of Dancer2, Dancer2's Git workflow, submission guidelines, and various coding standards. =item * Deprecation Policy The L defines the process for removing old, broken, unused, or outdated code from the Dancer2 codebase. This policy is critical for guiding and shaping future development of Dancer2. =back =head1 SECURITY REPORTS If you need to report a security vulnerability in Dancer2, send all pertinent information to L, or report it via the GitHub security tool. These reports will be addressed in the earliest possible timeframe. =head1 SUPPORT You are welcome to join our mailing list. For subscription information, mail address and archives see L. We are also on IRC: #dancer on irc.perl.org. =head1 AUTHORS =head2 CORE DEVELOPERS Alberto Simões Alexis Sukrieh D Ruth Holloway (GeekRuthie) Damien Krotkine David Precious Franck Cuny Jason A. Crome Mickey Nasriachi Peter Mottram (SysPete) Russell Jenkins Sawyer X Stefan Hornburg (Racke) Yanick Champoux =head2 CORE DEVELOPERS EMERITUS David Golden Steven Humphrey =head2 CONTRIBUTORS A. Sinan Unur Abdullah Diab Achyut Kumar Panda Ahmad M. Zawawi Alex Beamish Alexander Karelas Alexander Pankoff Alexandr Ciornii Andrew Beverley Andrew Grangaard Andrew Inishev Andrew Solomon Andy Jack Anton Lundin Ashvini V B10m Bas Bloemsaat baynes Ben Hutton Ben Kaufman biafra Blabos de Blebe Breno G. de Oliveira cdmalon Celogeek Cesare Gargano Charlie Gonzalez chenchen000 Chi Trinh Christian Walde Christopher White cloveistaken Colin Kuskie cym0n Dale Gallagher Dan Book (Grinnz) Daniel Böhmer Daniel Muey Daniel Perrett Dave Jacoby Dave Webb David (sbts) David Steinbrunner David Zurborg Davs Deirdre Moran Dennis Lichtenthäler Dinis Rebolo dtcyganov Elliot Holden Emil Perhinschi Erik Smit Fayland Lam ferki Gabor Szabo GeekRuthie geistteufel Gideon D'souza Gil Magno Glenn Fowler Graham Knop Gregor Herrmann Grzegorz Rożniecki Hobbestigrou Hunter McMillen ice-lenor icyavocado Ivan Bessarabov Ivan Kruglov JaHIY Jakob Voss James Aitken James Raspass James McCoy Jason Lewis Javier Rojas Jean Stebens Jens Rehsack Joel Berger Johannes Piehler Jonathan Cast Jonathan Scott Duff Joseph Frazer Julien Fiegehenn (simbabque) Julio Fraire Kaitlyn Parkhurst (SYMKAT) Karen Etheridge kbeyazli Keith Broughton lbeesley Lennart Hengstmengel Ludovic Tolhurst-Cleaver Mario Zieschang Mark A. Stratman Marketa Wachtlova Masaaki Saito Mateu X Hunter Matt Phillips Matt S Trout mauke Maurice MaxPerl Ma_Sys.ma Menno Blom Michael Kröll Michał Wojciechowski Mike Katasonov Mike Weisenborn Mikko Koivunalho Mohammad S Anwar mokko Nick Tonkin Nigel Gregoire Nikita K Nova Patch Nuno Carvalho Olaf Alders Olivier Mengué Omar M. Othman pants Patrick Zimmermann Pau Amma Paul Clements Paul Cochrane Paul Williams Pedro Bruno Pedro Melo Philippe Bricout Ricardo Signes Rick Yakubowski Ruben Amortegui Sakshee Vijay (sakshee3) Sam Kington Samit Badle Sebastien Deseille (sdeseille) Sergiy Borodych Shlomi Fish Slava Goltser Snigdha Sorin Pop Steve Bertrand Steve Dondley Steven Humphrey Tatsuhiko Miyagawa Timothy Alexis Vass Tina Müller Tom Hukins Upasana Shukla Utkarsh Gupta Vernon Lyon Victor Adam Vince Willems Vincent Bachelier xenu Yves Orton =head1 AUTHOR Dancer Core Developers =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2026 by Alexis Sukrieh. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut 00-compile.t100644000765000024 1105115267764222 15001 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; # this test was generated with Dist::Zilla::Plugin::Test::Compile 2.059 use Test::More; plan tests => 60 + ($ENV{AUTHOR_TESTING} ? 1 : 0); my @module_files = ( 'Dancer2.pm', 'Dancer2/CLI.pm', 'Dancer2/CLI/Gen.pm', 'Dancer2/CLI/Version.pm', 'Dancer2/ConfigReader.pm', 'Dancer2/ConfigReader/Config/Any.pm', 'Dancer2/ConfigUtils.pm', 'Dancer2/Core.pm', 'Dancer2/Core/App.pm', 'Dancer2/Core/Cookie.pm', 'Dancer2/Core/DSL.pm', 'Dancer2/Core/Dispatcher.pm', 'Dancer2/Core/Error.pm', 'Dancer2/Core/Factory.pm', 'Dancer2/Core/HTTP.pm', 'Dancer2/Core/Hook.pm', 'Dancer2/Core/MIME.pm', 'Dancer2/Core/Request.pm', 'Dancer2/Core/Request/Upload.pm', 'Dancer2/Core/Response.pm', 'Dancer2/Core/Response/Delayed.pm', 'Dancer2/Core/Role/ConfigReader.pm', 'Dancer2/Core/Role/DSL.pm', 'Dancer2/Core/Role/Engine.pm', 'Dancer2/Core/Role/Handler.pm', 'Dancer2/Core/Role/HasConfig.pm', 'Dancer2/Core/Role/HasEnvironment.pm', 'Dancer2/Core/Role/HasLocation.pm', 'Dancer2/Core/Role/Hookable.pm', 'Dancer2/Core/Role/Logger.pm', 'Dancer2/Core/Role/Serializer.pm', 'Dancer2/Core/Role/SessionFactory.pm', 'Dancer2/Core/Role/SessionFactory/File.pm', 'Dancer2/Core/Role/StandardResponses.pm', 'Dancer2/Core/Role/Template.pm', 'Dancer2/Core/Route.pm', 'Dancer2/Core/Runner.pm', 'Dancer2/Core/Session.pm', 'Dancer2/Core/Time.pm', 'Dancer2/Core/Types.pm', 'Dancer2/FileUtils.pm', 'Dancer2/Handler/AutoPage.pm', 'Dancer2/Handler/File.pm', 'Dancer2/Logger/Capture.pm', 'Dancer2/Logger/Capture/Trap.pm', 'Dancer2/Logger/Console.pm', 'Dancer2/Logger/Diag.pm', 'Dancer2/Logger/File.pm', 'Dancer2/Logger/Note.pm', 'Dancer2/Logger/Null.pm', 'Dancer2/Plugin.pm', 'Dancer2/Serializer/JSON.pm', 'Dancer2/Serializer/Mutable.pm', 'Dancer2/Serializer/YAML.pm', 'Dancer2/Session/Simple.pm', 'Dancer2/Session/YAML.pm', 'Dancer2/Template/TemplateToolkit.pm', 'Dancer2/Template/Tiny.pm', 'Dancer2/Test.pm' ); my @scripts = ( 'script/dancer2' ); # no fake home requested my @switches = ( -d 'blib' ? '-Mblib' : '-Ilib', ); use File::Spec; use IPC::Open3; use IO::Handle; open my $stdin, '<', File::Spec->devnull or die "can't open devnull: $!"; my @warnings; for my $lib (@module_files) { # see L my $stderr = IO::Handle->new; diag('Running: ', join(', ', map { my $str = $_; $str =~ s/'/\\'/g; q{'}.$str.q{'} } $^X, @switches, '-e', "require q[$lib]")) if $ENV{PERL_COMPILE_TEST_DEBUG}; my $pid = open3($stdin, '>&STDERR', $stderr, $^X, @switches, '-e', "require q[$lib]"); binmode $stderr, ':crlf' if $^O eq 'MSWin32'; my @_warnings = <$stderr>; waitpid($pid, 0); is($?, 0, "$lib loaded ok"); shift @_warnings if @_warnings and $_warnings[0] =~ /^Using .*\bblib/ and not eval { +require blib; blib->VERSION('1.01') }; if (@_warnings) { warn @_warnings; push @warnings, @_warnings; } } foreach my $file (@scripts) { SKIP: { open my $fh, '<', $file or warn("Unable to open $file: $!"), next; my $line = <$fh>; close $fh and skip("$file isn't perl", 1) unless $line =~ /^#!\s*(?:\S*(?:env )?perl\S*)((?:\s+-\w*)*)(?:\s*#.*)?$/; @switches = (@switches, split(' ', $1)) if $1; close $fh and skip("$file uses -T; not testable with PERL5LIB", 1) if grep $_ eq '-T', @switches and $ENV{PERL5LIB}; my $stderr = IO::Handle->new; diag('Running: ', join(', ', map { my $str = $_; $str =~ s/'/\\'/g; q{'}.$str.q{'} } $^X, @switches, '-c', $file)) if $ENV{PERL_COMPILE_TEST_DEBUG}; my $pid = open3($stdin, '>&STDERR', $stderr, $^X, @switches, '-c', $file); binmode $stderr, ':crlf' if $^O eq 'MSWin32'; my @_warnings = <$stderr>; waitpid($pid, 0); is($?, 0, "$file compiled ok"); shift @_warnings if @_warnings and $_warnings[0] =~ /^Using .*\bblib/ and not eval { +require blib; blib->VERSION('1.01') }; # in older perls, -c output is simply the file portion of the path being tested if (@_warnings = grep !/\bsyntax OK$/, grep { chomp; $_ ne (File::Spec->splitpath($file))[2] } @_warnings) { warn @_warnings; push @warnings, @_warnings; } } } is(scalar(@warnings), 0, 'no warnings found') or diag 'got warnings: ', ( Test::More->can('explain') ? Test::More::explain(\@warnings) : join("\n", '', @warnings) ) if $ENV{AUTHOR_TESTING}; CONTRIBUTING.md100644000765000024 2556515267764222 14754 0ustar00jasonstaff000000000000Dancer2-2.3.0# Contributing This guide has been written to help anyone interested in contributing to the development of Dancer2. First of all - thank you for your interest in the project! It's the community of helpful contributors who've helped Dancer grow phenomenally. Without the community we wouldn't be where we are today! Please read this guide before contributing to Dancer2 to avoid wasted or duplicated effort, and to maximize the chances of your contributions being used. There are many ways to contribute to the project beyond what is in this document. Dancer2 is an active project and any kind of help is very much appreciated! ### Asking Questions Yes, asking questions contributes to our overall community! Please use IRC or StackOverflow if possible. If these other options don't work for you, or it's taking too long to get an answer, you can leave a question on our [GitHub issues page](https://github.com/PerlDancer/Dancer2/issues). ### Reporting Bugs We prefer to have all our bug reports on GitHub, in the [issues section](https://github.com/PerlDancer/Dancer2/issues). Please make sure the bug you're reporting does not yet exist. If in doubt please ask on IRC. When reporting an issue, please include the following information in your post: - Describe what you expected to happen. - Describe what actually happened. - If possible, include a minimal reproducible example of the problem to help us identify and fix the issue. This also helps us make sure it's not an issue with your code. - List the Perl and Dancer2 versions you are using. If you can, see if this has already been reported and fixed with the latest code in GitHub. ### Improve Documentation We value documentation very much, but it's difficult to keep it up-to-date. If you find a typo or an error in the documentation please do let us know - ideally by submitting a patch (pull request) with your fix or suggestion (see [Patch Submission](#environment-and-patch-submission)). ### Writing Code You can write extensions (plugins) for Dancer2 extending core functionality or contribute to Dancer2's core code, see [Patch Submission](#environment-and-patch-submission) below. ### Blog or Write Articles Some of the best ways to spread the word about Dancer2 is to blog or write articles. Consider DEV.to, Medium, your personal blog, or even the [Perl blogs site](https://blogs.perl.org). ## General Development Guidelines This section lists high-level recommendations for developing Dancer2, for more detailed guidelines, see [Coding Guidelines](#coding-guidelines) below. ### Quality Assurance Dancer2 should be able to install for all Perl versions since 5.14, on any platform for which Perl exists. We focus mainly on GNU/Linux (any distribution), \*BSD and Windows (native and Cygwin). We should avoid regressions as much as possible and keep backwards compatibility in mind when refactoring. Just because the minimum supported version of Perl changes doesn't mean that existing code should immediately be rewritten to include new language features. For new code, or code that is being refactored for other reasons, it is acceptable to use language features that are new to the minimum supported version of Perl supported by the Dancer Core Team. Stable releases should not break functionality and new releases should provide an upgrade path and upgrade tips such as warning the user about deprecated functionality. ### Quality Supervision We can measure our quality using the [CPAN testers platform](https://www.cpantesters.org). A good way to help the project is to find a failing build log on the [CPAN testers](https://www.cpantesters.org/distro/D/Dancer2.html). If you find a failing test report, feel free to report it as a [GitHub issue](https://github.com/PerlDancer/Dancer2/issues). ## Environment and Patch Submission ### Set up a development environment If you want to submit a patch for Dancer2, you need git and very likely also [_Dist::Zilla_](https://metacpan.org/module/Dist::Zilla). We also recommend perlbrew (see below) or, alternatively, [_App::Plenv_](https://github.com/tokuhirom/plenv)) to test and develop Dancer2 on a recent version of Perl. We also suggest [_App::cpanminus_](https://metacpan.org/module/App::cpanminus) to quickly and comfortably install Perl modules. In the following sections we provide tips for the installation of some of these tools together with Dancer. Please also see the documentation that comes with these tools for more info. #### Perlbrew tips (Optional) Install perlbrew for example with $ cpanm App::perlbrew Check which Perls are available $ perlbrew available It should list the available Perl versions, like this (incomplete) list: perl-5.17.1 perl-5.16.0 perl-5.14.2 ... Now run the init command for perlbrew. The init command initializes and controls processes. The init command is run as the last step of any startup process. $ perlbrew init Then install a version inside perlbrew. We recommend you give a name to the installation (`--as` option), as well as compiling without the tests (`--n` option) to speed it up. $ perlbrew install -n perl-5.14.4 --as dancer_development -j 3 Wait a while, and it should be done. Switch to your new Perl with: $ perlbrew switch dancer_development Now you are using the fresh Perl, you can check it with: $ which perl Install cpanm on your brewed version of Perl. $ perlbrew install-cpanm ### Install various dependencies (required) Install Dist::Zilla $ cpanm Dist::Zilla ### Get Dancer2 sources Get the Dancer sources from github (for a more complete git workflow see below): Clone your fork to have a local copy using the following command: $ git clone git://github.com/yourname/Dancer2.git The Dancer2 sources come with a `dist.ini`. That's the configuration file for _Dist::Zilla_, so that it knows how to build Dancer2. Let's use dist.ini to install additional `Dist::Zilla` plugins which are not yet installed on your system (or Perl installation): $ dzil authordeps | cpanm -n That should install a bunch of stuff. Now that _Dist::Zilla_ is up and running, you should install the dependencies required by Dancer2: $ dzil listdeps | cpanm -n When that is done, you're good to go! You can use `dzil` to build and test Dancer2: $ dzil build $ dzil test --no-author ### Running your modified version If you have any version of Dancer2 installed on your system you will likely run into problems when you try and run the "Dancer2" binary due to the wrong lib's being used. The following command should resolve that. ```bash perl -Ilib script/dancer2 gen -s share/skel --overwrite --path /tmp/d2app -a MyApp::App ``` - It assumes we are in the git repo root dir - `-Ilib` - tells perl to include the lib dir in it's search path - in this case we run "gen" and - `-s share/skel` - specify the use of the local copy of the skel dir - `--overwrite` - we want to overwrite the generated scaffold project - `--path /tmp/d2app` - the dir to write the generated scaffold project dir to - `-a MyApp::App` - the name of the app project we want to generate ### Patch Submission (Github workflow) The Dancer2 development team uses GitHub to collaborate. We greatly appreciate contributions submitted via GitHub, as it makes tracking these contributions and applying them much, much easier. This gives your contribution a much better chance of being integrated into Dancer2 quickly! **NOTE:** All active development is performed in the _main_ branch. Therefore, all your contribution work should be done in a fork of the _main_ branch. Here is the workflow for submitting a patch: 1. Fork the repository: https://github.com/PerlDancer/Dancer2 and click "Fork"; 2. Clone your fork to have a local copy using the following command: $ git clone git://github.com/yourname/Dancer2.git 3. As a contributor, you should **always** work on the `main` branch of your clone. $ git remote add upstream https://github.com/PerlDancer/Dancer2.git $ git fetch upstream This will create a local branch in your clone named _main_ and that will track the official _main_ branch. That way, if you have more or less commits than the upstream repo, you'll be immediately notified by git. 4. You want to isolate all your commits in a _topic_ branch, this will make the reviewing much easier for the core team and will allow you to continue working on your clone without worrying about different commits mixing together. To do that, first create a local branch to build your pull request: # you should be in main here $ git checkout -b pr/$name Now you have created a local branch named _pr/$name_ where _$name_ is the name you want (it should describe the purpose of the pull request you're preparing). In that branch, do all the commits you need (the more the better) and when done, push the branch to your fork: # ... commits ... git push origin pr/$name You are now ready to send a pull request. 5. Run the test suite! Pull requests with failing tests will not be considered. You have two ways to run tests, either with prove: $ prove -lvr t/ or via _Dist::Zilla_: $ dzil test --all 6. Send a _pull request_ via the GitHub interface. Make sure your pull request is based on the _pr/$name_ branch you've just pushed, so that it incorporates the appropriate commits only. It's also a good idea to summarize your work in a report sent to the users' mailing list (see below), in order to make sure the team is aware of it. You could also notify the core team on IRC, on `irc.perl.org`, channel `#dancer` or via [web client](https://www.perldancer.org/irc). 7. When the core team reviews your pull request, it will either accept (and then merge into _main_) or refuse your request. If it's refused, try to understand the reasons explained by the team for the denial. Most of the time, communicating with the core team is enough to understand what the mistake was. Above all, please don't be offended. If your pull request is merged into _main_, then all you have to do is remove your local and remote _pr/$name_ branch: $ git checkout main $ git branch -D pr/$name $ git push origin :pr/$name And then, of course, you need to sync your local devel branch with upstream: $ git pull upstream main $ git push origin main You're now ready to start working on a new pull request! ### Pull Request Review Guidelines This is a non-exhaustive list of things that may cause the Dancer Core Team to request modifications to (or reject) your pull request: - Insufficient documentation - Failing tests in the test suite - Lack of tests provided covering new/changed functionality - Merge conflicts in git - Poor commit messages - Code or feature does not fit with the Core Team's vision or values for Dancer - Code introduces new bugs, or does not completely resolve an issue config_many.t100644000765000024 207415267764222 15412 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; BEGIN { # undefine ENV vars used as defaults for app environment in these tests local $ENV{DANCER_ENVIRONMENT}; local $ENV{PLACK_ENV}; $ENV{DANCER_CONFDIR} = './t/app/t1'; } use lib '.'; use lib './t/lib'; use Dancer2::Core::App; subtest basic => sub { $ENV{DANCER_CONFIG_READERS} = 'Dancer2::ConfigReader::Config::Any,Dancer2::ConfigReader::TestDummy'; my $app = Dancer2::Core::App->new( name => 'basic' ); is $app->config->{app}->{config}, 'ok', $app->name . ": config loaded properly"; is $app->config->{dummy}->{dummy_subitem}, 2, $app->name . ": dummy config loaded properly"; }; subtest additional_config_readers => sub { $ENV{DANCER_CONFIG_READERS} = 'Dancer2::ConfigReader::Additional'; my $app = Dancer2::Core::App->new( name => 'additional' ); is $app->config->{app}->{config}, 'ok', $app->name . ": config loaded properly"; is $app->config->{dummy}->{dummy_subitem}, 2, $app->name . ": dummy config loaded properly"; }; done_testing; route_query.t100644000765000024 761615267764222 15513 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request; use Dancer2::Core::Request; # Package 1: Standard app (no serialiser) for text & form endpoints { package TestApp::Standard; use Dancer2; query '/basic' => sub { return 'query ok'; }; query '/user/:id' => sub { return "User ID: " . route_parameters->get('id'); }; query '/search' => sub { my $q = body_parameters->get('q') // 'none'; return "Search term: $q"; }; query '/query-only' => sub { return 'only query allowed'; }; any '/all-methods' => sub { return request->method; }; } # Package 2: JSON-enabled app specifically for testing serialiser behaviour { package TestApp::JSON; use Dancer2; set serializer => 'JSON'; query '/json-search' => sub { my $data = request->data; unless ( ref $data eq 'HASH' ) { return status 400 => { error => 'Invalid JSON' }; } return { filter => $data->{filter} // 'none' }; }; } my $test_std = Plack::Test->create( TestApp::Standard->to_app ); my $test_json = Plack::Test->create( TestApp::JSON->to_app ); subtest 'Basic QUERY route dispatch' => sub { my $req = HTTP::Request->new( QUERY => '/basic' ); my $res = $test_std->request($req); is( $res->code, 200, 'Returns 200 OK' ); is( $res->content, 'query ok', 'Response body matches' ); }; subtest 'QUERY route with path parameters' => sub { my $req = HTTP::Request->new( QUERY => '/user/42' ); my $res = $test_std->request($req); is( $res->code, 200, 'Returns 200 OK' ); is( $res->content, 'User ID: 42', 'Route parameter correctly extracted' ); }; subtest 'QUERY request with form-encoded body payload' => sub { my $req = HTTP::Request->new( QUERY => '/search', [ 'Content-Type' => 'application/x-www-form-urlencoded' ], 'q=dancer2+query' ); my $res = $test_std->request($req); is( $res->code, 200, 'Returns 200 OK' ); is( $res->content, 'Search term: dancer2 query', 'Form parameters parsed correctly' ); }; subtest 'QUERY request with JSON payload' => sub { my $json_payload = '{"filter":"active_users"}'; my $req = HTTP::Request->new( QUERY => '/json-search', [ 'Content-Type' => 'application/json' ], $json_payload ); my $res = $test_json->request($req); is( $res->code, 200, 'Returns 200 OK' ); like( $res->content, qr/"filter"\s*:\s*"active_users"/, 'JSON body deserialised into request->data' ); }; subtest 'HTTP method matching isolation' => sub { my $req_get = HTTP::Request->new( GET => '/query-only' ); my $res_get = $test_std->request($req_get); is( $res_get->code, 404, 'GET request to query-only route returns 404' ); my $req_query = HTTP::Request->new( QUERY => '/query-only' ); my $res_query = $test_std->request($req_query); is( $res_query->code, 200, 'QUERY request succeeds on query route' ); }; subtest 'Request is_query predicate' => sub { can_ok( 'Dancer2::Core::Request', 'is_query' ); return unless Dancer2::Core::Request->can('is_query'); for my $method ( qw/ QUERY GET HEAD POST PUT DELETE OPTIONS PATCH / ) { my $req = Dancer2::Core::Request->new( env => { REQUEST_METHOD => $method }, ); if ( $method eq 'QUERY' ) { ok( $req->is_query, 'is_query is true for QUERY requests' ); } else { ok( !$req->is_query, "is_query is false for $method requests" ); } } }; subtest 'Default any route accepts QUERY' => sub { for my $method ( qw/ GET QUERY / ) { my $req = HTTP::Request->new( $method => '/all-methods' ); my $res = $test_std->request($req); is( $res->code, 200, "$method request succeeds on default any route" ); is( $res->content, $method, "Default any handler receives $method request" ); } }; done_testing; http_status.t100644000765000024 275715267764222 15513 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More tests => 5; use Dancer2::Core::HTTP; subtest "HTTP status" => sub { is( Dancer2::Core::HTTP->status( $_->{status} ) => $_->{expected}, 'status: '. ( $_->{status} || 'undef' ) ) for { status => undef, expected => undef }, { status => 200, expected => 200 }, { status => 'Not Found', expected => 404 }, { status => 'bad_request', expected => 400 }, { status => 'i_m_a_teapot', expected => 418 }, { status => 'error', expected => 500 }, { status => 911, expected => 911 }; }; subtest "HTTP status_message" => sub { is( Dancer2::Core::HTTP->status_message( $_->{status} ) => $_->{expected}, 'status: '. ( $_->{status} || 'undef' ) ) for { status => undef, expected => undef }, { status => 200, expected => 'OK' }, { status => 'error', expected => 'Internal Server Error' }, { status => 911, expected => undef }; }; is { Dancer2::Core::HTTP->status_mapping }->{"I'm a teapot"} => 418, 'status_mapping'; is { Dancer2::Core::HTTP->code_mapping }->{418} => "I'm a teapot", 'code_mapping'; subtest 'all_mappings' => sub { my %mappings = Dancer2::Core::HTTP->all_mappings; is $mappings{"I'm a teapot"} => 418; is $mappings{"i_m_a_teapot"} => 418; is $mappings{418} => "I'm a teapot"; }; dancer-test.t100644000765000024 1021515267764222 15346 0ustar00jasonstaff000000000000Dancer2-2.3.0/t# who test the tester? We do! use strict; use warnings; use Ref::Util qw; use Path::Tiny (); BEGIN { # Disable route handlers so we can actually test route_exists # and route_doesnt_exist. Use config that disables default route handlers. $ENV{DANCER_CONFDIR} = Path::Tiny::path( __FILE__ )->parent->child('dancer-test')->stringify; } use Test::More tests => 46; use Dancer2; use Dancer2::Test; use Dancer2::Core::Request; use File::Temp; use Encode; use URI::Escape; $Dancer2::Test::NO_WARN = 1; my @routes = ( '/foo', [ GET => '/foo' ], Dancer2::Core::Request->new( env => { 'psgi.url_scheme' => 'http', REQUEST_METHOD => 'GET', QUERY_STRING => '', SERVER_NAME => 'localhost', SERVER_PORT => 5000, SERVER_PROTOCOL => 'HTTP/1.1', SCRIPT_NAME => '', PATH_INFO => '/foo', REQUEST_URI => '/foo', } ), ); my $fighter = Dancer2::Core::Response->new( content => 'fighter', status => 404, ); route_doesnt_exist $_ for (@routes, $fighter); get '/foo' => sub {'fighter'}; route_exists $_, "route $_ exists" for @routes; $fighter->status(200); push @routes, $fighter; for (@routes) { my $response = dancer_response $_; isa_ok $response => 'Dancer2::Core::Response'; is $response->content => 'fighter'; } response_content_is $_ => 'fighter', "response_content_is with $_" for @routes; response_content_isnt $_ => 'platypus', "response_content_isnt with $_" for @routes; response_content_like $_ => qr/igh/ for @routes; response_content_unlike $_ => qr/ought/ for @routes; response_status_is $_ => 200 for @routes; response_status_isnt $_ => 203 for @routes; ## Check parameters get through ok get '/param' => sub { param('test') }; my $param_response = dancer_response( GET => '/param', { params => { test => 'hello' } } ); is $param_response->content, 'hello', 'PARAMS get echoed by route'; post '/upload' => sub { my $file = upload('test'); return $file->content; }; ## Check we can upload files my $file_response = dancer_response( POST => '/upload', { files => [ { filename => 'test.txt', name => 'test', data => 'testdata' } ] } ); is $file_response->content, 'testdata', 'file uploaded with supplied data'; my $temp = File::Temp->new; print $temp 'testfile'; close($temp); $file_response = dancer_response( POST => '/upload', { files => [ { filename => $temp->filename, name => 'test' } ] } ); is $file_response->content, 'testfile', 'file uploaded with supplied filename'; ## Check multiselect/multi parameters get through ok get '/multi' => sub { my $t = param('test'); return 'bad' if !is_arrayref($t); my $p = join( '', @$t ); return $p; }; $param_response = dancer_response( GET => '/multi', { params => { test => [ 'foo', 'bar' ] } } ); is $param_response->content, 'foobar', 'multi values for same key get echoed back'; my $russian_test = decode( 'UTF-8', uri_unescape("%D0%B8%D1%81%D0%BF%D1%8B%D1%82%D0%B0%D0%BD%D0%B8%D0%B5") ); $param_response = dancer_response( GET => '/multi', { params => { test => [ 'test/', $russian_test ] } } ); is decode( 'UTF-8', $param_response->content ), 'test/' . $russian_test, 'multi utf8 value properly merge'; get '/headers' => sub { join " : ", request->header('X-Sent-By'), request->cookies->{foo}; }; note "extra headers in request"; sub extra_headers { my $sent_by = 'Dancer2::Test'; my $headers_test = dancer_response( GET => '/headers', { headers => [ [ 'X-Sent-By' => $sent_by ], [ 'Cookie' => "foo=bar" ], ], } ); is $headers_test->content, "$sent_by : bar", "extra headers included in request"; } note "Run extra_headers test with XS_HTTP_COOKIES" if $Dancer2::Core::Request::XS_HTTP_COOKIES; extra_headers(); SKIP: { skip "HTTP::XSCookies not installed", 1 if !$Dancer2::Core::Request::XS_HTTP_COOKIES; note "Run extra_headers test without XS_HTTP_COOKIES"; $Dancer2::Core::Request::XS_HTTP_COOKIES = 0; extra_headers(); } deserialize.t100644000765000024 1403615267764222 15442 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More tests => 16; use Plack::Test; use HTTP::Request::Common; use Dancer2::Logger::Capture; my $logger = Dancer2::Logger::Capture->new; isa_ok( $logger, 'Dancer2::Logger::Capture' ); { package App; use Dancer2; # default, we're actually overriding this later set serializer => 'JSON'; # for now set logger => 'Console'; # deserialization fail hook hook 'core.error.before' => sub { my ($err) = @_; # Dancer2::Core::Error object if ( $err->message =~ m!Failed to deserialize content! ) { $err->status(444); # custom status } }; put '/from_params' => sub { my %p = params(); return [ map +( $_ => $p{$_} ), sort keys %p ]; }; put '/from_data' => sub { my $p = request->data; return [ map +( $_ => $p->{$_} ), sort keys %{$p} ]; }; # This route is used for both toure and body params. post '/from/:town' => sub { my $p = params; return [ map +( $_ => $p->{$_} ), sort keys %{$p} ]; }; any [qw/del patch/] => '/from/:town' => sub { my $p = params('body'); return [ map +( $_ => $p->{$_} ), sort keys %{$p} ]; }; } my $test = Plack::Test->create( App->to_app ); subtest 'PUT request with parameters' => sub { for my $type ( qw ) { my $res = $test->request( PUT "/from_$type", 'Content-Type' => 'application/json', Content => '{ "foo": 1, "bar": 2 }' ); is( $res->content, '["bar",2,"foo",1]', "Parameters deserialized from $type", ); } }; my $app = App->to_app; use utf8; use JSON::MaybeXS; use Encode; use Module::Runtime 'use_module'; note "Verify Serializers decode into characters"; { my $utf8 = '∮ E⋅da = Q, n → ∞, ∑ f(i) = � g(i)'; test_psgi $app, sub { my $cb = shift; for my $type ( qw/JSON YAML/ ) { my $class = "Dancer2::Serializer::$type"; use_module($class); my $serializer = $class->new(); my $body = $serializer->serialize({utf8 => $utf8}); # change the app serializer # we're overiding a RO attribute only for this test! Dancer2->runner->apps->[0]->set_serializer_engine( $serializer ); my $r = $cb->( PUT '/from_params', 'Content-Type' => $serializer->content_type, Content => $body, ); my $content = Encode::decode( 'UTF-8', $r->content ); like( $content, qr{\Q$utf8\E}, "utf-8 string returns the same using the $type serializer", ); } }; } # default back to JSON for the rest # we're overiding a RO attribute only for this test! Dancer2->runner->apps->[0]->set_serializer_engine( Dancer2::Serializer::JSON->new ); note "Decoding of mixed route and deserialized body params"; { # Check integers from request body remain integers # but route params get decoded. test_psgi $app, sub { my $cb = shift; my @req_params = ( "/from/D\x{c3}\x{bc}sseldorf", # /from/d%C3%BCsseldorf 'Content-Type' => 'application/json', Content => JSON::MaybeXS::encode_json({ population => 592393 }), ); my $r = $cb->( POST @req_params ); # Watch out for hash order randomization.. is_deeply( $r->content, '["population",592393,"town","'."D\x{c3}\x{bc}sseldorf".'"]', "Integer from JSON body remains integer and route params decoded", ); }; } # Check body is deserialized on PATCH and DELETE. # The RFC states the behaviour for DELETE is undefined; We take the lenient # and deserialize it. # http://tools.ietf.org/html/draft-ietf-httpbis-p2-semantics-24#section-4.3.5 note "Deserialze any body content that is allowed or undefined"; { test_psgi $app, sub { my $cb = shift; for my $method ( qw/DELETE PATCH/ ) { my $request = HTTP::Request->new( $method, "/from/D\x{c3}\x{bc}sseldorf", # /from/d%C3%BCsseldorf [ 'Content-Type' => 'application/json' ], JSON::MaybeXS::encode_json({ population => 592393 }), ); my $response = $cb->($request); my $content = Encode::decode( 'UTF-8', $response->content ); # Only body params returned is( $content, '["population",592393]', "JSON body deserialized for " . uc($method) . " requests", ); } } } note 'Check serialization errors'; { Dancer2->runner->apps->[0]->set_serializer_engine( Dancer2::Serializer::JSON->new( log_cb => sub { $logger->log(@_) } ) ); test_psgi $app, sub { my $cb = shift; my $r = $cb->( PUT '/from_params', 'Content-Type' => 'application/json', Content => '---', ); # Ensure error is logged my $trap = $logger->trapper; isa_ok( $trap, 'Dancer2::Logger::Capture::Trap' ); my $errors = $trap->read; isa_ok( $errors, 'ARRAY' ); is( scalar @{$errors}, 1, 'One error caught' ); my $msg = $errors->[0]; delete $msg->{'formatted'}; isa_ok( $msg, 'HASH' ); is( scalar keys %{$msg}, 2, 'Two items in the error' ); my $err_regex = qr{ ^ \QFailed to deserialize content: \E \Qmalformed number\E }x; is( $msg->{'level'}, 'core', 'Correct level' ); like( $msg->{'message'}, $err_regex, 'Logged correct error message' ); # Check we get a 444 response is( $r->code, 444, "444 custom response" ); my $content = Dancer2::Serializer::JSON::decode_json( $r->content ); like( $content->{message}, $err_regex, "Failed to deserialize content error"); } } prepare_app.t100644000765000024 201015267764222 15405 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Plack::Test; use HTTP::Request::Common; use Test::More 'tests' => 2; { package Foo; use Dancer2; prepare_app { set 'app_1' => 'called 1'; }; get '/' => sub {'OK'}; } { package Bar; use Dancer2 'appname' => 'Foo'; prepare_app { set 'app_2' => 'called 2'; }; get '/' => sub {'OK'}; } subtest 'Foo' => sub { my $app = Foo->to_app; is( Foo->config()->{'app_1'}, 'called 1', 'App 1 had prepare_app called', ); my $test = Plack::Test->create($app); my $res = $test->request( GET '/' )->content(); is( $res, 'OK', 'Correct content', ); }; subtest 'Bar' => sub { my $app = Bar->to_app; is( Bar->config()->{'app_2'}, 'called 2', 'App 2 had prepare_app called', ); my $test = Plack::Test->create($app); my $res = $test->request( GET '/' )->content(); is( $res, 'OK', 'Correct content', ); }; utf8_strict.t100644000765000024 423715267764222 15402 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Test::Fatal; use Encode qw(encode); use Dancer2::Core::Request; use Dancer2::Serializer::JSON; subtest 'request path decoding (lenient)' => sub { my $bytes = encode( 'UTF-8', "/\x{00F8}" ); my $req = Dancer2::Core::Request->new( env => { REQUEST_METHOD => 'GET', PATH_INFO => $bytes, }, ); my $path = $req->path; is( ord( substr( $path, 1, 1 ) ), 0xF8, 'decoded UTF-8 path' ); ok( utf8::is_utf8($path), 'decoded path is characters' ); }; subtest 'request path decoding (invalid utf8, lenient)' => sub { my $bad = '/' . pack( 'C', 0xFF ); my $req = Dancer2::Core::Request->new( env => { REQUEST_METHOD => 'GET', PATH_INFO => $bad, }, ); my @warnings; local $SIG{__WARN__} = sub { push @warnings, @_ }; my $path = $req->path; is( $path, $bad, 'invalid UTF-8 left as bytes' ); like( $warnings[0], qr/Invalid UTF-8/, 'warned on invalid UTF-8' ); }; subtest 'request path decoding (invalid utf8, strict)' => sub { my $req = Dancer2::Core::Request->new( env => { REQUEST_METHOD => 'GET', PATH_INFO => '/' . pack( 'C', 0xFF ), }, strict_utf8 => 1, ); my $error = exception { $req->path }; like( $error, qr/Invalid UTF-8/, 'strict mode rejects invalid UTF-8' ); }; subtest 'json serialization (lenient + strict)' => sub { my $bad = pack( 'C', 0xFF ); my @warnings; my $serializer = Dancer2::Serializer::JSON->new( config => { strict_utf8 => 0 }, log_cb => sub { push @warnings, $_[1] }, ); my $json = $serializer->serialize( { bad => $bad } ); ok( $json, 'serialize succeeded in lenient mode' ); like( $warnings[0], qr/Invalid UTF-8/, 'warned on invalid UTF-8' ); my $strict_bad = pack( 'C', 0xFF ); my $strict = Dancer2::Serializer::JSON->new( config => { strict_utf8 => 1 }, log_cb => sub {1}, ); my $error = exception { $strict->serialize( { bad => $strict_bad } ) }; like( $error, qr/Invalid UTF-8/, 'strict mode rejects invalid UTF-8' ); }; done_testing(); cli000755000765000024 015267764222 14013 5ustar00jasonstaff000000000000Dancer2-2.3.0/t/e2egen.t100644000765000024 4471315267764222 15142 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/e2e/cliuse strict; use warnings; use Test::More; use File::Temp qw< tempdir >; use Path::Tiny qw< path >; use Capture::Tiny qw< capture >; use Config; use File::Which qw< which >; use TAP::Harness; # End-to-end test for the `dancer2 gen` scaffold: it runs the real command as a # separate process against share/skel, then compiles and runs what came out. # # It runs the command out of *this* source tree, never an installed Dancer2: # -s share/skel points at the skeleton in the repo rather than the dist_dir # -x skips the "is there a newer Dancer2 on CPAN?" check, which # needs the network and warns when it cannot reach it -- but # only if this build still has that option, see below # The generated application only knows about Dancer2 through PERL5LIB, which is # set to this repo's lib for every child process below. my $repo = path('.')->absolute; my $script = $repo->child('script/dancer2'); my $skel = $repo->child('share/skel'); -f $script && -d $skel or plan skip_all => 'must be run from the distribution root'; my $lib = $repo->child('lib')->stringify; # Whether the generator understands -x has to be asked, not assumed. Debian # patches the CPAN version check out of Dancer2::CLI::Gen as a privacy measure # (debian/patches/no-phone-home.patch), and the option whose only job is to # switch that check off goes with it, so on a build made from those sources # passing -x aborts the generator with "Unknown option: x" before it writes a # single file -- and every assertion in this file then fails, pointing at the # scaffold rather than at the flag. Where the option is gone, so is the network # access it exists to suppress, so dropping it loses nothing. my @no_check = _no_check_option(); # Ask the command itself what it accepts. Osprey spells the long form with # dashes or underscores depending on its version, so match the short form, and # accept either spelling of the long one. sub _no_check_option { my ( $stdout, $stderr ) = capture { local $ENV{'PERL5LIB'} = _prepend_lib(); system( $Config{'perlpath'}, "-I$lib", "$script", 'gen', '--help' ); }; my $usage = "$stdout$stderr"; return '-x' if $usage =~ /(?:^|\s)-x\b/ || $usage =~ /--no.check\b/; note 'this dancer2 gen has no -x: running without it, on the assumption ' . 'that the version check it skips has been patched out too'; return; } # Run the generator. Returns the exit status and both output streams; the # generator is chatty on STDOUT ("+ path" per file written), and none of that # should reach this suite's own output. sub gen { my @args = @_; my ( $stdout, $stderr, $status ) = capture { local $ENV{'PERL5LIB'} = _prepend_lib(); system( $Config{'perlpath'}, "-I$lib", "$script", 'gen', '-s', "$skel", @no_check, '--overwrite', @args ); }; return { status => $status, stdout => $stdout, stderr => _without_version_check_warning($stderr), }; } # With no -x to pass, a build that kept the version check runs it, and it warns # when it cannot reach metacpan -- which is the normal state of a package build # daemon. That warning is the generator saying something this test has no way # to switch off in such a build, so it is dropped here rather than failing the # "says nothing on STDERR" assertion. Only those two lines go; anything else # the generator writes to STDERR is still caught. Builds that do have -x are # left alone, so the assertion stays strict where it can be. sub _without_version_check_warning { my $stderr = shift; return $stderr if @no_check; $stderr =~ s{\s*Couldn't determine latest version of Dancer2\b[^\n]*\n[^\n]*\n\s*}{}s; return $stderr; } # This repo's lib has to come first so the child sees *this* Dancer2, but it # must not replace whatever PERL5LIB already carries -- under local::lib (as # used in development and CI) that variable is how the child finds Dancer2's # own dependencies (e.g. CLI::Osprey), and clobbering it makes every child # process below fail before it even gets to the code under test. sub _prepend_lib { return join $Config{'path_sep'}, $lib, ( defined $ENV{'PERL5LIB'} && length $ENV{'PERL5LIB'} ? $ENV{'PERL5LIB'} : () ); } # Read a generated file, or give back the empty string if the generator never # wrote it. A missing file is already reported by the file-presence subtest; # this keeps the content assertions from dying and taking the rest of the run # down with them, so one broken thing produces one clear list of failures. sub slurp { my $file = shift; return $file->is_file ? $file->slurp_utf8 : ''; } # The application the scaffold has to produce, listed here rather than derived # from share/skel -- deriving it would make the check vacuous, since a file # deleted from the skeleton would also vanish from the expectation. Every entry # is something the generated app needs to start, serve its one route, or be # packaged. my @required = qw< bin/app.psgi config.yml cpanfile views/index.tt views/layouts/main.tt public/404.html public/500.html public/css/style.css public/dispatch.cgi public/dispatch.fcgi t/001_base.t t/002_index_route.t environments/development.yml environments/production.yml .dancer Makefile.PL MANIFEST MANIFEST.SKIP >; my $tmp = tempdir( CLEANUP => 1 ); my $run = gen( '-a', 'MyApp::App', '-d', 'myapp', '--path', $tmp ); my $app = path( $tmp, 'myapp' ); subtest 'the generator runs cleanly and writes where it was told' => sub { is( $run->{'status'}, 0, 'dancer2 gen exits successfully' ) or diag "STDERR:\n$run->{stderr}\nSTDOUT:\n$run->{stdout}"; is( $run->{'stderr'}, '', 'and says nothing on STDERR' ); ok( $app->is_dir, "--path and -d put the application in $app" ); like( $run->{'stdout'}, qr/Your new application is ready/, 'the run ends with the how-to-run banner' ); }; subtest 'every file the generated application needs is present' => sub { # This is the subtest that goes red if share/skel loses something. for my $file (@required) { ok( $app->child($file)->is_file, "generated $file" ); } # The skeleton marks these two with a leading '+' to request the exec bit; # the '+' must not survive into the generated name. ok( !$app->child('bin/+app.psgi')->exists, 'the + marker is stripped from the generated filename' ); SKIP: { skip 'no meaningful exec bit on this platform', 2 if $^O eq 'MSWin32'; ok( -x $app->child('bin/app.psgi')->stringify, 'bin/app.psgi is executable' ); ok( -x $app->child('public/dispatch.cgi')->stringify, 'public/dispatch.cgi is executable' ); } }; subtest '-a decides the package name and the module path' => sub { my $module = $app->child('lib/MyApp/App.pm'); ok( $module->is_file, 'the module is written at the path -a implies' ); my $source = slurp($module); like( $source, qr/^package MyApp::App;/m, 'and declares that package' ); ok( length $source && $source !~ /AppFile/, 'no trace of the skeleton\'s placeholder package name' ); # The same name has to reach the files that refer to the app by name, or # the generated app starts but cannot be packaged or served. like( slurp( $app->child('bin/app.psgi') ), qr/\buse MyApp::App\b/, 'the .psgi entry point loads the generated module' ); like( slurp( $app->child('Makefile.PL') ), qr/NAME\s*=>\s*'MyApp::App'/, 'Makefile.PL names the generated module' ); # Template tokens are [d2% ... %2d]; any left behind means a variable was # never substituted. for my $file (qw< bin/app.psgi Makefile.PL config.yml >) { my $content = slurp( $app->child($file) ); ok( length $content && $content !~ /\Q[d2%\E/, "no unsubstituted template token left in $file" ); } }; subtest 'the generated application compiles' => sub { my ( $stdout, $stderr, $status ) = capture { system( $Config{'perlpath'}, "-I$lib", '-I' . $app->child('lib')->stringify, '-c', $app->child('lib/MyApp/App.pm')->stringify ); }; is( $status, 0, 'perl -c on the generated module succeeds' ) or diag "STDERR:\n$stderr"; like( $stderr, qr/syntax OK/, 'and perl says so' ); }; subtest 'the generated application passes its own bundled tests' => sub { # The app's tests are run from inside the generated directory, the way its # author would run them. Using TAP::Harness (rather than shelling out to # prove) ensures the child perls are invoked via $^X -- the same interpreter # that is running this test suite -- so they see the same @INC and the same # XS extensions. Shelling out to prove lets PATH decide which perl to use, # which on smokers with multiple perls installed can differ from this one, # producing "Can't locate Module::Runtime" or XS handshake mismatches. my ( $stdout, $stderr, $status ) = capture { local $ENV{'PERL5LIB'} = _prepend_lib(); my $cwd = path('.')->absolute; chdir $app->stringify or die "cannot chdir to $app: $!"; my $harness = TAP::Harness->new({ lib => ['lib'] }); my $aggregator = $harness->runtests(glob 't/*.t'); chdir $cwd->stringify or die "cannot chdir back to $cwd: $!"; $aggregator->has_errors ? 1 : 0; }; is( $status, 0, 'the generated app passes its own tests' ) or diag "STDOUT:\n$stdout\nSTDERR:\n$stderr"; like( $stdout, qr/Result: PASS/, 'the harness agrees' ); }; subtest 'the application directory is named after the dashed app name (fixed)' => sub { # This used to leave the directory named after the application verbatim -- # colons and all -- even though the generator computes a dashed name for # exactly this purpose. Without -d, that dashed name is now what the # directory option falls back to, so the directory and the Makefile.PL it # contains agree with each other. my $dir = tempdir( CLEANUP => 1 ); my $res = gen( '-a', 'Other::App', '--path', $dir ); is( $res->{'status'}, 0, 'generating without -d still succeeds' ); ok( path( $dir, 'Other-App' )->is_dir, 'the directory is named Other-App, dashed' ); ok( !path( $dir, 'Other::App' )->exists, 'and the raw, colon-bearing name is not used' ); # The dashed name also reaches the generated Makefile.PL -- the two # spellings inside one generated application now agree. like( slurp( path( $dir, 'Other-App', 'Makefile.PL' ) ), qr/FILES\s*=>\s*'Other-App-\*'/, 'and Makefile.PL cleans the same dashed name', ); # An explicit -d must still be honoured verbatim -- the fallback only # kicks in when the option is absent -- so the directory that appears has # to be the one that was asked for, not the dashed name. The name used # here is one Windows can put on disk, so this runs everywhere; the # colon-bearing spelling of the same thing, which is what makes the point # most sharply, is checked below where such a directory can exist at all. my $res2 = gen( '-a', 'Other::App', '-d', 'Other_App', '--path', $dir ); is( $res2->{'status'}, 0, 'generating with an explicit -d still succeeds' ) or diag "STDERR:\n$res2->{stderr}\nSTDOUT:\n$res2->{stdout}"; ok( path( $dir, 'Other_App' )->is_dir, 'and an explicit -d is honoured verbatim, not dashed' ); SKIP: { # ':' is not a legal character in a Windows file name at all - it # introduces an alternate data stream - so no Windows file system can # hold a directory called Other::App, and a generator asked to make # one there can only fail. The dashes this used to be checked through # are checked above instead; this is the original spelling. skip 'Windows cannot put a colon in a directory name', 2 if $^O eq 'MSWin32'; my $res3 = gen( '-a', 'Other::App', '-d', 'Other::App', '--path', $dir ); is( $res3->{'status'}, 0, 'a colon-bearing -d still succeeds' ) or diag "STDERR:\n$res3->{stderr}\nSTDOUT:\n$res3->{stdout}"; ok( path( $dir, 'Other::App' )->is_dir, 'and is honoured verbatim, even with colons in it' ); } }; subtest 'MANIFEST.SKIP gets the relative dashed name (fixed)' => sub { # This used to build the appended line from the full filesystem path the # app was generated into, so it could never match anything -- MANIFEST.SKIP # patterns are matched against distribution-root-relative paths. The line # is now the dashed distribution name, matching what Makefile.PL cleans. my @lines = split /\n/, slurp( $app->child('MANIFEST.SKIP') ); my $appended = @lines ? $lines[-1] : ''; is( $appended, '^MyApp-App-', 'the appended pattern is the relative dashed name' ); unlike( $appended, qr{^\^/}, 'and is not anchored to an absolute filesystem path' ); # No line in the file should carry an absolute path -- that was the shape # of the bug, so make sure it is gone everywhere, not just the last line. ok( !( grep { m{^\^?/} } @lines ), 'no line in MANIFEST.SKIP is an absolute path' ); # The rest of the file is what a MANIFEST.SKIP is supposed to look like, # which is what makes the appended line fit in rather than stand out. ok( ( grep { $_ eq '^\.gitignore' || $_ eq '^.gitignore' } @lines ), 'the other patterns are relative, as MANIFEST.SKIP expects' ); }; subtest 'the skeleton environment configs are in git (fixed)' => sub { # This was caused by share/.gitignore: shipped as data -- Dancer2::CLI::Gen # copies it into a generated app when -g is given -- but because it sat # inside share/ under that name, git also applied its patterns to this # repository's own tree, and one of them was 'environments/'. That template # now lives at share/gitignore instead (a name git does not honour as an # ignore file), so the skeleton's environment configs can be committed, and # a fresh clone generates them. They also belong in @required above now. my $probe = 'share/skel/default/environments/development.yml'; # Being inside a work tree is not enough. 'dzil test' builds into # .build/XXXX *within* this checkout and runs the suite from there, so # --is-inside-work-tree succeeds while 'git ls-files ' -- which # resolves its pathspec relative to the current directory -- names a # build artifact rather than the source file, and reports nothing. # Ask git where the top of the tree is and query from there instead, # skipping where there is no git at all (a released tarball on a smoker). my ( $toplevel, undef, $rev_status ) = capture { system( 'git', 'rev-parse', '--show-toplevel' ); }; $rev_status == 0 or plan skip_all => 'not a git checkout, so tracking cannot be checked'; chomp $toplevel; -d path( $toplevel, 'share/skel/default' ) or plan skip_all => "git top level $toplevel is not this source tree"; my ($tracked) = capture { system( 'git', '-C', $toplevel, 'ls-files', 'share/skel/default/environments' ); }; like( $tracked, qr{development\.yml}, 'development.yml is tracked by git' ); like( $tracked, qr{production\.yml}, 'production.yml is tracked by git' ); # And the generator actually produced them for the app built at the top of # this file -- already covered by @required above, but asserted here too # since this is the subtest that explains why. ok( $app->child('environments/development.yml')->is_file, 'environments/development.yml is generated' ); ok( $app->child('environments/production.yml')->is_file, 'environments/production.yml is generated' ); }; subtest '-g creates a git repository with an initial commit (fixed)' => sub { # _check_git used to die with "Can't locate object method \"absolute\" # via package ..." -- it called ->absolute on $vars->{apppath}, a plain # string, instead of using $vars->{appdir}, which run() had already made # absolute for exactly this purpose. That happened after the application # was written but before git ever ran, so the user was left with a # generated app, no repository, and a stack-shaped error instead of the # how-to-run banner. -r implies -g and shares the same code path, so a # working -g is what -r's remote-adding also depends on. which('git') or plan skip_all => 'no usable git binary found'; my $dir = tempdir( CLEANUP => 1 ); # git needs a name and email to commit, and may consult other bits of # config (e.g. commit.gpgsign) that vary developer to developer and could # make this hang or fail for reasons that have nothing to do with the code # under test. Rather than rely on -- or fight with -- whatever is in the # developer's own ~/.gitconfig, give the child processes below a HOME of # their own (so no global or user config is read at all) and supply the # commit identity git itself reads via the environment. my $git_home = tempdir( CLEANUP => 1 ); local $ENV{'HOME'} = $git_home; local $ENV{'XDG_CONFIG_HOME'} = $git_home; local $ENV{'GIT_AUTHOR_NAME'} = 'Dancer2 Test Suite'; local $ENV{'GIT_AUTHOR_EMAIL'} = 'dancer2-tests@example.invalid'; local $ENV{'GIT_COMMITTER_NAME'} = 'Dancer2 Test Suite'; local $ENV{'GIT_COMMITTER_EMAIL'} = 'dancer2-tests@example.invalid'; my $res = gen( '-a', 'Git::App', '-d', 'gitapp', '--path', $dir, '-g' ); my $gitapp = path( $dir, 'gitapp' ); is( $res->{'status'}, 0, 'dancer2 gen -g exits successfully' ) or diag "STDERR:\n$res->{stderr}\nSTDOUT:\n$res->{stdout}"; like( $res->{'stdout'}, qr/Your new application is ready/, 'and still ends with the how-to-run banner, not a stack trace' ); ok( $gitapp->child('.git')->is_dir, 'a git repository was created' ); my $git_dir = $gitapp->child('.git')->stringify; my ( $log, undef, $log_status ) = capture { system( 'git', '--git-dir', $git_dir, 'log', '--oneline' ); }; is( $log_status, 0, 'the repository has at least one commit' ); like( $log, qr/Initial commit of Git::App by Dancer2/, 'and it is the generator\'s initial commit' ); my ( $tracked, undef, $ls_status ) = capture { system( 'git', '--git-dir', $git_dir, '--work-tree', $gitapp->stringify, 'ls-files' ); }; is( $ls_status, 0, 'can list the files the initial commit contains' ); like( $tracked, qr{bin/app\.psgi}, 'the generated application files are in it' ); like( $tracked, qr{lib/Git/App\.pm}, 'including the generated module' ); }; done_testing(); content.t100644000765000024 254215267764222 15355 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/dsluse strict; use warnings; use Test::More tests => 1; use Plack::Test; use HTTP::Request::Common; my $logger; { package App::ContentFail; ## no critic use Dancer2; set show_stacktrace => 1; set logger => 'Capture'; $logger = app->engine('logger'); get '/' => sub { content 'Foo' }; } subtest 'content keyword can only be used within delayed response' => sub { my $test = Plack::Test->create( App::ContentFail->to_app ); my $res = $test->request( GET '/' ); ok( ! $res->is_success, 'Request failed' ); is( $res->code, 500, 'Correct response code' ); like( $res->content, qr/Cannot use content keyword outside delayed response/, 'Failed to use content keyword outside delayed response', ); isa_ok( $logger, 'Dancer2::Logger::Capture' ); my $trapper = $logger->trapper; isa_ok( $trapper, 'Dancer2::Logger::Capture::Trap' ); my $error = $trapper->read; isa_ok( $error, 'ARRAY' ); is( scalar @{$error}, 1, 'Only one error' ); ok( delete $error->[0]{'formatted'}, 'Got formatted message' ); like( delete $error->[0]{'message'}, qr{^\QRoute exception: Cannot use content keyword outside delayed response\E}, 'Correct error message', ); is_deeply( $error, [ { level => 'error' } ], 'Rest of error okay', ); }; send_as.t100644000765000024 1001115267764222 15325 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/dsluse strict; use warnings; use Test::More import => ['!pass']; use Plack::Test; use HTTP::Request::Common; { package DummyObj; use Moo; has foo => (is => 'ro', default => 'bar'); sub TO_JSON { {foo => shift->foo}; } 1; } { package Test::App::SendAs; use Dancer2; set engines => { serializer => { JSON => { allow_blessed => 1, convert_blessed => 1, } } }; set logger => 'Capture'; set serializer => 'YAML'; set template => 'TemplateToolkit'; get '/html' => sub { send_as html => '' }; get '/plain' => sub { send_as plain => 'some plain text with '; }; get '/json/**' => sub { send_as JSON => splat; }; get '/json-object' => sub { send_as JSON => { data => DummyObj->new() }; }; get '/json-utf8/**' => sub { send_as JSON => splat, { content_type => 'application/json', charset => 'utf-8' }; }; get '/yaml/**' => sub { my @params = splat; \@params; }; get '/sendas/:type?' => sub { send_as route_parameters->{'type'} => 'test string'; }; } my $test = Plack::Test->create( Test::App::SendAs->to_app ); subtest "default serializer" => sub { my $res = $test->request( GET '/yaml/is/useful' ); is $res->code, '200'; is $res->content_type, 'text/x-yaml'; my $expected = <<'YAML'; --- - - is - useful YAML is $res->content, $expected; }; subtest "send_as json" => sub { my $res = $test->request( GET '/json/is/wonderful' ); is $res->code, '200'; is $res->content_type, 'application/json'; is $res->content, '["is","wonderful"]'; }; subtest "send_as json object" => sub { my $res = $test->request( GET '/json-object' ); is $res->code, '200'; is $res->content_type, 'application/json'; is $res->content, '{"data":{"foo":"bar"}}'; }; subtest "send_as json custom content-type" => sub { my $res = $test->request( GET '/json-utf8/is/wonderful' ); is $res->code, '200'; is $res->content_type, 'application/json'; is $res->content_type_charset, 'UTF-8'; is $res->content, '["is","wonderful"]'; }; subtest "send_as html" => sub { my $res = $test->request( GET '/html' ); is $res->code, '200'; is $res->content_type, 'text/html'; is $res->content_type_charset, 'UTF-8'; is $res->content, ''; }; subtest "send_as plain" => sub { my $res = $test->request( GET '/plain' ); is $res->code, '200'; is $res->content_type, 'text/plain'; is $res->content_type_charset, 'UTF-8'; is $res->content, 'some plain text with '; }; subtest "send_as error cases" => sub { my $logger = Test::App::SendAs::app->logger_engine; { my $res = $test->request( GET '/sendas/' ); is $res->code, '500', "send_as dies with no defined type"; my $logs = $logger->trapper->read; like $logs->[0]->{message}, qr!Route exception: Can not send_as using an undefined type!, ".. throws route exception"; } { local $SIG{__WARN__} = sub { return if $_[0] =~ /Subroutine .* redefined/; CORE::warn @_; }; my $res = $test->request( GET '/sendas/jSoN' ); is $res->code, '500', "send_as dies with incorrectly cased serializer name"; my $logs = $logger->trapper->read; like $logs->[0]->{message}, qr!Route exception: Unable to load serializer class for jSoN!, ".. throws route exception"; } { my $res = $test->request( GET '/sendas/SomeSerializerThatDoesNotExist' ); is $res->code, '500', "send_as dies when called with non-existant serializer"; my $logs = $logger->trapper->read; like $logs->[0]->{message}, qr!Route exception: Unable to load serializer class for SomeSerializerThatDoesNotExist!, ".. throws route exception"; } }; done_testing(); request.t100644000765000024 211315267764222 15365 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/dsluse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; { package App::DSL::Request; use Dancer2; any [ 'get', 'post' ], '/' => sub { request->method; }; get 'headers' => sub { request_header 'X-Foo'; }; } subtest 'Testing an app with request keyword' => sub { my $test = Plack::Test->create( App::DSL::Request->to_app ); { my $res = $test->request( GET '/' ); ok( $res->is_success, 'Successful GET request' ); is( $res->content, 'GET', 'GET / correct content' ); } { my $res = $test->request( POST '/' ); ok( $res->is_success, 'Successful POST request' ); is( $res->content, 'POST', 'POST / correct content' ); } }; subtest 'Testing app with request_header keyword' => sub { my $test = Plack::Test->create( App::DSL::Request->to_app ); my $res = $test->request( GET '/headers', 'X-Foo' => 'Bar' ); ok( $res->is_success, 'Successful GET request' ); is( $res->content, 'Bar', 'GET /headers correct content' ); }; done_testing; delayed.t100644000765000024 1150715267764222 15333 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/dsluse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; eval { require AnyEvent; 1; } or plan skip_all => 'AnyEvent required for this test'; plan tests => 6; { package App::Content; ## no critic use Dancer2; get '/' => sub { ::is( $Dancer2::Core::Route::RESPONDER, undef, 'No responder yet' ); delayed { ::isa_ok( $Dancer2::Core::Route::RESPONDER, 'CODE', 'Got a responder in the delayed callback', ); ::is( $Dancer2::Core::Route::WRITER, undef, 'No writer yet' ); content 'OK'; ::ok( $Dancer2::Core::Route::WRITER, 'Got a writer' ); done; }; }; } { package App::Content::MultiWrite; ## no critic use Dancer2; get '/' => sub { delayed { flush; content 'Foo'; content 'Bar'; done; }; }; } { package App::NoContent; ## no critic use Dancer2; get '/' => sub { delayed {content;done;'Not OK'}; }; } { package App::AddHeader; ## no critic use Dancer2; hook 'after' => sub { my $response = shift; $response->push_header('Authorization' => 'bar'); }; get '/' => sub { delayed { response->push_header('Content-Type' => 'foo'); flush; content "baz"; done; } }; } { package App::MultipleContent; ## no critic use Dancer2; get '/' => sub { delayed { content 'Bar'; done; }; return 'OK'; }; } my $caught_error; { package App::ErrorHandler; ## no critic use Dancer2; require AnyEvent; set logger => 'Capture'; get '/log' => sub { delayed { flush; content "ping\n"; done; content "failure\n"; }; }; get '/cb' => sub { delayed { flush; content "ping\n"; done; content "failure\n"; } on_error => sub { $caught_error = shift; }; }; } subtest 'Testing an app with content keyword' => sub { my $test = Plack::Test->create( App::Content->to_app ); my $res = $test->request( GET '/' ); ok( $res->is_success, 'Successful request' ); is( $res->content, 'OK', 'Correct content' ); }; subtest 'Testing an app with multiple content keyword calls' => sub { my $test = Plack::Test->create( App::Content::MultiWrite->to_app ); my $res = $test->request( GET '/' ); ok( $res->is_success, 'Successful request' ); is( $res->content, 'FooBar', 'Correct content' ); }; subtest 'Testing an app without content keyword' => sub { my $test = Plack::Test->create( App::NoContent->to_app ); my $res = $test->request( GET '/' ); ok( $res->is_success, 'Successful request' ); is( $res->content, '', 'Correct content' ); }; subtest 'Delayed response has push_header method' => sub { my $test = Plack::Test->create( App::AddHeader->to_app ); my $res = $test->request( GET '/' ); ok( $res->is_success, 'Successful request' ); is( $res->headers->header('Content-Type'), 'foo', 'Correct header pushed from inside "delayed"' ); is( $res->headers->header('Authorization'), 'bar', 'Correct header pushed from inside hook' ); }; subtest 'Delayed response ignored for non-delayed content' => sub { my $test = Plack::Test->create( App::MultipleContent->to_app ); my $res = $test->request( GET '/' ); ok( $res->is_success, 'Successful request' ); is( $res->content, 'OK', 'Correct content' ); }; subtest 'Delayed response error handling' => sub { my $test = Plack::Test->create( App::ErrorHandler->to_app ); TODO: { local $TODO = 'Does not work in development server'; my $res = $test->request( GET '/log' ); ok( $res->is_success, 'Successful request' ); is( $res->content, "ping\n", 'Correct content' ); my $logger = App::ErrorHandler::app->logger_engine; my $logs = $logger->trapper->read; isa_ok( $logs, 'ARRAY', 'Got logs' ); is( scalar @{$logs}, 1, 'Got a message' ); my $msg = shift @{$logs}; ok( $msg, 'Got message' ); isa_ok( $msg, 'HASH', 'Got message' ); is( $msg->{'level'}, 'core', 'Correct error message level', ); like( $msg->{'message'}, qr/^Error in delayed response:/, 'Got error', ); } TODO: { local $TODO = 'Does not work in development server'; my $res = $test->request( GET '/cb' ); ok( $res->is_success, 'Successful request' ); is( $res->content, "ping\n", 'Correct content' ); like( $caught_error, qr/^Error in delayed response:/, 'Got error' ); } }; uri_for.t100644000765000024 217615267764222 15353 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/dsluse strict; use warnings; use Test::More 'tests' => 2; use Plack::Test; use Plack::Builder; use HTTP::Request::Common; { package App; use Dancer2; get '/' => sub { return uri_for('/foo'); }; } { package MountedApp; use Dancer2; get '/' => sub { return uri_for('/bar'); }; } my $prefix = 'http://localhost'; subtest 'Non-mounted app' => sub { my $app = Plack::Test->create( App->to_app ); my $res; $res = $app->request( GET "$prefix/" ); ok( $res->is_success, 'Successful request' ); is( $res->content, "$prefix/foo", 'Correct regular path' ); }; subtest 'Mounted app' => sub { my $app = Plack::Test->create( builder { mount '/mount' => MountedApp->to_app; mount '/' => App->to_app; } ); my $res; $res = $app->request( GET "$prefix/" ); ok( $res->is_success, 'Successful request' ); is( $res->content, "$prefix/foo", 'Correct mounted regular path' ); $res = $app->request( GET "$prefix/mount" ); ok( $res->is_success, 'Successful request' ); is($res->content, "$prefix/mount/bar", 'Correct mounted regular path'); }; whitespace.t100644000765000024 31215267764222 15416 0ustar00jasonstaff000000000000Dancer2-2.3.0/xtuse Test::Whitespaces { dirs => [qw( lib script t tools xt )], ignore => [ qr{t/sessions/}, qr{t/template_tiny/samples}, ], }; share000755000765000024 015267764222 13430 5ustar00jasonstaff000000000000Dancer2-2.3.0gitignore100644000765000024 5015267764222 15435 0ustar00jasonstaff000000000000Dancer2-2.3.0/sharesessions/ logs/ *_local.* environments/ named_routes.t100644000765000024 432015267764222 15602 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More 'tests' => 5; use Plack::Test; use HTTP::Request::Common; { package MyApp; use Dancer2; # Name, Regexp, Code get 'view_static', '/view' => sub { 'View Static'; }; get 'view_regex', qr{^/view_r$} => sub { 'View Regex' }; # Name, Regexp, Options, Code get 'base_static', '/' => { 'user_agent' => 'XX' }, sub { 'Base Static'; }; # Name, Regexp, Options, Code get 'base_regex', qr{^/r$}, {}, sub { 'Base Regex'; }; get '/ignore1' => sub {1}; get '/ignore2' => sub {1}; get '/ignore3' => sub {1}; } my $test = Plack::Test->create( MyApp->to_app ); subtest 'Named static route' => sub { plan 'tests' => 2; my $response = $test->request( GET '/view' ); ok( $response->is_success, 'Successfully reached /view' ); is( $response->content, 'View Static', 'Static route with name' ); }; subtest 'Named regex route' => sub { plan 'tests' => 2; my $response = $test->request( GET '/view_r' ); ok( $response->is_success, 'Successfully reached /view_r' ); is( $response->content, 'View Regex', 'Regex route with name' ); }; subtest 'Named static route with options' => sub { plan 'tests' => 2; my $response = $test->request( GET '/', 'User-Agent' => 'XX' ); ok( $response->is_success, 'Successfully reached /' ); is($response->content, 'Base Static', 'Static route with name and options'); }; subtest 'Named regex route with options' => sub { plan 'tests' => 2; my $response = $test->request( GET '/r', 'User-Agent' => 'XX' ); ok( $response->is_success, 'Successfully reached /r' ); is($response->content, 'Base Regex', 'Regex route with name and options'); }; subtest 'Route objects' => sub { plan 'tests' => 3; my @apps = @{ Dancer2::runner->apps }; is( scalar @apps, 1, 'Only one app exists' ); my %routes = %{ $apps[0]->route_names() }; is( scalar keys %routes, 4, 'Four named routes registered' ); is_deeply( [ sort keys %routes ], [ 'base_regex', 'base_static', 'view_regex', 'view_static', ], 'All the right route names', ); }; config_utils.t100644000765000024 63615267764222 15570 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Test::Fatal; use Dancer2::ConfigUtils qw/normalize_config_entry/; is( normalize_config_entry( 'charset', 'UTF-8' ), 'utf-8', 'normalized UTF-8 to utf-8'); like( exception { normalize_config_entry( 'charset', 'BOGUS' ) }, qr{Charset defined in configuration is wrong : couldn't identify 'BOGUS'}, 'Configuration file charset failure', ); done_testing; http_methods.t100644000765000024 251015267764222 15616 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More tests => 13; use Plack::Test; use HTTP::Request; use Ref::Util qw; use Dancer2; my %method = ( get => 'GET', post => 'POST', del => 'DELETE', patch => 'PATCH', put => 'PUT', options => 'OPTIONS', query => 'QUERY', ); my $app = __PACKAGE__->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; while ( my ( $method, $http ) = each %method ) { eval "$method '/' => sub { '$method' }"; is( $cb->( HTTP::Request->new( $http => '/' ) )->content, $method, "$http /", ); } eval "get '/head' => sub {'HEAD'}"; my $res = $cb->( HTTP::Request->new( HEAD => '/head' ) ); is( $res->content, '', 'HEAD /' ); # HEAD requests have no content is( $res->headers->content_length, 4, 'Content-Length for HEAD' ); # Testing invalid HTTP methods. { my $req = HTTP::Request->new( "ILLEGAL" => '/' ); my $res = $cb->( $req ); ok( !$res->is_success, "Response->is_success is false when using illegal HTTP method" ); is( $res->code, 405, "Illegal method should return 405 code" ); like( $res->content, qr, q ); } }; template_ext.t100644000765000024 115615267764222 15614 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; eval { require Template; Template->import(); 1 } or plan skip_all => 'Template::Toolkit probably missing.'; use Dancer2; set engines => { template => { template_toolkit => { extension => 'foo', }, }, }; set template => 'template_toolkit'; my $tt = engine('template'); isa_ok( $tt, 'Dancer2::Template::TemplateToolkit' ); is( $tt->default_tmpl_ext, 'foo', "Template extension is 'foo' as configured", ); is( $tt->view_pathname('foo'), 'foo.foo' , "view('foo') gives filename with right extension as configured", ); done_testing; t2000755000765000024 015267764222 13676 5ustar00jasonstaff000000000000Dancer2-2.3.0/t/app.dancer100644000765000024 115267764222 15202 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/app/t2 TestPod.pm100644000765000024 112715267764222 15420 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/libpackage t::lib::TestPod; use Dancer2; =head1 NAME TestPod =head2 ROUTES =over =cut =item get "/in_testpod" testpod =cut get '/in_testpod' => sub { # code; }; =item get "/hello" testpod =cut get '/hello' => sub { # code; }; =item post '/in_testpod/*' post in_testpod =cut post '/in_testpod/*' => sub { return 'post in_testpod'; }; =back =head2 SPECIALS =head3 PUBLIC =over =item get "/me:id" =cut get "/me:id" => sub { # code; }; =back =head3 PRIVAT =over =item post "/me:id" post /me:id =cut post "/me:id" => sub { # code; }; =back =cut 1; SubApp2.pm100644000765000024 25315267764222 15271 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/libpackage t::lib::SubApp2; use strict; use warnings; use Dancer2; use lib 't/lib'; use Dancer2::Plugin::DancerPlugin; install_hooks; get '/subapp2' => sub { 2; }; 1; SubApp1.pm100644000765000024 25315267764222 15270 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/libpackage t::lib::SubApp1; use strict; use warnings; use Dancer2; use lib 't/lib'; use Dancer2::Plugin::DancerPlugin; install_hooks; get '/subapp1' => sub { 1; }; 1; TestApp.pm100644000765000024 476615267764222 15432 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/libpackage t::lib::TestApp; use Dancer2; # this app is intended to cover 100% of the DSL! # set some MIME aliases... mime->add_type( foo => 'text/foo' ); mime->add_alias( f => 'foo' ); set 'default_mime_type' => 'text/bar'; # hello route get '/' => sub { app->name }; # /haltme should bounce to / hook 'before' => sub { if ( request->path_info eq '/haltme' ) { redirect '/'; halt; } }; get '/haltme' => sub {"should not be there"}; hook 'after' => sub { my $response = shift; if ( request->path_info eq '/rewrite_me' ) { $response->content("rewritten!"); } }; get '/rewrite_me' => sub {"body should not be this one"}; # some settings set some_var => 1; setting some_other_var => 1; set multiple_vars => 4, can_be_set => 2; get '/config' => sub { return config->{some_var} . ' ' . config->{some_other_var} . ' and ' . setting('multiple_vars') . setting('can_be_set'); }; if ( $] >= 5.010 ) { # named captures get qr{/(? usr | content | post )/(? delete | find )/(? \d+ )}x => sub { join( ":", sort %{ captures() } ); }; } # chained routes with pass get '/user/**' => sub { my $user = params->{splat}; var user => $user->[0][0]; pass; }; get '/user/*/home' => sub { my $user = var('user'); # should be set by the previous route "hello $user"; }; # post & dirname post '/dirname' => sub { dirname('/etc/passwd'); }; # header get '/header/:name/:value' => sub { response_header param('name') => param('value'); 1; }; # push_header get '/header/:name/:valueA/:valueB' => sub { push_response_header param('name') => param('valueA'); push_response_header param('name') => param('valueB'); 1; }; # header get '/header_twice/:name/:valueA/:valueB' => sub { response_header param('name') => param('valueA'); response_header param('name') => param('valueB'); 1; }; # any any [ 'get', 'post' ], '/any' => sub { "Called with method " . request->method; }; # true and false get '/booleans' => sub { join( ":", true, false ); }; # mimes get '/mime/:name' => sub { mime->for_name( param('name') ); }; # content_type get '/content_type/:type' => sub { content_type param('type'); 1; }; # prefix prefix '/prefix' => sub { get '/bar' => sub {'/prefix/bar'}; prefix '/prefix1' => sub { get '/bar' => sub {'/prefix/prefix1/bar'}; }; prefix '/prefix2'; get '/foo' => sub {'/prefix/prefix2/foo'}; }; 1; views000755000765000024 015267764222 13726 5ustar00jasonstaff000000000000Dancer2-2.3.0/tindex.tt100644000765000024 20115267764222 15517 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/views[index] var = [% var %] before_layout_render = [% before_layout_render %] before_template_render = [% before_template_render %] perlcritic.rc100644000765000024 464315267764222 15616 0ustar00jasonstaff000000000000Dancer2-2.3.0/xt# nice output, to easily see the POD of the policy verbose = [%p] %m at %f line %l, near '%r'\n # severity of 3 is a good start (1 is very strict, 5 very tolerant) severity = 3 # we want to use // without //ms [-RegularExpressions::RequireDotMatchAnything] [-RegularExpressions::RequireLineBoundaryMatching] [-RegularExpressions::RequireExtendedFormatting] minimum_regex_length_to_complain_about = 5 [-RegularExpressions::ProhibitComplexRegexes] # we don't want these POD rules [-Documentation::RequirePodSections] # We don't care about POD links [-Documentation::RequirePodLinksIncludeText] # we use $@ and $! [-Variables::ProhibitPunctuationVars] # We want to be able to use Carp::Verbose in our tests scripts, so # we add Carp to the whitelist [Variables::ProhibitPackageVars] packages = Data::Dumper File::Find FindBin Log::Log4perl Carp [-ValuesAndExpressions::ProhibitEmptyQuotes] # I really don't think q{/} is more readable than '/'... [-ValuesAndExpressions::ProhibitNoisyQuotes] # Perl::Critic recommends Readonly, but this IS BAD! # we use Const::Fast instead, but this policy keeps popping up. [-ValuesAndExpressions::ProhibitMagicNumbers] # we want to be able to build DSLs [-Modules::ProhibitAutomaticExportation] # We only want the main module to provide $VERSION [-Modules::RequireVersionVar] # we want to be able to define short getters [-Subroutines::RequireFinalReturn] # we can't do @_ mesures with that one [-Subroutines::RequireArgUnpacking] # name is a common used name for methods # but forbidden by this policy ... [-Subroutines::ProhibitBuiltinHomonyms] # some old libs use many args, we don't want to block that for now [-Subroutines::ProhibitManyArgs] # we allo protected subs [-Subroutines::ProhibitUnusedPrivateSubroutines] # We're not under CVS! :) [-Miscellanea::RequireRcsKeywords] [TestingAndDebugging::ProhibitNoStrict] allow = refs [TestingAndDebugging::ProhibitNoWarnings] allow = redefine prototype [TestingAndDebugging::RequireUseStrict] equivalent_modules = strictures Moo Moo::Role [TestingAndDebugging::RequireUseWarnings] equivalent_modules = strictures Moo Moo::Role # we use postifx controls [-ControlStructures::ProhibitPostfixControls] [-ControlStructures::ProhibitCascadingIfElse] # We want to use croak everywhere instead of die [ErrorHandling::RequireCarping] # allow backtick if capture result [InputOutput::ProhibitBacktickOperators] only_in_void_context = 1 [-Variables::ProhibitAugmentedAssignmentInDeclaration] memory_cycles.t100644000765000024 111215267764222 15763 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Test::Fatal; use Plack::Test; eval { require Test::Memory::Cycle; 1; } or plan skip_all => 'Test::Memory::Cycle not present'; { package MyApp::Cycles; use Dancer2; set auto_page => 1; set serializer => 'JSON'; get '/**' => sub { return { hello => 'world' }; }; } my $app = MyApp::Cycles->to_app; my $runner = Dancer2->runner; Test::Memory::Cycle::memory_cycle_ok( $runner, "runner has no memory cycles" ); Test::Memory::Cycle::memory_cycle_ok( $app, "App has no memory cycles" ); done_testing(); plugin_import.t100644000765000024 236315267764222 16012 0ustar00jasonstaff000000000000Dancer2-2.3.0/t# plugin_import.t use strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; { use Dancer2; use lib 't/lib'; use Dancer2::Plugin::PluginWithImport; get '/test' => sub { dancer_plugin_with_import_keyword; }; } my $app = __PACKAGE__->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; is( $cb->( GET '/test' )->content, 'dancer_plugin_with_import_keyword', 'the plugin exported its keyword', ); }; is_deeply( Dancer2::Plugin::PluginWithImport->stuff, { 'Dancer2::Plugin::PluginWithImport' => 'imported' }, "the original import method of the plugin is still there" ); subtest 'import flags' => sub { eval " package Dancer2::Plugin::Some::Plugin1; use Dancer2::Plugin ':no_dsl'; register 'foo' => sub { request }; "; like $@, qr{Bareword "request" not allowed while "strict subs"}, "with :no_dsl, the Dancer's dsl is not imported."; eval " package Dancer2::Plugin::Some::Plugin2; use Dancer2::Plugin; register 'foo' => sub { request }; "; is $@, '', "without any import flag, the DSL is imported"; }; done_testing; config_reader.t100644000765000024 1160315267764222 15726 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Test::Fatal; use Carp 'croak'; use Path::Tiny qw< path >; use Dancer2::Core::Runner; use Dancer2::ConfigReader; use Dancer2::ConfigReader::Config::Any; # undefine ENV vars used as defaults for app environment in these tests local $ENV{DANCER_ENVIRONMENT}; local $ENV{PLACK_ENV}; my $runner = Dancer2::Core::Runner->new(); my $location = path( __FILE__ )->sibling('config'); my $location2 = path( __FILE__ )->sibling('config2'); { package ConfigUser; use Moo; with 'Dancer2::Core::Role::HasConfig'; has environment => ( is => 'ro', required => 1 ); has location => ( is => 'ro', required => 1 ); has default_config => ( is => 'ro', required => 1 ); sub _build_config { my $self = shift; return Dancer2::ConfigReader->new( environment => $self->environment, location => $self->location, default_config => $self->default_config, )->config; } } sub config_any { my ( $environment, $location ) = @_; return Dancer2::ConfigReader::Config::Any->new( environment => $environment, location => $location, ); } sub config_reader { my ( $environment, $location ) = @_; return Dancer2::ConfigReader->new( environment => $environment, location => $location, default_config => $runner->config, ); } sub config_user { my ( $environment, $location ) = @_; return ConfigUser->new( environment => $environment, location => $location, default_config => $runner->config, ); } my $d = config_any( 'development', $location ); is_deeply $d->config_files, [ path( $location, 'config.yml' ), ], "config_files() only sees existing files"; my $f_any = config_any( 'production', $location ); is $f_any->does('Dancer2::Core::Role::ConfigReader'), 1, "role Dancer2::Core::Role::ConfigReader is consumed"; is_deeply $f_any->config_files, [ path( $location, 'config.yml' ), path( $location, 'environments', 'production.yml' ), ], "config_files() works"; my $j = config_any( 'staging', $location ); is_deeply $j->config_files, [ path( $location, 'config.yml' ), path( $location, 'environments', 'staging.json' ), ], "config_files() does JSON too!"; note "bad YAML file"; my $fail_any = config_any( 'failure', $location ); is $fail_any->environment, 'failure'; is_deeply $fail_any->config_files, [ path( $location, 'config.yml' ), path( $location, 'environments', 'failure.yml' ), ], "config_files() works"; like( exception { config_reader( 'failure', $location->stringify )->config }, qr{Unable to parse the configuration file}, 'Configuration file parsing failure', ); note "config merging"; my $m = config_reader( 'merging', $location->stringify ); # Check the 'application' top-level key; its the only key that # is currently a HoH in the test configurations is_deeply $m->config->{application}, { some_feature => 'bar', another_setting => 'baz', }, "full merging of configuration hashes"; { my $l_any = config_any( 'lconfig', $location2->stringify ); is_deeply $l_any->config_files, [ path( $location2, 'config.yml' ), path( $location2, 'config_local.yml' ), path( $location2, 'environments', 'lconfig.yml' ), path( $location2, 'environments', 'lconfig_local.yml' ), ], "config_files() with local config works"; my $l = config_reader( 'lconfig', $location2->stringify ); is_deeply $l->config->{application}, { feature_1 => 'foo', feature_2 => 'alpha', feature_3 => 'replacement', feature_4 => 'blat', feature_5 => 'beta', feature_6 => 'bar', feature_7 => 'baz', feature_8 => 'goober', }, "full merging of local configuration hashes"; } note "config parsing"; my $f = config_user( 'production', $location->stringify ); is $f->config->{main}, 1; is $f->config->{charset}, 'utf-8', "normalized UTF-8 to utf-8"; ok( $f->has_setting('charset') ); ok( !$f->has_setting('foobarbaz') ); note "default values"; is $f->setting('apphandler'), 'Standalone'; like( exception { config_reader( 'production', $location->stringify ) ->_normalize_config( { charset => 'BOGUS' } ); }, qr{Charset defined in configuration is wrong : couldn't identify 'BOGUS'}, 'Configuration file charset failure', ); { package Foo; use Carp 'croak'; sub foo { croak "foo" } } is $f->setting('traces'), 0; unlike( exception { Foo->foo() }, qr{Foo::foo}, "traces are not enabled", ); $f->setting( traces => 1 ); like( exception { Foo->foo() }, qr{Foo::foo}, "traces are enabled", ); { my $tmpdir = Path::Tiny->tempdir( CLEANUP => 1, TMPDIR => 1 ); $ENV{DANCER_CONFDIR} = $tmpdir; my $f = config_any( 'production', $location->stringify ); is $f->config_location, $tmpdir; } done_testing; strict_config.t100644000765000024 721415267764222 15757 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Dancer2::ConfigReader; use Dancer2::FileUtils qw/dirname path/; use File::Spec; { package Dancer2::ConfigReader::TestWarn; use Moo; with 'Dancer2::Core::Role::ConfigReader'; has name => ( is => 'ro', default => sub { 'TestWarn' }, ); has config_data => ( is => 'ro', default => sub { {} }, ); sub read_config { my ($self) = @_; return $self->config_data; } } my $location = File::Spec->rel2abs( path( dirname(__FILE__), 'config' ) ); sub _read_config_with_warnings { my ($config_data) = @_; my $reader = Dancer2::ConfigReader::TestWarn->new( environment => 'test', location => $location, config_data => $config_data, ); my $cfgr = Dancer2::ConfigReader->new( environment => 'test', location => $location, default_config => {}, config_readers => [$reader], ); my @warnings; local $SIG{__WARN__} = sub { push @warnings, @_ }; $cfgr->config; return join q{}, @warnings; } subtest 'warns on unknown keys' => sub { my $warnings = _read_config_with_warnings({ strict_config => 1, typo => 1, engines => { logger => { File => { log_dir => '/tmp', log_level => 'debug', extra => 1, }, }, template => { template_toolkit => { foo => 1, }, }, serializer => { JSON => { allow_nonref => 1, foo => 2, }, }, }, }); like( $warnings, qr/Unknown configuration key 'typo'/, 'warns for unknown top-level key', ); like( $warnings, qr/Unknown configuration key 'extra' for engine 'logger\/File'/, 'warns for unknown engine key', ); unlike( $warnings, qr/template_toolkit/, 'does not warn for template_toolkit keys', ); unlike( $warnings, qr/serializer\/JSON/, 'does not warn for JSON serializer keys', ); }; subtest 'can disable warnings' => sub { my $warnings = _read_config_with_warnings({ strict_config => 0, typo => 1, engines => { logger => { File => { extra => 1, }, }, }, }); is( $warnings, q{}, 'warnings silenced' ); }; subtest 'can allow specific top-level keys' => sub { my $warnings = _read_config_with_warnings({ strict_config => 1, strict_config_allow => [ 'typo', 'extra_top_level' ], typo => 1, extra_top_level => 1, nope => 1, engines => { logger => { File => { extra => 1, }, }, }, }); unlike( $warnings, qr/Unknown configuration key 'typo'/, 'does not warn for allowlisted keys', ); unlike( $warnings, qr/Unknown configuration key 'extra_top_level'/, 'does not warn for allowlisted keys', ); like( $warnings, qr/Unknown configuration key 'nope'/, 'still warns for other top-level keys', ); like( $warnings, qr/Unknown configuration key 'extra' for engine 'logger\/File'/, 'still warns for unknown engine keys', ); }; done_testing; session_hooks.t100644000765000024 1665715267764222 16043 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Cookies; use HTTP::Request::Common; my @hooks_to_test = qw( engine.session.before_retrieve engine.session.after_retrieve engine.session.before_create engine.session.after_create engine.session.before_change_id engine.session.after_change_id engine.session.before_destroy engine.session.after_destroy engine.session.before_flush engine.session.after_flush ); # we'll set a flag here when each hook is called. Then our test will then verify this my $test_flags = {}; { package App; use Dancer2; set( show_stacktrace => 1, envoriment => 'production' ); setting( session => 'Simple' ); for my $hook (@hooks_to_test) { hook $hook => sub { $test_flags->{$hook} ||= 0; $test_flags->{$hook}++; } } get '/set_session' => sub { session foo => 'bar'; #setting causes a session flush return "ok"; }; get '/get_session' => sub { ::is session->read('foo'), 'bar', "Got the right session back"; return "ok"; }; get '/change_session_id' => sub { app->change_session_id; return "ok"; }; get '/destroy_session' => sub { app->destroy_session; return "ok"; }; #setup each hook again and test whether they return the correct type #there is unfortunately quite some duplication here. hook 'engine.session.before_create' => sub { my ($response) = @_; ::isa_ok( $response, 'Dancer2::Core::Session' ); }; hook 'engine.session.after_create' => sub { my ($response) = @_; ::isa_ok( $response, 'Dancer2::Core::Session' ); }; hook 'engine.session.after_retrieve' => sub { my ($response) = @_; ::isa_ok( $response, 'Dancer2::Core::Session' ); }; } my $test = Plack::Test->create( App->to_app ); my $jar = HTTP::Cookies->new; my $url = "http://localhost"; is_deeply( $test_flags, {}, 'Make sure flag hash is clear' ); subtest set_session => sub { my $res = $test->request( GET "$url/set_session" ); is $res->content, "ok", "set_session ran ok"; $jar->extract_cookies($res); }; # we verify whether the hooks were called correctly. subtest 'verify hooks for session create and session flush' => sub { is $test_flags->{'engine.session.before_create'}, 1, "session.before_create called"; is $test_flags->{'engine.session.after_create'}, 1, "session.after_create called"; is $test_flags->{'engine.session.before_flush'}, 1, "session.before_flush called"; is $test_flags->{'engine.session.after_flush'}, 1, "session.after_flush called"; is $test_flags->{'engine.session.before_change_id'}, undef, "session.before_change_id not called"; is $test_flags->{'engine.session.after_change_id'}, undef, "session.after_change_id not called"; is $test_flags->{'engine.session.before_retrieve'}, undef, "session.before_retrieve not called"; is $test_flags->{'engine.session.after_retrieve'}, undef, "session.after_retrieve not called"; is $test_flags->{'engine.session.before_destroy'}, undef, "session.before_destroy not called"; is $test_flags->{'engine.session.after_destroy'}, undef, "session.after_destroy not called"; }; subtest 'verify Handler::File (static content) does not retrieve session' => sub { my $req = GET "$url/file.txt"; $jar->add_cookie_header($req); my $res = $test->request($req); $jar->extract_cookies($res); # These should not change from previous subtest is $test_flags->{'engine.session.before_create'}, 1, "session.before_create not called"; is $test_flags->{'engine.session.after_create'}, 1, "session.after_create not called"; is $test_flags->{'engine.session.before_retrieve'}, undef, "session.before_retrieve not called"; is $test_flags->{'engine.session.after_retrieve'}, undef, "session.after_retrieve not called"; }; subtest get_session => sub { my $req = GET "$url/get_session"; $jar->add_cookie_header($req); my $res = $test->request($req); is $res->content, "ok", "get_session ran ok"; $jar->extract_cookies($res); }; subtest 'verify hooks for session retrieve' => sub { is $test_flags->{'engine.session.before_retrieve'}, 1, "session.before_retrieve called"; is $test_flags->{'engine.session.after_retrieve'}, 1, "session.after_retrieve called"; is $test_flags->{'engine.session.before_create'}, 1, "session.before_create not called"; is $test_flags->{'engine.session.after_create'}, 1, "session.after_create not called"; is $test_flags->{'engine.session.before_flush'}, 1, "session.before_flush not called"; is $test_flags->{'engine.session.after_flush'}, 1, "session.after_flush not called"; is $test_flags->{'engine.session.before_change_id'}, undef, "session.before_change_id not called"; is $test_flags->{'engine.session.after_change_id'}, undef, "session.after_change_id not called"; is $test_flags->{'engine.session.before_destroy'}, undef, "session.before_destroy not called"; is $test_flags->{'engine.session.after_destroy'}, undef, "session.after_destroy not called"; }; subtest change_session_id => sub { my $req = GET "$url/change_session_id"; $jar->add_cookie_header($req); my $res = $test->request($req); is $res->content, "ok", "get_session ran ok"; $jar->clear; $jar->extract_cookies($res); }; subtest 'verify hooks for change session id' => sub { # change_session_id causes a retrieve is $test_flags->{'engine.session.before_retrieve'}, 2, "session.before_retrieve called"; is $test_flags->{'engine.session.after_retrieve'}, 2, "session.after_retrieve called"; is $test_flags->{'engine.session.before_create'}, 1, "session.before_create not called"; is $test_flags->{'engine.session.after_create'}, 1, "session.after_create not called"; is $test_flags->{'engine.session.before_flush'}, 1, "session.before_flush not called"; is $test_flags->{'engine.session.after_flush'}, 1, "session.after_flush not called"; is $test_flags->{'engine.session.before_change_id'}, 1, "session.before_change_id called"; is $test_flags->{'engine.session.after_change_id'}, 1, "session.after_change_id called"; is $test_flags->{'engine.session.before_destroy'}, undef, "session.before_destroy not called"; is $test_flags->{'engine.session.after_destroy'}, undef, "session.after_destroy not called"; }; subtest destroy_session => sub { my $req = GET "$url/destroy_session"; $jar->add_cookie_header($req); my $res = $test->request($req); is $res->content, "ok", "destroy_session ran ok"; }; subtest 'verify session destroy hooks' => sub { is $test_flags->{'engine.session.before_destroy'}, 1, "session.before_destroy called"; is $test_flags->{'engine.session.after_destroy'}, 1, "session.after_destroy called"; #not sure if before and after retrieve should be called when the session is destroyed. But this happens. is $test_flags->{'engine.session.before_retrieve'}, 3, "session.before_retrieve called"; is $test_flags->{'engine.session.after_retrieve'}, 3, "session.after_retrieve called"; is $test_flags->{'engine.session.before_create'}, 1, "session.before_create not called"; is $test_flags->{'engine.session.after_create'}, 1, "session.after_create not called"; is $test_flags->{'engine.session.before_flush'}, 1, "session.before_flush not called"; is $test_flags->{'engine.session.after_flush'}, 1, "session.after_flush not called"; }; done_testing; template_name.t100644000765000024 66315267764222 15716 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; { package Foo; use Dancer2; get '/template_name' => sub { return engine('template')->name; }; } my $app = Foo->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; is( $cb->( GET '/template_name' )->content, 'Tiny', 'template name' ); }; done_testing; plugin_syntax.t100644000765000024 1001415267764222 16036 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More import => ['!pass']; use Plack::Test; use HTTP::Request::Common; use JSON::MaybeXS; use Ref::Util qw; subtest 'global and route keywords' => sub { { package App1; use Dancer2; use lib 't/lib'; use Dancer2::Plugin::FooPlugin; sub location {'/tmp'} get '/' => sub { foo_wrap_request->env->{'PATH_INFO'}; }; get '/app' => sub { app->name }; get '/plugin_setting' => sub { to_json(p_config) }; foo_route; } my $app = App1->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; is( $cb->( GET '/' )->content, '/', 'route defined by a plugin', ); is( $cb->( GET '/foo' )->content, 'foo', 'DSL keyword wrapped by a plugin', ); is( _normalize($cb->( GET '/plugin_setting' )->content), _normalize(encode_json( { plugin => '42' } )), 'plugin_setting returned the expected config' ); is( $cb->( GET '/app' )->content, 'App1', 'app name is correct', ); }; }; subtest 'plugin old syntax' => sub { { package App2; use Dancer2; use lib 't/lib'; use Dancer2::Plugin::DancerPlugin; around_get; } my $app = App2->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; is( $cb->( GET '/foo/plugin' )->content, 'foo plugin', 'foo plugin', ); }; }; subtest caller_dsl => sub { my $app = App1->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; is( $cb->( GET '/sitemap' )->content, '^\/$, ^\/app$, ^\/foo$, ^\/foo\/plugin$, ^\/plugin_setting$, ^\/sitemap$', 'Correct content', ); }; }; subtest 'hooks in plugins' => sub { my $counter = 0; { package App3; use Dancer2; use lib 't/lib'; use Dancer2::Plugin::OnPluginImport; use Dancer2::Plugin::Hookee; use Dancer2::Plugin::EmptyPlugin; hook 'third_hook' => sub { var( hook => 'third hook' ); }; hook 'start_hookee' => sub { 'this is the start hook'; }; get '/hook_with_var' => sub { some_other(); # executes 'third_hook' ::is var('hook') => 'third hook', "Vars preserved from hooks"; }; get '/hooks_plugin' => sub { $counter++; some_keyword(); # executes 'start_hookee' 'hook for plugin'; }; get '/hook_returns_stuff' => sub { some_keyword(); # executes 'start_hookee' }; get '/on_import' => sub { some_import(); # execute 'plugin_import' } } my $app = App3->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; is( $counter, 0, 'the hook has not been executed' ); is( $cb->( GET '/hooks_plugin' )->content, 'hook for plugin', '... route is rendered', ); is( $counter, 1, '... and the hook has been executed exactly once' ); is( $cb->( GET '/hook_returns_stuff' )->content, '', '... hook does not influence rendered content by return value', ); # call the route that has an additional test $cb->( GET '/hook_with_var' ); is ( $cb->( GET '/on_import' )->content, Dancer2->VERSION, 'hooks added by on_plugin_import don\'t stop hooks being added later' ); }; }; sub _normalize { my ($json) = @_; my $data = decode_json($json); foreach (keys %$data) { $data->{$_} = $data->{$_} * 1 if ($data->{$_} =~ m/^\d+$/); } return encode_json($data); } done_testing; public000755000765000024 015267764222 14047 5ustar00jasonstaff000000000000Dancer2-2.3.0/tfile.txt100644000765000024 2615267764222 15625 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/publicthis is a public file plugin2000755000765000024 015267764222 14151 5ustar00jasonstaff000000000000Dancer2-2.3.0/thooks.t100644000765000024 340215267764222 15620 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/plugin2use strict; use warnings; use Test::More tests => 3; use Plack::Test; use HTTP::Request::Common; { package Dancer2::Plugin::FooDetector; use Dancer2::Plugin; plugin_hooks 'foo'; sub BUILD { my $plugin = shift; $plugin->app->add_hook( Dancer2::Core::Hook->new( name => 'after', code => sub { $plugin->app->execute_hook( 'plugin.foodetector.foo' ) if $_[0]->content =~ /foo/; } ) ); } } { package PoC; use Dancer2; use Dancer2::Plugin::FooDetector; my $hooked = 'nope'; my $counter = 0; hook 'plugin.foodetector.foo' => sub { $counter++; $hooked = 'hooked'; }; get '/' => sub { "saying foo triggers the hook" }; get 'meh' => sub { 'meh' }; get '/hooked' => sub { $hooked }; get '/counter' => sub { $counter }; } my $test = Plack::Test->create( PoC->to_app ); subtest 'initial state' => sub { ok $test->request( GET '/meh' )->is_success; my $res = $test->request( GET '/hooked' ); ok $res->is_success; is $res->content, 'nope'; is $test->request( GET '/counter' )->content, '0'; }; subtest 'trigger hook' => sub { ok $test->request( GET '/' )->is_success; my $res = $test->request( GET '/hooked' ); ok $res->is_success; is $res->content, 'hooked'; is $test->request( GET '/counter' )->content, '1'; }; # GH #1018 - ensure hooks are called the correct number of times subtest 'execute hook counting' => sub { ok $test->request( GET '/' )->is_success; my $res = $test->request( GET '/hooked' ); ok $res->is_success; is $res->content, 'hooked'; is $test->request( GET '/counter' )->content, '2'; }; basic.t100644000765000024 120115267764222 15551 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/plugin2use strict; use warnings; use Test::More tests => 6; use Plack::Test; use HTTP::Request::Common; use lib 't/lib'; use poc; my $test = Plack::Test->create( poc->to_app ); note "poc root"; { my $res = $test->request( GET '/' ); ok $res->is_success; my $content = $res->content; like $content, qr/added by plugin/; like $content, qr/something:1/, 'config parameters are read'; like $content, qr/Bar loaded/, 'Plugin Bar has been loaded'; like $content, qr/bazbazbaz/, 'Foo has a copy of Bar'; } note "poc truncate"; { my $res = $test->request( GET '/truncate' ); like $res->content, qr'helladd'; } tokens.tt100644000765000024 27315267764222 15724 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/viewsperl_version: [% perl_version %] dancer_version: [% dancer_version %] settings.foo: [% settings.foo %] params.foo: [% params.foo %] session.foo [% session.foo %] vars.foo: [% vars.foo %] issues000755000765000024 015267764222 14104 5ustar00jasonstaff000000000000Dancer2-2.3.0/tgh-811.t100644000765000024 245115267764222 15340 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/issuesuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Cookies; use HTTP::Request::Common; eval { require Dancer2::Session::Cookie; 1 } or plan skip_all => 'Dancer2::Session::Cookie probably missing.'; { package App; use Dancer2; set engines => { session => { Cookie => { secret_key => 'you cannot buy happiness' } } }; set session => 'Cookie'; get '/set' => sub { session foo => 'bar'; redirect '/get'; }; get '/get' => sub { my $data = session->data; return to_json $data; }; } my $test = Plack::Test->create( App->to_app ); my $jar = HTTP::Cookies->new; my $url = 'http://localhost'; my $redir; subtest 'Creating a session' => sub { my $res = $test->request( GET "$url/set" ); ok( $res->is_redirect, 'Request causes redirect' ); ($redir) = $res->header('Location'); is( $redir, "/get", 'Redirects to correct url' ); $jar->extract_cookies($res); ok( $jar->as_string, 'Received a session cookie' ); }; subtest 'Retrieving a session' => sub { my $req = GET "$url/get"; $jar->add_cookie_header($req); my $res = $test->request($req); ok( $res->is_success, 'Successful request' ); is( $res->content, '{"foo":"bar"}', 'Correct response' ); }; done_testing; gh-596.t100644000765000024 66115267764222 15333 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/issuesuse strict; use warnings; use Test::More tests => 2; use Plack::Test; use HTTP::Request::Common; BEGIN { $ENV{'DANCER_NO_SERVER_TOKENS'} = 'foo' } { package App; use Dancer2; get '/' => sub { config->{'no_server_tokens'} }; } my $test = Plack::Test->create( App->to_app ); my $res = $test->request( GET '/' ); ok( $res->is_success, 'Successful' ); is( $res->content, 'foo', 'Correct server tokens configuration' ); gh-762.t100644000765000024 176015267764222 15347 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/issuesuse Test::More; use Plack::Test; use HTTP::Request::Common; { package FourOhFour; use Dancer2; set views => 't/issues/gh-762/views'; get '/error' => sub { send_error "oh my", 404; }; } my $fourohfour_app = FourOhFour->to_app; my $fourohfour_test = Plack::Test->create($fourohfour_app); subtest "/error" => sub { my $res = $fourohfour_test->request( GET '/error' ); is $res->code, 404, 'send_error sets the status to 404'; like $res->content, qr{Template selected}, 'Error message looks good'; like $res->content, qr{message: oh my}; like $res->content, qr{status: 404}; }; subtest 'FourOhFour with views template' => sub { my $path = "/middle/of/nowhere"; my $res = $fourohfour_test->request( GET $path ); is $res->code, 404, 'unknown route => 404'; like $res->content, qr{Template selected}, 'Error message looks good'; like $res->content, qr{message: $path}; like $res->content, qr{status: 404}; }; done_testing(); gh-936.t100644000765000024 122015267764222 15341 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/issuesuse warnings; use strict; use Test::More; use Plack::Test; use HTTP::Request::Common; { package TestApp; use Dancer2; set views => 't/issues/gh-936/views'; set error_template => 'error'; get '/does-not-exist' => sub { send_error "not found", 404; }; } my $test = Plack::Test->create( Dancer2->psgi_app ); for my $path ( qw{does-not-exist anywhere} ) { subtest "$path" => sub { my $res = $test->request( GET "/$path" ); is $res->code, 404, 'status is 404'; like $res->content, qr{CUSTOM ERROR TEMPLATE GOES HERE}, 'Error message looks good'; }; } done_testing(); gh-723.t100644000765000024 225515267764222 15344 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/issuesuse strict; use warnings; use Test::More tests => 4; use Plack::Test; use HTTP::Request::Common; { package App; use Dancer2; get '/' => sub {'OK'}; } { package App::Extended; use Dancer2; prefix '/test'; get '/' => sub {'Also OK'}; post '/' => sub { my $params = params; ::isa_ok( $params, 'HASH' ); ::is( $params->{'foo'}, 'bar', 'Got params' ); return $params->{'foo'}; }; } my $app = Dancer2->psgi_app; isa_ok( $app, 'CODE' ); my $test = Plack::Test->create($app); subtest 'GET /' => sub { plan tests => 2; my $res = $test->request( GET '/' ); is( $res->code, 200, 'Correct code' ); is( $res->content, 'OK', 'Correct content' ); }; subtest 'GET /test/' => sub { plan tests => 2; my $res = $test->request( GET '/test/' ); is( $res->code, 200, 'Correct code' ); is( $res->content, 'Also OK', 'Correct content' ); }; subtest 'Missing POST params' => sub { plan tests => 4; my $res = $test->request( POST '/test/', { foo => 'bar' }, ); is( $res->code, 200, 'Correct code' ); is( $res->content, 'bar', 'Correct content' ); }; gh-794.t100644000765000024 76315267764222 15336 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/issuesuse strict; use warnings; use Test::More tests => 2; use Plack::Test; use HTTP::Request::Common; { package App; use Dancer2; set serializer => 'JSON'; post '/' => sub { request->data }; } my $test = Plack::Test->create( App->to_app ); is( $test->request( POST '/', Content => '{"foo":42}' )->content, '{"foo":42}', 'Correct JSON content in POST', ); is( $test->request( POST '/', Content => 'invalid' )->code, 400, 'Failed to decode invalid content', ); gh-931.t100644000765000024 307615267764222 15347 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/issues# this test checks the order of parameters precedence # we run a few request to a route # first we check that the route parameters have precedence # then we check that the body parameters have the next # and finally, when others aren't available, query parameters use strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; { package App; ## no critic use Dancer2; sub query_ok { ::is( params('query')->{'var'}, 'QueryVar', 'Query variable exists', ); } sub body_ok { ::is( params('body')->{'var'}, 'BodyVar', 'Body variable exists', ); } sub route_ok { ::is( params('route')->{'var'}, 'RouteVar', 'Route variable exists', ); } post '/:var' => sub { query_ok(); body_ok(); route_ok(); ::is( params->{'var'}, 'RouteVar', 'Route variable wins', ); }; post '/' => sub { query_ok(); body_ok(); ::is( params->{'var'}, 'BodyVar', 'Body variable wins', ); }; } my $test = Plack::Test->create( App->to_app ); subtest 'Route takes precedence over all other parameters' => sub { $test->request( POST '/RouteVar?var=QueryVar', [ var => 'BodyVar' ] ); }; subtest 'When route parameters not available, POST takes precedence' => sub { $test->request( POST '/?var=QueryVar', [ var => 'BodyVar' ] ); }; done_testing(); gh-797.t100644000765000024 237615267764222 15363 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/issuesuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; use JSON::MaybeXS; { package App; use Dancer2; set serializer => 'JSON'; post '/' => sub { my $post_params = params('body'); # should work even with empty post body my $foo = $post_params->{'foo'}; return { foo => $foo }; }; } my $test = Plack::Test->create( App->to_app ); my %headers; subtest 'Basic response failing' => sub { TODO: { local $TODO = '500 when deserializing bad input'; my $res = $test->request( POST '/', { foo => 'bar' }, %headers ); is( $res->code, 500, '[POST /] Failed when sending regular params' ); } }; subtest 'Basic response' => sub { my $res = $test->request( POST '/', %headers, Content => encode_json { foo => 'bar' } ); is( $res->code, 200, '[POST /] Correct response code' ); my $response_data = decode_json( $res->decoded_content ); is($response_data->{foo}, 'bar', "[POST /] Correct response data"); }; subtest 'Empty POST' => sub { my $res = $test->request( POST '/', {}, %headers ); is( $res->code, 200, '[POST /] Correct response code with empty post body', ); }; done_testing(); gh-634.t100644000765000024 500315267764222 15337 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/issuesuse strict; use warnings; use Test::More tests=> 3; use File::Temp qw/tempdir/; use Path::Tiny qw< path >; my $log_dir = tempdir( CLEANUP => 1 ); sub config_location { my ($app) = @_; my %config_readers = map { $_->{name} => $_ } @{ $app->config_reader->config_readers }; return $config_readers{ 'Config::Any' }->config_location; } { package LogDirSpecified; use Dancer2; set engines => { logger => { File => { log_dir => $log_dir, file_name => 'test_log.log', } } }; set logger => 'file'; } { package NonExistLogDirSpecified; use Dancer2; set engines => { logger => { File => { log_dir => "$log_dir/notexist", file_name => 'test_log.log', } } }; set logger => 'file'; } { package LogDirNotSpecified; use Dancer2; set logger => 'file'; } my $check_cb = sub { my ( $app, $dir, $file ) = @_; my $logger = $app->logger_engine; isa_ok( $logger, 'Dancer2::Logger::File' ); is( $logger->environment, $app->environment, 'Logger got correct environment', ); is( $logger->location, config_location( $app ), 'Logger got correct location', ); is( $logger->log_dir, $dir, 'Logger got correct log directory', ); is( $logger->file_name, $file, 'Logger got correct filename', ); is( $logger->log_file, path( $dir, $file )->stringify, 'Logger got correct log file', ); }; subtest 'test Logger::File with log_dir specified' => sub { plan tests => 6; my $app = [ grep { $_->name eq 'LogDirSpecified' } @{ Dancer2->runner->apps } ]->[0]; $check_cb->( $app, $log_dir, 'test_log.log' ); }; subtest 'test Logger::File with log_dir NOT specified' => sub { plan tests => 6; my $app = [ grep { $_->name eq 'LogDirNotSpecified' } @{ Dancer2->runner->apps } ]->[0]; $check_cb->( $app, path( config_location( $app ), 'logs' )->stringify, $app->environment . '.log', ); }; subtest 'test Logger::File with non-existent log_dir specified' => sub { plan tests => 6; my $app = [ grep { $_->name eq 'NonExistLogDirSpecified'} @{ Dancer2->runner->apps } ]->[0]; my $logger = $app->logger_engine; $check_cb->( $app, "$log_dir/notexist", 'test_log.log', ); }; gh-799.t100644000765000024 313415267764222 15356 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/issuesuse strict; use warnings; use Test::More tests => 1; use Test::Fatal; use Plack::Test; use HTTP::Request::Common; { package App; use Dancer2; set log => 'core'; set engines => { logger => { Capture => { log_format => '%{x-test}h %i' } }, }; set logger => 'Capture'; get '/' => sub { my $req = app->request; ::isa_ok( $req, 'Dancer2::Core::Request' ); my $logger = app->engine('logger'); ::isa_ok( $logger, 'Dancer2::Logger::Capture' ); ::can_ok( $logger, 'format_message' ); my $trap = $logger->trapper; ::isa_ok( $trap, 'Dancer2::Logger::Capture::Trap' ); my $msg = $trap->read; ::is_deeply( $msg, [ { level => 'core', message => 'looking for get /', formatted => "- 1\n", }, { level => 'core', message => 'Entering hook core.app.before_request', formatted => "- 1\n", }, ], 'Messages logged successfully', ); ::can_ok( $logger, 'format_message' ); my $fmt_str = $logger->format_message( $msg->[0]{'debug'}, $msg->[0]{'message'} ); ::is( $fmt_str, "- 1\n", 'Correct formatted message created' ); return; }; } my $test = Plack::Test->create( App->to_app ); subtest 'Logger can access request' => sub { my $res = $test->request( GET '/' ); ok( $res->is_success, 'Successful request' ); }; gh-944.t100644000765000024 202115267764222 15340 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/issuesuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; { package RouteContentTest; ## no critic use Dancer2; set serializer => 'JSON'; hook before => sub { return if request->path eq '/content'; response->content({ foo => 'bar' }); response->halt; }; get '/' => sub {1}; get '/content' => sub { response->content({ foo => 'bar' }); return 'this is ignored'; }; } my $test = Plack::Test->create( RouteContentTest->to_app ); subtest "response set in before hook" => sub { my $res = $test->request( GET '/' ); ok( $res->is_success, 'Successful request' ); is( $res->content, '{"foo":"bar"}', 'Correct content' ); }; subtest "response content set in route" => sub { my $res = $test->request( GET '/content' ); ok( $res->is_success, 'Successful request' ); isnt( $res->content, 'this is ignored', 'route return value ignored' ); is( $res->content, '{"foo":"bar"}', 'Correct content' ); }; done_testing(); gh-730.t100644000765000024 255315267764222 15343 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/issuesuse strict; use warnings; use Test::More tests => 3; use Plack::Test; use HTTP::Request::Common; { package App; use Dancer2; get '/' => sub { request->is_behind_proxy }; } my $app = App->to_app; isa_ok( $app, 'CODE' ); my $test = Plack::Test->create($app); subtest 'Runner config' => sub { plan tests => 5; is( Dancer2->runner->config->{'behind_proxy'}, 0, 'No default behind_proxy', ); is( scalar @{ Dancer2->runner->apps }, 1, 'Single app registered', ); isa_ok( Dancer2->runner->apps->[0], 'Dancer2::Core::App', 'Correct app registered', ); is( Dancer2->runner->apps->[0]->setting('behind_proxy'), 0, 'behind_proxy not defined by default in an app', ); Dancer2->runner->apps->[0]->config->{'behind_proxy'} = 1; is( Dancer2->runner->apps->[0]->setting('behind_proxy'), 1, 'Set behind_proxy locally in the app to one', ); }; subtest 'Using App-level settings' => sub { plan tests => 3; is( Dancer2->runner->config->{'behind_proxy'}, 0, 'Runner\'s behind_proxy is still the default', ); my $res = $test->request( GET '/' ); is( $res->code, 200, '[GET /] Correct code' ); is( $res->content, '1', '[GET /] Local value achieved' ); }; send_file.t100644000765000024 1162515267764222 15655 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/dsluse strict; use warnings; use utf8; use Encode 'encode_utf8'; use Test::More; use Plack::Test; use HTTP::Request::Common; use Path::Tiny (); use Ref::Util qw; { package StaticContent; use Dancer2; use Encode 'encode_utf8'; set views => 't/corpus/static'; set public_dir => 't/corpus/static'; get '/' => sub { send_file 'index.html'; }; get '/illegal' => sub { send_file '../index.html'; }; prefix '/some' => sub { get '/image' => sub { send_file '1x1.png'; return "send_file returns; this content is ignored"; }; }; get '/stringref' => sub { my $string = encode_utf8("This is əɯosə�� an test string"); send_file( \$string ); }; get '/filehandle' => sub { open my $fh, "<:raw", __FILE__; send_file( $fh, content_type => 'text/plain', charset => 'utf-8' ); }; get '/check_content_type' => sub { my $file = Path::Tiny::path(__FILE__)->absolute->stringify; send_file($file, content_type => 'image/png', system_path => 1); }; get '/no_streaming' => sub { my $file = Path::Tiny::path(__FILE__)->absolute->stringify; send_file( $file, system_path => 1, streaming => 0 ); }; get '/options_streaming' => sub { my $file = Path::Tiny::path(__FILE__)->absolute->stringify; send_file( $file, system_path => 1, streaming => 1 ); }; get '/content_disposition/attachment' => sub { send_file('1x1.png', filename => '1x1.png'); }; get '/content_disposition/inline' => sub { send_file('1x1.png', filename => '1x1.png', content_disposition => 'inline'); }; } my $app = StaticContent->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; subtest "Text content" => sub { my $r = $cb->( GET '/' ); is( $r->code, 200, 'send_file sets the status to 200' ); my $charset = $r->headers->content_type_charset; is( $charset, 'UTF-8', 'Text content type has default charset' ); my $test_string = encode_utf8('áéíóú'); like( $r->content, qr{$test_string}, 'Text content contains UTF-8 characters', ); }; subtest "Binary content" => sub { my $r = $cb->( GET '/some/image' ); is( $r->code, 200, 'send_file sets the status to 200 (binary content)' ); unlike( $r->content, qr/send_file returns/, "send_file returns immediately with content"); is( $r->header( 'Content-Type' ), 'image/png', 'correct content_type in response' ); }; subtest "string refs" => sub { my $r = $cb->( GET '/stringref' ); is( $r->code, 200, 'send_file set status to 200 (string ref)'); like( $r->content, qr{test string}, 'stringref content' ); }; subtest "filehandles" => sub { my $r = $cb->( GET '/filehandle' ); is( $r->code, 200, 'send_file set status to 200 (filehandle)'); is( $r->content_type, 'text/plain', 'expected content_type'); is( $r->content_type_charset, 'UTF-8', 'expected charset'); like( $r->content, qr{package StaticContent}, 'filehandle content' ); }; subtest "no streaming" => sub { my $r = $cb->( GET '/no_streaming' ); is( $r->code, 200, 'send_file set status to 200 (no streaming)'); like( $r->content, qr{package StaticContent}, 'no streaming - content' ); }; subtest "options streaming" => sub { my $r = $cb->( GET '/options_streaming' ); is( $r->code, 200, 'send_file set status to 200 (options streaming)'); like( $r->content, qr{package StaticContent}, 'options streaming - content' ); }; subtest 'send_file returns correct content type' => sub { my $r = $cb->( GET '/check_content_type' ); ok($r->is_success, 'send_file returns success'); is($r->content_type, 'image/png', 'send_file returns correct content_type'); }; subtest 'Content-Disposition defaults to "attachment"' => sub { my $r = $cb->( GET '/content_disposition/attachment' ); ok($r->is_success, 'send_file returns success'); is($r->header('Content-Disposition'), 'attachment; filename="1x1.png"', 'send_file returns correct attachment Content-Disposition'); }; subtest 'Content-Disposition supports "inline"' => sub { my $r = $cb->( GET '/content_disposition/inline' ); ok($r->is_success, 'send_file returns success'); is($r->header('Content-Disposition'), 'inline; filename="1x1.png"', 'send_file returns correct inline Content-Disposition'); }; subtest "Illegal path" => sub { my $r = $cb->( GET '/illegal' ); is( $r->code, 403, 'Illegal path returns 403' ); is( $r->content, 'Forbidden', 'Text content contains UTF-8 characters', ); }; }; done_testing; CODE_OF_CONDUCT.md100644000765000024 655215267764222 15275 0ustar00jasonstaff000000000000Dancer2-2.3.0# Overview This document describes various policies (most notably, the standards of conduct) for the Dancer core developers and broad community. This is what we expect from our community and ourselves and these are the standards of behavior we set forth in order to make sure the community remains a safe space for all of its members, without exception. # Standards of Conduct These standards apply anywhere the community comes together as a group. This includes, but is not limited to, the Dancer IRC channel, the Dancer mailing list, Dancer hackathons, and Dancer conferences. - Always be civil. - Heed the moderators. - Abuse is not tolerated. Civility is simple: stick to the facts while avoiding demeaning remarks and sarcasm. It is not enough to be factual. You must also be civil. Responding in kind to incivility is not acceptable. If the list moderators tell you that you are not being civil, carefully consider how your words have appeared before responding in any way. You may protest, but repeated protest in the face of a repeatedly reaffirmed decision is not acceptable. Unacceptable behavior will result in a public and clearly identified warning. Repeated unacceptable behavior will result in removal from the mailing list and revocation of any commit bit. The first removal is for one month. Subsequent removals will double in length. After six months with no warning, a user's ban length is reset. Removals, like warnings, are public. The list of moderators consists of all active core developers. This includes, in alphabetical order, Alberto Simões, David Precious, Jason Crome, Mickey Nasriachi, Peter Mottram, Russell Jenkins, Sawyer X, Stefan Hornburg (Racke), and Yanick Champoux. This list might additionally grow to active members of the community who have stepped up to help handle abusive behavior. If this should happen, this document would be updated to include their names. Additionally, it's important to understand the self-regulating nature we foster at the Dancer community. This means anyone and everyone in the community - in the channel, on the list, at an event - has the ability to call out unacceptable behavior and incivility to others in the community. Moderators are responsible for issuing warnings and take disciplinary actions, but anyone may - and is encouraged - to publicly make note of unacceptable treatment of others. As a core principle, abuse is never tolerated. One cannot berate, insult, debase, deride, put down, or vilify anyone, or act towards anyone in a way intending to hurt them. The community specifically considers as abuse any attempts to otherize anyone by any individual characteristic, including, but not limited to, their technical skill, knowledge or by their age, colour, disability, gender, language, national or social origin, political or other opinion, race, religion, sex, or sexual orientation. The community aims to maintain a safe space for everyone, in any forum it has. If you ever feel this core principle has been compromised, you are strongly urged to contact a moderator. We are always here. Remember, this is **your** community, as much as it is anyone else's. # CREDITS This policy has been adopted and adapted from the policy available for the Perl language development, provided by **p5p** (the Perl 5 Porters). The original inspiration policy document can be read at [perlpolicy](https://metacpan.org/pod/perlpolicy). charset_server.t100644000765000024 241215267764222 16134 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse Test::More; use strict; use warnings; use Encode; use utf8; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; { package App; use Dancer2; get '/name/:name' => sub { "Your name: " . params->{name}; }; post '/name' => sub { "Your name: " . params->{name}; }; get '/unicode' => sub { "cyrillic shcha \x{0429}",; }; get '/symbols' => sub { '⚒ ⚓ ⚔ ⚕ ⚖ ⚗ ⚘ ⚙'; }; set charset => 'utf-8'; } my $app = Dancer2->psgi_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; my $res = $cb->( POST "/name", [ name => 'vasya'] ); is $res->content_type, 'text/html'; ok $res->content_type_charset ; # we always have charset if the setting is set is $res->content, 'Your name: vasya'; $res = $cb->( GET "/unicode" ); is $res->content_type, 'text/html'; is $res->content_type_charset, 'UTF-8'; is $res->content, Encode::encode( 'utf-8', "cyrillic shcha \x{0429}" ); $res = $cb->( GET "/symbols" ); is $res->content_type, 'text/html'; is $res->content_type_charset, 'UTF-8'; is $res->content, Encode::encode( 'utf-8', "⚒ ⚓ ⚔ ⚕ ⚖ ⚗ ⚘ ⚙" ); }; done_testing(); logger_console.t100644000765000024 264415267764222 16125 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Capture::Tiny 0.12 'capture_stderr'; use Dancer2::Logger::Console; my $file = __FILE__; my $l = Dancer2::Logger::Console->new( app_name => 'test', log_level => 'core' ); for my $level (qw{core debug info warning error}) { my $stderr = capture_stderr { $l->$level("$level") }; # We are dealing directly with the logger, not through the DSL. # Skipping 5 stack frames is likely to point to somewhere outside # this test; however Capture::Tiny adds in several call frames # (see below) to capture the output, giving a reasonable caller # to test for like $stderr, qr{$level in \Q$file\E l[.] 15}, "$level message sent"; } done_testing; __END__ # Stack frames involved where Role::Logger executes caller(5): # Dancer2::Core::Role::Logger::format_message(Dancer2::Logger::Console=HASH(0x7f8e41029c60), "error", "error") called at lib/Dancer2/Logger/Console.pm line 10 # Dancer2::Logger::Console::log(Dancer2::Logger::Console=HASH(0x7f8e41029c60), "error", "error") called at lib/Dancer2/Core/Role/Logger.pm line 183 # Dancer2::Core::Role::Logger::error(Dancer2::Logger::Console=HASH(0x7f8e41029c60), "error") called at t/logger_console.t line 12 # main::__ANON__() called at Capture/Tiny.pm line 369 # eval {...} called at Capture/Tiny.pm line 369 # Capture::Tiny::_capture_tee(0, 1, 0, 0, CODE(0x7f8e418181e0)) called at t/logger_console.t line 12 request_upload.t100644000765000024 1537215267764222 16202 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings FATAL => 'all'; use Test::More; use Test::Fatal; use Dancer2::Core::Request; use Carp; use File::Temp 0.22; use Path::Tiny qw< path >; use Encode qw(encode_utf8); diag "If you want extra speed, install URL::Encode::XS" if !$Dancer2::Core::Request::XS_URL_DECODE; diag "If you want extra speed, install CGI::Deurl::XS" if !$Dancer2::Core::Request::XS_PARSE_QUERY_STRING; sub test_path { my ( $file, $dir ) = @_; is path($file)->parent, $dir, "dir of $file is $dir"; } sub run_test { my $filename = "some_\x{1A9}_file.txt"; my $content = qq{------BOUNDARY Content-Disposition: form-data; name="test_upload_file"; filename="$filename" Content-Type: text/plain SHOGUN ------BOUNDARY Content-Disposition: form-data; name="test_upload_file"; filename="yappo2.txt" Content-Type: text/plain SHOGUN2 ------BOUNDARY Content-Disposition: form-data; name="test_upload_file3"; filename="yappo3.txt" Content-Type: text/plain SHOGUN3 ------BOUNDARY Content-Disposition: form-data; name="test_upload_file4"; filename="yappo4.txt" Content-Type: text/plain SHOGUN4 ------BOUNDARY Content-Disposition: form-data; name="test_upload_file4"; filename="yappo5.txt" Content-Type: text/plain SHOGUN4 ------BOUNDARY Content-Disposition: form-data; name="test_upload_file6"; filename="yappo6.txt" Content-Type: text/plain SHOGUN6 ------BOUNDARY-- }; $content =~ s/\r\n/\n/g; $content =~ s/\n/\r\n/g; $content = encode_utf8($content); do { open my $in, '<', \$content; my $req = Dancer2::Core::Request->new( env => { 'psgi.input' => $in, CONTENT_LENGTH => length($content), CONTENT_TYPE => 'multipart/form-data; boundary=----BOUNDARY', REQUEST_METHOD => 'POST', SCRIPT_NAME => '/', SERVER_PORT => 80, } ); my @undef = $req->upload('undef'); is @undef, 0, 'non-existent upload as array is empty'; my $undef = $req->upload('undef'); is $undef, undef, '... and non-existent upload as scalar is undef'; my @uploads = $req->upload('test_upload_file'); like $uploads[0]->content, qr|^SHOGUN|, "content for first upload is ok, via 'upload'"; like $uploads[1]->content, qr|^SHOGUN|, "... content for second as well"; is $req->uploads->{'test_upload_file4'}[0]->content, 'SHOGUN4', "... content for other also good"; note "headers and decoded filename"; my $encoded_filename = encode_utf8($filename); is_deeply $uploads[0]->headers, { 'Content-Disposition' => qq[form-data; name="test_upload_file"; filename="$encoded_filename"], 'Content-Type' => 'text/plain', }; is $uploads[0]->filename, $filename; note "type"; is $uploads[0]->type, 'text/plain'; my $test_upload_file3 = $req->upload('test_upload_file3'); is $test_upload_file3->content, 'SHOGUN3', "content for upload #3 as a scalar is good, via req->upload"; my @test_upload_file6 = $req->upload('test_upload_file6'); is $test_upload_file6[0]->content, 'SHOGUN6', "content for upload #6 is good"; is $test_upload_file6[0]->content(':raw'), 'SHOGUN6'; my $upload = $req->upload('test_upload_file6'); isa_ok $upload, 'Dancer2::Core::Request::Upload'; is $upload->filename, 'yappo6.txt', 'filename is ok'; ok $upload->file_handle, 'file handle is defined'; is $req->params->{'test_upload_file6'}, 'yappo6.txt', "filename is accessible via params"; # copy_to, link_to my $dest_dir = File::Temp::tempdir( CLEANUP => 1, TMPDIR => 1 ); my $dest_file = path( $dest_dir, $upload->basename )->stringify; $upload->copy_to($dest_file); ok( ( -f $dest_file ), "file '$dest_file' has been copied" ); my $dest_file_link = path( $dest_dir, "hardlink" )->stringify; $upload->link_to($dest_file_link); ok( ( -f $dest_file_link ), "hardlink '$dest_file_link' has been created" ); # make sure cleanup is performed when the HTTP::Body object is purged my $file = $upload->tempname; ok( ( -f $file ), 'temp file exists while request object lives' ); # On Windows, files cannot be unlinked while open. Close all cached # file handles before destroying $req so that the temp dir cleanup # (which runs in File::Temp::Dir's DESTROY, triggered by freeing # $req's PSGI env) can succeed. if ( $^O eq 'MSWin32' ) { for my $up_or_list ( values %{ $req->uploads } ) { my @ups = ref $up_or_list eq 'ARRAY' ? @{$up_or_list} : $up_or_list; $_->{_fh} = undef for @ups; } } undef $req; SKIP: { skip "Win32 can't remove file/link while open due to deadlock", 1 if ( $^O eq 'MSWin32' ); ok( ( !-f $file ), 'temp file is removed when request object dies' ); } note "testing failing open for tempfile"; { # mocking open_file to make it fail my $upload_file_coderef; { no strict 'refs'; $upload_file_coderef = *{"Dancer2::Core::Request::Upload::file_handle"}{CODE}; no warnings 'redefine'; *{"Dancer2::Core::Request::Upload::file_handle"} = sub { croak "Can't open mocked-tempfile using mode '<'"; }; } $upload->{_fh} = undef; like( exception { $upload->file_handle }, qr{Can't open mocked-tempfile using mode '<'}, ); # unmock open_file { no strict 'refs'; no warnings 'redefine'; *{"Dancer2::Core::Request::Upload::file_handle"} = $upload_file_coderef; } } unlink($file) if ( $^O eq 'MSWin32' ); }; } note "Run test with XS_URL_DECODE" if $Dancer2::Core::Request::XS_URL_DECODE; note "Run test with XS_PARSE_QUERY_STRING" if $Dancer2::Core::Request::XS_PARSE_QUERY_STRING; run_test(); if ($Dancer2::Core::Request::XS_PARSE_QUERY_STRING) { note "Run test without XS_PARSE_QUERY_STRING"; $Dancer2::Core::Request::XS_PARSE_QUERY_STRING = 0; $Dancer2::Core::Request::_count = 0; run_test(); } if ($Dancer2::Core::Request::XS_URL_DECODE) { note "Run test without XS_URL_DECODE"; $Dancer2::Core::Request::XS_URL_DECODE = 0; $Dancer2::Core::Request::_count = 0; run_test(); } done_testing; session_object.t100644000765000024 271415267764222 16133 0ustar00jasonstaff000000000000Dancer2-2.3.0/t# session_object.t use strict; use warnings; use Test::More; use Test::Fatal; use Dancer2::Core::Session; use Dancer2::Session::Simple; my $ENGINE = Dancer2::Session::Simple->new; note "Cryptographically-strong session ID generation"; subtest 'session attributes' => sub { my $s1 = $ENGINE->create; my $id = $s1->id; ok defined($id), 'id is defined'; is(exception { $s1->id("new_$id") }, undef, 'id can be set'); is($s1->id, "new_$id", '... new value found for id'); my $s2 = $ENGINE->create; isnt($s1->id, $s2->id, "IDs are not the same"); }; my $count = 10_000; subtest "$count session IDs and no dups" => sub { my $seen = {}; my $iteration = 0; foreach my $i (1 .. $count) { my $s1 = $ENGINE->create; my $id = $s1->id; if (exists $seen->{$id}) { last; } $seen->{$id} = 1; $iteration++; } is $iteration, $count, "no duplicate ID after $count iterations (done $iteration)"; }; subtest 'validate_id' => sub { my $id = $ENGINE->generate_id; ok $ENGINE->validate_id($id), 'generated ID validates'; ok !$ENGINE->validate_id("$id\n"), 'trailing newline rejected'; ok !$ENGINE->validate_id("a\nb"), 'embedded newline rejected'; ok $ENGINE->validate_id('a' x 4096), 'maximum-length ID validates'; ok !$ENGINE->validate_id('x' x 4097), 'overly long ID rejected'; ok !$ENGINE->validate_id(undef), 'undef rejected'; }; done_testing; shared_engines.t100644000765000024 212215267764222 16071 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Cookies; use HTTP::Request::Common; { package App; # call stuff before next use() statement BEGIN { use Dancer2; set session => 'Simple'; engine('session')->{'__marker__'} = 1; } use lib '.'; use t::lib::Foo with => { session => engine('session') }; get '/main' => sub { session( 'test' => 42 ); }; } my $jar = HTTP::Cookies->new; my $url = 'http://localhost'; { my $test = Plack::Test->create( App->to_app ); my $res = $test->request( GET "$url/main" ); like $res->content, qr{42}, "session is set in main"; $jar->extract_cookies($res); ok( $jar->as_string, 'Got cookie' ); } { my $test = Plack::Test->create( t::lib::Foo->to_app ); my $req = GET "$url/in_foo"; $jar->add_cookie_header($req); my $res = $test->request($req); like $res->content, qr{42}, "session is set in foo"; } my $engine = t::lib::Foo->dsl->engine('session'); is $engine->{__marker__}, 1, "the session engine in subapp is the same"; done_testing; static_content.t100644000765000024 115515267764222 16141 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use utf8; use Test::More; use Plack::Test; use HTTP::Request::Common; { package PublicContent; use Dancer2; set public_dir => 't/corpus/static'; get '/' => sub { return 'Welcome Home' }; } my $test = Plack::Test->create( PublicContent->to_app ); subtest 'public content' => sub { my $res = $test->request( GET '/1x1.png' ); is $res->code, 200, "200 response"; my $last_modified = $res->header('Last-Modified'); $res = $test->request( GET '/1x1.png', 'If-Modified-Since' => $last_modified ); is $res->code, 304, "304 response"; }; done_testing(); session_config.t100644000765000024 421315267764222 16126 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Cookies; use HTTP::Request::Common; { package App; use Dancer2; setting( engines => { session => { Simple => { cookie_name => 'dancer.sid', cookie_path => '/foo', cookie_duration => '1 hour', cookie_same_site => 'Strict', is_http_only => 0, # will not show up in cookie }, }, } ); setting( session => 'Simple' ); get '/has_session' => sub { return app->has_session; }; get '/foo/set_session/*' => sub { my ($name) = splat; session name => $name; }; get '/foo/read_session' => sub { my $name = session('name') || ''; "name='$name'"; }; get '/foo/destroy_session' => sub { my $name = session('name') || ''; app->destroy_session; return "destroyed='$name'"; }; } my $test = Plack::Test->create( App->to_app ); my $url = 'http://localhost'; my $jar = HTTP::Cookies->new; subtest 'Set session' => sub { my $res = $test->request( GET "$url/foo/set_session/larry" ); ok( $res->is_success, '/foo/set_session/larry' ); $jar->extract_cookies($res); ok( $jar->as_string, 'session cookie set' ); my ( $expires, $domain, $path, $opts ); my $cookie = $jar->scan( sub { ( $expires, $domain, $path, $opts ) = @_[ 8, 4, 3, 10 ]; } ); my $httponly = $opts->{'HttpOnly'}; ok $expires - time > 3540, "cookie expiration is in future"; is $domain, 'localhost.local', "cookie domain set"; is $path, '/foo', "cookie path set"; is $httponly, undef, "cookie has not set HttpOnly"; is $opts->{SameSite}, 'Strict', "cookie has same site set to strict"; # read value back }; subtest 'Read session' => sub { my $req = GET "$url/foo/read_session"; $jar->add_cookie_header($req); my $res = $test->request($req); ok $res->is_success, "/foo/read_session"; like $res->content, qr/name='larry'/, "session value looks good"; }; done_testing; gh-1098.t100644000765000024 513715267764222 15434 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/issuesuse Test::More tests => 3; use Test::Fatal; use Dancer2::Core::Error; use Dancer2::Core::Response; use Dancer2::Serializer::JSON; use HTTP::Headers; use HTTP::Headers::Fast; use JSON::MaybeXS; subtest 'Core::Error serializer isa tests' => sub { plan tests => 5; is exception { Dancer2::Core::Error->new }, undef, "Error->new lived"; like exception { Dancer2::Core::Error->new(show_stacktrace => []) }, qr/Reference \Q[]\E did not pass type constraint "Bool"/i, "Error->new(show_stacktrace => []) died"; is exception { Dancer2::Core::Error->new(serializer => undef) }, undef, "Error->new(serializer => undef) lived"; is exception { Dancer2::Core::Error->new(serializer => Dancer2::Serializer::JSON->new) }, undef, "Error->new(serializer => Dancer2::Serializer::JSON->new) lived"; like exception { Dancer2::Core::Error->new(serializer => JSON->new) }, qr/ ( requires\sthat\sthe\sreference\sdoes\sDancer2::Core::Role::Serializer | did\snot\spass\stype\sconstraint ) /x, "Error->new(serializer => JSON->new) died"; }; subtest 'Core::Response headers isa tests' => sub { plan tests => 5; is exception { Dancer2::Core::Response->new }, undef, "Response->new lived"; is exception { Dancer2::Core::Response->new(headers => [Header => 'Content']) }, undef, "Response->new( headers => [ Header => 'Content' ] ) lived"; is exception { Dancer2::Core::Response->new(headers => HTTP::Headers->new) }, undef, "Response->new( headers => HTTP::Headers->new ) lived"; is exception { Dancer2::Core::Response->new(headers => HTTP::Headers::Fast->new) }, undef, "Response->new( headers => HTTP::Headers::Fast->new ) lived"; like exception { Dancer2::Core::Response->new(headers => JSON->new) }, qr/coercion.+failed.+not.+array/i, "Response->new( headers => JSON->new ) died"; }; subtest 'Core::Role::Logger log_level isa tests' => sub { plan tests => 1 + 6 + 1; { package TestLogger; use Moo; with 'Dancer2::Core::Role::Logger'; sub log { } } is exception { TestLogger->new }, undef, "Logger->new lived"; my @levels = qw/core debug info warn warning error/; foreach my $level (@levels) { is exception { TestLogger->new(log_level => $level) }, undef, "Logger->new(log_level => $level) lives"; } like exception { TestLogger->new(log_level => 'BadLevel') }, qr/Value "BadLevel" did not pass type constraint "Enum/, "Logger->new(log_level => 'BadLevel') died"; }; gh-1232.t100644000765000024 236315267764222 15420 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/issuesuse strict; use warnings; use Test::More tests => 1; use Plack::Test; use Plack::Builder; use Plack::Request; use HTTP::Request::Common; use Encode qw(encode_utf8); { package App; use Dancer2; # default, we're actually overriding this later set serializer => 'JSON'; # for now set logger => 'Capture'; post '/json' => sub { my $p = body_parameters; return [ map +( $_ => $p->get($_) ), sort $p->keys ]; }; } my $psgi = builder { # inline middleware FTW! # Create a Plack::Request object and parse body to tickle #1232 enable sub { my $app = shift; sub { my $req = Plack::Request->new($_[0])->body_parameters; return $app->($_[0]); } }; App->to_app; }; my $test = Plack::Test->create( $psgi ); subtest 'POST request with parameters' => sub { my $characters = encode_utf8("∑�"); my $res = $test->request( POST "/json", 'Content-Type' => 'application/json', 'Content' => qq!{ "foo": 1, "bar": 2, "baz": "$characters" }! ); is( $res->content, qq!["bar",2,"baz","$characters","foo",1]!, "Body parameters deserialized", ); }; done_testing();gh-1863.t100644000765000024 373215267764222 15433 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/issuesuse strict; use warnings; use Test::More tests => 7; use Dancer2::Serializer::JSON; # GH #1863: serializing must not disturb the entity it is given. It belongs to # the application, which may hold on to it and serialize it again -- so a # decoded key or value has to go into a new container, not be written back. # A hash key that needs decoding used to be deleted from the caller's hash and # added only to the copy being encoded, so the entry vanished from the app's # data after the first serialize. { my $cache = { "caf\xc3\xa9" => 'open', bar => 'shut' }; my $first = Dancer2::Serializer::JSON::to_json( $cache, { canonical => 1 } ); my $second = Dancer2::Serializer::JSON::to_json( $cache, { canonical => 1 } ); is( $second, $first, 'serializing twice gives the same JSON' ); is_deeply( [ sort keys %$cache ], [ sort "caf\xc3\xa9", 'bar' ], 'the non-ASCII key survives serialization', ); is( $cache->{"caf\xc3\xa9"}, 'open', 'and still has its value' ); } # Arrayrefs were decoded in place, converting the app's bytes to characters. { my $data = { tags => [ "caf\xc3\xa9", 'plain' ] }; Dancer2::Serializer::JSON::to_json($data); is( $data->{tags}[0], "caf\xc3\xa9", 'array element left as the caller had it' ); ok( !utf8::is_utf8( $data->{tags}[0] ), 'and not decoded in place' ); } # Nested containers must be left alone too, not just the top level. { my $data = { outer => [ { "caf\xc3\xa9" => [ "na\xc3\xafve" ] } ] }; Dancer2::Serializer::JSON::to_json($data); is_deeply( $data, { outer => [ { "caf\xc3\xa9" => [ "na\xc3\xafve" ] } ] }, 'nested structure is untouched', ); } # ...while the output itself still has everything decoded properly. is( Dancer2::Serializer::JSON::to_json( { "caf\xc3\xa9" => [ "na\xc3\xafve" ] }, { canonical => 1 } ), "{\"caf\xc3\xa9\":[\"na\xc3\xafve\"]}", 'keys and values are still decoded and re-encoded in the output', ); gh-1070.t100644000765000024 75015267764222 15376 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/issuesuse strict; use warnings; use Test::More tests => 2; use Plack::Test; use HTTP::Request::Common; { package App; use Dancer2; set show_stacktrace => 1; } my $test = Plack::Test->create( App->to_app ); my $content = $test->request( GET '/nonexistent_pathcrazy' )->content; like $content, qr{/nonexistent_path<strong>crazy</strong>}, "Escaped message"; unlike $content, qr{/nonexistent_pathcrazy}, "No unescaped message"; gh-1564.t100644000765000024 651415267764222 15432 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/issues#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 2; use Plack::Test; use Plack::Builder; use HTTP::Request::Common; { package App; use Dancer2; get '/root' => sub { redirect '/'; }; get '/leading_slash' => sub { redirect '/expected'; }; get '/relative' => sub { redirect 'expected'; }; get '/relative/one-dot' => sub { redirect './expected'; }; get '/relative/two-dots' => sub { redirect '../expected'; }; get '/absolute' => sub { redirect 'http://expected' }; get '/schemeless' => sub { redirect '//expected' }; } my $app = builder { mount '/' => App->to_app(); mount '/other-mount-point' => App->to_app(); }; my @leading_slash_tests = ( # the expected result (last element) needs the mount prepended in the actual tests [ 'Relative with leading slash', '/leading_slash', '/expected' ], [ 'Relative root', '/root', '/' ], # Test for issue #1611 ); my @common_tests = ( [ 'Relative redirect', '/relative', 'expected' ], [ 'Relative redirect with ./', '/relative/one-dot', './expected' ], [ 'Relative redirect with ../', '/relative/two-dots', '../expected' ], [ 'Absolute URL redirect', '/absolute', 'http://expected' ], [ 'Schemeless redirect', '/schemeless', '//expected' ], ); subtest 'Testing app mounted to /' => sub { my $mount = ''; test_psgi $app, sub { my $cb = shift; for my $test (@leading_slash_tests) { my ($name, $url, $expected) = @$test; subtest $name => sub { my $res = $cb->( GET $url ); is($res->code, 302, 'Correct code'); is( $res->headers->header('Location'), $mount . $expected, 'Correct location header' ); } } foreach my $test (@common_tests) { my ($name, $url, $expected) = @$test; subtest $name => sub { my $res = $cb->( GET $url ); is($res->code, 302, 'Correct code'); is( $res->headers->header('Location'), $expected, 'Correct location header' ); } } }; }; subtest 'Testing app mounted to /other-mount-point' => sub { my $mount = '/other-mount-point'; test_psgi $app, sub { my $cb = shift; for my $test (@leading_slash_tests) { my ($name, $url, $expected) = @$test; $url = "$mount$url"; subtest $name => sub { my $res = $cb->( GET $url ); is($res->code, 302, 'Correct code'); is( $res->headers->header('Location'), "$mount$expected", # scriptname prepended to redirect path 'Correct location header' ); } } foreach my $test (@common_tests) { my ($name, $url, $expected) = @$test; $url = "$mount$url"; subtest $name => sub { my $res = $cb->( GET $url ); is($res->code, 302, 'Correct code'); is( $res->headers->header('Location'), $expected, 'Correct location header' ); } } }; }; note 'DONE!'; gh-1861.t100644000765000024 402515267764222 15425 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/issuesuse strict; use warnings; use Test::More tests => 8; use B (); use Dancer2::Serializer::JSON; # GH #1861: the UTF-8 scan in the serializer used to run its regex against the # values themselves. Matching a regex against a number gives that SV a string # form alongside its numeric one, and a JSON backend that picks number-vs-string # from the SV's flags (JSON::XS, and JSON::PP before 2.97) then emits 42 as # "42". Most installs run Cpanel::JSON::XS, which tolerates such a value, so the # breakage only showed up elsewhere -- assert on the flags rather than on # encoded output, so this test fails on any backend. sub is_pok { my ($value) = @_; return !!( B::svref_2object( \$value )->FLAGS & B::SVp_POK ); } ok( !is_pok(42), 'sanity: a fresh integer has no string form' ); my $hash = Dancer2::Serializer::JSON::_ensure_characters( { n => 42 } ); ok( !is_pok( $hash->{n} ), 'integer in a hash keeps no string form' ); my $array = Dancer2::Serializer::JSON::_ensure_characters( [ 42 ] ); ok( !is_pok( $array->[0] ), 'integer in an array keeps no string form' ); my $float = Dancer2::Serializer::JSON::_ensure_characters( { n => 1.5 } ); ok( !is_pok( $float->{n} ), 'float keeps no string form' ); my $nested = Dancer2::Serializer::JSON::_ensure_characters( { a => [ { b => 42 } ] } ); ok( !is_pok( $nested->{a}[0]{b} ), 'integer nested in a structure keeps no string form' ); # The caller's own data must not be left stringified either: _ensure_characters # writes back into arrayrefs in place, so a tainted value would outlive the call. my $mine = { list => [ 7 ] }; Dancer2::Serializer::JSON::to_json($mine); ok( !is_pok( $mine->{list}[0] ), 'serializing does not stringify the caller\'s data' ); # ...while the decoding the scan exists for still happens. is( Dancer2::Serializer::JSON::to_json( { n => 42 } ), '{"n":42}', 'integers encode unquoted', ); is( Dancer2::Serializer::JSON::to_json( { t => "D\x{c3}\x{bc}sseldorf" } ), "{\"t\":\"D\x{c3}\x{bc}sseldorf\"}", 'UTF-8 bytes are still decoded and re-encoded', ); gh-1289.t100644000765000024 137015267764222 15431 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/issuesuse strict; use warnings; use Test::More tests => 1; use Plack::Test; use HTTP::Request::Common; { package App; use Dancer2; post '/forwards' => sub { forward('/still_contains_uploads'); }; any '/still_contains_uploads' => sub { my $upload = request->upload('testupload'); if ($upload) { return; } die 'failed'; }; } { my $test = Plack::Test->create(App->to_app); my $response = $test->request( POST( '/forwards', Content_Type => 'form-data', Content => [testupload => [undef, 'testupload', Content => "testcontent"]] ) ); is($response->code, 200, 'Uploads survive forward'); } done_testing(); parameters.t100644000765000024 2377315267764222 16077 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/dsluse strict; use warnings; use utf8; use Test::More; use Plack::Test; use Encode 'encode_utf8'; use HTTP::Request::Common; subtest 'Query parameters' => sub { { package App::Basic; ## no critic use Dancer2; get '/' => sub { my $params = query_parameters; ::isa_ok( $params, 'Hash::MultiValue', 'parameters keyword', ); ::is( $params->get('foo'), 'bar', 'Got single value' ); ::is( $params->get('bar'), 'quux', 'Got single value from multi key', ); ::is_deeply( [ $params->get_all('bar') ], ['baz', 'quux'], 'Got multi value from multi key', ); ::is( $params->get('baz'), 'הלו', 'HMV interface returns encoded values', ); ::is( params->{'baz'}, 'הלו', 'Regular interface returns encoded values' ); }; } my $app = Plack::Test->create( App::Basic->to_app ); my $res = $app->request( GET '/?foo=bar&bar=baz&bar=quux&baz=הלו' ); ok( $res->is_success, 'Successful request' ); }; subtest 'Body parameters' => sub { { package App::Body; ## no critic use Dancer2; post '/' => sub { my $params = body_parameters; ::isa_ok( $params, 'Hash::MultiValue', 'parameters keyword', ); ::is( $params->get('foo'), 'bar', 'Got single value' ); ::is( $params->get('bar'), 'quux', 'Got single value from multi key', ); my $z = [ $params->get_all('bar') ]; ::is_deeply( [ $params->get_all('bar') ], ['baz', 'quux'], 'Got multi value from multi key', ); ::is( $params->get('baz'), 'הלו', 'HMV interface returns encoded values', ); ::is( params->{'baz'}, 'הלו', 'Regular interface returns encoded values' ); }; } my $app = Plack::Test->create( App::Body->to_app ); my $res = $app->request( POST '/', Content => [foo => 'bar', bar => 'baz', bar => 'quux', baz => 'הלו'] ); ok( $res->is_success, 'Successful request' ); }; subtest 'Body parameters with serialized data' => sub { { package App::Body::JSON; ## no critic use Dancer2; set serializer => 'JSON'; post '/' => sub { my $params = body_parameters; ::isa_ok( $params, 'Hash::MultiValue', 'parameters keyword', ); ::is( $params->get('foo'), 'bar', 'Got single value' ); ::is( $params->get('bar'), 'quux', 'Got single value from multi key', ); my $z = [ $params->get_all('bar') ]; ::is_deeply( [ $params->get_all('bar') ], ['baz', 'quux'], 'Got multi value from multi key', ); ::is( $params->get('baz'), 'הלו', 'HMV interface returns encoded values', ); ::is( params->{'baz'}, 'הלו', 'Regular interface returns encoded values' ); return { ok => 1 }; }; } my $app = Plack::Test->create( App::Body::JSON->to_app ); my $baz = encode_utf8('הלו'); my $res = $app->request( POST '/', Content => qq{{"foo":"bar","bar":["baz","quux"],"baz":"$baz"}} ); ok( $res->is_success, 'Successful request' ); }; subtest 'Route parameters' => sub { { package App::Route; ## no critic use Dancer2; get '/:foo' => sub { my $params = route_parameters; ::isa_ok( $params, 'Hash::MultiValue', 'parameters keyword', ); ::is( $params->get('foo'), 'bar', 'Got keyed value' ); }; get '/:name/:value' => sub { my $params = route_parameters; ::isa_ok( $params, 'Hash::MultiValue', 'parameters keyword returns Hash::MultiValue object', ); ::is( $params->get('name'), 'foo', 'Got first value' ); ::is( $params->get('value'), 'הלו', 'Got second value' ); ::is( params->{'value'}, 'הלו', 'Regular interface returns encoded values' ); }; } my $app = Plack::Test->create( App::Route->to_app ); { my $res = $app->request( GET '/bar' ); ok( $res->is_success, 'Successful request' ); } { my $res = $app->request( GET '/foo/הלו' ); ok( $res->is_success, 'Successful request' ); } }; subtest 'Splat and megasplat route parameters' => sub { { package App::Route::Splat; ## no critic use Dancer2; get '/*' => sub { my $params = route_parameters; ::isa_ok( $params, 'Hash::MultiValue', 'parameters keyword', ); ::is_deeply( { %{$params} }, {}, 'All route parameters are empty', ); ::is_deeply( [ splat ], [ 'foo' ], 'Got splat values', ); }; get '/*/*' => sub { my $params = route_parameters; ::isa_ok( $params, 'Hash::MultiValue', 'parameters keyword returns Hash::MultiValue object', ); ::is_deeply( { %{$params} }, {}, 'All route parameters are empty', ); ::is_deeply( [ splat ], [ qw ], 'Got splat values', ); }; # /foo/bar/baz/quux/quuks get '/*/*/*/**' => sub { my $params = route_parameters; ::isa_ok( $params, 'Hash::MultiValue', 'parameters keyword returns Hash::MultiValue object', ); ::is_deeply( { %{$params} }, {}, 'All route parameters are empty', ); ::is_deeply( [ splat ], [ qw, [ qw ] ], 'Got splat values', ); }; # /foo/bar/baz get '/*/:foo/**' => sub { my $params = route_parameters; ::isa_ok( $params, 'Hash::MultiValue', 'parameters keyword returns Hash::MultiValue object', ); ::is( $params->get('foo'), 'bar', 'Correct route parameter' ); ::is_deeply( [ splat ], [ 'foo', ['baz', ''] ], 'Got splat values', ); }; } my $app = Plack::Test->create( App::Route::Splat->to_app ); { my $res = $app->request( GET '/foo' ); ok( $res->is_success, 'Successful request' ); } { my $res = $app->request( GET '/foo/bar' ); ok( $res->is_success, 'Successful request' ); } { my $res = $app->request( GET '/foo/bar/baz/quux/quuks' ); ok( $res->is_success, 'Successful request' ); } { my $res = $app->request( GET '/foo/bar/baz/' ); ok( $res->is_success, 'Successful request' ); } }; subtest 'Captured route parameters' => sub { { package App::Route::Capture; ## no critic use Dancer2; get qr{^/foo/([^/]+)$} => sub { my $params = route_parameters; ::isa_ok( $params, 'Hash::MultiValue', 'parameters keyword', ); ::is_deeply( { %{$params} }, {}, 'All route parameters are empty', ); ::is_deeply( [ splat ], ['bar'], 'Correct splat values', ); ::is_deeply( captures(), +{}, 'capture values are empty', ); }; } my $app = Plack::Test->create( App::Route::Capture->to_app ); { my $res = $app->request( GET '/foo/bar' ); ok( $res->is_success, 'Successful request' ); } }; subtest 'Named captured route parameters' => sub { { package App::Route::NamedCapture; ## no critic use Dancer2; my $re = '^/bar/(?[^/]+)$'; get qr{$re} => sub { my $params = route_parameters; ::isa_ok( $params, 'Hash::MultiValue', 'parameters keyword', ); ::is_deeply( { %{$params} }, {}, 'All route parameters are empty', ); ::is_deeply( [ splat ], [], 'splat values are empty', ); ::is_deeply( captures(), { baz => 'quux' }, 'Correct capture values', ); }; } my $app = Plack::Test->create( App::Route::NamedCapture->to_app ); { my $res = $app->request( GET '/bar/quux' ); ok( $res->is_success, 'Successful request' ); }; }; done_testing(); Dancer2000755000765000024 015267764222 14352 5ustar00jasonstaff000000000000Dancer2-2.3.0/libCLI.pm100644000765000024 304715267764222 15463 0ustar00jasonstaff000000000000Dancer2-2.3.0/lib/Dancer2package Dancer2::CLI; # ABSTRACT: Dancer2 CLI application $Dancer2::CLI::VERSION = '2.3.0'; use Moo; use CLI::Osprey; use Path::Tiny; use File::Share 'dist_dir'; use Module::Runtime 'use_module'; subcommand gen => 'Dancer2::CLI::Gen'; subcommand 'gen-app' => 'Dancer2::CLI::Gen'; # Could have done this one inline, but wanted to remain consistent # across subcommands. subcommand version => 'Dancer2::CLI::Version'; # Thinking ahead, these might be useful in future subcommands has _dancer2_version => ( is => 'lazy', builder => sub { use_module( 'Dancer2' )->VERSION }, ); has _dist_dir => ( is => 'lazy', builder => sub{ dist_dir('Dancer2') }, ); sub run { my $self = shift; return $self->osprey_usage; } sub _get_app_path { my ( $self, $path, $appname ) = @_; return path( $path, $self->_get_dashed_name( $appname )); } sub _get_app_file { my ( $self, $appname ) = @_; $appname =~ s{::}{/}g; return path( 'lib', "$appname.pm" ); } sub _get_perl_interpreter { return -r '/usr/bin/env' ? '#!/usr/bin/env perl' : "#!$^X"; } sub _get_dashed_name { my ( $self, $name ) = @_; $name =~ s{::}{-}g; return $name; } 1; __END__ =pod =encoding UTF-8 =head1 NAME Dancer2::CLI - Dancer2 CLI application =head1 VERSION version 2.3.0 =head1 AUTHOR Dancer Core Developers =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2026 by Alexis Sukrieh. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut author-no-tabs.t100644000765000024 3375615267764222 16017 0ustar00jasonstaff000000000000Dancer2-2.3.0/t BEGIN { unless ($ENV{AUTHOR_TESTING}) { print qq{1..0 # SKIP these tests are for testing by the author\n}; exit } } use strict; use warnings; # this test was generated with Dist::Zilla::Plugin::Test::NoTabs 0.15 use Test::More 0.88; use Test::NoTabs; my @files = ( 'lib/Dancer2.pm', 'lib/Dancer2/CLI.pm', 'lib/Dancer2/CLI/Gen.pm', 'lib/Dancer2/CLI/Version.pm', 'lib/Dancer2/ConfigReader.pm', 'lib/Dancer2/ConfigReader/Config/Any.pm', 'lib/Dancer2/ConfigUtils.pm', 'lib/Dancer2/Core.pm', 'lib/Dancer2/Core/App.pm', 'lib/Dancer2/Core/Cookie.pm', 'lib/Dancer2/Core/DSL.pm', 'lib/Dancer2/Core/Dispatcher.pm', 'lib/Dancer2/Core/Error.pm', 'lib/Dancer2/Core/Factory.pm', 'lib/Dancer2/Core/HTTP.pm', 'lib/Dancer2/Core/Hook.pm', 'lib/Dancer2/Core/MIME.pm', 'lib/Dancer2/Core/Request.pm', 'lib/Dancer2/Core/Request/Upload.pm', 'lib/Dancer2/Core/Response.pm', 'lib/Dancer2/Core/Response/Delayed.pm', 'lib/Dancer2/Core/Role/ConfigReader.pm', 'lib/Dancer2/Core/Role/DSL.pm', 'lib/Dancer2/Core/Role/Engine.pm', 'lib/Dancer2/Core/Role/Handler.pm', 'lib/Dancer2/Core/Role/HasConfig.pm', 'lib/Dancer2/Core/Role/HasEnvironment.pm', 'lib/Dancer2/Core/Role/HasLocation.pm', 'lib/Dancer2/Core/Role/Hookable.pm', 'lib/Dancer2/Core/Role/Logger.pm', 'lib/Dancer2/Core/Role/Serializer.pm', 'lib/Dancer2/Core/Role/SessionFactory.pm', 'lib/Dancer2/Core/Role/SessionFactory/File.pm', 'lib/Dancer2/Core/Role/StandardResponses.pm', 'lib/Dancer2/Core/Role/Template.pm', 'lib/Dancer2/Core/Route.pm', 'lib/Dancer2/Core/Runner.pm', 'lib/Dancer2/Core/Session.pm', 'lib/Dancer2/Core/Time.pm', 'lib/Dancer2/Core/Types.pm', 'lib/Dancer2/DeprecationPolicy.pod', 'lib/Dancer2/FileUtils.pm', 'lib/Dancer2/Handler/AutoPage.pm', 'lib/Dancer2/Handler/File.pm', 'lib/Dancer2/Logger/Capture.pm', 'lib/Dancer2/Logger/Capture/Trap.pm', 'lib/Dancer2/Logger/Console.pm', 'lib/Dancer2/Logger/Diag.pm', 'lib/Dancer2/Logger/File.pm', 'lib/Dancer2/Logger/Note.pm', 'lib/Dancer2/Logger/Null.pm', 'lib/Dancer2/Manual.pod', 'lib/Dancer2/Manual/Config.pod', 'lib/Dancer2/Manual/Cookbook.pod', 'lib/Dancer2/Manual/Deployment.pod', 'lib/Dancer2/Manual/Extending.pod', 'lib/Dancer2/Manual/Keywords.pod', 'lib/Dancer2/Manual/Migration.pod', 'lib/Dancer2/Manual/Plugins.pod', 'lib/Dancer2/Manual/QuickStart.pod', 'lib/Dancer2/Manual/Testing.pod', 'lib/Dancer2/Manual/Tutorial.pod', 'lib/Dancer2/Plugin.pm', 'lib/Dancer2/Policy.pod', 'lib/Dancer2/Serializer/JSON.pm', 'lib/Dancer2/Serializer/Mutable.pm', 'lib/Dancer2/Serializer/YAML.pm', 'lib/Dancer2/Session/Simple.pm', 'lib/Dancer2/Session/YAML.pm', 'lib/Dancer2/Template/TemplateToolkit.pm', 'lib/Dancer2/Template/Tiny.pm', 'lib/Dancer2/Test.pm', 'script/dancer2', 't/00-compile.t', 't/00-report-prereqs.dd', 't/00-report-prereqs.t', 't/app.t', 't/app/t1/bin/app.psgi', 't/app/t1/config.yml', 't/app/t1/lib/App1.pm', 't/app/t1/lib/Sub/App2.pm', 't/app/t2/.dancer', 't/app/t2/config.yml', 't/app/t2/lib/App3.pm', 't/app/t3/lib/App4.pm', 't/app/t3/lib/config.yml', 't/app/t_config_file_extended/bin/app.psgi', 't/app/t_config_file_extended/config.yml', 't/app/t_config_file_extended/lib/App1.pm', 't/app_alone.t', 't/auto_page.t', 't/caller.t', 't/charset_server.t', 't/classes/Dancer2-Core-Factory/new.t', 't/classes/Dancer2-Core-Hook/new.t', 't/classes/Dancer2-Core-Request/new.t', 't/classes/Dancer2-Core-Request/serializers.t', 't/classes/Dancer2-Core-Response-Delayed/after_hooks.t', 't/classes/Dancer2-Core-Response-Delayed/new.t', 't/classes/Dancer2-Core-Response/new_from.t', 't/classes/Dancer2-Core-Role-Engine/with.t', 't/classes/Dancer2-Core-Role-Handler/with.t', 't/classes/Dancer2-Core-Role-HasConfig/with.t', 't/classes/Dancer2-Core-Role-HasEnvironment/with.t', 't/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/bin/.exists', 't/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/blib/lib/fakescript.pl', 't/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/lib/fake/inner/dir/.exists', 't/classes/Dancer2-Core-Role-HasLocation/FakeDancerFile/.dancer', 't/classes/Dancer2-Core-Role-HasLocation/FakeDancerFile/fakescript.pl', 't/classes/Dancer2-Core-Role-HasLocation/FakeDistRoot/bin/.exists', 't/classes/Dancer2-Core-Role-HasLocation/FakeDistRoot/configonly/config.yml', 't/classes/Dancer2-Core-Role-HasLocation/FakeDistRoot/configonly/fakescript.pl', 't/classes/Dancer2-Core-Role-HasLocation/FakeDistRoot/lib/.exists', 't/classes/Dancer2-Core-Role-HasLocation/FakeDistRoot/t/config.yml', 't/classes/Dancer2-Core-Role-HasLocation/FakeDistRoot/t/fakescript.pl', 't/classes/Dancer2-Core-Role-HasLocation/FakeDistRoot/t/public/.exists', 't/classes/Dancer2-Core-Role-HasLocation/FakeDistRoot/t/views/.exists', 't/classes/Dancer2-Core-Role-HasLocation/FakeDistRoot/viewsonly/fakescript.pl', 't/classes/Dancer2-Core-Role-HasLocation/FakeDistRoot/viewsonly/views/.exists', 't/classes/Dancer2-Core-Role-HasLocation/with.t', 't/classes/Dancer2-Core-Role-Serializer/with.t', 't/classes/Dancer2-Core-Role-StandardResponses/with.t', 't/classes/Dancer2-Core-Route/base.t', 't/classes/Dancer2-Core-Route/deprecated_param_keys.t', 't/classes/Dancer2-Core-Route/match.t', 't/classes/Dancer2-Core-Runner/environment.t', 't/classes/Dancer2-Core-Runner/new.t', 't/classes/Dancer2-Core-Runner/psgi_app.t', 't/classes/Dancer2-Core/camelize.t', 't/classes/Dancer2/import-pragmas.t', 't/classes/Dancer2/import.t', 't/config.t', 't/config.yml', 't/config/config.yml', 't/config/environments/failure.yml', 't/config/environments/merging.yml', 't/config/environments/production.yml', 't/config/environments/staging.json', 't/config2/config.yml', 't/config2/config_local.yml', 't/config2/environments/lconfig.yml', 't/config2/environments/lconfig_local.yml', 't/config_file_extended.t', 't/config_many.t', 't/config_many_failure.t', 't/config_multiapp.t', 't/config_reader.t', 't/config_settings.t', 't/config_utils.t', 't/context-in-before.t', 't/cookie.t', 't/corpus/pretty/505.tt', 't/corpus/pretty/relative.tt', 't/corpus/pretty_public/404.html', 't/corpus/pretty_public/510.html', 't/corpus/static/empty.foo', 't/corpus/static/index.html', 't/custom_dsl.t', 't/dancer-test.t', 't/dancer-test/config.yml', 't/deserialize.t', 't/disp_named_capture.t', 't/dispatcher.t', 't/dsl/any.t', 't/dsl/app.t', 't/dsl/content.t', 't/dsl/delayed.t', 't/dsl/error_template.t', 't/dsl/extend.t', 't/dsl/extend_config/config.yml', 't/dsl/halt.t', 't/dsl/halt_with_param.t', 't/dsl/json.t', 't/dsl/mime.t', 't/dsl/parameters.t', 't/dsl/pass.t', 't/dsl/path.t', 't/dsl/pod.t', 't/dsl/request.t', 't/dsl/request_data.t', 't/dsl/route_retvals.t', 't/dsl/send_as.t', 't/dsl/send_file.t', 't/dsl/splat.t', 't/dsl/to_app.t', 't/dsl/uri_for.t', 't/dsl/uri_for_route.t', 't/dsl/yaml.t', 't/dsl_path_dirname.t', 't/e2e/cli/gen.t', 't/engine.t', 't/error.t', 't/examples/hello_world.t', 't/examples/simple_calculator.t', 't/factory.t', 't/file_utils.t', 't/forward.t', 't/forward_before_hook.t', 't/forward_hmv_params.t', 't/forward_test_tcp.t', 't/hooks.t', 't/http_methods.t', 't/http_status.t', 't/integration/dispatch/flow.t', 't/integration/dispatch/multiapp.t', 't/integration/error/fallback.t', 't/integration/error/rendering.t', 't/integration/handler/file.t', 't/integration/hooks/chain.t', 't/integration/hooks/engine.t', 't/integration/serializer/json.t', 't/integration/serializer/mutable.t', 't/integration/serializer/yaml.t', 't/integration/session/lifecycle.t', 't/integration/template/autopage.t', 't/integration/template/rendering.t', 't/issues/config.yml', 't/issues/gh-1013/gh-1013.t', 't/issues/gh-1013/views/t.tt', 't/issues/gh-1046/config.yml', 't/issues/gh-1046/gh-1046.t', 't/issues/gh-1070.t', 't/issues/gh-1098.t', 't/issues/gh-1216/gh-1216.t', 't/issues/gh-1216/lib/App.pm', 't/issues/gh-1216/lib/App/Extra.pm', 't/issues/gh-1216/lib/Dancer2/Plugin/Null.pm', 't/issues/gh-1226/gh-1226.t', 't/issues/gh-1226/lib/App.pm', 't/issues/gh-1226/lib/App/Extra.pm', 't/issues/gh-1226/lib/Dancer2/Plugin/Test/AccessDSL.pm', 't/issues/gh-1230/gh-1230.t', 't/issues/gh-1230/lib/App.pm', 't/issues/gh-1230/lib/App/Extra.pm', 't/issues/gh-1230/lib/Dancer2/Plugin/Test/AccessDSL.pm', 't/issues/gh-1230/lib/Dancer2/Plugin/Test/AccessPluginDSL.pm', 't/issues/gh-1232.t', 't/issues/gh-1289.t', 't/issues/gh-1449/TestPlugin.pm', 't/issues/gh-1449/gh-1449.t', 't/issues/gh-1564.t', 't/issues/gh-1621/gh-1621.t', 't/issues/gh-1621/views/redirect.tt', 't/issues/gh-1664/gh-1664.t', 't/issues/gh-1712/gh-1712.t', 't/issues/gh-1712/views/exec.tt', 't/issues/gh-1861.t', 't/issues/gh-1863.t', 't/issues/gh-596.t', 't/issues/gh-634.t', 't/issues/gh-639/fails/.dancer', 't/issues/gh-639/fails/config.yml', 't/issues/gh-639/fails/issue.t', 't/issues/gh-639/succeeds/.dancer', 't/issues/gh-639/succeeds/config.yml', 't/issues/gh-639/succeeds/issue.t', 't/issues/gh-650/gh-650.t', 't/issues/gh-650/views/environment_setting.tt', 't/issues/gh-723.t', 't/issues/gh-730.t', 't/issues/gh-762.t', 't/issues/gh-762/views/404.tt', 't/issues/gh-794.t', 't/issues/gh-797.t', 't/issues/gh-799.t', 't/issues/gh-811.t', 't/issues/gh-931.t', 't/issues/gh-936.t', 't/issues/gh-936/views/error.tt', 't/issues/gh-944.t', 't/issues/gh-975/config.yml', 't/issues/gh-975/gh-975.t', 't/issues/gh-975/test_public_dir/test.txt', 't/issues/memleak/config.yml', 't/issues/memleak/die_in_hooks.t', 't/issues/vars-in-forward.t', 't/lib/App1.pm', 't/lib/App2.pm', 't/lib/Dancer2/ConfigReader/Additional.pm', 't/lib/Dancer2/ConfigReader/File/Extended.pm', 't/lib/Dancer2/ConfigReader/Recursive.pm', 't/lib/Dancer2/ConfigReader/TestDummy.pm', 't/lib/Dancer2/Plugin/Bar.pm', 't/lib/Dancer2/Plugin/DancerPlugin.pm', 't/lib/Dancer2/Plugin/DefineKeywords.pm', 't/lib/Dancer2/Plugin/EmptyPlugin.pm', 't/lib/Dancer2/Plugin/Foo.pm', 't/lib/Dancer2/Plugin/FooPlugin.pm', 't/lib/Dancer2/Plugin/Hookee.pm', 't/lib/Dancer2/Plugin/OnPluginImport.pm', 't/lib/Dancer2/Plugin/PluginWithImport.pm', 't/lib/Dancer2/Plugin/Polite.pm', 't/lib/Dancer2/Session/SimpleNoChangeId.pm', 't/lib/Dancer2/Template/TemplateToolkitFoo.pm', 't/lib/Foo.pm', 't/lib/MyDancerDSL.pm', 't/lib/PoC/Plugin/Polite.pm', 't/lib/SubApp1.pm', 't/lib/SubApp2.pm', 't/lib/TemplateFoo.pm', 't/lib/TestApp.pm', 't/lib/TestPod.pm', 't/lib/TestTypeLibrary.pm', 't/lib/poc.pm', 't/lib/poc2.pm', 't/log_die_before_hook.t', 't/log_levels.t', 't/logger.t', 't/logger_console.t', 't/memory_cycles.t', 't/mime.t', 't/multi_apps.t', 't/multi_apps_forward.t', 't/multiapp_template_hooks.t', 't/multipart_content.t', 't/named_apps.t', 't/named_routes.t', 't/no_default_middleware.t', 't/plugin2/app_dsl_cb/app_dsl_cb.t', 't/plugin2/app_dsl_cb/lib/App.pm', 't/plugin2/app_dsl_cb/lib/App/TestPlugin.pm', 't/plugin2/basic-2.t', 't/plugin2/basic.t', 't/plugin2/define-keywords.t', 't/plugin2/find_plugin.t', 't/plugin2/from-config.t', 't/plugin2/hooks.t', 't/plugin2/inside-plugin.t', 't/plugin2/keywords-hooks-namespace.t', 't/plugin2/memory_cycles.t', 't/plugin2/no-app-munging.t', 't/plugin2/no-clobbering.t', 't/plugin2/no-config.t', 't/plugin2/with-plugins.t', 't/plugin_import.t', 't/plugin_multiple_apps.t', 't/plugin_register.t', 't/plugin_syntax.t', 't/prepare_app.t', 't/psgi_app.t', 't/psgi_app_forward_and_pass.t', 't/public/file.txt', 't/redirect.t', 't/request.t', 't/request_make_forward_to.t', 't/request_multipart_formdata.t', 't/request_upload.t', 't/response.t', 't/roles/hook.t', 't/route-pod-coverage/route-pod-coverage.t', 't/route_query.t', 't/scope_problems/config.yml', 't/scope_problems/dispatcher_internal_request.t', 't/scope_problems/keywords_before_template_hook.t', 't/scope_problems/session_is_cleared.t', 't/scope_problems/views/500.tt', 't/scope_problems/with_return_dies.t', 't/serializer.t', 't/serializer_helpers.t', 't/serializer_json.t', 't/serializer_mutable.t', 't/serializer_mutable_custom.t', 't/session_bad_client_cookie.t', 't/session_config.t', 't/session_engines.t', 't/session_forward.t', 't/session_hooks.t', 't/session_hooks_no_change_id.t', 't/session_in_template.t', 't/session_lifecycle.t', 't/session_object.t', 't/session_yaml_object.t', 't/shared_engines.t', 't/static_content.t', 't/strict_config.t', 't/template.t', 't/template_default_tokens.t', 't/template_ext.t', 't/template_name.t', 't/time.t', 't/types.t', 't/unit/configreader/merging.t', 't/unit/configreader/readers.t', 't/unit/configreader/strict.t', 't/unit/core/cookie.t', 't/unit/core/request.t', 't/unit/core/response.t', 't/unit/core/route.t', 't/unit/logger/format.t', 't/unit/logger/levels.t', 't/utf8_strict.t', 't/utf8_url.t', 't/vars.t', 't/views/auto_page.tt', 't/views/beforetemplate.tt', 't/views/folder/page.tt', 't/views/index.tt', 't/views/layouts/main.tt', 't/views/session_in_template.tt', 't/views/template_simple_index.tt', 't/views/tokens.tt' ); notabs_ok($_) foreach @files; done_testing; plugin_register.t100644000765000024 231015267764222 16314 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More import => ['!pass']; use Test::Fatal; subtest 'reserved keywords' => sub { use Dancer2::Plugin; like( exception { register dance => sub {1} }, qr/You can't use 'dance', this is a reserved keyword/, "Can't use Dancer2's reserved keywords", ); like( exception { register '1function' => sub {1} }, qr/You can't use '1function', it is an invalid name/, "Can't use invalid names for keywords", ); }; subtest 'plugin reserved keywords' => sub { { package Foo; use Dancer2::Plugin; Test::More::is( Test::Fatal::exception { register 'foo_method' => sub {1} }, undef, "can register a new keyword", ); } { package Bar; use Dancer2::Plugin; Test::More::like( Test::Fatal::exception { register 'foo_method' => sub {1} }, qr{can't use foo_method, this is a keyword reserved by Foo}, "can't register a keyword already registered by another plugin", ); } }; done_testing; session_forward.t100644000765000024 1157215267764222 16353 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Cookies; use HTTP::Request::Common; { package Test::Forward::Single; use Dancer2; set session => 'Simple'; get '/main' => sub { session foo => 'Single/main'; forward '/outer'; }; get '/outer' => sub { session bar => 'Single/outer'; forward '/inner'; }; get '/inner' => sub { session baz => 'Single/inner'; return join ':', map +( session($_) || '' ), qw; }; get '/clear' => sub { session foo => undef; session bar => undef; session baz => undef; }; } { package Test::Forward::Multi::SameCookieName; use Dancer2; set session => 'Simple'; prefix '/same'; get '/main' => sub { session foo => 'SameCookieName/main'; forward '/outer'; }; get '/bad_chain' => sub { session foo => 'SameCookieName/bad_chain'; forward '/other/main'; }; } { package Test::Forward::Multi::OtherCookieName; use Dancer2; set engines => { session => { Simple => { cookie_name => 'session.dancer' } } }; set session => 'Simple'; prefix '/other'; get '/main' => sub { session foo => 'OtherCookieName/main'; # Forwards to another app with different cookie name forward '/outer'; }; get '/clear' => sub { session foo => undef; session bar => undef; session baz => undef; }; } # base uri for all requests. my $base = 'http://localhost'; subtest 'Forwards within a single app' => sub { my $test = Plack::Test->create( Test::Forward::Single->to_app ); my $jar = HTTP::Cookies->new; { my $res = $test->request( GET "$base/main" ); is( $res->content, q{Single/main:Single/outer:Single/inner}, 'session value preserved after chained forwards', ); $jar->extract_cookies($res); } { my $req = GET "$base/inner"; $jar->add_cookie_header($req); my $res = $test->request($req); is( $res->content, q{Single/main:Single/outer:Single/inner}, 'session values preserved between calls', ); $jar->extract_cookies($res); } { my $req = GET "$base/clear"; $jar->add_cookie_header($req); my $res = $test->request( GET "$base/clear" ); $jar->extract_cookies($res); } { my $req = GET "$base/outer"; $jar->add_cookie_header($req); my $res = $test->request( GET "$base/outer" ); is( $res->content, q{:Single/outer:Single/inner}, 'session value preserved after forward from route', ); $jar->extract_cookies($res); } }; subtest 'Forwards between multiple apps using the same cookie name' => sub { my $test = Plack::Test->create( Dancer2->psgi_app ); my $jar = HTTP::Cookies->new; { my $res = $test->request( GET "$base/same/main" ); is( $res->content, q{SameCookieName/main:Single/outer:Single/inner}, 'session value preserved after chained forwards between apps', ); $jar->extract_cookies($res); } { my $req = GET "$base/outer"; $jar->add_cookie_header($req); my $res = $test->request($req); is( $res->content, q{SameCookieName/main:Single/outer:Single/inner}, 'session value preserved after forward from route', ); } }; subtest 'Forwards between multiple apps using different cookie names' => sub { my $test = Plack::Test->create( Dancer2->psgi_app ); my $jar = HTTP::Cookies->new; my $res = $test->request( GET "$base/other/main" ); is( $res->content, q{:Single/outer:Single/inner}, 'session value only from forwarded app', ); }; # we need to make sure B doesn't override A when forwarding to C # A -> B -> C # This means that A (cookie_name "Homer") # forwarding to B (cookie_name "Marge") # forwarding to C (cookie_name again "Homer") # will cause a problem because we will lose "Homer" session data, # because it will be overwritten by "Marge" session data. # Suddenly A and C cannot communicate because it was flogged. # # if A -> Single, B -> OtherCookieName, C -> SameCookieName # call A, create session, then forward to B, create session, # then forward to C, check has values as in A and C subtest 'Forwards between multiple apps using multiple different cookie names' => sub { my $test = Plack::Test->create( Dancer2->psgi_app ); my $jar = HTTP::Cookies->new; my $res = $test->request( GET "$base/same/bad_chain" ); is( $res->content, q{SameCookieName/bad_chain:Single/outer:Single/inner}, 'session value only from apps with same session cookie name', ); }; done_testing; serializer_json.t100644000765000024 331515267764222 16322 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; use JSON::MaybeXS; use Dancer2::Serializer::JSON; # config { package MyApp; use Dancer2; our $entity; set engines => { serializer => { JSON => { canonical => 1, pretty => 1, } } }; set serializer => 'JSON'; get '/serialize' => sub { return $entity; }; } my @tests = ( { entity => { a => 1, b => 2, }, options => { canonical => 1, pretty => 1 }, name => "basic hash", }, { entity => { c => [ { d => 3, e => { f => 4, g => 'word', } } ], h => 6 }, options => { canonical => 1, pretty => 1 }, name => "nested", }, { entity => { data => "\x{2620}" x 10 }, options => { canonical => 1, pretty => 1, utf8 => 1 }, name => "utf8", } ); my $app = MyApp->to_app; for my $test (@tests) { my $expected = JSON::MaybeXS->new($test->{options})->encode($test->{entity}); # Helpers pass options my $actual = Dancer2::Serializer::JSON::to_json( $test->{entity}, $test->{options} ); is( $actual, $expected, "to_json: $test->{name}" ); # Options from config my $serializer = Dancer2::Serializer::JSON->new(config => $test->{options}); my $output = $serializer->serialize( $test->{entity} ); is( $output, $expected, "serialize: $test->{name}" ); $MyApp::entity = $test->{entity}; test_psgi $app, sub { my $cb = shift; my $res = $cb->( GET '/serialize' ); is($res->content, $expected, "serialized content in response: $test->{name}"); }; } done_testing(); session_engines.t100644000765000024 617315267764222 16320 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use YAML; use Plack::Test; use HTTP::Cookies; use HTTP::Request::Common; use Path::Tiny qw< path >; my $SESSION_DIR; BEGIN { $SESSION_DIR = path( __FILE__ )->parent->child('sessions')->stringify; } { package App; use Dancer2; my @to_destroy; set engines => { session => { YAML => { session_dir => $SESSION_DIR } } }; hook 'engine.session.before_destroy' => sub { my $session = shift; push @to_destroy, $session; }; get '/set_session/*' => sub { my ($name) = splat; session name => $name; }; get '/read_session' => sub { my $name = session('name') || ''; "name='$name'"; }; get '/clear_session' => sub { session name => undef; return exists( session->data->{'name'} ) ? "failed" : "cleared"; }; get '/cleanup' => sub { app->destroy_session; return scalar(@to_destroy); }; setting session => 'Simple'; set( show_stacktrace => 1, environment => 'production', ); } my $url = "http://localhost"; my $test = Plack::Test->create( App->to_app ); my $app = Dancer2->runner->apps->[0]; my @clients = qw(one two three); for my $engine ( qw(YAML Simple) ) { # clear current session engine, and rebuild for the test # This is *really* messy, playing in object hashrefs.. delete $app->{session_engine}; $app->config->{session} = $engine; $app->session_engine; # trigger a build # run the tests for this engine for my $client (@clients) { my $jar = HTTP::Cookies->new; subtest "[$engine][$client] Empty session" => sub { my $res = $test->request( GET "$url/read_session" ); like $res->content, qr/name=''/, "empty session for client $client"; $jar->extract_cookies($res); }; subtest "[$engine][$client] set_session" => sub { my $req = GET "$url/set_session/$client"; $jar->add_cookie_header($req); my $res = $test->request($req); ok( $res->is_success, "set_session for client $client" ); $jar->extract_cookies($res); }; subtest "[$engine][$client] session for client" => sub { my $req = GET "$url/read_session"; $jar->add_cookie_header($req); my $res = $test->request($req); like $res->content, qr/name='$client'/, "session looks good for client $client"; $jar->extract_cookies($res); }; subtest "[$engine][$client] delete session" => sub { my $req = GET "$url/clear_session"; $jar->add_cookie_header($req); my $res = $test->request($req); like $res->content, qr/cleared/, "deleted session key"; }; subtest "[$engine][$client] cleanup" => sub { my $req = GET "$url/cleanup"; $jar->add_cookie_header($req); my $res = $test->request($req); ok( $res->is_success, "cleanup done for $client" ); ok( $res->content, "session hook triggered" ); }; } } done_testing; config_multiapp.t100644000765000024 63715267764222 16264 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use lib '.'; use t::app::t1::lib::App1; use t::app::t1::lib::Sub::App2; use t::app::t2::lib::App3; for my $app ( @{ Dancer2->runner->apps } ) { # Need to determine path to config; use apps' name for now.. my $path = $app->name eq 'App3' ? 't2' : 't1'; is $app->config->{app}->{config}, 'ok', $app->name . ": config loaded properly" } done_testing; config_settings.t100644000765000024 132215267764222 16301 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Dancer2; # testing default values is( setting('port'), '3000', "default value for 'port' is OK" ); is( setting('content_type'), 'text/html', "default value for 'content_type' is OK" ); #should we test for all default values? # testing new settings ok( setting( 'foo' => '42' ), 'setting a new value' ); is( setting('foo'), 42, 'new value has been set' ); # test the alias 'set' ok( set( bar => 43 ), 'setting bar with set' ); is( setting('bar'), 43, 'new value has been set' ); #multiple values ok( setting( 'foo' => 43, bar => 44 ), 'set multiple values' ); ok( setting('foo') == 43 && setting('bar') == 44, 'set multiple values successful' ); done_testing; core000755000765000024 015267764222 14500 5ustar00jasonstaff000000000000Dancer2-2.3.0/t/unitroute.t100644000765000024 1570015267764222 16206 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/unit/coreuse strict; use warnings; use Test::More; use Test::Fatal; use Dancer2::Core::Route; use Dancer2::Core::Request; # Build a request whose path is $path. Extra pairs are merged into the PSGI # env, which is how we set headers (HTTP_USER_AGENT, CONTENT_TYPE, ...). sub request_for { my ( $path, %env ) = @_; return Dancer2::Core::Request->new( env => { REQUEST_METHOD => 'GET', PATH_INFO => $path, 'psgi.url_scheme' => 'http', HTTP_HOST => 'localhost', %env, }, ); } sub route_for { return Dancer2::Core::Route->new( method => 'get', code => sub {1}, @_ ); } # match() returns the captured data as a hashref on success (an empty hashref # for a route with nothing to capture) and undef when the path doesn't match. sub match_for { my ( $route, $path, %env ) = @_; return scalar $route->match( request_for( $path, %env ) ); } subtest 'a static path matches itself and nothing else' => sub { my $route = route_for( regexp => '/about' ); is_deeply( match_for( $route, '/about' ), {}, '/about matches, captures nothing' ); is( match_for( $route, '/about/us' ), undef, '/about does not match /about/us' ); is( match_for( $route, '/aboutus' ), undef, '/about does not match /aboutus' ); }; subtest 'a leading slash is added when the pattern omits one' => sub { my $route = route_for( regexp => 'noslash' ); is_deeply( match_for( $route, '/noslash' ), {}, q{'noslash' is treated as '/noslash'} ); }; subtest 'a :token captures one path segment and stops at the slash' => sub { my $route = route_for( regexp => '/user/:id' ); is_deeply( match_for( $route, '/user/42' ), { id => '42' }, 'the segment is captured under the token name' ); # This is the assertion that fails if the token pattern ever becomes # greedy enough to swallow a '/'. is( match_for( $route, '/user/1/2' ), undef, 'a token does not capture across a slash' ); is( match_for( $route, '/user/' ), undef, 'a token requires at least one character' ); is( match_for( $route, '/user' ), undef, 'the segment is not optional' ); }; subtest 'a trailing ? makes a token optional' => sub { my $route = route_for( regexp => '/o/:id?' ); is_deeply( match_for( $route, '/o/9' ), { id => '9' }, 'the token still captures when present' ); is_deeply( match_for( $route, '/o/' ), { id => undef }, 'the token is undef when absent, and the route still matches' ); }; subtest 'a dot in the pattern is a literal dot, not a regex wildcard' => sub { my $route = route_for( regexp => '/file.txt' ); is_deeply( match_for( $route, '/file.txt' ), {}, 'the literal path matches' ); # This is the assertion that fails if '.' stops being escaped. is( match_for( $route, '/fileXtxt' ), undef, 'the dot does not match an arbitrary character' ); }; subtest 'a * captures one segment into splat' => sub { my $route = route_for( regexp => '/a/*/b/*' ); is_deeply( match_for( $route, '/a/one/b/two' ), { splat => [ 'one', 'two' ] }, 'each wildcard contributes one value, in order', ); is( match_for( $route, '/a/one/two/b/three' ), undef, 'a single wildcard does not span a slash' ); }; subtest 'a ** captures the rest of the path, split on slashes' => sub { my $route = route_for( regexp => '/files/**' ); # The megasplat value is an arrayref *inside* splat: splat holds one entry # per wildcard, and this wildcard's entry is the list of segments. is_deeply( match_for( $route, '/files/a/b/c' ), { splat => [ [ 'a', 'b', 'c' ] ] }, 'the remaining path is split into segments', ); is_deeply( match_for( $route, '/files/a' ), { splat => [ ['a'] ] }, 'a single segment still arrives as a one-element list', ); }; subtest 'a typed token rejects a value of the wrong type' => sub { my $route = route_for( regexp => '/n/:id[Int]' ); is_deeply( match_for( $route, '/n/42' ), { id => '42' }, 'an integer is captured' ); # This is the assertion that fails if the type check is dropped: without # it, '/n/abc' matches the same shape as '/n/42'. is( match_for( $route, '/n/abc' ), undef, 'a non-integer does not match at all' ); }; subtest 'a prefix is prepended to the pattern' => sub { my $route = route_for( regexp => '/list', prefix => '/api' ); is( $route->spec_route, '/api/list', 'spec_route records the prefixed path' ); is_deeply( match_for( $route, '/api/list' ), {}, 'the prefixed path matches' ); is( match_for( $route, '/list' ), undef, 'the unprefixed path does not' ); }; subtest 'a regexp route returns named captures under captures' => sub { my $route = route_for( regexp => qr{^/x/(?\w+)$} ); is_deeply( match_for( $route, '/x/bob' ), { captures => { who => 'bob' } }, 'named captures are grouped under the captures key', ); is( match_for( $route, '/y/bob' ), undef, 'a non-matching path returns undef' ); }; subtest 'the deprecated placeholder names are refused at build time' => sub { like( exception { route_for( regexp => '/a/:splat' ) }, qr/Named placeholder 'splat' is deprecated/, q{:splat dies rather than being silently accepted}, ); like( exception { route_for( regexp => '/a/:captures' ) }, qr/Named placeholder 'captures' is deprecated/, q{:captures dies rather than being silently accepted}, ); }; subtest 'an unrecognised matching option is refused at build time' => sub { like( exception { route_for( regexp => '/a', options => { bogus => 1 } ) }, qr/Not a valid option for route matching: `bogus'/, 'a typo in an option name dies instead of being ignored', ); is( exception { route_for( regexp => '/a', options => { agent => 'x' } ) }, undef, 'a supported option name is accepted', ); }; subtest 'route options gate the match on request properties' => sub { my $route = route_for( regexp => '/only', options => { agent => 'Firefox' } ); # The option value is used as a pattern, not compared for equality. is_deeply( match_for( $route, '/only', HTTP_USER_AGENT => 'Mozilla Firefox 3' ), {}, 'the option matches anywhere within the header value', ); is( match_for( $route, '/only', HTTP_USER_AGENT => 'Chrome' ), undef, 'a non-matching agent blocks the route' ); is( match_for( $route, '/only' ), undef, 'a missing agent header blocks the route' ); my $typed = route_for( regexp => '/ct', options => { content_type => 'application/json' }, ); is_deeply( match_for( $typed, '/ct', CONTENT_TYPE => 'application/json' ), {}, 'a matching content type is allowed through' ); is( match_for( $typed, '/ct', CONTENT_TYPE => 'text/html' ), undef, 'a non-matching content type blocks the route' ); }; done_testing(); config.yml100644000765000024 4715267764222 15767 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/app/t2app: config: ok strict_config: 0 t1000755000765000024 015267764222 13675 5ustar00jasonstaff000000000000Dancer2-2.3.0/t/appconfig.yml100644000765000024 4715267764222 15766 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/app/t1app: config: ok strict_config: 0 config000755000765000024 015267764222 14036 5ustar00jasonstaff000000000000Dancer2-2.3.0/tconfig.yml100644000765000024 14015267764222 16141 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/configmain: 1 charset: 'UTF-8' show_stacktrace: 1 strict_config: 0 application: some_feature: foo basic-2.t100644000765000024 133115267764222 15714 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/plugin2use strict; use warnings; use Test::More tests => 8; use Plack::Test; use HTTP::Request::Common; use lib 't/lib'; use poc2; my $test = Plack::Test->create( poc2->to_app ); note "poc2 root"; { my $res = $test->request( GET '/' ); ok $res->is_success; my $content = $res->content; like $content, qr/please/; like $content, qr/8-D/; } note "pos2 goodbye"; { my $res = $test->request( GET '/goodbye' ); ok $res->is_success; my $content = $res->content; like $content, qr/farewell/; like $content, qr/please/; } note "pos2 hooked"; { my $res = $test->request( GET '/sudo' ); ok ! $res->is_success; my $content = $res->content; like $content, qr/Not in sudoers file/; } config.yml100644000765000024 1715267764222 16172 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/issueslogger: "Note" Test.pm100644000765000024 6673515267764222 16030 0ustar00jasonstaff000000000000Dancer2-2.3.0/lib/Dancer2package Dancer2::Test; # ABSTRACT: Useful routines for testing Dancer2 apps $Dancer2::Test::VERSION = '2.3.0'; use strict; use warnings; use Carp qw; use Test::More; use Test::Builder; use URI::Escape; use Data::Dumper; use File::Temp; use Ref::Util qw; use parent 'Exporter'; our @EXPORT = qw( dancer_response response_content_is response_content_isnt response_content_is_deeply response_content_like response_content_unlike response_status_is response_status_isnt response_headers_include response_headers_are_deeply response_is_file route_exists route_doesnt_exist is_pod_covered route_pod_coverage ); #dancer1 also has read_logs, response_redirect_location_is #cf. https://github.com/PerlDancer2/Dancer22/issues/25 use Dancer2::Core::MIME; use Dancer2::Core::Dispatcher; use Dancer2::Core::Request; # singleton to store all the apps my $_dispatcher = Dancer2::Core::Dispatcher->new; # prevent deprecation warnings our $NO_WARN = 0; # can be called with the ($method, $path, $option) triplet, # or can be fed a request object directly, or can be fed # a single string, assumed to be [ GET => $string ] # or can be fed a response (which is passed through without # any modification) sub dancer_response { croak 'DEPRECATED: Dancer2::Test. Please use Plack::Test instead' unless $NO_WARN; _find_dancer_apps_for_dispatcher(); # useful for the high-level tests return $_[0] if ref $_[0] eq 'Dancer2::Core::Response'; my ( $request, $env ) = ref $_[0] eq 'Dancer2::Core::Request' ? _build_env_from_request(@_) : _build_request_from_env(@_); # override the set_request so it actually sets our request instead { ## no critic qw(TestingAndDebugging::ProhibitNoWarnings) no warnings qw; *Dancer2::Core::App::set_request = sub { my $self = shift; $self->_set_request( $request ); $_->set_request( $request ) for @{ $self->defined_engines }; }; } # since the response is a PSGI response # we create a Response object which was originally expected my $psgi_response = $_dispatcher->dispatch($env); return Dancer2::Core::Response->new( status => $psgi_response->[0], headers => $psgi_response->[1], content => $psgi_response->[2][0], mime_type => Dancer2::Core::MIME->new(), ); } sub _build_request_from_env { # arguments can be passed as the triplet # or as a arrayref, or as a simple string my ( $method, $path, $options ) = @_ > 1 ? @_ : is_arrayref($_[0]) ? @{ $_[0] } : ( GET => $_[0], {} ); my $env = { %ENV, REQUEST_METHOD => uc($method), PATH_INFO => $path, QUERY_STRING => '', 'psgi.url_scheme' => 'http', SERVER_PROTOCOL => 'HTTP/1.0', SERVER_NAME => 'localhost', SERVER_PORT => 3000, HTTP_HOST => 'localhost', HTTP_USER_AGENT => "Dancer2::Test simulator v " . Dancer2->VERSION, }; if ( defined $options->{params} ) { my @params; while ( my ( $p, $value ) = each %{ $options->{params} } ) { if ( is_arrayref($value) ) { for my $v (@$value) { push @params, uri_escape_utf8($p) . '=' . uri_escape_utf8($v); } } else { push @params, uri_escape_utf8($p) . '=' . uri_escape_utf8($value); } } $env->{QUERY_STRING} = join( '&', @params ); } my $request = Dancer2::Core::Request->new( env => $env ); # body $request->body( $options->{body} ) if exists $options->{body}; # headers if ( $options->{headers} ) { for my $header ( @{ $options->{headers} } ) { my ( $name, $value ) = @{$header}; $request->header( $name => $value ); if ( $name =~ /^cookie$/i ) { $env->{HTTP_COOKIE} = $value; } } } # files if ( $options->{files} ) { for my $file ( @{ $options->{files} } ) { my $headers = $file->{headers}; $headers->{'Content-Type'} ||= 'text/plain'; my $temp = File::Temp->new(); if ( $file->{data} ) { print $temp $file->{data}; close($temp); } else { require File::Copy; File::Copy::copy( $file->{filename}, $temp ); } my $upload = Dancer2::Core::Request::Upload->new( filename => $file->{filename}, size => -s $temp->filename, tempname => $temp->filename, headers => $headers, ); ## keep temp_fh in scope so it doesn't get deleted too early ## But will get deleted by the time the test is finished. $upload->{temp_fh} = $temp; $request->uploads->{ $file->{name} } = $upload; } } # content-type if ( $options->{content_type} ) { $request->content_type( $options->{content_type} ); } return ( $request, $env ); } sub _build_env_from_request { my ($request) = @_; my $env = { REQUEST_METHOD => $request->method, PATH_INFO => $request->path, QUERY_STRING => '', 'psgi.url_scheme' => 'http', SERVER_PROTOCOL => 'HTTP/1.0', SERVER_NAME => 'localhost', SERVER_PORT => 3000, HTTP_HOST => 'localhost', HTTP_USER_AGENT => "Dancer2::Test simulator v" . Dancer2->VERSION, }; # TODO if ( my $params = $request->{_query_params} ) { my @params; while ( my ( $p, $value ) = each %{$params} ) { if ( is_arrayref($value) ) { for my $v (@$value) { push @params, uri_escape_utf8($p) . '=' . uri_escape_utf8($v); } } else { push @params, uri_escape_utf8($p) . '=' . uri_escape_utf8($value); } } $env->{QUERY_STRING} = join( '&', @params ); } # TODO files return ( $request, $env ); } sub response_status_is { my ( $req, $status, $test_name ) = @_; carp 'DEPRECATED: Dancer2::Test. Please use Plack::Test instead' unless $NO_WARN; $test_name ||= "response status is $status for " . _req_label($req); my $response = dancer_response($req); my $tb = Test::Builder->new; local $Test::Builder::Level = $Test::Builder::Level + 1; $tb->is_eq( $response->[0], $status, $test_name ); } sub _find_route_match { my ( $request, $env ) = ref $_[0] eq 'Dancer2::Core::Request' ? _build_env_from_request(@_) : _build_request_from_env(@_); for my $app (@{$_dispatcher->apps}) { for my $route (@{$app->routes->{lc($request->method)}}) { if ( $route->match($request) ) { return 1; } } } return 0; } sub route_exists { carp 'DEPRECATED: Dancer2::Test. Please use Plack::Test instead' unless $NO_WARN; my $tb = Test::Builder->new; local $Test::Builder::Level = $Test::Builder::Level + 1; $tb->ok( _find_route_match($_[0]), $_[1]); } sub route_doesnt_exist { carp 'DEPRECATED: Dancer2::Test. Please use Plack::Test instead' unless $NO_WARN; my $tb = Test::Builder->new; local $Test::Builder::Level = $Test::Builder::Level + 1; $tb->ok( !_find_route_match($_[0]), $_[1]); } sub response_status_isnt { my ( $req, $status, $test_name ) = @_; carp 'DEPRECATED: Dancer2::Test. Please use Plack::Test instead' unless $NO_WARN; $test_name ||= "response status is not $status for " . _req_label($req); my $response = dancer_response($req); my $tb = Test::Builder->new; local $Test::Builder::Level = $Test::Builder::Level + 1; $tb->isnt_eq( $response->[0], $status, $test_name ); } { # Map comparison operator names to human-friendly ones my %cmp_name = ( is_eq => "is", isnt_eq => "is not", like => "matches", unlike => "doesn't match", ); sub _cmp_response_content { my ( $req, $want, $test_name, $cmp ) = @_; if ( @_ == 3 ) { $cmp = $test_name; $test_name = $cmp_name{$cmp}; $test_name = "response content $test_name $want for " . _req_label($req); } my $response = dancer_response($req); my $tb = Test::Builder->new; local $Test::Builder::Level = $Test::Builder::Level + 1; $tb->$cmp( $response->[2][0], $want, $test_name ); } } sub response_content_is { carp 'DEPRECATED: Dancer2::Test. Please use Plack::Test instead' unless $NO_WARN; local $Test::Builder::Level = $Test::Builder::Level + 1; _cmp_response_content( @_, 'is_eq' ); } sub response_content_isnt { carp 'DEPRECATED: Dancer2::Test. Please use Plack::Test instead' unless $NO_WARN; local $Test::Builder::Level = $Test::Builder::Level + 1; _cmp_response_content( @_, 'isnt_eq' ); } sub response_content_like { carp 'DEPRECATED: Dancer2::Test. Please use Plack::Test instead' unless $NO_WARN; local $Test::Builder::Level = $Test::Builder::Level + 1; _cmp_response_content( @_, 'like' ); } sub response_content_unlike { carp 'DEPRECATED: Dancer2::Test. Please use Plack::Test instead' unless $NO_WARN; local $Test::Builder::Level = $Test::Builder::Level + 1; _cmp_response_content( @_, 'unlike' ); } sub response_content_is_deeply { my ( $req, $matcher, $test_name ) = @_; carp 'DEPRECATED: Dancer2::Test. Please use Plack::Test instead' unless $NO_WARN; $test_name ||= "response content looks good for " . _req_label($req); local $Test::Builder::Level = $Test::Builder::Level + 1; my $response = _req_to_response($req); is_deeply $response->[2][0], $matcher, $test_name; } sub response_is_file { my ( $req, $test_name ) = @_; carp 'DEPRECATED: Dancer2::Test. Please use Plack::Test instead' unless $NO_WARN; $test_name ||= "a file is returned for " . _req_label($req); my $response = _get_file_response($req); my $tb = Test::Builder->new; local $Test::Builder::Level = $Test::Builder::Level + 1; return $tb->ok( defined($response), $test_name ); } sub response_headers_are_deeply { my ( $req, $expected, $test_name ) = @_; carp 'DEPRECATED: Dancer2::Test. Please use Plack::Test instead' unless $NO_WARN; $test_name ||= "headers are as expected for " . _req_label($req); local $Test::Builder::Level = $Test::Builder::Level + 1; my $response = dancer_response( _expand_req($req) ); is_deeply( _sort_headers( $response->[1] ), _sort_headers($expected), $test_name ); } sub response_headers_include { my ( $req, $expected, $test_name ) = @_; carp 'DEPRECATED: Dancer2::Test. Please use Plack::Test instead' unless $NO_WARN; $test_name ||= "headers include expected data for " . _req_label($req); my $tb = Test::Builder->new; my $response = dancer_response($req); local $Test::Builder::Level = $Test::Builder::Level + 1; print STDERR "Headers are: " . Dumper( $response->[1] ) . "\n Expected to find header: " . Dumper($expected) if !$tb->ok( _include_in_headers( $response->[1], $expected ), $test_name ); } sub route_pod_coverage { require Pod::Simple::Search; require Pod::Simple::SimpleTree; my $all_routes = {}; foreach my $app ( @{ $_dispatcher->apps } ) { my $routes = $app->routes; my $available_routes = []; foreach my $method ( sort { $b cmp $a } keys %$routes ) { foreach my $r ( @{ $routes->{$method} } ) { # we don't need pod coverage for head next if $method eq 'head'; push @$available_routes, $method . ' ' . $r->spec_route; } } ## copy dereferenced array $all_routes->{ $app->name }{routes} = [@$available_routes] if @$available_routes; # Pod::Simple v3.30 excluded the current directory even when in @INC. # include the current directory as a search path; its backwards compatible # with previous version. my $undocumented_routes = []; my $file = Pod::Simple::Search->new->find( $app->name, '.' ); if ($file) { $all_routes->{ $app->name }{has_pod} = 1; my $parser = Pod::Simple::SimpleTree->new->parse_file($file); my $pod_dataref = $parser->root; my $found_routes = {}; for ( my $i = 0; $i < @$available_routes; $i++ ) { my $r = $available_routes->[$i]; my $app_string = lc $r; $app_string =~ s/\*/_REPLACED_STAR_/g; for ( my $idx = 0; $idx < @$pod_dataref; $idx++ ) { my $pod_part = $pod_dataref->[$idx]; next if !is_arrayref($pod_part); foreach my $ref_part (@$pod_part) { is_arrayref($ref_part) and push @$pod_dataref, $ref_part; } my $pod_string = lc $pod_part->[2]; $pod_string =~ s/['|"|\s]+/ /g; $pod_string =~ s/\s$//g; $pod_string =~ s/\*/_REPLACED_STAR_/g; if ( $pod_string =~ m/^$app_string$/ ) { $found_routes->{$app_string} = 1; next; } } if ( !$found_routes->{$app_string} ) { push @$undocumented_routes, $r; } } } else { ### no POD found $all_routes->{ $app->name }{has_pod} = 0; } if (@$undocumented_routes) { $all_routes->{ $app->name }{undocumented_routes} = $undocumented_routes; } elsif ( !$all_routes->{ $app->name }{has_pod} && @{ $all_routes->{ $app->name }{routes} } ) { ## copy dereferenced array $all_routes->{ $app->name }{undocumented_routes} = [ @{ $all_routes->{ $app->name }{routes} } ]; } } return $all_routes; } sub is_pod_covered { my ($test_name) = @_; $test_name ||= "is pod covered"; my $route_pod_coverage = route_pod_coverage(); my $tb = Test::Builder->new; local $Test::Builder::Level = $Test::Builder::Level + 1; foreach my $app ( @{ $_dispatcher->apps } ) { my %undocumented_route = ( map { $_ => 1 } @{ $route_pod_coverage->{ $app->name }{undocumented_routes} } ); $tb->subtest( $app->name . $test_name, sub { foreach my $route ( @{ $route_pod_coverage->{ $app->name }{routes} } ) { ok( !$undocumented_route{$route}, "$route is documented" ); } } ); } } sub import { my ( $class, %options ) = @_; my @applications; if ( ref $options{apps} eq ref( [] ) ) { @applications = @{ $options{apps} }; } else { my ( $caller, $script ) = caller; # if no app is passed, assume the caller is one. @applications = ($caller) if $caller->can('dancer_app'); } # register the apps to the test dispatcher $_dispatcher->apps( [ map { $_->dancer_app->finish(); $_->dancer_app; } @applications ] ); $class->export_to_level( 1, $class, @EXPORT ); } # private sub _req_label { my $req = shift; return ref $req eq 'Dancer2::Core::Response' ? 'response object' : ref $req eq 'Dancer2::Core::Request' ? join( ' ', map { $req->$_ } qw/ method path / ) : is_arrayref($req) ? join( ' ', @$req ) : "GET $req"; } sub _expand_req { my $req = shift; return is_arrayref($req) ? @$req : ( 'GET', $req ); } # Sort arrayref of headers (turn it into a list of arrayrefs, sort by the header # & value, then turn it back into an arrayref) sub _sort_headers { my @originalheaders = @{ shift() }; # take a copy we can modify my @headerpairs; while ( my ( $header, $value ) = splice @originalheaders, 0, 2 ) { push @headerpairs, [ $header, $value ]; } # We have an array of arrayrefs holding header => value pairs; sort them by # header then value, and return them flattened back into an arrayref return [ map {@$_} sort { $a->[0] cmp $b->[0] || $a->[1] cmp $b->[1] } @headerpairs ]; } # make sure the given header sublist is included in the full headers array sub _include_in_headers { my ( $full_headers, $expected_subset ) = @_; # walk through all the expected header pairs, make sure # they exist with the same value in the full_headers list # return false as soon as one is not. for ( my $i = 0; $i < scalar(@$expected_subset); $i += 2 ) { my ( $name, $value ) = ( $expected_subset->[$i], $expected_subset->[ $i + 1 ] ); return 0 unless _check_header( $full_headers, $name, $value ); } # we've found all the expected pairs in the $full_headers list return 1; } sub _check_header { my ( $headers, $key, $value ) = @_; for ( my $i = 0; $i < scalar(@$headers); $i += 2 ) { my ( $name, $val ) = ( $headers->[$i], $headers->[ $i + 1 ] ); return 1 if $name eq $key && $value eq $val; } return 0; } sub _req_to_response { my $req = shift; # already a response object return $req if ref $req eq 'Dancer2::Core::Response'; return dancer_response( is_arrayref($req) ? @$req : ( 'GET', $req ) ); } # make sure we have at least one app in the dispatcher, and if not, # we must have at this point an app within the caller sub _find_dancer_apps_for_dispatcher { return if scalar( @{ $_dispatcher->apps } ); for ( my $deep = 0; $deep < 5; $deep++ ) { my $caller = caller($deep); next if !$caller || !$caller->can('dancer_app'); return $_dispatcher->apps( [ $caller->dancer_app ] ); } croak "Unable to find a Dancer2 app, did you use Dancer2 in your test?"; } 1; __END__ =pod =encoding UTF-8 =head1 NAME Dancer2::Test - Useful routines for testing Dancer2 apps =head1 VERSION version 2.3.0 =head1 SYNOPSIS use Test::More; use Plack::Test; use HTTP::Request::Common; # install separately use YourDancerApp; my $app = YourDancerApp->to_app; my $test = Plack::Test->create($app); my $res = $test->request( GET '/' ); is( $res->code, 200, '[GET /] Request successful' ); like( $res->content, qr/hello, world/, '[GET /] Correct content' ); done_testing; =head1 DESCRIPTION B The routines provided by this module for testing Dancer2 apps are buggy and unnecessary. Instead, use the L module as shown in the SYNOPSIS above and ignore the functions in this documentation. Consult the L documentation for further details. This module will be removed from the Dancer2 distribution in the near future. You should migrate all tests that use it over to the L module and remove this module from your system. This module will throw warnings to remind you. For now, you can silence the warnings by setting the C option: $Dancer::Test::NO_WARN = 1; In the functions below, $test_name is always optional. =head1 FUNCTIONS =head2 dancer_response ($method, $path, $params, $arg_env); Returns a Dancer2::Core::Response object for the given request. Only $method and $path are required. $params is a hashref with 'body' as a string; 'headers' can be an arrayref or a HTTP::Headers object, 'files' can be arrayref of hashref, containing some files to upload: dancer_response($method, $path, { params => $params, body => $body, headers => $headers, files => [ { filename => '/path/to/file', name => 'my_file' } ], } ); A good reason to use this function is for testing POST requests. Since POST requests may not be idempotent, it is necessary to capture the content and status in one shot. Calling the response_status_is and response_content_is functions in succession would make two requests, each of which could alter the state of the application and cause Schrodinger's cat to die. my $response = dancer_response POST => '/widgets'; is $response->status, 202, "response for POST /widgets is 202"; is $response->content, "Widget #1 has been scheduled for creation", "response content looks good for first POST /widgets"; $response = dancer_response POST => '/widgets'; is $response->status, 202, "response for POST /widgets is 202"; is $response->content, "Widget #2 has been scheduled for creation", "response content looks good for second POST /widgets"; It's possible to test file uploads: post '/upload' => sub { return upload('image')->content }; $response = dancer_response(POST => '/upload', {files => [{name => 'image', filename => '/path/to/image.jpg'}]}); In addition, you can supply the file contents as the C key: my $data = 'A test string that will pretend to be file contents.'; $response = dancer_response(POST => '/upload', { files => [{name => 'test', filename => "filename.ext", data => $data}] }); You can also supply a hashref of headers: headers => { 'Content-Type' => 'text/plain' } =head2 response_status_is ($request, $expected, $test_name); Asserts that Dancer2's response for the given request has a status equal to the one given. response_status_is [GET => '/'], 200, "response for GET / is 200"; =head2 route_exists([$method, $path], $test_name) Asserts that the given request matches a route handler in Dancer2's registry. If the route would have returned a 404, the route still exists and this test will pass. Note that because Dancer2 uses the default route handler L to match files in the public folder when no other route matches, this test will always pass. You can disable the default route handlers in the configs but you probably want L or L route_exists [GET => '/'], "GET / is handled"; =head2 route_doesnt_exist([$method, $path], $test_name) Asserts that the given request does not match any route handler in Dancer2's registry. Note that this test is likely to always fail as any route not matched will be handled by the default route handler in L. This can be disabled in the configs. route_doesnt_exist [GET => '/bogus_path'], "GET /bogus_path is not handled"; =head2 response_status_isnt([$method, $path], $status, $test_name) Asserts that the status of Dancer2's response is not equal to the one given. response_status_isnt [GET => '/'], 404, "response for GET / is not a 404"; =head2 response_content_is([$method, $path], $expected, $test_name) Asserts that the response content is equal to the C<$expected> string. response_content_is [GET => '/'], "Hello, World", "got expected response content for GET /"; =head2 response_content_isnt([$method, $path], $not_expected, $test_name) Asserts that the response content is not equal to the C<$not_expected> string. response_content_isnt [GET => '/'], "Hello, World", "got expected response content for GET /"; =head2 response_content_like([$method, $path], $regexp, $test_name) Asserts that the response content for the given request matches the regexp given. response_content_like [GET => '/'], qr/Hello, World/, "response content looks good for GET /"; =head2 response_content_unlike([$method, $path], $regexp, $test_name) Asserts that the response content for the given request does not match the regexp given. response_content_unlike [GET => '/'], qr/Page not found/, "response content looks good for GET /"; =head2 response_content_is_deeply([$method, $path], $expected_struct, $test_name) Similar to response_content_is(), except that if response content and $expected_struct are references, it does a deep comparison walking each data structure to see if they are equivalent. If the two structures are different, it will display the place where they start differing. response_content_is_deeply [GET => '/complex_struct'], { foo => 42, bar => 24}, "got expected response structure for GET /complex_struct"; =head2 response_is_file ($request, $test_name); =head2 response_headers_are_deeply([$method, $path], $expected, $test_name) Asserts that the response headers data structure equals the one given. response_headers_are_deeply [GET => '/'], [ 'X-Powered-By' => 'Dancer2 1.150' ]; =head2 response_headers_include([$method, $path], $expected, $test_name) Asserts that the response headers data structure includes some of the defined ones. response_headers_include [GET => '/'], [ 'Content-Type' => 'text/plain' ]; =head2 route_pod_coverage() Returns a structure describing pod coverage in your apps for one app like this: package t::lib::TestPod; use Dancer2; =head1 NAME TestPod =head2 ROUTES =over =cut =item get "/in_testpod" testpod =cut get '/in_testpod' => sub { return 'get in_testpod'; }; get '/hello' => sub { return "hello world"; }; =item post '/in_testpod/*' post in_testpod =cut post '/in_testpod/*' => sub { return 'post in_testpod'; }; =back =head2 SPECIALS =head3 PUBLIC =over =item get "/me:id" =cut get "/me:id" => sub { return "ME"; }; =back =head3 PRIVAT =over =item post "/me:id" post /me:id =cut post "/me:id" => sub { return "ME"; }; =back =cut 1; route_pod_coverage; would return something like: { 't::lib::TestPod' => { 'has_pod' => 1, 'routes' => [ "post /in_testpod/*", "post /me:id", "get /in_testpod", "get /hello", "get /me:id" ], 'undocumented_routes' => [ "get /hello" ] } } =head2 is_pod_covered('is pod covered') Asserts that your apps have pods for all routes is_pod_covered 'is pod covered' to avoid test failures, you should document all your routes with one of the following: head1, head2,head3,head4, item. ex: =item get '/login' route to login =cut if you use: any '/myaction' => sub { # code } or any ['get', 'post'] => '/myaction' => sub { # code }; you need to create pods for each one of the routes created there. =head2 import When Dancer2::Test is imported, it should be passed all the applications that are supposed to be tested. If none passed, then the caller is supposed to be the sole application to test. # t/sometest.t use t::lib::Foo; use t::lib::Bar; use Dancer2::Test apps => ['t::lib::Foo', 't::lib::Bar']; =head1 AUTHOR Dancer Core Developers =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2026 by Alexis Sukrieh. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut Core.pm100644000765000024 140415267764222 15737 0ustar00jasonstaff000000000000Dancer2-2.3.0/lib/Dancer2package Dancer2::Core; # ABSTRACT: Core libraries for Dancer2 2.0 $Dancer2::Core::VERSION = '2.3.0'; use strict; use warnings; sub camelize { my ($value) = @_; my $camelized = ''; for my $word ( split /_/, $value ) { $camelized .= ucfirst($word); } return $camelized; } 1; __END__ =pod =encoding UTF-8 =head1 NAME Dancer2::Core - Core libraries for Dancer2 2.0 =head1 VERSION version 2.3.0 =head1 FUNCTIONS =head2 camelize Camelize a underscore-separated-string. =head1 AUTHOR Dancer Core Developers =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2026 by Alexis Sukrieh. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut author-distmeta.t100644000765000024 42415267764222 16210 0ustar00jasonstaff000000000000Dancer2-2.3.0/t#!perl BEGIN { unless ($ENV{AUTHOR_TESTING}) { print qq{1..0 # SKIP these tests are for testing by the author\n}; exit } } # This file was automatically generated by Dist::Zilla::Plugin::MetaTests use strict; use warnings; use Test::CPAN::Meta; meta_yaml_ok(); Releasing-Dancer2.md100644000765000024 475515267764222 16210 0ustar00jasonstaff000000000000Dancer2-2.3.0# Releasing Dancer2 This document is an overview of the process we go through when putting together a new release of Dancer2. This is a guide, not a bible - we don't follow this religiously - but it is better to do more of what's in here than less. ## Ongoing For all PRs and branches merged: - Make sure all tests pass on the branch to be merged. - Make sure Changes is updated. Include the GitHub issue or PR # in the changelog entry. - Make sure the author is reflected in the Contributors section of Dancer2.pm - `git merge --no-ff && git push` ## One week before release Notify the Core Team that a release is going out. Do this via the dancer-dev mailing list and Twist. If anyone has something to be merged, now is the time to bring it up. If the release needs to be delayed in order for someone to get a change into a release, this is the time to pump the brakes. ## Day of Release ### Pull the latest code from main Don't be That Person. Make sure everything you have is current. ### Ensure all changes are merged Each release should have a milestone in GitHub. Make sure all issues and PRs in the milestone are closed. If an issue or PR needs to move to a later release, do that now. ### Update the version in `dist.ini` We use [Semantic Versioning](https://semver.org/) for all Dancer2 releases. ### Do the release! Run `dzil release --all`. This will: - Run the test suite, including the author tests - Build a tarball for upload to PAUSE - Upload the new version to PAUSE - Commit `Dancer2.pm` and the new `README` file to the repo (after which, you need to `git push`) After this, you must `git push` to push the updated release files to GitHub. The test suite *must* pass before a release can be done. This is your last chance to resolve any test failures. Provided the tests pass, you will be prompted to continue the release process. Provided you have your PAUSE credentials set up, this will upload the new dist to PAUSE. If you don't have a credential file set up, you can manually update the new release via the PAUSE web interface. ### Send out release announcements Historically, release announcements have been sent out via the following channels: - dancer-users mailing list - Twitter/X - blogs.perl.org Going forward, we intend to also send out announcements via: - LinkedIn - Perl Community group on Facebook - dev.to - Medium Reshare from any accounts you have. Blog about it. ## After the Release Watch for bug reports, praise, criticism, and respond appropriately. forward_test_tcp.t100644000765000024 357715267764222 16503 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; { package App; use Dancer2; get '/' => sub { 'home:' . join( ',', params ); }; get '/bounce/' => sub { forward '/' }; get '/bounce/:withparams/' => sub { forward '/' }; get '/bounce2/adding_params/' => sub { forward '/', { withparams => 'foo' }; }; get '/go_to_post/' => sub { forward '/simple_post_route/', { foo => 'bar' }, { method => 'post' }; }; post '/simple_post_route/' => sub { 'post:' . join( ',', params ); }; post '/' => sub {'post-home'}; post '/bounce/' => sub { forward '/' }; } my $app = Dancer2->psgi_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; my $res = $cb->(GET "/"); is $res->code => 200; like $res->content => qr/home:/; $res = $cb->(GET "/bounce/"); is $res->code => 200; like $res->content => qr/home:/; $res = $cb->(GET "/bounce/thesethings/"); is $res->code => 200; is $res->content => 'home:withparams,thesethings'; $res = $cb->(GET "/bounce2/adding_params/"); is $res->code => 200; is $res->content => 'home:withparams,foo'; $res = $cb->(GET "/go_to_post/"); is $res->code => 200; is $res->content => 'post:foo,bar'; $res = $cb->(GET "/bounce/"); is $res->header('Content-Length') => 5; is $res->header('Content-Type') => 'text/html; charset=utf-8'; $res = $cb->(POST "/"); is $res->code => 200; is $res->content => 'post-home'; $res = $cb->(POST "/bounce/"); is $res->code => 200; is $res->content => 'post-home'; is $res->header('Content-Length') => 9; is $res->header('Content-Type') => 'text/html; charset=utf-8'; }; done_testing(); dsl_path_dirname.t100644000765000024 421415267764222 16414 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; # Dancer2 2.1.0 regression: the `path` (and `dirname`) DSL keywords silently # drop their first argument when called with 2+ path segments. # # Root cause (Dancer2::Core::DSL, 2.1.0): # sub _path_obj { shift and Path::Tiny::path(@_) } # sub path { shift and _path_obj(@_)->stringify } # # path() already shifts off its DSL invocant before calling _path_obj(@_). # _path_obj then shifts *again*, as if it too were called as a method, but # it's a plain function call -- so it eats the first real path segment # instead of an invocant. dirname() shares the same broken helper. # # Documented behavior (Dancer2::Manual::Keywords, "path"): "Concatenates # multiple paths together" -- i.e. path('t','views') should be 't/views'. # Confirmed correct under Dancer2 2.0.1; broken under 2.1.0. package TestApp; use Dancer2; package main; my $dsl = TestApp->dsl; is( $dsl->path('t', 'views'), 't/views', 'path() with 2 segments keeps all segments (documented: concatenates)' ); is( $dsl->path('a', 'b', 'c'), 'a/b/c', 'path() with 3 segments keeps all segments' ); # Single-argument calls are even worse: _path_obj's spurious shift eats the # only argument, leaving Path::Tiny::path() with zero args, which dies # outright instead of returning a wrong-but-harmless string. my $single_arg_result = eval { $dsl->dirname('t/views/x.tt') }; my $single_arg_error = $@; ok( defined $single_arg_result && !$single_arg_error, 'dirname() with a single argument does not crash' ) or diag("dirname('t/views/x.tt') died: $single_arg_error"); is( $single_arg_result, 't/views', 'dirname() of a single-argument path is correct' ) if defined $single_arg_result; # Knock-on effect: this is exactly how Dancer2::Plugin::FlashNote's test # suite sets its views directory (t/01-default.t): # setting views => path( 't', 'views' ); # Under the bug, `views` silently becomes just 'views' (the 't' is lost), # so every template lookup 404s and every route handler that renders a # template returns a 500 -- which is what caused ~114/160 FlashNote # subtests to fail under 2.1.0 while the plugin's own code never changed. done_testing(); cookie.t100644000765000024 3052215267764222 16320 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/unit/coreuse strict; use warnings; use Test::More; use Test::Fatal qw; use Dancer2::Core::Cookie; use Dancer2::Core::Time; # Cookie header generation and the time expressions that feed the Expires # attribute. # # Time expressions come in two kinds, and the difference is easy to get wrong: # # a bare digit string -> an absolute Unix epoch # anything else -> an offset from now ("2 hours", "1h30m", "1w") # # So expires => 3600 is 1 Jan 1970, not "in an hour". That is what # Dancer2::Core::Cookie's own documentation says ("Unix epoch time like # 1288817656 ... It also supports a human readable offset from the current time # such as '2 hours'"), and Dancer2::Core::Session compensates for it by adding # time() before handing the value over. Both halves are pinned below. # # Absolute epochs are used wherever an exact string is asserted, so no clock # needs faking. 1288817656 is the value from the module's own POD, together # with the string that POD says it produces. my $POD_EPOCH = 1288817656; my $POD_STRING = 'Wed, 03-Nov-2010 20:54:16 GMT'; sub header_for { return Dancer2::Core::Cookie->new(@_)->pp_to_header; } # Split a Set-Cookie header into its name=value pair plus the sorted set of # its attributes, for comparing two headers that must carry the same # information without asserting an order they do not have. RFC 6265 gives # Set-Cookie attributes no significant order, and the XS builder does not # emit them in a stable one -- see the XS-versus-pure-Perl subtest below. sub header_parts { my ( $name_value, @attributes ) = split /; /, shift; return [ $name_value, sort @attributes ]; } # --- time expressions ---------------------------------------------------- subtest 'a bare number is an absolute epoch' => sub { my $time = Dancer2::Core::Time->new( expression => $POD_EPOCH ); is( $time->epoch, $POD_EPOCH, 'the epoch is the number itself' ); is( $time->gmt_string, $POD_STRING, 'and formats to the string the documentation promises' ); # Not time() + 1288817656, which is what treating it as an offset would # give. Asserted explicitly because it is the confusing half. cmp_ok( $time->epoch, '<', time, 'a 2010 epoch stays in the past rather than being added to now' ); # The consequence, stated plainly: a number that looks like a duration is # interpreted as an epoch in 1970. is( Dancer2::Core::Time->new( expression => 3600 )->gmt_string, 'Thu, 01-Jan-1970 01:00:00 GMT', 'expires => 3600 means one hour after the epoch, not one hour from now' ); }; subtest 'a human-readable expression is an offset from now' => sub { my $now = time; my %offset = ( '1s' => 1, '1m' => 60, '1h' => 3600, '2 hours' => 7200, '1d' => 86400, '1w' => 604800, '1y' => 31536000, '1h30m' => 5400, '1.5h' => 5400, '01:30' => 5400, '01:30:00' => 5400, ); for my $expression ( sort keys %offset ) { my $seconds = $offset{$expression}; my $time = Dancer2::Core::Time->new( expression => $expression ); is( $time->seconds, $seconds, "'$expression' is $seconds seconds" ); # The epoch is now + seconds. Allowing a few seconds of slack keeps # this from being flaky if the clock ticks mid-test. cmp_ok( $time->epoch, '>=', $now + $seconds, "'$expression' resolves to a time at least that far ahead" ); cmp_ok( $time->epoch, '<=', $now + $seconds + 10, "'$expression' resolves to a time not much further ahead" ); } # 'M' is months and 'm' is minutes: single-character units are not # lower-cased, and confusing them is a factor of 43200. is( Dancer2::Core::Time->new( expression => '1M' )->seconds, 2592000, 'a capital M is one month' ); is( Dancer2::Core::Time->new( expression => '1m' )->seconds, 60, 'while a lower-case m is one minute' ); # A negative offset is how a cookie is expired in the past - this is what # destroy_session relies on. my $past = Dancer2::Core::Time->new( expression => -86400 ); is( $past->seconds, -86400, 'a negative expression stays negative' ); cmp_ok( $past->epoch, '<', time, 'and resolves to a time in the past' ); }; subtest 'an unparseable expression is passed through unchanged' => sub { # Deliberate: Dancer2::Core::Time documents that "anything else is used # verbatim". Silently turning it into a wrong date would be worse - the # value reaches the header where it is visible. for my $expression ( 'not a time', '3 fortnights', '2 hours and cheese' ) { my $time = Dancer2::Core::Time->new( expression => $expression ); is( $time->seconds, $expression, "'$expression': seconds is verbatim" ); is( $time->epoch, $expression, "'$expression': epoch is verbatim" ); is( $time->gmt_string, $expression, "'$expression': gmt_string is verbatim" ); } # A partially-valid expression is rejected whole, not silently truncated # to the part that parsed. isnt( Dancer2::Core::Time->new( expression => '2 hours and cheese' )->seconds, 7200, 'the parseable prefix of a bad expression is not used on its own' ); }; # --- cookie headers ------------------------------------------------------ subtest 'a minimal cookie gets a path and HttpOnly' => sub { is( header_for( name => 'n', value => 'v' ), 'n=v; Path=/; HttpOnly', 'name, value, the default path, and HttpOnly' ); }; subtest 'every attribute reaches the header' => sub { my $header = header_for( name => 'sid', value => 'abc', path => '/app', domain => '.example.com', expires => $POD_EPOCH, same_site => 'Strict', secure => 1, http_only => 1, ); # Asserted as one exact string: it pins the attribute order too, which a # set of independent regexes would not. is( $header, "sid=abc; Path=/app; Expires=$POD_STRING; Domain=.example.com;" . ' SameSite=Strict; Secure; HttpOnly', 'all attributes are present, in order', ); # And individually, so a failure says which one went missing. like( $header, qr/\bPath=\/app\b/, 'Path' ); like( $header, qr/\bExpires=\Q$POD_STRING\E/, 'Expires' ); like( $header, qr/\bDomain=\.example\.com\b/, 'Domain' ); like( $header, qr/\bSameSite=Strict\b/, 'SameSite' ); like( $header, qr/\bSecure\b/, 'Secure' ); like( $header, qr/\bHttpOnly\b/, 'HttpOnly' ); }; subtest 'the boolean attributes are omitted rather than negated' => sub { # There is no "Secure=0" in HTTP - a false flag means the attribute is # absent. unlike( header_for( name => 'n', value => 'v', secure => 0 ), qr/Secure/, 'secure => 0 omits Secure' ); like( header_for( name => 'n', value => 'v', secure => 1 ), qr/; Secure\b/, 'secure => 1 emits it' ); unlike( header_for( name => 'n', value => 'v', http_only => 0 ), qr/HttpOnly/, 'http_only => 0 omits HttpOnly' ); like( header_for( name => 'n', value => 'v', http_only => 1 ), qr/HttpOnly/, 'http_only => 1 emits it' ); # HttpOnly is on by default, so leaving it out is not the same as # turning it off. like( header_for( name => 'n', value => 'v' ), qr/HttpOnly/, 'HttpOnly is emitted when nothing was said about it' ); # A falsy path is omitted too. unlike( header_for( name => 'n', value => 'v', path => '' ), qr/Path/, 'an empty path omits the Path attribute' ); }; subtest 'same_site only accepts the values the standard defines' => sub { for my $value (qw< Strict Lax None >) { like( header_for( name => 'n', value => 'v', same_site => $value ), qr/\bSameSite=$value\b/, "SameSite=$value is accepted" ); } # A typo is refused at construction rather than emitted as a header a # browser would ignore. ok( defined exception { Dancer2::Core::Cookie->new( name => 'n', value => 'v', same_site => 'strict' ) }, 'a lower-case value is refused', ); ok( defined exception { Dancer2::Core::Cookie->new( name => 'n', value => 'v', same_site => 'Nonsense' ) }, 'and so is a value that is not in the enum', ); }; subtest 'a multi-value cookie keeps every value, escaped' => sub { is( header_for( name => 'n', value => [ 'a', 'b', 'c' ] ), 'n=a&b&c; Path=/; HttpOnly', 'values are joined with an ampersand' ); # Each value is escaped individually, so a separator inside a value cannot # be mistaken for a separator between values. is( header_for( name => 'n', value => [ 'a b', 'c;d' ] ), 'n=a%20b&c%3Bd; Path=/; HttpOnly', 'each value is URI-escaped on its own' ); # A single value with every character that would otherwise confuse a # parser. is( header_for( name => 'n', value => 'a b;c,d=e&f' ), 'n=a%20b%3Bc%2Cd%3De%26f; Path=/; HttpOnly', 'space, semicolon, comma, equals and ampersand are all escaped' ); my $cookie = Dancer2::Core::Cookie->new( name => 'n', value => [ 'a', 'b' ] ); is_deeply( [ $cookie->value ], [ 'a', 'b' ], 'the accessor returns every value in list context' ); is_deeply( [ $cookie->values ], [ 'a', 'b' ], 'and values() does the same' ); is( scalar $cookie->value, 'a', 'while scalar context gives the first' ); is( "$cookie", 'a', 'and stringification gives the first, per the documented overload' ); }; subtest 'an unparseable expires reaches the header verbatim' => sub { # Following from the Time behavior above: the bad value is visible in the # header rather than being turned into a plausible-looking wrong date. my $header = header_for( name => 'n', value => 'v', expires => 'not a time' ); is( $header, 'n=v; Path=/; Expires=not a time; HttpOnly', 'the expression appears as given' ); unlike( $header, qr/1970|GMT/, 'and is not converted into a date' ); }; subtest 'the XS header builder agrees with the pure-Perl one' => sub { # Dancer2::Core::Cookie picks one of two implementations at load time and # aliases to_header to it. Both must produce the same header, or which # optional modules happen to be installed changes the response. SKIP: { skip 'HTTP::XSCookies is not installed, so xs_to_header cannot run', 3 if !Dancer2::Core::Cookie::_USE_XS(); my %args = ( name => 'sid', value => 'abc', path => '/app', expires => $POD_EPOCH, secure => 1, http_only => 1, ); my $cookie = Dancer2::Core::Cookie->new(%args); my $xs = $cookie->xs_to_header; like( $xs, qr/^sid=abc/, 'the XS builder emits the name and value' ); like( $xs, qr/\bSecure\b/, 'and the Secure attribute' ); # Multi-value cookies are explicitly delegated to the pure-Perl # builder, so those must match exactly. my $multi = Dancer2::Core::Cookie->new( name => 'n', value => [ 'a', 'b' ] ); is( $multi->xs_to_header, $multi->pp_to_header, 'a multi-value cookie is delegated to the pure-Perl builder' ); } # Whichever implementation is active, to_header must be one of the two - # asserted unconditionally so this subtest is never vacuous. # # Compared as a set rather than as a string. HTTP::XSCookies::bake_cookie # is handed a hashref and walks it to build the attribute list, so with # XS active the order of Path, HttpOnly and the rest varies from call to # call -- two calls in one process can disagree: # # to_header n=v; Path=/; HttpOnly # xs_to_header n=v; HttpOnly; Path=/ # # Asserting the string would fail about half the time. RFC 6265 gives # these attributes no significant order, so the content is what matters # and is all this can honestly check. my $cookie = Dancer2::Core::Cookie->new( name => 'n', value => 'v' ); my $expected = Dancer2::Core::Cookie::_USE_XS() ? $cookie->xs_to_header : $cookie->pp_to_header; is_deeply( header_parts( $cookie->to_header ), header_parts($expected), 'to_header is aliased to the implementation this build selected' ); }; done_testing(); config2000755000765000024 015267764222 14120 5ustar00jasonstaff000000000000Dancer2-2.3.0/tconfig.yml100644000765000024 22415267764222 16226 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/config2main: 1 charset: 'UTF-8' show_stacktrace: 1 strict_config: 0 application: feature_1: foo feature_2: bar feature_3: baz feature_4: blat lib000755000765000024 015267764222 14444 5ustar00jasonstaff000000000000Dancer2-2.3.0/t/app/t2App3.pm100644000765000024 7015267764222 15702 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/app/t2/libpackage App3; use strict; use warnings; use Dancer2; 1; lib000755000765000024 015267764222 14445 5ustar00jasonstaff000000000000Dancer2-2.3.0/t/app/t3App4.pm100644000765000024 7215267764222 15706 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/app/t3/libpackage App4; use strict; use warnings; use Dancer2; 1; lib000755000765000024 015267764222 14443 5ustar00jasonstaff000000000000Dancer2-2.3.0/t/app/t1App1.pm100644000765000024 7115267764222 15700 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/app/t1/libpackage App1; use strict; use warnings; use Dancer2; 1; MyDancerDSL.pm100644000765000024 163315267764222 16105 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/libpackage MyDancerDSL; use Moo; extends 'Dancer2::Core::DSL'; around dsl_keywords => sub { my $orig = shift; my $keywords = $orig->(@_); $keywords->{gateau} = { is_global => 0 }; # cookie $keywords->{moteur} = { is_global => 1 }; # engine $keywords->{stop} = { is_global => 0 }; # halt $keywords->{prend} = { is_global => 1, prototype => '@' }; # get $keywords->{envoie} = { is_global => 1, prototype => '$&' }; # post $keywords->{entete} = { is_global => 0 }; #header $keywords->{proto} = { is_global => 1, prototype => '&' }; # prototype return $keywords; }; sub gateau { goto &Dancer2::Core::DSL::cookie } sub moteur { goto &Dancer2::Core::DSL::engine } sub stop { goto &Dancer2::Core::DSL::halt } sub prend { goto &Dancer2::Core::DSL::get } sub envoie { goto &Dancer2::Core::DSL::post } sub entete { goto &Dancer2::Core::DSL::header } sub proto { $_[1]->() } 1; TemplateFoo.pm100644000765000024 31515267764222 16233 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/libpackage TemplateFoo; # Custom Template::Toolkit class use base 'Template'; sub process { my ($self, $template, $vars, $outstream, @opts) = @_; $$outstream = "Custom Render Template"; 1; } 1; auto_page.tt100644000765000024 4115267764222 16336 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/viewsHey! This is Auto Page wörking. request_data.t100644000765000024 240015267764222 16355 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/dsluse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; subtest 'request data basic' => sub { { package App::Body::Str; ## no critic use Dancer2; post '/' => sub { my $data = request_data; ::is( $data, 'a string body', 'string content ok' ); }; } my $app = Plack::Test->create( App::Body::Str->to_app ); my $res = $app->request( POST '/', Content_Type => 'text/plain', Content => "a string body" ); ok( $res->is_success, 'Successful request' ); }; subtest 'request data serialized' => sub { { package App::Body::JSON; ## no critic use Dancer2; set serializer => 'JSON'; post '/' => sub { my $data = request_data; ::is_deeply( $data, { body => { is => [ "json" ] } }, 'json content ok' ); return +{ ok => 1 }; }; } my $app = Plack::Test->create( App::Body::JSON->to_app ); my $res = $app->request( POST '/', Content_Type => 'application/json', Content => '{"body":{"is":["json"]}}' ); ok( $res->is_success, 'Successful request' ); }; done_testing(); context-in-before.t100644000765000024 211715267764222 16447 0ustar00jasonstaff000000000000Dancer2-2.3.0/t#!perl use strict; use warnings; use Test::More tests => 10; use Plack::Test; use HTTP::Request::Common; my $before; { package OurApp; use Dancer2 '!pass'; use Test::More; hook before => sub { my $ctx = shift; isa_ok( $ctx, 'Dancer2::Core::App', 'Context is actually an app now', ); is( $ctx->name, 'OurApp', 'It is the correct app' ); can_ok( $ctx, 'app' ); my $app = $ctx->app; isa_ok( $app, 'Dancer2::Core::App', 'When called ->app, we get te app again', ); is( $app->name, 'OurApp', 'It is the correct app' ); is( $ctx, $app, 'Same exact application (by reference)' ); $before++; }; get '/' => sub {'OK'}; } my $app = OurApp->to_app; isa_ok( $app, 'CODE', 'Got app' ); test_psgi $app, sub { my $cb = shift; my $res = $cb->( GET '/' ); is( $res->code, 200, '[GET /] status OK' ); is( $res->content, 'OK', '[GET /] content OK' ); ok( $before == 1, 'before hook called' ); }; multipart_content.t100644000765000024 216215267764222 16672 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; { package App; use Dancer2; get '/' => sub {1}; } # # Test for this issue: https://github.com/PerlDancer/Dancer2/issues/1507 # When a request comes with Content-Type: multipart/form-data with no boundary, # Dancer currently wrongly returns HTTP code 500 Internal Server Error. # It should return HTTP code 400 Bad Request. # We also test that a request with Content-Type: multipart/form-data boundary=------boundary-------' returns 200. my $app = App->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; is( $cb->( GET '/' => 'Content-Type' => 'multipart/form-data' )->code, 400, 'multipart with incorrect boundary returns 400', ); my $headers = is( $cb->( GET '/' => 'Content-Type' => 'Content-Type: multipart/form-data boundary=------boundary-------', )->code, 200, 'Providing multipart with correct boundary works', ); }; done_testing(); session_lifecycle.t100644000765000024 1461215267764222 16644 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; use HTTP::Cookies; use lib 't/lib'; { package App; use Dancer2; set session => 'Simple'; set show_stacktrace => 1; get '/no_session_data' => sub { return "session not modified"; }; get '/set_session/*' => sub { my ($name) = splat; session name => $name; }; get '/read_session' => sub { my $name = session('name') || ''; "name='$name'"; }; get '/change_session_id' => sub { app->change_session_id; }; get '/destroy_session' => sub { my $name = session('name') || ''; app->destroy_session; return "destroyed='$name'"; }; get '/churn_session' => sub { app->destroy_session; session name => 'damian'; return "churned"; }; } my $url = 'http://localhost'; my $test = Plack::Test->create( App->to_app ); my $app = Dancer2->runner->apps->[0]; for my $engine (qw(YAML Simple SimpleNoChangeId)) { # clear current session engine, and rebuild for the test # This is *really* messy, playing in object hashrefs.. delete $app->{session_engine}; $app->config->{session} = $engine; $app->session_engine; # trigger a build my $jar = HTTP::Cookies->new(); subtest "[$engine] No cookie set if session not referenced" => sub { my $res = $test->request(GET "$url/no_session_data"); ok $res->is_success, "/no_session_data" or diag explain $res; $jar->extract_cookies($res); ok(!$jar->as_string, 'No cookie set'); }; subtest "[$engine] No empty session created if session read attempted" => sub { my $res = $test->request(GET "$url/read_session"); ok $res->is_success, "/read_session"; $jar->extract_cookies($res); ok(!$jar->as_string, 'No cookie set'); }; my $sid1; subtest "[$engine] Set value into session" => sub { my $res = $test->request(GET "$url/set_session/larry"); ok $res->is_success, "/set_session/larry"; $jar->extract_cookies($res); ok($jar->as_string, 'Cookie set'); # extract SID $jar->scan(sub { $sid1 = $_[2] }); ok($sid1, 'Got SID from cookie'); }; subtest "[$engine] Read value back" => sub { # read value back my $req = GET "$url/read_session"; $jar->add_cookie_header($req); my $res = $test->request($req); ok $res->is_success, "/read_session"; $jar->clear; ok(!$jar->as_string, 'Jar cleared'); $jar->extract_cookies($res); ok($jar->as_string, 'session cookie set again'); like $res->content, qr/name='larry'/, "session value looks good"; }; subtest "[$engine] Session cookie persists even if we do not touch sessions" => sub { my $req = GET "$url/no_session_data"; $jar->add_cookie_header($req); my $res = $test->request($req); ok $res->is_success, "/no_session_data"; $jar->clear; ok(!$jar->as_string, 'Jar cleared'); $jar->extract_cookies($res); ok($jar->as_string, 'session cookie set again'); }; my $sid2; subtest "[$engine] Change session ID" => sub { my $req = GET "$url/change_session_id"; $jar->add_cookie_header($req); my $res = $test->request($req); ok $res->is_success, "/change_session_id"; $jar->clear; ok(!$jar->as_string, 'Jar cleared'); $jar->extract_cookies($res); ok($jar->as_string, 'session cookie set again'); # extract SID $jar->scan(sub { $sid2 = $_[2] }); isnt $sid2, $sid1, "New session has different ID"; is $res->content, $sid2, "new session ID returned"; }; subtest "[$engine] Read value back after change_session_id" => sub { # read value back my $req = GET "$url/read_session"; $jar->add_cookie_header($req); my $res = $test->request($req); ok $res->is_success, "/read_session"; $jar->clear; ok(!$jar->as_string, 'Jar cleared'); $jar->extract_cookies($res); ok($jar->as_string, 'session cookie set again'); like $res->content, qr/name='larry'/, "session value looks good"; }; subtest "[$engine] Destroy session and check that cookies expiration is set" => sub { my $req = GET "$url/destroy_session"; $jar->add_cookie_header($req); my $res = $test->request($req); ok $res->is_success, "/destroy_session"; ok($jar->as_string, 'We have a cookie before reading response'); $jar->extract_cookies($res); ok(!$jar->as_string, 'Cookie was removed from jar'); }; subtest "[$engine] Session cookie not sent after session destruction" => sub { my $req = GET "$url/no_session_data"; $jar->add_cookie_header($req); my $res = $test->request($req); ok $res->is_success, "/no_session_data"; ok(!$jar->as_string, 'Jar is empty'); $jar->extract_cookies($res); ok(!$jar->as_string, 'Jar still empty (no new session cookie)'); }; my $sid3; subtest "[$engine] Set value into session again" => sub { my $res = $test->request(GET "$url/set_session/curly"); ok $res->is_success, "/set_session/larry"; $jar->extract_cookies($res); ok($jar->as_string, 'session cookie set'); # extract SID $jar->scan(sub { $sid3 = $_[2] }); isnt $sid3, $sid2, "New session has different ID"; }; subtest "[$engine] Destroy and create a session in one request" => sub { my $req = GET "$url/churn_session"; $jar->add_cookie_header($req); my $res = $test->request($req); ok $res->is_success, "/churn_session"; $jar->extract_cookies($res); ok($jar->as_string, 'session cookie set'); my $sid4; $jar->scan(sub { $sid4 = $_[2] }); isnt $sid4, $sid3, "Changed session has different ID"; }; subtest "[$engine] Read value back" => sub { my $req = GET "$url/read_session"; $jar->add_cookie_header($req); my $res = $test->request($req); ok $res->is_success, "/read_session"; $jar->extract_cookies($res); ok($jar->as_string, "session cookie set"); like $res->content, qr/name='damian'/, "session value looks good"; }; } done_testing; request.t100644000765000024 3144015267764222 16537 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/unit/coreuse strict; use warnings; use Test::More; use Test::Fatal qw; use Encode (); use Dancer2::Core::Request; # Unit tests: request objects are built straight from a PSGI environment, with # no app and no dispatcher, so a failure here names the request layer rather # than something further up. # # Two accessor families coexist and do not agree, deliberately: # # params() - the legacy flat hash. Repeated values become an # arrayref, and it is where splat/captures live. # *_parameters() - Hash::MultiValue, per source, plus a merged view. # splat and captures are kept out of these. # # Both are asserted, because "fixing" either to look like the other would be a # silent behavior change for existing applications. # A minimal but complete PSGI env. CONTENT_LENGTH matters: without it # Plack::Request will not read the body at all. sub psgi_env { my %opt = @_; my $body = defined $opt{body} ? $opt{body} : ''; open my $input, '<', \$body or die "cannot open in-memory body: $!"; return { REQUEST_METHOD => $opt{method} || 'GET', PATH_INFO => defined $opt{path} ? $opt{path} : '/', QUERY_STRING => defined $opt{query} ? $opt{query} : '', SCRIPT_NAME => '', SERVER_NAME => 'localhost', SERVER_PORT => 80, CONTENT_LENGTH => length $body, 'psgi.url_scheme' => 'http', 'psgi.input' => $input, 'psgi.errors' => \*STDERR, ( $opt{ctype} ? ( CONTENT_TYPE => $opt{ctype} ) : () ), %{ $opt{extra} || {} }, }; } sub form_request { my %opt = @_; return Dancer2::Core::Request->new( env => psgi_env( %opt, ctype => 'application/x-www-form-urlencoded' ), ( $opt{strict_utf8} ? ( strict_utf8 => 1 ) : () ), ); } subtest 'route parameters outrank body, body outranks query' => sub { my $req = form_request( method => 'POST', query => 'who=query&only_query=q', body => 'who=body&only_body=b', ); # Before a route matches there are no route parameters, so body wins. is( $req->params->{who}, 'body', 'a body parameter beats a query parameter of the same name' ); # This is what App::_dispatch does once a route matches. $req->_set_route_params( { who => 'route' } ); $req->_set_route_parameters( { who => 'route' } ); is( $req->params->{who}, 'route', 'a route parameter beats both' ); # The losers are still reachable through their own source. is( $req->params('query')->{who}, 'query', 'the query value is still there' ); is( $req->params('body')->{who}, 'body', 'the body value is still there' ); is( $req->params('route')->{who}, 'route', 'and so is the route value' ); # Names that appear in only one place are unaffected by any of this. is( $req->params->{only_query}, 'q', 'a query-only name comes through' ); is( $req->params->{only_body}, 'b', 'a body-only name comes through' ); like( exception { $req->params('nonsense') }, qr/Unknown source params/, 'an unknown source is refused rather than silently returning nothing' ); # The merged Hash::MultiValue keeps all three, in query-body-route order. is_deeply( [ $req->parameters->get_all('who') ], [ 'query', 'body', 'route' ], 'the merged view keeps every value, query first and route last', ); is( scalar $req->parameters->get('who'), 'route', 'and its single-value read gives the last one, so route still wins' ); }; subtest 'a repeated query parameter keeps every value' => sub { my $req = Dancer2::Core::Request->new( env => psgi_env( query => 'a=1&a=2&a=3&b=solo' ) ); is_deeply( $req->params->{a}, [ '1', '2', '3' ], 'params() collapses a repeated name to an arrayref of all values' ); is( $req->params->{b}, 'solo', 'a single value stays a plain scalar, not a one-element array' ); is_deeply( [ $req->query_parameters->get_all('a') ], [ '1', '2', '3' ], 'get_all on the query parameters returns all three' ); is( scalar $req->query_parameters->get('a'), '3', 'and the single-value read gives the last, per Hash::MultiValue' ); }; subtest 'UTF-8 in the query string and the path is decoded to characters' => sub { # 'été' and '/café' as the bytes a client would actually send. my $req = Dancer2::Core::Request->new( env => psgi_env( query => 'name=%C3%A9t%C3%A9', path => "/caf\xc3\xa9", ) ); my $name = $req->params->{name}; ok( utf8::is_utf8($name), 'the query value is decoded, not raw bytes' ); is( length $name, 3, 'and is three characters, not five bytes' ); is( Encode::encode_utf8($name), "\xc3\xa9t\xc3\xa9", 'round-tripping it reproduces the original bytes' ); my $path = $req->path; ok( utf8::is_utf8($path), 'the path is decoded too' ); is( length $path, 5, 'and is five characters, not six bytes' ); my $via_hmv = scalar $req->query_parameters->get('name'); ok( utf8::is_utf8($via_hmv), 'the Hash::MultiValue accessor decodes as well, not only params()' ); is( $via_hmv, $name, 'and agrees with params()' ); }; subtest 'invalid UTF-8 is passed through with a warning by default' => sub { my $req = Dancer2::Core::Request->new( env => psgi_env( query => 'x=%FF%FE' ) ); my @warnings; my $value = do { local $SIG{__WARN__} = sub { push @warnings, $_[0] }; $req->params->{x}; }; # Lenient mode: the request survives and the caller gets the bytes. ok( !utf8::is_utf8($value), 'the value is left as bytes, undecoded' ); is( $value, "\xFF\xFE", 'and the bytes are unchanged' ); is( scalar @warnings, 1, 'exactly one warning is emitted' ); like( $warnings[0], qr/Invalid UTF-8 in query parameters/, 'naming what was invalid and where' ); like( $warnings[0], qr/leaving bytes unchanged/, 'and saying what it did about it' ); }; subtest 'invalid UTF-8 goes to the PSGI logger when there is one' => sub { my @logged; my $req = Dancer2::Core::Request->new( env => psgi_env( query => 'x=%FF%FE', extra => { 'psgix.logger' => sub { push @logged, $_[0] } }, ) ); my @warnings; my $value = do { local $SIG{__WARN__} = sub { push @warnings, $_[0] }; $req->params->{x}; }; is( scalar @logged, 1, 'the message goes to the PSGI logger' ); is( $logged[0]{level}, 'warning', 'at warning level' ); like( $logged[0]{message}, qr/Invalid UTF-8 in query parameters/, 'with the same message' ); # This is the point of the branch: a server that provides a logger does # not also get warnings on STDERR. is( scalar @warnings, 0, 'and not also to STDERR' ); is( $value, "\xFF\xFE", 'the bytes still come through unchanged' ); }; subtest 'invalid UTF-8 is fatal when strict_utf8 is on' => sub { my $req = Dancer2::Core::Request->new( env => psgi_env( query => 'x=%FF%FE' ), strict_utf8 => 1, ); my $err = exception { $req->params->{x} }; ok( defined $err, 'reading the parameter throws' ); like( $err, qr/Invalid UTF-8 in query parameters/, 'naming what was invalid and where' ); unlike( $err, qr/leaving bytes unchanged/, 'and not claiming to have carried on' ); # Valid UTF-8 is unaffected by strict mode. my $ok = Dancer2::Core::Request->new( env => psgi_env( query => 'x=%C3%A9' ), strict_utf8 => 1, ); is( $ok->params->{x}, "\x{e9}", 'valid UTF-8 still decodes normally under strict_utf8' ); }; subtest 'the XS and pure-Perl query parsers agree' => sub { # This distribution must stay usable with no XS modules installed # (CLAUDE.md: "core must stay pure-Perl-loadable"), so both parsers have # to produce the same result. Whichever one is active on this machine, # the other is still exercised here. # # CGI::Deurl::XS is optional. Forcing $XS_PARSE_QUERY_STRING on below # calls CGI::Deurl::XS::parse_query_string() directly, which dies with # "Undefined subroutine" if the module was never loaded - so skip # outright rather than let that take the rest of the file down with it. unless ( eval { require CGI::Deurl::XS; 1 } ) { plan skip_all => 'CGI::Deurl::XS is not installed'; } my $query = 'a=1&a=2&plus=a+b&utf=%C3%A9&empty=&solo=x'; my %result; for my $mode ( [ xs => 1 ], [ pp => 0 ] ) { my ( $label, $on ) = @$mode; no warnings 'once'; local $Dancer2::Core::Request::XS_PARSE_QUERY_STRING = $on; local $Dancer2::Core::Request::XS_URL_DECODE = $on; my $req = Dancer2::Core::Request->new( env => psgi_env( query => $query ) ); $result{$label} = $req->params; } is_deeply( $result{pp}, $result{xs}, 'both parsers produce identical parameters for the same query string' ); # Spot-check the things a hand-rolled parser gets wrong, so this is not # just "two broken parsers agree". for my $label ( 'xs', 'pp' ) { my $p = $result{$label}; is_deeply( $p->{a}, [ '1', '2' ], "$label: repeated name becomes a list" ); is( $p->{plus}, 'a b', "$label: '+' decodes to a space" ); is( $p->{utf}, "\x{e9}", "$label: percent-encoded UTF-8 decodes" ); is( $p->{empty}, '', "$label: a name with no value is the empty string" ); is( $p->{solo}, 'x', "$label: an ordinary pair survives" ); } }; subtest 'splat and captures stay out of route_parameters' => sub { my $req = Dancer2::Core::Request->new( env => psgi_env( query => 'q=1' ) ); # App::_dispatch passes the *same* hashref to both setters, in this order, # and the second one deletes splat/captures from it. So the order is # load-bearing: reversed, splat() below would come back empty. my $match = { id => '42', splat => [ 'a', 'b' ], captures => { name => 'x' } }; $req->_set_route_params($match); $req->_set_route_parameters($match); # The legacy hash keeps them - that is how splat() and captures() work. is_deeply( [ $req->splat ], [ 'a', 'b' ], 'splat() returns the captures' ); is_deeply( $req->captures, { name => 'x' }, 'captures() returns the named ones' ); # The Hash::MultiValue accessors must not carry them, or an application # iterating its route parameters would see reserved names as user data. is_deeply( [ sort $req->route_parameters->keys ], ['id'], 'route_parameters holds only the real named parameter' ); is_deeply( [ sort $req->parameters->keys ], [ 'id', 'q' ], 'and neither does the merged view' ); is( $req->route_parameters->get('id'), '42', 'the named route parameter is still readable' ); }; subtest 'a forwarded request keeps its decoded body parameters' => sub { my $req = form_request( method => 'POST', query => 'from=query', body => 'name=%C3%A9t%C3%A9&keep=yes', ); my $before = $req->body_parameters->get('name'); ok( utf8::is_utf8($before), 'the original body parameter is decoded' ); # What App::make_forward_to does: clone with extra params and a new path. my $clone = $req->_shallow_clone( { added => 'new' }, { PATH_INFO => '/dst' } ); # The regression this guards (GH#1116, GH#1269): the clone used to lose # already-decoded body parameters, or decode them a second time. my $after = $clone->body_parameters->get('name'); ok( utf8::is_utf8($after), 'the clone still has it decoded' ); is( $after, $before, 'with the same value, not double-decoded' ); is_deeply( [ sort $clone->body_parameters->keys ], [ 'keep', 'name' ], 'and every body parameter came across' ); # The legacy per-source view is carried by its own line in _shallow_clone, # separate from both the Hash::MultiValue clone above and the merged # params below. Dropping it leaves params('body') empty while those two # still look right, so it needs asserting in its own right. my $clone_body = $clone->params('body'); is_deeply( [ sort keys %$clone_body ], [ 'keep', 'name' ], "params('body') on the clone still lists every body parameter" ); is( $clone_body->{keep}, 'yes', "and their values" ); is( $clone_body->{name}, $before, "still decoded, matching the original request" ); is( $clone->params->{keep}, 'yes', 'body parameters are in the clone params' ); is( $clone->params->{from}, 'query', 'so are query parameters' ); is( $clone->params->{added}, 'new', 'and the parameters added at forward time' ); is( $clone->query_parameters->get('added'), 'new', 'the added parameters also reach query_parameters' ); is( $clone->env->{PATH_INFO}, '/dst', 'the clone has the new path' ); is( $req->env->{PATH_INFO}, '/', 'and the original request is not modified by the clone' ); }; done_testing(); bin000755000765000024 015267764222 14445 5ustar00jasonstaff000000000000Dancer2-2.3.0/t/app/t1app.psgi100644000765000024 4715267764222 16212 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/app/t1/bin#!perl use Dancer2; use App1; start; no-config.t100644000765000024 77315267764222 16344 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/plugin2use strict; use warnings; use Test::More tests => 1; BEGIN { package Dancer2::Plugin::Foo; use Dancer2::Plugin; has bar => ( is => 'ro', from_config => 1, ); has baz => ( is => 'ro', default => sub { $_[0]->config->{baz} }, ); plugin_keywords qw/ bar baz /; } { package MyApp; use Dancer2; use Dancer2::Plugin::Foo; bar(); baz(); } pass "we survived bar() and baz()"; uri_for_route.t100644000765000024 1563715267764222 16617 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/dsluse strict; use warnings; use Test::More 'tests' => 3; use Plack::Test; use Plack::Builder; use HTTP::Request::Common; use JSON::MaybeXS; { package App; use Dancer2; our $tested; # Static with route params # Static with code # Static with options and code get 'view_entry_static1' => '/view1/:id' => sub {1}; get 'view_entry_static2' => '/view2/:id' => { 'user_agent' => 'UA/1.0' }, sub {1}; # static with typed route param get 'view_user' => '/:prefix/user/:username[Str]' => sub {1}; # splat / megasplat get 'view_entry_splat' => '/viewsplat/*/*/**' => sub {1}; # Mixed with splat/megasplat # Different method patch 'view_entry_mixed' => '/view_mixed/*/**/:id' => sub {1}; # Regexp - fails get 'view_entry_regexp1' => qr{/rview1/[0-9]+} => sub {1}; post '/uri_for_route' => sub { my $params = JSON::MaybeXS::decode_json( request->content ); return uri_for_route( $params->{'route_name'}, $params->{'route_params'}, $params->{'query_params'} // {}, !!$params->{'dont_escape'}, ); }; get '/fail_uri_for_route' => sub { my $failed = 0; eval { uri_for_route('vvv'); 1; } or do { ::like( $@, qr/\QCannot find route named 'vvv'\E/xms, 'Cannot retrieve nonexistent route', ); $failed++; }; return $failed; }; get '/fail_uri_for_route_splat_args' => sub { my $failed = 0; eval { uri_for_route( 'view_entry_splat', ['foo'], ); 1; } or do { ::like( $@, qr/\QMismatch in amount of splat args and splat elements\E/xms, 'Cannot handle mismatched splat args and elements', ); $failed++; }; return $failed; }; get '/fail_uri_for_route_leftovers' => sub { my $failed = 0; eval { uri_for_route('view_entry_static1'); 1; } or do { my $msg = 'Route view_entry_static1 uses the parameter \'id\', ' . 'which was not provided'; ::like( $@, qr/\Q$msg\E/xms, 'Cannot handle leftover route parameters', ); $failed++; }; return $failed; }; # Error defining two routes with the same name, regardless of method eval { get 'view_entry_splat' => '/' => sub {1}; 1; } or do { ::like( $@, qr/\QRoute with this name (view_entry_splat) already exists\E/xms, 'Cannot register two routes with same name', ); $tested = 1; }; } sub test_app { my ( $app, $mount_path ) = @_; my $prefix = 'http://localhost'; $mount_path and $prefix .= $mount_path; my ( $path, $res ); # Test static paths foreach my $idx ( 1 .. 2 ) { $res = $app->request( POST( "$prefix/uri_for_route", 'Content' => JSON::MaybeXS::encode_json({ 'route_name' => "view_entry_static$idx", 'route_params' => { 'id' => $idx }, 'query_params' => { 'foo' => $idx }, }), ) ); $path = "$prefix/view$idx/$idx?foo=$idx"; ok( $res->is_success, 'Successful request' ); is( $res->content, $path, "Correct path: $path" ); } # Test splat + megasplat $res = $app->request( POST( "$prefix/uri_for_route", 'Content' => JSON::MaybeXS::encode_json({ 'route_name' => 'view_entry_splat', 'route_params' => [ 'foo', 'bar', [ 'baz', 'quux' ] ], 'query_params' => { 'id' => 'di' }, }), ) ); $path = "$prefix/viewsplat/foo/bar/baz/quux?id=di"; ok( $res->is_success, 'Successful request' ); is( $res->content, $path, "Correct path: $path" ); # Test mixed $res = $app->request( POST( "$prefix/uri_for_route", 'Content' => JSON::MaybeXS::encode_json( { 'route_name' => 'view_entry_mixed', 'route_params' => { 'id' => 'di', 'splat' => ['foo', ['bar', 'baz']] }, 'query_params' => {'foo' => 'bar'}, } ), ) ); $path = "$prefix/view_mixed/foo/bar/baz/di?foo=bar"; ok( $res->is_success, 'Successful request' ); is( $res->content, $path, "Correct path: $path" ); # Test escaping $res = $app->request( POST( "$prefix/uri_for_route", 'Content' => JSON::MaybeXS::encode_json({ 'route_name' => 'view_entry_static1', 'route_params' => { 'id' => '!@£$%' }, }), ) ); $path = "$prefix/view1/!@%C3%82%C2%A3\$%"; ok( $res->is_success, 'Successful request' ); is( $res->content, $path, "Correct path: $path" ); # Test nonexistent route name $res = $app->request( GET "$prefix/fail_uri_for_route" ); ok( $res->is_success, 'Successful request' ); is( $res->content, '1', 'Successfully tested nonexistent failure mode' ); # Test splat + megasplat (incorrect amount) $res = $app->request( GET "$prefix/fail_uri_for_route_splat_args" ); ok( $res->is_success, 'Successful request' ); is( $res->content, '1', 'Successfully tested mismatch splat args/elements failure mode' ); # Test mixed with not all filled (named args left) $res = $app->request( GET "$prefix/fail_uri_for_route_leftovers" ); ok( $res->is_success, 'Successful request' ); is( $res->content, '1', 'Successfully tested leftover args failure mode' ); # Static with typed route parameters $res = $app->request( POST( "$prefix/uri_for_route", 'Content' => JSON::MaybeXS::encode_json({ 'route_name' => 'view_user', 'route_params' => { 'prefix' => 'foo', 'username' => 'sawyer' }, 'query_params' => { 'foo' => 1 }, }), ) ); $path = "$prefix/foo/user/sawyer?foo=1"; ok( $res->is_success, 'Successful request' ); is( $res->content, $path, "Correct path for typed route param: $path" ); } subtest 'Non-mounted app' => sub { my $app = Plack::Test->create( App->to_app ); test_app($app); ok( $App::tested, 'Check for duplicate route names done successfully' ); }; subtest 'Mounted app' => sub { my $app = Plack::Test->create( builder { mount '/mount' => App->to_app; mount '/' => sub { return { Plack::Response->new(200, [], ['OK'] ) } }, } ); test_app( $app, '/mount' ); }; route_retvals.t100644000765000024 63115267764222 16556 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/dsluse strict; use warnings; use Dancer2; use Test::More (); my @routes = get '/' => sub {1}; Test::More::is( scalar @routes, 2, 'Two routes available' ); foreach my $route (@routes) { Test::More::isa_ok( $route, 'Dancer2::Core::Route' ); } Test::More::is( $routes[0]->method, 'get', 'Created GET route' ); Test::More::is( $routes[1]->method, 'head', 'Created HEAD route too' ); Test::More::done_testing; Plugin.pm100644000765000024 7636115267764222 16343 0ustar00jasonstaff000000000000Dancer2-2.3.0/lib/Dancer2package Dancer2::Plugin; # ABSTRACT: base class for Dancer2 plugins $Dancer2::Plugin::VERSION = '2.3.0'; use strict; use warnings; use Moo; use Carp; use List::Util qw/ reduce /; use Module::Runtime 'require_module'; use Attribute::Handlers; use Scalar::Util; use Ref::Util qw; our $CUR_PLUGIN; extends 'Exporter::Tiny'; with 'Dancer2::Core::Role::Hookable'; has app => ( is => 'ro', weak_ref => 1, required => 1, ); has config => ( is => 'ro', lazy => 1, default => sub { my $self = shift; my $config = $self->app->config; my $package = ref $self; # TODO $package =~ s/Dancer2::Plugin:://; $config->{plugins}{$package} || {}; }, ); my $_keywords = {}; sub keywords { $_keywords } my $REF_ADDR_REGEX = qr{ [A-Za-z0-9\:\_]+ =HASH \( ([0-9a-fx]+) \) }x; my %instances; # backwards compatibility our $_keywords_by_plugin = {}; has '+hooks' => ( default => sub { my $plugin = shift; my $name = 'plugin.' . lc ref $plugin; $name =~ s/Dancer2::Plugin:://i; $name =~ s/::/_/g; +{ map { join( '.', $name, $_ ) => [] } @{ $plugin->ClassHooks } }; }, ); sub add_hooks { my $class = shift; push @{ $class->ClassHooks }, @_; } sub execute_plugin_hook { my ( $self, $name, @args ) = @_; my $plugin_class = ref $self; $self->isa('Dancer2::Plugin') or croak "Cannot call plugin hook ($name) from outside plugin"; $plugin_class =~ s/^Dancer2::Plugin:://; # short names my $full_name = 'plugin.' . lc($plugin_class) . ".$name"; $full_name =~ s/::/_/g; $self->app->execute_hook( $full_name, @args ); } sub find_plugin { my ( $self, $name ) = @_; return $self->app->find_plugin($name); } # both functions are there for D2::Core::Role::Hookable # back-compatibility. Aren't used sub supported_hooks { [] } sub hook_aliases { $_[0]->{'hook_aliases'} ||= {} } ### has() STUFF ######################################## # our wrapping around Moo::has, done to be able to intercept # both 'from_config' and 'plugin_keyword' sub _p2_has { my $class = shift; $class->_p2_has_from_config( $class->_p2_has_keyword( @_ ) ); }; sub _p2_has_from_config { my( $class, $name, %args ) = @_; my $config_name = delete $args{'from_config'} or return ( $name, %args ); $args{lazy} = 1; if ( is_coderef($config_name) ) { $args{default} ||= $config_name; $config_name = 1; } $config_name = $name if $config_name eq '1'; my $orig_default = $args{default} || sub{}; $args{default} = sub { my $plugin = shift; my $value = reduce { eval { $a->{$b} } } $plugin->config, split /\./, $config_name; return defined $value ? $value: $orig_default->($plugin); }; return $name => %args; } sub _p2_has_keyword { my( $class, $name, %args ) = @_; if( my $keyword = delete $args{plugin_keyword} ) { $keyword = $name if $keyword eq '1'; $class->keywords->{$_} = sub { (shift)->$name(@_) } for ref $keyword ? @$keyword : $keyword; } return $name => %args; } ### ATTRIBUTE HANDLER STUFF ######################################## # :PluginKeyword shenanigans sub PluginKeyword :ATTR(CODE,BEGIN) { my( $class, $sym_ref, $code, undef, $args ) = @_; # importing at BEGIN stage doesn't work with 5.10 :-( return unless ref $sym_ref; my $func_name = *{$sym_ref}{NAME}; $args = join '', @$args if is_arrayref($args); for my $name ( split ' ', $args || $func_name ) { $class->keywords->{$name} = $code; } } ## EXPORT STUFF ############################################################## # this @EXPORT will only be taken # into account when we do a 'use Dancer2::Plugin' # I.e., it'll only do its magic for the # plugins themselves, not when they are # called our @EXPORT = qw/ :plugin /; # compatibility - it will be removed soon! my $no_dsl = {}; my $exported_app = {}; sub _exporter_expand_tag { my( $class, $name, $args, $global ) = @_; my $caller = $global->{into}; $name eq 'no_dsl' and $no_dsl->{$caller} = 1; # no_dsl check here is for compatibility only # it will be removed soon! return _exporter_plugin($caller) if $name eq 'plugin' or $name eq 'no_dsl'; return _exporter_app($class,$caller,$global) if $name eq 'app' and $caller->can('app') and !$no_dsl->{$class}; return; } # plugin has been called within a D2 app. Modify # the app and export keywords sub _exporter_app { my( $class, $caller, $global ) = @_; $exported_app->{$caller} = 1; # The check for ->dsl->app is to handle plugins as well. # Otherwise you can only import from a plugin to an app, # but with this, you can import to anything # that has a DSL with an app, which translates to "also plugins" my $app = eval("${caller}::app()") || eval { $caller->dsl->app } ## no critic qw(BuiltinFunctions::ProhibitStringyEval) or return; ## no critic return unless $app->can('with_plugin'); my $plugin = $app->with_plugin( '+' . $class ); $global->{'plugin'} = $plugin; return unless $class->can('keywords'); # Add our hooks to the app, so they're recognized # this is for compatibility so you can call execute_hook() # without the fully qualified plugin name. # The reason we need to do this here instead of when adding a # hook is because we need to register in the app, and only now it # exists. # This adds a caveat that two plugins cannot register # the same hook name, but that will be deprecated anyway. {; foreach my $hook ( @{ $plugin->ClassHooks } ) { my $full_name = 'plugin.' . lc($class) . ".$hook"; $full_name =~ s/Dancer2::Plugin:://i; $full_name =~ s/::/_/g; # this adds it to the plugin $plugin->hook_aliases->{$hook} = $full_name; # this adds it to the app $plugin->app->hook_aliases->{$hook} = $full_name; # copy the hooks from the plugin to the app # this is in case they were created at import time # rather than after @{ $plugin->app->hooks }{ keys %{ $plugin->hooks } } = values %{ $plugin->hooks }; } } { # get the reference my ($plugin_addr) = "$plugin" =~ $REF_ADDR_REGEX; $instances{$plugin_addr}{'config'} = sub { $plugin->config }; $instances{$plugin_addr}{'app'} = $plugin->app; Scalar::Util::weaken( $instances{$plugin_addr}{'app'} ); ## no critic no strict 'refs'; # we used a forward declaration # so the compiled form "plugin_setting;" can be overridden # with this implementation, # which works on runtime ("plugin_setting()") # we can't use can() here because the forward declaration will # create a CODE stub no warnings 'redefine'; *{"${class}::plugin_setting"} = sub { my ($plugin_addr) = "$CUR_PLUGIN" =~ $REF_ADDR_REGEX; $plugin_addr or Carp::croak('Can\'t find originating plugin'); # we need to do this because plugins might call "set" # in order to change plugin configuration but it doesn't # change the plugin object, it changes the app object # so we merge them. my $name = ref $CUR_PLUGIN; $name =~ s/^Dancer2::Plugin:://g; my $plugin_inst = $instances{$plugin_addr}; my $plugin_config = $plugin_inst->{'config'}->(); my $app_plugin_config = $plugin_inst->{'app'}->config->{'plugins'}{$name}; return { %{ $plugin_config || {} }, %{ $app_plugin_config || {} } }; }; # FIXME: # why doesn't this work? it's like it's already defined somewhere # but i'm not sure where. seems like AUTOLOAD runs it. #$class->can('execute_hook') or *{"${class}::execute_hook"} = sub { # this can also be called by App.pm itself # if the plugin is a # "candidate" for a hook # See: App.pm "execute_hook" method, "around" modifier if ( $_[0]->isa('Dancer2::Plugin') ) { # this means it's probably our hook, we need to verify it my ( $plugin_self, $hook_name, @args ) = @_; my $plugin_class = lc $class; $plugin_class =~ s/^dancer2::plugin:://; $plugin_class =~ s{::}{_}g; # you're either calling it with the full qualifier or not # if not, use the execute_plugin_hook instead if ( $plugin->hooks->{"plugin.$plugin_class.$hook_name"} ) { Carp::carp("Please use fully qualified hook name or " . "the method execute_plugin_hook"); $hook_name = "plugin.$plugin_class.$hook_name"; } $hook_name =~ /^plugin\.$plugin_class/ or Carp::croak('Unknown plugin called through other plugin'); # now we can't really use the app to execute it because # the "around" modifier is the one calling us to begin # with, so we need to call it directly ourselves # this is okay because the modifier is there only to # call candidates, like us (this is in fact how and # why we were called) $_->( $plugin_self, @args ) for @{ $plugin->hooks->{$hook_name} }; return; } return $plugin->app->execute_hook(@_); }; } local $CUR_PLUGIN = $plugin; $_->($plugin) for @{ $plugin->_DANCER2_IMPORT_TIME_SUBS() }; map { [ $_ => {plugin => $plugin} ] } keys %{ $plugin->keywords }; } # turns the caller namespace into # a D2P2 class, with exported keywords sub _exporter_plugin { my $caller = shift; require_module('Dancer2::Core::DSL'); my $keywords_list = join ' ', keys %{ Dancer2::Core::DSL->dsl_keywords }; eval <<"END"; ## no critic { package $caller; use Moo; use Carp (); use Attribute::Handlers; extends 'Dancer2::Plugin'; our \@EXPORT = ( ':app' ); around has => sub { my( \$orig, \$name, \%args ) = \@_; if (ref \$name eq 'ARRAY' && exists \$args{'plugin_keyword'} && ref \$args{'plugin_keyword'} eq 'ARRAY') { Carp::croak('Setting "plugin_keyword" to an array is disallowed' . ' when defining multiple attributes simultaneously'); } \$orig->( ${caller}->_p2_has( \$_, \%args) ) for ref \$name ? @\$name : \$name; }; sub PluginKeyword :ATTR(CODE,BEGIN) { goto &Dancer2::Plugin::PluginKeyword; } sub execute_plugin_hook { goto &Dancer2::Plugin::execute_plugin_hook; } my \$_keywords = {}; sub keywords { \$_keywords } my \$_ClassHooks = []; sub ClassHooks { \$_ClassHooks } # this is important as it'll do the keywords mapping between the # plugin and the app sub register_plugin { Dancer2::Plugin::register_plugin(\@_) } sub register { my ( \$keyword, \$sub ) = \@_; \$_keywords->{\$keyword} = \$sub; \$keyword =~ /^[a-zA-Z_]+[a-zA-Z0-9_]*\$/ or Carp::croak( "You can't use '\$keyword', it is an invalid name" . " (it should match ^[a-zA-Z_]+[a-zA-Z0-9_]*\\\$ )"); grep +( \$keyword eq \$_ ), qw<$keywords_list> and Carp::croak("You can't use '\$keyword', this is a reserved keyword"); \$Dancer2::Plugin::_keywords_by_plugin->{\$keyword} and Carp::croak("You can't use \$keyword, " . "this is a keyword reserved by " . \$Dancer2::Plugin::_keywords_by_plugin->{\$keyword}); \$Dancer2::Plugin::_keywords_by_plugin->{\$keyword} = "$caller"; # Exporter::Tiny doesn't seem to generate the subs # in the caller properly, so we have to do it manually { no strict 'refs'; *{"${caller}::\$keyword"} = \$sub; } } my \@_DANCER2_IMPORT_TIME_SUBS; sub _DANCER2_IMPORT_TIME_SUBS {\\\@_DANCER2_IMPORT_TIME_SUBS} sub on_plugin_import (&) { push \@_DANCER2_IMPORT_TIME_SUBS, \$_[0]; } sub register_hook { goto &plugin_hooks } sub plugin_setting { Carp::croak "DEPRECATED: Plugin DSL method 'plugin_setting'. " . "Please use '\\\$self->config' instead\n" }; sub plugin_args { Carp::carp "Plugin DSL method 'plugin_args' is deprecated. " . "Use '\\\@_' instead'.\n"; \@_; } } END $no_dsl->{$caller} or eval <<"END"; ## no critic { package $caller; # FIXME: AUTOLOAD might pick up on this sub dancer_app { Carp::croak "DEPRECATED: Plugin DSL method 'dancer_app'. " . "Please use '\\\$self->app' instead'.\n"; } # FIXME: AUTOLOAD might pick up on this sub request { Carp::croak "DEPRECATED: Plugin DSL method 'request'. " . "Please use '\\\$self->app->request' instead'.\n"; } # FIXME: AUTOLOAD might pick up on this sub var { Carp::croak "DEPRECATED: Plugin DSL method 'var'. " . "Please use '\\\$self->app->request->var' instead'.\n"; } # FIXME: AUTOLOAD might pick up on this sub hook { Carp::croak "DEPRECATED: Plugin DSL method 'hook'. " . "Please use '\\\$self->app->add_hook' instead'.\n"; } } END die $@ if $@; { ## no critic qw(TestingAndDebugging::ProhibitNoWarnings) no strict 'refs'; no warnings 'redefine'; *{"${caller}::dsl"} = sub { my $app_dsl_cb = _find_consumer($caller); return $app_dsl_cb ? $app_dsl_cb->() : undef; }; } return map { [ $_ => { class => $caller } ] } qw/ plugin_keywords plugin_hooks /; } sub _find_consumer { my %skip = map +( $_ => 1 ), @_; my $class; ## no critic qw(ControlStructures::ProhibitCStyleForLoops) for ( my $i = 1; my $caller = caller($i); $i++ ) { next if $skip{$caller}; next if eval { $caller->isa('Dancer2::Plugin') }; $class = $caller->can('dsl') and last; } # If you use a Dancer2 plugin outside a Dancer App, this fails. # It also breaks a bunch of the tests. -- SX #$class # or croak('Could not find Dancer2 app'); return $class; } # This has to be called for now at the end of every plugin package, in order to # map the keywords of the associated app to the plugin, so that these keywords # can be called from within the plugin code. This function is deprecated, as # it's tied to the old plugin system. It's kept here for backcompat reason, but # should go away with the old plugin system. sub register_plugin { my $plugin_module = caller(1); # if you ask yourself why we do the injection in the plugin # module namespace every time the plugin is used, and not only # once, it's because it can be used by different app that could # have a different DSL with a different list of keywords. my $_DANCER2_IMPORT_TIME_SUBS = $plugin_module->_DANCER2_IMPORT_TIME_SUBS; unshift(@$_DANCER2_IMPORT_TIME_SUBS, sub { my $app_dsl_cb = _find_consumer($plugin_module); # Here we want to verify that "register_plugin" compat keyword # was in fact only called from an app. $app_dsl_cb or Carp::croak( 'I could not find a Dancer App for this plugin'); my $dsl = $app_dsl_cb->(); foreach my $keyword ( keys %{ $dsl->dsl_keywords} ) { # if not yet defined, inject the keyword in the plugin # namespace, but make sure the code will always get the # coderef from the right associated app, because one plugin # can be used by multiple apps. Note that we remove the # first parameter (plugin instance) from what we pass to # the keyword implementation of the App no strict 'refs'; $plugin_module->can($keyword) or *{"${plugin_module}::$keyword"} = sub { $_[0] ? do { my $cb = shift()->app->name->can($keyword); $cb->(@_); } : $app_dsl_cb->(@_); }; } }); } sub _exporter_expand_sub { my( $plugin, $name, $args, $global ) = @_; my $class = $args->{class}; return _exported_plugin_keywords($plugin,$class) if $name eq 'plugin_keywords'; return _exported_plugin_hooks($class) if $name eq 'plugin_hooks'; $exported_app->{ $global->{'into'} } or Carp::croak('Specific subroutines cannot be exported from plugin'); # otherwise, we're exporting a keyword my $p = $args->{plugin}; my $sub = $p->keywords->{$name}; return $name => sub(@) { # localize the plugin so we can get it later local $CUR_PLUGIN = $p; $sub->($p,@_); } } # "There's a good reason for this, I swear!" # -- Sawyer X # basically, if someone adds a hook to the app directly # that needs to access a DSL that needs the current object # (such as "plugin_setting"), # that object needs to be available # So: # we override App's "add_hook" to provide a register a # different hook callback, that closes over the plugin when # it's available, relocalizes it when the callback runs and # after localizing it, calls the original hook callback { ## no critic; no strict 'refs'; no warnings 'redefine'; my $orig_cb = Dancer2::Core::App->can('add_hook'); $orig_cb and *{'Dancer2::Core::App::add_hook'} = sub { my ( $app, $hook ) = @_; my $hook_code = Scalar::Util::blessed($hook) ? $hook->code : $hook->{code}; my $plugin = $CUR_PLUGIN; $hook->{'code'} = sub { local $CUR_PLUGIN = $plugin; $hook_code->(@_); }; $orig_cb->(@_); }; } # define the exported 'plugin_keywords' sub _exported_plugin_keywords{ my( $plugin, $class ) = @_; return plugin_keywords => sub(@) { while( my $name = shift @_ ) { ## no critic my $sub = is_coderef($_[0]) ? shift @_ : eval '\&'.$class."::" . ( ref $name ? $name->[0] : $name ); $class->keywords->{$_} = $sub for ref $name ? @$name : $name; } } } sub _exported_plugin_hooks { my $class = shift; return plugin_hooks => sub (@) { $class->add_hooks(@_) } } 1; __END__ =pod =encoding UTF-8 =head1 NAME Dancer2::Plugin - base class for Dancer2 plugins =head1 VERSION version 2.3.0 =head1 SYNOPSIS The plugin itself: package Dancer2::Plugin::Polite; use strict; use warnings; use Dancer2::Plugin; has smiley => ( is => 'ro', default => sub { $_[0]->config->{smiley} || ':-)' } ); plugin_keywords 'add_smileys'; sub BUILD { my $plugin = shift; $plugin->app->add_hook( Dancer2::Core::Hook->new( name => 'after', code => sub { $_[0]->content( $_[0]->content . " ... please?" ) } )); $plugin->app->add_route( method => 'get', regexp => '/goodbye', code => sub { my $app = shift; 'farewell, ' . $app->request->params->{name}; }, ); } sub add_smileys { my( $plugin, $text ) = @_; $text =~ s/ (?<= \. ) / $plugin->smiley /xeg; return $text; } 1; then to load into the app: package MyApp; use strict; use warnings; use Dancer2; BEGIN { # would usually be in config.yml set plugins => { Polite => { smiley => '8-D', }, }; } use Dancer2::Plugin::Polite; get '/' => sub { add_smileys( 'make me a sandwich.' ); }; 1; =head1 DESCRIPTION =head2 Writing the plugin =head3 C The plugin must begin with use Dancer2::Plugin; which will turn the package into a L class that inherits from L. The base class provides the plugin with two attributes: C, which is populated with the Dancer2 app object for which the plugin is being initialized for, and C which holds the plugin section of the application configuration. =head3 Modifying the app at building time If the plugin needs to tinker with the application -- add routes or hooks, for example -- it can do so within its C function. sub BUILD { my $plugin = shift; $plugin->app->add_route( ... ); } =head3 Adding keywords =head4 Via C Keywords that the plugin wishes to export to the Dancer2 app can be defined via the C keyword: plugin_keywords qw/ add_smileys add_sad_kitten /; Each of the keyword will resolve to the class method of the same name. When invoked as keyword, it'll be passed the plugin object as its first argument. sub add_smileys { my( $plugin, $text ) = @_; return join ' ', $text, $plugin->smiley; } # and then in the app get '/' => sub { add_smileys( "Hi there!" ); }; You can also pass the functions directly to C. plugin_keywords add_smileys => sub { my( $plugin, $text ) = @_; $text =~ s/ (?<= \. ) / $plugin->smiley /xeg; return $text; }, add_sad_kitten => sub { ... }; Or a mix of both styles. We're easy that way: plugin_keywords add_smileys => sub { my( $plugin, $text ) = @_; $text =~ s/ (?<= \. ) / $plugin->smiley /xeg; return $text; }, 'add_sad_kitten'; sub add_sad_kitten { ...; } If you want several keywords to be synonyms calling the same function, you can list them in an arrayref. The first function of the list is taken to be the "real" method to link to the keywords. plugin_keywords [qw/ add_smileys add_happy_face /]; sub add_smileys { ... } Calls to C are cumulative. =head4 Via the C<:PluginKeyword> function attribute For perl 5.12 and higher, keywords can also be defined by adding the C<:PluginKeyword> attribute to the function you wish to export. For Perl 5.10, the export triggered by the sub attribute comes too late in the game, and the keywords won't be exported in the application namespace. sub foo :PluginKeyword { ... } sub bar :PluginKeyword( baz quux ) { ... } # equivalent to sub foo { ... } sub bar { ... } plugin_keywords 'foo', [ qw/ baz quux / ] => \&bar; =head4 For an attribute You can also turn an attribute of the plugin into a keyword. has foo => ( is => 'ro', plugin_keyword => 1, # keyword will be 'foo' ); has bar => ( is => 'ro', plugin_keyword => 'quux', # keyword will be 'quux' ); has baz => ( is => 'ro', plugin_keyword => [ 'baz', 'bazz' ], # keywords will be 'baz' and 'bazz' ); =head3 Accessing the plugin configuration The plugin configuration is available via the C method. sub BUILD { my $plugin = shift; if ( $plugin->config->{feeling_polite} ) { $plugin->app->add_hook( Dancer2::Core::Hook->new( name => 'after', code => sub { $_[0]->content( $_[0]->content . " ... please?" ) } )); } } =head3 Getting default values from config file Since initializing a plugin with either a default or a value passed via the configuration file, like has smiley => ( is => 'ro', default => sub { $_[0]->config->{smiley} || ':-)' } ); C allows for a C key in the attribute definition. Its value is the plugin configuration key that will be used to initialize the attribute. If it's given the value C<1>, the name of the attribute will be taken as the configuration key. Nested hash keys can also be referred to using a dot notation. If the plugin configuration has no value for the given key, the attribute default, if specified, will be honored. If the key is given a coderef as value, it's considered to be a C value combo: has foo => ( is => 'ro', from_config => sub { 'my default' }, ); # equivalent to has foo => ( is => 'ro', from_config => 'foo', default => sub { 'my default' }, ); For example: # in config.yml plugins: Polite: smiley: ':-)' greeting: casual: Hi! formal: How do you do? # in the plugin has smiley => ( # will be ':-)' is => 'ro', from_config => 1, default => sub { ':-(' }, ); has casual_greeting => ( # will be 'Hi!' is => 'ro', from_config => 'greeting.casual', ); has apology => ( # will be 'sorry' is => 'ro', from_config => 'apology', default => sub { 'sorry' }, ) has closing => ( # will be 'See ya!' is => 'ro', from_config => sub { 'See ya!' }, ); =head3 Config becomes immutable The plugin's C attribute is loaded lazily on the first call to C. After this first call C becomes immutable so you cannot do the following in a test: use Dancer2; use Dancer2::Plugin::FooBar; set plugins => { FooBar => { wibble => 1, # this is OK }, }; flibble(45); # plugin keyword called which causes config read set plugins => { FooBar => { wibble => 0, # this will NOT change plugin config }, }; =head3 Accessing the parent Dancer app If the plugin is instantiated within a Dancer app, it'll be accessible via the method C. sub BUILD { my $plugin = shift; $plugin->app->add_route( ... ); } To use Dancer's DSL in your plugin: $self->dsl->debug( “Hi! I’m logging from your plugin!� ); See L for a full list of Dancer2 DSL. =head2 Using the plugin within the app A plugin is loaded via use Dancer2::Plugin::Polite; The plugin will assume that it's loading within a Dancer module and will automatically register itself against its C and export its keywords to the local namespace. If you don't want this to happen, specify that you don't want anything imported via empty parentheses when Cing the module: use Dancer2::Plugin::Polite (); =head2 Plugins using plugins It's easy to use plugins from within a plugin: package Dancer2::Plugin::SourPuss; use Dancer2::Plugin; use Dancer2::Plugin::Polite; sub my_keyword { my $smiley = smiley(); } 1; This does not export C into your application - it is only available from within your plugin. However, from the example above, you can wrap DSL from other plugins and make it available from your plugin. =head2 Utilizing other plugins You can use the C to locate other plugins loaded by the user, in order to use them, or their information, directly: # MyApp.pm use Dancer2; use Dancer2::Plugin::Foo; use Dancer2::Plugin::Bar; # Dancer2::Plugin::Bar; ... sub my_keyword { my $self = shift; my $foo = $self->find_plugin('Dancer2::Plugin::Foo') or $self->dsl->send_error('Could not find Foo'); return $foo->foo_keyword(...); } =head2 Hooks New plugin hooks are declared via C. plugin_hooks 'my_hook', 'my_other_hook'; Hooks are prefixed with C. So the plugin C coming from the plugin C will have the hook name C. Hooks are executed within the plugin by calling them via the associated I. $plugin->execute_plugin_hook( 'my_hook' ); You can also call any other hook if you provide the full name using the C method: $plugin->app->execute_hook( 'core.app.route_exception' ); Or using their alias: $plugin->app->execute_hook( 'on_route_exception' ); B If your plugin consumes a plugin that declares any hooks, those hooks are added to your application, even though DSL is not. =head2 Writing Test Gotchas =head3 Constructor for Dancer2::Plugin::Foo has been inlined and cannot be updated You'll usually get this one because you are defining both the plugin and app in your test file, and the runtime creation of Moo's attributes happens after the compile-time import voodoo dance. To get around this nightmare, wrap your plugin definition in a C block. BEGIN { package Dancer2::Plugin::Foo; use Dancer2::Plugin; has bar => ( is => 'ro', from_config => 1, ); plugin_keywords qw/ bar /; } { package MyApp; use Dancer2; use Dancer2::Plugin::Foo; bar(); } =head3 You cannot overwrite a locally defined method (bar) with a reader If you set an object attribute of your plugin to be a keyword as well, you need to call C after the attribute definition. package Dancer2::Plugin::Foo; use Dancer2::Plugin; has bar => ( is => 'ro', ); plugin_keywords 'bar'; =head3 Coverage for Dancer2::Plugin:: is 0.0%, with 15 naked subroutines To avoid errors caused by missing pod for private plugin methods, write your pod coverage test like so: pod_coverage_ok( "Dancer2::Plugin::MyAwesomePlugin", { also_private => [ qw/ BUILDARGS BUILD ClassHooks PluginKeyword dancer_app execute_plugin_hook hook keywords on_plugin_import plugin_args plugin_setting realms realm realm_providers register register_hook register_plugin request var / ] }); =head1 AUTHOR Dancer Core Developers =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2026 by Alexis Sukrieh. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut author-pod-syntax.t100644000765000024 45315267764222 16506 0ustar00jasonstaff000000000000Dancer2-2.3.0/t#!perl BEGIN { unless ($ENV{AUTHOR_TESTING}) { print qq{1..0 # SKIP these tests are for testing by the author\n}; exit } } # This file was automatically generated by Dist::Zilla::Plugin::PodSyntaxTests use strict; use warnings; use Test::More; use Test::Pod 1.41; all_pod_files_ok(); 00-report-prereqs.t100644000765000024 1360115267764222 16346 0ustar00jasonstaff000000000000Dancer2-2.3.0/t#!perl use strict; use warnings; # This test was generated by Dist::Zilla::Plugin::Test::ReportPrereqs 0.029 use Test::More tests => 1; use ExtUtils::MakeMaker; use File::Spec; # from $version::LAX my $lax_version_re = qr/(?: undef | (?: (?:[0-9]+) (?: \. | (?:\.[0-9]+) (?:_[0-9]+)? )? | (?:\.[0-9]+) (?:_[0-9]+)? ) | (?: v (?:[0-9]+) (?: (?:\.[0-9]+)+ (?:_[0-9]+)? )? | (?:[0-9]+)? (?:\.[0-9]+){2,} (?:_[0-9]+)? ) )/x; # hide optional CPAN::Meta modules from prereq scanner # and check if they are available my $cpan_meta = "CPAN::Meta"; my $cpan_meta_pre = "CPAN::Meta::Prereqs"; my $HAS_CPAN_META = eval "require $cpan_meta; $cpan_meta->VERSION('2.120900')" && eval "require $cpan_meta_pre"; ## no critic # Verify requirements? my $DO_VERIFY_PREREQS = 1; sub _max { my $max = shift; $max = ( $_ > $max ) ? $_ : $max for @_; return $max; } sub _merge_prereqs { my ($collector, $prereqs) = @_; # CPAN::Meta::Prereqs object if (ref $collector eq $cpan_meta_pre) { return $collector->with_merged_prereqs( CPAN::Meta::Prereqs->new( $prereqs ) ); } # Raw hashrefs for my $phase ( keys %$prereqs ) { for my $type ( keys %{ $prereqs->{$phase} } ) { for my $module ( keys %{ $prereqs->{$phase}{$type} } ) { $collector->{$phase}{$type}{$module} = $prereqs->{$phase}{$type}{$module}; } } } return $collector; } my @include = qw( ); my @exclude = qw( ); # Add static prereqs to the included modules list my $static_prereqs = do './t/00-report-prereqs.dd'; # Merge all prereqs (either with ::Prereqs or a hashref) my $full_prereqs = _merge_prereqs( ( $HAS_CPAN_META ? $cpan_meta_pre->new : {} ), $static_prereqs ); # Add dynamic prereqs to the included modules list (if we can) my ($source) = grep { -f } 'MYMETA.json', 'MYMETA.yml'; my $cpan_meta_error; if ( $source && $HAS_CPAN_META && (my $meta = eval { CPAN::Meta->load_file($source) } ) ) { $full_prereqs = _merge_prereqs($full_prereqs, $meta->prereqs); } else { $cpan_meta_error = $@; # capture error from CPAN::Meta->load_file($source) $source = 'static metadata'; } my @full_reports; my @dep_errors; my $req_hash = $HAS_CPAN_META ? $full_prereqs->as_string_hash : $full_prereqs; # Add static includes into a fake section for my $mod (@include) { $req_hash->{other}{modules}{$mod} = 0; } for my $phase ( qw(configure build test runtime develop other) ) { next unless $req_hash->{$phase}; next if ($phase eq 'develop' and not $ENV{AUTHOR_TESTING}); for my $type ( qw(requires recommends suggests conflicts modules) ) { next unless $req_hash->{$phase}{$type}; my $title = ucfirst($phase).' '.ucfirst($type); my @reports = [qw/Module Want Have/]; for my $mod ( sort keys %{ $req_hash->{$phase}{$type} } ) { next if grep { $_ eq $mod } @exclude; my $want = $req_hash->{$phase}{$type}{$mod}; $want = "undef" unless defined $want; $want = "any" if !$want && $want == 0; if ($mod eq 'perl') { push @reports, ['perl', $want, $]]; next; } my $req_string = $want eq 'any' ? 'any version required' : "version '$want' required"; my $file = $mod; $file =~ s{::}{/}g; $file .= ".pm"; my ($prefix) = grep { -e File::Spec->catfile($_, $file) } @INC; if ($prefix) { my $have = MM->parse_version( File::Spec->catfile($prefix, $file) ); $have = "undef" unless defined $have; push @reports, [$mod, $want, $have]; if ( $DO_VERIFY_PREREQS && $HAS_CPAN_META && $type eq 'requires' ) { if ( $have !~ /\A$lax_version_re\z/ ) { push @dep_errors, "$mod version '$have' cannot be parsed ($req_string)"; } elsif ( ! $full_prereqs->requirements_for( $phase, $type )->accepts_module( $mod => $have ) ) { push @dep_errors, "$mod version '$have' is not in required range '$want'"; } } } else { push @reports, [$mod, $want, "missing"]; if ( $DO_VERIFY_PREREQS && $type eq 'requires' ) { push @dep_errors, "$mod is not installed ($req_string)"; } } } if ( @reports ) { push @full_reports, "=== $title ===\n\n"; my $ml = _max( map { length $_->[0] } @reports ); my $wl = _max( map { length $_->[1] } @reports ); my $hl = _max( map { length $_->[2] } @reports ); if ($type eq 'modules') { splice @reports, 1, 0, ["-" x $ml, "", "-" x $hl]; push @full_reports, map { sprintf(" %*s %*s\n", -$ml, $_->[0], $hl, $_->[2]) } @reports; } else { splice @reports, 1, 0, ["-" x $ml, "-" x $wl, "-" x $hl]; push @full_reports, map { sprintf(" %*s %*s %*s\n", -$ml, $_->[0], $wl, $_->[1], $hl, $_->[2]) } @reports; } push @full_reports, "\n"; } } } if ( @full_reports ) { diag "\nVersions for all modules listed in $source (including optional ones):\n\n", @full_reports; } if ( $cpan_meta_error || @dep_errors ) { diag "\n*** WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING ***\n"; } if ( $cpan_meta_error ) { my ($orig_source) = grep { -f } 'MYMETA.json', 'MYMETA.yml'; diag "\nCPAN::Meta->load_file('$orig_source') failed with: $cpan_meta_error\n"; } if ( @dep_errors ) { diag join("\n", "\nThe following REQUIRED prerequisites were not satisfied:\n", @dep_errors, "\n" ); } pass('Reported prereqs'); # vim: ts=4 sts=4 sw=4 et: serializer_helpers.t100644000765000024 1031515267764222 17031 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More; use Dancer2::Serializer::JSON; use Dancer2::Serializer::YAML; # The from_json/to_json/from_yaml/to_yaml helpers are the class-method face of # the serializer engines: they are called as plain functions, outside any # Dancer2 app, so there is no app logger for the engine to log to. # # That used to mean a failed parse raised "Class::XSAccessor: invalid instance # method invocant" from inside the error handler -- which replaced the real # parse error with an unrelated one (GH #1837). The helpers now build an engine # instance with a log_cb that warns, so the diagnostic reaches the caller on # STDERR instead of being lost. These tests pin both halves of that: the real # error is reported, and nothing about the accessor comes back. # Collect what the code under test says on STDERR, and give back both the # return value and the warnings, so each subtest can assert on the pair. sub capture_warnings { my $code = shift; my @warnings; local $SIG{'__WARN__'} = sub { push @warnings, @_ }; my $result = $code->(); return ( $result, join '', @warnings ); } subtest 'from_json reports a parse failure instead of hiding it' => sub { my ( $result, $warnings ) = capture_warnings( sub { Dancer2::Serializer::JSON::from_json('{not json') } ); is( $result, undef, 'malformed JSON deserializes to undef' ); like( $warnings, qr/Failed to deserialize content/, 'and the failure is reported on STDERR' ); unlike( $warnings, qr/Class::XSAccessor/, 'not as an accessor error from inside the error handler' ); }; subtest 'from_yaml reports a parse failure instead of hiding it' => sub { my ( $result, $warnings ) = capture_warnings( sub { Dancer2::Serializer::YAML::from_yaml("\tbad:\n - [unclosed") } ); is( $result, undef, 'malformed YAML deserializes to undef' ); like( $warnings, qr/Failed to deserialize content/, 'and the failure is reported on STDERR' ); unlike( $warnings, qr/Class::XSAccessor/, 'not as an accessor error from inside the error handler' ); }; subtest 'to_json still warns about invalid UTF-8' => sub { # Dancer2::Serializer::JSON::_invalid_utf8 has always let this one out -- # it warned directly whenever there was no object to log through. Now that # the helpers do have an object, the notice travels through log_cb, and # this is the test that keeps it visible rather than silently swallowed. my ( $json, $warnings ) = capture_warnings( sub { Dancer2::Serializer::JSON::to_json( { bad => pack( 'C', 0xFF ) } ) } ); ok( $json, 'serializing bytes that are not UTF-8 still produces JSON' ); like( $warnings, qr/Invalid UTF-8/, 'and says so on STDERR' ); }; subtest 'the helpers stay quiet when nothing is wrong' => sub { my ( $data, $json_warnings ) = capture_warnings( sub { Dancer2::Serializer::JSON::from_json( Dancer2::Serializer::JSON::to_json( { a => 1, b => [ 1, 2 ] } ) ); } ); is_deeply( $data, { a => 1, b => [ 1, 2 ] }, 'JSON round trips' ); is( $json_warnings, '', 'and warns about nothing' ); my ( $yaml_data, $yaml_warnings ) = capture_warnings( sub { Dancer2::Serializer::YAML::from_yaml( Dancer2::Serializer::YAML::to_yaml( { a => 1, b => [ 1, 2 ] } ) ); } ); is_deeply( $yaml_data, { a => 1, b => [ 1, 2 ] }, 'YAML round trips' ); is( $yaml_warnings, '', 'and warns about nothing either' ); }; subtest 'a bare class-method call fails quietly rather than dying' => sub { # Not just the helpers: any caller can invoke the engines as class methods, # and Role::Serializer's error handler now guards its log_cb call with # blessed(). There is nowhere to report to on this path, so the failure is # silent -- but it must not raise. my ( $outcome, $warnings ) = capture_warnings( sub { my $result = eval { Dancer2::Serializer::JSON->deserialize('{not json') }; return { result => $result, error => $@ }; } ); is( $outcome->{'error'}, '', 'a class-method deserialize of malformed JSON does not die' ); is( $outcome->{'result'}, undef, 'it returns undef' ); unlike( $warnings, qr/Class::XSAccessor/, 'and raises no accessor error' ); }; done_testing(); disp_named_capture.t100644000765000024 117515267764222 16750 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse warnings; use strict; use Plack::Test; use HTTP::Request; use Test::More tests => 2; { package app; use Dancer2; get '/1' => sub { return '1'; }; get '/2' => sub { return '2'; }; } my $test = Plack::Test->create( app->to_app ); my $request = HTTP::Request->new( GET => 'http://localhost/1' ); my $response = $test->request( $request ); is( $response->content, 1 ); # "Dummy" regex to populate global $+ "12345" =~ m#(?23)#; my $c = $+{capture}; $request = HTTP::Request->new( GET => 'http://localhost/2' ); $response = $test->request( $request ); is( $response->content, 2 ); multi_apps_forward.t100644000765000024 332615267764222 17023 0ustar00jasonstaff000000000000Dancer2-2.3.0/t#!perl use strict; use warnings; use Test::More tests => 9; use Plack::Test; use HTTP::Request::Common; { package App1; use Dancer2; get '/' => sub {'App1'}; get '/forward' => sub { forward '/'; ::ok( 0, 'Foward not returning right away!' ); }; get '/forward_to_new' => sub { forward '/new'; ::ok( 0, 'Foward not returning right away!' ); }; } { package App2; use Dancer2; get '/' => sub {'App2'}; get '/new' => sub {'New'}; } { # test each single app my $app1 = App1->to_app; test_psgi $app1, sub { my $cb = shift; is( $cb->( GET '/' )->code, 200, '[GET /] OK' ); is( $cb->( GET '/' )->content, 'App1', '[GET /] OK content' ); is( $cb->( GET '/forward' )->code, 200, '[GET /forward] OK' ); is( $cb->( GET '/forward' )->content, 'App1', '[GET /forward] OK content' ); is( $cb->( GET '/forward_to_new' )->code, 404, 'Cannot find /new', ); }; my $app2 = App2->to_app; test_psgi $app2, sub { my $cb = shift; is( $cb->( GET '/' )->code, 200, '[GET /] OK' ); is( $cb->( GET '/' )->content, 'App2', '[GET /] OK content' ); }; } note 'Old format using psgi_app to loop over multiple apps'; { # test global my $app = Dancer2->psgi_app; test_psgi $app, sub { my $cb = shift; is( $cb->( GET '/forward_to_new' )->code, 200, '[GET /forward_to_new] OK', ); is( $cb->( GET '/forward_to_new' )->content, 'New', '[GET /forward_to_new] OK content', ); }; } forward_hmv_params.t100644000765000024 327415267764222 17005 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More import => ['!pass']; use Plack::Test; use HTTP::Request::Common; use Encode qw(decode); use utf8; { package Test::Forward::HMV; use Dancer2; any '/' => sub { 'home:' . join( ',', request->parameters->flatten ); }; get '/get' => sub { forward '/', { get => 'bâz' }; }; post '/post' => sub { forward '/', { post => 'bâz' }; }; post '/change/:me' => sub { forward '/', { post => route_parameters->get('me') }, { method => 'GET' }; }; } my $test = Plack::Test->create( Test::Forward::HMV->to_app ); subtest 'query parameters (#1245)' => sub { my $res = $test->request( GET '/get?foo=bâr' ); is $res->code, 200, "success forward for /get"; my $content = decode( 'UTF-8', $res->content ); is $content, 'home:foo,bâr,get,bâz', "query parameters merged after forward"; }; subtest 'body parameters (#1116)' => sub { my $res = $test->request( POST '/post', { foo => 'bâr' } ); is $res->code, 200, "success forward for /post"; # The order is important: post,baz are QUERY params # foo,bar are the original body params my $content = decode( 'UTF-8', $res->content ); like $content, qr/^home:post,bâz/, "forward params become query params"; is $content, 'home:post,bâz,foo,bâr', "body parameters available after forward"; }; subtest 'params when method changes' => sub { my $res = $test->request( POST '/change/1234', { foo => 'bâr' } ); is $res->code, 200, "success forward for /change/:me"; my $content = decode( 'UTF-8', $res->content ); is $content, 'home:post,1234,foo,bâr', "body parameters available after forward"; }; done_testing(); serializer_mutable.t100644000765000024 703215267764222 17002 0ustar00jasonstaff000000000000Dancer2-2.3.0/tuse strict; use warnings; use Test::More tests => 4 ; use Dancer2::Serializer::Mutable; use Plack::Test; use HTTP::Request::Common; use Encode; use JSON::MaybeXS; use YAML; use Ref::Util qw; { package MyApp; use Dancer2; use Ref::Util qw; set serializer => 'Mutable'; get '/serialize' => sub { +{ bar => 'baz' } }; post '/deserialize' => sub { return request->data && is_hashref( request->data ) && request->data->{bar} ? { bar => request->data->{bar} } : { ret => '?' }; }; } my $test = Plack::Test->create( MyApp->to_app ); subtest "serializer returns to default state" => sub { my $res = $test->request( GET '/serialize' ); is( $res->headers->content_type, 'application/json', "Default content-type header", ); $res = $test->request( GET '/serialize', 'Accept' => 'text/x-data-dumper' ); is( $res->headers->content_type, 'application/json', "Dumper content type falls back to default as it is not enabled", ); $res = $test->request( GET '/serialize', 'Accept' => 'text/x-json' ); is( $res->headers->content_type, 'text/x-json', "Correct content-type header", ); $res = $test->request( GET '/serialize' ); is( $res->headers->content_type, 'application/json', "Correct default content-type header after a request that used another", ); }; # Configure test content-type cases my $d = { yaml => { types => [ qw(text/x-yaml text/html) ], value => encode('UTF-8', YAML::Dump({ bar => 'baz' })), last_val => "---bar:baz", }, json => { types => [ qw(text/x-json application/json) ], value => JSON::MaybeXS::encode_json({ bar => 'baz' }), last_val => '{"bar":"baz"}', }, default => { types => [ '*/*', '' ], value => JSON::MaybeXS::encode_json({ bar => 'baz' }), last_val => '{"bar":"baz"}', return_content_type => 'application/json', }, }; for my $format (keys %$d) { subtest "Format: $format" => sub { my $s = $d->{$format}; # Response with implicit call to the serializer for my $content_type ( @{ $s->{types} } ) { for my $ct (qw/Content-Type Accept/) { # Test getting the value serialized in the correct format my $res = $test->request( GET '/serialize', $ct => $content_type ); is( $res->code, 200, "[/$format] Correct status" ); is( $res->content, $s->{value}, "[/$format] Correct content" ); is( $res->headers->content_type, $s->{return_content_type} || $content_type, "[/$format] Correct content-type headers", ); } # Test sending the value serialized in the correct format # needs to be de-serialized and returned my $req = $test->request( POST '/deserialize', 'Content-Type' => $content_type, content => $s->{value} ); my $content = $req->content; $content =~ s/\s//g; is( $req->code, 200, "[/$format] Deserialize: correct status" ); is( $content, $s->{last_val}, "[/$format] Deserialize: correct content" ); } #/ for my $content_type }; #/ subtest } #/ for my $format response.t100644000765000024 3312215267764222 16704 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/unit/coreuse strict; use warnings; use Test::More; use Test::Fatal qw; use Dancer2::Core::Response; # Unit tests for the response object: character encoding, Content-Length, and # the conversion to a PSGI array. # # U+263A is used throughout as the non-ASCII character. It matters that it is # above U+00FF: Perl stores a string of only Latin-1 codepoints as bytes with # no UTF8 flag, and several branches in encode_content key off # utf8::is_utf8(), so "caf\x{e9}" takes a different path from "hi\x{263A}" # and would quietly test the wrong thing. my $CHAR = "\x{263A}"; my $BYTES = "\xe2\x98\xba"; # the same character, UTF-8 encoded # Helper: the PSGI triplet with the headers as a hash, plus the raw header # array for the cases where the array itself is what is being asserted. sub psgi_parts { my $response = shift; my $psgi = $response->to_psgi; my %header; for ( my $i = 0; $i < @{ $psgi->[1] }; $i += 2 ) { $header{ $psgi->[1][$i] } = $psgi->[1][ $i + 1 ]; } return { status => $psgi->[0], header => \%header, raw => $psgi->[1], body => $psgi->[2], }; } # THIS SUBTEST MUST RUN FIRST. # # The "no charset configured" report is gated by a file-scoped counter in # Dancer2::Core::Response ($WARNED_NO_CHARSET), so it fires at most once per # process and cannot be reset from outside. Any earlier subtest that assigned # character content with no charset would consume it and make this one # unfalsifiable. subtest 'a missing charset is reported once per process' => sub { my @logged; my @warnings; my $response = Dancer2::Core::Response->new( log_cb => sub { push @logged, [@_] } ); { local $SIG{__WARN__} = sub { push @warnings, $_[0] }; $response->content("hi$CHAR"); } is( scalar @logged, 1, 'the app is told, once' ); is( $logged[0][0], 'warning', 'at warning level' ); like( $logged[0][1], qr/no charset is configured/, 'saying no charset was configured' ); like( $logged[0][1], qr/assuming UTF-8/, 'and what it assumed instead' ); is( scalar @warnings, 0, 'and nothing goes to STDERR when the app provides a log callback' ); # Having assumed UTF-8, it must actually encode as UTF-8. my $got = psgi_parts($response); is( $got->{body}[0], "hi$BYTES", 'the body is UTF-8 encoded' ); like( $got->{header}{'Content-Type'}, qr/charset=UTF-8/, 'and the assumption is declared in the Content-Type' ); # The second response is silent - the counter is per process, not per # response. Pinned because it is surprising: an app that starts warning # about this warns exactly once and then never again. my ( @logged2, @warnings2 ); my $second = Dancer2::Core::Response->new( log_cb => sub { push @logged2, [@_] } ); { local $SIG{__WARN__} = sub { push @warnings2, $_[0] }; $second->content("again$CHAR"); } is( scalar @logged2, 0, 'a later response does not report it again' ); is( scalar @warnings2, 0, 'nor warn about it' ); # But it still encodes. Only the reporting is suppressed, not the # behavior - which is why the silence above is safe rather than a bug. is( $second->content, "again$BYTES", 'and it still encodes as UTF-8 despite saying nothing' ); }; subtest 'a missing charset is fatal under strict_utf8' => sub { # Unlike the report above, the croak happens before the once-per-process # counter is consulted, so this is repeatable. for my $attempt ( 1, 2 ) { my $response = Dancer2::Core::Response->new( strict_utf8 => 1 ); my $err = exception { $response->content("hi$CHAR") }; ok( defined $err, "attempt $attempt throws" ); like( $err, qr/no charset is configured/, "attempt $attempt says why" ); } # Byte content is not affected: there is nothing to encode, so strict # mode has no complaint. my $bytes = Dancer2::Core::Response->new( strict_utf8 => 1 ); is( exception { $bytes->content("plain ascii") }, undef, 'ASCII content is fine under strict_utf8' ); is( $bytes->content, 'plain ascii', 'and passes through unchanged' ); }; subtest 'text content is encoded once, and Content-Length counts bytes' => sub { my $response = Dancer2::Core::Response->new( charset => 'UTF-8' ); $response->content("hi$CHAR"); ok( !utf8::is_utf8( $response->content ), 'the stored content is bytes, not characters' ); is( $response->content, "hi$BYTES", 'encoded as UTF-8' ); ok( $response->is_encoded, 'and marked as encoded' ); my $got = psgi_parts($response); is( $got->{body}[0], "hi$BYTES", 'the PSGI body is those bytes' ); # 5 bytes, 3 characters. This is the assertion that fails if # Content-Length is ever taken before encoding. is( $got->{header}{'Content-Length'}, 5, 'Content-Length is the byte count' ); is( length $got->{body}[0], 5, 'which matches the body actually sent' ); isnt( $got->{header}{'Content-Length'}, 3, 'and is not the character count' ); is( $got->{header}{'Content-Type'}, 'text/html; charset=UTF-8', 'the charset is declared once, appended to the default type' ); # Converting twice must not append a second charset or a second # Content-Length, and must not re-encode. my $again = psgi_parts($response); is( $again->{header}{'Content-Type'}, 'text/html; charset=UTF-8', 'a second conversion does not append the charset again' ); is( $again->{header}{'Content-Length'}, 5, 'nor a second Content-Length' ); is( $again->{body}[0], "hi$BYTES", 'and does not double-encode the body' ); }; subtest 'an explicit charset in the content type is respected' => sub { my $response = Dancer2::Core::Response->new( charset => 'UTF-8' ); $response->content_type('text/plain; charset=UTF-8'); $response->content("hi$CHAR"); my $got = psgi_parts($response); is( $got->{header}{'Content-Type'}, 'text/plain; charset=UTF-8', 'the content type is left exactly as given, with one charset' ); is( $got->{body}[0], "hi$BYTES", 'and the content is encoded to match' ); }; subtest 'no charset is appended to a non-text content type' => sub { my $response = Dancer2::Core::Response->new( charset => 'UTF-8' ); $response->content_type('application/json'); $response->content(qq[{"m":"$CHAR"}]); my $got = psgi_parts($response); is( $got->{header}{'Content-Type'}, 'application/json', 'the content type gains no charset parameter' ); unlike( $got->{header}{'Content-Type'}, qr/charset/, 'not even an empty one' ); # Non-text content is deliberately not encoded here - a serializer is # expected to have produced bytes already and to have set is_encoded. # Pinned as current behavior: assigning characters under a non-text type # by hand leaves them as characters, and Content-Length then counts # characters rather than bytes. ok( !$response->is_encoded, 'and the content is not encoded' ); ok( utf8::is_utf8( $got->{body}[0] ), 'so a character string stays characters' ); is( $got->{header}{'Content-Length'}, 9, 'with Content-Length counting those characters' ); }; subtest 'replacing the content after the first set is encoded on its own merits (fixed)' => sub { # is_encoded describes the content currently assigned, not the response # for all time. The 'around content' modifier (Response.pm:137-...) # resets is_encoded on every fresh assignment before calling # encode_content, so a *second* assignment of character content is # encoded exactly like the first would have been - it does not inherit # the previous content's encoded status. # # This matters because it is reachable from ordinary code: an 'after' # hook that rewrites response->content, or halt() called once content # was already set. my $response = Dancer2::Core::Response->new( charset => 'UTF-8' ); $response->content('first'); # ASCII, latches is_encoded ok( $response->is_encoded, 'the first assignment marks the response encoded' ); $response->content("hi$CHAR"); # characters, must be encoded afresh ok( !utf8::is_utf8( $response->content ), 'the stored content is bytes, not characters' ); ok( $response->is_encoded, 'and the replacement is marked as encoded too' ); my $got = psgi_parts($response); is( $got->{body}[0], "hi$BYTES", 'the replacement content was UTF-8 encoded' ); is( $got->{header}{'Content-Length'}, 5, 'Content-Length is the 5 bytes UTF-8 needs, not the 3-character count' ); like( $got->{header}{'Content-Type'}, qr/charset=UTF-8/, 'and the Content-Type still declares UTF-8, now truthfully' ); # A second read/conversion must not re-encode already-encoded bytes. my $again = psgi_parts($response); is( $again->{body}[0], "hi$BYTES", 'a second to_psgi does not double-encode the body' ); is( $again->{header}{'Content-Length'}, 5, 'nor change the Content-Length' ); }; subtest 'statuses that forbid a body get no body and no Content-Length' => sub { for my $status ( 100, 101, 204, 304 ) { my $response = Dancer2::Core::Response->new( charset => 'UTF-8' ); $response->status($status); $response->header( 'X-Keep' => 'yes' ); $response->content('BODY'); my $got = psgi_parts($response); is( $got->{status}, $status, "$status: the status is preserved" ); is_deeply( $got->{body}, [], "$status: the body is dropped entirely" ); is( $got->{header}{'Content-Length'}, undef, "$status: and no Content-Length is sent" ); is( $got->{header}{'X-Keep'}, 'yes', "$status: other headers are still sent" ); } # The neighbours must keep their bodies, or the check above is just # "everything is empty". for my $status ( 200, 205, 404 ) { my $response = Dancer2::Core::Response->new( charset => 'UTF-8' ); $response->status($status); $response->content('BODY'); my $got = psgi_parts($response); is_deeply( $got->{body}, ['BODY'], "$status: keeps its body" ); is( $got->{header}{'Content-Length'}, 4, "$status: and reports its length" ); } }; subtest 'the default content type is applied when none was set' => sub { my $default = Dancer2::Core::Response->new( charset => 'UTF-8' ); $default->content('x'); is( psgi_parts($default)->{header}{'Content-Type'}, 'text/html; charset=UTF-8', 'text/html is the default' ); my $configured = Dancer2::Core::Response->new( charset => 'UTF-8', default_content_type => 'text/plain', ); $configured->content('x'); is( psgi_parts($configured)->{header}{'Content-Type'}, 'text/plain; charset=UTF-8', 'a configured default is used instead of text/html' ); # A response nothing was ever done to still converts cleanly - this is # the "all routes passed" case. my $untouched = Dancer2::Core::Response->new; my $got = psgi_parts($untouched); is( $got->{status}, 200, 'an untouched response is a 200' ); is( $got->{header}{'Content-Type'}, 'text/html', 'with the default content type' ); is( $got->{header}{'Content-Length'}, 0, 'a zero Content-Length' ); is_deeply( $got->{body}, [''], 'and an empty body' ); }; subtest 'CR and LF are stripped from header values' => sub { my $response = Dancer2::Core::Response->new( charset => 'UTF-8' ); $response->content('x'); # A CRLF in a header value is how a response-splitting attack injects a # header of its own. It must not survive into the PSGI array. $response->header( 'X-Split' => "ok\r\nInjected: yes" ); # Folded continuation lines collapse to a single space instead. $response->header( 'X-Folded' => "a\r\n\tb" ); my $got = psgi_parts($response); is( $got->{header}{'X-Split'}, 'okInjected: yes', 'the CRLF is removed, leaving one header value' ); unlike( $got->{header}{'X-Split'}, qr/[\r\n]/, 'no carriage return or line feed remains' ); is( $got->{header}{'X-Folded'}, 'a b', 'linear whitespace folding collapses to a single space' ); # Nothing anywhere in the array may carry a bare CR or LF. my @offending = grep { /[\r\n]/ } @{ $got->{raw} }; is_deeply( \@offending, [], 'no element of the PSGI header array contains CR or LF' ); }; subtest 'CR and LF are stripped from a header name (fixed)' => sub { # headers_to_array strips CR and LF from each header *value* # (Dancer2/Core/Response.pm:70-..., commented "remove CR and LF since the # char is invalid here") and now applies the same sanitisation to the # *name*, a few lines away in the same loop. A CRLF injected into the # name must not reach the PSGI array intact - that is the same # response-splitting shape the value is guarded against. my $response = Dancer2::Core::Response->new( charset => 'UTF-8' ); $response->content('x'); $response->header( "X-Bad\r\nInjected: yes" => 'v' ); my $got = psgi_parts($response); # Nothing anywhere in the array may carry a bare CR or LF - name or # value. my @with_newlines = grep { /[\r\n]/ } @{ $got->{raw} }; is_deeply( \@with_newlines, [], 'no element of the PSGI header array contains CR or LF' ); # The sanitised name is still present - the header was neutralised, not # silently dropped. (HTTP::Headers::Fast canonicalises the field name # case, which is why "yes" becomes "Yes" here.) ok( ( grep { /^X-BadInjected: Yes$/ } @{ $got->{raw} } ), 'the sanitised header name is still present' ); }; done_testing(); logger000755000765000024 015267764222 15027 5ustar00jasonstaff000000000000Dancer2-2.3.0/t/unitformat.t100644000765000024 2773315267764222 16700 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/unit/loggeruse strict; use warnings; use Test::More; use POSIX (); use Dancer2::Core::Time (); use Dancer2::Logger::Capture; use Dancer2::Core::Request; # log_format substitution. Two syntaxes share one regex # (Dancer2/Core/Role/Logger.pm:110-116): # # %X a single-character code, looked up in map_chars_to_subs # %{thing}X a block code, handled by the block handler (only t and h) # # An unrecognised code in either syntax must warn and render '-' rather than # abort the log call: losing a log line because its format string has a typo in # it would be a poor trade. # # format_message is called directly rather than through a request, because the # substitutions are what is under test and a full dispatch would add nothing. my $REQUEST = do { my $body = ''; open my $input, '<', \$body or die "cannot open in-memory body: $!"; Dancer2::Core::Request->new( env => { REQUEST_METHOD => 'GET', PATH_INFO => '/some/path', QUERY_STRING => '', SCRIPT_NAME => '', SERVER_NAME => 'localhost', SERVER_PORT => 80, CONTENT_LENGTH => 0, REMOTE_ADDR => '10.0.0.9', HTTP_USER_AGENT => 'TestAgent/1.0', HTTP_X_CUSTOM => 'CUSTOM-HEADER-VALUE', 'psgi.url_scheme' => 'http', 'psgi.input' => $input, 'psgi.errors' => \*STDERR, }, ); }; # Format one message with the given format string, returning the rendered line # and any warnings it produced. sub render { my ( $format, %opt ) = @_; my $logger = Dancer2::Logger::Capture->new( log_format => $format, app_name => 'MyApp', config => {}, ( exists $opt{caller_stack_size} ? ( caller_stack_size => $opt{caller_stack_size} ) : () ), ); $logger->set_request($REQUEST) unless $opt{no_request}; my @warnings; my $line = do { local $SIG{__WARN__} = sub { push @warnings, $_[0] }; $logger->format_message( 'warning', 'THE-MESSAGE' ); }; chomp $line; return ( $line, \@warnings ); } sub rendered { my ($line) = render(@_); return $line } subtest 'the message, level and app name substitute' => sub { is( rendered('%m'), 'THE-MESSAGE', '%m is the message' ); is( rendered('%L'), 'warning', '%L is the level' ); is( rendered('%a'), 'MyApp', '%a is the app name' ); is( rendered('%P'), $$, '%P is the process id' ); # Surrounding literal text is preserved, and several codes can appear in # one format. is( rendered('[%L] %m (%a)'), '[warning] THE-MESSAGE (MyApp)', 'codes substitute in place, literals are kept' ); # An app that set no name gets the documented default. my $unnamed = Dancer2::Logger::Capture->new( log_format => '%a', config => {} ); is( $unnamed->format_message( 'info', 'x' ), "-\n", 'the app name defaults to "-"' ); }; subtest 'the request-derived codes substitute' => sub { is( rendered('%h'), '10.0.0.9', '%h is the host that made the request' ); is( rendered('%i'), $REQUEST->id, '%i is the request id' ); # With no request attached, both fall back rather than dying. is( rendered( '%h', no_request => 1 ), '-', '%h is "-" when there is no request' ); is( rendered( '%i', no_request => 1 ), '-', '%i is "-" when there is no request' ); }; subtest 'the date codes substitute in their documented formats' => sub { # The exact instant is not asserted - only the shape, which is what # distinguishes the four codes from each other. # # The numeric fields are held to their documented shape, since every # locale renders a number as a number. The month name is not: %b is # whatever the platform's locale calls the month, and that varies more # than one might hope - German gives "Mär" (non-ASCII), French "sept." # or "mars" (lower-case, and sometimes punctuated). Pinning it to three # ASCII letters, as this subtest used to, fails the suite on any such # system even when the code is behaving exactly as documented. # # So all it asks of the month is that it contain a name rather than be one # number, which is what keeps the case that actually broke - some Windows # perls, where %b returns the month *number*, giving "29/9/2026" for # September - from passing as though it were fine. It must not require the # name to *start* with a non-digit, or CJK month names such as "10月" would # be rejected although they are perfectly good names. my $MONTH = qr{[^/]*[^\d/][^/]*}; like( rendered('%t'), qr{^\d{2}/$MONTH/\d{4} \d{2}:\d{2}:\d{2}$}, '%t is dd/Mon/yyyy hh:mm:ss' ); like( rendered('%T'), qr{^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$}, '%T is yyyy-mm-dd hh:mm:ss' ); like( rendered('%u'), qr{^\d{2}/$MONTH/\d{4} \d{2}:\d{2}:\d{2}$}, '%u is dd/Mon/yyyy hh:mm:ss, in UTC' ); like( rendered('%U'), qr{^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$}, '%U is yyyy-mm-dd hh:mm:ss, in UTC' ); }; subtest 'a numeric %b falls back to the English month name' => sub { # The fallback only runs where strftime's %b is not a name. On Linux and # macOS it always is, so without forcing the broken behaviour here the # fallback would never be exercised, and a mistake in it would pass the # whole suite unnoticed. Pretend, for the duration, that the platform is # one of those where %b answers with the month number. no warnings 'redefine'; local *POSIX::strftime = sub {'9'}; # Check for this month's name, not just the shape of one: any three # letters would pass a shape check, so an off-by-one in the month index # the fallback is handed would slip through. my @en = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec); my $local = $en[ (localtime)[4] ]; my $utc = $en[ (gmtime)[4] ]; like( rendered('%t'), qr{^\d{2}/\Q$local\E/\d{4} }, '%t falls back to the right month name' ); like( rendered('%u'), qr{^\d{2}/\Q$utc\E/\d{4} }, '%u falls back to the right month name' ); # And the table behind it. is( Dancer2::Core::Time::month_abbrev($_), $en[$_], "month_abbrev($_)" ) for 0 .. 11; }; subtest 'the caller codes substitute from the stack' => sub { # caller_stack_size decides which frame %p, %f and %l report. It defaults # to 9, which is where the frame sits when the call came through a route; # calling format_message directly here means that frame does not exist, so # the codes fall back to '-'. is( rendered('%p|%f|%l'), '-|-|-', 'the codes are "-" when the configured frame is off the top of the stack' ); # Pointed at a frame that does exist, they substitute for real. This is # what shows the codes are wired up and not simply always '-'. my $line = rendered( '%p|%f|%l', caller_stack_size => 1 ); my ( $package, $file, $lineno ) = split /\|/, $line; is( $package, 'main', '%p is the calling package' ); like( $file, qr/format\.t$/, '%f is the calling file' ); like( $lineno, qr/^\d+$/, '%l is a line number' ); }; subtest 'a %{...}h block reads the named request header' => sub { is( rendered('%{X-Custom}h'), 'CUSTOM-HEADER-VALUE', 'a custom header is read by name' ); is( rendered('%{User-Agent}h'), 'TestAgent/1.0', 'and so is a standard one' ); # A header that is not present renders '-' rather than the empty string, so # the field stays visible in the log line. is( rendered('%{X-Missing}h'), '-', 'an absent header renders as "-"' ); is( rendered( '%{X-Custom}h', no_request => 1 ), '-', 'and so does any header when there is no request' ); }; subtest 'a %{...}t block is a strftime format' => sub { is( rendered('%{%Y}t'), POSIX::strftime( '%Y', localtime(time) ), 'the block is passed to strftime' ); like( rendered('%{%Y-%m}t'), qr/^\d{4}-\d{2}$/, 'a multi-part format works too' ); }; subtest 'an unrecognised code warns and renders "-"' => sub { # The log call must survive. A format string with a typo should degrade the # one field, not lose the message. my ( $line, $warnings ) = render('%Z'); is( $line, '-', 'the unknown code renders as "-"' ); is( scalar @$warnings, 1, 'and warns once' ); like( $warnings->[0], qr/%Z not supported/, 'naming the code that was not recognised' ); # The rest of the format still renders, which is the part that matters: # the message is not lost. my ( $mixed, $mixed_warnings ) = render('%L|%Z|%m'); is( $mixed, 'warning|-|THE-MESSAGE', 'the surrounding codes still substitute' ); is( scalar @$mixed_warnings, 1, 'with one warning for the bad code' ); }; subtest 'an unrecognised block type warns and renders "-"' => sub { my ( $line, $warnings ) = render('%{whatever}z'); is( $line, '-', 'the unknown block renders as "-"' ); is( scalar @$warnings, 1, 'and warns once' ); like( $warnings->[0], qr/\{whatever\}z not supported/, 'naming the block and its type' ); }; subtest 'an unsupported format character behaves the same whether or not it was ever documented' => sub { # %D used to be listed in the log_format POD ("timer") despite # map_chars_to_subs never implementing it. Rather than invent a timer # with no state in the logger role to back it, the fix was to remove %D # from the docs, so it is now just another unsupported character. This # checks it is indistinguishable from %Z, which was never documented at # all: both warn once per logged message and render '-'. for my $char (qw(D Z)) { my ( $line, $warnings ) = render("%$char"); is( $line, '-', "%$char renders as \"-\"" ); is( scalar @$warnings, 1, "%$char warns once per logged message" ); like( $warnings->[0], qr/%\Q$char\E not supported/, "the warning names %$char" ); } # The comparison that makes the point: a once-documented code and one that # was never documented are treated identically. my ( $d_line ) = render('%D'); my ( $z_line ) = render('%Z'); is( $d_line, $z_line, '%D behaves exactly like %Z' ); }; subtest 'every documented format character renders without warning' => sub { # The flip side of the subtest above: every character actually listed in # the log_format POD must resolve through map_chars_to_subs (or the block # handler), never fall through to the unsupported-code path. If this list # and the POD list ever drift apart, this is where it shows up. for my $char (qw(a h t T u U P L m p f l i)) { my ( undef, $warnings ) = render("%$char"); is( scalar @$warnings, 0, "%$char does not warn" ); } # The two block forms take an argument, so they are exercised separately # from the single-character codes above. for my $block ( '%{X-Custom}h', '%{%Y}t' ) { my ( undef, $warnings ) = render($block); is( scalar @$warnings, 0, "$block does not warn" ); } }; subtest 'the default log_format renders every one of its parts' => sub { # '[%a:%P] %L @%T> %m in %f l. %l' - the default. Rendered with a stack # size that resolves, so no part is '-' for reasons unrelated to the format. my $logger = Dancer2::Logger::Capture->new( app_name => 'MyApp', caller_stack_size => 1, config => {}, ); $logger->set_request($REQUEST); my $line = $logger->format_message( 'warning', 'THE-MESSAGE' ); chomp $line; like( $line, qr{^ \[MyApp:$$\]\ # [%a:%P] warning\ @ # %L @ \d{4}-\d{2}-\d{2}\ \d{2}:\d{2}:\d{2}>\ # %T> THE-MESSAGE\ in\ # %m in \S+format\.t\ l\.\ \d+ # %f l. %l $}x, 'the default format renders as documented, with no unsubstituted codes', ); unlike( $line, qr/%[a-zA-Z]/, 'no format code is left unsubstituted' ); unlike( $line, qr/(?; use Dancer2::Logger::Capture; # Which messages get through, and what a message turns into on the way. # # The level table in Dancer2::Core::Role::Logger puts 'core' at -10 and the # user-facing levels at 1..4, and _should is a single numeric comparison: # # $levels->{configured} <= $levels->{message} # # So 'core' is below every user level by construction, and the only way to see # core messages is to configure log_level: core. That is what keeps Dancer2's # own internal chatter ("looking for get /", "Entering hook ...") out of an # application's log. # # Dancer2::Logger::Capture is used as the engine: it stores messages instead of # printing them and ships as a supported part of this distribution, so no # stand-in is involved. my @LEVELS = qw< core debug info warning error >; sub logger_at { my $level = shift; return Dancer2::Logger::Capture->new( log_level => $level, config => {} ); } # Emit one message at every level, return the levels that actually got through. sub emitted_under { my $configured = shift; my $logger = logger_at($configured); $logger->$_("message-at-$_") for @LEVELS; return [ map { $_->{level} } @{ $logger->trapper->read } ]; } subtest 'a level lets through itself and everything above it' => sub { # Written out in full rather than computed, so the expectation is readable # and a change has to be made deliberately. my %expected = ( core => [qw< core debug info warning error >], debug => [qw< debug info warning error >], info => [qw< info warning error >], warning => [qw< warning error >], error => [qw< error >], ); for my $configured ( @LEVELS ) { is_deeply( emitted_under($configured), $expected{$configured}, "log_level '$configured' emits exactly @{$expected{$configured}}" ); } }; subtest 'core messages are hidden from every user-facing level' => sub { # The point of the negative level. Dancer2 logs its own dispatch trace at # core, and an application that asked for debug must not receive it. for my $configured ( qw< debug info warning error > ) { my $got = emitted_under($configured); ok( !grep( { $_ eq 'core' } @$got ), "log_level '$configured' does not emit core messages" ); } # And the control: they are not simply never emitted. my $got = emitted_under('core'); ok( scalar grep( { $_ eq 'core' } @$got ), "log_level 'core' does emit them" ); }; subtest 'warn is accepted as a synonym for warning' => sub { # 'warn' is in the level table at the same value as 'warning', so it is a # legal log_level even though there is no warn() method. my $logger = logger_at('warn'); $logger->debug('below'); $logger->warning('at'); $logger->error('above'); is_deeply( [ map { $_->{level} } @{ $logger->trapper->read } ], [ 'warning', 'error' ], "log_level 'warn' filters exactly as 'warning' does" ); }; subtest 'an unknown level is refused at construction' => sub { # Better than silently defaulting: a typo in the config is fatal rather # than quietly turning logging up or off. my $err = exception { Dancer2::Logger::Capture->new( log_level => 'verbose', config => {} ); }; ok( defined $err, "log_level 'verbose' is refused" ); like( $err, qr/verbose/, 'and the value is named in the error' ); # Every level in the table is accepted, including the synonym. for my $level ( @LEVELS, 'warn' ) { is( exception { Dancer2::Logger::Capture->new( log_level => $level, config => {} ) }, undef, "log_level '$level' is accepted", ); } }; subtest 'message arguments are serialised for logging' => sub { my $logger = Dancer2::Logger::Capture->new( log_format => '[%m]', config => {}, ); # A trailing newline is chomped, so a log line is not double-spaced. $logger->info("trailing newline\n"); # A reference is dumped compactly, with keys sorted so the output is # deterministic. $logger->info( { b => 2, a => 1 } ); # Several arguments are concatenated, and undef becomes the string 'undef' # rather than triggering a warning. $logger->info( 'x', 'y', undef ); my @formatted = map { $_->{formatted} } @{ $logger->trapper->read }; chomp @formatted; is( $formatted[0], '[trailing newline]', 'a trailing newline is removed from the message' ); is( $formatted[1], "[{'a' => 1,'b' => 2}]", 'a hashref is dumped on one line with sorted keys' ); is( $formatted[2], '[xyundef]', 'multiple arguments concatenate and undef becomes "undef"' ); }; subtest 'a formatted line always ends with a newline' => sub { # The engines write the formatted string as-is, so the trailing newline has # to come from format_message. my $logger = Dancer2::Logger::Capture->new( log_format => '%L: %m', config => {}, ); $logger->info('no newline'); $logger->info("one newline\n"); $logger->info("two newlines\n\n"); $logger->info("newline\nin the middle"); my @formatted = map { $_->{formatted} } @{ $logger->trapper->read }; like( $_, qr/\n\z/, 'the line ends with a newline' ) for @formatted; is( $formatted[0], "info: no newline\n", 'a message with no newline gets exactly one' ); is( $formatted[1], "info: one newline\n", 'a message with one trailing newline still gets exactly one' ); # chomp removes a single trailing newline, so a message that ended with two # produces a blank line. Pinned as current behavior: it is a cosmetic wart, # not a correctness problem, and the fix would be a chomp-while loop. is( $formatted[2], "info: two newlines\n\n", 'a message ending in two newlines leaves a blank line behind' ); # An interior newline is untouched - only the trailing one is chomped. is( $formatted[3], "info: newline\nin the middle\n", 'a newline inside the message is left alone' ); }; done_testing(); pretty000755000765000024 015267764222 15433 5ustar00jasonstaff000000000000Dancer2-2.3.0/t/corpus505.tt100644000765000024 10015267764222 16424 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/corpus/prettyTemplate selected. message: [% content %] status: [% status %] folder000755000765000024 015267764222 15201 5ustar00jasonstaff000000000000Dancer2-2.3.0/t/viewspage.tt100644000765000024 2315267764222 16561 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/views/folderPage under folder. error_template.t100644000765000024 370515267764222 16731 0ustar00jasonstaff000000000000Dancer2-2.3.0/t/dsluse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; { package CustomError; use Dancer2; set views => 't/corpus/pretty'; set public_dir => 't/corpus/pretty_public'; get '/error' => sub { send_error "oh my", 505; }; get '/public' => sub { send_error "static", 510; }; } { package StandardError; use Dancer2; set show_stacktrace => 1; get '/no_template' => sub { send_error "oopsie", 404; }; } my $custom_error_app = CustomError->to_app; my $standard_error_app = StandardError->to_app; ok( is_coderef($custom_error_app), 'Got app' ); ok( is_coderef($standard_error_app), 'Got app' ); my $custom_error_test = Plack::Test->create($custom_error_app); my $standard_error_test = Plack::Test->create($standard_error_app); subtest "/error" => sub { my $res = $custom_error_test->request( GET '/error' ); is $res->code, 505, 'send_error sets the status to 505'; like $res->content, qr{Template selected}, 'Error message looks good'; like $res->content, qr{message: oh my}; like $res->content, qr{status: 505}; }; subtest "/public" => sub { my $res = $custom_error_test->request( GET '/public' ); is $res->code, 510, 'send_error sets the status to 510'; like $res->content, qr{Static page}, 'Error message looks good'; }; subtest '404 with static template' => sub { my $res = $custom_error_test->request( GET '/middle/of/nowhere' ); is $res->code, 404, 'unknown route => 404'; like $res->content, qr{you're lost}i, 'Error message looks good'; }; subtest "/no_template" => sub { my $res = $standard_error_test->request( GET '/no_template' ); is $res->code, 404, 'send_error sets the status to 404'; like $res->content, qr{

Error 404 - Not Found

}, 'Error message looks good'; unlike $res->content, qr{Stack}, 'Error contains no stack trace'; }; done_testing; Policy.pod100644000765000024 764015267764222 16464 0ustar00jasonstaff000000000000Dancer2-2.3.0/lib/Dancer2package Dancer2::Policy; # ABSTRACT: Dancer core and community policy and standards of conduct use strict; use warnings; 1; __END__ =pod =encoding UTF-8 =head1 NAME Dancer2::Policy - Dancer core and community policy and standards of conduct =head1 VERSION version 2.3.0 =head1 DESCRIPTION This document describes various policies (most notably, the standards of conduct) for the Dancer core developers and broad community. This is what we expect from our community and ourselves and these are the standards of behavior we set forth in order to make sure the community remains a safe space for all of its members, without exception. =head1 STANDARDS OF CONDUCT These standards apply anywhere the community comes together as a group. This includes, but is not limited to, the Dancer IRC channel, the Dancer mailing list, Dancer hackathons, and Dancer conferences. =over 4 =item * Always be civil. =item * Heed the moderators. =item * Abuse is not tolerated. =back Civility is simple: stick to the facts while avoiding demeaning remarks and sarcasm. It is not enough to be factual. You must also be civil. Responding in kind to incivility is not acceptable. If the list moderators tell you that you are not being civil, carefully consider how your words have appeared before responding in any way. You may protest, but repeated protest in the face of a repeatedly reaffirmed decision is not acceptable. Unacceptable behavior will result in a public and clearly identified warning. Repeated unacceptable behavior will result in removal from the mailing list and revocation of any commit bit. The first removal is for one month. Subsequent removals will double in length. After six months with no warning, a user's ban length is reset. Removals, like warnings, are public. The list of moderators consists of all active core developers. This includes, in alphabetical order, Alberto Simões, David Precious, Jason Crome, Mickey Nasriachi, Peter Mottram, Russell Jenkins, Sawyer X, Stefan Hornburg (Racke), and Yanick Champoux. This list might additionally grow to active members of the community who have stepped up to help handle abusive behavior. If this should happen, this document would be updated to include their names. Additionally, it's important to understand the self-regulating nature we foster at the Dancer community. This means anyone and everyone in the community - in the channel, on the list, at an event - has the ability to call out unacceptable behavior and incivility to others in the community. Moderators are responsible for issuing warnings and take disciplinary actions, but anyone may - and is encouraged - to publicly make note of unacceptable treatment of others. As a core principle, abuse is never tolerated. One cannot berate, insult, debase, deride, put down, or vilify anyone, or act towards anyone in a way intending to hurt them. The community specifically considers as abuse any attempts to otherize anyone by any individual characteristic, including, but not limited to, their technical skill, knowledge or by their age, colour, disability, gender, language, national or social origin, political or other opinion, race, religion, sex, or sexual orientation. The community aims to maintain a safe space for everyone, in any forum it has. If you ever feel this core principle has been compromised, you are strongly urged to contact a moderator. We are always here. Remember, this is B community, as much as it is anyone else's. =head1 CREDITS This policy has been adopted and adapted from the policy available for the Perl language development, provided by B (the Perl 5 Porters). The original inspiration policy document can be read at L. =head1 AUTHOR Dancer Core Developers =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2026 by Alexis Sukrieh. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut Manual.pod100644000765000024 22735515267764222 16511 0ustar00jasonstaff000000000000Dancer2-2.3.0/lib/Dancer2# ABSTRACT: A guide to building web applications with Dancer2 package Dancer2::Manual; __END__ =pod =encoding UTF-8 =head1 NAME Dancer2::Manual - A guide to building web applications with Dancer2 =head1 VERSION version 2.3.0 =head1 Introduction to Dancer2: Managing Danceyland Welcome to Danceyland! As the new manager of this amazing park, you'll be maintaining and enhancing the experience for all your visitors. Imagine each attraction, food stall, and ticket booth as different locations in your web application. Let's explore how to manage these various components using Dancer2. If you're not sure if you're at the correct spot in the park, the L can help you find your way. =head2 What is Dancer2? Dancer2 is a free and open-source web application framework written in Perl. It’s a complete rewrite of the original L, designed to be powerful and flexible, yet incredibly easy to use. With Dancer2, getting your web app up and running is a breeze. It boasts a rich ecosystem of adapters for popular template engines, session storage solutions, logging methods, serializers, and plugins. This means you can build your app your way, effortlessly. In this guide, we'll leverage those strengths to build and manage Danceyland. Before we learn the ins and outs of managing your park, make sure you head over to the L and get your development machine set up with Dancer2. Once it's installed, make sure to build your park: dancer2 gen -a Danceyland =head1 Routes: Different Attractions and Facilities Core to Dancer2 is the concept of routes. Each attraction in your theme park is like a route in your web application. Visitors (users) can navigate to different attractions, just as they would visit different routes in your app. Let's show some of the rides and facilities in our park: # Defining routes for our theme park get '/' => sub { return "Welcome to Danceyland!"; }; get '/roller-coaster' => sub { return "Enjoy the thrilling roller coaster ride!"; }; post '/buy-ticket' => sub { my $ticket = body_parameters->get('ticket'); # Do something with ticket data return "You bought a $ticket ticket!"; }; =over =item The `/` route is like the main entrance to our theme park. Visitors are greeted with a welcome message. =item The `/roller-coaster` route is a thrilling ride. When visitors take this path, they get a special message. =item The `/buy-ticket` route is the ticket booth. Visitors buy their tickets here. =back =head2 New Keywords =over =item get This keyword defines a route that responds to HTTP GET requests. It takes two parameters: the route path and a subroutine that defines the response. get '/path' => sub { return "Response text"; }; Note that a route to match C requests is automatically created when you create a C route. =item post This keyword defines a route that responds to HTTP POST requests. It works similarly to C, but is used for actions like submitting forms or buying tickets. post '/path' => sub { my $param = body_parameters->get('param'); return "You submitted: $param"; }; =item query This keyword defines a route that responds to HTTP QUERY requests. It works similarly to C, in that the search or lookup details are sent in the request body, but it is used for read-only actions like looking up tickets. Like C, it should not change anything on the server. query '/path' => sub { my $param = body_parameters->get('param'); return "You searched for: $param"; }; =back So exactly what are these HTTP requests, and what are they all about? =head2 HTTP Methods: Visitor Actions Think of HTTP methods as the different actions visitors can take in your theme park. Entering the park, buying tickets, updating ticket details, and leaving the park are all actions represented by C, C, C, C, C, C, and C methods respectively. Handling visitor actions in the park: # Defining HTTP methods for visitor actions get '/' => sub { return "Welcome to Danceyland!"; }; post '/buy-ticket' => sub { my $ticket = body_parameters->get('ticket'); return "You bought a $ticket ticket!"; }; query '/search-ticket' => sub { my $ticket = body_parameters->get('ticket'); return "You searched for $ticket ticket!"; }; put '/update-ticket' => sub { my $new_ticket = body_parameters->get('new_ticket'); return "Your ticket has been updated to $new_ticket!"; }; del '/leave-park' => sub { return "Thank you for visiting! Please come again!"; }; options '/park/info' => sub { return "Allowed methods: GET, POST, PUT"; }; patch '/profile/:id' => sub { my $user_id = route_parameters->get('id'); my $new_email = body_parameters->get('email'); return "Updated profile for user $user_id with email $new_email"; }; =over =item GET: Visitors enter the park and see a welcome message. =item POST: Visitors buy a ticket. =item QUERY: Visitors search for a ticket. =item PUT: Visitors update their ticket details. =item DELETE: Visitors leave the park. =item PATCH: Update part of a visitor's profile (in this case, email). =item OPTIONS: Describing the available operations on a specific route. Keep in mind, you would rarely implement this in your web application. =back These are good conventions to follow, but you can make your route handlers do whatever makes the most sense for your application. =head2 Routes, Route Definitions, and Route Handlers You may hear other Dancer developers talk about "routes", "route definitions", and "route handlers". "Route definitions" refers to the HTTP method and URL to respond to, while "route handler" is only the code implementing functionality for that definition. The two of these together make what's known as a route. get '/' => sub {...}; =over =item The verb, path, and subroutine is a route definition (AKA "route"). =item Only the subroutine reference (C) is the route handler. =item The route definition I the route handler collectively are the route. =back Each route requires a defintion and handler. The route needs to either return a string or Perl data structure to be rendered for the client. We'll learn more about rendering data structures later in our guide to Danceyland; for now, we're going to focus on returning strings, which will be rendered as HTML. =head3 What if we want a single park location to respond to multiple request types? Route definitions can use C to match all, or a specified list of HTTP methods. The following will match any HTTP request to the path C: any '/visitor-center' => sub { # Write code to do something at the visitor center! } The following will match GET or POST requests to C: any ['get', 'post'] => '/visitor-center' => sub { # Write code to do something at the visitor center! }; =head2 URI Generation Dancer2 can generate URIs using the C and C keywords. Letting Dancer2 generate URIs helps ensure consistency, and reduces the amount of maintenance needed by making sure URIs are always up to date as the application evolves over time. =head3 uri_for The C keyword is used to generate a URI for a given path within your application, including query parameters. It's especially useful when you want to construct URLs dynamically inside your routes. =head4 Example: Generating a URI in Danceyland get '/ride/:name' => sub { my $ride_name = route_parameters->get('name'); return 'Enjoy the ride at ' . uri_for("/ride/$ride_name"); }; In this example, C generates a full URI for the ride name in Danceyland. =head3 uri_for_route The C keyword creates a URL for a named route. In Danceyland, it can be used to generate a URL to any part of the park that has a named route. For more information, see L. =head3 Example 1: Basic Usage get 'films' => '/cinemaland/film-gallery' => sub { return "See the films at " . uri_for_route('films'); }; In this example, the route is named C, and C generates a URL pointing to it. =head3 Example 2: Using Route Parameters get 'ride' => '/ride/:name' => sub { my $ride_name = route_parameters->get('name'); return "Ride details: " . uri_for_route('ride', { name => $ride_name }); }; This example uses C to generate a URL that includes the named route parameter C. =head3 Example 3: Including Query Parameters get 'search' => '/search' => sub { return uri_for_route('search', { q => 'roller coaster' }); }; get '/search' => sub { my $query = query_parameters->get('q'); return "Search results for: $query"; }; In this example, C generates a URL for the named route C with the query parameter C set to "roller coaster". =head2 Parameter Handling In Danceyland, visitors will often need to communicate with park staff. Similarly, your apps will need to take in information from application users via parameters. Dancer2 provides several methods for handling different types of parameters. =head3 Keywords for working with parameters =head4 param and params (Not Recommended) The C and C keywords are legacy methods to access request parameters. While still functional, they are not recommended for new applications. Instead, you should use the more specific keywords like C, C, and C for clarity and precision. =head4 Example: Using param (Not Recommended) get '/submit' => sub { # Please use query_parameters() shown below my $name = param('name'); return "Submitted name: $name"; }; =head4 Example: Using params (Not Recommended) get '/all-params' => sub { # Pleaase use query_parameters() shown below my %all_params = params; return "All parameters: " . join(', ', %all_params); }; C and C are included here for completeness but are not the preferred methods for handling parameters in Danceyland. =head4 body_parameters This keyword retrieves parameters from the body of the request, typically used in POST requests. Example of C with multiple values: post '/submit' => sub { my $name = body_parameters->get('name'); my $email = body_parameters->get('email'); return "Submitted name: $name, email: $email"; }; =head4 query_parameters This keyword retrieves parameters from the query string of the request URL: # Matches URL: /search?q=rides&cat=thrill get '/search' => sub { my $query = query_parameters->get('q'); my $category = query_parameters->get('cat'); return "Search query: $query in category: $category"; }; The above route would match the URL C. =head4 route_parameters This keyword retrieves named parameters from the route declaration: # Matches URL: /user/123 get '/user/:id' => sub { my $user_id = route_parameters->get('id'); return "User ID: $user_id"; }; =head4 get_all This method works for all of the above parameter-fetching keywords. If you have a parameter that may contain more than one value, C will return an array reference containing all selected values, even if there is only a single value returned. Example of C: # Matches URL: /all-params?name=John&age=30 get '/all-params' => sub { my $params = query_parameters->get_all(); return "All query parameters: " . join(', ', %$params); }; =head3 Named Route Parameters Named route parameters allow you to capture specific segments of the URL and use them in your route handler: # Matches URL: /ride/roller-coaster get '/ride/:name' => sub { my $ride_name = route_parameters->get('name'); return "Welcome to the $ride_name ride!"; }; Named route parameters are retrieved with the L keyword. =head3 Wildcard Route Parameters Wildcard route parameters allow you to capture arbitrary parts of the URL. There are two types: C and C. C is represented by a single asterisk (C<*>), and megaspat is represented by a double asterisk (C<**>). Examples of wildcard route parameters include: =over =item splat: Captures one segment of the URL # Matches URL: /files/document.txt get '/files/*' => sub { my ($file) = splat; return "You requested the file: $file"; }; =item megasplat: Captures multiple segments of the URL # Matches URL: /files/documents/reports/2023/summary.txt get '/files/**' => sub { my @files = splat; return "You requested the files: " . join(', ', @files); }; =back =head3 Combining named and wildcard parameters You can combine named and wildcard parameters in your routes to capture both specific and arbitrary segments of the URL. Example combining named and wildcard parameters: # Matches URL: /user/123/files/documents/reports/2023/summary.txt get '/user/:id/files/**' => sub { my $user_id = route_parameters->get('id'); my @files = splat; return "User ID: $user_id requested the files: " . join(', ', @files); }; =head3 Named parameters with type constraints Type constraints allow you to enforce specific types for named parameters, ensuring that the parameters meet certain criteria. Dancer2 natively supports named parameters with type constraints without needing to rely on external plugins. Here’s how to declare and use type constraints for named route parameters in Dancer2: use Dancer2; get '/ride/:id[Int]' => sub { my $id = route_parameters->get('id'); return "Ride ID: $id"; }; get '/guest/:name[Str]' => sub { my $name = route_parameters->get('name'); return "Guest Name: $name"; }; MyApp->to_app(); =over 4 =item * B: This constraint ensures that the C parameter must be an integer. =item * B: This ensures that the C parameter must be a string. =back Note: For more complex parameter types, the C module provides additional constraints and validation for all parameter types. =head3 Wildcard Parameters with Type Constraints You can also enforce type constraints on wildcard parameters: # Matches URL: /images/photo.jpg get '/images/*.*[ArrayRef[Str]]' => sub { my ($filename, $extension) = splat; return "Filename: $filename, Extension: $extension"; }; # Matches URL: /documents/folder/subfolder/file.txt get '/documents/**[ArrayRef[Str]]' => sub { my @path = splat; return "Document path: " . join('/', @path); }; =head3 Regex Route Matching Regex route matching allows you to define routes using regular expressions, providing more flexibility in matching URLs: # Matches URL: /product/12345 get qr{/product/(\d+)} => sub { my ($product_id) = splat; return "Product ID: $product_id"; }; # Matches URL: /category/electronics get qr{/category/(\w+)} => sub { my ($category_name) = splat; return "Category: $category_name"; }; It is also possible to use named captures in regular expressions: # Matches URL: /product/12345 get qr{/product/(?\d+)} => sub { my $product_id = captures->{'product_id'}; return "Product ID: $product_id"; }; =head3 Combining Examples # Matches URL: /item/42/specifications get '/item/:id[Int]/*[ArrayRef[Str]]' => sub { my $item_id = route_parameters->get('id'); my ($detail) = splat; return "Item ID: $item_id, Detail: $detail"; }; # Matches URL: /archive/2023/07/10 get qr{/archive/(\d{4})/(\d{2})/(\d{2})} => sub { my ($year, $month, $day) = splat; return "Archive for: $year-$month-$day"; }; =head2 Organizing routes and growing your app using prefix The prefix DSL keyword helps you group related routes. For example, you can organize all the routes of a given section in Danceyland with the C keyword as such: package MyApp; use Dancer2; # Prefix for Cinemaland prefix '/cinemaland' => sub { get '/film-gallery' => sub { return "Welcome to Cinemaland! Here you can learn about famous movies."; }; get '/movie-schedule' => sub { return "Movie Schedule: 10 AM and 4 PM."; }; }; # Prefix for Actionland prefix '/actionland' => sub { get '/thrill-rides' => sub { return "Welcome to Actionland! Enjoy the thrill rides."; }; get '/roller-coaster' => sub { return "The best roller coaster in the park!"; }; }; MyApp->to_app(); This example organizes routes by grouping all cinema-related activities under C and all rides under C. =head2 Controlling the flow with forward, redirect, and pass These DSL keywords in Dancer2 allow you to manage the flow of actions within your routes. Here’s how they work in Danceyland. =head3 forward C allows you to forward the current request to another route handler, as if it were redirected, but without sending a new HTTP request. Put another way, a different route than the one requested is processed, but the address in the user's browser's bar stays the same. =head4 Example get '/guest-services' => sub { forward '/info'; # Forwards to /info }; get '/info' => sub { return "Welcome to Guest Services. How can we help you today?"; }; In this example, when a visitor goes to C, they are forwarded to the C route internally, however, the vistor's browser still shows they are in C. =head3 redirect C sends an HTTP redirect to the client, instructing their browser to make a new request to a different URL. At the end of the request, the user's browser will show a different URL than the one they originally navigated to. =head4 Example get '/old-roller-coaster' => sub { redirect '/new-roller-coaster'; }; get '/new-roller-coaster' => sub { return "Welcome to the new and improved roller coaster!"; }; When a visitor requests C, they are redirected to C. The browser's URL will now show C. =head3 pass C tells Dancer2 to skip the current route and continue looking for the next matching route. =head4 Example get '/vip-area' => sub { if (!session('is_vip')) { pass; # Skip to the next matching route if the user is not VIP } return "Welcome to the VIP area!"; }; get '/vip-area' => sub { return "Access Denied. This area is for VIPs only."; }; In this example, if the session doesn’t indicate the user is a VIP, the request will skip to the next matching route, which denies access. =head1 Templates: Displaying Information, and More! Templates in a web application help present information in a structured and visually appealing way, much like maps, schedules, and banners in a theme park. =head2 Why do we use templates? Templates help separate your display logic from your programming logic. The same code that sends JSON to satisfy an API quest could also be used to display an HTML page containing park information to a user. If the code needed to display HTML is intertwined with the code to gather the data needed, the code necessary to just produce JSON becomes unnecessarily complicated. Templates take the HTML out of your Perl code. =head2 Views In Dancer2, "views" refer to the template files that define the structure of the content presented to the users. Views are typically HTML files with embedded placeholders that are dynamically filled with data when rendered. =head3 How Views Work Suppose you have a template file called C:

<% ride_name %> Ride

Enjoy the thrilling <% ride_name %> ride at Danceyland!

You can use this view in your route handler: get '/ride/:name' => sub { my $ride_name = route_parameters->get('name'); template 'ride' => { ride_name => $ride_name }; }; =head3 Example: Displaying a welcome message on a pretty banner get '/' => sub { template 'welcome', { message => "Welcome to Danceyland!" }; }; In this example, the C route uses a template to display a welcome message. =head3 Example: Displaying the park map get '/map' => sub { template 'map', { attractions => \@attractions }; }; =head3 Example: Using templates to display various information get '/info' => sub { template 'info', { title => "Park Information", content => "Here you can find all the information about Danceyland.", hours => "Open from 10 AM to 8 PM", contact => "Call us at 123-456-7890", }; }; =over 4 =item * The C route uses a template to display a welcome message. =item * The C route uses a template to display the park's map. =item * The C route uses a template to display various pieces of information about the park. =back =head2 New Keywords =head3 template The C