From f33dc6bff6bb3f474860c68c06f486c9a5831284 Mon Sep 17 00:00:00 2001 From: nwb Date: Mon, 23 Jun 2014 14:51:57 +0800 Subject: [PATCH] =?UTF-8?q?1.=E4=BF=AE=E6=94=B9=E6=B5=8B=E8=AF=95=E7=8E=AF?= =?UTF-8?q?=E5=A2=83=E6=90=AD=E5=BB=BA=E6=96=87=E6=A1=A3=202.=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E9=83=A8=E5=88=86=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gemfile | 18 + Gemfile.lock | 37 +- db/schema.rb | 14 +- doc/测试环境搭建.doc | Bin 27136 -> 27648 bytes lib/redmine/version.rb | 6 +- test/extra/redmine_pm/repository_git_test.rb | 97 + .../redmine_pm/repository_subversion_test.rb | 294 ++ test/extra/redmine_pm/test_case.rb | 81 + test/fixtures/attachments.yml | 28 - test/fixtures/auth_sources.yml | 13 + test/fixtures/changes.yml | 22 + test/fixtures/changesets.yml | 104 + test/fixtures/comments.yml | 17 + test/fixtures/configuration/default.yml | 8 + test/fixtures/configuration/empty.yml | 7 + test/fixtures/configuration/no_default.yml | 8 + test/fixtures/configuration/overrides.yml | 9 + test/fixtures/custom_fields.yml | 165 + test/fixtures/custom_fields_projects.yml | 4 + test/fixtures/custom_fields_trackers.yml | 19 + test/fixtures/custom_values.yml | 103 + test/fixtures/diffs/issue-12641-ja.diff | 25 + test/fixtures/diffs/issue-12641-ru.diff | 19 + test/fixtures/diffs/issue-13644-1.diff | 7 + test/fixtures/diffs/issue-13644-2.diff | 7 + test/fixtures/diffs/partials.diff | 46 + test/fixtures/diffs/subversion.diff | 79 + test/fixtures/documents.yml | 14 + test/fixtures/enabled_modules.yml | 105 + test/fixtures/encoding/iso-8859-1.txt | 1 + test/fixtures/enumerations.yml | 103 + .../files/2006/07/060719210727_archive.zip | Bin 0 -> 157 bytes .../07/060719210727_changeset_iso8859-1.diff | 13 + .../2006/07/060719210727_changeset_utf8.diff | 13 + .../files/2006/07/060719210727_source.rb | 10 + .../files/2010/11/101123161450_testfile_1.png | Bin 0 -> 2654 bytes .../files/2010/12/101223161450_testfile_2.png | Bin 0 -> 3582 bytes test/fixtures/files/hg-export.diff | 13 + test/fixtures/files/iso8859-1.txt | 13 + test/fixtures/files/japanese-utf-8.txt | 1 + test/fixtures/files/testfile.txt | 2 + test/fixtures/groups_users.yml | 7 + test/fixtures/issue_categories.yml | 21 + test/fixtures/issue_relations.yml | 12 + test/fixtures/issue_statuses.yml | 37 + test/fixtures/issues.yml | 268 ++ test/fixtures/journal_details.yml | 43 + test/fixtures/ldap/slapd.conf | 19 + test/fixtures/ldap/test-ldap.ldif | 82 + .../apple_mail_with_attachment.eml | 240 + .../fullname_of_sender_as_utf8_encoded.eml | 5 + .../gmail_with_attachment_iso-8859-1.eml | 26 + .../mail_handler/gmail_with_attachment_ja.eml | 20 + ...pdate_with_multiple_quoted_reply_above.eml | 48 + .../issue_update_with_quoted_reply_above.eml | 48 + .../japanese_keywords_iso_2022_jp.eml | 60 + test/fixtures/mail_handler/message_reply.eml | 15 + .../mail_handler/message_reply_by_subject.eml | 13 + .../mail_handler/no_subject_header.eml | 10 + .../mail_handler/subject_as_iso-8859-1.eml | 11 + .../mail_handler/subject_japanese_1.eml | 7 + .../mail_handler/subject_japanese_2.eml | 7 + ...thunderbird_with_attachment_iso-8859-1.eml | 34 + .../thunderbird_with_attachment_ja.eml | 26 + .../mail_handler/ticket_by_empty_user.eml | 17 + .../mail_handler/ticket_by_unknown_user.eml | 18 + .../ticket_from_emission_address.eml | 19 + .../mail_handler/ticket_html_only.eml | 22 + .../mail_handler/ticket_on_given_project.eml | 60 + test/fixtures/mail_handler/ticket_reply.eml | 74 + .../mail_handler/ticket_reply_with_status.eml | 80 + .../mail_handler/ticket_with_attachment.eml | 248 ++ .../mail_handler/ticket_with_attributes.eml | 43 + test/fixtures/mail_handler/ticket_with_cc.eml | 40 + .../ticket_with_custom_fields.eml | 42 + .../ticket_with_invalid_attributes.eml | 47 + .../ticket_with_localized_attributes.eml | 43 + .../mail_handler/ticket_with_long_subject.eml | 57 + ...spaces_between_attribute_and_separator.eml | 43 + .../ticket_without_from_header.eml | 40 + test/fixtures/members.yml | 24 - .../foo_plugin/_foo_plugin_settings.html.erb | 1 + test/fixtures/projects.yml | 64 - test/fixtures/queries.yml | 165 + test/fixtures/repositories.yml | 19 + .../repositories/bazaar_repository.tar.gz | Bin 0 -> 25056 bytes .../repositories/cvs_repository.tar.gz | Bin 0 -> 12206 bytes .../repositories/darcs_repository.tar.gz | Bin 0 -> 8075 bytes .../repositories/filesystem_repository.tar.gz | Bin 0 -> 479 bytes .../repositories/git_repository.tar.gz | Bin 0 -> 21021 bytes .../repositories/mercurial_repository.hg | Bin 0 -> 11046 bytes .../subversion_repository.dump.gz | Bin 0 -> 12819 bytes test/fixtures/time_entries.yml | 72 + test/fixtures/users.yml | 373 +- test/fixtures/versions.yml | 71 + test/fixtures/wiki_content_versions.yml | 116 + test/fixtures/wiki_contents.yml | 136 + test/fixtures/wiki_pages.yml | 85 + test/fixtures/wikis.yml | 11 + test/fixtures/workflows.yml | 1884 ++++++++ .../account_controller_openid_test.rb | 165 + test/functional/account_controller_test.rb | 277 ++ test/functional/activities_controller_test.rb | 150 + test/functional/admin_controller_test.rb | 167 + .../functional/attachments_controller_test.rb | 385 ++ .../auth_sources_controller_test.rb | 168 + .../auto_completes_controller_test.rb | 90 + test/functional/boards_controller_test.rb | 217 + test/functional/calendars_controller_test.rb | 84 + test/functional/comments_controller_test.rb | 64 + .../context_menus_controller_test.rb | 252 ++ .../custom_fields_controller_test.rb | 176 + test/functional/documents_controller_test.rb | 186 + .../enumerations_controller_test.rb | 136 + test/functional/files_controller_test.rb | 109 + test/functional/gantts_controller_test.rb | 122 + test/functional/groups_controller_test.rb | 202 + .../issue_categories_controller_test.rb | 145 + .../issue_relations_controller_test.rb | 147 + .../issue_statuses_controller_test.rb | 123 + test/functional/issues_controller_test.rb | 3898 +++++++++++++++++ .../issues_controller_transaction_test.rb | 263 ++ test/functional/journals_controller_test.rb | 147 + .../mail_handler_controller_test.rb | 74 + test/functional/members_controller_test.rb | 111 + test/functional/messages_controller_test.rb | 217 + test/functional/my_controller_test.rb | 248 ++ test/functional/news_controller_test.rb | 165 + test/functional/previews_controller_test.rb | 81 + .../project_enumerations_controller_test.rb | 217 + test/functional/projects_controller_test.rb | 592 +++ test/functional/queries_controller_test.rb | 290 ++ test/functional/reports_controller_test.rb | 67 + .../repositories_bazaar_controller_test.rb | 198 + .../repositories_controller_test.rb | 264 ++ .../repositories_cvs_controller_test.rb | 274 ++ .../repositories_darcs_controller_test.rb | 165 + ...repositories_filesystem_controller_test.rb | 164 + .../repositories_git_controller_test.rb | 638 +++ .../repositories_mercurial_controller_test.rb | 525 +++ ...repositories_subversion_controller_test.rb | 425 ++ test/functional/roles_controller_test.rb | 216 + test/functional/search_controller_test.rb | 265 ++ test/functional/sessions_test.rb | 117 + test/functional/settings_controller_test.rb | 131 + test/functional/sys_controller_test.rb | 125 + .../time_entry_reports_controller_test.rb | 372 ++ test/functional/timelog_controller_test.rb | 604 +++ test/functional/trackers_controller_test.rb | 211 + test/functional/users_controller_test.rb | 469 +- test/functional/versions_controller_test.rb | 232 + test/functional/watchers_controller_test.rb | 195 + test/functional/welcome_controller_test.rb | 165 + test/functional/wiki_controller_test.rb | 933 ++++ test/functional/wikis_controller_test.rb | 83 + test/functional/workflows_controller_test.rb | 328 ++ test/integration/account_test.rb | 212 + test/integration/admin_test.rb | 61 + test/integration/api_test/attachments_test.rb | 149 + .../api_test/authentication_test.rb | 73 + .../api_test/disabled_rest_api_test.rb | 78 + .../integration/api_test/enumerations_test.rb | 44 + test/integration/api_test/groups_test.rb | 212 + .../api_test/http_basic_login_test.rb | 54 + .../http_basic_login_with_api_token_test.rb | 50 + .../api_test/issue_categories_test.rb | 126 + .../api_test/issue_relations_test.rb | 106 + .../api_test/issue_statuses_test.rb | 51 + test/integration/api_test/issues_test.rb | 846 ++++ test/integration/api_test/jsonp_test.rb | 72 + test/integration/api_test/memberships_test.rb | 200 + test/integration/api_test/news_test.rb | 97 + test/integration/api_test/projects_test.rb | 297 ++ test/integration/api_test/queries_test.rb | 58 + test/integration/api_test/roles_test.rb | 90 + .../integration/api_test/time_entries_test.rb | 164 + .../api_test/token_authentication_test.rb | 49 + test/integration/api_test/trackers_test.rb | 51 + test/integration/api_test/users_test.rb | 371 ++ test/integration/api_test/versions_test.rb | 158 + test/integration/api_test/wiki_pages_test.rb | 193 + test/integration/application_test.rb | 67 + test/integration/attachments_test.rb | 132 + test/integration/issues_test.rb | 220 + test/integration/layout_test.rb | 119 + test/integration/lib/redmine/hook_test.rb | 89 + .../lib/redmine/menu_manager_test.rb | 76 + test/integration/lib/redmine/themes_test.rb | 74 + test/integration/projects_test.rb | 51 + test/integration/repositories_git_test.rb | 50 + test/integration/routing/account_test.rb | 51 + test/integration/routing/activities_test.rb | 40 + test/integration/routing/admin_test.rb | 47 + test/integration/routing/attachments_test.rb | 69 + test/integration/routing/auth_sources_test.rb | 59 + .../routing/auto_completes_test.rb | 27 + test/integration/routing/boards_test.rb | 60 + test/integration/routing/calendars_test.rb | 32 + test/integration/routing/comments_test.rb | 32 + .../integration/routing/context_menus_test.rb | 38 + .../integration/routing/custom_fields_test.rb | 47 + test/integration/routing/documents_test.rb | 58 + test/integration/routing/enumerations_test.rb | 51 + test/integration/routing/files_test.rb | 35 + test/integration/routing/gantts_test.rb | 41 + test/integration/routing/groups_test.rb | 106 + .../routing/issue_categories_test.rb | 107 + .../routing/issue_relations_test.rb | 81 + .../routing/issue_statuses_test.rb | 80 + test/integration/routing/issues_test.rb | 134 + test/integration/routing/journals_test.rb | 41 + test/integration/routing/mail_handler_test.rb | 27 + test/integration/routing/members_test.rb | 63 + test/integration/routing/messages_test.rb | 66 + test/integration/routing/my_test.rb | 73 + test/integration/routing/news_test.rb | 92 + test/integration/routing/previews_test.rb | 37 + .../routing/project_enumerations_test.rb | 33 + test/integration/routing/projects_test.rb | 99 + test/integration/routing/queries_test.rb | 62 + test/integration/routing/reports_test.rb | 32 + test/integration/routing/repositories_test.rb | 431 ++ test/integration/routing/roles_test.rb | 61 + test/integration/routing/search_test.rb | 31 + test/integration/routing/settings_test.rb | 40 + test/integration/routing/sys_test.rb | 35 + test/integration/routing/timelog_test.rb | 240 + test/integration/routing/trackers_test.rb | 79 + test/integration/routing/users_test.rb | 98 + test/integration/routing/versions_test.rb | 119 + test/integration/routing/watchers_test.rb | 61 + test/integration/routing/welcome_test.rb | 31 + test/integration/routing/wiki_test.rb | 186 + test/integration/routing/wikis_test.rb | 33 + test/integration/routing/workflows_test.rb | 45 + test/integration/users_test.rb | 29 + test/mocks/open_id_authentication_mock.rb | 46 + test/object_helpers.rb | 162 + test/test_helper.rb | 481 +- test/ui/base.rb | 63 + test/ui/issues_test.rb | 217 + test/unit/activity_test.rb | 136 +- test/unit/attachment_test.rb | 303 +- test/unit/auth_source_ldap_test.rb | 141 + test/unit/board_test.rb | 116 + test/unit/changeset_test.rb | 476 ++ test/unit/comment_test.rb | 57 + test/unit/custom_field_test.rb | 244 ++ test/unit/custom_field_user_format_test.rb | 77 + test/unit/custom_field_version_format_test.rb | 76 + test/unit/custom_value_test.rb | 39 + test/unit/default_data_test.rb | 49 + test/unit/document_category_test.rb | 47 + test/unit/document_test.rb | 62 + test/unit/enabled_module_test.rb | 43 + test/unit/enumeration_test.rb | 130 + test/unit/group_test.rb | 136 + test/unit/helpers/activities_helper_test.rb | 101 + test/unit/helpers/application_helper_test.rb | 1223 ++++++ .../unit/helpers/custom_fields_helper_test.rb | 47 + test/unit/helpers/issues_helper_test.rb | 194 + test/unit/helpers/projects_helper_test.rb | 76 + test/unit/helpers/queries_helper_test.rb | 69 + test/unit/helpers/search_helper_test.rb | 48 + test/unit/helpers/sort_helper_test.rb | 86 + test/unit/helpers/timelog_helper_test.rb | 57 + test/unit/helpers/watchers_helper_test.rb | 69 + test/unit/initializers/patches_test.rb | 40 + test/unit/issue_category_test.rb | 54 + test/unit/issue_nested_set_test.rb | 358 ++ test/unit/issue_priority_test.rb | 106 + test/unit/issue_relation_test.rb | 170 + test/unit/issue_status_test.rb | 124 + test/unit/issue_test.rb | 2234 ++++++++++ test/unit/issue_transaction_test.rb | 54 + test/unit/journal_observer_test.rb | 139 + test/unit/journal_test.rb | 178 + test/unit/lib/redmine/access_control_test.rb | 59 + test/unit/lib/redmine/ciphering_test.rb | 106 + test/unit/lib/redmine/codeset_util_test.rb | 115 + test/unit/lib/redmine/configuration_test.rb | 61 + test/unit/lib/redmine/export/pdf_test.rb | 128 + .../unit/lib/redmine/helpers/calendar_test.rb | 63 + test/unit/lib/redmine/helpers/gantt_test.rb | 749 ++++ test/unit/lib/redmine/hook_test.rb | 178 + test/unit/lib/redmine/i18n_test.rb | 269 ++ test/unit/lib/redmine/info_test.rb | 27 + .../lib/redmine/menu_manager/mapper_test.rb | 182 + .../redmine/menu_manager/menu_helper_test.rb | 253 ++ .../redmine/menu_manager/menu_item_test.rb | 122 + test/unit/lib/redmine/menu_manager_test.rb | 34 + test/unit/lib/redmine/mime_type_test.rb | 61 + test/unit/lib/redmine/notifiable_test.rb | 31 + .../lib/redmine/pagination_helper_test.rb | 34 + test/unit/lib/redmine/pagination_test.rb | 94 + test/unit/lib/redmine/plugin_test.rb | 176 + test/unit/lib/redmine/safe_attributes_test.rb | 102 + .../scm/adapters/bazaar_adapter_test.rb | 225 + .../redmine/scm/adapters/cvs_adapter_test.rb | 99 + .../scm/adapters/darcs_adapter_test.rb | 69 + .../scm/adapters/filesystem_adapter_test.rb | 68 + .../redmine/scm/adapters/git_adapter_test.rb | 598 +++ .../scm/adapters/mercurial_adapter_test.rb | 434 ++ .../scm/adapters/subversion_adapter_test.rb | 71 + test/unit/lib/redmine/themes_test.rb | 59 + test/unit/lib/redmine/unified_diff_test.rb | 316 ++ .../lib/redmine/utils/date_calculation.rb | 76 + .../lib/redmine/views/builders/json_test.rb | 94 + .../lib/redmine/views/builders/xml_test.rb | 67 + .../redmine/wiki_formatting/macros_test.rb | 362 ++ .../wiki_formatting/textile_formatter_test.rb | 456 ++ test/unit/lib/redmine/wiki_formatting_test.rb | 60 + test/unit/lib/redmine_test.rb | 86 + test/unit/mail_handler_test.rb | 822 ++++ test/unit/mailer_test.rb | 625 +++ test/unit/member_test.rb | 124 + test/unit/message_test.rb | 184 + test/unit/news_test.rb | 90 + test/unit/principal_test.rb | 114 + test/unit/project_copy_test.rb | 337 ++ test/unit/project_members_inheritance_test.rb | 260 ++ test/unit/project_nested_set_test.rb | 183 + test/unit/project_test.rb | 937 ++++ test/unit/query_test.rb | 1248 ++++++ test/unit/repository_bazaar_test.rb | 306 ++ test/unit/repository_cvs_test.rb | 241 + test/unit/repository_darcs_test.rb | 129 + test/unit/repository_filesystem_test.rb | 89 + test/unit/repository_git_test.rb | 601 +++ test/unit/repository_mercurial_test.rb | 377 ++ test/unit/repository_subversion_test.rb | 231 + test/unit/repository_test.rb | 373 ++ test/unit/role_test.rb | 145 + test/unit/search_test.rb | 144 + test/unit/setting_test.rb | 90 + test/unit/time_entry_activity_test.rb | 116 + test/unit/time_entry_test.rb | 135 + test/unit/token_test.rb | 113 + test/unit/tracker_test.rb | 118 + test/unit/user_preference_test.rb | 72 + test/unit/user_test.rb | 1085 ++++- test/unit/version_test.rb | 254 ++ test/unit/watcher_test.rb | 165 + test/unit/wiki_content_test.rb | 163 + test/unit/wiki_content_version_test.rb | 68 + test/unit/wiki_page_test.rb | 163 + test/unit/wiki_redirect_test.rb | 74 + test/unit/wiki_test.rb | 105 + test/unit/workflow_test.rb | 65 + 349 files changed, 55863 insertions(+), 406 deletions(-) create mode 100644 test/extra/redmine_pm/repository_git_test.rb create mode 100644 test/extra/redmine_pm/repository_subversion_test.rb create mode 100644 test/extra/redmine_pm/test_case.rb create mode 100644 test/fixtures/auth_sources.yml create mode 100644 test/fixtures/changes.yml create mode 100644 test/fixtures/changesets.yml create mode 100644 test/fixtures/comments.yml create mode 100644 test/fixtures/configuration/default.yml create mode 100644 test/fixtures/configuration/empty.yml create mode 100644 test/fixtures/configuration/no_default.yml create mode 100644 test/fixtures/configuration/overrides.yml create mode 100644 test/fixtures/custom_fields.yml create mode 100644 test/fixtures/custom_fields_projects.yml create mode 100644 test/fixtures/custom_fields_trackers.yml create mode 100644 test/fixtures/custom_values.yml create mode 100644 test/fixtures/diffs/issue-12641-ja.diff create mode 100644 test/fixtures/diffs/issue-12641-ru.diff create mode 100644 test/fixtures/diffs/issue-13644-1.diff create mode 100644 test/fixtures/diffs/issue-13644-2.diff create mode 100644 test/fixtures/diffs/partials.diff create mode 100644 test/fixtures/diffs/subversion.diff create mode 100644 test/fixtures/documents.yml create mode 100644 test/fixtures/enabled_modules.yml create mode 100644 test/fixtures/encoding/iso-8859-1.txt create mode 100644 test/fixtures/enumerations.yml create mode 100644 test/fixtures/files/2006/07/060719210727_archive.zip create mode 100644 test/fixtures/files/2006/07/060719210727_changeset_iso8859-1.diff create mode 100644 test/fixtures/files/2006/07/060719210727_changeset_utf8.diff create mode 100644 test/fixtures/files/2006/07/060719210727_source.rb create mode 100644 test/fixtures/files/2010/11/101123161450_testfile_1.png create mode 100644 test/fixtures/files/2010/12/101223161450_testfile_2.png create mode 100644 test/fixtures/files/hg-export.diff create mode 100644 test/fixtures/files/iso8859-1.txt create mode 100644 test/fixtures/files/japanese-utf-8.txt create mode 100644 test/fixtures/files/testfile.txt create mode 100644 test/fixtures/groups_users.yml create mode 100644 test/fixtures/issue_categories.yml create mode 100644 test/fixtures/issue_relations.yml create mode 100644 test/fixtures/issue_statuses.yml create mode 100644 test/fixtures/issues.yml create mode 100644 test/fixtures/journal_details.yml create mode 100644 test/fixtures/ldap/slapd.conf create mode 100644 test/fixtures/ldap/test-ldap.ldif create mode 100644 test/fixtures/mail_handler/apple_mail_with_attachment.eml create mode 100644 test/fixtures/mail_handler/fullname_of_sender_as_utf8_encoded.eml create mode 100644 test/fixtures/mail_handler/gmail_with_attachment_iso-8859-1.eml create mode 100644 test/fixtures/mail_handler/gmail_with_attachment_ja.eml create mode 100644 test/fixtures/mail_handler/issue_update_with_multiple_quoted_reply_above.eml create mode 100644 test/fixtures/mail_handler/issue_update_with_quoted_reply_above.eml create mode 100644 test/fixtures/mail_handler/japanese_keywords_iso_2022_jp.eml create mode 100644 test/fixtures/mail_handler/message_reply.eml create mode 100644 test/fixtures/mail_handler/message_reply_by_subject.eml create mode 100644 test/fixtures/mail_handler/no_subject_header.eml create mode 100644 test/fixtures/mail_handler/subject_as_iso-8859-1.eml create mode 100644 test/fixtures/mail_handler/subject_japanese_1.eml create mode 100644 test/fixtures/mail_handler/subject_japanese_2.eml create mode 100644 test/fixtures/mail_handler/thunderbird_with_attachment_iso-8859-1.eml create mode 100644 test/fixtures/mail_handler/thunderbird_with_attachment_ja.eml create mode 100644 test/fixtures/mail_handler/ticket_by_empty_user.eml create mode 100644 test/fixtures/mail_handler/ticket_by_unknown_user.eml create mode 100644 test/fixtures/mail_handler/ticket_from_emission_address.eml create mode 100644 test/fixtures/mail_handler/ticket_html_only.eml create mode 100644 test/fixtures/mail_handler/ticket_on_given_project.eml create mode 100644 test/fixtures/mail_handler/ticket_reply.eml create mode 100644 test/fixtures/mail_handler/ticket_reply_with_status.eml create mode 100644 test/fixtures/mail_handler/ticket_with_attachment.eml create mode 100644 test/fixtures/mail_handler/ticket_with_attributes.eml create mode 100644 test/fixtures/mail_handler/ticket_with_cc.eml create mode 100644 test/fixtures/mail_handler/ticket_with_custom_fields.eml create mode 100644 test/fixtures/mail_handler/ticket_with_invalid_attributes.eml create mode 100644 test/fixtures/mail_handler/ticket_with_localized_attributes.eml create mode 100644 test/fixtures/mail_handler/ticket_with_long_subject.eml create mode 100644 test/fixtures/mail_handler/ticket_with_spaces_between_attribute_and_separator.eml create mode 100644 test/fixtures/mail_handler/ticket_without_from_header.eml create mode 100644 test/fixtures/plugins/foo_plugin/_foo_plugin_settings.html.erb create mode 100644 test/fixtures/queries.yml create mode 100644 test/fixtures/repositories.yml create mode 100644 test/fixtures/repositories/bazaar_repository.tar.gz create mode 100644 test/fixtures/repositories/cvs_repository.tar.gz create mode 100644 test/fixtures/repositories/darcs_repository.tar.gz create mode 100644 test/fixtures/repositories/filesystem_repository.tar.gz create mode 100644 test/fixtures/repositories/git_repository.tar.gz create mode 100644 test/fixtures/repositories/mercurial_repository.hg create mode 100644 test/fixtures/repositories/subversion_repository.dump.gz create mode 100644 test/fixtures/time_entries.yml create mode 100644 test/fixtures/versions.yml create mode 100644 test/fixtures/wiki_content_versions.yml create mode 100644 test/fixtures/wiki_contents.yml create mode 100644 test/fixtures/wiki_pages.yml create mode 100644 test/fixtures/wikis.yml create mode 100644 test/fixtures/workflows.yml create mode 100644 test/functional/account_controller_openid_test.rb create mode 100644 test/functional/account_controller_test.rb create mode 100644 test/functional/activities_controller_test.rb create mode 100644 test/functional/admin_controller_test.rb create mode 100644 test/functional/attachments_controller_test.rb create mode 100644 test/functional/auth_sources_controller_test.rb create mode 100644 test/functional/auto_completes_controller_test.rb create mode 100644 test/functional/boards_controller_test.rb create mode 100644 test/functional/calendars_controller_test.rb create mode 100644 test/functional/comments_controller_test.rb create mode 100644 test/functional/context_menus_controller_test.rb create mode 100644 test/functional/custom_fields_controller_test.rb create mode 100644 test/functional/documents_controller_test.rb create mode 100644 test/functional/enumerations_controller_test.rb create mode 100644 test/functional/files_controller_test.rb create mode 100644 test/functional/gantts_controller_test.rb create mode 100644 test/functional/groups_controller_test.rb create mode 100644 test/functional/issue_categories_controller_test.rb create mode 100644 test/functional/issue_relations_controller_test.rb create mode 100644 test/functional/issue_statuses_controller_test.rb create mode 100644 test/functional/issues_controller_test.rb create mode 100644 test/functional/issues_controller_transaction_test.rb create mode 100644 test/functional/journals_controller_test.rb create mode 100644 test/functional/mail_handler_controller_test.rb create mode 100644 test/functional/members_controller_test.rb create mode 100644 test/functional/messages_controller_test.rb create mode 100644 test/functional/my_controller_test.rb create mode 100644 test/functional/news_controller_test.rb create mode 100644 test/functional/previews_controller_test.rb create mode 100644 test/functional/project_enumerations_controller_test.rb create mode 100644 test/functional/projects_controller_test.rb create mode 100644 test/functional/queries_controller_test.rb create mode 100644 test/functional/reports_controller_test.rb create mode 100644 test/functional/repositories_bazaar_controller_test.rb create mode 100644 test/functional/repositories_controller_test.rb create mode 100644 test/functional/repositories_cvs_controller_test.rb create mode 100644 test/functional/repositories_darcs_controller_test.rb create mode 100644 test/functional/repositories_filesystem_controller_test.rb create mode 100644 test/functional/repositories_git_controller_test.rb create mode 100644 test/functional/repositories_mercurial_controller_test.rb create mode 100644 test/functional/repositories_subversion_controller_test.rb create mode 100644 test/functional/roles_controller_test.rb create mode 100644 test/functional/search_controller_test.rb create mode 100644 test/functional/sessions_test.rb create mode 100644 test/functional/settings_controller_test.rb create mode 100644 test/functional/sys_controller_test.rb create mode 100644 test/functional/time_entry_reports_controller_test.rb create mode 100644 test/functional/timelog_controller_test.rb create mode 100644 test/functional/trackers_controller_test.rb create mode 100644 test/functional/versions_controller_test.rb create mode 100644 test/functional/watchers_controller_test.rb create mode 100644 test/functional/welcome_controller_test.rb create mode 100644 test/functional/wiki_controller_test.rb create mode 100644 test/functional/wikis_controller_test.rb create mode 100644 test/functional/workflows_controller_test.rb create mode 100644 test/integration/account_test.rb create mode 100644 test/integration/admin_test.rb create mode 100644 test/integration/api_test/attachments_test.rb create mode 100644 test/integration/api_test/authentication_test.rb create mode 100644 test/integration/api_test/disabled_rest_api_test.rb create mode 100644 test/integration/api_test/enumerations_test.rb create mode 100644 test/integration/api_test/groups_test.rb create mode 100644 test/integration/api_test/http_basic_login_test.rb create mode 100644 test/integration/api_test/http_basic_login_with_api_token_test.rb create mode 100644 test/integration/api_test/issue_categories_test.rb create mode 100644 test/integration/api_test/issue_relations_test.rb create mode 100644 test/integration/api_test/issue_statuses_test.rb create mode 100644 test/integration/api_test/issues_test.rb create mode 100644 test/integration/api_test/jsonp_test.rb create mode 100644 test/integration/api_test/memberships_test.rb create mode 100644 test/integration/api_test/news_test.rb create mode 100644 test/integration/api_test/projects_test.rb create mode 100644 test/integration/api_test/queries_test.rb create mode 100644 test/integration/api_test/roles_test.rb create mode 100644 test/integration/api_test/time_entries_test.rb create mode 100644 test/integration/api_test/token_authentication_test.rb create mode 100644 test/integration/api_test/trackers_test.rb create mode 100644 test/integration/api_test/users_test.rb create mode 100644 test/integration/api_test/versions_test.rb create mode 100644 test/integration/api_test/wiki_pages_test.rb create mode 100644 test/integration/application_test.rb create mode 100644 test/integration/attachments_test.rb create mode 100644 test/integration/issues_test.rb create mode 100644 test/integration/layout_test.rb create mode 100644 test/integration/lib/redmine/hook_test.rb create mode 100644 test/integration/lib/redmine/menu_manager_test.rb create mode 100644 test/integration/lib/redmine/themes_test.rb create mode 100644 test/integration/projects_test.rb create mode 100644 test/integration/repositories_git_test.rb create mode 100644 test/integration/routing/account_test.rb create mode 100644 test/integration/routing/activities_test.rb create mode 100644 test/integration/routing/admin_test.rb create mode 100644 test/integration/routing/attachments_test.rb create mode 100644 test/integration/routing/auth_sources_test.rb create mode 100644 test/integration/routing/auto_completes_test.rb create mode 100644 test/integration/routing/boards_test.rb create mode 100644 test/integration/routing/calendars_test.rb create mode 100644 test/integration/routing/comments_test.rb create mode 100644 test/integration/routing/context_menus_test.rb create mode 100644 test/integration/routing/custom_fields_test.rb create mode 100644 test/integration/routing/documents_test.rb create mode 100644 test/integration/routing/enumerations_test.rb create mode 100644 test/integration/routing/files_test.rb create mode 100644 test/integration/routing/gantts_test.rb create mode 100644 test/integration/routing/groups_test.rb create mode 100644 test/integration/routing/issue_categories_test.rb create mode 100644 test/integration/routing/issue_relations_test.rb create mode 100644 test/integration/routing/issue_statuses_test.rb create mode 100644 test/integration/routing/issues_test.rb create mode 100644 test/integration/routing/journals_test.rb create mode 100644 test/integration/routing/mail_handler_test.rb create mode 100644 test/integration/routing/members_test.rb create mode 100644 test/integration/routing/messages_test.rb create mode 100644 test/integration/routing/my_test.rb create mode 100644 test/integration/routing/news_test.rb create mode 100644 test/integration/routing/previews_test.rb create mode 100644 test/integration/routing/project_enumerations_test.rb create mode 100644 test/integration/routing/projects_test.rb create mode 100644 test/integration/routing/queries_test.rb create mode 100644 test/integration/routing/reports_test.rb create mode 100644 test/integration/routing/repositories_test.rb create mode 100644 test/integration/routing/roles_test.rb create mode 100644 test/integration/routing/search_test.rb create mode 100644 test/integration/routing/settings_test.rb create mode 100644 test/integration/routing/sys_test.rb create mode 100644 test/integration/routing/timelog_test.rb create mode 100644 test/integration/routing/trackers_test.rb create mode 100644 test/integration/routing/users_test.rb create mode 100644 test/integration/routing/versions_test.rb create mode 100644 test/integration/routing/watchers_test.rb create mode 100644 test/integration/routing/welcome_test.rb create mode 100644 test/integration/routing/wiki_test.rb create mode 100644 test/integration/routing/wikis_test.rb create mode 100644 test/integration/routing/workflows_test.rb create mode 100644 test/integration/users_test.rb create mode 100644 test/mocks/open_id_authentication_mock.rb create mode 100644 test/object_helpers.rb create mode 100644 test/ui/base.rb create mode 100644 test/ui/issues_test.rb create mode 100644 test/unit/auth_source_ldap_test.rb create mode 100644 test/unit/board_test.rb create mode 100644 test/unit/changeset_test.rb create mode 100644 test/unit/comment_test.rb create mode 100644 test/unit/custom_field_test.rb create mode 100644 test/unit/custom_field_user_format_test.rb create mode 100644 test/unit/custom_field_version_format_test.rb create mode 100644 test/unit/custom_value_test.rb create mode 100644 test/unit/default_data_test.rb create mode 100644 test/unit/document_category_test.rb create mode 100644 test/unit/document_test.rb create mode 100644 test/unit/enabled_module_test.rb create mode 100644 test/unit/enumeration_test.rb create mode 100644 test/unit/group_test.rb create mode 100644 test/unit/helpers/activities_helper_test.rb create mode 100644 test/unit/helpers/application_helper_test.rb create mode 100644 test/unit/helpers/custom_fields_helper_test.rb create mode 100644 test/unit/helpers/issues_helper_test.rb create mode 100644 test/unit/helpers/projects_helper_test.rb create mode 100644 test/unit/helpers/queries_helper_test.rb create mode 100644 test/unit/helpers/search_helper_test.rb create mode 100644 test/unit/helpers/sort_helper_test.rb create mode 100644 test/unit/helpers/timelog_helper_test.rb create mode 100644 test/unit/helpers/watchers_helper_test.rb create mode 100644 test/unit/initializers/patches_test.rb create mode 100644 test/unit/issue_category_test.rb create mode 100644 test/unit/issue_nested_set_test.rb create mode 100644 test/unit/issue_priority_test.rb create mode 100644 test/unit/issue_relation_test.rb create mode 100644 test/unit/issue_status_test.rb create mode 100644 test/unit/issue_test.rb create mode 100644 test/unit/issue_transaction_test.rb create mode 100644 test/unit/journal_observer_test.rb create mode 100644 test/unit/journal_test.rb create mode 100644 test/unit/lib/redmine/access_control_test.rb create mode 100644 test/unit/lib/redmine/ciphering_test.rb create mode 100644 test/unit/lib/redmine/codeset_util_test.rb create mode 100644 test/unit/lib/redmine/configuration_test.rb create mode 100644 test/unit/lib/redmine/export/pdf_test.rb create mode 100644 test/unit/lib/redmine/helpers/calendar_test.rb create mode 100644 test/unit/lib/redmine/helpers/gantt_test.rb create mode 100644 test/unit/lib/redmine/hook_test.rb create mode 100644 test/unit/lib/redmine/i18n_test.rb create mode 100644 test/unit/lib/redmine/info_test.rb create mode 100644 test/unit/lib/redmine/menu_manager/mapper_test.rb create mode 100644 test/unit/lib/redmine/menu_manager/menu_helper_test.rb create mode 100644 test/unit/lib/redmine/menu_manager/menu_item_test.rb create mode 100644 test/unit/lib/redmine/menu_manager_test.rb create mode 100644 test/unit/lib/redmine/mime_type_test.rb create mode 100644 test/unit/lib/redmine/notifiable_test.rb create mode 100644 test/unit/lib/redmine/pagination_helper_test.rb create mode 100644 test/unit/lib/redmine/pagination_test.rb create mode 100644 test/unit/lib/redmine/plugin_test.rb create mode 100644 test/unit/lib/redmine/safe_attributes_test.rb create mode 100644 test/unit/lib/redmine/scm/adapters/bazaar_adapter_test.rb create mode 100644 test/unit/lib/redmine/scm/adapters/cvs_adapter_test.rb create mode 100644 test/unit/lib/redmine/scm/adapters/darcs_adapter_test.rb create mode 100644 test/unit/lib/redmine/scm/adapters/filesystem_adapter_test.rb create mode 100644 test/unit/lib/redmine/scm/adapters/git_adapter_test.rb create mode 100644 test/unit/lib/redmine/scm/adapters/mercurial_adapter_test.rb create mode 100644 test/unit/lib/redmine/scm/adapters/subversion_adapter_test.rb create mode 100644 test/unit/lib/redmine/themes_test.rb create mode 100644 test/unit/lib/redmine/unified_diff_test.rb create mode 100644 test/unit/lib/redmine/utils/date_calculation.rb create mode 100644 test/unit/lib/redmine/views/builders/json_test.rb create mode 100644 test/unit/lib/redmine/views/builders/xml_test.rb create mode 100644 test/unit/lib/redmine/wiki_formatting/macros_test.rb create mode 100644 test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb create mode 100644 test/unit/lib/redmine/wiki_formatting_test.rb create mode 100644 test/unit/lib/redmine_test.rb create mode 100644 test/unit/mail_handler_test.rb create mode 100644 test/unit/mailer_test.rb create mode 100644 test/unit/member_test.rb create mode 100644 test/unit/message_test.rb create mode 100644 test/unit/news_test.rb create mode 100644 test/unit/principal_test.rb create mode 100644 test/unit/project_copy_test.rb create mode 100644 test/unit/project_members_inheritance_test.rb create mode 100644 test/unit/project_nested_set_test.rb create mode 100644 test/unit/project_test.rb create mode 100644 test/unit/query_test.rb create mode 100644 test/unit/repository_bazaar_test.rb create mode 100644 test/unit/repository_cvs_test.rb create mode 100644 test/unit/repository_darcs_test.rb create mode 100644 test/unit/repository_filesystem_test.rb create mode 100644 test/unit/repository_git_test.rb create mode 100644 test/unit/repository_mercurial_test.rb create mode 100644 test/unit/repository_subversion_test.rb create mode 100644 test/unit/repository_test.rb create mode 100644 test/unit/role_test.rb create mode 100644 test/unit/search_test.rb create mode 100644 test/unit/setting_test.rb create mode 100644 test/unit/time_entry_activity_test.rb create mode 100644 test/unit/time_entry_test.rb create mode 100644 test/unit/token_test.rb create mode 100644 test/unit/tracker_test.rb create mode 100644 test/unit/user_preference_test.rb create mode 100644 test/unit/version_test.rb create mode 100644 test/unit/watcher_test.rb create mode 100644 test/unit/wiki_content_test.rb create mode 100644 test/unit/wiki_content_version_test.rb create mode 100644 test/unit/wiki_page_test.rb create mode 100644 test/unit/wiki_redirect_test.rb create mode 100644 test/unit/wiki_test.rb create mode 100644 test/unit/workflow_test.rb diff --git a/Gemfile b/Gemfile index 9dae2827..400cdab3 100644 --- a/Gemfile +++ b/Gemfile @@ -42,6 +42,24 @@ group :ldap do gem "net-ldap", "~> 0.3.1" end +group :test do + # shoulda的版本做了改动 + #gem "shoulda", "~> 3.3.2" + gem "shoulda", "> 3.3.2" + gem "mocha", "~> 0.13.3" + gem 'capybara', '~> 2.0.0' + gem 'nokogiri', '< 1.6.0' +end + +platforms :mri, :mingw do + group :rmagick do + # RMagick 2 supports ruby 1.9 + # RMagick 1 would be fine for ruby 1.8 but Bundler does not support + # different requirements for the same gem on different platforms + gem "rmagick", ">= 2.0.0" + end +end + # Optional gem for OpenID authentication group :openid do gem "ruby-openid", "~> 2.1.4", :require => "openid" diff --git a/Gemfile.lock b/Gemfile.lock index e6ee7da7..14f6aa04 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -52,6 +52,15 @@ GEM rails (>= 3, < 5) arel (3.0.2) builder (3.0.0) + capybara (2.0.3) + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + selenium-webdriver (~> 2.0) + xpath (~> 1.0.0) + childprocess (0.5.3) + ffi (~> 1.0, >= 1.0.11) coderay (1.0.9) coffee-rails (3.2.2) coffee-script (>= 2.2.0) @@ -64,6 +73,7 @@ GEM execjs (1.4.0) multi_json (~> 1.0) fastercsv (1.5.0) + ffi (1.9.3-x86-mingw32) hike (1.2.3) i18n (0.6.1) journey (1.0.4) @@ -74,10 +84,14 @@ GEM mail (2.5.4) mime-types (~> 1.16) treetop (~> 1.4.8) + metaclass (0.0.4) mime-types (1.23) + mocha (0.13.3) + metaclass (~> 0.0.1) multi_json (1.7.6) mysql2 (0.3.11-x86-mingw32) net-ldap (0.3.1) + nokogiri (1.5.11-x86-mingw32) polyglot (0.3.3) rack (1.4.5) rack-cache (1.2) @@ -104,15 +118,28 @@ GEM rake (>= 0.8.7) rdoc (~> 3.4) thor (>= 0.14.6, < 2.0) - rake (10.0.4) + rake (10.3.2) rdoc (3.12.2) json (~> 1.4) + rmagick (2.13.2) ruby-openid (2.1.8) + rubyzip (1.1.4) sass (3.2.7) sass-rails (3.2.6) railties (~> 3.2.0) sass (>= 3.1.10) tilt (~> 1.3) + selenium-webdriver (2.42.0) + childprocess (>= 0.5.0) + multi_json (~> 1.0) + rubyzip (~> 1.0) + websocket (~> 1.0.4) + shoulda (3.5.0) + shoulda-context (~> 1.0, >= 1.0.1) + shoulda-matchers (>= 1.4.1, < 3.0) + shoulda-context (1.2.1) + shoulda-matchers (2.6.1) + activesupport (>= 3.0.0) sprockets (2.2.2) hike (~> 1.2) multi_json (~> 1.0) @@ -127,6 +154,9 @@ GEM uglifier (1.0.3) execjs (>= 0.3.0) multi_json (>= 1.0.2) + websocket (1.0.7) + xpath (1.0.0) + nokogiri (~> 1.3) PLATFORMS x86-mingw32 @@ -137,17 +167,22 @@ DEPENDENCIES acts-as-taggable-on (= 2.4.1) better_errors! builder (= 3.0.0) + capybara (~> 2.0.0) coderay (~> 1.0.6) coffee-rails (~> 3.2.1) fastercsv (~> 1.5.0) i18n (~> 0.6.0) jquery-rails (~> 2.0.2) + mocha (~> 0.13.3) mysql2 (~> 0.3.11) net-ldap (~> 0.3.1) + nokogiri (< 1.6.0) rack-mini-profiler! rack-openid rails (= 3.2.13) + rmagick (>= 2.0.0) ruby-openid (~> 2.1.4) sass-rails (~> 3.2.3) seems_rateable! + shoulda (> 3.3.2) uglifier (>= 1.0.3) diff --git a/db/schema.rb b/db/schema.rb index 18cc2a71..63020ad6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -713,7 +713,7 @@ ActiveRecord::Schema.define(:version => 20140618020535) do end create_table "relative_memos", :force => true do |t| - t.integer "osp_id", :null => false + t.integer "osp_id" t.integer "parent_id" t.string "subject", :null => false t.text "content", :null => false @@ -961,13 +961,11 @@ ActiveRecord::Schema.define(:version => 20140618020535) do end create_table "user_scores", :force => true do |t| - t.integer "user_id", :null => false - t.integer "collaboration" - t.integer "influence" - t.integer "skill" - t.integer "active" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.integer "user_id" + t.integer "collaboration" + t.integer "influence" + t.integer "skill" + t.integer "activity" end create_table "user_statuses", :force => true do |t| diff --git a/doc/测试环境搭建.doc b/doc/测试环境搭建.doc index 7aa1372d10bd15492d6b411d3cb093b98a6d1fb6..106a5db4218e5fb3d14e14f56c25ee83528e5b41 100644 GIT binary patch delta 1612 zcma)+e{54#6vxkf@3njT(Y5P-l?7*EV=i-a9V{C#aN~z;(Kz`LWDoACqTd6yvX$;P`_jLPj%9#AGC9NlE-sBqG-DdF@Mx@`tDW z++XLOd(S!V-kZFtk|{M74z^NWd_=3!RN(JyHY=lHV3j>Lt;V&b+IkANq0FFXQc6 zIe)5mt2p1$J64QhtO~lt3KaLL&y^GTdACvL7ccIW#R~q#Xm64UBL7wA#+?$=pE3Iq z&OWnx+E|&3s?S z1o5NQ(dTAio&0`c--|&fET>-NPWO{Vl0lJpCB9XIhP6=fydF?j_>TJv)n#2MctvB) zS5Z-t9okp0BRcTg>%$W_hi(q-+Fz5knv3=-j+)yOCQ&VD22X)OFabUS*TL_gs*orE zf*=I?zzHx7s1!Ef5I7881>;~2%!4K`(L3N&GmkK)(#&*=sE{g_VS zHF3)>b^aKem zJ0XS8?g#e^OF|0g!*JYpT*jr?RQerXFZJ+C=9hfKTcgT&r1MLnwuI-GW)|QGABJs)+{kc7;-x^>0nkv!^m+ms$x@Pc_&h`y2n{Nh)-p9O{y;b#8 z!^Oc14G+l*29=?F3`{|%Y$puT$8=}ZI@bAua%Z5-D7-0;RLf(Rlfu<+h%w}^y9!TN zz=~~{&^V%%obL`R)yAzG8`~!T*e@2{GVQG`dei8vvfCg0FfC7t>?2M&`$;`zjc%;- zs*LsFwu9bAxl!^;<5-a-)B|vf2)BlOKwJ<#_eR3zgH9(I378U~t$8mJ@U-MRcQJL9 z^;M)(jYs2!$xx5|vbFJ%XhIcnCYo@`;qBt#fu!>Dp94o#kdGWls75PupxE^fHcd4d delta 1547 zcma)+U2IfU5P;|0ySr=)%PzEq6`H!VOKlMBwk6fJX=yA0l13~QwGWC|TT)_`L_;j~ zXE&Oll3;4jlROzQniveFo2Zcouo#FwKvYbO386%JXu_I60Gn;Fesg!PHjxLX{qC7L zGiT16xo2dU%_O!px zh2;eYz44keHoGCX{_q9umnUBq{S(x+us+}4K{Zy;DW;aBDv*Tmi}m82>)QOMpv2G(b1SQ zS^jCV`U)?a!*TtC;#~ofXw^aOuBY4Y9?GS;xUw3mp&pu{1=`?gcm@u@tMDeg4e!Ex zFb=2S8#n`J;X2%eS(pPu87_oj38bJATA5r?0jqePSvz`?CXgR^PO%+ zylQl389K&^jAv_zH^=Bys-;4Zrsyf;m!9mww%3i`w4&HZdvkk3_?@`O$HWb3QdxPd z_S{-f-PJoOTj2;k-Av1M$6S9ln`z%`Lb~&`;Um#uH*6|(!j0LMO(&hnhf~Hh*@mvV zjTb+^sY^RS4sCYV@$>ieT{r%`A6?U_-%qRKk?i5Fny8u1p6J~dt8a9hyC#i_1u95CRF!0v7^b14-A*={;I|MFg4lz l18GxYZw;h_%LweY2L>}HX7hui=3cvhPukp '1' do + assert_failure "ls", svn_url + end + end + + def test_authenticated_read_should_succeed_with_login_required + with_settings :login_required => '1' do + with_credentials "miscuser8", "foo" do + assert_success "ls", svn_url + end + end + end + + def test_read_on_archived_projects_should_fail + Project.find(1).update_attribute :status, Project::STATUS_ARCHIVED + assert_failure "ls", svn_url + end + + def test_read_on_archived_private_projects_should_fail + Project.find(1).update_attribute :status, Project::STATUS_ARCHIVED + Project.find(1).update_attribute :is_public, false + with_credentials "dlopper", "foo" do + assert_failure "ls", svn_url + end + end + + def test_read_on_closed_projects_should_succeed + Project.find(1).update_attribute :status, Project::STATUS_CLOSED + assert_success "ls", svn_url + end + + def test_read_on_closed_private_projects_should_succeed + Project.find(1).update_attribute :status, Project::STATUS_CLOSED + Project.find(1).update_attribute :is_public, false + with_credentials "dlopper", "foo" do + assert_success "ls", svn_url + end + end + + def test_commit_on_closed_projects_should_fail + Project.find(1).update_attribute :status, Project::STATUS_CLOSED + Role.find(2).add_permission! :commit_access + with_credentials "dlopper", "foo" do + assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename) + end + end + + def test_commit_on_closed_private_projects_should_fail + Project.find(1).update_attribute :status, Project::STATUS_CLOSED + Project.find(1).update_attribute :is_public, false + Role.find(2).add_permission! :commit_access + with_credentials "dlopper", "foo" do + assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename) + end + end + + if ldap_configured? + def test_user_with_ldap_auth_source_should_authenticate_with_ldap_credentials + ldap_user = User.new(:mail => 'example1@redmine.org', :firstname => 'LDAP', :lastname => 'user', :auth_source_id => 1) + ldap_user.login = 'example1' + ldap_user.save! + + with_settings :login_required => '1' do + with_credentials "example1", "123456" do + assert_success "ls", svn_url + end + end + + with_settings :login_required => '1' do + with_credentials "example1", "wrong" do + assert_failure "ls", svn_url + end + end + end + end + + def test_checkout + Dir.mktmpdir do |dir| + assert_success "checkout", svn_url, dir + end + end + + def test_read_commands + assert_success "info", svn_url + assert_success "ls", svn_url + assert_success "log", svn_url + end + + def test_write_commands + Role.find(2).add_permission! :commit_access + filename = random_filename + + Dir.mktmpdir do |dir| + assert_success "checkout", svn_url, dir + Dir.chdir(dir) do + # creates a file in the working copy + f = File.new(File.join(dir, filename), "w") + f.write "test file content" + f.close + + assert_success "add", filename + with_credentials "dlopper", "foo" do + assert_success "commit --message Committing_a_file" + assert_success "copy --message Copying_a_file", svn_url(filename), svn_url("#{filename}_copy") + assert_success "delete --message Deleting_a_file", svn_url(filename) + assert_success "mkdir --message Creating_a_directory", svn_url("#{filename}_dir") + end + assert_success "update" + + # checks that the working copy was updated + assert File.exists?(File.join(dir, "#{filename}_copy")) + assert File.directory?(File.join(dir, "#{filename}_dir")) + end + end + end + + def test_read_invalid_repo_should_fail + assert_failure "ls", svn_url("invalid") + end + + protected + + def execute(*args) + a = [SVN_BIN, "--no-auth-cache --non-interactive"] + a << "--username #{username}" if username + a << "--password #{password}" if password + + super a, *args + end + + def svn_url(path=nil) + host = ENV['REDMINE_TEST_DAV_SERVER'] || '127.0.0.1' + url = "http://#{host}/svn/ecookbook" + url << "/#{path}" if path + url + end +end diff --git a/test/extra/redmine_pm/test_case.rb b/test/extra/redmine_pm/test_case.rb new file mode 100644 index 00000000..594a58a8 --- /dev/null +++ b/test/extra/redmine_pm/test_case.rb @@ -0,0 +1,81 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +module RedminePmTest + class TestCase < ActiveSupport::TestCase + attr_reader :command, :response, :status, :username, :password + + # Cannot use transactional fixtures here: database + # will be accessed from Redmine.pm with its own connection + self.use_transactional_fixtures = false + + def test_dummy + end + + protected + + def assert_response(expected, msg=nil) + case expected + when :success + assert_equal 0, status, + (msg || "The command failed (exit: #{status}):\n #{command}\nOutput was:\n#{formatted_response}") + when :failure + assert_not_equal 0, status, + (msg || "The command succeed (exit: #{status}):\n #{command}\nOutput was:\n#{formatted_response}") + else + assert_equal expected, status, msg + end + end + + def assert_success(*args) + execute *args + assert_response :success + end + + def assert_failure(*args) + execute *args + assert_response :failure + end + + def with_credentials(username, password) + old_username, old_password = @username, @password + @username, @password = username, password + yield if block_given? + ensure + @username, @password = old_username, old_password + end + + def execute(*args) + @command = args.join(' ') + @status = nil + IO.popen("#{command} 2>&1") do |io| + @response = io.read + end + @status = $?.exitstatus + end + + def formatted_response + "#{'='*40}\n#{response}#{'='*40}" + end + + def random_filename + Redmine::Utils.random_hex(16) + end + end +end diff --git a/test/fixtures/attachments.yml b/test/fixtures/attachments.yml index 33bd6015..6384c8ee 100644 --- a/test/fixtures/attachments.yml +++ b/test/fixtures/attachments.yml @@ -267,31 +267,3 @@ attachments_020: filename: root_attachment.txt filesize: 54 author_id: 2 -attachments_021: - content_type: text/plain - downloads: 0 - created_on: 2012-05-12 16:14:50 +09:00 - disk_filename: 120512161450_project_test.txt - disk_directory: - container_id: 2 - digest: b0fe2abdb2599743d554a61d7da7ff74 - id: 21 - container_type: Project - description: "" - filename: project_test.txt - filesize: 54 - author_id: 2 -attachments_022: - content_type: text/plain - downloads: 0 - created_on: 2012-05-12 16:14:50 +09:00 - disk_filename: 120512161450_course_test.txt - disk_directory: - container_id: 1 - digest: b0fe2abdb2599743d554a61d7da7ff74 - id: 22 - container_type: Project - description: "" - filename: course_test.txt - filesize: 54 - author_id: 2 \ No newline at end of file diff --git a/test/fixtures/auth_sources.yml b/test/fixtures/auth_sources.yml new file mode 100644 index 00000000..a58a2888 --- /dev/null +++ b/test/fixtures/auth_sources.yml @@ -0,0 +1,13 @@ +--- +auth_sources_001: + id: 1 + type: AuthSourceLdap + name: 'LDAP test server' + host: '127.0.0.1' + port: 389 + base_dn: 'OU=Person,DC=redmine,DC=org' + attr_login: uid + attr_firstname: givenName + attr_lastname: sn + attr_mail: mail + onthefly_register: false diff --git a/test/fixtures/changes.yml b/test/fixtures/changes.yml new file mode 100644 index 00000000..487aad77 --- /dev/null +++ b/test/fixtures/changes.yml @@ -0,0 +1,22 @@ +--- +changes_001: + id: 1 + changeset_id: 100 + action: A + path: /test/some/path/in/the/repo + from_path: + from_revision: +changes_002: + id: 2 + changeset_id: 100 + action: A + path: /test/some/path/elsewhere/in/the/repo + from_path: + from_revision: +changes_003: + id: 3 + changeset_id: 101 + action: M + path: /test/some/path/in/the/repo + from_path: + from_revision: diff --git a/test/fixtures/changesets.yml b/test/fixtures/changesets.yml new file mode 100644 index 00000000..49bd18cc --- /dev/null +++ b/test/fixtures/changesets.yml @@ -0,0 +1,104 @@ +--- +changesets_001: + commit_date: 2007-04-11 + committed_on: 2007-04-11 15:14:44 +02:00 + revision: 1 + scmid: 691322a8eb01e11fd7 + id: 100 + comments: 'My very first commit do not escaping #<>&' + repository_id: 10 + committer: dlopper + user_id: 3 +changesets_002: + commit_date: 2007-04-12 + committed_on: 2007-04-12 15:14:44 +02:00 + revision: 2 + id: 101 + comments: 'This commit fixes #1, #2 and references #1 & #3' + repository_id: 10 + committer: dlopper + user_id: 3 +changesets_003: + commit_date: 2007-04-12 + committed_on: 2007-04-12 15:14:44 +02:00 + revision: 3 + id: 102 + comments: |- + A commit with wrong issue ids + IssueID #666 #3 + repository_id: 10 + committer: dlopper + user_id: 3 +changesets_004: + commit_date: 2007-04-12 + committed_on: 2007-04-12 15:14:44 +02:00 + revision: 4 + id: 103 + comments: |- + A commit with an issue id of an other project + IssueID 4 2 + repository_id: 10 + committer: dlopper + user_id: 3 +changesets_005: + commit_date: "2007-09-10" + comments: Modified one file in the folder. + committed_on: 2007-09-10 19:01:08 + revision: "5" + id: 104 + scmid: + user_id: 3 + repository_id: 10 + committer: dlopper +changesets_006: + commit_date: "2007-09-10" + comments: Moved helloworld.rb from / to /folder. + committed_on: 2007-09-10 19:01:47 + revision: "6" + id: 105 + scmid: + user_id: 3 + repository_id: 10 + committer: dlopper +changesets_007: + commit_date: "2007-09-10" + comments: Removed one file. + committed_on: 2007-09-10 19:02:16 + revision: "7" + id: 106 + scmid: + user_id: 3 + repository_id: 10 + committer: dlopper +changesets_008: + commit_date: "2007-09-10" + comments: |- + This commits references an issue. + Refs #2 + committed_on: 2007-09-10 19:04:35 + revision: "8" + id: 107 + scmid: + user_id: 3 + repository_id: 10 + committer: dlopper +changesets_009: + commit_date: "2009-09-10" + comments: One file added. + committed_on: 2009-09-10 19:04:35 + revision: "9" + id: 108 + scmid: + user_id: 3 + repository_id: 10 + committer: dlopper +changesets_010: + commit_date: "2009-09-10" + comments: Same file modified. + committed_on: 2009-09-10 19:04:35 + revision: "10" + id: 109 + scmid: + user_id: 3 + repository_id: 10 + committer: dlopper diff --git a/test/fixtures/comments.yml b/test/fixtures/comments.yml new file mode 100644 index 00000000..b69b28c7 --- /dev/null +++ b/test/fixtures/comments.yml @@ -0,0 +1,17 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +comments_001: + commented_type: News + commented_id: 1 + id: 1 + author_id: 1 + comments: my first comment + created_on: 2006-12-10 18:10:10 +01:00 + updated_on: 2006-12-10 18:10:10 +01:00 +comments_002: + commented_type: News + commented_id: 1 + id: 2 + author_id: 2 + comments: This is an other comment + created_on: 2006-12-10 18:12:10 +01:00 + updated_on: 2006-12-10 18:12:10 +01:00 diff --git a/test/fixtures/configuration/default.yml b/test/fixtures/configuration/default.yml new file mode 100644 index 00000000..fcca8268 --- /dev/null +++ b/test/fixtures/configuration/default.yml @@ -0,0 +1,8 @@ +default: + somesetting: foo + +production: + +development: + +test: diff --git a/test/fixtures/configuration/empty.yml b/test/fixtures/configuration/empty.yml new file mode 100644 index 00000000..24b33942 --- /dev/null +++ b/test/fixtures/configuration/empty.yml @@ -0,0 +1,7 @@ +default: + +production: + +development: + +test: diff --git a/test/fixtures/configuration/no_default.yml b/test/fixtures/configuration/no_default.yml new file mode 100644 index 00000000..b5895a29 --- /dev/null +++ b/test/fixtures/configuration/no_default.yml @@ -0,0 +1,8 @@ +default: + +production: + +development: + +test: + somesetting: foo diff --git a/test/fixtures/configuration/overrides.yml b/test/fixtures/configuration/overrides.yml new file mode 100644 index 00000000..cab915a3 --- /dev/null +++ b/test/fixtures/configuration/overrides.yml @@ -0,0 +1,9 @@ +default: + somesetting: foo + +production: + +development: + +test: + somesetting: bar diff --git a/test/fixtures/custom_fields.yml b/test/fixtures/custom_fields.yml new file mode 100644 index 00000000..7cbd5613 --- /dev/null +++ b/test/fixtures/custom_fields.yml @@ -0,0 +1,165 @@ +--- +custom_fields_001: + name: Database + min_length: 0 + regexp: "" + is_for_all: true + is_filter: true + type: IssueCustomField + max_length: 0 + possible_values: + - MySQL + - PostgreSQL + - Oracle + id: 1 + is_required: false + field_format: list + default_value: "" + editable: true + position: 2 +custom_fields_002: + name: Searchable field + min_length: 1 + regexp: "" + is_for_all: true + is_filter: true + type: IssueCustomField + max_length: 100 + possible_values: "" + id: 2 + is_required: false + field_format: string + searchable: true + default_value: "Default string" + editable: true + position: 1 +custom_fields_003: + name: Development status + min_length: 0 + regexp: "" + is_for_all: false + is_filter: true + type: ProjectCustomField + max_length: 0 + possible_values: + - Stable + - Beta + - Alpha + - Planning + id: 3 + is_required: false + field_format: list + default_value: "" + editable: true + position: 1 +custom_fields_004: + name: Phone number + min_length: 0 + regexp: "" + is_for_all: false + type: UserCustomField + max_length: 0 + possible_values: "" + id: 4 + is_required: false + field_format: string + default_value: "" + editable: true + position: 1 +custom_fields_005: + name: Money + min_length: 0 + regexp: "" + is_for_all: false + type: UserCustomField + max_length: 0 + possible_values: "" + id: 5 + is_required: false + field_format: float + default_value: "" + editable: true + position: 2 +custom_fields_006: + name: Float field + min_length: 0 + regexp: "" + is_for_all: true + type: IssueCustomField + max_length: 0 + possible_values: "" + id: 6 + is_required: false + field_format: float + default_value: "" + editable: true + position: 3 +custom_fields_007: + name: Billable + min_length: 0 + regexp: "" + is_for_all: false + is_filter: true + type: TimeEntryActivityCustomField + max_length: 0 + possible_values: "" + id: 7 + is_required: false + field_format: bool + default_value: "" + editable: true + position: 1 +custom_fields_008: + name: Custom date + min_length: 0 + regexp: "" + is_for_all: true + is_filter: false + type: IssueCustomField + max_length: 0 + possible_values: "" + id: 8 + is_required: false + field_format: date + default_value: "" + editable: true + position: 4 +custom_fields_009: + name: Project 1 cf + min_length: 0 + regexp: "" + is_for_all: false + is_filter: true + type: IssueCustomField + max_length: 0 + possible_values: "" + id: 9 + is_required: false + field_format: date + default_value: "" + editable: true + position: 5 +custom_fields_010: + name: Overtime + min_length: 0 + regexp: "" + is_for_all: false + is_filter: false + type: TimeEntryCustomField + max_length: 0 + possible_values: "" + id: 10 + is_required: false + field_format: bool + default_value: 0 + editable: true + position: 1 +custom_fields_011: + id: 11 + name: Binary + type: CustomField + possible_values: + - !binary | + SGXDqWzDp2prc2Tigqw2NTTDuQ== + - Other value + field_format: list diff --git a/test/fixtures/custom_fields_projects.yml b/test/fixtures/custom_fields_projects.yml new file mode 100644 index 00000000..295919b0 --- /dev/null +++ b/test/fixtures/custom_fields_projects.yml @@ -0,0 +1,4 @@ +--- +custom_fields_projects_001: + custom_field_id: 9 + project_id: 1 diff --git a/test/fixtures/custom_fields_trackers.yml b/test/fixtures/custom_fields_trackers.yml new file mode 100644 index 00000000..bfbe0d24 --- /dev/null +++ b/test/fixtures/custom_fields_trackers.yml @@ -0,0 +1,19 @@ +--- +custom_fields_trackers_001: + custom_field_id: 1 + tracker_id: 1 +custom_fields_trackers_002: + custom_field_id: 2 + tracker_id: 1 +custom_fields_trackers_003: + custom_field_id: 2 + tracker_id: 3 +custom_fields_trackers_004: + custom_field_id: 6 + tracker_id: 1 +custom_fields_trackers_005: + custom_field_id: 6 + tracker_id: 2 +custom_fields_trackers_006: + custom_field_id: 6 + tracker_id: 3 diff --git a/test/fixtures/custom_values.yml b/test/fixtures/custom_values.yml new file mode 100644 index 00000000..efc8a292 --- /dev/null +++ b/test/fixtures/custom_values.yml @@ -0,0 +1,103 @@ +--- +custom_values_006: + customized_type: Issue + custom_field_id: 2 + customized_id: 3 + id: 6 + value: "125" +custom_values_007: + customized_type: Project + custom_field_id: 3 + customized_id: 1 + id: 7 + value: Stable +custom_values_001: + customized_type: Principal + custom_field_id: 4 + customized_id: 3 + id: 1 + value: "" +custom_values_002: + customized_type: Principal + custom_field_id: 4 + customized_id: 4 + id: 2 + value: 01 23 45 67 89 +custom_values_003: + customized_type: Principal + custom_field_id: 4 + customized_id: 2 + id: 3 + value: "01 42 50 00 00" +custom_values_004: + customized_type: Issue + custom_field_id: 2 + customized_id: 1 + id: 4 + value: "125" +custom_values_005: + customized_type: Issue + custom_field_id: 2 + customized_id: 2 + id: 5 + value: "" +custom_values_008: + customized_type: Issue + custom_field_id: 1 + customized_id: 3 + id: 8 + value: "MySQL" +custom_values_009: + customized_type: Issue + custom_field_id: 2 + customized_id: 7 + id: 9 + value: "this is a stringforcustomfield search" +custom_values_010: + customized_type: Issue + custom_field_id: 6 + customized_id: 1 + id: 10 + value: "2.1" +custom_values_011: + customized_type: Issue + custom_field_id: 6 + customized_id: 2 + id: 11 + value: "2.05" +custom_values_012: + customized_type: Issue + custom_field_id: 6 + customized_id: 3 + id: 12 + value: "11.65" +custom_values_013: + customized_type: Issue + custom_field_id: 6 + customized_id: 7 + id: 13 + value: "" +custom_values_014: + customized_type: Issue + custom_field_id: 6 + customized_id: 5 + id: 14 + value: "-7.6" +custom_values_015: + customized_type: Enumeration + custom_field_id: 7 + customized_id: 10 + id: 15 + value: true +custom_values_016: + customized_type: Enumeration + custom_field_id: 7 + customized_id: 11 + id: 16 + value: '1' +custom_values_017: + customized_type: Issue + custom_field_id: 8 + customized_id: 1 + id: 17 + value: '2009-12-01' diff --git a/test/fixtures/diffs/issue-12641-ja.diff b/test/fixtures/diffs/issue-12641-ja.diff new file mode 100644 index 00000000..f19df33f --- /dev/null +++ b/test/fixtures/diffs/issue-12641-ja.diff @@ -0,0 +1,25 @@ +# HG changeset patch +# User tmaruyama +# Date 1362559296 0 +# Node ID ee54942e0289c30bea1b1973750b698b1ee7c466 +# Parent 738777832f379f6f099c25251593fc57bc17f586 +fix some Japanese "issue" translations (#13350) + +Contributed by Go MAEDA. + +diff --git a/config/locales/ja.yml b/config/locales/ja.yml +--- a/config/locales/ja.yml ++++ b/config/locales/ja.yml +@@ -904,9 +904,9 @@ ja: + text_journal_set_to: "%{label} を %{value} にセット" + text_journal_deleted: "%{label} を削除 (%{old})" + text_journal_added: "%{label} %{value} を追加" +- text_tip_issue_begin_day: この日に開始するタスク +- text_tip_issue_end_day: この日に終了するタスク +- text_tip_issue_begin_end_day: この日のうちに開始して終了するタスク ++ text_tip_issue_begin_day: この日に開始するチケット ++ text_tip_issue_end_day: この日に終了するチケット ++ text_tip_issue_begin_end_day: この日に開始・終了するチケット + text_caracters_maximum: "最大%{count}文字です。" + text_caracters_minimum: "最低%{count}文字の長さが必要です" + text_length_between: "長さは%{min}から%{max}文字までです。" diff --git a/test/fixtures/diffs/issue-12641-ru.diff b/test/fixtures/diffs/issue-12641-ru.diff new file mode 100644 index 00000000..acb8d542 --- /dev/null +++ b/test/fixtures/diffs/issue-12641-ru.diff @@ -0,0 +1,19 @@ +# HG changeset patch +# User tmaruyama +# Date 1355872765 0 +# Node ID 8a13ebed1779c2e85fa644ecdd0de81996c969c4 +# Parent 5c3c5f917ae92f278fe42c6978366996595b0796 +Russian "about_x_hours" translation changed by Mikhail Velkin (#12640) + +diff --git a/config/locales/ru.yml b/config/locales/ru.yml +--- a/config/locales/ru.yml ++++ b/config/locales/ru.yml +@@ -115,7 +115,7 @@ ru: + one: "около %{count} часа" + few: "около %{count} часов" + many: "около %{count} часов" +- other: "около %{count} часа" ++ other: "около %{count} часов" + x_hours: + one: "1 час" + other: "%{count} часов" diff --git a/test/fixtures/diffs/issue-13644-1.diff b/test/fixtures/diffs/issue-13644-1.diff new file mode 100644 index 00000000..95c48ef4 --- /dev/null +++ b/test/fixtures/diffs/issue-13644-1.diff @@ -0,0 +1,7 @@ +--- a.txt 2013-04-05 14:19:39.000000000 +0900 ++++ b.txt 2013-04-05 14:19:51.000000000 +0900 +@@ -1,3 +1,3 @@ + aaaa +-日本 ++日本語 + bbbb diff --git a/test/fixtures/diffs/issue-13644-2.diff b/test/fixtures/diffs/issue-13644-2.diff new file mode 100644 index 00000000..f8a4076e --- /dev/null +++ b/test/fixtures/diffs/issue-13644-2.diff @@ -0,0 +1,7 @@ +--- a.txt 2013-04-05 14:19:39.000000000 +0900 ++++ b.txt 2013-04-05 14:19:51.000000000 +0900 +@@ -1,3 +1,3 @@ + aaaa +-日本 ++にっぽん日本 + bbbb diff --git a/test/fixtures/diffs/partials.diff b/test/fixtures/diffs/partials.diff new file mode 100644 index 00000000..f6879bc8 --- /dev/null +++ b/test/fixtures/diffs/partials.diff @@ -0,0 +1,46 @@ +--- partials.txt Wed Jan 19 12:06:17 2011 ++++ partials.1.txt Wed Jan 19 12:06:10 2011 +@@ -1,31 +1,31 @@ +-Lorem ipsum dolor sit amet, consectetur adipiscing elit ++Lorem ipsum dolor sit amet, consectetur adipiscing xx + Praesent et sagittis dui. Vivamus ac diam diam +-Ut sed auctor justo ++xxx auctor justo + Suspendisse venenatis sollicitudin magna quis suscipit +-Sed blandit gravida odio ac ultrices ++Sed blandit gxxxxa odio ac ultrices + Morbi rhoncus est ut est aliquam tempus +-Morbi id nisi vel felis tincidunt tempus ++Morbi id nisi vel felis xx tempus + Mauris auctor sagittis ante eu luctus +-Fusce commodo felis sed ligula congue molestie ++Fusce commodo felis sed ligula congue + Lorem ipsum dolor sit amet, consectetur adipiscing elit +-Praesent et sagittis dui. Vivamus ac diam diam ++et sagittis dui. Vivamus ac diam diam + Ut sed auctor justo + Suspendisse venenatis sollicitudin magna quis suscipit + Sed blandit gravida odio ac ultrices + +-Lorem ipsum dolor sit amet, consectetur adipiscing elit +-Praesent et sagittis dui. Vivamus ac diam diam ++Lorem ipsum dolor sit amet, xxxx adipiscing elit + Ut sed auctor justo + Suspendisse venenatis sollicitudin magna quis suscipit + Sed blandit gravida odio ac ultrices +-Morbi rhoncus est ut est aliquam tempus ++Morbi rhoncus est ut est xxxx tempus ++New line + Morbi id nisi vel felis tincidunt tempus + Mauris auctor sagittis ante eu luctus + Fusce commodo felis sed ligula congue molestie + +-Lorem ipsum dolor sit amet, consectetur adipiscing elit +-Praesent et sagittis dui. Vivamus ac diam diam +-Ut sed auctor justo ++Lorem ipsum dolor sit amet, xxxxtetur adipiscing elit ++Praesent et xxxxx. Vivamus ac diam diam ++Ut sed auctor + Suspendisse venenatis sollicitudin magna quis suscipit + Sed blandit gravida odio ac ultrices + Morbi rhoncus est ut est aliquam tempus diff --git a/test/fixtures/diffs/subversion.diff b/test/fixtures/diffs/subversion.diff new file mode 100644 index 00000000..2b1ae520 --- /dev/null +++ b/test/fixtures/diffs/subversion.diff @@ -0,0 +1,79 @@ +Index: app/views/settings/_general.rhtml +=================================================================== +--- app/views/settings/_general.rhtml (revision 2094) ++++ app/views/settings/_general.rhtml (working copy) +@@ -48,6 +48,9 @@ +

+ <%= text_field_tag 'settings[feeds_limit]', Setting.feeds_limit, :size => 6 %>

+ ++

++<%= text_field_tag 'settings[diff_max_lines_displayed]', Setting.diff_max_lines_displayed, :size => 6 %>

++ +

+ <%= check_box_tag 'settings[gravatar_enabled]', 1, Setting.gravatar_enabled? %><%= hidden_field_tag 'settings[gravatar_enabled]', 0 %>

+ +Index: app/views/common/_diff.rhtml +=================================================================== +--- app/views/common/_diff.rhtml (revision 2111) ++++ app/views/common/_diff.rhtml (working copy) +@@ -1,4 +1,5 @@ +-<% Redmine::UnifiedDiff.new(diff, :type => diff_type).each do |table_file| -%> ++<% diff = Redmine::UnifiedDiff.new(diff, :type => diff_type, :max_lines => Setting.diff_max_lines_displayed.to_i) -%> ++<% diff.each do |table_file| -%> +
+ <% if diff_type == 'sbs' -%> + +@@ -62,3 +63,5 @@ + + + <% end -%> ++ ++<%= l(:text_diff_truncated) if diff.truncated? %> +Index: lang/lt.yml +=================================================================== +--- config/settings.yml (revision 2094) ++++ config/settings.yml (working copy) +@@ -61,6 +61,9 @@ + feeds_limit: + format: int + default: 15 ++diff_max_lines_displayed: ++ format: int ++ default: 1500 + enabled_scm: + serialized: true + default: +Index: lib/redmine/unified_diff.rb +=================================================================== +--- lib/redmine/unified_diff.rb (revision 2110) ++++ lib/redmine/unified_diff.rb (working copy) +@@ -19,8 +19,11 @@ + # Class used to parse unified diffs + class UnifiedDiff < Array + def initialize(diff, options={}) ++ options.assert_valid_keys(:type, :max_lines) + diff_type = options[:type] || 'inline' + ++ lines = 0 ++ @truncated = false + diff_table = DiffTable.new(diff_type) + diff.each do |line| + if line =~ /^(---|\+\+\+) (.*)$/ +@@ -28,10 +31,17 @@ + diff_table = DiffTable.new(diff_type) + end + diff_table.add_line line ++ lines += 1 ++ if options[:max_lines] && lines > options[:max_lines] ++ @truncated = true ++ break ++ end + end + self << diff_table unless diff_table.empty? + self + end ++ ++ def truncated?; @truncated; end + end + + # Class that represents a file diff diff --git a/test/fixtures/documents.yml b/test/fixtures/documents.yml new file mode 100644 index 00000000..9398a5b4 --- /dev/null +++ b/test/fixtures/documents.yml @@ -0,0 +1,14 @@ +documents_001: + created_on: 2007-01-27 15:08:27 +01:00 + project_id: 1 + title: "Test document" + id: 1 + description: "Document description" + category_id: 1 +documents_002: + created_on: 2007-02-12 15:08:27 +01:00 + project_id: 1 + title: "An other document" + id: 2 + description: "" + category_id: 1 diff --git a/test/fixtures/enabled_modules.yml b/test/fixtures/enabled_modules.yml new file mode 100644 index 00000000..edb2e697 --- /dev/null +++ b/test/fixtures/enabled_modules.yml @@ -0,0 +1,105 @@ +--- +enabled_modules_001: + name: issue_tracking + project_id: 1 + id: 1 +enabled_modules_002: + name: time_tracking + project_id: 1 + id: 2 +enabled_modules_003: + name: news + project_id: 1 + id: 3 +enabled_modules_004: + name: documents + project_id: 1 + id: 4 +enabled_modules_005: + name: files + project_id: 1 + id: 5 +enabled_modules_006: + name: wiki + project_id: 1 + id: 6 +enabled_modules_007: + name: repository + project_id: 1 + id: 7 +enabled_modules_008: + name: boards + project_id: 1 + id: 8 +enabled_modules_009: + name: repository + project_id: 3 + id: 9 +enabled_modules_010: + name: wiki + project_id: 3 + id: 10 +enabled_modules_011: + name: issue_tracking + project_id: 2 + id: 11 +enabled_modules_012: + name: time_tracking + project_id: 3 + id: 12 +enabled_modules_013: + name: issue_tracking + project_id: 3 + id: 13 +enabled_modules_014: + name: issue_tracking + project_id: 5 + id: 14 +enabled_modules_015: + name: wiki + project_id: 2 + id: 15 +enabled_modules_016: + name: boards + project_id: 2 + id: 16 +enabled_modules_017: + name: calendar + project_id: 1 + id: 17 +enabled_modules_018: + name: gantt + project_id: 1 + id: 18 +enabled_modules_019: + name: calendar + project_id: 2 + id: 19 +enabled_modules_020: + name: gantt + project_id: 2 + id: 20 +enabled_modules_021: + name: calendar + project_id: 3 + id: 21 +enabled_modules_022: + name: gantt + project_id: 3 + id: 22 +enabled_modules_023: + name: calendar + project_id: 5 + id: 23 +enabled_modules_024: + name: gantt + project_id: 5 + id: 24 +enabled_modules_025: + name: news + project_id: 2 + id: 25 +enabled_modules_026: + name: repository + project_id: 2 + id: 26 diff --git a/test/fixtures/encoding/iso-8859-1.txt b/test/fixtures/encoding/iso-8859-1.txt new file mode 100644 index 00000000..8ad6cc0f --- /dev/null +++ b/test/fixtures/encoding/iso-8859-1.txt @@ -0,0 +1 @@ +Texte encod en ISO-8859-1. \ No newline at end of file diff --git a/test/fixtures/enumerations.yml b/test/fixtures/enumerations.yml new file mode 100644 index 00000000..f9757122 --- /dev/null +++ b/test/fixtures/enumerations.yml @@ -0,0 +1,103 @@ +--- +enumerations_001: + name: Uncategorized + id: 1 + type: DocumentCategory + active: true + position: 1 +enumerations_002: + name: User documentation + id: 2 + type: DocumentCategory + active: true + position: 2 +enumerations_003: + name: Technical documentation + id: 3 + type: DocumentCategory + active: true + position: 3 +enumerations_004: + name: Low + id: 4 + type: IssuePriority + active: true + position: 1 + position_name: lowest +enumerations_005: + name: Normal + id: 5 + type: IssuePriority + is_default: true + active: true + position: 2 + position_name: default +enumerations_006: + name: High + id: 6 + type: IssuePriority + active: true + position: 3 + position_name: high3 +enumerations_007: + name: Urgent + id: 7 + type: IssuePriority + active: true + position: 4 + position_name: high2 +enumerations_008: + name: Immediate + id: 8 + type: IssuePriority + active: true + position: 5 + position_name: highest +enumerations_009: + name: Design + id: 9 + type: TimeEntryActivity + position: 1 + active: true +enumerations_010: + name: Development + id: 10 + type: TimeEntryActivity + position: 2 + is_default: true + active: true +enumerations_011: + name: QA + id: 11 + type: TimeEntryActivity + position: 3 + active: true +enumerations_012: + name: Default Enumeration + id: 12 + type: Enumeration + is_default: true + active: true +enumerations_013: + name: Another Enumeration + id: 13 + type: Enumeration + active: true +enumerations_014: + name: Inactive Activity + id: 14 + type: TimeEntryActivity + position: 4 + active: false +enumerations_015: + name: Inactive Priority + id: 15 + type: IssuePriority + position: 6 + active: false +enumerations_016: + name: Inactive Document Category + id: 16 + type: DocumentCategory + active: false + position: 4 diff --git a/test/fixtures/files/2006/07/060719210727_archive.zip b/test/fixtures/files/2006/07/060719210727_archive.zip new file mode 100644 index 0000000000000000000000000000000000000000..5467885d4b28eedd531d78c32380ffd3e69eded0 GIT binary patch literal 157 zcmWIWW@Zs#U|`^2Fv$%vtB#e4Q3moW^-3yAv`?PU31e8~(mLr% zmr(1LGjl>(xBO9Rz0@@4&k>OY35A}93<2JZOd<@pjRTs 'issues', :action => 'show', :id => @issue, :project_id => @project ++ redirect_to :controller => 'issues', :action => 'show', :id => @issue + return + end + end diff --git a/test/fixtures/files/2006/07/060719210727_changeset_utf8.diff b/test/fixtures/files/2006/07/060719210727_changeset_utf8.diff new file mode 100644 index 00000000..e594f203 --- /dev/null +++ b/test/fixtures/files/2006/07/060719210727_changeset_utf8.diff @@ -0,0 +1,13 @@ +Index: trunk/app/controllers/issues_controller.rb +=================================================================== +--- trunk/app/controllers/issues_controller.rb (révision 1483) ++++ trunk/app/controllers/issues_controller.rb (révision 1484) +@@ -149,7 +149,7 @@ + attach_files(@issue, params[:attachments]) + flash[:notice] = 'Demande créée avec succès' + Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added') +- redirect_to :controller => 'issues', :action => 'show', :id => @issue, :project_id => @project ++ redirect_to :controller => 'issues', :action => 'show', :id => @issue + return + end + end diff --git a/test/fixtures/files/2006/07/060719210727_source.rb b/test/fixtures/files/2006/07/060719210727_source.rb new file mode 100644 index 00000000..3f928963 --- /dev/null +++ b/test/fixtures/files/2006/07/060719210727_source.rb @@ -0,0 +1,10 @@ +# The Greeter class +class Greeter + def initialize(name) + @name = name.capitalize + end + + def salute + puts "Hello #{@name}!" + end +end diff --git a/test/fixtures/files/2010/11/101123161450_testfile_1.png b/test/fixtures/files/2010/11/101123161450_testfile_1.png new file mode 100644 index 0000000000000000000000000000000000000000..6dad7f1d83781a2bb8216cd5a353205cced12d74 GIT binary patch literal 2654 zcmdT`X;f0{8b%uwO*>`ILv<@nMOj&TQado4%ONu}W$CS!}z|yiDh!Ai{%z^uL*8Oqs>epTC{JCqdz4yDn{Y~%tKF{-hnLgew zDoQ#^5C}xY&D9ABfylvt-Kn@9JdvaC{lH7^9MZ)RLhIAxgOByW9xhH0+0Uz#`7j@} zK+m|M&p{x{YCmr|Nb#RqpizO~=5b7H64;% zTwb@oS&vSIcAx_vw@YAxL1~}}+q)kq&y`#3elUst!$dLZHz&fO9gPl}@GDLjM^iIX zC4503v>!u?qt7OZWh-NLj0AMgQIV$N;s-95%aP#1N=np%wbHjkrXY?*@{qO45Je40 z+Pc5fXqEr>gnz4X;kX>+uL=LdpofvkWF;t6B9)t6_lpx3`gUg-i)&acY-ngG<lM8rvpqAlv`6;Ge;-(_fM0)xf-P7!QkWveNEC{!n(&@GIkMsjG^u3q&KjWE@9 zb*bAArth|~DYjExb>E(TLQ6>8ap}xAACJ2@B9WzPaG>h>bH@~^xK$K`t4A9f8}pt- zk#63+DbJ&w9%b>xpW1AQ%TJ?6nz@gJS(%w{%H8t!rLHDdR8$ySSk!q|a%$Yrrb=r!v=9Vqt?OEE1Ghd+j8wr){TIXz?dBLb^#=&UDut<2Zv%C9k zr57oAdZzczo`av;vwd5LDuZ@TJ%fg6#0rMI%nLoXB9dR1x#Xg-SlTe-G?m3-^-d@s zFRU#>B9Up0OGp&zMMp5~)a;Os9BnI7Z|AF5_`yYa-rVQ=E!IbXGqsp%1A&eWq3WM#H1}KV zz~G>^??dl-UZt|8xs=J4NAEe4Me;v_;Gxeh)3@crQAiNe5LD#*K_3)RX1(g^v&>NjfAsau(TlJ)raMF zy!$R3R@c>EIbe3|sSn7> zyp~KVDXXiiyH!xIE5B&5l6QkIrk=hi6t8x0QK)4Y(iOeXhtOeZ3VXHk7TJ&hN}Kp!G^TKBFh6BKqCYIslvDj-(w0u!vZB3ViPUH+^ z&XLRuI2tvgAM*ldM)Zf~A?x55iaU&qIsnI9@jHNRiGb}cI_Yr_s38{UnRupn0R zo9hvm)6;d%O}yHZqh}pFFfg!BuqQ{~7I(+fuqLXvuP>So$c)T*`YPZc6bePazrUtO z2E2UnVz-6GTLCiQyl7;;i3J2DV*cwO0vh9keu~vXe+8 z?E+5C@JSZLJMX<`S67!!(sz7WK;Bpcgw-YlV5 zU0t2mlG>uAq%;RYZCD5jQWM^a1Qc~2AnvZ6JNM1g{NtJepFMkKVq)^P`Y59~w*Af2 zRJa+Y!4=>{dFp*&Y6J_CHt5})wBQZYBvH1?q)@5jaZT!*Hf;wqrw&p+wPl!^n|lW~ z5}?vO#MNaoPy>MeDL|bVn-b95cVYTNs1+%cjw!ftqtxNjdWnKuYp@Q4u=W_m;FqHO sYnlF^TkLD^{J$Sf{>$eJx0e)#iiz>% literal 0 HcmV?d00001 diff --git a/test/fixtures/files/2010/12/101223161450_testfile_2.png b/test/fixtures/files/2010/12/101223161450_testfile_2.png new file mode 100644 index 0000000000000000000000000000000000000000..aec3fcfdffe03043c2d9055b1c570a16e7a6159c GIT binary patch literal 3582 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/files/hg-export.diff b/test/fixtures/files/hg-export.diff new file mode 100644 index 00000000..54dfdf5c --- /dev/null +++ b/test/fixtures/files/hg-export.diff @@ -0,0 +1,13 @@ +# HG changeset patch +# User test +# Date 1348014182 -32400 +# Node ID d1c871b8ef113df7f1c56d41e6e3bfbaff976e1f +# Parent 180b6605936cdc7909c5f08b59746ec1a7c99b3e +modify test1.txt + +diff -r 180b6605936c -r d1c871b8ef11 test1.txt +--- a/test1.txt ++++ b/test1.txt +@@ -1,1 +1,1 @@ +-test1 ++modify test1 diff --git a/test/fixtures/files/iso8859-1.txt b/test/fixtures/files/iso8859-1.txt new file mode 100644 index 00000000..1eca5644 --- /dev/null +++ b/test/fixtures/files/iso8859-1.txt @@ -0,0 +1,13 @@ +Index: trunk/app/controllers/issues_controller.rb +=================================================================== +--- trunk/app/controllers/issues_controller.rb (rvision 1483) ++++ trunk/app/controllers/issues_controller.rb (rvision 1484) +@@ -149,7 +149,7 @@ + attach_files(@issue, params[:attachments]) + flash[:notice] = 'Demande cre avec succs' + Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added') +- redirect_to :controller => 'issues', :action => 'show', :id => @issue, :project_id => @project ++ redirect_to :controller => 'issues', :action => 'show', :id => @issue + return + end + end diff --git a/test/fixtures/files/japanese-utf-8.txt b/test/fixtures/files/japanese-utf-8.txt new file mode 100644 index 00000000..c77dbef7 --- /dev/null +++ b/test/fixtures/files/japanese-utf-8.txt @@ -0,0 +1 @@ +日本語 diff --git a/test/fixtures/files/testfile.txt b/test/fixtures/files/testfile.txt new file mode 100644 index 00000000..84f601ee --- /dev/null +++ b/test/fixtures/files/testfile.txt @@ -0,0 +1,2 @@ +this is a text file for upload tests +with multiple lines diff --git a/test/fixtures/groups_users.yml b/test/fixtures/groups_users.yml new file mode 100644 index 00000000..e5da1738 --- /dev/null +++ b/test/fixtures/groups_users.yml @@ -0,0 +1,7 @@ +--- +groups_users_001: + group_id: 10 + user_id: 8 +groups_users_002: + group_id: 11 + user_id: 8 diff --git a/test/fixtures/issue_categories.yml b/test/fixtures/issue_categories.yml new file mode 100644 index 00000000..0a5c820b --- /dev/null +++ b/test/fixtures/issue_categories.yml @@ -0,0 +1,21 @@ +--- +issue_categories_001: + name: Printing + project_id: 1 + assigned_to_id: 2 + id: 1 +issue_categories_002: + name: Recipes + project_id: 1 + assigned_to_id: + id: 2 +issue_categories_003: + name: Stock management + project_id: 2 + assigned_to_id: + id: 3 +issue_categories_004: + name: Printing + project_id: 2 + assigned_to_id: + id: 4 diff --git a/test/fixtures/issue_relations.yml b/test/fixtures/issue_relations.yml new file mode 100644 index 00000000..7a1d5a20 --- /dev/null +++ b/test/fixtures/issue_relations.yml @@ -0,0 +1,12 @@ +issue_relation_001: + id: 1 + issue_from_id: 10 + issue_to_id: 9 + relation_type: blocks + delay: +issue_relation_002: + id: 2 + issue_from_id: 2 + issue_to_id: 3 + relation_type: relates + delay: diff --git a/test/fixtures/issue_statuses.yml b/test/fixtures/issue_statuses.yml new file mode 100644 index 00000000..a2212bd3 --- /dev/null +++ b/test/fixtures/issue_statuses.yml @@ -0,0 +1,37 @@ +--- +issue_statuses_001: + id: 1 + name: New + is_default: true + is_closed: false + position: 1 +issue_statuses_002: + id: 2 + name: Assigned + is_default: false + is_closed: false + position: 2 +issue_statuses_003: + id: 3 + name: Resolved + is_default: false + is_closed: false + position: 3 +issue_statuses_004: + name: Feedback + id: 4 + is_default: false + is_closed: false + position: 4 +issue_statuses_005: + id: 5 + name: Closed + is_default: false + is_closed: true + position: 5 +issue_statuses_006: + id: 6 + name: Rejected + is_default: false + is_closed: true + position: 6 diff --git a/test/fixtures/issues.yml b/test/fixtures/issues.yml new file mode 100644 index 00000000..68f5fb27 --- /dev/null +++ b/test/fixtures/issues.yml @@ -0,0 +1,268 @@ +--- +issues_001: + created_on: <%= 3.days.ago.to_s(:db) %> + project_id: 1 + updated_on: <%= 1.day.ago.to_s(:db) %> + priority_id: 4 + subject: Can't print recipes + id: 1 + fixed_version_id: + category_id: 1 + description: Unable to print recipes + tracker_id: 1 + assigned_to_id: + author_id: 2 + status_id: 1 + start_date: <%= 1.day.ago.to_date.to_s(:db) %> + due_date: <%= 10.day.from_now.to_date.to_s(:db) %> + root_id: 1 + lft: 1 + rgt: 2 + lock_version: 3 +issues_002: + created_on: 2006-07-19 21:04:21 +02:00 + project_id: 1 + updated_on: 2006-07-19 21:09:50 +02:00 + priority_id: 5 + subject: Add ingredients categories + id: 2 + fixed_version_id: 2 + category_id: + description: Ingredients of the recipe should be classified by categories + tracker_id: 2 + assigned_to_id: 3 + author_id: 2 + status_id: 2 + start_date: <%= 2.day.ago.to_date.to_s(:db) %> + due_date: + root_id: 2 + lft: 1 + rgt: 2 + lock_version: 3 + done_ratio: 30 +issues_003: + created_on: 2006-07-19 21:07:27 +02:00 + project_id: 1 + updated_on: 2006-07-19 21:07:27 +02:00 + priority_id: 4 + subject: Error 281 when updating a recipe + id: 3 + fixed_version_id: + category_id: + description: Error 281 is encountered when saving a recipe + tracker_id: 1 + assigned_to_id: 3 + author_id: 2 + status_id: 1 + start_date: <%= 15.day.ago.to_date.to_s(:db) %> + due_date: <%= 5.day.ago.to_date.to_s(:db) %> + root_id: 3 + lft: 1 + rgt: 2 +issues_004: + created_on: <%= 5.days.ago.to_s(:db) %> + project_id: 2 + updated_on: <%= 2.days.ago.to_s(:db) %> + priority_id: 4 + subject: Issue on project 2 + id: 4 + fixed_version_id: + category_id: + description: Issue on project 2 + tracker_id: 1 + assigned_to_id: 2 + author_id: 2 + status_id: 1 + root_id: 4 + lft: 1 + rgt: 2 +issues_005: + created_on: <%= 5.days.ago.to_s(:db) %> + project_id: 3 + updated_on: <%= 2.days.ago.to_s(:db) %> + priority_id: 4 + subject: Subproject issue + id: 5 + fixed_version_id: + category_id: + description: This is an issue on a cookbook subproject + tracker_id: 1 + assigned_to_id: + author_id: 2 + status_id: 1 + root_id: 5 + lft: 1 + rgt: 2 +issues_006: + created_on: <%= 1.minute.ago.to_s(:db) %> + project_id: 5 + updated_on: <%= 1.minute.ago.to_s(:db) %> + priority_id: 4 + subject: Issue of a private subproject + id: 6 + fixed_version_id: + category_id: + description: This is an issue of a private subproject of cookbook + tracker_id: 1 + assigned_to_id: + author_id: 2 + status_id: 1 + start_date: <%= Date.today.to_s(:db) %> + due_date: <%= 1.days.from_now.to_date.to_s(:db) %> + root_id: 6 + lft: 1 + rgt: 2 +issues_007: + created_on: <%= 10.days.ago.to_s(:db) %> + project_id: 1 + updated_on: <%= 10.days.ago.to_s(:db) %> + priority_id: 5 + subject: Issue due today + id: 7 + fixed_version_id: + category_id: + description: This is an issue that is due today + tracker_id: 1 + assigned_to_id: + author_id: 2 + status_id: 1 + start_date: <%= 10.days.ago.to_s(:db) %> + due_date: <%= Date.today.to_s(:db) %> + lock_version: 0 + root_id: 7 + lft: 1 + rgt: 2 +issues_008: + created_on: <%= 10.days.ago.to_s(:db) %> + project_id: 1 + updated_on: <%= 10.days.ago.to_s(:db) %> + priority_id: 5 + subject: Closed issue + id: 8 + fixed_version_id: + category_id: + description: This is a closed issue. + tracker_id: 1 + assigned_to_id: + author_id: 2 + status_id: 5 + start_date: + due_date: + lock_version: 0 + root_id: 8 + lft: 1 + rgt: 2 + closed_on: <%= 3.days.ago.to_s(:db) %> +issues_009: + created_on: <%= 1.minute.ago.to_s(:db) %> + project_id: 5 + updated_on: <%= 1.minute.ago.to_s(:db) %> + priority_id: 5 + subject: Blocked Issue + id: 9 + fixed_version_id: + category_id: + description: This is an issue that is blocked by issue #10 + tracker_id: 1 + assigned_to_id: + author_id: 2 + status_id: 1 + start_date: <%= Date.today.to_s(:db) %> + due_date: <%= 1.days.from_now.to_date.to_s(:db) %> + root_id: 9 + lft: 1 + rgt: 2 +issues_010: + created_on: <%= 1.minute.ago.to_s(:db) %> + project_id: 5 + updated_on: <%= 1.minute.ago.to_s(:db) %> + priority_id: 5 + subject: Issue Doing the Blocking + id: 10 + fixed_version_id: + category_id: + description: This is an issue that blocks issue #9 + tracker_id: 1 + assigned_to_id: + author_id: 2 + status_id: 1 + start_date: <%= Date.today.to_s(:db) %> + due_date: <%= 1.days.from_now.to_date.to_s(:db) %> + root_id: 10 + lft: 1 + rgt: 2 +issues_011: + created_on: <%= 3.days.ago.to_s(:db) %> + project_id: 1 + updated_on: <%= 1.day.ago.to_s(:db) %> + priority_id: 5 + subject: Closed issue on a closed version + id: 11 + fixed_version_id: 1 + category_id: 1 + description: + tracker_id: 1 + assigned_to_id: + author_id: 2 + status_id: 5 + start_date: <%= 1.day.ago.to_date.to_s(:db) %> + due_date: + root_id: 11 + lft: 1 + rgt: 2 + closed_on: <%= 1.day.ago.to_s(:db) %> +issues_012: + created_on: <%= 3.days.ago.to_s(:db) %> + project_id: 1 + updated_on: <%= 1.day.ago.to_s(:db) %> + priority_id: 5 + subject: Closed issue on a locked version + id: 12 + fixed_version_id: 2 + category_id: 1 + description: + tracker_id: 1 + assigned_to_id: + author_id: 3 + status_id: 5 + start_date: <%= 1.day.ago.to_date.to_s(:db) %> + due_date: + root_id: 12 + lft: 1 + rgt: 2 + closed_on: <%= 1.day.ago.to_s(:db) %> +issues_013: + created_on: <%= 5.days.ago.to_s(:db) %> + project_id: 3 + updated_on: <%= 2.days.ago.to_s(:db) %> + priority_id: 4 + subject: Subproject issue two + id: 13 + fixed_version_id: + category_id: + description: This is a second issue on a cookbook subproject + tracker_id: 1 + assigned_to_id: + author_id: 2 + status_id: 1 + root_id: 13 + lft: 1 + rgt: 2 +issues_014: + id: 14 + created_on: <%= 15.days.ago.to_s(:db) %> + project_id: 3 + updated_on: <%= 15.days.ago.to_s(:db) %> + priority_id: 5 + subject: Private issue on public project + fixed_version_id: + category_id: + description: This is a private issue + tracker_id: 1 + assigned_to_id: + author_id: 2 + status_id: 1 + is_private: true + root_id: 14 + lft: 1 + rgt: 2 diff --git a/test/fixtures/journal_details.yml b/test/fixtures/journal_details.yml new file mode 100644 index 00000000..77f9318f --- /dev/null +++ b/test/fixtures/journal_details.yml @@ -0,0 +1,43 @@ +--- +journal_details_001: + old_value: "1" + property: attr + id: 1 + value: "2" + prop_key: status_id + journal_id: 1 +journal_details_002: + old_value: "40" + property: attr + id: 2 + value: "30" + prop_key: done_ratio + journal_id: 1 +journal_details_003: + old_value: + property: attr + id: 3 + value: "6" + prop_key: fixed_version_id + journal_id: 4 +journal_details_004: + old_value: "This word was removed and an other was" + property: attr + id: 4 + value: "This word was and an other was added" + prop_key: description + journal_id: 3 +journal_details_005: + old_value: Old value + property: cf + id: 5 + value: New value + prop_key: 2 + journal_id: 3 +journal_details_006: + old_value: + property: attachment + id: 6 + value: 060719210727_picture.jpg + prop_key: 4 + journal_id: 3 diff --git a/test/fixtures/ldap/slapd.conf b/test/fixtures/ldap/slapd.conf new file mode 100644 index 00000000..78c44fb9 --- /dev/null +++ b/test/fixtures/ldap/slapd.conf @@ -0,0 +1,19 @@ +# Sample OpenLDAP configuration file for Redmine LDAP test server +# +ucdata-path ./ucdata +include ./schema/core.schema +include ./schema/cosine.schema +include ./schema/inetorgperson.schema +include ./schema/openldap.schema +include ./schema/nis.schema + +pidfile ./run/slapd.pid +argsfile ./run/slapd.args + +database bdb +suffix "dc=redmine,dc=org" +rootdn "cn=Manager,dc=redmine,dc=org" +rootpw secret +directory ./redmine +# Indices to maintain +index objectClass eq diff --git a/test/fixtures/ldap/test-ldap.ldif b/test/fixtures/ldap/test-ldap.ldif new file mode 100644 index 00000000..f4cc5557 --- /dev/null +++ b/test/fixtures/ldap/test-ldap.ldif @@ -0,0 +1,82 @@ +dn: dc=redmine,dc=org +objectClass: top +objectClass: dcObject +objectClass: organization +o: redmine.org +dc: redmine +structuralObjectClass: organization +entryUUID: 886f5fca-0a87-102e-8d06-67c361d9bd2d +creatorsName: +createTimestamp: 20090721211642Z +entryCSN: 20090721211642.955188Z#000000#000#000000 +modifiersName: +modifyTimestamp: 20090721211642Z + +dn: cn=admin,dc=redmine,dc=org +objectClass: simpleSecurityObject +objectClass: organizationalRole +cn: admin +description: LDAP administrator +userPassword:: e2NyeXB0fWlWTU9DcUt6WWxXRDI= +structuralObjectClass: organizationalRole +entryUUID: 88704e44-0a87-102e-8d07-67c361d9bd2d +creatorsName: +createTimestamp: 20090721211642Z +entryCSN: 20090721211642.961418Z#000000#000#000000 +modifiersName: +modifyTimestamp: 20090721211642Z + +dn: ou=Person,dc=redmine,dc=org +ou: Person +objectClass: top +objectClass: organizationalUnit +structuralObjectClass: organizationalUnit +entryUUID: d39dd388-0c84-102e-82fa-dff86c63a7d6 +creatorsName: cn=admin,dc=redmine,dc=org +createTimestamp: 20090724100222Z +entryCSN: 20090724100222.924226Z#000000#000#000000 +modifiersName: cn=admin,dc=redmine,dc=org +modifyTimestamp: 20090724100222Z + +dn: uid=example1,ou=Person,dc=redmine,dc=org +objectClass: posixAccount +objectClass: top +objectClass: inetOrgPerson +gidNumber: 0 +givenName: Example +sn: One +uid: example1 +homeDirectory: /home/example1 +cn: Example One +structuralObjectClass: inetOrgPerson +entryUUID: 285d304e-0c8a-102e-82fc-dff86c63a7d6 +creatorsName: cn=admin,dc=redmine,dc=org +createTimestamp: 20090724104032Z +uidNumber: 0 +mail: example1@redmine.org +userPassword:: e1NIQX1mRXFOQ2NvM1lxOWg1WlVnbEQzQ1pKVDRsQnM9 +entryCSN: 20090724105945.375801Z#000000#000#000000 +modifiersName: cn=admin,dc=redmine,dc=org +modifyTimestamp: 20090724105945Z + +dn: uid=edavis,ou=Person,dc=redmine,dc=org +objectClass: posixAccount +objectClass: top +objectClass: inetOrgPerson +gidNumber: 0 +givenName: Eric +sn: Davis +uid: edavis +mail: edavis@littlestreamsoftware.com +structuralObjectClass: inetOrgPerson +entryUUID: 9c5f0502-0c8b-102e-82fe-dff86c63a7d6 +creatorsName: cn=admin,dc=redmine,dc=org +createTimestamp: 20090724105056Z +homeDirectory: /home/edavis +cn: Eric Davis +uidNumber: 0 +userPassword:: e1NIQX1mRXFOQ2NvM1lxOWg1WlVnbEQzQ1pKVDRsQnM9 +entryCSN: 20090724105937.734480Z#000000#000#000000 +modifiersName: cn=admin,dc=redmine,dc=org +modifyTimestamp: 20090724105937Z + diff --git a/test/fixtures/mail_handler/apple_mail_with_attachment.eml b/test/fixtures/mail_handler/apple_mail_with_attachment.eml new file mode 100644 index 00000000..1485d7e9 --- /dev/null +++ b/test/fixtures/mail_handler/apple_mail_with_attachment.eml @@ -0,0 +1,240 @@ +From JSmith@somenet.foo Mon Jun 27 06:55:56 2011 +Return-Path: +X-Original-To: redmine@somenet.foo +Delivered-To: redmine@somenet.foo +From: John Smith +Mime-Version: 1.0 (Apple Message framework v1084) +Content-Type: multipart/alternative; boundary=Apple-Mail-3-163265085 +Subject: Test attaching images to tickets by HTML mail +Date: Mon, 27 Jun 2011 16:55:46 +0300 +To: redmine@somenet.foo +Message-Id: <7ABE3636-07E8-47C9-90A1-FCB1AA894DA1@somenet.foo> +X-Mailer: Apple Mail (2.1084) + + +--Apple-Mail-3-163265085 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; + charset=us-ascii + +Yet another test! + + +--Apple-Mail-3-163265085 +Content-Type: multipart/related; + type="text/html"; + boundary=Apple-Mail-4-163265085 + + +--Apple-Mail-4-163265085 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/html; + charset=us-ascii + + +
= + +--Apple-Mail-4-163265085 +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename=paella.jpg +Content-Type: image/jpg; + x-unix-mode=0644; + name="paella.jpg" +Content-Id: <1207F0B5-9F9D-4AB4-B547-AF9033E82111> + +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 +cbis+/WpUqUcMZKdF44n/9k= + +--Apple-Mail-4-163265085-- + +--Apple-Mail-3-163265085-- diff --git a/test/fixtures/mail_handler/fullname_of_sender_as_utf8_encoded.eml b/test/fixtures/mail_handler/fullname_of_sender_as_utf8_encoded.eml new file mode 100644 index 00000000..e8e52063 --- /dev/null +++ b/test/fixtures/mail_handler/fullname_of_sender_as_utf8_encoded.eml @@ -0,0 +1,5 @@ +From: =?utf-8?b?w4TDpCDDlsO2?= +Subject: foo +Content-Type: text/plain; charset=utf-8 + +testing user creation with quoted From-header diff --git a/test/fixtures/mail_handler/gmail_with_attachment_iso-8859-1.eml b/test/fixtures/mail_handler/gmail_with_attachment_iso-8859-1.eml new file mode 100644 index 00000000..4f1f6f34 --- /dev/null +++ b/test/fixtures/mail_handler/gmail_with_attachment_iso-8859-1.eml @@ -0,0 +1,26 @@ +Date: Tue, 20 Nov 2012 23:08:25 +0900 +Message-ID: +Subject: test +From: John Smith +To: redmine@somenet.foo +Content-Type: multipart/mixed; boundary=14dae93a13bf76ca5d04ceedc458 + +--14dae93a13bf76ca5d04ceedc458 +Content-Type: text/plain; charset=ISO-8859-1 + +test + +--14dae93a13bf76ca5d04ceedc458 +Content-Type: text/plain; charset=US-ASCII; + name="=?ISO-8859-1?B?xOTW9tz8xOTW9tz8xOTW9tz8xOTW9tz8xOTW9tw=?= + =?ISO-8859-1?B?/MTk1vbc/MTk1vbc/MTk1vbc/MTk1vbc/MTk1vbc?= + =?ISO-8859-1?B?/MTk1vbc/C50eHQ=?=" +Content-Disposition: attachment; + filename="=?ISO-8859-1?B?xOTW9tz8xOTW9tz8xOTW9tz8xOTW9tz8xOTW9tw=?= + =?ISO-8859-1?B?/MTk1vbc/MTk1vbc/MTk1vbc/MTk1vbc/MTk1vbc?= + =?ISO-8859-1?B?/MTk1vbc/C50eHQ=?=" +Content-Transfer-Encoding: base64 +X-Attachment-Id: f_h9r3mcjz0 + +dGVzdAo= +--14dae93a13bf76ca5d04ceedc458-- diff --git a/test/fixtures/mail_handler/gmail_with_attachment_ja.eml b/test/fixtures/mail_handler/gmail_with_attachment_ja.eml new file mode 100644 index 00000000..8d4e4f33 --- /dev/null +++ b/test/fixtures/mail_handler/gmail_with_attachment_ja.eml @@ -0,0 +1,20 @@ +Date: Mon, 19 Nov 2012 10:17:45 +0900 +Message-ID: +Subject: test +From: John Smith +To: redmine@somenet.foo +Content-Type: multipart/mixed; boundary=bcaec54ee4ea84f77904cecee22e + +--bcaec54ee4ea84f77904cecee22e +Content-Type: text/plain; charset=ISO-8859-1 + +test + +--bcaec54ee4ea84f77904cecee22e +Content-Type: text/plain; charset=US-ASCII; name="=?ISO-2022-JP?B?GyRCJUYlOSVIGyhCLnR4dA==?=" +Content-Disposition: attachment; filename="=?ISO-2022-JP?B?GyRCJUYlOSVIGyhCLnR4dA==?=" +Content-Transfer-Encoding: base64 +X-Attachment-Id: f_h9owndpv0 + +dGVzdAo= +--bcaec54ee4ea84f77904cecee22e-- diff --git a/test/fixtures/mail_handler/issue_update_with_multiple_quoted_reply_above.eml b/test/fixtures/mail_handler/issue_update_with_multiple_quoted_reply_above.eml new file mode 100644 index 00000000..ff8f63fd --- /dev/null +++ b/test/fixtures/mail_handler/issue_update_with_multiple_quoted_reply_above.eml @@ -0,0 +1,48 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +In-Reply-To: +From: "John Smith" +To: +Subject: Re: update to issue 2 +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +An update to the issue by the sender. + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris --- Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +>> > --- Reply above. Do not remove this line. --- +>> > +>> > Issue #6779 has been updated by Eric Davis. +>> > +>> > Subject changed from Projects with JSON to Project JSON API +>> > Status changed from New to Assigned +>> > Assignee set to Eric Davis +>> > Priority changed from Low to Normal +>> > Estimated time deleted (1.00) +>> > +>> > Looks like the JSON api for projects was missed. I'm going to be +>> > reviewing the existing APIs and trying to clean them up over the next +>> > few weeks. diff --git a/test/fixtures/mail_handler/issue_update_with_quoted_reply_above.eml b/test/fixtures/mail_handler/issue_update_with_quoted_reply_above.eml new file mode 100644 index 00000000..848382ca --- /dev/null +++ b/test/fixtures/mail_handler/issue_update_with_quoted_reply_above.eml @@ -0,0 +1,48 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +In-Reply-To: +From: "John Smith" +To: +Subject: Re: update to issue 2 +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +An update to the issue by the sender. + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris --- Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +> --- Reply above. Do not remove this line. --- +> +> Issue #6779 has been updated by Eric Davis. +> +> Subject changed from Projects with JSON to Project JSON API +> Status changed from New to Assigned +> Assignee set to Eric Davis +> Priority changed from Low to Normal +> Estimated time deleted (1.00) +> +> Looks like the JSON api for projects was missed. I'm going to be +> reviewing the existing APIs and trying to clean them up over the next +> few weeks. diff --git a/test/fixtures/mail_handler/japanese_keywords_iso_2022_jp.eml b/test/fixtures/mail_handler/japanese_keywords_iso_2022_jp.eml new file mode 100644 index 00000000..cb4e3ce9 --- /dev/null +++ b/test/fixtures/mail_handler/japanese_keywords_iso_2022_jp.eml @@ -0,0 +1,60 @@ +Message-ID: <001101ca9762$293d68c0$0600a8c0@osiris> +From: "jsmith" +To: +Subject: Japanese Character pattern matching +Date: Sun, 17 Jan 2010 11:45:18 +0100 +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_000E_01CA976A.8AF5E9E0" +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +This is a multi-part message in MIME format. + +------=_NextPart_000_000E_01CA976A.8AF5E9E0 +Content-Type: text/plain; + charset="iso-2022-jp" +Content-Transfer-Encoding: quoted-printable + +It should be noted that I am receiving emails using pop and the patch in = +Issue #2420 but I don't think the problem lies with this. + +When I try and send emails to the redmine server with Japanese = +characters in them it appears to work apart from the pattern matching. + +For example if I send an email with the following keywords. + +Tracker: =1B$B3+H/=1B(B + +------=_NextPart_000_000E_01CA976A.8AF5E9E0 +Content-Type: text/html; + charset="iso-2022-jp" +Content-Transfer-Encoding: quoted-printable + + + + + + + + +
+

It should be noted that I am receiving emails using pop and the patch = +in=20 +Issue #2420 but I don't think = +the=20 +problem lies with this.

+

When I try and send emails to the redmine server with Japanese = +characters in=20 +them it appears to work apart from the pattern matching.

+

For example if I send an email with the following keywords.

+

Tracker: = +=1B$B3+H/=1B(B

+ +------=_NextPart_000_000E_01CA976A.8AF5E9E0-- + diff --git a/test/fixtures/mail_handler/message_reply.eml b/test/fixtures/mail_handler/message_reply.eml new file mode 100644 index 00000000..78e7f110 --- /dev/null +++ b/test/fixtures/mail_handler/message_reply.eml @@ -0,0 +1,15 @@ +Message-ID: <4974C93E.3070005@somenet.foo> +Date: Mon, 19 Jan 2009 19:41:02 +0100 +From: "John Smith" +User-Agent: Thunderbird 2.0.0.19 (Windows/20081209) +MIME-Version: 1.0 +To: redmine@somenet.foo +Subject: Reply via email +References: +In-Reply-To: +Content-Type: text/plain; charset=UTF-8; format=flowed +Content-Transfer-Encoding: 7bit + +This is a reply to a forum message. + + diff --git a/test/fixtures/mail_handler/message_reply_by_subject.eml b/test/fixtures/mail_handler/message_reply_by_subject.eml new file mode 100644 index 00000000..05d65861 --- /dev/null +++ b/test/fixtures/mail_handler/message_reply_by_subject.eml @@ -0,0 +1,13 @@ +Message-ID: <4974C93E.3070005@somenet.foo> +Date: Mon, 19 Jan 2009 19:41:02 +0100 +From: "John Smith" +User-Agent: Thunderbird 2.0.0.19 (Windows/20081209) +MIME-Version: 1.0 +To: redmine@somenet.foo +Subject: Re: [eCookbook - Help board - msg2] Reply to the first post +Content-Type: text/plain; charset=UTF-8; format=flowed +Content-Transfer-Encoding: 7bit + +This is a reply to a forum message. + + diff --git a/test/fixtures/mail_handler/no_subject_header.eml b/test/fixtures/mail_handler/no_subject_header.eml new file mode 100644 index 00000000..465a636b --- /dev/null +++ b/test/fixtures/mail_handler/no_subject_header.eml @@ -0,0 +1,10 @@ +Content-Type: application/ms-tnef; name="winmail.dat" +Content-Transfer-Encoding: binary +From: John Smith +To: "redmine@somenet.foo" +Date: Fri, 1 Jun 2012 14:39:38 +0200 +Message-ID: <87C31D42249DD0489D1A1444E3232DD7019D6183@foo.bar> +Accept-Language: de-CH, en-US +Content-Language: de-CH + +Fixture diff --git a/test/fixtures/mail_handler/subject_as_iso-8859-1.eml b/test/fixtures/mail_handler/subject_as_iso-8859-1.eml new file mode 100644 index 00000000..38699d0f --- /dev/null +++ b/test/fixtures/mail_handler/subject_as_iso-8859-1.eml @@ -0,0 +1,11 @@ +Content-Type: application/ms-tnef; name="winmail.dat" +Content-Transfer-Encoding: binary +From: John Smith +To: "redmine@somenet.foo" +Subject: =?iso-8859-1?Q?Testmail_from_Webmail:_=E4_=F6_=FC...?= +Date: Fri, 1 Jun 2012 14:39:38 +0200 +Message-ID: <87C31D42249DD0489D1A1444E3232DD7019D6183@foo.bar> +Accept-Language: de-CH, en-US +Content-Language: de-CH + +Fixture diff --git a/test/fixtures/mail_handler/subject_japanese_1.eml b/test/fixtures/mail_handler/subject_japanese_1.eml new file mode 100644 index 00000000..8cebfa7b --- /dev/null +++ b/test/fixtures/mail_handler/subject_japanese_1.eml @@ -0,0 +1,7 @@ +From: John Smith +To: "redmine@somenet.foo" +Subject: =?iso-2022-jp?b?GyRCJUYlOSVIGyhCCg=?= +Date: Fri, 1 Jun 2012 14:39:38 +0200 +Message-ID: <87C31D42249DD0489D1A1444E3232DD7019D6183@foo.bar> + +Fixture diff --git a/test/fixtures/mail_handler/subject_japanese_2.eml b/test/fixtures/mail_handler/subject_japanese_2.eml new file mode 100644 index 00000000..7577921d --- /dev/null +++ b/test/fixtures/mail_handler/subject_japanese_2.eml @@ -0,0 +1,7 @@ +From: John Smith +To: "redmine@somenet.foo" +Subject: Re: =?iso-2022-jp?b?GyRCJUYlOSVIGyhCCg=?= +Date: Fri, 1 Jun 2012 14:39:38 +0200 +Message-ID: <87C31D42249DD0489D1A1444E3232DD7019D6183@foo.bar> + +Fixture diff --git a/test/fixtures/mail_handler/thunderbird_with_attachment_iso-8859-1.eml b/test/fixtures/mail_handler/thunderbird_with_attachment_iso-8859-1.eml new file mode 100644 index 00000000..89ad4a90 --- /dev/null +++ b/test/fixtures/mail_handler/thunderbird_with_attachment_iso-8859-1.eml @@ -0,0 +1,34 @@ +Message-ID: <50AB9546.7020800@gmail.com> +Date: Tue, 20 Nov 2012 23:35:50 +0900 +From: John Smith +User-Agent: Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.17) Gecko/20110428 Fedora/3.1.10-1.fc13 Thunderbird/3.1.10 +MIME-Version: 1.0 +To: redmine@somenet.foo +Subject: test +Content-Type: multipart/mixed; + boundary="------------050902080306030406090208" + +This is a multi-part message in MIME format. +--------------050902080306030406090208 +Content-Type: text/plain; charset=ISO-8859-1; format=flowed +Content-Transfer-Encoding: 7bit + +test + +--------------050902080306030406090208 +Content-Type: image/png; + name="=?ISO-8859-1?Q?=C4=E4=D6=F6=DC=FC=C4=E4=D6=F6=DC=FC=C4=E4=D6=F6=DC=FC=C4=E4?= + =?ISO-8859-1?Q?=D6=F6=DC=FC=C4=E4=D6=F6=DC=FC=C4=E4=D6=F6=DC=FC=C4=E4=D6?= + =?ISO-8859-1?Q?=F6=DC=FC=C4=E4=D6=F6=DC=FC=C4=E4=D6=F6=DC=FC=C4=E4=D6=F6?= + =?ISO-8859-1?Q?=DC=FC=C4=E4=D6=F6=DC=FC=2Epng?=" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename*0*=ISO-8859-1''%C4%E4%D6%F6%DC%FC%C4%E4%D6%F6%DC%FC%C4%E4%D6%F6; + filename*1*=%DC%FC%C4%E4%D6%F6%DC%FC%C4%E4%D6%F6%DC%FC%C4%E4%D6%F6%DC%FC; + filename*2*=%C4%E4%D6%F6%DC%FC%C4%E4%D6%F6%DC%FC%C4%E4%D6%F6%DC%FC%C4%E4; + filename*3*=%D6%F6%DC%FC%C4%E4%D6%F6%DC%FC%2E%70%6E%67 + +iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAAAXNSR0IArs4c6QAAAAlwSFlz +AAALEwAACxMBAJqcGAAAAAd0SU1FB9wLFA4fJhRKIUQAAAAUSURBVAjXY/z//z8DEmBiQAWk +8gHq9gMHP8uZWAAAAABJRU5ErkJggg== +--------------050902080306030406090208-- diff --git a/test/fixtures/mail_handler/thunderbird_with_attachment_ja.eml b/test/fixtures/mail_handler/thunderbird_with_attachment_ja.eml new file mode 100644 index 00000000..af7cbdf4 --- /dev/null +++ b/test/fixtures/mail_handler/thunderbird_with_attachment_ja.eml @@ -0,0 +1,26 @@ +Message-ID: <50AA00C6.4070108@gmail.com> +Date: Mon, 19 Nov 2012 18:49:58 +0900 +From: John Smith +User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.15) Gecko/20101027 Fedora/3.0.10-1.fc12 Lightning/1.0b1 Thunderbird/3.0.10 +MIME-Version: 1.0 +To: redmine@somenet.foo +Subject: test +Content-Type: multipart/mixed; + boundary="------------030104060902010800050907" + +This is a multi-part message in MIME format. +--------------030104060902010800050907 +Content-Type: text/plain; charset=ISO-2022-JP +Content-Transfer-Encoding: 7bit + +test + +--------------030104060902010800050907 +Content-Type: text/plain; + name="=?ISO-2022-JP?B?GyRCJUYlOSVIGyhCLnR4dA==?=" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename*=ISO-2022-JP''%1B%24%42%25%46%25%39%25%48%1B%28%42%2E%74%78%74 + +dGVzdAo= +--------------030104060902010800050907-- diff --git a/test/fixtures/mail_handler/ticket_by_empty_user.eml b/test/fixtures/mail_handler/ticket_by_empty_user.eml new file mode 100644 index 00000000..e0d168a7 --- /dev/null +++ b/test/fixtures/mail_handler/ticket_by_empty_user.eml @@ -0,0 +1,17 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +To: +Subject: Ticket by unknown user +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit + +This is a ticket submitted by an unknown user. + diff --git a/test/fixtures/mail_handler/ticket_by_unknown_user.eml b/test/fixtures/mail_handler/ticket_by_unknown_user.eml new file mode 100644 index 00000000..a7abb05e --- /dev/null +++ b/test/fixtures/mail_handler/ticket_by_unknown_user.eml @@ -0,0 +1,18 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "John Doe" +To: +Subject: Ticket by unknown user +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit + +This is a ticket submitted by an unknown user. + diff --git a/test/fixtures/mail_handler/ticket_from_emission_address.eml b/test/fixtures/mail_handler/ticket_from_emission_address.eml new file mode 100644 index 00000000..bf7320ce --- /dev/null +++ b/test/fixtures/mail_handler/ticket_from_emission_address.eml @@ -0,0 +1,19 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "John Doe" +To: +Subject: Ticket with the Redmine emission address +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit + +This is a ticket submitted with the Redmine emission address. +It should be ignored. + diff --git a/test/fixtures/mail_handler/ticket_html_only.eml b/test/fixtures/mail_handler/ticket_html_only.eml new file mode 100644 index 00000000..21b7ae4f --- /dev/null +++ b/test/fixtures/mail_handler/ticket_html_only.eml @@ -0,0 +1,22 @@ +x-sender: +x-receiver: +Received: from [127.0.0.1] ([127.0.0.1]) by somenet.foo with Quick 'n Easy Mail Server SMTP (1.0.0.0); + Sun, 14 Dec 2008 16:18:06 GMT +Message-ID: <494531B9.1070709@somenet.foo> +Date: Sun, 14 Dec 2008 17:18:01 +0100 +From: "John Smith" +User-Agent: Thunderbird 2.0.0.18 (Windows/20081105) +MIME-Version: 1.0 +To: redmine@somenet.foo +Subject: HTML email +Content-Type: text/html; charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit + + + + + + +This is a html-only email.
+ + diff --git a/test/fixtures/mail_handler/ticket_on_given_project.eml b/test/fixtures/mail_handler/ticket_on_given_project.eml new file mode 100644 index 00000000..39790f28 --- /dev/null +++ b/test/fixtures/mail_handler/ticket_on_given_project.eml @@ -0,0 +1,60 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "John Smith" +To: +Subject: New ticket on a given project +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris --- Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +--- This line starts with a delimiter and should not be stripped + +This paragraph is before delimiters. + +BREAK + +This paragraph is between delimiters. + +--- + +This paragraph is after the delimiter so it shouldn't appear. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. + +Project: onlinestore +Status: Resolved +due date: 2010-12-31 +Start Date:2010-01-01 +Assigned to: John Smith +fixed version: alpha +estimated hours: 2.5 +done ratio: 30 + diff --git a/test/fixtures/mail_handler/ticket_reply.eml b/test/fixtures/mail_handler/ticket_reply.eml new file mode 100644 index 00000000..74724ccf --- /dev/null +++ b/test/fixtures/mail_handler/ticket_reply.eml @@ -0,0 +1,74 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sat, 21 Jun 2008 18:41:39 +0200 +Message-ID: <006a01c8d3bd$ad9baec0$0a00a8c0@osiris> +In-Reply-To: +From: "John Smith" +To: +References: <485d0ad366c88_d7014663a025f@osiris.tmail> +Subject: Re: Add ingredients categories +Date: Sat, 21 Jun 2008 18:41:39 +0200 +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_0067_01C8D3CE.711F9CC0" +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +This is a multi-part message in MIME format. + +------=_NextPart_000_0067_01C8D3CE.711F9CC0 +Content-Type: text/plain; + charset="utf-8" +Content-Transfer-Encoding: quoted-printable + +This is reply +------=_NextPart_000_0067_01C8D3CE.711F9CC0 +Content-Type: text/html; + charset="utf-8" +Content-Transfer-Encoding: quoted-printable + +=EF=BB=BF + + + + + + +
This is=20 +reply
+ +------=_NextPart_000_0067_01C8D3CE.711F9CC0-- + diff --git a/test/fixtures/mail_handler/ticket_reply_with_status.eml b/test/fixtures/mail_handler/ticket_reply_with_status.eml new file mode 100644 index 00000000..6a295685 --- /dev/null +++ b/test/fixtures/mail_handler/ticket_reply_with_status.eml @@ -0,0 +1,80 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sat, 21 Jun 2008 18:41:39 +0200 +Message-ID: <006a01c8d3bd$ad9baec0$0a00a8c0@osiris> +From: "John Smith" +To: +References: <485d0ad366c88_d7014663a025f@osiris.tmail> +Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories +Date: Sat, 21 Jun 2008 18:41:39 +0200 +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_0067_01C8D3CE.711F9CC0" +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +This is a multi-part message in MIME format. + +------=_NextPart_000_0067_01C8D3CE.711F9CC0 +Content-Type: text/plain; + charset="utf-8" +Content-Transfer-Encoding: quoted-printable + +This is reply + +Status: Resolved +due date: 2010-12-31 +Start Date:2010-01-01 +Assigned to: jsmith@somenet.foo +float field: 52.6 + +------=_NextPart_000_0067_01C8D3CE.711F9CC0 +Content-Type: text/html; + charset="utf-8" +Content-Transfer-Encoding: quoted-printable + +=EF=BB=BF + + + + + + +
This is=20 +reply Status: Resolved
+ +------=_NextPart_000_0067_01C8D3CE.711F9CC0-- + diff --git a/test/fixtures/mail_handler/ticket_with_attachment.eml b/test/fixtures/mail_handler/ticket_with_attachment.eml new file mode 100644 index 00000000..c85f6b4a --- /dev/null +++ b/test/fixtures/mail_handler/ticket_with_attachment.eml @@ -0,0 +1,248 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sat, 21 Jun 2008 15:53:25 +0200 +Message-ID: <002301c8d3a6$2cdf6950$0a00a8c0@osiris> +From: "John Smith" +To: +Subject: Ticket created by email with attachment +Date: Sat, 21 Jun 2008 15:53:25 +0200 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_NextPart_000_001F_01C8D3B6.F05C5270" +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +This is a multi-part message in MIME format. + +------=_NextPart_000_001F_01C8D3B6.F05C5270 +Content-Type: multipart/alternative; + boundary="----=_NextPart_001_0020_01C8D3B6.F05C5270" + + +------=_NextPart_001_0020_01C8D3B6.F05C5270 +Content-Type: text/plain; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +This is a new ticket with attachments +------=_NextPart_001_0020_01C8D3B6.F05C5270 +Content-Type: text/html; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + + + + + + + +
This is  a new ticket with=20 +attachments
+ +------=_NextPart_001_0020_01C8D3B6.F05C5270-- + +------=_NextPart_000_001F_01C8D3B6.F05C5270 +Content-Type: image/jpeg; + name="Paella.jpg" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="Paella.jpg" + +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 +cbis+/WpUqUcMZKdF44n/9k= + +------=_NextPart_000_001F_01C8D3B6.F05C5270-- + diff --git a/test/fixtures/mail_handler/ticket_with_attributes.eml b/test/fixtures/mail_handler/ticket_with_attributes.eml new file mode 100644 index 00000000..ca8b1970 --- /dev/null +++ b/test/fixtures/mail_handler/ticket_with_attributes.eml @@ -0,0 +1,43 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "John Smith" +To: +Subject: New ticket on a given project +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. + +Project: onlinestore +Tracker: Feature Request +category: stock management +priority: URGENT diff --git a/test/fixtures/mail_handler/ticket_with_cc.eml b/test/fixtures/mail_handler/ticket_with_cc.eml new file mode 100644 index 00000000..a67889d8 --- /dev/null +++ b/test/fixtures/mail_handler/ticket_with_cc.eml @@ -0,0 +1,40 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "John Smith" +To: +Cc: +Subject: New ticket on a given project +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. + diff --git a/test/fixtures/mail_handler/ticket_with_custom_fields.eml b/test/fixtures/mail_handler/ticket_with_custom_fields.eml new file mode 100644 index 00000000..e21e116f --- /dev/null +++ b/test/fixtures/mail_handler/ticket_with_custom_fields.eml @@ -0,0 +1,42 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "John Smith" +To: +Subject: New ticket with custom field values +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. + +category: Stock management +searchable field: Value for a custom field +Database: postgresql diff --git a/test/fixtures/mail_handler/ticket_with_invalid_attributes.eml b/test/fixtures/mail_handler/ticket_with_invalid_attributes.eml new file mode 100644 index 00000000..4506078d --- /dev/null +++ b/test/fixtures/mail_handler/ticket_with_invalid_attributes.eml @@ -0,0 +1,47 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "John Smith" +To: +Subject: New ticket on a given project +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. + +Project: onlinestore +Tracker: Feature request +category: Stock management +assigned to: miscuser9@foo.bar +priority: foo +done ratio: x +start date: some day +due date: never diff --git a/test/fixtures/mail_handler/ticket_with_localized_attributes.eml b/test/fixtures/mail_handler/ticket_with_localized_attributes.eml new file mode 100644 index 00000000..64fabee6 --- /dev/null +++ b/test/fixtures/mail_handler/ticket_with_localized_attributes.eml @@ -0,0 +1,43 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "John Smith" +To: +Subject: New ticket on a given project +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. + +Projet: onlinestore +Tracker: Feature request +catgorie: Stock management +priorit: Urgent diff --git a/test/fixtures/mail_handler/ticket_with_long_subject.eml b/test/fixtures/mail_handler/ticket_with_long_subject.eml new file mode 100644 index 00000000..ee0bfcff --- /dev/null +++ b/test/fixtures/mail_handler/ticket_with_long_subject.eml @@ -0,0 +1,57 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "John Smith" +To: +Subject: New ticket on a given project with a very long subject line which exceeds 255 chars and should not be ignored but chopped off. And if the subject line is still not long enough, we just add more text. And more text. Wow, this is really annoying. Especially, if you have nothing to say... +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris --- Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +--- This line starts with a delimiter and should not be stripped + +This paragraph is before delimiters. + +BREAK + +This paragraph is between delimiters. + +--- + +This paragraph is after the delimiter so it shouldn't appear. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. + +Project: onlinestore +Status: Resolved +due date: 2010-12-31 +Start Date:2010-01-01 +Assigned to: John Smith + diff --git a/test/fixtures/mail_handler/ticket_with_spaces_between_attribute_and_separator.eml b/test/fixtures/mail_handler/ticket_with_spaces_between_attribute_and_separator.eml new file mode 100644 index 00000000..fce1b9a0 --- /dev/null +++ b/test/fixtures/mail_handler/ticket_with_spaces_between_attribute_and_separator.eml @@ -0,0 +1,43 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "John Smith" +To: +Subject: New ticket on a given project +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. + +Project : onlinestore +Tracker: Feature request +category : Stock management +priority: Urgent diff --git a/test/fixtures/mail_handler/ticket_without_from_header.eml b/test/fixtures/mail_handler/ticket_without_from_header.eml new file mode 100644 index 00000000..7b237261 --- /dev/null +++ b/test/fixtures/mail_handler/ticket_without_from_header.eml @@ -0,0 +1,40 @@ +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +To: +Subject: New ticket on a given project +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. + +Project: onlinestore +Status: Resolved + diff --git a/test/fixtures/members.yml b/test/fixtures/members.yml index 5c9f9d3d..10d52f30 100644 --- a/test/fixtures/members.yml +++ b/test/fixtures/members.yml @@ -60,27 +60,3 @@ members_010: project_id: 2 user_id: 8 mail_notification: false -members_011: - id: 115 - user_id: 193 - project_id: 39 - created_on: 2013-09-30 08:11:15.000000000 +08:00 - mail_notification: false -members_012: - id: 124 - user_id: 193 - project_id: 36 - created_on: 2013-09-30 22:37:31.000000000 +08:00 - mail_notification: false -members_013: - id: 140 - user_id: 193 - project_id: 46 - created_on: 2013-10-08 20:52:10.000000000 +08:00 - mail_notification: false -members_014: - id: 242 - user_id: 193 - project_id: 74 - created_on: 2013-10-21 20:47:59.000000000 +08:00 - mail_notification: false diff --git a/test/fixtures/plugins/foo_plugin/_foo_plugin_settings.html.erb b/test/fixtures/plugins/foo_plugin/_foo_plugin_settings.html.erb new file mode 100644 index 00000000..8ff778f2 --- /dev/null +++ b/test/fixtures/plugins/foo_plugin/_foo_plugin_settings.html.erb @@ -0,0 +1 @@ +

<%= text_field_tag 'settings[sample_setting]', @settings['sample_setting'] %>

diff --git a/test/fixtures/projects.yml b/test/fixtures/projects.yml index 6c321dab..0105f935 100644 --- a/test/fixtures/projects.yml +++ b/test/fixtures/projects.yml @@ -71,67 +71,3 @@ projects_006: parent_id: 5 lft: 3 rgt: 4 -projects_007: - id: 39 - name: "信息系统前沿技术" - description: "针对研究生的800级课程" - homepage: '' - is_public: true - parent_id: - created_on: 2013-09-30 08:11:15.000000000 +08:00 - updated_on: 2013-09-30 08:11:15.000000000 +08:00 - identifier: "course2013-09-30_08-11-15" - status: 1 - lft: 81 - rgt: 82 - inherit_members: false - project_type: 1 - hidden_repo: false -projects_008: - id: 36 - name: "软件工程" - description: "针对软件工程专业的高年级本科生和硕士生" - homepage: '' - is_public: true - parent_id: - created_on: 2013-09-27 11:16:11.000000000 +08:00 - updated_on: 2013-09-27 11:16:11.000000000 +08:00 - identifier: "course2013-09-27_11-16-11" - status: 1 - lft: 205 - rgt: 206 - inherit_members: false - project_type: 1 - hidden_repo: false -projects_009: - id: 46 - name: "计算机逻辑学" - description: "研究生课程" - homepage: '' - is_public: true - parent_id: - created_on: 2013-10-08 20:52:10.000000000 +08:00 - updated_on: 2013-10-09 20:29:21.000000000 +08:00 - identifier: "course2013-10-08_20-52-10" - status: 1 - lft: 201 - rgt: 202 - inherit_members: false - project_type: 1 - hidden_repo: false -projects_010: - id: 74 - name: "毛新军老师的研究课题组" - description: '' - homepage: '' - is_public: true - parent_id: - created_on: 2013-10-21 20:47:58.000000000 +08:00 - updated_on: 2014-01-08 09:48:55.000000000 +08:00 - identifier: "course2013-10-21_20-47-58" - status: 1 - lft: 151 - rgt: 152 - inherit_members: false - project_type: 1 - hidden_repo: false diff --git a/test/fixtures/queries.yml b/test/fixtures/queries.yml new file mode 100644 index 00000000..2e91d0ba --- /dev/null +++ b/test/fixtures/queries.yml @@ -0,0 +1,165 @@ +--- +queries_001: + id: 1 + type: IssueQuery + project_id: 1 + is_public: true + name: Multiple custom fields query + filters: | + --- + cf_1: + :values: + - MySQL + :operator: "=" + status_id: + :values: + - "1" + :operator: o + cf_2: + :values: + - "125" + :operator: "=" + + user_id: 1 + column_names: +queries_002: + id: 2 + type: IssueQuery + project_id: 1 + is_public: false + name: Private query for cookbook + filters: | + --- + tracker_id: + :values: + - "3" + :operator: "=" + status_id: + :values: + - "1" + :operator: o + + user_id: 3 + column_names: +queries_003: + id: 3 + type: IssueQuery + project_id: + is_public: false + name: Private query for all projects + filters: | + --- + tracker_id: + :values: + - "3" + :operator: "=" + + user_id: 3 + column_names: +queries_004: + id: 4 + type: IssueQuery + project_id: + is_public: true + name: Public query for all projects + filters: | + --- + tracker_id: + :values: + - "3" + :operator: "=" + + user_id: 2 + column_names: +queries_005: + id: 5 + type: IssueQuery + project_id: + is_public: true + name: Open issues by priority and tracker + filters: | + --- + status_id: + :values: + - "1" + :operator: o + + user_id: 1 + column_names: + sort_criteria: | + --- + - - priority + - desc + - - tracker + - asc +queries_006: + id: 6 + type: IssueQuery + project_id: + is_public: true + name: Open issues grouped by tracker + filters: | + --- + status_id: + :values: + - "1" + :operator: o + + user_id: 1 + column_names: + group_by: tracker + sort_criteria: | + --- + - - priority + - desc +queries_007: + id: 7 + type: IssueQuery + project_id: 2 + is_public: true + name: Public query for project 2 + filters: | + --- + tracker_id: + :values: + - "3" + :operator: "=" + + user_id: 2 + column_names: +queries_008: + id: 8 + type: IssueQuery + project_id: 2 + is_public: false + name: Private query for project 2 + filters: | + --- + tracker_id: + :values: + - "3" + :operator: "=" + + user_id: 2 + column_names: +queries_009: + id: 9 + type: IssueQuery + project_id: + is_public: true + name: Open issues grouped by list custom field + filters: | + --- + status_id: + :values: + - "1" + :operator: o + + user_id: 1 + column_names: + group_by: cf_1 + sort_criteria: | + --- + - - priority + - desc + diff --git a/test/fixtures/repositories.yml b/test/fixtures/repositories.yml new file mode 100644 index 00000000..c3fea303 --- /dev/null +++ b/test/fixtures/repositories.yml @@ -0,0 +1,19 @@ +--- +repositories_001: + project_id: 1 + url: file:///<%= Rails.root %>/tmp/test/subversion_repository + id: 10 + root_url: file:///<%= Rails.root %>/tmp/test/subversion_repository + password: "" + login: "" + type: Repository::Subversion + is_default: true +repositories_002: + project_id: 2 + url: svn://localhost/test + id: 11 + root_url: svn://localhost + password: "" + login: "" + type: Repository::Subversion + is_default: true diff --git a/test/fixtures/repositories/bazaar_repository.tar.gz b/test/fixtures/repositories/bazaar_repository.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..084c84899b609248ebb32451ec89de7151c6ab6a GIT binary patch literal 25056 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
  • *`>}qj1NU(HFh%c(HOr~r0|f( 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 Z3=9km3=9km42-`w{{;gOQ;h(i006{M#a;ja literal 0 HcmV?d00001 diff --git a/test/fixtures/repositories/filesystem_repository.tar.gz b/test/fixtures/repositories/filesystem_repository.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..075d8c72a815b06669b03637867bc61c5ae1064c GIT binary patch literal 479 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 WR#CO4t$zpC3YF-1_)a~4O#crMC$5?R literal 0 HcmV?d00001 diff --git a/test/fixtures/repositories/subversion_repository.dump.gz b/test/fixtures/repositories/subversion_repository.dump.gz new file mode 100644 index 0000000000000000000000000000000000000000..b47542f174432006c3faa6affbe4d45954064f9a GIT binary patch literal 12819 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/time_entries.yml b/test/fixtures/time_entries.yml new file mode 100644 index 00000000..93a971ad --- /dev/null +++ b/test/fixtures/time_entries.yml @@ -0,0 +1,72 @@ +--- +time_entries_001: + created_on: 2007-03-23 12:54:18 +01:00 + tweek: 12 + tmonth: 3 + project_id: 1 + comments: My hours + updated_on: 2007-03-23 12:54:18 +01:00 + activity_id: 9 + spent_on: 2007-03-23 + issue_id: 1 + id: 1 + hours: 4.25 + user_id: 2 + tyear: 2007 +time_entries_002: + created_on: 2007-03-23 14:11:04 +01:00 + tweek: 11 + tmonth: 3 + project_id: 1 + comments: "" + updated_on: 2007-03-23 14:11:04 +01:00 + activity_id: 9 + spent_on: 2007-03-12 + issue_id: 1 + id: 2 + hours: 150.0 + user_id: 1 + tyear: 2007 +time_entries_003: + created_on: 2007-04-21 12:20:48 +02:00 + tweek: 16 + tmonth: 4 + project_id: 1 + comments: "" + updated_on: 2007-04-21 12:20:48 +02:00 + activity_id: 9 + spent_on: 2007-04-21 + issue_id: 3 + id: 3 + hours: 1.0 + user_id: 1 + tyear: 2007 +time_entries_004: + created_on: 2007-04-22 12:20:48 +02:00 + tweek: 16 + tmonth: 4 + project_id: 3 + comments: Time spent on a subproject + updated_on: 2007-04-22 12:20:48 +02:00 + activity_id: 10 + spent_on: 2007-04-22 + issue_id: + id: 4 + hours: 7.65 + user_id: 1 + tyear: 2007 +time_entries_005: + created_on: 2011-03-22 12:20:48 +02:00 + tweek: 12 + tmonth: 3 + project_id: 5 + comments: Time spent on a subproject + updated_on: 2011-03-22 12:20:48 +02:00 + activity_id: 10 + spent_on: 2011-03-22 + issue_id: + id: 5 + hours: 7.65 + user_id: 1 + tyear: 2011 + diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index e03ae482..587199f6 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -1,204 +1,169 @@ ---- -users_004: - created_on: 2006-07-19 19:34:07 +02:00 - status: 1 - last_login_on: - language: en - # password = foo - salt: 3126f764c3c5ac61cbfc103f25f934cf - hashed_password: 9e4dd7eeb172c12a0691a6d9d3a269f7e9fe671b - updated_on: 2006-07-19 19:34:07 +02:00 - admin: false - mail: rhill@somenet.foo - lastname: Hill - firstname: Robert - id: 4 - auth_source_id: - mail_notification: all - login: rhill - type: User -users_001: - created_on: 2006-07-19 19:12:21 +02:00 - status: 1 - last_login_on: 2006-07-19 22:57:52 +02:00 - language: en - # password = admin - salt: 82090c953c4a0000a7db253b0691a6b4 - hashed_password: b5b6ff9543bf1387374cdfa27a54c96d236a7150 - updated_on: 2006-07-19 22:57:52 +02:00 - admin: true - mail: admin@somenet.foo - lastname: Admin - firstname: Redmine - id: 1 - auth_source_id: - mail_notification: all - login: admin - type: User -users_002: - created_on: 2006-07-19 19:32:09 +02:00 - status: 1 - last_login_on: 2006-07-19 22:42:15 +02:00 - language: en - # password = jsmith - salt: 67eb4732624d5a7753dcea7ce0bb7d7d - hashed_password: bfbe06043353a677d0215b26a5800d128d5413bc - updated_on: 2006-07-19 22:42:15 +02:00 - admin: false - mail: jsmith@somenet.foo - lastname: Smith - firstname: John - id: 2 - auth_source_id: - mail_notification: all - login: jsmith - type: User -users_003: - created_on: 2006-07-19 19:33:19 +02:00 - status: 1 - last_login_on: - language: en - # password = foo - salt: 7599f9963ec07b5a3b55b354407120c0 - hashed_password: 8f659c8d7c072f189374edacfa90d6abbc26d8ed - updated_on: 2006-07-19 19:33:19 +02:00 - admin: false - mail: dlopper@somenet.foo - lastname: Lopper - firstname: Dave - id: 3 - auth_source_id: - mail_notification: all - login: dlopper - type: User -users_005: - id: 5 - created_on: 2006-07-19 19:33:19 +02:00 - # Locked - status: 3 - last_login_on: - language: en - hashed_password: 1 - updated_on: 2006-07-19 19:33:19 +02:00 - admin: false - mail: dlopper2@somenet.foo - lastname: Lopper2 - firstname: Dave2 - auth_source_id: - mail_notification: all - login: dlopper2 - type: User -users_006: - id: 6 - created_on: 2006-07-19 19:33:19 +02:00 - status: 0 - last_login_on: - language: '' - hashed_password: 1 - updated_on: 2006-07-19 19:33:19 +02:00 - admin: false - mail: '' - lastname: Anonymous - firstname: '' - auth_source_id: - mail_notification: only_my_events - login: '' - type: AnonymousUser -users_007: - # A user who does not belong to any project - id: 7 - created_on: 2006-07-19 19:33:19 +02:00 - status: 1 - last_login_on: - language: 'en' - # password = foo - salt: 7599f9963ec07b5a3b55b354407120c0 - hashed_password: 8f659c8d7c072f189374edacfa90d6abbc26d8ed - updated_on: 2006-07-19 19:33:19 +02:00 - admin: false - mail: someone@foo.bar - lastname: One - firstname: Some - auth_source_id: - mail_notification: only_my_events - login: someone - type: User -users_008: - id: 8 - created_on: 2006-07-19 19:33:19 +02:00 - status: 1 - last_login_on: - language: 'it' - # password = foo - salt: 7599f9963ec07b5a3b55b354407120c0 - hashed_password: 8f659c8d7c072f189374edacfa90d6abbc26d8ed - updated_on: 2006-07-19 19:33:19 +02:00 - admin: false - mail: miscuser8@foo.bar - lastname: Misc - firstname: User - auth_source_id: - mail_notification: only_my_events - login: miscuser8 - type: User -users_009: - id: 9 - created_on: 2006-07-19 19:33:19 +02:00 - status: 1 - last_login_on: - language: 'it' - hashed_password: 1 - updated_on: 2006-07-19 19:33:19 +02:00 - admin: false - mail: miscuser9@foo.bar - lastname: Misc - firstname: User - auth_source_id: - mail_notification: only_my_events - login: miscuser9 - type: User -groups_010: - id: 10 - lastname: A Team - type: Group -groups_011: - id: 11 - lastname: B Team - type: Group -person_one: - id: 29 - login: "yanxd" - hashed_password: "432257ccaebe6b33158a88b2db2135796505762b" - firstname: "Inc." - lastname: "yan" - mail: "test@hotmail.com" - admin: 0 - status: 1 - last_login_on: "2014-02-17 08:27:52" - language: "zh" - auth_source_id: nil - created_on: "2013-07-11 08:33:38" - updated_on: "2013-10-25 09:37:40" - type: "User" - identity_url: nil - mail_notification: "only_my_events" - salt: "84dc6508506671255b120d28e348f3ad" -person_mao: - id: 193 - login: "xjmao" - hashed_password: "38e4b5d28bb260441dd9a0bd9c33efd3013bbb3e" - firstname: "新军" - lastname: "毛" - mail: "mao.xinjun@gmail.com" - admin: 0 - status: 1 - last_login_on: "2014-01-21 14:26:31" - language: "zh" - auth_source_id: nil - created_on: "2013-09-27 11:08:49" - updated_on: "2013-09-30 07:42:49" - type: "User" - identity_url: nil - mail_notification: "all" - salt: "dbec9ab9065a69022a5f4694ec0d9620" - \ No newline at end of file +--- +users_004: + created_on: 2006-07-19 19:34:07 +02:00 + status: 1 + last_login_on: + language: en + # password = foo + salt: 3126f764c3c5ac61cbfc103f25f934cf + hashed_password: 9e4dd7eeb172c12a0691a6d9d3a269f7e9fe671b + updated_on: 2006-07-19 19:34:07 +02:00 + admin: false + mail: rhill@somenet.foo + lastname: Hill + firstname: Robert + id: 4 + auth_source_id: + mail_notification: all + login: rhill + type: User +users_001: + created_on: 2006-07-19 19:12:21 +02:00 + status: 1 + last_login_on: 2006-07-19 22:57:52 +02:00 + language: en + # password = admin + salt: 82090c953c4a0000a7db253b0691a6b4 + hashed_password: b5b6ff9543bf1387374cdfa27a54c96d236a7150 + updated_on: 2006-07-19 22:57:52 +02:00 + admin: true + mail: admin@somenet.foo + lastname: Admin + firstname: Redmine + id: 1 + auth_source_id: + mail_notification: all + login: admin + type: User +users_002: + created_on: 2006-07-19 19:32:09 +02:00 + status: 1 + last_login_on: 2006-07-19 22:42:15 +02:00 + language: en + # password = jsmith + salt: 67eb4732624d5a7753dcea7ce0bb7d7d + hashed_password: bfbe06043353a677d0215b26a5800d128d5413bc + updated_on: 2006-07-19 22:42:15 +02:00 + admin: false + mail: jsmith@somenet.foo + lastname: Smith + firstname: John + id: 2 + auth_source_id: + mail_notification: all + login: jsmith + type: User +users_003: + created_on: 2006-07-19 19:33:19 +02:00 + status: 1 + last_login_on: + language: en + # password = foo + salt: 7599f9963ec07b5a3b55b354407120c0 + hashed_password: 8f659c8d7c072f189374edacfa90d6abbc26d8ed + updated_on: 2006-07-19 19:33:19 +02:00 + admin: false + mail: dlopper@somenet.foo + lastname: Lopper + firstname: Dave + id: 3 + auth_source_id: + mail_notification: all + login: dlopper + type: User +users_005: + id: 5 + created_on: 2006-07-19 19:33:19 +02:00 + # Locked + status: 3 + last_login_on: + language: en + hashed_password: 1 + updated_on: 2006-07-19 19:33:19 +02:00 + admin: false + mail: dlopper2@somenet.foo + lastname: Lopper2 + firstname: Dave2 + auth_source_id: + mail_notification: all + login: dlopper2 + type: User +users_006: + id: 6 + created_on: 2006-07-19 19:33:19 +02:00 + status: 0 + last_login_on: + language: '' + hashed_password: 1 + updated_on: 2006-07-19 19:33:19 +02:00 + admin: false + mail: '' + lastname: Anonymous + firstname: '' + auth_source_id: + mail_notification: only_my_events + login: '' + type: AnonymousUser +users_007: + # A user who does not belong to any project + id: 7 + created_on: 2006-07-19 19:33:19 +02:00 + status: 1 + last_login_on: + language: 'en' + # password = foo + salt: 7599f9963ec07b5a3b55b354407120c0 + hashed_password: 8f659c8d7c072f189374edacfa90d6abbc26d8ed + updated_on: 2006-07-19 19:33:19 +02:00 + admin: false + mail: someone@foo.bar + lastname: One + firstname: Some + auth_source_id: + mail_notification: only_my_events + login: someone + type: User +users_008: + id: 8 + created_on: 2006-07-19 19:33:19 +02:00 + status: 1 + last_login_on: + language: 'it' + # password = foo + salt: 7599f9963ec07b5a3b55b354407120c0 + hashed_password: 8f659c8d7c072f189374edacfa90d6abbc26d8ed + updated_on: 2006-07-19 19:33:19 +02:00 + admin: false + mail: miscuser8@foo.bar + lastname: Misc + firstname: User + auth_source_id: + mail_notification: only_my_events + login: miscuser8 + type: User +users_009: + id: 9 + created_on: 2006-07-19 19:33:19 +02:00 + status: 1 + last_login_on: + language: 'it' + hashed_password: 1 + updated_on: 2006-07-19 19:33:19 +02:00 + admin: false + mail: miscuser9@foo.bar + lastname: Misc + firstname: User + auth_source_id: + mail_notification: only_my_events + login: miscuser9 + type: User +groups_010: + id: 10 + lastname: A Team + type: Group +groups_011: + id: 11 + lastname: B Team + type: Group + + diff --git a/test/fixtures/versions.yml b/test/fixtures/versions.yml new file mode 100644 index 00000000..856cb5da --- /dev/null +++ b/test/fixtures/versions.yml @@ -0,0 +1,71 @@ +--- +versions_001: + created_on: 2006-07-19 21:00:07 +02:00 + name: "0.1" + project_id: 1 + updated_on: 2006-07-19 21:00:07 +02:00 + id: 1 + description: Beta + effective_date: 2006-07-01 + status: closed + sharing: 'none' +versions_002: + created_on: 2006-07-19 21:00:33 +02:00 + name: "1.0" + project_id: 1 + updated_on: 2006-07-19 21:00:33 +02:00 + id: 2 + description: Stable release + effective_date: <%= 20.day.from_now.to_date.to_s(:db) %> + status: locked + sharing: 'none' +versions_003: + created_on: 2006-07-19 21:00:33 +02:00 + name: "2.0" + project_id: 1 + updated_on: 2006-07-19 21:00:33 +02:00 + id: 3 + description: Future version + effective_date: + status: open + sharing: 'none' +versions_004: + created_on: 2006-07-19 21:00:33 +02:00 + name: "2.0" + project_id: 3 + updated_on: 2006-07-19 21:00:33 +02:00 + id: 4 + description: Future version on subproject + effective_date: + status: open + sharing: 'tree' +versions_005: + created_on: 2006-07-19 21:00:07 +02:00 + name: "Alpha" + project_id: 2 + updated_on: 2006-07-19 21:00:07 +02:00 + id: 5 + description: Private Alpha + effective_date: 2006-07-01 + status: open + sharing: 'none' +versions_006: + created_on: 2006-07-19 21:00:07 +02:00 + name: "Private Version of public subproject" + project_id: 5 + updated_on: 2006-07-19 21:00:07 +02:00 + id: 6 + description: "Should be done any day now..." + effective_date: + status: open + sharing: 'tree' +versions_007: + created_on: 2006-07-19 21:00:07 +02:00 + name: "Systemwide visible version" + project_id: 2 + updated_on: 2006-07-19 21:00:07 +02:00 + id: 7 + description: + effective_date: + status: open + sharing: 'system' diff --git a/test/fixtures/wiki_content_versions.yml b/test/fixtures/wiki_content_versions.yml new file mode 100644 index 00000000..38b9e608 --- /dev/null +++ b/test/fixtures/wiki_content_versions.yml @@ -0,0 +1,116 @@ +--- +wiki_content_versions_001: + updated_on: 2007-03-07 00:08:07 +01:00 + page_id: 1 + id: 1 + version: 1 + author_id: 2 + comments: Page creation + wiki_content_id: 1 + compression: "" + data: |- + h1. CookBook documentation + + + + Some [[documentation]] here... +wiki_content_versions_002: + updated_on: 2007-03-07 00:08:34 +01:00 + page_id: 1 + id: 2 + version: 2 + author_id: 1 + comments: Small update + wiki_content_id: 1 + compression: "" + data: |- + h1. CookBook documentation + + + + Some updated [[documentation]] here... +wiki_content_versions_003: + updated_on: 2007-03-07 00:10:51 +01:00 + page_id: 1 + id: 3 + version: 3 + author_id: 1 + comments: "" + wiki_content_id: 1 + compression: "" + data: |- + h1. CookBook documentation + Some updated [[documentation]] here... +wiki_content_versions_004: + data: |- + h1. Another page + + This is a link to a ticket: #2 + updated_on: 2007-03-08 00:18:07 +01:00 + page_id: 2 + wiki_content_id: 2 + id: 4 + version: 1 + author_id: 1 + comments: +wiki_content_versions_005: + data: |- + h1. Title + + Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero. + + h2. Heading 1 + + @WHATEVER@ + + Maecenas sed elit sit amet mi accumsan vestibulum non nec velit. Proin porta tincidunt lorem, consequat rhoncus dolor fermentum in. + + Cras ipsum felis, ultrices at porttitor vel, faucibus eu nunc. + + h2. Heading 2 + + Morbi facilisis accumsan orci non pharetra. + updated_on: 2007-03-08 00:16:07 +01:00 + page_id: 11 + wiki_content_id: 11 + id: 5 + version: 2 + author_id: 1 + comments: +wiki_content_versions_006: + data: |- + h1. Title + + Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero. + + h2. Heading 1 + + @WHATEVER@ + + Maecenas sed elit sit amet mi accumsan vestibulum non nec velit. Proin porta tincidunt lorem, consequat rhoncus dolor fermentum in. + + h2. Heading 2 + + Morbi facilisis accumsan orci non pharetra. + updated_on: 2007-03-08 00:18:07 +01:00 + page_id: 11 + wiki_content_id: 11 + id: 6 + version: 3 + author_id: 1 + comments: +wiki_content_versions_007: + data: |- + h1. Page with an inline image + + This is an inline image: + + !logo.gif! + updated_on: 2007-03-08 00:18:07 +01:00 + page_id: 4 + wiki_content_id: 4 + id: 7 + version: 1 + author_id: 1 + comments: + diff --git a/test/fixtures/wiki_contents.yml b/test/fixtures/wiki_contents.yml new file mode 100644 index 00000000..a4d4de4b --- /dev/null +++ b/test/fixtures/wiki_contents.yml @@ -0,0 +1,136 @@ +--- +wiki_contents_001: + text: |- + h1. CookBook documentation + + {{child_pages}} + + Some updated [[documentation]] here with gzipped history + updated_on: 2007-03-07 00:10:51 +01:00 + page_id: 1 + id: 1 + version: 3 + author_id: 1 + comments: Gzip compression activated +wiki_contents_002: + text: |- + h1. Another page + + This is a link to a ticket: #2 + And this is an included page: + {{include(Page with an inline image)}} + updated_on: 2007-03-08 00:18:07 +01:00 + page_id: 2 + id: 2 + version: 1 + author_id: 1 + comments: +wiki_contents_003: + text: |- + h1. Start page + + E-commerce web site start page + updated_on: 2007-03-08 00:18:07 +01:00 + page_id: 3 + id: 3 + version: 1 + author_id: 1 + comments: +wiki_contents_004: + text: |- + h1. Page with an inline image + + This is an inline image: + + !logo.gif! + updated_on: 2007-03-08 00:18:07 +01:00 + page_id: 4 + id: 4 + version: 1 + author_id: 1 + comments: +wiki_contents_005: + text: |- + h1. Child page 1 + + This is a child page + updated_on: 2007-03-08 00:18:07 +01:00 + page_id: 5 + id: 5 + version: 1 + author_id: 1 + comments: +wiki_contents_006: + text: |- + h1. Child page 2 + + This is a child page + updated_on: 2007-03-08 00:18:07 +01:00 + page_id: 6 + id: 6 + version: 1 + author_id: 1 + comments: +wiki_contents_007: + text: This is a child page + updated_on: 2007-03-08 00:18:07 +01:00 + page_id: 7 + id: 7 + version: 1 + author_id: 1 + comments: +wiki_contents_008: + text: This is a parent page + updated_on: 2007-03-08 00:18:07 +01:00 + page_id: 8 + id: 8 + version: 1 + author_id: 1 + comments: +wiki_contents_009: + text: This is a child page + updated_on: 2007-03-08 00:18:07 +01:00 + page_id: 9 + id: 9 + version: 1 + author_id: 1 + comments: +wiki_contents_010: + text: Page with cyrillic title + updated_on: 2007-03-08 00:18:07 +01:00 + page_id: 10 + id: 10 + version: 1 + author_id: 1 + comments: +wiki_contents_011: + text: |- + h1. Title + + Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero. + + h2. Heading 1 + + @WHATEVER@ + + Maecenas sed elit sit amet mi accumsan vestibulum non nec velit. Proin porta tincidunt lorem, consequat rhoncus dolor fermentum in. + + Cras ipsum felis, ultrices at porttitor vel, faucibus eu nunc. + + h2. Heading 2 + + Morbi facilisis accumsan orci non pharetra. + updated_on: 2007-03-08 00:18:07 +01:00 + page_id: 11 + id: 11 + version: 3 + author_id: 1 + comments: +wiki_contents_012: + text: This is a grandchild page + updated_on: 2007-03-08 00:18:07 +01:00 + page_id: 12 + id: 12 + version: 1 + author_id: 1 + comments: diff --git a/test/fixtures/wiki_pages.yml b/test/fixtures/wiki_pages.yml new file mode 100644 index 00000000..163fea4a --- /dev/null +++ b/test/fixtures/wiki_pages.yml @@ -0,0 +1,85 @@ +--- +wiki_pages_001: + created_on: 2007-03-07 00:08:07 +01:00 + title: CookBook_documentation + id: 1 + wiki_id: 1 + protected: true + parent_id: +wiki_pages_002: + created_on: 2007-03-08 00:18:07 +01:00 + title: Another_page + id: 2 + wiki_id: 1 + protected: false + parent_id: +wiki_pages_003: + created_on: 2007-03-08 00:18:07 +01:00 + title: Start_page + id: 3 + wiki_id: 2 + protected: false + parent_id: +wiki_pages_004: + created_on: 2007-03-08 00:18:07 +01:00 + title: Page_with_an_inline_image + id: 4 + wiki_id: 1 + protected: false + parent_id: 1 +wiki_pages_005: + created_on: 2007-03-08 00:18:07 +01:00 + title: Child_1 + id: 5 + wiki_id: 1 + protected: false + parent_id: 2 +wiki_pages_006: + created_on: 2007-03-08 00:18:07 +01:00 + title: Child_2 + id: 6 + wiki_id: 1 + protected: false + parent_id: 2 +wiki_pages_007: + created_on: 2007-03-08 00:18:07 +01:00 + title: Child_page_1 + id: 7 + wiki_id: 2 + protected: false + parent_id: 8 +wiki_pages_008: + created_on: 2007-03-08 00:18:07 +01:00 + title: Parent_page + id: 8 + wiki_id: 2 + protected: false + parent_id: +wiki_pages_009: + created_on: 2007-03-08 00:18:07 +01:00 + title: Child_page_2 + id: 9 + wiki_id: 2 + protected: false + parent_id: 8 +wiki_pages_010: + created_on: 2007-03-08 00:18:07 +01:00 + title: Этика_менеджмента + id: 10 + wiki_id: 1 + protected: false + parent_id: +wiki_pages_011: + created_on: 2007-03-08 00:18:07 +01:00 + title: Page_with_sections + id: 11 + wiki_id: 1 + protected: false + parent_id: +wiki_pages_012: + created_on: 2007-03-08 00:18:07 +01:00 + title: Child_1_1 + id: 12 + wiki_id: 1 + protected: false + parent_id: 5 diff --git a/test/fixtures/wikis.yml b/test/fixtures/wikis.yml new file mode 100644 index 00000000..bfa7c81c --- /dev/null +++ b/test/fixtures/wikis.yml @@ -0,0 +1,11 @@ +--- +wikis_001: + status: 1 + start_page: CookBook documentation + project_id: 1 + id: 1 +wikis_002: + status: 1 + start_page: Start page + project_id: 2 + id: 2 diff --git a/test/fixtures/workflows.yml b/test/fixtures/workflows.yml new file mode 100644 index 00000000..1ef5a4f0 --- /dev/null +++ b/test/fixtures/workflows.yml @@ -0,0 +1,1884 @@ +--- +WorkflowTransitions_189: + new_status_id: 5 + role_id: 1 + old_status_id: 2 + id: 189 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_001: + new_status_id: 2 + role_id: 1 + old_status_id: 1 + id: 1 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_002: + new_status_id: 3 + role_id: 1 + old_status_id: 1 + id: 2 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_003: + new_status_id: 4 + role_id: 1 + old_status_id: 1 + id: 3 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_110: + new_status_id: 6 + role_id: 1 + old_status_id: 4 + id: 110 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_004: + new_status_id: 5 + role_id: 1 + old_status_id: 1 + id: 4 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_030: + new_status_id: 5 + role_id: 1 + old_status_id: 6 + id: 30 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_111: + new_status_id: 1 + role_id: 1 + old_status_id: 5 + id: 111 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_005: + new_status_id: 6 + role_id: 1 + old_status_id: 1 + id: 5 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_031: + new_status_id: 2 + role_id: 2 + old_status_id: 1 + id: 31 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_112: + new_status_id: 2 + role_id: 1 + old_status_id: 5 + id: 112 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_006: + new_status_id: 1 + role_id: 1 + old_status_id: 2 + id: 6 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_032: + new_status_id: 3 + role_id: 2 + old_status_id: 1 + id: 32 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_113: + new_status_id: 3 + role_id: 1 + old_status_id: 5 + id: 113 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_220: + new_status_id: 6 + role_id: 2 + old_status_id: 2 + id: 220 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_007: + new_status_id: 3 + role_id: 1 + old_status_id: 2 + id: 7 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_033: + new_status_id: 4 + role_id: 2 + old_status_id: 1 + id: 33 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_060: + new_status_id: 5 + role_id: 2 + old_status_id: 6 + id: 60 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_114: + new_status_id: 4 + role_id: 1 + old_status_id: 5 + id: 114 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_140: + new_status_id: 6 + role_id: 2 + old_status_id: 4 + id: 140 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_221: + new_status_id: 1 + role_id: 2 + old_status_id: 3 + id: 221 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_008: + new_status_id: 4 + role_id: 1 + old_status_id: 2 + id: 8 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_034: + new_status_id: 5 + role_id: 2 + old_status_id: 1 + id: 34 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_115: + new_status_id: 6 + role_id: 1 + old_status_id: 5 + id: 115 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_141: + new_status_id: 1 + role_id: 2 + old_status_id: 5 + id: 141 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_222: + new_status_id: 2 + role_id: 2 + old_status_id: 3 + id: 222 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_223: + new_status_id: 4 + role_id: 2 + old_status_id: 3 + id: 223 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_009: + new_status_id: 5 + role_id: 1 + old_status_id: 2 + id: 9 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_035: + new_status_id: 6 + role_id: 2 + old_status_id: 1 + id: 35 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_061: + new_status_id: 2 + role_id: 3 + old_status_id: 1 + id: 61 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_116: + new_status_id: 1 + role_id: 1 + old_status_id: 6 + id: 116 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_142: + new_status_id: 2 + role_id: 2 + old_status_id: 5 + id: 142 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_250: + new_status_id: 6 + role_id: 3 + old_status_id: 2 + id: 250 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_224: + new_status_id: 5 + role_id: 2 + old_status_id: 3 + id: 224 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_036: + new_status_id: 1 + role_id: 2 + old_status_id: 2 + id: 36 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_062: + new_status_id: 3 + role_id: 3 + old_status_id: 1 + id: 62 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_117: + new_status_id: 2 + role_id: 1 + old_status_id: 6 + id: 117 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_143: + new_status_id: 3 + role_id: 2 + old_status_id: 5 + id: 143 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_170: + new_status_id: 6 + role_id: 3 + old_status_id: 4 + id: 170 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_251: + new_status_id: 1 + role_id: 3 + old_status_id: 3 + id: 251 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_225: + new_status_id: 6 + role_id: 2 + old_status_id: 3 + id: 225 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_063: + new_status_id: 4 + role_id: 3 + old_status_id: 1 + id: 63 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_090: + new_status_id: 5 + role_id: 3 + old_status_id: 6 + id: 90 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_118: + new_status_id: 3 + role_id: 1 + old_status_id: 6 + id: 118 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_144: + new_status_id: 4 + role_id: 2 + old_status_id: 5 + id: 144 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_252: + new_status_id: 2 + role_id: 3 + old_status_id: 3 + id: 252 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_226: + new_status_id: 1 + role_id: 2 + old_status_id: 4 + id: 226 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_038: + new_status_id: 4 + role_id: 2 + old_status_id: 2 + id: 38 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_064: + new_status_id: 5 + role_id: 3 + old_status_id: 1 + id: 64 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_091: + new_status_id: 2 + role_id: 1 + old_status_id: 1 + id: 91 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_119: + new_status_id: 4 + role_id: 1 + old_status_id: 6 + id: 119 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_145: + new_status_id: 6 + role_id: 2 + old_status_id: 5 + id: 145 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_171: + new_status_id: 1 + role_id: 3 + old_status_id: 5 + id: 171 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_253: + new_status_id: 4 + role_id: 3 + old_status_id: 3 + id: 253 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_227: + new_status_id: 2 + role_id: 2 + old_status_id: 4 + id: 227 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_039: + new_status_id: 5 + role_id: 2 + old_status_id: 2 + id: 39 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_065: + new_status_id: 6 + role_id: 3 + old_status_id: 1 + id: 65 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_092: + new_status_id: 3 + role_id: 1 + old_status_id: 1 + id: 92 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_146: + new_status_id: 1 + role_id: 2 + old_status_id: 6 + id: 146 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_172: + new_status_id: 2 + role_id: 3 + old_status_id: 5 + id: 172 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_254: + new_status_id: 5 + role_id: 3 + old_status_id: 3 + id: 254 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_228: + new_status_id: 3 + role_id: 2 + old_status_id: 4 + id: 228 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_066: + new_status_id: 1 + role_id: 3 + old_status_id: 2 + id: 66 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_093: + new_status_id: 4 + role_id: 1 + old_status_id: 1 + id: 93 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_147: + new_status_id: 2 + role_id: 2 + old_status_id: 6 + id: 147 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_173: + new_status_id: 3 + role_id: 3 + old_status_id: 5 + id: 173 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_255: + new_status_id: 6 + role_id: 3 + old_status_id: 3 + id: 255 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_229: + new_status_id: 5 + role_id: 2 + old_status_id: 4 + id: 229 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_067: + new_status_id: 3 + role_id: 3 + old_status_id: 2 + id: 67 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_148: + new_status_id: 3 + role_id: 2 + old_status_id: 6 + id: 148 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_174: + new_status_id: 4 + role_id: 3 + old_status_id: 5 + id: 174 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_256: + new_status_id: 1 + role_id: 3 + old_status_id: 4 + id: 256 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_068: + new_status_id: 4 + role_id: 3 + old_status_id: 2 + id: 68 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_094: + new_status_id: 5 + role_id: 1 + old_status_id: 1 + id: 94 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_149: + new_status_id: 4 + role_id: 2 + old_status_id: 6 + id: 149 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_175: + new_status_id: 6 + role_id: 3 + old_status_id: 5 + id: 175 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_257: + new_status_id: 2 + role_id: 3 + old_status_id: 4 + id: 257 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_069: + new_status_id: 5 + role_id: 3 + old_status_id: 2 + id: 69 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_095: + new_status_id: 6 + role_id: 1 + old_status_id: 1 + id: 95 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_176: + new_status_id: 1 + role_id: 3 + old_status_id: 6 + id: 176 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_258: + new_status_id: 3 + role_id: 3 + old_status_id: 4 + id: 258 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_096: + new_status_id: 1 + role_id: 1 + old_status_id: 2 + id: 96 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_177: + new_status_id: 2 + role_id: 3 + old_status_id: 6 + id: 177 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_259: + new_status_id: 5 + role_id: 3 + old_status_id: 4 + id: 259 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_097: + new_status_id: 3 + role_id: 1 + old_status_id: 2 + id: 97 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_178: + new_status_id: 3 + role_id: 3 + old_status_id: 6 + id: 178 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_098: + new_status_id: 4 + role_id: 1 + old_status_id: 2 + id: 98 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_179: + new_status_id: 4 + role_id: 3 + old_status_id: 6 + id: 179 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_099: + new_status_id: 5 + role_id: 1 + old_status_id: 2 + id: 99 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_100: + new_status_id: 6 + role_id: 1 + old_status_id: 2 + id: 100 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_020: + new_status_id: 6 + role_id: 1 + old_status_id: 4 + id: 20 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_101: + new_status_id: 1 + role_id: 1 + old_status_id: 3 + id: 101 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_021: + new_status_id: 1 + role_id: 1 + old_status_id: 5 + id: 21 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_102: + new_status_id: 2 + role_id: 1 + old_status_id: 3 + id: 102 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_210: + new_status_id: 5 + role_id: 1 + old_status_id: 6 + id: 210 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_022: + new_status_id: 2 + role_id: 1 + old_status_id: 5 + id: 22 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_103: + new_status_id: 4 + role_id: 1 + old_status_id: 3 + id: 103 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_023: + new_status_id: 3 + role_id: 1 + old_status_id: 5 + id: 23 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_104: + new_status_id: 5 + role_id: 1 + old_status_id: 3 + id: 104 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_130: + new_status_id: 6 + role_id: 2 + old_status_id: 2 + id: 130 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_211: + new_status_id: 2 + role_id: 2 + old_status_id: 1 + id: 211 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_024: + new_status_id: 4 + role_id: 1 + old_status_id: 5 + id: 24 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_050: + new_status_id: 6 + role_id: 2 + old_status_id: 4 + id: 50 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_105: + new_status_id: 6 + role_id: 1 + old_status_id: 3 + id: 105 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_131: + new_status_id: 1 + role_id: 2 + old_status_id: 3 + id: 131 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_212: + new_status_id: 3 + role_id: 2 + old_status_id: 1 + id: 212 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_025: + new_status_id: 6 + role_id: 1 + old_status_id: 5 + id: 25 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_051: + new_status_id: 1 + role_id: 2 + old_status_id: 5 + id: 51 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_106: + new_status_id: 1 + role_id: 1 + old_status_id: 4 + id: 106 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_132: + new_status_id: 2 + role_id: 2 + old_status_id: 3 + id: 132 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_213: + new_status_id: 4 + role_id: 2 + old_status_id: 1 + id: 213 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_240: + new_status_id: 5 + role_id: 2 + old_status_id: 6 + id: 240 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_026: + new_status_id: 1 + role_id: 1 + old_status_id: 6 + id: 26 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_052: + new_status_id: 2 + role_id: 2 + old_status_id: 5 + id: 52 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_107: + new_status_id: 2 + role_id: 1 + old_status_id: 4 + id: 107 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_133: + new_status_id: 4 + role_id: 2 + old_status_id: 3 + id: 133 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_214: + new_status_id: 5 + role_id: 2 + old_status_id: 1 + id: 214 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_241: + new_status_id: 2 + role_id: 3 + old_status_id: 1 + id: 241 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_027: + new_status_id: 2 + role_id: 1 + old_status_id: 6 + id: 27 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_053: + new_status_id: 3 + role_id: 2 + old_status_id: 5 + id: 53 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_080: + new_status_id: 6 + role_id: 3 + old_status_id: 4 + id: 80 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_108: + new_status_id: 3 + role_id: 1 + old_status_id: 4 + id: 108 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_134: + new_status_id: 5 + role_id: 2 + old_status_id: 3 + id: 134 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_160: + new_status_id: 6 + role_id: 3 + old_status_id: 2 + id: 160 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_215: + new_status_id: 6 + role_id: 2 + old_status_id: 1 + id: 215 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_242: + new_status_id: 3 + role_id: 3 + old_status_id: 1 + id: 242 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_028: + new_status_id: 3 + role_id: 1 + old_status_id: 6 + id: 28 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_054: + new_status_id: 4 + role_id: 2 + old_status_id: 5 + id: 54 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_081: + new_status_id: 1 + role_id: 3 + old_status_id: 5 + id: 81 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_109: + new_status_id: 5 + role_id: 1 + old_status_id: 4 + id: 109 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_135: + new_status_id: 6 + role_id: 2 + old_status_id: 3 + id: 135 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_161: + new_status_id: 1 + role_id: 3 + old_status_id: 3 + id: 161 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_216: + new_status_id: 1 + role_id: 2 + old_status_id: 2 + id: 216 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_243: + new_status_id: 4 + role_id: 3 + old_status_id: 1 + id: 243 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_029: + new_status_id: 4 + role_id: 1 + old_status_id: 6 + id: 29 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_055: + new_status_id: 6 + role_id: 2 + old_status_id: 5 + id: 55 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_082: + new_status_id: 2 + role_id: 3 + old_status_id: 5 + id: 82 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_136: + new_status_id: 1 + role_id: 2 + old_status_id: 4 + id: 136 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_162: + new_status_id: 2 + role_id: 3 + old_status_id: 3 + id: 162 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_217: + new_status_id: 3 + role_id: 2 + old_status_id: 2 + id: 217 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_270: + new_status_id: 5 + role_id: 3 + old_status_id: 6 + id: 270 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_244: + new_status_id: 5 + role_id: 3 + old_status_id: 1 + id: 244 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_056: + new_status_id: 1 + role_id: 2 + old_status_id: 6 + id: 56 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_137: + new_status_id: 2 + role_id: 2 + old_status_id: 4 + id: 137 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_163: + new_status_id: 4 + role_id: 3 + old_status_id: 3 + id: 163 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_190: + new_status_id: 6 + role_id: 1 + old_status_id: 2 + id: 190 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_218: + new_status_id: 4 + role_id: 2 + old_status_id: 2 + id: 218 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_245: + new_status_id: 6 + role_id: 3 + old_status_id: 1 + id: 245 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_057: + new_status_id: 2 + role_id: 2 + old_status_id: 6 + id: 57 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_083: + new_status_id: 3 + role_id: 3 + old_status_id: 5 + id: 83 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_138: + new_status_id: 3 + role_id: 2 + old_status_id: 4 + id: 138 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_164: + new_status_id: 5 + role_id: 3 + old_status_id: 3 + id: 164 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_191: + new_status_id: 1 + role_id: 1 + old_status_id: 3 + id: 191 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_219: + new_status_id: 5 + role_id: 2 + old_status_id: 2 + id: 219 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_246: + new_status_id: 1 + role_id: 3 + old_status_id: 2 + id: 246 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_058: + new_status_id: 3 + role_id: 2 + old_status_id: 6 + id: 58 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_084: + new_status_id: 4 + role_id: 3 + old_status_id: 5 + id: 84 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_139: + new_status_id: 5 + role_id: 2 + old_status_id: 4 + id: 139 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_165: + new_status_id: 6 + role_id: 3 + old_status_id: 3 + id: 165 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_192: + new_status_id: 2 + role_id: 1 + old_status_id: 3 + id: 192 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_247: + new_status_id: 3 + role_id: 3 + old_status_id: 2 + id: 247 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_059: + new_status_id: 4 + role_id: 2 + old_status_id: 6 + id: 59 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_085: + new_status_id: 6 + role_id: 3 + old_status_id: 5 + id: 85 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_166: + new_status_id: 1 + role_id: 3 + old_status_id: 4 + id: 166 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_248: + new_status_id: 4 + role_id: 3 + old_status_id: 2 + id: 248 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_086: + new_status_id: 1 + role_id: 3 + old_status_id: 6 + id: 86 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_167: + new_status_id: 2 + role_id: 3 + old_status_id: 4 + id: 167 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_193: + new_status_id: 4 + role_id: 1 + old_status_id: 3 + id: 193 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_249: + new_status_id: 5 + role_id: 3 + old_status_id: 2 + id: 249 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_087: + new_status_id: 2 + role_id: 3 + old_status_id: 6 + id: 87 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_168: + new_status_id: 3 + role_id: 3 + old_status_id: 4 + id: 168 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_194: + new_status_id: 5 + role_id: 1 + old_status_id: 3 + id: 194 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_088: + new_status_id: 3 + role_id: 3 + old_status_id: 6 + id: 88 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_169: + new_status_id: 5 + role_id: 3 + old_status_id: 4 + id: 169 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_195: + new_status_id: 6 + role_id: 1 + old_status_id: 3 + id: 195 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_089: + new_status_id: 4 + role_id: 3 + old_status_id: 6 + id: 89 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_196: + new_status_id: 1 + role_id: 1 + old_status_id: 4 + id: 196 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_197: + new_status_id: 2 + role_id: 1 + old_status_id: 4 + id: 197 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_198: + new_status_id: 3 + role_id: 1 + old_status_id: 4 + id: 198 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_199: + new_status_id: 5 + role_id: 1 + old_status_id: 4 + id: 199 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_010: + new_status_id: 6 + role_id: 1 + old_status_id: 2 + id: 10 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_011: + new_status_id: 1 + role_id: 1 + old_status_id: 3 + id: 11 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_012: + new_status_id: 2 + role_id: 1 + old_status_id: 3 + id: 12 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_200: + new_status_id: 6 + role_id: 1 + old_status_id: 4 + id: 200 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_013: + new_status_id: 4 + role_id: 1 + old_status_id: 3 + id: 13 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_120: + new_status_id: 5 + role_id: 1 + old_status_id: 6 + id: 120 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_201: + new_status_id: 1 + role_id: 1 + old_status_id: 5 + id: 201 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_040: + new_status_id: 6 + role_id: 2 + old_status_id: 2 + id: 40 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_121: + new_status_id: 2 + role_id: 2 + old_status_id: 1 + id: 121 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_202: + new_status_id: 2 + role_id: 1 + old_status_id: 5 + id: 202 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_014: + new_status_id: 5 + role_id: 1 + old_status_id: 3 + id: 14 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_041: + new_status_id: 1 + role_id: 2 + old_status_id: 3 + id: 41 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_122: + new_status_id: 3 + role_id: 2 + old_status_id: 1 + id: 122 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_203: + new_status_id: 3 + role_id: 1 + old_status_id: 5 + id: 203 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_015: + new_status_id: 6 + role_id: 1 + old_status_id: 3 + id: 15 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_230: + new_status_id: 6 + role_id: 2 + old_status_id: 4 + id: 230 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_123: + new_status_id: 4 + role_id: 2 + old_status_id: 1 + id: 123 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_204: + new_status_id: 4 + role_id: 1 + old_status_id: 5 + id: 204 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_016: + new_status_id: 1 + role_id: 1 + old_status_id: 4 + id: 16 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_042: + new_status_id: 2 + role_id: 2 + old_status_id: 3 + id: 42 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_231: + new_status_id: 1 + role_id: 2 + old_status_id: 5 + id: 231 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_070: + new_status_id: 6 + role_id: 3 + old_status_id: 2 + id: 70 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_124: + new_status_id: 5 + role_id: 2 + old_status_id: 1 + id: 124 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_150: + new_status_id: 5 + role_id: 2 + old_status_id: 6 + id: 150 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_205: + new_status_id: 6 + role_id: 1 + old_status_id: 5 + id: 205 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_017: + new_status_id: 2 + role_id: 1 + old_status_id: 4 + id: 17 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_043: + new_status_id: 4 + role_id: 2 + old_status_id: 3 + id: 43 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_232: + new_status_id: 2 + role_id: 2 + old_status_id: 5 + id: 232 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_125: + new_status_id: 6 + role_id: 2 + old_status_id: 1 + id: 125 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_151: + new_status_id: 2 + role_id: 3 + old_status_id: 1 + id: 151 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_206: + new_status_id: 1 + role_id: 1 + old_status_id: 6 + id: 206 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_018: + new_status_id: 3 + role_id: 1 + old_status_id: 4 + id: 18 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_044: + new_status_id: 5 + role_id: 2 + old_status_id: 3 + id: 44 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_071: + new_status_id: 1 + role_id: 3 + old_status_id: 3 + id: 71 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_233: + new_status_id: 3 + role_id: 2 + old_status_id: 5 + id: 233 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_126: + new_status_id: 1 + role_id: 2 + old_status_id: 2 + id: 126 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_152: + new_status_id: 3 + role_id: 3 + old_status_id: 1 + id: 152 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_207: + new_status_id: 2 + role_id: 1 + old_status_id: 6 + id: 207 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_019: + new_status_id: 5 + role_id: 1 + old_status_id: 4 + id: 19 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_045: + new_status_id: 6 + role_id: 2 + old_status_id: 3 + id: 45 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_260: + new_status_id: 6 + role_id: 3 + old_status_id: 4 + id: 260 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_234: + new_status_id: 4 + role_id: 2 + old_status_id: 5 + id: 234 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_127: + new_status_id: 3 + role_id: 2 + old_status_id: 2 + id: 127 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_153: + new_status_id: 4 + role_id: 3 + old_status_id: 1 + id: 153 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_180: + new_status_id: 5 + role_id: 3 + old_status_id: 6 + id: 180 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_208: + new_status_id: 3 + role_id: 1 + old_status_id: 6 + id: 208 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_046: + new_status_id: 1 + role_id: 2 + old_status_id: 4 + id: 46 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_072: + new_status_id: 2 + role_id: 3 + old_status_id: 3 + id: 72 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_261: + new_status_id: 1 + role_id: 3 + old_status_id: 5 + id: 261 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_235: + new_status_id: 6 + role_id: 2 + old_status_id: 5 + id: 235 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_154: + new_status_id: 5 + role_id: 3 + old_status_id: 1 + id: 154 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_181: + new_status_id: 2 + role_id: 1 + old_status_id: 1 + id: 181 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_209: + new_status_id: 4 + role_id: 1 + old_status_id: 6 + id: 209 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_047: + new_status_id: 2 + role_id: 2 + old_status_id: 4 + id: 47 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_073: + new_status_id: 4 + role_id: 3 + old_status_id: 3 + id: 73 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_128: + new_status_id: 4 + role_id: 2 + old_status_id: 2 + id: 128 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_262: + new_status_id: 2 + role_id: 3 + old_status_id: 5 + id: 262 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_236: + new_status_id: 1 + role_id: 2 + old_status_id: 6 + id: 236 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_155: + new_status_id: 6 + role_id: 3 + old_status_id: 1 + id: 155 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_048: + new_status_id: 3 + role_id: 2 + old_status_id: 4 + id: 48 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_074: + new_status_id: 5 + role_id: 3 + old_status_id: 3 + id: 74 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_129: + new_status_id: 5 + role_id: 2 + old_status_id: 2 + id: 129 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_263: + new_status_id: 3 + role_id: 3 + old_status_id: 5 + id: 263 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_237: + new_status_id: 2 + role_id: 2 + old_status_id: 6 + id: 237 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_182: + new_status_id: 3 + role_id: 1 + old_status_id: 1 + id: 182 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_049: + new_status_id: 5 + role_id: 2 + old_status_id: 4 + id: 49 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_075: + new_status_id: 6 + role_id: 3 + old_status_id: 3 + id: 75 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_156: + new_status_id: 1 + role_id: 3 + old_status_id: 2 + id: 156 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_264: + new_status_id: 4 + role_id: 3 + old_status_id: 5 + id: 264 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_238: + new_status_id: 3 + role_id: 2 + old_status_id: 6 + id: 238 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_183: + new_status_id: 4 + role_id: 1 + old_status_id: 1 + id: 183 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_076: + new_status_id: 1 + role_id: 3 + old_status_id: 4 + id: 76 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_157: + new_status_id: 3 + role_id: 3 + old_status_id: 2 + id: 157 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_265: + new_status_id: 6 + role_id: 3 + old_status_id: 5 + id: 265 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_239: + new_status_id: 4 + role_id: 2 + old_status_id: 6 + id: 239 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_077: + new_status_id: 2 + role_id: 3 + old_status_id: 4 + id: 77 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_158: + new_status_id: 4 + role_id: 3 + old_status_id: 2 + id: 158 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_184: + new_status_id: 5 + role_id: 1 + old_status_id: 1 + id: 184 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_266: + new_status_id: 1 + role_id: 3 + old_status_id: 6 + id: 266 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_078: + new_status_id: 3 + role_id: 3 + old_status_id: 4 + id: 78 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_159: + new_status_id: 5 + role_id: 3 + old_status_id: 2 + id: 159 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_185: + new_status_id: 6 + role_id: 1 + old_status_id: 1 + id: 185 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_267: + new_status_id: 2 + role_id: 3 + old_status_id: 6 + id: 267 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_079: + new_status_id: 5 + role_id: 3 + old_status_id: 4 + id: 79 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_186: + new_status_id: 1 + role_id: 1 + old_status_id: 2 + id: 186 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_268: + new_status_id: 3 + role_id: 3 + old_status_id: 6 + id: 268 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_187: + new_status_id: 3 + role_id: 1 + old_status_id: 2 + id: 187 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_269: + new_status_id: 4 + role_id: 3 + old_status_id: 6 + id: 269 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_188: + new_status_id: 4 + role_id: 1 + old_status_id: 2 + id: 188 + tracker_id: 3 + type: WorkflowTransition diff --git a/test/functional/account_controller_openid_test.rb b/test/functional/account_controller_openid_test.rb new file mode 100644 index 00000000..8a0d81ab --- /dev/null +++ b/test/functional/account_controller_openid_test.rb @@ -0,0 +1,165 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class AccountControllerOpenidTest < ActionController::TestCase + tests AccountController + fixtures :users, :roles + + def setup + User.current = nil + Setting.openid = '1' + end + + def teardown + Setting.openid = '0' + end + + if Object.const_defined?(:OpenID) + + def test_login_with_openid_for_existing_user + Setting.self_registration = '3' + existing_user = User.new(:firstname => 'Cool', + :lastname => 'User', + :mail => 'user@somedomain.com', + :identity_url => 'http://openid.example.com/good_user') + existing_user.login = 'cool_user' + assert existing_user.save! + + post :login, :openid_url => existing_user.identity_url + assert_redirected_to '/my/page' + end + + def test_login_with_invalid_openid_provider + Setting.self_registration = '0' + post :login, :openid_url => 'http;//openid.example.com/good_user' + assert_redirected_to home_url + end + + def test_login_with_openid_for_existing_non_active_user + Setting.self_registration = '2' + existing_user = User.new(:firstname => 'Cool', + :lastname => 'User', + :mail => 'user@somedomain.com', + :identity_url => 'http://openid.example.com/good_user', + :status => User::STATUS_REGISTERED) + existing_user.login = 'cool_user' + assert existing_user.save! + + post :login, :openid_url => existing_user.identity_url + assert_redirected_to '/login' + end + + def test_login_with_openid_with_new_user_created + Setting.self_registration = '3' + post :login, :openid_url => 'http://openid.example.com/good_user' + assert_redirected_to '/my/account' + user = User.find_by_login('cool_user') + assert user + assert_equal 'Cool', user.firstname + assert_equal 'User', user.lastname + end + + def test_login_with_openid_with_new_user_and_self_registration_off + Setting.self_registration = '0' + post :login, :openid_url => 'http://openid.example.com/good_user' + assert_redirected_to home_url + user = User.find_by_login('cool_user') + assert_nil user + end + + def test_login_with_openid_with_new_user_created_with_email_activation_should_have_a_token + Setting.self_registration = '1' + post :login, :openid_url => 'http://openid.example.com/good_user' + assert_redirected_to '/login' + user = User.find_by_login('cool_user') + assert user + + token = Token.find_by_user_id_and_action(user.id, 'register') + assert token + end + + def test_login_with_openid_with_new_user_created_with_manual_activation + Setting.self_registration = '2' + post :login, :openid_url => 'http://openid.example.com/good_user' + assert_redirected_to '/login' + user = User.find_by_login('cool_user') + assert user + assert_equal User::STATUS_REGISTERED, user.status + end + + def test_login_with_openid_with_new_user_with_conflict_should_register + Setting.self_registration = '3' + existing_user = User.new(:firstname => 'Cool', :lastname => 'User', :mail => 'user@somedomain.com') + existing_user.login = 'cool_user' + assert existing_user.save! + + post :login, :openid_url => 'http://openid.example.com/good_user' + assert_response :success + assert_template 'register' + assert assigns(:user) + assert_equal 'http://openid.example.com/good_user', assigns(:user)[:identity_url] + end + + def test_login_with_openid_with_new_user_with_missing_information_should_register + Setting.self_registration = '3' + + post :login, :openid_url => 'http://openid.example.com/good_blank_user' + assert_response :success + assert_template 'register' + assert assigns(:user) + assert_equal 'http://openid.example.com/good_blank_user', assigns(:user)[:identity_url] + + assert_select 'input[name=?]', 'user[login]' + assert_select 'input[name=?]', 'user[password]' + assert_select 'input[name=?]', 'user[password_confirmation]' + assert_select 'input[name=?][value=?]', 'user[identity_url]', 'http://openid.example.com/good_blank_user' + end + + def test_register_after_login_failure_should_not_require_user_to_enter_a_password + Setting.self_registration = '3' + + assert_difference 'User.count' do + post :register, :user => { + :login => 'good_blank_user', + :password => '', + :password_confirmation => '', + :firstname => 'Cool', + :lastname => 'User', + :mail => 'user@somedomain.com', + :identity_url => 'http://openid.example.com/good_blank_user' + } + assert_response 302 + end + + user = User.first(:order => 'id DESC') + assert_equal 'http://openid.example.com/good_blank_user', user.identity_url + assert user.hashed_password.blank?, "Hashed password was #{user.hashed_password}" + end + + def test_setting_openid_should_return_true_when_set_to_true + assert_equal true, Setting.openid? + end + + else + puts "Skipping openid tests." + + def test_dummy + end + end +end diff --git a/test/functional/account_controller_test.rb b/test/functional/account_controller_test.rb new file mode 100644 index 00000000..ca30cf5d --- /dev/null +++ b/test/functional/account_controller_test.rb @@ -0,0 +1,277 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class AccountControllerTest < ActionController::TestCase + fixtures :users, :roles + + def setup + User.current = nil + end + + def test_get_login + get :login + assert_response :success + assert_template 'login' + + assert_select 'input[name=username]' + assert_select 'input[name=password]' + end + + def test_get_login_while_logged_in_should_redirect_to_home + @request.session[:user_id] = 2 + + get :login + assert_redirected_to '/' + assert_equal 2, @request.session[:user_id] + end + + def test_login_should_redirect_to_back_url_param + # request.uri is "test.host" in test environment + post :login, :username => 'jsmith', :password => 'jsmith', :back_url => 'http://test.host/issues/show/1' + assert_redirected_to '/issues/show/1' + end + + def test_login_should_not_redirect_to_another_host + post :login, :username => 'jsmith', :password => 'jsmith', :back_url => 'http://test.foo/fake' + assert_redirected_to '/my/page' + end + + def test_login_with_wrong_password + post :login, :username => 'admin', :password => 'bad' + assert_response :success + assert_template 'login' + + assert_select 'div.flash.error', :text => /Invalid user or password/ + assert_select 'input[name=username][value=admin]' + assert_select 'input[name=password]' + assert_select 'input[name=password][value]', 0 + end + + def test_login_should_rescue_auth_source_exception + source = AuthSource.create!(:name => 'Test') + User.find(2).update_attribute :auth_source_id, source.id + AuthSource.any_instance.stubs(:authenticate).raises(AuthSourceException.new("Something wrong")) + + post :login, :username => 'jsmith', :password => 'jsmith' + assert_response 500 + assert_error_tag :content => /Something wrong/ + end + + def test_login_should_reset_session + @controller.expects(:reset_session).once + + post :login, :username => 'jsmith', :password => 'jsmith' + assert_response 302 + end + + def test_get_logout_should_not_logout + @request.session[:user_id] = 2 + get :logout + assert_response :success + assert_template 'logout' + + assert_equal 2, @request.session[:user_id] + end + + def test_logout + @request.session[:user_id] = 2 + post :logout + assert_redirected_to '/' + assert_nil @request.session[:user_id] + end + + def test_logout_should_reset_session + @controller.expects(:reset_session).once + + @request.session[:user_id] = 2 + post :logout + assert_response 302 + end + + def test_get_register_with_registration_on + with_settings :self_registration => '3' do + get :register + assert_response :success + assert_template 'register' + assert_not_nil assigns(:user) + + assert_select 'input[name=?]', 'user[password]' + assert_select 'input[name=?]', 'user[password_confirmation]' + end + end + + def test_get_register_should_detect_user_language + with_settings :self_registration => '3' do + @request.env['HTTP_ACCEPT_LANGUAGE'] = 'fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3' + get :register + assert_response :success + assert_not_nil assigns(:user) + assert_equal 'fr', assigns(:user).language + assert_select 'select[name=?]', 'user[language]' do + assert_select 'option[value=fr][selected=selected]' + end + end + end + + def test_get_register_with_registration_off_should_redirect + with_settings :self_registration => '0' do + get :register + assert_redirected_to '/' + end + end + + # See integration/account_test.rb for the full test + def test_post_register_with_registration_on + with_settings :self_registration => '3' do + assert_difference 'User.count' do + post :register, :user => { + :login => 'register', + :password => 'secret123', + :password_confirmation => 'secret123', + :firstname => 'John', + :lastname => 'Doe', + :mail => 'register@example.com' + } + assert_redirected_to '/my/account' + end + user = User.first(:order => 'id DESC') + assert_equal 'register', user.login + assert_equal 'John', user.firstname + assert_equal 'Doe', user.lastname + assert_equal 'register@example.com', user.mail + assert user.check_password?('secret123') + assert user.active? + end + end + + def test_post_register_with_registration_off_should_redirect + with_settings :self_registration => '0' do + assert_no_difference 'User.count' do + post :register, :user => { + :login => 'register', + :password => 'test', + :password_confirmation => 'test', + :firstname => 'John', + :lastname => 'Doe', + :mail => 'register@example.com' + } + assert_redirected_to '/' + end + end + end + + def test_get_lost_password_should_display_lost_password_form + get :lost_password + assert_response :success + assert_select 'input[name=mail]' + end + + def test_lost_password_for_active_user_should_create_a_token + Token.delete_all + ActionMailer::Base.deliveries.clear + assert_difference 'ActionMailer::Base.deliveries.size' do + assert_difference 'Token.count' do + with_settings :host_name => 'mydomain.foo', :protocol => 'http' do + post :lost_password, :mail => 'JSmith@somenet.foo' + assert_redirected_to '/login' + end + end + end + + token = Token.order('id DESC').first + assert_equal User.find(2), token.user + assert_equal 'recovery', token.action + + assert_select_email do + assert_select "a[href=?]", "http://mydomain.foo/account/lost_password?token=#{token.value}" + end + end + + def test_lost_password_for_unknown_user_should_fail + Token.delete_all + assert_no_difference 'Token.count' do + post :lost_password, :mail => 'invalid@somenet.foo' + assert_response :success + end + end + + def test_lost_password_for_non_active_user_should_fail + Token.delete_all + assert User.find(2).lock! + + assert_no_difference 'Token.count' do + post :lost_password, :mail => 'JSmith@somenet.foo' + assert_response :success + end + end + + def test_get_lost_password_with_token_should_display_the_password_recovery_form + user = User.find(2) + token = Token.create!(:action => 'recovery', :user => user) + + get :lost_password, :token => token.value + assert_response :success + assert_template 'password_recovery' + + assert_select 'input[type=hidden][name=token][value=?]', token.value + end + + def test_get_lost_password_with_invalid_token_should_redirect + get :lost_password, :token => "abcdef" + assert_redirected_to '/' + end + + def test_post_lost_password_with_token_should_change_the_user_password + user = User.find(2) + token = Token.create!(:action => 'recovery', :user => user) + + post :lost_password, :token => token.value, :new_password => 'newpass123', :new_password_confirmation => 'newpass123' + assert_redirected_to '/login' + user.reload + assert user.check_password?('newpass123') + assert_nil Token.find_by_id(token.id), "Token was not deleted" + end + + def test_post_lost_password_with_token_for_non_active_user_should_fail + user = User.find(2) + token = Token.create!(:action => 'recovery', :user => user) + user.lock! + + post :lost_password, :token => token.value, :new_password => 'newpass123', :new_password_confirmation => 'newpass123' + assert_redirected_to '/' + assert ! user.check_password?('newpass123') + end + + def test_post_lost_password_with_token_and_password_confirmation_failure_should_redisplay_the_form + user = User.find(2) + token = Token.create!(:action => 'recovery', :user => user) + + post :lost_password, :token => token.value, :new_password => 'newpass', :new_password_confirmation => 'wrongpass' + assert_response :success + assert_template 'password_recovery' + assert_not_nil Token.find_by_id(token.id), "Token was deleted" + + assert_select 'input[type=hidden][name=token][value=?]', token.value + end + + def test_post_lost_password_with_invalid_token_should_redirect + post :lost_password, :token => "abcdef", :new_password => 'newpass', :new_password_confirmation => 'newpass' + assert_redirected_to '/' + end +end diff --git a/test/functional/activities_controller_test.rb b/test/functional/activities_controller_test.rb new file mode 100644 index 00000000..d1b4396d --- /dev/null +++ b/test/functional/activities_controller_test.rb @@ -0,0 +1,150 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class ActivitiesControllerTest < ActionController::TestCase + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :groups_users, + :enabled_modules, + :journals, :journal_details + + + def test_project_index + get :index, :id => 1, :with_subprojects => 0 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:events_by_day) + + assert_select 'h3', :text => /#{2.days.ago.to_date.day}/ + assert_select 'dl dt.issue-edit a', :text => /(#{IssueStatus.find(2).name})/ + end + + def test_project_index_with_invalid_project_id_should_respond_404 + get :index, :id => 299 + assert_response 404 + end + + def test_previous_project_index + get :index, :id => 1, :from => 2.days.ago.to_date + assert_response :success + assert_template 'index' + assert_not_nil assigns(:events_by_day) + + assert_select 'h3', :text => /#{3.days.ago.to_date.day}/ + assert_select 'dl dt.issue a', :text => /Can't print recipes/ + end + + def test_global_index + @request.session[:user_id] = 1 + get :index + assert_response :success + assert_template 'index' + assert_not_nil assigns(:events_by_day) + + i5 = Issue.find(5) + d5 = User.find(1).time_to_date(i5.created_on) + + assert_select 'h3', :text => /#{d5.day}/ + assert_select 'dl dt.issue a', :text => /Subproject issue/ + end + + def test_user_index + @request.session[:user_id] = 1 + get :index, :user_id => 2 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:events_by_day) + + assert_select 'h2 a[href=/users/2]', :text => 'John Smith' + + i1 = Issue.find(1) + d1 = User.find(1).time_to_date(i1.created_on) + + assert_select 'h3', :text => /#{d1.day}/ + assert_select 'dl dt.issue a', :text => /Can't print recipes/ + end + + def test_user_index_with_invalid_user_id_should_respond_404 + get :index, :user_id => 299 + assert_response 404 + end + + def test_index_atom_feed + get :index, :format => 'atom', :with_subprojects => 0 + assert_response :success + assert_template 'common/feed' + + assert_select 'feed' do + assert_select 'link[rel=self][href=?]', 'http://test.host/activity.atom?with_subprojects=0' + assert_select 'link[rel=alternate][href=?]', 'http://test.host/activity?with_subprojects=0' + assert_select 'entry' do + assert_select 'link[href=?]', 'http://test.host/issues/11' + end + end + end + + def test_index_atom_feed_with_explicit_selection + get :index, :format => 'atom', :with_subprojects => 0, + :show_changesets => 1, + :show_documents => 1, + :show_files => 1, + :show_issues => 1, + :show_messages => 1, + :show_news => 1, + :show_time_entries => 1, + :show_wiki_edits => 1 + + assert_response :success + assert_template 'common/feed' + + assert_select 'feed' do + assert_select 'link[rel=self][href=?]', 'http://test.host/activity.atom?show_changesets=1&show_documents=1&show_files=1&show_issues=1&show_messages=1&show_news=1&show_time_entries=1&show_wiki_edits=1&with_subprojects=0' + assert_select 'link[rel=alternate][href=?]', 'http://test.host/activity?show_changesets=1&show_documents=1&show_files=1&show_issues=1&show_messages=1&show_news=1&show_time_entries=1&show_wiki_edits=1&with_subprojects=0' + assert_select 'entry' do + assert_select 'link[href=?]', 'http://test.host/issues/11' + end + end + end + + def test_index_atom_feed_with_one_item_type + get :index, :format => 'atom', :show_issues => '1' + assert_response :success + assert_template 'common/feed' + + assert_select 'title', :text => /Issues/ + end + + def test_index_should_show_private_notes_with_permission_only + journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Private notes with searchkeyword', :private_notes => true) + @request.session[:user_id] = 2 + + get :index + assert_response :success + assert_include journal, assigns(:events_by_day).values.flatten + + Role.find(1).remove_permission! :view_private_notes + get :index + assert_response :success + assert_not_include journal, assigns(:events_by_day).values.flatten + end +end diff --git a/test/functional/admin_controller_test.rb b/test/functional/admin_controller_test.rb new file mode 100644 index 00000000..e6d8fcff --- /dev/null +++ b/test/functional/admin_controller_test.rb @@ -0,0 +1,167 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class AdminControllerTest < ActionController::TestCase + fixtures :projects, :users, :roles + + def setup + User.current = nil + @request.session[:user_id] = 1 # admin + end + + def test_index + get :index + assert_select 'div.nodata', 0 + end + + def test_index_with_no_configuration_data + delete_configuration_data + get :index + assert_select 'div.nodata' + end + + def test_projects + get :projects + assert_response :success + assert_template 'projects' + assert_not_nil assigns(:projects) + # active projects only + assert_nil assigns(:projects).detect {|u| !u.active?} + end + + def test_projects_with_status_filter + get :projects, :status => 1 + assert_response :success + assert_template 'projects' + assert_not_nil assigns(:projects) + # active projects only + assert_nil assigns(:projects).detect {|u| !u.active?} + end + + def test_projects_with_name_filter + get :projects, :name => 'store', :status => '' + assert_response :success + assert_template 'projects' + projects = assigns(:projects) + assert_not_nil projects + assert_equal 1, projects.size + assert_equal 'OnlineStore', projects.first.name + end + + def test_load_default_configuration_data + delete_configuration_data + post :default_configuration, :lang => 'fr' + assert_response :redirect + assert_nil flash[:error] + assert IssueStatus.find_by_name('Nouveau') + end + + def test_load_default_configuration_data_should_rescue_error + delete_configuration_data + Redmine::DefaultData::Loader.stubs(:load).raises(Exception.new("Something went wrong")) + post :default_configuration, :lang => 'fr' + assert_response :redirect + assert_not_nil flash[:error] + assert_match /Something went wrong/, flash[:error] + end + + def test_test_email + user = User.find(1) + user.pref.no_self_notified = '1' + user.pref.save! + ActionMailer::Base.deliveries.clear + + get :test_email + assert_redirected_to '/settings?tab=notifications' + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + user = User.find(1) + assert_equal [user.mail], mail.bcc + end + + def test_test_email_failure_should_display_the_error + Mailer.stubs(:test_email).raises(Exception, 'Some error message') + get :test_email + assert_redirected_to '/settings?tab=notifications' + assert_match /Some error message/, flash[:error] + end + + def test_no_plugins + Redmine::Plugin.clear + + get :plugins + assert_response :success + assert_template 'plugins' + end + + def test_plugins + # Register a few plugins + Redmine::Plugin.register :foo do + name 'Foo plugin' + author 'John Smith' + description 'This is a test plugin' + version '0.0.1' + settings :default => {'sample_setting' => 'value', 'foo'=>'bar'}, :partial => 'foo/settings' + end + Redmine::Plugin.register :bar do + end + + get :plugins + assert_response :success + assert_template 'plugins' + + assert_select 'tr#plugin-foo' do + assert_select 'td span.name', :text => 'Foo plugin' + assert_select 'td.configure a[href=/settings/plugin/foo]' + end + assert_select 'tr#plugin-bar' do + assert_select 'td span.name', :text => 'Bar' + assert_select 'td.configure a', 0 + end + end + + def test_info + get :info + assert_response :success + assert_template 'info' + end + + def test_admin_menu_plugin_extension + Redmine::MenuManager.map :admin_menu do |menu| + menu.push :test_admin_menu_plugin_extension, '/foo/bar', :caption => 'Test' + end + + get :index + assert_response :success + assert_select 'div#admin-menu a[href=/foo/bar]', :text => 'Test' + + Redmine::MenuManager.map :admin_menu do |menu| + menu.delete :test_admin_menu_plugin_extension + end + end + + private + + def delete_configuration_data + Role.delete_all('builtin = 0') + Tracker.delete_all + IssueStatus.delete_all + Enumeration.delete_all + end +end diff --git a/test/functional/attachments_controller_test.rb b/test/functional/attachments_controller_test.rb new file mode 100644 index 00000000..8458e971 --- /dev/null +++ b/test/functional/attachments_controller_test.rb @@ -0,0 +1,385 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class AttachmentsControllerTest < ActionController::TestCase + fixtures :users, :projects, :roles, :members, :member_roles, + :enabled_modules, :issues, :trackers, :attachments, + :versions, :wiki_pages, :wikis, :documents + + def setup + User.current = nil + set_fixtures_attachments_directory + end + + def teardown + set_tmp_attachments_directory + end + + def test_show_diff + ['inline', 'sbs'].each do |dt| + # 060719210727_changeset_utf8.diff + get :show, :id => 14, :type => dt + assert_response :success + assert_template 'diff' + assert_equal 'text/html', @response.content_type + assert_tag 'th', + :attributes => {:class => /filename/}, + :content => /issues_controller.rb\t\(révision 1484\)/ + assert_tag 'td', + :attributes => {:class => /line-code/}, + :content => /Demande créée avec succès/ + end + set_tmp_attachments_directory + end + + def test_show_diff_replace_cannot_convert_content + with_settings :repositories_encodings => 'UTF-8' do + ['inline', 'sbs'].each do |dt| + # 060719210727_changeset_iso8859-1.diff + get :show, :id => 5, :type => dt + assert_response :success + assert_template 'diff' + assert_equal 'text/html', @response.content_type + assert_tag 'th', + :attributes => {:class => "filename"}, + :content => /issues_controller.rb\t\(r\?vision 1484\)/ + assert_tag 'td', + :attributes => {:class => /line-code/}, + :content => /Demande cr\?\?e avec succ\?s/ + end + end + set_tmp_attachments_directory + end + + def test_show_diff_latin_1 + with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do + ['inline', 'sbs'].each do |dt| + # 060719210727_changeset_iso8859-1.diff + get :show, :id => 5, :type => dt + assert_response :success + assert_template 'diff' + assert_equal 'text/html', @response.content_type + assert_tag 'th', + :attributes => {:class => "filename"}, + :content => /issues_controller.rb\t\(révision 1484\)/ + assert_tag 'td', + :attributes => {:class => /line-code/}, + :content => /Demande créée avec succès/ + end + end + set_tmp_attachments_directory + end + + def test_save_diff_type + user1 = User.find(1) + user1.pref[:diff_type] = nil + user1.preference.save + user = User.find(1) + assert_nil user.pref[:diff_type] + + @request.session[:user_id] = 1 # admin + get :show, :id => 5 + assert_response :success + assert_template 'diff' + user.reload + assert_equal "inline", user.pref[:diff_type] + get :show, :id => 5, :type => 'sbs' + assert_response :success + assert_template 'diff' + user.reload + assert_equal "sbs", user.pref[:diff_type] + end + + def test_diff_show_filename_in_mercurial_export + set_tmp_attachments_directory + a = Attachment.new(:container => Issue.find(1), + :file => uploaded_test_file("hg-export.diff", "text/plain"), + :author => User.find(1)) + assert a.save + assert_equal 'hg-export.diff', a.filename + + get :show, :id => a.id, :type => 'inline' + assert_response :success + assert_template 'diff' + assert_equal 'text/html', @response.content_type + assert_select 'th.filename', :text => 'test1.txt' + end + + def test_show_text_file + get :show, :id => 4 + assert_response :success + assert_template 'file' + assert_equal 'text/html', @response.content_type + set_tmp_attachments_directory + end + + def test_show_text_file_utf_8 + set_tmp_attachments_directory + a = Attachment.new(:container => Issue.find(1), + :file => uploaded_test_file("japanese-utf-8.txt", "text/plain"), + :author => User.find(1)) + assert a.save + assert_equal 'japanese-utf-8.txt', a.filename + + str_japanese = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e" + str_japanese.force_encoding('UTF-8') if str_japanese.respond_to?(:force_encoding) + + get :show, :id => a.id + assert_response :success + assert_template 'file' + assert_equal 'text/html', @response.content_type + assert_tag :tag => 'th', + :content => '1', + :attributes => { :class => 'line-num' }, + :sibling => { :tag => 'td', :content => /#{str_japanese}/ } + end + + def test_show_text_file_replace_cannot_convert_content + set_tmp_attachments_directory + with_settings :repositories_encodings => 'UTF-8' do + a = Attachment.new(:container => Issue.find(1), + :file => uploaded_test_file("iso8859-1.txt", "text/plain"), + :author => User.find(1)) + assert a.save + assert_equal 'iso8859-1.txt', a.filename + + get :show, :id => a.id + assert_response :success + assert_template 'file' + assert_equal 'text/html', @response.content_type + assert_tag :tag => 'th', + :content => '7', + :attributes => { :class => 'line-num' }, + :sibling => { :tag => 'td', :content => /Demande cr\?\?e avec succ\?s/ } + end + end + + def test_show_text_file_latin_1 + set_tmp_attachments_directory + with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do + a = Attachment.new(:container => Issue.find(1), + :file => uploaded_test_file("iso8859-1.txt", "text/plain"), + :author => User.find(1)) + assert a.save + assert_equal 'iso8859-1.txt', a.filename + + get :show, :id => a.id + assert_response :success + assert_template 'file' + assert_equal 'text/html', @response.content_type + assert_tag :tag => 'th', + :content => '7', + :attributes => { :class => 'line-num' }, + :sibling => { :tag => 'td', :content => /Demande créée avec succès/ } + end + end + + def test_show_text_file_should_send_if_too_big + Setting.file_max_size_displayed = 512 + Attachment.find(4).update_attribute :filesize, 754.kilobyte + + get :show, :id => 4 + assert_response :success + assert_equal 'application/x-ruby', @response.content_type + set_tmp_attachments_directory + end + + def test_show_other + get :show, :id => 6 + assert_response :success + assert_equal 'application/octet-stream', @response.content_type + set_tmp_attachments_directory + end + + def test_show_file_from_private_issue_without_permission + get :show, :id => 15 + assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2F15' + set_tmp_attachments_directory + end + + def test_show_file_from_private_issue_with_permission + @request.session[:user_id] = 2 + get :show, :id => 15 + assert_response :success + assert_tag 'h2', :content => /private.diff/ + set_tmp_attachments_directory + end + + def test_show_file_without_container_should_be_allowed_to_author + set_tmp_attachments_directory + attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2) + + @request.session[:user_id] = 2 + get :show, :id => attachment.id + assert_response 200 + end + + def test_show_file_without_container_should_be_denied_to_other_users + set_tmp_attachments_directory + attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2) + + @request.session[:user_id] = 3 + get :show, :id => attachment.id + assert_response 403 + end + + def test_show_invalid_should_respond_with_404 + get :show, :id => 999 + assert_response 404 + end + + def test_download_text_file + get :download, :id => 4 + assert_response :success + assert_equal 'application/x-ruby', @response.content_type + set_tmp_attachments_directory + end + + def test_download_version_file_with_issue_tracking_disabled + Project.find(1).disable_module! :issue_tracking + get :download, :id => 9 + assert_response :success + end + + def test_download_should_assign_content_type_if_blank + Attachment.find(4).update_attribute(:content_type, '') + + get :download, :id => 4 + assert_response :success + assert_equal 'text/x-ruby', @response.content_type + set_tmp_attachments_directory + end + + def test_download_missing_file + get :download, :id => 2 + assert_response 404 + set_tmp_attachments_directory + end + + def test_download_should_be_denied_without_permission + get :download, :id => 7 + assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fdownload%2F7' + set_tmp_attachments_directory + end + + if convert_installed? + def test_thumbnail + Attachment.clear_thumbnails + @request.session[:user_id] = 2 + + get :thumbnail, :id => 16 + assert_response :success + assert_equal 'image/png', response.content_type + end + + def test_thumbnail_should_not_exceed_maximum_size + Redmine::Thumbnail.expects(:generate).with {|source, target, size| size == 800} + + @request.session[:user_id] = 2 + get :thumbnail, :id => 16, :size => 2000 + end + + def test_thumbnail_should_round_size + Redmine::Thumbnail.expects(:generate).with {|source, target, size| size == 250} + + @request.session[:user_id] = 2 + get :thumbnail, :id => 16, :size => 260 + end + + def test_thumbnail_should_return_404_for_non_image_attachment + @request.session[:user_id] = 2 + + get :thumbnail, :id => 15 + assert_response 404 + end + + def test_thumbnail_should_return_404_if_thumbnail_generation_failed + Attachment.any_instance.stubs(:thumbnail).returns(nil) + @request.session[:user_id] = 2 + + get :thumbnail, :id => 16 + assert_response 404 + end + + def test_thumbnail_should_be_denied_without_permission + get :thumbnail, :id => 16 + assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fthumbnail%2F16' + end + else + puts '(ImageMagick convert not available)' + end + + def test_destroy_issue_attachment + set_tmp_attachments_directory + issue = Issue.find(3) + @request.session[:user_id] = 2 + + assert_difference 'issue.attachments.count', -1 do + assert_difference 'Journal.count' do + delete :destroy, :id => 1 + assert_redirected_to '/projects/ecookbook' + end + end + assert_nil Attachment.find_by_id(1) + j = Journal.first(:order => 'id DESC') + assert_equal issue, j.journalized + assert_equal 'attachment', j.details.first.property + assert_equal '1', j.details.first.prop_key + assert_equal 'error281.txt', j.details.first.old_value + assert_equal User.find(2), j.user + end + + def test_destroy_wiki_page_attachment + set_tmp_attachments_directory + @request.session[:user_id] = 2 + assert_difference 'Attachment.count', -1 do + delete :destroy, :id => 3 + assert_response 302 + end + end + + def test_destroy_project_attachment + set_tmp_attachments_directory + @request.session[:user_id] = 2 + assert_difference 'Attachment.count', -1 do + delete :destroy, :id => 8 + assert_response 302 + end + end + + def test_destroy_version_attachment + set_tmp_attachments_directory + @request.session[:user_id] = 2 + assert_difference 'Attachment.count', -1 do + delete :destroy, :id => 9 + assert_response 302 + end + end + + def test_destroy_without_permission + set_tmp_attachments_directory + assert_no_difference 'Attachment.count' do + delete :destroy, :id => 3 + end + assert_response 302 + assert Attachment.find_by_id(3) + end +end diff --git a/test/functional/auth_sources_controller_test.rb b/test/functional/auth_sources_controller_test.rb new file mode 100644 index 00000000..0ee9e353 --- /dev/null +++ b/test/functional/auth_sources_controller_test.rb @@ -0,0 +1,168 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class AuthSourcesControllerTest < ActionController::TestCase + fixtures :users, :auth_sources + + def setup + @request.session[:user_id] = 1 + end + + def test_index + get :index + + assert_response :success + assert_template 'index' + assert_not_nil assigns(:auth_sources) + end + + def test_new + get :new + + assert_response :success + assert_template 'new' + + source = assigns(:auth_source) + assert_equal AuthSourceLdap, source.class + assert source.new_record? + + assert_select 'form#auth_source_form' do + assert_select 'input[name=type][value=AuthSourceLdap]' + assert_select 'input[name=?]', 'auth_source[host]' + end + end + + def test_new_with_invalid_type_should_respond_with_404 + get :new, :type => 'foo' + assert_response 404 + end + + def test_create + assert_difference 'AuthSourceLdap.count' do + post :create, :type => 'AuthSourceLdap', :auth_source => {:name => 'Test', :host => '127.0.0.1', :port => '389', :attr_login => 'cn'} + assert_redirected_to '/auth_sources' + end + + source = AuthSourceLdap.order('id DESC').first + assert_equal 'Test', source.name + assert_equal '127.0.0.1', source.host + assert_equal 389, source.port + assert_equal 'cn', source.attr_login + end + + def test_create_with_failure + assert_no_difference 'AuthSourceLdap.count' do + post :create, :type => 'AuthSourceLdap', :auth_source => {:name => 'Test', :host => '', :port => '389', :attr_login => 'cn'} + assert_response :success + assert_template 'new' + end + assert_error_tag :content => /host can't be blank/i + end + + def test_edit + get :edit, :id => 1 + + assert_response :success + assert_template 'edit' + + assert_select 'form#auth_source_form' do + assert_select 'input[name=?]', 'auth_source[host]' + end + end + + def test_edit_should_not_contain_password + AuthSource.find(1).update_column :account_password, 'secret' + + get :edit, :id => 1 + assert_response :success + assert_select 'input[value=secret]', 0 + assert_select 'input[name=dummy_password][value=?]', /x+/ + end + + def test_edit_invalid_should_respond_with_404 + get :edit, :id => 99 + assert_response 404 + end + + def test_update + put :update, :id => 1, :auth_source => {:name => 'Renamed', :host => '192.168.0.10', :port => '389', :attr_login => 'uid'} + assert_redirected_to '/auth_sources' + + source = AuthSourceLdap.find(1) + assert_equal 'Renamed', source.name + assert_equal '192.168.0.10', source.host + end + + def test_update_with_failure + put :update, :id => 1, :auth_source => {:name => 'Renamed', :host => '', :port => '389', :attr_login => 'uid'} + assert_response :success + assert_template 'edit' + assert_error_tag :content => /host can't be blank/i + end + + def test_destroy + assert_difference 'AuthSourceLdap.count', -1 do + delete :destroy, :id => 1 + assert_redirected_to '/auth_sources' + end + end + + def test_destroy_auth_source_in_use + User.find(2).update_attribute :auth_source_id, 1 + + assert_no_difference 'AuthSourceLdap.count' do + delete :destroy, :id => 1 + assert_redirected_to '/auth_sources' + end + end + + def test_test_connection + AuthSourceLdap.any_instance.stubs(:test_connection).returns(true) + + get :test_connection, :id => 1 + assert_redirected_to '/auth_sources' + assert_not_nil flash[:notice] + assert_match /successful/i, flash[:notice] + end + + def test_test_connection_with_failure + AuthSourceLdap.any_instance.stubs(:initialize_ldap_con).raises(Net::LDAP::LdapError.new("Something went wrong")) + + get :test_connection, :id => 1 + assert_redirected_to '/auth_sources' + assert_not_nil flash[:error] + assert_include 'Something went wrong', flash[:error] + end + + def test_autocomplete_for_new_user + AuthSource.expects(:search).with('foo').returns([ + {:login => 'foo1', :firstname => 'John', :lastname => 'Smith', :mail => 'foo1@example.net', :auth_source_id => 1}, + {:login => 'Smith', :firstname => 'John', :lastname => 'Doe', :mail => 'foo2@example.net', :auth_source_id => 1} + ]) + + get :autocomplete_for_new_user, :term => 'foo' + assert_response :success + assert_equal 'application/json', response.content_type + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Array, json + assert_equal 2, json.size + assert_equal 'foo1', json.first['value'] + assert_equal 'foo1 (John Smith)', json.first['label'] + end +end diff --git a/test/functional/auto_completes_controller_test.rb b/test/functional/auto_completes_controller_test.rb new file mode 100644 index 00000000..5009bc7b --- /dev/null +++ b/test/functional/auto_completes_controller_test.rb @@ -0,0 +1,90 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class AutoCompletesControllerTest < ActionController::TestCase + fixtures :projects, :issues, :issue_statuses, + :enumerations, :users, :issue_categories, + :trackers, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :journals, :journal_details + + def test_issues_should_not_be_case_sensitive + get :issues, :project_id => 'ecookbook', :q => 'ReCiPe' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).detect {|issue| issue.subject.match /recipe/} + end + + def test_issues_should_accept_term_param + get :issues, :project_id => 'ecookbook', :term => 'ReCiPe' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).detect {|issue| issue.subject.match /recipe/} + end + + def test_issues_should_return_issue_with_given_id + get :issues, :project_id => 'subproject1', :q => '13' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).include?(Issue.find(13)) + end + + def test_issues_should_return_issue_with_given_id_preceded_with_hash + get :issues, :project_id => 'subproject1', :q => '#13' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).include?(Issue.find(13)) + end + + def test_auto_complete_with_scope_all_should_search_other_projects + get :issues, :project_id => 'ecookbook', :q => '13', :scope => 'all' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).include?(Issue.find(13)) + end + + def test_auto_complete_without_project_should_search_all_projects + get :issues, :q => '13' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).include?(Issue.find(13)) + end + + def test_auto_complete_without_scope_all_should_not_search_other_projects + get :issues, :project_id => 'ecookbook', :q => '13' + assert_response :success + assert_equal [], assigns(:issues) + end + + def test_issues_should_return_json + get :issues, :project_id => 'subproject1', :q => '13' + assert_response :success + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Array, json + issue = json.first + assert_kind_of Hash, issue + assert_equal 13, issue['id'] + assert_equal 13, issue['value'] + assert_equal 'Bug #13: Subproject issue two', issue['label'] + end +end diff --git a/test/functional/boards_controller_test.rb b/test/functional/boards_controller_test.rb new file mode 100644 index 00000000..5cfd23ad --- /dev/null +++ b/test/functional/boards_controller_test.rb @@ -0,0 +1,217 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class BoardsControllerTest < ActionController::TestCase + fixtures :projects, :users, :members, :member_roles, :roles, :boards, :messages, :enabled_modules + + def setup + User.current = nil + end + + def test_index + get :index, :project_id => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:boards) + assert_not_nil assigns(:project) + end + + def test_index_not_found + get :index, :project_id => 97 + assert_response 404 + end + + def test_index_should_show_messages_if_only_one_board + Project.find(1).boards.slice(1..-1).each(&:destroy) + + get :index, :project_id => 1 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:topics) + end + + def test_show + get :show, :project_id => 1, :id => 1 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:board) + assert_not_nil assigns(:project) + assert_not_nil assigns(:topics) + end + + def test_show_should_display_sticky_messages_first + Message.update_all(:sticky => 0) + Message.update_all({:sticky => 1}, {:id => 1}) + + get :show, :project_id => 1, :id => 1 + assert_response :success + + topics = assigns(:topics) + assert_not_nil topics + assert topics.size > 1, "topics size was #{topics.size}" + assert topics.first.sticky? + assert topics.first.updated_on < topics.second.updated_on + end + + def test_show_should_display_message_with_last_reply_first + Message.update_all(:sticky => 0) + + # Reply to an old topic + old_topic = Message.where(:board_id => 1, :parent_id => nil).order('created_on ASC').first + reply = Message.new(:board_id => 1, :subject => 'New reply', :content => 'New reply', :author_id => 2) + old_topic.children << reply + + get :show, :project_id => 1, :id => 1 + assert_response :success + topics = assigns(:topics) + assert_not_nil topics + assert_equal old_topic, topics.first + end + + def test_show_with_permission_should_display_the_new_message_form + @request.session[:user_id] = 2 + get :show, :project_id => 1, :id => 1 + assert_response :success + assert_template 'show' + + assert_select 'form#message-form' do + assert_select 'input[name=?]', 'message[subject]' + end + end + + def test_show_atom + get :show, :project_id => 1, :id => 1, :format => 'atom' + assert_response :success + assert_template 'common/feed' + assert_not_nil assigns(:board) + assert_not_nil assigns(:project) + assert_not_nil assigns(:messages) + end + + def test_show_not_found + get :index, :project_id => 1, :id => 97 + assert_response 404 + end + + def test_new + @request.session[:user_id] = 2 + get :new, :project_id => 1 + assert_response :success + assert_template 'new' + + assert_select 'select[name=?]', 'board[parent_id]' do + assert_select 'option', (Project.find(1).boards.size + 1) + assert_select 'option[value=]', :text => '' + assert_select 'option[value=1]', :text => 'Help' + end + end + + def test_new_without_project_boards + Project.find(1).boards.delete_all + @request.session[:user_id] = 2 + + get :new, :project_id => 1 + assert_response :success + assert_template 'new' + + assert_select 'select[name=?]', 'board[parent_id]', 0 + end + + def test_create + @request.session[:user_id] = 2 + assert_difference 'Board.count' do + post :create, :project_id => 1, :board => { :name => 'Testing', :description => 'Testing board creation'} + end + assert_redirected_to '/projects/ecookbook/settings/boards' + board = Board.first(:order => 'id DESC') + assert_equal 'Testing', board.name + assert_equal 'Testing board creation', board.description + end + + def test_create_with_parent + @request.session[:user_id] = 2 + assert_difference 'Board.count' do + post :create, :project_id => 1, :board => { :name => 'Testing', :description => 'Testing', :parent_id => 2} + end + assert_redirected_to '/projects/ecookbook/settings/boards' + board = Board.first(:order => 'id DESC') + assert_equal Board.find(2), board.parent + end + + def test_create_with_failure + @request.session[:user_id] = 2 + assert_no_difference 'Board.count' do + post :create, :project_id => 1, :board => { :name => '', :description => 'Testing board creation'} + end + assert_response :success + assert_template 'new' + end + + def test_edit + @request.session[:user_id] = 2 + get :edit, :project_id => 1, :id => 2 + assert_response :success + assert_template 'edit' + end + + def test_edit_with_parent + board = Board.generate!(:project_id => 1, :parent_id => 2) + @request.session[:user_id] = 2 + get :edit, :project_id => 1, :id => board.id + assert_response :success + assert_template 'edit' + + assert_select 'select[name=?]', 'board[parent_id]' do + assert_select 'option[value=2][selected=selected]' + end + end + + def test_update + @request.session[:user_id] = 2 + assert_no_difference 'Board.count' do + put :update, :project_id => 1, :id => 2, :board => { :name => 'Testing', :description => 'Testing board update'} + end + assert_redirected_to '/projects/ecookbook/settings/boards' + assert_equal 'Testing', Board.find(2).name + end + + def test_update_position + @request.session[:user_id] = 2 + put :update, :project_id => 1, :id => 2, :board => { :move_to => 'highest'} + assert_redirected_to '/projects/ecookbook/settings/boards' + board = Board.find(2) + assert_equal 1, board.position + end + + def test_update_with_failure + @request.session[:user_id] = 2 + put :update, :project_id => 1, :id => 2, :board => { :name => '', :description => 'Testing board update'} + assert_response :success + assert_template 'edit' + end + + def test_destroy + @request.session[:user_id] = 2 + assert_difference 'Board.count', -1 do + delete :destroy, :project_id => 1, :id => 2 + end + assert_redirected_to '/projects/ecookbook/settings/boards' + assert_nil Board.find_by_id(2) + end +end diff --git a/test/functional/calendars_controller_test.rb b/test/functional/calendars_controller_test.rb new file mode 100644 index 00000000..2c00cd5f --- /dev/null +++ b/test/functional/calendars_controller_test.rb @@ -0,0 +1,84 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class CalendarsControllerTest < ActionController::TestCase + fixtures :projects, + :trackers, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules + + def test_show + get :show, :project_id => 1 + assert_response :success + assert_template 'calendar' + assert_not_nil assigns(:calendar) + end + + def test_show_should_run_custom_queries + @query = IssueQuery.create!(:name => 'Calendar', :is_public => true) + + get :show, :query_id => @query.id + assert_response :success + end + + def test_cross_project_calendar + get :show + assert_response :success + assert_template 'calendar' + assert_not_nil assigns(:calendar) + end + + def test_week_number_calculation + Setting.start_of_week = 7 + + get :show, :month => '1', :year => '2010' + assert_response :success + + assert_select 'tr' do + assert_select 'td.week-number', :text => '53' + assert_select 'td.odd', :text => '27' + assert_select 'td.even', :text => '2' + end + + assert_select 'tr' do + assert_select 'td.week-number', :text => '1' + assert_select 'td.odd', :text => '3' + assert_select 'td.even', :text => '9' + end + + Setting.start_of_week = 1 + get :show, :month => '1', :year => '2010' + assert_response :success + + assert_select 'tr' do + assert_select 'td.week-number', :text => '53' + assert_select 'td.even', :text => '28' + assert_select 'td.even', :text => '3' + end + + assert_select 'tr' do + assert_select 'td.week-number', :text => '1' + assert_select 'td.even', :text => '4' + assert_select 'td.even', :text => '10' + end + end +end diff --git a/test/functional/comments_controller_test.rb b/test/functional/comments_controller_test.rb new file mode 100644 index 00000000..bf25d591 --- /dev/null +++ b/test/functional/comments_controller_test.rb @@ -0,0 +1,64 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class CommentsControllerTest < ActionController::TestCase + fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :news, :comments + + def setup + User.current = nil + end + + def test_add_comment + @request.session[:user_id] = 2 + post :create, :id => 1, :comment => { :comments => 'This is a test comment' } + assert_redirected_to '/news/1' + + comment = News.find(1).comments.last + assert_not_nil comment + assert_equal 'This is a test comment', comment.comments + assert_equal User.find(2), comment.author + end + + def test_empty_comment_should_not_be_added + @request.session[:user_id] = 2 + assert_no_difference 'Comment.count' do + post :create, :id => 1, :comment => { :comments => '' } + assert_response :redirect + assert_redirected_to '/news/1' + end + end + + def test_create_should_be_denied_if_news_is_not_commentable + News.any_instance.stubs(:commentable?).returns(false) + @request.session[:user_id] = 2 + assert_no_difference 'Comment.count' do + post :create, :id => 1, :comment => { :comments => 'This is a test comment' } + assert_response 403 + end + end + + def test_destroy_comment + comments_count = News.find(1).comments.size + @request.session[:user_id] = 2 + delete :destroy, :id => 1, :comment_id => 2 + assert_redirected_to '/news/1' + assert_nil Comment.find_by_id(2) + assert_equal comments_count - 1, News.find(1).comments.size + end +end diff --git a/test/functional/context_menus_controller_test.rb b/test/functional/context_menus_controller_test.rb new file mode 100644 index 00000000..451ea0c9 --- /dev/null +++ b/test/functional/context_menus_controller_test.rb @@ -0,0 +1,252 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class ContextMenusControllerTest < ActionController::TestCase + fixtures :projects, + :trackers, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :workflows, + :journals, :journal_details, + :versions, + :issues, :issue_statuses, :issue_categories, + :users, + :enumerations, + :time_entries + + def test_context_menu_one_issue + @request.session[:user_id] = 2 + get :issues, :ids => [1] + assert_response :success + assert_template 'context_menu' + + assert_select 'a.icon-edit[href=?]', '/issues/1/edit', :text => 'Edit' + assert_select 'a.icon-copy[href=?]', '/projects/ecookbook/issues/1/copy', :text => 'Copy' + assert_select 'a.icon-del[href=?]', '/issues?ids%5B%5D=1', :text => 'Delete' + + # Statuses + assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&issue%5Bstatus_id%5D=5', :text => 'Closed' + assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&issue%5Bpriority_id%5D=8', :text => 'Immediate' + # No inactive priorities + assert_select 'a', :text => /Inactive Priority/, :count => 0 + # Versions + assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&issue%5Bfixed_version_id%5D=3', :text => '2.0' + assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&issue%5Bfixed_version_id%5D=4', :text => 'eCookbook Subproject 1 - 2.0' + # Assignees + assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&issue%5Bassigned_to_id%5D=3', :text => 'Dave Lopper' + end + + def test_context_menu_one_issue_by_anonymous + get :issues, :ids => [1] + assert_response :success + assert_template 'context_menu' + assert_tag :tag => 'a', :content => 'Delete', + :attributes => { :href => '#', + :class => 'icon-del disabled' } + end + + def test_context_menu_multiple_issues_of_same_project + @request.session[:user_id] = 2 + get :issues, :ids => [1, 2] + assert_response :success + assert_template 'context_menu' + assert_not_nil assigns(:issues) + assert_equal [1, 2], assigns(:issues).map(&:id).sort + + ids = assigns(:issues).map(&:id).sort.map {|i| "ids%5B%5D=#{i}"}.join('&') + + assert_select 'a.icon-edit[href=?]', "/issues/bulk_edit?#{ids}", :text => 'Edit' + assert_select 'a.icon-copy[href=?]', "/issues/bulk_edit?copy=1&#{ids}", :text => 'Copy' + assert_select 'a.icon-del[href=?]', "/issues?#{ids}", :text => 'Delete' + + assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&issue%5Bstatus_id%5D=5", :text => 'Closed' + assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&issue%5Bpriority_id%5D=8", :text => 'Immediate' + assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&issue%5Bassigned_to_id%5D=3", :text => 'Dave Lopper' + end + + def test_context_menu_multiple_issues_of_different_projects + @request.session[:user_id] = 2 + get :issues, :ids => [1, 2, 6] + assert_response :success + assert_template 'context_menu' + assert_not_nil assigns(:issues) + assert_equal [1, 2, 6], assigns(:issues).map(&:id).sort + + ids = assigns(:issues).map(&:id).sort.map {|i| "ids%5B%5D=#{i}"}.join('&') + + assert_select 'a.icon-edit[href=?]', "/issues/bulk_edit?#{ids}", :text => 'Edit' + assert_select 'a.icon-del[href=?]', "/issues?#{ids}", :text => 'Delete' + + assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&issue%5Bstatus_id%5D=5", :text => 'Closed' + assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&issue%5Bpriority_id%5D=8", :text => 'Immediate' + assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&issue%5Bassigned_to_id%5D=2", :text => 'John Smith' + end + + def test_context_menu_should_include_list_custom_fields + field = IssueCustomField.create!(:name => 'List', :field_format => 'list', + :possible_values => ['Foo', 'Bar'], :is_for_all => true, :tracker_ids => [1, 2, 3]) + @request.session[:user_id] = 2 + get :issues, :ids => [1] + + assert_select "li.cf_#{field.id}" do + assert_select 'a[href=#]', :text => 'List' + assert_select 'ul' do + assert_select 'a', 3 + assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=Foo", :text => 'Foo' + assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none' + end + end + end + + def test_context_menu_should_not_include_null_value_for_required_custom_fields + field = IssueCustomField.create!(:name => 'List', :is_required => true, :field_format => 'list', + :possible_values => ['Foo', 'Bar'], :is_for_all => true, :tracker_ids => [1, 2, 3]) + @request.session[:user_id] = 2 + get :issues, :ids => [1, 2] + + assert_select "li.cf_#{field.id}" do + assert_select 'a[href=#]', :text => 'List' + assert_select 'ul' do + assert_select 'a', 2 + assert_select 'a', :text => 'none', :count => 0 + end + end + end + + def test_context_menu_on_single_issue_should_select_current_custom_field_value + field = IssueCustomField.create!(:name => 'List', :field_format => 'list', + :possible_values => ['Foo', 'Bar'], :is_for_all => true, :tracker_ids => [1, 2, 3]) + issue = Issue.find(1) + issue.custom_field_values = {field.id => 'Bar'} + issue.save! + @request.session[:user_id] = 2 + get :issues, :ids => [1] + + assert_select "li.cf_#{field.id}" do + assert_select 'a[href=#]', :text => 'List' + assert_select 'ul' do + assert_select 'a', 3 + assert_select 'a.icon-checked', :text => 'Bar' + end + end + end + + def test_context_menu_should_include_bool_custom_fields + field = IssueCustomField.create!(:name => 'Bool', :field_format => 'bool', + :is_for_all => true, :tracker_ids => [1, 2, 3]) + @request.session[:user_id] = 2 + get :issues, :ids => [1] + + assert_select "li.cf_#{field.id}" do + assert_select 'a[href=#]', :text => 'Bool' + assert_select 'ul' do + assert_select 'a', 3 + assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=0", :text => 'No' + assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=1", :text => 'Yes' + assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none' + end + end + end + + def test_context_menu_should_include_user_custom_fields + field = IssueCustomField.create!(:name => 'User', :field_format => 'user', + :is_for_all => true, :tracker_ids => [1, 2, 3]) + @request.session[:user_id] = 2 + get :issues, :ids => [1] + + assert_select "li.cf_#{field.id}" do + assert_select 'a[href=#]', :text => 'User' + assert_select 'ul' do + assert_select 'a', Project.find(1).members.count + 1 + assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=2", :text => 'John Smith' + assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none' + end + end + end + + def test_context_menu_should_include_version_custom_fields + field = IssueCustomField.create!(:name => 'Version', :field_format => 'version', :is_for_all => true, :tracker_ids => [1, 2, 3]) + @request.session[:user_id] = 2 + get :issues, :ids => [1] + + assert_select "li.cf_#{field.id}" do + assert_select 'a[href=#]', :text => 'Version' + assert_select 'ul' do + assert_select 'a', Project.find(1).shared_versions.count + 1 + assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=3", :text => '2.0' + assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none' + end + end + end + + def test_context_menu_by_assignable_user_should_include_assigned_to_me_link + @request.session[:user_id] = 2 + get :issues, :ids => [1] + assert_response :success + assert_template 'context_menu' + + assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&issue%5Bassigned_to_id%5D=2', :text => / me / + end + + def test_context_menu_should_propose_shared_versions_for_issues_from_different_projects + @request.session[:user_id] = 2 + version = Version.create!(:name => 'Shared', :sharing => 'system', :project_id => 1) + + get :issues, :ids => [1, 4] + assert_response :success + assert_template 'context_menu' + + assert_include version, assigns(:versions) + assert_select 'a', :text => 'eCookbook - Shared' + end + + def test_context_menu_issue_visibility + get :issues, :ids => [1, 4] + assert_response :success + assert_template 'context_menu' + assert_equal [1], assigns(:issues).collect(&:id) + end + + def test_should_respond_with_404_without_ids + get :issues + assert_response 404 + end + + def test_time_entries_context_menu + @request.session[:user_id] = 2 + get :time_entries, :ids => [1, 2] + assert_response :success + assert_template 'time_entries' + + assert_select 'a:not(.disabled)', :text => 'Edit' + end + + def test_time_entries_context_menu_without_edit_permission + @request.session[:user_id] = 2 + Role.find_by_name('Manager').remove_permission! :edit_time_entries + + get :time_entries, :ids => [1, 2] + assert_response :success + assert_template 'time_entries' + assert_select 'a.disabled', :text => 'Edit' + end +end diff --git a/test/functional/custom_fields_controller_test.rb b/test/functional/custom_fields_controller_test.rb new file mode 100644 index 00000000..bc642c72 --- /dev/null +++ b/test/functional/custom_fields_controller_test.rb @@ -0,0 +1,176 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class CustomFieldsControllerTest < ActionController::TestCase + fixtures :custom_fields, :custom_values, :trackers, :users + + def setup + @request.session[:user_id] = 1 + end + + def test_index + get :index + assert_response :success + assert_template 'index' + end + + def test_new + custom_field_classes.each do |klass| + get :new, :type => klass.name + assert_response :success + assert_template 'new' + assert_kind_of klass, assigns(:custom_field) + assert_select 'form#custom_field_form' do + assert_select 'select#custom_field_field_format[name=?]', 'custom_field[field_format]' + assert_select 'input[type=hidden][name=type][value=?]', klass.name + end + end + end + + def test_new_issue_custom_field + get :new, :type => 'IssueCustomField' + assert_response :success + assert_template 'new' + assert_select 'form#custom_field_form' do + assert_select 'select#custom_field_field_format[name=?]', 'custom_field[field_format]' do + assert_select 'option[value=user]', :text => 'User' + assert_select 'option[value=version]', :text => 'Version' + end + assert_select 'input[type=hidden][name=type][value=IssueCustomField]' + end + end + + def test_default_value_should_be_an_input_for_string_custom_field + get :new, :type => 'IssueCustomField', :custom_field => {:field_format => 'string'} + assert_response :success + assert_select 'input[name=?]', 'custom_field[default_value]' + end + + def test_default_value_should_be_a_textarea_for_text_custom_field + get :new, :type => 'IssueCustomField', :custom_field => {:field_format => 'text'} + assert_response :success + assert_select 'textarea[name=?]', 'custom_field[default_value]' + end + + def test_default_value_should_be_a_checkbox_for_bool_custom_field + get :new, :type => 'IssueCustomField', :custom_field => {:field_format => 'bool'} + assert_response :success + assert_select 'input[name=?][type=checkbox]', 'custom_field[default_value]' + end + + def test_default_value_should_not_be_present_for_user_custom_field + get :new, :type => 'IssueCustomField', :custom_field => {:field_format => 'user'} + assert_response :success + assert_select '[name=?]', 'custom_field[default_value]', 0 + end + + def test_new_js + get :new, :type => 'IssueCustomField', :custom_field => {:field_format => 'list'}, :format => 'js' + assert_response :success + assert_template 'new' + assert_equal 'text/javascript', response.content_type + + field = assigns(:custom_field) + assert_equal 'list', field.field_format + end + + def test_new_with_invalid_custom_field_class_should_render_404 + get :new, :type => 'UnknownCustomField' + assert_response 404 + end + + def test_create_list_custom_field + assert_difference 'CustomField.count' do + post :create, :type => "IssueCustomField", + :custom_field => {:name => "test_post_new_list", + :default_value => "", + :min_length => "0", + :searchable => "0", + :regexp => "", + :is_for_all => "1", + :possible_values => "0.1\n0.2\n", + :max_length => "0", + :is_filter => "0", + :is_required =>"0", + :field_format => "list", + :tracker_ids => ["1", ""]} + end + assert_redirected_to '/custom_fields?tab=IssueCustomField' + field = IssueCustomField.find_by_name('test_post_new_list') + assert_not_nil field + assert_equal ["0.1", "0.2"], field.possible_values + assert_equal 1, field.trackers.size + end + + def test_create_with_failure + assert_no_difference 'CustomField.count' do + post :create, :type => "IssueCustomField", :custom_field => {:name => ''} + end + assert_response :success + assert_template 'new' + end + + def test_edit + get :edit, :id => 1 + assert_response :success + assert_template 'edit' + assert_tag 'input', :attributes => {:name => 'custom_field[name]', :value => 'Database'} + end + + def test_edit_invalid_custom_field_should_render_404 + get :edit, :id => 99 + assert_response 404 + end + + def test_update + put :update, :id => 1, :custom_field => {:name => 'New name'} + assert_redirected_to '/custom_fields?tab=IssueCustomField' + + field = CustomField.find(1) + assert_equal 'New name', field.name + end + + def test_update_with_failure + put :update, :id => 1, :custom_field => {:name => ''} + assert_response :success + assert_template 'edit' + end + + def test_destroy + custom_values_count = CustomValue.count(:conditions => {:custom_field_id => 1}) + assert custom_values_count > 0 + + assert_difference 'CustomField.count', -1 do + assert_difference 'CustomValue.count', - custom_values_count do + delete :destroy, :id => 1 + end + end + + assert_redirected_to '/custom_fields?tab=IssueCustomField' + assert_nil CustomField.find_by_id(1) + assert_nil CustomValue.find_by_custom_field_id(1) + end + + def custom_field_classes + files = Dir.glob(File.join(Rails.root, 'app/models/*_custom_field.rb')).map {|f| File.basename(f).sub(/\.rb$/, '') } + classes = files.map(&:classify).map(&:constantize) + assert classes.size > 0 + classes + end +end diff --git a/test/functional/documents_controller_test.rb b/test/functional/documents_controller_test.rb new file mode 100644 index 00000000..580ffe80 --- /dev/null +++ b/test/functional/documents_controller_test.rb @@ -0,0 +1,186 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class DocumentsControllerTest < ActionController::TestCase + fixtures :projects, :users, :roles, :members, :member_roles, + :enabled_modules, :documents, :enumerations, + :groups_users, :attachments + + def setup + User.current = nil + end + + def test_index + # Sets a default category + e = Enumeration.find_by_name('Technical documentation') + e.update_attributes(:is_default => true) + + get :index, :project_id => 'ecookbook' + assert_response :success + assert_template 'index' + assert_not_nil assigns(:grouped) + + # Default category selected in the new document form + assert_tag :select, :attributes => {:name => 'document[category_id]'}, + :child => {:tag => 'option', :attributes => {:selected => 'selected'}, + :content => 'Technical documentation'} + + assert ! DocumentCategory.find(16).active? + assert_no_tag :option, :attributes => {:value => '16'}, + :parent => {:tag => 'select', :attributes => {:id => 'document_category_id'} } + end + + def test_index_grouped_by_date + get :index, :project_id => 'ecookbook', :sort_by => 'date' + assert_response :success + assert_tag 'h3', :content => '2007-02-12' + end + + def test_index_grouped_by_title + get :index, :project_id => 'ecookbook', :sort_by => 'title' + assert_response :success + assert_tag 'h3', :content => 'T' + end + + def test_index_grouped_by_author + get :index, :project_id => 'ecookbook', :sort_by => 'author' + assert_response :success + assert_tag 'h3', :content => 'John Smith' + end + + def test_index_with_long_description + # adds a long description to the first document + doc = documents(:documents_001) + doc.update_attributes(:description => < 'ecookbook' + assert_response :success + assert_template 'index' + + # should only truncate on new lines to avoid breaking wiki formatting + assert_select '.wiki p', :text => (doc.description.split("\n").first + '...') + assert_select '.wiki p', :text => Regexp.new(Regexp.escape("EndOfLineHere...")) + end + + def test_show + get :show, :id => 1 + assert_response :success + assert_template 'show' + end + + def test_new + @request.session[:user_id] = 2 + get :new, :project_id => 1 + assert_response :success + assert_template 'new' + end + + def test_create_with_one_attachment + ActionMailer::Base.deliveries.clear + @request.session[:user_id] = 2 + set_tmp_attachments_directory + + with_settings :notified_events => %w(document_added) do + post :create, :project_id => 'ecookbook', + :document => { :title => 'DocumentsControllerTest#test_post_new', + :description => 'This is a new document', + :category_id => 2}, + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}} + end + assert_redirected_to '/projects/ecookbook/documents' + + document = Document.find_by_title('DocumentsControllerTest#test_post_new') + assert_not_nil document + assert_equal Enumeration.find(2), document.category + assert_equal 1, document.attachments.size + assert_equal 'testfile.txt', document.attachments.first.filename + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_create_with_failure + @request.session[:user_id] = 2 + assert_no_difference 'Document.count' do + post :create, :project_id => 'ecookbook', :document => { :title => ''} + end + assert_response :success + assert_template 'new' + end + + def test_create_non_default_category + @request.session[:user_id] = 2 + category2 = Enumeration.find_by_name('User documentation') + category2.update_attributes(:is_default => true) + category1 = Enumeration.find_by_name('Uncategorized') + post :create, + :project_id => 'ecookbook', + :document => { :title => 'no default', + :description => 'This is a new document', + :category_id => category1.id } + assert_redirected_to '/projects/ecookbook/documents' + doc = Document.find_by_title('no default') + assert_not_nil doc + assert_equal category1.id, doc.category_id + assert_equal category1, doc.category + end + + def test_edit + @request.session[:user_id] = 2 + get :edit, :id => 1 + assert_response :success + assert_template 'edit' + end + + def test_update + @request.session[:user_id] = 2 + put :update, :id => 1, :document => {:title => 'test_update'} + assert_redirected_to '/documents/1' + document = Document.find(1) + assert_equal 'test_update', document.title + end + + def test_update_with_failure + @request.session[:user_id] = 2 + put :update, :id => 1, :document => {:title => ''} + assert_response :success + assert_template 'edit' + end + + def test_destroy + @request.session[:user_id] = 2 + assert_difference 'Document.count', -1 do + delete :destroy, :id => 1 + end + assert_redirected_to '/projects/ecookbook/documents' + assert_nil Document.find_by_id(1) + end + + def test_add_attachment + @request.session[:user_id] = 2 + assert_difference 'Attachment.count' do + post :add_attachment, :id => 1, + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}} + end + attachment = Attachment.first(:order => 'id DESC') + assert_equal Document.find(1), attachment.container + end +end diff --git a/test/functional/enumerations_controller_test.rb b/test/functional/enumerations_controller_test.rb new file mode 100644 index 00000000..9dc273d4 --- /dev/null +++ b/test/functional/enumerations_controller_test.rb @@ -0,0 +1,136 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class EnumerationsControllerTest < ActionController::TestCase + fixtures :enumerations, :issues, :users + + def setup + @request.session[:user_id] = 1 # admin + end + + def test_index + get :index + assert_response :success + assert_template 'index' + end + + def test_index_should_require_admin + @request.session[:user_id] = nil + get :index + assert_response 302 + end + + def test_new + get :new, :type => 'IssuePriority' + assert_response :success + assert_template 'new' + assert_kind_of IssuePriority, assigns(:enumeration) + assert_tag 'input', :attributes => {:name => 'enumeration[type]', :value => 'IssuePriority'} + assert_tag 'input', :attributes => {:name => 'enumeration[name]'} + end + + def test_new_with_invalid_type_should_respond_with_404 + get :new, :type => 'UnknownType' + assert_response 404 + end + + def test_create + assert_difference 'IssuePriority.count' do + post :create, :enumeration => {:type => 'IssuePriority', :name => 'Lowest'} + end + assert_redirected_to '/enumerations' + e = IssuePriority.find_by_name('Lowest') + assert_not_nil e + end + + def test_create_with_failure + assert_no_difference 'IssuePriority.count' do + post :create, :enumeration => {:type => 'IssuePriority', :name => ''} + end + assert_response :success + assert_template 'new' + end + + def test_edit + get :edit, :id => 6 + assert_response :success + assert_template 'edit' + assert_tag 'input', :attributes => {:name => 'enumeration[name]', :value => 'High'} + end + + def test_edit_invalid_should_respond_with_404 + get :edit, :id => 999 + assert_response 404 + end + + def test_update + assert_no_difference 'IssuePriority.count' do + put :update, :id => 6, :enumeration => {:type => 'IssuePriority', :name => 'New name'} + end + assert_redirected_to '/enumerations' + e = IssuePriority.find(6) + assert_equal 'New name', e.name + end + + def test_update_with_failure + assert_no_difference 'IssuePriority.count' do + put :update, :id => 6, :enumeration => {:type => 'IssuePriority', :name => ''} + end + assert_response :success + assert_template 'edit' + end + + def test_destroy_enumeration_not_in_use + assert_difference 'IssuePriority.count', -1 do + delete :destroy, :id => 7 + end + assert_redirected_to :controller => 'enumerations', :action => 'index' + assert_nil Enumeration.find_by_id(7) + end + + def test_destroy_enumeration_in_use + assert_no_difference 'IssuePriority.count' do + delete :destroy, :id => 4 + end + assert_response :success + assert_template 'destroy' + assert_not_nil Enumeration.find_by_id(4) + assert_select 'select[name=reassign_to_id]' do + assert_select 'option[value=6]', :text => 'High' + end + end + + def test_destroy_enumeration_in_use_with_reassignment + issue = Issue.where(:priority_id => 4).first + assert_difference 'IssuePriority.count', -1 do + delete :destroy, :id => 4, :reassign_to_id => 6 + end + assert_redirected_to :controller => 'enumerations', :action => 'index' + assert_nil Enumeration.find_by_id(4) + # check that the issue was reassign + assert_equal 6, issue.reload.priority_id + end + + def test_destroy_enumeration_in_use_with_blank_reassignment + assert_no_difference 'IssuePriority.count' do + delete :destroy, :id => 4, :reassign_to_id => '' + end + assert_response :success + end +end diff --git a/test/functional/files_controller_test.rb b/test/functional/files_controller_test.rb new file mode 100644 index 00000000..879ec0c9 --- /dev/null +++ b/test/functional/files_controller_test.rb @@ -0,0 +1,109 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class FilesControllerTest < ActionController::TestCase + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :journals, :journal_details, + :attachments, + :versions + + def setup + @request.session[:user_id] = nil + Setting.default_language = 'en' + end + + def test_index + get :index, :project_id => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:containers) + + # file attached to the project + assert_tag :a, :content => 'project_file.zip', + :attributes => { :href => '/attachments/download/8/project_file.zip' } + + # file attached to a project's version + assert_tag :a, :content => 'version_file.zip', + :attributes => { :href => '/attachments/download/9/version_file.zip' } + end + + def test_new + @request.session[:user_id] = 2 + get :new, :project_id => 1 + assert_response :success + assert_template 'new' + + assert_tag 'select', :attributes => {:name => 'version_id'} + end + + def test_new_without_versions + Version.delete_all + @request.session[:user_id] = 2 + get :new, :project_id => 1 + assert_response :success + assert_template 'new' + + assert_no_tag 'select', :attributes => {:name => 'version_id'} + end + + def test_create_file + set_tmp_attachments_directory + @request.session[:user_id] = 2 + ActionMailer::Base.deliveries.clear + + with_settings :notified_events => %w(file_added) do + assert_difference 'Attachment.count' do + post :create, :project_id => 1, :version_id => '', + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}} + assert_response :redirect + end + end + assert_redirected_to '/projects/ecookbook/files' + a = Attachment.order('created_on DESC').first + assert_equal 'testfile.txt', a.filename + assert_equal Project.find(1), a.container + + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + assert_equal "[eCookbook] New file", mail.subject + assert_mail_body_match 'testfile.txt', mail + end + + def test_create_version_file + set_tmp_attachments_directory + @request.session[:user_id] = 2 + + assert_difference 'Attachment.count' do + post :create, :project_id => 1, :version_id => '2', + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}} + assert_response :redirect + end + assert_redirected_to '/projects/ecookbook/files' + a = Attachment.order('created_on DESC').first + assert_equal 'testfile.txt', a.filename + assert_equal Version.find(2), a.container + end + +end diff --git a/test/functional/gantts_controller_test.rb b/test/functional/gantts_controller_test.rb new file mode 100644 index 00000000..ce8dd983 --- /dev/null +++ b/test/functional/gantts_controller_test.rb @@ -0,0 +1,122 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class GanttsControllerTest < ActionController::TestCase + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :versions + + def test_gantt_should_work + i2 = Issue.find(2) + i2.update_attribute(:due_date, 1.month.from_now) + get :show, :project_id => 1 + assert_response :success + assert_template 'gantts/show' + assert_not_nil assigns(:gantt) + # Issue with start and due dates + i = Issue.find(1) + assert_not_nil i.due_date + assert_select "div a.issue", /##{i.id}/ + # Issue with on a targeted version should not be in the events but loaded in the html + i = Issue.find(2) + assert_select "div a.issue", /##{i.id}/ + end + + def test_gantt_should_work_without_issue_due_dates + Issue.update_all("due_date = NULL") + get :show, :project_id => 1 + assert_response :success + assert_template 'gantts/show' + assert_not_nil assigns(:gantt) + end + + def test_gantt_should_work_without_issue_and_version_due_dates + Issue.update_all("due_date = NULL") + Version.update_all("effective_date = NULL") + get :show, :project_id => 1 + assert_response :success + assert_template 'gantts/show' + assert_not_nil assigns(:gantt) + end + + def test_gantt_should_work_cross_project + get :show + assert_response :success + assert_template 'gantts/show' + assert_not_nil assigns(:gantt) + assert_not_nil assigns(:gantt).query + assert_nil assigns(:gantt).project + end + + def test_gantt_should_not_disclose_private_projects + get :show + assert_response :success + assert_template 'gantts/show' + assert_tag 'a', :content => /eCookbook/ + # Root private project + assert_no_tag 'a', {:content => /OnlineStore/} + # Private children of a public project + assert_no_tag 'a', :content => /Private child of eCookbook/ + end + + def test_gantt_should_display_relations + IssueRelation.delete_all + issue1 = Issue.generate!(:start_date => 1.day.from_now, :due_date => 3.day.from_now) + issue2 = Issue.generate!(:start_date => 1.day.from_now, :due_date => 3.day.from_now) + IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => 'precedes') + + get :show + assert_response :success + + relations = assigns(:gantt).relations + assert_kind_of Hash, relations + assert relations.present? + assert_select 'div.task_todo[id=?][data-rels*=?]', "task-todo-issue-#{issue1.id}", issue2.id.to_s + assert_select 'div.task_todo[id=?]:not([data-rels])', "task-todo-issue-#{issue2.id}" + end + + def test_gantt_should_export_to_pdf + get :show, :project_id => 1, :format => 'pdf' + assert_response :success + assert_equal 'application/pdf', @response.content_type + assert @response.body.starts_with?('%PDF') + assert_not_nil assigns(:gantt) + end + + def test_gantt_should_export_to_pdf_cross_project + get :show, :format => 'pdf' + assert_response :success + assert_equal 'application/pdf', @response.content_type + assert @response.body.starts_with?('%PDF') + assert_not_nil assigns(:gantt) + end + + if Object.const_defined?(:Magick) + def test_gantt_should_export_to_png + get :show, :project_id => 1, :format => 'png' + assert_response :success + assert_equal 'image/png', @response.content_type + end + end +end diff --git a/test/functional/groups_controller_test.rb b/test/functional/groups_controller_test.rb new file mode 100644 index 00000000..2034d485 --- /dev/null +++ b/test/functional/groups_controller_test.rb @@ -0,0 +1,202 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class GroupsControllerTest < ActionController::TestCase + fixtures :projects, :users, :members, :member_roles, :roles, :groups_users + + def setup + @request.session[:user_id] = 1 + end + + def test_index + get :index + assert_response :success + assert_template 'index' + end + + def test_show + get :show, :id => 10 + assert_response :success + assert_template 'show' + end + + def test_show_invalid_should_return_404 + get :show, :id => 99 + assert_response 404 + end + + def test_new + get :new + assert_response :success + assert_template 'new' + assert_select 'input[name=?]', 'group[name]' + end + + def test_create + assert_difference 'Group.count' do + post :create, :group => {:name => 'New group'} + end + assert_redirected_to '/groups' + group = Group.first(:order => 'id DESC') + assert_equal 'New group', group.name + assert_equal [], group.users + end + + def test_create_and_continue + assert_difference 'Group.count' do + post :create, :group => {:name => 'New group'}, :continue => 'Create and continue' + end + assert_redirected_to '/groups/new' + group = Group.first(:order => 'id DESC') + assert_equal 'New group', group.name + end + + def test_create_with_failure + assert_no_difference 'Group.count' do + post :create, :group => {:name => ''} + end + assert_response :success + assert_template 'new' + end + + def test_edit + get :edit, :id => 10 + assert_response :success + assert_template 'edit' + + assert_select 'div#tab-content-users' + assert_select 'div#tab-content-memberships' do + assert_select 'a', :text => 'Private child of eCookbook' + end + end + + def test_update + new_name = 'New name' + put :update, :id => 10, :group => {:name => new_name} + assert_redirected_to '/groups' + group = Group.find(10) + assert_equal new_name, group.name + end + + def test_update_with_failure + put :update, :id => 10, :group => {:name => ''} + assert_response :success + assert_template 'edit' + end + + def test_destroy + assert_difference 'Group.count', -1 do + post :destroy, :id => 10 + end + assert_redirected_to '/groups' + end + + def test_add_users + assert_difference 'Group.find(10).users.count', 2 do + post :add_users, :id => 10, :user_ids => ['2', '3'] + end + end + + def test_xhr_add_users + assert_difference 'Group.find(10).users.count', 2 do + xhr :post, :add_users, :id => 10, :user_ids => ['2', '3'] + assert_response :success + assert_template 'add_users' + assert_equal 'text/javascript', response.content_type + end + assert_match /John Smith/, response.body + end + + def test_remove_user + assert_difference 'Group.find(10).users.count', -1 do + delete :remove_user, :id => 10, :user_id => '8' + end + end + + def test_xhr_remove_user + assert_difference 'Group.find(10).users.count', -1 do + xhr :delete, :remove_user, :id => 10, :user_id => '8' + assert_response :success + assert_template 'remove_user' + assert_equal 'text/javascript', response.content_type + end + end + + def test_new_membership + assert_difference 'Group.find(10).members.count' do + post :edit_membership, :id => 10, :membership => { :project_id => 2, :role_ids => ['1', '2']} + end + end + + def test_xhr_new_membership + assert_difference 'Group.find(10).members.count' do + xhr :post, :edit_membership, :id => 10, :membership => { :project_id => 2, :role_ids => ['1', '2']} + assert_response :success + assert_template 'edit_membership' + assert_equal 'text/javascript', response.content_type + end + assert_match /OnlineStore/, response.body + end + + def test_xhr_new_membership_with_failure + assert_no_difference 'Group.find(10).members.count' do + xhr :post, :edit_membership, :id => 10, :membership => { :project_id => 999, :role_ids => ['1', '2']} + assert_response :success + assert_template 'edit_membership' + assert_equal 'text/javascript', response.content_type + end + assert_match /alert/, response.body, "Alert message not sent" + end + + def test_edit_membership + assert_no_difference 'Group.find(10).members.count' do + post :edit_membership, :id => 10, :membership_id => 6, :membership => { :role_ids => ['1', '3']} + end + end + + def test_xhr_edit_membership + assert_no_difference 'Group.find(10).members.count' do + xhr :post, :edit_membership, :id => 10, :membership_id => 6, :membership => { :role_ids => ['1', '3']} + assert_response :success + assert_template 'edit_membership' + assert_equal 'text/javascript', response.content_type + end + end + + def test_destroy_membership + assert_difference 'Group.find(10).members.count', -1 do + post :destroy_membership, :id => 10, :membership_id => 6 + end + end + + def test_xhr_destroy_membership + assert_difference 'Group.find(10).members.count', -1 do + xhr :post, :destroy_membership, :id => 10, :membership_id => 6 + assert_response :success + assert_template 'destroy_membership' + assert_equal 'text/javascript', response.content_type + end + end + + def test_autocomplete_for_user + get :autocomplete_for_user, :id => 10, :q => 'smi', :format => 'js' + assert_response :success + assert_include 'John Smith', response.body + end +end diff --git a/test/functional/issue_categories_controller_test.rb b/test/functional/issue_categories_controller_test.rb new file mode 100644 index 00000000..eef52bf5 --- /dev/null +++ b/test/functional/issue_categories_controller_test.rb @@ -0,0 +1,145 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class IssueCategoriesControllerTest < ActionController::TestCase + fixtures :projects, :users, :members, :member_roles, :roles, :enabled_modules, :issue_categories, + :issues + + def setup + User.current = nil + @request.session[:user_id] = 2 + end + + def test_new + @request.session[:user_id] = 2 # manager + get :new, :project_id => '1' + assert_response :success + assert_template 'new' + assert_select 'input[name=?]', 'issue_category[name]' + end + + def test_new_from_issue_form + @request.session[:user_id] = 2 # manager + xhr :get, :new, :project_id => '1' + + assert_response :success + assert_template 'new' + assert_equal 'text/javascript', response.content_type + end + + def test_create + @request.session[:user_id] = 2 # manager + assert_difference 'IssueCategory.count' do + post :create, :project_id => '1', :issue_category => {:name => 'New category'} + end + assert_redirected_to '/projects/ecookbook/settings/categories' + category = IssueCategory.find_by_name('New category') + assert_not_nil category + assert_equal 1, category.project_id + end + + def test_create_failure + @request.session[:user_id] = 2 + post :create, :project_id => '1', :issue_category => {:name => ''} + assert_response :success + assert_template 'new' + end + + def test_create_from_issue_form + @request.session[:user_id] = 2 # manager + assert_difference 'IssueCategory.count' do + xhr :post, :create, :project_id => '1', :issue_category => {:name => 'New category'} + end + category = IssueCategory.first(:order => 'id DESC') + assert_equal 'New category', category.name + + assert_response :success + assert_template 'create' + assert_equal 'text/javascript', response.content_type + end + + def test_create_from_issue_form_with_failure + @request.session[:user_id] = 2 # manager + assert_no_difference 'IssueCategory.count' do + xhr :post, :create, :project_id => '1', :issue_category => {:name => ''} + end + + assert_response :success + assert_template 'new' + assert_equal 'text/javascript', response.content_type + end + + def test_edit + @request.session[:user_id] = 2 + get :edit, :id => 2 + assert_response :success + assert_template 'edit' + assert_select 'input[name=?][value=?]', 'issue_category[name]', 'Recipes' + end + + def test_update + assert_no_difference 'IssueCategory.count' do + put :update, :id => 2, :issue_category => { :name => 'Testing' } + end + assert_redirected_to '/projects/ecookbook/settings/categories' + assert_equal 'Testing', IssueCategory.find(2).name + end + + def test_update_failure + put :update, :id => 2, :issue_category => { :name => '' } + assert_response :success + assert_template 'edit' + end + + def test_update_not_found + put :update, :id => 97, :issue_category => { :name => 'Testing' } + assert_response 404 + end + + def test_destroy_category_not_in_use + delete :destroy, :id => 2 + assert_redirected_to '/projects/ecookbook/settings/categories' + assert_nil IssueCategory.find_by_id(2) + end + + def test_destroy_category_in_use + delete :destroy, :id => 1 + assert_response :success + assert_template 'destroy' + assert_not_nil IssueCategory.find_by_id(1) + end + + def test_destroy_category_in_use_with_reassignment + issue = Issue.where(:category_id => 1).first + delete :destroy, :id => 1, :todo => 'reassign', :reassign_to_id => 2 + assert_redirected_to '/projects/ecookbook/settings/categories' + assert_nil IssueCategory.find_by_id(1) + # check that the issue was reassign + assert_equal 2, issue.reload.category_id + end + + def test_destroy_category_in_use_without_reassignment + issue = Issue.where(:category_id => 1).first + delete :destroy, :id => 1, :todo => 'nullify' + assert_redirected_to '/projects/ecookbook/settings/categories' + assert_nil IssueCategory.find_by_id(1) + # check that the issue category was nullified + assert_nil issue.reload.category_id + end +end diff --git a/test/functional/issue_relations_controller_test.rb b/test/functional/issue_relations_controller_test.rb new file mode 100644 index 00000000..055c5835 --- /dev/null +++ b/test/functional/issue_relations_controller_test.rb @@ -0,0 +1,147 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class IssueRelationsControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :issue_relations, + :enabled_modules, + :enumerations, + :trackers, + :projects_trackers + + def setup + User.current = nil + @request.session[:user_id] = 3 + end + + def test_create + assert_difference 'IssueRelation.count' do + post :create, :issue_id => 1, + :relation => {:issue_to_id => '2', :relation_type => 'relates', :delay => ''} + end + relation = IssueRelation.first(:order => 'id DESC') + assert_equal 1, relation.issue_from_id + assert_equal 2, relation.issue_to_id + assert_equal 'relates', relation.relation_type + end + + def test_create_xhr + assert_difference 'IssueRelation.count' do + xhr :post, :create, :issue_id => 3, :relation => {:issue_to_id => '1', :relation_type => 'relates', :delay => ''} + assert_response :success + assert_template 'create' + assert_equal 'text/javascript', response.content_type + end + relation = IssueRelation.first(:order => 'id DESC') + assert_equal 3, relation.issue_from_id + assert_equal 1, relation.issue_to_id + + assert_match /Bug #1/, response.body + end + + def test_create_should_accept_id_with_hash + assert_difference 'IssueRelation.count' do + post :create, :issue_id => 1, + :relation => {:issue_to_id => '#2', :relation_type => 'relates', :delay => ''} + end + relation = IssueRelation.first(:order => 'id DESC') + assert_equal 2, relation.issue_to_id + end + + def test_create_should_strip_id + assert_difference 'IssueRelation.count' do + post :create, :issue_id => 1, + :relation => {:issue_to_id => ' 2 ', :relation_type => 'relates', :delay => ''} + end + relation = IssueRelation.first(:order => 'id DESC') + assert_equal 2, relation.issue_to_id + end + + def test_create_should_not_break_with_non_numerical_id + assert_no_difference 'IssueRelation.count' do + assert_nothing_raised do + post :create, :issue_id => 1, + :relation => {:issue_to_id => 'foo', :relation_type => 'relates', :delay => ''} + end + end + end + + def test_create_follows_relation_should_update_relations_list + issue1 = Issue.generate!(:subject => 'Followed issue', :start_date => Date.yesterday, :due_date => Date.today) + issue2 = Issue.generate! + + assert_difference 'IssueRelation.count' do + xhr :post, :create, :issue_id => issue2.id, + :relation => {:issue_to_id => issue1.id, :relation_type => 'follows', :delay => ''} + end + assert_match /Followed issue/, response.body + end + + def test_should_create_relations_with_visible_issues_only + Setting.cross_project_issue_relations = '1' + assert_nil Issue.visible(User.find(3)).find_by_id(4) + + assert_no_difference 'IssueRelation.count' do + post :create, :issue_id => 1, + :relation => {:issue_to_id => '4', :relation_type => 'relates', :delay => ''} + end + end + + should "prevent relation creation when there's a circular dependency" + + def test_create_xhr_with_failure + assert_no_difference 'IssueRelation.count' do + xhr :post, :create, :issue_id => 3, :relation => {:issue_to_id => '999', :relation_type => 'relates', :delay => ''} + + assert_response :success + assert_template 'create' + assert_equal 'text/javascript', response.content_type + end + + assert_match /errorExplanation/, response.body + end + + def test_destroy + assert_difference 'IssueRelation.count', -1 do + delete :destroy, :id => '2' + end + end + + def test_destroy_xhr + IssueRelation.create!(:relation_type => IssueRelation::TYPE_RELATES) do |r| + r.issue_from_id = 3 + r.issue_to_id = 1 + end + + assert_difference 'IssueRelation.count', -1 do + xhr :delete, :destroy, :id => '2' + + assert_response :success + assert_template 'destroy' + assert_equal 'text/javascript', response.content_type + assert_match /relation-2/, response.body + end + end +end diff --git a/test/functional/issue_statuses_controller_test.rb b/test/functional/issue_statuses_controller_test.rb new file mode 100644 index 00000000..1e7d2fab --- /dev/null +++ b/test/functional/issue_statuses_controller_test.rb @@ -0,0 +1,123 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class IssueStatusesControllerTest < ActionController::TestCase + fixtures :issue_statuses, :issues, :users + + def setup + User.current = nil + @request.session[:user_id] = 1 # admin + end + + def test_index + get :index + assert_response :success + assert_template 'index' + end + + def test_index_by_anonymous_should_redirect_to_login_form + @request.session[:user_id] = nil + get :index + assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fissue_statuses' + end + + def test_index_by_user_should_respond_with_406 + @request.session[:user_id] = 2 + get :index + assert_response 406 + end + + def test_new + get :new + assert_response :success + assert_template 'new' + end + + def test_create + assert_difference 'IssueStatus.count' do + post :create, :issue_status => {:name => 'New status'} + end + assert_redirected_to :action => 'index' + status = IssueStatus.order('id DESC').first + assert_equal 'New status', status.name + end + + def test_create_with_failure + post :create, :issue_status => {:name => ''} + assert_response :success + assert_template 'new' + assert_error_tag :content => /name can't be blank/i + end + + def test_edit + get :edit, :id => '3' + assert_response :success + assert_template 'edit' + end + + def test_update + put :update, :id => '3', :issue_status => {:name => 'Renamed status'} + assert_redirected_to :action => 'index' + status = IssueStatus.find(3) + assert_equal 'Renamed status', status.name + end + + def test_update_with_failure + put :update, :id => '3', :issue_status => {:name => ''} + assert_response :success + assert_template 'edit' + assert_error_tag :content => /name can't be blank/i + end + + def test_destroy + Issue.delete_all("status_id = 1") + + assert_difference 'IssueStatus.count', -1 do + delete :destroy, :id => '1' + end + assert_redirected_to :action => 'index' + assert_nil IssueStatus.find_by_id(1) + end + + def test_destroy_should_block_if_status_in_use + assert_not_nil Issue.find_by_status_id(1) + + assert_no_difference 'IssueStatus.count' do + delete :destroy, :id => '1' + end + assert_redirected_to :action => 'index' + assert_not_nil IssueStatus.find_by_id(1) + end + + def test_update_issue_done_ratio_with_issue_done_ratio_set_to_issue_field + with_settings :issue_done_ratio => 'issue_field' do + post :update_issue_done_ratio + assert_match /not updated/, flash[:error].to_s + assert_redirected_to '/issue_statuses' + end + end + + def test_update_issue_done_ratio_with_issue_done_ratio_set_to_issue_status + with_settings :issue_done_ratio => 'issue_status' do + post :update_issue_done_ratio + assert_match /Issue done ratios updated/, flash[:notice].to_s + assert_redirected_to '/issue_statuses' + end + end +end diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb new file mode 100644 index 00000000..d0ead9b0 --- /dev/null +++ b/test/functional/issues_controller_test.rb @@ -0,0 +1,3898 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class IssuesControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries, + :repositories, + :changesets + + include Redmine::I18n + + def setup + User.current = nil + end + + def test_index + with_settings :default_language => "en" do + get :index + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues) + assert_nil assigns(:project) + + # links to visible issues + assert_select 'a[href=/issues/1]', :text => /Can't print recipes/ + assert_select 'a[href=/issues/5]', :text => /Subproject issue/ + # private projects hidden + assert_select 'a[href=/issues/6]', 0 + assert_select 'a[href=/issues/4]', 0 + # project column + assert_select 'th', :text => /Project/ + end + end + + def test_index_should_not_list_issues_when_module_disabled + EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1") + get :index + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues) + assert_nil assigns(:project) + + assert_select 'a[href=/issues/1]', 0 + assert_select 'a[href=/issues/5]', :text => /Subproject issue/ + end + + def test_index_should_list_visible_issues_only + get :index, :per_page => 100 + assert_response :success + assert_not_nil assigns(:issues) + assert_nil assigns(:issues).detect {|issue| !issue.visible?} + end + + def test_index_with_project + Setting.display_subprojects_issues = 0 + get :index, :project_id => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues) + + assert_select 'a[href=/issues/1]', :text => /Can't print recipes/ + assert_select 'a[href=/issues/5]', 0 + end + + def test_index_with_project_and_subprojects + Setting.display_subprojects_issues = 1 + get :index, :project_id => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues) + + assert_select 'a[href=/issues/1]', :text => /Can't print recipes/ + assert_select 'a[href=/issues/5]', :text => /Subproject issue/ + assert_select 'a[href=/issues/6]', 0 + end + + def test_index_with_project_and_subprojects_should_show_private_subprojects_with_permission + @request.session[:user_id] = 2 + Setting.display_subprojects_issues = 1 + get :index, :project_id => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues) + + assert_select 'a[href=/issues/1]', :text => /Can't print recipes/ + assert_select 'a[href=/issues/5]', :text => /Subproject issue/ + assert_select 'a[href=/issues/6]', :text => /Issue of a private subproject/ + end + + def test_index_with_project_and_default_filter + get :index, :project_id => 1, :set_filter => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues) + + query = assigns(:query) + assert_not_nil query + # default filter + assert_equal({'status_id' => {:operator => 'o', :values => ['']}}, query.filters) + end + + def test_index_with_project_and_filter + get :index, :project_id => 1, :set_filter => 1, + :f => ['tracker_id'], + :op => {'tracker_id' => '='}, + :v => {'tracker_id' => ['1']} + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues) + + query = assigns(:query) + assert_not_nil query + assert_equal({'tracker_id' => {:operator => '=', :values => ['1']}}, query.filters) + end + + def test_index_with_short_filters + to_test = { + 'status_id' => { + 'o' => { :op => 'o', :values => [''] }, + 'c' => { :op => 'c', :values => [''] }, + '7' => { :op => '=', :values => ['7'] }, + '7|3|4' => { :op => '=', :values => ['7', '3', '4'] }, + '=7' => { :op => '=', :values => ['7'] }, + '!3' => { :op => '!', :values => ['3'] }, + '!7|3|4' => { :op => '!', :values => ['7', '3', '4'] }}, + 'subject' => { + 'This is a subject' => { :op => '=', :values => ['This is a subject'] }, + 'o' => { :op => '=', :values => ['o'] }, + '~This is part of a subject' => { :op => '~', :values => ['This is part of a subject'] }, + '!~This is part of a subject' => { :op => '!~', :values => ['This is part of a subject'] }}, + 'tracker_id' => { + '3' => { :op => '=', :values => ['3'] }, + '=3' => { :op => '=', :values => ['3'] }}, + 'start_date' => { + '2011-10-12' => { :op => '=', :values => ['2011-10-12'] }, + '=2011-10-12' => { :op => '=', :values => ['2011-10-12'] }, + '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] }, + '<=2011-10-12' => { :op => '<=', :values => ['2011-10-12'] }, + '><2011-10-01|2011-10-30' => { :op => '><', :values => ['2011-10-01', '2011-10-30'] }, + ' { :op => ' ['2'] }, + '>t+2' => { :op => '>t+', :values => ['2'] }, + 't+2' => { :op => 't+', :values => ['2'] }, + 't' => { :op => 't', :values => [''] }, + 'w' => { :op => 'w', :values => [''] }, + '>t-2' => { :op => '>t-', :values => ['2'] }, + ' { :op => ' ['2'] }, + 't-2' => { :op => 't-', :values => ['2'] }}, + 'created_on' => { + '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] }, + ' { :op => ' ['2'] }, + '>t-2' => { :op => '>t-', :values => ['2'] }, + 't-2' => { :op => 't-', :values => ['2'] }}, + 'cf_1' => { + 'c' => { :op => '=', :values => ['c'] }, + '!c' => { :op => '!', :values => ['c'] }, + '!*' => { :op => '!*', :values => [''] }, + '*' => { :op => '*', :values => [''] }}, + 'estimated_hours' => { + '=13.4' => { :op => '=', :values => ['13.4'] }, + '>=45' => { :op => '>=', :values => ['45'] }, + '<=125' => { :op => '<=', :values => ['125'] }, + '><10.5|20.5' => { :op => '><', :values => ['10.5', '20.5'] }, + '!*' => { :op => '!*', :values => [''] }, + '*' => { :op => '*', :values => [''] }} + } + + default_filter = { 'status_id' => {:operator => 'o', :values => [''] }} + + to_test.each do |field, expression_and_expected| + expression_and_expected.each do |filter_expression, expected| + + get :index, :set_filter => 1, field => filter_expression + + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues) + + query = assigns(:query) + assert_not_nil query + assert query.has_filter?(field) + assert_equal(default_filter.merge({field => {:operator => expected[:op], :values => expected[:values]}}), query.filters) + end + end + end + + def test_index_with_project_and_empty_filters + get :index, :project_id => 1, :set_filter => 1, :fields => [''] + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues) + + query = assigns(:query) + assert_not_nil query + # no filter + assert_equal({}, query.filters) + end + + def test_index_with_project_custom_field_filter + field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string') + CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo') + CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo') + filter_name = "project.cf_#{field.id}" + @request.session[:user_id] = 1 + + get :index, :set_filter => 1, + :f => [filter_name], + :op => {filter_name => '='}, + :v => {filter_name => ['Foo']} + assert_response :success + assert_template 'index' + assert_equal [3, 5], assigns(:issues).map(&:project_id).uniq.sort + end + + def test_index_with_query + get :index, :project_id => 1, :query_id => 5 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues) + assert_nil assigns(:issue_count_by_group) + end + + def test_index_with_query_grouped_by_tracker + get :index, :project_id => 1, :query_id => 6 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues) + assert_not_nil assigns(:issue_count_by_group) + end + + def test_index_with_query_grouped_by_list_custom_field + get :index, :project_id => 1, :query_id => 9 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues) + assert_not_nil assigns(:issue_count_by_group) + end + + def test_index_with_query_grouped_by_user_custom_field + cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'user') + CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '2') + CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => '3') + CustomValue.create!(:custom_field => cf, :customized => Issue.find(3), :value => '3') + CustomValue.create!(:custom_field => cf, :customized => Issue.find(5), :value => '') + + get :index, :project_id => 1, :set_filter => 1, :group_by => "cf_#{cf.id}" + assert_response :success + + assert_select 'tr.group', 3 + assert_select 'tr.group' do + assert_select 'a', :text => 'John Smith' + assert_select 'span.count', :text => '1' + end + assert_select 'tr.group' do + assert_select 'a', :text => 'Dave Lopper' + assert_select 'span.count', :text => '2' + end + end + + def test_index_with_query_grouped_by_tracker + 3.times {|i| Issue.generate!(:tracker_id => (i + 1))} + + get :index, :set_filter => 1, :group_by => 'tracker', :sort => 'id:desc' + assert_response :success + + trackers = assigns(:issues).map(&:tracker).uniq + assert_equal [1, 2, 3], trackers.map(&:id) + end + + def test_index_with_query_grouped_by_tracker_in_reverse_order + 3.times {|i| Issue.generate!(:tracker_id => (i + 1))} + + get :index, :set_filter => 1, :group_by => 'tracker', :sort => 'id:desc,tracker:desc' + assert_response :success + + trackers = assigns(:issues).map(&:tracker).uniq + assert_equal [3, 2, 1], trackers.map(&:id) + end + + def test_index_with_query_id_and_project_id_should_set_session_query + get :index, :project_id => 1, :query_id => 4 + assert_response :success + assert_kind_of Hash, session[:query] + assert_equal 4, session[:query][:id] + assert_equal 1, session[:query][:project_id] + end + + def test_index_with_invalid_query_id_should_respond_404 + get :index, :project_id => 1, :query_id => 999 + assert_response 404 + end + + def test_index_with_cross_project_query_in_session_should_show_project_issues + q = IssueQuery.create!(:name => "test", :user_id => 2, :is_public => false, :project => nil) + @request.session[:query] = {:id => q.id, :project_id => 1} + + with_settings :display_subprojects_issues => '0' do + get :index, :project_id => 1 + end + assert_response :success + assert_not_nil assigns(:query) + assert_equal q.id, assigns(:query).id + assert_equal 1, assigns(:query).project_id + assert_equal [1], assigns(:issues).map(&:project_id).uniq + end + + def test_private_query_should_not_be_available_to_other_users + q = IssueQuery.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil) + @request.session[:user_id] = 3 + + get :index, :query_id => q.id + assert_response 403 + end + + def test_private_query_should_be_available_to_its_user + q = IssueQuery.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil) + @request.session[:user_id] = 2 + + get :index, :query_id => q.id + assert_response :success + end + + def test_public_query_should_be_available_to_other_users + q = IssueQuery.create!(:name => "private", :user => User.find(2), :is_public => true, :project => nil) + @request.session[:user_id] = 3 + + get :index, :query_id => q.id + assert_response :success + end + + def test_index_should_omit_page_param_in_export_links + get :index, :page => 2 + assert_response :success + assert_select 'a.atom[href=/issues.atom]' + assert_select 'a.csv[href=/issues.csv]' + assert_select 'a.pdf[href=/issues.pdf]' + assert_select 'form#csv-export-form[action=/issues.csv]' + end + + def test_index_csv + get :index, :format => 'csv' + assert_response :success + assert_not_nil assigns(:issues) + assert_equal 'text/csv; header=present', @response.content_type + assert @response.body.starts_with?("#,") + lines = @response.body.chomp.split("\n") + assert_equal assigns(:query).columns.size, lines[0].split(',').size + end + + def test_index_csv_with_project + get :index, :project_id => 1, :format => 'csv' + assert_response :success + assert_not_nil assigns(:issues) + assert_equal 'text/csv; header=present', @response.content_type + end + + def test_index_csv_with_description + Issue.generate!(:description => 'test_index_csv_with_description') + + with_settings :default_language => 'en' do + get :index, :format => 'csv', :description => '1' + assert_response :success + assert_not_nil assigns(:issues) + end + + assert_equal 'text/csv; header=present', response.content_type + headers = response.body.chomp.split("\n").first.split(',') + assert_include 'Description', headers + assert_include 'test_index_csv_with_description', response.body + end + + def test_index_csv_with_spent_time_column + issue = Issue.create!(:project_id => 1, :tracker_id => 1, :subject => 'test_index_csv_with_spent_time_column', :author_id => 2) + TimeEntry.create!(:project => issue.project, :issue => issue, :hours => 7.33, :user => User.find(2), :spent_on => Date.today) + + get :index, :format => 'csv', :set_filter => '1', :c => %w(subject spent_hours) + assert_response :success + assert_equal 'text/csv; header=present', @response.content_type + lines = @response.body.chomp.split("\n") + assert_include "#{issue.id},#{issue.subject},7.33", lines + end + + def test_index_csv_with_all_columns + get :index, :format => 'csv', :columns => 'all' + assert_response :success + assert_not_nil assigns(:issues) + assert_equal 'text/csv; header=present', @response.content_type + assert_match /\A#,/, response.body + lines = response.body.chomp.split("\n") + assert_equal assigns(:query).available_inline_columns.size, lines[0].split(',').size + end + + def test_index_csv_with_multi_column_field + CustomField.find(1).update_attribute :multiple, true + issue = Issue.find(1) + issue.custom_field_values = {1 => ['MySQL', 'Oracle']} + issue.save! + + get :index, :format => 'csv', :columns => 'all' + assert_response :success + lines = @response.body.chomp.split("\n") + assert lines.detect {|line| line.include?('"MySQL, Oracle"')} + end + + def test_index_csv_should_format_float_custom_fields_with_csv_decimal_separator + field = IssueCustomField.create!(:name => 'Float', :is_for_all => true, :tracker_ids => [1], :field_format => 'float') + issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id => '185.6'}) + + with_settings :default_language => 'fr' do + get :index, :format => 'csv', :columns => 'all' + assert_response :success + issue_line = response.body.chomp.split("\n").map {|line| line.split(';')}.detect {|line| line[0]==issue.id.to_s} + assert_include '185,60', issue_line + end + + with_settings :default_language => 'en' do + get :index, :format => 'csv', :columns => 'all' + assert_response :success + issue_line = response.body.chomp.split("\n").map {|line| line.split(',')}.detect {|line| line[0]==issue.id.to_s} + assert_include '185.60', issue_line + end + end + + def test_index_csv_big_5 + with_settings :default_language => "zh-TW" do + str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88" + str_big5 = "\xa4@\xa4\xeb" + if str_utf8.respond_to?(:force_encoding) + str_utf8.force_encoding('UTF-8') + str_big5.force_encoding('Big5') + end + issue = Issue.generate!(:subject => str_utf8) + + get :index, :project_id => 1, + :f => ['subject'], + :op => '=', :values => [str_utf8], + :format => 'csv' + assert_equal 'text/csv; header=present', @response.content_type + lines = @response.body.chomp.split("\n") + s1 = "\xaa\xac\xbaA" + if str_utf8.respond_to?(:force_encoding) + s1.force_encoding('Big5') + end + assert_include s1, lines[0] + assert_include str_big5, lines[1] + end + end + + def test_index_csv_cannot_convert_should_be_replaced_big_5 + with_settings :default_language => "zh-TW" do + str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85" + if str_utf8.respond_to?(:force_encoding) + str_utf8.force_encoding('UTF-8') + end + issue = Issue.generate!(:subject => str_utf8) + + get :index, :project_id => 1, + :f => ['subject'], + :op => '=', :values => [str_utf8], + :c => ['status', 'subject'], + :format => 'csv', + :set_filter => 1 + assert_equal 'text/csv; header=present', @response.content_type + lines = @response.body.chomp.split("\n") + s1 = "\xaa\xac\xbaA" # status + if str_utf8.respond_to?(:force_encoding) + s1.force_encoding('Big5') + end + assert lines[0].include?(s1) + s2 = lines[1].split(",")[2] + if s1.respond_to?(:force_encoding) + s3 = "\xa5H?" # subject + s3.force_encoding('Big5') + assert_equal s3, s2 + elsif RUBY_PLATFORM == 'java' + assert_equal "??", s2 + else + assert_equal "\xa5H???", s2 + end + end + end + + def test_index_csv_tw + with_settings :default_language => "zh-TW" do + str1 = "test_index_csv_tw" + issue = Issue.generate!(:subject => str1, :estimated_hours => '1234.5') + + get :index, :project_id => 1, + :f => ['subject'], + :op => '=', :values => [str1], + :c => ['estimated_hours', 'subject'], + :format => 'csv', + :set_filter => 1 + assert_equal 'text/csv; header=present', @response.content_type + lines = @response.body.chomp.split("\n") + assert_equal "#{issue.id},1234.50,#{str1}", lines[1] + end + end + + def test_index_csv_fr + with_settings :default_language => "fr" do + str1 = "test_index_csv_fr" + issue = Issue.generate!(:subject => str1, :estimated_hours => '1234.5') + + get :index, :project_id => 1, + :f => ['subject'], + :op => '=', :values => [str1], + :c => ['estimated_hours', 'subject'], + :format => 'csv', + :set_filter => 1 + assert_equal 'text/csv; header=present', @response.content_type + lines = @response.body.chomp.split("\n") + assert_equal "#{issue.id};1234,50;#{str1}", lines[1] + end + end + + def test_index_pdf + ["en", "zh", "zh-TW", "ja", "ko"].each do |lang| + with_settings :default_language => lang do + + get :index + assert_response :success + assert_template 'index' + + if lang == "ja" + if RUBY_PLATFORM != 'java' + assert_equal "CP932", l(:general_pdf_encoding) + end + if RUBY_PLATFORM == 'java' && l(:general_pdf_encoding) == "CP932" + next + end + end + + get :index, :format => 'pdf' + assert_response :success + assert_not_nil assigns(:issues) + assert_equal 'application/pdf', @response.content_type + + get :index, :project_id => 1, :format => 'pdf' + assert_response :success + assert_not_nil assigns(:issues) + assert_equal 'application/pdf', @response.content_type + + get :index, :project_id => 1, :query_id => 6, :format => 'pdf' + assert_response :success + assert_not_nil assigns(:issues) + assert_equal 'application/pdf', @response.content_type + end + end + end + + def test_index_pdf_with_query_grouped_by_list_custom_field + get :index, :project_id => 1, :query_id => 9, :format => 'pdf' + assert_response :success + assert_not_nil assigns(:issues) + assert_not_nil assigns(:issue_count_by_group) + assert_equal 'application/pdf', @response.content_type + end + + def test_index_atom + get :index, :project_id => 'ecookbook', :format => 'atom' + assert_response :success + assert_template 'common/feed' + assert_equal 'application/atom+xml', response.content_type + + assert_select 'feed' do + assert_select 'link[rel=self][href=?]', 'http://test.host/projects/ecookbook/issues.atom' + assert_select 'link[rel=alternate][href=?]', 'http://test.host/projects/ecookbook/issues' + assert_select 'entry link[href=?]', 'http://test.host/issues/1' + end + end + + def test_index_sort + get :index, :sort => 'tracker,id:desc' + assert_response :success + + sort_params = @request.session['issues_index_sort'] + assert sort_params.is_a?(String) + assert_equal 'tracker,id:desc', sort_params + + issues = assigns(:issues) + assert_not_nil issues + assert !issues.empty? + assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id) + end + + def test_index_sort_by_field_not_included_in_columns + Setting.issue_list_default_columns = %w(subject author) + get :index, :sort => 'tracker' + end + + def test_index_sort_by_assigned_to + get :index, :sort => 'assigned_to' + assert_response :success + assignees = assigns(:issues).collect(&:assigned_to).compact + assert_equal assignees.sort, assignees + end + + def test_index_sort_by_assigned_to_desc + get :index, :sort => 'assigned_to:desc' + assert_response :success + assignees = assigns(:issues).collect(&:assigned_to).compact + assert_equal assignees.sort.reverse, assignees + end + + def test_index_group_by_assigned_to + get :index, :group_by => 'assigned_to', :sort => 'priority' + assert_response :success + end + + def test_index_sort_by_author + get :index, :sort => 'author' + assert_response :success + authors = assigns(:issues).collect(&:author) + assert_equal authors.sort, authors + end + + def test_index_sort_by_author_desc + get :index, :sort => 'author:desc' + assert_response :success + authors = assigns(:issues).collect(&:author) + assert_equal authors.sort.reverse, authors + end + + def test_index_group_by_author + get :index, :group_by => 'author', :sort => 'priority' + assert_response :success + end + + def test_index_sort_by_spent_hours + get :index, :sort => 'spent_hours:desc' + assert_response :success + hours = assigns(:issues).collect(&:spent_hours) + assert_equal hours.sort.reverse, hours + end + + def test_index_sort_by_user_custom_field + cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'user') + CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '2') + CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => '3') + CustomValue.create!(:custom_field => cf, :customized => Issue.find(3), :value => '3') + CustomValue.create!(:custom_field => cf, :customized => Issue.find(5), :value => '') + + get :index, :project_id => 1, :set_filter => 1, :sort => "cf_#{cf.id},id" + assert_response :success + + assert_equal [2, 3, 1], assigns(:issues).select {|issue| issue.custom_field_value(cf).present?}.map(&:id) + end + + def test_index_with_columns + columns = ['tracker', 'subject', 'assigned_to'] + get :index, :set_filter => 1, :c => columns + assert_response :success + + # query should use specified columns + query = assigns(:query) + assert_kind_of IssueQuery, query + assert_equal columns, query.column_names.map(&:to_s) + + # columns should be stored in session + assert_kind_of Hash, session[:query] + assert_kind_of Array, session[:query][:column_names] + assert_equal columns, session[:query][:column_names].map(&:to_s) + + # ensure only these columns are kept in the selected columns list + assert_select 'select#selected_columns option' do + assert_select 'option', 3 + assert_select 'option[value=tracker]' + assert_select 'option[value=project]', 0 + end + end + + def test_index_without_project_should_implicitly_add_project_column_to_default_columns + Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to'] + get :index, :set_filter => 1 + + # query should use specified columns + query = assigns(:query) + assert_kind_of IssueQuery, query + assert_equal [:id, :project, :tracker, :subject, :assigned_to], query.columns.map(&:name) + end + + def test_index_without_project_and_explicit_default_columns_should_not_add_project_column + Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to'] + columns = ['id', 'tracker', 'subject', 'assigned_to'] + get :index, :set_filter => 1, :c => columns + + # query should use specified columns + query = assigns(:query) + assert_kind_of IssueQuery, query + assert_equal columns.map(&:to_sym), query.columns.map(&:name) + end + + def test_index_with_custom_field_column + columns = %w(tracker subject cf_2) + get :index, :set_filter => 1, :c => columns + assert_response :success + + # query should use specified columns + query = assigns(:query) + assert_kind_of IssueQuery, query + assert_equal columns, query.column_names.map(&:to_s) + + assert_select 'table.issues td.cf_2.string' + end + + def test_index_with_multi_custom_field_column + field = CustomField.find(1) + field.update_attribute :multiple, true + issue = Issue.find(1) + issue.custom_field_values = {1 => ['MySQL', 'Oracle']} + issue.save! + + get :index, :set_filter => 1, :c => %w(tracker subject cf_1) + assert_response :success + + assert_select 'table.issues td.cf_1', :text => 'MySQL, Oracle' + end + + def test_index_with_multi_user_custom_field_column + field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true, + :tracker_ids => [1], :is_for_all => true) + issue = Issue.find(1) + issue.custom_field_values = {field.id => ['2', '3']} + issue.save! + + get :index, :set_filter => 1, :c => ['tracker', 'subject', "cf_#{field.id}"] + assert_response :success + + assert_select "table.issues td.cf_#{field.id}" do + assert_select 'a', 2 + assert_select 'a[href=?]', '/users/2', :text => 'John Smith' + assert_select 'a[href=?]', '/users/3', :text => 'Dave Lopper' + end + end + + def test_index_with_date_column + with_settings :date_format => '%d/%m/%Y' do + Issue.find(1).update_attribute :start_date, '1987-08-24' + + get :index, :set_filter => 1, :c => %w(start_date) + + assert_select "table.issues td.start_date", :text => '24/08/1987' + end + end + + def test_index_with_done_ratio_column + Issue.find(1).update_attribute :done_ratio, 40 + + get :index, :set_filter => 1, :c => %w(done_ratio) + + assert_select 'table.issues td.done_ratio' do + assert_select 'table.progress' do + assert_select 'td.closed[style=?]', 'width: 40%;' + end + end + end + + def test_index_with_spent_hours_column + get :index, :set_filter => 1, :c => %w(subject spent_hours) + + assert_select 'table.issues tr#issue-3 td.spent_hours', :text => '1.00' + end + + def test_index_should_not_show_spent_hours_column_without_permission + Role.anonymous.remove_permission! :view_time_entries + get :index, :set_filter => 1, :c => %w(subject spent_hours) + + assert_select 'td.spent_hours', 0 + end + + def test_index_with_fixed_version_column + get :index, :set_filter => 1, :c => %w(fixed_version) + + assert_select 'table.issues td.fixed_version' do + assert_select 'a[href=?]', '/versions/2', :text => '1.0' + end + end + + def test_index_with_relations_column + IssueRelation.delete_all + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(7)) + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(8), :issue_to => Issue.find(1)) + IssueRelation.create!(:relation_type => "blocks", :issue_from => Issue.find(1), :issue_to => Issue.find(11)) + IssueRelation.create!(:relation_type => "blocks", :issue_from => Issue.find(12), :issue_to => Issue.find(2)) + + get :index, :set_filter => 1, :c => %w(subject relations) + assert_response :success + assert_select "tr#issue-1 td.relations" do + assert_select "span", 3 + assert_select "span", :text => "Related to #7" + assert_select "span", :text => "Related to #8" + assert_select "span", :text => "Blocks #11" + end + assert_select "tr#issue-2 td.relations" do + assert_select "span", 1 + assert_select "span", :text => "Blocked by #12" + end + assert_select "tr#issue-3 td.relations" do + assert_select "span", 0 + end + + get :index, :set_filter => 1, :c => %w(relations), :format => 'csv' + assert_response :success + assert_equal 'text/csv; header=present', response.content_type + lines = response.body.chomp.split("\n") + assert_include '1,"Related to #7, Related to #8, Blocks #11"', lines + assert_include '2,Blocked by #12', lines + assert_include '3,""', lines + + get :index, :set_filter => 1, :c => %w(subject relations), :format => 'pdf' + assert_response :success + assert_equal 'application/pdf', response.content_type + end + + def test_index_with_description_column + get :index, :set_filter => 1, :c => %w(subject description) + + assert_select 'table.issues thead th', 3 # columns: chekbox + id + subject + assert_select 'td.description[colspan=3]', :text => 'Unable to print recipes' + + get :index, :set_filter => 1, :c => %w(subject description), :format => 'pdf' + assert_response :success + assert_equal 'application/pdf', response.content_type + end + + def test_index_send_html_if_query_is_invalid + get :index, :f => ['start_date'], :op => {:start_date => '='} + assert_equal 'text/html', @response.content_type + assert_template 'index' + end + + def test_index_send_nothing_if_query_is_invalid + get :index, :f => ['start_date'], :op => {:start_date => '='}, :format => 'csv' + assert_equal 'text/csv', @response.content_type + assert @response.body.blank? + end + + def test_show_by_anonymous + get :show, :id => 1 + assert_response :success + assert_template 'show' + assert_equal Issue.find(1), assigns(:issue) + + assert_select 'div.issue div.description', :text => /Unable to print recipes/ + + # anonymous role is allowed to add a note + assert_select 'form#issue-form' do + assert_select 'fieldset' do + assert_select 'legend', :text => 'Notes' + assert_select 'textarea[name=?]', 'issue[notes]' + end + end + + assert_select 'title', :text => "Bug #1: Can't print recipes - eCookbook - Redmine" + end + + def test_show_by_manager + @request.session[:user_id] = 2 + get :show, :id => 1 + assert_response :success + + assert_select 'a', :text => /Quote/ + + assert_select 'form#issue-form' do + assert_select 'fieldset' do + assert_select 'legend', :text => 'Change properties' + assert_select 'input[name=?]', 'issue[subject]' + end + assert_select 'fieldset' do + assert_select 'legend', :text => 'Log time' + assert_select 'input[name=?]', 'time_entry[hours]' + end + assert_select 'fieldset' do + assert_select 'legend', :text => 'Notes' + assert_select 'textarea[name=?]', 'issue[notes]' + end + end + end + + def test_show_should_display_update_form + @request.session[:user_id] = 2 + get :show, :id => 1 + assert_response :success + + assert_select 'form#issue-form' do + assert_select 'input[name=?]', 'issue[is_private]' + assert_select 'select[name=?]', 'issue[project_id]' + assert_select 'select[name=?]', 'issue[tracker_id]' + assert_select 'input[name=?]', 'issue[subject]' + assert_select 'textarea[name=?]', 'issue[description]' + assert_select 'select[name=?]', 'issue[status_id]' + assert_select 'select[name=?]', 'issue[priority_id]' + assert_select 'select[name=?]', 'issue[assigned_to_id]' + assert_select 'select[name=?]', 'issue[category_id]' + assert_select 'select[name=?]', 'issue[fixed_version_id]' + assert_select 'input[name=?]', 'issue[parent_issue_id]' + assert_select 'input[name=?]', 'issue[start_date]' + assert_select 'input[name=?]', 'issue[due_date]' + assert_select 'select[name=?]', 'issue[done_ratio]' + assert_select 'input[name=?]', 'issue[custom_field_values][2]' + assert_select 'input[name=?]', 'issue[watcher_user_ids][]', 0 + assert_select 'textarea[name=?]', 'issue[notes]' + end + end + + def test_show_should_display_update_form_with_minimal_permissions + Role.find(1).update_attribute :permissions, [:view_issues, :add_issue_notes] + WorkflowTransition.delete_all :role_id => 1 + + @request.session[:user_id] = 2 + get :show, :id => 1 + assert_response :success + + assert_select 'form#issue-form' do + assert_select 'input[name=?]', 'issue[is_private]', 0 + assert_select 'select[name=?]', 'issue[project_id]', 0 + assert_select 'select[name=?]', 'issue[tracker_id]', 0 + assert_select 'input[name=?]', 'issue[subject]', 0 + assert_select 'textarea[name=?]', 'issue[description]', 0 + assert_select 'select[name=?]', 'issue[status_id]', 0 + assert_select 'select[name=?]', 'issue[priority_id]', 0 + assert_select 'select[name=?]', 'issue[assigned_to_id]', 0 + assert_select 'select[name=?]', 'issue[category_id]', 0 + assert_select 'select[name=?]', 'issue[fixed_version_id]', 0 + assert_select 'input[name=?]', 'issue[parent_issue_id]', 0 + assert_select 'input[name=?]', 'issue[start_date]', 0 + assert_select 'input[name=?]', 'issue[due_date]', 0 + assert_select 'select[name=?]', 'issue[done_ratio]', 0 + assert_select 'input[name=?]', 'issue[custom_field_values][2]', 0 + assert_select 'input[name=?]', 'issue[watcher_user_ids][]', 0 + assert_select 'textarea[name=?]', 'issue[notes]' + end + end + + def test_show_should_display_update_form_with_workflow_permissions + Role.find(1).update_attribute :permissions, [:view_issues, :add_issue_notes] + + @request.session[:user_id] = 2 + get :show, :id => 1 + assert_response :success + + assert_select 'form#issue-form' do + assert_select 'input[name=?]', 'issue[is_private]', 0 + assert_select 'select[name=?]', 'issue[project_id]', 0 + assert_select 'select[name=?]', 'issue[tracker_id]', 0 + assert_select 'input[name=?]', 'issue[subject]', 0 + assert_select 'textarea[name=?]', 'issue[description]', 0 + assert_select 'select[name=?]', 'issue[status_id]' + assert_select 'select[name=?]', 'issue[priority_id]', 0 + assert_select 'select[name=?]', 'issue[assigned_to_id]' + assert_select 'select[name=?]', 'issue[category_id]', 0 + assert_select 'select[name=?]', 'issue[fixed_version_id]' + assert_select 'input[name=?]', 'issue[parent_issue_id]', 0 + assert_select 'input[name=?]', 'issue[start_date]', 0 + assert_select 'input[name=?]', 'issue[due_date]', 0 + assert_select 'select[name=?]', 'issue[done_ratio]' + assert_select 'input[name=?]', 'issue[custom_field_values][2]', 0 + assert_select 'input[name=?]', 'issue[watcher_user_ids][]', 0 + assert_select 'textarea[name=?]', 'issue[notes]' + end + end + + def test_show_should_not_display_update_form_without_permissions + Role.find(1).update_attribute :permissions, [:view_issues] + + @request.session[:user_id] = 2 + get :show, :id => 1 + assert_response :success + + assert_select 'form#issue-form', 0 + end + + def test_update_form_should_not_display_inactive_enumerations + assert !IssuePriority.find(15).active? + + @request.session[:user_id] = 2 + get :show, :id => 1 + assert_response :success + + assert_select 'form#issue-form' do + assert_select 'select[name=?]', 'issue[priority_id]' do + assert_select 'option[value=4]' + assert_select 'option[value=15]', 0 + end + end + end + + def test_update_form_should_allow_attachment_upload + @request.session[:user_id] = 2 + get :show, :id => 1 + + assert_select 'form#issue-form[method=post][enctype=multipart/form-data]' do + assert_select 'input[type=file][name=?]', 'attachments[dummy][file]' + end + end + + def test_show_should_deny_anonymous_access_without_permission + Role.anonymous.remove_permission!(:view_issues) + get :show, :id => 1 + assert_response :redirect + end + + def test_show_should_deny_anonymous_access_to_private_issue + Issue.update_all(["is_private = ?", true], "id = 1") + get :show, :id => 1 + assert_response :redirect + end + + def test_show_should_deny_non_member_access_without_permission + Role.non_member.remove_permission!(:view_issues) + @request.session[:user_id] = 9 + get :show, :id => 1 + assert_response 403 + end + + def test_show_should_deny_non_member_access_to_private_issue + Issue.update_all(["is_private = ?", true], "id = 1") + @request.session[:user_id] = 9 + get :show, :id => 1 + assert_response 403 + end + + def test_show_should_deny_member_access_without_permission + Role.find(1).remove_permission!(:view_issues) + @request.session[:user_id] = 2 + get :show, :id => 1 + assert_response 403 + end + + def test_show_should_deny_member_access_to_private_issue_without_permission + Issue.update_all(["is_private = ?", true], "id = 1") + @request.session[:user_id] = 3 + get :show, :id => 1 + assert_response 403 + end + + def test_show_should_allow_author_access_to_private_issue + Issue.update_all(["is_private = ?, author_id = 3", true], "id = 1") + @request.session[:user_id] = 3 + get :show, :id => 1 + assert_response :success + end + + def test_show_should_allow_assignee_access_to_private_issue + Issue.update_all(["is_private = ?, assigned_to_id = 3", true], "id = 1") + @request.session[:user_id] = 3 + get :show, :id => 1 + assert_response :success + end + + def test_show_should_allow_member_access_to_private_issue_with_permission + Issue.update_all(["is_private = ?", true], "id = 1") + User.find(3).roles_for_project(Project.find(1)).first.update_attribute :issues_visibility, 'all' + @request.session[:user_id] = 3 + get :show, :id => 1 + assert_response :success + end + + def test_show_should_not_disclose_relations_to_invisible_issues + Setting.cross_project_issue_relations = '1' + IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates') + # Relation to a private project issue + IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates') + + get :show, :id => 1 + assert_response :success + + assert_select 'div#relations' do + assert_select 'a', :text => /#2$/ + assert_select 'a', :text => /#4$/, :count => 0 + end + end + + def test_show_should_list_subtasks + Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :parent_issue_id => 1, :subject => 'Child Issue') + + get :show, :id => 1 + assert_response :success + + assert_select 'div#issue_tree' do + assert_select 'td.subject', :text => /Child Issue/ + end + end + + def test_show_should_list_parents + issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :parent_issue_id => 1, :subject => 'Child Issue') + + get :show, :id => issue.id + assert_response :success + + assert_select 'div.subject' do + assert_select 'h3', 'Child Issue' + assert_select 'a[href=/issues/1]' + end + end + + def test_show_should_not_display_prev_next_links_without_query_in_session + get :show, :id => 1 + assert_response :success + assert_nil assigns(:prev_issue_id) + assert_nil assigns(:next_issue_id) + + assert_select 'div.next-prev-links', 0 + end + + def test_show_should_display_prev_next_links_with_query_in_session + @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => nil} + @request.session['issues_index_sort'] = 'id' + + with_settings :display_subprojects_issues => '0' do + get :show, :id => 3 + end + + assert_response :success + # Previous and next issues for all projects + assert_equal 2, assigns(:prev_issue_id) + assert_equal 5, assigns(:next_issue_id) + + count = Issue.open.visible.count + + assert_select 'div.next-prev-links' do + assert_select 'a[href=/issues/2]', :text => /Previous/ + assert_select 'a[href=/issues/5]', :text => /Next/ + assert_select 'span.position', :text => "3 of #{count}" + end + end + + def test_show_should_display_prev_next_links_with_saved_query_in_session + query = IssueQuery.create!(:name => 'test', :is_public => true, :user_id => 1, + :filters => {'status_id' => {:values => ['5'], :operator => '='}}, + :sort_criteria => [['id', 'asc']]) + @request.session[:query] = {:id => query.id, :project_id => nil} + + get :show, :id => 11 + + assert_response :success + assert_equal query, assigns(:query) + # Previous and next issues for all projects + assert_equal 8, assigns(:prev_issue_id) + assert_equal 12, assigns(:next_issue_id) + + assert_select 'div.next-prev-links' do + assert_select 'a[href=/issues/8]', :text => /Previous/ + assert_select 'a[href=/issues/12]', :text => /Next/ + end + end + + def test_show_should_display_prev_next_links_with_query_and_sort_on_association + @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => nil} + + %w(project tracker status priority author assigned_to category fixed_version).each do |assoc_sort| + @request.session['issues_index_sort'] = assoc_sort + + get :show, :id => 3 + assert_response :success, "Wrong response status for #{assoc_sort} sort" + + assert_select 'div.next-prev-links' do + assert_select 'a', :text => /(Previous|Next)/ + end + end + end + + def test_show_should_display_prev_next_links_with_project_query_in_session + @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => 1} + @request.session['issues_index_sort'] = 'id' + + with_settings :display_subprojects_issues => '0' do + get :show, :id => 3 + end + + assert_response :success + # Previous and next issues inside project + assert_equal 2, assigns(:prev_issue_id) + assert_equal 7, assigns(:next_issue_id) + + assert_select 'div.next-prev-links' do + assert_select 'a[href=/issues/2]', :text => /Previous/ + assert_select 'a[href=/issues/7]', :text => /Next/ + end + end + + def test_show_should_not_display_prev_link_for_first_issue + @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => 1} + @request.session['issues_index_sort'] = 'id' + + with_settings :display_subprojects_issues => '0' do + get :show, :id => 1 + end + + assert_response :success + assert_nil assigns(:prev_issue_id) + assert_equal 2, assigns(:next_issue_id) + + assert_select 'div.next-prev-links' do + assert_select 'a', :text => /Previous/, :count => 0 + assert_select 'a[href=/issues/2]', :text => /Next/ + end + end + + def test_show_should_not_display_prev_next_links_for_issue_not_in_query_results + @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'c'}}, :project_id => 1} + @request.session['issues_index_sort'] = 'id' + + get :show, :id => 1 + + assert_response :success + assert_nil assigns(:prev_issue_id) + assert_nil assigns(:next_issue_id) + + assert_select 'a', :text => /Previous/, :count => 0 + assert_select 'a', :text => /Next/, :count => 0 + end + + def test_show_show_should_display_prev_next_links_with_query_sort_by_user_custom_field + cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'user') + CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '2') + CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => '3') + CustomValue.create!(:custom_field => cf, :customized => Issue.find(3), :value => '3') + CustomValue.create!(:custom_field => cf, :customized => Issue.find(5), :value => '') + + query = IssueQuery.create!(:name => 'test', :is_public => true, :user_id => 1, :filters => {}, + :sort_criteria => [["cf_#{cf.id}", 'asc'], ['id', 'asc']]) + @request.session[:query] = {:id => query.id, :project_id => nil} + + get :show, :id => 3 + assert_response :success + + assert_equal 2, assigns(:prev_issue_id) + assert_equal 1, assigns(:next_issue_id) + + assert_select 'div.next-prev-links' do + assert_select 'a[href=/issues/2]', :text => /Previous/ + assert_select 'a[href=/issues/1]', :text => /Next/ + end + end + + def test_show_should_display_link_to_the_assignee + get :show, :id => 2 + assert_response :success + assert_select '.assigned-to' do + assert_select 'a[href=/users/3]' + end + end + + def test_show_should_display_visible_changesets_from_other_projects + project = Project.find(2) + issue = project.issues.first + issue.changeset_ids = [102] + issue.save! + # changesets from other projects should be displayed even if repository + # is disabled on issue's project + project.disable_module! :repository + + @request.session[:user_id] = 2 + get :show, :id => issue.id + + assert_select 'a[href=?]', '/projects/ecookbook/repository/revisions/3' + end + + def test_show_should_display_watchers + @request.session[:user_id] = 2 + Issue.find(1).add_watcher User.find(2) + + get :show, :id => 1 + assert_select 'div#watchers ul' do + assert_select 'li' do + assert_select 'a[href=/users/2]' + assert_select 'a img[alt=Delete]' + end + end + end + + def test_show_should_display_watchers_with_gravatars + @request.session[:user_id] = 2 + Issue.find(1).add_watcher User.find(2) + + with_settings :gravatar_enabled => '1' do + get :show, :id => 1 + end + + assert_select 'div#watchers ul' do + assert_select 'li' do + assert_select 'img.gravatar' + assert_select 'a[href=/users/2]' + assert_select 'a img[alt=Delete]' + end + end + end + + def test_show_with_thumbnails_enabled_should_display_thumbnails + @request.session[:user_id] = 2 + + with_settings :thumbnails_enabled => '1' do + get :show, :id => 14 + assert_response :success + end + + assert_select 'div.thumbnails' do + assert_select 'a[href=/attachments/16/testfile.png]' do + assert_select 'img[src=/attachments/thumbnail/16]' + end + end + end + + def test_show_with_thumbnails_disabled_should_not_display_thumbnails + @request.session[:user_id] = 2 + + with_settings :thumbnails_enabled => '0' do + get :show, :id => 14 + assert_response :success + end + + assert_select 'div.thumbnails', 0 + end + + def test_show_with_multi_custom_field + field = CustomField.find(1) + field.update_attribute :multiple, true + issue = Issue.find(1) + issue.custom_field_values = {1 => ['MySQL', 'Oracle']} + issue.save! + + get :show, :id => 1 + assert_response :success + + assert_select 'td', :text => 'MySQL, Oracle' + end + + def test_show_with_multi_user_custom_field + field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true, + :tracker_ids => [1], :is_for_all => true) + issue = Issue.find(1) + issue.custom_field_values = {field.id => ['2', '3']} + issue.save! + + get :show, :id => 1 + assert_response :success + + # TODO: should display links + assert_select 'td', :text => 'Dave Lopper, John Smith' + end + + def test_show_should_display_private_notes_with_permission_only + journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Privates notes', :private_notes => true, :user_id => 1) + @request.session[:user_id] = 2 + + get :show, :id => 2 + assert_response :success + assert_include journal, assigns(:journals) + + Role.find(1).remove_permission! :view_private_notes + get :show, :id => 2 + assert_response :success + assert_not_include journal, assigns(:journals) + end + + def test_show_atom + get :show, :id => 2, :format => 'atom' + assert_response :success + assert_template 'journals/index' + # Inline image + assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10')) + end + + def test_show_export_to_pdf + get :show, :id => 3, :format => 'pdf' + assert_response :success + assert_equal 'application/pdf', @response.content_type + assert @response.body.starts_with?('%PDF') + assert_not_nil assigns(:issue) + end + + def test_show_export_to_pdf_with_ancestors + issue = Issue.generate!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => 1) + + get :show, :id => issue.id, :format => 'pdf' + assert_response :success + assert_equal 'application/pdf', @response.content_type + assert @response.body.starts_with?('%PDF') + end + + def test_show_export_to_pdf_with_descendants + c1 = Issue.generate!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => 1) + c2 = Issue.generate!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => 1) + c3 = Issue.generate!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => c1.id) + + get :show, :id => 1, :format => 'pdf' + assert_response :success + assert_equal 'application/pdf', @response.content_type + assert @response.body.starts_with?('%PDF') + end + + def test_show_export_to_pdf_with_journals + get :show, :id => 1, :format => 'pdf' + assert_response :success + assert_equal 'application/pdf', @response.content_type + assert @response.body.starts_with?('%PDF') + end + + def test_show_export_to_pdf_with_changesets + Issue.find(3).changesets = Changeset.find_all_by_id(100, 101, 102) + + get :show, :id => 3, :format => 'pdf' + assert_response :success + assert_equal 'application/pdf', @response.content_type + assert @response.body.starts_with?('%PDF') + end + + def test_show_invalid_should_respond_with_404 + get :show, :id => 999 + assert_response 404 + end + + def test_get_new + @request.session[:user_id] = 2 + get :new, :project_id => 1, :tracker_id => 1 + assert_response :success + assert_template 'new' + + assert_select 'form#issue-form' do + assert_select 'input[name=?]', 'issue[is_private]' + assert_select 'select[name=?]', 'issue[project_id]', 0 + assert_select 'select[name=?]', 'issue[tracker_id]' + assert_select 'input[name=?]', 'issue[subject]' + assert_select 'textarea[name=?]', 'issue[description]' + assert_select 'select[name=?]', 'issue[status_id]' + assert_select 'select[name=?]', 'issue[priority_id]' + assert_select 'select[name=?]', 'issue[assigned_to_id]' + assert_select 'select[name=?]', 'issue[category_id]' + assert_select 'select[name=?]', 'issue[fixed_version_id]' + assert_select 'input[name=?]', 'issue[parent_issue_id]' + assert_select 'input[name=?]', 'issue[start_date]' + assert_select 'input[name=?]', 'issue[due_date]' + assert_select 'select[name=?]', 'issue[done_ratio]' + assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', 'Default string' + assert_select 'input[name=?]', 'issue[watcher_user_ids][]' + end + + # Be sure we don't display inactive IssuePriorities + assert ! IssuePriority.find(15).active? + assert_select 'select[name=?]', 'issue[priority_id]' do + assert_select 'option[value=15]', 0 + end + end + + def test_get_new_with_minimal_permissions + Role.find(1).update_attribute :permissions, [:add_issues] + WorkflowTransition.delete_all :role_id => 1 + + @request.session[:user_id] = 2 + get :new, :project_id => 1, :tracker_id => 1 + assert_response :success + assert_template 'new' + + assert_select 'form#issue-form' do + assert_select 'input[name=?]', 'issue[is_private]', 0 + assert_select 'select[name=?]', 'issue[project_id]', 0 + assert_select 'select[name=?]', 'issue[tracker_id]' + assert_select 'input[name=?]', 'issue[subject]' + assert_select 'textarea[name=?]', 'issue[description]' + assert_select 'select[name=?]', 'issue[status_id]' + assert_select 'select[name=?]', 'issue[priority_id]' + assert_select 'select[name=?]', 'issue[assigned_to_id]' + assert_select 'select[name=?]', 'issue[category_id]' + assert_select 'select[name=?]', 'issue[fixed_version_id]' + assert_select 'input[name=?]', 'issue[parent_issue_id]', 0 + assert_select 'input[name=?]', 'issue[start_date]' + assert_select 'input[name=?]', 'issue[due_date]' + assert_select 'select[name=?]', 'issue[done_ratio]' + assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', 'Default string' + assert_select 'input[name=?]', 'issue[watcher_user_ids][]', 0 + end + end + + def test_get_new_with_list_custom_field + @request.session[:user_id] = 2 + get :new, :project_id => 1, :tracker_id => 1 + assert_response :success + assert_template 'new' + + assert_select 'select.list_cf[name=?]', 'issue[custom_field_values][1]' do + assert_select 'option', 4 + assert_select 'option[value=MySQL]', :text => 'MySQL' + end + end + + def test_get_new_with_multi_custom_field + field = IssueCustomField.find(1) + field.update_attribute :multiple, true + + @request.session[:user_id] = 2 + get :new, :project_id => 1, :tracker_id => 1 + assert_response :success + assert_template 'new' + + assert_select 'select[name=?][multiple=multiple]', 'issue[custom_field_values][1][]' do + assert_select 'option', 3 + assert_select 'option[value=MySQL]', :text => 'MySQL' + end + assert_select 'input[name=?][type=hidden][value=?]', 'issue[custom_field_values][1][]', '' + end + + def test_get_new_with_multi_user_custom_field + field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true, + :tracker_ids => [1], :is_for_all => true) + + @request.session[:user_id] = 2 + get :new, :project_id => 1, :tracker_id => 1 + assert_response :success + assert_template 'new' + + assert_select 'select[name=?][multiple=multiple]', "issue[custom_field_values][#{field.id}][]" do + assert_select 'option', Project.find(1).users.count + assert_select 'option[value=2]', :text => 'John Smith' + end + assert_select 'input[name=?][type=hidden][value=?]', "issue[custom_field_values][#{field.id}][]", '' + end + + def test_get_new_with_date_custom_field + field = IssueCustomField.create!(:name => 'Date', :field_format => 'date', :tracker_ids => [1], :is_for_all => true) + + @request.session[:user_id] = 2 + get :new, :project_id => 1, :tracker_id => 1 + assert_response :success + + assert_select 'input[name=?]', "issue[custom_field_values][#{field.id}]" + end + + def test_get_new_with_text_custom_field + field = IssueCustomField.create!(:name => 'Text', :field_format => 'text', :tracker_ids => [1], :is_for_all => true) + + @request.session[:user_id] = 2 + get :new, :project_id => 1, :tracker_id => 1 + assert_response :success + + assert_select 'textarea[name=?]', "issue[custom_field_values][#{field.id}]" + end + + def test_get_new_without_default_start_date_is_creation_date + Setting.default_issue_start_date_to_creation_date = 0 + + @request.session[:user_id] = 2 + get :new, :project_id => 1, :tracker_id => 1 + assert_response :success + assert_template 'new' + + assert_select 'input[name=?]', 'issue[start_date]' + assert_select 'input[name=?][value]', 'issue[start_date]', 0 + end + + def test_get_new_with_default_start_date_is_creation_date + Setting.default_issue_start_date_to_creation_date = 1 + + @request.session[:user_id] = 2 + get :new, :project_id => 1, :tracker_id => 1 + assert_response :success + assert_template 'new' + + assert_select 'input[name=?][value=?]', 'issue[start_date]', Date.today.to_s + end + + def test_get_new_form_should_allow_attachment_upload + @request.session[:user_id] = 2 + get :new, :project_id => 1, :tracker_id => 1 + + assert_select 'form[id=issue-form][method=post][enctype=multipart/form-data]' do + assert_select 'input[name=?][type=file]', 'attachments[dummy][file]' + end + end + + def test_get_new_should_prefill_the_form_from_params + @request.session[:user_id] = 2 + get :new, :project_id => 1, + :issue => {:tracker_id => 3, :description => 'Prefilled', :custom_field_values => {'2' => 'Custom field value'}} + + issue = assigns(:issue) + assert_equal 3, issue.tracker_id + assert_equal 'Prefilled', issue.description + assert_equal 'Custom field value', issue.custom_field_value(2) + + assert_select 'select[name=?]', 'issue[tracker_id]' do + assert_select 'option[value=3][selected=selected]' + end + assert_select 'textarea[name=?]', 'issue[description]', :text => /Prefilled/ + assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', 'Custom field value' + end + + def test_get_new_should_mark_required_fields + cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'required') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'required') + @request.session[:user_id] = 2 + + get :new, :project_id => 1 + assert_response :success + assert_template 'new' + + assert_select 'label[for=issue_start_date]' do + assert_select 'span[class=required]', 0 + end + assert_select 'label[for=issue_due_date]' do + assert_select 'span[class=required]' + end + assert_select 'label[for=?]', "issue_custom_field_values_#{cf1.id}" do + assert_select 'span[class=required]', 0 + end + assert_select 'label[for=?]', "issue_custom_field_values_#{cf2.id}" do + assert_select 'span[class=required]' + end + end + + def test_get_new_should_not_display_readonly_fields + cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'readonly') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'readonly') + @request.session[:user_id] = 2 + + get :new, :project_id => 1 + assert_response :success + assert_template 'new' + + assert_select 'input[name=?]', 'issue[start_date]' + assert_select 'input[name=?]', 'issue[due_date]', 0 + assert_select 'input[name=?]', "issue[custom_field_values][#{cf1.id}]" + assert_select 'input[name=?]', "issue[custom_field_values][#{cf2.id}]", 0 + end + + def test_get_new_without_tracker_id + @request.session[:user_id] = 2 + get :new, :project_id => 1 + assert_response :success + assert_template 'new' + + issue = assigns(:issue) + assert_not_nil issue + assert_equal Project.find(1).trackers.first, issue.tracker + end + + def test_get_new_with_no_default_status_should_display_an_error + @request.session[:user_id] = 2 + IssueStatus.delete_all + + get :new, :project_id => 1 + assert_response 500 + assert_error_tag :content => /No default issue/ + end + + def test_get_new_with_no_tracker_should_display_an_error + @request.session[:user_id] = 2 + Tracker.delete_all + + get :new, :project_id => 1 + assert_response 500 + assert_error_tag :content => /No tracker/ + end + + def test_update_form_for_new_issue + @request.session[:user_id] = 2 + xhr :post, :update_form, :project_id => 1, + :issue => {:tracker_id => 2, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5} + assert_response :success + assert_template 'update_form' + assert_template 'form' + assert_equal 'text/javascript', response.content_type + + issue = assigns(:issue) + assert_kind_of Issue, issue + assert_equal 1, issue.project_id + assert_equal 2, issue.tracker_id + assert_equal 'This is the test_new issue', issue.subject + end + + def test_update_form_for_new_issue_should_propose_transitions_based_on_initial_status + @request.session[:user_id] = 2 + WorkflowTransition.delete_all + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4) + + xhr :post, :update_form, :project_id => 1, + :issue => {:tracker_id => 1, + :status_id => 5, + :subject => 'This is an issue'} + + assert_equal 5, assigns(:issue).status_id + assert_equal [1,2,5], assigns(:allowed_statuses).map(&:id).sort + end + + def test_post_create + @request.session[:user_id] = 2 + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 3, + :status_id => 2, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5, + :start_date => '2010-11-07', + :estimated_hours => '', + :custom_field_values => {'2' => 'Value for field 2'}} + end + assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id + + issue = Issue.find_by_subject('This is the test_new issue') + assert_not_nil issue + assert_equal 2, issue.author_id + assert_equal 3, issue.tracker_id + assert_equal 2, issue.status_id + assert_equal Date.parse('2010-11-07'), issue.start_date + assert_nil issue.estimated_hours + v = issue.custom_values.where(:custom_field_id => 2).first + assert_not_nil v + assert_equal 'Value for field 2', v.value + end + + def test_post_new_with_group_assignment + group = Group.find(11) + project = Project.find(1) + project.members << Member.new(:principal => group, :roles => [Role.givable.first]) + + with_settings :issue_group_assignment => '1' do + @request.session[:user_id] = 2 + assert_difference 'Issue.count' do + post :create, :project_id => project.id, + :issue => {:tracker_id => 3, + :status_id => 1, + :subject => 'This is the test_new_with_group_assignment issue', + :assigned_to_id => group.id} + end + end + assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id + + issue = Issue.find_by_subject('This is the test_new_with_group_assignment issue') + assert_not_nil issue + assert_equal group, issue.assigned_to + end + + def test_post_create_without_start_date_and_default_start_date_is_not_creation_date + Setting.default_issue_start_date_to_creation_date = 0 + + @request.session[:user_id] = 2 + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 3, + :status_id => 2, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5, + :estimated_hours => '', + :custom_field_values => {'2' => 'Value for field 2'}} + end + assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id + + issue = Issue.find_by_subject('This is the test_new issue') + assert_not_nil issue + assert_nil issue.start_date + end + + def test_post_create_without_start_date_and_default_start_date_is_creation_date + Setting.default_issue_start_date_to_creation_date = 1 + + @request.session[:user_id] = 2 + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 3, + :status_id => 2, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5, + :estimated_hours => '', + :custom_field_values => {'2' => 'Value for field 2'}} + end + assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id + + issue = Issue.find_by_subject('This is the test_new issue') + assert_not_nil issue + assert_equal Date.today, issue.start_date + end + + def test_post_create_and_continue + @request.session[:user_id] = 2 + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 3, :subject => 'This is first issue', :priority_id => 5}, + :continue => '' + end + + issue = Issue.first(:order => 'id DESC') + assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook', :issue => {:tracker_id => 3} + assert_not_nil flash[:notice], "flash was not set" + assert_include %|##{issue.id}|, flash[:notice], "issue link not found in the flash message" + end + + def test_post_create_without_custom_fields_param + @request.session[:user_id] = 2 + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5} + end + assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id + end + + def test_post_create_with_multi_custom_field + field = IssueCustomField.find_by_name('Database') + field.update_attribute(:multiple, true) + + @request.session[:user_id] = 2 + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5, + :custom_field_values => {'1' => ['', 'MySQL', 'Oracle']}} + end + assert_response 302 + issue = Issue.first(:order => 'id DESC') + assert_equal ['MySQL', 'Oracle'], issue.custom_field_value(1).sort + end + + def test_post_create_with_empty_multi_custom_field + field = IssueCustomField.find_by_name('Database') + field.update_attribute(:multiple, true) + + @request.session[:user_id] = 2 + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5, + :custom_field_values => {'1' => ['']}} + end + assert_response 302 + issue = Issue.first(:order => 'id DESC') + assert_equal [''], issue.custom_field_value(1).sort + end + + def test_post_create_with_multi_user_custom_field + field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true, + :tracker_ids => [1], :is_for_all => true) + + @request.session[:user_id] = 2 + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5, + :custom_field_values => {field.id.to_s => ['', '2', '3']}} + end + assert_response 302 + issue = Issue.first(:order => 'id DESC') + assert_equal ['2', '3'], issue.custom_field_value(field).sort + end + + def test_post_create_with_required_custom_field_and_without_custom_fields_param + field = IssueCustomField.find_by_name('Database') + field.update_attribute(:is_required, true) + + @request.session[:user_id] = 2 + assert_no_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5} + end + assert_response :success + assert_template 'new' + issue = assigns(:issue) + assert_not_nil issue + assert_error_tag :content => /Database can't be blank/ + end + + def test_create_should_validate_required_fields + cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => 'due_date', :rule => 'required') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'required') + @request.session[:user_id] = 2 + + assert_no_difference 'Issue.count' do + post :create, :project_id => 1, :issue => { + :tracker_id => 2, + :status_id => 1, + :subject => 'Test', + :start_date => '', + :due_date => '', + :custom_field_values => {cf1.id.to_s => '', cf2.id.to_s => ''} + } + assert_response :success + assert_template 'new' + end + + assert_error_tag :content => /Due date can't be blank/i + assert_error_tag :content => /Bar can't be blank/i + end + + def test_create_should_ignore_readonly_fields + cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => 'due_date', :rule => 'readonly') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'readonly') + @request.session[:user_id] = 2 + + assert_difference 'Issue.count' do + post :create, :project_id => 1, :issue => { + :tracker_id => 2, + :status_id => 1, + :subject => 'Test', + :start_date => '2012-07-14', + :due_date => '2012-07-16', + :custom_field_values => {cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'} + } + assert_response 302 + end + + issue = Issue.first(:order => 'id DESC') + assert_equal Date.parse('2012-07-14'), issue.start_date + assert_nil issue.due_date + assert_equal 'value1', issue.custom_field_value(cf1) + assert_nil issue.custom_field_value(cf2) + end + + def test_post_create_with_watchers + @request.session[:user_id] = 2 + ActionMailer::Base.deliveries.clear + + assert_difference 'Watcher.count', 2 do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is a new issue with watchers', + :description => 'This is the description', + :priority_id => 5, + :watcher_user_ids => ['2', '3']} + end + issue = Issue.find_by_subject('This is a new issue with watchers') + assert_not_nil issue + assert_redirected_to :controller => 'issues', :action => 'show', :id => issue + + # Watchers added + assert_equal [2, 3], issue.watcher_user_ids.sort + assert issue.watched_by?(User.find(3)) + # Watchers notified + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail) + end + + def test_post_create_subissue + @request.session[:user_id] = 2 + + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is a child issue', + :parent_issue_id => '2'} + assert_response 302 + end + issue = Issue.order('id DESC').first + assert_equal Issue.find(2), issue.parent + end + + def test_post_create_subissue_with_sharp_parent_id + @request.session[:user_id] = 2 + + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is a child issue', + :parent_issue_id => '#2'} + assert_response 302 + end + issue = Issue.order('id DESC').first + assert_equal Issue.find(2), issue.parent + end + + def test_post_create_subissue_with_non_visible_parent_id_should_not_validate + @request.session[:user_id] = 2 + + assert_no_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is a child issue', + :parent_issue_id => '4'} + + assert_response :success + assert_select 'input[name=?][value=?]', 'issue[parent_issue_id]', '4' + assert_error_tag :content => /Parent task is invalid/i + end + end + + def test_post_create_subissue_with_non_numeric_parent_id_should_not_validate + @request.session[:user_id] = 2 + + assert_no_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is a child issue', + :parent_issue_id => '01ABC'} + + assert_response :success + assert_select 'input[name=?][value=?]', 'issue[parent_issue_id]', '01ABC' + assert_error_tag :content => /Parent task is invalid/i + end + end + + def test_post_create_private + @request.session[:user_id] = 2 + + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is a private issue', + :is_private => '1'} + end + issue = Issue.first(:order => 'id DESC') + assert issue.is_private? + end + + def test_post_create_private_with_set_own_issues_private_permission + role = Role.find(1) + role.remove_permission! :set_issues_private + role.add_permission! :set_own_issues_private + + @request.session[:user_id] = 2 + + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is a private issue', + :is_private => '1'} + end + issue = Issue.first(:order => 'id DESC') + assert issue.is_private? + end + + def test_post_create_should_send_a_notification + ActionMailer::Base.deliveries.clear + @request.session[:user_id] = 2 + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 3, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5, + :estimated_hours => '', + :custom_field_values => {'2' => 'Value for field 2'}} + end + assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id + + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_post_create_should_preserve_fields_values_on_validation_failure + @request.session[:user_id] = 2 + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + # empty subject + :subject => '', + :description => 'This is a description', + :priority_id => 6, + :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}} + assert_response :success + assert_template 'new' + + assert_select 'textarea[name=?]', 'issue[description]', :text => 'This is a description' + assert_select 'select[name=?]', 'issue[priority_id]' do + assert_select 'option[value=6][selected=selected]', :text => 'High' + end + # Custom fields + assert_select 'select[name=?]', 'issue[custom_field_values][1]' do + assert_select 'option[value=Oracle][selected=selected]', :text => 'Oracle' + end + assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', 'Value for field 2' + end + + def test_post_create_with_failure_should_preserve_watchers + assert !User.find(8).member_of?(Project.find(1)) + + @request.session[:user_id] = 2 + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :watcher_user_ids => ['3', '8']} + assert_response :success + assert_template 'new' + + assert_select 'input[name=?][value=2]:not(checked)', 'issue[watcher_user_ids][]' + assert_select 'input[name=?][value=3][checked=checked]', 'issue[watcher_user_ids][]' + assert_select 'input[name=?][value=8][checked=checked]', 'issue[watcher_user_ids][]' + end + + def test_post_create_should_ignore_non_safe_attributes + @request.session[:user_id] = 2 + assert_nothing_raised do + post :create, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" } + end + end + + def test_post_create_with_attachment + set_tmp_attachments_directory + @request.session[:user_id] = 2 + + assert_difference 'Issue.count' do + assert_difference 'Attachment.count' do + post :create, :project_id => 1, + :issue => { :tracker_id => '1', :subject => 'With attachment' }, + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}} + end + end + + issue = Issue.first(:order => 'id DESC') + attachment = Attachment.first(:order => 'id DESC') + + assert_equal issue, attachment.container + assert_equal 2, attachment.author_id + assert_equal 'testfile.txt', attachment.filename + assert_equal 'text/plain', attachment.content_type + assert_equal 'test file', attachment.description + assert_equal 59, attachment.filesize + assert File.exists?(attachment.diskfile) + assert_equal 59, File.size(attachment.diskfile) + end + + def test_post_create_with_failure_should_save_attachments + set_tmp_attachments_directory + @request.session[:user_id] = 2 + + assert_no_difference 'Issue.count' do + assert_difference 'Attachment.count' do + post :create, :project_id => 1, + :issue => { :tracker_id => '1', :subject => '' }, + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}} + assert_response :success + assert_template 'new' + end + end + + attachment = Attachment.first(:order => 'id DESC') + assert_equal 'testfile.txt', attachment.filename + assert File.exists?(attachment.diskfile) + assert_nil attachment.container + + assert_select 'input[name=?][value=?]', 'attachments[p0][token]', attachment.token + assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'testfile.txt' + end + + def test_post_create_with_failure_should_keep_saved_attachments + set_tmp_attachments_directory + attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2) + @request.session[:user_id] = 2 + + assert_no_difference 'Issue.count' do + assert_no_difference 'Attachment.count' do + post :create, :project_id => 1, + :issue => { :tracker_id => '1', :subject => '' }, + :attachments => {'p0' => {'token' => attachment.token}} + assert_response :success + assert_template 'new' + end + end + + assert_select 'input[name=?][value=?]', 'attachments[p0][token]', attachment.token + assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'testfile.txt' + end + + def test_post_create_should_attach_saved_attachments + set_tmp_attachments_directory + attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2) + @request.session[:user_id] = 2 + + assert_difference 'Issue.count' do + assert_no_difference 'Attachment.count' do + post :create, :project_id => 1, + :issue => { :tracker_id => '1', :subject => 'Saved attachments' }, + :attachments => {'p0' => {'token' => attachment.token}} + assert_response 302 + end + end + + issue = Issue.first(:order => 'id DESC') + assert_equal 1, issue.attachments.count + + attachment.reload + assert_equal issue, attachment.container + end + + context "without workflow privilege" do + setup do + WorkflowTransition.delete_all(["role_id = ?", Role.anonymous.id]) + Role.anonymous.add_permission! :add_issues, :add_issue_notes + end + + context "#new" do + should "propose default status only" do + get :new, :project_id => 1 + assert_response :success + assert_template 'new' + assert_select 'select[name=?]', 'issue[status_id]' do + assert_select 'option', 1 + assert_select 'option[value=?]', IssueStatus.default.id.to_s + end + end + + should "accept default status" do + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is an issue', + :status_id => 1} + end + issue = Issue.last(:order => 'id') + assert_equal IssueStatus.default, issue.status + end + + should "ignore unauthorized status" do + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is an issue', + :status_id => 3} + end + issue = Issue.last(:order => 'id') + assert_equal IssueStatus.default, issue.status + end + end + + context "#update" do + should "ignore status change" do + assert_difference 'Journal.count' do + put :update, :id => 1, :issue => {:status_id => 3, :notes => 'just trying'} + end + assert_equal 1, Issue.find(1).status_id + end + + should "ignore attributes changes" do + assert_difference 'Journal.count' do + put :update, :id => 1, :issue => {:subject => 'changed', :assigned_to_id => 2, :notes => 'just trying'} + end + issue = Issue.find(1) + assert_equal "Can't print recipes", issue.subject + assert_nil issue.assigned_to + end + end + end + + context "with workflow privilege" do + setup do + WorkflowTransition.delete_all(["role_id = ?", Role.anonymous.id]) + WorkflowTransition.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3) + WorkflowTransition.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4) + Role.anonymous.add_permission! :add_issues, :add_issue_notes + end + + context "#update" do + should "accept authorized status" do + assert_difference 'Journal.count' do + put :update, :id => 1, :issue => {:status_id => 3, :notes => 'just trying'} + end + assert_equal 3, Issue.find(1).status_id + end + + should "ignore unauthorized status" do + assert_difference 'Journal.count' do + put :update, :id => 1, :issue => {:status_id => 2, :notes => 'just trying'} + end + assert_equal 1, Issue.find(1).status_id + end + + should "accept authorized attributes changes" do + assert_difference 'Journal.count' do + put :update, :id => 1, :issue => {:assigned_to_id => 2, :notes => 'just trying'} + end + issue = Issue.find(1) + assert_equal 2, issue.assigned_to_id + end + + should "ignore unauthorized attributes changes" do + assert_difference 'Journal.count' do + put :update, :id => 1, :issue => {:subject => 'changed', :notes => 'just trying'} + end + issue = Issue.find(1) + assert_equal "Can't print recipes", issue.subject + end + end + + context "and :edit_issues permission" do + setup do + Role.anonymous.add_permission! :add_issues, :edit_issues + end + + should "accept authorized status" do + assert_difference 'Journal.count' do + put :update, :id => 1, :issue => {:status_id => 3, :notes => 'just trying'} + end + assert_equal 3, Issue.find(1).status_id + end + + should "ignore unauthorized status" do + assert_difference 'Journal.count' do + put :update, :id => 1, :issue => {:status_id => 2, :notes => 'just trying'} + end + assert_equal 1, Issue.find(1).status_id + end + + should "accept authorized attributes changes" do + assert_difference 'Journal.count' do + put :update, :id => 1, :issue => {:subject => 'changed', :assigned_to_id => 2, :notes => 'just trying'} + end + issue = Issue.find(1) + assert_equal "changed", issue.subject + assert_equal 2, issue.assigned_to_id + end + end + end + + def test_new_as_copy + @request.session[:user_id] = 2 + get :new, :project_id => 1, :copy_from => 1 + + assert_response :success + assert_template 'new' + + assert_not_nil assigns(:issue) + orig = Issue.find(1) + assert_equal 1, assigns(:issue).project_id + assert_equal orig.subject, assigns(:issue).subject + assert assigns(:issue).copy? + + assert_select 'form[id=issue-form][action=/projects/ecookbook/issues]' do + assert_select 'select[name=?]', 'issue[project_id]' do + assert_select 'option[value=1][selected=selected]', :text => 'eCookbook' + assert_select 'option[value=2]:not([selected])', :text => 'OnlineStore' + end + assert_select 'input[name=copy_from][value=1]' + end + + # "New issue" menu item should not link to copy + assert_select '#main-menu a.new-issue[href=/projects/ecookbook/issues/new]' + end + + def test_new_as_copy_with_attachments_should_show_copy_attachments_checkbox + @request.session[:user_id] = 2 + issue = Issue.find(3) + assert issue.attachments.count > 0 + get :new, :project_id => 1, :copy_from => 3 + + assert_select 'input[name=copy_attachments][type=checkbox][checked=checked][value=1]' + end + + def test_new_as_copy_without_attachments_should_not_show_copy_attachments_checkbox + @request.session[:user_id] = 2 + issue = Issue.find(3) + issue.attachments.delete_all + get :new, :project_id => 1, :copy_from => 3 + + assert_select 'input[name=copy_attachments]', 0 + end + + def test_new_as_copy_with_subtasks_should_show_copy_subtasks_checkbox + @request.session[:user_id] = 2 + issue = Issue.generate_with_descendants! + get :new, :project_id => 1, :copy_from => issue.id + + assert_select 'input[type=checkbox][name=copy_subtasks][checked=checked][value=1]' + end + + def test_new_as_copy_with_invalid_issue_should_respond_with_404 + @request.session[:user_id] = 2 + get :new, :project_id => 1, :copy_from => 99999 + assert_response 404 + end + + def test_create_as_copy_on_different_project + @request.session[:user_id] = 2 + assert_difference 'Issue.count' do + post :create, :project_id => 1, :copy_from => 1, + :issue => {:project_id => '2', :tracker_id => '3', :status_id => '1', :subject => 'Copy'} + + assert_not_nil assigns(:issue) + assert assigns(:issue).copy? + end + issue = Issue.first(:order => 'id DESC') + assert_redirected_to "/issues/#{issue.id}" + + assert_equal 2, issue.project_id + assert_equal 3, issue.tracker_id + assert_equal 'Copy', issue.subject + end + + def test_create_as_copy_should_copy_attachments + @request.session[:user_id] = 2 + issue = Issue.find(3) + count = issue.attachments.count + assert count > 0 + + assert_difference 'Issue.count' do + assert_difference 'Attachment.count', count do + assert_no_difference 'Journal.count' do + post :create, :project_id => 1, :copy_from => 3, + :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with attachments'}, + :copy_attachments => '1' + end + end + end + copy = Issue.first(:order => 'id DESC') + assert_equal count, copy.attachments.count + assert_equal issue.attachments.map(&:filename).sort, copy.attachments.map(&:filename).sort + end + + def test_create_as_copy_without_copy_attachments_option_should_not_copy_attachments + @request.session[:user_id] = 2 + issue = Issue.find(3) + count = issue.attachments.count + assert count > 0 + + assert_difference 'Issue.count' do + assert_no_difference 'Attachment.count' do + assert_no_difference 'Journal.count' do + post :create, :project_id => 1, :copy_from => 3, + :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with attachments'} + end + end + end + copy = Issue.first(:order => 'id DESC') + assert_equal 0, copy.attachments.count + end + + def test_create_as_copy_with_attachments_should_add_new_files + @request.session[:user_id] = 2 + issue = Issue.find(3) + count = issue.attachments.count + assert count > 0 + + assert_difference 'Issue.count' do + assert_difference 'Attachment.count', count + 1 do + assert_no_difference 'Journal.count' do + post :create, :project_id => 1, :copy_from => 3, + :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with attachments'}, + :copy_attachments => '1', + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}} + end + end + end + copy = Issue.first(:order => 'id DESC') + assert_equal count + 1, copy.attachments.count + end + + def test_create_as_copy_should_add_relation_with_copied_issue + @request.session[:user_id] = 2 + + assert_difference 'Issue.count' do + assert_difference 'IssueRelation.count' do + post :create, :project_id => 1, :copy_from => 1, + :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy'} + end + end + copy = Issue.first(:order => 'id DESC') + assert_equal 1, copy.relations.size + end + + def test_create_as_copy_should_copy_subtasks + @request.session[:user_id] = 2 + issue = Issue.generate_with_descendants! + count = issue.descendants.count + + assert_difference 'Issue.count', count+1 do + assert_no_difference 'Journal.count' do + post :create, :project_id => 1, :copy_from => issue.id, + :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with subtasks'}, + :copy_subtasks => '1' + end + end + copy = Issue.where(:parent_id => nil).first(:order => 'id DESC') + assert_equal count, copy.descendants.count + assert_equal issue.descendants.map(&:subject).sort, copy.descendants.map(&:subject).sort + end + + def test_create_as_copy_without_copy_subtasks_option_should_not_copy_subtasks + @request.session[:user_id] = 2 + issue = Issue.generate_with_descendants! + + assert_difference 'Issue.count', 1 do + assert_no_difference 'Journal.count' do + post :create, :project_id => 1, :copy_from => 3, + :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with subtasks'} + end + end + copy = Issue.where(:parent_id => nil).first(:order => 'id DESC') + assert_equal 0, copy.descendants.count + end + + def test_create_as_copy_with_failure + @request.session[:user_id] = 2 + post :create, :project_id => 1, :copy_from => 1, + :issue => {:project_id => '2', :tracker_id => '3', :status_id => '1', :subject => ''} + + assert_response :success + assert_template 'new' + + assert_not_nil assigns(:issue) + assert assigns(:issue).copy? + + assert_select 'form#issue-form[action=/projects/ecookbook/issues]' do + assert_select 'select[name=?]', 'issue[project_id]' do + assert_select 'option[value=1]:not([selected])', :text => 'eCookbook' + assert_select 'option[value=2][selected=selected]', :text => 'OnlineStore' + end + assert_select 'input[name=copy_from][value=1]' + end + end + + def test_create_as_copy_on_project_without_permission_should_ignore_target_project + @request.session[:user_id] = 2 + assert !User.find(2).member_of?(Project.find(4)) + + assert_difference 'Issue.count' do + post :create, :project_id => 1, :copy_from => 1, + :issue => {:project_id => '4', :tracker_id => '3', :status_id => '1', :subject => 'Copy'} + end + issue = Issue.first(:order => 'id DESC') + assert_equal 1, issue.project_id + end + + def test_get_edit + @request.session[:user_id] = 2 + get :edit, :id => 1 + assert_response :success + assert_template 'edit' + assert_not_nil assigns(:issue) + assert_equal Issue.find(1), assigns(:issue) + + # Be sure we don't display inactive IssuePriorities + assert ! IssuePriority.find(15).active? + assert_select 'select[name=?]', 'issue[priority_id]' do + assert_select 'option[value=15]', 0 + end + end + + def test_get_edit_should_display_the_time_entry_form_with_log_time_permission + @request.session[:user_id] = 2 + Role.find_by_name('Manager').update_attribute :permissions, [:view_issues, :edit_issues, :log_time] + + get :edit, :id => 1 + assert_select 'input[name=?]', 'time_entry[hours]' + end + + def test_get_edit_should_not_display_the_time_entry_form_without_log_time_permission + @request.session[:user_id] = 2 + Role.find_by_name('Manager').remove_permission! :log_time + + get :edit, :id => 1 + assert_select 'input[name=?]', 'time_entry[hours]', 0 + end + + def test_get_edit_with_params + @request.session[:user_id] = 2 + get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }, + :time_entry => { :hours => '2.5', :comments => 'test_get_edit_with_params', :activity_id => 10 } + assert_response :success + assert_template 'edit' + + issue = assigns(:issue) + assert_not_nil issue + + assert_equal 5, issue.status_id + assert_select 'select[name=?]', 'issue[status_id]' do + assert_select 'option[value=5][selected=selected]', :text => 'Closed' + end + + assert_equal 7, issue.priority_id + assert_select 'select[name=?]', 'issue[priority_id]' do + assert_select 'option[value=7][selected=selected]', :text => 'Urgent' + end + + assert_select 'input[name=?][value=2.5]', 'time_entry[hours]' + assert_select 'select[name=?]', 'time_entry[activity_id]' do + assert_select 'option[value=10][selected=selected]', :text => 'Development' + end + assert_select 'input[name=?][value=test_get_edit_with_params]', 'time_entry[comments]' + end + + def test_get_edit_with_multi_custom_field + field = CustomField.find(1) + field.update_attribute :multiple, true + issue = Issue.find(1) + issue.custom_field_values = {1 => ['MySQL', 'Oracle']} + issue.save! + + @request.session[:user_id] = 2 + get :edit, :id => 1 + assert_response :success + assert_template 'edit' + + assert_select 'select[name=?][multiple=multiple]', 'issue[custom_field_values][1][]' do + assert_select 'option', 3 + assert_select 'option[value=MySQL][selected=selected]' + assert_select 'option[value=Oracle][selected=selected]' + assert_select 'option[value=PostgreSQL]:not([selected])' + end + end + + def test_update_form_for_existing_issue + @request.session[:user_id] = 2 + xhr :put, :update_form, :project_id => 1, + :id => 1, + :issue => {:tracker_id => 2, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5} + assert_response :success + assert_equal 'text/javascript', response.content_type + assert_template 'update_form' + assert_template 'form' + + issue = assigns(:issue) + assert_kind_of Issue, issue + assert_equal 1, issue.id + assert_equal 1, issue.project_id + assert_equal 2, issue.tracker_id + assert_equal 'This is the test_new issue', issue.subject + end + + def test_update_form_for_existing_issue_should_keep_issue_author + @request.session[:user_id] = 3 + xhr :put, :update_form, :project_id => 1, :id => 1, :issue => {:subject => 'Changed'} + assert_response :success + assert_equal 'text/javascript', response.content_type + + issue = assigns(:issue) + assert_equal User.find(2), issue.author + assert_equal 2, issue.author_id + assert_not_equal User.current, issue.author + end + + def test_update_form_for_existing_issue_should_propose_transitions_based_on_initial_status + @request.session[:user_id] = 2 + WorkflowTransition.delete_all + WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 1) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 5) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 5, :new_status_id => 4) + + xhr :put, :update_form, :project_id => 1, + :id => 2, + :issue => {:tracker_id => 2, + :status_id => 5, + :subject => 'This is an issue'} + + assert_equal 5, assigns(:issue).status_id + assert_equal [1,2,5], assigns(:allowed_statuses).map(&:id).sort + end + + def test_update_form_for_existing_issue_with_project_change + @request.session[:user_id] = 2 + xhr :put, :update_form, :project_id => 1, + :id => 1, + :issue => {:project_id => 2, + :tracker_id => 2, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5} + assert_response :success + assert_template 'form' + + issue = assigns(:issue) + assert_kind_of Issue, issue + assert_equal 1, issue.id + assert_equal 2, issue.project_id + assert_equal 2, issue.tracker_id + assert_equal 'This is the test_new issue', issue.subject + end + + def test_put_update_without_custom_fields_param + @request.session[:user_id] = 2 + ActionMailer::Base.deliveries.clear + + issue = Issue.find(1) + assert_equal '125', issue.custom_value_for(2).value + old_subject = issue.subject + new_subject = 'Subject modified by IssuesControllerTest#test_post_edit' + + assert_difference('Journal.count') do + assert_difference('JournalDetail.count', 2) do + put :update, :id => 1, :issue => {:subject => new_subject, + :priority_id => '6', + :category_id => '1' # no change + } + end + end + assert_redirected_to :action => 'show', :id => '1' + issue.reload + assert_equal new_subject, issue.subject + # Make sure custom fields were not cleared + assert_equal '125', issue.custom_value_for(2).value + + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]") + assert_mail_body_match "Subject changed from #{old_subject} to #{new_subject}", mail + end + + def test_put_update_with_project_change + @request.session[:user_id] = 2 + ActionMailer::Base.deliveries.clear + + assert_difference('Journal.count') do + assert_difference('JournalDetail.count', 3) do + put :update, :id => 1, :issue => {:project_id => '2', + :tracker_id => '1', # no change + :priority_id => '6', + :category_id => '3' + } + end + end + assert_redirected_to :action => 'show', :id => '1' + issue = Issue.find(1) + assert_equal 2, issue.project_id + assert_equal 1, issue.tracker_id + assert_equal 6, issue.priority_id + assert_equal 3, issue.category_id + + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]") + assert_mail_body_match "Project changed from eCookbook to OnlineStore", mail + end + + def test_put_update_with_tracker_change + @request.session[:user_id] = 2 + ActionMailer::Base.deliveries.clear + + assert_difference('Journal.count') do + assert_difference('JournalDetail.count', 2) do + put :update, :id => 1, :issue => {:project_id => '1', + :tracker_id => '2', + :priority_id => '6' + } + end + end + assert_redirected_to :action => 'show', :id => '1' + issue = Issue.find(1) + assert_equal 1, issue.project_id + assert_equal 2, issue.tracker_id + assert_equal 6, issue.priority_id + assert_equal 1, issue.category_id + + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]") + assert_mail_body_match "Tracker changed from Bug to Feature request", mail + end + + def test_put_update_with_custom_field_change + @request.session[:user_id] = 2 + issue = Issue.find(1) + assert_equal '125', issue.custom_value_for(2).value + + assert_difference('Journal.count') do + assert_difference('JournalDetail.count', 3) do + put :update, :id => 1, :issue => {:subject => 'Custom field change', + :priority_id => '6', + :category_id => '1', # no change + :custom_field_values => { '2' => 'New custom value' } + } + end + end + assert_redirected_to :action => 'show', :id => '1' + issue.reload + assert_equal 'New custom value', issue.custom_value_for(2).value + + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + assert_mail_body_match "Searchable field changed from 125 to New custom value", mail + end + + def test_put_update_with_multi_custom_field_change + field = CustomField.find(1) + field.update_attribute :multiple, true + issue = Issue.find(1) + issue.custom_field_values = {1 => ['MySQL', 'Oracle']} + issue.save! + + @request.session[:user_id] = 2 + assert_difference('Journal.count') do + assert_difference('JournalDetail.count', 3) do + put :update, :id => 1, + :issue => { + :subject => 'Custom field change', + :custom_field_values => { '1' => ['', 'Oracle', 'PostgreSQL'] } + } + end + end + assert_redirected_to :action => 'show', :id => '1' + assert_equal ['Oracle', 'PostgreSQL'], Issue.find(1).custom_field_value(1).sort + end + + def test_put_update_with_status_and_assignee_change + issue = Issue.find(1) + assert_equal 1, issue.status_id + @request.session[:user_id] = 2 + assert_difference('TimeEntry.count', 0) do + put :update, + :id => 1, + :issue => { :status_id => 2, :assigned_to_id => 3, :notes => 'Assigned to dlopper' }, + :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first } + end + assert_redirected_to :action => 'show', :id => '1' + issue.reload + assert_equal 2, issue.status_id + j = Journal.order('id DESC').first + assert_equal 'Assigned to dlopper', j.notes + assert_equal 2, j.details.size + + mail = ActionMailer::Base.deliveries.last + assert_mail_body_match "Status changed from New to Assigned", mail + # subject should contain the new status + assert mail.subject.include?("(#{ IssueStatus.find(2).name })") + end + + def test_put_update_with_note_only + notes = 'Note added by IssuesControllerTest#test_update_with_note_only' + # anonymous user + put :update, + :id => 1, + :issue => { :notes => notes } + assert_redirected_to :action => 'show', :id => '1' + j = Journal.order('id DESC').first + assert_equal notes, j.notes + assert_equal 0, j.details.size + assert_equal User.anonymous, j.user + + mail = ActionMailer::Base.deliveries.last + assert_mail_body_match notes, mail + end + + def test_put_update_with_private_note_only + notes = 'Private note' + @request.session[:user_id] = 2 + + assert_difference 'Journal.count' do + put :update, :id => 1, :issue => {:notes => notes, :private_notes => '1'} + assert_redirected_to :action => 'show', :id => '1' + end + + j = Journal.order('id DESC').first + assert_equal notes, j.notes + assert_equal true, j.private_notes + end + + def test_put_update_with_private_note_and_changes + notes = 'Private note' + @request.session[:user_id] = 2 + + assert_difference 'Journal.count', 2 do + put :update, :id => 1, :issue => {:subject => 'New subject', :notes => notes, :private_notes => '1'} + assert_redirected_to :action => 'show', :id => '1' + end + + j = Journal.order('id DESC').first + assert_equal notes, j.notes + assert_equal true, j.private_notes + assert_equal 0, j.details.count + + j = Journal.order('id DESC').offset(1).first + assert_nil j.notes + assert_equal false, j.private_notes + assert_equal 1, j.details.count + end + + def test_put_update_with_note_and_spent_time + @request.session[:user_id] = 2 + spent_hours_before = Issue.find(1).spent_hours + assert_difference('TimeEntry.count') do + put :update, + :id => 1, + :issue => { :notes => '2.5 hours added' }, + :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first.id } + end + assert_redirected_to :action => 'show', :id => '1' + + issue = Issue.find(1) + + j = Journal.order('id DESC').first + assert_equal '2.5 hours added', j.notes + assert_equal 0, j.details.size + + t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time') + assert_not_nil t + assert_equal 2.5, t.hours + assert_equal spent_hours_before + 2.5, issue.spent_hours + end + + def test_put_update_should_preserve_parent_issue_even_if_not_visible + parent = Issue.generate!(:project_id => 1, :is_private => true) + issue = Issue.generate!(:parent_issue_id => parent.id) + assert !parent.visible?(User.find(3)) + @request.session[:user_id] = 3 + + get :edit, :id => issue.id + assert_select 'input[name=?][value=?]', 'issue[parent_issue_id]', parent.id.to_s + + put :update, :id => issue.id, :issue => {:subject => 'New subject', :parent_issue_id => parent.id.to_s} + assert_response 302 + assert_equal parent, issue.parent + end + + def test_put_update_with_attachment_only + set_tmp_attachments_directory + + # Delete all fixtured journals, a race condition can occur causing the wrong + # journal to get fetched in the next find. + Journal.delete_all + + # anonymous user + assert_difference 'Attachment.count' do + put :update, :id => 1, + :issue => {:notes => ''}, + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}} + end + + assert_redirected_to :action => 'show', :id => '1' + j = Issue.find(1).journals.reorder('id DESC').first + assert j.notes.blank? + assert_equal 1, j.details.size + assert_equal 'testfile.txt', j.details.first.value + assert_equal User.anonymous, j.user + + attachment = Attachment.first(:order => 'id DESC') + assert_equal Issue.find(1), attachment.container + assert_equal User.anonymous, attachment.author + assert_equal 'testfile.txt', attachment.filename + assert_equal 'text/plain', attachment.content_type + assert_equal 'test file', attachment.description + assert_equal 59, attachment.filesize + assert File.exists?(attachment.diskfile) + assert_equal 59, File.size(attachment.diskfile) + + mail = ActionMailer::Base.deliveries.last + assert_mail_body_match 'testfile.txt', mail + end + + def test_put_update_with_failure_should_save_attachments + set_tmp_attachments_directory + @request.session[:user_id] = 2 + + assert_no_difference 'Journal.count' do + assert_difference 'Attachment.count' do + put :update, :id => 1, + :issue => { :subject => '' }, + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}} + assert_response :success + assert_template 'edit' + end + end + + attachment = Attachment.first(:order => 'id DESC') + assert_equal 'testfile.txt', attachment.filename + assert File.exists?(attachment.diskfile) + assert_nil attachment.container + + assert_select 'input[name=?][value=?]', 'attachments[p0][token]', attachment.token + assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'testfile.txt' + end + + def test_put_update_with_failure_should_keep_saved_attachments + set_tmp_attachments_directory + attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2) + @request.session[:user_id] = 2 + + assert_no_difference 'Journal.count' do + assert_no_difference 'Attachment.count' do + put :update, :id => 1, + :issue => { :subject => '' }, + :attachments => {'p0' => {'token' => attachment.token}} + assert_response :success + assert_template 'edit' + end + end + + assert_select 'input[name=?][value=?]', 'attachments[p0][token]', attachment.token + assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'testfile.txt' + end + + def test_put_update_should_attach_saved_attachments + set_tmp_attachments_directory + attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2) + @request.session[:user_id] = 2 + + assert_difference 'Journal.count' do + assert_difference 'JournalDetail.count' do + assert_no_difference 'Attachment.count' do + put :update, :id => 1, + :issue => {:notes => 'Attachment added'}, + :attachments => {'p0' => {'token' => attachment.token}} + assert_redirected_to '/issues/1' + end + end + end + + attachment.reload + assert_equal Issue.find(1), attachment.container + + journal = Journal.first(:order => 'id DESC') + assert_equal 1, journal.details.size + assert_equal 'testfile.txt', journal.details.first.value + end + + def test_put_update_with_attachment_that_fails_to_save + set_tmp_attachments_directory + + # Delete all fixtured journals, a race condition can occur causing the wrong + # journal to get fetched in the next find. + Journal.delete_all + + # Mock out the unsaved attachment + Attachment.any_instance.stubs(:create).returns(Attachment.new) + + # anonymous user + put :update, + :id => 1, + :issue => {:notes => ''}, + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}} + assert_redirected_to :action => 'show', :id => '1' + assert_equal '1 file(s) could not be saved.', flash[:warning] + end + + def test_put_update_with_no_change + issue = Issue.find(1) + issue.journals.clear + ActionMailer::Base.deliveries.clear + + put :update, + :id => 1, + :issue => {:notes => ''} + assert_redirected_to :action => 'show', :id => '1' + + issue.reload + assert issue.journals.empty? + # No email should be sent + assert ActionMailer::Base.deliveries.empty? + end + + def test_put_update_should_send_a_notification + @request.session[:user_id] = 2 + ActionMailer::Base.deliveries.clear + issue = Issue.find(1) + old_subject = issue.subject + new_subject = 'Subject modified by IssuesControllerTest#test_post_edit' + + put :update, :id => 1, :issue => {:subject => new_subject, + :priority_id => '6', + :category_id => '1' # no change + } + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_put_update_with_invalid_spent_time_hours_only + @request.session[:user_id] = 2 + notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time' + + assert_no_difference('Journal.count') do + put :update, + :id => 1, + :issue => {:notes => notes}, + :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"} + end + assert_response :success + assert_template 'edit' + + assert_error_tag :descendant => {:content => /Activity can't be blank/} + assert_select 'textarea[name=?]', 'issue[notes]', :text => notes + assert_select 'input[name=?][value=?]', 'time_entry[hours]', '2z' + end + + def test_put_update_with_invalid_spent_time_comments_only + @request.session[:user_id] = 2 + notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time' + + assert_no_difference('Journal.count') do + put :update, + :id => 1, + :issue => {:notes => notes}, + :time_entry => {"comments"=>"this is my comment", "activity_id"=>"", "hours"=>""} + end + assert_response :success + assert_template 'edit' + + assert_error_tag :descendant => {:content => /Activity can't be blank/} + assert_error_tag :descendant => {:content => /Hours can't be blank/} + assert_select 'textarea[name=?]', 'issue[notes]', :text => notes + assert_select 'input[name=?][value=?]', 'time_entry[comments]', 'this is my comment' + end + + def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject + issue = Issue.find(2) + @request.session[:user_id] = 2 + + put :update, + :id => issue.id, + :issue => { + :fixed_version_id => 4 + } + + assert_response :redirect + issue.reload + assert_equal 4, issue.fixed_version_id + assert_not_equal issue.project_id, issue.fixed_version.project_id + end + + def test_put_update_should_redirect_back_using_the_back_url_parameter + issue = Issue.find(2) + @request.session[:user_id] = 2 + + put :update, + :id => issue.id, + :issue => { + :fixed_version_id => 4 + }, + :back_url => '/issues' + + assert_response :redirect + assert_redirected_to '/issues' + end + + def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host + issue = Issue.find(2) + @request.session[:user_id] = 2 + + put :update, + :id => issue.id, + :issue => { + :fixed_version_id => 4 + }, + :back_url => 'http://google.com' + + assert_response :redirect + assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id + end + + def test_get_bulk_edit + @request.session[:user_id] = 2 + get :bulk_edit, :ids => [1, 2] + assert_response :success + assert_template 'bulk_edit' + + assert_select 'ul#bulk-selection' do + assert_select 'li', 2 + assert_select 'li a', :text => 'Bug #1' + end + + assert_select 'form#bulk_edit_form[action=?]', '/issues/bulk_update' do + assert_select 'input[name=?]', 'ids[]', 2 + assert_select 'input[name=?][value=1][type=hidden]', 'ids[]' + + assert_select 'select[name=?]', 'issue[project_id]' + assert_select 'input[name=?]', 'issue[parent_issue_id]' + + # Project specific custom field, date type + field = CustomField.find(9) + assert !field.is_for_all? + assert_equal 'date', field.field_format + assert_select 'input[name=?]', 'issue[custom_field_values][9]' + + # System wide custom field + assert CustomField.find(1).is_for_all? + assert_select 'select[name=?]', 'issue[custom_field_values][1]' + + # Be sure we don't display inactive IssuePriorities + assert ! IssuePriority.find(15).active? + assert_select 'select[name=?]', 'issue[priority_id]' do + assert_select 'option[value=15]', 0 + end + end + end + + def test_get_bulk_edit_on_different_projects + @request.session[:user_id] = 2 + get :bulk_edit, :ids => [1, 2, 6] + assert_response :success + assert_template 'bulk_edit' + + # Can not set issues from different projects as children of an issue + assert_select 'input[name=?]', 'issue[parent_issue_id]', 0 + + # Project specific custom field, date type + field = CustomField.find(9) + assert !field.is_for_all? + assert !field.project_ids.include?(Issue.find(6).project_id) + assert_select 'input[name=?]', 'issue[custom_field_values][9]', 0 + end + + def test_get_bulk_edit_with_user_custom_field + field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user', :is_for_all => true) + + @request.session[:user_id] = 2 + get :bulk_edit, :ids => [1, 2] + assert_response :success + assert_template 'bulk_edit' + + assert_select 'select.user_cf[name=?]', "issue[custom_field_values][#{field.id}]" do + assert_select 'option', Project.find(1).users.count + 2 # "no change" + "none" options + end + end + + def test_get_bulk_edit_with_version_custom_field + field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true) + + @request.session[:user_id] = 2 + get :bulk_edit, :ids => [1, 2] + assert_response :success + assert_template 'bulk_edit' + + assert_select 'select.version_cf[name=?]', "issue[custom_field_values][#{field.id}]" do + assert_select 'option', Project.find(1).shared_versions.count + 2 # "no change" + "none" options + end + end + + def test_get_bulk_edit_with_multi_custom_field + field = CustomField.find(1) + field.update_attribute :multiple, true + + @request.session[:user_id] = 2 + get :bulk_edit, :ids => [1, 2] + assert_response :success + assert_template 'bulk_edit' + + assert_select 'select[name=?]', 'issue[custom_field_values][1][]' do + assert_select 'option', field.possible_values.size + 1 # "none" options + end + end + + def test_bulk_edit_should_only_propose_statuses_allowed_for_all_issues + WorkflowTransition.delete_all + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 1) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 1) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 3) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 5) + @request.session[:user_id] = 2 + get :bulk_edit, :ids => [1, 2] + + assert_response :success + statuses = assigns(:available_statuses) + assert_not_nil statuses + assert_equal [1, 3], statuses.map(&:id).sort + + assert_select 'select[name=?]', 'issue[status_id]' do + assert_select 'option', 3 # 2 statuses + "no change" option + end + end + + def test_bulk_edit_should_propose_target_project_open_shared_versions + @request.session[:user_id] = 2 + post :bulk_edit, :ids => [1, 2, 6], :issue => {:project_id => 1} + assert_response :success + assert_template 'bulk_edit' + assert_equal Project.find(1).shared_versions.open.all.sort, assigns(:versions).sort + + assert_select 'select[name=?]', 'issue[fixed_version_id]' do + assert_select 'option', :text => '2.0' + end + end + + def test_bulk_edit_should_propose_target_project_categories + @request.session[:user_id] = 2 + post :bulk_edit, :ids => [1, 2, 6], :issue => {:project_id => 1} + assert_response :success + assert_template 'bulk_edit' + assert_equal Project.find(1).issue_categories.sort, assigns(:categories).sort + + assert_select 'select[name=?]', 'issue[category_id]' do + assert_select 'option', :text => 'Recipes' + end + end + + def test_bulk_update + @request.session[:user_id] = 2 + # update issues priority + post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing', + :issue => {:priority_id => 7, + :assigned_to_id => '', + :custom_field_values => {'2' => ''}} + + assert_response 302 + # check that the issues were updated + assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id} + + issue = Issue.find(1) + journal = issue.journals.reorder('created_on DESC').first + assert_equal '125', issue.custom_value_for(2).value + assert_equal 'Bulk editing', journal.notes + assert_equal 1, journal.details.size + end + + def test_bulk_update_with_group_assignee + group = Group.find(11) + project = Project.find(1) + project.members << Member.new(:principal => group, :roles => [Role.givable.first]) + + @request.session[:user_id] = 2 + # update issues assignee + post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing', + :issue => {:priority_id => '', + :assigned_to_id => group.id, + :custom_field_values => {'2' => ''}} + + assert_response 302 + assert_equal [group, group], Issue.find_all_by_id([1, 2]).collect {|i| i.assigned_to} + end + + def test_bulk_update_on_different_projects + @request.session[:user_id] = 2 + # update issues priority + post :bulk_update, :ids => [1, 2, 6], :notes => 'Bulk editing', + :issue => {:priority_id => 7, + :assigned_to_id => '', + :custom_field_values => {'2' => ''}} + + assert_response 302 + # check that the issues were updated + assert_equal [7, 7, 7], Issue.find([1,2,6]).map(&:priority_id) + + issue = Issue.find(1) + journal = issue.journals.reorder('created_on DESC').first + assert_equal '125', issue.custom_value_for(2).value + assert_equal 'Bulk editing', journal.notes + assert_equal 1, journal.details.size + end + + def test_bulk_update_on_different_projects_without_rights + @request.session[:user_id] = 3 + user = User.find(3) + action = { :controller => "issues", :action => "bulk_update" } + assert user.allowed_to?(action, Issue.find(1).project) + assert ! user.allowed_to?(action, Issue.find(6).project) + post :bulk_update, :ids => [1, 6], :notes => 'Bulk should fail', + :issue => {:priority_id => 7, + :assigned_to_id => '', + :custom_field_values => {'2' => ''}} + assert_response 403 + assert_not_equal "Bulk should fail", Journal.last.notes + end + + def test_bullk_update_should_send_a_notification + @request.session[:user_id] = 2 + ActionMailer::Base.deliveries.clear + post(:bulk_update, + { + :ids => [1, 2], + :notes => 'Bulk editing', + :issue => { + :priority_id => 7, + :assigned_to_id => '', + :custom_field_values => {'2' => ''} + } + }) + + assert_response 302 + assert_equal 2, ActionMailer::Base.deliveries.size + end + + def test_bulk_update_project + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1, 2], :issue => {:project_id => '2'} + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook' + # Issues moved to project 2 + assert_equal 2, Issue.find(1).project_id + assert_equal 2, Issue.find(2).project_id + # No tracker change + assert_equal 1, Issue.find(1).tracker_id + assert_equal 2, Issue.find(2).tracker_id + end + + def test_bulk_update_project_on_single_issue_should_follow_when_needed + @request.session[:user_id] = 2 + post :bulk_update, :id => 1, :issue => {:project_id => '2'}, :follow => '1' + assert_redirected_to '/issues/1' + end + + def test_bulk_update_project_on_multiple_issues_should_follow_when_needed + @request.session[:user_id] = 2 + post :bulk_update, :id => [1, 2], :issue => {:project_id => '2'}, :follow => '1' + assert_redirected_to '/projects/onlinestore/issues' + end + + def test_bulk_update_tracker + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1, 2], :issue => {:tracker_id => '2'} + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook' + assert_equal 2, Issue.find(1).tracker_id + assert_equal 2, Issue.find(2).tracker_id + end + + def test_bulk_update_status + @request.session[:user_id] = 2 + # update issues priority + post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status', + :issue => {:priority_id => '', + :assigned_to_id => '', + :status_id => '5'} + + assert_response 302 + issue = Issue.find(1) + assert issue.closed? + end + + def test_bulk_update_priority + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1, 2], :issue => {:priority_id => 6} + + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook' + assert_equal 6, Issue.find(1).priority_id + assert_equal 6, Issue.find(2).priority_id + end + + def test_bulk_update_with_notes + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1, 2], :notes => 'Moving two issues' + + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook' + assert_equal 'Moving two issues', Issue.find(1).journals.sort_by(&:id).last.notes + assert_equal 'Moving two issues', Issue.find(2).journals.sort_by(&:id).last.notes + end + + def test_bulk_update_parent_id + IssueRelation.delete_all + + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1, 3], + :notes => 'Bulk editing parent', + :issue => {:priority_id => '', :assigned_to_id => '', :status_id => '', :parent_issue_id => '2'} + + assert_response 302 + parent = Issue.find(2) + assert_equal parent.id, Issue.find(1).parent_id + assert_equal parent.id, Issue.find(3).parent_id + assert_equal [1, 3], parent.children.collect(&:id).sort + end + + def test_bulk_update_custom_field + @request.session[:user_id] = 2 + # update issues priority + post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field', + :issue => {:priority_id => '', + :assigned_to_id => '', + :custom_field_values => {'2' => '777'}} + + assert_response 302 + + issue = Issue.find(1) + journal = issue.journals.reorder('created_on DESC').first + assert_equal '777', issue.custom_value_for(2).value + assert_equal 1, journal.details.size + assert_equal '125', journal.details.first.old_value + assert_equal '777', journal.details.first.value + end + + def test_bulk_update_custom_field_to_blank + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1, 3], :notes => 'Bulk editing custom field', + :issue => {:priority_id => '', + :assigned_to_id => '', + :custom_field_values => {'1' => '__none__'}} + assert_response 302 + assert_equal '', Issue.find(1).custom_field_value(1) + assert_equal '', Issue.find(3).custom_field_value(1) + end + + def test_bulk_update_multi_custom_field + field = CustomField.find(1) + field.update_attribute :multiple, true + + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1, 2, 3], :notes => 'Bulk editing multi custom field', + :issue => {:priority_id => '', + :assigned_to_id => '', + :custom_field_values => {'1' => ['MySQL', 'Oracle']}} + + assert_response 302 + + assert_equal ['MySQL', 'Oracle'], Issue.find(1).custom_field_value(1).sort + assert_equal ['MySQL', 'Oracle'], Issue.find(3).custom_field_value(1).sort + # the custom field is not associated with the issue tracker + assert_nil Issue.find(2).custom_field_value(1) + end + + def test_bulk_update_multi_custom_field_to_blank + field = CustomField.find(1) + field.update_attribute :multiple, true + + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1, 3], :notes => 'Bulk editing multi custom field', + :issue => {:priority_id => '', + :assigned_to_id => '', + :custom_field_values => {'1' => ['__none__']}} + assert_response 302 + assert_equal [''], Issue.find(1).custom_field_value(1) + assert_equal [''], Issue.find(3).custom_field_value(1) + end + + def test_bulk_update_unassign + assert_not_nil Issue.find(2).assigned_to + @request.session[:user_id] = 2 + # unassign issues + post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'} + assert_response 302 + # check that the issues were updated + assert_nil Issue.find(2).assigned_to + end + + def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject + @request.session[:user_id] = 2 + + post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4} + + assert_response :redirect + issues = Issue.find([1,2]) + issues.each do |issue| + assert_equal 4, issue.fixed_version_id + assert_not_equal issue.project_id, issue.fixed_version.project_id + end + end + + def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1,2], :back_url => '/issues' + + assert_response :redirect + assert_redirected_to '/issues' + end + + def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1,2], :back_url => 'http://google.com' + + assert_response :redirect + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier + end + + def test_bulk_update_with_failure_should_set_flash + @request.session[:user_id] = 2 + Issue.update_all("subject = ''", "id = 2") # Make it invalid + post :bulk_update, :ids => [1, 2], :issue => {:priority_id => 6} + + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook' + assert_equal 'Failed to save 1 issue(s) on 2 selected: #2.', flash[:error] + end + + def test_get_bulk_copy + @request.session[:user_id] = 2 + get :bulk_edit, :ids => [1, 2, 3], :copy => '1' + assert_response :success + assert_template 'bulk_edit' + + issues = assigns(:issues) + assert_not_nil issues + assert_equal [1, 2, 3], issues.map(&:id).sort + + assert_select 'input[name=copy_attachments]' + end + + def test_bulk_copy_to_another_project + @request.session[:user_id] = 2 + assert_difference 'Issue.count', 2 do + assert_no_difference 'Project.find(1).issues.count' do + post :bulk_update, :ids => [1, 2], :issue => {:project_id => '2'}, :copy => '1' + end + end + assert_redirected_to '/projects/ecookbook/issues' + + copies = Issue.all(:order => 'id DESC', :limit => issues.size) + copies.each do |copy| + assert_equal 2, copy.project_id + end + end + + def test_bulk_copy_should_allow_not_changing_the_issue_attributes + @request.session[:user_id] = 2 + issues = [ + Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 1, :priority_id => 2, :subject => 'issue 1', :author_id => 1, :assigned_to_id => nil), + Issue.create!(:project_id => 2, :tracker_id => 3, :status_id => 2, :priority_id => 1, :subject => 'issue 2', :author_id => 2, :assigned_to_id => 3) + ] + + assert_difference 'Issue.count', issues.size do + post :bulk_update, :ids => issues.map(&:id), :copy => '1', + :issue => { + :project_id => '', :tracker_id => '', :assigned_to_id => '', + :status_id => '', :start_date => '', :due_date => '' + } + end + + copies = Issue.all(:order => 'id DESC', :limit => issues.size) + issues.each do |orig| + copy = copies.detect {|c| c.subject == orig.subject} + assert_not_nil copy + assert_equal orig.project_id, copy.project_id + assert_equal orig.tracker_id, copy.tracker_id + assert_equal orig.status_id, copy.status_id + assert_equal orig.assigned_to_id, copy.assigned_to_id + assert_equal orig.priority_id, copy.priority_id + end + end + + def test_bulk_copy_should_allow_changing_the_issue_attributes + # Fixes random test failure with Mysql + # where Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2}) + # doesn't return the expected results + Issue.delete_all("project_id=2") + + @request.session[:user_id] = 2 + assert_difference 'Issue.count', 2 do + assert_no_difference 'Project.find(1).issues.count' do + post :bulk_update, :ids => [1, 2], :copy => '1', + :issue => { + :project_id => '2', :tracker_id => '', :assigned_to_id => '4', + :status_id => '1', :start_date => '2009-12-01', :due_date => '2009-12-31' + } + end + end + + copied_issues = Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2}) + assert_equal 2, copied_issues.size + copied_issues.each do |issue| + assert_equal 2, issue.project_id, "Project is incorrect" + assert_equal 4, issue.assigned_to_id, "Assigned to is incorrect" + assert_equal 1, issue.status_id, "Status is incorrect" + assert_equal '2009-12-01', issue.start_date.to_s, "Start date is incorrect" + assert_equal '2009-12-31', issue.due_date.to_s, "Due date is incorrect" + end + end + + def test_bulk_copy_should_allow_adding_a_note + @request.session[:user_id] = 2 + assert_difference 'Issue.count', 1 do + post :bulk_update, :ids => [1], :copy => '1', + :notes => 'Copying one issue', + :issue => { + :project_id => '', :tracker_id => '', :assigned_to_id => '4', + :status_id => '3', :start_date => '2009-12-01', :due_date => '2009-12-31' + } + end + + issue = Issue.first(:order => 'id DESC') + assert_equal 1, issue.journals.size + journal = issue.journals.first + assert_equal 0, journal.details.size + assert_equal 'Copying one issue', journal.notes + end + + def test_bulk_copy_should_allow_not_copying_the_attachments + attachment_count = Issue.find(3).attachments.size + assert attachment_count > 0 + @request.session[:user_id] = 2 + + assert_difference 'Issue.count', 1 do + assert_no_difference 'Attachment.count' do + post :bulk_update, :ids => [3], :copy => '1', + :issue => { + :project_id => '' + } + end + end + end + + def test_bulk_copy_should_allow_copying_the_attachments + attachment_count = Issue.find(3).attachments.size + assert attachment_count > 0 + @request.session[:user_id] = 2 + + assert_difference 'Issue.count', 1 do + assert_difference 'Attachment.count', attachment_count do + post :bulk_update, :ids => [3], :copy => '1', :copy_attachments => '1', + :issue => { + :project_id => '' + } + end + end + end + + def test_bulk_copy_should_add_relations_with_copied_issues + @request.session[:user_id] = 2 + + assert_difference 'Issue.count', 2 do + assert_difference 'IssueRelation.count', 2 do + post :bulk_update, :ids => [1, 3], :copy => '1', + :issue => { + :project_id => '1' + } + end + end + end + + def test_bulk_copy_should_allow_not_copying_the_subtasks + issue = Issue.generate_with_descendants! + @request.session[:user_id] = 2 + + assert_difference 'Issue.count', 1 do + post :bulk_update, :ids => [issue.id], :copy => '1', + :issue => { + :project_id => '' + } + end + end + + def test_bulk_copy_should_allow_copying_the_subtasks + issue = Issue.generate_with_descendants! + count = issue.descendants.count + @request.session[:user_id] = 2 + + assert_difference 'Issue.count', count+1 do + post :bulk_update, :ids => [issue.id], :copy => '1', :copy_subtasks => '1', + :issue => { + :project_id => '' + } + end + copy = Issue.where(:parent_id => nil).order("id DESC").first + assert_equal count, copy.descendants.count + end + + def test_bulk_copy_should_not_copy_selected_subtasks_twice + issue = Issue.generate_with_descendants! + count = issue.descendants.count + @request.session[:user_id] = 2 + + assert_difference 'Issue.count', count+1 do + post :bulk_update, :ids => issue.self_and_descendants.map(&:id), :copy => '1', :copy_subtasks => '1', + :issue => { + :project_id => '' + } + end + copy = Issue.where(:parent_id => nil).order("id DESC").first + assert_equal count, copy.descendants.count + end + + def test_bulk_copy_to_another_project_should_follow_when_needed + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1], :copy => '1', :issue => {:project_id => 2}, :follow => '1' + issue = Issue.first(:order => 'id DESC') + assert_redirected_to :controller => 'issues', :action => 'show', :id => issue + end + + def test_destroy_issue_with_no_time_entries + assert_nil TimeEntry.find_by_issue_id(2) + @request.session[:user_id] = 2 + + assert_difference 'Issue.count', -1 do + delete :destroy, :id => 2 + end + assert_redirected_to :action => 'index', :project_id => 'ecookbook' + assert_nil Issue.find_by_id(2) + end + + def test_destroy_issues_with_time_entries + @request.session[:user_id] = 2 + + assert_no_difference 'Issue.count' do + delete :destroy, :ids => [1, 3] + end + assert_response :success + assert_template 'destroy' + assert_not_nil assigns(:hours) + assert Issue.find_by_id(1) && Issue.find_by_id(3) + + assert_select 'form' do + assert_select 'input[name=_method][value=delete]' + end + end + + def test_destroy_issues_and_destroy_time_entries + @request.session[:user_id] = 2 + + assert_difference 'Issue.count', -2 do + assert_difference 'TimeEntry.count', -3 do + delete :destroy, :ids => [1, 3], :todo => 'destroy' + end + end + assert_redirected_to :action => 'index', :project_id => 'ecookbook' + assert !(Issue.find_by_id(1) || Issue.find_by_id(3)) + assert_nil TimeEntry.find_by_id([1, 2]) + end + + def test_destroy_issues_and_assign_time_entries_to_project + @request.session[:user_id] = 2 + + assert_difference 'Issue.count', -2 do + assert_no_difference 'TimeEntry.count' do + delete :destroy, :ids => [1, 3], :todo => 'nullify' + end + end + assert_redirected_to :action => 'index', :project_id => 'ecookbook' + assert !(Issue.find_by_id(1) || Issue.find_by_id(3)) + assert_nil TimeEntry.find(1).issue_id + assert_nil TimeEntry.find(2).issue_id + end + + def test_destroy_issues_and_reassign_time_entries_to_another_issue + @request.session[:user_id] = 2 + + assert_difference 'Issue.count', -2 do + assert_no_difference 'TimeEntry.count' do + delete :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2 + end + end + assert_redirected_to :action => 'index', :project_id => 'ecookbook' + assert !(Issue.find_by_id(1) || Issue.find_by_id(3)) + assert_equal 2, TimeEntry.find(1).issue_id + assert_equal 2, TimeEntry.find(2).issue_id + end + + def test_destroy_issues_from_different_projects + @request.session[:user_id] = 2 + + assert_difference 'Issue.count', -3 do + delete :destroy, :ids => [1, 2, 6], :todo => 'destroy' + end + assert_redirected_to :controller => 'issues', :action => 'index' + assert !(Issue.find_by_id(1) || Issue.find_by_id(2) || Issue.find_by_id(6)) + end + + def test_destroy_parent_and_child_issues + parent = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'Parent Issue') + child = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'Child Issue', :parent_issue_id => parent.id) + assert child.is_descendant_of?(parent.reload) + + @request.session[:user_id] = 2 + assert_difference 'Issue.count', -2 do + delete :destroy, :ids => [parent.id, child.id], :todo => 'destroy' + end + assert_response 302 + end + + def test_destroy_invalid_should_respond_with_404 + @request.session[:user_id] = 2 + assert_no_difference 'Issue.count' do + delete :destroy, :id => 999 + end + assert_response 404 + end + + def test_default_search_scope + get :index + + assert_select 'div#quick-search form' do + assert_select 'input[name=issues][value=1][type=hidden]' + end + end +end diff --git a/test/functional/issues_controller_transaction_test.rb b/test/functional/issues_controller_transaction_test.rb new file mode 100644 index 00000000..4841b2ef --- /dev/null +++ b/test/functional/issues_controller_transaction_test.rb @@ -0,0 +1,263 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) +require 'issues_controller' + +class IssuesControllerTransactionTest < ActionController::TestCase + tests IssuesController + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + self.use_transactional_fixtures = false + + def setup + User.current = nil + end + + def test_update_stale_issue_should_not_update_the_issue + issue = Issue.find(2) + @request.session[:user_id] = 2 + + assert_no_difference 'Journal.count' do + assert_no_difference 'TimeEntry.count' do + put :update, + :id => issue.id, + :issue => { + :fixed_version_id => 4, + :notes => 'My notes', + :lock_version => (issue.lock_version - 1) + }, + :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id } + end + end + + assert_response :success + assert_template 'edit' + + assert_select 'div.conflict' + assert_select 'input[name=?][value=?]', 'conflict_resolution', 'overwrite' + assert_select 'input[name=?][value=?]', 'conflict_resolution', 'add_notes' + assert_select 'label' do + assert_select 'input[name=?][value=?]', 'conflict_resolution', 'cancel' + assert_select 'a[href=/issues/2]' + end + end + + def test_update_stale_issue_should_save_attachments + set_tmp_attachments_directory + issue = Issue.find(2) + @request.session[:user_id] = 2 + + assert_no_difference 'Journal.count' do + assert_no_difference 'TimeEntry.count' do + assert_difference 'Attachment.count' do + put :update, + :id => issue.id, + :issue => { + :fixed_version_id => 4, + :notes => 'My notes', + :lock_version => (issue.lock_version - 1) + }, + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}, + :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id } + end + end + end + + assert_response :success + assert_template 'edit' + attachment = Attachment.first(:order => 'id DESC') + assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token} + assert_tag 'input', :attributes => {:name => 'attachments[p0][filename]', :value => 'testfile.txt'} + end + + def test_update_stale_issue_without_notes_should_not_show_add_notes_option + issue = Issue.find(2) + @request.session[:user_id] = 2 + + put :update, :id => issue.id, + :issue => { + :fixed_version_id => 4, + :notes => '', + :lock_version => (issue.lock_version - 1) + } + + assert_tag 'div', :attributes => {:class => 'conflict'} + assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'overwrite'} + assert_no_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'add_notes'} + assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'cancel'} + end + + def test_update_stale_issue_should_show_conflicting_journals + @request.session[:user_id] = 2 + + put :update, :id => 1, + :issue => { + :fixed_version_id => 4, + :notes => '', + :lock_version => 2 + }, + :last_journal_id => 1 + + assert_not_nil assigns(:conflict_journals) + assert_equal 1, assigns(:conflict_journals).size + assert_equal 2, assigns(:conflict_journals).first.id + assert_tag 'div', :attributes => {:class => 'conflict'}, + :descendant => {:content => /Some notes with Redmine links/} + end + + def test_update_stale_issue_without_previous_journal_should_show_all_journals + @request.session[:user_id] = 2 + + put :update, :id => 1, + :issue => { + :fixed_version_id => 4, + :notes => '', + :lock_version => 2 + }, + :last_journal_id => '' + + assert_not_nil assigns(:conflict_journals) + assert_equal 2, assigns(:conflict_journals).size + assert_tag 'div', :attributes => {:class => 'conflict'}, + :descendant => {:content => /Some notes with Redmine links/} + assert_tag 'div', :attributes => {:class => 'conflict'}, + :descendant => {:content => /Journal notes/} + end + + def test_update_stale_issue_should_show_private_journals_with_permission_only + journal = Journal.create!(:journalized => Issue.find(1), :notes => 'Privates notes', :private_notes => true, :user_id => 1) + + @request.session[:user_id] = 2 + put :update, :id => 1, :issue => {:fixed_version_id => 4, :lock_version => 2}, :last_journal_id => '' + assert_include journal, assigns(:conflict_journals) + + Role.find(1).remove_permission! :view_private_notes + put :update, :id => 1, :issue => {:fixed_version_id => 4, :lock_version => 2}, :last_journal_id => '' + assert_not_include journal, assigns(:conflict_journals) + end + + def test_update_stale_issue_with_overwrite_conflict_resolution_should_update + @request.session[:user_id] = 2 + + assert_difference 'Journal.count' do + put :update, :id => 1, + :issue => { + :fixed_version_id => 4, + :notes => 'overwrite_conflict_resolution', + :lock_version => 2 + }, + :conflict_resolution => 'overwrite' + end + + assert_response 302 + issue = Issue.find(1) + assert_equal 4, issue.fixed_version_id + journal = Journal.first(:order => 'id DESC') + assert_equal 'overwrite_conflict_resolution', journal.notes + assert journal.details.any? + end + + def test_update_stale_issue_with_add_notes_conflict_resolution_should_update + @request.session[:user_id] = 2 + + assert_difference 'Journal.count' do + put :update, :id => 1, + :issue => { + :fixed_version_id => 4, + :notes => 'add_notes_conflict_resolution', + :lock_version => 2 + }, + :conflict_resolution => 'add_notes' + end + + assert_response 302 + issue = Issue.find(1) + assert_nil issue.fixed_version_id + journal = Journal.first(:order => 'id DESC') + assert_equal 'add_notes_conflict_resolution', journal.notes + assert journal.details.empty? + end + + def test_update_stale_issue_with_cancel_conflict_resolution_should_redirect_without_updating + @request.session[:user_id] = 2 + + assert_no_difference 'Journal.count' do + put :update, :id => 1, + :issue => { + :fixed_version_id => 4, + :notes => 'add_notes_conflict_resolution', + :lock_version => 2 + }, + :conflict_resolution => 'cancel' + end + + assert_redirected_to '/issues/1' + issue = Issue.find(1) + assert_nil issue.fixed_version_id + end + + def test_put_update_with_spent_time_and_failure_should_not_add_spent_time + @request.session[:user_id] = 2 + + assert_no_difference('TimeEntry.count') do + put :update, + :id => 1, + :issue => { :subject => '' }, + :time_entry => { :hours => '2.5', :comments => 'should not be added', :activity_id => TimeEntryActivity.first.id } + assert_response :success + end + + assert_select 'input[name=?][value=?]', 'time_entry[hours]', '2.5' + assert_select 'input[name=?][value=?]', 'time_entry[comments]', 'should not be added' + assert_select 'select[name=?]', 'time_entry[activity_id]' do + assert_select 'option[value=?][selected=selected]', TimeEntryActivity.first.id + end + end + + def test_index_should_rescue_invalid_sql_query + IssueQuery.any_instance.stubs(:statement).returns("INVALID STATEMENT") + + get :index + assert_response 500 + assert_tag 'p', :content => /An error occurred/ + assert_nil session[:query] + assert_nil session[:issues_index_sort] + end +end diff --git a/test/functional/journals_controller_test.rb b/test/functional/journals_controller_test.rb new file mode 100644 index 00000000..22125709 --- /dev/null +++ b/test/functional/journals_controller_test.rb @@ -0,0 +1,147 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class JournalsControllerTest < ActionController::TestCase + fixtures :projects, :users, :members, :member_roles, :roles, :issues, :journals, :journal_details, :enabled_modules, + :trackers, :issue_statuses, :enumerations, :custom_fields, :custom_values, :custom_fields_projects + + def setup + User.current = nil + end + + def test_index + get :index, :project_id => 1 + assert_response :success + assert_not_nil assigns(:journals) + assert_equal 'application/atom+xml', @response.content_type + end + + def test_index_should_return_privates_notes_with_permission_only + journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Privates notes', :private_notes => true, :user_id => 1) + @request.session[:user_id] = 2 + + get :index, :project_id => 1 + assert_response :success + assert_include journal, assigns(:journals) + + Role.find(1).remove_permission! :view_private_notes + get :index, :project_id => 1 + assert_response :success + assert_not_include journal, assigns(:journals) + end + + def test_diff + get :diff, :id => 3, :detail_id => 4 + assert_response :success + assert_template 'diff' + + assert_tag 'span', + :attributes => {:class => 'diff_out'}, + :content => /removed/ + assert_tag 'span', + :attributes => {:class => 'diff_in'}, + :content => /added/ + end + + def test_reply_to_issue + @request.session[:user_id] = 2 + xhr :get, :new, :id => 6 + assert_response :success + assert_template 'new' + assert_equal 'text/javascript', response.content_type + assert_include '> This is an issue', response.body + end + + def test_reply_to_issue_without_permission + @request.session[:user_id] = 7 + xhr :get, :new, :id => 6 + assert_response 403 + end + + def test_reply_to_note + @request.session[:user_id] = 2 + xhr :get, :new, :id => 6, :journal_id => 4 + assert_response :success + assert_template 'new' + assert_equal 'text/javascript', response.content_type + assert_include '> A comment with a private version', response.body + end + + def test_reply_to_private_note_should_fail_without_permission + journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Privates notes', :private_notes => true) + @request.session[:user_id] = 2 + + xhr :get, :new, :id => 2, :journal_id => journal.id + assert_response :success + assert_template 'new' + assert_equal 'text/javascript', response.content_type + assert_include '> Privates notes', response.body + + Role.find(1).remove_permission! :view_private_notes + xhr :get, :new, :id => 2, :journal_id => journal.id + assert_response 404 + end + + def test_edit_xhr + @request.session[:user_id] = 1 + xhr :get, :edit, :id => 2 + assert_response :success + assert_template 'edit' + assert_equal 'text/javascript', response.content_type + assert_include 'textarea', response.body + end + + def test_edit_private_note_should_fail_without_permission + journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Privates notes', :private_notes => true) + @request.session[:user_id] = 2 + Role.find(1).add_permission! :edit_issue_notes + + xhr :get, :edit, :id => journal.id + assert_response :success + assert_template 'edit' + assert_equal 'text/javascript', response.content_type + assert_include 'textarea', response.body + + Role.find(1).remove_permission! :view_private_notes + xhr :get, :edit, :id => journal.id + assert_response 404 + end + + def test_update_xhr + @request.session[:user_id] = 1 + xhr :post, :edit, :id => 2, :notes => 'Updated notes' + assert_response :success + assert_template 'update' + assert_equal 'text/javascript', response.content_type + assert_equal 'Updated notes', Journal.find(2).notes + assert_include 'journal-2-notes', response.body + end + + def test_update_xhr_with_empty_notes_should_delete_the_journal + @request.session[:user_id] = 1 + assert_difference 'Journal.count', -1 do + xhr :post, :edit, :id => 2, :notes => '' + assert_response :success + assert_template 'update' + assert_equal 'text/javascript', response.content_type + end + assert_nil Journal.find_by_id(2) + assert_include 'change-2', response.body + end +end diff --git a/test/functional/mail_handler_controller_test.rb b/test/functional/mail_handler_controller_test.rb new file mode 100644 index 00000000..7431066f --- /dev/null +++ b/test/functional/mail_handler_controller_test.rb @@ -0,0 +1,74 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class MailHandlerControllerTest < ActionController::TestCase + fixtures :users, :projects, :enabled_modules, :roles, :members, :member_roles, :issues, :issue_statuses, + :trackers, :projects_trackers, :enumerations + + FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler' + + def setup + User.current = nil + end + + def test_should_create_issue + # Enable API and set a key + Setting.mail_handler_api_enabled = 1 + Setting.mail_handler_api_key = 'secret' + + assert_difference 'Issue.count' do + post :index, :key => 'secret', :email => IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml')) + end + assert_response 201 + end + + def test_should_respond_with_422_if_not_created + Project.find('onlinestore').destroy + + Setting.mail_handler_api_enabled = 1 + Setting.mail_handler_api_key = 'secret' + + assert_no_difference 'Issue.count' do + post :index, :key => 'secret', :email => IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml')) + end + assert_response 422 + end + + def test_should_not_allow_with_api_disabled + # Disable API + Setting.mail_handler_api_enabled = 0 + Setting.mail_handler_api_key = 'secret' + + assert_no_difference 'Issue.count' do + post :index, :key => 'secret', :email => IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml')) + end + assert_response 403 + end + + def test_should_not_allow_with_wrong_key + # Disable API + Setting.mail_handler_api_enabled = 1 + Setting.mail_handler_api_key = 'secret' + + assert_no_difference 'Issue.count' do + post :index, :key => 'wrong', :email => IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml')) + end + assert_response 403 + end +end diff --git a/test/functional/members_controller_test.rb b/test/functional/members_controller_test.rb new file mode 100644 index 00000000..993dfaed --- /dev/null +++ b/test/functional/members_controller_test.rb @@ -0,0 +1,111 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class MembersControllerTest < ActionController::TestCase + fixtures :projects, :members, :member_roles, :roles, :users + + def setup + User.current = nil + @request.session[:user_id] = 2 + end + + def test_create + assert_difference 'Member.count' do + post :create, :project_id => 1, :membership => {:role_ids => [1], :user_id => 7} + end + assert_redirected_to '/projects/ecookbook/settings/members' + assert User.find(7).member_of?(Project.find(1)) + end + + def test_create_multiple + assert_difference 'Member.count', 3 do + post :create, :project_id => 1, :membership => {:role_ids => [1], :user_ids => [7, 8, 9]} + end + assert_redirected_to '/projects/ecookbook/settings/members' + assert User.find(7).member_of?(Project.find(1)) + end + + def test_xhr_create + assert_difference 'Member.count', 3 do + xhr :post, :create, :project_id => 1, :membership => {:role_ids => [1], :user_ids => [7, 8, 9]} + assert_response :success + assert_template 'create' + assert_equal 'text/javascript', response.content_type + end + assert User.find(7).member_of?(Project.find(1)) + assert User.find(8).member_of?(Project.find(1)) + assert User.find(9).member_of?(Project.find(1)) + assert_include 'tab-content-members', response.body + end + + def test_xhr_create_with_failure + assert_no_difference 'Member.count' do + xhr :post, :create, :project_id => 1, :membership => {:role_ids => [], :user_ids => [7, 8, 9]} + assert_response :success + assert_template 'create' + assert_equal 'text/javascript', response.content_type + end + assert_match /alert/, response.body, "Alert message not sent" + end + + def test_edit + assert_no_difference 'Member.count' do + put :update, :id => 2, :membership => {:role_ids => [1], :user_id => 3} + end + assert_redirected_to '/projects/ecookbook/settings/members' + end + + def test_xhr_edit + assert_no_difference 'Member.count' do + xhr :put, :update, :id => 2, :membership => {:role_ids => [1], :user_id => 3} + assert_response :success + assert_template 'update' + assert_equal 'text/javascript', response.content_type + end + member = Member.find(2) + assert_equal [1], member.role_ids + assert_equal 3, member.user_id + assert_include 'tab-content-members', response.body + end + + def test_destroy + assert_difference 'Member.count', -1 do + delete :destroy, :id => 2 + end + assert_redirected_to '/projects/ecookbook/settings/members' + assert !User.find(3).member_of?(Project.find(1)) + end + + def test_xhr_destroy + assert_difference 'Member.count', -1 do + xhr :delete, :destroy, :id => 2 + assert_response :success + assert_template 'destroy' + assert_equal 'text/javascript', response.content_type + end + assert_nil Member.find_by_id(2) + assert_include 'tab-content-members', response.body + end + + def test_autocomplete + get :autocomplete, :project_id => 1, :q => 'mis', :format => 'js' + assert_response :success + assert_include 'User Misc', response.body + end +end diff --git a/test/functional/messages_controller_test.rb b/test/functional/messages_controller_test.rb new file mode 100644 index 00000000..8091610c --- /dev/null +++ b/test/functional/messages_controller_test.rb @@ -0,0 +1,217 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class MessagesControllerTest < ActionController::TestCase + fixtures :projects, :users, :members, :member_roles, :roles, :boards, :messages, :enabled_modules + + def setup + User.current = nil + end + + def test_show + get :show, :board_id => 1, :id => 1 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:board) + assert_not_nil assigns(:project) + assert_not_nil assigns(:topic) + end + + def test_show_should_contain_reply_field_tags_for_quoting + @request.session[:user_id] = 2 + get :show, :board_id => 1, :id => 1 + assert_response :success + + # tags required by MessagesController#quote + assert_tag 'input', :attributes => {:id => 'message_subject'} + assert_tag 'textarea', :attributes => {:id => 'message_content'} + assert_tag 'div', :attributes => {:id => 'reply'} + end + + def test_show_with_pagination + message = Message.find(1) + assert_difference 'Message.count', 30 do + 30.times do + message.children << Message.new(:subject => 'Reply', :content => 'Reply body', :author_id => 2, :board_id => 1) + end + end + get :show, :board_id => 1, :id => 1, :r => message.children.last(:order => 'id').id + assert_response :success + assert_template 'show' + replies = assigns(:replies) + assert_not_nil replies + assert !replies.include?(message.children.first(:order => 'id')) + assert replies.include?(message.children.last(:order => 'id')) + end + + def test_show_with_reply_permission + @request.session[:user_id] = 2 + get :show, :board_id => 1, :id => 1 + assert_response :success + assert_template 'show' + assert_tag :div, :attributes => { :id => 'reply' }, + :descendant => { :tag => 'textarea', :attributes => { :id => 'message_content' } } + end + + def test_show_message_not_found + get :show, :board_id => 1, :id => 99999 + assert_response 404 + end + + def test_show_message_from_invalid_board_should_respond_with_404 + get :show, :board_id => 999, :id => 1 + assert_response 404 + end + + def test_get_new + @request.session[:user_id] = 2 + get :new, :board_id => 1 + assert_response :success + assert_template 'new' + end + + def test_get_new_with_invalid_board + @request.session[:user_id] = 2 + get :new, :board_id => 99 + assert_response 404 + end + + def test_post_new + @request.session[:user_id] = 2 + ActionMailer::Base.deliveries.clear + + with_settings :notified_events => %w(message_posted) do + post :new, :board_id => 1, + :message => { :subject => 'Test created message', + :content => 'Message body'} + end + message = Message.find_by_subject('Test created message') + assert_not_nil message + assert_redirected_to "/boards/1/topics/#{message.to_param}" + assert_equal 'Message body', message.content + assert_equal 2, message.author_id + assert_equal 1, message.board_id + + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + assert_equal "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] Test created message", mail.subject + assert_mail_body_match 'Message body', mail + # author + assert mail.bcc.include?('jsmith@somenet.foo') + # project member + assert mail.bcc.include?('dlopper@somenet.foo') + end + + def test_get_edit + @request.session[:user_id] = 2 + get :edit, :board_id => 1, :id => 1 + assert_response :success + assert_template 'edit' + end + + def test_post_edit + @request.session[:user_id] = 2 + post :edit, :board_id => 1, :id => 1, + :message => { :subject => 'New subject', + :content => 'New body'} + assert_redirected_to '/boards/1/topics/1' + message = Message.find(1) + assert_equal 'New subject', message.subject + assert_equal 'New body', message.content + end + + def test_post_edit_sticky_and_locked + @request.session[:user_id] = 2 + post :edit, :board_id => 1, :id => 1, + :message => { :subject => 'New subject', + :content => 'New body', + :locked => '1', + :sticky => '1'} + assert_redirected_to '/boards/1/topics/1' + message = Message.find(1) + assert_equal true, message.sticky? + assert_equal true, message.locked? + end + + def test_post_edit_should_allow_to_change_board + @request.session[:user_id] = 2 + post :edit, :board_id => 1, :id => 1, + :message => { :subject => 'New subject', + :content => 'New body', + :board_id => 2} + assert_redirected_to '/boards/2/topics/1' + message = Message.find(1) + assert_equal Board.find(2), message.board + end + + def test_reply + @request.session[:user_id] = 2 + post :reply, :board_id => 1, :id => 1, :reply => { :content => 'This is a test reply', :subject => 'Test reply' } + reply = Message.order('id DESC').first + assert_redirected_to "/boards/1/topics/1?r=#{reply.id}" + assert Message.find_by_subject('Test reply') + end + + def test_destroy_topic + @request.session[:user_id] = 2 + assert_difference 'Message.count', -3 do + post :destroy, :board_id => 1, :id => 1 + end + assert_redirected_to '/projects/ecookbook/boards/1' + assert_nil Message.find_by_id(1) + end + + def test_destroy_reply + @request.session[:user_id] = 2 + assert_difference 'Message.count', -1 do + post :destroy, :board_id => 1, :id => 2 + end + assert_redirected_to '/boards/1/topics/1?r=2' + assert_nil Message.find_by_id(2) + end + + def test_quote + @request.session[:user_id] = 2 + xhr :get, :quote, :board_id => 1, :id => 3 + assert_response :success + assert_equal 'text/javascript', response.content_type + assert_template 'quote' + assert_include 'RE: First post', response.body + assert_include '> An other reply', response.body + end + + def test_preview_new + @request.session[:user_id] = 2 + post :preview, + :board_id => 1, + :message => {:subject => "", :content => "Previewed text"} + assert_response :success + assert_template 'common/_preview' + end + + def test_preview_edit + @request.session[:user_id] = 2 + post :preview, + :id => 4, + :board_id => 1, + :message => {:subject => "", :content => "Previewed text"} + assert_response :success + assert_template 'common/_preview' + end +end diff --git a/test/functional/my_controller_test.rb b/test/functional/my_controller_test.rb new file mode 100644 index 00000000..c15cbcc0 --- /dev/null +++ b/test/functional/my_controller_test.rb @@ -0,0 +1,248 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class MyControllerTest < ActionController::TestCase + fixtures :users, :user_preferences, :roles, :projects, :members, :member_roles, + :issues, :issue_statuses, :trackers, :enumerations, :custom_fields, :auth_sources + + def setup + @request.session[:user_id] = 2 + end + + def test_index + get :index + assert_response :success + assert_template 'page' + end + + def test_page + get :page + assert_response :success + assert_template 'page' + end + + def test_page_with_timelog_block + preferences = User.find(2).pref + preferences[:my_page_layout] = {'top' => ['timelog']} + preferences.save! + TimeEntry.create!(:user => User.find(2), :spent_on => Date.yesterday, :issue_id => 1, :hours => 2.5, :activity_id => 10) + + get :page + assert_response :success + assert_select 'tr.time-entry' do + assert_select 'td.subject a[href=/issues/1]' + assert_select 'td.hours', :text => '2.50' + end + end + + def test_page_with_all_blocks + blocks = MyController::BLOCKS.keys + preferences = User.find(2).pref + preferences[:my_page_layout] = {'top' => blocks} + preferences.save! + + get :page + assert_response :success + assert_select 'div.mypage-box', blocks.size + end + + def test_my_account_should_show_editable_custom_fields + get :account + assert_response :success + assert_template 'account' + assert_equal User.find(2), assigns(:user) + + assert_tag :input, :attributes => { :name => 'user[custom_field_values][4]'} + end + + def test_my_account_should_not_show_non_editable_custom_fields + UserCustomField.find(4).update_attribute :editable, false + + get :account + assert_response :success + assert_template 'account' + assert_equal User.find(2), assigns(:user) + + assert_no_tag :input, :attributes => { :name => 'user[custom_field_values][4]'} + end + + def test_update_account + post :account, + :user => { + :firstname => "Joe", + :login => "root", + :admin => 1, + :group_ids => ['10'], + :custom_field_values => {"4" => "0100562500"} + } + + assert_redirected_to '/my/account' + user = User.find(2) + assert_equal user, assigns(:user) + assert_equal "Joe", user.firstname + assert_equal "jsmith", user.login + assert_equal "0100562500", user.custom_value_for(4).value + # ignored + assert !user.admin? + assert user.groups.empty? + end + + def test_my_account_should_show_destroy_link + get :account + assert_select 'a[href=/my/account/destroy]' + end + + def test_get_destroy_should_display_the_destroy_confirmation + get :destroy + assert_response :success + assert_template 'destroy' + assert_select 'form[action=/my/account/destroy]' do + assert_select 'input[name=confirm]' + end + end + + def test_post_destroy_without_confirmation_should_not_destroy_account + assert_no_difference 'User.count' do + post :destroy + end + assert_response :success + assert_template 'destroy' + end + + def test_post_destroy_without_confirmation_should_destroy_account + assert_difference 'User.count', -1 do + post :destroy, :confirm => '1' + end + assert_redirected_to '/' + assert_match /deleted/i, flash[:notice] + end + + def test_post_destroy_with_unsubscribe_not_allowed_should_not_destroy_account + User.any_instance.stubs(:own_account_deletable?).returns(false) + + assert_no_difference 'User.count' do + post :destroy, :confirm => '1' + end + assert_redirected_to '/my/account' + end + + def test_change_password + get :password + assert_response :success + assert_template 'password' + + # non matching password confirmation + post :password, :password => 'jsmith', + :new_password => 'secret123', + :new_password_confirmation => 'secret1234' + assert_response :success + assert_template 'password' + assert_error_tag :content => /Password doesn't match confirmation/ + + # wrong password + post :password, :password => 'wrongpassword', + :new_password => 'secret123', + :new_password_confirmation => 'secret123' + assert_response :success + assert_template 'password' + assert_equal 'Wrong password', flash[:error] + + # good password + post :password, :password => 'jsmith', + :new_password => 'secret123', + :new_password_confirmation => 'secret123' + assert_redirected_to '/my/account' + assert User.try_to_login('jsmith', 'secret123') + end + + def test_change_password_should_redirect_if_user_cannot_change_its_password + User.find(2).update_attribute(:auth_source_id, 1) + + get :password + assert_not_nil flash[:error] + assert_redirected_to '/my/account' + end + + def test_page_layout + get :page_layout + assert_response :success + assert_template 'page_layout' + end + + def test_add_block + post :add_block, :block => 'issuesreportedbyme' + assert_redirected_to '/my/page_layout' + assert User.find(2).pref[:my_page_layout]['top'].include?('issuesreportedbyme') + end + + def test_add_invalid_block_should_redirect + post :add_block, :block => 'invalid' + assert_redirected_to '/my/page_layout' + end + + def test_remove_block + post :remove_block, :block => 'issuesassignedtome' + assert_redirected_to '/my/page_layout' + assert !User.find(2).pref[:my_page_layout].values.flatten.include?('issuesassignedtome') + end + + def test_order_blocks + xhr :post, :order_blocks, :group => 'left', 'blocks' => ['documents', 'calendar', 'latestnews'] + assert_response :success + assert_equal ['documents', 'calendar', 'latestnews'], User.find(2).pref[:my_page_layout]['left'] + end + + def test_reset_rss_key_with_existing_key + @previous_token_value = User.find(2).rss_key # Will generate one if it's missing + post :reset_rss_key + + assert_not_equal @previous_token_value, User.find(2).rss_key + assert User.find(2).rss_token + assert_match /reset/, flash[:notice] + assert_redirected_to '/my/account' + end + + def test_reset_rss_key_without_existing_key + assert_nil User.find(2).rss_token + post :reset_rss_key + + assert User.find(2).rss_token + assert_match /reset/, flash[:notice] + assert_redirected_to '/my/account' + end + + def test_reset_api_key_with_existing_key + @previous_token_value = User.find(2).api_key # Will generate one if it's missing + post :reset_api_key + + assert_not_equal @previous_token_value, User.find(2).api_key + assert User.find(2).api_token + assert_match /reset/, flash[:notice] + assert_redirected_to '/my/account' + end + + def test_reset_api_key_without_existing_key + assert_nil User.find(2).api_token + post :reset_api_key + + assert User.find(2).api_token + assert_match /reset/, flash[:notice] + assert_redirected_to '/my/account' + end +end diff --git a/test/functional/news_controller_test.rb b/test/functional/news_controller_test.rb new file mode 100644 index 00000000..ed7a9c10 --- /dev/null +++ b/test/functional/news_controller_test.rb @@ -0,0 +1,165 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class NewsControllerTest < ActionController::TestCase + fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :news, :comments + + def setup + User.current = nil + end + + def test_index + get :index + assert_response :success + assert_template 'index' + assert_not_nil assigns(:newss) + assert_nil assigns(:project) + end + + def test_index_with_project + get :index, :project_id => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:newss) + end + + def test_index_with_invalid_project_should_respond_with_404 + get :index, :project_id => 999 + assert_response 404 + end + + def test_show + get :show, :id => 1 + assert_response :success + assert_template 'show' + assert_tag :tag => 'h2', :content => /eCookbook first release/ + end + + def test_show_should_show_attachments + attachment = Attachment.first + attachment.container = News.find(1) + attachment.save! + + get :show, :id => 1 + assert_response :success + assert_tag 'a', :content => attachment.filename + end + + def test_show_not_found + get :show, :id => 999 + assert_response 404 + end + + def test_get_new + @request.session[:user_id] = 2 + get :new, :project_id => 1 + assert_response :success + assert_template 'new' + end + + def test_post_create + ActionMailer::Base.deliveries.clear + @request.session[:user_id] = 2 + + with_settings :notified_events => %w(news_added) do + post :create, :project_id => 1, :news => { :title => 'NewsControllerTest', + :description => 'This is the description', + :summary => '' } + end + assert_redirected_to '/projects/ecookbook/news' + + news = News.find_by_title('NewsControllerTest') + assert_not_nil news + assert_equal 'This is the description', news.description + assert_equal User.find(2), news.author + assert_equal Project.find(1), news.project + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_post_create_with_attachment + set_tmp_attachments_directory + @request.session[:user_id] = 2 + assert_difference 'News.count' do + assert_difference 'Attachment.count' do + post :create, :project_id => 1, + :news => { :title => 'Test', :description => 'This is the description' }, + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}} + end + end + attachment = Attachment.first(:order => 'id DESC') + news = News.first(:order => 'id DESC') + assert_equal news, attachment.container + end + + def test_post_create_with_validation_failure + @request.session[:user_id] = 2 + post :create, :project_id => 1, :news => { :title => '', + :description => 'This is the description', + :summary => '' } + assert_response :success + assert_template 'new' + assert_not_nil assigns(:news) + assert assigns(:news).new_record? + assert_error_tag :content => /title can't be blank/i + end + + def test_get_edit + @request.session[:user_id] = 2 + get :edit, :id => 1 + assert_response :success + assert_template 'edit' + end + + def test_put_update + @request.session[:user_id] = 2 + put :update, :id => 1, :news => { :description => 'Description changed by test_post_edit' } + assert_redirected_to '/news/1' + news = News.find(1) + assert_equal 'Description changed by test_post_edit', news.description + end + + def test_put_update_with_attachment + set_tmp_attachments_directory + @request.session[:user_id] = 2 + assert_no_difference 'News.count' do + assert_difference 'Attachment.count' do + put :update, :id => 1, + :news => { :description => 'This is the description' }, + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}} + end + end + attachment = Attachment.first(:order => 'id DESC') + assert_equal News.find(1), attachment.container + end + + def test_update_with_failure + @request.session[:user_id] = 2 + put :update, :id => 1, :news => { :description => '' } + assert_response :success + assert_template 'edit' + assert_error_tag :content => /description can't be blank/i + end + + def test_destroy + @request.session[:user_id] = 2 + delete :destroy, :id => 1 + assert_redirected_to '/projects/ecookbook/news' + assert_nil News.find_by_id(1) + end +end diff --git a/test/functional/previews_controller_test.rb b/test/functional/previews_controller_test.rb new file mode 100644 index 00000000..80d7f2f8 --- /dev/null +++ b/test/functional/previews_controller_test.rb @@ -0,0 +1,81 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class PreviewsControllerTest < ActionController::TestCase + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :journals, :journal_details, + :news + + def test_preview_new_issue + @request.session[:user_id] = 2 + post :issue, :project_id => '1', :issue => {:description => 'Foo'} + assert_response :success + assert_template 'preview' + assert_not_nil assigns(:description) + end + + def test_preview_issue_notes + @request.session[:user_id] = 2 + post :issue, :project_id => '1', :id => 1, + :issue => {:description => Issue.find(1).description, :notes => 'Foo'} + assert_response :success + assert_template 'preview' + assert_not_nil assigns(:notes) + end + + def test_preview_journal_notes_for_update + @request.session[:user_id] = 2 + post :issue, :project_id => '1', :id => 1, :notes => 'Foo' + assert_response :success + assert_template 'preview' + assert_not_nil assigns(:notes) + assert_tag :p, :content => 'Foo' + end + + def test_preview_new_news + get :news, :project_id => 1, + :news => {:title => '', + :description => 'News description', + :summary => ''} + assert_response :success + assert_template 'common/_preview' + assert_tag :tag => 'fieldset', :attributes => { :class => 'preview' }, + :content => /News description/ + end + + def test_existing_new_news + get :news, :project_id => 1, :id => 2, + :news => {:title => '', + :description => 'News description', + :summary => ''} + assert_response :success + assert_template 'common/_preview' + assert_equal News.find(2), assigns(:previewed) + assert_not_nil assigns(:attachments) + + assert_tag :tag => 'fieldset', :attributes => { :class => 'preview' }, + :content => /News description/ + end +end diff --git a/test/functional/project_enumerations_controller_test.rb b/test/functional/project_enumerations_controller_test.rb new file mode 100644 index 00000000..e00abd47 --- /dev/null +++ b/test/functional/project_enumerations_controller_test.rb @@ -0,0 +1,217 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class ProjectEnumerationsControllerTest < ActionController::TestCase + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :custom_fields, :custom_fields_projects, + :custom_fields_trackers, :custom_values, + :time_entries + + self.use_transactional_fixtures = false + + def setup + @request.session[:user_id] = nil + Setting.default_language = 'en' + end + + def test_update_to_override_system_activities + @request.session[:user_id] = 2 # manager + billable_field = TimeEntryActivityCustomField.find_by_name("Billable") + + put :update, :project_id => 1, :enumerations => { + "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design, De-activate + "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"}, # Development, Change custom value + "14"=>{"parent_id"=>"14", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"}, # Inactive Activity, Activate with custom value + "11"=>{"parent_id"=>"11", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"} # QA, no changes + } + + assert_response :redirect + assert_redirected_to '/projects/ecookbook/settings/activities' + + # Created project specific activities... + project = Project.find('ecookbook') + + # ... Design + design = project.time_entry_activities.find_by_name("Design") + assert design, "Project activity not found" + + assert_equal 9, design.parent_id # Relate to the system activity + assert_not_equal design.parent.id, design.id # Different records + assert_equal design.parent.name, design.name # Same name + assert !design.active? + + # ... Development + development = project.time_entry_activities.find_by_name("Development") + assert development, "Project activity not found" + + assert_equal 10, development.parent_id # Relate to the system activity + assert_not_equal development.parent.id, development.id # Different records + assert_equal development.parent.name, development.name # Same name + assert development.active? + assert_equal "0", development.custom_value_for(billable_field).value + + # ... Inactive Activity + previously_inactive = project.time_entry_activities.find_by_name("Inactive Activity") + assert previously_inactive, "Project activity not found" + + assert_equal 14, previously_inactive.parent_id # Relate to the system activity + assert_not_equal previously_inactive.parent.id, previously_inactive.id # Different records + assert_equal previously_inactive.parent.name, previously_inactive.name # Same name + assert previously_inactive.active? + assert_equal "1", previously_inactive.custom_value_for(billable_field).value + + # ... QA + assert_equal nil, project.time_entry_activities.find_by_name("QA"), "Custom QA activity created when it wasn't modified" + end + + def test_update_will_update_project_specific_activities + @request.session[:user_id] = 2 # manager + + project_activity = TimeEntryActivity.new({ + :name => 'Project Specific', + :parent => TimeEntryActivity.first, + :project => Project.find(1), + :active => true + }) + assert project_activity.save + project_activity_two = TimeEntryActivity.new({ + :name => 'Project Specific Two', + :parent => TimeEntryActivity.last, + :project => Project.find(1), + :active => true + }) + assert project_activity_two.save + + + put :update, :project_id => 1, :enumerations => { + project_activity.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # De-activate + project_activity_two.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"} # De-activate + } + + assert_response :redirect + assert_redirected_to '/projects/ecookbook/settings/activities' + + # Created project specific activities... + project = Project.find('ecookbook') + assert_equal 2, project.time_entry_activities.count + + activity_one = project.time_entry_activities.find_by_name(project_activity.name) + assert activity_one, "Project activity not found" + assert_equal project_activity.id, activity_one.id + assert !activity_one.active? + + activity_two = project.time_entry_activities.find_by_name(project_activity_two.name) + assert activity_two, "Project activity not found" + assert_equal project_activity_two.id, activity_two.id + assert !activity_two.active? + end + + def test_update_when_creating_new_activities_will_convert_existing_data + assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size + + @request.session[:user_id] = 2 # manager + put :update, :project_id => 1, :enumerations => { + "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"} # Design, De-activate + } + assert_response :redirect + + # No more TimeEntries using the system activity + assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries still assigned to system activities" + # All TimeEntries using project activity + project_specific_activity = TimeEntryActivity.find_by_parent_id_and_project_id(9, 1) + assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(project_specific_activity.id, 1).size, "No Time Entries assigned to the project activity" + end + + def test_update_when_creating_new_activities_will_not_convert_existing_data_if_an_exception_is_raised + # TODO: Need to cause an exception on create but these tests + # aren't setup for mocking. Just create a record now so the + # second one is a dupicate + parent = TimeEntryActivity.find(9) + TimeEntryActivity.create!({:name => parent.name, :project_id => 1, :position => parent.position, :active => true}) + TimeEntry.create!({:project_id => 1, :hours => 1.0, :user => User.find(1), :issue_id => 3, :activity_id => 10, :spent_on => '2009-01-01'}) + + assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size + assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size + + @request.session[:user_id] = 2 # manager + put :update, :project_id => 1, :enumerations => { + "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design + "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"} # Development, Change custom value + } + assert_response :redirect + + # TimeEntries shouldn't have been reassigned on the failed record + assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries are not assigned to system activities" + # TimeEntries shouldn't have been reassigned on the saved record either + assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size, "Time Entries are not assigned to system activities" + end + + def test_destroy + @request.session[:user_id] = 2 # manager + project_activity = TimeEntryActivity.new({ + :name => 'Project Specific', + :parent => TimeEntryActivity.first, + :project => Project.find(1), + :active => true + }) + assert project_activity.save + project_activity_two = TimeEntryActivity.new({ + :name => 'Project Specific Two', + :parent => TimeEntryActivity.last, + :project => Project.find(1), + :active => true + }) + assert project_activity_two.save + + delete :destroy, :project_id => 1 + assert_response :redirect + assert_redirected_to '/projects/ecookbook/settings/activities' + + assert_nil TimeEntryActivity.find_by_id(project_activity.id) + assert_nil TimeEntryActivity.find_by_id(project_activity_two.id) + end + + def test_destroy_should_reassign_time_entries_back_to_the_system_activity + @request.session[:user_id] = 2 # manager + project_activity = TimeEntryActivity.new({ + :name => 'Project Specific Design', + :parent => TimeEntryActivity.find(9), + :project => Project.find(1), + :active => true + }) + assert project_activity.save + assert TimeEntry.update_all("activity_id = '#{project_activity.id}'", ["project_id = ? AND activity_id = ?", 1, 9]) + assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size + + delete :destroy, :project_id => 1 + assert_response :redirect + assert_redirected_to '/projects/ecookbook/settings/activities' + + assert_nil TimeEntryActivity.find_by_id(project_activity.id) + assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size, "TimeEntries still assigned to project specific activity" + assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "TimeEntries still assigned to project specific activity" + end + +end diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb new file mode 100644 index 00000000..848b3fa2 --- /dev/null +++ b/test/functional/projects_controller_test.rb @@ -0,0 +1,592 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class ProjectsControllerTest < ActionController::TestCase + fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details, + :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages, + :attachments, :custom_fields, :custom_values, :time_entries + + def setup + @request.session[:user_id] = nil + Setting.default_language = 'en' + end + + def test_index_by_anonymous_should_not_show_private_projects + get :index + assert_response :success + assert_template 'index' + projects = assigns(:projects) + assert_not_nil projects + assert projects.all?(&:is_public?) + + assert_select 'ul' do + assert_select 'li' do + assert_select 'a', :text => 'eCookbook' + assert_select 'ul' do + assert_select 'a', :text => 'Child of private child' + end + end + end + assert_select 'a', :text => /Private child of eCookbook/, :count => 0 + end + + def test_index_atom + get :index, :format => 'atom' + assert_response :success + assert_template 'common/feed' + assert_select 'feed>title', :text => 'Redmine: Latest projects' + assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_condition(User.current)) + end + + test "#index by non-admin user with view_time_entries permission should show overall spent time link" do + @request.session[:user_id] = 3 + get :index + assert_template 'index' + assert_select 'a[href=?]', '/time_entries' + end + + test "#index by non-admin user without view_time_entries permission should not show overall spent time link" do + Role.find(2).remove_permission! :view_time_entries + Role.non_member.remove_permission! :view_time_entries + Role.anonymous.remove_permission! :view_time_entries + @request.session[:user_id] = 3 + + get :index + assert_template 'index' + assert_select 'a[href=?]', '/time_entries', 0 + end + + test "#new by admin user should accept get" do + @request.session[:user_id] = 1 + + get :new + assert_response :success + assert_template 'new' + end + + test "#new by non-admin user with add_project permission should accept get" do + Role.non_member.add_permission! :add_project + @request.session[:user_id] = 9 + + get :new + assert_response :success + assert_template 'new' + assert_select 'select[name=?]', 'project[parent_id]', 0 + end + + test "#new by non-admin user with add_subprojects permission should accept get" do + Role.find(1).remove_permission! :add_project + Role.find(1).add_permission! :add_subprojects + @request.session[:user_id] = 2 + + get :new, :parent_id => 'ecookbook' + assert_response :success + assert_template 'new' + + assert_select 'select[name=?]', 'project[parent_id]' do + # parent project selected + assert_select 'option[value=1][selected=selected]' + # no empty value + assert_select 'option[value=]', 0 + end + end + + test "#create by admin user should create a new project" do + @request.session[:user_id] = 1 + + post :create, + :project => { + :name => "blog", + :description => "weblog", + :homepage => 'http://weblog', + :identifier => "blog", + :is_public => 1, + :custom_field_values => { '3' => 'Beta' }, + :tracker_ids => ['1', '3'], + # an issue custom field that is not for all project + :issue_custom_field_ids => ['9'], + :enabled_module_names => ['issue_tracking', 'news', 'repository'] + } + assert_redirected_to '/projects/blog/settings' + + project = Project.find_by_name('blog') + assert_kind_of Project, project + assert project.active? + assert_equal 'weblog', project.description + assert_equal 'http://weblog', project.homepage + assert_equal true, project.is_public? + assert_nil project.parent + assert_equal 'Beta', project.custom_value_for(3).value + assert_equal [1, 3], project.trackers.map(&:id).sort + assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort + assert project.issue_custom_fields.include?(IssueCustomField.find(9)) + end + + test "#create by admin user should create a new subproject" do + @request.session[:user_id] = 1 + + assert_difference 'Project.count' do + post :create, :project => { :name => "blog", + :description => "weblog", + :identifier => "blog", + :is_public => 1, + :custom_field_values => { '3' => 'Beta' }, + :parent_id => 1 + } + assert_redirected_to '/projects/blog/settings' + end + + project = Project.find_by_name('blog') + assert_kind_of Project, project + assert_equal Project.find(1), project.parent + end + + test "#create by admin user should continue" do + @request.session[:user_id] = 1 + + assert_difference 'Project.count' do + post :create, :project => {:name => "blog", :identifier => "blog"}, :continue => 'Create and continue' + end + assert_redirected_to '/projects/new' + end + + test "#create by non-admin user with add_project permission should create a new project" do + Role.non_member.add_permission! :add_project + @request.session[:user_id] = 9 + + post :create, :project => { :name => "blog", + :description => "weblog", + :identifier => "blog", + :is_public => 1, + :custom_field_values => { '3' => 'Beta' }, + :tracker_ids => ['1', '3'], + :enabled_module_names => ['issue_tracking', 'news', 'repository'] + } + + assert_redirected_to '/projects/blog/settings' + + project = Project.find_by_name('blog') + assert_kind_of Project, project + assert_equal 'weblog', project.description + assert_equal true, project.is_public? + assert_equal [1, 3], project.trackers.map(&:id).sort + assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort + + # User should be added as a project member + assert User.find(9).member_of?(project) + assert_equal 1, project.members.size + end + + test "#create by non-admin user with add_project permission should fail with parent_id" do + Role.non_member.add_permission! :add_project + @request.session[:user_id] = 9 + + assert_no_difference 'Project.count' do + post :create, :project => { :name => "blog", + :description => "weblog", + :identifier => "blog", + :is_public => 1, + :custom_field_values => { '3' => 'Beta' }, + :parent_id => 1 + } + end + assert_response :success + project = assigns(:project) + assert_kind_of Project, project + assert_not_nil project.errors[:parent_id] + end + + test "#create by non-admin user with add_subprojects permission should create a project with a parent_id" do + Role.find(1).remove_permission! :add_project + Role.find(1).add_permission! :add_subprojects + @request.session[:user_id] = 2 + + post :create, :project => { :name => "blog", + :description => "weblog", + :identifier => "blog", + :is_public => 1, + :custom_field_values => { '3' => 'Beta' }, + :parent_id => 1 + } + assert_redirected_to '/projects/blog/settings' + project = Project.find_by_name('blog') + end + + test "#create by non-admin user with add_subprojects permission should fail without parent_id" do + Role.find(1).remove_permission! :add_project + Role.find(1).add_permission! :add_subprojects + @request.session[:user_id] = 2 + + assert_no_difference 'Project.count' do + post :create, :project => { :name => "blog", + :description => "weblog", + :identifier => "blog", + :is_public => 1, + :custom_field_values => { '3' => 'Beta' } + } + end + assert_response :success + project = assigns(:project) + assert_kind_of Project, project + assert_not_nil project.errors[:parent_id] + end + + test "#create by non-admin user with add_subprojects permission should fail with unauthorized parent_id" do + Role.find(1).remove_permission! :add_project + Role.find(1).add_permission! :add_subprojects + @request.session[:user_id] = 2 + + assert !User.find(2).member_of?(Project.find(6)) + assert_no_difference 'Project.count' do + post :create, :project => { :name => "blog", + :description => "weblog", + :identifier => "blog", + :is_public => 1, + :custom_field_values => { '3' => 'Beta' }, + :parent_id => 6 + } + end + assert_response :success + project = assigns(:project) + assert_kind_of Project, project + assert_not_nil project.errors[:parent_id] + end + + def test_create_subproject_with_inherit_members_should_inherit_members + Role.find_by_name('Manager').add_permission! :add_subprojects + parent = Project.find(1) + @request.session[:user_id] = 2 + + assert_difference 'Project.count' do + post :create, :project => { + :name => 'inherited', :identifier => 'inherited', :parent_id => parent.id, :inherit_members => '1' + } + assert_response 302 + end + + project = Project.order('id desc').first + assert_equal 'inherited', project.name + assert_equal parent, project.parent + assert project.memberships.count > 0 + assert_equal parent.memberships.count, project.memberships.count + end + + def test_create_should_preserve_modules_on_validation_failure + with_settings :default_projects_modules => ['issue_tracking', 'repository'] do + @request.session[:user_id] = 1 + assert_no_difference 'Project.count' do + post :create, :project => { + :name => "blog", + :identifier => "", + :enabled_module_names => %w(issue_tracking news) + } + end + assert_response :success + project = assigns(:project) + assert_equal %w(issue_tracking news), project.enabled_module_names.sort + end + end + + def test_show_by_id + get :show, :id => 1 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:project) + end + + def test_show_by_identifier + get :show, :id => 'ecookbook' + assert_response :success + assert_template 'show' + assert_not_nil assigns(:project) + assert_equal Project.find_by_identifier('ecookbook'), assigns(:project) + + assert_select 'li', :text => /Development status/ + end + + def test_show_should_not_display_hidden_custom_fields + ProjectCustomField.find_by_name('Development status').update_attribute :visible, false + get :show, :id => 'ecookbook' + assert_response :success + assert_template 'show' + assert_not_nil assigns(:project) + + assert_select 'li', :text => /Development status/, :count => 0 + end + + def test_show_should_not_fail_when_custom_values_are_nil + project = Project.find_by_identifier('ecookbook') + project.custom_values.first.update_attribute(:value, nil) + get :show, :id => 'ecookbook' + assert_response :success + assert_template 'show' + assert_not_nil assigns(:project) + assert_equal Project.find_by_identifier('ecookbook'), assigns(:project) + end + + def show_archived_project_should_be_denied + project = Project.find_by_identifier('ecookbook') + project.archive! + + get :show, :id => 'ecookbook' + assert_response 403 + assert_nil assigns(:project) + assert_select 'p', :text => /archived/ + end + + def test_show_should_not_show_private_subprojects_that_are_not_visible + get :show, :id => 'ecookbook' + assert_response :success + assert_template 'show' + assert_select 'a', :text => /Private child/, :count => 0 + end + + def test_show_should_show_private_subprojects_that_are_visible + @request.session[:user_id] = 2 # manager who is a member of the private subproject + get :show, :id => 'ecookbook' + assert_response :success + assert_template 'show' + assert_select 'a', :text => /Private child/ + end + + def test_settings + @request.session[:user_id] = 2 # manager + get :settings, :id => 1 + assert_response :success + assert_template 'settings' + end + + def test_settings_of_subproject + @request.session[:user_id] = 2 + get :settings, :id => 'private-child' + assert_response :success + assert_template 'settings' + + assert_select 'input[type=checkbox][name=?]', 'project[inherit_members]' + end + + def test_settings_should_be_denied_for_member_on_closed_project + Project.find(1).close + @request.session[:user_id] = 2 # manager + + get :settings, :id => 1 + assert_response 403 + end + + def test_settings_should_be_denied_for_anonymous_on_closed_project + Project.find(1).close + + get :settings, :id => 1 + assert_response 302 + end + + def test_update + @request.session[:user_id] = 2 # manager + post :update, :id => 1, :project => {:name => 'Test changed name', + :issue_custom_field_ids => ['']} + assert_redirected_to '/projects/ecookbook/settings' + project = Project.find(1) + assert_equal 'Test changed name', project.name + end + + def test_update_with_failure + @request.session[:user_id] = 2 # manager + post :update, :id => 1, :project => {:name => ''} + assert_response :success + assert_template 'settings' + assert_error_tag :content => /name can't be blank/i + end + + def test_update_should_be_denied_for_member_on_closed_project + Project.find(1).close + @request.session[:user_id] = 2 # manager + + post :update, :id => 1, :project => {:name => 'Closed'} + assert_response 403 + assert_equal 'eCookbook', Project.find(1).name + end + + def test_update_should_be_denied_for_anonymous_on_closed_project + Project.find(1).close + + post :update, :id => 1, :project => {:name => 'Closed'} + assert_response 302 + assert_equal 'eCookbook', Project.find(1).name + end + + def test_modules + @request.session[:user_id] = 2 + Project.find(1).enabled_module_names = ['issue_tracking', 'news'] + + post :modules, :id => 1, :enabled_module_names => ['issue_tracking', 'repository', 'documents'] + assert_redirected_to '/projects/ecookbook/settings/modules' + assert_equal ['documents', 'issue_tracking', 'repository'], Project.find(1).enabled_module_names.sort + end + + def test_destroy_leaf_project_without_confirmation_should_show_confirmation + @request.session[:user_id] = 1 # admin + + assert_no_difference 'Project.count' do + delete :destroy, :id => 2 + assert_response :success + assert_template 'destroy' + end + end + + def test_destroy_without_confirmation_should_show_confirmation_with_subprojects + @request.session[:user_id] = 1 # admin + + assert_no_difference 'Project.count' do + delete :destroy, :id => 1 + assert_response :success + assert_template 'destroy' + end + assert_select 'strong', + :text => ['Private child of eCookbook', + 'Child of private child, eCookbook Subproject 1', + 'eCookbook Subproject 2'].join(', ') + end + + def test_destroy_with_confirmation_should_destroy_the_project_and_subprojects + @request.session[:user_id] = 1 # admin + + assert_difference 'Project.count', -5 do + delete :destroy, :id => 1, :confirm => 1 + assert_redirected_to '/admin/projects' + end + assert_nil Project.find_by_id(1) + end + + def test_archive + @request.session[:user_id] = 1 # admin + post :archive, :id => 1 + assert_redirected_to '/admin/projects' + assert !Project.find(1).active? + end + + def test_archive_with_failure + @request.session[:user_id] = 1 + Project.any_instance.stubs(:archive).returns(false) + post :archive, :id => 1 + assert_redirected_to '/admin/projects' + assert_match /project cannot be archived/i, flash[:error] + end + + def test_unarchive + @request.session[:user_id] = 1 # admin + Project.find(1).archive + post :unarchive, :id => 1 + assert_redirected_to '/admin/projects' + assert Project.find(1).active? + end + + def test_close + @request.session[:user_id] = 2 + post :close, :id => 1 + assert_redirected_to '/projects/ecookbook' + assert_equal Project::STATUS_CLOSED, Project.find(1).status + end + + def test_reopen + Project.find(1).close + @request.session[:user_id] = 2 + post :reopen, :id => 1 + assert_redirected_to '/projects/ecookbook' + assert Project.find(1).active? + end + + def test_project_breadcrumbs_should_be_limited_to_3_ancestors + CustomField.delete_all + parent = nil + 6.times do |i| + p = Project.generate_with_parent!(parent) + get :show, :id => p + assert_select '#header h1' do + assert_select 'a', :count => [i, 3].min + end + + parent = p + end + end + + def test_get_copy + @request.session[:user_id] = 1 # admin + get :copy, :id => 1 + assert_response :success + assert_template 'copy' + assert assigns(:project) + assert_equal Project.find(1).description, assigns(:project).description + assert_nil assigns(:project).id + + assert_select 'input[name=?][value=?]', 'project[enabled_module_names][]', 'issue_tracking', 1 + end + + def test_get_copy_with_invalid_source_should_respond_with_404 + @request.session[:user_id] = 1 + get :copy, :id => 99 + assert_response 404 + end + + def test_post_copy_should_copy_requested_items + @request.session[:user_id] = 1 # admin + CustomField.delete_all + + assert_difference 'Project.count' do + post :copy, :id => 1, + :project => { + :name => 'Copy', + :identifier => 'unique-copy', + :tracker_ids => ['1', '2', '3', ''], + :enabled_module_names => %w(issue_tracking time_tracking) + }, + :only => %w(issues versions) + end + project = Project.find('unique-copy') + source = Project.find(1) + assert_equal %w(issue_tracking time_tracking), project.enabled_module_names.sort + + assert_equal source.versions.count, project.versions.count, "All versions were not copied" + assert_equal source.issues.count, project.issues.count, "All issues were not copied" + assert_equal 0, project.members.count + end + + def test_post_copy_should_redirect_to_settings_when_successful + @request.session[:user_id] = 1 # admin + post :copy, :id => 1, :project => {:name => 'Copy', :identifier => 'unique-copy'} + assert_response :redirect + assert_redirected_to :controller => 'projects', :action => 'settings', :id => 'unique-copy' + end + + def test_jump_should_redirect_to_active_tab + get :show, :id => 1, :jump => 'issues' + assert_redirected_to '/projects/ecookbook/issues' + end + + def test_jump_should_not_redirect_to_inactive_tab + get :show, :id => 3, :jump => 'documents' + assert_response :success + assert_template 'show' + end + + def test_jump_should_not_redirect_to_unknown_tab + get :show, :id => 3, :jump => 'foobar' + assert_response :success + assert_template 'show' + end +end diff --git a/test/functional/queries_controller_test.rb b/test/functional/queries_controller_test.rb new file mode 100644 index 00000000..076f0b39 --- /dev/null +++ b/test/functional/queries_controller_test.rb @@ -0,0 +1,290 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class QueriesControllerTest < ActionController::TestCase + fixtures :projects, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries, :enabled_modules + + def setup + User.current = nil + end + + def test_index + get :index + # HTML response not implemented + assert_response 406 + end + + def test_new_project_query + @request.session[:user_id] = 2 + get :new, :project_id => 1 + assert_response :success + assert_template 'new' + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'query[is_public]', + :checked => nil } + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'query_is_for_all', + :checked => nil, + :disabled => nil } + assert_select 'select[name=?]', 'c[]' do + assert_select 'option[value=tracker]' + assert_select 'option[value=subject]' + end + end + + def test_new_global_query + @request.session[:user_id] = 2 + get :new + assert_response :success + assert_template 'new' + assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'query[is_public]' } + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'query_is_for_all', + :checked => 'checked', + :disabled => nil } + end + + def test_new_on_invalid_project + @request.session[:user_id] = 2 + get :new, :project_id => 'invalid' + assert_response 404 + end + + def test_create_project_public_query + @request.session[:user_id] = 2 + post :create, + :project_id => 'ecookbook', + :default_columns => '1', + :f => ["status_id", "assigned_to_id"], + :op => {"assigned_to_id" => "=", "status_id" => "o"}, + :v => { "assigned_to_id" => ["1"], "status_id" => ["1"]}, + :query => {"name" => "test_new_project_public_query", "is_public" => "1"} + + q = Query.find_by_name('test_new_project_public_query') + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q + assert q.is_public? + assert q.has_default_columns? + assert q.valid? + end + + def test_create_project_private_query + @request.session[:user_id] = 3 + post :create, + :project_id => 'ecookbook', + :default_columns => '1', + :fields => ["status_id", "assigned_to_id"], + :operators => {"assigned_to_id" => "=", "status_id" => "o"}, + :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]}, + :query => {"name" => "test_new_project_private_query", "is_public" => "1"} + + q = Query.find_by_name('test_new_project_private_query') + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q + assert !q.is_public? + assert q.has_default_columns? + assert q.valid? + end + + def test_create_global_private_query_with_custom_columns + @request.session[:user_id] = 3 + post :create, + :fields => ["status_id", "assigned_to_id"], + :operators => {"assigned_to_id" => "=", "status_id" => "o"}, + :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]}, + :query => {"name" => "test_new_global_private_query", "is_public" => "1"}, + :c => ["", "tracker", "subject", "priority", "category"] + + q = Query.find_by_name('test_new_global_private_query') + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q + assert !q.is_public? + assert !q.has_default_columns? + assert_equal [:id, :tracker, :subject, :priority, :category], q.columns.collect {|c| c.name} + assert q.valid? + end + + def test_create_global_query_with_custom_filters + @request.session[:user_id] = 3 + post :create, + :fields => ["assigned_to_id"], + :operators => {"assigned_to_id" => "="}, + :values => { "assigned_to_id" => ["me"]}, + :query => {"name" => "test_new_global_query"} + + q = Query.find_by_name('test_new_global_query') + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q + assert !q.has_filter?(:status_id) + assert_equal ['assigned_to_id'], q.filters.keys + assert q.valid? + end + + def test_create_with_sort + @request.session[:user_id] = 1 + post :create, + :default_columns => '1', + :operators => {"status_id" => "o"}, + :values => {"status_id" => ["1"]}, + :query => {:name => "test_new_with_sort", + :is_public => "1", + :sort_criteria => {"0" => ["due_date", "desc"], "1" => ["tracker", ""]}} + + query = Query.find_by_name("test_new_with_sort") + assert_not_nil query + assert_equal [['due_date', 'desc'], ['tracker', 'asc']], query.sort_criteria + end + + def test_create_with_failure + @request.session[:user_id] = 2 + assert_no_difference '::Query.count' do + post :create, :project_id => 'ecookbook', :query => {:name => ''} + end + assert_response :success + assert_template 'new' + assert_select 'input[name=?]', 'query[name]' + end + + def test_edit_global_public_query + @request.session[:user_id] = 1 + get :edit, :id => 4 + assert_response :success + assert_template 'edit' + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'query[is_public]', + :checked => 'checked' } + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'query_is_for_all', + :checked => 'checked', + :disabled => 'disabled' } + end + + def test_edit_global_private_query + @request.session[:user_id] = 3 + get :edit, :id => 3 + assert_response :success + assert_template 'edit' + assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'query[is_public]' } + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'query_is_for_all', + :checked => 'checked', + :disabled => 'disabled' } + end + + def test_edit_project_private_query + @request.session[:user_id] = 3 + get :edit, :id => 2 + assert_response :success + assert_template 'edit' + assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'query[is_public]' } + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'query_is_for_all', + :checked => nil, + :disabled => nil } + end + + def test_edit_project_public_query + @request.session[:user_id] = 2 + get :edit, :id => 1 + assert_response :success + assert_template 'edit' + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'query[is_public]', + :checked => 'checked' + } + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'query_is_for_all', + :checked => nil, + :disabled => 'disabled' } + end + + def test_edit_sort_criteria + @request.session[:user_id] = 1 + get :edit, :id => 5 + assert_response :success + assert_template 'edit' + assert_tag :tag => 'select', :attributes => { :name => 'query[sort_criteria][0][]' }, + :child => { :tag => 'option', :attributes => { :value => 'priority', + :selected => 'selected' } } + assert_tag :tag => 'select', :attributes => { :name => 'query[sort_criteria][0][]' }, + :child => { :tag => 'option', :attributes => { :value => 'desc', + :selected => 'selected' } } + end + + def test_edit_invalid_query + @request.session[:user_id] = 2 + get :edit, :id => 99 + assert_response 404 + end + + def test_udpate_global_private_query + @request.session[:user_id] = 3 + put :update, + :id => 3, + :default_columns => '1', + :fields => ["status_id", "assigned_to_id"], + :operators => {"assigned_to_id" => "=", "status_id" => "o"}, + :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]}, + :query => {"name" => "test_edit_global_private_query", "is_public" => "1"} + + assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 3 + q = Query.find_by_name('test_edit_global_private_query') + assert !q.is_public? + assert q.has_default_columns? + assert q.valid? + end + + def test_update_global_public_query + @request.session[:user_id] = 1 + put :update, + :id => 4, + :default_columns => '1', + :fields => ["status_id", "assigned_to_id"], + :operators => {"assigned_to_id" => "=", "status_id" => "o"}, + :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]}, + :query => {"name" => "test_edit_global_public_query", "is_public" => "1"} + + assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 4 + q = Query.find_by_name('test_edit_global_public_query') + assert q.is_public? + assert q.has_default_columns? + assert q.valid? + end + + def test_update_with_failure + @request.session[:user_id] = 1 + put :update, :id => 4, :query => {:name => ''} + assert_response :success + assert_template 'edit' + end + + def test_destroy + @request.session[:user_id] = 2 + delete :destroy, :id => 1 + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :set_filter => 1, :query_id => nil + assert_nil Query.find_by_id(1) + end + + def test_backslash_should_be_escaped_in_filters + @request.session[:user_id] = 2 + get :new, :subject => 'foo/bar' + assert_response :success + assert_template 'new' + assert_include 'addFilter("subject", "=", ["foo\/bar"]);', response.body + end +end diff --git a/test/functional/reports_controller_test.rb b/test/functional/reports_controller_test.rb new file mode 100644 index 00000000..a7faaf55 --- /dev/null +++ b/test/functional/reports_controller_test.rb @@ -0,0 +1,67 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class ReportsControllerTest < ActionController::TestCase + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :versions + + def test_get_issue_report + get :issue_report, :id => 1 + + assert_response :success + assert_template 'issue_report' + + [:issues_by_tracker, :issues_by_version, :issues_by_category, :issues_by_assigned_to, + :issues_by_author, :issues_by_subproject, :issues_by_priority].each do |ivar| + assert_not_nil assigns(ivar) + end + + assert_equal IssuePriority.all.reverse, assigns(:priorities) + end + + def test_get_issue_report_details + %w(tracker version priority category assigned_to author subproject).each do |detail| + get :issue_report_details, :id => 1, :detail => detail + + assert_response :success + assert_template 'issue_report_details' + assert_not_nil assigns(:field) + assert_not_nil assigns(:rows) + assert_not_nil assigns(:data) + assert_not_nil assigns(:report_title) + end + end + + def test_get_issue_report_details_by_priority + get :issue_report_details, :id => 1, :detail => 'priority' + assert_equal IssuePriority.all.reverse, assigns(:rows) + end + + def test_get_issue_report_details_with_an_invalid_detail + get :issue_report_details, :id => 1, :detail => 'invalid' + + assert_redirected_to '/projects/ecookbook/issues/report' + end +end diff --git a/test/functional/repositories_bazaar_controller_test.rb b/test/functional/repositories_bazaar_controller_test.rb new file mode 100644 index 00000000..1b34f76b --- /dev/null +++ b/test/functional/repositories_bazaar_controller_test.rb @@ -0,0 +1,198 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class RepositoriesBazaarControllerTest < ActionController::TestCase + tests RepositoriesController + + fixtures :projects, :users, :roles, :members, :member_roles, + :repositories, :enabled_modules + + REPOSITORY_PATH = Rails.root.join('tmp/test/bazaar_repository/trunk').to_s + PRJ_ID = 3 + + def setup + User.current = nil + @project = Project.find(PRJ_ID) + @repository = Repository::Bazaar.create( + :project => @project, + :url => REPOSITORY_PATH, + :log_encoding => 'UTF-8') + assert @repository + end + + if File.directory?(REPOSITORY_PATH) + def test_get_new + @request.session[:user_id] = 1 + @project.repository.destroy + get :new, :project_id => 'subproject1', :repository_scm => 'Bazaar' + assert_response :success + assert_template 'new' + assert_kind_of Repository::Bazaar, assigns(:repository) + assert assigns(:repository).new_record? + end + + def test_browse_root + get :show, :id => PRJ_ID + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal 2, assigns(:entries).size + assert assigns(:entries).detect {|e| e.name == 'directory' && e.kind == 'dir'} + assert assigns(:entries).detect {|e| e.name == 'doc-mkdir.txt' && e.kind == 'file'} + end + + def test_browse_directory + get :show, :id => PRJ_ID, :path => repository_path_hash(['directory'])[:param] + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal ['doc-ls.txt', 'document.txt', 'edit.png'], assigns(:entries).collect(&:name) + entry = assigns(:entries).detect {|e| e.name == 'edit.png'} + assert_not_nil entry + assert_equal 'file', entry.kind + assert_equal 'directory/edit.png', entry.path + end + + def test_browse_at_given_revision + get :show, :id => PRJ_ID, :path => repository_path_hash([])[:param], + :rev => 3 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal ['directory', 'doc-deleted.txt', 'doc-ls.txt', 'doc-mkdir.txt'], + assigns(:entries).collect(&:name) + end + + def test_changes + get :changes, :id => PRJ_ID, + :path => repository_path_hash(['doc-mkdir.txt'])[:param] + assert_response :success + assert_template 'changes' + assert_tag :tag => 'h2', :content => 'doc-mkdir.txt' + end + + def test_entry_show + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['directory', 'doc-ls.txt'])[:param] + assert_response :success + assert_template 'entry' + # Line 19 + assert_tag :tag => 'th', + :content => /29/, + :attributes => { :class => /line-num/ }, + :sibling => { :tag => 'td', :content => /Show help message/ } + end + + def test_entry_download + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['directory', 'doc-ls.txt'])[:param], + :format => 'raw' + assert_response :success + # File content + assert @response.body.include?('Show help message') + end + + def test_directory_entry + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['directory'])[:param] + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entry) + assert_equal 'directory', assigns(:entry).name + end + + def test_diff + # Full diff of changeset 3 + ['inline', 'sbs'].each do |dt| + get :diff, :id => PRJ_ID, :rev => 3, :type => dt + assert_response :success + assert_template 'diff' + # Line 11 removed + assert_tag :tag => 'th', + :content => '11', + :sibling => { :tag => 'td', + :attributes => { :class => /diff_out/ }, + :content => /Display more information/ } + end + end + + def test_annotate + get :annotate, :id => PRJ_ID, + :path => repository_path_hash(['doc-mkdir.txt'])[:param] + assert_response :success + assert_template 'annotate' + assert_tag :tag => 'th', :content => '2', + :sibling => { + :tag => 'td', + :child => { + :tag => 'a', + :content => '3' + } + } + assert_tag :tag => 'th', :content => '2', + :sibling => { :tag => 'td', :content => /jsmith/ } + assert_tag :tag => 'th', :content => '2', + :sibling => { + :tag => 'td', + :child => { + :tag => 'a', + :content => '3' + } + } + assert_tag :tag => 'th', :content => '2', + :sibling => { :tag => 'td', :content => /Main purpose/ } + end + + def test_destroy_valid_repository + @request.session[:user_id] = 1 # admin + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + assert @repository.changesets.count > 0 + + assert_difference 'Repository.count', -1 do + delete :destroy, :id => @repository.id + end + assert_response 302 + @project.reload + assert_nil @project.repository + end + + def test_destroy_invalid_repository + @request.session[:user_id] = 1 # admin + @project.repository.destroy + @repository = Repository::Bazaar.create!( + :project => @project, + :url => "/invalid", + :log_encoding => 'UTF-8') + @repository.fetch_changesets + @repository.reload + assert_equal 0, @repository.changesets.count + + assert_difference 'Repository.count', -1 do + delete :destroy, :id => @repository.id + end + assert_response 302 + @project.reload + assert_nil @project.repository + end + else + puts "Bazaar test repository NOT FOUND. Skipping functional tests !!!" + def test_fake; assert true end + end +end diff --git a/test/functional/repositories_controller_test.rb b/test/functional/repositories_controller_test.rb new file mode 100644 index 00000000..f7df422a --- /dev/null +++ b/test/functional/repositories_controller_test.rb @@ -0,0 +1,264 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class RepositoriesControllerTest < ActionController::TestCase + fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, + :repositories, :issues, :issue_statuses, :changesets, :changes, + :issue_categories, :enumerations, :custom_fields, :custom_values, :trackers + + def setup + User.current = nil + end + + def test_new + @request.session[:user_id] = 1 + get :new, :project_id => 'subproject1' + assert_response :success + assert_template 'new' + assert_kind_of Repository::Subversion, assigns(:repository) + assert assigns(:repository).new_record? + assert_tag 'input', :attributes => {:name => 'repository[url]', :disabled => nil} + end + + def test_new_should_propose_enabled_scm_only + @request.session[:user_id] = 1 + with_settings :enabled_scm => ['Mercurial', 'Git'] do + get :new, :project_id => 'subproject1' + end + assert_response :success + assert_template 'new' + assert_kind_of Repository::Mercurial, assigns(:repository) + assert_tag 'select', :attributes => {:name => 'repository_scm'}, + :children => {:count => 3} + assert_tag 'select', :attributes => {:name => 'repository_scm'}, + :child => {:tag => 'option', :attributes => {:value => 'Mercurial', :selected => 'selected'}} + assert_tag 'select', :attributes => {:name => 'repository_scm'}, + :child => {:tag => 'option', :attributes => {:value => 'Git', :selected => nil}} + end + + def test_create + @request.session[:user_id] = 1 + assert_difference 'Repository.count' do + post :create, :project_id => 'subproject1', + :repository_scm => 'Subversion', + :repository => {:url => 'file:///test', :is_default => '1', :identifier => ''} + end + assert_response 302 + repository = Repository.first(:order => 'id DESC') + assert_kind_of Repository::Subversion, repository + assert_equal 'file:///test', repository.url + end + + def test_create_with_failure + @request.session[:user_id] = 1 + assert_no_difference 'Repository.count' do + post :create, :project_id => 'subproject1', + :repository_scm => 'Subversion', + :repository => {:url => 'invalid'} + end + assert_response :success + assert_template 'new' + assert_kind_of Repository::Subversion, assigns(:repository) + assert assigns(:repository).new_record? + end + + def test_edit + @request.session[:user_id] = 1 + get :edit, :id => 11 + assert_response :success + assert_template 'edit' + assert_equal Repository.find(11), assigns(:repository) + assert_tag 'input', :attributes => {:name => 'repository[url]', :value => 'svn://localhost/test', :disabled => 'disabled'} + end + + def test_update + @request.session[:user_id] = 1 + put :update, :id => 11, :repository => {:password => 'test_update'} + assert_response 302 + assert_equal 'test_update', Repository.find(11).password + end + + def test_update_with_failure + @request.session[:user_id] = 1 + put :update, :id => 11, :repository => {:password => 'x'*260} + assert_response :success + assert_template 'edit' + assert_equal Repository.find(11), assigns(:repository) + end + + def test_destroy + @request.session[:user_id] = 1 + assert_difference 'Repository.count', -1 do + delete :destroy, :id => 11 + end + assert_response 302 + assert_nil Repository.find_by_id(11) + end + + def test_revisions + get :revisions, :id => 1 + assert_response :success + assert_template 'revisions' + assert_equal Repository.find(10), assigns(:repository) + assert_not_nil assigns(:changesets) + end + + def test_revisions_for_other_repository + repository = Repository::Subversion.create!(:project_id => 1, :identifier => 'foo', :url => 'file:///foo') + + get :revisions, :id => 1, :repository_id => 'foo' + assert_response :success + assert_template 'revisions' + assert_equal repository, assigns(:repository) + assert_not_nil assigns(:changesets) + end + + def test_revisions_for_invalid_repository + get :revisions, :id => 1, :repository_id => 'foo' + assert_response 404 + end + + def test_revision + get :revision, :id => 1, :rev => 1 + assert_response :success + assert_not_nil assigns(:changeset) + assert_equal "1", assigns(:changeset).revision + end + + def test_revision_should_not_change_the_project_menu_link + get :revision, :id => 1, :rev => 1 + assert_response :success + + assert_tag 'a', :attributes => {:href => '/projects/ecookbook/repository', :class => /repository/}, + :ancestor => {:attributes => {:id => 'main-menu'}} + end + + def test_revision_with_before_nil_and_afer_normal + get :revision, {:id => 1, :rev => 1} + assert_response :success + assert_template 'revision' + assert_no_tag :tag => "div", :attributes => { :class => "contextual" }, + :child => { :tag => "a", :attributes => { :href => '/projects/ecookbook/repository/revisions/0'} + } + assert_tag :tag => "div", :attributes => { :class => "contextual" }, + :child => { :tag => "a", :attributes => { :href => '/projects/ecookbook/repository/revisions/2'} + } + end + + def test_add_related_issue + @request.session[:user_id] = 2 + assert_difference 'Changeset.find(103).issues.size' do + xhr :post, :add_related_issue, :id => 1, :rev => 4, :issue_id => 2, :format => 'js' + assert_response :success + assert_template 'add_related_issue' + assert_equal 'text/javascript', response.content_type + end + assert_equal [2], Changeset.find(103).issue_ids + assert_include 'related-issues', response.body + assert_include 'Feature request #2', response.body + end + + def test_add_related_issue_with_invalid_issue_id + @request.session[:user_id] = 2 + assert_no_difference 'Changeset.find(103).issues.size' do + xhr :post, :add_related_issue, :id => 1, :rev => 4, :issue_id => 9999, :format => 'js' + assert_response :success + assert_template 'add_related_issue' + assert_equal 'text/javascript', response.content_type + end + assert_include 'alert("Issue is invalid")', response.body + end + + def test_remove_related_issue + Changeset.find(103).issues << Issue.find(1) + Changeset.find(103).issues << Issue.find(2) + + @request.session[:user_id] = 2 + assert_difference 'Changeset.find(103).issues.size', -1 do + xhr :delete, :remove_related_issue, :id => 1, :rev => 4, :issue_id => 2, :format => 'js' + assert_response :success + assert_template 'remove_related_issue' + assert_equal 'text/javascript', response.content_type + end + assert_equal [1], Changeset.find(103).issue_ids + assert_include 'related-issue-2', response.body + end + + def test_graph_commits_per_month + # Make sure there's some data to display + latest = Project.find(1).repository.changesets.maximum(:commit_date) + assert_not_nil latest + Date.stubs(:today).returns(latest.to_date + 10) + + get :graph, :id => 1, :graph => 'commits_per_month' + assert_response :success + assert_equal 'image/svg+xml', @response.content_type + end + + def test_graph_commits_per_author + get :graph, :id => 1, :graph => 'commits_per_author' + assert_response :success + assert_equal 'image/svg+xml', @response.content_type + end + + def test_get_committers + @request.session[:user_id] = 2 + # add a commit with an unknown user + Changeset.create!( + :repository => Project.find(1).repository, + :committer => 'foo', + :committed_on => Time.now, + :revision => 100, + :comments => 'Committed by foo.' + ) + + get :committers, :id => 10 + assert_response :success + assert_template 'committers' + + assert_tag :td, :content => 'dlopper', + :sibling => { :tag => 'td', + :child => { :tag => 'select', :attributes => { :name => %r{^committers\[\d+\]\[\]$} }, + :child => { :tag => 'option', :content => 'Dave Lopper', + :attributes => { :value => '3', :selected => 'selected' }}}} + assert_tag :td, :content => 'foo', + :sibling => { :tag => 'td', + :child => { :tag => 'select', :attributes => { :name => %r{^committers\[\d+\]\[\]$} }}} + assert_no_tag :td, :content => 'foo', + :sibling => { :tag => 'td', + :descendant => { :tag => 'option', :attributes => { :selected => 'selected' }}} + end + + def test_post_committers + @request.session[:user_id] = 2 + # add a commit with an unknown user + c = Changeset.create!( + :repository => Project.find(1).repository, + :committer => 'foo', + :committed_on => Time.now, + :revision => 100, + :comments => 'Committed by foo.' + ) + assert_no_difference "Changeset.count(:conditions => 'user_id = 3')" do + post :committers, :id => 10, :committers => { '0' => ['foo', '2'], '1' => ['dlopper', '3']} + assert_response 302 + assert_equal User.find(2), c.reload.user + end + end +end diff --git a/test/functional/repositories_cvs_controller_test.rb b/test/functional/repositories_cvs_controller_test.rb new file mode 100644 index 00000000..4a443c5a --- /dev/null +++ b/test/functional/repositories_cvs_controller_test.rb @@ -0,0 +1,274 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class RepositoriesCvsControllerTest < ActionController::TestCase + tests RepositoriesController + + fixtures :projects, :users, :roles, :members, :member_roles, + :repositories, :enabled_modules + + REPOSITORY_PATH = Rails.root.join('tmp/test/cvs_repository').to_s + REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin? + # CVS module + MODULE_NAME = 'test' + PRJ_ID = 3 + NUM_REV = 7 + + def setup + Setting.default_language = 'en' + User.current = nil + + @project = Project.find(PRJ_ID) + @repository = Repository::Cvs.create(:project => Project.find(PRJ_ID), + :root_url => REPOSITORY_PATH, + :url => MODULE_NAME, + :log_encoding => 'UTF-8') + assert @repository + end + + if File.directory?(REPOSITORY_PATH) + def test_get_new + @request.session[:user_id] = 1 + @project.repository.destroy + get :new, :project_id => 'subproject1', :repository_scm => 'Cvs' + assert_response :success + assert_template 'new' + assert_kind_of Repository::Cvs, assigns(:repository) + assert assigns(:repository).new_record? + end + + def test_browse_root + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :show, :id => PRJ_ID + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal 3, assigns(:entries).size + + entry = assigns(:entries).detect {|e| e.name == 'images'} + assert_equal 'dir', entry.kind + + entry = assigns(:entries).detect {|e| e.name == 'README'} + assert_equal 'file', entry.kind + + assert_not_nil assigns(:changesets) + assert assigns(:changesets).size > 0 + end + + def test_browse_directory + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param] + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal ['add.png', 'delete.png', 'edit.png'], assigns(:entries).collect(&:name) + entry = assigns(:entries).detect {|e| e.name == 'edit.png'} + assert_not_nil entry + assert_equal 'file', entry.kind + assert_equal 'images/edit.png', entry.path + end + + def test_browse_at_given_revision + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param], + :rev => 1 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name) + end + + def test_entry + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param] + assert_response :success + assert_template 'entry' + assert_no_tag :tag => 'td', + :attributes => { :class => /line-code/}, + :content => /before_filter/ + end + + def test_entry_at_given_revision + # changesets must be loaded + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param], + :rev => 2 + assert_response :success + assert_template 'entry' + # this line was removed in r3 + assert_tag :tag => 'td', + :attributes => { :class => /line-code/}, + :content => /before_filter/ + end + + def test_entry_not_found + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['sources', 'zzz.c'])[:param] + assert_tag :tag => 'p', + :attributes => { :id => /errorExplanation/ }, + :content => /The entry or revision was not found in the repository/ + end + + def test_entry_download + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param], + :format => 'raw' + assert_response :success + end + + def test_directory_entry + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['sources'])[:param] + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entry) + assert_equal 'sources', assigns(:entry).name + end + + def test_diff + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + ['inline', 'sbs'].each do |dt| + get :diff, :id => PRJ_ID, :rev => 3, :type => dt + assert_response :success + assert_template 'diff' + assert_tag :tag => 'td', :attributes => { :class => 'line-code diff_out' }, + :content => /before_filter :require_login/ + assert_tag :tag => 'td', :attributes => { :class => 'line-code diff_in' }, + :content => /with one change/ + end + end + + def test_diff_new_files + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + ['inline', 'sbs'].each do |dt| + get :diff, :id => PRJ_ID, :rev => 1, :type => dt + assert_response :success + assert_template 'diff' + assert_tag :tag => 'td', :attributes => { :class => 'line-code diff_in' }, + :content => /watched.remove_watcher/ + assert_tag :tag => 'th', :attributes => { :class => 'filename' }, + :content => /test\/README/ + assert_tag :tag => 'th', :attributes => { :class => 'filename' }, + :content => /test\/images\/delete.png / + assert_tag :tag => 'th', :attributes => { :class => 'filename' }, + :content => /test\/images\/edit.png/ + assert_tag :tag => 'th', :attributes => { :class => 'filename' }, + :content => /test\/sources\/watchers_controller.rb/ + end + end + + def test_annotate + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :annotate, :id => PRJ_ID, + :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param] + assert_response :success + assert_template 'annotate' + + # 1.1 line + assert_select 'tr' do + assert_select 'th.line-num', :text => '21' + assert_select 'td.revision', :text => /1.1/ + assert_select 'td.author', :text => /LANG/ + end + # 1.2 line + assert_select 'tr' do + assert_select 'th.line-num', :text => '32' + assert_select 'td.revision', :text => /1.2/ + assert_select 'td.author', :text => /LANG/ + end + end + + def test_destroy_valid_repository + @request.session[:user_id] = 1 # admin + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + + assert_difference 'Repository.count', -1 do + delete :destroy, :id => @repository.id + end + assert_response 302 + @project.reload + assert_nil @project.repository + end + + def test_destroy_invalid_repository + @request.session[:user_id] = 1 # admin + @project.repository.destroy + @repository = Repository::Cvs.create!( + :project => Project.find(PRJ_ID), + :root_url => "/invalid", + :url => MODULE_NAME, + :log_encoding => 'UTF-8' + ) + @repository.fetch_changesets + @project.reload + assert_equal 0, @repository.changesets.count + + assert_difference 'Repository.count', -1 do + delete :destroy, :id => @repository.id + end + assert_response 302 + @project.reload + assert_nil @project.repository + end + else + puts "CVS test repository NOT FOUND. Skipping functional tests !!!" + def test_fake; assert true end + end +end diff --git a/test/functional/repositories_darcs_controller_test.rb b/test/functional/repositories_darcs_controller_test.rb new file mode 100644 index 00000000..28265d48 --- /dev/null +++ b/test/functional/repositories_darcs_controller_test.rb @@ -0,0 +1,165 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class RepositoriesDarcsControllerTest < ActionController::TestCase + tests RepositoriesController + + fixtures :projects, :users, :roles, :members, :member_roles, + :repositories, :enabled_modules + + REPOSITORY_PATH = Rails.root.join('tmp/test/darcs_repository').to_s + PRJ_ID = 3 + NUM_REV = 6 + + def setup + User.current = nil + @project = Project.find(PRJ_ID) + @repository = Repository::Darcs.create( + :project => @project, + :url => REPOSITORY_PATH, + :log_encoding => 'UTF-8' + ) + assert @repository + end + + if File.directory?(REPOSITORY_PATH) + def test_get_new + @request.session[:user_id] = 1 + @project.repository.destroy + get :new, :project_id => 'subproject1', :repository_scm => 'Darcs' + assert_response :success + assert_template 'new' + assert_kind_of Repository::Darcs, assigns(:repository) + assert assigns(:repository).new_record? + end + + def test_browse_root + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :show, :id => PRJ_ID + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal 3, assigns(:entries).size + assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'} + assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'} + assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'} + end + + def test_browse_directory + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param] + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name) + entry = assigns(:entries).detect {|e| e.name == 'edit.png'} + assert_not_nil entry + assert_equal 'file', entry.kind + assert_equal 'images/edit.png', entry.path + end + + def test_browse_at_given_revision + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param], + :rev => 1 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal ['delete.png'], assigns(:entries).collect(&:name) + end + + def test_changes + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :changes, :id => PRJ_ID, + :path => repository_path_hash(['images', 'edit.png'])[:param] + assert_response :success + assert_template 'changes' + assert_tag :tag => 'h2', :content => 'edit.png' + end + + def test_diff + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + # Full diff of changeset 5 + ['inline', 'sbs'].each do |dt| + get :diff, :id => PRJ_ID, :rev => 5, :type => dt + assert_response :success + assert_template 'diff' + # Line 22 removed + assert_tag :tag => 'th', + :content => '22', + :sibling => { :tag => 'td', + :attributes => { :class => /diff_out/ }, + :content => /def remove/ } + end + end + + def test_destroy_valid_repository + @request.session[:user_id] = 1 # admin + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + + assert_difference 'Repository.count', -1 do + delete :destroy, :id => @repository.id + end + assert_response 302 + @project.reload + assert_nil @project.repository + end + + def test_destroy_invalid_repository + @request.session[:user_id] = 1 # admin + @project.repository.destroy + @repository = Repository::Darcs.create!( + :project => @project, + :url => "/invalid", + :log_encoding => 'UTF-8' + ) + @repository.fetch_changesets + @project.reload + assert_equal 0, @repository.changesets.count + + assert_difference 'Repository.count', -1 do + delete :destroy, :id => @repository.id + end + assert_response 302 + @project.reload + assert_nil @project.repository + end + else + puts "Darcs test repository NOT FOUND. Skipping functional tests !!!" + def test_fake; assert true end + end +end diff --git a/test/functional/repositories_filesystem_controller_test.rb b/test/functional/repositories_filesystem_controller_test.rb new file mode 100644 index 00000000..ebed7c69 --- /dev/null +++ b/test/functional/repositories_filesystem_controller_test.rb @@ -0,0 +1,164 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class RepositoriesFilesystemControllerTest < ActionController::TestCase + tests RepositoriesController + + fixtures :projects, :users, :roles, :members, :member_roles, + :repositories, :enabled_modules + + REPOSITORY_PATH = Rails.root.join('tmp/test/filesystem_repository').to_s + PRJ_ID = 3 + + def setup + @ruby19_non_utf8_pass = + (RUBY_VERSION >= '1.9' && Encoding.default_external.to_s != 'UTF-8') + User.current = nil + Setting.enabled_scm << 'Filesystem' unless Setting.enabled_scm.include?('Filesystem') + @project = Project.find(PRJ_ID) + @repository = Repository::Filesystem.create( + :project => @project, + :url => REPOSITORY_PATH, + :path_encoding => '' + ) + assert @repository + end + + if File.directory?(REPOSITORY_PATH) + def test_get_new + @request.session[:user_id] = 1 + @project.repository.destroy + get :new, :project_id => 'subproject1', :repository_scm => 'Filesystem' + assert_response :success + assert_template 'new' + assert_kind_of Repository::Filesystem, assigns(:repository) + assert assigns(:repository).new_record? + end + + def test_browse_root + @repository.fetch_changesets + @repository.reload + get :show, :id => PRJ_ID + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert assigns(:entries).size > 0 + assert_not_nil assigns(:changesets) + assert assigns(:changesets).size == 0 + + assert_no_tag 'input', :attributes => {:name => 'rev'} + assert_no_tag 'a', :content => 'Statistics' + assert_no_tag 'a', :content => 'Atom' + end + + def test_show_no_extension + get :entry, :id => PRJ_ID, :path => repository_path_hash(['test'])[:param] + assert_response :success + assert_template 'entry' + assert_tag :tag => 'th', + :content => '1', + :attributes => { :class => 'line-num' }, + :sibling => { :tag => 'td', :content => /TEST CAT/ } + end + + def test_entry_download_no_extension + get :raw, :id => PRJ_ID, :path => repository_path_hash(['test'])[:param] + assert_response :success + assert_equal 'application/octet-stream', @response.content_type + end + + def test_show_non_ascii_contents + with_settings :repositories_encodings => 'UTF-8,EUC-JP' do + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['japanese', 'euc-jp.txt'])[:param] + assert_response :success + assert_template 'entry' + assert_tag :tag => 'th', + :content => '2', + :attributes => { :class => 'line-num' }, + :sibling => { :tag => 'td', :content => /japanese/ } + if @ruby19_non_utf8_pass + puts "TODO: show repository file contents test fails in Ruby 1.9 " + + "and Encoding.default_external is not UTF-8. " + + "Current value is '#{Encoding.default_external.to_s}'" + else + str_japanese = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e" + str_japanese.force_encoding('UTF-8') if str_japanese.respond_to?(:force_encoding) + assert_tag :tag => 'th', + :content => '3', + :attributes => { :class => 'line-num' }, + :sibling => { :tag => 'td', :content => /#{str_japanese}/ } + end + end + end + + def test_show_utf16 + enc = (RUBY_VERSION == "1.9.2" ? 'UTF-16LE' : 'UTF-16') + with_settings :repositories_encodings => enc do + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['japanese', 'utf-16.txt'])[:param] + assert_response :success + assert_tag :tag => 'th', + :content => '2', + :attributes => { :class => 'line-num' }, + :sibling => { :tag => 'td', :content => /japanese/ } + end + end + + def test_show_text_file_should_send_if_too_big + with_settings :file_max_size_displayed => 1 do + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['japanese', 'big-file.txt'])[:param] + assert_response :success + assert_equal 'text/plain', @response.content_type + end + end + + def test_destroy_valid_repository + @request.session[:user_id] = 1 # admin + + assert_difference 'Repository.count', -1 do + delete :destroy, :id => @repository.id + end + assert_response 302 + @project.reload + assert_nil @project.repository + end + + def test_destroy_invalid_repository + @request.session[:user_id] = 1 # admin + @project.repository.destroy + @repository = Repository::Filesystem.create!( + :project => @project, + :url => "/invalid", + :path_encoding => '' + ) + + assert_difference 'Repository.count', -1 do + delete :destroy, :id => @repository.id + end + assert_response 302 + @project.reload + assert_nil @project.repository + end + else + puts "Filesystem test repository NOT FOUND. Skipping functional tests !!!" + def test_fake; assert true end + end +end diff --git a/test/functional/repositories_git_controller_test.rb b/test/functional/repositories_git_controller_test.rb new file mode 100644 index 00000000..fcdf7013 --- /dev/null +++ b/test/functional/repositories_git_controller_test.rb @@ -0,0 +1,638 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class RepositoriesGitControllerTest < ActionController::TestCase + tests RepositoriesController + + fixtures :projects, :users, :roles, :members, :member_roles, + :repositories, :enabled_modules + + REPOSITORY_PATH = Rails.root.join('tmp/test/git_repository').to_s + REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin? + PRJ_ID = 3 + CHAR_1_HEX = "\xc3\x9c" + NUM_REV = 28 + + ## Git, Mercurial and CVS path encodings are binary. + ## Subversion supports URL encoding for path. + ## Redmine Mercurial adapter and extension use URL encoding. + ## Git accepts only binary path in command line parameter. + ## So, there is no way to use binary command line parameter in JRuby. + JRUBY_SKIP = (RUBY_PLATFORM == 'java') + JRUBY_SKIP_STR = "TODO: This test fails in JRuby" + + def setup + @ruby19_non_utf8_pass = + (RUBY_VERSION >= '1.9' && Encoding.default_external.to_s != 'UTF-8') + + User.current = nil + @project = Project.find(PRJ_ID) + @repository = Repository::Git.create( + :project => @project, + :url => REPOSITORY_PATH, + :path_encoding => 'ISO-8859-1' + ) + assert @repository + @char_1 = CHAR_1_HEX.dup + if @char_1.respond_to?(:force_encoding) + @char_1.force_encoding('UTF-8') + end + end + + def test_create_and_update + @request.session[:user_id] = 1 + assert_difference 'Repository.count' do + post :create, :project_id => 'subproject1', + :repository_scm => 'Git', + :repository => { + :url => '/test', + :is_default => '0', + :identifier => 'test-create', + :extra_report_last_commit => '1', + } + end + assert_response 302 + repository = Repository.first(:order => 'id DESC') + assert_kind_of Repository::Git, repository + assert_equal '/test', repository.url + assert_equal true, repository.extra_report_last_commit + + put :update, :id => repository.id, + :repository => { + :extra_report_last_commit => '0' + } + assert_response 302 + repo2 = Repository.find(repository.id) + assert_equal false, repo2.extra_report_last_commit + end + + if File.directory?(REPOSITORY_PATH) + ## Ruby uses ANSI api to fork a process on Windows. + ## Japanese Shift_JIS and Traditional Chinese Big5 have 0x5c(backslash) problem + ## and these are incompatible with ASCII. + ## Git for Windows (msysGit) changed internal API from ANSI to Unicode in 1.7.10 + ## http://code.google.com/p/msysgit/issues/detail?id=80 + ## So, Latin-1 path tests fail on Japanese Windows + WINDOWS_PASS = (Redmine::Platform.mswin? && + Redmine::Scm::Adapters::GitAdapter.client_version_above?([1, 7, 10])) + WINDOWS_SKIP_STR = "TODO: This test fails in Git for Windows above 1.7.10" + + def test_get_new + @request.session[:user_id] = 1 + @project.repository.destroy + get :new, :project_id => 'subproject1', :repository_scm => 'Git' + assert_response :success + assert_template 'new' + assert_kind_of Repository::Git, assigns(:repository) + assert assigns(:repository).new_record? + end + + def test_browse_root + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + + get :show, :id => PRJ_ID + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal 9, assigns(:entries).size + assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'} + assert assigns(:entries).detect {|e| e.name == 'this_is_a_really_long_and_verbose_directory_name' && e.kind == 'dir'} + assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'} + assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'} + assert assigns(:entries).detect {|e| e.name == 'copied_README' && e.kind == 'file'} + assert assigns(:entries).detect {|e| e.name == 'new_file.txt' && e.kind == 'file'} + assert assigns(:entries).detect {|e| e.name == 'renamed_test.txt' && e.kind == 'file'} + assert assigns(:entries).detect {|e| e.name == 'filemane with spaces.txt' && e.kind == 'file'} + assert assigns(:entries).detect {|e| e.name == ' filename with a leading space.txt ' && e.kind == 'file'} + assert_not_nil assigns(:changesets) + assert assigns(:changesets).size > 0 + end + + def test_browse_branch + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :show, :id => PRJ_ID, :rev => 'test_branch' + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal 4, assigns(:entries).size + assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'} + assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'} + assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'} + assert assigns(:entries).detect {|e| e.name == 'test.txt' && e.kind == 'file'} + assert_not_nil assigns(:changesets) + assert assigns(:changesets).size > 0 + end + + def test_browse_tag + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + [ + "tag00.lightweight", + "tag01.annotated", + ].each do |t1| + get :show, :id => PRJ_ID, :rev => t1 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert assigns(:entries).size > 0 + assert_not_nil assigns(:changesets) + assert assigns(:changesets).size > 0 + end + end + + def test_browse_directory + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param] + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal ['edit.png'], assigns(:entries).collect(&:name) + entry = assigns(:entries).detect {|e| e.name == 'edit.png'} + assert_not_nil entry + assert_equal 'file', entry.kind + assert_equal 'images/edit.png', entry.path + assert_not_nil assigns(:changesets) + assert assigns(:changesets).size > 0 + end + + def test_browse_at_given_revision + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param], + :rev => '7234cb2750b63f47bff735edc50a1c0a433c2518' + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal ['delete.png'], assigns(:entries).collect(&:name) + assert_not_nil assigns(:changesets) + assert assigns(:changesets).size > 0 + end + + def test_changes + get :changes, :id => PRJ_ID, + :path => repository_path_hash(['images', 'edit.png'])[:param] + assert_response :success + assert_template 'changes' + assert_tag :tag => 'h2', :content => 'edit.png' + end + + def test_entry_show + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param] + assert_response :success + assert_template 'entry' + # Line 19 + assert_tag :tag => 'th', + :content => '11', + :attributes => { :class => 'line-num' }, + :sibling => { :tag => 'td', :content => /WITHOUT ANY WARRANTY/ } + end + + def test_entry_show_latin_1 + if @ruby19_non_utf8_pass + puts_ruby19_non_utf8_pass() + elsif WINDOWS_PASS + puts WINDOWS_SKIP_STR + elsif JRUBY_SKIP + puts JRUBY_SKIP_STR + else + with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do + ['57ca437c', '57ca437c0acbbcb749821fdf3726a1367056d364'].each do |r1| + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['latin-1-dir', "test-#{@char_1}.txt"])[:param], + :rev => r1 + assert_response :success + assert_template 'entry' + assert_tag :tag => 'th', + :content => '1', + :attributes => { :class => 'line-num' }, + :sibling => { :tag => 'td', + :content => /test-#{@char_1}.txt/ } + end + end + end + end + + def test_entry_download + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param], + :format => 'raw' + assert_response :success + # File content + assert @response.body.include?('WITHOUT ANY WARRANTY') + end + + def test_directory_entry + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['sources'])[:param] + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entry) + assert_equal 'sources', assigns(:entry).name + end + + def test_diff + assert_equal true, @repository.is_default + assert_nil @repository.identifier + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + # Full diff of changeset 2f9c0091 + ['inline', 'sbs'].each do |dt| + get :diff, + :id => PRJ_ID, + :rev => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7', + :type => dt + assert_response :success + assert_template 'diff' + # Line 22 removed + assert_tag :tag => 'th', + :content => /22/, + :sibling => { :tag => 'td', + :attributes => { :class => /diff_out/ }, + :content => /def remove/ } + assert_tag :tag => 'h2', :content => /2f9c0091/ + end + end + + def test_diff_with_rev_and_path + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + with_settings :diff_max_lines_displayed => 1000 do + # Full diff of changeset 2f9c0091 + ['inline', 'sbs'].each do |dt| + get :diff, + :id => PRJ_ID, + :rev => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7', + :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param], + :type => dt + assert_response :success + assert_template 'diff' + # Line 22 removed + assert_tag :tag => 'th', + :content => '22', + :sibling => { :tag => 'td', + :attributes => { :class => /diff_out/ }, + :content => /def remove/ } + assert_tag :tag => 'h2', :content => /2f9c0091/ + end + end + end + + def test_diff_truncated + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + + with_settings :diff_max_lines_displayed => 5 do + # Truncated diff of changeset 2f9c0091 + with_cache do + with_settings :default_language => 'en' do + get :diff, :id => PRJ_ID, :type => 'inline', + :rev => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7' + assert_response :success + assert @response.body.include?("... This diff was truncated") + end + with_settings :default_language => 'fr' do + get :diff, :id => PRJ_ID, :type => 'inline', + :rev => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7' + assert_response :success + assert ! @response.body.include?("... This diff was truncated") + assert @response.body.include?("... Ce diff") + end + end + end + end + + def test_diff_two_revs + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + ['inline', 'sbs'].each do |dt| + get :diff, + :id => PRJ_ID, + :rev => '61b685fbe55ab05b5ac68402d5720c1a6ac973d1', + :rev_to => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7', + :type => dt + assert_response :success + assert_template 'diff' + diff = assigns(:diff) + assert_not_nil diff + assert_tag :tag => 'h2', :content => /2f9c0091:61b685fb/ + assert_tag :tag => "form", + :attributes => { + :action => "/projects/subproject1/repository/revisions/" + + "61b685fbe55ab05b5ac68402d5720c1a6ac973d1/diff" + } + assert_tag :tag => 'input', + :attributes => { + :id => "rev_to", + :name => "rev_to", + :type => "hidden", + :value => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7' + } + end + end + + def test_diff_path_in_subrepo + repo = Repository::Git.create( + :project => @project, + :url => REPOSITORY_PATH, + :identifier => 'test-diff-path', + :path_encoding => 'ISO-8859-1' + ); + assert repo + assert_equal false, repo.is_default + assert_equal 'test-diff-path', repo.identifier + get :diff, + :id => PRJ_ID, + :repository_id => 'test-diff-path', + :rev => '61b685fbe55ab05b', + :rev_to => '2f9c0091c754a91a', + :type => 'inline' + assert_response :success + assert_template 'diff' + diff = assigns(:diff) + assert_not_nil diff + assert_tag :tag => "form", + :attributes => { + :action => "/projects/subproject1/repository/test-diff-path/" + + "revisions/61b685fbe55ab05b/diff" + } + assert_tag :tag => 'input', + :attributes => { + :id => "rev_to", + :name => "rev_to", + :type => "hidden", + :value => '2f9c0091c754a91a' + } + end + + def test_diff_latin_1 + if @ruby19_non_utf8_pass + puts_ruby19_non_utf8_pass() + else + with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do + ['57ca437c', '57ca437c0acbbcb749821fdf3726a1367056d364'].each do |r1| + ['inline', 'sbs'].each do |dt| + get :diff, :id => PRJ_ID, :rev => r1, :type => dt + assert_response :success + assert_template 'diff' + assert_tag :tag => 'thead', + :descendant => { + :tag => 'th', + :attributes => { :class => 'filename' } , + :content => /latin-1-dir\/test-#{@char_1}.txt/ , + }, + :sibling => { + :tag => 'tbody', + :descendant => { + :tag => 'td', + :attributes => { :class => /diff_in/ }, + :content => /test-#{@char_1}.txt/ + } + } + end + end + end + end + end + + def test_diff_should_show_filenames + get :diff, :id => PRJ_ID, :rev => 'deff712f05a90d96edbd70facc47d944be5897e3', :type => 'inline' + assert_response :success + assert_template 'diff' + # modified file + assert_select 'th.filename', :text => 'sources/watchers_controller.rb' + # deleted file + assert_select 'th.filename', :text => 'test.txt' + end + + def test_save_diff_type + user1 = User.find(1) + user1.pref[:diff_type] = nil + user1.preference.save + user = User.find(1) + assert_nil user.pref[:diff_type] + + @request.session[:user_id] = 1 # admin + get :diff, + :id => PRJ_ID, + :rev => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7' + assert_response :success + assert_template 'diff' + user.reload + assert_equal "inline", user.pref[:diff_type] + get :diff, + :id => PRJ_ID, + :rev => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7', + :type => 'sbs' + assert_response :success + assert_template 'diff' + user.reload + assert_equal "sbs", user.pref[:diff_type] + end + + def test_annotate + get :annotate, :id => PRJ_ID, + :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param] + assert_response :success + assert_template 'annotate' + + # Line 23, changeset 2f9c0091 + assert_select 'tr' do + assert_select 'th.line-num', :text => '23' + assert_select 'td.revision', :text => /2f9c0091/ + assert_select 'td.author', :text => 'jsmith' + assert_select 'td', :text => /remove_watcher/ + end + end + + def test_annotate_at_given_revision + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :annotate, :id => PRJ_ID, :rev => 'deff7', + :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param] + assert_response :success + assert_template 'annotate' + assert_tag :tag => 'h2', :content => /@ deff712f/ + end + + def test_annotate_binary_file + get :annotate, :id => PRJ_ID, + :path => repository_path_hash(['images', 'edit.png'])[:param] + assert_response 500 + assert_tag :tag => 'p', :attributes => { :id => /errorExplanation/ }, + :content => /cannot be annotated/ + end + + def test_annotate_error_when_too_big + with_settings :file_max_size_displayed => 1 do + get :annotate, :id => PRJ_ID, + :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param], + :rev => 'deff712f' + assert_response 500 + assert_tag :tag => 'p', :attributes => { :id => /errorExplanation/ }, + :content => /exceeds the maximum text file size/ + + get :annotate, :id => PRJ_ID, + :path => repository_path_hash(['README'])[:param], + :rev => '7234cb2' + assert_response :success + assert_template 'annotate' + end + end + + def test_annotate_latin_1 + if @ruby19_non_utf8_pass + puts_ruby19_non_utf8_pass() + elsif WINDOWS_PASS + puts WINDOWS_SKIP_STR + elsif JRUBY_SKIP + puts JRUBY_SKIP_STR + else + with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do + ['57ca437c', '57ca437c0acbbcb749821fdf3726a1367056d364'].each do |r1| + get :annotate, :id => PRJ_ID, + :path => repository_path_hash(['latin-1-dir', "test-#{@char_1}.txt"])[:param], + :rev => r1 + assert_tag :tag => 'th', + :content => '1', + :attributes => { :class => 'line-num' }, + :sibling => { :tag => 'td', + :content => /test-#{@char_1}.txt/ } + end + end + end + end + + def test_revisions + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :revisions, :id => PRJ_ID + assert_response :success + assert_template 'revisions' + assert_tag :tag => 'form', + :attributes => { + :method => 'get', + :action => '/projects/subproject1/repository/revision' + } + end + + def test_revision + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + ['61b685fbe55ab05b5ac68402d5720c1a6ac973d1', '61b685f'].each do |r| + get :revision, :id => PRJ_ID, :rev => r + assert_response :success + assert_template 'revision' + end + end + + def test_empty_revision + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + ['', ' ', nil].each do |r| + get :revision, :id => PRJ_ID, :rev => r + assert_response 404 + assert_error_tag :content => /was not found/ + end + end + + def test_destroy_valid_repository + @request.session[:user_id] = 1 # admin + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + + assert_difference 'Repository.count', -1 do + delete :destroy, :id => @repository.id + end + assert_response 302 + @project.reload + assert_nil @project.repository + end + + def test_destroy_invalid_repository + @request.session[:user_id] = 1 # admin + @project.repository.destroy + @repository = Repository::Git.create!( + :project => @project, + :url => "/invalid", + :path_encoding => 'ISO-8859-1' + ) + @repository.fetch_changesets + @repository.reload + assert_equal 0, @repository.changesets.count + + assert_difference 'Repository.count', -1 do + delete :destroy, :id => @repository.id + end + assert_response 302 + @project.reload + assert_nil @project.repository + end + + private + + def puts_ruby19_non_utf8_pass + puts "TODO: This test fails in Ruby 1.9 " + + "and Encoding.default_external is not UTF-8. " + + "Current value is '#{Encoding.default_external.to_s}'" + end + else + puts "Git test repository NOT FOUND. Skipping functional tests !!!" + def test_fake; assert true end + end + + private + def with_cache(&block) + before = ActionController::Base.perform_caching + ActionController::Base.perform_caching = true + block.call + ActionController::Base.perform_caching = before + end +end diff --git a/test/functional/repositories_mercurial_controller_test.rb b/test/functional/repositories_mercurial_controller_test.rb new file mode 100644 index 00000000..a4dffd96 --- /dev/null +++ b/test/functional/repositories_mercurial_controller_test.rb @@ -0,0 +1,525 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class RepositoriesMercurialControllerTest < ActionController::TestCase + tests RepositoriesController + + fixtures :projects, :users, :roles, :members, :member_roles, + :repositories, :enabled_modules + + REPOSITORY_PATH = Rails.root.join('tmp/test/mercurial_repository').to_s + CHAR_1_HEX = "\xc3\x9c" + PRJ_ID = 3 + NUM_REV = 32 + + ruby19_non_utf8_pass = + (RUBY_VERSION >= '1.9' && Encoding.default_external.to_s != 'UTF-8') + + def setup + User.current = nil + @project = Project.find(PRJ_ID) + @repository = Repository::Mercurial.create( + :project => @project, + :url => REPOSITORY_PATH, + :path_encoding => 'ISO-8859-1' + ) + assert @repository + @diff_c_support = true + @char_1 = CHAR_1_HEX.dup + @tag_char_1 = "tag-#{CHAR_1_HEX}-00" + @branch_char_0 = "branch-#{CHAR_1_HEX}-00" + @branch_char_1 = "branch-#{CHAR_1_HEX}-01" + if @char_1.respond_to?(:force_encoding) + @char_1.force_encoding('UTF-8') + @tag_char_1.force_encoding('UTF-8') + @branch_char_0.force_encoding('UTF-8') + @branch_char_1.force_encoding('UTF-8') + end + end + + if ruby19_non_utf8_pass + puts "TODO: Mercurial functional test fails in Ruby 1.9 " + + "and Encoding.default_external is not UTF-8. " + + "Current value is '#{Encoding.default_external.to_s}'" + def test_fake; assert true end + elsif File.directory?(REPOSITORY_PATH) + + def test_get_new + @request.session[:user_id] = 1 + @project.repository.destroy + get :new, :project_id => 'subproject1', :repository_scm => 'Mercurial' + assert_response :success + assert_template 'new' + assert_kind_of Repository::Mercurial, assigns(:repository) + assert assigns(:repository).new_record? + end + + def test_show_root + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :show, :id => PRJ_ID + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal 4, assigns(:entries).size + assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'} + assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'} + assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'} + assert_not_nil assigns(:changesets) + assert assigns(:changesets).size > 0 + end + + def test_show_directory + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param] + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name) + entry = assigns(:entries).detect {|e| e.name == 'edit.png'} + assert_not_nil entry + assert_equal 'file', entry.kind + assert_equal 'images/edit.png', entry.path + assert_not_nil assigns(:changesets) + assert assigns(:changesets).size > 0 + end + + def test_show_at_given_revision + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + [0, '0', '0885933ad4f6'].each do |r1| + get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param], + :rev => r1 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal ['delete.png'], assigns(:entries).collect(&:name) + assert_not_nil assigns(:changesets) + assert assigns(:changesets).size > 0 + end + end + + def test_show_directory_sql_escape_percent + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + [13, '13', '3a330eb32958'].each do |r1| + get :show, :id => PRJ_ID, + :path => repository_path_hash(['sql_escape', 'percent%dir'])[:param], + :rev => r1 + assert_response :success + assert_template 'show' + + assert_not_nil assigns(:entries) + assert_equal ['percent%file1.txt', 'percentfile1.txt'], + assigns(:entries).collect(&:name) + changesets = assigns(:changesets) + assert_not_nil changesets + assert assigns(:changesets).size > 0 + assert_equal %w(13 11 10 9), changesets.collect(&:revision) + end + end + + def test_show_directory_latin_1_path + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + [21, '21', 'adf805632193'].each do |r1| + get :show, :id => PRJ_ID, + :path => repository_path_hash(['latin-1-dir'])[:param], + :rev => r1 + assert_response :success + assert_template 'show' + + assert_not_nil assigns(:entries) + assert_equal ["make-latin-1-file.rb", + "test-#{@char_1}-1.txt", + "test-#{@char_1}-2.txt", + "test-#{@char_1}.txt"], assigns(:entries).collect(&:name) + changesets = assigns(:changesets) + assert_not_nil changesets + assert_equal %w(21 20 19 18 17), changesets.collect(&:revision) + end + end + + def show_should_show_branch_selection_form + @repository.fetch_changesets + @project.reload + get :show, :id => PRJ_ID + assert_tag 'form', :attributes => {:id => 'revision_selector', :action => '/projects/subproject1/repository/show'} + assert_tag 'select', :attributes => {:name => 'branch'}, + :child => {:tag => 'option', :attributes => {:value => 'test-branch-01'}}, + :parent => {:tag => 'form', :attributes => {:id => 'revision_selector'}} + end + + def test_show_branch + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + [ + 'default', + @branch_char_1, + 'branch (1)[2]&,%.-3_4', + @branch_char_0, + 'test_branch.latin-1', + 'test-branch-00', + ].each do |bra| + get :show, :id => PRJ_ID, :rev => bra + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert assigns(:entries).size > 0 + assert_not_nil assigns(:changesets) + assert assigns(:changesets).size > 0 + end + end + + def test_show_tag + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + [ + @tag_char_1, + 'tag_test.00', + 'tag-init-revision' + ].each do |tag| + get :show, :id => PRJ_ID, :rev => tag + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert assigns(:entries).size > 0 + assert_not_nil assigns(:changesets) + assert assigns(:changesets).size > 0 + end + end + + def test_changes + get :changes, :id => PRJ_ID, + :path => repository_path_hash(['images', 'edit.png'])[:param] + assert_response :success + assert_template 'changes' + assert_tag :tag => 'h2', :content => 'edit.png' + end + + def test_entry_show + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param] + assert_response :success + assert_template 'entry' + # Line 10 + assert_tag :tag => 'th', + :content => '10', + :attributes => { :class => 'line-num' }, + :sibling => { :tag => 'td', :content => /WITHOUT ANY WARRANTY/ } + end + + def test_entry_show_latin_1_path + [21, '21', 'adf805632193'].each do |r1| + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['latin-1-dir', "test-#{@char_1}-2.txt"])[:param], + :rev => r1 + assert_response :success + assert_template 'entry' + assert_tag :tag => 'th', + :content => '1', + :attributes => { :class => 'line-num' }, + :sibling => { :tag => 'td', + :content => /Mercurial is a distributed version control system/ } + end + end + + def test_entry_show_latin_1_contents + with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do + [27, '27', '7bbf4c738e71'].each do |r1| + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['latin-1-dir', "test-#{@char_1}.txt"])[:param], + :rev => r1 + assert_response :success + assert_template 'entry' + assert_tag :tag => 'th', + :content => '1', + :attributes => { :class => 'line-num' }, + :sibling => { :tag => 'td', + :content => /test-#{@char_1}.txt/ } + end + end + end + + def test_entry_download + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param], + :format => 'raw' + assert_response :success + # File content + assert @response.body.include?('WITHOUT ANY WARRANTY') + end + + def test_entry_binary_force_download + get :entry, :id => PRJ_ID, :rev => 1, + :path => repository_path_hash(['images', 'edit.png'])[:param] + assert_response :success + assert_equal 'image/png', @response.content_type + end + + def test_directory_entry + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['sources'])[:param] + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entry) + assert_equal 'sources', assigns(:entry).name + end + + def test_diff + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + [4, '4', 'def6d2f1254a'].each do |r1| + # Full diff of changeset 4 + ['inline', 'sbs'].each do |dt| + get :diff, :id => PRJ_ID, :rev => r1, :type => dt + assert_response :success + assert_template 'diff' + if @diff_c_support + # Line 22 removed + assert_tag :tag => 'th', + :content => '22', + :sibling => { :tag => 'td', + :attributes => { :class => /diff_out/ }, + :content => /def remove/ } + assert_tag :tag => 'h2', :content => /4:def6d2f1254a/ + end + end + end + end + + def test_diff_two_revs + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + [2, '400bb8672109', '400', 400].each do |r1| + [4, 'def6d2f1254a'].each do |r2| + ['inline', 'sbs'].each do |dt| + get :diff, + :id => PRJ_ID, + :rev => r1, + :rev_to => r2, + :type => dt + assert_response :success + assert_template 'diff' + diff = assigns(:diff) + assert_not_nil diff + assert_tag :tag => 'h2', + :content => /4:def6d2f1254a 2:400bb8672109/ + end + end + end + end + + def test_diff_latin_1_path + with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do + [21, 'adf805632193'].each do |r1| + ['inline', 'sbs'].each do |dt| + get :diff, :id => PRJ_ID, :rev => r1, :type => dt + assert_response :success + assert_template 'diff' + assert_tag :tag => 'thead', + :descendant => { + :tag => 'th', + :attributes => { :class => 'filename' } , + :content => /latin-1-dir\/test-#{@char_1}-2.txt/ , + }, + :sibling => { + :tag => 'tbody', + :descendant => { + :tag => 'td', + :attributes => { :class => /diff_in/ }, + :content => /It is written in Python/ + } + } + end + end + end + end + + def test_diff_should_show_modified_filenames + get :diff, :id => PRJ_ID, :rev => '400bb8672109', :type => 'inline' + assert_response :success + assert_template 'diff' + assert_select 'th.filename', :text => 'sources/watchers_controller.rb' + end + + def test_diff_should_show_deleted_filenames + get :diff, :id => PRJ_ID, :rev => 'b3a615152df8', :type => 'inline' + assert_response :success + assert_template 'diff' + assert_select 'th.filename', :text => 'sources/welcome_controller.rb' + end + + def test_annotate + get :annotate, :id => PRJ_ID, + :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param] + assert_response :success + assert_template 'annotate' + + # Line 22, revision 4:def6d2f1254a + assert_select 'tr' do + assert_select 'th.line-num', :text => '22' + assert_select 'td.revision', :text => '4:def6d2f1254a' + assert_select 'td.author', :text => 'jsmith' + assert_select 'td', :text => /remove_watcher/ + end + end + + def test_annotate_not_in_tip + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :annotate, :id => PRJ_ID, + :path => repository_path_hash(['sources', 'welcome_controller.rb'])[:param] + assert_response 404 + assert_error_tag :content => /was not found/ + end + + def test_annotate_at_given_revision + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + [2, '400bb8672109', '400', 400].each do |r1| + get :annotate, :id => PRJ_ID, :rev => r1, + :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param] + assert_response :success + assert_template 'annotate' + assert_tag :tag => 'h2', :content => /@ 2:400bb8672109/ + end + end + + def test_annotate_latin_1_path + [21, '21', 'adf805632193'].each do |r1| + get :annotate, :id => PRJ_ID, + :path => repository_path_hash(['latin-1-dir', "test-#{@char_1}-2.txt"])[:param], + :rev => r1 + assert_response :success + assert_template 'annotate' + assert_tag :tag => 'th', + :content => '1', + :attributes => { :class => 'line-num' }, + :sibling => + { + :tag => 'td', + :attributes => { :class => 'revision' }, + :child => { :tag => 'a', :content => '20:709858aafd1b' } + } + assert_tag :tag => 'th', + :content => '1', + :attributes => { :class => 'line-num' }, + :sibling => + { + :tag => 'td' , + :content => 'jsmith' , + :attributes => { :class => 'author' }, + } + assert_tag :tag => 'th', + :content => '1', + :attributes => { :class => 'line-num' }, + :sibling => { :tag => 'td', + :content => /Mercurial is a distributed version control system/ } + + end + end + + def test_annotate_latin_1_contents + with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do + [27, '7bbf4c738e71'].each do |r1| + get :annotate, :id => PRJ_ID, + :path => repository_path_hash(['latin-1-dir', "test-#{@char_1}.txt"])[:param], + :rev => r1 + assert_tag :tag => 'th', + :content => '1', + :attributes => { :class => 'line-num' }, + :sibling => { :tag => 'td', + :content => /test-#{@char_1}.txt/ } + end + end + end + + def test_empty_revision + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + ['', ' ', nil].each do |r| + get :revision, :id => PRJ_ID, :rev => r + assert_response 404 + assert_error_tag :content => /was not found/ + end + end + + def test_destroy_valid_repository + @request.session[:user_id] = 1 # admin + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + assert_equal NUM_REV, @repository.changesets.count + + assert_difference 'Repository.count', -1 do + delete :destroy, :id => @repository.id + end + assert_response 302 + @project.reload + assert_nil @project.repository + end + + def test_destroy_invalid_repository + @request.session[:user_id] = 1 # admin + @project.repository.destroy + @repository = Repository::Mercurial.create!( + :project => Project.find(PRJ_ID), + :url => "/invalid", + :path_encoding => 'ISO-8859-1' + ) + @repository.fetch_changesets + assert_equal 0, @repository.changesets.count + + assert_difference 'Repository.count', -1 do + delete :destroy, :id => @repository.id + end + assert_response 302 + @project.reload + assert_nil @project.repository + end + else + puts "Mercurial test repository NOT FOUND. Skipping functional tests !!!" + def test_fake; assert true end + end +end diff --git a/test/functional/repositories_subversion_controller_test.rb b/test/functional/repositories_subversion_controller_test.rb new file mode 100644 index 00000000..07d4686d --- /dev/null +++ b/test/functional/repositories_subversion_controller_test.rb @@ -0,0 +1,425 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class RepositoriesSubversionControllerTest < ActionController::TestCase + tests RepositoriesController + + fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, + :repositories, :issues, :issue_statuses, :changesets, :changes, + :issue_categories, :enumerations, :custom_fields, :custom_values, :trackers + + PRJ_ID = 3 + NUM_REV = 11 + + def setup + Setting.default_language = 'en' + User.current = nil + + @project = Project.find(PRJ_ID) + @repository = Repository::Subversion.create(:project => @project, + :url => self.class.subversion_repository_url) + assert @repository + end + + if repository_configured?('subversion') + def test_new + @request.session[:user_id] = 1 + @project.repository.destroy + get :new, :project_id => 'subproject1', :repository_scm => 'Subversion' + assert_response :success + assert_template 'new' + assert_kind_of Repository::Subversion, assigns(:repository) + assert assigns(:repository).new_record? + end + + def test_show + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :show, :id => PRJ_ID + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_not_nil assigns(:changesets) + + entry = assigns(:entries).detect {|e| e.name == 'subversion_test'} + assert_not_nil entry + assert_equal 'dir', entry.kind + assert_select 'tr.dir a[href=/projects/subproject1/repository/show/subversion_test]' + + assert_tag 'input', :attributes => {:name => 'rev'} + assert_tag 'a', :content => 'Statistics' + assert_tag 'a', :content => 'Atom' + assert_tag :tag => 'a', + :attributes => {:href => '/projects/subproject1/repository'}, + :content => 'root' + end + + def test_show_non_default + Repository::Subversion.create(:project => @project, + :url => self.class.subversion_repository_url, + :is_default => false, :identifier => 'svn') + + get :show, :id => PRJ_ID, :repository_id => 'svn' + assert_response :success + assert_template 'show' + assert_select 'tr.dir a[href=/projects/subproject1/repository/svn/show/subversion_test]' + # Repository menu should link to the main repo + assert_select '#main-menu a[href=/projects/subproject1/repository]' + end + + def test_browse_directory + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :show, :id => PRJ_ID, :path => repository_path_hash(['subversion_test'])[:param] + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal [ + '[folder_with_brackets]', 'folder', '.project', + 'helloworld.c', 'textfile.txt' + ], + assigns(:entries).collect(&:name) + entry = assigns(:entries).detect {|e| e.name == 'helloworld.c'} + assert_equal 'file', entry.kind + assert_equal 'subversion_test/helloworld.c', entry.path + assert_tag :a, :content => 'helloworld.c', :attributes => { :class => /text\-x\-c/ } + end + + def test_browse_at_given_revision + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :show, :id => PRJ_ID, :path => repository_path_hash(['subversion_test'])[:param], + :rev => 4 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal ['folder', '.project', 'helloworld.c', 'helloworld.rb', 'textfile.txt'], + assigns(:entries).collect(&:name) + end + + def test_file_changes + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :changes, :id => PRJ_ID, + :path => repository_path_hash(['subversion_test', 'folder', 'helloworld.rb'])[:param] + assert_response :success + assert_template 'changes' + + changesets = assigns(:changesets) + assert_not_nil changesets + assert_equal %w(6 3 2), changesets.collect(&:revision) + + # svn properties displayed with svn >= 1.5 only + if Redmine::Scm::Adapters::SubversionAdapter.client_version_above?([1, 5, 0]) + assert_not_nil assigns(:properties) + assert_equal 'native', assigns(:properties)['svn:eol-style'] + assert_tag :ul, + :child => { :tag => 'li', + :child => { :tag => 'b', :content => 'svn:eol-style' }, + :child => { :tag => 'span', :content => 'native' } } + end + end + + def test_directory_changes + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :changes, :id => PRJ_ID, + :path => repository_path_hash(['subversion_test', 'folder'])[:param] + assert_response :success + assert_template 'changes' + + changesets = assigns(:changesets) + assert_not_nil changesets + assert_equal %w(10 9 7 6 5 2), changesets.collect(&:revision) + end + + def test_entry + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['subversion_test', 'helloworld.c'])[:param] + assert_response :success + assert_template 'entry' + end + + def test_entry_should_send_if_too_big + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + # no files in the test repo is larger than 1KB... + with_settings :file_max_size_displayed => 0 do + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['subversion_test', 'helloworld.c'])[:param] + assert_response :success + assert_equal 'attachment; filename="helloworld.c"', + @response.headers['Content-Disposition'] + end + end + + def test_entry_should_send_images_inline + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['subversion_test', 'folder', 'subfolder', 'rubylogo.gif'])[:param] + assert_response :success + assert_equal 'inline; filename="rubylogo.gif"', response.headers['Content-Disposition'] + end + + def test_entry_at_given_revision + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['subversion_test', 'helloworld.rb'])[:param], + :rev => 2 + assert_response :success + assert_template 'entry' + # this line was removed in r3 and file was moved in r6 + assert_tag :tag => 'td', :attributes => { :class => /line-code/}, + :content => /Here's the code/ + end + + def test_entry_not_found + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['subversion_test', 'zzz.c'])[:param] + assert_tag :tag => 'p', :attributes => { :id => /errorExplanation/ }, + :content => /The entry or revision was not found in the repository/ + end + + def test_entry_download + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :raw, :id => PRJ_ID, + :path => repository_path_hash(['subversion_test', 'helloworld.c'])[:param] + assert_response :success + assert_equal 'attachment; filename="helloworld.c"', @response.headers['Content-Disposition'] + end + + def test_directory_entry + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['subversion_test', 'folder'])[:param] + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entry) + assert_equal 'folder', assigns(:entry).name + end + + # TODO: this test needs fixtures. + def test_revision + get :revision, :id => 1, :rev => 2 + assert_response :success + assert_template 'revision' + + assert_select 'ul' do + assert_select 'li' do + # link to the entry at rev 2 + assert_select 'a[href=?]', '/projects/ecookbook/repository/revisions/2/entry/test/some/path/in/the/repo', :text => 'repo' + # link to partial diff + assert_select 'a[href=?]', '/projects/ecookbook/repository/revisions/2/diff/test/some/path/in/the/repo' + end + end + end + + def test_invalid_revision + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :revision, :id => PRJ_ID, :rev => 'something_weird' + assert_response 404 + assert_error_tag :content => /was not found/ + end + + def test_invalid_revision_diff + get :diff, :id => PRJ_ID, :rev => '1', :rev_to => 'something_weird' + assert_response 404 + assert_error_tag :content => /was not found/ + end + + def test_empty_revision + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + ['', ' ', nil].each do |r| + get :revision, :id => PRJ_ID, :rev => r + assert_response 404 + assert_error_tag :content => /was not found/ + end + end + + # TODO: this test needs fixtures. + def test_revision_with_repository_pointing_to_a_subdirectory + r = Project.find(1).repository + # Changes repository url to a subdirectory + r.update_attribute :url, (r.url + '/test/some') + + get :revision, :id => 1, :rev => 2 + assert_response :success + assert_template 'revision' + + assert_select 'ul' do + assert_select 'li' do + # link to the entry at rev 2 + assert_select 'a[href=?]', '/projects/ecookbook/repository/revisions/2/entry/path/in/the/repo', :text => 'repo' + # link to partial diff + assert_select 'a[href=?]', '/projects/ecookbook/repository/revisions/2/diff/path/in/the/repo' + end + end + end + + def test_revision_diff + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + ['inline', 'sbs'].each do |dt| + get :diff, :id => PRJ_ID, :rev => 3, :type => dt + assert_response :success + assert_template 'diff' + assert_select 'h2', :text => /Revision 3/ + assert_select 'th.filename', :text => 'subversion_test/textfile.txt' + end + end + + def test_revision_diff_raw_format + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + + get :diff, :id => PRJ_ID, :rev => 3, :format => 'diff' + assert_response :success + assert_equal 'text/x-patch', @response.content_type + assert_equal 'Index: subversion_test/textfile.txt', @response.body.split(/\r?\n/).first + end + + def test_directory_diff + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + ['inline', 'sbs'].each do |dt| + get :diff, :id => PRJ_ID, :rev => 6, :rev_to => 2, + :path => repository_path_hash(['subversion_test', 'folder'])[:param], + :type => dt + assert_response :success + assert_template 'diff' + + diff = assigns(:diff) + assert_not_nil diff + # 2 files modified + assert_equal 2, Redmine::UnifiedDiff.new(diff).size + assert_tag :tag => 'h2', :content => /2:6/ + end + end + + def test_annotate + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :annotate, :id => PRJ_ID, + :path => repository_path_hash(['subversion_test', 'helloworld.c'])[:param] + assert_response :success + assert_template 'annotate' + + assert_select 'tr' do + assert_select 'th.line-num', :text => '1' + assert_select 'td.revision', :text => '4' + assert_select 'td.author', :text => 'jp' + assert_select 'td', :text => /stdio.h/ + end + # Same revision + assert_select 'tr' do + assert_select 'th.line-num', :text => '2' + assert_select 'td.revision', :text => '' + assert_select 'td.author', :text => '' + end + end + + def test_annotate_at_given_revision + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :annotate, :id => PRJ_ID, :rev => 8, + :path => repository_path_hash(['subversion_test', 'helloworld.c'])[:param] + assert_response :success + assert_template 'annotate' + assert_tag :tag => 'h2', :content => /@ 8/ + end + + def test_destroy_valid_repository + @request.session[:user_id] = 1 # admin + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + assert_equal NUM_REV, @repository.changesets.count + + assert_difference 'Repository.count', -1 do + delete :destroy, :id => @repository.id + end + assert_response 302 + @project.reload + assert_nil @project.repository + end + + def test_destroy_invalid_repository + @request.session[:user_id] = 1 # admin + @project.repository.destroy + @repository = Repository::Subversion.create!( + :project => @project, + :url => "file:///invalid") + @repository.fetch_changesets + assert_equal 0, @repository.changesets.count + + assert_difference 'Repository.count', -1 do + delete :destroy, :id => @repository.id + end + assert_response 302 + @project.reload + assert_nil @project.repository + end + else + puts "Subversion test repository NOT FOUND. Skipping functional tests !!!" + def test_fake; assert true end + end +end diff --git a/test/functional/roles_controller_test.rb b/test/functional/roles_controller_test.rb new file mode 100644 index 00000000..8bcbb2ea --- /dev/null +++ b/test/functional/roles_controller_test.rb @@ -0,0 +1,216 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class RolesControllerTest < ActionController::TestCase + fixtures :roles, :users, :members, :member_roles, :workflows, :trackers + + def setup + User.current = nil + @request.session[:user_id] = 1 # admin + end + + def test_index + get :index + assert_response :success + assert_template 'index' + + assert_not_nil assigns(:roles) + assert_equal Role.order('builtin, position').all, assigns(:roles) + + assert_tag :tag => 'a', :attributes => { :href => '/roles/1/edit' }, + :content => 'Manager' + end + + def test_new + get :new + assert_response :success + assert_template 'new' + end + + def test_new_with_copy + copy_from = Role.find(2) + + get :new, :copy => copy_from.id.to_s + assert_response :success + assert_template 'new' + + role = assigns(:role) + assert_equal copy_from.permissions, role.permissions + + assert_select 'form' do + # blank name + assert_select 'input[name=?][value=]', 'role[name]' + # edit_project permission checked + assert_select 'input[type=checkbox][name=?][value=edit_project][checked=checked]', 'role[permissions][]' + # add_project permission not checked + assert_select 'input[type=checkbox][name=?][value=add_project]', 'role[permissions][]' + assert_select 'input[type=checkbox][name=?][value=add_project][checked=checked]', 'role[permissions][]', 0 + # workflow copy selected + assert_select 'select[name=?]', 'copy_workflow_from' do + assert_select 'option[value=2][selected=selected]' + end + end + end + + def test_create_with_validaton_failure + post :create, :role => {:name => '', + :permissions => ['add_issues', 'edit_issues', 'log_time', ''], + :assignable => '0'} + + assert_response :success + assert_template 'new' + assert_tag :tag => 'div', :attributes => { :id => 'errorExplanation' } + end + + def test_create_without_workflow_copy + post :create, :role => {:name => 'RoleWithoutWorkflowCopy', + :permissions => ['add_issues', 'edit_issues', 'log_time', ''], + :assignable => '0'} + + assert_redirected_to '/roles' + role = Role.find_by_name('RoleWithoutWorkflowCopy') + assert_not_nil role + assert_equal [:add_issues, :edit_issues, :log_time], role.permissions + assert !role.assignable? + end + + def test_create_with_workflow_copy + post :create, :role => {:name => 'RoleWithWorkflowCopy', + :permissions => ['add_issues', 'edit_issues', 'log_time', ''], + :assignable => '0'}, + :copy_workflow_from => '1' + + assert_redirected_to '/roles' + role = Role.find_by_name('RoleWithWorkflowCopy') + assert_not_nil role + assert_equal Role.find(1).workflow_rules.size, role.workflow_rules.size + end + + def test_edit + get :edit, :id => 1 + assert_response :success + assert_template 'edit' + assert_equal Role.find(1), assigns(:role) + assert_select 'select[name=?]', 'role[issues_visibility]' + end + + def test_edit_anonymous + get :edit, :id => Role.anonymous.id + assert_response :success + assert_template 'edit' + assert_select 'select[name=?]', 'role[issues_visibility]', 0 + end + + def test_edit_invalid_should_respond_with_404 + get :edit, :id => 999 + assert_response 404 + end + + def test_update + put :update, :id => 1, + :role => {:name => 'Manager', + :permissions => ['edit_project', ''], + :assignable => '0'} + + assert_redirected_to '/roles' + role = Role.find(1) + assert_equal [:edit_project], role.permissions + end + + def test_update_with_failure + put :update, :id => 1, :role => {:name => ''} + assert_response :success + assert_template 'edit' + end + + def test_destroy + r = Role.create!(:name => 'ToBeDestroyed', :permissions => [:view_wiki_pages]) + + delete :destroy, :id => r + assert_redirected_to '/roles' + assert_nil Role.find_by_id(r.id) + end + + def test_destroy_role_in_use + delete :destroy, :id => 1 + assert_redirected_to '/roles' + assert_equal 'This role is in use and cannot be deleted.', flash[:error] + assert_not_nil Role.find_by_id(1) + end + + def test_get_permissions + get :permissions + assert_response :success + assert_template 'permissions' + + assert_not_nil assigns(:roles) + assert_equal Role.order('builtin, position').all, assigns(:roles) + + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'permissions[3][]', + :value => 'add_issues', + :checked => 'checked' } + + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'permissions[3][]', + :value => 'delete_issues', + :checked => nil } + end + + def test_post_permissions + post :permissions, :permissions => { '0' => '', '1' => ['edit_issues'], '3' => ['add_issues', 'delete_issues']} + assert_redirected_to '/roles' + + assert_equal [:edit_issues], Role.find(1).permissions + assert_equal [:add_issues, :delete_issues], Role.find(3).permissions + assert Role.find(2).permissions.empty? + end + + def test_clear_all_permissions + post :permissions, :permissions => { '0' => '' } + assert_redirected_to '/roles' + assert Role.find(1).permissions.empty? + end + + def test_move_highest + put :update, :id => 3, :role => {:move_to => 'highest'} + assert_redirected_to '/roles' + assert_equal 1, Role.find(3).position + end + + def test_move_higher + position = Role.find(3).position + put :update, :id => 3, :role => {:move_to => 'higher'} + assert_redirected_to '/roles' + assert_equal position - 1, Role.find(3).position + end + + def test_move_lower + position = Role.find(2).position + put :update, :id => 2, :role => {:move_to => 'lower'} + assert_redirected_to '/roles' + assert_equal position + 1, Role.find(2).position + end + + def test_move_lowest + put :update, :id => 2, :role => {:move_to => 'lowest'} + assert_redirected_to '/roles' + assert_equal Role.count, Role.find(2).position + end +end diff --git a/test/functional/search_controller_test.rb b/test/functional/search_controller_test.rb new file mode 100644 index 00000000..d292bbe7 --- /dev/null +++ b/test/functional/search_controller_test.rb @@ -0,0 +1,265 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class SearchControllerTest < ActionController::TestCase + fixtures :projects, :enabled_modules, :roles, :users, :members, :member_roles, + :issues, :trackers, :issue_statuses, :enumerations, + :custom_fields, :custom_values, + :repositories, :changesets + + def setup + User.current = nil + end + + def test_search_for_projects + get :index + assert_response :success + assert_template 'index' + + get :index, :q => "cook" + assert_response :success + assert_template 'index' + assert assigns(:results).include?(Project.find(1)) + end + + def test_search_all_projects + get :index, :q => 'recipe subproject commit', :all_words => '' + assert_response :success + assert_template 'index' + + assert assigns(:results).include?(Issue.find(2)) + assert assigns(:results).include?(Issue.find(5)) + assert assigns(:results).include?(Changeset.find(101)) + assert_tag :dt, :attributes => { :class => /issue/ }, + :child => { :tag => 'a', :content => /Add ingredients categories/ }, + :sibling => { :tag => 'dd', :content => /should be classified by categories/ } + + assert assigns(:results_by_type).is_a?(Hash) + assert_equal 5, assigns(:results_by_type)['changesets'] + assert_tag :a, :content => 'Changesets (5)' + end + + def test_search_issues + get :index, :q => 'issue', :issues => 1 + assert_response :success + assert_template 'index' + + assert_equal true, assigns(:all_words) + assert_equal false, assigns(:titles_only) + assert assigns(:results).include?(Issue.find(8)) + assert assigns(:results).include?(Issue.find(5)) + assert_tag :dt, :attributes => { :class => /issue closed/ }, + :child => { :tag => 'a', :content => /Closed/ } + end + + def test_search_issues_should_search_notes + Journal.create!(:journalized => Issue.find(2), :notes => 'Issue notes with searchkeyword') + + get :index, :q => 'searchkeyword', :issues => 1 + assert_response :success + assert_include Issue.find(2), assigns(:results) + end + + def test_search_issues_with_multiple_matches_in_journals_should_return_issue_once + Journal.create!(:journalized => Issue.find(2), :notes => 'Issue notes with searchkeyword') + Journal.create!(:journalized => Issue.find(2), :notes => 'Issue notes with searchkeyword') + + get :index, :q => 'searchkeyword', :issues => 1 + assert_response :success + assert_include Issue.find(2), assigns(:results) + assert_equal 1, assigns(:results).size + end + + def test_search_issues_should_search_private_notes_with_permission_only + Journal.create!(:journalized => Issue.find(2), :notes => 'Private notes with searchkeyword', :private_notes => true) + @request.session[:user_id] = 2 + + Role.find(1).add_permission! :view_private_notes + get :index, :q => 'searchkeyword', :issues => 1 + assert_response :success + assert_include Issue.find(2), assigns(:results) + + Role.find(1).remove_permission! :view_private_notes + get :index, :q => 'searchkeyword', :issues => 1 + assert_response :success + assert_not_include Issue.find(2), assigns(:results) + end + + def test_search_all_projects_with_scope_param + get :index, :q => 'issue', :scope => 'all' + assert_response :success + assert_template 'index' + assert assigns(:results).present? + end + + def test_search_my_projects + @request.session[:user_id] = 2 + get :index, :id => 1, :q => 'recipe subproject', :scope => 'my_projects', :all_words => '' + assert_response :success + assert_template 'index' + assert assigns(:results).include?(Issue.find(1)) + assert !assigns(:results).include?(Issue.find(5)) + end + + def test_search_my_projects_without_memberships + # anonymous user has no memberships + get :index, :id => 1, :q => 'recipe subproject', :scope => 'my_projects', :all_words => '' + assert_response :success + assert_template 'index' + assert assigns(:results).empty? + end + + def test_search_project_and_subprojects + get :index, :id => 1, :q => 'recipe subproject', :scope => 'subprojects', :all_words => '' + assert_response :success + assert_template 'index' + assert assigns(:results).include?(Issue.find(1)) + assert assigns(:results).include?(Issue.find(5)) + end + + def test_search_without_searchable_custom_fields + CustomField.update_all "searchable = #{ActiveRecord::Base.connection.quoted_false}" + + get :index, :id => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:project) + + get :index, :id => 1, :q => "can" + assert_response :success + assert_template 'index' + end + + def test_search_with_searchable_custom_fields + get :index, :id => 1, :q => "stringforcustomfield" + assert_response :success + results = assigns(:results) + assert_not_nil results + assert_equal 1, results.size + assert results.include?(Issue.find(7)) + end + + def test_search_all_words + # 'all words' is on by default + get :index, :id => 1, :q => 'recipe updating saving', :all_words => '1' + assert_equal true, assigns(:all_words) + results = assigns(:results) + assert_not_nil results + assert_equal 1, results.size + assert results.include?(Issue.find(3)) + end + + def test_search_one_of_the_words + get :index, :id => 1, :q => 'recipe updating saving', :all_words => '' + assert_equal false, assigns(:all_words) + results = assigns(:results) + assert_not_nil results + assert_equal 3, results.size + assert results.include?(Issue.find(3)) + end + + def test_search_titles_only_without_result + get :index, :id => 1, :q => 'recipe updating saving', :titles_only => '1' + results = assigns(:results) + assert_not_nil results + assert_equal 0, results.size + end + + def test_search_titles_only + get :index, :id => 1, :q => 'recipe', :titles_only => '1' + assert_equal true, assigns(:titles_only) + results = assigns(:results) + assert_not_nil results + assert_equal 2, results.size + end + + def test_search_content + Issue.update_all("description = 'This is a searchkeywordinthecontent'", "id=1") + + get :index, :id => 1, :q => 'searchkeywordinthecontent', :titles_only => '' + assert_equal false, assigns(:titles_only) + results = assigns(:results) + assert_not_nil results + assert_equal 1, results.size + end + + def test_search_with_offset + get :index, :q => 'coo', :offset => '20080806073000' + assert_response :success + results = assigns(:results) + assert results.any? + assert results.map(&:event_datetime).max < '20080806T073000'.to_time + end + + def test_search_previous_with_offset + get :index, :q => 'coo', :offset => '20080806073000', :previous => '1' + assert_response :success + results = assigns(:results) + assert results.any? + assert results.map(&:event_datetime).min >= '20080806T073000'.to_time + end + + def test_search_with_invalid_project_id + get :index, :id => 195, :q => 'recipe' + assert_response 404 + assert_nil assigns(:results) + end + + def test_quick_jump_to_issue + # issue of a public project + get :index, :q => "3" + assert_redirected_to '/issues/3' + + # issue of a private project + get :index, :q => "4" + assert_response :success + assert_template 'index' + end + + def test_large_integer + get :index, :q => '4615713488' + assert_response :success + assert_template 'index' + end + + def test_tokens_with_quotes + get :index, :id => 1, :q => '"good bye" hello "bye bye"' + assert_equal ["good bye", "hello", "bye bye"], assigns(:tokens) + end + + def test_results_should_be_escaped_once + assert Issue.find(1).update_attributes(:subject => ' escaped_once', :description => ' escaped_once') + get :index, :q => 'escaped_once' + assert_response :success + assert_select '#search-results' do + assert_select 'dt.issue a', :text => /<subject>/ + assert_select 'dd', :text => /<description>/ + end + end + + def test_keywords_should_be_highlighted + assert Issue.find(1).update_attributes(:subject => 'subject highlighted', :description => 'description highlighted') + get :index, :q => 'highlighted' + assert_response :success + assert_select '#search-results' do + assert_select 'dt.issue a span.highlight', :text => 'highlighted' + assert_select 'dd span.highlight', :text => 'highlighted' + end + end +end diff --git a/test/functional/sessions_test.rb b/test/functional/sessions_test.rb new file mode 100644 index 00000000..90aa24a1 --- /dev/null +++ b/test/functional/sessions_test.rb @@ -0,0 +1,117 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class SessionStartTest < ActionController::TestCase + tests AccountController + + fixtures :users + + def test_login_should_set_session_timestamps + post :login, :username => 'jsmith', :password => 'jsmith' + assert_response 302 + assert_equal 2, session[:user_id] + assert_not_nil session[:ctime] + assert_not_nil session[:atime] + end +end + +class SessionsTest < ActionController::TestCase + tests WelcomeController + + fixtures :users + + def test_atime_from_user_session_should_be_updated + created = 2.hours.ago.utc.to_i + get :index, {}, {:user_id => 2, :ctime => created, :atime => created} + assert_response :success + assert_equal created, session[:ctime] + assert_not_equal created, session[:atime] + assert session[:atime] > created + end + + def test_user_session_should_not_be_reset_if_lifetime_and_timeout_disabled + with_settings :session_lifetime => '0', :session_timeout => '0' do + get :index, {}, {:user_id => 2} + assert_response :success + end + end + + def test_user_session_without_ctime_should_be_reset_if_lifetime_enabled + with_settings :session_lifetime => '720' do + get :index, {}, {:user_id => 2} + assert_redirected_to '/login' + end + end + + def test_user_session_with_expired_ctime_should_be_reset_if_lifetime_enabled + with_settings :session_timeout => '720' do + get :index, {}, {:user_id => 2, :atime => 2.days.ago.utc.to_i} + assert_redirected_to '/login' + end + end + + def test_user_session_with_valid_ctime_should_not_be_reset_if_lifetime_enabled + with_settings :session_timeout => '720' do + get :index, {}, {:user_id => 2, :atime => 3.hours.ago.utc.to_i} + assert_response :success + end + end + + def test_user_session_without_atime_should_be_reset_if_timeout_enabled + with_settings :session_timeout => '60' do + get :index, {}, {:user_id => 2} + assert_redirected_to '/login' + end + end + + def test_user_session_with_expired_atime_should_be_reset_if_timeout_enabled + with_settings :session_timeout => '60' do + get :index, {}, {:user_id => 2, :atime => 4.hours.ago.utc.to_i} + assert_redirected_to '/login' + end + end + + def test_user_session_with_valid_atime_should_not_be_reset_if_timeout_enabled + with_settings :session_timeout => '60' do + get :index, {}, {:user_id => 2, :atime => 10.minutes.ago.utc.to_i} + assert_response :success + end + end + + def test_expired_user_session_should_be_restarted_if_autologin + with_settings :session_lifetime => '720', :session_timeout => '60', :autologin => 7 do + token = Token.create!(:user_id => 2, :action => 'autologin', :created_on => 1.day.ago) + @request.cookies['autologin'] = token.value + created = 2.hours.ago.utc.to_i + + get :index, {}, {:user_id => 2, :ctime => created, :atime => created} + assert_equal 2, session[:user_id] + assert_response :success + assert_not_equal created, session[:ctime] + assert session[:ctime] >= created + end + end + + def test_anonymous_session_should_not_be_reset + with_settings :session_lifetime => '720', :session_timeout => '60' do + get :index + assert_response :success + end + end +end diff --git a/test/functional/settings_controller_test.rb b/test/functional/settings_controller_test.rb new file mode 100644 index 00000000..4c49a327 --- /dev/null +++ b/test/functional/settings_controller_test.rb @@ -0,0 +1,131 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class SettingsControllerTest < ActionController::TestCase + fixtures :users + + def setup + User.current = nil + @request.session[:user_id] = 1 # admin + end + + def test_index + get :index + assert_response :success + assert_template 'edit' + end + + def test_get_edit + get :edit + assert_response :success + assert_template 'edit' + + assert_tag 'input', :attributes => {:name => 'settings[enabled_scm][]', :value => ''} + end + + def test_get_edit_should_preselect_default_issue_list_columns + with_settings :issue_list_default_columns => %w(tracker subject status updated_on) do + get :edit + assert_response :success + end + + assert_select 'select[id=selected_columns][name=?]', 'settings[issue_list_default_columns][]' do + assert_select 'option', 4 + assert_select 'option[value=tracker]', :text => 'Tracker' + assert_select 'option[value=subject]', :text => 'Subject' + assert_select 'option[value=status]', :text => 'Status' + assert_select 'option[value=updated_on]', :text => 'Updated' + end + + assert_select 'select[id=available_columns]' do + assert_select 'option[value=tracker]', 0 + assert_select 'option[value=priority]', :text => 'Priority' + end + end + + def test_get_edit_without_trackers_should_succeed + Tracker.delete_all + + get :edit + assert_response :success + end + + def test_post_edit_notifications + post :edit, :settings => {:mail_from => 'functional@test.foo', + :bcc_recipients => '0', + :notified_events => %w(issue_added issue_updated news_added), + :emails_footer => 'Test footer' + } + assert_redirected_to '/settings' + assert_equal 'functional@test.foo', Setting.mail_from + assert !Setting.bcc_recipients? + assert_equal %w(issue_added issue_updated news_added), Setting.notified_events + assert_equal 'Test footer', Setting.emails_footer + Setting.clear_cache + end + + def test_get_plugin_settings + Setting.stubs(:plugin_foo).returns({'sample_setting' => 'Plugin setting value'}) + ActionController::Base.append_view_path(File.join(Rails.root, "test/fixtures/plugins")) + Redmine::Plugin.register :foo do + settings :partial => "foo_plugin/foo_plugin_settings" + end + + get :plugin, :id => 'foo' + assert_response :success + assert_template 'plugin' + assert_tag 'form', :attributes => {:action => '/settings/plugin/foo'}, + :descendant => {:tag => 'input', :attributes => {:name => 'settings[sample_setting]', :value => 'Plugin setting value'}} + + Redmine::Plugin.clear + end + + def test_get_invalid_plugin_settings + get :plugin, :id => 'none' + assert_response 404 + end + + def test_get_non_configurable_plugin_settings + Redmine::Plugin.register(:foo) {} + + get :plugin, :id => 'foo' + assert_response 404 + + Redmine::Plugin.clear + end + + def test_post_plugin_settings + Setting.expects(:plugin_foo=).with({'sample_setting' => 'Value'}).returns(true) + Redmine::Plugin.register(:foo) do + settings :partial => 'not blank' # so that configurable? is true + end + + post :plugin, :id => 'foo', :settings => {'sample_setting' => 'Value'} + assert_redirected_to '/settings/plugin/foo' + end + + def test_post_non_configurable_plugin_settings + Redmine::Plugin.register(:foo) {} + + post :plugin, :id => 'foo', :settings => {'sample_setting' => 'Value'} + assert_response 404 + + Redmine::Plugin.clear + end +end diff --git a/test/functional/sys_controller_test.rb b/test/functional/sys_controller_test.rb new file mode 100644 index 00000000..7a113ae0 --- /dev/null +++ b/test/functional/sys_controller_test.rb @@ -0,0 +1,125 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class SysControllerTest < ActionController::TestCase + fixtures :projects, :repositories, :enabled_modules + + def setup + Setting.sys_api_enabled = '1' + Setting.enabled_scm = %w(Subversion Git) + end + + def teardown + Setting.clear_cache + end + + def test_projects_with_repository_enabled + get :projects + assert_response :success + assert_equal 'application/xml', @response.content_type + with_options :tag => 'projects' do |test| + test.assert_tag :children => { :count => Project.active.has_module(:repository).count } + test.assert_tag 'project', :child => {:tag => 'identifier', :sibling => {:tag => 'is-public'}} + end + assert_no_tag 'extra-info' + assert_no_tag 'extra_info' + end + + def test_create_project_repository + assert_nil Project.find(4).repository + + post :create_project_repository, :id => 4, + :vendor => 'Subversion', + :repository => { :url => 'file:///create/project/repository/subproject2'} + assert_response :created + assert_equal 'application/xml', @response.content_type + + r = Project.find(4).repository + assert r.is_a?(Repository::Subversion) + assert_equal 'file:///create/project/repository/subproject2', r.url + + assert_tag 'repository-subversion', + :child => { + :tag => 'id', :content => r.id.to_s, + :sibling => {:tag => 'url', :content => r.url} + } + assert_no_tag 'extra-info' + assert_no_tag 'extra_info' + end + + def test_create_already_existing + post :create_project_repository, :id => 1, + :vendor => 'Subversion', + :repository => { :url => 'file:///create/project/repository/subproject2'} + + assert_response :conflict + end + + def test_create_with_failure + post :create_project_repository, :id => 4, + :vendor => 'Subversion', + :repository => { :url => 'invalid url'} + + assert_response :unprocessable_entity + end + + def test_fetch_changesets + Repository::Subversion.any_instance.expects(:fetch_changesets).twice.returns(true) + get :fetch_changesets + assert_response :success + end + + def test_fetch_changesets_one_project_by_identifier + Repository::Subversion.any_instance.expects(:fetch_changesets).once.returns(true) + get :fetch_changesets, :id => 'ecookbook' + assert_response :success + end + + def test_fetch_changesets_one_project_by_id + Repository::Subversion.any_instance.expects(:fetch_changesets).once.returns(true) + get :fetch_changesets, :id => '1' + assert_response :success + end + + def test_fetch_changesets_unknown_project + get :fetch_changesets, :id => 'unknown' + assert_response 404 + end + + def test_disabled_ws_should_respond_with_403_error + with_settings :sys_api_enabled => '0' do + get :projects + assert_response 403 + end + end + + def test_api_key + with_settings :sys_api_key => 'my_secret_key' do + get :projects, :key => 'my_secret_key' + assert_response :success + end + end + + def test_wrong_key_should_respond_with_403_error + with_settings :sys_api_enabled => 'my_secret_key' do + get :projects, :key => 'wrong_key' + assert_response 403 + end + end +end diff --git a/test/functional/time_entry_reports_controller_test.rb b/test/functional/time_entry_reports_controller_test.rb new file mode 100644 index 00000000..751e8459 --- /dev/null +++ b/test/functional/time_entry_reports_controller_test.rb @@ -0,0 +1,372 @@ +# -*- coding: utf-8 -*- +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class TimeEntryReportsControllerTest < ActionController::TestCase + tests TimelogController + + fixtures :projects, :enabled_modules, :roles, :members, :member_roles, + :issues, :time_entries, :users, :trackers, :enumerations, + :issue_statuses, :custom_fields, :custom_values + + include Redmine::I18n + + def setup + Setting.default_language = "en" + end + + def test_report_at_project_level + get :report, :project_id => 'ecookbook' + assert_response :success + assert_template 'report' + assert_tag :form, + :attributes => {:action => "/projects/ecookbook/time_entries/report", :id => 'query_form'} + end + + def test_report_all_projects + get :report + assert_response :success + assert_template 'report' + assert_tag :form, + :attributes => {:action => "/time_entries/report", :id => 'query_form'} + end + + def test_report_all_projects_denied + r = Role.anonymous + r.permissions.delete(:view_time_entries) + r.permissions_will_change! + r.save + get :report + assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Ftime_entries%2Freport' + end + + def test_report_all_projects_one_criteria + get :report, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criteria => ['project'] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:report) + assert_equal "8.65", "%.2f" % assigns(:report).total_hours + end + + def test_report_all_time + get :report, :project_id => 1, :criteria => ['project', 'issue'] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:report) + assert_equal "162.90", "%.2f" % assigns(:report).total_hours + end + + def test_report_all_time_by_day + get :report, :project_id => 1, :criteria => ['project', 'issue'], :columns => 'day' + assert_response :success + assert_template 'report' + assert_not_nil assigns(:report) + assert_equal "162.90", "%.2f" % assigns(:report).total_hours + assert_tag :tag => 'th', :content => '2007-03-12' + end + + def test_report_one_criteria + get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criteria => ['project'] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:report) + assert_equal "8.65", "%.2f" % assigns(:report).total_hours + end + + def test_report_two_criteria + get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criteria => ["user", "activity"] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:report) + assert_equal "162.90", "%.2f" % assigns(:report).total_hours + end + + def test_report_custom_field_criteria_with_multiple_values + field = TimeEntryCustomField.create!(:name => 'multi', :field_format => 'list', :possible_values => ['value1', 'value2']) + entry = TimeEntry.create!(:project => Project.find(1), :hours => 1, :activity_id => 10, :user => User.find(2), :spent_on => Date.today) + CustomValue.create!(:customized => entry, :custom_field => field, :value => 'value1') + CustomValue.create!(:customized => entry, :custom_field => field, :value => 'value2') + + get :report, :project_id => 1, :columns => 'day', :criteria => ["cf_#{field.id}"] + assert_response :success + end + + def test_report_one_day + get :report, :project_id => 1, :columns => 'day', :from => "2007-03-23", :to => "2007-03-23", :criteria => ["user", "activity"] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:report) + assert_equal "4.25", "%.2f" % assigns(:report).total_hours + end + + def test_report_at_issue_level + get :report, :project_id => 1, :issue_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criteria => ["user", "activity"] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:report) + assert_equal "154.25", "%.2f" % assigns(:report).total_hours + assert_tag :form, + :attributes => {:action => "/projects/ecookbook/issues/1/time_entries/report", :id => 'query_form'} + end + + def test_report_by_week_should_use_commercial_year + TimeEntry.delete_all + TimeEntry.generate!(:hours => '2', :spent_on => '2009-12-25') # 2009-52 + TimeEntry.generate!(:hours => '4', :spent_on => '2009-12-31') # 2009-53 + TimeEntry.generate!(:hours => '8', :spent_on => '2010-01-01') # 2009-53 + TimeEntry.generate!(:hours => '16', :spent_on => '2010-01-05') # 2010-1 + + get :report, :columns => 'week', :from => "2009-12-25", :to => "2010-01-05", :criteria => ["project"] + assert_response :success + + assert_select '#time-report thead tr' do + assert_select 'th:nth-child(1)', :text => 'Project' + assert_select 'th:nth-child(2)', :text => '2009-52' + assert_select 'th:nth-child(3)', :text => '2009-53' + assert_select 'th:nth-child(4)', :text => '2010-1' + assert_select 'th:nth-child(5)', :text => 'Total time' + end + assert_select '#time-report tbody tr' do + assert_select 'td:nth-child(1)', :text => 'eCookbook' + assert_select 'td:nth-child(2)', :text => '2.00' + assert_select 'td:nth-child(3)', :text => '12.00' + assert_select 'td:nth-child(4)', :text => '16.00' + assert_select 'td:nth-child(5)', :text => '30.00' # Total + end + end + + def test_report_should_propose_association_custom_fields + get :report + assert_response :success + assert_template 'report' + + assert_select 'select[name=?]', 'criteria[]' do + assert_select 'option[value=cf_1]', {:text => 'Database'}, 'Issue custom field not found' + assert_select 'option[value=cf_3]', {:text => 'Development status'}, 'Project custom field not found' + assert_select 'option[value=cf_7]', {:text => 'Billable'}, 'TimeEntryActivity custom field not found' + end + end + + def test_report_with_association_custom_fields + get :report, :criteria => ['cf_1', 'cf_3', 'cf_7'] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:report) + assert_equal 3, assigns(:report).criteria.size + assert_equal "162.90", "%.2f" % assigns(:report).total_hours + + # Custom fields columns + assert_select 'th', :text => 'Database' + assert_select 'th', :text => 'Development status' + assert_select 'th', :text => 'Billable' + + # Custom field row + assert_select 'tr' do + assert_select 'td', :text => 'MySQL' + assert_select 'td.hours', :text => '1.00' + end + end + + def test_report_one_criteria_no_result + get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criteria => ['project'] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:report) + assert_equal "0.00", "%.2f" % assigns(:report).total_hours + end + + def test_report_status_criterion + get :report, :project_id => 1, :criteria => ['status'] + assert_response :success + assert_template 'report' + assert_tag :tag => 'th', :content => 'Status' + assert_tag :tag => 'td', :content => 'New' + end + + def test_report_all_projects_csv_export + get :report, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30", + :criteria => ["project", "user", "activity"], :format => "csv" + assert_response :success + assert_equal 'text/csv; header=present', @response.content_type + lines = @response.body.chomp.split("\n") + # Headers + assert_equal 'Project,User,Activity,2007-3,2007-4,Total time', lines.first + # Total row + assert_equal 'Total time,"","",154.25,8.65,162.90', lines.last + end + + def test_report_csv_export + get :report, :project_id => 1, :columns => 'month', + :from => "2007-01-01", :to => "2007-06-30", + :criteria => ["project", "user", "activity"], :format => "csv" + assert_response :success + assert_equal 'text/csv; header=present', @response.content_type + lines = @response.body.chomp.split("\n") + # Headers + assert_equal 'Project,User,Activity,2007-3,2007-4,Total time', lines.first + # Total row + assert_equal 'Total time,"","",154.25,8.65,162.90', lines.last + end + + def test_csv_big_5 + Setting.default_language = "zh-TW" + str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88" + str_big5 = "\xa4@\xa4\xeb" + if str_utf8.respond_to?(:force_encoding) + str_utf8.force_encoding('UTF-8') + str_big5.force_encoding('Big5') + end + user = User.find_by_id(3) + user.firstname = str_utf8 + user.lastname = "test-lastname" + assert user.save + comments = "test_csv_big_5" + te1 = TimeEntry.create(:spent_on => '2011-11-11', + :hours => 7.3, + :project => Project.find(1), + :user => user, + :activity => TimeEntryActivity.find_by_name('Design'), + :comments => comments) + + te2 = TimeEntry.find_by_comments(comments) + assert_not_nil te2 + assert_equal 7.3, te2.hours + assert_equal 3, te2.user_id + + get :report, :project_id => 1, :columns => 'day', + :from => "2011-11-11", :to => "2011-11-11", + :criteria => ["user"], :format => "csv" + assert_response :success + assert_equal 'text/csv; header=present', @response.content_type + lines = @response.body.chomp.split("\n") + # Headers + s1 = "\xa5\xce\xa4\xe1,2011-11-11,\xc1`\xadp" + s2 = "\xc1`\xadp" + if s1.respond_to?(:force_encoding) + s1.force_encoding('Big5') + s2.force_encoding('Big5') + end + assert_equal s1, lines.first + # Total row + assert_equal "#{str_big5} #{user.lastname},7.30,7.30", lines[1] + assert_equal "#{s2},7.30,7.30", lines[2] + + str_tw = "Traditional Chinese (\xe7\xb9\x81\xe9\xab\x94\xe4\xb8\xad\xe6\x96\x87)" + if str_tw.respond_to?(:force_encoding) + str_tw.force_encoding('UTF-8') + end + assert_equal str_tw, l(:general_lang_name) + assert_equal 'Big5', l(:general_csv_encoding) + assert_equal ',', l(:general_csv_separator) + assert_equal '.', l(:general_csv_decimal_separator) + end + + def test_csv_cannot_convert_should_be_replaced_big_5 + Setting.default_language = "zh-TW" + str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85" + if str_utf8.respond_to?(:force_encoding) + str_utf8.force_encoding('UTF-8') + end + user = User.find_by_id(3) + user.firstname = str_utf8 + user.lastname = "test-lastname" + assert user.save + comments = "test_replaced" + te1 = TimeEntry.create(:spent_on => '2011-11-11', + :hours => 7.3, + :project => Project.find(1), + :user => user, + :activity => TimeEntryActivity.find_by_name('Design'), + :comments => comments) + + te2 = TimeEntry.find_by_comments(comments) + assert_not_nil te2 + assert_equal 7.3, te2.hours + assert_equal 3, te2.user_id + + get :report, :project_id => 1, :columns => 'day', + :from => "2011-11-11", :to => "2011-11-11", + :criteria => ["user"], :format => "csv" + assert_response :success + assert_equal 'text/csv; header=present', @response.content_type + lines = @response.body.chomp.split("\n") + # Headers + s1 = "\xa5\xce\xa4\xe1,2011-11-11,\xc1`\xadp" + if s1.respond_to?(:force_encoding) + s1.force_encoding('Big5') + end + assert_equal s1, lines.first + # Total row + s2 = "" + if s2.respond_to?(:force_encoding) + s2 = "\xa5H?" + s2.force_encoding('Big5') + elsif RUBY_PLATFORM == 'java' + s2 = "??" + else + s2 = "\xa5H???" + end + assert_equal "#{s2} #{user.lastname},7.30,7.30", lines[1] + end + + def test_csv_fr + with_settings :default_language => "fr" do + str1 = "test_csv_fr" + user = User.find_by_id(3) + te1 = TimeEntry.create(:spent_on => '2011-11-11', + :hours => 7.3, + :project => Project.find(1), + :user => user, + :activity => TimeEntryActivity.find_by_name('Design'), + :comments => str1) + + te2 = TimeEntry.find_by_comments(str1) + assert_not_nil te2 + assert_equal 7.3, te2.hours + assert_equal 3, te2.user_id + + get :report, :project_id => 1, :columns => 'day', + :from => "2011-11-11", :to => "2011-11-11", + :criteria => ["user"], :format => "csv" + assert_response :success + assert_equal 'text/csv; header=present', @response.content_type + lines = @response.body.chomp.split("\n") + # Headers + s1 = "Utilisateur;2011-11-11;Temps total" + s2 = "Temps total" + if s1.respond_to?(:force_encoding) + s1.force_encoding('ISO-8859-1') + s2.force_encoding('ISO-8859-1') + end + assert_equal s1, lines.first + # Total row + assert_equal "#{user.firstname} #{user.lastname};7,30;7,30", lines[1] + assert_equal "#{s2};7,30;7,30", lines[2] + + str_fr = "Fran\xc3\xa7ais" + if str_fr.respond_to?(:force_encoding) + str_fr.force_encoding('UTF-8') + end + assert_equal str_fr, l(:general_lang_name) + assert_equal 'ISO-8859-1', l(:general_csv_encoding) + assert_equal ';', l(:general_csv_separator) + assert_equal ',', l(:general_csv_decimal_separator) + end + end +end diff --git a/test/functional/timelog_controller_test.rb b/test/functional/timelog_controller_test.rb new file mode 100644 index 00000000..a5bd2046 --- /dev/null +++ b/test/functional/timelog_controller_test.rb @@ -0,0 +1,604 @@ +# -*- coding: utf-8 -*- +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class TimelogControllerTest < ActionController::TestCase + fixtures :projects, :enabled_modules, :roles, :members, + :member_roles, :issues, :time_entries, :users, + :trackers, :enumerations, :issue_statuses, + :custom_fields, :custom_values, + :projects_trackers, :custom_fields_trackers, + :custom_fields_projects + + include Redmine::I18n + + def test_new_with_project_id + @request.session[:user_id] = 3 + get :new, :project_id => 1 + assert_response :success + assert_template 'new' + assert_select 'select[name=?]', 'time_entry[project_id]', 0 + assert_select 'input[name=?][value=1][type=hidden]', 'time_entry[project_id]' + end + + def test_new_with_issue_id + @request.session[:user_id] = 3 + get :new, :issue_id => 2 + assert_response :success + assert_template 'new' + assert_select 'select[name=?]', 'time_entry[project_id]', 0 + assert_select 'input[name=?][value=1][type=hidden]', 'time_entry[project_id]' + end + + def test_new_without_project + @request.session[:user_id] = 3 + get :new + assert_response :success + assert_template 'new' + assert_select 'select[name=?]', 'time_entry[project_id]' + assert_select 'input[name=?]', 'time_entry[project_id]', 0 + end + + def test_new_without_project_should_prefill_the_form + @request.session[:user_id] = 3 + get :new, :time_entry => {:project_id => '1'} + assert_response :success + assert_template 'new' + assert_select 'select[name=?]', 'time_entry[project_id]' do + assert_select 'option[value=1][selected=selected]' + end + assert_select 'input[name=?]', 'time_entry[project_id]', 0 + end + + def test_new_without_project_should_deny_without_permission + Role.all.each {|role| role.remove_permission! :log_time} + @request.session[:user_id] = 3 + + get :new + assert_response 403 + end + + def test_new_should_select_default_activity + @request.session[:user_id] = 3 + get :new, :project_id => 1 + assert_response :success + assert_select 'select[name=?]', 'time_entry[activity_id]' do + assert_select 'option[selected=selected]', :text => 'Development' + end + end + + def test_new_should_only_show_active_time_entry_activities + @request.session[:user_id] = 3 + get :new, :project_id => 1 + assert_response :success + assert_no_tag 'option', :content => 'Inactive Activity' + end + + def test_get_edit_existing_time + @request.session[:user_id] = 2 + get :edit, :id => 2, :project_id => nil + assert_response :success + assert_template 'edit' + # Default activity selected + assert_tag :tag => 'form', :attributes => { :action => '/projects/ecookbook/time_entries/2' } + end + + def test_get_edit_with_an_existing_time_entry_with_inactive_activity + te = TimeEntry.find(1) + te.activity = TimeEntryActivity.find_by_name("Inactive Activity") + te.save! + + @request.session[:user_id] = 1 + get :edit, :project_id => 1, :id => 1 + assert_response :success + assert_template 'edit' + # Blank option since nothing is pre-selected + assert_tag :tag => 'option', :content => '--- Please select ---' + end + + def test_post_create + # TODO: should POST to issues’ time log instead of project. change form + # and routing + @request.session[:user_id] = 3 + post :create, :project_id => 1, + :time_entry => {:comments => 'Some work on TimelogControllerTest', + # Not the default activity + :activity_id => '11', + :spent_on => '2008-03-14', + :issue_id => '1', + :hours => '7.3'} + assert_redirected_to :action => 'index', :project_id => 'ecookbook' + + i = Issue.find(1) + t = TimeEntry.find_by_comments('Some work on TimelogControllerTest') + assert_not_nil t + assert_equal 11, t.activity_id + assert_equal 7.3, t.hours + assert_equal 3, t.user_id + assert_equal i, t.issue + assert_equal i.project, t.project + end + + def test_post_create_with_blank_issue + # TODO: should POST to issues’ time log instead of project. change form + # and routing + @request.session[:user_id] = 3 + post :create, :project_id => 1, + :time_entry => {:comments => 'Some work on TimelogControllerTest', + # Not the default activity + :activity_id => '11', + :issue_id => '', + :spent_on => '2008-03-14', + :hours => '7.3'} + assert_redirected_to :action => 'index', :project_id => 'ecookbook' + + t = TimeEntry.find_by_comments('Some work on TimelogControllerTest') + assert_not_nil t + assert_equal 11, t.activity_id + assert_equal 7.3, t.hours + assert_equal 3, t.user_id + end + + def test_create_and_continue + @request.session[:user_id] = 2 + post :create, :project_id => 1, + :time_entry => {:activity_id => '11', + :issue_id => '', + :spent_on => '2008-03-14', + :hours => '7.3'}, + :continue => '1' + assert_redirected_to '/projects/ecookbook/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=' + end + + def test_create_and_continue_with_issue_id + @request.session[:user_id] = 2 + post :create, :project_id => 1, + :time_entry => {:activity_id => '11', + :issue_id => '1', + :spent_on => '2008-03-14', + :hours => '7.3'}, + :continue => '1' + assert_redirected_to '/projects/ecookbook/issues/1/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=1' + end + + def test_create_and_continue_without_project + @request.session[:user_id] = 2 + post :create, :time_entry => {:project_id => '1', + :activity_id => '11', + :issue_id => '', + :spent_on => '2008-03-14', + :hours => '7.3'}, + :continue => '1' + + assert_redirected_to '/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=&time_entry%5Bproject_id%5D=1' + end + + def test_create_without_log_time_permission_should_be_denied + @request.session[:user_id] = 2 + Role.find_by_name('Manager').remove_permission! :log_time + post :create, :project_id => 1, + :time_entry => {:activity_id => '11', + :issue_id => '', + :spent_on => '2008-03-14', + :hours => '7.3'} + + assert_response 403 + end + + def test_create_with_failure + @request.session[:user_id] = 2 + post :create, :project_id => 1, + :time_entry => {:activity_id => '', + :issue_id => '', + :spent_on => '2008-03-14', + :hours => '7.3'} + + assert_response :success + assert_template 'new' + end + + def test_create_without_project + @request.session[:user_id] = 2 + assert_difference 'TimeEntry.count' do + post :create, :time_entry => {:project_id => '1', + :activity_id => '11', + :issue_id => '', + :spent_on => '2008-03-14', + :hours => '7.3'} + end + + assert_redirected_to '/projects/ecookbook/time_entries' + time_entry = TimeEntry.first(:order => 'id DESC') + assert_equal 1, time_entry.project_id + end + + def test_create_without_project_should_fail_with_issue_not_inside_project + @request.session[:user_id] = 2 + assert_no_difference 'TimeEntry.count' do + post :create, :time_entry => {:project_id => '1', + :activity_id => '11', + :issue_id => '5', + :spent_on => '2008-03-14', + :hours => '7.3'} + end + + assert_response :success + assert assigns(:time_entry).errors[:issue_id].present? + end + + def test_create_without_project_should_deny_without_permission + @request.session[:user_id] = 2 + Project.find(3).disable_module!(:time_tracking) + + assert_no_difference 'TimeEntry.count' do + post :create, :time_entry => {:project_id => '3', + :activity_id => '11', + :issue_id => '', + :spent_on => '2008-03-14', + :hours => '7.3'} + end + + assert_response 403 + end + + def test_create_without_project_with_failure + @request.session[:user_id] = 2 + assert_no_difference 'TimeEntry.count' do + post :create, :time_entry => {:project_id => '1', + :activity_id => '11', + :issue_id => '', + :spent_on => '2008-03-14', + :hours => ''} + end + + assert_response :success + assert_tag 'select', :attributes => {:name => 'time_entry[project_id]'}, + :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}} + end + + def test_update + entry = TimeEntry.find(1) + assert_equal 1, entry.issue_id + assert_equal 2, entry.user_id + + @request.session[:user_id] = 1 + put :update, :id => 1, + :time_entry => {:issue_id => '2', + :hours => '8'} + assert_redirected_to :action => 'index', :project_id => 'ecookbook' + entry.reload + + assert_equal 8, entry.hours + assert_equal 2, entry.issue_id + assert_equal 2, entry.user_id + end + + def test_get_bulk_edit + @request.session[:user_id] = 2 + get :bulk_edit, :ids => [1, 2] + assert_response :success + assert_template 'bulk_edit' + + assert_select 'ul#bulk-selection' do + assert_select 'li', 2 + assert_select 'li a', :text => '03/23/2007 - eCookbook: 4.25 hours' + end + + assert_select 'form#bulk_edit_form[action=?]', '/time_entries/bulk_update' do + # System wide custom field + assert_select 'select[name=?]', 'time_entry[custom_field_values][10]' + + # Activities + assert_select 'select[name=?]', 'time_entry[activity_id]' do + assert_select 'option[value=]', :text => '(No change)' + assert_select 'option[value=9]', :text => 'Design' + end + end + end + + def test_get_bulk_edit_on_different_projects + @request.session[:user_id] = 2 + get :bulk_edit, :ids => [1, 2, 6] + assert_response :success + assert_template 'bulk_edit' + end + + def test_bulk_update + @request.session[:user_id] = 2 + # update time entry activity + post :bulk_update, :ids => [1, 2], :time_entry => { :activity_id => 9} + + assert_response 302 + # check that the issues were updated + assert_equal [9, 9], TimeEntry.find_all_by_id([1, 2]).collect {|i| i.activity_id} + end + + def test_bulk_update_with_failure + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1, 2], :time_entry => { :hours => 'A'} + + assert_response 302 + assert_match /Failed to save 2 time entrie/, flash[:error] + end + + def test_bulk_update_on_different_projects + @request.session[:user_id] = 2 + # makes user a manager on the other project + Member.create!(:user_id => 2, :project_id => 3, :role_ids => [1]) + + # update time entry activity + post :bulk_update, :ids => [1, 2, 4], :time_entry => { :activity_id => 9 } + + assert_response 302 + # check that the issues were updated + assert_equal [9, 9, 9], TimeEntry.find_all_by_id([1, 2, 4]).collect {|i| i.activity_id} + end + + def test_bulk_update_on_different_projects_without_rights + @request.session[:user_id] = 3 + user = User.find(3) + action = { :controller => "timelog", :action => "bulk_update" } + assert user.allowed_to?(action, TimeEntry.find(1).project) + assert ! user.allowed_to?(action, TimeEntry.find(5).project) + post :bulk_update, :ids => [1, 5], :time_entry => { :activity_id => 9 } + assert_response 403 + end + + def test_bulk_update_custom_field + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1, 2], :time_entry => { :custom_field_values => {'10' => '0'} } + + assert_response 302 + assert_equal ["0", "0"], TimeEntry.find_all_by_id([1, 2]).collect {|i| i.custom_value_for(10).value} + end + + def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1,2], :back_url => '/time_entries' + + assert_response :redirect + assert_redirected_to '/time_entries' + end + + def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1,2], :back_url => 'http://google.com' + + assert_response :redirect + assert_redirected_to :controller => 'timelog', :action => 'index', :project_id => Project.find(1).identifier + end + + def test_post_bulk_update_without_edit_permission_should_be_denied + @request.session[:user_id] = 2 + Role.find_by_name('Manager').remove_permission! :edit_time_entries + post :bulk_update, :ids => [1,2] + + assert_response 403 + end + + def test_destroy + @request.session[:user_id] = 2 + delete :destroy, :id => 1 + assert_redirected_to :action => 'index', :project_id => 'ecookbook' + assert_equal I18n.t(:notice_successful_delete), flash[:notice] + assert_nil TimeEntry.find_by_id(1) + end + + def test_destroy_should_fail + # simulate that this fails (e.g. due to a plugin), see #5700 + TimeEntry.any_instance.expects(:destroy).returns(false) + + @request.session[:user_id] = 2 + delete :destroy, :id => 1 + assert_redirected_to :action => 'index', :project_id => 'ecookbook' + assert_equal I18n.t(:notice_unable_delete_time_entry), flash[:error] + assert_not_nil TimeEntry.find_by_id(1) + end + + def test_index_all_projects + get :index + assert_response :success + assert_template 'index' + assert_not_nil assigns(:total_hours) + assert_equal "162.90", "%.2f" % assigns(:total_hours) + assert_tag :form, + :attributes => {:action => "/time_entries", :id => 'query_form'} + end + + def test_index_all_projects_should_show_log_time_link + @request.session[:user_id] = 2 + get :index + assert_response :success + assert_template 'index' + assert_tag 'a', :attributes => {:href => '/time_entries/new'}, :content => /Log time/ + end + + def test_index_at_project_level + get :index, :project_id => 'ecookbook' + assert_response :success + assert_template 'index' + assert_not_nil assigns(:entries) + assert_equal 4, assigns(:entries).size + # project and subproject + assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort + assert_not_nil assigns(:total_hours) + assert_equal "162.90", "%.2f" % assigns(:total_hours) + assert_tag :form, + :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'} + end + + def test_index_at_project_level_with_date_range + get :index, :project_id => 'ecookbook', + :f => ['spent_on'], + :op => {'spent_on' => '><'}, + :v => {'spent_on' => ['2007-03-20', '2007-04-30']} + assert_response :success + assert_template 'index' + assert_not_nil assigns(:entries) + assert_equal 3, assigns(:entries).size + assert_not_nil assigns(:total_hours) + assert_equal "12.90", "%.2f" % assigns(:total_hours) + assert_tag :form, + :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'} + end + + def test_index_at_project_level_with_date_range_using_from_and_to_params + get :index, :project_id => 'ecookbook', :from => '2007-03-20', :to => '2007-04-30' + assert_response :success + assert_template 'index' + assert_not_nil assigns(:entries) + assert_equal 3, assigns(:entries).size + assert_not_nil assigns(:total_hours) + assert_equal "12.90", "%.2f" % assigns(:total_hours) + assert_tag :form, + :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'} + end + + def test_index_at_project_level_with_period + get :index, :project_id => 'ecookbook', + :f => ['spent_on'], + :op => {'spent_on' => '>t-'}, + :v => {'spent_on' => ['7']} + assert_response :success + assert_template 'index' + assert_not_nil assigns(:entries) + assert_not_nil assigns(:total_hours) + assert_tag :form, + :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'} + end + + def test_index_at_issue_level + get :index, :issue_id => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:entries) + assert_equal 2, assigns(:entries).size + assert_not_nil assigns(:total_hours) + assert_equal 154.25, assigns(:total_hours) + # display all time + assert_nil assigns(:from) + assert_nil assigns(:to) + # TODO: remove /projects/:project_id/issues/:issue_id/time_entries routes + # to use /issues/:issue_id/time_entries + assert_tag :form, + :attributes => {:action => "/projects/ecookbook/issues/1/time_entries", :id => 'query_form'} + end + + def test_index_should_sort_by_spent_on_and_created_on + t1 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:00:00', :activity_id => 10) + t2 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:05:00', :activity_id => 10) + t3 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-15', :created_on => '2012-06-16 20:10:00', :activity_id => 10) + + get :index, :project_id => 1, + :f => ['spent_on'], + :op => {'spent_on' => '><'}, + :v => {'spent_on' => ['2012-06-15', '2012-06-16']} + assert_response :success + assert_equal [t2, t1, t3], assigns(:entries) + + get :index, :project_id => 1, + :f => ['spent_on'], + :op => {'spent_on' => '><'}, + :v => {'spent_on' => ['2012-06-15', '2012-06-16']}, + :sort => 'spent_on' + assert_response :success + assert_equal [t3, t1, t2], assigns(:entries) + end + + def test_index_with_filter_on_issue_custom_field + issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {2 => 'filter_on_issue_custom_field'}) + entry = TimeEntry.generate!(:issue => issue, :hours => 2.5) + + get :index, :f => ['issue.cf_2'], :op => {'issue.cf_2' => '='}, :v => {'issue.cf_2' => ['filter_on_issue_custom_field']} + assert_response :success + assert_equal [entry], assigns(:entries) + end + + def test_index_with_issue_custom_field_column + issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {2 => 'filter_on_issue_custom_field'}) + entry = TimeEntry.generate!(:issue => issue, :hours => 2.5) + + get :index, :c => %w(project spent_on issue comments hours issue.cf_2) + assert_response :success + assert_include :'issue.cf_2', assigns(:query).column_names + assert_select 'td.issue_cf_2', :text => 'filter_on_issue_custom_field' + end + + def test_index_atom_feed + get :index, :project_id => 1, :format => 'atom' + assert_response :success + assert_equal 'application/atom+xml', @response.content_type + assert_not_nil assigns(:items) + assert assigns(:items).first.is_a?(TimeEntry) + end + + def test_index_at_project_level_should_include_csv_export_dialog + get :index, :project_id => 'ecookbook', + :f => ['spent_on'], + :op => {'spent_on' => '>='}, + :v => {'spent_on' => ['2007-04-01']}, + :c => ['spent_on', 'user'] + assert_response :success + + assert_select '#csv-export-options' do + assert_select 'form[action=?][method=get]', '/projects/ecookbook/time_entries.csv' do + # filter + assert_select 'input[name=?][value=?]', 'f[]', 'spent_on' + assert_select 'input[name=?][value=?]', 'op[spent_on]', '>=' + assert_select 'input[name=?][value=?]', 'v[spent_on][]', '2007-04-01' + # columns + assert_select 'input[name=?][value=?]', 'c[]', 'spent_on' + assert_select 'input[name=?][value=?]', 'c[]', 'user' + assert_select 'input[name=?]', 'c[]', 2 + end + end + end + + def test_index_cross_project_should_include_csv_export_dialog + get :index + assert_response :success + + assert_select '#csv-export-options' do + assert_select 'form[action=?][method=get]', '/time_entries.csv' + end + end + + def test_index_at_issue_level_should_include_csv_export_dialog + get :index, :project_id => 'ecookbook', :issue_id => 3 + assert_response :success + + assert_select '#csv-export-options' do + assert_select 'form[action=?][method=get]', '/projects/ecookbook/issues/3/time_entries.csv' + end + end + + def test_index_csv_all_projects + Setting.date_format = '%m/%d/%Y' + get :index, :format => 'csv' + assert_response :success + assert_equal 'text/csv; header=present', response.content_type + end + + def test_index_csv + Setting.date_format = '%m/%d/%Y' + get :index, :project_id => 1, :format => 'csv' + assert_response :success + assert_equal 'text/csv; header=present', response.content_type + end +end diff --git a/test/functional/trackers_controller_test.rb b/test/functional/trackers_controller_test.rb new file mode 100644 index 00000000..8038d681 --- /dev/null +++ b/test/functional/trackers_controller_test.rb @@ -0,0 +1,211 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class TrackersControllerTest < ActionController::TestCase + fixtures :trackers, :projects, :projects_trackers, :users, :issues, :custom_fields + + def setup + User.current = nil + @request.session[:user_id] = 1 # admin + end + + def test_index + get :index + assert_response :success + assert_template 'index' + end + + def test_index_by_anonymous_should_redirect_to_login_form + @request.session[:user_id] = nil + get :index + assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Ftrackers' + end + + def test_index_by_user_should_respond_with_406 + @request.session[:user_id] = 2 + get :index + assert_response 406 + end + + def test_new + get :new + assert_response :success + assert_template 'new' + end + + def test_create + assert_difference 'Tracker.count' do + post :create, :tracker => { :name => 'New tracker', :project_ids => ['1', '', ''], :custom_field_ids => ['1', '6', ''] } + end + assert_redirected_to :action => 'index' + tracker = Tracker.first(:order => 'id DESC') + assert_equal 'New tracker', tracker.name + assert_equal [1], tracker.project_ids.sort + assert_equal Tracker::CORE_FIELDS, tracker.core_fields + assert_equal [1, 6], tracker.custom_field_ids.sort + assert_equal 0, tracker.workflow_rules.count + end + + def create_with_disabled_core_fields + assert_difference 'Tracker.count' do + post :create, :tracker => { :name => 'New tracker', :core_fields => ['assigned_to_id', 'fixed_version_id', ''] } + end + assert_redirected_to :action => 'index' + tracker = Tracker.first(:order => 'id DESC') + assert_equal 'New tracker', tracker.name + assert_equal %w(assigned_to_id fixed_version_id), tracker.core_fields + end + + def test_create_new_with_workflow_copy + assert_difference 'Tracker.count' do + post :create, :tracker => { :name => 'New tracker' }, :copy_workflow_from => 1 + end + assert_redirected_to :action => 'index' + tracker = Tracker.find_by_name('New tracker') + assert_equal 0, tracker.projects.count + assert_equal Tracker.find(1).workflow_rules.count, tracker.workflow_rules.count + end + + def test_create_with_failure + assert_no_difference 'Tracker.count' do + post :create, :tracker => { :name => '', :project_ids => ['1', '', ''], :custom_field_ids => ['1', '6', ''] } + end + assert_response :success + assert_template 'new' + assert_error_tag :content => /name can't be blank/i + end + + def test_edit + Tracker.find(1).project_ids = [1, 3] + + get :edit, :id => 1 + assert_response :success + assert_template 'edit' + + assert_tag :input, :attributes => { :name => 'tracker[project_ids][]', + :value => '1', + :checked => 'checked' } + + assert_tag :input, :attributes => { :name => 'tracker[project_ids][]', + :value => '2', + :checked => nil } + + assert_tag :input, :attributes => { :name => 'tracker[project_ids][]', + :value => '', + :type => 'hidden'} + end + + def test_edit_should_check_core_fields + tracker = Tracker.find(1) + tracker.core_fields = %w(assigned_to_id fixed_version_id) + tracker.save! + + get :edit, :id => 1 + assert_response :success + assert_template 'edit' + + assert_select 'input[name=?][value=assigned_to_id][checked=checked]', 'tracker[core_fields][]' + assert_select 'input[name=?][value=fixed_version_id][checked=checked]', 'tracker[core_fields][]' + + assert_select 'input[name=?][value=category_id]', 'tracker[core_fields][]' + assert_select 'input[name=?][value=category_id][checked=checked]', 'tracker[core_fields][]', 0 + + assert_select 'input[name=?][value=][type=hidden]', 'tracker[core_fields][]' + end + + def test_update + put :update, :id => 1, :tracker => { :name => 'Renamed', + :project_ids => ['1', '2', ''] } + assert_redirected_to :action => 'index' + assert_equal [1, 2], Tracker.find(1).project_ids.sort + end + + def test_update_without_projects + put :update, :id => 1, :tracker => { :name => 'Renamed', + :project_ids => [''] } + assert_redirected_to :action => 'index' + assert Tracker.find(1).project_ids.empty? + end + + def test_update_without_core_fields + put :update, :id => 1, :tracker => { :name => 'Renamed', :core_fields => [''] } + assert_redirected_to :action => 'index' + assert Tracker.find(1).core_fields.empty? + end + + def test_update_with_failure + put :update, :id => 1, :tracker => { :name => '' } + assert_response :success + assert_template 'edit' + assert_error_tag :content => /name can't be blank/i + end + + def test_move_lower + tracker = Tracker.find_by_position(1) + put :update, :id => 1, :tracker => { :move_to => 'lower' } + assert_equal 2, tracker.reload.position + end + + def test_destroy + tracker = Tracker.create!(:name => 'Destroyable') + assert_difference 'Tracker.count', -1 do + delete :destroy, :id => tracker.id + end + assert_redirected_to :action => 'index' + assert_nil flash[:error] + end + + def test_destroy_tracker_in_use + assert_no_difference 'Tracker.count' do + delete :destroy, :id => 1 + end + assert_redirected_to :action => 'index' + assert_not_nil flash[:error] + end + + def test_get_fields + get :fields + assert_response :success + assert_template 'fields' + + assert_select 'form' do + assert_select 'input[type=checkbox][name=?][value=assigned_to_id]', 'trackers[1][core_fields][]' + assert_select 'input[type=checkbox][name=?][value=2]', 'trackers[1][custom_field_ids][]' + + assert_select 'input[type=hidden][name=?][value=]', 'trackers[1][core_fields][]' + assert_select 'input[type=hidden][name=?][value=]', 'trackers[1][custom_field_ids][]' + end + end + + def test_post_fields + post :fields, :trackers => { + '1' => {'core_fields' => ['assigned_to_id', 'due_date', ''], 'custom_field_ids' => ['1', '2']}, + '2' => {'core_fields' => [''], 'custom_field_ids' => ['']} + } + assert_redirected_to '/trackers/fields' + + tracker = Tracker.find(1) + assert_equal %w(assigned_to_id due_date), tracker.core_fields + assert_equal [1, 2], tracker.custom_field_ids.sort + + tracker = Tracker.find(2) + assert_equal [], tracker.core_fields + assert_equal [], tracker.custom_field_ids.sort + end +end diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb index acaf6b56..33f9e529 100644 --- a/test/functional/users_controller_test.rb +++ b/test/functional/users_controller_test.rb @@ -1,32 +1,437 @@ -require 'test_helper' - -class UsersControllerTest < ActionController::TestCase - fixtures :users, :user_extensions - def setup - initial_user_controller - end - def teardown - teardown_user_controller - end - - test "test user valid" do - assert @user.valid?, "user valid." - end - - test "get user_courses page" do - get :user_courses, {:id => @user.id} - assert_response :success - end - - private - - def initial_user_controller - @user = users(:person_mao) - @user_yan = users(:person_one) - end - - def teardown_user_controller - @user = nil - @user_yan = nil - end -end +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class UsersControllerTest < ActionController::TestCase + include Redmine::I18n + + fixtures :users, :projects, :members, :member_roles, :roles, + :custom_fields, :custom_values, :groups_users, + :auth_sources + + def setup + User.current = nil + @request.session[:user_id] = 1 # admin + end + + def test_index + get :index + assert_response :success + assert_template 'index' + end + + def test_index + get :index + assert_response :success + assert_template 'index' + assert_not_nil assigns(:users) + # active users only + assert_nil assigns(:users).detect {|u| !u.active?} + end + + def test_index_with_status_filter + get :index, :status => 3 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:users) + assert_equal [3], assigns(:users).map(&:status).uniq + end + + def test_index_with_name_filter + get :index, :name => 'john' + assert_response :success + assert_template 'index' + users = assigns(:users) + assert_not_nil users + assert_equal 1, users.size + assert_equal 'John', users.first.firstname + end + + def test_index_with_group_filter + get :index, :group_id => '10' + assert_response :success + assert_template 'index' + users = assigns(:users) + assert users.any? + assert_equal([], (users - Group.find(10).users)) + assert_select 'select[name=group_id]' do + assert_select 'option[value=10][selected=selected]' + end + end + + def test_show + @request.session[:user_id] = nil + get :show, :id => 2 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:user) + + assert_tag 'li', :content => /Phone number/ + end + + def test_show_should_not_display_hidden_custom_fields + @request.session[:user_id] = nil + UserCustomField.find_by_name('Phone number').update_attribute :visible, false + get :show, :id => 2 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:user) + + assert_no_tag 'li', :content => /Phone number/ + end + + def test_show_should_not_fail_when_custom_values_are_nil + user = User.find(2) + + # Create a custom field to illustrate the issue + custom_field = CustomField.create!(:name => 'Testing', :field_format => 'text') + custom_value = user.custom_values.build(:custom_field => custom_field).save! + + get :show, :id => 2 + assert_response :success + end + + def test_show_inactive + @request.session[:user_id] = nil + get :show, :id => 5 + assert_response 404 + end + + def test_show_should_not_reveal_users_with_no_visible_activity_or_project + @request.session[:user_id] = nil + get :show, :id => 9 + assert_response 404 + end + + def test_show_inactive_by_admin + @request.session[:user_id] = 1 + get :show, :id => 5 + assert_response 200 + assert_not_nil assigns(:user) + end + + def test_show_displays_memberships_based_on_project_visibility + @request.session[:user_id] = 1 + get :show, :id => 2 + assert_response :success + memberships = assigns(:memberships) + assert_not_nil memberships + project_ids = memberships.map(&:project_id) + assert project_ids.include?(2) #private project admin can see + end + + def test_show_current_should_require_authentication + @request.session[:user_id] = nil + get :show, :id => 'current' + assert_response 302 + end + + def test_show_current + @request.session[:user_id] = 2 + get :show, :id => 'current' + assert_response :success + assert_template 'show' + assert_equal User.find(2), assigns(:user) + end + + def test_new + get :new + assert_response :success + assert_template :new + assert assigns(:user) + end + + def test_create + Setting.bcc_recipients = '1' + + assert_difference 'User.count' do + assert_difference 'ActionMailer::Base.deliveries.size' do + post :create, + :user => { + :firstname => 'John', + :lastname => 'Doe', + :login => 'jdoe', + :password => 'secret123', + :password_confirmation => 'secret123', + :mail => 'jdoe@gmail.com', + :mail_notification => 'none' + }, + :send_information => '1' + end + end + + user = User.first(:order => 'id DESC') + assert_redirected_to :controller => 'users', :action => 'edit', :id => user.id + + assert_equal 'John', user.firstname + assert_equal 'Doe', user.lastname + assert_equal 'jdoe', user.login + assert_equal 'jdoe@gmail.com', user.mail + assert_equal 'none', user.mail_notification + assert user.check_password?('secret123') + + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + assert_equal [user.mail], mail.bcc + assert_mail_body_match 'secret', mail + end + + def test_create_with_preferences + assert_difference 'User.count' do + post :create, + :user => { + :firstname => 'John', + :lastname => 'Doe', + :login => 'jdoe', + :password => 'secret123', + :password_confirmation => 'secret123', + :mail => 'jdoe@gmail.com', + :mail_notification => 'none' + }, + :pref => { + 'hide_mail' => '1', + 'time_zone' => 'Paris', + 'comments_sorting' => 'desc', + 'warn_on_leaving_unsaved' => '0' + } + end + user = User.first(:order => 'id DESC') + assert_equal 'jdoe', user.login + assert_equal true, user.pref.hide_mail + assert_equal 'Paris', user.pref.time_zone + assert_equal 'desc', user.pref[:comments_sorting] + assert_equal '0', user.pref[:warn_on_leaving_unsaved] + end + + def test_create_with_failure + assert_no_difference 'User.count' do + post :create, :user => {} + end + assert_response :success + assert_template 'new' + end + + def test_edit + get :edit, :id => 2 + assert_response :success + assert_template 'edit' + assert_equal User.find(2), assigns(:user) + end + + def test_update + ActionMailer::Base.deliveries.clear + put :update, :id => 2, + :user => {:firstname => 'Changed', :mail_notification => 'only_assigned'}, + :pref => {:hide_mail => '1', :comments_sorting => 'desc'} + user = User.find(2) + assert_equal 'Changed', user.firstname + assert_equal 'only_assigned', user.mail_notification + assert_equal true, user.pref[:hide_mail] + assert_equal 'desc', user.pref[:comments_sorting] + assert ActionMailer::Base.deliveries.empty? + end + + def test_update_with_failure + assert_no_difference 'User.count' do + put :update, :id => 2, :user => {:firstname => ''} + end + assert_response :success + assert_template 'edit' + end + + def test_update_with_group_ids_should_assign_groups + put :update, :id => 2, :user => {:group_ids => ['10']} + user = User.find(2) + assert_equal [10], user.group_ids + end + + def test_update_with_activation_should_send_a_notification + u = User.new(:firstname => 'Foo', :lastname => 'Bar', :mail => 'foo.bar@somenet.foo', :language => 'fr') + u.login = 'foo' + u.status = User::STATUS_REGISTERED + u.save! + ActionMailer::Base.deliveries.clear + Setting.bcc_recipients = '1' + + put :update, :id => u.id, :user => {:status => User::STATUS_ACTIVE} + assert u.reload.active? + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + assert_equal ['foo.bar@somenet.foo'], mail.bcc + assert_mail_body_match ll('fr', :notice_account_activated), mail + end + + def test_update_with_password_change_should_send_a_notification + ActionMailer::Base.deliveries.clear + Setting.bcc_recipients = '1' + + put :update, :id => 2, :user => {:password => 'newpass123', :password_confirmation => 'newpass123'}, :send_information => '1' + u = User.find(2) + assert u.check_password?('newpass123') + + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + assert_equal [u.mail], mail.bcc + assert_mail_body_match 'newpass123', mail + end + + def test_update_user_switchin_from_auth_source_to_password_authentication + # Configure as auth source + u = User.find(2) + u.auth_source = AuthSource.find(1) + u.save! + + put :update, :id => u.id, :user => {:auth_source_id => '', :password => 'newpass123', :password_confirmation => 'newpass123'} + + assert_equal nil, u.reload.auth_source + assert u.check_password?('newpass123') + end + + def test_update_notified_project + get :edit, :id => 2 + assert_response :success + assert_template 'edit' + u = User.find(2) + assert_equal [1, 2, 5], u.projects.collect{|p| p.id}.sort + assert_equal [1, 2, 5], u.notified_projects_ids.sort + assert_tag :tag => 'input', + :attributes => { + :id => 'notified_project_ids_', + :value => 1, + } + assert_equal 'all', u.mail_notification + put :update, :id => 2, + :user => { + :mail_notification => 'selected', + }, + :notified_project_ids => [1, 2] + u = User.find(2) + assert_equal 'selected', u.mail_notification + assert_equal [1, 2], u.notified_projects_ids.sort + end + + def test_update_status_should_not_update_attributes + user = User.find(2) + user.pref[:no_self_notified] = '1' + user.pref.save + + put :update, :id => 2, :user => {:status => 3} + assert_response 302 + user = User.find(2) + assert_equal 3, user.status + assert_equal '1', user.pref[:no_self_notified] + end + + def test_destroy + assert_difference 'User.count', -1 do + delete :destroy, :id => 2 + end + assert_redirected_to '/users' + assert_nil User.find_by_id(2) + end + + def test_destroy_should_be_denied_for_non_admin_users + @request.session[:user_id] = 3 + + assert_no_difference 'User.count' do + get :destroy, :id => 2 + end + assert_response 403 + end + + def test_destroy_should_redirect_to_back_url_param + assert_difference 'User.count', -1 do + delete :destroy, :id => 2, :back_url => '/users?name=foo' + end + assert_redirected_to '/users?name=foo' + end + + def test_create_membership + assert_difference 'Member.count' do + post :edit_membership, :id => 7, :membership => { :project_id => 3, :role_ids => [2]} + end + assert_redirected_to :action => 'edit', :id => '7', :tab => 'memberships' + member = Member.first(:order => 'id DESC') + assert_equal User.find(7), member.principal + assert_equal [2], member.role_ids + assert_equal 3, member.project_id + end + + def test_create_membership_js_format + assert_difference 'Member.count' do + post :edit_membership, :id => 7, :membership => {:project_id => 3, :role_ids => [2]}, :format => 'js' + assert_response :success + assert_template 'edit_membership' + assert_equal 'text/javascript', response.content_type + end + member = Member.first(:order => 'id DESC') + assert_equal User.find(7), member.principal + assert_equal [2], member.role_ids + assert_equal 3, member.project_id + assert_include 'tab-content-memberships', response.body + end + + def test_create_membership_js_format_with_failure + assert_no_difference 'Member.count' do + post :edit_membership, :id => 7, :membership => {:project_id => 3}, :format => 'js' + assert_response :success + assert_template 'edit_membership' + assert_equal 'text/javascript', response.content_type + end + assert_include 'alert', response.body, "Alert message not sent" + assert_include 'Role can\\\'t be empty', response.body, "Error message not sent" + end + + def test_update_membership + assert_no_difference 'Member.count' do + put :edit_membership, :id => 2, :membership_id => 1, :membership => { :role_ids => [2]} + assert_redirected_to :action => 'edit', :id => '2', :tab => 'memberships' + end + assert_equal [2], Member.find(1).role_ids + end + + def test_update_membership_js_format + assert_no_difference 'Member.count' do + put :edit_membership, :id => 2, :membership_id => 1, :membership => {:role_ids => [2]}, :format => 'js' + assert_response :success + assert_template 'edit_membership' + assert_equal 'text/javascript', response.content_type + end + assert_equal [2], Member.find(1).role_ids + assert_include 'tab-content-memberships', response.body + end + + def test_destroy_membership + assert_difference 'Member.count', -1 do + delete :destroy_membership, :id => 2, :membership_id => 1 + end + assert_redirected_to :action => 'edit', :id => '2', :tab => 'memberships' + assert_nil Member.find_by_id(1) + end + + def test_destroy_membership_js_format + assert_difference 'Member.count', -1 do + delete :destroy_membership, :id => 2, :membership_id => 1, :format => 'js' + assert_response :success + assert_template 'destroy_membership' + assert_equal 'text/javascript', response.content_type + end + assert_nil Member.find_by_id(1) + assert_include 'tab-content-memberships', response.body + end +end diff --git a/test/functional/versions_controller_test.rb b/test/functional/versions_controller_test.rb new file mode 100644 index 00000000..a8a3f5b8 --- /dev/null +++ b/test/functional/versions_controller_test.rb @@ -0,0 +1,232 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class VersionsControllerTest < ActionController::TestCase + fixtures :projects, :versions, :issues, :users, :roles, :members, :member_roles, :enabled_modules, :issue_statuses, :issue_categories + + def setup + User.current = nil + end + + def test_index + get :index, :project_id => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:versions) + # Version with no date set appears + assert assigns(:versions).include?(Version.find(3)) + # Completed version doesn't appear + assert !assigns(:versions).include?(Version.find(1)) + # Context menu on issues + assert_select "script", :text => Regexp.new(Regexp.escape("contextMenuInit('/issues/context_menu')")) + # Links to versions anchors + assert_tag 'a', :attributes => {:href => '#2.0'}, + :ancestor => {:tag => 'div', :attributes => {:id => 'sidebar'}} + # Links to completed versions in the sidebar + assert_tag 'a', :attributes => {:href => '/versions/1'}, + :ancestor => {:tag => 'div', :attributes => {:id => 'sidebar'}} + end + + def test_index_with_completed_versions + get :index, :project_id => 1, :completed => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:versions) + # Version with no date set appears + assert assigns(:versions).include?(Version.find(3)) + # Completed version appears + assert assigns(:versions).include?(Version.find(1)) + end + + def test_index_with_tracker_ids + get :index, :project_id => 1, :tracker_ids => [1, 3] + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues_by_version) + assert_nil assigns(:issues_by_version).values.flatten.detect {|issue| issue.tracker_id == 2} + end + + def test_index_showing_subprojects_versions + @subproject_version = Version.create!(:project => Project.find(3), :name => "Subproject version") + get :index, :project_id => 1, :with_subprojects => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:versions) + + assert assigns(:versions).include?(Version.find(4)), "Shared version not found" + assert assigns(:versions).include?(@subproject_version), "Subproject version not found" + end + + def test_index_should_prepend_shared_versions + get :index, :project_id => 1 + assert_response :success + + assert_select '#sidebar' do + assert_select 'a[href=?]', '#2.0', :text => '2.0' + assert_select 'a[href=?]', '#subproject1-2.0', :text => 'eCookbook Subproject 1 - 2.0' + end + assert_select '#content' do + assert_select 'a[name=?]', '2.0', :text => '2.0' + assert_select 'a[name=?]', 'subproject1-2.0', :text => 'eCookbook Subproject 1 - 2.0' + end + end + + def test_show + get :show, :id => 2 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:version) + + assert_tag :tag => 'h2', :content => /1.0/ + end + + def test_show_should_display_nil_counts + with_settings :default_language => 'en' do + get :show, :id => 2, :status_by => 'category' + assert_response :success + assert_select 'div#status_by' do + assert_select 'select[name=status_by]' do + assert_select 'option[value=category][selected=selected]' + end + assert_select 'a', :text => 'none' + end + end + end + + def test_new + @request.session[:user_id] = 2 + get :new, :project_id => '1' + assert_response :success + assert_template 'new' + end + + def test_new_from_issue_form + @request.session[:user_id] = 2 + xhr :get, :new, :project_id => '1' + assert_response :success + assert_template 'new' + assert_equal 'text/javascript', response.content_type + end + + def test_create + @request.session[:user_id] = 2 # manager + assert_difference 'Version.count' do + post :create, :project_id => '1', :version => {:name => 'test_add_version'} + end + assert_redirected_to '/projects/ecookbook/settings/versions' + version = Version.find_by_name('test_add_version') + assert_not_nil version + assert_equal 1, version.project_id + end + + def test_create_from_issue_form + @request.session[:user_id] = 2 + assert_difference 'Version.count' do + xhr :post, :create, :project_id => '1', :version => {:name => 'test_add_version_from_issue_form'} + end + version = Version.find_by_name('test_add_version_from_issue_form') + assert_not_nil version + assert_equal 1, version.project_id + + assert_response :success + assert_template 'create' + assert_equal 'text/javascript', response.content_type + assert_include 'test_add_version_from_issue_form', response.body + end + + def test_create_from_issue_form_with_failure + @request.session[:user_id] = 2 + assert_no_difference 'Version.count' do + xhr :post, :create, :project_id => '1', :version => {:name => ''} + end + assert_response :success + assert_template 'new' + assert_equal 'text/javascript', response.content_type + end + + def test_get_edit + @request.session[:user_id] = 2 + get :edit, :id => 2 + assert_response :success + assert_template 'edit' + end + + def test_close_completed + Version.update_all("status = 'open'") + @request.session[:user_id] = 2 + put :close_completed, :project_id => 'ecookbook' + assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => 'ecookbook' + assert_not_nil Version.find_by_status('closed') + end + + def test_post_update + @request.session[:user_id] = 2 + put :update, :id => 2, + :version => { :name => 'New version name', + :effective_date => Date.today.strftime("%Y-%m-%d")} + assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => 'ecookbook' + version = Version.find(2) + assert_equal 'New version name', version.name + assert_equal Date.today, version.effective_date + end + + def test_post_update_with_validation_failure + @request.session[:user_id] = 2 + put :update, :id => 2, + :version => { :name => '', + :effective_date => Date.today.strftime("%Y-%m-%d")} + assert_response :success + assert_template 'edit' + end + + def test_destroy + @request.session[:user_id] = 2 + assert_difference 'Version.count', -1 do + delete :destroy, :id => 3 + end + assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => 'ecookbook' + assert_nil Version.find_by_id(3) + end + + def test_destroy_version_in_use_should_fail + @request.session[:user_id] = 2 + assert_no_difference 'Version.count' do + delete :destroy, :id => 2 + end + assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => 'ecookbook' + assert flash[:error].match(/Unable to delete version/) + assert Version.find_by_id(2) + end + + def test_issue_status_by + xhr :get, :status_by, :id => 2 + assert_response :success + assert_template 'status_by' + assert_template '_issue_counts' + end + + def test_issue_status_by_status + xhr :get, :status_by, :id => 2, :status_by => 'status' + assert_response :success + assert_template 'status_by' + assert_template '_issue_counts' + assert_include 'Assigned', response.body + assert_include 'Closed', response.body + end +end diff --git a/test/functional/watchers_controller_test.rb b/test/functional/watchers_controller_test.rb new file mode 100644 index 00000000..a6aaa27f --- /dev/null +++ b/test/functional/watchers_controller_test.rb @@ -0,0 +1,195 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class WatchersControllerTest < ActionController::TestCase + fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, + :issues, :trackers, :projects_trackers, :issue_statuses, :enumerations, :watchers + + def setup + User.current = nil + end + + def test_watch_a_single_object + @request.session[:user_id] = 3 + assert_difference('Watcher.count') do + xhr :post, :watch, :object_type => 'issue', :object_id => '1' + assert_response :success + assert_include '$(".issue-1-watcher")', response.body + end + assert Issue.find(1).watched_by?(User.find(3)) + end + + def test_watch_a_collection_with_a_single_object + @request.session[:user_id] = 3 + assert_difference('Watcher.count') do + xhr :post, :watch, :object_type => 'issue', :object_id => ['1'] + assert_response :success + assert_include '$(".issue-1-watcher")', response.body + end + assert Issue.find(1).watched_by?(User.find(3)) + end + + def test_watch_a_collection_with_multiple_objects + @request.session[:user_id] = 3 + assert_difference('Watcher.count', 2) do + xhr :post, :watch, :object_type => 'issue', :object_id => ['1', '3'] + assert_response :success + assert_include '$(".issue-bulk-watcher")', response.body + end + assert Issue.find(1).watched_by?(User.find(3)) + assert Issue.find(3).watched_by?(User.find(3)) + end + + def test_watch_should_be_denied_without_permission + Role.find(2).remove_permission! :view_issues + @request.session[:user_id] = 3 + assert_no_difference('Watcher.count') do + xhr :post, :watch, :object_type => 'issue', :object_id => '1' + assert_response 403 + end + end + + def test_watch_invalid_class_should_respond_with_404 + @request.session[:user_id] = 3 + assert_no_difference('Watcher.count') do + xhr :post, :watch, :object_type => 'foo', :object_id => '1' + assert_response 404 + end + end + + def test_watch_invalid_object_should_respond_with_404 + @request.session[:user_id] = 3 + assert_no_difference('Watcher.count') do + xhr :post, :watch, :object_type => 'issue', :object_id => '999' + assert_response 404 + end + end + + def test_unwatch + @request.session[:user_id] = 3 + assert_difference('Watcher.count', -1) do + xhr :delete, :unwatch, :object_type => 'issue', :object_id => '2' + assert_response :success + assert_include '$(".issue-2-watcher")', response.body + end + assert !Issue.find(1).watched_by?(User.find(3)) + end + + def test_unwatch_a_collection_with_multiple_objects + @request.session[:user_id] = 3 + Watcher.create!(:user_id => 3, :watchable => Issue.find(1)) + Watcher.create!(:user_id => 3, :watchable => Issue.find(3)) + + assert_difference('Watcher.count', -2) do + xhr :delete, :unwatch, :object_type => 'issue', :object_id => ['1', '3'] + assert_response :success + assert_include '$(".issue-bulk-watcher")', response.body + end + assert !Issue.find(1).watched_by?(User.find(3)) + assert !Issue.find(3).watched_by?(User.find(3)) + end + + def test_new + @request.session[:user_id] = 2 + xhr :get, :new, :object_type => 'issue', :object_id => '2' + assert_response :success + assert_match /ajax-modal/, response.body + end + + def test_new_for_new_record_with_project_id + @request.session[:user_id] = 2 + xhr :get, :new, :project_id => 1 + assert_response :success + assert_equal Project.find(1), assigns(:project) + assert_match /ajax-modal/, response.body + end + + def test_new_for_new_record_with_project_identifier + @request.session[:user_id] = 2 + xhr :get, :new, :project_id => 'ecookbook' + assert_response :success + assert_equal Project.find(1), assigns(:project) + assert_match /ajax-modal/, response.body + end + + def test_create + @request.session[:user_id] = 2 + assert_difference('Watcher.count') do + xhr :post, :create, :object_type => 'issue', :object_id => '2', :watcher => {:user_id => '4'} + assert_response :success + assert_match /watchers/, response.body + assert_match /ajax-modal/, response.body + end + assert Issue.find(2).watched_by?(User.find(4)) + end + + def test_create_multiple + @request.session[:user_id] = 2 + assert_difference('Watcher.count', 2) do + xhr :post, :create, :object_type => 'issue', :object_id => '2', :watcher => {:user_ids => ['4', '7']} + assert_response :success + assert_match /watchers/, response.body + assert_match /ajax-modal/, response.body + end + assert Issue.find(2).watched_by?(User.find(4)) + assert Issue.find(2).watched_by?(User.find(7)) + end + + def test_autocomplete_on_watchable_creation + @request.session[:user_id] = 2 + xhr :get, :autocomplete_for_user, :q => 'mi', :project_id => 'ecookbook' + assert_response :success + assert_select 'input', :count => 4 + assert_select 'input[name=?][value=1]', 'watcher[user_ids][]' + assert_select 'input[name=?][value=2]', 'watcher[user_ids][]' + assert_select 'input[name=?][value=8]', 'watcher[user_ids][]' + assert_select 'input[name=?][value=9]', 'watcher[user_ids][]' + end + + def test_autocomplete_on_watchable_update + @request.session[:user_id] = 2 + xhr :get, :autocomplete_for_user, :q => 'mi', :object_id => '2' , :object_type => 'issue', :project_id => 'ecookbook' + assert_response :success + assert_select 'input', :count => 3 + assert_select 'input[name=?][value=2]', 'watcher[user_ids][]' + assert_select 'input[name=?][value=8]', 'watcher[user_ids][]' + assert_select 'input[name=?][value=9]', 'watcher[user_ids][]' + + end + + def test_append + @request.session[:user_id] = 2 + assert_no_difference 'Watcher.count' do + xhr :post, :append, :watcher => {:user_ids => ['4', '7']}, :project_id => 'ecookbook' + assert_response :success + assert_include 'watchers_inputs', response.body + assert_include 'issue[watcher_user_ids][]', response.body + end + end + + def test_remove_watcher + @request.session[:user_id] = 2 + assert_difference('Watcher.count', -1) do + xhr :delete, :destroy, :object_type => 'issue', :object_id => '2', :user_id => '3' + assert_response :success + assert_match /watchers/, response.body + end + assert !Issue.find(2).watched_by?(User.find(3)) + end +end diff --git a/test/functional/welcome_controller_test.rb b/test/functional/welcome_controller_test.rb new file mode 100644 index 00000000..07a69eed --- /dev/null +++ b/test/functional/welcome_controller_test.rb @@ -0,0 +1,165 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class WelcomeControllerTest < ActionController::TestCase + fixtures :projects, :news, :users, :members + + def setup + User.current = nil + end + + def test_index + get :index + assert_response :success + assert_template 'index' + assert_not_nil assigns(:news) + assert_not_nil assigns(:projects) + assert !assigns(:projects).include?(Project.where(:is_public => false).first) + end + + def test_browser_language + Setting.default_language = 'en' + @request.env['HTTP_ACCEPT_LANGUAGE'] = 'fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3' + get :index + assert_equal :fr, @controller.current_language + end + + def test_browser_language_alternate + Setting.default_language = 'en' + @request.env['HTTP_ACCEPT_LANGUAGE'] = 'zh-TW' + get :index + assert_equal :"zh-TW", @controller.current_language + end + + def test_browser_language_alternate_not_valid + Setting.default_language = 'en' + @request.env['HTTP_ACCEPT_LANGUAGE'] = 'fr-CA' + get :index + assert_equal :fr, @controller.current_language + end + + def test_robots + get :robots + assert_response :success + assert_equal 'text/plain', @response.content_type + assert @response.body.match(%r{^Disallow: /projects/ecookbook/issues\r?$}) + end + + def test_warn_on_leaving_unsaved_turn_on + user = User.find(2) + user.pref.warn_on_leaving_unsaved = '1' + user.pref.save! + @request.session[:user_id] = 2 + + get :index + assert_tag 'script', + :attributes => {:type => "text/javascript"}, + :content => %r{warnLeavingUnsaved} + end + + def test_warn_on_leaving_unsaved_turn_off + user = User.find(2) + user.pref.warn_on_leaving_unsaved = '0' + user.pref.save! + @request.session[:user_id] = 2 + + get :index + assert_no_tag 'script', + :attributes => {:type => "text/javascript"}, + :content => %r{warnLeavingUnsaved} + end + + def test_logout_link_should_post + @request.session[:user_id] = 2 + + get :index + assert_select 'a[href=/logout][data-method=post]', :text => 'Sign out' + end + + def test_call_hook_mixed_in + assert @controller.respond_to?(:call_hook) + end + + def test_project_jump_box_should_escape_names_once + Project.find(1).update_attribute :name, 'Foo & Bar' + @request.session[:user_id] = 2 + + get :index + assert_select "#header select" do + assert_select "option", :text => 'Foo & Bar' + end + end + + context "test_api_offset_and_limit" do + context "without params" do + should "return 0, 25" do + assert_equal [0, 25], @controller.api_offset_and_limit({}) + end + end + + context "with limit" do + should "return 0, limit" do + assert_equal [0, 30], @controller.api_offset_and_limit({:limit => 30}) + end + + should "not exceed 100" do + assert_equal [0, 100], @controller.api_offset_and_limit({:limit => 120}) + end + + should "not be negative" do + assert_equal [0, 25], @controller.api_offset_and_limit({:limit => -10}) + end + end + + context "with offset" do + should "return offset, 25" do + assert_equal [10, 25], @controller.api_offset_and_limit({:offset => 10}) + end + + should "not be negative" do + assert_equal [0, 25], @controller.api_offset_and_limit({:offset => -10}) + end + + context "and limit" do + should "return offset, limit" do + assert_equal [10, 50], @controller.api_offset_and_limit({:offset => 10, :limit => 50}) + end + end + end + + context "with page" do + should "return offset, 25" do + assert_equal [0, 25], @controller.api_offset_and_limit({:page => 1}) + assert_equal [50, 25], @controller.api_offset_and_limit({:page => 3}) + end + + should "not be negative" do + assert_equal [0, 25], @controller.api_offset_and_limit({:page => 0}) + assert_equal [0, 25], @controller.api_offset_and_limit({:page => -2}) + end + + context "and limit" do + should "return offset, limit" do + assert_equal [0, 100], @controller.api_offset_and_limit({:page => 1, :limit => 100}) + assert_equal [200, 100], @controller.api_offset_and_limit({:page => 3, :limit => 100}) + end + end + end + end +end diff --git a/test/functional/wiki_controller_test.rb b/test/functional/wiki_controller_test.rb new file mode 100644 index 00000000..df98987c --- /dev/null +++ b/test/functional/wiki_controller_test.rb @@ -0,0 +1,933 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class WikiControllerTest < ActionController::TestCase + fixtures :projects, :users, :roles, :members, :member_roles, + :enabled_modules, :wikis, :wiki_pages, :wiki_contents, + :wiki_content_versions, :attachments + + def setup + User.current = nil + end + + def test_show_start_page + get :show, :project_id => 'ecookbook' + assert_response :success + assert_template 'show' + assert_tag :tag => 'h1', :content => /CookBook documentation/ + + # child_pages macro + assert_tag :ul, :attributes => { :class => 'pages-hierarchy' }, + :child => { :tag => 'li', + :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' }, + :content => 'Page with an inline image' } } + end + + def test_export_link + Role.anonymous.add_permission! :export_wiki_pages + get :show, :project_id => 'ecookbook' + assert_response :success + assert_tag 'a', :attributes => {:href => '/projects/ecookbook/wiki/CookBook_documentation.txt'} + end + + def test_show_page_with_name + get :show, :project_id => 1, :id => 'Another_page' + assert_response :success + assert_template 'show' + assert_tag :tag => 'h1', :content => /Another page/ + # Included page with an inline image + assert_tag :tag => 'p', :content => /This is an inline image/ + assert_tag :tag => 'img', :attributes => { :src => '/attachments/download/3/logo.gif', + :alt => 'This is a logo' } + end + + def test_show_old_version + get :show, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => '2' + assert_response :success + assert_template 'show' + + assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/1', :text => /Previous/ + assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2/diff', :text => /diff/ + assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/3', :text => /Next/ + assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation', :text => /Current version/ + end + + def test_show_old_version_with_attachments + page = WikiPage.find(4) + assert page.attachments.any? + content = page.content + content.text = "update" + content.save! + + get :show, :project_id => 'ecookbook', :id => page.title, :version => '1' + assert_kind_of WikiContent::Version, assigns(:content) + assert_response :success + assert_template 'show' + end + + def test_show_old_version_without_permission_should_be_denied + Role.anonymous.remove_permission! :view_wiki_edits + + get :show, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => '2' + assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fprojects%2Fecookbook%2Fwiki%2FCookBook_documentation%2F2' + end + + def test_show_first_version + get :show, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => '1' + assert_response :success + assert_template 'show' + + assert_select 'a', :text => /Previous/, :count => 0 + assert_select 'a', :text => /diff/, :count => 0 + assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => /Next/ + assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation', :text => /Current version/ + end + + def test_show_redirected_page + WikiRedirect.create!(:wiki_id => 1, :title => 'Old_title', :redirects_to => 'Another_page') + + get :show, :project_id => 'ecookbook', :id => 'Old_title' + assert_redirected_to '/projects/ecookbook/wiki/Another_page' + end + + def test_show_with_sidebar + page = Project.find(1).wiki.pages.new(:title => 'Sidebar') + page.content = WikiContent.new(:text => 'Side bar content for test_show_with_sidebar') + page.save! + + get :show, :project_id => 1, :id => 'Another_page' + assert_response :success + assert_tag :tag => 'div', :attributes => {:id => 'sidebar'}, + :content => /Side bar content for test_show_with_sidebar/ + end + + def test_show_should_display_section_edit_links + @request.session[:user_id] = 2 + get :show, :project_id => 1, :id => 'Page with sections' + assert_no_tag 'a', :attributes => { + :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=1' + } + assert_tag 'a', :attributes => { + :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2' + } + assert_tag 'a', :attributes => { + :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=3' + } + end + + def test_show_current_version_should_display_section_edit_links + @request.session[:user_id] = 2 + get :show, :project_id => 1, :id => 'Page with sections', :version => 3 + + assert_tag 'a', :attributes => { + :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2' + } + end + + def test_show_old_version_should_not_display_section_edit_links + @request.session[:user_id] = 2 + get :show, :project_id => 1, :id => 'Page with sections', :version => 2 + + assert_no_tag 'a', :attributes => { + :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2' + } + end + + def test_show_unexistent_page_without_edit_right + get :show, :project_id => 1, :id => 'Unexistent page' + assert_response 404 + end + + def test_show_unexistent_page_with_edit_right + @request.session[:user_id] = 2 + get :show, :project_id => 1, :id => 'Unexistent page' + assert_response :success + assert_template 'edit' + end + + def test_show_unexistent_page_with_parent_should_preselect_parent + @request.session[:user_id] = 2 + get :show, :project_id => 1, :id => 'Unexistent page', :parent => 'Another_page' + assert_response :success + assert_template 'edit' + assert_tag 'select', :attributes => {:name => 'wiki_page[parent_id]'}, + :child => {:tag => 'option', :attributes => {:value => '2', :selected => 'selected'}} + end + + def test_show_should_not_show_history_without_permission + Role.anonymous.remove_permission! :view_wiki_edits + get :show, :project_id => 1, :id => 'Page with sections', :version => 2 + + assert_response 302 + end + + def test_create_page + @request.session[:user_id] = 2 + assert_difference 'WikiPage.count' do + assert_difference 'WikiContent.count' do + put :update, :project_id => 1, + :id => 'New page', + :content => {:comments => 'Created the page', + :text => "h1. New page\n\nThis is a new page", + :version => 0} + end + end + assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'New_page' + page = Project.find(1).wiki.find_page('New page') + assert !page.new_record? + assert_not_nil page.content + assert_nil page.parent + assert_equal 'Created the page', page.content.comments + end + + def test_create_page_with_attachments + @request.session[:user_id] = 2 + assert_difference 'WikiPage.count' do + assert_difference 'Attachment.count' do + put :update, :project_id => 1, + :id => 'New page', + :content => {:comments => 'Created the page', + :text => "h1. New page\n\nThis is a new page", + :version => 0}, + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}} + end + end + page = Project.find(1).wiki.find_page('New page') + assert_equal 1, page.attachments.count + assert_equal 'testfile.txt', page.attachments.first.filename + end + + def test_create_page_with_parent + @request.session[:user_id] = 2 + assert_difference 'WikiPage.count' do + put :update, :project_id => 1, :id => 'New page', + :content => {:text => "h1. New page\n\nThis is a new page", :version => 0}, + :wiki_page => {:parent_id => 2} + end + page = Project.find(1).wiki.find_page('New page') + assert_equal WikiPage.find(2), page.parent + end + + def test_edit_page + @request.session[:user_id] = 2 + get :edit, :project_id => 'ecookbook', :id => 'Another_page' + + assert_response :success + assert_template 'edit' + + assert_tag 'textarea', + :attributes => { :name => 'content[text]' }, + :content => "\n"+WikiPage.find_by_title('Another_page').content.text + end + + def test_edit_section + @request.session[:user_id] = 2 + get :edit, :project_id => 'ecookbook', :id => 'Page_with_sections', :section => 2 + + assert_response :success + assert_template 'edit' + + page = WikiPage.find_by_title('Page_with_sections') + section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2) + + assert_tag 'textarea', + :attributes => { :name => 'content[text]' }, + :content => "\n"+section + assert_tag 'input', + :attributes => { :name => 'section', :type => 'hidden', :value => '2' } + assert_tag 'input', + :attributes => { :name => 'section_hash', :type => 'hidden', :value => hash } + end + + def test_edit_invalid_section_should_respond_with_404 + @request.session[:user_id] = 2 + get :edit, :project_id => 'ecookbook', :id => 'Page_with_sections', :section => 10 + + assert_response 404 + end + + def test_update_page + @request.session[:user_id] = 2 + assert_no_difference 'WikiPage.count' do + assert_no_difference 'WikiContent.count' do + assert_difference 'WikiContent::Version.count' do + put :update, :project_id => 1, + :id => 'Another_page', + :content => { + :comments => "my comments", + :text => "edited", + :version => 1 + } + end + end + end + assert_redirected_to '/projects/ecookbook/wiki/Another_page' + + page = Wiki.find(1).pages.find_by_title('Another_page') + assert_equal "edited", page.content.text + assert_equal 2, page.content.version + assert_equal "my comments", page.content.comments + end + + def test_update_page_with_parent + @request.session[:user_id] = 2 + assert_no_difference 'WikiPage.count' do + assert_no_difference 'WikiContent.count' do + assert_difference 'WikiContent::Version.count' do + put :update, :project_id => 1, + :id => 'Another_page', + :content => { + :comments => "my comments", + :text => "edited", + :version => 1 + }, + :wiki_page => {:parent_id => '1'} + end + end + end + assert_redirected_to '/projects/ecookbook/wiki/Another_page' + + page = Wiki.find(1).pages.find_by_title('Another_page') + assert_equal "edited", page.content.text + assert_equal 2, page.content.version + assert_equal "my comments", page.content.comments + assert_equal WikiPage.find(1), page.parent + end + + def test_update_page_with_failure + @request.session[:user_id] = 2 + assert_no_difference 'WikiPage.count' do + assert_no_difference 'WikiContent.count' do + assert_no_difference 'WikiContent::Version.count' do + put :update, :project_id => 1, + :id => 'Another_page', + :content => { + :comments => 'a' * 300, # failure here, comment is too long + :text => 'edited', + :version => 1 + } + end + end + end + assert_response :success + assert_template 'edit' + + assert_error_tag :descendant => {:content => /Comment is too long/} + assert_tag :tag => 'textarea', :attributes => {:id => 'content_text'}, :content => "\nedited" + assert_tag :tag => 'input', :attributes => {:id => 'content_version', :value => '1'} + end + + def test_update_page_with_parent_change_only_should_not_create_content_version + @request.session[:user_id] = 2 + assert_no_difference 'WikiPage.count' do + assert_no_difference 'WikiContent.count' do + assert_no_difference 'WikiContent::Version.count' do + put :update, :project_id => 1, + :id => 'Another_page', + :content => { + :comments => '', + :text => Wiki.find(1).find_page('Another_page').content.text, + :version => 1 + }, + :wiki_page => {:parent_id => '1'} + end + end + end + page = Wiki.find(1).pages.find_by_title('Another_page') + assert_equal 1, page.content.version + assert_equal WikiPage.find(1), page.parent + end + + def test_update_page_with_attachments_only_should_not_create_content_version + @request.session[:user_id] = 2 + assert_no_difference 'WikiPage.count' do + assert_no_difference 'WikiContent.count' do + assert_no_difference 'WikiContent::Version.count' do + assert_difference 'Attachment.count' do + put :update, :project_id => 1, + :id => 'Another_page', + :content => { + :comments => '', + :text => Wiki.find(1).find_page('Another_page').content.text, + :version => 1 + }, + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}} + end + end + end + end + page = Wiki.find(1).pages.find_by_title('Another_page') + assert_equal 1, page.content.version + end + + def test_update_stale_page_should_not_raise_an_error + @request.session[:user_id] = 2 + c = Wiki.find(1).find_page('Another_page').content + c.text = 'Previous text' + c.save! + assert_equal 2, c.version + + assert_no_difference 'WikiPage.count' do + assert_no_difference 'WikiContent.count' do + assert_no_difference 'WikiContent::Version.count' do + put :update, :project_id => 1, + :id => 'Another_page', + :content => { + :comments => 'My comments', + :text => 'Text should not be lost', + :version => 1 + } + end + end + end + assert_response :success + assert_template 'edit' + assert_tag :div, + :attributes => { :class => /error/ }, + :content => /Data has been updated by another user/ + assert_tag 'textarea', + :attributes => { :name => 'content[text]' }, + :content => /Text should not be lost/ + assert_tag 'input', + :attributes => { :name => 'content[comments]', :value => 'My comments' } + + c.reload + assert_equal 'Previous text', c.text + assert_equal 2, c.version + end + + def test_update_section + @request.session[:user_id] = 2 + page = WikiPage.find_by_title('Page_with_sections') + section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2) + text = page.content.text + + assert_no_difference 'WikiPage.count' do + assert_no_difference 'WikiContent.count' do + assert_difference 'WikiContent::Version.count' do + put :update, :project_id => 1, :id => 'Page_with_sections', + :content => { + :text => "New section content", + :version => 3 + }, + :section => 2, + :section_hash => hash + end + end + end + assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections' + assert_equal Redmine::WikiFormatting::Textile::Formatter.new(text).update_section(2, "New section content"), page.reload.content.text + end + + def test_update_section_should_allow_stale_page_update + @request.session[:user_id] = 2 + page = WikiPage.find_by_title('Page_with_sections') + section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2) + text = page.content.text + + assert_no_difference 'WikiPage.count' do + assert_no_difference 'WikiContent.count' do + assert_difference 'WikiContent::Version.count' do + put :update, :project_id => 1, :id => 'Page_with_sections', + :content => { + :text => "New section content", + :version => 2 # Current version is 3 + }, + :section => 2, + :section_hash => hash + end + end + end + assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections' + page.reload + assert_equal Redmine::WikiFormatting::Textile::Formatter.new(text).update_section(2, "New section content"), page.content.text + assert_equal 4, page.content.version + end + + def test_update_section_should_not_allow_stale_section_update + @request.session[:user_id] = 2 + + assert_no_difference 'WikiPage.count' do + assert_no_difference 'WikiContent.count' do + assert_no_difference 'WikiContent::Version.count' do + put :update, :project_id => 1, :id => 'Page_with_sections', + :content => { + :comments => 'My comments', + :text => "Text should not be lost", + :version => 3 + }, + :section => 2, + :section_hash => Digest::MD5.hexdigest("wrong hash") + end + end + end + assert_response :success + assert_template 'edit' + assert_tag :div, + :attributes => { :class => /error/ }, + :content => /Data has been updated by another user/ + assert_tag 'textarea', + :attributes => { :name => 'content[text]' }, + :content => /Text should not be lost/ + assert_tag 'input', + :attributes => { :name => 'content[comments]', :value => 'My comments' } + end + + def test_preview + @request.session[:user_id] = 2 + xhr :post, :preview, :project_id => 1, :id => 'CookBook_documentation', + :content => { :comments => '', + :text => 'this is a *previewed text*', + :version => 3 } + assert_response :success + assert_template 'common/_preview' + assert_tag :tag => 'strong', :content => /previewed text/ + end + + def test_preview_new_page + @request.session[:user_id] = 2 + xhr :post, :preview, :project_id => 1, :id => 'New page', + :content => { :text => 'h1. New page', + :comments => '', + :version => 0 } + assert_response :success + assert_template 'common/_preview' + assert_tag :tag => 'h1', :content => /New page/ + end + + def test_history + @request.session[:user_id] = 2 + get :history, :project_id => 'ecookbook', :id => 'CookBook_documentation' + assert_response :success + assert_template 'history' + assert_not_nil assigns(:versions) + assert_equal 3, assigns(:versions).size + + assert_select "input[type=submit][name=commit]" + assert_select 'td' do + assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => '2' + assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2/annotate', :text => 'Annotate' + assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => 'Delete' + end + end + + def test_history_with_one_version + @request.session[:user_id] = 2 + get :history, :project_id => 'ecookbook', :id => 'Another_page' + assert_response :success + assert_template 'history' + assert_not_nil assigns(:versions) + assert_equal 1, assigns(:versions).size + assert_select "input[type=submit][name=commit]", false + assert_select 'td' do + assert_select 'a[href=?]', '/projects/ecookbook/wiki/Another_page/1', :text => '1' + assert_select 'a[href=?]', '/projects/ecookbook/wiki/Another_page/1/annotate', :text => 'Annotate' + assert_select 'a[href=?]', '/projects/ecookbook/wiki/Another_page/1', :text => 'Delete', :count => 0 + end + end + + def test_diff + content = WikiPage.find(1).content + assert_difference 'WikiContent::Version.count', 2 do + content.text = "Line removed\nThis is a sample text for testing diffs" + content.save! + content.text = "This is a sample text for testing diffs\nLine added" + content.save! + end + + get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => content.version, :version_from => (content.version - 1) + assert_response :success + assert_template 'diff' + assert_select 'span.diff_out', :text => 'Line removed' + assert_select 'span.diff_in', :text => 'Line added' + end + + def test_diff_with_invalid_version_should_respond_with_404 + get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => '99' + assert_response 404 + end + + def test_diff_with_invalid_version_from_should_respond_with_404 + get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => '99', :version_from => '98' + assert_response 404 + end + + def test_annotate + get :annotate, :project_id => 1, :id => 'CookBook_documentation', :version => 2 + assert_response :success + assert_template 'annotate' + + # Line 1 + assert_tag :tag => 'tr', :child => { + :tag => 'th', :attributes => {:class => 'line-num'}, :content => '1', :sibling => { + :tag => 'td', :attributes => {:class => 'author'}, :content => /John Smith/, :sibling => { + :tag => 'td', :content => /h1\. CookBook documentation/ + } + } + } + + # Line 5 + assert_tag :tag => 'tr', :child => { + :tag => 'th', :attributes => {:class => 'line-num'}, :content => '5', :sibling => { + :tag => 'td', :attributes => {:class => 'author'}, :content => /Redmine Admin/, :sibling => { + :tag => 'td', :content => /Some updated \[\[documentation\]\] here/ + } + } + } + end + + def test_annotate_with_invalid_version_should_respond_with_404 + get :annotate, :project_id => 1, :id => 'CookBook_documentation', :version => '99' + assert_response 404 + end + + def test_get_rename + @request.session[:user_id] = 2 + get :rename, :project_id => 1, :id => 'Another_page' + assert_response :success + assert_template 'rename' + assert_tag 'option', + :attributes => {:value => ''}, + :content => '', + :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}} + assert_no_tag 'option', + :attributes => {:selected => 'selected'}, + :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}} + end + + def test_get_rename_child_page + @request.session[:user_id] = 2 + get :rename, :project_id => 1, :id => 'Child_1' + assert_response :success + assert_template 'rename' + assert_tag 'option', + :attributes => {:value => ''}, + :content => '', + :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}} + assert_tag 'option', + :attributes => {:value => '2', :selected => 'selected'}, + :content => /Another page/, + :parent => { + :tag => 'select', + :attributes => {:name => 'wiki_page[parent_id]'} + } + end + + def test_rename_with_redirect + @request.session[:user_id] = 2 + post :rename, :project_id => 1, :id => 'Another_page', + :wiki_page => { :title => 'Another renamed page', + :redirect_existing_links => 1 } + assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_renamed_page' + wiki = Project.find(1).wiki + # Check redirects + assert_not_nil wiki.find_page('Another page') + assert_nil wiki.find_page('Another page', :with_redirect => false) + end + + def test_rename_without_redirect + @request.session[:user_id] = 2 + post :rename, :project_id => 1, :id => 'Another_page', + :wiki_page => { :title => 'Another renamed page', + :redirect_existing_links => "0" } + assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_renamed_page' + wiki = Project.find(1).wiki + # Check that there's no redirects + assert_nil wiki.find_page('Another page') + end + + def test_rename_with_parent_assignment + @request.session[:user_id] = 2 + post :rename, :project_id => 1, :id => 'Another_page', + :wiki_page => { :title => 'Another page', :redirect_existing_links => "0", :parent_id => '4' } + assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_page' + assert_equal WikiPage.find(4), WikiPage.find_by_title('Another_page').parent + end + + def test_rename_with_parent_unassignment + @request.session[:user_id] = 2 + post :rename, :project_id => 1, :id => 'Child_1', + :wiki_page => { :title => 'Child 1', :redirect_existing_links => "0", :parent_id => '' } + assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Child_1' + assert_nil WikiPage.find_by_title('Child_1').parent + end + + def test_destroy_a_page_without_children_should_not_ask_confirmation + @request.session[:user_id] = 2 + delete :destroy, :project_id => 1, :id => 'Child_2' + assert_redirected_to :action => 'index', :project_id => 'ecookbook' + end + + def test_destroy_parent_should_ask_confirmation + @request.session[:user_id] = 2 + assert_no_difference('WikiPage.count') do + delete :destroy, :project_id => 1, :id => 'Another_page' + end + assert_response :success + assert_template 'destroy' + assert_select 'form' do + assert_select 'input[name=todo][value=nullify]' + assert_select 'input[name=todo][value=destroy]' + assert_select 'input[name=todo][value=reassign]' + end + end + + def test_destroy_parent_with_nullify_should_delete_parent_only + @request.session[:user_id] = 2 + assert_difference('WikiPage.count', -1) do + delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'nullify' + end + assert_redirected_to :action => 'index', :project_id => 'ecookbook' + assert_nil WikiPage.find_by_id(2) + end + + def test_destroy_parent_with_cascade_should_delete_descendants + @request.session[:user_id] = 2 + assert_difference('WikiPage.count', -4) do + delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'destroy' + end + assert_redirected_to :action => 'index', :project_id => 'ecookbook' + assert_nil WikiPage.find_by_id(2) + assert_nil WikiPage.find_by_id(5) + end + + def test_destroy_parent_with_reassign + @request.session[:user_id] = 2 + assert_difference('WikiPage.count', -1) do + delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'reassign', :reassign_to_id => 1 + end + assert_redirected_to :action => 'index', :project_id => 'ecookbook' + assert_nil WikiPage.find_by_id(2) + assert_equal WikiPage.find(1), WikiPage.find_by_id(5).parent + end + + def test_destroy_version + @request.session[:user_id] = 2 + assert_difference 'WikiContent::Version.count', -1 do + assert_no_difference 'WikiContent.count' do + assert_no_difference 'WikiPage.count' do + delete :destroy_version, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => 2 + assert_redirected_to '/projects/ecookbook/wiki/CookBook_documentation/history' + end + end + end + end + + def test_index + get :index, :project_id => 'ecookbook' + assert_response :success + assert_template 'index' + pages = assigns(:pages) + assert_not_nil pages + assert_equal Project.find(1).wiki.pages.size, pages.size + assert_equal pages.first.content.updated_on, pages.first.updated_on + + assert_tag :ul, :attributes => { :class => 'pages-hierarchy' }, + :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/CookBook_documentation' }, + :content => 'CookBook documentation' }, + :child => { :tag => 'ul', + :child => { :tag => 'li', + :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' }, + :content => 'Page with an inline image' } } } }, + :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Another_page' }, + :content => 'Another page' } } + end + + def test_index_should_include_atom_link + get :index, :project_id => 'ecookbook' + assert_tag 'a', :attributes => { :href => '/projects/ecookbook/activity.atom?show_wiki_edits=1'} + end + + def test_export_to_html + @request.session[:user_id] = 2 + get :export, :project_id => 'ecookbook' + + assert_response :success + assert_not_nil assigns(:pages) + assert assigns(:pages).any? + assert_equal "text/html", @response.content_type + + assert_select "a[name=?]", "CookBook_documentation" + assert_select "a[name=?]", "Another_page" + assert_select "a[name=?]", "Page_with_an_inline_image" + end + + def test_export_to_pdf + @request.session[:user_id] = 2 + get :export, :project_id => 'ecookbook', :format => 'pdf' + + assert_response :success + assert_not_nil assigns(:pages) + assert assigns(:pages).any? + assert_equal 'application/pdf', @response.content_type + assert_equal 'attachment; filename="ecookbook.pdf"', @response.headers['Content-Disposition'] + assert @response.body.starts_with?('%PDF') + end + + def test_export_without_permission_should_be_denied + @request.session[:user_id] = 2 + Role.find_by_name('Manager').remove_permission! :export_wiki_pages + get :export, :project_id => 'ecookbook' + + assert_response 403 + end + + def test_date_index + get :date_index, :project_id => 'ecookbook' + + assert_response :success + assert_template 'date_index' + assert_not_nil assigns(:pages) + assert_not_nil assigns(:pages_by_date) + + assert_tag 'a', :attributes => { :href => '/projects/ecookbook/activity.atom?show_wiki_edits=1'} + end + + def test_not_found + get :show, :project_id => 999 + assert_response 404 + end + + def test_protect_page + page = WikiPage.find_by_wiki_id_and_title(1, 'Another_page') + assert !page.protected? + @request.session[:user_id] = 2 + post :protect, :project_id => 1, :id => page.title, :protected => '1' + assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_page' + assert page.reload.protected? + end + + def test_unprotect_page + page = WikiPage.find_by_wiki_id_and_title(1, 'CookBook_documentation') + assert page.protected? + @request.session[:user_id] = 2 + post :protect, :project_id => 1, :id => page.title, :protected => '0' + assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'CookBook_documentation' + assert !page.reload.protected? + end + + def test_show_page_with_edit_link + @request.session[:user_id] = 2 + get :show, :project_id => 1 + assert_response :success + assert_template 'show' + assert_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' } + end + + def test_show_page_without_edit_link + @request.session[:user_id] = 4 + get :show, :project_id => 1 + assert_response :success + assert_template 'show' + assert_no_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' } + end + + def test_show_pdf + @request.session[:user_id] = 2 + get :show, :project_id => 1, :format => 'pdf' + assert_response :success + assert_not_nil assigns(:page) + assert_equal 'application/pdf', @response.content_type + assert_equal 'attachment; filename="CookBook_documentation.pdf"', + @response.headers['Content-Disposition'] + end + + def test_show_html + @request.session[:user_id] = 2 + get :show, :project_id => 1, :format => 'html' + assert_response :success + assert_not_nil assigns(:page) + assert_equal 'text/html', @response.content_type + assert_equal 'attachment; filename="CookBook_documentation.html"', + @response.headers['Content-Disposition'] + assert_tag 'h1', :content => 'CookBook documentation' + end + + def test_show_versioned_html + @request.session[:user_id] = 2 + get :show, :project_id => 1, :format => 'html', :version => 2 + assert_response :success + assert_not_nil assigns(:content) + assert_equal 2, assigns(:content).version + assert_equal 'text/html', @response.content_type + assert_equal 'attachment; filename="CookBook_documentation.html"', + @response.headers['Content-Disposition'] + assert_tag 'h1', :content => 'CookBook documentation' + end + + def test_show_txt + @request.session[:user_id] = 2 + get :show, :project_id => 1, :format => 'txt' + assert_response :success + assert_not_nil assigns(:page) + assert_equal 'text/plain', @response.content_type + assert_equal 'attachment; filename="CookBook_documentation.txt"', + @response.headers['Content-Disposition'] + assert_include 'h1. CookBook documentation', @response.body + end + + def test_show_versioned_txt + @request.session[:user_id] = 2 + get :show, :project_id => 1, :format => 'txt', :version => 2 + assert_response :success + assert_not_nil assigns(:content) + assert_equal 2, assigns(:content).version + assert_equal 'text/plain', @response.content_type + assert_equal 'attachment; filename="CookBook_documentation.txt"', + @response.headers['Content-Disposition'] + assert_include 'h1. CookBook documentation', @response.body + end + + def test_edit_unprotected_page + # Non members can edit unprotected wiki pages + @request.session[:user_id] = 4 + get :edit, :project_id => 1, :id => 'Another_page' + assert_response :success + assert_template 'edit' + end + + def test_edit_protected_page_by_nonmember + # Non members can't edit protected wiki pages + @request.session[:user_id] = 4 + get :edit, :project_id => 1, :id => 'CookBook_documentation' + assert_response 403 + end + + def test_edit_protected_page_by_member + @request.session[:user_id] = 2 + get :edit, :project_id => 1, :id => 'CookBook_documentation' + assert_response :success + assert_template 'edit' + end + + def test_history_of_non_existing_page_should_return_404 + get :history, :project_id => 1, :id => 'Unknown_page' + assert_response 404 + end + + def test_add_attachment + @request.session[:user_id] = 2 + assert_difference 'Attachment.count' do + post :add_attachment, :project_id => 1, :id => 'CookBook_documentation', + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}} + end + attachment = Attachment.first(:order => 'id DESC') + assert_equal Wiki.find(1).find_page('CookBook_documentation'), attachment.container + end +end diff --git a/test/functional/wikis_controller_test.rb b/test/functional/wikis_controller_test.rb new file mode 100644 index 00000000..5ff26873 --- /dev/null +++ b/test/functional/wikis_controller_test.rb @@ -0,0 +1,83 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class WikisControllerTest < ActionController::TestCase + fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :wikis + + def setup + User.current = nil + end + + def test_create + @request.session[:user_id] = 1 + assert_nil Project.find(3).wiki + + assert_difference 'Wiki.count' do + xhr :post, :edit, :id => 3, :wiki => { :start_page => 'Start page' } + assert_response :success + assert_template 'edit' + assert_equal 'text/javascript', response.content_type + end + + wiki = Project.find(3).wiki + assert_not_nil wiki + assert_equal 'Start page', wiki.start_page + end + + def test_create_with_failure + @request.session[:user_id] = 1 + + assert_no_difference 'Wiki.count' do + xhr :post, :edit, :id => 3, :wiki => { :start_page => '' } + assert_response :success + assert_template 'edit' + assert_equal 'text/javascript', response.content_type + end + + assert_include 'errorExplanation', response.body + assert_include 'Start page can't be blank', response.body + end + + def test_update + @request.session[:user_id] = 1 + + assert_no_difference 'Wiki.count' do + xhr :post, :edit, :id => 1, :wiki => { :start_page => 'Other start page' } + assert_response :success + assert_template 'edit' + assert_equal 'text/javascript', response.content_type + end + + wiki = Project.find(1).wiki + assert_equal 'Other start page', wiki.start_page + end + + def test_destroy + @request.session[:user_id] = 1 + post :destroy, :id => 1, :confirm => 1 + assert_redirected_to :controller => 'projects', :action => 'settings', :id => 'ecookbook', :tab => 'wiki' + assert_nil Project.find(1).wiki + end + + def test_not_found + @request.session[:user_id] = 1 + post :destroy, :id => 999, :confirm => 1 + assert_response 404 + end +end diff --git a/test/functional/workflows_controller_test.rb b/test/functional/workflows_controller_test.rb new file mode 100644 index 00000000..747040c1 --- /dev/null +++ b/test/functional/workflows_controller_test.rb @@ -0,0 +1,328 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class WorkflowsControllerTest < ActionController::TestCase + fixtures :roles, :trackers, :workflows, :users, :issue_statuses + + def setup + User.current = nil + @request.session[:user_id] = 1 # admin + end + + def test_index + get :index + assert_response :success + assert_template 'index' + + count = WorkflowTransition.count(:all, :conditions => 'role_id = 1 AND tracker_id = 2') + assert_tag :tag => 'a', :content => count.to_s, + :attributes => { :href => '/workflows/edit?role_id=1&tracker_id=2' } + end + + def test_get_edit + get :edit + assert_response :success + assert_template 'edit' + assert_not_nil assigns(:roles) + assert_not_nil assigns(:trackers) + end + + def test_get_edit_with_role_and_tracker + WorkflowTransition.delete_all + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 2, :new_status_id => 3) + WorkflowTransition.create!(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 5) + + get :edit, :role_id => 2, :tracker_id => 1 + assert_response :success + assert_template 'edit' + + # used status only + assert_not_nil assigns(:statuses) + assert_equal [2, 3, 5], assigns(:statuses).collect(&:id) + + # allowed transitions + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'issue_status[3][5][]', + :value => 'always', + :checked => 'checked' } + # not allowed + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'issue_status[3][2][]', + :value => 'always', + :checked => nil } + # unused + assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'issue_status[1][1][]' } + end + + def test_get_edit_with_role_and_tracker_and_all_statuses + WorkflowTransition.delete_all + + get :edit, :role_id => 2, :tracker_id => 1, :used_statuses_only => '0' + assert_response :success + assert_template 'edit' + + assert_not_nil assigns(:statuses) + assert_equal IssueStatus.count, assigns(:statuses).size + + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'issue_status[1][1][]', + :value => 'always', + :checked => nil } + end + + def test_post_edit + post :edit, :role_id => 2, :tracker_id => 1, + :issue_status => { + '4' => {'5' => ['always']}, + '3' => {'1' => ['always'], '2' => ['always']} + } + assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1' + + assert_equal 3, WorkflowTransition.where(:tracker_id => 1, :role_id => 2).count + assert_not_nil WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2).first + assert_nil WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4).first + end + + def test_post_edit_with_additional_transitions + post :edit, :role_id => 2, :tracker_id => 1, + :issue_status => { + '4' => {'5' => ['always']}, + '3' => {'1' => ['author'], '2' => ['assignee'], '4' => ['author', 'assignee']} + } + assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1' + + assert_equal 4, WorkflowTransition.where(:tracker_id => 1, :role_id => 2).count + + w = WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 4, :new_status_id => 5).first + assert ! w.author + assert ! w.assignee + w = WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 1).first + assert w.author + assert ! w.assignee + w = WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2).first + assert ! w.author + assert w.assignee + w = WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 4).first + assert w.author + assert w.assignee + end + + def test_clear_workflow + assert WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2}) > 0 + + post :edit, :role_id => 2, :tracker_id => 1 + assert_equal 0, WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2}) + end + + def test_get_permissions + get :permissions + + assert_response :success + assert_template 'permissions' + assert_not_nil assigns(:roles) + assert_not_nil assigns(:trackers) + end + + def test_get_permissions_with_role_and_tracker + WorkflowPermission.delete_all + WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'assigned_to_id', :rule => 'required') + WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required') + WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 3, :field_name => 'fixed_version_id', :rule => 'readonly') + + get :permissions, :role_id => 1, :tracker_id => 2 + assert_response :success + assert_template 'permissions' + + assert_select 'input[name=role_id][value=1]' + assert_select 'input[name=tracker_id][value=2]' + + # Required field + assert_select 'select[name=?]', 'permissions[assigned_to_id][2]' do + assert_select 'option[value=]' + assert_select 'option[value=][selected=selected]', 0 + assert_select 'option[value=readonly]', :text => 'Read-only' + assert_select 'option[value=readonly][selected=selected]', 0 + assert_select 'option[value=required]', :text => 'Required' + assert_select 'option[value=required][selected=selected]' + end + + # Read-only field + assert_select 'select[name=?]', 'permissions[fixed_version_id][3]' do + assert_select 'option[value=]' + assert_select 'option[value=][selected=selected]', 0 + assert_select 'option[value=readonly]', :text => 'Read-only' + assert_select 'option[value=readonly][selected=selected]' + assert_select 'option[value=required]', :text => 'Required' + assert_select 'option[value=required][selected=selected]', 0 + end + + # Other field + assert_select 'select[name=?]', 'permissions[due_date][3]' do + assert_select 'option[value=]' + assert_select 'option[value=][selected=selected]', 0 + assert_select 'option[value=readonly]', :text => 'Read-only' + assert_select 'option[value=readonly][selected=selected]', 0 + assert_select 'option[value=required]', :text => 'Required' + assert_select 'option[value=required][selected=selected]', 0 + end + end + + def test_get_permissions_with_required_custom_field_should_not_show_required_option + cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :tracker_ids => [1], :is_required => true) + + get :permissions, :role_id => 1, :tracker_id => 1 + assert_response :success + assert_template 'permissions' + + # Custom field that is always required + # The default option is "(Required)" + assert_select 'select[name=?]', "permissions[#{cf.id}][3]" do + assert_select 'option[value=]' + assert_select 'option[value=readonly]', :text => 'Read-only' + assert_select 'option[value=required]', 0 + end + end + + def test_get_permissions_with_role_and_tracker_and_all_statuses + WorkflowTransition.delete_all + + get :permissions, :role_id => 1, :tracker_id => 2, :used_statuses_only => '0' + assert_response :success + assert_equal IssueStatus.sorted.all, assigns(:statuses) + end + + def test_post_permissions + WorkflowPermission.delete_all + + post :permissions, :role_id => 1, :tracker_id => 2, :permissions => { + 'assigned_to_id' => {'1' => '', '2' => 'readonly', '3' => ''}, + 'fixed_version_id' => {'1' => 'required', '2' => 'readonly', '3' => ''}, + 'due_date' => {'1' => '', '2' => '', '3' => ''}, + } + assert_redirected_to '/workflows/permissions?role_id=1&tracker_id=2' + + workflows = WorkflowPermission.all + assert_equal 3, workflows.size + workflows.each do |workflow| + assert_equal 1, workflow.role_id + assert_equal 2, workflow.tracker_id + end + assert workflows.detect {|wf| wf.old_status_id == 2 && wf.field_name == 'assigned_to_id' && wf.rule == 'readonly'} + assert workflows.detect {|wf| wf.old_status_id == 1 && wf.field_name == 'fixed_version_id' && wf.rule == 'required'} + assert workflows.detect {|wf| wf.old_status_id == 2 && wf.field_name == 'fixed_version_id' && wf.rule == 'readonly'} + end + + def test_post_permissions_should_clear_permissions + WorkflowPermission.delete_all + WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'assigned_to_id', :rule => 'required') + WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required') + wf1 = WorkflowPermission.create!(:role_id => 1, :tracker_id => 3, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required') + wf2 = WorkflowPermission.create!(:role_id => 2, :tracker_id => 2, :old_status_id => 3, :field_name => 'fixed_version_id', :rule => 'readonly') + + post :permissions, :role_id => 1, :tracker_id => 2 + assert_redirected_to '/workflows/permissions?role_id=1&tracker_id=2' + + workflows = WorkflowPermission.all + assert_equal 2, workflows.size + assert wf1.reload + assert wf2.reload + end + + def test_get_copy + get :copy + assert_response :success + assert_template 'copy' + assert_select 'select[name=source_tracker_id]' do + assert_select 'option[value=1]', :text => 'Bug' + end + assert_select 'select[name=source_role_id]' do + assert_select 'option[value=2]', :text => 'Developer' + end + assert_select 'select[name=?]', 'target_tracker_ids[]' do + assert_select 'option[value=3]', :text => 'Support request' + end + assert_select 'select[name=?]', 'target_role_ids[]' do + assert_select 'option[value=1]', :text => 'Manager' + end + end + + def test_post_copy_one_to_one + source_transitions = status_transitions(:tracker_id => 1, :role_id => 2) + + post :copy, :source_tracker_id => '1', :source_role_id => '2', + :target_tracker_ids => ['3'], :target_role_ids => ['1'] + assert_response 302 + assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1) + end + + def test_post_copy_one_to_many + source_transitions = status_transitions(:tracker_id => 1, :role_id => 2) + + post :copy, :source_tracker_id => '1', :source_role_id => '2', + :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3'] + assert_response 302 + assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 1) + assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1) + assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 3) + assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 3) + end + + def test_post_copy_many_to_many + source_t2 = status_transitions(:tracker_id => 2, :role_id => 2) + source_t3 = status_transitions(:tracker_id => 3, :role_id => 2) + + post :copy, :source_tracker_id => 'any', :source_role_id => '2', + :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3'] + assert_response 302 + assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 1) + assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 1) + assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 3) + assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 3) + end + + def test_post_copy_with_incomplete_source_specification_should_fail + assert_no_difference 'WorkflowRule.count' do + post :copy, + :source_tracker_id => '', :source_role_id => '2', + :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3'] + assert_response 200 + assert_select 'div.flash.error', :text => 'Please select a source tracker or role' + end + end + + def test_post_copy_with_incomplete_target_specification_should_fail + assert_no_difference 'WorkflowRule.count' do + post :copy, + :source_tracker_id => '1', :source_role_id => '2', + :target_tracker_ids => ['2', '3'] + assert_response 200 + assert_select 'div.flash.error', :text => 'Please select target tracker(s) and role(s)' + end + end + + # Returns an array of status transitions that can be compared + def status_transitions(conditions) + WorkflowTransition. + where(conditions). + order('tracker_id, role_id, old_status_id, new_status_id'). + all. + collect {|w| [w.old_status, w.new_status_id]} + end +end diff --git a/test/integration/account_test.rb b/test/integration/account_test.rb new file mode 100644 index 00000000..7562bcf7 --- /dev/null +++ b/test/integration/account_test.rb @@ -0,0 +1,212 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +begin + require 'mocha' +rescue + # Won't run some tests +end + +class AccountTest < ActionController::IntegrationTest + fixtures :users, :roles + + # Replace this with your real tests. + def test_login + get "my/page" + assert_redirected_to "/login?back_url=http%3A%2F%2Fwww.example.com%2Fmy%2Fpage" + log_user('jsmith', 'jsmith') + + get "my/account" + assert_response :success + assert_template "my/account" + end + + def test_autologin + user = User.find(1) + Setting.autologin = "7" + Token.delete_all + + # User logs in with 'autologin' checked + post '/login', :username => user.login, :password => 'admin', :autologin => 1 + assert_redirected_to '/my/page' + token = Token.first + assert_not_nil token + assert_equal user, token.user + assert_equal 'autologin', token.action + assert_equal user.id, session[:user_id] + assert_equal token.value, cookies['autologin'] + + # Session is cleared + reset! + User.current = nil + # Clears user's last login timestamp + user.update_attribute :last_login_on, nil + assert_nil user.reload.last_login_on + + # User comes back with his autologin cookie + cookies[:autologin] = token.value + get '/my/page' + assert_response :success + assert_template 'my/page' + assert_equal user.id, session[:user_id] + assert_not_nil user.reload.last_login_on + end + + def test_autologin_should_use_autologin_cookie_name + Token.delete_all + Redmine::Configuration.stubs(:[]).with('autologin_cookie_name').returns('custom_autologin') + Redmine::Configuration.stubs(:[]).with('autologin_cookie_path').returns('/') + Redmine::Configuration.stubs(:[]).with('autologin_cookie_secure').returns(false) + + with_settings :autologin => '7' do + assert_difference 'Token.count' do + post '/login', :username => 'admin', :password => 'admin', :autologin => 1 + end + assert_response 302 + assert cookies['custom_autologin'].present? + token = cookies['custom_autologin'] + + # Session is cleared + reset! + cookies['custom_autologin'] = token + get '/my/page' + assert_response :success + + assert_difference 'Token.count', -1 do + post '/logout' + end + assert cookies['custom_autologin'].blank? + end + end + + def test_lost_password + Token.delete_all + + get "account/lost_password" + assert_response :success + assert_template "account/lost_password" + assert_select 'input[name=mail]' + + post "account/lost_password", :mail => 'jSmith@somenet.foo' + assert_redirected_to "/login" + + token = Token.first + assert_equal 'recovery', token.action + assert_equal 'jsmith@somenet.foo', token.user.mail + assert !token.expired? + + get "account/lost_password", :token => token.value + assert_response :success + assert_template "account/password_recovery" + assert_select 'input[type=hidden][name=token][value=?]', token.value + assert_select 'input[name=new_password]' + assert_select 'input[name=new_password_confirmation]' + + post "account/lost_password", :token => token.value, :new_password => 'newpass123', :new_password_confirmation => 'newpass123' + assert_redirected_to "/login" + assert_equal 'Password was successfully updated.', flash[:notice] + + log_user('jsmith', 'newpass123') + assert_equal 0, Token.count + end + + def test_register_with_automatic_activation + Setting.self_registration = '3' + + get 'account/register' + assert_response :success + assert_template 'account/register' + + post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar", + :password => "newpass123", :password_confirmation => "newpass123"} + assert_redirected_to '/my/account' + follow_redirect! + assert_response :success + assert_template 'my/account' + + user = User.find_by_login('newuser') + assert_not_nil user + assert user.active? + assert_not_nil user.last_login_on + end + + def test_register_with_manual_activation + Setting.self_registration = '2' + + post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar", + :password => "newpass123", :password_confirmation => "newpass123"} + assert_redirected_to '/login' + assert !User.find_by_login('newuser').active? + end + + def test_register_with_email_activation + Setting.self_registration = '1' + Token.delete_all + + post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar", + :password => "newpass123", :password_confirmation => "newpass123"} + assert_redirected_to '/login' + assert !User.find_by_login('newuser').active? + + token = Token.first + assert_equal 'register', token.action + assert_equal 'newuser@foo.bar', token.user.mail + assert !token.expired? + + get 'account/activate', :token => token.value + assert_redirected_to '/login' + log_user('newuser', 'newpass123') + end + + def test_onthefly_registration + # disable registration + Setting.self_registration = '0' + AuthSource.expects(:authenticate).returns({:login => 'foo', :firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com', :auth_source_id => 66}) + + post '/login', :username => 'foo', :password => 'bar' + assert_redirected_to '/my/page' + + user = User.find_by_login('foo') + assert user.is_a?(User) + assert_equal 66, user.auth_source_id + assert user.hashed_password.blank? + end + + def test_onthefly_registration_with_invalid_attributes + # disable registration + Setting.self_registration = '0' + AuthSource.expects(:authenticate).returns({:login => 'foo', :lastname => 'Smith', :auth_source_id => 66}) + + post '/login', :username => 'foo', :password => 'bar' + assert_response :success + assert_template 'account/register' + assert_tag :input, :attributes => { :name => 'user[firstname]', :value => '' } + assert_tag :input, :attributes => { :name => 'user[lastname]', :value => 'Smith' } + assert_no_tag :input, :attributes => { :name => 'user[login]' } + assert_no_tag :input, :attributes => { :name => 'user[password]' } + + post 'account/register', :user => {:firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com'} + assert_redirected_to '/my/account' + + user = User.find_by_login('foo') + assert user.is_a?(User) + assert_equal 66, user.auth_source_id + assert user.hashed_password.blank? + end +end diff --git a/test/integration/admin_test.rb b/test/integration/admin_test.rb new file mode 100644 index 00000000..3dfd4a1c --- /dev/null +++ b/test/integration/admin_test.rb @@ -0,0 +1,61 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class AdminTest < ActionController::IntegrationTest + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules + + def test_add_user + log_user("admin", "admin") + get "/users/new" + assert_response :success + assert_template "users/new" + post "/users", + :user => { :login => "psmith", :firstname => "Paul", + :lastname => "Smith", :mail => "psmith@somenet.foo", + :language => "en", :password => "psmith09", + :password_confirmation => "psmith09" } + + user = User.find_by_login("psmith") + assert_kind_of User, user + assert_redirected_to "/users/#{ user.id }/edit" + + logged_user = User.try_to_login("psmith", "psmith09") + assert_kind_of User, logged_user + assert_equal "Paul", logged_user.firstname + + put "users/#{user.id}", :id => user.id, :user => { :status => User::STATUS_LOCKED } + assert_redirected_to "/users/#{ user.id }/edit" + locked_user = User.try_to_login("psmith", "psmith09") + assert_equal nil, locked_user + end + + test "Add a user as an anonymous user should fail" do + post '/users', + :user => { :login => 'psmith', :firstname => 'Paul'}, + :password => "psmith09", :password_confirmation => "psmith09" + assert_response :redirect + assert_redirected_to "/login?back_url=http%3A%2F%2Fwww.example.com%2Fusers" + end +end diff --git a/test/integration/api_test/attachments_test.rb b/test/integration/api_test/attachments_test.rb new file mode 100644 index 00000000..c8a0132a --- /dev/null +++ b/test/integration/api_test/attachments_test.rb @@ -0,0 +1,149 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::ApiTest::AttachmentsTest < Redmine::ApiTest::Base + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :attachments + + def setup + Setting.rest_api_enabled = '1' + set_fixtures_attachments_directory + end + + def teardown + set_tmp_attachments_directory + end + + test "GET /attachments/:id.xml should return the attachment" do + get '/attachments/7.xml', {}, credentials('jsmith') + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'attachment', + :child => { + :tag => 'id', + :content => '7', + :sibling => { + :tag => 'filename', + :content => 'archive.zip', + :sibling => { + :tag => 'content_url', + :content => 'http://www.example.com/attachments/download/7/archive.zip' + } + } + } + end + + test "GET /attachments/:id.xml should deny access without credentials" do + get '/attachments/7.xml' + assert_response 401 + set_tmp_attachments_directory + end + + test "GET /attachments/download/:id/:filename should return the attachment content" do + get '/attachments/download/7/archive.zip', {}, credentials('jsmith') + assert_response :success + assert_equal 'application/octet-stream', @response.content_type + set_tmp_attachments_directory + end + + test "GET /attachments/download/:id/:filename should deny access without credentials" do + get '/attachments/download/7/archive.zip' + assert_response 302 + set_tmp_attachments_directory + end + + test "POST /uploads.xml should return the token" do + set_tmp_attachments_directory + assert_difference 'Attachment.count' do + post '/uploads.xml', 'File content', {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith')) + assert_response :created + assert_equal 'application/xml', response.content_type + end + + xml = Hash.from_xml(response.body) + assert_kind_of Hash, xml['upload'] + token = xml['upload']['token'] + assert_not_nil token + + attachment = Attachment.first(:order => 'id DESC') + assert_equal token, attachment.token + assert_nil attachment.container + assert_equal 2, attachment.author_id + assert_equal 'File content'.size, attachment.filesize + assert attachment.content_type.blank? + assert attachment.filename.present? + assert_match /\d+_[0-9a-z]+/, attachment.diskfile + assert File.exist?(attachment.diskfile) + assert_equal 'File content', File.read(attachment.diskfile) + end + + test "POST /uploads.json should return the token" do + set_tmp_attachments_directory + assert_difference 'Attachment.count' do + post '/uploads.json', 'File content', {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith')) + assert_response :created + assert_equal 'application/json', response.content_type + end + + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json['upload'] + token = json['upload']['token'] + assert_not_nil token + + attachment = Attachment.first(:order => 'id DESC') + assert_equal token, attachment.token + end + + test "POST /uploads.xml should accept :filename param as the attachment filename" do + set_tmp_attachments_directory + assert_difference 'Attachment.count' do + post '/uploads.xml?filename=test.txt', 'File content', {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith')) + assert_response :created + end + + attachment = Attachment.order('id DESC').first + assert_equal 'test.txt', attachment.filename + assert_match /_test\.txt$/, attachment.diskfile + end + + test "POST /uploads.xml should not accept other content types" do + set_tmp_attachments_directory + assert_no_difference 'Attachment.count' do + post '/uploads.xml', 'PNG DATA', {"CONTENT_TYPE" => 'image/png'}.merge(credentials('jsmith')) + assert_response 406 + end + end + + test "POST /uploads.xml should return errors if file is too big" do + set_tmp_attachments_directory + with_settings :attachment_max_size => 1 do + assert_no_difference 'Attachment.count' do + post '/uploads.xml', ('x' * 2048), {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith')) + assert_response 422 + assert_tag 'error', :content => /exceeds the maximum allowed file size/ + end + end + end +end diff --git a/test/integration/api_test/authentication_test.rb b/test/integration/api_test/authentication_test.rb new file mode 100644 index 00000000..61b44d4d --- /dev/null +++ b/test/integration/api_test/authentication_test.rb @@ -0,0 +1,73 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::ApiTest::AuthenticationTest < Redmine::ApiTest::Base + fixtures :users + + def setup + Setting.rest_api_enabled = '1' + end + + def teardown + Setting.rest_api_enabled = '0' + end + + def test_api_request_should_not_use_user_session + log_user('jsmith', 'jsmith') + + get '/users/current' + assert_response :success + + get '/users/current.json' + assert_response 401 + end + + def test_api_should_accept_switch_user_header_for_admin_user + user = User.find(1) + su = User.find(4) + + get '/users/current', {}, {'X-Redmine-API-Key' => user.api_key, 'X-Redmine-Switch-User' => su.login} + assert_response :success + assert_equal su, assigns(:user) + assert_equal su, User.current + end + + def test_api_should_respond_with_412_when_trying_to_switch_to_a_invalid_user + get '/users/current', {}, {'X-Redmine-API-Key' => User.find(1).api_key, 'X-Redmine-Switch-User' => 'foobar'} + assert_response 412 + end + + def test_api_should_respond_with_412_when_trying_to_switch_to_a_locked_user + user = User.find(5) + assert user.locked? + + get '/users/current', {}, {'X-Redmine-API-Key' => User.find(1).api_key, 'X-Redmine-Switch-User' => user.login} + assert_response 412 + end + + def test_api_should_not_accept_switch_user_header_for_non_admin_user + user = User.find(2) + su = User.find(4) + + get '/users/current', {}, {'X-Redmine-API-Key' => user.api_key, 'X-Redmine-Switch-User' => su.login} + assert_response :success + assert_equal user, assigns(:user) + assert_equal user, User.current + end +end diff --git a/test/integration/api_test/disabled_rest_api_test.rb b/test/integration/api_test/disabled_rest_api_test.rb new file mode 100644 index 00000000..e40e9882 --- /dev/null +++ b/test/integration/api_test/disabled_rest_api_test.rb @@ -0,0 +1,78 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::ApiTest::DisabledRestApiTest < Redmine::ApiTest::Base + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules + + def setup + Setting.rest_api_enabled = '0' + Setting.login_required = '1' + end + + def teardown + Setting.rest_api_enabled = '1' + Setting.login_required = '0' + end + + def test_with_a_valid_api_token + @user = User.generate! + @token = Token.create!(:user => @user, :action => 'api') + + get "/news.xml?key=#{@token.value}" + assert_response :unauthorized + assert_equal User.anonymous, User.current + + get "/news.json?key=#{@token.value}" + assert_response :unauthorized + assert_equal User.anonymous, User.current + end + + def test_with_valid_username_password_http_authentication + @user = User.generate! do |user| + user.password = 'my_password' + end + + get "/news.xml", nil, credentials(@user.login, 'my_password') + assert_response :unauthorized + assert_equal User.anonymous, User.current + + get "/news.json", nil, credentials(@user.login, 'my_password') + assert_response :unauthorized + assert_equal User.anonymous, User.current + end + + def test_with_valid_token_http_authentication + @user = User.generate! + @token = Token.create!(:user => @user, :action => 'api') + + get "/news.xml", nil, credentials(@token.value, 'X') + assert_response :unauthorized + assert_equal User.anonymous, User.current + + get "/news.json", nil, credentials(@token.value, 'X') + assert_response :unauthorized + assert_equal User.anonymous, User.current + end +end diff --git a/test/integration/api_test/enumerations_test.rb b/test/integration/api_test/enumerations_test.rb new file mode 100644 index 00000000..cf9b8cba --- /dev/null +++ b/test/integration/api_test/enumerations_test.rb @@ -0,0 +1,44 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::ApiTest::EnumerationsTest < Redmine::ApiTest::Base + fixtures :enumerations + + def setup + Setting.rest_api_enabled = '1' + end + + context "/enumerations/issue_priorities" do + context "GET" do + + should "return priorities" do + get '/enumerations/issue_priorities.xml' + + assert_response :success + assert_equal 'application/xml', response.content_type + assert_select 'issue_priorities[type=array]' do + assert_select 'issue_priority' do + assert_select 'id', :text => '6' + assert_select 'name', :text => 'High' + end + end + end + end + end +end diff --git a/test/integration/api_test/groups_test.rb b/test/integration/api_test/groups_test.rb new file mode 100644 index 00000000..cedbb07b --- /dev/null +++ b/test/integration/api_test/groups_test.rb @@ -0,0 +1,212 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::ApiTest::GroupsTest < Redmine::ApiTest::Base + fixtures :users, :groups_users + + def setup + Setting.rest_api_enabled = '1' + end + + context "GET /groups" do + context ".xml" do + should "require authentication" do + get '/groups.xml' + assert_response 401 + end + + should "return groups" do + get '/groups.xml', {}, credentials('admin') + assert_response :success + assert_equal 'application/xml', response.content_type + + assert_select 'groups' do + assert_select 'group' do + assert_select 'name', :text => 'A Team' + assert_select 'id', :text => '10' + end + end + end + end + + context ".json" do + should "require authentication" do + get '/groups.json' + assert_response 401 + end + + should "return groups" do + get '/groups.json', {}, credentials('admin') + assert_response :success + assert_equal 'application/json', response.content_type + + json = MultiJson.load(response.body) + groups = json['groups'] + assert_kind_of Array, groups + group = groups.detect {|g| g['name'] == 'A Team'} + assert_not_nil group + assert_equal({'id' => 10, 'name' => 'A Team'}, group) + end + end + end + + context "GET /groups/:id" do + context ".xml" do + should "return the group with its users" do + get '/groups/10.xml', {}, credentials('admin') + assert_response :success + assert_equal 'application/xml', response.content_type + + assert_select 'group' do + assert_select 'name', :text => 'A Team' + assert_select 'id', :text => '10' + end + end + + should "include users if requested" do + get '/groups/10.xml?include=users', {}, credentials('admin') + assert_response :success + assert_equal 'application/xml', response.content_type + + assert_select 'group' do + assert_select 'users' do + assert_select 'user', Group.find(10).users.count + assert_select 'user[id=8]' + end + end + end + + should "include memberships if requested" do + get '/groups/10.xml?include=memberships', {}, credentials('admin') + assert_response :success + assert_equal 'application/xml', response.content_type + + assert_select 'group' do + assert_select 'memberships' + end + end + end + end + + context "POST /groups" do + context "with valid parameters" do + context ".xml" do + should "create groups" do + assert_difference('Group.count') do + post '/groups.xml', {:group => {:name => 'Test', :user_ids => [2, 3]}}, credentials('admin') + assert_response :created + assert_equal 'application/xml', response.content_type + end + + group = Group.order('id DESC').first + assert_equal 'Test', group.name + assert_equal [2, 3], group.users.map(&:id).sort + + assert_select 'group' do + assert_select 'name', :text => 'Test' + end + end + end + end + + context "with invalid parameters" do + context ".xml" do + should "return errors" do + assert_no_difference('Group.count') do + post '/groups.xml', {:group => {:name => ''}}, credentials('admin') + end + assert_response :unprocessable_entity + assert_equal 'application/xml', response.content_type + + assert_select 'errors' do + assert_select 'error', :text => /Name can't be blank/ + end + end + end + end + end + + context "PUT /groups/:id" do + context "with valid parameters" do + context ".xml" do + should "update the group" do + put '/groups/10.xml', {:group => {:name => 'New name', :user_ids => [2, 3]}}, credentials('admin') + assert_response :ok + assert_equal '', @response.body + + group = Group.find(10) + assert_equal 'New name', group.name + assert_equal [2, 3], group.users.map(&:id).sort + end + end + end + + context "with invalid parameters" do + context ".xml" do + should "return errors" do + put '/groups/10.xml', {:group => {:name => ''}}, credentials('admin') + assert_response :unprocessable_entity + assert_equal 'application/xml', response.content_type + + assert_select 'errors' do + assert_select 'error', :text => /Name can't be blank/ + end + end + end + end + end + + context "DELETE /groups/:id" do + context ".xml" do + should "delete the group" do + assert_difference 'Group.count', -1 do + delete '/groups/10.xml', {}, credentials('admin') + assert_response :ok + assert_equal '', @response.body + end + end + end + end + + context "POST /groups/:id/users" do + context ".xml" do + should "add user to the group" do + assert_difference 'Group.find(10).users.count' do + post '/groups/10/users.xml', {:user_id => 5}, credentials('admin') + assert_response :ok + assert_equal '', @response.body + end + assert_include User.find(5), Group.find(10).users + end + end + end + + context "DELETE /groups/:id/users/:user_id" do + context ".xml" do + should "remove user from the group" do + assert_difference 'Group.find(10).users.count', -1 do + delete '/groups/10/users/8.xml', {}, credentials('admin') + assert_response :ok + assert_equal '', @response.body + end + assert_not_include User.find(8), Group.find(10).users + end + end + end +end diff --git a/test/integration/api_test/http_basic_login_test.rb b/test/integration/api_test/http_basic_login_test.rb new file mode 100644 index 00000000..217c3ac6 --- /dev/null +++ b/test/integration/api_test/http_basic_login_test.rb @@ -0,0 +1,54 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::ApiTest::HttpBasicLoginTest < Redmine::ApiTest::Base + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules + + def setup + Setting.rest_api_enabled = '1' + Setting.login_required = '1' + end + + def teardown + Setting.rest_api_enabled = '0' + Setting.login_required = '0' + end + + # Using the NewsController because it's a simple API. + context "get /news" do + setup do + project = Project.find('onlinestore') + EnabledModule.create(:project => project, :name => 'news') + end + + context "in :xml format" do + should_allow_http_basic_auth_with_username_and_password(:get, "/projects/onlinestore/news.xml") + end + + context "in :json format" do + should_allow_http_basic_auth_with_username_and_password(:get, "/projects/onlinestore/news.json") + end + end +end diff --git a/test/integration/api_test/http_basic_login_with_api_token_test.rb b/test/integration/api_test/http_basic_login_with_api_token_test.rb new file mode 100644 index 00000000..cda237b7 --- /dev/null +++ b/test/integration/api_test/http_basic_login_with_api_token_test.rb @@ -0,0 +1,50 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::ApiTest::HttpBasicLoginWithApiTokenTest < Redmine::ApiTest::Base + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules + + def setup + Setting.rest_api_enabled = '1' + Setting.login_required = '1' + end + + def teardown + Setting.rest_api_enabled = '0' + Setting.login_required = '0' + end + + # Using the NewsController because it's a simple API. + context "get /news" do + + context "in :xml format" do + should_allow_http_basic_auth_with_key(:get, "/news.xml") + end + + context "in :json format" do + should_allow_http_basic_auth_with_key(:get, "/news.json") + end + end +end diff --git a/test/integration/api_test/issue_categories_test.rb b/test/integration/api_test/issue_categories_test.rb new file mode 100644 index 00000000..2644b391 --- /dev/null +++ b/test/integration/api_test/issue_categories_test.rb @@ -0,0 +1,126 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::ApiTest::IssueCategoriesTest < Redmine::ApiTest::Base + fixtures :projects, :users, :issue_categories, :issues, + :roles, + :member_roles, + :members, + :enabled_modules + + def setup + Setting.rest_api_enabled = '1' + end + + context "GET /projects/:project_id/issue_categories.xml" do + should "return issue categories" do + get '/projects/1/issue_categories.xml', {}, credentials('jsmith') + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'issue_categories', + :child => {:tag => 'issue_category', :child => {:tag => 'id', :content => '2'}} + end + end + + context "GET /issue_categories/2.xml" do + should "return requested issue category" do + get '/issue_categories/2.xml', {}, credentials('jsmith') + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'issue_category', + :child => {:tag => 'id', :content => '2'} + end + end + + context "POST /projects/:project_id/issue_categories.xml" do + should "return create issue category" do + assert_difference 'IssueCategory.count' do + post '/projects/1/issue_categories.xml', {:issue_category => {:name => 'API'}}, credentials('jsmith') + end + assert_response :created + assert_equal 'application/xml', @response.content_type + + category = IssueCategory.first(:order => 'id DESC') + assert_equal 'API', category.name + assert_equal 1, category.project_id + end + + context "with invalid parameters" do + should "return errors" do + assert_no_difference 'IssueCategory.count' do + post '/projects/1/issue_categories.xml', {:issue_category => {:name => ''}}, credentials('jsmith') + end + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + + assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"} + end + end + end + + context "PUT /issue_categories/2.xml" do + context "with valid parameters" do + should "update issue category" do + assert_no_difference 'IssueCategory.count' do + put '/issue_categories/2.xml', {:issue_category => {:name => 'API Update'}}, credentials('jsmith') + end + assert_response :ok + assert_equal '', @response.body + assert_equal 'API Update', IssueCategory.find(2).name + end + end + + context "with invalid parameters" do + should "return errors" do + assert_no_difference 'IssueCategory.count' do + put '/issue_categories/2.xml', {:issue_category => {:name => ''}}, credentials('jsmith') + end + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + + assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"} + end + end + end + + context "DELETE /issue_categories/1.xml" do + should "destroy issue categories" do + assert_difference 'IssueCategory.count', -1 do + delete '/issue_categories/1.xml', {}, credentials('jsmith') + end + assert_response :ok + assert_equal '', @response.body + assert_nil IssueCategory.find_by_id(1) + end + + should "reassign issues with :reassign_to_id param" do + issue_count = Issue.count(:conditions => {:category_id => 1}) + assert issue_count > 0 + + assert_difference 'IssueCategory.count', -1 do + assert_difference 'Issue.count(:conditions => {:category_id => 2})', 3 do + delete '/issue_categories/1.xml', {:reassign_to_id => 2}, credentials('jsmith') + end + end + assert_response :ok + assert_equal '', @response.body + assert_nil IssueCategory.find_by_id(1) + end + end +end diff --git a/test/integration/api_test/issue_relations_test.rb b/test/integration/api_test/issue_relations_test.rb new file mode 100644 index 00000000..ceb4713b --- /dev/null +++ b/test/integration/api_test/issue_relations_test.rb @@ -0,0 +1,106 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::ApiTest::IssueRelationsTest < Redmine::ApiTest::Base + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :issue_relations + + def setup + Setting.rest_api_enabled = '1' + end + + context "/issues/:issue_id/relations" do + context "GET" do + should "return issue relations" do + get '/issues/9/relations.xml', {}, credentials('jsmith') + + assert_response :success + assert_equal 'application/xml', @response.content_type + + assert_tag :tag => 'relations', + :attributes => { :type => 'array' }, + :child => { + :tag => 'relation', + :child => { + :tag => 'id', + :content => '1' + } + } + end + end + + context "POST" do + should "create a relation" do + assert_difference('IssueRelation.count') do + post '/issues/2/relations.xml', {:relation => {:issue_to_id => 7, :relation_type => 'relates'}}, credentials('jsmith') + end + + relation = IssueRelation.first(:order => 'id DESC') + assert_equal 2, relation.issue_from_id + assert_equal 7, relation.issue_to_id + assert_equal 'relates', relation.relation_type + + assert_response :created + assert_equal 'application/xml', @response.content_type + assert_tag 'relation', :child => {:tag => 'id', :content => relation.id.to_s} + end + + context "with failure" do + should "return the errors" do + assert_no_difference('IssueRelation.count') do + post '/issues/2/relations.xml', {:relation => {:issue_to_id => 7, :relation_type => 'foo'}}, credentials('jsmith') + end + + assert_response :unprocessable_entity + assert_tag :errors, :child => {:tag => 'error', :content => /relation_type is not included in the list/} + end + end + end + end + + context "/relations/:id" do + context "GET" do + should "return the relation" do + get '/relations/2.xml', {}, credentials('jsmith') + + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag 'relation', :child => {:tag => 'id', :content => '2'} + end + end + + context "DELETE" do + should "delete the relation" do + assert_difference('IssueRelation.count', -1) do + delete '/relations/2.xml', {}, credentials('jsmith') + end + + assert_response :ok + assert_equal '', @response.body + assert_nil IssueRelation.find_by_id(2) + end + end + end +end diff --git a/test/integration/api_test/issue_statuses_test.rb b/test/integration/api_test/issue_statuses_test.rb new file mode 100644 index 00000000..a17a25a5 --- /dev/null +++ b/test/integration/api_test/issue_statuses_test.rb @@ -0,0 +1,51 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::ApiTest::IssueStatusesTest < Redmine::ApiTest::Base + fixtures :issue_statuses + + def setup + Setting.rest_api_enabled = '1' + end + + context "/issue_statuses" do + context "GET" do + + should "return issue statuses" do + get '/issue_statuses.xml' + + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'issue_statuses', + :attributes => {:type => 'array'}, + :child => { + :tag => 'issue_status', + :child => { + :tag => 'id', + :content => '2', + :sibling => { + :tag => 'name', + :content => 'Assigned' + } + } + } + end + end + end +end diff --git a/test/integration/api_test/issues_test.rb b/test/integration/api_test/issues_test.rb new file mode 100644 index 00000000..53cc954f --- /dev/null +++ b/test/integration/api_test/issues_test.rb @@ -0,0 +1,846 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::ApiTest::IssuesTest < Redmine::ApiTest::Base + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :issue_relations, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries, + :attachments + + def setup + Setting.rest_api_enabled = '1' + end + + context "/issues" do + # Use a private project to make sure auth is really working and not just + # only showing public issues. + should_allow_api_authentication(:get, "/projects/private-child/issues.xml") + + should "contain metadata" do + get '/issues.xml' + + assert_tag :tag => 'issues', + :attributes => { + :type => 'array', + :total_count => assigns(:issue_count), + :limit => 25, + :offset => 0 + } + end + + context "with offset and limit" do + should "use the params" do + get '/issues.xml?offset=2&limit=3' + + assert_equal 3, assigns(:limit) + assert_equal 2, assigns(:offset) + assert_tag :tag => 'issues', :children => {:count => 3, :only => {:tag => 'issue'}} + end + end + + context "with nometa param" do + should "not contain metadata" do + get '/issues.xml?nometa=1' + + assert_tag :tag => 'issues', + :attributes => { + :type => 'array', + :total_count => nil, + :limit => nil, + :offset => nil + } + end + end + + context "with nometa header" do + should "not contain metadata" do + get '/issues.xml', {}, {'X-Redmine-Nometa' => '1'} + + assert_tag :tag => 'issues', + :attributes => { + :type => 'array', + :total_count => nil, + :limit => nil, + :offset => nil + } + end + end + + context "with relations" do + should "display relations" do + get '/issues.xml?include=relations' + + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag 'relations', + :parent => {:tag => 'issue', :child => {:tag => 'id', :content => '3'}}, + :children => {:count => 1}, + :child => { + :tag => 'relation', + :attributes => {:id => '2', :issue_id => '2', :issue_to_id => '3', + :relation_type => 'relates'} + } + assert_tag 'relations', + :parent => {:tag => 'issue', :child => {:tag => 'id', :content => '1'}}, + :children => {:count => 0} + end + end + + context "with invalid query params" do + should "return errors" do + get '/issues.xml', {:f => ['start_date'], :op => {:start_date => '='}} + + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + assert_tag 'errors', :child => {:tag => 'error', :content => "Start date can't be blank"} + end + end + + context "with custom field filter" do + should "show only issues with the custom field value" do + get '/issues.xml', + {:set_filter => 1, :f => ['cf_1'], :op => {:cf_1 => '='}, + :v => {:cf_1 => ['MySQL']}} + expected_ids = Issue.visible.all( + :include => :custom_values, + :conditions => {:custom_values => {:custom_field_id => 1, :value => 'MySQL'}}).map(&:id) + assert_select 'issues > issue > id', :count => expected_ids.count do |ids| + ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) } + end + end + end + + context "with custom field filter (shorthand method)" do + should "show only issues with the custom field value" do + get '/issues.xml', { :cf_1 => 'MySQL' } + + expected_ids = Issue.visible.all( + :include => :custom_values, + :conditions => {:custom_values => {:custom_field_id => 1, :value => 'MySQL'}}).map(&:id) + + assert_select 'issues > issue > id', :count => expected_ids.count do |ids| + ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) } + end + end + end + end + + context "/index.json" do + should_allow_api_authentication(:get, "/projects/private-child/issues.json") + end + + context "/index.xml with filter" do + should "show only issues with the status_id" do + get '/issues.xml?status_id=5' + + expected_ids = Issue.visible.all(:conditions => {:status_id => 5}).map(&:id) + + assert_select 'issues > issue > id', :count => expected_ids.count do |ids| + ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) } + end + end + end + + context "/index.json with filter" do + should "show only issues with the status_id" do + get '/issues.json?status_id=5' + + json = ActiveSupport::JSON.decode(response.body) + status_ids_used = json['issues'].collect {|j| j['status']['id'] } + assert_equal 3, status_ids_used.length + assert status_ids_used.all? {|id| id == 5 } + end + + end + + # Issue 6 is on a private project + context "/issues/6.xml" do + should_allow_api_authentication(:get, "/issues/6.xml") + end + + context "/issues/6.json" do + should_allow_api_authentication(:get, "/issues/6.json") + end + + context "GET /issues/:id" do + context "with journals" do + context ".xml" do + should "display journals" do + get '/issues/1.xml?include=journals' + + assert_tag :tag => 'issue', + :child => { + :tag => 'journals', + :attributes => { :type => 'array' }, + :child => { + :tag => 'journal', + :attributes => { :id => '1'}, + :child => { + :tag => 'details', + :attributes => { :type => 'array' }, + :child => { + :tag => 'detail', + :attributes => { :name => 'status_id' }, + :child => { + :tag => 'old_value', + :content => '1', + :sibling => { + :tag => 'new_value', + :content => '2' + } + } + } + } + } + } + end + end + end + + context "with custom fields" do + context ".xml" do + should "display custom fields" do + get '/issues/3.xml' + + assert_tag :tag => 'issue', + :child => { + :tag => 'custom_fields', + :attributes => { :type => 'array' }, + :child => { + :tag => 'custom_field', + :attributes => { :id => '1'}, + :child => { + :tag => 'value', + :content => 'MySQL' + } + } + } + + assert_nothing_raised do + Hash.from_xml(response.body).to_xml + end + end + end + end + + context "with multi custom fields" do + setup do + field = CustomField.find(1) + field.update_attribute :multiple, true + issue = Issue.find(3) + issue.custom_field_values = {1 => ['MySQL', 'Oracle']} + issue.save! + end + + context ".xml" do + should "display custom fields" do + get '/issues/3.xml' + assert_response :success + assert_tag :tag => 'issue', + :child => { + :tag => 'custom_fields', + :attributes => { :type => 'array' }, + :child => { + :tag => 'custom_field', + :attributes => { :id => '1'}, + :child => { + :tag => 'value', + :attributes => { :type => 'array' }, + :children => { :count => 2 } + } + } + } + + xml = Hash.from_xml(response.body) + custom_fields = xml['issue']['custom_fields'] + assert_kind_of Array, custom_fields + field = custom_fields.detect {|f| f['id'] == '1'} + assert_kind_of Hash, field + assert_equal ['MySQL', 'Oracle'], field['value'].sort + end + end + + context ".json" do + should "display custom fields" do + get '/issues/3.json' + assert_response :success + json = ActiveSupport::JSON.decode(response.body) + custom_fields = json['issue']['custom_fields'] + assert_kind_of Array, custom_fields + field = custom_fields.detect {|f| f['id'] == 1} + assert_kind_of Hash, field + assert_equal ['MySQL', 'Oracle'], field['value'].sort + end + end + end + + context "with empty value for multi custom field" do + setup do + field = CustomField.find(1) + field.update_attribute :multiple, true + issue = Issue.find(3) + issue.custom_field_values = {1 => ['']} + issue.save! + end + + context ".xml" do + should "display custom fields" do + get '/issues/3.xml' + assert_response :success + assert_tag :tag => 'issue', + :child => { + :tag => 'custom_fields', + :attributes => { :type => 'array' }, + :child => { + :tag => 'custom_field', + :attributes => { :id => '1'}, + :child => { + :tag => 'value', + :attributes => { :type => 'array' }, + :children => { :count => 0 } + } + } + } + + xml = Hash.from_xml(response.body) + custom_fields = xml['issue']['custom_fields'] + assert_kind_of Array, custom_fields + field = custom_fields.detect {|f| f['id'] == '1'} + assert_kind_of Hash, field + assert_equal [], field['value'] + end + end + + context ".json" do + should "display custom fields" do + get '/issues/3.json' + assert_response :success + json = ActiveSupport::JSON.decode(response.body) + custom_fields = json['issue']['custom_fields'] + assert_kind_of Array, custom_fields + field = custom_fields.detect {|f| f['id'] == 1} + assert_kind_of Hash, field + assert_equal [], field['value'].sort + end + end + end + + context "with attachments" do + context ".xml" do + should "display attachments" do + get '/issues/3.xml?include=attachments' + + assert_tag :tag => 'issue', + :child => { + :tag => 'attachments', + :children => {:count => 5}, + :child => { + :tag => 'attachment', + :child => { + :tag => 'filename', + :content => 'source.rb', + :sibling => { + :tag => 'content_url', + :content => 'http://www.example.com/attachments/download/4/source.rb' + } + } + } + } + end + end + end + + context "with subtasks" do + setup do + @c1 = Issue.create!( + :status_id => 1, :subject => "child c1", + :tracker_id => 1, :project_id => 1, :author_id => 1, + :parent_issue_id => 1 + ) + @c2 = Issue.create!( + :status_id => 1, :subject => "child c2", + :tracker_id => 1, :project_id => 1, :author_id => 1, + :parent_issue_id => 1 + ) + @c3 = Issue.create!( + :status_id => 1, :subject => "child c3", + :tracker_id => 1, :project_id => 1, :author_id => 1, + :parent_issue_id => @c1.id + ) + end + + context ".xml" do + should "display children" do + get '/issues/1.xml?include=children' + + assert_tag :tag => 'issue', + :child => { + :tag => 'children', + :children => {:count => 2}, + :child => { + :tag => 'issue', + :attributes => {:id => @c1.id.to_s}, + :child => { + :tag => 'subject', + :content => 'child c1', + :sibling => { + :tag => 'children', + :children => {:count => 1}, + :child => { + :tag => 'issue', + :attributes => {:id => @c3.id.to_s} + } + } + } + } + } + end + + context ".json" do + should "display children" do + get '/issues/1.json?include=children' + + json = ActiveSupport::JSON.decode(response.body) + assert_equal([ + { + 'id' => @c1.id, 'subject' => 'child c1', 'tracker' => {'id' => 1, 'name' => 'Bug'}, + 'children' => [{'id' => @c3.id, 'subject' => 'child c3', + 'tracker' => {'id' => 1, 'name' => 'Bug'} }] + }, + { 'id' => @c2.id, 'subject' => 'child c2', 'tracker' => {'id' => 1, 'name' => 'Bug'} } + ], + json['issue']['children']) + end + end + end + end + end + + test "GET /issues/:id.xml?include=watchers should include watchers" do + Watcher.create!(:user_id => 3, :watchable => Issue.find(1)) + + get '/issues/1.xml?include=watchers', {}, credentials('jsmith') + + assert_response :ok + assert_equal 'application/xml', response.content_type + assert_select 'issue' do + assert_select 'watchers', Issue.find(1).watchers.count + assert_select 'watchers' do + assert_select 'user[id=3]' + end + end + end + + context "POST /issues.xml" do + should_allow_api_authentication( + :post, + '/issues.xml', + {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, + {:success_code => :created} + ) + should "create an issue with the attributes" do + assert_difference('Issue.count') do + post '/issues.xml', + {:issue => {:project_id => 1, :subject => 'API test', + :tracker_id => 2, :status_id => 3}}, credentials('jsmith') + end + issue = Issue.first(:order => 'id DESC') + assert_equal 1, issue.project_id + assert_equal 2, issue.tracker_id + assert_equal 3, issue.status_id + assert_equal 'API test', issue.subject + + assert_response :created + assert_equal 'application/xml', @response.content_type + assert_tag 'issue', :child => {:tag => 'id', :content => issue.id.to_s} + end + end + + test "POST /issues.xml with watcher_user_ids should create issue with watchers" do + assert_difference('Issue.count') do + post '/issues.xml', + {:issue => {:project_id => 1, :subject => 'Watchers', + :tracker_id => 2, :status_id => 3, :watcher_user_ids => [3, 1]}}, credentials('jsmith') + assert_response :created + end + issue = Issue.order('id desc').first + assert_equal 2, issue.watchers.size + assert_equal [1, 3], issue.watcher_user_ids.sort + end + + context "POST /issues.xml with failure" do + should "have an errors tag" do + assert_no_difference('Issue.count') do + post '/issues.xml', {:issue => {:project_id => 1}}, credentials('jsmith') + end + + assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"} + end + end + + context "POST /issues.json" do + should_allow_api_authentication(:post, + '/issues.json', + {:issue => {:project_id => 1, :subject => 'API test', + :tracker_id => 2, :status_id => 3}}, + {:success_code => :created}) + + should "create an issue with the attributes" do + assert_difference('Issue.count') do + post '/issues.json', + {:issue => {:project_id => 1, :subject => 'API test', + :tracker_id => 2, :status_id => 3}}, + credentials('jsmith') + end + + issue = Issue.first(:order => 'id DESC') + assert_equal 1, issue.project_id + assert_equal 2, issue.tracker_id + assert_equal 3, issue.status_id + assert_equal 'API test', issue.subject + end + + end + + context "POST /issues.json with failure" do + should "have an errors element" do + assert_no_difference('Issue.count') do + post '/issues.json', {:issue => {:project_id => 1}}, credentials('jsmith') + end + + json = ActiveSupport::JSON.decode(response.body) + assert json['errors'].include?("Subject can't be blank") + end + end + + # Issue 6 is on a private project + context "PUT /issues/6.xml" do + setup do + @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}} + end + + should_allow_api_authentication(:put, + '/issues/6.xml', + {:issue => {:subject => 'API update', :notes => 'A new note'}}, + {:success_code => :ok}) + + should "not create a new issue" do + assert_no_difference('Issue.count') do + put '/issues/6.xml', @parameters, credentials('jsmith') + end + end + + should "create a new journal" do + assert_difference('Journal.count') do + put '/issues/6.xml', @parameters, credentials('jsmith') + end + end + + should "add the note to the journal" do + put '/issues/6.xml', @parameters, credentials('jsmith') + + journal = Journal.last + assert_equal "A new note", journal.notes + end + + should "update the issue" do + put '/issues/6.xml', @parameters, credentials('jsmith') + + issue = Issue.find(6) + assert_equal "API update", issue.subject + end + + end + + context "PUT /issues/3.xml with custom fields" do + setup do + @parameters = { + :issue => {:custom_fields => [{'id' => '1', 'value' => 'PostgreSQL' }, + {'id' => '2', 'value' => '150'}]} + } + end + + should "update custom fields" do + assert_no_difference('Issue.count') do + put '/issues/3.xml', @parameters, credentials('jsmith') + end + + issue = Issue.find(3) + assert_equal '150', issue.custom_value_for(2).value + assert_equal 'PostgreSQL', issue.custom_value_for(1).value + end + end + + context "PUT /issues/3.xml with multi custom fields" do + setup do + field = CustomField.find(1) + field.update_attribute :multiple, true + @parameters = { + :issue => {:custom_fields => [{'id' => '1', 'value' => ['MySQL', 'PostgreSQL'] }, + {'id' => '2', 'value' => '150'}]} + } + end + + should "update custom fields" do + assert_no_difference('Issue.count') do + put '/issues/3.xml', @parameters, credentials('jsmith') + end + + issue = Issue.find(3) + assert_equal '150', issue.custom_value_for(2).value + assert_equal ['MySQL', 'PostgreSQL'], issue.custom_field_value(1).sort + end + end + + context "PUT /issues/3.xml with project change" do + setup do + @parameters = {:issue => {:project_id => 2, :subject => 'Project changed'}} + end + + should "update project" do + assert_no_difference('Issue.count') do + put '/issues/3.xml', @parameters, credentials('jsmith') + end + + issue = Issue.find(3) + assert_equal 2, issue.project_id + assert_equal 'Project changed', issue.subject + end + end + + context "PUT /issues/6.xml with failed update" do + setup do + @parameters = {:issue => {:subject => ''}} + end + + should "not create a new issue" do + assert_no_difference('Issue.count') do + put '/issues/6.xml', @parameters, credentials('jsmith') + end + end + + should "not create a new journal" do + assert_no_difference('Journal.count') do + put '/issues/6.xml', @parameters, credentials('jsmith') + end + end + + should "have an errors tag" do + put '/issues/6.xml', @parameters, credentials('jsmith') + + assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"} + end + end + + context "PUT /issues/6.json" do + setup do + @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}} + end + + should_allow_api_authentication(:put, + '/issues/6.json', + {:issue => {:subject => 'API update', :notes => 'A new note'}}, + {:success_code => :ok}) + + should "update the issue" do + assert_no_difference('Issue.count') do + assert_difference('Journal.count') do + put '/issues/6.json', @parameters, credentials('jsmith') + + assert_response :ok + assert_equal '', response.body + end + end + + issue = Issue.find(6) + assert_equal "API update", issue.subject + journal = Journal.last + assert_equal "A new note", journal.notes + end + end + + context "PUT /issues/6.json with failed update" do + should "return errors" do + assert_no_difference('Issue.count') do + assert_no_difference('Journal.count') do + put '/issues/6.json', {:issue => {:subject => ''}}, credentials('jsmith') + + assert_response :unprocessable_entity + end + end + + json = ActiveSupport::JSON.decode(response.body) + assert json['errors'].include?("Subject can't be blank") + end + end + + context "DELETE /issues/1.xml" do + should_allow_api_authentication(:delete, + '/issues/6.xml', + {}, + {:success_code => :ok}) + + should "delete the issue" do + assert_difference('Issue.count', -1) do + delete '/issues/6.xml', {}, credentials('jsmith') + + assert_response :ok + assert_equal '', response.body + end + + assert_nil Issue.find_by_id(6) + end + end + + context "DELETE /issues/1.json" do + should_allow_api_authentication(:delete, + '/issues/6.json', + {}, + {:success_code => :ok}) + + should "delete the issue" do + assert_difference('Issue.count', -1) do + delete '/issues/6.json', {}, credentials('jsmith') + + assert_response :ok + assert_equal '', response.body + end + + assert_nil Issue.find_by_id(6) + end + end + + test "POST /issues/:id/watchers.xml should add watcher" do + assert_difference 'Watcher.count' do + post '/issues/1/watchers.xml', {:user_id => 3}, credentials('jsmith') + + assert_response :ok + assert_equal '', response.body + end + watcher = Watcher.order('id desc').first + assert_equal Issue.find(1), watcher.watchable + assert_equal User.find(3), watcher.user + end + + test "DELETE /issues/:id/watchers/:user_id.xml should remove watcher" do + Watcher.create!(:user_id => 3, :watchable => Issue.find(1)) + + assert_difference 'Watcher.count', -1 do + delete '/issues/1/watchers/3.xml', {}, credentials('jsmith') + + assert_response :ok + assert_equal '', response.body + end + assert_equal false, Issue.find(1).watched_by?(User.find(3)) + end + + def test_create_issue_with_uploaded_file + set_tmp_attachments_directory + # upload the file + assert_difference 'Attachment.count' do + post '/uploads.xml', 'test_create_with_upload', + {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith')) + assert_response :created + end + xml = Hash.from_xml(response.body) + token = xml['upload']['token'] + attachment = Attachment.first(:order => 'id DESC') + + # create the issue with the upload's token + assert_difference 'Issue.count' do + post '/issues.xml', + {:issue => {:project_id => 1, :subject => 'Uploaded file', + :uploads => [{:token => token, :filename => 'test.txt', + :content_type => 'text/plain'}]}}, + credentials('jsmith') + assert_response :created + end + issue = Issue.first(:order => 'id DESC') + assert_equal 1, issue.attachments.count + assert_equal attachment, issue.attachments.first + + attachment.reload + assert_equal 'test.txt', attachment.filename + assert_equal 'text/plain', attachment.content_type + assert_equal 'test_create_with_upload'.size, attachment.filesize + assert_equal 2, attachment.author_id + + # get the issue with its attachments + get "/issues/#{issue.id}.xml", :include => 'attachments' + assert_response :success + xml = Hash.from_xml(response.body) + attachments = xml['issue']['attachments'] + assert_kind_of Array, attachments + assert_equal 1, attachments.size + url = attachments.first['content_url'] + assert_not_nil url + + # download the attachment + get url + assert_response :success + end + + def test_update_issue_with_uploaded_file + set_tmp_attachments_directory + # upload the file + assert_difference 'Attachment.count' do + post '/uploads.xml', 'test_upload_with_upload', + {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith')) + assert_response :created + end + xml = Hash.from_xml(response.body) + token = xml['upload']['token'] + attachment = Attachment.first(:order => 'id DESC') + + # update the issue with the upload's token + assert_difference 'Journal.count' do + put '/issues/1.xml', + {:issue => {:notes => 'Attachment added', + :uploads => [{:token => token, :filename => 'test.txt', + :content_type => 'text/plain'}]}}, + credentials('jsmith') + assert_response :ok + assert_equal '', @response.body + end + + issue = Issue.find(1) + assert_include attachment, issue.attachments + end +end diff --git a/test/integration/api_test/jsonp_test.rb b/test/integration/api_test/jsonp_test.rb new file mode 100644 index 00000000..75ba0432 --- /dev/null +++ b/test/integration/api_test/jsonp_test.rb @@ -0,0 +1,72 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::ApiTest::JsonpTest < Redmine::ApiTest::Base + fixtures :trackers + + def test_should_ignore_jsonp_callback_with_jsonp_disabled + with_settings :jsonp_enabled => '0' do + get '/trackers.json?jsonp=handler' + end + + assert_response :success + assert_match %r{^\{"trackers":.+\}$}, response.body + assert_equal 'application/json; charset=utf-8', response.headers['Content-Type'] + end + + def test_jsonp_should_accept_callback_param + with_settings :jsonp_enabled => '1' do + get '/trackers.json?callback=handler' + end + + assert_response :success + assert_match %r{^handler\(\{"trackers":.+\}\)$}, response.body + assert_equal 'application/javascript; charset=utf-8', response.headers['Content-Type'] + end + + def test_jsonp_should_accept_jsonp_param + with_settings :jsonp_enabled => '1' do + get '/trackers.json?jsonp=handler' + end + + assert_response :success + assert_match %r{^handler\(\{"trackers":.+\}\)$}, response.body + assert_equal 'application/javascript; charset=utf-8', response.headers['Content-Type'] + end + + def test_jsonp_should_strip_invalid_characters_from_callback + with_settings :jsonp_enabled => '1' do + get '/trackers.json?callback=+-aA$1_' + end + + assert_response :success + assert_match %r{^aA1_\(\{"trackers":.+\}\)$}, response.body + assert_equal 'application/javascript; charset=utf-8', response.headers['Content-Type'] + end + + def test_jsonp_without_callback_should_return_json + with_settings :jsonp_enabled => '1' do + get '/trackers.json?callback=' + end + + assert_response :success + assert_match %r{^\{"trackers":.+\}$}, response.body + assert_equal 'application/json; charset=utf-8', response.headers['Content-Type'] + end +end diff --git a/test/integration/api_test/memberships_test.rb b/test/integration/api_test/memberships_test.rb new file mode 100644 index 00000000..ad8aa42e --- /dev/null +++ b/test/integration/api_test/memberships_test.rb @@ -0,0 +1,200 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::ApiTest::MembershipsTest < Redmine::ApiTest::Base + fixtures :projects, :users, :roles, :members, :member_roles + + def setup + Setting.rest_api_enabled = '1' + end + + context "/projects/:project_id/memberships" do + context "GET" do + context "xml" do + should "return memberships" do + get '/projects/1/memberships.xml', {}, credentials('jsmith') + + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'memberships', + :attributes => {:type => 'array'}, + :child => { + :tag => 'membership', + :child => { + :tag => 'id', + :content => '2', + :sibling => { + :tag => 'user', + :attributes => {:id => '3', :name => 'Dave Lopper'}, + :sibling => { + :tag => 'roles', + :child => { + :tag => 'role', + :attributes => {:id => '2', :name => 'Developer'} + } + } + } + } + } + end + end + + context "json" do + should "return memberships" do + get '/projects/1/memberships.json', {}, credentials('jsmith') + + assert_response :success + assert_equal 'application/json', @response.content_type + json = ActiveSupport::JSON.decode(response.body) + assert_equal({ + "memberships" => + [{"id"=>1, + "project" => {"name"=>"eCookbook", "id"=>1}, + "roles" => [{"name"=>"Manager", "id"=>1}], + "user" => {"name"=>"John Smith", "id"=>2}}, + {"id"=>2, + "project" => {"name"=>"eCookbook", "id"=>1}, + "roles" => [{"name"=>"Developer", "id"=>2}], + "user" => {"name"=>"Dave Lopper", "id"=>3}}], + "limit" => 25, + "total_count" => 2, + "offset" => 0}, + json) + end + end + end + + context "POST" do + context "xml" do + should "create membership" do + assert_difference 'Member.count' do + post '/projects/1/memberships.xml', {:membership => {:user_id => 7, :role_ids => [2,3]}}, credentials('jsmith') + + assert_response :created + end + end + + should "return errors on failure" do + assert_no_difference 'Member.count' do + post '/projects/1/memberships.xml', {:membership => {:role_ids => [2,3]}}, credentials('jsmith') + + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + assert_tag 'errors', :child => {:tag => 'error', :content => "Principal can't be blank"} + end + end + end + end + end + + context "/memberships/:id" do + context "GET" do + context "xml" do + should "return the membership" do + get '/memberships/2.xml', {}, credentials('jsmith') + + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'membership', + :child => { + :tag => 'id', + :content => '2', + :sibling => { + :tag => 'user', + :attributes => {:id => '3', :name => 'Dave Lopper'}, + :sibling => { + :tag => 'roles', + :child => { + :tag => 'role', + :attributes => {:id => '2', :name => 'Developer'} + } + } + } + } + end + end + + context "json" do + should "return the membership" do + get '/memberships/2.json', {}, credentials('jsmith') + + assert_response :success + assert_equal 'application/json', @response.content_type + json = ActiveSupport::JSON.decode(response.body) + assert_equal( + {"membership" => { + "id" => 2, + "project" => {"name"=>"eCookbook", "id"=>1}, + "roles" => [{"name"=>"Developer", "id"=>2}], + "user" => {"name"=>"Dave Lopper", "id"=>3}} + }, + json) + end + end + end + + context "PUT" do + context "xml" do + should "update membership" do + assert_not_equal [1,2], Member.find(2).role_ids.sort + assert_no_difference 'Member.count' do + put '/memberships/2.xml', {:membership => {:user_id => 3, :role_ids => [1,2]}}, credentials('jsmith') + + assert_response :ok + assert_equal '', @response.body + end + member = Member.find(2) + assert_equal [1,2], member.role_ids.sort + end + + should "return errors on failure" do + put '/memberships/2.xml', {:membership => {:user_id => 3, :role_ids => [99]}}, credentials('jsmith') + + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + assert_tag 'errors', :child => {:tag => 'error', :content => /member_roles is invalid/} + end + end + end + + context "DELETE" do + context "xml" do + should "destroy membership" do + assert_difference 'Member.count', -1 do + delete '/memberships/2.xml', {}, credentials('jsmith') + + assert_response :ok + assert_equal '', @response.body + end + assert_nil Member.find_by_id(2) + end + + should "respond with 422 on failure" do + assert_no_difference 'Member.count' do + # A membership with an inherited role can't be deleted + Member.find(2).member_roles.first.update_attribute :inherited_from, 99 + delete '/memberships/2.xml', {}, credentials('jsmith') + + assert_response :unprocessable_entity + end + end + end + end + end +end diff --git a/test/integration/api_test/news_test.rb b/test/integration/api_test/news_test.rb new file mode 100644 index 00000000..0319b46b --- /dev/null +++ b/test/integration/api_test/news_test.rb @@ -0,0 +1,97 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::ApiTest::NewsTest < Redmine::ApiTest::Base + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :news + + def setup + Setting.rest_api_enabled = '1' + end + + context "GET /news" do + context ".xml" do + should "return news" do + get '/news.xml' + + assert_tag :tag => 'news', + :attributes => {:type => 'array'}, + :child => { + :tag => 'news', + :child => { + :tag => 'id', + :content => '2' + } + } + end + end + + context ".json" do + should "return news" do + get '/news.json' + + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json + assert_kind_of Array, json['news'] + assert_kind_of Hash, json['news'].first + assert_equal 2, json['news'].first['id'] + end + end + end + + context "GET /projects/:project_id/news" do + context ".xml" do + should_allow_api_authentication(:get, "/projects/onlinestore/news.xml") + + should "return news" do + get '/projects/ecookbook/news.xml' + + assert_tag :tag => 'news', + :attributes => {:type => 'array'}, + :child => { + :tag => 'news', + :child => { + :tag => 'id', + :content => '2' + } + } + end + end + + context ".json" do + should_allow_api_authentication(:get, "/projects/onlinestore/news.json") + + should "return news" do + get '/projects/ecookbook/news.json' + + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json + assert_kind_of Array, json['news'] + assert_kind_of Hash, json['news'].first + assert_equal 2, json['news'].first['id'] + end + end + end +end diff --git a/test/integration/api_test/projects_test.rb b/test/integration/api_test/projects_test.rb new file mode 100644 index 00000000..7a7d2c12 --- /dev/null +++ b/test/integration/api_test/projects_test.rb @@ -0,0 +1,297 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::ApiTest::ProjectsTest < Redmine::ApiTest::Base + fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details, + :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages, + :attachments, :custom_fields, :custom_values, :time_entries, :issue_categories + + def setup + Setting.rest_api_enabled = '1' + set_tmp_attachments_directory + end + + context "GET /projects" do + context ".xml" do + should "return projects" do + get '/projects.xml' + assert_response :success + assert_equal 'application/xml', @response.content_type + + assert_tag :tag => 'projects', + :child => {:tag => 'project', :child => {:tag => 'id', :content => '1'}} + end + end + + context ".json" do + should "return projects" do + get '/projects.json' + assert_response :success + assert_equal 'application/json', @response.content_type + + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json + assert_kind_of Array, json['projects'] + assert_kind_of Hash, json['projects'].first + assert json['projects'].first.has_key?('id') + end + end + end + + context "GET /projects/:id" do + context ".xml" do + # TODO: A private project is needed because should_allow_api_authentication + # actually tests that authentication is *required*, not just allowed + should_allow_api_authentication(:get, "/projects/2.xml") + + should "return requested project" do + get '/projects/1.xml' + assert_response :success + assert_equal 'application/xml', @response.content_type + + assert_tag :tag => 'project', + :child => {:tag => 'id', :content => '1'} + assert_tag :tag => 'custom_field', + :attributes => {:name => 'Development status'}, :content => 'Stable' + + assert_no_tag 'trackers' + assert_no_tag 'issue_categories' + end + + context "with hidden custom fields" do + setup do + ProjectCustomField.find_by_name('Development status').update_attribute :visible, false + end + + should "not display hidden custom fields" do + get '/projects/1.xml' + assert_response :success + assert_equal 'application/xml', @response.content_type + + assert_no_tag 'custom_field', + :attributes => {:name => 'Development status'} + end + end + + should "return categories with include=issue_categories" do + get '/projects/1.xml?include=issue_categories' + assert_response :success + assert_equal 'application/xml', @response.content_type + + assert_tag 'issue_categories', + :attributes => {:type => 'array'}, + :child => { + :tag => 'issue_category', + :attributes => { + :id => '2', + :name => 'Recipes' + } + } + end + + should "return trackers with include=trackers" do + get '/projects/1.xml?include=trackers' + assert_response :success + assert_equal 'application/xml', @response.content_type + + assert_tag 'trackers', + :attributes => {:type => 'array'}, + :child => { + :tag => 'tracker', + :attributes => { + :id => '2', + :name => 'Feature request' + } + } + end + end + + context ".json" do + should_allow_api_authentication(:get, "/projects/2.json") + + should "return requested project" do + get '/projects/1.json' + + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json + assert_kind_of Hash, json['project'] + assert_equal 1, json['project']['id'] + end + end + end + + context "POST /projects" do + context "with valid parameters" do + setup do + Setting.default_projects_modules = ['issue_tracking', 'repository'] + @parameters = {:project => {:name => 'API test', :identifier => 'api-test'}} + end + + context ".xml" do + should_allow_api_authentication(:post, + '/projects.xml', + {:project => {:name => 'API test', :identifier => 'api-test'}}, + {:success_code => :created}) + + + should "create a project with the attributes" do + assert_difference('Project.count') do + post '/projects.xml', @parameters, credentials('admin') + end + + project = Project.first(:order => 'id DESC') + assert_equal 'API test', project.name + assert_equal 'api-test', project.identifier + assert_equal ['issue_tracking', 'repository'], project.enabled_module_names.sort + assert_equal Tracker.all.size, project.trackers.size + + assert_response :created + assert_equal 'application/xml', @response.content_type + assert_tag 'project', :child => {:tag => 'id', :content => project.id.to_s} + end + + should "accept enabled_module_names attribute" do + @parameters[:project].merge!({:enabled_module_names => ['issue_tracking', 'news', 'time_tracking']}) + + assert_difference('Project.count') do + post '/projects.xml', @parameters, credentials('admin') + end + + project = Project.first(:order => 'id DESC') + assert_equal ['issue_tracking', 'news', 'time_tracking'], project.enabled_module_names.sort + end + + should "accept tracker_ids attribute" do + @parameters[:project].merge!({:tracker_ids => [1, 3]}) + + assert_difference('Project.count') do + post '/projects.xml', @parameters, credentials('admin') + end + + project = Project.first(:order => 'id DESC') + assert_equal [1, 3], project.trackers.map(&:id).sort + end + end + end + + context "with invalid parameters" do + setup do + @parameters = {:project => {:name => 'API test'}} + end + + context ".xml" do + should "return errors" do + assert_no_difference('Project.count') do + post '/projects.xml', @parameters, credentials('admin') + end + + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + assert_tag 'errors', :child => {:tag => 'error', :content => "Identifier can't be blank"} + end + end + end + end + + context "PUT /projects/:id" do + context "with valid parameters" do + setup do + @parameters = {:project => {:name => 'API update'}} + end + + context ".xml" do + should_allow_api_authentication(:put, + '/projects/2.xml', + {:project => {:name => 'API update'}}, + {:success_code => :ok}) + + should "update the project" do + assert_no_difference 'Project.count' do + put '/projects/2.xml', @parameters, credentials('jsmith') + end + assert_response :ok + assert_equal '', @response.body + assert_equal 'application/xml', @response.content_type + project = Project.find(2) + assert_equal 'API update', project.name + end + + should "accept enabled_module_names attribute" do + @parameters[:project].merge!({:enabled_module_names => ['issue_tracking', 'news', 'time_tracking']}) + + assert_no_difference 'Project.count' do + put '/projects/2.xml', @parameters, credentials('admin') + end + assert_response :ok + assert_equal '', @response.body + project = Project.find(2) + assert_equal ['issue_tracking', 'news', 'time_tracking'], project.enabled_module_names.sort + end + + should "accept tracker_ids attribute" do + @parameters[:project].merge!({:tracker_ids => [1, 3]}) + + assert_no_difference 'Project.count' do + put '/projects/2.xml', @parameters, credentials('admin') + end + assert_response :ok + assert_equal '', @response.body + project = Project.find(2) + assert_equal [1, 3], project.trackers.map(&:id).sort + end + end + end + + context "with invalid parameters" do + setup do + @parameters = {:project => {:name => ''}} + end + + context ".xml" do + should "return errors" do + assert_no_difference('Project.count') do + put '/projects/2.xml', @parameters, credentials('admin') + end + + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"} + end + end + end + end + + context "DELETE /projects/:id" do + context ".xml" do + should_allow_api_authentication(:delete, + '/projects/2.xml', + {}, + {:success_code => :ok}) + + should "delete the project" do + assert_difference('Project.count',-1) do + delete '/projects/2.xml', {}, credentials('admin') + end + assert_response :ok + assert_equal '', @response.body + assert_nil Project.find_by_id(2) + end + end + end +end diff --git a/test/integration/api_test/queries_test.rb b/test/integration/api_test/queries_test.rb new file mode 100644 index 00000000..e34f0480 --- /dev/null +++ b/test/integration/api_test/queries_test.rb @@ -0,0 +1,58 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::ApiTest::QueriesTest < Redmine::ApiTest::Base + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :queries + + def setup + Setting.rest_api_enabled = '1' + end + + context "/queries" do + context "GET" do + + should "return queries" do + get '/queries.xml' + + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'queries', + :attributes => {:type => 'array'}, + :child => { + :tag => 'query', + :child => { + :tag => 'id', + :content => '4', + :sibling => { + :tag => 'name', + :content => 'Public query for all projects' + } + } + } + end + end + end +end diff --git a/test/integration/api_test/roles_test.rb b/test/integration/api_test/roles_test.rb new file mode 100644 index 00000000..2d88f285 --- /dev/null +++ b/test/integration/api_test/roles_test.rb @@ -0,0 +1,90 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::ApiTest::RolesTest < Redmine::ApiTest::Base + fixtures :roles + + def setup + Setting.rest_api_enabled = '1' + end + + context "/roles" do + context "GET" do + context "xml" do + should "return the roles" do + get '/roles.xml' + + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_equal 3, assigns(:roles).size + + assert_tag :tag => 'roles', + :attributes => {:type => 'array'}, + :child => { + :tag => 'role', + :child => { + :tag => 'id', + :content => '2', + :sibling => { + :tag => 'name', + :content => 'Developer' + } + } + } + end + end + + context "json" do + should "return the roles" do + get '/roles.json' + + assert_response :success + assert_equal 'application/json', @response.content_type + assert_equal 3, assigns(:roles).size + + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json + assert_kind_of Array, json['roles'] + assert_include({'id' => 2, 'name' => 'Developer'}, json['roles']) + end + end + end + end + + context "/roles/:id" do + context "GET" do + context "xml" do + should "return the role" do + get '/roles/1.xml' + + assert_response :success + assert_equal 'application/xml', @response.content_type + + assert_select 'role' do + assert_select 'name', :text => 'Manager' + assert_select 'role permissions[type=array]' do + assert_select 'permission', Role.find(1).permissions.size + assert_select 'permission', :text => 'view_issues' + end + end + end + end + end + end +end diff --git a/test/integration/api_test/time_entries_test.rb b/test/integration/api_test/time_entries_test.rb new file mode 100644 index 00000000..f9723e6e --- /dev/null +++ b/test/integration/api_test/time_entries_test.rb @@ -0,0 +1,164 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::ApiTest::TimeEntriesTest < Redmine::ApiTest::Base + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :time_entries + + def setup + Setting.rest_api_enabled = '1' + end + + context "GET /time_entries.xml" do + should "return time entries" do + get '/time_entries.xml', {}, credentials('jsmith') + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'time_entries', + :child => {:tag => 'time_entry', :child => {:tag => 'id', :content => '2'}} + end + + context "with limit" do + should "return limited results" do + get '/time_entries.xml?limit=2', {}, credentials('jsmith') + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'time_entries', + :children => {:count => 2} + end + end + end + + context "GET /time_entries/2.xml" do + should "return requested time entry" do + get '/time_entries/2.xml', {}, credentials('jsmith') + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'time_entry', + :child => {:tag => 'id', :content => '2'} + end + end + + context "POST /time_entries.xml" do + context "with issue_id" do + should "return create time entry" do + assert_difference 'TimeEntry.count' do + post '/time_entries.xml', {:time_entry => {:issue_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11'}}, credentials('jsmith') + end + assert_response :created + assert_equal 'application/xml', @response.content_type + + entry = TimeEntry.first(:order => 'id DESC') + assert_equal 'jsmith', entry.user.login + assert_equal Issue.find(1), entry.issue + assert_equal Project.find(1), entry.project + assert_equal Date.parse('2010-12-02'), entry.spent_on + assert_equal 3.5, entry.hours + assert_equal TimeEntryActivity.find(11), entry.activity + end + + should "accept custom fields" do + field = TimeEntryCustomField.create!(:name => 'Test', :field_format => 'string') + + assert_difference 'TimeEntry.count' do + post '/time_entries.xml', {:time_entry => { + :issue_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11', :custom_fields => [{:id => field.id.to_s, :value => 'accepted'}] + }}, credentials('jsmith') + end + assert_response :created + assert_equal 'application/xml', @response.content_type + + entry = TimeEntry.first(:order => 'id DESC') + assert_equal 'accepted', entry.custom_field_value(field) + end + end + + context "with project_id" do + should "return create time entry" do + assert_difference 'TimeEntry.count' do + post '/time_entries.xml', {:time_entry => {:project_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11'}}, credentials('jsmith') + end + assert_response :created + assert_equal 'application/xml', @response.content_type + + entry = TimeEntry.first(:order => 'id DESC') + assert_equal 'jsmith', entry.user.login + assert_nil entry.issue + assert_equal Project.find(1), entry.project + assert_equal Date.parse('2010-12-02'), entry.spent_on + assert_equal 3.5, entry.hours + assert_equal TimeEntryActivity.find(11), entry.activity + end + end + + context "with invalid parameters" do + should "return errors" do + assert_no_difference 'TimeEntry.count' do + post '/time_entries.xml', {:time_entry => {:project_id => '1', :spent_on => '2010-12-02', :activity_id => '11'}}, credentials('jsmith') + end + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + + assert_tag 'errors', :child => {:tag => 'error', :content => "Hours can't be blank"} + end + end + end + + context "PUT /time_entries/2.xml" do + context "with valid parameters" do + should "update time entry" do + assert_no_difference 'TimeEntry.count' do + put '/time_entries/2.xml', {:time_entry => {:comments => 'API Update'}}, credentials('jsmith') + end + assert_response :ok + assert_equal '', @response.body + assert_equal 'API Update', TimeEntry.find(2).comments + end + end + + context "with invalid parameters" do + should "return errors" do + assert_no_difference 'TimeEntry.count' do + put '/time_entries/2.xml', {:time_entry => {:hours => '', :comments => 'API Update'}}, credentials('jsmith') + end + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + + assert_tag 'errors', :child => {:tag => 'error', :content => "Hours can't be blank"} + end + end + end + + context "DELETE /time_entries/2.xml" do + should "destroy time entry" do + assert_difference 'TimeEntry.count', -1 do + delete '/time_entries/2.xml', {}, credentials('jsmith') + end + assert_response :ok + assert_equal '', @response.body + assert_nil TimeEntry.find_by_id(2) + end + end +end diff --git a/test/integration/api_test/token_authentication_test.rb b/test/integration/api_test/token_authentication_test.rb new file mode 100644 index 00000000..835adc4b --- /dev/null +++ b/test/integration/api_test/token_authentication_test.rb @@ -0,0 +1,49 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::ApiTest::TokenAuthenticationTest < Redmine::ApiTest::Base + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules + + def setup + Setting.rest_api_enabled = '1' + Setting.login_required = '1' + end + + def teardown + Setting.rest_api_enabled = '0' + Setting.login_required = '0' + end + + # Using the NewsController because it's a simple API. + context "get /news" do + context "in :xml format" do + should_allow_key_based_auth(:get, "/news.xml") + end + + context "in :json format" do + should_allow_key_based_auth(:get, "/news.json") + end + end +end diff --git a/test/integration/api_test/trackers_test.rb b/test/integration/api_test/trackers_test.rb new file mode 100644 index 00000000..d0dea71e --- /dev/null +++ b/test/integration/api_test/trackers_test.rb @@ -0,0 +1,51 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::ApiTest::TrackersTest < Redmine::ApiTest::Base + fixtures :trackers + + def setup + Setting.rest_api_enabled = '1' + end + + context "/trackers" do + context "GET" do + + should "return trackers" do + get '/trackers.xml' + + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'trackers', + :attributes => {:type => 'array'}, + :child => { + :tag => 'tracker', + :child => { + :tag => 'id', + :content => '2', + :sibling => { + :tag => 'name', + :content => 'Feature request' + } + } + } + end + end + end +end diff --git a/test/integration/api_test/users_test.rb b/test/integration/api_test/users_test.rb new file mode 100644 index 00000000..3b12fed4 --- /dev/null +++ b/test/integration/api_test/users_test.rb @@ -0,0 +1,371 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::ApiTest::UsersTest < Redmine::ApiTest::Base + fixtures :users, :members, :member_roles, :roles, :projects + + def setup + Setting.rest_api_enabled = '1' + end + + context "GET /users" do + should_allow_api_authentication(:get, "/users.xml") + should_allow_api_authentication(:get, "/users.json") + end + + context "GET /users/2" do + context ".xml" do + should "return requested user" do + get '/users/2.xml' + + assert_response :success + assert_tag :tag => 'user', + :child => {:tag => 'id', :content => '2'} + end + + context "with include=memberships" do + should "include memberships" do + get '/users/2.xml?include=memberships' + + assert_response :success + assert_tag :tag => 'memberships', + :parent => {:tag => 'user'}, + :children => {:count => 1} + end + end + end + + context ".json" do + should "return requested user" do + get '/users/2.json' + + assert_response :success + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json + assert_kind_of Hash, json['user'] + assert_equal 2, json['user']['id'] + end + + context "with include=memberships" do + should "include memberships" do + get '/users/2.json?include=memberships' + + assert_response :success + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Array, json['user']['memberships'] + assert_equal [{ + "id"=>1, + "project"=>{"name"=>"eCookbook", "id"=>1}, + "roles"=>[{"name"=>"Manager", "id"=>1}] + }], json['user']['memberships'] + end + end + end + end + + context "GET /users/current" do + context ".xml" do + should "require authentication" do + get '/users/current.xml' + + assert_response 401 + end + + should "return current user" do + get '/users/current.xml', {}, credentials('jsmith') + + assert_tag :tag => 'user', + :child => {:tag => 'id', :content => '2'} + end + end + end + + test "GET /users/:id should not return login for other user" do + get '/users/3.xml', {}, credentials('jsmith') + assert_response :success + assert_no_tag 'user', :child => {:tag => 'login'} + end + + test "GET /users/:id should return login for current user" do + get '/users/2.xml', {}, credentials('jsmith') + assert_response :success + assert_tag 'user', :child => {:tag => 'login', :content => 'jsmith'} + end + + test "GET /users/:id should not return api_key for other user" do + get '/users/3.xml', {}, credentials('jsmith') + assert_response :success + assert_no_tag 'user', :child => {:tag => 'api_key'} + end + + test "GET /users/:id should return api_key for current user" do + get '/users/2.xml', {}, credentials('jsmith') + assert_response :success + assert_tag 'user', :child => {:tag => 'api_key', :content => User.find(2).api_key} + end + + context "POST /users" do + context "with valid parameters" do + setup do + @parameters = { + :user => { + :login => 'foo', :firstname => 'Firstname', :lastname => 'Lastname', + :mail => 'foo@example.net', :password => 'secret123', + :mail_notification => 'only_assigned' + } + } + end + + context ".xml" do + should_allow_api_authentication(:post, + '/users.xml', + {:user => { + :login => 'foo', :firstname => 'Firstname', :lastname => 'Lastname', + :mail => 'foo@example.net', :password => 'secret123' + }}, + {:success_code => :created}) + + should "create a user with the attributes" do + assert_difference('User.count') do + post '/users.xml', @parameters, credentials('admin') + end + + user = User.first(:order => 'id DESC') + assert_equal 'foo', user.login + assert_equal 'Firstname', user.firstname + assert_equal 'Lastname', user.lastname + assert_equal 'foo@example.net', user.mail + assert_equal 'only_assigned', user.mail_notification + assert !user.admin? + assert user.check_password?('secret123') + + assert_response :created + assert_equal 'application/xml', @response.content_type + assert_tag 'user', :child => {:tag => 'id', :content => user.id.to_s} + end + end + + context ".json" do + should_allow_api_authentication(:post, + '/users.json', + {:user => { + :login => 'foo', :firstname => 'Firstname', :lastname => 'Lastname', + :mail => 'foo@example.net' + }}, + {:success_code => :created}) + + should "create a user with the attributes" do + assert_difference('User.count') do + post '/users.json', @parameters, credentials('admin') + end + + user = User.first(:order => 'id DESC') + assert_equal 'foo', user.login + assert_equal 'Firstname', user.firstname + assert_equal 'Lastname', user.lastname + assert_equal 'foo@example.net', user.mail + assert !user.admin? + + assert_response :created + assert_equal 'application/json', @response.content_type + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json + assert_kind_of Hash, json['user'] + assert_equal user.id, json['user']['id'] + end + end + end + + context "with invalid parameters" do + setup do + @parameters = {:user => {:login => 'foo', :lastname => 'Lastname', :mail => 'foo'}} + end + + context ".xml" do + should "return errors" do + assert_no_difference('User.count') do + post '/users.xml', @parameters, credentials('admin') + end + + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + assert_tag 'errors', :child => { + :tag => 'error', + :content => "First name can't be blank" + } + end + end + + context ".json" do + should "return errors" do + assert_no_difference('User.count') do + post '/users.json', @parameters, credentials('admin') + end + + assert_response :unprocessable_entity + assert_equal 'application/json', @response.content_type + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json + assert json.has_key?('errors') + assert_kind_of Array, json['errors'] + end + end + end + end + + context "PUT /users/2" do + context "with valid parameters" do + setup do + @parameters = { + :user => { + :login => 'jsmith', :firstname => 'John', :lastname => 'Renamed', + :mail => 'jsmith@somenet.foo' + } + } + end + + context ".xml" do + should_allow_api_authentication(:put, + '/users/2.xml', + {:user => { + :login => 'jsmith', :firstname => 'John', :lastname => 'Renamed', + :mail => 'jsmith@somenet.foo' + }}, + {:success_code => :ok}) + + should "update user with the attributes" do + assert_no_difference('User.count') do + put '/users/2.xml', @parameters, credentials('admin') + end + + user = User.find(2) + assert_equal 'jsmith', user.login + assert_equal 'John', user.firstname + assert_equal 'Renamed', user.lastname + assert_equal 'jsmith@somenet.foo', user.mail + assert !user.admin? + + assert_response :ok + assert_equal '', @response.body + end + end + + context ".json" do + should_allow_api_authentication(:put, + '/users/2.json', + {:user => { + :login => 'jsmith', :firstname => 'John', :lastname => 'Renamed', + :mail => 'jsmith@somenet.foo' + }}, + {:success_code => :ok}) + + should "update user with the attributes" do + assert_no_difference('User.count') do + put '/users/2.json', @parameters, credentials('admin') + end + + user = User.find(2) + assert_equal 'jsmith', user.login + assert_equal 'John', user.firstname + assert_equal 'Renamed', user.lastname + assert_equal 'jsmith@somenet.foo', user.mail + assert !user.admin? + + assert_response :ok + assert_equal '', @response.body + end + end + end + + context "with invalid parameters" do + setup do + @parameters = { + :user => { + :login => 'jsmith', :firstname => '', :lastname => 'Lastname', + :mail => 'foo' + } + } + end + + context ".xml" do + should "return errors" do + assert_no_difference('User.count') do + put '/users/2.xml', @parameters, credentials('admin') + end + + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + assert_tag 'errors', :child => { + :tag => 'error', + :content => "First name can't be blank" + } + end + end + + context ".json" do + should "return errors" do + assert_no_difference('User.count') do + put '/users/2.json', @parameters, credentials('admin') + end + + assert_response :unprocessable_entity + assert_equal 'application/json', @response.content_type + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json + assert json.has_key?('errors') + assert_kind_of Array, json['errors'] + end + end + end + end + + context "DELETE /users/2" do + context ".xml" do + should_allow_api_authentication(:delete, + '/users/2.xml', + {}, + {:success_code => :ok}) + + should "delete user" do + assert_difference('User.count', -1) do + delete '/users/2.xml', {}, credentials('admin') + end + + assert_response :ok + assert_equal '', @response.body + end + end + + context ".json" do + should_allow_api_authentication(:delete, + '/users/2.xml', + {}, + {:success_code => :ok}) + + should "delete user" do + assert_difference('User.count', -1) do + delete '/users/2.json', {}, credentials('admin') + end + + assert_response :ok + assert_equal '', @response.body + end + end + end +end diff --git a/test/integration/api_test/versions_test.rb b/test/integration/api_test/versions_test.rb new file mode 100644 index 00000000..c7c49606 --- /dev/null +++ b/test/integration/api_test/versions_test.rb @@ -0,0 +1,158 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::ApiTest::VersionsTest < Redmine::ApiTest::Base + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :versions + + def setup + Setting.rest_api_enabled = '1' + end + + context "/projects/:project_id/versions" do + context "GET" do + should "return project versions" do + get '/projects/1/versions.xml' + + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'versions', + :attributes => {:type => 'array'}, + :child => { + :tag => 'version', + :child => { + :tag => 'id', + :content => '2', + :sibling => { + :tag => 'name', + :content => '1.0' + } + } + } + end + end + + context "POST" do + should "create the version" do + assert_difference 'Version.count' do + post '/projects/1/versions.xml', {:version => {:name => 'API test'}}, credentials('jsmith') + end + + version = Version.first(:order => 'id DESC') + assert_equal 'API test', version.name + + assert_response :created + assert_equal 'application/xml', @response.content_type + assert_tag 'version', :child => {:tag => 'id', :content => version.id.to_s} + end + + should "create the version with due date" do + assert_difference 'Version.count' do + post '/projects/1/versions.xml', {:version => {:name => 'API test', :due_date => '2012-01-24'}}, credentials('jsmith') + end + + version = Version.first(:order => 'id DESC') + assert_equal 'API test', version.name + assert_equal Date.parse('2012-01-24'), version.due_date + + assert_response :created + assert_equal 'application/xml', @response.content_type + assert_tag 'version', :child => {:tag => 'id', :content => version.id.to_s} + end + + should "create the version with custom fields" do + field = VersionCustomField.generate! + + assert_difference 'Version.count' do + post '/projects/1/versions.xml', { + :version => { + :name => 'API test', + :custom_fields => [ + {'id' => field.id.to_s, 'value' => 'Some value'} + ] + } + }, credentials('jsmith') + end + + version = Version.first(:order => 'id DESC') + assert_equal 'API test', version.name + assert_equal 'Some value', version.custom_field_value(field) + + assert_response :created + assert_equal 'application/xml', @response.content_type + assert_select 'version>custom_fields>custom_field[id=?]>value', field.id.to_s, 'Some value' + end + + context "with failure" do + should "return the errors" do + assert_no_difference('Version.count') do + post '/projects/1/versions.xml', {:version => {:name => ''}}, credentials('jsmith') + end + + assert_response :unprocessable_entity + assert_tag :errors, :child => {:tag => 'error', :content => "Name can't be blank"} + end + end + end + end + + context "/versions/:id" do + context "GET" do + should "return the version" do + get '/versions/2.xml' + + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_select 'version' do + assert_select 'id', :text => '2' + assert_select 'name', :text => '1.0' + assert_select 'sharing', :text => 'none' + end + end + end + + context "PUT" do + should "update the version" do + put '/versions/2.xml', {:version => {:name => 'API update'}}, credentials('jsmith') + + assert_response :ok + assert_equal '', @response.body + assert_equal 'API update', Version.find(2).name + end + end + + context "DELETE" do + should "destroy the version" do + assert_difference 'Version.count', -1 do + delete '/versions/3.xml', {}, credentials('jsmith') + end + + assert_response :ok + assert_equal '', @response.body + assert_nil Version.find_by_id(3) + end + end + end +end diff --git a/test/integration/api_test/wiki_pages_test.rb b/test/integration/api_test/wiki_pages_test.rb new file mode 100644 index 00000000..77ada733 --- /dev/null +++ b/test/integration/api_test/wiki_pages_test.rb @@ -0,0 +1,193 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::ApiTest::WikiPagesTest < Redmine::ApiTest::Base + fixtures :projects, :users, :roles, :members, :member_roles, + :enabled_modules, :wikis, :wiki_pages, :wiki_contents, + :wiki_content_versions, :attachments + + def setup + Setting.rest_api_enabled = '1' + end + + test "GET /projects/:project_id/wiki/index.xml should return wiki pages" do + get '/projects/ecookbook/wiki/index.xml' + assert_response 200 + assert_equal 'application/xml', response.content_type + assert_select 'wiki_pages[type=array]' do + assert_select 'wiki_page', :count => Wiki.find(1).pages.count + assert_select 'wiki_page' do + assert_select 'title', :text => 'CookBook_documentation' + assert_select 'version', :text => '3' + assert_select 'created_on' + assert_select 'updated_on' + end + assert_select 'wiki_page' do + assert_select 'title', :text => 'Page_with_an_inline_image' + assert_select 'parent[title=?]', 'CookBook_documentation' + end + end + end + + test "GET /projects/:project_id/wiki/:title.xml should return wiki page" do + get '/projects/ecookbook/wiki/CookBook_documentation.xml' + assert_response 200 + assert_equal 'application/xml', response.content_type + assert_select 'wiki_page' do + assert_select 'title', :text => 'CookBook_documentation' + assert_select 'version', :text => '3' + assert_select 'text' + assert_select 'author' + assert_select 'comments' + assert_select 'created_on' + assert_select 'updated_on' + end + end + + test "GET /projects/:project_id/wiki/:title.xml?include=attachments should include attachments" do + get '/projects/ecookbook/wiki/Page_with_an_inline_image.xml?include=attachments' + assert_response 200 + assert_equal 'application/xml', response.content_type + assert_select 'wiki_page' do + assert_select 'title', :text => 'Page_with_an_inline_image' + assert_select 'attachments[type=array]' do + assert_select 'attachment' do + assert_select 'id', :text => '3' + assert_select 'filename', :text => 'logo.gif' + end + end + end + end + + test "GET /projects/:project_id/wiki/:title.xml with unknown title and edit permission should respond with 404" do + get '/projects/ecookbook/wiki/Invalid_Page.xml', {}, credentials('jsmith') + assert_response 404 + assert_equal 'application/xml', response.content_type + end + + test "GET /projects/:project_id/wiki/:title/:version.xml should return wiki page version" do + get '/projects/ecookbook/wiki/CookBook_documentation/2.xml' + assert_response 200 + assert_equal 'application/xml', response.content_type + assert_select 'wiki_page' do + assert_select 'title', :text => 'CookBook_documentation' + assert_select 'version', :text => '2' + assert_select 'text' + assert_select 'author' + assert_select 'created_on' + assert_select 'updated_on' + end + end + + test "GET /projects/:project_id/wiki/:title/:version.xml without permission should be denied" do + Role.anonymous.remove_permission! :view_wiki_edits + + get '/projects/ecookbook/wiki/CookBook_documentation/2.xml' + assert_response 401 + assert_equal 'application/xml', response.content_type + end + + test "PUT /projects/:project_id/wiki/:title.xml should update wiki page" do + assert_no_difference 'WikiPage.count' do + assert_difference 'WikiContent::Version.count' do + put '/projects/ecookbook/wiki/CookBook_documentation.xml', + {:wiki_page => {:text => 'New content from API', :comments => 'API update'}}, + credentials('jsmith') + assert_response 200 + end + end + + page = WikiPage.find(1) + assert_equal 'New content from API', page.content.text + assert_equal 4, page.content.version + assert_equal 'API update', page.content.comments + assert_equal 'jsmith', page.content.author.login + end + + test "PUT /projects/:project_id/wiki/:title.xml with current versino should update wiki page" do + assert_no_difference 'WikiPage.count' do + assert_difference 'WikiContent::Version.count' do + put '/projects/ecookbook/wiki/CookBook_documentation.xml', + {:wiki_page => {:text => 'New content from API', :comments => 'API update', :version => '3'}}, + credentials('jsmith') + assert_response 200 + end + end + + page = WikiPage.find(1) + assert_equal 'New content from API', page.content.text + assert_equal 4, page.content.version + assert_equal 'API update', page.content.comments + assert_equal 'jsmith', page.content.author.login + end + + test "PUT /projects/:project_id/wiki/:title.xml with stale version should respond with 409" do + assert_no_difference 'WikiPage.count' do + assert_no_difference 'WikiContent::Version.count' do + put '/projects/ecookbook/wiki/CookBook_documentation.xml', + {:wiki_page => {:text => 'New content from API', :comments => 'API update', :version => '2'}}, + credentials('jsmith') + assert_response 409 + end + end + end + + test "PUT /projects/:project_id/wiki/:title.xml should create the page if it does not exist" do + assert_difference 'WikiPage.count' do + assert_difference 'WikiContent::Version.count' do + put '/projects/ecookbook/wiki/New_page_from_API.xml', + {:wiki_page => {:text => 'New content from API', :comments => 'API create'}}, + credentials('jsmith') + assert_response 201 + end + end + + page = WikiPage.order('id DESC').first + assert_equal 'New_page_from_API', page.title + assert_equal 'New content from API', page.content.text + assert_equal 1, page.content.version + assert_equal 'API create', page.content.comments + assert_equal 'jsmith', page.content.author.login + assert_nil page.parent + end + + test "PUT /projects/:project_id/wiki/:title.xml with parent" do + assert_difference 'WikiPage.count' do + assert_difference 'WikiContent::Version.count' do + put '/projects/ecookbook/wiki/New_subpage_from_API.xml', + {:wiki_page => {:parent_title => 'CookBook_documentation', :text => 'New content from API', :comments => 'API create'}}, + credentials('jsmith') + assert_response 201 + end + end + + page = WikiPage.order('id DESC').first + assert_equal 'New_subpage_from_API', page.title + assert_equal WikiPage.find(1), page.parent + end + + test "DELETE /projects/:project_id/wiki/:title.xml should destroy the page" do + assert_difference 'WikiPage.count', -1 do + delete '/projects/ecookbook/wiki/CookBook_documentation.xml', {}, credentials('jsmith') + assert_response 200 + end + + assert_nil WikiPage.find_by_id(1) + end +end diff --git a/test/integration/application_test.rb b/test/integration/application_test.rb new file mode 100644 index 00000000..66be18e9 --- /dev/null +++ b/test/integration/application_test.rb @@ -0,0 +1,67 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class ApplicationTest < ActionController::IntegrationTest + include Redmine::I18n + + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules + + def test_set_localization + Setting.default_language = 'en' + + # a french user + get 'projects', { }, 'HTTP_ACCEPT_LANGUAGE' => 'fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3' + assert_response :success + assert_tag :tag => 'h2', :content => 'Projets' + assert_equal :fr, current_language + + # then an italien user + get 'projects', { }, 'HTTP_ACCEPT_LANGUAGE' => 'it;q=0.8,en-us;q=0.5,en;q=0.3' + assert_response :success + assert_tag :tag => 'h2', :content => 'Progetti' + assert_equal :it, current_language + + # not a supported language: default language should be used + get 'projects', { }, 'HTTP_ACCEPT_LANGUAGE' => 'zz' + assert_response :success + assert_tag :tag => 'h2', :content => 'Projects' + end + + def test_token_based_access_should_not_start_session + # issue of a private project + get 'issues/4.atom' + assert_response 302 + + rss_key = User.find(2).rss_key + get "issues/4.atom?key=#{rss_key}" + assert_response 200 + assert_nil session[:user_id] + end + + def test_missing_template_should_respond_with_404 + get '/login.png' + assert_response 404 + end +end diff --git a/test/integration/attachments_test.rb b/test/integration/attachments_test.rb new file mode 100644 index 00000000..d73b636e --- /dev/null +++ b/test/integration/attachments_test.rb @@ -0,0 +1,132 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class AttachmentsTest < ActionController::IntegrationTest + fixtures :projects, :enabled_modules, + :users, :roles, :members, :member_roles, + :trackers, :projects_trackers, + :issue_statuses, :enumerations + + def test_upload_as_js_and_attach_to_an_issue + log_user('jsmith', 'jsmith') + + token = ajax_upload('myupload.txt', 'File content') + + assert_difference 'Issue.count' do + post '/projects/ecookbook/issues', { + :issue => {:tracker_id => 1, :subject => 'Issue with upload'}, + :attachments => {'1' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}} + } + assert_response 302 + end + + issue = Issue.order('id DESC').first + assert_equal 'Issue with upload', issue.subject + assert_equal 1, issue.attachments.count + + attachment = issue.attachments.first + assert_equal 'myupload.txt', attachment.filename + assert_equal 'My uploaded file', attachment.description + assert_equal 'File content'.length, attachment.filesize + end + + def test_upload_as_js_and_preview_as_inline_attachment + log_user('jsmith', 'jsmith') + + token = ajax_upload('myupload.jpg', 'JPEG content') + + post '/issues/preview/new/ecookbook', { + :issue => {:tracker_id => 1, :description => 'Inline upload: !myupload.jpg!'}, + :attachments => {'1' => {:filename => 'myupload.jpg', :description => 'My uploaded file', :token => token}} + } + assert_response :success + + attachment_path = response.body.match(%r{ {:tracker_id => 1, :subject => ''}, + :attachments => {'1' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}} + } + assert_response :success + end + assert_select 'input[type=hidden][name=?][value=?]', 'attachments[p0][token]', token + assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'myupload.txt' + assert_select 'input[name=?][value=?]', 'attachments[p0][description]', 'My uploaded file' + + assert_difference 'Issue.count' do + post '/projects/ecookbook/issues', { + :issue => {:tracker_id => 1, :subject => 'Issue with upload'}, + :attachments => {'p0' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}} + } + assert_response 302 + end + + issue = Issue.order('id DESC').first + assert_equal 'Issue with upload', issue.subject + assert_equal 1, issue.attachments.count + + attachment = issue.attachments.first + assert_equal 'myupload.txt', attachment.filename + assert_equal 'My uploaded file', attachment.description + assert_equal 'File content'.length, attachment.filesize + end + + def test_upload_as_js_and_destroy + log_user('jsmith', 'jsmith') + + token = ajax_upload('myupload.txt', 'File content') + + attachment = Attachment.order('id DESC').first + attachment_path = "/attachments/#{attachment.id}.js?attachment_id=1" + assert_include "href: '#{attachment_path}'", response.body, "Path to attachment: #{attachment_path} not found in response:\n#{response.body}" + + assert_difference 'Attachment.count', -1 do + delete attachment_path + assert_response :success + end + + assert_include "$('#attachments_1').remove();", response.body + end + + private + + def ajax_upload(filename, content, attachment_id=1) + assert_difference 'Attachment.count' do + post "/uploads.js?attachment_id=#{attachment_id}&filename=#{filename}", content, {"CONTENT_TYPE" => 'application/octet-stream'} + assert_response :success + assert_equal 'text/javascript', response.content_type + end + + token = response.body.match(/\.val\('(\d+\.[0-9a-f]+)'\)/)[1] + assert_not_nil token, "No upload token found in response:\n#{response.body}" + token + end +end diff --git a/test/integration/issues_test.rb b/test/integration/issues_test.rb new file mode 100644 index 00000000..34052189 --- /dev/null +++ b/test/integration/issues_test.rb @@ -0,0 +1,220 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class IssuesTest < ActionController::IntegrationTest + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :trackers, + :projects_trackers, + :enabled_modules, + :issue_statuses, + :issues, + :enumerations, + :custom_fields, + :custom_values, + :custom_fields_trackers + + # create an issue + def test_add_issue + log_user('jsmith', 'jsmith') + get 'projects/1/issues/new', :tracker_id => '1' + assert_response :success + assert_template 'issues/new' + + post 'projects/1/issues', :tracker_id => "1", + :issue => { :start_date => "2006-12-26", + :priority_id => "4", + :subject => "new test issue", + :category_id => "", + :description => "new issue", + :done_ratio => "0", + :due_date => "", + :assigned_to_id => "" }, + :custom_fields => {'2' => 'Value for field 2'} + # find created issue + issue = Issue.find_by_subject("new test issue") + assert_kind_of Issue, issue + + # check redirection + assert_redirected_to :controller => 'issues', :action => 'show', :id => issue + follow_redirect! + assert_equal issue, assigns(:issue) + + # check issue attributes + assert_equal 'jsmith', issue.author.login + assert_equal 1, issue.project.id + assert_equal 1, issue.status.id + end + + # add then remove 2 attachments to an issue + def test_issue_attachments + log_user('jsmith', 'jsmith') + set_tmp_attachments_directory + + put 'issues/1', + :notes => 'Some notes', + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'This is an attachment'}} + assert_redirected_to "/issues/1" + + # make sure attachment was saved + attachment = Issue.find(1).attachments.find_by_filename("testfile.txt") + assert_kind_of Attachment, attachment + assert_equal Issue.find(1), attachment.container + assert_equal 'This is an attachment', attachment.description + # verify the size of the attachment stored in db + #assert_equal file_data_1.length, attachment.filesize + # verify that the attachment was written to disk + assert File.exist?(attachment.diskfile) + + # remove the attachments + Issue.find(1).attachments.each(&:destroy) + assert_equal 0, Issue.find(1).attachments.length + end + + def test_other_formats_links_on_index + get '/projects/ecookbook/issues' + + %w(Atom PDF CSV).each do |format| + assert_tag :a, :content => format, + :attributes => { :href => "/projects/ecookbook/issues.#{format.downcase}", + :rel => 'nofollow' } + end + end + + def test_other_formats_links_on_index_without_project_id_in_url + get '/issues', :project_id => 'ecookbook' + + %w(Atom PDF CSV).each do |format| + assert_tag :a, :content => format, + :attributes => { :href => "/projects/ecookbook/issues.#{format.downcase}", + :rel => 'nofollow' } + end + end + + def test_pagination_links_on_index + Setting.per_page_options = '2' + get '/projects/ecookbook/issues' + + assert_tag :a, :content => '2', + :attributes => { :href => '/projects/ecookbook/issues?page=2' } + + end + + def test_pagination_links_on_index_without_project_id_in_url + Setting.per_page_options = '2' + get '/issues', :project_id => 'ecookbook' + + assert_tag :a, :content => '2', + :attributes => { :href => '/projects/ecookbook/issues?page=2' } + + end + + def test_issue_with_user_custom_field + @field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user', :is_for_all => true, :trackers => Tracker.all) + Role.anonymous.add_permission! :add_issues, :edit_issues + users = Project.find(1).users + tester = users.first + + # Issue form + get '/projects/ecookbook/issues/new' + assert_response :success + assert_tag :select, + :attributes => {:name => "issue[custom_field_values][#{@field.id}]"}, + :children => {:count => (users.size + 1)}, # +1 for blank value + :child => { + :tag => 'option', + :attributes => {:value => tester.id.to_s}, + :content => tester.name + } + + # Create issue + assert_difference 'Issue.count' do + post '/projects/ecookbook/issues', + :issue => { + :tracker_id => '1', + :priority_id => '4', + :subject => 'Issue with user custom field', + :custom_field_values => {@field.id.to_s => users.first.id.to_s} + } + end + issue = Issue.first(:order => 'id DESC') + assert_response 302 + + # Issue view + follow_redirect! + assert_tag :th, + :content => /Tester/, + :sibling => { + :tag => 'td', + :content => tester.name + } + assert_tag :select, + :attributes => {:name => "issue[custom_field_values][#{@field.id}]"}, + :children => {:count => (users.size + 1)}, # +1 for blank value + :child => { + :tag => 'option', + :attributes => {:value => tester.id.to_s, :selected => 'selected'}, + :content => tester.name + } + + # Update issue + new_tester = users[1] + assert_difference 'Journal.count' do + put "/issues/#{issue.id}", + :notes => 'Updating custom field', + :issue => { + :custom_field_values => {@field.id.to_s => new_tester.id.to_s} + } + end + assert_response 302 + + # Issue view + follow_redirect! + assert_tag :content => 'Tester', + :ancestor => {:tag => 'ul', :attributes => {:class => /details/}}, + :sibling => { + :content => tester.name, + :sibling => { + :content => new_tester.name + } + } + end + + def test_update_using_invalid_http_verbs + subject = 'Updated by an invalid http verb' + + get '/issues/update/1', {:issue => {:subject => subject}}, credentials('jsmith') + assert_response 404 + assert_not_equal subject, Issue.find(1).subject + + post '/issues/1', {:issue => {:subject => subject}}, credentials('jsmith') + assert_response 404 + assert_not_equal subject, Issue.find(1).subject + end + + def test_get_watch_should_be_invalid + assert_no_difference 'Watcher.count' do + get '/watchers/watch?object_type=issue&object_id=1', {}, credentials('jsmith') + assert_response 404 + end + end +end diff --git a/test/integration/layout_test.rb b/test/integration/layout_test.rb new file mode 100644 index 00000000..4d2e5649 --- /dev/null +++ b/test/integration/layout_test.rb @@ -0,0 +1,119 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class LayoutTest < ActionController::IntegrationTest + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules + + test "browsing to a missing page should render the base layout" do + get "/users/100000000" + + assert_response :not_found + + # UsersController uses the admin layout by default + assert_select "#admin-menu", :count => 0 + end + + test "browsing to an unauthorized page should render the base layout" do + change_user_password('miscuser9', 'test1234') + + log_user('miscuser9','test1234') + + get "/admin" + assert_response :forbidden + assert_select "#admin-menu", :count => 0 + end + + def test_top_menu_and_search_not_visible_when_login_required + with_settings :login_required => '1' do + get '/' + assert_select "#top-menu > ul", 0 + assert_select "#quick-search", 0 + end + end + + def test_top_menu_and_search_visible_when_login_not_required + with_settings :login_required => '0' do + get '/' + assert_select "#top-menu > ul" + assert_select "#quick-search" + end + end + + def test_wiki_formatter_header_tags + Role.anonymous.add_permission! :add_issues + + get '/projects/ecookbook/issues/new' + assert_tag :script, + :attributes => {:src => %r{^/javascripts/jstoolbar/jstoolbar-textile.min.js}}, + :parent => {:tag => 'head'} + end + + def test_calendar_header_tags + with_settings :default_language => 'fr' do + get '/issues' + assert_include "/javascripts/i18n/jquery.ui.datepicker-fr.js", response.body + end + + with_settings :default_language => 'en-GB' do + get '/issues' + assert_include "/javascripts/i18n/jquery.ui.datepicker-en-GB.js", response.body + end + + with_settings :default_language => 'en' do + get '/issues' + assert_not_include "/javascripts/i18n/jquery.ui.datepicker", response.body + end + + with_settings :default_language => 'zh' do + get '/issues' + assert_include "/javascripts/i18n/jquery.ui.datepicker-zh-CN.js", response.body + end + + with_settings :default_language => 'zh-TW' do + get '/issues' + assert_include "/javascripts/i18n/jquery.ui.datepicker-zh-TW.js", response.body + end + + with_settings :default_language => 'pt' do + get '/issues' + assert_include "/javascripts/i18n/jquery.ui.datepicker-pt.js", response.body + end + + with_settings :default_language => 'pt-BR' do + get '/issues' + assert_include "/javascripts/i18n/jquery.ui.datepicker-pt-BR.js", response.body + end + end + + def test_search_field_outside_project_should_link_to_global_search + get '/' + assert_select 'div#quick-search form[action=/search]' + end + + def test_search_field_inside_project_should_link_to_project_search + get '/projects/ecookbook' + assert_select 'div#quick-search form[action=/projects/ecookbook/search]' + end +end diff --git a/test/integration/lib/redmine/hook_test.rb b/test/integration/lib/redmine/hook_test.rb new file mode 100644 index 00000000..dc53c92f --- /dev/null +++ b/test/integration/lib/redmine/hook_test.rb @@ -0,0 +1,89 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../../test_helper', __FILE__) + +class HookTest < ActionController::IntegrationTest + fixtures :users, :roles, :projects, :members, :member_roles + + # Hooks that are manually registered later + class ProjectBasedTemplate < Redmine::Hook::ViewListener + def view_layouts_base_html_head(context) + # Adds a project stylesheet + stylesheet_link_tag(context[:project].identifier) if context[:project] + end + end + + class SidebarContent < Redmine::Hook::ViewListener + def view_layouts_base_sidebar(context) + content_tag('p', 'Sidebar hook') + end + end + + class ContentForInsideHook < Redmine::Hook::ViewListener + render_on :view_welcome_index_left, :inline => <<-VIEW +<% content_for :header_tags do %> + <%= javascript_include_tag 'test_plugin.js', :plugin => 'test_plugin' %> + <%= stylesheet_link_tag 'test_plugin.css', :plugin => 'test_plugin' %> +<% end %> + +

    ContentForInsideHook content

    +VIEW + end + + def setup + Redmine::Hook.clear_listeners + end + + def teardown + Redmine::Hook.clear_listeners + end + + def test_html_head_hook_response + Redmine::Hook.add_listener(ProjectBasedTemplate) + + get '/projects/ecookbook' + assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'}, + :parent => {:tag => 'head'} + end + + def test_empty_sidebar_should_be_hidden + get '/' + assert_select 'div#main.nosidebar' + end + + def test_sidebar_with_hook_content_should_not_be_hidden + Redmine::Hook.add_listener(SidebarContent) + + get '/' + assert_select 'div#sidebar p', :text => 'Sidebar hook' + assert_select 'div#main' + assert_select 'div#main.nosidebar', 0 + end + + def test_hook_with_content_for_should_append_content + Redmine::Hook.add_listener(ContentForInsideHook) + + get '/' + assert_response :success + assert_select 'p', :text => 'ContentForInsideHook content' + assert_select 'head' do + assert_select 'script[src=/plugin_assets/test_plugin/javascripts/test_plugin.js]' + assert_select 'link[href=/plugin_assets/test_plugin/stylesheets/test_plugin.css]' + end + end +end diff --git a/test/integration/lib/redmine/menu_manager_test.rb b/test/integration/lib/redmine/menu_manager_test.rb new file mode 100644 index 00000000..db9d8d56 --- /dev/null +++ b/test/integration/lib/redmine/menu_manager_test.rb @@ -0,0 +1,76 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../../test_helper', __FILE__) + +class MenuManagerTest < ActionController::IntegrationTest + include Redmine::I18n + + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules + + def test_project_menu_with_specific_locale + get 'projects/ecookbook/issues', { }, 'HTTP_ACCEPT_LANGUAGE' => 'fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3' + + assert_tag :div, :attributes => { :id => 'main-menu' }, + :descendant => { :tag => 'li', :child => { :tag => 'a', :content => ll('fr', :label_activity), + :attributes => { :href => '/projects/ecookbook/activity', + :class => 'activity' } } } + assert_tag :div, :attributes => { :id => 'main-menu' }, + :descendant => { :tag => 'li', :child => { :tag => 'a', :content => ll('fr', :label_issue_plural), + :attributes => { :href => '/projects/ecookbook/issues', + :class => 'issues selected' } } } + end + + def test_project_menu_with_additional_menu_items + Setting.default_language = 'en' + assert_no_difference 'Redmine::MenuManager.items(:project_menu).size' do + Redmine::MenuManager.map :project_menu do |menu| + menu.push :foo, { :controller => 'projects', :action => 'show' }, :caption => 'Foo' + menu.push :bar, { :controller => 'projects', :action => 'show' }, :before => :activity + menu.push :hello, { :controller => 'projects', :action => 'show' }, :caption => Proc.new {|p| p.name.upcase }, :after => :bar + end + + get 'projects/ecookbook' + assert_tag :div, :attributes => { :id => 'main-menu' }, + :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Foo', + :attributes => { :class => 'foo' } } } + + assert_tag :div, :attributes => { :id => 'main-menu' }, + :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Bar', + :attributes => { :class => 'bar' } }, + :before => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK' } } } + + assert_tag :div, :attributes => { :id => 'main-menu' }, + :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK', + :attributes => { :class => 'hello' } }, + :before => { :tag => 'li', :child => { :tag => 'a', :content => 'Activity' } } } + + # Remove the menu items + Redmine::MenuManager.map :project_menu do |menu| + menu.delete :foo + menu.delete :bar + menu.delete :hello + end + end + end +end diff --git a/test/integration/lib/redmine/themes_test.rb b/test/integration/lib/redmine/themes_test.rb new file mode 100644 index 00000000..7c21a24b --- /dev/null +++ b/test/integration/lib/redmine/themes_test.rb @@ -0,0 +1,74 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../../test_helper', __FILE__) + +class ThemesTest < ActionController::IntegrationTest + + def setup + @theme = Redmine::Themes.themes.last + Setting.ui_theme = @theme.id + end + + def teardown + Setting.ui_theme = '' + end + + def test_application_css + get '/' + + assert_response :success + assert_tag :tag => 'link', + :attributes => {:href => %r{^/themes/#{@theme.dir}/stylesheets/application.css}} + end + + def test_without_theme_js + get '/' + + assert_response :success + assert_no_tag :tag => 'script', + :attributes => {:src => %r{^/themes/#{@theme.dir}/javascripts/theme.js}} + end + + def test_with_theme_js + # Simulates a theme.js + @theme.javascripts << 'theme' + get '/' + + assert_response :success + assert_tag :tag => 'script', + :attributes => {:src => %r{^/themes/#{@theme.dir}/javascripts/theme.js}} + + ensure + @theme.javascripts.delete 'theme' + end + + def test_with_sub_uri + Redmine::Utils.relative_url_root = '/foo' + @theme.javascripts << 'theme' + get '/' + + assert_response :success + assert_tag :tag => 'link', + :attributes => {:href => %r{^/foo/themes/#{@theme.dir}/stylesheets/application.css}} + assert_tag :tag => 'script', + :attributes => {:src => %r{^/foo/themes/#{@theme.dir}/javascripts/theme.js}} + + ensure + Redmine::Utils.relative_url_root = '' + end +end diff --git a/test/integration/projects_test.rb b/test/integration/projects_test.rb new file mode 100644 index 00000000..70d4d9dd --- /dev/null +++ b/test/integration/projects_test.rb @@ -0,0 +1,51 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class ProjectsTest < ActionController::IntegrationTest + fixtures :projects, :users, :members, :enabled_modules + + def test_archive_project + subproject = Project.find(1).children.first + log_user("admin", "admin") + get "admin/projects" + assert_response :success + assert_template "admin/projects" + post "projects/1/archive" + assert_redirected_to "/admin/projects" + assert !Project.find(1).active? + + get 'projects/1' + assert_response 403 + get "projects/#{subproject.id}" + assert_response 403 + + post "projects/1/unarchive" + assert_redirected_to "/admin/projects" + assert Project.find(1).active? + get "projects/1" + assert_response :success + end + + def test_modules_should_not_allow_get + assert_no_difference 'EnabledModule.count' do + get '/projects/1/modules', {:enabled_module_names => ['']}, credentials('jsmith') + assert_response 404 + end + end +end diff --git a/test/integration/repositories_git_test.rb b/test/integration/repositories_git_test.rb new file mode 100644 index 00000000..e5c55690 --- /dev/null +++ b/test/integration/repositories_git_test.rb @@ -0,0 +1,50 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class RepositoriesGitTest < ActionController::IntegrationTest + fixtures :projects, :users, :roles, :members, :member_roles, + :repositories, :enabled_modules + + REPOSITORY_PATH = Rails.root.join('tmp/test/git_repository').to_s + REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin? + PRJ_ID = 3 + + def setup + User.current = nil + @project = Project.find(PRJ_ID) + @repository = Repository::Git.create( + :project => @project, + :url => REPOSITORY_PATH, + :path_encoding => 'ISO-8859-1' + ) + assert @repository + end + + if File.directory?(REPOSITORY_PATH) + def test_index + get '/projects/subproject1/repository/' + assert_response :success + end + + def test_diff_two_revs + get '/projects/subproject1/repository/diff?rev=61b685fbe&rev_to=2f9c0091' + assert_response :success + end + end +end diff --git a/test/integration/routing/account_test.rb b/test/integration/routing/account_test.rb new file mode 100644 index 00000000..f1939f69 --- /dev/null +++ b/test/integration/routing/account_test.rb @@ -0,0 +1,51 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingAccountTest < ActionController::IntegrationTest + def test_account + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/login" }, + { :controller => 'account', :action => 'login' } + ) + end + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/logout" }, + { :controller => 'account', :action => 'logout' } + ) + end + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/account/register" }, + { :controller => 'account', :action => 'register' } + ) + end + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/account/lost_password" }, + { :controller => 'account', :action => 'lost_password' } + ) + end + assert_routing( + { :method => 'get', :path => "/account/activate" }, + { :controller => 'account', :action => 'activate' } + ) + end +end diff --git a/test/integration/routing/activities_test.rb b/test/integration/routing/activities_test.rb new file mode 100644 index 00000000..01b198ad --- /dev/null +++ b/test/integration/routing/activities_test.rb @@ -0,0 +1,40 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingActivitiesTest < ActionController::IntegrationTest + def test_activities + assert_routing( + { :method => 'get', :path => "/activity" }, + { :controller => 'activities', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/activity.atom" }, + { :controller => 'activities', :action => 'index', :format => 'atom' } + ) + assert_routing( + { :method => 'get', :path => "/projects/33/activity" }, + { :controller => 'activities', :action => 'index', :id => '33' } + ) + assert_routing( + { :method => 'get', :path => "/projects/33/activity.atom" }, + { :controller => 'activities', :action => 'index', :id => '33', + :format => 'atom' } + ) + end +end diff --git a/test/integration/routing/admin_test.rb b/test/integration/routing/admin_test.rb new file mode 100644 index 00000000..c828c0c4 --- /dev/null +++ b/test/integration/routing/admin_test.rb @@ -0,0 +1,47 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingAdminTest < ActionController::IntegrationTest + def test_administration_panel + assert_routing( + { :method => 'get', :path => "/admin" }, + { :controller => 'admin', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/admin/projects" }, + { :controller => 'admin', :action => 'projects' } + ) + assert_routing( + { :method => 'get', :path => "/admin/plugins" }, + { :controller => 'admin', :action => 'plugins' } + ) + assert_routing( + { :method => 'get', :path => "/admin/info" }, + { :controller => 'admin', :action => 'info' } + ) + assert_routing( + { :method => 'get', :path => "/admin/test_email" }, + { :controller => 'admin', :action => 'test_email' } + ) + assert_routing( + { :method => 'post', :path => "/admin/default_configuration" }, + { :controller => 'admin', :action => 'default_configuration' } + ) + end +end diff --git a/test/integration/routing/attachments_test.rb b/test/integration/routing/attachments_test.rb new file mode 100644 index 00000000..2d0e2565 --- /dev/null +++ b/test/integration/routing/attachments_test.rb @@ -0,0 +1,69 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingAttachmentsTest < ActionController::IntegrationTest + def test_attachments + assert_routing( + { :method => 'get', :path => "/attachments/1" }, + { :controller => 'attachments', :action => 'show', :id => '1' } + ) + assert_routing( + { :method => 'get', :path => "/attachments/1.xml" }, + { :controller => 'attachments', :action => 'show', :id => '1', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/attachments/1.json" }, + { :controller => 'attachments', :action => 'show', :id => '1', :format => 'json' } + ) + assert_routing( + { :method => 'get', :path => "/attachments/1/filename.ext" }, + { :controller => 'attachments', :action => 'show', :id => '1', + :filename => 'filename.ext' } + ) + assert_routing( + { :method => 'get', :path => "/attachments/download/1" }, + { :controller => 'attachments', :action => 'download', :id => '1' } + ) + assert_routing( + { :method => 'get', :path => "/attachments/download/1/filename.ext" }, + { :controller => 'attachments', :action => 'download', :id => '1', + :filename => 'filename.ext' } + ) + assert_routing( + { :method => 'get', :path => "/attachments/thumbnail/1" }, + { :controller => 'attachments', :action => 'thumbnail', :id => '1' } + ) + assert_routing( + { :method => 'get', :path => "/attachments/thumbnail/1/200" }, + { :controller => 'attachments', :action => 'thumbnail', :id => '1', :size => '200' } + ) + assert_routing( + { :method => 'delete', :path => "/attachments/1" }, + { :controller => 'attachments', :action => 'destroy', :id => '1' } + ) + assert_routing( + { :method => 'post', :path => '/uploads.xml' }, + { :controller => 'attachments', :action => 'upload', :format => 'xml' } + ) + assert_routing( + { :method => 'post', :path => '/uploads.json' }, + { :controller => 'attachments', :action => 'upload', :format => 'json' } + ) + end +end diff --git a/test/integration/routing/auth_sources_test.rb b/test/integration/routing/auth_sources_test.rb new file mode 100644 index 00000000..e0041c7b --- /dev/null +++ b/test/integration/routing/auth_sources_test.rb @@ -0,0 +1,59 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingAuthSourcesTest < ActionController::IntegrationTest + def test_auth_sources + assert_routing( + { :method => 'get', :path => "/auth_sources" }, + { :controller => 'auth_sources', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/auth_sources/new" }, + { :controller => 'auth_sources', :action => 'new' } + ) + assert_routing( + { :method => 'post', :path => "/auth_sources" }, + { :controller => 'auth_sources', :action => 'create' } + ) + assert_routing( + { :method => 'get', :path => "/auth_sources/1234/edit" }, + { :controller => 'auth_sources', :action => 'edit', + :id => '1234' } + ) + assert_routing( + { :method => 'put', :path => "/auth_sources/1234" }, + { :controller => 'auth_sources', :action => 'update', + :id => '1234' } + ) + assert_routing( + { :method => 'delete', :path => "/auth_sources/1234" }, + { :controller => 'auth_sources', :action => 'destroy', + :id => '1234' } + ) + assert_routing( + { :method => 'get', :path => "/auth_sources/1234/test_connection" }, + { :controller => 'auth_sources', :action => 'test_connection', + :id => '1234' } + ) + assert_routing( + { :method => 'get', :path => "/auth_sources/autocomplete_for_new_user" }, + { :controller => 'auth_sources', :action => 'autocomplete_for_new_user' } + ) + end +end diff --git a/test/integration/routing/auto_completes_test.rb b/test/integration/routing/auto_completes_test.rb new file mode 100644 index 00000000..bd3502d3 --- /dev/null +++ b/test/integration/routing/auto_completes_test.rb @@ -0,0 +1,27 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingAutoCompletesTest < ActionController::IntegrationTest + def test_auto_completes + assert_routing( + { :method => 'get', :path => "/issues/auto_complete" }, + { :controller => 'auto_completes', :action => 'issues' } + ) + end +end diff --git a/test/integration/routing/boards_test.rb b/test/integration/routing/boards_test.rb new file mode 100644 index 00000000..aca98244 --- /dev/null +++ b/test/integration/routing/boards_test.rb @@ -0,0 +1,60 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingBoardsTest < ActionController::IntegrationTest + def test_boards + assert_routing( + { :method => 'get', :path => "/projects/world_domination/boards" }, + { :controller => 'boards', :action => 'index', :project_id => 'world_domination' } + ) + assert_routing( + { :method => 'get', :path => "/projects/world_domination/boards/new" }, + { :controller => 'boards', :action => 'new', :project_id => 'world_domination' } + ) + assert_routing( + { :method => 'get', :path => "/projects/world_domination/boards/44" }, + { :controller => 'boards', :action => 'show', :project_id => 'world_domination', + :id => '44' } + ) + assert_routing( + { :method => 'get', :path => "/projects/world_domination/boards/44.atom" }, + { :controller => 'boards', :action => 'show', :project_id => 'world_domination', + :id => '44', :format => 'atom' } + ) + assert_routing( + { :method => 'get', :path => "/projects/world_domination/boards/44/edit" }, + { :controller => 'boards', :action => 'edit', :project_id => 'world_domination', + :id => '44' } + ) + assert_routing( + { :method => 'post', :path => "/projects/world_domination/boards" }, + { :controller => 'boards', :action => 'create', :project_id => 'world_domination' } + ) + assert_routing( + { :method => 'put', :path => "/projects/world_domination/boards/44" }, + { :controller => 'boards', :action => 'update', :project_id => 'world_domination', + :id => '44' } + ) + assert_routing( + { :method => 'delete', :path => "/projects/world_domination/boards/44" }, + { :controller => 'boards', :action => 'destroy', :project_id => 'world_domination', + :id => '44' } + ) + end +end diff --git a/test/integration/routing/calendars_test.rb b/test/integration/routing/calendars_test.rb new file mode 100644 index 00000000..a927c713 --- /dev/null +++ b/test/integration/routing/calendars_test.rb @@ -0,0 +1,32 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingCalendarsTest < ActionController::IntegrationTest + def test_calendars + assert_routing( + { :method => 'get', :path => "/issues/calendar" }, + { :controller => 'calendars', :action => 'show' } + ) + assert_routing( + { :method => 'get', :path => "/projects/project-name/issues/calendar" }, + { :controller => 'calendars', :action => 'show', + :project_id => 'project-name' } + ) + end +end diff --git a/test/integration/routing/comments_test.rb b/test/integration/routing/comments_test.rb new file mode 100644 index 00000000..672ae0dd --- /dev/null +++ b/test/integration/routing/comments_test.rb @@ -0,0 +1,32 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingCommentsTest < ActionController::IntegrationTest + def test_comments + assert_routing( + { :method => 'post', :path => "/news/567/comments" }, + { :controller => 'comments', :action => 'create', :id => '567' } + ) + assert_routing( + { :method => 'delete', :path => "/news/567/comments/15" }, + { :controller => 'comments', :action => 'destroy', :id => '567', + :comment_id => '15' } + ) + end +end diff --git a/test/integration/routing/context_menus_test.rb b/test/integration/routing/context_menus_test.rb new file mode 100644 index 00000000..eab06886 --- /dev/null +++ b/test/integration/routing/context_menus_test.rb @@ -0,0 +1,38 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingContextMenusTest < ActionController::IntegrationTest + def test_context_menus_time_entries + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/time_entries/context_menu" }, + { :controller => 'context_menus', :action => 'time_entries' } + ) + end + end + + def test_context_menus_issues + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/issues/context_menu" }, + { :controller => 'context_menus', :action => 'issues' } + ) + end + end +end diff --git a/test/integration/routing/custom_fields_test.rb b/test/integration/routing/custom_fields_test.rb new file mode 100644 index 00000000..85d801c5 --- /dev/null +++ b/test/integration/routing/custom_fields_test.rb @@ -0,0 +1,47 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingCustomFieldsTest < ActionController::IntegrationTest + def test_custom_fields + assert_routing( + { :method => 'get', :path => "/custom_fields" }, + { :controller => 'custom_fields', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/custom_fields/new" }, + { :controller => 'custom_fields', :action => 'new' } + ) + assert_routing( + { :method => 'post', :path => "/custom_fields" }, + { :controller => 'custom_fields', :action => 'create' } + ) + assert_routing( + { :method => 'get', :path => "/custom_fields/2/edit" }, + { :controller => 'custom_fields', :action => 'edit', :id => '2' } + ) + assert_routing( + { :method => 'put', :path => "/custom_fields/2" }, + { :controller => 'custom_fields', :action => 'update', :id => '2' } + ) + assert_routing( + { :method => 'delete', :path => "/custom_fields/2" }, + { :controller => 'custom_fields', :action => 'destroy', :id => '2' } + ) + end +end diff --git a/test/integration/routing/documents_test.rb b/test/integration/routing/documents_test.rb new file mode 100644 index 00000000..05d7cfb1 --- /dev/null +++ b/test/integration/routing/documents_test.rb @@ -0,0 +1,58 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingDocumentsTest < ActionController::IntegrationTest + def test_documents_scoped_under_project + assert_routing( + { :method => 'get', :path => "/projects/567/documents" }, + { :controller => 'documents', :action => 'index', :project_id => '567' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/documents/new" }, + { :controller => 'documents', :action => 'new', :project_id => '567' } + ) + assert_routing( + { :method => 'post', :path => "/projects/567/documents" }, + { :controller => 'documents', :action => 'create', :project_id => '567' } + ) + end + + def test_documents + assert_routing( + { :method => 'get', :path => "/documents/22" }, + { :controller => 'documents', :action => 'show', :id => '22' } + ) + assert_routing( + { :method => 'get', :path => "/documents/22/edit" }, + { :controller => 'documents', :action => 'edit', :id => '22' } + ) + assert_routing( + { :method => 'put', :path => "/documents/22" }, + { :controller => 'documents', :action => 'update', :id => '22' } + ) + assert_routing( + { :method => 'delete', :path => "/documents/22" }, + { :controller => 'documents', :action => 'destroy', :id => '22' } + ) + assert_routing( + { :method => 'post', :path => "/documents/22/add_attachment" }, + { :controller => 'documents', :action => 'add_attachment', :id => '22' } + ) + end +end diff --git a/test/integration/routing/enumerations_test.rb b/test/integration/routing/enumerations_test.rb new file mode 100644 index 00000000..ada152ab --- /dev/null +++ b/test/integration/routing/enumerations_test.rb @@ -0,0 +1,51 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingEnumerationsTest < ActionController::IntegrationTest + def test_enumerations + assert_routing( + { :method => 'get', :path => "/enumerations" }, + { :controller => 'enumerations', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/enumerations/new" }, + { :controller => 'enumerations', :action => 'new' } + ) + assert_routing( + { :method => 'post', :path => "/enumerations" }, + { :controller => 'enumerations', :action => 'create' } + ) + assert_routing( + { :method => 'get', :path => "/enumerations/2/edit" }, + { :controller => 'enumerations', :action => 'edit', :id => '2' } + ) + assert_routing( + { :method => 'put', :path => "/enumerations/2" }, + { :controller => 'enumerations', :action => 'update', :id => '2' } + ) + assert_routing( + { :method => 'delete', :path => "/enumerations/2" }, + { :controller => 'enumerations', :action => 'destroy', :id => '2' } + ) + assert_routing( + { :method => 'get', :path => "/enumerations/issue_priorities.xml" }, + { :controller => 'enumerations', :action => 'index', :type => 'issue_priorities', :format => 'xml' } + ) + end +end diff --git a/test/integration/routing/files_test.rb b/test/integration/routing/files_test.rb new file mode 100644 index 00000000..dd974dde --- /dev/null +++ b/test/integration/routing/files_test.rb @@ -0,0 +1,35 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingFilesTest < ActionController::IntegrationTest + def test_files + assert_routing( + { :method => 'get', :path => "/projects/33/files" }, + { :controller => 'files', :action => 'index', :project_id => '33' } + ) + assert_routing( + { :method => 'get', :path => "/projects/33/files/new" }, + { :controller => 'files', :action => 'new', :project_id => '33' } + ) + assert_routing( + { :method => 'post', :path => "/projects/33/files" }, + { :controller => 'files', :action => 'create', :project_id => '33' } + ) + end +end diff --git a/test/integration/routing/gantts_test.rb b/test/integration/routing/gantts_test.rb new file mode 100644 index 00000000..01659253 --- /dev/null +++ b/test/integration/routing/gantts_test.rb @@ -0,0 +1,41 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingGanttsTest < ActionController::IntegrationTest + def test_gantts + assert_routing( + { :method => 'get', :path => "/issues/gantt" }, + { :controller => 'gantts', :action => 'show' } + ) + assert_routing( + { :method => 'get', :path => "/issues/gantt.pdf" }, + { :controller => 'gantts', :action => 'show', :format => 'pdf' } + ) + assert_routing( + { :method => 'get', :path => "/projects/project-name/issues/gantt" }, + { :controller => 'gantts', :action => 'show', + :project_id => 'project-name' } + ) + assert_routing( + { :method => 'get', :path => "/projects/project-name/issues/gantt.pdf" }, + { :controller => 'gantts', :action => 'show', + :project_id => 'project-name', :format => 'pdf' } + ) + end +end diff --git a/test/integration/routing/groups_test.rb b/test/integration/routing/groups_test.rb new file mode 100644 index 00000000..0e38946f --- /dev/null +++ b/test/integration/routing/groups_test.rb @@ -0,0 +1,106 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingGroupsTest < ActionController::IntegrationTest + def test_groups_resources + assert_routing( + { :method => 'get', :path => "/groups" }, + { :controller => 'groups', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/groups.xml" }, + { :controller => 'groups', :action => 'index', :format => 'xml' } + ) + assert_routing( + { :method => 'post', :path => "/groups" }, + { :controller => 'groups', :action => 'create' } + ) + assert_routing( + { :method => 'post', :path => "/groups.xml" }, + { :controller => 'groups', :action => 'create', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/groups/new" }, + { :controller => 'groups', :action => 'new' } + ) + assert_routing( + { :method => 'get', :path => "/groups/1/edit" }, + { :controller => 'groups', :action => 'edit', :id => '1' } + ) + assert_routing( + { :method => 'get', :path => "/groups/1/autocomplete_for_user" }, + { :controller => 'groups', :action => 'autocomplete_for_user', :id => '1' } + ) + assert_routing( + { :method => 'get', :path => "/groups/1/autocomplete_for_user.js" }, + { :controller => 'groups', :action => 'autocomplete_for_user', :id => '1', :format => 'js' } + ) + assert_routing( + { :method => 'get', :path => "/groups/1" }, + { :controller => 'groups', :action => 'show', :id => '1' } + ) + assert_routing( + { :method => 'get', :path => "/groups/1.xml" }, + { :controller => 'groups', :action => 'show', :id => '1', :format => 'xml' } + ) + assert_routing( + { :method => 'put', :path => "/groups/1" }, + { :controller => 'groups', :action => 'update', :id => '1' } + ) + assert_routing( + { :method => 'put', :path => "/groups/1.xml" }, + { :controller => 'groups', :action => 'update', :id => '1', :format => 'xml' } + ) + assert_routing( + { :method => 'delete', :path => "/groups/1" }, + { :controller => 'groups', :action => 'destroy', :id => '1' } + ) + assert_routing( + { :method => 'delete', :path => "/groups/1.xml" }, + { :controller => 'groups', :action => 'destroy', :id => '1', :format => 'xml' } + ) + end + + def test_groups + assert_routing( + { :method => 'post', :path => "/groups/567/users" }, + { :controller => 'groups', :action => 'add_users', :id => '567' } + ) + assert_routing( + { :method => 'post', :path => "/groups/567/users.xml" }, + { :controller => 'groups', :action => 'add_users', :id => '567', :format => 'xml' } + ) + assert_routing( + { :method => 'delete', :path => "/groups/567/users/12" }, + { :controller => 'groups', :action => 'remove_user', :id => '567', :user_id => '12' } + ) + assert_routing( + { :method => 'delete', :path => "/groups/567/users/12.xml" }, + { :controller => 'groups', :action => 'remove_user', :id => '567', :user_id => '12', :format => 'xml' } + ) + assert_routing( + { :method => 'post', :path => "/groups/destroy_membership/567" }, + { :controller => 'groups', :action => 'destroy_membership', :id => '567' } + ) + assert_routing( + { :method => 'post', :path => "/groups/edit_membership/567" }, + { :controller => 'groups', :action => 'edit_membership', :id => '567' } + ) + end +end diff --git a/test/integration/routing/issue_categories_test.rb b/test/integration/routing/issue_categories_test.rb new file mode 100644 index 00000000..295c405e --- /dev/null +++ b/test/integration/routing/issue_categories_test.rb @@ -0,0 +1,107 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingIssueCategoriesTest < ActionController::IntegrationTest + def test_issue_categories_scoped_under_project + assert_routing( + { :method => 'get', :path => "/projects/foo/issue_categories" }, + { :controller => 'issue_categories', :action => 'index', + :project_id => 'foo' } + ) + assert_routing( + { :method => 'get', :path => "/projects/foo/issue_categories.xml" }, + { :controller => 'issue_categories', :action => 'index', + :project_id => 'foo', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/projects/foo/issue_categories.json" }, + { :controller => 'issue_categories', :action => 'index', + :project_id => 'foo', :format => 'json' } + ) + assert_routing( + { :method => 'get', :path => "/projects/foo/issue_categories/new" }, + { :controller => 'issue_categories', :action => 'new', + :project_id => 'foo' } + ) + assert_routing( + { :method => 'post', :path => "/projects/foo/issue_categories" }, + { :controller => 'issue_categories', :action => 'create', + :project_id => 'foo' } + ) + assert_routing( + { :method => 'post', :path => "/projects/foo/issue_categories.xml" }, + { :controller => 'issue_categories', :action => 'create', + :project_id => 'foo', :format => 'xml' } + ) + assert_routing( + { :method => 'post', :path => "/projects/foo/issue_categories.json" }, + { :controller => 'issue_categories', :action => 'create', + :project_id => 'foo', :format => 'json' } + ) + end + + def test_issue_categories + assert_routing( + { :method => 'get', :path => "/issue_categories/1" }, + { :controller => 'issue_categories', :action => 'show', :id => '1' } + ) + assert_routing( + { :method => 'get', :path => "/issue_categories/1.xml" }, + { :controller => 'issue_categories', :action => 'show', :id => '1', + :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/issue_categories/1.json" }, + { :controller => 'issue_categories', :action => 'show', :id => '1', + :format => 'json' } + ) + assert_routing( + { :method => 'get', :path => "/issue_categories/1/edit" }, + { :controller => 'issue_categories', :action => 'edit', :id => '1' } + ) + assert_routing( + { :method => 'put', :path => "/issue_categories/1" }, + { :controller => 'issue_categories', :action => 'update', :id => '1' } + ) + assert_routing( + { :method => 'put', :path => "/issue_categories/1.xml" }, + { :controller => 'issue_categories', :action => 'update', :id => '1', + :format => 'xml' } + ) + assert_routing( + { :method => 'put', :path => "/issue_categories/1.json" }, + { :controller => 'issue_categories', :action => 'update', :id => '1', + :format => 'json' } + ) + assert_routing( + { :method => 'delete', :path => "/issue_categories/1" }, + { :controller => 'issue_categories', :action => 'destroy', :id => '1' } + ) + assert_routing( + { :method => 'delete', :path => "/issue_categories/1.xml" }, + { :controller => 'issue_categories', :action => 'destroy', :id => '1', + :format => 'xml' } + ) + assert_routing( + { :method => 'delete', :path => "/issue_categories/1.json" }, + { :controller => 'issue_categories', :action => 'destroy', :id => '1', + :format => 'json' } + ) + end +end diff --git a/test/integration/routing/issue_relations_test.rb b/test/integration/routing/issue_relations_test.rb new file mode 100644 index 00000000..bf1ae9df --- /dev/null +++ b/test/integration/routing/issue_relations_test.rb @@ -0,0 +1,81 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingIssueRelationsTest < ActionController::IntegrationTest + def test_issue_relations + assert_routing( + { :method => 'get', :path => "/issues/1/relations" }, + { :controller => 'issue_relations', :action => 'index', + :issue_id => '1' } + ) + assert_routing( + { :method => 'get', :path => "/issues/1/relations.xml" }, + { :controller => 'issue_relations', :action => 'index', + :issue_id => '1', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/issues/1/relations.json" }, + { :controller => 'issue_relations', :action => 'index', + :issue_id => '1', :format => 'json' } + ) + assert_routing( + { :method => 'post', :path => "/issues/1/relations" }, + { :controller => 'issue_relations', :action => 'create', + :issue_id => '1' } + ) + assert_routing( + { :method => 'post', :path => "/issues/1/relations.xml" }, + { :controller => 'issue_relations', :action => 'create', + :issue_id => '1', :format => 'xml' } + ) + assert_routing( + { :method => 'post', :path => "/issues/1/relations.json" }, + { :controller => 'issue_relations', :action => 'create', + :issue_id => '1', :format => 'json' } + ) + assert_routing( + { :method => 'get', :path => "/relations/23" }, + { :controller => 'issue_relations', :action => 'show', :id => '23' } + ) + assert_routing( + { :method => 'get', :path => "/relations/23.xml" }, + { :controller => 'issue_relations', :action => 'show', :id => '23', + :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/relations/23.json" }, + { :controller => 'issue_relations', :action => 'show', :id => '23', + :format => 'json' } + ) + assert_routing( + { :method => 'delete', :path => "/relations/23" }, + { :controller => 'issue_relations', :action => 'destroy', :id => '23' } + ) + assert_routing( + { :method => 'delete', :path => "/relations/23.xml" }, + { :controller => 'issue_relations', :action => 'destroy', :id => '23', + :format => 'xml' } + ) + assert_routing( + { :method => 'delete', :path => "/relations/23.json" }, + { :controller => 'issue_relations', :action => 'destroy', :id => '23', + :format => 'json' } + ) + end +end diff --git a/test/integration/routing/issue_statuses_test.rb b/test/integration/routing/issue_statuses_test.rb new file mode 100644 index 00000000..1d961265 --- /dev/null +++ b/test/integration/routing/issue_statuses_test.rb @@ -0,0 +1,80 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingIssueStatusesTest < ActionController::IntegrationTest + def test_issue_statuses + assert_routing( + { :method => 'get', :path => "/issue_statuses" }, + { :controller => 'issue_statuses', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/issue_statuses.xml" }, + { :controller => 'issue_statuses', :action => 'index', :format => 'xml' } + ) + assert_routing( + { :method => 'post', :path => "/issue_statuses" }, + { :controller => 'issue_statuses', :action => 'create' } + ) + assert_routing( + { :method => 'post', :path => "/issue_statuses.xml" }, + { :controller => 'issue_statuses', :action => 'create', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/issue_statuses/new" }, + { :controller => 'issue_statuses', :action => 'new' } + ) + assert_routing( + { :method => 'get', :path => "/issue_statuses/new.xml" }, + { :controller => 'issue_statuses', :action => 'new', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/issue_statuses/1/edit" }, + { :controller => 'issue_statuses', :action => 'edit', :id => '1' } + ) + assert_routing( + { :method => 'put', :path => "/issue_statuses/1" }, + { :controller => 'issue_statuses', :action => 'update', + :id => '1' } + ) + assert_routing( + { :method => 'put', :path => "/issue_statuses/1.xml" }, + { :controller => 'issue_statuses', :action => 'update', + :format => 'xml', :id => '1' } + ) + assert_routing( + { :method => 'delete', :path => "/issue_statuses/1" }, + { :controller => 'issue_statuses', :action => 'destroy', + :id => '1' } + ) + assert_routing( + { :method => 'delete', :path => "/issue_statuses/1.xml" }, + { :controller => 'issue_statuses', :action => 'destroy', + :format => 'xml', :id => '1' } + ) + assert_routing( + { :method => 'post', :path => "/issue_statuses/update_issue_done_ratio" }, + { :controller => 'issue_statuses', :action => 'update_issue_done_ratio' } + ) + assert_routing( + { :method => 'post', :path => "/issue_statuses/update_issue_done_ratio.xml" }, + { :controller => 'issue_statuses', :action => 'update_issue_done_ratio', + :format => 'xml' } + ) + end +end diff --git a/test/integration/routing/issues_test.rb b/test/integration/routing/issues_test.rb new file mode 100644 index 00000000..bc8d2063 --- /dev/null +++ b/test/integration/routing/issues_test.rb @@ -0,0 +1,134 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingIssuesTest < ActionController::IntegrationTest + def test_issues_rest_actions + assert_routing( + { :method => 'get', :path => "/issues" }, + { :controller => 'issues', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/issues.pdf" }, + { :controller => 'issues', :action => 'index', :format => 'pdf' } + ) + assert_routing( + { :method => 'get', :path => "/issues.atom" }, + { :controller => 'issues', :action => 'index', :format => 'atom' } + ) + assert_routing( + { :method => 'get', :path => "/issues.xml" }, + { :controller => 'issues', :action => 'index', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/issues/64" }, + { :controller => 'issues', :action => 'show', :id => '64' } + ) + assert_routing( + { :method => 'get', :path => "/issues/64.pdf" }, + { :controller => 'issues', :action => 'show', :id => '64', + :format => 'pdf' } + ) + assert_routing( + { :method => 'get', :path => "/issues/64.atom" }, + { :controller => 'issues', :action => 'show', :id => '64', + :format => 'atom' } + ) + assert_routing( + { :method => 'get', :path => "/issues/64.xml" }, + { :controller => 'issues', :action => 'show', :id => '64', + :format => 'xml' } + ) + assert_routing( + { :method => 'post', :path => "/issues.xml" }, + { :controller => 'issues', :action => 'create', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/issues/64/edit" }, + { :controller => 'issues', :action => 'edit', :id => '64' } + ) + assert_routing( + { :method => 'put', :path => "/issues/1.xml" }, + { :controller => 'issues', :action => 'update', :id => '1', + :format => 'xml' } + ) + assert_routing( + { :method => 'delete', :path => "/issues/1.xml" }, + { :controller => 'issues', :action => 'destroy', :id => '1', + :format => 'xml' } + ) + end + + def test_issues_rest_actions_scoped_under_project + assert_routing( + { :method => 'get', :path => "/projects/23/issues" }, + { :controller => 'issues', :action => 'index', :project_id => '23' } + ) + assert_routing( + { :method => 'get', :path => "/projects/23/issues.pdf" }, + { :controller => 'issues', :action => 'index', :project_id => '23', + :format => 'pdf' } + ) + assert_routing( + { :method => 'get', :path => "/projects/23/issues.atom" }, + { :controller => 'issues', :action => 'index', :project_id => '23', + :format => 'atom' } + ) + assert_routing( + { :method => 'get', :path => "/projects/23/issues.xml" }, + { :controller => 'issues', :action => 'index', :project_id => '23', + :format => 'xml' } + ) + assert_routing( + { :method => 'post', :path => "/projects/23/issues" }, + { :controller => 'issues', :action => 'create', :project_id => '23' } + ) + assert_routing( + { :method => 'get', :path => "/projects/23/issues/new" }, + { :controller => 'issues', :action => 'new', :project_id => '23' } + ) + end + + def test_issues_form_update + ["post", "put"].each do |method| + assert_routing( + { :method => method, :path => "/projects/23/issues/update_form" }, + { :controller => 'issues', :action => 'update_form', :project_id => '23' } + ) + end + end + + def test_issues_extra_actions + assert_routing( + { :method => 'get', :path => "/projects/23/issues/64/copy" }, + { :controller => 'issues', :action => 'new', :project_id => '23', + :copy_from => '64' } + ) + # For updating the bulk edit form + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/issues/bulk_edit" }, + { :controller => 'issues', :action => 'bulk_edit' } + ) + end + assert_routing( + { :method => 'post', :path => "/issues/bulk_update" }, + { :controller => 'issues', :action => 'bulk_update' } + ) + end +end diff --git a/test/integration/routing/journals_test.rb b/test/integration/routing/journals_test.rb new file mode 100644 index 00000000..c1a08b61 --- /dev/null +++ b/test/integration/routing/journals_test.rb @@ -0,0 +1,41 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingJournalsTest < ActionController::IntegrationTest + def test_journals + assert_routing( + { :method => 'post', :path => "/issues/1/quoted" }, + { :controller => 'journals', :action => 'new', :id => '1' } + ) + assert_routing( + { :method => 'get', :path => "/issues/changes" }, + { :controller => 'journals', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/journals/diff/1" }, + { :controller => 'journals', :action => 'diff', :id => '1' } + ) + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/journals/edit/1" }, + { :controller => 'journals', :action => 'edit', :id => '1' } + ) + end + end +end diff --git a/test/integration/routing/mail_handler_test.rb b/test/integration/routing/mail_handler_test.rb new file mode 100644 index 00000000..4617c6b4 --- /dev/null +++ b/test/integration/routing/mail_handler_test.rb @@ -0,0 +1,27 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingMailHandlerTest < ActionController::IntegrationTest + def test_mail_handler + assert_routing( + { :method => "post", :path => "/mail_handler" }, + { :controller => 'mail_handler', :action => 'index' } + ) + end +end diff --git a/test/integration/routing/members_test.rb b/test/integration/routing/members_test.rb new file mode 100644 index 00000000..fce9890e --- /dev/null +++ b/test/integration/routing/members_test.rb @@ -0,0 +1,63 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingMembersTest < ActionController::IntegrationTest + def test_members + assert_routing( + { :method => 'get', :path => "/projects/5234/memberships.xml" }, + { :controller => 'members', :action => 'index', :project_id => '5234', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/memberships/5234.xml" }, + { :controller => 'members', :action => 'show', :id => '5234', :format => 'xml' } + ) + assert_routing( + { :method => 'post', :path => "/projects/5234/memberships" }, + { :controller => 'members', :action => 'create', :project_id => '5234' } + ) + assert_routing( + { :method => 'post', :path => "/projects/5234/memberships.xml" }, + { :controller => 'members', :action => 'create', :project_id => '5234', :format => 'xml' } + ) + assert_routing( + { :method => 'put', :path => "/memberships/5234" }, + { :controller => 'members', :action => 'update', :id => '5234' } + ) + assert_routing( + { :method => 'put', :path => "/memberships/5234.xml" }, + { :controller => 'members', :action => 'update', :id => '5234', :format => 'xml' } + ) + assert_routing( + { :method => 'delete', :path => "/memberships/5234" }, + { :controller => 'members', :action => 'destroy', :id => '5234' } + ) + assert_routing( + { :method => 'delete', :path => "/memberships/5234.xml" }, + { :controller => 'members', :action => 'destroy', :id => '5234', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/projects/5234/memberships/autocomplete" }, + { :controller => 'members', :action => 'autocomplete', :project_id => '5234' } + ) + assert_routing( + { :method => 'get', :path => "/projects/5234/memberships/autocomplete.js" }, + { :controller => 'members', :action => 'autocomplete', :project_id => '5234', :format => 'js' } + ) + end +end diff --git a/test/integration/routing/messages_test.rb b/test/integration/routing/messages_test.rb new file mode 100644 index 00000000..bbb1635a --- /dev/null +++ b/test/integration/routing/messages_test.rb @@ -0,0 +1,66 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingMessagesTest < ActionController::IntegrationTest + def test_messages + assert_routing( + { :method => 'get', :path => "/boards/22/topics/2" }, + { :controller => 'messages', :action => 'show', :id => '2', + :board_id => '22' } + ) + assert_routing( + { :method => 'get', :path => "/boards/lala/topics/new" }, + { :controller => 'messages', :action => 'new', :board_id => 'lala' } + ) + assert_routing( + { :method => 'get', :path => "/boards/lala/topics/22/edit" }, + { :controller => 'messages', :action => 'edit', :id => '22', + :board_id => 'lala' } + ) + assert_routing( + { :method => 'post', :path => "/boards/lala/topics/quote/22" }, + { :controller => 'messages', :action => 'quote', :id => '22', + :board_id => 'lala' } + ) + assert_routing( + { :method => 'post', :path => "/boards/lala/topics/new" }, + { :controller => 'messages', :action => 'new', :board_id => 'lala' } + ) + assert_routing( + { :method => 'post', :path => "/boards/lala/topics/preview" }, + { :controller => 'messages', :action => 'preview', + :board_id => 'lala' } + ) + assert_routing( + { :method => 'post', :path => "/boards/lala/topics/22/edit" }, + { :controller => 'messages', :action => 'edit', :id => '22', + :board_id => 'lala' } + ) + assert_routing( + { :method => 'post', :path => "/boards/22/topics/555/replies" }, + { :controller => 'messages', :action => 'reply', :id => '555', + :board_id => '22' } + ) + assert_routing( + { :method => 'post', :path => "/boards/22/topics/555/destroy" }, + { :controller => 'messages', :action => 'destroy', :id => '555', + :board_id => '22' } + ) + end +end diff --git a/test/integration/routing/my_test.rb b/test/integration/routing/my_test.rb new file mode 100644 index 00000000..13595313 --- /dev/null +++ b/test/integration/routing/my_test.rb @@ -0,0 +1,73 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingMyTest < ActionController::IntegrationTest + def test_my + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/my/account" }, + { :controller => 'my', :action => 'account' } + ) + end + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/my/account/destroy" }, + { :controller => 'my', :action => 'destroy' } + ) + end + assert_routing( + { :method => 'get', :path => "/my/page" }, + { :controller => 'my', :action => 'page' } + ) + assert_routing( + { :method => 'get', :path => "/my" }, + { :controller => 'my', :action => 'index' } + ) + assert_routing( + { :method => 'post', :path => "/my/reset_rss_key" }, + { :controller => 'my', :action => 'reset_rss_key' } + ) + assert_routing( + { :method => 'post', :path => "/my/reset_api_key" }, + { :controller => 'my', :action => 'reset_api_key' } + ) + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/my/password" }, + { :controller => 'my', :action => 'password' } + ) + end + assert_routing( + { :method => 'get', :path => "/my/page_layout" }, + { :controller => 'my', :action => 'page_layout' } + ) + assert_routing( + { :method => 'post', :path => "/my/add_block" }, + { :controller => 'my', :action => 'add_block' } + ) + assert_routing( + { :method => 'post', :path => "/my/remove_block" }, + { :controller => 'my', :action => 'remove_block' } + ) + assert_routing( + { :method => 'post', :path => "/my/order_blocks" }, + { :controller => 'my', :action => 'order_blocks' } + ) + end +end diff --git a/test/integration/routing/news_test.rb b/test/integration/routing/news_test.rb new file mode 100644 index 00000000..b60374e3 --- /dev/null +++ b/test/integration/routing/news_test.rb @@ -0,0 +1,92 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingNewsTest < ActionController::IntegrationTest + def test_news_index + assert_routing( + { :method => 'get', :path => "/news" }, + { :controller => 'news', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/news.atom" }, + { :controller => 'news', :action => 'index', :format => 'atom' } + ) + assert_routing( + { :method => 'get', :path => "/news.xml" }, + { :controller => 'news', :action => 'index', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/news.json" }, + { :controller => 'news', :action => 'index', :format => 'json' } + ) + end + + def test_news + assert_routing( + { :method => 'get', :path => "/news/2" }, + { :controller => 'news', :action => 'show', :id => '2' } + ) + assert_routing( + { :method => 'get', :path => "/news/234" }, + { :controller => 'news', :action => 'show', :id => '234' } + ) + assert_routing( + { :method => 'get', :path => "/news/567/edit" }, + { :controller => 'news', :action => 'edit', :id => '567' } + ) + assert_routing( + { :method => 'put', :path => "/news/567" }, + { :controller => 'news', :action => 'update', :id => '567' } + ) + assert_routing( + { :method => 'delete', :path => "/news/567" }, + { :controller => 'news', :action => 'destroy', :id => '567' } + ) + end + + def test_news_scoped_under_project + assert_routing( + { :method => 'get', :path => "/projects/567/news" }, + { :controller => 'news', :action => 'index', :project_id => '567' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/news.atom" }, + { :controller => 'news', :action => 'index', :format => 'atom', + :project_id => '567' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/news.xml" }, + { :controller => 'news', :action => 'index', :format => 'xml', + :project_id => '567' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/news.json" }, + { :controller => 'news', :action => 'index', :format => 'json', + :project_id => '567' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/news/new" }, + { :controller => 'news', :action => 'new', :project_id => '567' } + ) + assert_routing( + { :method => 'post', :path => "/projects/567/news" }, + { :controller => 'news', :action => 'create', :project_id => '567' } + ) + end +end diff --git a/test/integration/routing/previews_test.rb b/test/integration/routing/previews_test.rb new file mode 100644 index 00000000..830fe94b --- /dev/null +++ b/test/integration/routing/previews_test.rb @@ -0,0 +1,37 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingPreviewsTest < ActionController::IntegrationTest + def test_previews + ["get", "post", "put"].each do |method| + assert_routing( + { :method => method, :path => "/issues/preview/new/123" }, + { :controller => 'previews', :action => 'issue', :project_id => '123' } + ) + assert_routing( + { :method => method, :path => "/issues/preview/edit/321" }, + { :controller => 'previews', :action => 'issue', :id => '321' } + ) + end + assert_routing( + { :method => 'get', :path => "/news/preview" }, + { :controller => 'previews', :action => 'news' } + ) + end +end diff --git a/test/integration/routing/project_enumerations_test.rb b/test/integration/routing/project_enumerations_test.rb new file mode 100644 index 00000000..3547d034 --- /dev/null +++ b/test/integration/routing/project_enumerations_test.rb @@ -0,0 +1,33 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingProjectEnumerationsTest < ActionController::IntegrationTest + def test_project_enumerations + assert_routing( + { :method => 'put', :path => "/projects/64/enumerations" }, + { :controller => 'project_enumerations', :action => 'update', + :project_id => '64' } + ) + assert_routing( + { :method => 'delete', :path => "/projects/64/enumerations" }, + { :controller => 'project_enumerations', :action => 'destroy', + :project_id => '64' } + ) + end +end diff --git a/test/integration/routing/projects_test.rb b/test/integration/routing/projects_test.rb new file mode 100644 index 00000000..46ee57ee --- /dev/null +++ b/test/integration/routing/projects_test.rb @@ -0,0 +1,99 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingProjectsTest < ActionController::IntegrationTest + def test_projects + assert_routing( + { :method => 'get', :path => "/projects" }, + { :controller => 'projects', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/projects.atom" }, + { :controller => 'projects', :action => 'index', :format => 'atom' } + ) + assert_routing( + { :method => 'get', :path => "/projects.xml" }, + { :controller => 'projects', :action => 'index', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/projects/new" }, + { :controller => 'projects', :action => 'new' } + ) + assert_routing( + { :method => 'get', :path => "/projects/test" }, + { :controller => 'projects', :action => 'show', :id => 'test' } + ) + assert_routing( + { :method => 'get', :path => "/projects/1.xml" }, + { :controller => 'projects', :action => 'show', :id => '1', + :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/projects/4223/settings" }, + { :controller => 'projects', :action => 'settings', :id => '4223' } + ) + assert_routing( + { :method => 'get', :path => "/projects/4223/settings/members" }, + { :controller => 'projects', :action => 'settings', :id => '4223', + :tab => 'members' } + ) + assert_routing( + { :method => 'post', :path => "/projects" }, + { :controller => 'projects', :action => 'create' } + ) + assert_routing( + { :method => 'post', :path => "/projects.xml" }, + { :controller => 'projects', :action => 'create', :format => 'xml' } + ) + assert_routing( + { :method => 'post', :path => "/projects/64/archive" }, + { :controller => 'projects', :action => 'archive', :id => '64' } + ) + assert_routing( + { :method => 'post', :path => "/projects/64/unarchive" }, + { :controller => 'projects', :action => 'unarchive', :id => '64' } + ) + assert_routing( + { :method => 'post', :path => "/projects/64/close" }, + { :controller => 'projects', :action => 'close', :id => '64' } + ) + assert_routing( + { :method => 'post', :path => "/projects/64/reopen" }, + { :controller => 'projects', :action => 'reopen', :id => '64' } + ) + assert_routing( + { :method => 'put', :path => "/projects/4223" }, + { :controller => 'projects', :action => 'update', :id => '4223' } + ) + assert_routing( + { :method => 'put', :path => "/projects/1.xml" }, + { :controller => 'projects', :action => 'update', :id => '1', + :format => 'xml' } + ) + assert_routing( + { :method => 'delete', :path => "/projects/64" }, + { :controller => 'projects', :action => 'destroy', :id => '64' } + ) + assert_routing( + { :method => 'delete', :path => "/projects/1.xml" }, + { :controller => 'projects', :action => 'destroy', :id => '1', + :format => 'xml' } + ) + end +end diff --git a/test/integration/routing/queries_test.rb b/test/integration/routing/queries_test.rb new file mode 100644 index 00000000..24983f2b --- /dev/null +++ b/test/integration/routing/queries_test.rb @@ -0,0 +1,62 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingQueriesTest < ActionController::IntegrationTest + def test_queries + assert_routing( + { :method => 'get', :path => "/queries.xml" }, + { :controller => 'queries', :action => 'index', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/queries.json" }, + { :controller => 'queries', :action => 'index', :format => 'json' } + ) + assert_routing( + { :method => 'get', :path => "/queries/new" }, + { :controller => 'queries', :action => 'new' } + ) + assert_routing( + { :method => 'post', :path => "/queries" }, + { :controller => 'queries', :action => 'create' } + ) + assert_routing( + { :method => 'get', :path => "/queries/1/edit" }, + { :controller => 'queries', :action => 'edit', :id => '1' } + ) + assert_routing( + { :method => 'put', :path => "/queries/1" }, + { :controller => 'queries', :action => 'update', :id => '1' } + ) + assert_routing( + { :method => 'delete', :path => "/queries/1" }, + { :controller => 'queries', :action => 'destroy', :id => '1' } + ) + end + + def test_queries_scoped_under_project + assert_routing( + { :method => 'get', :path => "/projects/redmine/queries/new" }, + { :controller => 'queries', :action => 'new', :project_id => 'redmine' } + ) + assert_routing( + { :method => 'post', :path => "/projects/redmine/queries" }, + { :controller => 'queries', :action => 'create', :project_id => 'redmine' } + ) + end +end diff --git a/test/integration/routing/reports_test.rb b/test/integration/routing/reports_test.rb new file mode 100644 index 00000000..ceb5539e --- /dev/null +++ b/test/integration/routing/reports_test.rb @@ -0,0 +1,32 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingReportsTest < ActionController::IntegrationTest + def test_reports + assert_routing( + { :method => 'get', :path => "/projects/567/issues/report" }, + { :controller => 'reports', :action => 'issue_report', :id => '567' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/issues/report/assigned_to" }, + { :controller => 'reports', :action => 'issue_report_details', + :id => '567', :detail => 'assigned_to' } + ) + end +end diff --git a/test/integration/routing/repositories_test.rb b/test/integration/routing/repositories_test.rb new file mode 100644 index 00000000..9f05d40f --- /dev/null +++ b/test/integration/routing/repositories_test.rb @@ -0,0 +1,431 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingRepositoriesTest < ActionController::IntegrationTest + def setup + @path_hash = repository_path_hash(%w[path to file.c]) + assert_equal "path/to/file.c", @path_hash[:path] + assert_equal "path/to/file.c", @path_hash[:param] + end + + def test_repositories_resources + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repositories/new" }, + { :controller => 'repositories', :action => 'new', :project_id => 'redmine' } + ) + assert_routing( + { :method => 'post', + :path => "/projects/redmine/repositories" }, + { :controller => 'repositories', :action => 'create', :project_id => 'redmine' } + ) + assert_routing( + { :method => 'get', + :path => "/repositories/1/edit" }, + { :controller => 'repositories', :action => 'edit', :id => '1' } + ) + assert_routing( + { :method => 'put', + :path => "/repositories/1" }, + { :controller => 'repositories', :action => 'update', :id => '1' } + ) + assert_routing( + { :method => 'delete', + :path => "/repositories/1" }, + { :controller => 'repositories', :action => 'destroy', :id => '1' } + ) + ["get", "post"].each do |method| + assert_routing( + { :method => method, + :path => "/repositories/1/committers" }, + { :controller => 'repositories', :action => 'committers', :id => '1' } + ) + end + end + + def test_repositories_show + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository" }, + { :controller => 'repositories', :action => 'show', :id => 'redmine' } + ) + end + + def test_repositories + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/statistics" }, + { :controller => 'repositories', :action => 'stats', :id => 'redmine' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/graph" }, + { :controller => 'repositories', :action => 'graph', :id => 'redmine' } + ) + end + + def test_repositories_show_with_repository_id + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo" }, + { :controller => 'repositories', :action => 'show', :id => 'redmine', :repository_id => 'foo' } + ) + end + + def test_repositories_with_repository_id + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/statistics" }, + { :controller => 'repositories', :action => 'stats', :id => 'redmine', :repository_id => 'foo' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/graph" }, + { :controller => 'repositories', :action => 'graph', :id => 'redmine', :repository_id => 'foo' } + ) + end + + def test_repositories_revisions + empty_path_param = [] + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revisions" }, + { :controller => 'repositories', :action => 'revisions', :id => 'redmine' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revisions.atom" }, + { :controller => 'repositories', :action => 'revisions', :id => 'redmine', + :format => 'atom' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revisions/2457" }, + { :controller => 'repositories', :action => 'revision', :id => 'redmine', + :rev => '2457' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revisions/2457/show" }, + { :controller => 'repositories', :action => 'show', :id => 'redmine', + :rev => '2457' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revisions/2457/show/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'show', :id => 'redmine', + :path => @path_hash[:param] , :rev => '2457'} + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revisions/2457/diff" }, + { :controller => 'repositories', :action => 'diff', :id => 'redmine', + :rev => '2457' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revisions/2457/diff" }, + { :controller => 'repositories', :action => 'diff', :id => 'redmine', + :rev => '2457', :format => 'diff' }, + {}, + { :format => 'diff' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revisions/2/diff/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'diff', :id => 'redmine', + :path => @path_hash[:param], :rev => '2' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revisions/2/diff/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'diff', :id => 'redmine', + :path => @path_hash[:param], :rev => '2', :format => 'diff' }, + {}, + { :format => 'diff' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revisions/2/entry/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'entry', :id => 'redmine', + :path => @path_hash[:param], :rev => '2' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revisions/2/raw/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'raw', :id => 'redmine', + :path => @path_hash[:param], :rev => '2' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revisions/2/annotate/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'annotate', :id => 'redmine', + :path => @path_hash[:param], :rev => '2' } + ) + end + + def test_repositories_revisions_with_repository_id + empty_path_param = [] + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revisions" }, + { :controller => 'repositories', :action => 'revisions', :id => 'redmine', :repository_id => 'foo' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revisions.atom" }, + { :controller => 'repositories', :action => 'revisions', :id => 'redmine', :repository_id => 'foo', + :format => 'atom' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revisions/2457" }, + { :controller => 'repositories', :action => 'revision', :id => 'redmine', :repository_id => 'foo', + :rev => '2457' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revisions/2457/show" }, + { :controller => 'repositories', :action => 'show', :id => 'redmine', :repository_id => 'foo', + :rev => '2457' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revisions/2457/show/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'show', :id => 'redmine', :repository_id => 'foo', + :path => @path_hash[:param] , :rev => '2457'} + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revisions/2457/diff" }, + { :controller => 'repositories', :action => 'diff', :id => 'redmine', :repository_id => 'foo', + :rev => '2457' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revisions/2457/diff" }, + { :controller => 'repositories', :action => 'diff', :id => 'redmine', :repository_id => 'foo', + :rev => '2457', :format => 'diff' }, + {}, + { :format => 'diff' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revisions/2/diff/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'diff', :id => 'redmine', :repository_id => 'foo', + :path => @path_hash[:param], :rev => '2' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revisions/2/diff/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'diff', :id => 'redmine', :repository_id => 'foo', + :path => @path_hash[:param], :rev => '2', :format => 'diff' }, + {}, + { :format => 'diff' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revisions/2/entry/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'entry', :id => 'redmine', :repository_id => 'foo', + :path => @path_hash[:param], :rev => '2' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revisions/2/raw/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'raw', :id => 'redmine', :repository_id => 'foo', + :path => @path_hash[:param], :rev => '2' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revisions/2/annotate/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'annotate', :id => 'redmine', :repository_id => 'foo', + :path => @path_hash[:param], :rev => '2' } + ) + end + + def test_repositories_non_revisions_path + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/changes" }, + { :controller => 'repositories', :action => 'changes', :id => 'redmine' } + ) + ['2457', 'master', 'slash/slash'].each do |rev| + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/changes" }, + { :controller => 'repositories', :action => 'changes', :id => 'redmine', + :rev => rev }, + {}, + { :rev => rev } + ) + end + ['2457', 'master', 'slash/slash'].each do |rev| + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/changes/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'changes', :id => 'redmine', + :path => @path_hash[:param], :rev => rev }, + {}, + { :rev => rev } + ) + end + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/diff/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'diff', :id => 'redmine', + :path => @path_hash[:param] } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/browse/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'browse', :id => 'redmine', + :path => @path_hash[:param] } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/entry/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'entry', :id => 'redmine', + :path => @path_hash[:param] } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/raw/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'raw', :id => 'redmine', + :path => @path_hash[:param] } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/annotate/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'annotate', :id => 'redmine', + :path => @path_hash[:param] } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/changes/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'changes', :id => 'redmine', + :path => @path_hash[:param] } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revision" }, + { :controller => 'repositories', :action => 'revision', :id => 'redmine' } + ) + end + + def test_repositories_non_revisions_path_with_repository_id + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/changes" }, + { :controller => 'repositories', :action => 'changes', + :id => 'redmine', :repository_id => 'foo' } + ) + ['2457', 'master', 'slash/slash'].each do |rev| + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/changes" }, + { :controller => 'repositories', :action => 'changes', + :id => 'redmine', + :repository_id => 'foo', :rev => rev }, + {}, + { :rev => rev } + ) + end + ['2457', 'master', 'slash/slash'].each do |rev| + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/changes/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'changes', :id => 'redmine', + :repository_id => 'foo', :path => @path_hash[:param], :rev => rev }, + {}, + { :rev => rev } + ) + end + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/diff/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'diff', :id => 'redmine', :repository_id => 'foo', + :path => @path_hash[:param] } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/browse/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'browse', :id => 'redmine', :repository_id => 'foo', + :path => @path_hash[:param] } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/entry/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'entry', :id => 'redmine', :repository_id => 'foo', + :path => @path_hash[:param] } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/raw/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'raw', :id => 'redmine', :repository_id => 'foo', + :path => @path_hash[:param] } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/annotate/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'annotate', :id => 'redmine', :repository_id => 'foo', + :path => @path_hash[:param] } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/changes/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'changes', :id => 'redmine', :repository_id => 'foo', + :path => @path_hash[:param] } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revision" }, + { :controller => 'repositories', :action => 'revision', :id => 'redmine', :repository_id => 'foo'} + ) + end + + def test_repositories_related_issues + assert_routing( + { :method => 'post', + :path => "/projects/redmine/repository/revisions/123/issues" }, + { :controller => 'repositories', :action => 'add_related_issue', + :id => 'redmine', :rev => '123' } + ) + assert_routing( + { :method => 'delete', + :path => "/projects/redmine/repository/revisions/123/issues/25" }, + { :controller => 'repositories', :action => 'remove_related_issue', + :id => 'redmine', :rev => '123', :issue_id => '25' } + ) + end + + def test_repositories_related_issues_with_repository_id + assert_routing( + { :method => 'post', + :path => "/projects/redmine/repository/foo/revisions/123/issues" }, + { :controller => 'repositories', :action => 'add_related_issue', + :id => 'redmine', :repository_id => 'foo', :rev => '123' } + ) + assert_routing( + { :method => 'delete', + :path => "/projects/redmine/repository/foo/revisions/123/issues/25" }, + { :controller => 'repositories', :action => 'remove_related_issue', + :id => 'redmine', :repository_id => 'foo', :rev => '123', :issue_id => '25' } + ) + end +end diff --git a/test/integration/routing/roles_test.rb b/test/integration/routing/roles_test.rb new file mode 100644 index 00000000..13513079 --- /dev/null +++ b/test/integration/routing/roles_test.rb @@ -0,0 +1,61 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingRolesTest < ActionController::IntegrationTest + def test_roles + assert_routing( + { :method => 'get', :path => "/roles" }, + { :controller => 'roles', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/roles.xml" }, + { :controller => 'roles', :action => 'index', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/roles/2.xml" }, + { :controller => 'roles', :action => 'show', :id => '2', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/roles/new" }, + { :controller => 'roles', :action => 'new' } + ) + assert_routing( + { :method => 'post', :path => "/roles" }, + { :controller => 'roles', :action => 'create' } + ) + assert_routing( + { :method => 'get', :path => "/roles/2/edit" }, + { :controller => 'roles', :action => 'edit', :id => '2' } + ) + assert_routing( + { :method => 'put', :path => "/roles/2" }, + { :controller => 'roles', :action => 'update', :id => '2' } + ) + assert_routing( + { :method => 'delete', :path => "/roles/2" }, + { :controller => 'roles', :action => 'destroy', :id => '2' } + ) + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/roles/permissions" }, + { :controller => 'roles', :action => 'permissions' } + ) + end + end +end diff --git a/test/integration/routing/search_test.rb b/test/integration/routing/search_test.rb new file mode 100644 index 00000000..cfda8067 --- /dev/null +++ b/test/integration/routing/search_test.rb @@ -0,0 +1,31 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingSearchTest < ActionController::IntegrationTest + def test_search + assert_routing( + { :method => 'get', :path => "/search" }, + { :controller => 'search', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/projects/foo/search" }, + { :controller => 'search', :action => 'index', :id => 'foo' } + ) + end +end diff --git a/test/integration/routing/settings_test.rb b/test/integration/routing/settings_test.rb new file mode 100644 index 00000000..53693db6 --- /dev/null +++ b/test/integration/routing/settings_test.rb @@ -0,0 +1,40 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingSettingsTest < ActionController::IntegrationTest + def test_settings + assert_routing( + { :method => 'get', :path => "/settings" }, + { :controller => 'settings', :action => 'index' } + ) + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/settings/edit" }, + { :controller => 'settings', :action => 'edit' } + ) + end + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/settings/plugin/testid" }, + { :controller => 'settings', :action => 'plugin', + :id => 'testid' } + ) + end + end +end diff --git a/test/integration/routing/sys_test.rb b/test/integration/routing/sys_test.rb new file mode 100644 index 00000000..229d64d8 --- /dev/null +++ b/test/integration/routing/sys_test.rb @@ -0,0 +1,35 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingSysTest < ActionController::IntegrationTest + def test_sys + assert_routing( + { :method => 'get', :path => "/sys/projects" }, + { :controller => 'sys', :action => 'projects' } + ) + assert_routing( + { :method => 'post', :path => "/sys/projects/testid/repository" }, + { :controller => 'sys', :action => 'create_project_repository', :id => 'testid' } + ) + assert_routing( + { :method => 'get', :path => "/sys/fetch_changesets" }, + { :controller => 'sys', :action => 'fetch_changesets' } + ) + end +end diff --git a/test/integration/routing/timelog_test.rb b/test/integration/routing/timelog_test.rb new file mode 100644 index 00000000..de8e6fb2 --- /dev/null +++ b/test/integration/routing/timelog_test.rb @@ -0,0 +1,240 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingTimelogsTest < ActionController::IntegrationTest + def test_timelogs_global + assert_routing( + { :method => 'get', :path => "/time_entries" }, + { :controller => 'timelog', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/time_entries.csv" }, + { :controller => 'timelog', :action => 'index', :format => 'csv' } + ) + assert_routing( + { :method => 'get', :path => "/time_entries.atom" }, + { :controller => 'timelog', :action => 'index', :format => 'atom' } + ) + assert_routing( + { :method => 'get', :path => "/time_entries/new" }, + { :controller => 'timelog', :action => 'new' } + ) + assert_routing( + { :method => 'get', :path => "/time_entries/22/edit" }, + { :controller => 'timelog', :action => 'edit', :id => '22' } + ) + assert_routing( + { :method => 'post', :path => "/time_entries" }, + { :controller => 'timelog', :action => 'create' } + ) + assert_routing( + { :method => 'put', :path => "/time_entries/22" }, + { :controller => 'timelog', :action => 'update', :id => '22' } + ) + assert_routing( + { :method => 'delete', :path => "/time_entries/55" }, + { :controller => 'timelog', :action => 'destroy', :id => '55' } + ) + end + + def test_timelogs_scoped_under_project + assert_routing( + { :method => 'get', :path => "/projects/567/time_entries" }, + { :controller => 'timelog', :action => 'index', :project_id => '567' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/time_entries.csv" }, + { :controller => 'timelog', :action => 'index', :project_id => '567', + :format => 'csv' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/time_entries.atom" }, + { :controller => 'timelog', :action => 'index', :project_id => '567', + :format => 'atom' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/time_entries/new" }, + { :controller => 'timelog', :action => 'new', :project_id => '567' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/time_entries/22/edit" }, + { :controller => 'timelog', :action => 'edit', + :id => '22', :project_id => '567' } + ) + assert_routing( + { :method => 'post', :path => "/projects/567/time_entries" }, + { :controller => 'timelog', :action => 'create', + :project_id => '567' } + ) + assert_routing( + { :method => 'put', :path => "/projects/567/time_entries/22" }, + { :controller => 'timelog', :action => 'update', + :id => '22', :project_id => '567' } + ) + assert_routing( + { :method => 'delete', :path => "/projects/567/time_entries/55" }, + { :controller => 'timelog', :action => 'destroy', + :id => '55', :project_id => '567' } + ) + end + + def test_timelogs_scoped_under_issues + assert_routing( + { :method => 'get', :path => "/issues/234/time_entries" }, + { :controller => 'timelog', :action => 'index', :issue_id => '234' } + ) + assert_routing( + { :method => 'get', :path => "/issues/234/time_entries.csv" }, + { :controller => 'timelog', :action => 'index', :issue_id => '234', + :format => 'csv' } + ) + assert_routing( + { :method => 'get', :path => "/issues/234/time_entries.atom" }, + { :controller => 'timelog', :action => 'index', :issue_id => '234', + :format => 'atom' } + ) + assert_routing( + { :method => 'get', :path => "/issues/234/time_entries/new" }, + { :controller => 'timelog', :action => 'new', :issue_id => '234' } + ) + assert_routing( + { :method => 'get', :path => "/issues/234/time_entries/22/edit" }, + { :controller => 'timelog', :action => 'edit', :id => '22', + :issue_id => '234' } + ) + assert_routing( + { :method => 'post', :path => "/issues/234/time_entries" }, + { :controller => 'timelog', :action => 'create', :issue_id => '234' } + ) + assert_routing( + { :method => 'put', :path => "/issues/234/time_entries/22" }, + { :controller => 'timelog', :action => 'update', :id => '22', + :issue_id => '234' } + ) + assert_routing( + { :method => 'delete', :path => "/issues/234/time_entries/55" }, + { :controller => 'timelog', :action => 'destroy', :id => '55', + :issue_id => '234' } + ) + end + + def test_timelogs_scoped_under_project_and_issues + assert_routing( + { :method => 'get', + :path => "/projects/ecookbook/issues/234/time_entries" }, + { :controller => 'timelog', :action => 'index', + :issue_id => '234', :project_id => 'ecookbook' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/ecookbook/issues/234/time_entries.csv" }, + { :controller => 'timelog', :action => 'index', + :issue_id => '234', :project_id => 'ecookbook', :format => 'csv' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/ecookbook/issues/234/time_entries.atom" }, + { :controller => 'timelog', :action => 'index', + :issue_id => '234', :project_id => 'ecookbook', :format => 'atom' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/ecookbook/issues/234/time_entries/new" }, + { :controller => 'timelog', :action => 'new', + :issue_id => '234', :project_id => 'ecookbook' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/ecookbook/issues/234/time_entries/22/edit" }, + { :controller => 'timelog', :action => 'edit', :id => '22', + :issue_id => '234', :project_id => 'ecookbook' } + ) + assert_routing( + { :method => 'post', + :path => "/projects/ecookbook/issues/234/time_entries" }, + { :controller => 'timelog', :action => 'create', + :issue_id => '234', :project_id => 'ecookbook' } + ) + assert_routing( + { :method => 'put', + :path => "/projects/ecookbook/issues/234/time_entries/22" }, + { :controller => 'timelog', :action => 'update', :id => '22', + :issue_id => '234', :project_id => 'ecookbook' } + ) + assert_routing( + { :method => 'delete', + :path => "/projects/ecookbook/issues/234/time_entries/55" }, + { :controller => 'timelog', :action => 'destroy', :id => '55', + :issue_id => '234', :project_id => 'ecookbook' } + ) + end + + def test_timelogs_report + assert_routing( + { :method => 'get', + :path => "/time_entries/report" }, + { :controller => 'timelog', :action => 'report' } + ) + assert_routing( + { :method => 'get', + :path => "/time_entries/report.csv" }, + { :controller => 'timelog', :action => 'report', :format => 'csv' } + ) + assert_routing( + { :method => 'get', + :path => "/issues/234/time_entries/report" }, + { :controller => 'timelog', :action => 'report', :issue_id => '234' } + ) + assert_routing( + { :method => 'get', + :path => "/issues/234/time_entries/report.csv" }, + { :controller => 'timelog', :action => 'report', :issue_id => '234', + :format => 'csv' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/567/time_entries/report" }, + { :controller => 'timelog', :action => 'report', :project_id => '567' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/567/time_entries/report.csv" }, + { :controller => 'timelog', :action => 'report', :project_id => '567', + :format => 'csv' } + ) + end + + def test_timelogs_bulk_edit + assert_routing( + { :method => 'delete', + :path => "/time_entries/destroy" }, + { :controller => 'timelog', :action => 'destroy' } + ) + assert_routing( + { :method => 'post', + :path => "/time_entries/bulk_update" }, + { :controller => 'timelog', :action => 'bulk_update' } + ) + assert_routing( + { :method => 'get', + :path => "/time_entries/bulk_edit" }, + { :controller => 'timelog', :action => 'bulk_edit' } + ) + end +end diff --git a/test/integration/routing/trackers_test.rb b/test/integration/routing/trackers_test.rb new file mode 100644 index 00000000..a211991c --- /dev/null +++ b/test/integration/routing/trackers_test.rb @@ -0,0 +1,79 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingTrackersTest < ActionController::IntegrationTest + def test_trackers + assert_routing( + { :method => 'get', :path => "/trackers" }, + { :controller => 'trackers', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/trackers.xml" }, + { :controller => 'trackers', :action => 'index', :format => 'xml' } + ) + assert_routing( + { :method => 'post', :path => "/trackers" }, + { :controller => 'trackers', :action => 'create' } + ) + assert_routing( + { :method => 'post', :path => "/trackers.xml" }, + { :controller => 'trackers', :action => 'create', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/trackers/new" }, + { :controller => 'trackers', :action => 'new' } + ) + assert_routing( + { :method => 'get', :path => "/trackers/new.xml" }, + { :controller => 'trackers', :action => 'new', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/trackers/1/edit" }, + { :controller => 'trackers', :action => 'edit', :id => '1' } + ) + assert_routing( + { :method => 'put', :path => "/trackers/1" }, + { :controller => 'trackers', :action => 'update', + :id => '1' } + ) + assert_routing( + { :method => 'put', :path => "/trackers/1.xml" }, + { :controller => 'trackers', :action => 'update', + :format => 'xml', :id => '1' } + ) + assert_routing( + { :method => 'delete', :path => "/trackers/1" }, + { :controller => 'trackers', :action => 'destroy', + :id => '1' } + ) + assert_routing( + { :method => 'delete', :path => "/trackers/1.xml" }, + { :controller => 'trackers', :action => 'destroy', + :format => 'xml', :id => '1' } + ) + assert_routing( + { :method => 'get', :path => "/trackers/fields" }, + { :controller => 'trackers', :action => 'fields' } + ) + assert_routing( + { :method => 'post', :path => "/trackers/fields" }, + { :controller => 'trackers', :action => 'fields' } + ) + end +end diff --git a/test/integration/routing/users_test.rb b/test/integration/routing/users_test.rb new file mode 100644 index 00000000..c1195f56 --- /dev/null +++ b/test/integration/routing/users_test.rb @@ -0,0 +1,98 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingUsersTest < ActionController::IntegrationTest + def test_users + assert_routing( + { :method => 'get', :path => "/users" }, + { :controller => 'users', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/users.xml" }, + { :controller => 'users', :action => 'index', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/users/44" }, + { :controller => 'users', :action => 'show', :id => '44' } + ) + assert_routing( + { :method => 'get', :path => "/users/44.xml" }, + { :controller => 'users', :action => 'show', :id => '44', + :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/users/current" }, + { :controller => 'users', :action => 'show', :id => 'current' } + ) + assert_routing( + { :method => 'get', :path => "/users/current.xml" }, + { :controller => 'users', :action => 'show', :id => 'current', + :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/users/new" }, + { :controller => 'users', :action => 'new' } + ) + assert_routing( + { :method => 'get', :path => "/users/444/edit" }, + { :controller => 'users', :action => 'edit', :id => '444' } + ) + assert_routing( + { :method => 'post', :path => "/users" }, + { :controller => 'users', :action => 'create' } + ) + assert_routing( + { :method => 'post', :path => "/users.xml" }, + { :controller => 'users', :action => 'create', :format => 'xml' } + ) + assert_routing( + { :method => 'put', :path => "/users/444" }, + { :controller => 'users', :action => 'update', :id => '444' } + ) + assert_routing( + { :method => 'put', :path => "/users/444.xml" }, + { :controller => 'users', :action => 'update', :id => '444', + :format => 'xml' } + ) + assert_routing( + { :method => 'delete', :path => "/users/44" }, + { :controller => 'users', :action => 'destroy', :id => '44' } + ) + assert_routing( + { :method => 'delete', :path => "/users/44.xml" }, + { :controller => 'users', :action => 'destroy', :id => '44', + :format => 'xml' } + ) + assert_routing( + { :method => 'post', :path => "/users/123/memberships" }, + { :controller => 'users', :action => 'edit_membership', + :id => '123' } + ) + assert_routing( + { :method => 'put', :path => "/users/123/memberships/55" }, + { :controller => 'users', :action => 'edit_membership', + :id => '123', :membership_id => '55' } + ) + assert_routing( + { :method => 'delete', :path => "/users/123/memberships/55" }, + { :controller => 'users', :action => 'destroy_membership', + :id => '123', :membership_id => '55' } + ) + end +end diff --git a/test/integration/routing/versions_test.rb b/test/integration/routing/versions_test.rb new file mode 100644 index 00000000..2ad38dbb --- /dev/null +++ b/test/integration/routing/versions_test.rb @@ -0,0 +1,119 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingVersionsTest < ActionController::IntegrationTest + def test_roadmap + # /projects/foo/versions is /projects/foo/roadmap + assert_routing( + { :method => 'get', :path => "/projects/33/roadmap" }, + { :controller => 'versions', :action => 'index', :project_id => '33' } + ) + end + + def test_versions_scoped_under_project + assert_routing( + { :method => 'put', :path => "/projects/foo/versions/close_completed" }, + { :controller => 'versions', :action => 'close_completed', + :project_id => 'foo' } + ) + assert_routing( + { :method => 'get', :path => "/projects/foo/versions.xml" }, + { :controller => 'versions', :action => 'index', + :project_id => 'foo', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/projects/foo/versions.json" }, + { :controller => 'versions', :action => 'index', + :project_id => 'foo', :format => 'json' } + ) + assert_routing( + { :method => 'get', :path => "/projects/foo/versions/new" }, + { :controller => 'versions', :action => 'new', + :project_id => 'foo' } + ) + assert_routing( + { :method => 'post', :path => "/projects/foo/versions" }, + { :controller => 'versions', :action => 'create', + :project_id => 'foo' } + ) + assert_routing( + { :method => 'post', :path => "/projects/foo/versions.xml" }, + { :controller => 'versions', :action => 'create', + :project_id => 'foo', :format => 'xml' } + ) + assert_routing( + { :method => 'post', :path => "/projects/foo/versions.json" }, + { :controller => 'versions', :action => 'create', + :project_id => 'foo', :format => 'json' } + ) + end + + def test_versions + assert_routing( + { :method => 'get', :path => "/versions/1" }, + { :controller => 'versions', :action => 'show', :id => '1' } + ) + assert_routing( + { :method => 'get', :path => "/versions/1.xml" }, + { :controller => 'versions', :action => 'show', :id => '1', + :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/versions/1.json" }, + { :controller => 'versions', :action => 'show', :id => '1', + :format => 'json' } + ) + assert_routing( + { :method => 'get', :path => "/versions/1/edit" }, + { :controller => 'versions', :action => 'edit', :id => '1' } + ) + assert_routing( + { :method => 'put', :path => "/versions/1" }, + { :controller => 'versions', :action => 'update', :id => '1' } + ) + assert_routing( + { :method => 'put', :path => "/versions/1.xml" }, + { :controller => 'versions', :action => 'update', :id => '1', + :format => 'xml' } + ) + assert_routing( + { :method => 'put', :path => "/versions/1.json" }, + { :controller => 'versions', :action => 'update', :id => '1', + :format => 'json' } + ) + assert_routing( + { :method => 'delete', :path => "/versions/1" }, + { :controller => 'versions', :action => 'destroy', :id => '1' } + ) + assert_routing( + { :method => 'delete', :path => "/versions/1.xml" }, + { :controller => 'versions', :action => 'destroy', :id => '1', + :format => 'xml' } + ) + assert_routing( + { :method => 'delete', :path => "/versions/1.json" }, + { :controller => 'versions', :action => 'destroy', :id => '1', + :format => 'json' } + ) + assert_routing( + { :method => 'post', :path => "/versions/1/status_by" }, + { :controller => 'versions', :action => 'status_by', :id => '1' } + ) + end +end diff --git a/test/integration/routing/watchers_test.rb b/test/integration/routing/watchers_test.rb new file mode 100644 index 00000000..c46edda3 --- /dev/null +++ b/test/integration/routing/watchers_test.rb @@ -0,0 +1,61 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingWatchersTest < ActionController::IntegrationTest + def test_watchers + assert_routing( + { :method => 'get', :path => "/watchers/new" }, + { :controller => 'watchers', :action => 'new' } + ) + assert_routing( + { :method => 'post', :path => "/watchers/append" }, + { :controller => 'watchers', :action => 'append' } + ) + assert_routing( + { :method => 'post', :path => "/watchers" }, + { :controller => 'watchers', :action => 'create' } + ) + assert_routing( + { :method => 'delete', :path => "/watchers" }, + { :controller => 'watchers', :action => 'destroy' } + ) + assert_routing( + { :method => 'get', :path => "/watchers/autocomplete_for_user" }, + { :controller => 'watchers', :action => 'autocomplete_for_user' } + ) + assert_routing( + { :method => 'post', :path => "/watchers/watch" }, + { :controller => 'watchers', :action => 'watch' } + ) + assert_routing( + { :method => 'delete', :path => "/watchers/watch" }, + { :controller => 'watchers', :action => 'unwatch' } + ) + assert_routing( + { :method => 'post', :path => "/issues/12/watchers.xml" }, + { :controller => 'watchers', :action => 'create', + :object_type => 'issue', :object_id => '12', :format => 'xml' } + ) + assert_routing( + { :method => 'delete', :path => "/issues/12/watchers/3.xml" }, + { :controller => 'watchers', :action => 'destroy', + :object_type => 'issue', :object_id => '12', :user_id => '3', :format => 'xml'} + ) + end +end diff --git a/test/integration/routing/welcome_test.rb b/test/integration/routing/welcome_test.rb new file mode 100644 index 00000000..73eaa975 --- /dev/null +++ b/test/integration/routing/welcome_test.rb @@ -0,0 +1,31 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingWelcomeTest < ActionController::IntegrationTest + def test_welcome + assert_routing( + { :method => 'get', :path => "/" }, + { :controller => 'welcome', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/robots.txt" }, + { :controller => 'welcome', :action => 'robots' } + ) + end +end diff --git a/test/integration/routing/wiki_test.rb b/test/integration/routing/wiki_test.rb new file mode 100644 index 00000000..051f8bc6 --- /dev/null +++ b/test/integration/routing/wiki_test.rb @@ -0,0 +1,186 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingWikiTest < ActionController::IntegrationTest + def test_wiki_matching + assert_routing( + { :method => 'get', :path => "/projects/567/wiki" }, + { :controller => 'wiki', :action => 'show', :project_id => '567' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/wiki/lalala" }, + { :controller => 'wiki', :action => 'show', :project_id => '567', + :id => 'lalala' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/wiki/lalala.pdf" }, + { :controller => 'wiki', :action => 'show', :project_id => '567', + :id => 'lalala', :format => 'pdf' } + ) + assert_routing( + { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/diff" }, + { :controller => 'wiki', :action => 'diff', :project_id => '1', + :id => 'CookBook_documentation' } + ) + assert_routing( + { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2" }, + { :controller => 'wiki', :action => 'show', :project_id => '1', + :id => 'CookBook_documentation', :version => '2' } + ) + assert_routing( + { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2/diff" }, + { :controller => 'wiki', :action => 'diff', :project_id => '1', + :id => 'CookBook_documentation', :version => '2' } + ) + assert_routing( + { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2/annotate" }, + { :controller => 'wiki', :action => 'annotate', :project_id => '1', + :id => 'CookBook_documentation', :version => '2' } + ) + # Make sure we don't route wiki page sub-uris to let plugins handle them + assert_raise(ActionController::RoutingError) do + assert_recognizes({}, {:method => 'get', :path => "/projects/1/wiki/CookBook_documentation/whatever"}) + end + end + + def test_wiki_misc + assert_routing( + { :method => 'get', :path => "/projects/567/wiki/date_index" }, + { :controller => 'wiki', :action => 'date_index', :project_id => '567' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/wiki/export" }, + { :controller => 'wiki', :action => 'export', :project_id => '567' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/wiki/export.pdf" }, + { :controller => 'wiki', :action => 'export', :project_id => '567', :format => 'pdf' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/wiki/index" }, + { :controller => 'wiki', :action => 'index', :project_id => '567' } + ) + end + + def test_wiki_resources + assert_routing( + { :method => 'get', :path => "/projects/567/wiki/my_page/edit" }, + { :controller => 'wiki', :action => 'edit', :project_id => '567', + :id => 'my_page' } + ) + assert_routing( + { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/history" }, + { :controller => 'wiki', :action => 'history', :project_id => '1', + :id => 'CookBook_documentation' } + ) + assert_routing( + { :method => 'get', :path => "/projects/22/wiki/ladida/rename" }, + { :controller => 'wiki', :action => 'rename', :project_id => '22', + :id => 'ladida' } + ) + ["post", "put"].each do |method| + assert_routing( + { :method => method, :path => "/projects/567/wiki/CookBook_documentation/preview" }, + { :controller => 'wiki', :action => 'preview', :project_id => '567', + :id => 'CookBook_documentation' } + ) + end + assert_routing( + { :method => 'post', :path => "/projects/22/wiki/ladida/rename" }, + { :controller => 'wiki', :action => 'rename', :project_id => '22', + :id => 'ladida' } + ) + assert_routing( + { :method => 'post', :path => "/projects/22/wiki/ladida/protect" }, + { :controller => 'wiki', :action => 'protect', :project_id => '22', + :id => 'ladida' } + ) + assert_routing( + { :method => 'post', :path => "/projects/22/wiki/ladida/add_attachment" }, + { :controller => 'wiki', :action => 'add_attachment', :project_id => '22', + :id => 'ladida' } + ) + assert_routing( + { :method => 'put', :path => "/projects/567/wiki/my_page" }, + { :controller => 'wiki', :action => 'update', :project_id => '567', + :id => 'my_page' } + ) + assert_routing( + { :method => 'delete', :path => "/projects/22/wiki/ladida" }, + { :controller => 'wiki', :action => 'destroy', :project_id => '22', + :id => 'ladida' } + ) + assert_routing( + { :method => 'delete', :path => "/projects/22/wiki/ladida/3" }, + { :controller => 'wiki', :action => 'destroy_version', :project_id => '22', + :id => 'ladida', :version => '3' } + ) + end + + def test_api + assert_routing( + { :method => 'get', :path => "/projects/567/wiki/my_page.xml" }, + { :controller => 'wiki', :action => 'show', :project_id => '567', + :id => 'my_page', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/wiki/my_page.json" }, + { :controller => 'wiki', :action => 'show', :project_id => '567', + :id => 'my_page', :format => 'json' } + ) + assert_routing( + { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2.xml" }, + { :controller => 'wiki', :action => 'show', :project_id => '1', + :id => 'CookBook_documentation', :version => '2', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2.json" }, + { :controller => 'wiki', :action => 'show', :project_id => '1', + :id => 'CookBook_documentation', :version => '2', :format => 'json' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/wiki/index.xml" }, + { :controller => 'wiki', :action => 'index', :project_id => '567', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/wiki/index.json" }, + { :controller => 'wiki', :action => 'index', :project_id => '567', :format => 'json' } + ) + assert_routing( + { :method => 'put', :path => "/projects/567/wiki/my_page.xml" }, + { :controller => 'wiki', :action => 'update', :project_id => '567', + :id => 'my_page', :format => 'xml' } + ) + assert_routing( + { :method => 'put', :path => "/projects/567/wiki/my_page.json" }, + { :controller => 'wiki', :action => 'update', :project_id => '567', + :id => 'my_page', :format => 'json' } + ) + assert_routing( + { :method => 'delete', :path => "/projects/567/wiki/my_page.xml" }, + { :controller => 'wiki', :action => 'destroy', :project_id => '567', + :id => 'my_page', :format => 'xml' } + ) + assert_routing( + { :method => 'delete', :path => "/projects/567/wiki/my_page.json" }, + { :controller => 'wiki', :action => 'destroy', :project_id => '567', + :id => 'my_page', :format => 'json' } + ) + end +end diff --git a/test/integration/routing/wikis_test.rb b/test/integration/routing/wikis_test.rb new file mode 100644 index 00000000..611fccbd --- /dev/null +++ b/test/integration/routing/wikis_test.rb @@ -0,0 +1,33 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingWikisTest < ActionController::IntegrationTest + def test_wikis_plural_admin_setup + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/projects/ladida/wiki/destroy" }, + { :controller => 'wikis', :action => 'destroy', :id => 'ladida' } + ) + end + assert_routing( + { :method => 'post', :path => "/projects/ladida/wiki" }, + { :controller => 'wikis', :action => 'edit', :id => 'ladida' } + ) + end +end diff --git a/test/integration/routing/workflows_test.rb b/test/integration/routing/workflows_test.rb new file mode 100644 index 00000000..b2b20000 --- /dev/null +++ b/test/integration/routing/workflows_test.rb @@ -0,0 +1,45 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingWorkflowsTest < ActionController::IntegrationTest + def test_workflows + assert_routing( + { :method => 'get', :path => "/workflows" }, + { :controller => 'workflows', :action => 'index' } + ) + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/workflows/edit" }, + { :controller => 'workflows', :action => 'edit' } + ) + end + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/workflows/permissions" }, + { :controller => 'workflows', :action => 'permissions' } + ) + end + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/workflows/copy" }, + { :controller => 'workflows', :action => 'copy' } + ) + end + end +end diff --git a/test/integration/users_test.rb b/test/integration/users_test.rb new file mode 100644 index 00000000..dea40a1f --- /dev/null +++ b/test/integration/users_test.rb @@ -0,0 +1,29 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class UsersTest < ActionController::IntegrationTest + fixtures :users + + def test_destroy_should_not_accept_get_requests + assert_no_difference 'User.count' do + get '/users/destroy/2', {}, credentials('admin') + assert_response 404 + end + end +end diff --git a/test/mocks/open_id_authentication_mock.rb b/test/mocks/open_id_authentication_mock.rb new file mode 100644 index 00000000..d45272da --- /dev/null +++ b/test/mocks/open_id_authentication_mock.rb @@ -0,0 +1,46 @@ +# Mocks out OpenID +# +# http://www.northpub.com/articles/2007/04/02/testing-openid-support +module OpenIdAuthentication + + EXTENSION_FIELDS = {'email' => 'user@somedomain.com', + 'nickname' => 'cool_user', + 'country' => 'US', + 'postcode' => '12345', + 'fullname' => 'Cool User', + 'dob' => '1970-04-01', + 'language' => 'en', + 'timezone' => 'America/New_York'} + + protected + + def authenticate_with_open_id(identity_url = params[:openid_url], options = {}) #:doc: + if User.find_by_identity_url(identity_url) || identity_url.include?('good') + extension_response_fields = {} + + # Don't process registration fields unless it is requested. + unless identity_url.include?('blank') || (options[:required].nil? && options[:optional].nil?) + + options[:required].each do |field| + extension_response_fields[field.to_s] = EXTENSION_FIELDS[field.to_s] + end unless options[:required].nil? + + options[:optional].each do |field| + extension_response_fields[field.to_s] = EXTENSION_FIELDS[field.to_s] + end unless options[:optional].nil? + end + + yield Result[:successful], identity_url , extension_response_fields + else + logger.info "OpenID authentication failed: #{identity_url}" + yield Result[:failed], identity_url, nil + end + end + + private + + def add_simple_registration_fields(open_id_response, fields) + open_id_response.add_extension_arg('sreg', 'required', [ fields[:required] ].flatten * ',') if fields[:required] + open_id_response.add_extension_arg('sreg', 'optional', [ fields[:optional] ].flatten * ',') if fields[:optional] + end +end diff --git a/test/object_helpers.rb b/test/object_helpers.rb new file mode 100644 index 00000000..f60ccbad --- /dev/null +++ b/test/object_helpers.rb @@ -0,0 +1,162 @@ +module ObjectHelpers + def User.generate!(attributes={}) + @generated_user_login ||= 'user0' + @generated_user_login.succ! + user = User.new(attributes) + user.login = @generated_user_login.dup if user.login.blank? + user.mail = "#{@generated_user_login}@example.com" if user.mail.blank? + user.firstname = "Bob" if user.firstname.blank? + user.lastname = "Doe" if user.lastname.blank? + yield user if block_given? + user.save! + user + end + + def User.add_to_project(user, project, roles=nil) + roles = Role.find(1) if roles.nil? + roles = [roles] unless roles.is_a?(Array) + Member.create!(:principal => user, :project => project, :roles => roles) + end + + def Group.generate!(attributes={}) + @generated_group_name ||= 'Group 0' + @generated_group_name.succ! + group = Group.new(attributes) + group.name = @generated_group_name.dup if group.name.blank? + yield group if block_given? + group.save! + group + end + + def Project.generate!(attributes={}) + @generated_project_identifier ||= 'project-0000' + @generated_project_identifier.succ! + project = Project.new(attributes) + project.name = @generated_project_identifier.dup if project.name.blank? + project.identifier = @generated_project_identifier.dup if project.identifier.blank? + yield project if block_given? + project.save! + project + end + + def Project.generate_with_parent!(parent, attributes={}) + project = Project.generate!(attributes) + project.set_parent!(parent) + project + end + + def Tracker.generate!(attributes={}) + @generated_tracker_name ||= 'Tracker 0' + @generated_tracker_name.succ! + tracker = Tracker.new(attributes) + tracker.name = @generated_tracker_name.dup if tracker.name.blank? + yield tracker if block_given? + tracker.save! + tracker + end + + def Role.generate!(attributes={}) + @generated_role_name ||= 'Role 0' + @generated_role_name.succ! + role = Role.new(attributes) + role.name = @generated_role_name.dup if role.name.blank? + yield role if block_given? + role.save! + role + end + + def Issue.generate!(attributes={}) + issue = Issue.new(attributes) + issue.project ||= Project.find(1) + issue.tracker ||= issue.project.trackers.first + issue.subject = 'Generated' if issue.subject.blank? + issue.author ||= User.find(2) + yield issue if block_given? + issue.save! + issue + end + + # Generates an issue with 2 children and a grandchild + def Issue.generate_with_descendants!(attributes={}) + issue = Issue.generate!(attributes) + child = Issue.generate!(:project => issue.project, :subject => 'Child1', :parent_issue_id => issue.id) + Issue.generate!(:project => issue.project, :subject => 'Child2', :parent_issue_id => issue.id) + Issue.generate!(:project => issue.project, :subject => 'Child11', :parent_issue_id => child.id) + issue.reload + end + + def Journal.generate!(attributes={}) + journal = Journal.new(attributes) + journal.user ||= User.first + journal.journalized ||= Issue.first + yield journal if block_given? + journal.save! + journal + end + + def Version.generate!(attributes={}) + @generated_version_name ||= 'Version 0' + @generated_version_name.succ! + version = Version.new(attributes) + version.name = @generated_version_name.dup if version.name.blank? + yield version if block_given? + version.save! + version + end + + def TimeEntry.generate!(attributes={}) + entry = TimeEntry.new(attributes) + entry.user ||= User.find(2) + entry.issue ||= Issue.find(1) unless entry.project + entry.project ||= entry.issue.project + entry.activity ||= TimeEntryActivity.first + entry.spent_on ||= Date.today + entry.hours ||= 1.0 + entry.save! + entry + end + + def AuthSource.generate!(attributes={}) + @generated_auth_source_name ||= 'Auth 0' + @generated_auth_source_name.succ! + source = AuthSource.new(attributes) + source.name = @generated_auth_source_name.dup if source.name.blank? + yield source if block_given? + source.save! + source + end + + def Board.generate!(attributes={}) + @generated_board_name ||= 'Forum 0' + @generated_board_name.succ! + board = Board.new(attributes) + board.name = @generated_board_name.dup if board.name.blank? + board.description = @generated_board_name.dup if board.description.blank? + yield board if block_given? + board.save! + board + end + + def Attachment.generate!(attributes={}) + @generated_filename ||= 'testfile0' + @generated_filename.succ! + attributes = attributes.dup + attachment = Attachment.new(attributes) + attachment.container ||= Issue.find(1) + attachment.author ||= User.find(2) + attachment.filename = @generated_filename.dup if attachment.filename.blank? + attachment.save! + attachment + end + + def CustomField.generate!(attributes={}) + @generated_custom_field_name ||= 'Custom field 0' + @generated_custom_field_name.succ! + field = new(attributes) + field.name = @generated_custom_field_name.dup if field.name.blank? + field.field_format = 'string' if field.field_format.blank? + yield field if block_given? + field.save! + field + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 8bf1192f..8d066685 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,13 +1,468 @@ -ENV["RAILS_ENV"] = "test" -require File.expand_path('../../config/environment', __FILE__) -require 'rails/test_help' - -class ActiveSupport::TestCase - # Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order. - # - # Note: You'll currently still have to declare fixtures explicitly in integration tests - # -- they do not yet inherit this setting - fixtures :all - - # Add more helper methods to be used by all tests here... -end +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +#require 'shoulda' +ENV["RAILS_ENV"] = "test" +require File.expand_path(File.dirname(__FILE__) + "/../config/environment") +require 'rails/test_help' +require Rails.root.join('test', 'mocks', 'open_id_authentication_mock.rb').to_s + +require File.expand_path(File.dirname(__FILE__) + '/object_helpers') +include ObjectHelpers + +class ActiveSupport::TestCase + include ActionDispatch::TestProcess + + self.use_transactional_fixtures = true + self.use_instantiated_fixtures = false + + def log_user(login, password) + User.anonymous + get "/login" + assert_equal nil, session[:user_id] + assert_response :success + assert_template "account/login" + post "/login", :username => login, :password => password + assert_equal login, User.find(session[:user_id]).login + end + + def uploaded_test_file(name, mime) + fixture_file_upload("files/#{name}", mime, true) + end + + def credentials(user, password=nil) + {'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)} + end + + # Mock out a file + def self.mock_file + file = 'a_file.png' + file.stubs(:size).returns(32) + file.stubs(:original_filename).returns('a_file.png') + file.stubs(:content_type).returns('image/png') + file.stubs(:read).returns(false) + file + end + + def mock_file + self.class.mock_file + end + + def mock_file_with_options(options={}) + file = '' + file.stubs(:size).returns(32) + original_filename = options[:original_filename] || nil + file.stubs(:original_filename).returns(original_filename) + content_type = options[:content_type] || nil + file.stubs(:content_type).returns(content_type) + file.stubs(:read).returns(false) + file + end + + # Use a temporary directory for attachment related tests + def set_tmp_attachments_directory + Dir.mkdir "#{Rails.root}/tmp/test" unless File.directory?("#{Rails.root}/tmp/test") + unless File.directory?("#{Rails.root}/tmp/test/attachments") + Dir.mkdir "#{Rails.root}/tmp/test/attachments" + end + Attachment.storage_path = "#{Rails.root}/tmp/test/attachments" + end + + def set_fixtures_attachments_directory + Attachment.storage_path = "#{Rails.root}/test/fixtures/files" + end + + def with_settings(options, &block) + saved_settings = options.keys.inject({}) do |h, k| + h[k] = case Setting[k] + when Symbol, false, true, nil + Setting[k] + else + Setting[k].dup + end + h + end + options.each {|k, v| Setting[k] = v} + yield + ensure + saved_settings.each {|k, v| Setting[k] = v} if saved_settings + end + + # Yields the block with user as the current user + def with_current_user(user, &block) + saved_user = User.current + User.current = user + yield + ensure + User.current = saved_user + end + + def change_user_password(login, new_password) + user = User.first(:conditions => {:login => login}) + user.password, user.password_confirmation = new_password, new_password + user.save! + end + + def self.ldap_configured? + @test_ldap = Net::LDAP.new(:host => '127.0.0.1', :port => 389) + return @test_ldap.bind + rescue Exception => e + # LDAP is not listening + return nil + end + + def self.convert_installed? + Redmine::Thumbnail.convert_available? + end + + # Returns the path to the test +vendor+ repository + def self.repository_path(vendor) + Rails.root.join("tmp/test/#{vendor.downcase}_repository").to_s + end + + # Returns the url of the subversion test repository + def self.subversion_repository_url + path = repository_path('subversion') + path = '/' + path unless path.starts_with?('/') + "file://#{path}" + end + + # Returns true if the +vendor+ test repository is configured + def self.repository_configured?(vendor) + File.directory?(repository_path(vendor)) + end + + def repository_path_hash(arr) + hs = {} + hs[:path] = arr.join("/") + hs[:param] = arr.join("/") + hs + end + + def assert_save(object) + saved = object.save + message = "#{object.class} could not be saved" + errors = object.errors.full_messages.map {|m| "- #{m}"} + message << ":\n#{errors.join("\n")}" if errors.any? + assert_equal true, saved, message + end + + def assert_error_tag(options={}) + assert_tag({:attributes => { :id => 'errorExplanation' }}.merge(options)) + end + + def assert_include(expected, s, message=nil) + assert s.include?(expected), (message || "\"#{expected}\" not found in \"#{s}\"") + end + + def assert_not_include(expected, s) + assert !s.include?(expected), "\"#{expected}\" found in \"#{s}\"" + end + + def assert_select_in(text, *args, &block) + d = HTML::Document.new(CGI::unescapeHTML(String.new(text))).root + assert_select(d, *args, &block) + end + + def assert_mail_body_match(expected, mail) + if expected.is_a?(String) + assert_include expected, mail_body(mail) + else + assert_match expected, mail_body(mail) + end + end + + def assert_mail_body_no_match(expected, mail) + if expected.is_a?(String) + assert_not_include expected, mail_body(mail) + else + assert_no_match expected, mail_body(mail) + end + end + + def mail_body(mail) + mail.parts.first.body.encoded + end +end + +module Redmine + module ApiTest + # Base class for API tests + class Base < ActionDispatch::IntegrationTest + # Test that a request allows the three types of API authentication + # + # * HTTP Basic with username and password + # * HTTP Basic with an api key for the username + # * Key based with the key=X parameter + # + # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete) + # @param [String] url the request url + # @param [optional, Hash] parameters additional request parameters + # @param [optional, Hash] options additional options + # @option options [Symbol] :success_code Successful response code (:success) + # @option options [Symbol] :failure_code Failure response code (:unauthorized) + def self.should_allow_api_authentication(http_method, url, parameters={}, options={}) + should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters, options) + should_allow_http_basic_auth_with_key(http_method, url, parameters, options) + should_allow_key_based_auth(http_method, url, parameters, options) + end + + # Test that a request allows the username and password for HTTP BASIC + # + # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete) + # @param [String] url the request url + # @param [optional, Hash] parameters additional request parameters + # @param [optional, Hash] options additional options + # @option options [Symbol] :success_code Successful response code (:success) + # @option options [Symbol] :failure_code Failure response code (:unauthorized) + def self.should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters={}, options={}) + success_code = options[:success_code] || :success + failure_code = options[:failure_code] || :unauthorized + + context "should allow http basic auth using a username and password for #{http_method} #{url}" do + context "with a valid HTTP authentication" do + setup do + @user = User.generate! do |user| + user.admin = true + user.password = 'my_password' + end + send(http_method, url, parameters, credentials(@user.login, 'my_password')) + end + + should_respond_with success_code + should_respond_with_content_type_based_on_url(url) + should "login as the user" do + assert_equal @user, User.current + end + end + + context "with an invalid HTTP authentication" do + setup do + @user = User.generate! + send(http_method, url, parameters, credentials(@user.login, 'wrong_password')) + end + + should_respond_with failure_code + should_respond_with_content_type_based_on_url(url) + should "not login as the user" do + assert_equal User.anonymous, User.current + end + end + + context "without credentials" do + setup do + send(http_method, url, parameters) + end + + should_respond_with failure_code + should_respond_with_content_type_based_on_url(url) + should "include_www_authenticate_header" do + assert @controller.response.headers.has_key?('WWW-Authenticate') + end + end + end + end + + # Test that a request allows the API key with HTTP BASIC + # + # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete) + # @param [String] url the request url + # @param [optional, Hash] parameters additional request parameters + # @param [optional, Hash] options additional options + # @option options [Symbol] :success_code Successful response code (:success) + # @option options [Symbol] :failure_code Failure response code (:unauthorized) + def self.should_allow_http_basic_auth_with_key(http_method, url, parameters={}, options={}) + success_code = options[:success_code] || :success + failure_code = options[:failure_code] || :unauthorized + + context "should allow http basic auth with a key for #{http_method} #{url}" do + context "with a valid HTTP authentication using the API token" do + setup do + @user = User.generate! do |user| + user.admin = true + end + @token = Token.create!(:user => @user, :action => 'api') + send(http_method, url, parameters, credentials(@token.value, 'X')) + end + should_respond_with success_code + should_respond_with_content_type_based_on_url(url) + should_be_a_valid_response_string_based_on_url(url) + should "login as the user" do + assert_equal @user, User.current + end + end + + context "with an invalid HTTP authentication" do + setup do + @user = User.generate! + @token = Token.create!(:user => @user, :action => 'feeds') + send(http_method, url, parameters, credentials(@token.value, 'X')) + end + should_respond_with failure_code + should_respond_with_content_type_based_on_url(url) + should "not login as the user" do + assert_equal User.anonymous, User.current + end + end + end + end + + # Test that a request allows full key authentication + # + # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete) + # @param [String] url the request url, without the key=ZXY parameter + # @param [optional, Hash] parameters additional request parameters + # @param [optional, Hash] options additional options + # @option options [Symbol] :success_code Successful response code (:success) + # @option options [Symbol] :failure_code Failure response code (:unauthorized) + def self.should_allow_key_based_auth(http_method, url, parameters={}, options={}) + success_code = options[:success_code] || :success + failure_code = options[:failure_code] || :unauthorized + + context "should allow key based auth using key=X for #{http_method} #{url}" do + context "with a valid api token" do + setup do + @user = User.generate! do |user| + user.admin = true + end + @token = Token.create!(:user => @user, :action => 'api') + # Simple url parse to add on ?key= or &key= + request_url = if url.match(/\?/) + url + "&key=#{@token.value}" + else + url + "?key=#{@token.value}" + end + send(http_method, request_url, parameters) + end + should_respond_with success_code + should_respond_with_content_type_based_on_url(url) + should_be_a_valid_response_string_based_on_url(url) + should "login as the user" do + assert_equal @user, User.current + end + end + + context "with an invalid api token" do + setup do + @user = User.generate! do |user| + user.admin = true + end + @token = Token.create!(:user => @user, :action => 'feeds') + # Simple url parse to add on ?key= or &key= + request_url = if url.match(/\?/) + url + "&key=#{@token.value}" + else + url + "?key=#{@token.value}" + end + send(http_method, request_url, parameters) + end + should_respond_with failure_code + should_respond_with_content_type_based_on_url(url) + should "not login as the user" do + assert_equal User.anonymous, User.current + end + end + end + + context "should allow key based auth using X-Redmine-API-Key header for #{http_method} #{url}" do + setup do + @user = User.generate! do |user| + user.admin = true + end + @token = Token.create!(:user => @user, :action => 'api') + send(http_method, url, parameters, {'X-Redmine-API-Key' => @token.value.to_s}) + end + should_respond_with success_code + should_respond_with_content_type_based_on_url(url) + should_be_a_valid_response_string_based_on_url(url) + should "login as the user" do + assert_equal @user, User.current + end + end + end + + # Uses should_respond_with_content_type based on what's in the url: + # + # '/project/issues.xml' => should_respond_with_content_type :xml + # '/project/issues.json' => should_respond_with_content_type :json + # + # @param [String] url Request + def self.should_respond_with_content_type_based_on_url(url) + case + when url.match(/xml/i) + should "respond with XML" do + assert_equal 'application/xml', @response.content_type + end + when url.match(/json/i) + should "respond with JSON" do + assert_equal 'application/json', @response.content_type + end + else + raise "Unknown content type for should_respond_with_content_type_based_on_url: #{url}" + end + end + + # Uses the url to assert which format the response should be in + # + # '/project/issues.xml' => should_be_a_valid_xml_string + # '/project/issues.json' => should_be_a_valid_json_string + # + # @param [String] url Request + def self.should_be_a_valid_response_string_based_on_url(url) + case + when url.match(/xml/i) + should_be_a_valid_xml_string + when url.match(/json/i) + should_be_a_valid_json_string + else + raise "Unknown content type for should_be_a_valid_response_based_on_url: #{url}" + end + end + + # Checks that the response is a valid JSON string + def self.should_be_a_valid_json_string + should "be a valid JSON string (or empty)" do + assert(response.body.blank? || ActiveSupport::JSON.decode(response.body)) + end + end + + # Checks that the response is a valid XML string + def self.should_be_a_valid_xml_string + should "be a valid XML string" do + assert REXML::Document.new(response.body) + end + end + + def self.should_respond_with(status) + should "respond with #{status}" do + assert_response status + end + end + end + end +end + +# URL helpers do not work with config.threadsafe! +# https://github.com/rspec/rspec-rails/issues/476#issuecomment-4705454 +ActionView::TestCase::TestController.instance_eval do + helper Rails.application.routes.url_helpers +end +ActionView::TestCase::TestController.class_eval do + def _routes + Rails.application.routes + end +end diff --git a/test/ui/base.rb b/test/ui/base.rb new file mode 100644 index 00000000..de2f53d0 --- /dev/null +++ b/test/ui/base.rb @@ -0,0 +1,63 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) +require 'capybara/rails' + +Capybara.default_driver = :selenium +Capybara.register_driver :selenium do |app| + # Use the following driver definition to test locally using Chrome (also requires chromedriver to be in PATH) + # Capybara::Selenium::Driver.new(app, :browser => :chrome) + # Add :switches => %w[--lang=en] to force default browser locale to English + # Default for Selenium remote driver is to connect to local host on port 4444 + # This can be change using :url => 'http://localhost:9195' if necessary + # PhantomJS 1.8 now directly supports Webdriver Wire API, simply run it with `phantomjs --webdriver 4444` + # Add :desired_capabilities => Selenium::WebDriver::Remote::Capabilities.internet_explorer) to run on Selenium Grid Hub with IE + Capybara::Selenium::Driver.new(app, :browser => :remote) +end + +module Redmine + module UiTest + # Base class for UI tests + class Base < ActionDispatch::IntegrationTest + include Capybara::DSL + + # Stop ActiveRecord from wrapping tests in transactions + # Transactional fixtures do not work with Selenium tests, because Capybara + # uses a separate server thread, which the transactions would be hidden + self.use_transactional_fixtures = false + + # Should not depend on locale since Redmine displays login page + # using default browser locale which depend on system locale for "real" browsers drivers + def log_user(login, password) + visit '/my/page' + assert_equal '/login', current_path + within('#login-form form') do + fill_in 'username', :with => login + fill_in 'password', :with => password + find('input[name=login]').click + end + assert_equal '/my/page', current_path + end + + teardown do + Capybara.reset_sessions! # Forget the (simulated) browser state + Capybara.use_default_driver # Revert Capybara.current_driver to Capybara.default_driver + end + end + end +end diff --git a/test/ui/issues_test.rb b/test/ui/issues_test.rb new file mode 100644 index 00000000..0b9af427 --- /dev/null +++ b/test/ui/issues_test.rb @@ -0,0 +1,217 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../base', __FILE__) + +class Redmine::UiTest::IssuesTest < Redmine::UiTest::Base + fixtures :projects, :users, :roles, :members, :member_roles, + :trackers, :projects_trackers, :enabled_modules, :issue_statuses, :issues, + :enumerations, :custom_fields, :custom_values, :custom_fields_trackers, + :watchers + + def test_create_issue + log_user('jsmith', 'jsmith') + visit '/projects/ecookbook/issues/new' + within('form#issue-form') do + select 'Bug', :from => 'Tracker' + select 'Low', :from => 'Priority' + fill_in 'Subject', :with => 'new test issue' + fill_in 'Description', :with => 'new issue' + select '0 %', :from => 'Done' + fill_in 'Due date', :with => '' + select '', :from => 'Assignee' + fill_in 'Searchable field', :with => 'Value for field 2' + # click_button 'Create' would match both 'Create' and 'Create and continue' buttons + find('input[name=commit]').click + end + + # find created issue + issue = Issue.find_by_subject("new test issue") + assert_kind_of Issue, issue + + # check redirection + find 'div#flash_notice', :visible => true, :text => "Issue \##{issue.id} created." + assert_equal issue_path(:id => issue), current_path + + # check issue attributes + assert_equal 'jsmith', issue.author.login + assert_equal 1, issue.project.id + assert_equal IssueStatus.find_by_name('New'), issue.status + assert_equal Tracker.find_by_name('Bug'), issue.tracker + assert_equal IssuePriority.find_by_name('Low'), issue.priority + assert_equal 'Value for field 2', issue.custom_field_value(CustomField.find_by_name('Searchable field')) + end + + def test_create_issue_with_form_update + field1 = IssueCustomField.create!( + :field_format => 'string', + :name => 'Field1', + :is_for_all => true, + :trackers => Tracker.find_all_by_id([1, 2]) + ) + field2 = IssueCustomField.create!( + :field_format => 'string', + :name => 'Field2', + :is_for_all => true, + :trackers => Tracker.find_all_by_id(2) + ) + + Role.non_member.add_permission! :add_issues + Role.non_member.remove_permission! :edit_issues, :add_issue_notes + + log_user('someone', 'foo') + visit '/projects/ecookbook/issues/new' + assert page.has_no_content?(field2.name) + assert page.has_content?(field1.name) + + fill_in 'Subject', :with => 'New test issue' + fill_in 'Description', :with => 'New test issue description' + fill_in field1.name, :with => 'CF1 value' + select 'Low', :from => 'Priority' + + # field2 should show up when changing tracker + select 'Feature request', :from => 'Tracker' + assert page.has_content?(field2.name) + assert page.has_content?(field1.name) + + fill_in field2.name, :with => 'CF2 value' + assert_difference 'Issue.count' do + page.first(:button, 'Create').click + end + + issue = Issue.order('id desc').first + assert_equal 'New test issue', issue.subject + assert_equal 'New test issue description', issue.description + assert_equal 'Low', issue.priority.name + assert_equal 'CF1 value', issue.custom_field_value(field1) + assert_equal 'CF2 value', issue.custom_field_value(field2) + end + + def test_create_issue_with_watchers + User.generate!(:firstname => 'Some', :lastname => 'Watcher') + + log_user('jsmith', 'jsmith') + visit '/projects/ecookbook/issues/new' + fill_in 'Subject', :with => 'Issue with watchers' + # Add a project member as watcher + check 'Dave Lopper' + # Search for another user + click_link 'Search for watchers to add' + within('form#new-watcher-form') do + assert page.has_content?('Some One') + fill_in 'user_search', :with => 'watch' + assert page.has_no_content?('Some One') + check 'Some Watcher' + click_button 'Add' + end + assert_difference 'Issue.count' do + find('input[name=commit]').click + end + + issue = Issue.order('id desc').first + assert_equal ['Dave Lopper', 'Some Watcher'], issue.watcher_users.map(&:name).sort + end + + def test_preview_issue_description + log_user('jsmith', 'jsmith') + visit '/projects/ecookbook/issues/new' + within('form#issue-form') do + fill_in 'Subject', :with => 'new issue subject' + fill_in 'Description', :with => 'new issue description' + click_link 'Preview' + end + find 'div#preview fieldset', :visible => true, :text => 'new issue description' + assert_difference 'Issue.count' do + find('input[name=commit]').click + end + + issue = Issue.order('id desc').first + assert_equal 'new issue description', issue.description + end + + def test_update_issue_with_form_update + field = IssueCustomField.create!( + :field_format => 'string', + :name => 'Form update CF', + :is_for_all => true, + :trackers => Tracker.find_all_by_name('Feature request') + ) + + Role.non_member.add_permission! :edit_issues + Role.non_member.remove_permission! :add_issues, :add_issue_notes + + log_user('someone', 'foo') + visit '/issues/1' + assert page.has_no_content?('Form update CF') + + page.first(:link, 'Update').click + # the custom field should show up when changing tracker + select 'Feature request', :from => 'Tracker' + assert page.has_content?('Form update CF') + + fill_in 'Form update', :with => 'CF value' + assert_no_difference 'Issue.count' do + page.first(:button, 'Submit').click + end + + issue = Issue.find(1) + assert_equal 'CF value', issue.custom_field_value(field) + end + + def test_remove_issue_watcher_from_sidebar + user = User.find(3) + Watcher.create!(:watchable => Issue.find(1), :user => user) + + log_user('jsmith', 'jsmith') + visit '/issues/1' + assert page.first('#sidebar').has_content?('Watchers (1)') + assert page.first('#sidebar').has_content?(user.name) + assert_difference 'Watcher.count', -1 do + page.first('ul.watchers .user-3 a.delete').click + end + assert page.first('#sidebar').has_content?('Watchers (0)') + assert page.first('#sidebar').has_no_content?(user.name) + end + + def test_watch_issue_via_context_menu + log_user('jsmith', 'jsmith') + visit '/issues' + find('tr#issue-1 td.updated_on').click + page.execute_script "$('tr#issue-1 td.updated_on').trigger('contextmenu');" + assert_difference 'Watcher.count' do + within('#context-menu') do + click_link 'Watch' + end + end + assert Issue.find(1).watched_by?(User.find_by_login('jsmith')) + end + + def test_bulk_watch_issues_via_context_menu + log_user('jsmith', 'jsmith') + visit '/issues' + find('tr#issue-1 input[type=checkbox]').click + find('tr#issue-4 input[type=checkbox]').click + page.execute_script "$('tr#issue-1 td.updated_on').trigger('contextmenu');" + assert_difference 'Watcher.count', 2 do + within('#context-menu') do + click_link 'Watch' + end + end + assert Issue.find(1).watched_by?(User.find_by_login('jsmith')) + assert Issue.find(4).watched_by?(User.find_by_login('jsmith')) + end +end diff --git a/test/unit/activity_test.rb b/test/unit/activity_test.rb index eddcccdf..81fecf65 100644 --- a/test/unit/activity_test.rb +++ b/test/unit/activity_test.rb @@ -1,7 +1,129 @@ -require 'test_helper' - -class ActivityTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class ActivityTest < ActiveSupport::TestCase + fixtures :projects, :versions, :attachments, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details, + :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages, :time_entries, + :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions + + def setup + @project = Project.find(1) + end + + def test_activity_without_subprojects + events = find_events(User.anonymous, :project => @project) + assert_not_nil events + + assert events.include?(Issue.find(1)) + assert !events.include?(Issue.find(4)) + # subproject issue + assert !events.include?(Issue.find(5)) + end + + def test_activity_with_subprojects + events = find_events(User.anonymous, :project => @project, :with_subprojects => 1) + assert_not_nil events + + assert events.include?(Issue.find(1)) + # subproject issue + assert events.include?(Issue.find(5)) + end + + def test_global_activity_anonymous + events = find_events(User.anonymous) + assert_not_nil events + + assert events.include?(Issue.find(1)) + assert events.include?(Message.find(5)) + # Issue of a private project + assert !events.include?(Issue.find(4)) + # Private issue and comment + assert !events.include?(Issue.find(14)) + assert !events.include?(Journal.find(5)) + end + + def test_global_activity_logged_user + events = find_events(User.find(2)) # manager + assert_not_nil events + + assert events.include?(Issue.find(1)) + # Issue of a private project the user belongs to + assert events.include?(Issue.find(4)) + end + + def test_user_activity + user = User.find(2) + events = Redmine::Activity::Fetcher.new(User.anonymous, :author => user).events(nil, nil, :limit => 10) + + assert(events.size > 0) + assert(events.size <= 10) + assert_nil(events.detect {|e| e.event_author != user}) + end + + def test_files_activity + f = Redmine::Activity::Fetcher.new(User.anonymous, :project => Project.find(1)) + f.scope = ['files'] + events = f.events + + assert_kind_of Array, events + assert events.include?(Attachment.find_by_container_type_and_container_id('Project', 1)) + assert events.include?(Attachment.find_by_container_type_and_container_id('Version', 1)) + assert_equal [Attachment], events.collect(&:class).uniq + assert_equal %w(Project Version), events.collect(&:container_type).uniq.sort + end + + def test_event_group_for_issue + issue = Issue.find(1) + assert_equal issue, issue.event_group + end + + def test_event_group_for_journal + issue = Issue.find(1) + journal = issue.journals.first + assert_equal issue, journal.event_group + end + + def test_event_group_for_issue_time_entry + time = TimeEntry.where(:issue_id => 1).first + assert_equal time.issue, time.event_group + end + + def test_event_group_for_project_time_entry + time = TimeEntry.where(:issue_id => nil).first + assert_equal time, time.event_group + end + + def test_event_group_for_message + message = Message.find(1) + reply = message.children.first + assert_equal message, message.event_group + assert_equal message, reply.event_group + end + + def test_event_group_for_wiki_content_version + content = WikiContent::Version.find(1) + assert_equal content.page, content.event_group + end + + private + + def find_events(user, options={}) + Redmine::Activity::Fetcher.new(user, options).events(Date.today - 30, Date.today + 1) + end +end diff --git a/test/unit/attachment_test.rb b/test/unit/attachment_test.rb index bd3f4659..c98a0ceb 100644 --- a/test/unit/attachment_test.rb +++ b/test/unit/attachment_test.rb @@ -1,20 +1,283 @@ -# encoding: utf-8 -require 'test_helper' - -class AttachmentsTest < ActiveSupport::TestCase - include AttachmentsHelper - - def setup - @file_belong_to = attachments(:attachments_021) - @file_not_belong_to = attachments(:attachments_022) - @project = @file_belong_to.container - end - - test "can be find file not belong to project" do - params = {:q => @file_not_belong_to} - found_file = render_attachments_for_new_project(@project) - assert found_file.include?(@file_not_belong_to) - assert_not found_file.include?(@file_belong_to) - end - -end +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class AttachmentTest < ActiveSupport::TestCase + fixtures :users, :projects, :roles, :members, :member_roles, + :enabled_modules, :issues, :trackers, :attachments + + class MockFile + attr_reader :original_filename, :content_type, :content, :size + + def initialize(attributes) + @original_filename = attributes[:original_filename] + @content_type = attributes[:content_type] + @content = attributes[:content] || "Content" + @size = content.size + end + end + + def setup + set_tmp_attachments_directory + end + + def test_container_for_new_attachment_should_be_nil + assert_nil Attachment.new.container + end + + def test_create + a = Attachment.new(:container => Issue.find(1), + :file => uploaded_test_file("testfile.txt", "text/plain"), + :author => User.find(1)) + assert a.save + assert_equal 'testfile.txt', a.filename + assert_equal 59, a.filesize + assert_equal 'text/plain', a.content_type + assert_equal 0, a.downloads + assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest + + assert a.disk_directory + assert_match %r{\A\d{4}/\d{2}\z}, a.disk_directory + + assert File.exist?(a.diskfile) + assert_equal 59, File.size(a.diskfile) + end + + def test_copy_should_preserve_attributes + a = Attachment.find(1) + copy = a.copy + + assert_save copy + copy = Attachment.order('id DESC').first + %w(filename filesize content_type author_id created_on description digest disk_filename disk_directory diskfile).each do |attribute| + assert_equal a.send(attribute), copy.send(attribute), "#{attribute} was different" + end + end + + def test_size_should_be_validated_for_new_file + with_settings :attachment_max_size => 0 do + a = Attachment.new(:container => Issue.find(1), + :file => uploaded_test_file("testfile.txt", "text/plain"), + :author => User.find(1)) + assert !a.save + end + end + + def test_size_should_not_be_validated_when_copying + a = Attachment.create!(:container => Issue.find(1), + :file => uploaded_test_file("testfile.txt", "text/plain"), + :author => User.find(1)) + with_settings :attachment_max_size => 0 do + copy = a.copy + assert copy.save + end + end + + def test_description_length_should_be_validated + a = Attachment.new(:description => 'a' * 300) + assert !a.save + assert_not_nil a.errors[:description] + end + + def test_destroy + a = Attachment.new(:container => Issue.find(1), + :file => uploaded_test_file("testfile.txt", "text/plain"), + :author => User.find(1)) + assert a.save + assert_equal 'testfile.txt', a.filename + assert_equal 59, a.filesize + assert_equal 'text/plain', a.content_type + assert_equal 0, a.downloads + assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest + diskfile = a.diskfile + assert File.exist?(diskfile) + assert_equal 59, File.size(a.diskfile) + assert a.destroy + assert !File.exist?(diskfile) + end + + def test_destroy_should_not_delete_file_referenced_by_other_attachment + a = Attachment.create!(:container => Issue.find(1), + :file => uploaded_test_file("testfile.txt", "text/plain"), + :author => User.find(1)) + diskfile = a.diskfile + + copy = a.copy + copy.save! + + assert File.exists?(diskfile) + a.destroy + assert File.exists?(diskfile) + copy.destroy + assert !File.exists?(diskfile) + end + + def test_create_should_auto_assign_content_type + a = Attachment.new(:container => Issue.find(1), + :file => uploaded_test_file("testfile.txt", ""), + :author => User.find(1)) + assert a.save + assert_equal 'text/plain', a.content_type + end + + def test_identical_attachments_at_the_same_time_should_not_overwrite + a1 = Attachment.create!(:container => Issue.find(1), + :file => uploaded_test_file("testfile.txt", ""), + :author => User.find(1)) + a2 = Attachment.create!(:container => Issue.find(1), + :file => uploaded_test_file("testfile.txt", ""), + :author => User.find(1)) + assert a1.disk_filename != a2.disk_filename + end + + def test_filename_should_be_basenamed + a = Attachment.new(:file => MockFile.new(:original_filename => "path/to/the/file")) + assert_equal 'file', a.filename + end + + def test_filename_should_be_sanitized + a = Attachment.new(:file => MockFile.new(:original_filename => "valid:[] invalid:?%*|\"'<>chars")) + assert_equal 'valid_[] invalid_chars', a.filename + end + + def test_diskfilename + assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/ + assert_equal 'test_file.txt', Attachment.disk_filename("test_file.txt")[13..-1] + assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentué.txt")[13..-1] + assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentué")[13..-1] + assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentué.ça")[13..-1] + end + + def test_title + a = Attachment.new(:filename => "test.png") + assert_equal "test.png", a.title + + a = Attachment.new(:filename => "test.png", :description => "Cool image") + assert_equal "test.png (Cool image)", a.title + end + + def test_prune_should_destroy_old_unattached_attachments + Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1, :created_on => 2.days.ago) + Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1, :created_on => 2.days.ago) + Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1) + + assert_difference 'Attachment.count', -2 do + Attachment.prune + end + end + + def test_move_from_root_to_target_directory_should_move_root_files + a = Attachment.find(20) + assert a.disk_directory.blank? + # Create a real file for this fixture + File.open(a.diskfile, "w") do |f| + f.write "test file at the root of files directory" + end + assert a.readable? + Attachment.move_from_root_to_target_directory + + a.reload + assert_equal '2012/05', a.disk_directory + assert a.readable? + end + + test "Attachmnet.attach_files should attach the file" do + issue = Issue.first + assert_difference 'Attachment.count' do + Attachment.attach_files(issue, + '1' => { + 'file' => uploaded_test_file('testfile.txt', 'text/plain'), + 'description' => 'test' + }) + end + + attachment = Attachment.first(:order => 'id DESC') + assert_equal issue, attachment.container + assert_equal 'testfile.txt', attachment.filename + assert_equal 59, attachment.filesize + assert_equal 'test', attachment.description + assert_equal 'text/plain', attachment.content_type + assert File.exists?(attachment.diskfile) + assert_equal 59, File.size(attachment.diskfile) + end + + test "Attachmnet.attach_files should add unsaved files to the object as unsaved attachments" do + # Max size of 0 to force Attachment creation failures + with_settings(:attachment_max_size => 0) do + @project = Project.find(1) + response = Attachment.attach_files(@project, { + '1' => {'file' => mock_file, 'description' => 'test'}, + '2' => {'file' => mock_file, 'description' => 'test'} + }) + + assert response[:unsaved].present? + assert_equal 2, response[:unsaved].length + assert response[:unsaved].first.new_record? + assert response[:unsaved].second.new_record? + assert_equal response[:unsaved], @project.unsaved_attachments + end + end + + def test_latest_attach + set_fixtures_attachments_directory + a1 = Attachment.find(16) + assert_equal "testfile.png", a1.filename + assert a1.readable? + assert (! a1.visible?(User.anonymous)) + assert a1.visible?(User.find(2)) + a2 = Attachment.find(17) + assert_equal "testfile.PNG", a2.filename + assert a2.readable? + assert (! a2.visible?(User.anonymous)) + assert a2.visible?(User.find(2)) + assert a1.created_on < a2.created_on + + la1 = Attachment.latest_attach([a1, a2], "testfile.png") + assert_equal 17, la1.id + la2 = Attachment.latest_attach([a1, a2], "Testfile.PNG") + assert_equal 17, la2.id + + set_tmp_attachments_directory + end + + def test_thumbnailable_should_be_true_for_images + assert_equal true, Attachment.new(:filename => 'test.jpg').thumbnailable? + end + + def test_thumbnailable_should_be_true_for_non_images + assert_equal false, Attachment.new(:filename => 'test.txt').thumbnailable? + end + + if convert_installed? + def test_thumbnail_should_generate_the_thumbnail + set_fixtures_attachments_directory + attachment = Attachment.find(16) + Attachment.clear_thumbnails + + assert_difference "Dir.glob(File.join(Attachment.thumbnails_storage_path, '*.thumb')).size" do + thumbnail = attachment.thumbnail + assert_equal "16_8e0294de2441577c529f170b6fb8f638_100.thumb", File.basename(thumbnail) + assert File.exists?(thumbnail) + end + end + else + puts '(ImageMagick convert not available)' + end +end diff --git a/test/unit/auth_source_ldap_test.rb b/test/unit/auth_source_ldap_test.rb new file mode 100644 index 00000000..6febe303 --- /dev/null +++ b/test/unit/auth_source_ldap_test.rb @@ -0,0 +1,141 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class AuthSourceLdapTest < ActiveSupport::TestCase + include Redmine::I18n + fixtures :auth_sources + + def setup + end + + def test_create + a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName') + assert a.save + end + + def test_should_strip_ldap_attributes + a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName', + :attr_firstname => 'givenName ') + assert a.save + assert_equal 'givenName', a.reload.attr_firstname + end + + def test_replace_port_zero_to_389 + a = AuthSourceLdap.new( + :name => 'My LDAP', :host => 'ldap.example.net', :port => 0, + :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName', + :attr_firstname => 'givenName ') + assert a.save + assert_equal 389, a.port + end + + def test_filter_should_be_validated + set_language_if_valid 'en' + + a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :attr_login => 'sn') + a.filter = "(mail=*@redmine.org" + assert !a.valid? + assert_include "LDAP filter is invalid", a.errors.full_messages + + a.filter = "(mail=*@redmine.org)" + assert a.valid? + end + + if ldap_configured? + test '#authenticate with a valid LDAP user should return the user attributes' do + auth = AuthSourceLdap.find(1) + auth.update_attribute :onthefly_register, true + + attributes = auth.authenticate('example1','123456') + assert attributes.is_a?(Hash), "An hash was not returned" + assert_equal 'Example', attributes[:firstname] + assert_equal 'One', attributes[:lastname] + assert_equal 'example1@redmine.org', attributes[:mail] + assert_equal auth.id, attributes[:auth_source_id] + attributes.keys.each do |attribute| + assert User.new.respond_to?("#{attribute}="), "Unexpected :#{attribute} attribute returned" + end + end + + test '#authenticate with an invalid LDAP user should return nil' do + auth = AuthSourceLdap.find(1) + assert_equal nil, auth.authenticate('nouser','123456') + end + + test '#authenticate without a login should return nil' do + auth = AuthSourceLdap.find(1) + assert_equal nil, auth.authenticate('','123456') + end + + test '#authenticate without a password should return nil' do + auth = AuthSourceLdap.find(1) + assert_equal nil, auth.authenticate('edavis','') + end + + test '#authenticate without filter should return any user' do + auth = AuthSourceLdap.find(1) + assert auth.authenticate('example1','123456') + assert auth.authenticate('edavis', '123456') + end + + test '#authenticate with filter should return user who matches the filter only' do + auth = AuthSourceLdap.find(1) + auth.filter = "(mail=*@redmine.org)" + + assert auth.authenticate('example1','123456') + assert_nil auth.authenticate('edavis', '123456') + end + + def test_authenticate_should_timeout + auth_source = AuthSourceLdap.find(1) + auth_source.timeout = 1 + def auth_source.initialize_ldap_con(*args); sleep(5); end + + assert_raise AuthSourceTimeoutException do + auth_source.authenticate 'example1', '123456' + end + end + + def test_search_should_return_matching_entries + results = AuthSource.search("exa") + assert_equal 1, results.size + result = results.first + assert_kind_of Hash, result + assert_equal "example1", result[:login] + assert_equal "Example", result[:firstname] + assert_equal "One", result[:lastname] + assert_equal "example1@redmine.org", result[:mail] + assert_equal 1, result[:auth_source_id] + end + + def test_search_with_no_match_should_return_an_empty_array + results = AuthSource.search("wro") + assert_equal [], results + end + + def test_search_with_exception_should_return_an_empty_array + Net::LDAP.stubs(:new).raises(Net::LDAP::LdapError, 'Cannot connect') + + results = AuthSource.search("exa") + assert_equal [], results + end + else + puts '(Test LDAP server not configured)' + end +end diff --git a/test/unit/board_test.rb b/test/unit/board_test.rb new file mode 100644 index 00000000..50f0c160 --- /dev/null +++ b/test/unit/board_test.rb @@ -0,0 +1,116 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class BoardTest < ActiveSupport::TestCase + fixtures :projects, :boards, :messages, :attachments, :watchers + + include Redmine::I18n + + def setup + @project = Project.find(1) + end + + def test_create + board = Board.new(:project => @project, :name => 'Test board', :description => 'Test board description') + assert board.save + board.reload + assert_equal 'Test board', board.name + assert_equal 'Test board description', board.description + assert_equal @project, board.project + assert_equal 0, board.topics_count + assert_equal 0, board.messages_count + assert_nil board.last_message + # last position + assert_equal @project.boards.size, board.position + end + + def test_parent_should_be_in_same_project + set_language_if_valid 'en' + board = Board.new(:project_id => 3, :name => 'Test', :description => 'Test', :parent_id => 1) + assert !board.save + assert_include "Parent forum is invalid", board.errors.full_messages + end + + def test_valid_parents_should_not_include_self_nor_a_descendant + board1 = Board.generate!(:project_id => 3) + board2 = Board.generate!(:project_id => 3, :parent => board1) + board3 = Board.generate!(:project_id => 3, :parent => board2) + board4 = Board.generate!(:project_id => 3) + + assert_equal [board4], board1.reload.valid_parents.sort_by(&:id) + assert_equal [board1, board4], board2.reload.valid_parents.sort_by(&:id) + assert_equal [board1, board2, board4], board3.reload.valid_parents.sort_by(&:id) + assert_equal [board1, board2, board3], board4.reload.valid_parents.sort_by(&:id) + end + + def test_position_should_be_assigned_with_parent_scope + parent1 = Board.generate!(:project_id => 3) + parent2 = Board.generate!(:project_id => 3) + child1 = Board.generate!(:project_id => 3, :parent => parent1) + child2 = Board.generate!(:project_id => 3, :parent => parent1) + + assert_equal 1, parent1.reload.position + assert_equal 1, child1.reload.position + assert_equal 2, child2.reload.position + assert_equal 2, parent2.reload.position + end + + def test_board_tree_should_yield_boards_with_level + parent1 = Board.generate!(:project_id => 3) + parent2 = Board.generate!(:project_id => 3) + child1 = Board.generate!(:project_id => 3, :parent => parent1) + child2 = Board.generate!(:project_id => 3, :parent => parent1) + child3 = Board.generate!(:project_id => 3, :parent => child1) + + tree = Board.board_tree(Project.find(3).boards) + + assert_equal [ + [parent1, 0], + [child1, 1], + [child3, 2], + [child2, 1], + [parent2, 0] + ], tree + end + + def test_destroy + board = Board.find(1) + assert_difference 'Message.count', -6 do + assert_difference 'Attachment.count', -1 do + assert_difference 'Watcher.count', -1 do + assert board.destroy + end + end + end + assert_equal 0, Message.count(:conditions => {:board_id => 1}) + end + + def test_destroy_should_nullify_children + parent = Board.generate!(:project => @project) + child = Board.generate!(:project => @project, :parent => parent) + assert_equal parent, child.parent + + assert parent.destroy + child.reload + assert_nil child.parent + assert_nil child.parent_id + end +end diff --git a/test/unit/changeset_test.rb b/test/unit/changeset_test.rb new file mode 100644 index 00000000..41679175 --- /dev/null +++ b/test/unit/changeset_test.rb @@ -0,0 +1,476 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class ChangesetTest < ActiveSupport::TestCase + fixtures :projects, :repositories, + :issues, :issue_statuses, :issue_categories, + :changesets, :changes, + :enumerations, + :custom_fields, :custom_values, + :users, :members, :member_roles, :trackers, + :enabled_modules, :roles + + def test_ref_keywords_any + ActionMailer::Base.deliveries.clear + Setting.commit_fix_status_id = IssueStatus.find( + :first, :conditions => ["is_closed = ?", true]).id + Setting.commit_fix_done_ratio = '90' + Setting.commit_ref_keywords = '*' + Setting.commit_fix_keywords = 'fixes , closes' + + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => 'New commit (#2). Fixes #1', + :revision => '12345') + assert c.save + assert_equal [1, 2], c.issue_ids.sort + fixed = Issue.find(1) + assert fixed.closed? + assert_equal 90, fixed.done_ratio + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_ref_keywords + Setting.commit_ref_keywords = 'refs' + Setting.commit_fix_keywords = '' + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => 'Ignores #2. Refs #1', + :revision => '12345') + assert c.save + assert_equal [1], c.issue_ids.sort + end + + def test_ref_keywords_any_only + Setting.commit_ref_keywords = '*' + Setting.commit_fix_keywords = '' + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => 'Ignores #2. Refs #1', + :revision => '12345') + assert c.save + assert_equal [1, 2], c.issue_ids.sort + end + + def test_ref_keywords_any_with_timelog + Setting.commit_ref_keywords = '*' + Setting.commit_logtime_enabled = '1' + + { + '2' => 2.0, + '2h' => 2.0, + '2hours' => 2.0, + '15m' => 0.25, + '15min' => 0.25, + '3h15' => 3.25, + '3h15m' => 3.25, + '3h15min' => 3.25, + '3:15' => 3.25, + '3.25' => 3.25, + '3.25h' => 3.25, + '3,25' => 3.25, + '3,25h' => 3.25, + }.each do |syntax, expected_hours| + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => 24.hours.ago, + :comments => "Worked on this issue #1 @#{syntax}", + :revision => '520', + :user => User.find(2)) + assert_difference 'TimeEntry.count' do + c.scan_comment_for_issue_ids + end + assert_equal [1], c.issue_ids.sort + + time = TimeEntry.first(:order => 'id desc') + assert_equal 1, time.issue_id + assert_equal 1, time.project_id + assert_equal 2, time.user_id + assert_equal expected_hours, time.hours, + "@#{syntax} should be logged as #{expected_hours} hours but was #{time.hours}" + assert_equal Date.yesterday, time.spent_on + assert time.activity.is_default? + assert time.comments.include?('r520'), + "r520 was expected in time_entry comments: #{time.comments}" + end + end + + def test_ref_keywords_closing_with_timelog + Setting.commit_fix_status_id = IssueStatus.find( + :first, :conditions => ["is_closed = ?", true]).id + Setting.commit_ref_keywords = '*' + Setting.commit_fix_keywords = 'fixes , closes' + Setting.commit_logtime_enabled = '1' + + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => 'This is a comment. Fixes #1 @4.5, #2 @1', + :user => User.find(2)) + assert_difference 'TimeEntry.count', 2 do + c.scan_comment_for_issue_ids + end + + assert_equal [1, 2], c.issue_ids.sort + assert Issue.find(1).closed? + assert Issue.find(2).closed? + + times = TimeEntry.all(:order => 'id desc', :limit => 2) + assert_equal [1, 2], times.collect(&:issue_id).sort + end + + def test_ref_keywords_any_line_start + Setting.commit_ref_keywords = '*' + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => '#1 is the reason of this commit', + :revision => '12345') + assert c.save + assert_equal [1], c.issue_ids.sort + end + + def test_ref_keywords_allow_brackets_around_a_issue_number + Setting.commit_ref_keywords = '*' + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => '[#1] Worked on this issue', + :revision => '12345') + assert c.save + assert_equal [1], c.issue_ids.sort + end + + def test_ref_keywords_allow_brackets_around_multiple_issue_numbers + Setting.commit_ref_keywords = '*' + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => '[#1 #2, #3] Worked on these', + :revision => '12345') + assert c.save + assert_equal [1,2,3], c.issue_ids.sort + end + + def test_commit_referencing_a_subproject_issue + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => 'refs #5, a subproject issue', + :revision => '12345') + assert c.save + assert_equal [5], c.issue_ids.sort + assert c.issues.first.project != c.project + end + + def test_commit_closing_a_subproject_issue + with_settings :commit_fix_status_id => 5, :commit_fix_keywords => 'closes', + :default_language => 'en' do + issue = Issue.find(5) + assert !issue.closed? + assert_difference 'Journal.count' do + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => 'closes #5, a subproject issue', + :revision => '12345') + assert c.save + end + assert issue.reload.closed? + journal = Journal.first(:order => 'id DESC') + assert_equal issue, journal.issue + assert_include "Applied in changeset ecookbook:r12345.", journal.notes + end + end + + def test_commit_referencing_a_parent_project_issue + # repository of child project + r = Repository::Subversion.create!( + :project => Project.find(3), + :url => 'svn://localhost/test') + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :comments => 'refs #2, an issue of a parent project', + :revision => '12345') + assert c.save + assert_equal [2], c.issue_ids.sort + assert c.issues.first.project != c.project + end + + def test_commit_referencing_a_project_with_commit_cross_project_ref_disabled + r = Repository::Subversion.create!( + :project => Project.find(3), + :url => 'svn://localhost/test') + + with_settings :commit_cross_project_ref => '0' do + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :comments => 'refs #4, an issue of a different project', + :revision => '12345') + assert c.save + assert_equal [], c.issue_ids + end + end + + def test_commit_referencing_a_project_with_commit_cross_project_ref_enabled + r = Repository::Subversion.create!( + :project => Project.find(3), + :url => 'svn://localhost/test') + + with_settings :commit_cross_project_ref => '1' do + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :comments => 'refs #4, an issue of a different project', + :revision => '12345') + assert c.save + assert_equal [4], c.issue_ids + end + end + + def test_text_tag_revision + c = Changeset.new(:revision => '520') + assert_equal 'r520', c.text_tag + end + + def test_text_tag_revision_with_same_project + c = Changeset.new(:revision => '520', :repository => Project.find(1).repository) + assert_equal 'r520', c.text_tag(Project.find(1)) + end + + def test_text_tag_revision_with_different_project + c = Changeset.new(:revision => '520', :repository => Project.find(1).repository) + assert_equal 'ecookbook:r520', c.text_tag(Project.find(2)) + end + + def test_text_tag_revision_with_repository_identifier + r = Repository::Subversion.create!( + :project_id => 1, + :url => 'svn://localhost/test', + :identifier => 'documents') + + c = Changeset.new(:revision => '520', :repository => r) + assert_equal 'documents|r520', c.text_tag + assert_equal 'ecookbook:documents|r520', c.text_tag(Project.find(2)) + end + + def test_text_tag_hash + c = Changeset.new( + :scmid => '7234cb2750b63f47bff735edc50a1c0a433c2518', + :revision => '7234cb2750b63f47bff735edc50a1c0a433c2518') + assert_equal 'commit:7234cb2750b63f47bff735edc50a1c0a433c2518', c.text_tag + end + + def test_text_tag_hash_with_same_project + c = Changeset.new(:revision => '7234cb27', :scmid => '7234cb27', :repository => Project.find(1).repository) + assert_equal 'commit:7234cb27', c.text_tag(Project.find(1)) + end + + def test_text_tag_hash_with_different_project + c = Changeset.new(:revision => '7234cb27', :scmid => '7234cb27', :repository => Project.find(1).repository) + assert_equal 'ecookbook:commit:7234cb27', c.text_tag(Project.find(2)) + end + + def test_text_tag_hash_all_number + c = Changeset.new(:scmid => '0123456789', :revision => '0123456789') + assert_equal 'commit:0123456789', c.text_tag + end + + def test_previous + changeset = Changeset.find_by_revision('3') + assert_equal Changeset.find_by_revision('2'), changeset.previous + end + + def test_previous_nil + changeset = Changeset.find_by_revision('1') + assert_nil changeset.previous + end + + def test_next + changeset = Changeset.find_by_revision('2') + assert_equal Changeset.find_by_revision('3'), changeset.next + end + + def test_next_nil + changeset = Changeset.find_by_revision('10') + assert_nil changeset.next + end + + def test_comments_should_be_converted_to_utf8 + proj = Project.find(3) + # str = File.read("#{RAILS_ROOT}/test/fixtures/encoding/iso-8859-1.txt") + str = "Texte encod\xe9 en ISO-8859-1." + str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding) + r = Repository::Bazaar.create!( + :project => proj, + :url => '/tmp/test/bazaar', + :log_encoding => 'ISO-8859-1' ) + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => '12345', + :comments => str) + assert( c.save ) + str_utf8 = "Texte encod\xc3\xa9 en ISO-8859-1." + str_utf8.force_encoding("UTF-8") if str_utf8.respond_to?(:force_encoding) + assert_equal str_utf8, c.comments + end + + def test_invalid_utf8_sequences_in_comments_should_be_replaced_latin1 + proj = Project.find(3) + # str = File.read("#{RAILS_ROOT}/test/fixtures/encoding/iso-8859-1.txt") + str1 = "Texte encod\xe9 en ISO-8859-1." + str2 = "\xe9a\xe9b\xe9c\xe9d\xe9e test" + str1.force_encoding("UTF-8") if str1.respond_to?(:force_encoding) + str2.force_encoding("ASCII-8BIT") if str2.respond_to?(:force_encoding) + r = Repository::Bazaar.create!( + :project => proj, + :url => '/tmp/test/bazaar', + :log_encoding => 'UTF-8' ) + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => '12345', + :comments => str1, + :committer => str2) + assert( c.save ) + assert_equal "Texte encod? en ISO-8859-1.", c.comments + assert_equal "?a?b?c?d?e test", c.committer + end + + def test_invalid_utf8_sequences_in_comments_should_be_replaced_ja_jis + proj = Project.find(3) + str = "test\xb5\xfetest\xb5\xfe" + if str.respond_to?(:force_encoding) + str.force_encoding('ASCII-8BIT') + end + r = Repository::Bazaar.create!( + :project => proj, + :url => '/tmp/test/bazaar', + :log_encoding => 'ISO-2022-JP' ) + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => '12345', + :comments => str) + assert( c.save ) + assert_equal "test??test??", c.comments + end + + def test_comments_should_be_converted_all_latin1_to_utf8 + s1 = "\xC2\x80" + s2 = "\xc3\x82\xc2\x80" + s4 = s2.dup + if s1.respond_to?(:force_encoding) + s3 = s1.dup + s1.force_encoding('ASCII-8BIT') + s2.force_encoding('ASCII-8BIT') + s3.force_encoding('ISO-8859-1') + s4.force_encoding('UTF-8') + assert_equal s3.encode('UTF-8'), s4 + end + proj = Project.find(3) + r = Repository::Bazaar.create!( + :project => proj, + :url => '/tmp/test/bazaar', + :log_encoding => 'ISO-8859-1' ) + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => '12345', + :comments => s1) + assert( c.save ) + assert_equal s4, c.comments + end + + def test_invalid_utf8_sequences_in_paths_should_be_replaced + proj = Project.find(3) + str1 = "Texte encod\xe9 en ISO-8859-1" + str2 = "\xe9a\xe9b\xe9c\xe9d\xe9e test" + str1.force_encoding("UTF-8") if str1.respond_to?(:force_encoding) + str2.force_encoding("ASCII-8BIT") if str2.respond_to?(:force_encoding) + r = Repository::Bazaar.create!( + :project => proj, + :url => '/tmp/test/bazaar', + :log_encoding => 'UTF-8' ) + assert r + cs = Changeset.new( + :repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => '12345', + :comments => "test") + assert(cs.save) + ch = Change.new( + :changeset => cs, + :action => "A", + :path => str1, + :from_path => str2, + :from_revision => "345") + assert(ch.save) + assert_equal "Texte encod? en ISO-8859-1", ch.path + assert_equal "?a?b?c?d?e test", ch.from_path + end + + def test_comments_nil + proj = Project.find(3) + r = Repository::Bazaar.create!( + :project => proj, + :url => '/tmp/test/bazaar', + :log_encoding => 'ISO-8859-1' ) + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => '12345', + :comments => nil, + :committer => nil) + assert( c.save ) + assert_equal "", c.comments + assert_equal nil, c.committer + if c.comments.respond_to?(:force_encoding) + assert_equal "UTF-8", c.comments.encoding.to_s + end + end + + def test_comments_empty + proj = Project.find(3) + r = Repository::Bazaar.create!( + :project => proj, + :url => '/tmp/test/bazaar', + :log_encoding => 'ISO-8859-1' ) + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => '12345', + :comments => "", + :committer => "") + assert( c.save ) + assert_equal "", c.comments + assert_equal "", c.committer + if c.comments.respond_to?(:force_encoding) + assert_equal "UTF-8", c.comments.encoding.to_s + assert_equal "UTF-8", c.committer.encoding.to_s + end + end + + def test_identifier + c = Changeset.find_by_revision('1') + assert_equal c.revision, c.identifier + end +end diff --git a/test/unit/comment_test.rb b/test/unit/comment_test.rb new file mode 100644 index 00000000..11ab50af --- /dev/null +++ b/test/unit/comment_test.rb @@ -0,0 +1,57 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class CommentTest < ActiveSupport::TestCase + fixtures :users, :news, :comments, :projects, :enabled_modules + + def setup + @jsmith = User.find(2) + @news = News.find(1) + end + + def test_create + comment = Comment.new(:commented => @news, :author => @jsmith, :comments => "my comment") + assert comment.save + @news.reload + assert_equal 2, @news.comments_count + end + + def test_create_should_send_notification + Watcher.create!(:watchable => @news, :user => @jsmith) + + with_settings :notified_events => %w(news_comment_added) do + assert_difference 'ActionMailer::Base.deliveries.size' do + Comment.create!(:commented => @news, :author => @jsmith, :comments => "my comment") + end + end + end + + def test_validate + comment = Comment.new(:commented => @news) + assert !comment.save + assert_equal 2, comment.errors.count + end + + def test_destroy + comment = Comment.find(1) + assert comment.destroy + @news.reload + assert_equal 0, @news.comments_count + end +end diff --git a/test/unit/custom_field_test.rb b/test/unit/custom_field_test.rb new file mode 100644 index 00000000..c8db3da9 --- /dev/null +++ b/test/unit/custom_field_test.rb @@ -0,0 +1,244 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class CustomFieldTest < ActiveSupport::TestCase + fixtures :custom_fields + + def test_create + field = UserCustomField.new(:name => 'Money money money', :field_format => 'float') + assert field.save + end + + def test_before_validation + field = CustomField.new(:name => 'test_before_validation', :field_format => 'int') + field.searchable = true + assert field.save + assert_equal false, field.searchable + field.searchable = true + assert field.save + assert_equal false, field.searchable + end + + def test_regexp_validation + field = IssueCustomField.new(:name => 'regexp', :field_format => 'text', :regexp => '[a-z0-9') + assert !field.save + assert_include I18n.t('activerecord.errors.messages.invalid'), + field.errors[:regexp] + field.regexp = '[a-z0-9]' + assert field.save + end + + def test_default_value_should_be_validated + field = CustomField.new(:name => 'Test', :field_format => 'int') + field.default_value = 'abc' + assert !field.valid? + field.default_value = '6' + assert field.valid? + end + + def test_default_value_should_not_be_validated_when_blank + field = CustomField.new(:name => 'Test', :field_format => 'list', :possible_values => ['a', 'b'], :is_required => true, :default_value => '') + assert field.valid? + end + + def test_should_not_change_field_format_of_existing_custom_field + field = CustomField.find(1) + field.field_format = 'int' + assert_equal 'list', field.field_format + end + + def test_possible_values_should_accept_an_array + field = CustomField.new + field.possible_values = ["One value", ""] + assert_equal ["One value"], field.possible_values + end + + def test_possible_values_should_accept_a_string + field = CustomField.new + field.possible_values = "One value" + assert_equal ["One value"], field.possible_values + end + + def test_possible_values_should_accept_a_multiline_string + field = CustomField.new + field.possible_values = "One value\nAnd another one \r\n \n" + assert_equal ["One value", "And another one"], field.possible_values + end + + if "string".respond_to?(:encoding) + def test_possible_values_stored_as_binary_should_be_utf8_encoded + field = CustomField.find(11) + assert_kind_of Array, field.possible_values + assert field.possible_values.size > 0 + field.possible_values.each do |value| + assert_equal "UTF-8", value.encoding.name + end + end + end + + def test_destroy + field = CustomField.find(1) + assert field.destroy + end + + def test_new_subclass_instance_should_return_an_instance + f = CustomField.new_subclass_instance('IssueCustomField') + assert_kind_of IssueCustomField, f + end + + def test_new_subclass_instance_should_set_attributes + f = CustomField.new_subclass_instance('IssueCustomField', :name => 'Test') + assert_kind_of IssueCustomField, f + assert_equal 'Test', f.name + end + + def test_new_subclass_instance_with_invalid_class_name_should_return_nil + assert_nil CustomField.new_subclass_instance('WrongClassName') + end + + def test_new_subclass_instance_with_non_subclass_name_should_return_nil + assert_nil CustomField.new_subclass_instance('Project') + end + + def test_string_field_validation_with_blank_value + f = CustomField.new(:field_format => 'string') + + assert f.valid_field_value?(nil) + assert f.valid_field_value?('') + + f.is_required = true + assert !f.valid_field_value?(nil) + assert !f.valid_field_value?('') + end + + def test_string_field_validation_with_min_and_max_lengths + f = CustomField.new(:field_format => 'string', :min_length => 2, :max_length => 5) + + assert f.valid_field_value?(nil) + assert f.valid_field_value?('') + assert f.valid_field_value?('a' * 2) + assert !f.valid_field_value?('a') + assert !f.valid_field_value?('a' * 6) + end + + def test_string_field_validation_with_regexp + f = CustomField.new(:field_format => 'string', :regexp => '^[A-Z0-9]*$') + + assert f.valid_field_value?(nil) + assert f.valid_field_value?('') + assert f.valid_field_value?('ABC') + assert !f.valid_field_value?('abc') + end + + def test_date_field_validation + f = CustomField.new(:field_format => 'date') + + assert f.valid_field_value?(nil) + assert f.valid_field_value?('') + assert f.valid_field_value?('1975-07-14') + assert !f.valid_field_value?('1975-07-33') + assert !f.valid_field_value?('abc') + end + + def test_list_field_validation + f = CustomField.new(:field_format => 'list', :possible_values => ['value1', 'value2']) + + assert f.valid_field_value?(nil) + assert f.valid_field_value?('') + assert f.valid_field_value?('value2') + assert !f.valid_field_value?('abc') + end + + def test_int_field_validation + f = CustomField.new(:field_format => 'int') + + assert f.valid_field_value?(nil) + assert f.valid_field_value?('') + assert f.valid_field_value?('123') + assert f.valid_field_value?('+123') + assert f.valid_field_value?('-123') + assert !f.valid_field_value?('6abc') + end + + def test_float_field_validation + f = CustomField.new(:field_format => 'float') + + assert f.valid_field_value?(nil) + assert f.valid_field_value?('') + assert f.valid_field_value?('11.2') + assert f.valid_field_value?('-6.250') + assert f.valid_field_value?('5') + assert !f.valid_field_value?('6abc') + end + + def test_multi_field_validation + f = CustomField.new(:field_format => 'list', :multiple => 'true', :possible_values => ['value1', 'value2']) + + assert f.valid_field_value?(nil) + assert f.valid_field_value?('') + assert f.valid_field_value?([]) + assert f.valid_field_value?([nil]) + assert f.valid_field_value?(['']) + + assert f.valid_field_value?('value2') + assert !f.valid_field_value?('abc') + + assert f.valid_field_value?(['value2']) + assert !f.valid_field_value?(['abc']) + + assert f.valid_field_value?(['', 'value2']) + assert !f.valid_field_value?(['', 'abc']) + + assert f.valid_field_value?(['value1', 'value2']) + assert !f.valid_field_value?(['value1', 'abc']) + end + + def test_changing_multiple_to_false_should_delete_multiple_values + field = ProjectCustomField.create!(:name => 'field', :field_format => 'list', :multiple => 'true', :possible_values => ['field1', 'field2']) + other = ProjectCustomField.create!(:name => 'other', :field_format => 'list', :multiple => 'true', :possible_values => ['other1', 'other2']) + + item_with_multiple_values = Project.generate!(:custom_field_values => {field.id => ['field1', 'field2'], other.id => ['other1', 'other2']}) + item_with_single_values = Project.generate!(:custom_field_values => {field.id => ['field1'], other.id => ['other2']}) + + assert_difference 'CustomValue.count', -1 do + field.multiple = false + field.save! + end + + item_with_multiple_values = Project.find(item_with_multiple_values.id) + assert_kind_of String, item_with_multiple_values.custom_field_value(field) + assert_kind_of Array, item_with_multiple_values.custom_field_value(other) + assert_equal 2, item_with_multiple_values.custom_field_value(other).size + end + + def test_value_class_should_return_the_class_used_for_fields_values + assert_equal User, CustomField.new(:field_format => 'user').value_class + assert_equal Version, CustomField.new(:field_format => 'version').value_class + end + + def test_value_class_should_return_nil_for_other_fields + assert_nil CustomField.new(:field_format => 'text').value_class + assert_nil CustomField.new.value_class + end + + def test_value_from_keyword_for_list_custom_field + field = CustomField.find(1) + assert_equal 'PostgreSQL', field.value_from_keyword('postgresql', Issue.find(1)) + end +end diff --git a/test/unit/custom_field_user_format_test.rb b/test/unit/custom_field_user_format_test.rb new file mode 100644 index 00000000..042b5e12 --- /dev/null +++ b/test/unit/custom_field_user_format_test.rb @@ -0,0 +1,77 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class CustomFieldUserFormatTest < ActiveSupport::TestCase + fixtures :custom_fields, :projects, :members, :users, :member_roles, :trackers, :issues + + def setup + @field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user') + end + + def test_possible_values_with_no_arguments + assert_equal [], @field.possible_values + assert_equal [], @field.possible_values(nil) + end + + def test_possible_values_with_project_resource + project = Project.find(1) + possible_values = @field.possible_values(project.issues.first) + assert possible_values.any? + assert_equal project.users.sort.collect(&:id).map(&:to_s), possible_values + end + + def test_possible_values_with_nil_project_resource + project = Project.find(1) + assert_equal [], @field.possible_values(Issue.new) + end + + def test_possible_values_options_with_no_arguments + assert_equal [], @field.possible_values_options + assert_equal [], @field.possible_values_options(nil) + end + + def test_possible_values_options_with_project_resource + project = Project.find(1) + possible_values_options = @field.possible_values_options(project.issues.first) + assert possible_values_options.any? + assert_equal project.users.sort.map {|u| [u.name, u.id.to_s]}, possible_values_options + end + + def test_possible_values_options_with_array + projects = Project.find([1, 2]) + possible_values_options = @field.possible_values_options(projects) + assert possible_values_options.any? + assert_equal (projects.first.users & projects.last.users).sort.map {|u| [u.name, u.id.to_s]}, possible_values_options + end + + def test_cast_blank_value + assert_equal nil, @field.cast_value(nil) + assert_equal nil, @field.cast_value("") + end + + def test_cast_valid_value + user = @field.cast_value("2") + assert_kind_of User, user + assert_equal User.find(2), user + end + + def test_cast_invalid_value + assert_equal nil, @field.cast_value("187") + end +end diff --git a/test/unit/custom_field_version_format_test.rb b/test/unit/custom_field_version_format_test.rb new file mode 100644 index 00000000..9b1cf678 --- /dev/null +++ b/test/unit/custom_field_version_format_test.rb @@ -0,0 +1,76 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class CustomFieldVersionFormatTest < ActiveSupport::TestCase + fixtures :custom_fields, :projects, :members, :users, :member_roles, :trackers, :issues, :versions + + def setup + @field = IssueCustomField.create!(:name => 'Tester', :field_format => 'version') + end + + def test_possible_values_with_no_arguments + assert_equal [], @field.possible_values + assert_equal [], @field.possible_values(nil) + end + + def test_possible_values_with_project_resource + project = Project.find(1) + possible_values = @field.possible_values(project.issues.first) + assert possible_values.any? + assert_equal project.shared_versions.sort.collect(&:id).map(&:to_s), possible_values + end + + def test_possible_values_with_nil_project_resource + assert_equal [], @field.possible_values(Issue.new) + end + + def test_possible_values_options_with_no_arguments + assert_equal [], @field.possible_values_options + assert_equal [], @field.possible_values_options(nil) + end + + def test_possible_values_options_with_project_resource + project = Project.find(1) + possible_values_options = @field.possible_values_options(project.issues.first) + assert possible_values_options.any? + assert_equal project.shared_versions.sort.map {|u| [u.name, u.id.to_s]}, possible_values_options + end + + def test_possible_values_options_with_array + projects = Project.find([1, 2]) + possible_values_options = @field.possible_values_options(projects) + assert possible_values_options.any? + assert_equal (projects.first.shared_versions & projects.last.shared_versions).sort.map {|u| [u.name, u.id.to_s]}, possible_values_options + end + + def test_cast_blank_value + assert_equal nil, @field.cast_value(nil) + assert_equal nil, @field.cast_value("") + end + + def test_cast_valid_value + version = @field.cast_value("2") + assert_kind_of Version, version + assert_equal Version.find(2), version + end + + def test_cast_invalid_value + assert_equal nil, @field.cast_value("187") + end +end diff --git a/test/unit/custom_value_test.rb b/test/unit/custom_value_test.rb new file mode 100644 index 00000000..9f057e69 --- /dev/null +++ b/test/unit/custom_value_test.rb @@ -0,0 +1,39 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class CustomValueTest < ActiveSupport::TestCase + fixtures :custom_fields, :custom_values, :users + + def test_default_value + field = CustomField.find_by_default_value('Default string') + assert_not_nil field + + v = CustomValue.new(:custom_field => field) + assert_equal 'Default string', v.value + + v = CustomValue.new(:custom_field => field, :value => 'Not empty') + assert_equal 'Not empty', v.value + end + + def test_sti_polymorphic_association + # Rails uses top level sti class for polymorphic association. See #3978. + assert !User.find(4).custom_values.empty? + assert !CustomValue.find(2).customized.nil? + end +end diff --git a/test/unit/default_data_test.rb b/test/unit/default_data_test.rb new file mode 100644 index 00000000..07421bcf --- /dev/null +++ b/test/unit/default_data_test.rb @@ -0,0 +1,49 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class DefaultDataTest < ActiveSupport::TestCase + include Redmine::I18n + fixtures :roles + + def test_no_data + assert !Redmine::DefaultData::Loader::no_data? + Role.delete_all("builtin = 0") + Tracker.delete_all + IssueStatus.delete_all + Enumeration.delete_all + assert Redmine::DefaultData::Loader::no_data? + end + + def test_load + valid_languages.each do |lang| + begin + Role.delete_all("builtin = 0") + Tracker.delete_all + IssueStatus.delete_all + Enumeration.delete_all + assert Redmine::DefaultData::Loader::load(lang) + assert_not_nil DocumentCategory.first + assert_not_nil IssuePriority.first + assert_not_nil TimeEntryActivity.first + rescue ActiveRecord::RecordInvalid => e + assert false, ":#{lang} default data is invalid (#{e.message})." + end + end + end +end diff --git a/test/unit/document_category_test.rb b/test/unit/document_category_test.rb new file mode 100644 index 00000000..c7b5bfbe --- /dev/null +++ b/test/unit/document_category_test.rb @@ -0,0 +1,47 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class DocumentCategoryTest < ActiveSupport::TestCase + fixtures :enumerations, :documents, :issues + + def test_should_be_an_enumeration + assert DocumentCategory.ancestors.include?(Enumeration) + end + + def test_objects_count + assert_equal 2, DocumentCategory.find_by_name("Uncategorized").objects_count + assert_equal 0, DocumentCategory.find_by_name("User documentation").objects_count + end + + def test_option_name + assert_equal :enumeration_doc_categories, DocumentCategory.new.option_name + end + + def test_default + assert_nil DocumentCategory.where(:is_default => true).first + e = Enumeration.find_by_name('Technical documentation') + e.update_attributes(:is_default => true) + assert_equal 3, DocumentCategory.default.id + end + + def test_force_default + assert_nil DocumentCategory.where(:is_default => true).first + assert_equal 1, DocumentCategory.default.id + end +end diff --git a/test/unit/document_test.rb b/test/unit/document_test.rb new file mode 100644 index 00000000..218f78d1 --- /dev/null +++ b/test/unit/document_test.rb @@ -0,0 +1,62 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class DocumentTest < ActiveSupport::TestCase + fixtures :projects, :enumerations, :documents, :attachments, + :enabled_modules, + :users, :members, :member_roles, :roles, + :groups_users + + def test_create + doc = Document.new(:project => Project.find(1), :title => 'New document', :category => Enumeration.find_by_name('User documentation')) + assert doc.save + end + + def test_create_should_send_email_notification + ActionMailer::Base.deliveries.clear + + with_settings :notified_events => %w(document_added) do + doc = Document.new(:project => Project.find(1), :title => 'New document', :category => Enumeration.find_by_name('User documentation')) + assert doc.save + end + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_create_with_default_category + # Sets a default category + e = Enumeration.find_by_name('Technical documentation') + e.update_attributes(:is_default => true) + + doc = Document.new(:project => Project.find(1), :title => 'New document') + assert_equal e, doc.category + assert doc.save + end + + def test_updated_on_with_attachments + d = Document.find(1) + assert d.attachments.any? + assert_equal d.attachments.map(&:created_on).max, d.updated_on + end + + def test_updated_on_without_attachments + d = Document.find(2) + assert d.attachments.empty? + assert_equal d.created_on, d.updated_on + end +end diff --git a/test/unit/enabled_module_test.rb b/test/unit/enabled_module_test.rb new file mode 100644 index 00000000..0abc9375 --- /dev/null +++ b/test/unit/enabled_module_test.rb @@ -0,0 +1,43 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class EnabledModuleTest < ActiveSupport::TestCase + fixtures :projects, :wikis + + def test_enabling_wiki_should_create_a_wiki + CustomField.delete_all + project = Project.create!(:name => 'Project with wiki', :identifier => 'wikiproject') + assert_nil project.wiki + project.enabled_module_names = ['wiki'] + project.reload + assert_not_nil project.wiki + assert_equal 'Wiki', project.wiki.start_page + end + + def test_reenabling_wiki_should_not_create_another_wiki + project = Project.find(1) + assert_not_nil project.wiki + project.enabled_module_names = [] + project.reload + assert_no_difference 'Wiki.count' do + project.enabled_module_names = ['wiki'] + end + assert_not_nil project.wiki + end +end diff --git a/test/unit/enumeration_test.rb b/test/unit/enumeration_test.rb new file mode 100644 index 00000000..1b0b46d0 --- /dev/null +++ b/test/unit/enumeration_test.rb @@ -0,0 +1,130 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class EnumerationTest < ActiveSupport::TestCase + fixtures :enumerations, :issues, :custom_fields, :custom_values + + def test_objects_count + # low priority + assert_equal 6, Enumeration.find(4).objects_count + # urgent + assert_equal 0, Enumeration.find(7).objects_count + end + + def test_in_use + # low priority + assert Enumeration.find(4).in_use? + # urgent + assert !Enumeration.find(7).in_use? + end + + def test_default + e = Enumeration.default + assert e.is_a?(Enumeration) + assert e.is_default? + assert e.active? + assert_equal 'Default Enumeration', e.name + end + + def test_default_non_active + e = Enumeration.find(12) + assert e.is_a?(Enumeration) + assert e.is_default? + assert e.active? + e.update_attributes(:active => false) + assert e.is_default? + assert !e.active? + end + + def test_create + e = Enumeration.new(:name => 'Not default', :is_default => false) + e.type = 'Enumeration' + assert e.save + assert_equal 'Default Enumeration', Enumeration.default.name + end + + def test_create_as_default + e = Enumeration.new(:name => 'Very urgent', :is_default => true) + e.type = 'Enumeration' + assert e.save + assert_equal e, Enumeration.default + end + + def test_update_default + e = Enumeration.default + e.update_attributes(:name => 'Changed', :is_default => true) + assert_equal e, Enumeration.default + end + + def test_update_default_to_non_default + e = Enumeration.default + e.update_attributes(:name => 'Changed', :is_default => false) + assert_nil Enumeration.default + end + + def test_change_default + e = Enumeration.find_by_name('Default Enumeration') + e.update_attributes(:name => 'Changed Enumeration', :is_default => true) + assert_equal e, Enumeration.default + end + + def test_destroy_with_reassign + Enumeration.find(4).destroy(Enumeration.find(6)) + assert_nil Issue.where(:priority_id => 4).first + assert_equal 6, Enumeration.find(6).objects_count + end + + def test_should_be_customizable + assert Enumeration.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods) + end + + def test_should_belong_to_a_project + association = Enumeration.reflect_on_association(:project) + assert association, "No Project association found" + assert_equal :belongs_to, association.macro + end + + def test_should_act_as_tree + enumeration = Enumeration.find(4) + + assert enumeration.respond_to?(:parent) + assert enumeration.respond_to?(:children) + end + + def test_is_override + # Defaults to off + enumeration = Enumeration.find(4) + assert !enumeration.is_override? + + # Setup as an override + enumeration.parent = Enumeration.find(5) + assert enumeration.is_override? + end + + def test_get_subclasses + classes = Enumeration.get_subclasses + assert_include IssuePriority, classes + assert_include DocumentCategory, classes + assert_include TimeEntryActivity, classes + + classes.each do |klass| + assert_equal Enumeration, klass.superclass + end + end +end diff --git a/test/unit/group_test.rb b/test/unit/group_test.rb new file mode 100644 index 00000000..6d9a9170 --- /dev/null +++ b/test/unit/group_test.rb @@ -0,0 +1,136 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../test_helper', __FILE__) + +class GroupTest < ActiveSupport::TestCase + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, + :projects_trackers, + :roles, + :member_roles, + :members, + :groups_users + + include Redmine::I18n + + def test_create + g = Group.new(:name => 'New group') + assert g.save + g.reload + assert_equal 'New group', g.name + end + + def test_name_should_accept_255_characters + name = 'a' * 255 + g = Group.new(:name => name) + assert g.save + g.reload + assert_equal name, g.name + end + + def test_blank_name_error_message + set_language_if_valid 'en' + g = Group.new + assert !g.save + assert_include "Name can't be blank", g.errors.full_messages + end + + def test_blank_name_error_message_fr + set_language_if_valid 'fr' + str = "Nom doit \xc3\xaatre renseign\xc3\xa9(e)" + str.force_encoding('UTF-8') if str.respond_to?(:force_encoding) + g = Group.new + assert !g.save + assert_include str, g.errors.full_messages + end + + def test_group_roles_should_be_given_to_added_user + group = Group.find(11) + user = User.find(9) + project = Project.first + + Member.create!(:principal => group, :project => project, :role_ids => [1, 2]) + group.users << user + assert user.member_of?(project) + end + + def test_new_roles_should_be_given_to_existing_user + group = Group.find(11) + user = User.find(9) + project = Project.first + + group.users << user + m = Member.create!(:principal => group, :project => project, :role_ids => [1, 2]) + assert user.member_of?(project) + end + + def test_user_roles_should_updated_when_updating_user_ids + group = Group.find(11) + user = User.find(9) + project = Project.first + + Member.create!(:principal => group, :project => project, :role_ids => [1, 2]) + group.user_ids = [user.id] + group.save! + assert User.find(9).member_of?(project) + + group.user_ids = [1] + group.save! + assert !User.find(9).member_of?(project) + end + + def test_user_roles_should_updated_when_updating_group_roles + group = Group.find(11) + user = User.find(9) + project = Project.first + group.users << user + m = Member.create!(:principal => group, :project => project, :role_ids => [1]) + assert_equal [1], user.reload.roles_for_project(project).collect(&:id).sort + + m.role_ids = [1, 2] + assert_equal [1, 2], user.reload.roles_for_project(project).collect(&:id).sort + + m.role_ids = [2] + assert_equal [2], user.reload.roles_for_project(project).collect(&:id).sort + + m.role_ids = [1] + assert_equal [1], user.reload.roles_for_project(project).collect(&:id).sort + end + + def test_user_memberships_should_be_removed_when_removing_group_membership + assert User.find(8).member_of?(Project.find(5)) + Member.find_by_project_id_and_user_id(5, 10).destroy + assert !User.find(8).member_of?(Project.find(5)) + end + + def test_user_roles_should_be_removed_when_removing_user_from_group + assert User.find(8).member_of?(Project.find(5)) + User.find(8).groups = [] + assert !User.find(8).member_of?(Project.find(5)) + end + + def test_destroy_should_unassign_issues + group = Group.first + Issue.update_all(["assigned_to_id = ?", group.id], 'id = 1') + + assert group.destroy + assert group.destroyed? + + assert_equal nil, Issue.find(1).assigned_to_id + end +end diff --git a/test/unit/helpers/activities_helper_test.rb b/test/unit/helpers/activities_helper_test.rb new file mode 100644 index 00000000..e90f25ad --- /dev/null +++ b/test/unit/helpers/activities_helper_test.rb @@ -0,0 +1,101 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class ActivitiesHelperTest < ActionView::TestCase + include ActivitiesHelper + + class MockEvent + attr_reader :event_datetime, :event_group, :name + + def initialize(group=nil) + @@count ||= 0 + @name = "e#{@@count}" + @event_datetime = Time.now + @@count.hours + @event_group = group || self + @@count += 1 + end + + def self.clear + @@count = 0 + end + end + + def setup + MockEvent.clear + end + + def test_sort_activity_events_should_sort_by_datetime + events = [] + events << MockEvent.new + events << MockEvent.new + events << MockEvent.new + + assert_equal [ + ['e2', false], + ['e1', false], + ['e0', false] + ], sort_activity_events(events).map {|event, grouped| [event.name, grouped]} + end + + def test_sort_activity_events_should_group_events + events = [] + events << MockEvent.new + events << MockEvent.new(events[0]) + events << MockEvent.new(events[0]) + + assert_equal [ + ['e2', false], + ['e1', true], + ['e0', true] + ], sort_activity_events(events).map {|event, grouped| [event.name, grouped]} + end + + def test_sort_activity_events_with_group_not_in_set_should_group_events + e = MockEvent.new + events = [] + events << MockEvent.new(e) + events << MockEvent.new(e) + + assert_equal [ + ['e2', false], + ['e1', true] + ], sort_activity_events(events).map {|event, grouped| [event.name, grouped]} + end + + def test_sort_activity_events_should_sort_by_datetime_and_group + events = [] + events << MockEvent.new + events << MockEvent.new + events << MockEvent.new + events << MockEvent.new(events[1]) + events << MockEvent.new(events[2]) + events << MockEvent.new + events << MockEvent.new(events[2]) + + assert_equal [ + ['e6', false], + ['e4', true], + ['e2', true], + ['e5', false], + ['e3', false], + ['e1', true], + ['e0', false] + ], sort_activity_events(events).map {|event, grouped| [event.name, grouped]} + end +end diff --git a/test/unit/helpers/application_helper_test.rb b/test/unit/helpers/application_helper_test.rb new file mode 100644 index 00000000..7a70ddb5 --- /dev/null +++ b/test/unit/helpers/application_helper_test.rb @@ -0,0 +1,1223 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU 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. + +require File.expand_path('../../../test_helper', __FILE__) + +class ApplicationHelperTest < ActionView::TestCase + include ERB::Util + include Rails.application.routes.url_helpers + + fixtures :projects, :roles, :enabled_modules, :users, + :repositories, :changesets, + :trackers, :issue_statuses, :issues, :versions, :documents, + :wikis, :wiki_pages, :wiki_contents, + :boards, :messages, :news, + :attachments, :enumerations + + def setup + super + set_tmp_attachments_directory + end + + context "#link_to_if_authorized" do + context "authorized user" do + should "be tested" + end + + context "unauthorized user" do + should "be tested" + end + + should "allow using the :controller and :action for the target link" do + User.current = User.find_by_login('admin') + + @project = Issue.first.project # Used by helper + response = link_to_if_authorized("By controller/action", + {:controller => 'issues', :action => 'edit', :id => Issue.first.id}) + assert_match /href/, response + end + + end + + def test_auto_links + to_test = { + 'http://foo.bar' => 'http://foo.bar', + 'http://foo.bar/~user' => 'http://foo.bar/~user', + 'http://foo.bar.' => 'http://foo.bar.', + 'https://foo.bar.' => 'https://foo.bar.', + 'This is a link: http://foo.bar.' => 'This is a link: http://foo.bar.', + 'A link (eg. http://foo.bar).' => 'A link (eg. http://foo.bar).', + 'http://foo.bar/foo.bar#foo.bar.' => 'http://foo.bar/foo.bar#foo.bar.', + 'http://www.foo.bar/Test_(foobar)' => 'http://www.foo.bar/Test_(foobar)', + '(see inline link : http://www.foo.bar/Test_(foobar))' => '(see inline link : http://www.foo.bar/Test_(foobar))', + '(see inline link : http://www.foo.bar/Test)' => '(see inline link : http://www.foo.bar/Test)', + '(see inline link : http://www.foo.bar/Test).' => '(see inline link : http://www.foo.bar/Test).', + '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see inline link)', + '(see "inline link":http://www.foo.bar/Test)' => '(see inline link)', + '(see "inline link":http://www.foo.bar/Test).' => '(see inline link).', + 'www.foo.bar' => 'www.foo.bar', + 'http://foo.bar/page?p=1&t=z&s=' => 'http://foo.bar/page?p=1&t=z&s=', + 'http://foo.bar/page#125' => 'http://foo.bar/page#125', + 'http://foo@www.bar.com' => 'http://foo@www.bar.com', + 'http://foo:bar@www.bar.com' => 'http://foo:bar@www.bar.com', + 'ftp://foo.bar' => 'ftp://foo.bar', + 'ftps://foo.bar' => 'ftps://foo.bar', + 'sftp://foo.bar' => 'sftp://foo.bar', + # two exclamation marks + 'http://example.net/path!602815048C7B5C20!302.html' => 'http://example.net/path!602815048C7B5C20!302.html', + # escaping + 'http://foo"bar' => 'http://foo"bar', + # wrap in angle brackets + '' => '<http://foo.bar>', + # invalid urls + 'http://' => 'http://', + 'www.' => 'www.', + 'test-www.bar.com' => 'test-www.bar.com', + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + if 'ruby'.respond_to?(:encoding) + def test_auto_links_with_non_ascii_characters + to_test = { + 'http://foo.bar/тест' => 'http://foo.bar/тест' + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + else + puts 'Skipping test_auto_links_with_non_ascii_characters, unsupported ruby version' + end + + def test_auto_mailto + to_test = { + 'test@foo.bar' => '', + 'test@www.foo.bar' => '', + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + def test_inline_images + to_test = { + '!http://foo.bar/image.jpg!' => '', + 'floating !>http://foo.bar/image.jpg!' => 'floating
    ', + 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class ', + 'with style !{width:100px;height:100px}http://foo.bar/image.jpg!' => 'with style ', + 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title This is a title', + 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title This is a double-quoted "title"', + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + def test_inline_images_inside_tags + raw = <<-RAW +h1. !foo.png! Heading + +Centered image: + +p=. !bar.gif! +RAW + + assert textilizable(raw).include?('') + assert textilizable(raw).include?('') + end + + def test_attached_images + to_test = { + 'Inline image: !logo.gif!' => 'Inline image: This is a logo', + 'Inline image: !logo.GIF!' => 'Inline image: This is a logo', + 'No match: !ogo.gif!' => 'No match: ', + 'No match: !ogo.GIF!' => 'No match: ', + # link image + '!logo.gif!:http://foo.bar/' => 'This is a logo', + } + attachments = Attachment.all + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text, :attachments => attachments) } + end + + def test_attached_images_filename_extension + set_tmp_attachments_directory + a1 = Attachment.new( + :container => Issue.find(1), + :file => mock_file_with_options({:original_filename => "testtest.JPG"}), + :author => User.find(1)) + assert a1.save + assert_equal "testtest.JPG", a1.filename + assert_equal "image/jpeg", a1.content_type + assert a1.image? + + a2 = Attachment.new( + :container => Issue.find(1), + :file => mock_file_with_options({:original_filename => "testtest.jpeg"}), + :author => User.find(1)) + assert a2.save + assert_equal "testtest.jpeg", a2.filename + assert_equal "image/jpeg", a2.content_type + assert a2.image? + + a3 = Attachment.new( + :container => Issue.find(1), + :file => mock_file_with_options({:original_filename => "testtest.JPE"}), + :author => User.find(1)) + assert a3.save + assert_equal "testtest.JPE", a3.filename + assert_equal "image/jpeg", a3.content_type + assert a3.image? + + a4 = Attachment.new( + :container => Issue.find(1), + :file => mock_file_with_options({:original_filename => "Testtest.BMP"}), + :author => User.find(1)) + assert a4.save + assert_equal "Testtest.BMP", a4.filename + assert_equal "image/x-ms-bmp", a4.content_type + assert a4.image? + + to_test = { + 'Inline image: !testtest.jpg!' => + 'Inline image: ', + 'Inline image: !testtest.jpeg!' => + 'Inline image: ', + 'Inline image: !testtest.jpe!' => + 'Inline image: ', + 'Inline image: !testtest.bmp!' => + 'Inline image: ', + } + + attachments = [a1, a2, a3, a4] + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text, :attachments => attachments) } + end + + def test_attached_images_should_read_later + set_fixtures_attachments_directory + a1 = Attachment.find(16) + assert_equal "testfile.png", a1.filename + assert a1.readable? + assert (! a1.visible?(User.anonymous)) + assert a1.visible?(User.find(2)) + a2 = Attachment.find(17) + assert_equal "testfile.PNG", a2.filename + assert a2.readable? + assert (! a2.visible?(User.anonymous)) + assert a2.visible?(User.find(2)) + assert a1.created_on < a2.created_on + + to_test = { + 'Inline image: !testfile.png!' => + 'Inline image: ', + 'Inline image: !Testfile.PNG!' => + 'Inline image: ', + } + attachments = [a1, a2] + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text, :attachments => attachments) } + set_tmp_attachments_directory + end + + def test_textile_external_links + to_test = { + 'This is a "link":http://foo.bar' => 'This is a link', + 'This is an intern "link":/foo/bar' => 'This is an intern link', + '"link (Link title)":http://foo.bar' => 'link', + '"link (Link title with "double-quotes")":http://foo.bar' => 'link', + "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":

    \n\n\n\t

    Another paragraph", + # no multiline link text + "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line
    and another on a second line\":test", + # mailto link + "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "system administrator", + # two exclamation marks + '"a link":http://example.net/path!602815048C7B5C20!302.html' => 'a link', + # escaping + '"test":http://foo"bar' => 'test', + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + if 'ruby'.respond_to?(:encoding) + def test_textile_external_links_with_non_ascii_characters + to_test = { + 'This is a "link":http://foo.bar/тест' => 'This is a link' + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + else + puts 'Skipping test_textile_external_links_with_non_ascii_characters, unsupported ruby version' + end + + def test_redmine_links + issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3}, + :class => Issue.find(3).css_classes, :title => 'Error 281 when updating a recipe (New)') + note_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'}, + :class => Issue.find(3).css_classes, :title => 'Error 281 when updating a recipe (New)') + + revision_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1}, + :class => 'changeset', :title => 'My very first commit do not escaping #<>&') + revision_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2}, + :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3') + + changeset_link2 = link_to('691322a8eb01e11fd7', + {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1}, + :class => 'changeset', :title => 'My very first commit do not escaping #<>&') + + document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1}, + :class => 'document') + + version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2}, + :class => 'version') + + board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'} + + message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4} + + news_url = {:controller => 'news', :action => 'show', :id => 1} + + project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'} + + source_url = '/projects/ecookbook/repository/entry/some/file' + source_url_with_rev = '/projects/ecookbook/repository/revisions/52/entry/some/file' + source_url_with_ext = '/projects/ecookbook/repository/entry/some/file.ext' + source_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/entry/some/file.ext' + source_url_with_branch = '/projects/ecookbook/repository/revisions/branch/entry/some/file' + + export_url = '/projects/ecookbook/repository/raw/some/file' + export_url_with_rev = '/projects/ecookbook/repository/revisions/52/raw/some/file' + export_url_with_ext = '/projects/ecookbook/repository/raw/some/file.ext' + export_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/raw/some/file.ext' + export_url_with_branch = '/projects/ecookbook/repository/revisions/branch/raw/some/file' + + to_test = { + # tickets + '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.", + # ticket notes + '#3-14' => note_link, + '#3#note-14' => note_link, + # should not ignore leading zero + '#03' => '#03', + # changesets + 'r1' => revision_link, + 'r1.' => "#{revision_link}.", + 'r1, r2' => "#{revision_link}, #{revision_link2}", + 'r1,r2' => "#{revision_link},#{revision_link2}", + 'commit:691322a8eb01e11fd7' => changeset_link2, + # documents + 'document#1' => document_link, + 'document:"Test document"' => document_link, + # versions + 'version#2' => version_link, + 'version:1.0' => version_link, + 'version:"1.0"' => version_link, + # source + 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'), + 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'), + 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".", + 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".", + 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".", + 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".", + 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",", + 'source:/some/file@52' => link_to('source:/some/file@52', source_url_with_rev, :class => 'source'), + 'source:/some/file@branch' => link_to('source:/some/file@branch', source_url_with_branch, :class => 'source'), + 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_rev_and_ext, :class => 'source'), + 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url + "#L110", :class => 'source'), + 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext + "#L110", :class => 'source'), + 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url_with_rev + "#L110", :class => 'source'), + # export + 'export:/some/file' => link_to('export:/some/file', export_url, :class => 'source download'), + 'export:/some/file.ext' => link_to('export:/some/file.ext', export_url_with_ext, :class => 'source download'), + 'export:/some/file@52' => link_to('export:/some/file@52', export_url_with_rev, :class => 'source download'), + 'export:/some/file.ext@52' => link_to('export:/some/file.ext@52', export_url_with_rev_and_ext, :class => 'source download'), + 'export:/some/file@branch' => link_to('export:/some/file@branch', export_url_with_branch, :class => 'source download'), + # forum + 'forum#2' => link_to('Discussion', board_url, :class => 'board'), + 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'), + # message + 'message#4' => link_to('Post 2', message_url, :class => 'message'), + 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'), + # news + 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'), + 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'), + # project + 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'), + 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'), + 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'), + # not found + '#0123456789' => '#0123456789', + # invalid expressions + 'source:' => 'source:', + # url hash + "http://foo.bar/FAQ#3" => 'http://foo.bar/FAQ#3', + } + @project = Project.find(1) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text), "#{text} failed" } + end + + def test_redmine_links_with_a_different_project_before_current_project + vp1 = Version.generate!(:project_id => 1, :name => '1.4.4') + vp3 = Version.generate!(:project_id => 3, :name => '1.4.4') + + @project = Project.find(3) + assert_equal %(

    1.4.4 1.4.4

    ), + textilizable("ecookbook:version:1.4.4 version:1.4.4") + end + + def test_escaped_redmine_links_should_not_be_parsed + to_test = [ + '#3.', + '#3-14.', + '#3#-note14.', + 'r1', + 'document#1', + 'document:"Test document"', + 'version#2', + 'version:1.0', + 'version:"1.0"', + 'source:/some/file' + ] + @project = Project.find(1) + to_test.each { |text| assert_equal "

    #{text}

    ", textilizable("!" + text), "#{text} failed" } + end + + def test_cross_project_redmine_links + source_link = link_to('ecookbook:source:/some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, + :class => 'source') + + changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2}, + :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3') + + to_test = { + # documents + 'document:"Test document"' => 'document:"Test document"', + 'ecookbook:document:"Test document"' => 'Test document', + 'invalid:document:"Test document"' => 'invalid:document:"Test document"', + # versions + 'version:"1.0"' => 'version:"1.0"', + 'ecookbook:version:"1.0"' => '1.0', + 'invalid:version:"1.0"' => 'invalid:version:"1.0"', + # changeset + 'r2' => 'r2', + 'ecookbook:r2' => changeset_link, + 'invalid:r2' => 'invalid:r2', + # source + 'source:/some/file' => 'source:/some/file', + 'ecookbook:source:/some/file' => source_link, + 'invalid:source:/some/file' => 'invalid:source:/some/file', + } + @project = Project.find(3) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text), "#{text} failed" } + end + + def test_multiple_repositories_redmine_links + svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn_repo-1', :url => 'file:///foo/hg') + Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123') + hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg') + Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd') + + changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2}, + :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3') + svn_changeset_link = link_to('svn_repo-1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn_repo-1', :rev => 123}, + :class => 'changeset', :title => '') + hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'}, + :class => 'changeset', :title => '') + + source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source') + hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source') + + to_test = { + 'r2' => changeset_link, + 'svn_repo-1|r123' => svn_changeset_link, + 'invalid|r123' => 'invalid|r123', + 'commit:hg1|abcd' => hg_changeset_link, + 'commit:invalid|abcd' => 'commit:invalid|abcd', + # source + 'source:some/file' => source_link, + 'source:hg1|some/file' => hg_source_link, + 'source:invalid|some/file' => 'source:invalid|some/file', + } + + @project = Project.find(1) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text), "#{text} failed" } + end + + def test_cross_project_multiple_repositories_redmine_links + svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg') + Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123') + hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg') + Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd') + + changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2}, + :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3') + svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123}, + :class => 'changeset', :title => '') + hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'}, + :class => 'changeset', :title => '') + + source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source') + hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source') + + to_test = { + 'ecookbook:r2' => changeset_link, + 'ecookbook:svn1|r123' => svn_changeset_link, + 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123', + 'ecookbook:commit:hg1|abcd' => hg_changeset_link, + 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd', + 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd', + # source + 'ecookbook:source:some/file' => source_link, + 'ecookbook:source:hg1|some/file' => hg_source_link, + 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file', + 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file', + } + + @project = Project.find(3) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text), "#{text} failed" } + end + + def test_redmine_links_git_commit + changeset_link = link_to('abcd', + { + :controller => 'repositories', + :action => 'revision', + :id => 'subproject1', + :rev => 'abcd', + }, + :class => 'changeset', :title => 'test commit') + to_test = { + 'commit:abcd' => changeset_link, + } + @project = Project.find(3) + r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git') + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => 'abcd', + :scmid => 'abcd', + :comments => 'test commit') + assert( c.save ) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'. + def test_redmine_links_darcs_commit + changeset_link = link_to('20080308225258-98289-abcd456efg.gz', + { + :controller => 'repositories', + :action => 'revision', + :id => 'subproject1', + :rev => '123', + }, + :class => 'changeset', :title => 'test commit') + to_test = { + 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link, + } + @project = Project.find(3) + r = Repository::Darcs.create!( + :project => @project, :url => '/tmp/test/darcs', + :log_encoding => 'UTF-8') + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => '20080308225258-98289-abcd456efg.gz', + :comments => 'test commit') + assert( c.save ) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + def test_redmine_links_mercurial_commit + changeset_link_rev = link_to('r123', + { + :controller => 'repositories', + :action => 'revision', + :id => 'subproject1', + :rev => '123' , + }, + :class => 'changeset', :title => 'test commit') + changeset_link_commit = link_to('abcd', + { + :controller => 'repositories', + :action => 'revision', + :id => 'subproject1', + :rev => 'abcd' , + }, + :class => 'changeset', :title => 'test commit') + to_test = { + 'r123' => changeset_link_rev, + 'commit:abcd' => changeset_link_commit, + } + @project = Project.find(3) + r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test') + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => 'abcd', + :comments => 'test commit') + assert( c.save ) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + def test_attachment_links + to_test = { + 'attachment:error281.txt' => 'error281.txt' + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" } + end + + def test_attachment_link_should_link_to_latest_attachment + set_tmp_attachments_directory + a1 = Attachment.generate!(:filename => "test.txt", :created_on => 1.hour.ago) + a2 = Attachment.generate!(:filename => "test.txt") + + assert_equal %(

    test.txt

    ), + textilizable('attachment:test.txt', :attachments => [a1, a2]) + end + + def test_wiki_links + to_test = { + '[[CookBook documentation]]' => 'CookBook documentation', + '[[Another page|Page]]' => 'Page', + # title content should be formatted + '[[Another page|With _styled_ *title*]]' => 'With styled title', + '[[Another page|With title containing HTML entities & markups]]' => 'With title containing <strong>HTML entities & markups</strong>', + # link with anchor + '[[CookBook documentation#One-section]]' => 'CookBook documentation', + '[[Another page#anchor|Page]]' => 'Page', + # UTF8 anchor + '[[Another_page#Тест|Тест]]' => %|Тест|, + # page that doesn't exist + '[[Unknown page]]' => 'Unknown page', + '[[Unknown page|404]]' => '404', + # link to another project wiki + '[[onlinestore:]]' => 'onlinestore', + '[[onlinestore:|Wiki]]' => 'Wiki', + '[[onlinestore:Start page]]' => 'Start page', + '[[onlinestore:Start page|Text]]' => 'Text', + '[[onlinestore:Unknown page]]' => 'Unknown page', + # striked through link + '-[[Another page|Page]]-' => 'Page', + '-[[Another page|Page]] link-' => 'Page link', + # escaping + '![[Another page|Page]]' => '[[Another page|Page]]', + # project does not exist + '[[unknowproject:Start]]' => '[[unknowproject:Start]]', + '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]', + } + + @project = Project.find(1) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + def test_wiki_links_within_local_file_generation_context + + to_test = { + # link to a page + '[[CookBook documentation]]' => 'CookBook documentation', + '[[CookBook documentation|documentation]]' => 'documentation', + '[[CookBook documentation#One-section]]' => 'CookBook documentation', + '[[CookBook documentation#One-section|documentation]]' => 'documentation', + # page that doesn't exist + '[[Unknown page]]' => 'Unknown page', + '[[Unknown page|404]]' => '404', + '[[Unknown page#anchor]]' => 'Unknown page', + '[[Unknown page#anchor|404]]' => '404', + } + + @project = Project.find(1) + + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text, :wiki_links => :local) } + end + + def test_wiki_links_within_wiki_page_context + + page = WikiPage.find_by_title('Another_page' ) + + to_test = { + # link to another page + '[[CookBook documentation]]' => 'CookBook documentation', + '[[CookBook documentation|documentation]]' => 'documentation', + '[[CookBook documentation#One-section]]' => 'CookBook documentation', + '[[CookBook documentation#One-section|documentation]]' => 'documentation', + # link to the current page + '[[Another page]]' => 'Another page', + '[[Another page|Page]]' => 'Page', + '[[Another page#anchor]]' => 'Another page', + '[[Another page#anchor|Page]]' => 'Page', + # page that doesn't exist + '[[Unknown page]]' => 'Unknown page', + '[[Unknown page|404]]' => '404', + '[[Unknown page#anchor]]' => 'Unknown page', + '[[Unknown page#anchor|404]]' => '404', + } + + @project = Project.find(1) + + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(WikiContent.new( :text => text, :page => page ), :text) } + end + + def test_wiki_links_anchor_option_should_prepend_page_title_to_href + + to_test = { + # link to a page + '[[CookBook documentation]]' => 'CookBook documentation', + '[[CookBook documentation|documentation]]' => 'documentation', + '[[CookBook documentation#One-section]]' => 'CookBook documentation', + '[[CookBook documentation#One-section|documentation]]' => 'documentation', + # page that doesn't exist + '[[Unknown page]]' => 'Unknown page', + '[[Unknown page|404]]' => '404', + '[[Unknown page#anchor]]' => 'Unknown page', + '[[Unknown page#anchor|404]]' => '404', + } + + @project = Project.find(1) + + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text, :wiki_links => :anchor) } + end + + def test_html_tags + to_test = { + "
    content
    " => "

    <div>content</div>

    ", + "
    content
    " => "

    <div class=\"bold\">content</div>

    ", + "" => "

    <script>some script;</script>

    ", + # do not escape pre/code tags + "
    \nline 1\nline2
    " => "
    \nline 1\nline2
    ", + "
    \nline 1\nline2
    " => "
    \nline 1\nline2
    ", + "
    content
    " => "
    <div>content</div>
    ", + "HTML comment: " => "

    HTML comment: <!-- no comments -->

    ", + "