aboutsummaryrefslogtreecommitdiffstats
path: root/tests/src/test
diff options
context:
space:
mode:
authorSimon Brandhof <simon.brandhof@sonarsource.com>2017-06-23 21:31:56 +0200
committerSimon Brandhof <simon.brandhof@sonarsource.com>2017-06-25 23:42:50 +0200
commit70b6899988da0d2ba0a39b846e4f1bd3fa27304f (patch)
tree1ac093a87e0fba6b07c6feb6aceae89bdd9663cf /tests/src/test
parent5dd574819854e9ce7e2f4e181e78153a7ecbf828 (diff)
downloadsonarqube-70b6899988da0d2ba0a39b846e4f1bd3fa27304f.tar.gz
sonarqube-70b6899988da0d2ba0a39b846e4f1bd3fa27304f.zip
Move integration tests to directory tests/
Diffstat (limited to 'tests/src/test')
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/BackgroundTaskItem.java67
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/BackgroundTasksPage.java47
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/EncryptionPage.java53
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/LoginPage.java66
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/Navigation.java234
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/NotificationsPage.java120
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/ProjectActivityPage.java63
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/ProjectAnalysisItem.java103
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/ProjectDashboardPage.java91
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/ProjectKeyPage.java103
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/ProjectLinkItem.java52
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/ProjectLinksPage.java47
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/ProjectPermissionsPage.java61
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/ProjectQualityGatePage.java45
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/ProjectsManagementPage.java51
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/RuleItem.java41
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/RulesPage.java52
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/SelenideConfig.java70
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/ServerIdPage.java64
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/issues/Issue.java66
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/issues/IssuesPage.java66
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/licenses/LicenseItem.java46
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/licenses/LicensesPage.java52
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/organization/MemberItem.java95
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/organization/MembersPage.java85
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/projects/FacetItem.java49
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/projects/ProjectItem.java37
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/projects/ProjectsPage.java120
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/settings/PropertySetInput.java48
-rw-r--r--tests/src/test/java/org/sonarqube/pageobjects/settings/SettingsPage.java84
-rw-r--r--tests/src/test/java/org/sonarqube/tests/Category1Suite.java133
-rw-r--r--tests/src/test/java/org/sonarqube/tests/Category2Suite.java114
-rw-r--r--tests/src/test/java/org/sonarqube/tests/Category3Suite.java90
-rw-r--r--tests/src/test/java/org/sonarqube/tests/Category4Suite.java124
-rw-r--r--tests/src/test/java/org/sonarqube/tests/Category5Suite.java54
-rw-r--r--tests/src/test/java/org/sonarqube/tests/Category6Suite.java75
-rw-r--r--tests/src/test/java/org/sonarqube/tests/GroupTester.java102
-rw-r--r--tests/src/test/java/org/sonarqube/tests/OrganizationTester.java134
-rw-r--r--tests/src/test/java/org/sonarqube/tests/ProjectTester.java63
-rw-r--r--tests/src/test/java/org/sonarqube/tests/QProfileTester.java118
-rw-r--r--tests/src/test/java/org/sonarqube/tests/Session.java38
-rw-r--r--tests/src/test/java/org/sonarqube/tests/Tester.java201
-rw-r--r--tests/src/test/java/org/sonarqube/tests/UserTester.java101
-rw-r--r--tests/src/test/java/org/sonarqube/tests/analysis/ExtensionLifecycleTest.java51
-rw-r--r--tests/src/test/java/org/sonarqube/tests/analysis/FavoriteTest.java124
-rw-r--r--tests/src/test/java/org/sonarqube/tests/analysis/FileExclusionsTest.java139
-rw-r--r--tests/src/test/java/org/sonarqube/tests/analysis/IssueExclusionsTest.java263
-rw-r--r--tests/src/test/java/org/sonarqube/tests/analysis/IssueJsonReportTest.java313
-rw-r--r--tests/src/test/java/org/sonarqube/tests/analysis/IssuesModeTest.java461
-rw-r--r--tests/src/test/java/org/sonarqube/tests/analysis/LinksTest.java93
-rw-r--r--tests/src/test/java/org/sonarqube/tests/analysis/MultiLanguageTest.java76
-rw-r--r--tests/src/test/java/org/sonarqube/tests/analysis/PermissionTest.java177
-rw-r--r--tests/src/test/java/org/sonarqube/tests/analysis/ProjectBuilderTest.java86
-rw-r--r--tests/src/test/java/org/sonarqube/tests/analysis/ReportDumpTest.java73
-rw-r--r--tests/src/test/java/org/sonarqube/tests/analysis/SSLTest.java197
-rw-r--r--tests/src/test/java/org/sonarqube/tests/analysis/ScannerTest.java395
-rw-r--r--tests/src/test/java/org/sonarqube/tests/analysis/SettingsEncryptionTest.java83
-rw-r--r--tests/src/test/java/org/sonarqube/tests/analysis/TempFolderTest.java98
-rw-r--r--tests/src/test/java/org/sonarqube/tests/authorisation/ExecuteAnalysisPermissionTest.java151
-rw-r--r--tests/src/test/java/org/sonarqube/tests/authorisation/IssuePermissionTest.java276
-rw-r--r--tests/src/test/java/org/sonarqube/tests/authorisation/PermissionSearchTest.java203
-rw-r--r--tests/src/test/java/org/sonarqube/tests/authorisation/PermissionTemplatesPageTest.java45
-rw-r--r--tests/src/test/java/org/sonarqube/tests/authorisation/ProvisioningPermissionTest.java150
-rw-r--r--tests/src/test/java/org/sonarqube/tests/authorisation/QualityProfileAdminPermissionTest.java83
-rw-r--r--tests/src/test/java/org/sonarqube/tests/ce/CeWsTest.java87
-rw-r--r--tests/src/test/java/org/sonarqube/tests/complexity/ComplexityMeasuresTest.java90
-rw-r--r--tests/src/test/java/org/sonarqube/tests/component/ComponentsWsTest.java75
-rw-r--r--tests/src/test/java/org/sonarqube/tests/component/ProjectsWsTest.java125
-rw-r--r--tests/src/test/java/org/sonarqube/tests/customMeasure/CustomMeasuresTest.java117
-rw-r--r--tests/src/test/java/org/sonarqube/tests/dbCleaner/PurgeTest.java377
-rw-r--r--tests/src/test/java/org/sonarqube/tests/duplication/CrossModuleDuplicationsTest.java143
-rw-r--r--tests/src/test/java/org/sonarqube/tests/duplication/CrossProjectDuplicationsOnRemoveFileTest.java91
-rw-r--r--tests/src/test/java/org/sonarqube/tests/duplication/CrossProjectDuplicationsTest.java171
-rw-r--r--tests/src/test/java/org/sonarqube/tests/duplication/DuplicationsTest.java189
-rw-r--r--tests/src/test/java/org/sonarqube/tests/duplication/NewDuplicationsTest.java84
-rw-r--r--tests/src/test/java/org/sonarqube/tests/i18n/I18nTest.java61
-rw-r--r--tests/src/test/java/org/sonarqube/tests/issue/AbstractIssueTest.java68
-rw-r--r--tests/src/test/java/org/sonarqube/tests/issue/AutoAssignTest.java171
-rw-r--r--tests/src/test/java/org/sonarqube/tests/issue/CommonRulesTest.java95
-rw-r--r--tests/src/test/java/org/sonarqube/tests/issue/CustomRulesTest.java68
-rw-r--r--tests/src/test/java/org/sonarqube/tests/issue/IssueActionTest.java200
-rw-r--r--tests/src/test/java/org/sonarqube/tests/issue/IssueBulkChangeTest.java272
-rw-r--r--tests/src/test/java/org/sonarqube/tests/issue/IssueChangelogTest.java124
-rw-r--r--tests/src/test/java/org/sonarqube/tests/issue/IssueCreationDateTest.java378
-rw-r--r--tests/src/test/java/org/sonarqube/tests/issue/IssueCreationTest.java78
-rw-r--r--tests/src/test/java/org/sonarqube/tests/issue/IssueFilterExtensionTest.java119
-rw-r--r--tests/src/test/java/org/sonarqube/tests/issue/IssueFilterOnCommonRulesTest.java126
-rw-r--r--tests/src/test/java/org/sonarqube/tests/issue/IssueFilterTest.java115
-rw-r--r--tests/src/test/java/org/sonarqube/tests/issue/IssueMeasureTest.java130
-rw-r--r--tests/src/test/java/org/sonarqube/tests/issue/IssueNotificationsTest.java236
-rw-r--r--tests/src/test/java/org/sonarqube/tests/issue/IssuePurgeTest.java187
-rw-r--r--tests/src/test/java/org/sonarqube/tests/issue/IssueSearchTest.java277
-rw-r--r--tests/src/test/java/org/sonarqube/tests/issue/IssueTagsTest.java142
-rw-r--r--tests/src/test/java/org/sonarqube/tests/issue/IssueTrackingTest.java220
-rw-r--r--tests/src/test/java/org/sonarqube/tests/issue/IssueWorkflowTest.java314
-rw-r--r--tests/src/test/java/org/sonarqube/tests/issue/IssuesPageTest.java80
-rw-r--r--tests/src/test/java/org/sonarqube/tests/issue/NewIssuesMeasureTest.java144
-rw-r--r--tests/src/test/java/org/sonarqube/tests/issue/OrganizationIssueAssignTest.java219
-rw-r--r--tests/src/test/java/org/sonarqube/tests/issueFilter/ToDoTest.java23
-rw-r--r--tests/src/test/java/org/sonarqube/tests/lite/LiteSuite.java31
-rw-r--r--tests/src/test/java/org/sonarqube/tests/lite/LiteTest.java108
-rw-r--r--tests/src/test/java/org/sonarqube/tests/measure/DecimalScaleMetricTest.java53
-rw-r--r--tests/src/test/java/org/sonarqube/tests/measure/DifferentialPeriodsTest.java177
-rw-r--r--tests/src/test/java/org/sonarqube/tests/measure/MeasuresWsTest.java174
-rw-r--r--tests/src/test/java/org/sonarqube/tests/measure/ProjectDashboardTest.java140
-rw-r--r--tests/src/test/java/org/sonarqube/tests/measure/ProjectMeasuresPageTest.java80
-rw-r--r--tests/src/test/java/org/sonarqube/tests/measure/SincePreviousVersionHistoryTest.java133
-rw-r--r--tests/src/test/java/org/sonarqube/tests/measure/SinceXDaysHistoryTest.java119
-rw-r--r--tests/src/test/java/org/sonarqube/tests/measure/TimeMachineTest.java163
-rw-r--r--tests/src/test/java/org/sonarqube/tests/organization/BillingTest.java238
-rw-r--r--tests/src/test/java/org/sonarqube/tests/organization/OrganizationMembershipTest.java133
-rw-r--r--tests/src/test/java/org/sonarqube/tests/organization/OrganizationMembershipUiTest.java179
-rw-r--r--tests/src/test/java/org/sonarqube/tests/organization/OrganizationTest.java370
-rw-r--r--tests/src/test/java/org/sonarqube/tests/organization/PersonalOrganizationTest.java72
-rw-r--r--tests/src/test/java/org/sonarqube/tests/organization/RootUserOnOrganizationTest.java91
-rw-r--r--tests/src/test/java/org/sonarqube/tests/organization/RootUserTest.java47
-rw-r--r--tests/src/test/java/org/sonarqube/tests/performance/AbstractPerfTest.java104
-rw-r--r--tests/src/test/java/org/sonarqube/tests/performance/MavenLogs.java65
-rw-r--r--tests/src/test/java/org/sonarqube/tests/performance/MavenLogsTest.java42
-rw-r--r--tests/src/test/java/org/sonarqube/tests/performance/PerfRule.java123
-rw-r--r--tests/src/test/java/org/sonarqube/tests/performance/ServerLogs.java93
-rw-r--r--tests/src/test/java/org/sonarqube/tests/performance/ServerLogsTest.java36
-rw-r--r--tests/src/test/java/org/sonarqube/tests/performance/scanner/BootstrappingTest.java150
-rw-r--r--tests/src/test/java/org/sonarqube/tests/performance/scanner/DuplicationTest.java96
-rw-r--r--tests/src/test/java/org/sonarqube/tests/performance/scanner/FileSystemTest.java106
-rw-r--r--tests/src/test/java/org/sonarqube/tests/performance/scanner/HighlightingTest.java107
-rw-r--r--tests/src/test/java/org/sonarqube/tests/performance/scanner/IssuesModeTest.java73
-rw-r--r--tests/src/test/java/org/sonarqube/tests/performance/scanner/MemoryTest.java120
-rw-r--r--tests/src/test/java/org/sonarqube/tests/performance/scanner/ScannerPerformanceSuite.java59
-rw-r--r--tests/src/test/java/org/sonarqube/tests/performance/server/ComputeEnginePerfTest.java125
-rw-r--r--tests/src/test/java/org/sonarqube/tests/performance/server/ServerPerfTest.java111
-rw-r--r--tests/src/test/java/org/sonarqube/tests/performance/server/ServerPerformanceSuite.java31
-rw-r--r--tests/src/test/java/org/sonarqube/tests/plugins/PluginsSuite.java31
-rw-r--r--tests/src/test/java/org/sonarqube/tests/plugins/PluginsTest.java202
-rw-r--r--tests/src/test/java/org/sonarqube/tests/plugins/Project.java50
-rw-r--r--tests/src/test/java/org/sonarqube/tests/plugins/VersionPluginTest.java56
-rw-r--r--tests/src/test/java/org/sonarqube/tests/plugins/checks/AbapCheck.java34
-rw-r--r--tests/src/test/java/org/sonarqube/tests/plugins/checks/CCheck.java34
-rw-r--r--tests/src/test/java/org/sonarqube/tests/plugins/checks/Check.java24
-rw-r--r--tests/src/test/java/org/sonarqube/tests/plugins/checks/CobolCheck.java34
-rw-r--r--tests/src/test/java/org/sonarqube/tests/plugins/checks/CppCheck.java34
-rw-r--r--tests/src/test/java/org/sonarqube/tests/plugins/checks/CssCheck.java33
-rw-r--r--tests/src/test/java/org/sonarqube/tests/plugins/checks/FlexCheck.java34
-rw-r--r--tests/src/test/java/org/sonarqube/tests/plugins/checks/GroovyCheck.java34
-rw-r--r--tests/src/test/java/org/sonarqube/tests/plugins/checks/JavaCheck.java34
-rw-r--r--tests/src/test/java/org/sonarqube/tests/plugins/checks/JavascriptCheck.java35
-rw-r--r--tests/src/test/java/org/sonarqube/tests/plugins/checks/PhpCheck.java34
-rw-r--r--tests/src/test/java/org/sonarqube/tests/plugins/checks/PliCheck.java34
-rw-r--r--tests/src/test/java/org/sonarqube/tests/plugins/checks/PythonCheck.java46
-rw-r--r--tests/src/test/java/org/sonarqube/tests/plugins/checks/RpgCheck.java34
-rw-r--r--tests/src/test/java/org/sonarqube/tests/plugins/checks/SwiftCheck.java33
-rw-r--r--tests/src/test/java/org/sonarqube/tests/plugins/checks/Validation.java163
-rw-r--r--tests/src/test/java/org/sonarqube/tests/plugins/checks/VbCheck.java34
-rw-r--r--tests/src/test/java/org/sonarqube/tests/plugins/checks/WebCheck.java33
-rw-r--r--tests/src/test/java/org/sonarqube/tests/previewAnalysis/ToDoTest.java23
-rw-r--r--tests/src/test/java/org/sonarqube/tests/projectAdministration/BackgroundTasksTest.java115
-rw-r--r--tests/src/test/java/org/sonarqube/tests/projectAdministration/BulkDeletionTest.java76
-rw-r--r--tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectAdministrationTest.java220
-rw-r--r--tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectKeyPageTest.java182
-rw-r--r--tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectLinksPageTest.java158
-rw-r--r--tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectPermissionsTest.java80
-rw-r--r--tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectQualityGatePageTest.java147
-rw-r--r--tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectVisibilityTest.java98
-rw-r--r--tests/src/test/java/org/sonarqube/tests/projectEvent/EventTest.java97
-rw-r--r--tests/src/test/java/org/sonarqube/tests/projectEvent/ProjectActivityPageTest.java99
-rw-r--r--tests/src/test/java/org/sonarqube/tests/projectSearch/LeakProjectsPageTest.java119
-rw-r--r--tests/src/test/java/org/sonarqube/tests/projectSearch/ProjectsPageTest.java203
-rw-r--r--tests/src/test/java/org/sonarqube/tests/projectSearch/SearchProjectsTest.java306
-rw-r--r--tests/src/test/java/org/sonarqube/tests/qualityGate/QualityGateNotificationTest.java160
-rw-r--r--tests/src/test/java/org/sonarqube/tests/qualityGate/QualityGateOnRatingMeasuresTest.java124
-rw-r--r--tests/src/test/java/org/sonarqube/tests/qualityGate/QualityGateTest.java405
-rw-r--r--tests/src/test/java/org/sonarqube/tests/qualityGate/QualityGateUiTest.java125
-rw-r--r--tests/src/test/java/org/sonarqube/tests/qualityModel/DebtConfigurationRule.java114
-rw-r--r--tests/src/test/java/org/sonarqube/tests/qualityModel/MaintainabilityMeasureTest.java111
-rw-r--r--tests/src/test/java/org/sonarqube/tests/qualityModel/MaintainabilityRatingMeasureTest.java164
-rw-r--r--tests/src/test/java/org/sonarqube/tests/qualityModel/NewDebtRatioMeasureTest.java142
-rw-r--r--tests/src/test/java/org/sonarqube/tests/qualityModel/ReliabilityMeasureTest.java90
-rw-r--r--tests/src/test/java/org/sonarqube/tests/qualityModel/SecurityMeasureTest.java89
-rw-r--r--tests/src/test/java/org/sonarqube/tests/qualityModel/TechnicalDebtInIssueChangelogTest.java84
-rw-r--r--tests/src/test/java/org/sonarqube/tests/qualityModel/TechnicalDebtMeasureVariationTest.java154
-rw-r--r--tests/src/test/java/org/sonarqube/tests/qualityModel/TechnicalDebtTest.java70
-rw-r--r--tests/src/test/java/org/sonarqube/tests/qualityProfile/BuiltInQualityProfilesTest.java157
-rw-r--r--tests/src/test/java/org/sonarqube/tests/qualityProfile/CustomQualityProfilesTest.java328
-rw-r--r--tests/src/test/java/org/sonarqube/tests/qualityProfile/OrganizationQualityProfilesUiTest.java181
-rw-r--r--tests/src/test/java/org/sonarqube/tests/qualityProfile/QualityProfilesUiTest.java196
-rw-r--r--tests/src/test/java/org/sonarqube/tests/rule/RuleTagsTest.java110
-rw-r--r--tests/src/test/java/org/sonarqube/tests/scm/ScmTest.java139
-rw-r--r--tests/src/test/java/org/sonarqube/tests/serverSystem/ClusterTest.java215
-rw-r--r--tests/src/test/java/org/sonarqube/tests/serverSystem/HttpHeadersTest.java159
-rw-r--r--tests/src/test/java/org/sonarqube/tests/serverSystem/LogsTest.java148
-rw-r--r--tests/src/test/java/org/sonarqube/tests/serverSystem/PingTest.java42
-rw-r--r--tests/src/test/java/org/sonarqube/tests/serverSystem/RestartTest.java135
-rw-r--r--tests/src/test/java/org/sonarqube/tests/serverSystem/ServerSystemRestartingOrchestrator.java111
-rw-r--r--tests/src/test/java/org/sonarqube/tests/serverSystem/ServerSystemTest.java223
-rw-r--r--tests/src/test/java/org/sonarqube/tests/settings/DeprecatedPropertiesWsTest.java417
-rw-r--r--tests/src/test/java/org/sonarqube/tests/settings/EmailsTest.java140
-rw-r--r--tests/src/test/java/org/sonarqube/tests/settings/LicensesPageTest.java93
-rw-r--r--tests/src/test/java/org/sonarqube/tests/settings/PropertySetsTest.java153
-rw-r--r--tests/src/test/java/org/sonarqube/tests/settings/SettingsTest.java208
-rw-r--r--tests/src/test/java/org/sonarqube/tests/settings/SettingsTestRestartingOrchestrator.java115
-rw-r--r--tests/src/test/java/org/sonarqube/tests/sourceCode/EncodingTest.java45
-rw-r--r--tests/src/test/java/org/sonarqube/tests/sourceCode/HighlightingTest.java62
-rw-r--r--tests/src/test/java/org/sonarqube/tests/sourceCode/ProjectCodeTest.java64
-rw-r--r--tests/src/test/java/org/sonarqube/tests/test/CoverageTest.java221
-rw-r--r--tests/src/test/java/org/sonarqube/tests/test/CoverageTrackingTest.java64
-rw-r--r--tests/src/test/java/org/sonarqube/tests/test/NewCoverageTest.java69
-rw-r--r--tests/src/test/java/org/sonarqube/tests/test/TestExecutionTest.java76
-rw-r--r--tests/src/test/java/org/sonarqube/tests/ui/OrganizationUiExtensionsTest.java72
-rw-r--r--tests/src/test/java/org/sonarqube/tests/ui/SourceViewerTest.java57
-rw-r--r--tests/src/test/java/org/sonarqube/tests/ui/UiExtensionsTest.java115
-rw-r--r--tests/src/test/java/org/sonarqube/tests/ui/UiTest.java155
-rw-r--r--tests/src/test/java/org/sonarqube/tests/updateCenter/UpdateCenterTest.java55
-rw-r--r--tests/src/test/java/org/sonarqube/tests/upgrade/MssqlConfig.java47
-rw-r--r--tests/src/test/java/org/sonarqube/tests/upgrade/SelenideConfig.java49
-rw-r--r--tests/src/test/java/org/sonarqube/tests/upgrade/ServerMigrationCall.java64
-rw-r--r--tests/src/test/java/org/sonarqube/tests/upgrade/ServerMigrationResponse.java52
-rw-r--r--tests/src/test/java/org/sonarqube/tests/upgrade/ServerStatusCall.java46
-rw-r--r--tests/src/test/java/org/sonarqube/tests/upgrade/ServerStatusResponse.java48
-rw-r--r--tests/src/test/java/org/sonarqube/tests/upgrade/UpgradeSuite.java30
-rw-r--r--tests/src/test/java/org/sonarqube/tests/upgrade/UpgradeTest.java281
-rw-r--r--tests/src/test/java/org/sonarqube/tests/upgrade/WsCallAndWait.java102
-rw-r--r--tests/src/test/java/org/sonarqube/tests/user/BaseIdentityProviderTest.java324
-rw-r--r--tests/src/test/java/org/sonarqube/tests/user/FavoritesWsTest.java72
-rw-r--r--tests/src/test/java/org/sonarqube/tests/user/ForceAuthenticationTest.java132
-rw-r--r--tests/src/test/java/org/sonarqube/tests/user/LocalAuthenticationTest.java234
-rw-r--r--tests/src/test/java/org/sonarqube/tests/user/MyAccountPageTest.java131
-rw-r--r--tests/src/test/java/org/sonarqube/tests/user/OAuth2IdentityProviderTest.java220
-rw-r--r--tests/src/test/java/org/sonarqube/tests/user/OnboardingTest.java161
-rw-r--r--tests/src/test/java/org/sonarqube/tests/user/OrganizationIdentityProviderTest.java119
-rw-r--r--tests/src/test/java/org/sonarqube/tests/user/RealmAuthenticationTest.java389
-rw-r--r--tests/src/test/java/org/sonarqube/tests/user/SsoAuthenticationTest.java162
-rw-r--r--tests/src/test/java/org/sonarqube/tests/user/UsersPageTest.java84
-rw-r--r--tests/src/test/java/org/sonarqube/tests/webhook/ExternalServer.java97
-rw-r--r--tests/src/test/java/org/sonarqube/tests/webhook/PayloadRequest.java51
-rw-r--r--tests/src/test/java/org/sonarqube/tests/webhook/WebhooksTest.java290
-rw-r--r--tests/src/test/java/org/sonarqube/tests/ws/RoutesTest.java48
-rw-r--r--tests/src/test/java/org/sonarqube/tests/ws/WsLocalCallTest.java73
-rw-r--r--tests/src/test/java/org/sonarqube/tests/ws/WsTest.java62
-rw-r--r--tests/src/test/java/util/ItUtils.java534
-rw-r--r--tests/src/test/java/util/LoadedProfiles.java87
-rw-r--r--tests/src/test/java/util/LoadedProjects.java82
-rw-r--r--tests/src/test/java/util/Profile.java49
-rw-r--r--tests/src/test/java/util/ProjectAnalysis.java51
-rw-r--r--tests/src/test/java/util/ProjectAnalysisRule.java220
-rw-r--r--tests/src/test/java/util/ProjectState.java71
-rw-r--r--tests/src/test/java/util/issue/IssueRule.java79
-rw-r--r--tests/src/test/java/util/selenium/ByCssSelectorOrByNameOrById.java107
-rw-r--r--tests/src/test/java/util/selenium/Consumer.java24
-rw-r--r--tests/src/test/java/util/selenium/ElementFilter.java68
-rw-r--r--tests/src/test/java/util/selenium/Failure.java49
-rw-r--r--tests/src/test/java/util/selenium/LazyDomElement.java174
-rw-r--r--tests/src/test/java/util/selenium/LazyShould.java190
-rw-r--r--tests/src/test/java/util/selenium/Optional.java57
-rw-r--r--tests/src/test/java/util/selenium/Retry.java152
-rw-r--r--tests/src/test/java/util/selenium/Selenese.java91
-rw-r--r--tests/src/test/java/util/selenium/SeleneseRunner.java440
-rw-r--r--tests/src/test/java/util/selenium/Text.java58
-rw-r--r--tests/src/test/java/util/selenium/WebElementHelper.java41
-rw-r--r--tests/src/test/java/util/user/GroupManagement.java48
-rw-r--r--tests/src/test/java/util/user/Groups.java58
-rw-r--r--tests/src/test/java/util/user/UserRule.java395
-rw-r--r--tests/src/test/java/util/user/Users.java108
-rw-r--r--tests/src/test/resources/analysis/BatchTest/one-issue-per-line.xml12
-rw-r--r--tests/src/test/resources/analysis/IssueJsonReportTest/multiline.xml12
-rw-r--r--tests/src/test/resources/analysis/IssueJsonReportTest/no-server-analysis.json212
-rw-r--r--tests/src/test/resources/analysis/IssueJsonReportTest/one-issue-per-line.xml12
-rw-r--r--tests/src/test/resources/analysis/IssueJsonReportTest/report-on-root-module.json880
-rw-r--r--tests/src/test/resources/analysis/IssueJsonReportTest/report-on-single-module-branch.json255
-rw-r--r--tests/src/test/resources/analysis/IssueJsonReportTest/report-on-single-module.json255
-rw-r--r--tests/src/test/resources/analysis/IssueJsonReportTest/report-on-sub-module.json238
-rw-r--r--tests/src/test/resources/analysis/IssuesModeTest/empty.xml7
-rw-r--r--tests/src/test/resources/analysis/IssuesModeTest/one-issue-per-line-empty.xml7
-rw-r--r--tests/src/test/resources/analysis/IssuesModeTest/one-issue-per-line.xml12
-rw-r--r--tests/src/test/resources/analysis/IssuesModeTest/with-many-rules.xml32
-rw-r--r--tests/src/test/resources/analysis/MultiLanguageTest/one-issue-per-line-xoo2.xml12
-rw-r--r--tests/src/test/resources/analysis/MultiLanguageTest/one-issue-per-line.xml12
-rw-r--r--tests/src/test/resources/analysis/SSLTest/README184
-rw-r--r--tests/src/test/resources/analysis/SSLTest/clientkeystore.jksbin0 -> 2260 bytes
-rw-r--r--tests/src/test/resources/analysis/SSLTest/clienttruststore.jksbin0 -> 971 bytes
-rw-r--r--tests/src/test/resources/analysis/SSLTest/openssl.cnf38
-rw-r--r--tests/src/test/resources/analysis/SSLTest/serverkeystore.jksbin0 -> 2258 bytes
-rw-r--r--tests/src/test/resources/analysis/SSLTest/servertruststore.jksbin0 -> 1109 bytes
-rw-r--r--tests/src/test/resources/analysis/SettingsEncryptionTest/sonar-secret.txt1
-rw-r--r--tests/src/test/resources/analysis/TempFolderTest/one-issue-per-line.xml12
-rw-r--r--tests/src/test/resources/authorisation/PermissionTemplatesPageTest/should_create.html99
-rw-r--r--tests/src/test/resources/authorisation/PermissionTemplatesPageTest/should_display_page.html89
-rw-r--r--tests/src/test/resources/authorisation/PermissionTemplatesPageTest/should_manage_project_creators.html109
-rw-r--r--tests/src/test/resources/authorisation/ProvisioningPermissionTest/should-be-able-to-provision-project.html59
-rw-r--r--tests/src/test/resources/authorisation/ProvisioningPermissionTest/should-not-be-able-to-provision-project.html59
-rw-r--r--tests/src/test/resources/authorisation/QualityProfileAdminPermissionTest/normal-user.html69
-rw-r--r--tests/src/test/resources/authorisation/QualityProfileAdminPermissionTest/profile-admin.html84
-rw-r--r--tests/src/test/resources/authorisation/one-issue-per-line-profile.xml12
-rw-r--r--tests/src/test/resources/dbCleaner/one-issue-per-line-profile.xml12
-rw-r--r--tests/src/test/resources/duplication/CrossProjectDuplicationsOnRemoveFileTest/duplications-with-deleted-project.html49
-rw-r--r--tests/src/test/resources/duplication/CrossProjectDuplicationsOnRemoveFileTest/duplications_on_removed_file-expected.json25
-rw-r--r--tests/src/test/resources/duplication/CrossProjectDuplicationsTest/cross-project-duplications-viewer.html59
-rw-r--r--tests/src/test/resources/duplication/CrossProjectDuplicationsTest/duplications_show-expected.json32
-rw-r--r--tests/src/test/resources/duplication/CrossProjectDuplicationsTest/sources_lines_duplication-expected.json184
-rw-r--r--tests/src/test/resources/duplication/DuplicationsTest/duplications_show-expected.json26
-rw-r--r--tests/src/test/resources/duplication/DuplicationsTest/sources_lines_duplication-expected.json359
-rw-r--r--tests/src/test/resources/duplication/xoo-duplication-profile.xml12
-rw-r--r--tests/src/test/resources/exclusions/IssueExclusionsTest/with-many-rules.xml32
-rw-r--r--tests/src/test/resources/i18n/default-locale-is-english.html34
-rw-r--r--tests/src/test/resources/i18n/french-locale.html34
-rw-r--r--tests/src/test/resources/i18n/french-pack.html29
-rw-r--r--tests/src/test/resources/i18n/locale-with-france-country.html34
-rw-r--r--tests/src/test/resources/i18n/locale-with-swiss-country.html34
-rw-r--r--tests/src/test/resources/issue/CommonRulesTest/xoo-common-rules-profile.xml55
-rw-r--r--tests/src/test/resources/issue/CustomRulesTest/custom.xml12
-rw-r--r--tests/src/test/resources/issue/IssueActionTest/xoo-one-issue-per-line-profile.xml12
-rw-r--r--tests/src/test/resources/issue/IssueBulkChangeTest/one-issue-per-line-profile.xml12
-rw-r--r--tests/src/test/resources/issue/IssueChangelogTest/one-issue-per-line-profile.xml12
-rw-r--r--tests/src/test/resources/issue/IssueCreationDateTest/no-rules.xml6
-rw-r--r--tests/src/test/resources/issue/IssueCreationDateTest/one-rule.xml11
-rw-r--r--tests/src/test/resources/issue/IssueCreationTest/override-profile-severity.xml11
-rw-r--r--tests/src/test/resources/issue/IssueCreationTest/with-custom-message.xml11
-rw-r--r--tests/src/test/resources/issue/IssueFilterExtensionTest/xoo-with-many-rules.xml32
-rw-r--r--tests/src/test/resources/issue/IssueFilterOnCommonRulesTest/xoo-common-rules-profile.xml55
-rw-r--r--tests/src/test/resources/issue/IssueFilterTest/with-many-rules.xml43
-rw-r--r--tests/src/test/resources/issue/IssuePurgeTest/with-many-rules.xml32
-rw-r--r--tests/src/test/resources/issue/IssueTrackingTest/one-issue-per-module-profile.xml12
-rw-r--r--tests/src/test/resources/issue/IssueWorkflowTest/xoo-one-issue-per-line-profile.xml12
-rw-r--r--tests/src/test/resources/issue/NewIssuesMeasureTest/profile1.xml27
-rw-r--r--tests/src/test/resources/issue/NewIssuesMeasureTest/profile2.xml32
-rw-r--r--tests/src/test/resources/issue/issue-on-tag-foobar.xml18
-rw-r--r--tests/src/test/resources/issue/one-issue-per-file-profile.xml12
-rw-r--r--tests/src/test/resources/issue/one-issue-per-line-profile.xml12
-rw-r--r--tests/src/test/resources/issue/with-many-rules.xml42
-rw-r--r--tests/src/test/resources/logback-test.xml19
-rw-r--r--tests/src/test/resources/measure/ProjectDashboardTest/test_project_overview_after_first_analysis.html55
-rw-r--r--tests/src/test/resources/measure/ProjectMeasuresPageTest/should_display_measures_page.html49
-rw-r--r--tests/src/test/resources/measure/ProjectMeasuresPageTest/should_drilldown_on_list_view.html69
-rw-r--r--tests/src/test/resources/measure/ProjectMeasuresPageTest/should_drilldown_on_tree_view.html84
-rw-r--r--tests/src/test/resources/measure/one-issue-per-file.xml12
-rw-r--r--tests/src/test/resources/measure/one-issue-per-line-profile.xml12
-rw-r--r--tests/src/test/resources/measure/one-issue-per-line.xml12
-rw-r--r--tests/src/test/resources/one-xoo-issue-per-line.xml12
-rw-r--r--tests/src/test/resources/organization/IssueAssignTest/one-issue-per-file-profile.xml12
-rw-r--r--tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_compare.html60
-rw-r--r--tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_copy.html105
-rw-r--r--tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_create.html105
-rw-r--r--tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_delete.html110
-rw-r--r--tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_display_changelog.html55
-rw-r--r--tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_display_list.html69
-rw-r--r--tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_display_profile_exporters.html50
-rw-r--r--tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_display_profile_inheritance.html60
-rw-r--r--tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_display_profile_projects.html50
-rw-r--r--tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_display_profile_rules.html45
-rw-r--r--tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_filter_by_language.html89
-rw-r--r--tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_open_from_list.html54
-rw-r--r--tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_rename.html105
-rw-r--r--tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_restore.html75
-rw-r--r--tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_set_default.html95
-rw-r--r--tests/src/test/resources/projectAdministration/BackgroundTasksTest/should_not_display_failing_and_search_and_filter_elements_on_project_level_page.html80
-rw-r--r--tests/src/test/resources/projectAdministration/BulkDeletionTest/bulk-delete-filter-projects.html79
-rw-r--r--tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-administration/multimodule-project-delete-version.html75
-rw-r--r--tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-administration/multimodule-project-modify-version.html89
-rw-r--r--tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-deletion/project-deletion.html89
-rw-r--r--tests/src/test/resources/projectAdministration/ProjectPermissionsTest/test_project_permissions_page_shows_only_single_project.html75
-rw-r--r--tests/src/test/resources/projectComparison/ProjectComparisonTest/should-add-metrics.html39
-rw-r--r--tests/src/test/resources/projectComparison/ProjectComparisonTest/should-add-projects.html44
-rw-r--r--tests/src/test/resources/projectComparison/ProjectComparisonTest/should-display-basic-set-of-metrics.html24
-rw-r--r--tests/src/test/resources/projectComparison/ProjectComparisonTest/should-move-and-remove-metrics.html69
-rw-r--r--tests/src/test/resources/projectComparison/ProjectComparisonTest/should-move-and-remove-projects.html54
-rw-r--r--tests/src/test/resources/projectComparison/ProjectComparisonTest/should-not-add-differential-metrics.html44
-rw-r--r--tests/src/test/resources/projectSearch/SearchProjectsTest/with-many-rules.xml43
-rw-r--r--tests/src/test/resources/qualityGate/QualityGateOnRatingMeasuresTest/with-many-rules.xml42
-rw-r--r--tests/src/test/resources/qualityGate/QualityGateUiTest/should-display-alerts-correctly-history-page.html94
-rw-r--r--tests/src/test/resources/qualityGate/QualityGateUiTest/should_display_quality_gates_page.html59
-rw-r--r--tests/src/test/resources/qualityGate/notifications/activate_notification_channels.html64
-rw-r--r--tests/src/test/resources/qualityGate/notifications/email_configuration.html69
-rw-r--r--tests/src/test/resources/qualityModel/has-hello-tag.xml18
-rw-r--r--tests/src/test/resources/qualityModel/one-day-debt-per-file.xml12
-rw-r--r--tests/src/test/resources/qualityModel/one-issue-per-file.xml12
-rw-r--r--tests/src/test/resources/qualityModel/one-issue-per-line.xml12
-rw-r--r--tests/src/test/resources/qualityModel/with-many-rules.xml42
-rw-r--r--tests/src/test/resources/qualityModel/without-type-bug.xml26
-rw-r--r--tests/src/test/resources/qualityModel/without-type-code-smells.xml16
-rw-r--r--tests/src/test/resources/qualityModel/without-type-vulnerability.xml26
-rw-r--r--tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_compare.html60
-rw-r--r--tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_copy.html105
-rw-r--r--tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_create.html100
-rw-r--r--tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_delete.html105
-rw-r--r--tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_display_changelog.html55
-rw-r--r--tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_display_list.html69
-rw-r--r--tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_display_profile_exporters.html50
-rw-r--r--tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_display_profile_inheritance.html60
-rw-r--r--tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_display_profile_projects.html50
-rw-r--r--tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_display_profile_rules.html45
-rw-r--r--tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_filter_by_language.html89
-rw-r--r--tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_open_from_list.html54
-rw-r--r--tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_rename.html105
-rw-r--r--tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_restore.html70
-rw-r--r--tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_set_default.html90
-rw-r--r--tests/src/test/resources/serverSystem/HttpsTest/keystore.jksbin0 -> 2236 bytes
-rw-r--r--tests/src/test/resources/serverSystem/HttpsTest/keystore.jks.txt3
-rw-r--r--tests/src/test/resources/serverSystem/ServerSystemTest/derby-warning.html35
-rw-r--r--tests/src/test/resources/serverSystem/ServerSystemTest/hide-jdbc-settings.html50
-rw-r--r--tests/src/test/resources/serverSystem/ServerSystemTest/incompatible-plugin-1.0.jarbin0 -> 2188 bytes
-rw-r--r--tests/src/test/resources/serverSystem/ServerSystemTest/system_info.html84
-rw-r--r--tests/src/test/resources/serverSystem/ServerSystemTest/url_ending_by_jsp.html42
-rw-r--r--tests/src/test/resources/settings/SettingsTest/sonar-secret.txt1
-rw-r--r--tests/src/test/resources/sourceCode/EncodingTest/japanese_sources.html39
-rw-r--r--tests/src/test/resources/sourceCode/HighlightingTest/symbol-usages-highlighting.html49
-rw-r--r--tests/src/test/resources/sourceCode/HighlightingTest/syntax-highlighting-v1.html55
-rw-r--r--tests/src/test/resources/sourceCode/HighlightingTest/syntax-highlighting-v2.html54
-rw-r--r--tests/src/test/resources/sourceCode/HighlightingTest/syntax-highlighting.html54
-rw-r--r--tests/src/test/resources/sourceCode/ProjectCodeTest/code_page_should_expand_root_dir.html30
-rw-r--r--tests/src/test/resources/sourceCode/ProjectCodeTest/permalink.html35
-rw-r--r--tests/src/test/resources/sourceCode/ProjectCodeTest/search.html60
-rw-r--r--tests/src/test/resources/sourceCode/ProjectCodeTest/test_project_code_page.html55
-rw-r--r--tests/src/test/resources/test/CoverageTest/it_coverage-expected.json84
-rw-r--r--tests/src/test/resources/test/CoverageTest/unit_test_coverage-expected.json84
-rw-r--r--tests/src/test/resources/test/CoverageTest/unit_test_coverage_no_condition-expected.json80
-rw-r--r--tests/src/test/resources/test/CoverageTest/ut_and_it_coverage-expected.json85
-rw-r--r--tests/src/test/resources/test/CoverageTrackingTest/covered_files-expected.json14
-rw-r--r--tests/src/test/resources/test/CoverageTrackingTest/tests-expected.json30
-rw-r--r--tests/src/test/resources/test/TestExecutionTest/expected.json31
-rw-r--r--tests/src/test/resources/ui/UiExtensionsTest/static-files.html24
-rw-r--r--tests/src/test/resources/updateCenter/UpdateCenterTest/update-center.properties62
-rw-r--r--tests/src/test/resources/updateCenter/installed-plugins.html64
-rw-r--r--tests/src/test/resources/user/BaseIdentityProviderTest/display_message_in_ui_but_not_in_log_when_unauthorized_exception.html44
-rw-r--r--tests/src/test/resources/user/BaseIdentityProviderTest/display_unauthorized_page_when_authentication_failed.html39
-rw-r--r--tests/src/test/resources/user/BaseIdentityProviderTest/fail_to_authenticate_when_not_allowed_to_sign_up.html39
-rw-r--r--tests/src/test/resources/user/BaseIdentityProviderTest/fail_when_email_already_exists.html44
-rw-r--r--tests/src/test/resources/user/ExternalAuthenticationTest/create-and-delete-user.html144
-rw-r--r--tests/src/test/resources/user/ExternalAuthenticationTest/external-user-details.html64
-rw-r--r--tests/src/test/resources/user/ExternalAuthenticationTest/external-user-details2.html64
-rw-r--r--tests/src/test/resources/user/ExternalAuthenticationTest/system-info.html44
-rw-r--r--tests/src/test/resources/user/LocalAuthenticationTest/allow_users_to_sign_up.html109
-rw-r--r--tests/src/test/resources/user/LocalAuthenticationTest/force-authentication.html59
-rw-r--r--tests/src/test/resources/user/LocalAuthenticationTest/login_successful.html69
-rw-r--r--tests/src/test/resources/user/LocalAuthenticationTest/login_wrong_password.html45
-rw-r--r--tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_login_when_not_enough_privilege.html59
-rw-r--r--tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_original_url_after_direct_login.html59
-rw-r--r--tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_original_url_after_indirect_login.html58
-rw-r--r--tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_original_url_with_parameters_after_direct_login.html68
-rw-r--r--tests/src/test/resources/user/LocalAuthenticationTest/should_not_be_unlogged_when_going_to_login_page.html64
-rw-r--r--tests/src/test/resources/user/MyAccountPageTest/should_display_no_projects.html60
-rw-r--r--tests/src/test/resources/user/MyAccountPageTest/should_display_projects.html85
-rw-r--r--tests/src/test/resources/user/OAuth2IdentityProviderTest/display_message_in_ui_but_not_in_log_when_unauthorized_exception.html44
-rw-r--r--tests/src/test/resources/user/OAuth2IdentityProviderTest/display_unauthorized_page_when_authentication_failed.html39
-rw-r--r--tests/src/test/resources/user/OAuth2IdentityProviderTest/fail_to_authenticate_when_not_allowed_to_sign_up.html39
-rw-r--r--tests/src/test/resources/user/OAuth2IdentityProviderTest/fail_when_email_already_exists.html44
-rw-r--r--tests/src/test/resources/user/UsersPageTest/admin_should_change_its_own_password.html95
-rw-r--r--tests/src/test/resources/user/UsersPageTest/generate_and_revoke_user_token.html115
446 files changed, 42830 insertions, 0 deletions
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/BackgroundTaskItem.java b/tests/src/test/java/org/sonarqube/pageobjects/BackgroundTaskItem.java
new file mode 100644
index 00000000000..aedd45ed305
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/BackgroundTaskItem.java
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects;
+
+import com.codeborne.selenide.SelenideElement;
+
+import static com.codeborne.selenide.Condition.hasText;
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+
+public class BackgroundTaskItem {
+
+ private final SelenideElement elt;
+
+ public BackgroundTaskItem(SelenideElement elt) {
+ this.elt = elt;
+ }
+
+ public SelenideElement getComponent() {
+ return elt.$("td:nth-child(2)");
+ }
+
+ public BackgroundTaskItem openActions() {
+ elt.$(".js-task-action > .dropdown-toggle").click();
+ elt.$(".js-task-action > .dropdown-menu").shouldBe(visible);
+ return this;
+ }
+
+ public BackgroundTaskItem openScannerContext () {
+ elt.$(".js-task-show-scanner-context").click();
+ $(".js-task-scanner-context").shouldBe(visible);
+ return this;
+ }
+
+ public BackgroundTaskItem assertScannerContextContains(String text) {
+ $(".js-task-scanner-context").should(hasText(text));
+ return this;
+ }
+
+ public BackgroundTaskItem openErrorStacktrace () {
+ elt.$(".js-task-show-stacktrace").click();
+ $(".js-task-stacktrace").shouldBe(visible);
+ return this;
+ }
+
+ public BackgroundTaskItem assertErrorStacktraceContains(String text) {
+ $(".js-task-stacktrace").should(hasText(text));
+ return this;
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/BackgroundTasksPage.java b/tests/src/test/java/org/sonarqube/pageobjects/BackgroundTasksPage.java
new file mode 100644
index 00000000000..432e1addd8c
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/BackgroundTasksPage.java
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects;
+
+import com.codeborne.selenide.ElementsCollection;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.openqa.selenium.By;
+
+import static com.codeborne.selenide.Condition.exist;
+import static com.codeborne.selenide.Selenide.$;
+import static com.codeborne.selenide.Selenide.$$;
+
+public class BackgroundTasksPage {
+
+ public BackgroundTasksPage() {
+ $(By.cssSelector(".background-tasks")).should(exist);
+ }
+
+ public ElementsCollection getTasks() {
+ return $$(".background-tasks > tbody > tr");
+ }
+
+ public List<BackgroundTaskItem> getTasksAsItems() {
+ return getTasks()
+ .stream()
+ .map(BackgroundTaskItem::new)
+ .collect(Collectors.toList());
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/EncryptionPage.java b/tests/src/test/java/org/sonarqube/pageobjects/EncryptionPage.java
new file mode 100644
index 00000000000..f0c68849bfe
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/EncryptionPage.java
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects;
+
+import com.codeborne.selenide.SelenideElement;
+
+import static com.codeborne.selenide.Condition.exist;
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+
+public class EncryptionPage extends Navigation {
+
+ public EncryptionPage() {
+ $("#encryption-page").should(exist);
+ }
+
+ public SelenideElement generationForm() {
+ return $("#generate-secret-key-form");
+ }
+
+ public SelenideElement newSecretKey() {
+ return $("#secret-key");
+ }
+
+ public String encryptValue(String value) {
+ $("#encryption-form-value").val(value);
+ $("#encryption-form").submit();
+ return $("#encrypted-value").shouldBe(visible).val();
+ }
+
+ public EncryptionPage generateNewKey() {
+ $("#encryption-new-key-form").submit();
+ $("#generate-secret-key-form").shouldBe(visible);
+ return this;
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/LoginPage.java b/tests/src/test/java/org/sonarqube/pageobjects/LoginPage.java
new file mode 100644
index 00000000000..bb17d0961e3
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/LoginPage.java
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects;
+
+import com.codeborne.selenide.Condition;
+import com.codeborne.selenide.SelenideElement;
+import org.openqa.selenium.By;
+
+import static com.codeborne.selenide.Selenide.$;
+import static com.codeborne.selenide.Selenide.page;
+
+public class LoginPage {
+
+ public LoginPage() {
+ $("#login_form").should(Condition.exist);
+ }
+
+ public Navigation submitCredentials(String login) {
+ return submitCredentials(login, login, Navigation.class);
+ }
+
+ public Navigation submitCredentials(String login, String password) {
+ return submitCredentials(login, password, Navigation.class);
+ }
+
+ public Navigation useOAuth2() {
+ $(".oauth-providers a").click();
+ return page(Navigation.class);
+ }
+
+ public LoginPage submitWrongCredentials(String login, String password) {
+ $("#login").val(login);
+ $("#password").val(password);
+ $(By.name("commit")).click();
+ return page(LoginPage.class);
+ }
+
+ public SelenideElement getErrorMessage() {
+ return $(".process-spinner-failed");
+ }
+
+ private <T> T submitCredentials(String login, String password, Class<T> expectedResultPage) {
+ $("#login").val(login);
+ $("#password").val(password);
+ $(By.name("commit")).click();
+ $("#login").should(Condition.disappear);
+ return page(expectedResultPage);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/Navigation.java b/tests/src/test/java/org/sonarqube/pageobjects/Navigation.java
new file mode 100644
index 00000000000..54363129fec
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/Navigation.java
@@ -0,0 +1,234 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects;
+
+import com.codeborne.selenide.Condition;
+import com.codeborne.selenide.Selenide;
+import com.codeborne.selenide.SelenideElement;
+import com.codeborne.selenide.WebDriverRunner;
+import com.sonar.orchestrator.Orchestrator;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.function.Consumer;
+import javax.annotation.Nullable;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.html5.WebStorage;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.pageobjects.issues.IssuesPage;
+import org.sonarqube.pageobjects.licenses.LicensesPage;
+import org.sonarqube.pageobjects.organization.MembersPage;
+import org.sonarqube.pageobjects.projects.ProjectsPage;
+import org.sonarqube.pageobjects.settings.SettingsPage;
+
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+import static com.codeborne.selenide.Selenide.clearBrowserLocalStorage;
+import static com.codeborne.selenide.Selenide.page;
+
+public class Navigation {
+
+ public Navigation() {
+ $("#content").shouldBe(Condition.exist);
+ }
+
+ /**
+ * @deprecated use {@link Tester#openBrowser()}
+ */
+ @Deprecated
+ public static Navigation create(Orchestrator orchestrator) {
+ WebDriver driver = SelenideConfig.configure(orchestrator);
+ driver.manage().deleteAllCookies();
+ clearStorage(d -> d.getLocalStorage().clear());
+ clearStorage(d -> d.getSessionStorage().clear());
+ clearStorage(d -> clearBrowserLocalStorage());
+ return Selenide.open("/", Navigation.class);
+ }
+
+ private static void clearStorage(Consumer<WebStorage> cleaner) {
+ try {
+ cleaner.accept((WebStorage) WebDriverRunner.getWebDriver());
+ } catch (Exception e) {
+ // ignore, it may occur when the first test opens browser. No pages are loaded
+ // and local/session storages are not available yet.
+ // Example with Chrome: "Failed to read the 'localStorage' property from 'Window': Storage is disabled inside 'data:' URLs."
+ }
+ }
+
+ public Navigation openHome() {
+ return open("/", Navigation.class);
+ }
+
+ public ProjectsPage openProjects() {
+ return open("/projects", ProjectsPage.class);
+ }
+
+ public ProjectsPage openProjects(String organization) {
+ return open("/organizations/" + organization + "/projects", ProjectsPage.class);
+ }
+
+ public IssuesPage openIssues() {
+ return open("/issues", IssuesPage.class);
+ }
+
+ public IssuesPage openComponentIssues(String component) {
+ return open("/component_issues?id=" + component, IssuesPage.class);
+ }
+
+ public ProjectDashboardPage openProjectDashboard(String projectKey) {
+ // TODO encode projectKey
+ String url = "/dashboard?id=" + projectKey;
+ return open(url, ProjectDashboardPage.class);
+ }
+
+ public ProjectLinksPage openProjectLinks(String projectKey) {
+ // TODO encode projectKey
+ String url = "/project/links?id=" + projectKey;
+ return open(url, ProjectLinksPage.class);
+ }
+
+ public ProjectQualityGatePage openProjectQualityGate(String projectKey) {
+ // TODO encode projectKey
+ String url = "/project/quality_gate?id=" + projectKey;
+ return open(url, ProjectQualityGatePage.class);
+ }
+
+ public ProjectKeyPage openProjectKey(String projectKey) {
+ // TODO encode projectKey
+ String url = "/project/key?id=" + projectKey;
+ return open(url, ProjectKeyPage.class);
+ }
+
+ public ProjectActivityPage openProjectActivity(String projectKey) {
+ // TODO encode projectKey
+ String url = "/project/activity?id=" + projectKey;
+ return open(url, ProjectActivityPage.class);
+ }
+
+ public MembersPage openOrganizationMembers(String orgKey) {
+ String url = "/organizations/" + orgKey + "/members";
+ return open(url, MembersPage.class);
+ }
+
+ public BackgroundTasksPage openBackgroundTasksPage() {
+ return open("/background_tasks", BackgroundTasksPage.class);
+ }
+
+ public SettingsPage openSettings(@Nullable String projectKey) throws UnsupportedEncodingException {
+ String url = projectKey != null ? "/project/settings?id=" + URLEncoder.encode(projectKey, "UTF-8") : "/settings";
+ return open(url, SettingsPage.class);
+ }
+
+ public LicensesPage openLicenses() {
+ return open("/settings/licenses", LicensesPage.class);
+ }
+
+ public EncryptionPage openEncryption() {
+ return open("/settings/encryption", EncryptionPage.class);
+ }
+
+ public ServerIdPage openServerId() {
+ return open("/settings/server_id", ServerIdPage.class);
+ }
+
+ public NotificationsPage openNotifications() {
+ return open("/account/notifications", NotificationsPage.class);
+ }
+
+ public ProjectPermissionsPage openProjectPermissions(String projectKey) {
+ String url = "/project_roles?id=" + projectKey;
+ return open(url, ProjectPermissionsPage.class);
+ }
+
+ public ProjectsManagementPage openProjectsManagement() {
+ return open("/projects_admin", ProjectsManagementPage.class);
+ }
+
+ public LoginPage openLogin() {
+ return open("/sessions/login", LoginPage.class);
+ }
+
+ public void open(String relativeUrl) {
+ Selenide.open(relativeUrl);
+ }
+
+ public <P> P open(String relativeUrl, Class<P> pageObjectClassClass) {
+ return Selenide.open(relativeUrl, pageObjectClassClass);
+ }
+
+ public Navigation shouldBeLoggedIn() {
+ loggedInDropdown().should(visible);
+ return this;
+ }
+
+ public Navigation shouldNotBeLoggedIn() {
+ logInLink().should(visible);
+ return this;
+ }
+
+ public LoginPage logIn() {
+ logInLink().click();
+ return page(LoginPage.class);
+ }
+
+ public Navigation logOut() {
+ SelenideElement dropdown = loggedInDropdown();
+ // click must be on the <a> but not on the dropdown <li>
+ // for compatibility with phantomjs
+ dropdown.find(".dropdown-toggle").click();
+ dropdown.find(By.linkText("Log out")).click();
+ return this;
+ }
+
+ public RulesPage clickOnRules() {
+ $(By.linkText("Rules")).click();
+ return page(RulesPage.class);
+ }
+
+ public SelenideElement clickOnQualityProfiles() {
+ return $(By.linkText("Quality Profiles"));
+ }
+
+ public SelenideElement getRightBar() {
+ return $("#global-navigation .navbar-right");
+ }
+
+ public SelenideElement getFooter() {
+ return $("#footer");
+ }
+
+ public SelenideElement getErrorMessage() {
+ return $("#error");
+ }
+
+ private SelenideElement logInLink() {
+ return $(By.linkText("Log in"));
+ }
+
+ private SelenideElement loggedInDropdown() {
+ return $(".js-user-authenticated");
+ }
+
+ public Navigation shouldBeRedirectedToLogin() {
+ $("#login_form").should(visible);
+ return this;
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/NotificationsPage.java b/tests/src/test/java/org/sonarqube/pageobjects/NotificationsPage.java
new file mode 100644
index 00000000000..456f08ca957
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/NotificationsPage.java
@@ -0,0 +1,120 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects;
+
+import static com.codeborne.selenide.Condition.cssClass;
+import static com.codeborne.selenide.Condition.text;
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+
+public class NotificationsPage extends Navigation {
+
+ private final String EMAIL = "EmailNotificationChannel";
+
+ public NotificationsPage() {
+ $("#account-page").shouldHave(text("Overall notifications"));
+ }
+
+ public NotificationsPage shouldHaveGlobalNotification(String type) {
+ return shouldHaveGlobalNotification(type, EMAIL);
+ }
+
+ public NotificationsPage shouldHaveGlobalNotification(String type, String channel) {
+ return shouldBeChecked(globalCheckboxSelector(type, channel));
+ }
+
+ public NotificationsPage shouldNotHaveGlobalNotification(String type) {
+ return shouldNotHaveGlobalNotification(type, EMAIL);
+ }
+
+ public NotificationsPage shouldNotHaveGlobalNotification(String type, String channel) {
+ return shouldNotBeChecked(globalCheckboxSelector(type, channel));
+ }
+
+ public NotificationsPage shouldHaveProjectNotification(String project, String type, String channel) {
+ return shouldBeChecked(projectCheckboxSelector(project, type, channel));
+ }
+
+ public NotificationsPage shouldNotHaveProjectNotification(String project, String type, String channel) {
+ return shouldNotBeChecked(projectCheckboxSelector(project, type, channel));
+ }
+
+ public NotificationsPage addGlobalNotification(String type) {
+ return addGlobalNotification(type, EMAIL);
+ }
+
+ public NotificationsPage addGlobalNotification(String type, String channel) {
+ shouldNotHaveGlobalNotification(type, channel);
+ toggleCheckbox(globalCheckboxSelector(type, channel));
+ shouldHaveGlobalNotification(type, channel);
+ return this;
+ }
+
+ public NotificationsPage removeGlobalNotification(String type) {
+ return removeGlobalNotification(type, EMAIL);
+ }
+
+ public NotificationsPage removeGlobalNotification(String type, String channel) {
+ shouldHaveGlobalNotification(type, channel);
+ toggleCheckbox(globalCheckboxSelector(type, channel));
+ shouldNotHaveGlobalNotification(type, channel);
+ return this;
+ }
+
+ public NotificationsPage addProjectNotification(String project, String type, String channel) {
+ shouldNotHaveProjectNotification(project, type, channel);
+ toggleCheckbox(projectCheckboxSelector(project, type, channel));
+ shouldHaveProjectNotification(project, type, channel);
+ return this;
+ }
+
+ public NotificationsPage removeProjectNotification(String project, String type, String channel) {
+ shouldHaveProjectNotification(project, type, channel);
+ toggleCheckbox(projectCheckboxSelector(project, type, channel));
+ shouldNotHaveProjectNotification(project, type, channel);
+ return this;
+ }
+
+ private String globalCheckboxSelector(String type, String channel) {
+ return "#global-notification-" + type + "-" + channel;
+ }
+
+ private String projectCheckboxSelector(String project, String type, String channel) {
+ return "#project-notification-" + project + "-" + type + "-" + channel;
+ }
+
+ private NotificationsPage shouldBeChecked(String selector) {
+ $(selector)
+ .shouldBe(visible)
+ .shouldHave(cssClass("icon-checkbox-checked"));
+ return this;
+ }
+
+ private NotificationsPage shouldNotBeChecked(String selector) {
+ $(selector)
+ .shouldBe(visible)
+ .shouldNotHave(cssClass("icon-checkbox-checked"));
+ return this;
+ }
+
+ private void toggleCheckbox(String selector) {
+ $(selector).click();
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/ProjectActivityPage.java b/tests/src/test/java/org/sonarqube/pageobjects/ProjectActivityPage.java
new file mode 100644
index 00000000000..05479cb6275
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/ProjectActivityPage.java
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects;
+
+import com.codeborne.selenide.Condition;
+import com.codeborne.selenide.ElementsCollection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static com.codeborne.selenide.Condition.hasText;
+import static com.codeborne.selenide.Selenide.$;
+import static com.codeborne.selenide.Selenide.$$;
+
+public class ProjectActivityPage {
+
+ public ProjectActivityPage() {
+ $("#project-activity").should(Condition.exist);
+ }
+
+ public ElementsCollection getAnalyses() {
+ return $$(".project-activity-analysis");
+ }
+
+ public List<ProjectAnalysisItem> getAnalysesAsItems() {
+ return getAnalyses()
+ .stream()
+ .map(ProjectAnalysisItem::new)
+ .collect(Collectors.toList());
+ }
+
+ public ProjectAnalysisItem getLastAnalysis() {
+ return new ProjectAnalysisItem($(".project-activity-analysis"));
+ }
+
+ public ProjectAnalysisItem getFirstAnalysis() {
+ return new ProjectAnalysisItem($$(".project-activity-analysis").last());
+ }
+
+ public ProjectActivityPage assertFirstAnalysisOfTheDayHasText(String day, String text) {
+ $("#project-activity")
+ .find(".project-activity-day[data-day=\"" + day + "\"]")
+ .find(".project-activity-analysis")
+ .should(hasText(text));
+ return this;
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/ProjectAnalysisItem.java b/tests/src/test/java/org/sonarqube/pageobjects/ProjectAnalysisItem.java
new file mode 100644
index 00000000000..2589fa20f02
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/ProjectAnalysisItem.java
@@ -0,0 +1,103 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects;
+
+import com.codeborne.selenide.SelenideElement;
+
+import static com.codeborne.selenide.Condition.text;
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+
+public class ProjectAnalysisItem {
+
+ private final SelenideElement elt;
+
+ public ProjectAnalysisItem(SelenideElement elt) {
+ this.elt = elt;
+ }
+
+ public ProjectAnalysisItem shouldHaveEventWithText(String text) {
+ elt.find(".project-activity-events").shouldHave(text(text));
+ return this;
+ }
+
+ public ProjectAnalysisItem shouldHaveDeleteButton() {
+ elt.find(".js-delete-analysis").shouldBe(visible);
+ return this;
+ }
+
+ public ProjectAnalysisItem shouldNotHaveDeleteButton() {
+ elt.find(".js-delete-analysis").shouldNotBe(visible);
+ return this;
+ }
+
+ public void delete() {
+ elt.find(".js-delete-analysis").click();
+
+ SelenideElement modal = $(".modal");
+ modal.shouldBe(visible);
+ modal.find("button[type=\"submit\"]").click();
+
+ elt.shouldNotBe(visible);
+ }
+
+ public ProjectAnalysisItem addCustomEvent(String name) {
+ elt.find(".js-create").click();
+ elt.find(".js-add-event").click();
+
+ SelenideElement modal = $(".modal");
+ modal.shouldBe(visible);
+ modal.find("input").setValue(name);
+ modal.find("button[type=\"submit\"]").click();
+
+ elt.find(".project-activity-event:last-child").shouldHave(text(name));
+
+ return this;
+ }
+
+ public ProjectAnalysisItem changeLastEvent(String newName) {
+ SelenideElement lastEvent = elt.find(".project-activity-event:last-child");
+ lastEvent.find(".js-change-event").click();
+
+ SelenideElement modal = $(".modal");
+ modal.shouldBe(visible);
+ modal.find("input").setValue(newName);
+ modal.find("button[type=\"submit\"]").click();
+
+ lastEvent.shouldHave(text(newName));
+
+ return this;
+ }
+
+ public ProjectAnalysisItem deleteLastEvent() {
+ int eventsCount = elt.findAll(".project-activity-event").size();
+
+ SelenideElement lastEvent = elt.find(".project-activity-event:last-child");
+ lastEvent.find(".js-delete-event").click();
+
+ SelenideElement modal = $(".modal");
+ modal.shouldBe(visible);
+ modal.find("button[type=\"submit\"]").click();
+
+ elt.findAll(".project-activity-event").shouldHaveSize(eventsCount - 1);
+
+ return this;
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/ProjectDashboardPage.java b/tests/src/test/java/org/sonarqube/pageobjects/ProjectDashboardPage.java
new file mode 100644
index 00000000000..f576ea4cc32
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/ProjectDashboardPage.java
@@ -0,0 +1,91 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects;
+
+import com.codeborne.selenide.SelenideElement;
+import java.util.Arrays;
+
+import static com.codeborne.selenide.Condition.exist;
+import static com.codeborne.selenide.Condition.hasText;
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+
+public class ProjectDashboardPage {
+
+ public ProjectDashboardPage() {
+ $(".overview").shouldBe(visible);
+ }
+
+ public SelenideElement getLinesOfCode() {
+ SelenideElement element = $("#overview-ncloc");
+ element.shouldBe(visible);
+ return element;
+ }
+
+ public SelenideElement getLanguageDistribution() {
+ SelenideElement element = $("#overview-language-distribution");
+ element.shouldBe(visible);
+ return element;
+ }
+
+ private SelenideElement getTagsMeta() {
+ SelenideElement element = $(".overview-meta-tags");
+ element.shouldBe(visible);
+ return element;
+ }
+
+ public ProjectDashboardPage shouldHaveTags(String... tags) {
+ String tagsList = String.join(", ", Arrays.asList(tags));
+ this.getTagsMeta().$(".tags-list > span").should(hasText(tagsList));
+ return this;
+ }
+
+ public ProjectDashboardPage shouldNotBeEditable() {
+ SelenideElement tagsElem = this.getTagsMeta();
+ tagsElem.$("button").shouldNot(exist);
+ tagsElem.$("div.multi-select").shouldNot(exist);
+ return this;
+ }
+
+ public ProjectDashboardPage shouldBeEditable() {
+ SelenideElement tagsElem = this.getTagsMeta();
+ tagsElem.$("button").shouldBe(visible);
+ return this;
+ }
+
+ public ProjectDashboardPage openTagEditor() {
+ SelenideElement tagsElem = this.getTagsMeta();
+ tagsElem.$("button").shouldBe(visible).click();
+ tagsElem.$("div.multi-select").shouldBe(visible);
+ return this;
+ }
+
+ public SelenideElement getTagAtIdx(Integer idx) {
+ SelenideElement tagsElem = this.getTagsMeta();
+ tagsElem.$("div.multi-select").shouldBe(visible);
+ return tagsElem.$$("ul.menu a").get(idx);
+ }
+
+ public ProjectDashboardPage sendKeysToTagsInput(CharSequence... charSequences) {
+ SelenideElement tagsInput = this.getTagsMeta().find("input");
+ tagsInput.sendKeys(charSequences);
+ return this;
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/ProjectKeyPage.java b/tests/src/test/java/org/sonarqube/pageobjects/ProjectKeyPage.java
new file mode 100644
index 00000000000..cf72bdadbe5
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/ProjectKeyPage.java
@@ -0,0 +1,103 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects;
+
+import com.codeborne.selenide.SelenideElement;
+
+import static com.codeborne.selenide.Condition.exist;
+import static com.codeborne.selenide.Condition.hasText;
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+
+public class ProjectKeyPage {
+
+ public ProjectKeyPage() {
+ $("#project-key").should(exist);
+ }
+
+ public ProjectKeyPage assertSimpleUpdate() {
+ $("#update-key-new-key").shouldBe(visible);
+ $("#update-key-submit").shouldBe(visible);
+ return this;
+ }
+
+ public ProjectKeyPage trySimpleUpdate(String newKey) {
+ $("#update-key-new-key").val(newKey);
+ $("#update-key-submit").click();
+ $("#update-key-confirm").click();
+ return this;
+ }
+
+ public ProjectKeyPage openFineGrainedUpdate() {
+ $("#update-key-tab-fine").click();
+ $("#project-key-fine-grained-update").shouldBe(visible);
+ return this;
+ }
+
+ public ProjectKeyPage tryFineGrainedUpdate(String key, String newKey) {
+ SelenideElement form = $(".js-fine-grained-update[data-key=\"" + key + "\"]");
+ form.shouldBe(visible);
+
+ form.$("input").val(newKey);
+ form.$("button").click();
+
+ $("#update-key-confirm").click();
+ return this;
+ }
+
+ public ProjectKeyPage assertBulkChange() {
+ $("#bulk-update-replace").shouldBe(visible);
+ $("#bulk-update-by").shouldBe(visible);
+ $("#bulk-update-see-results").shouldBe(visible);
+ return this;
+ }
+
+ public ProjectKeyPage simulateBulkChange(String replace, String by) {
+ $("#bulk-update-replace").val(replace);
+ $("#bulk-update-by").val(by);
+ $("#bulk-update-see-results").click();
+
+ $("#bulk-update-simulation").shouldBe(visible);
+ return this;
+ }
+
+ public ProjectKeyPage assertBulkChangeSimulationResult(String oldKey, String newKey) {
+ SelenideElement row = $("#bulk-update-results").$("[data-key=\"" + oldKey + "\"]");
+ row.$(".js-old-key").should(hasText(oldKey));
+ row.$(".js-new-key").should(hasText(newKey));
+ return this;
+ }
+
+ public ProjectKeyPage assertDuplicated(String oldKey) {
+ SelenideElement row = $("#bulk-update-results").$("[data-key=\"" + oldKey + "\"]");
+ row.$(".js-new-key").$(".badge-danger").shouldBe(visible);
+ return this;
+ }
+
+ public ProjectKeyPage confirmBulkUpdate() {
+ $("#bulk-update-confirm").click();
+ return this;
+ }
+
+ public ProjectKeyPage assertSuccessfulBulkUpdate() {
+ $("#project-key-bulk-update").$(".alert.alert-success").shouldBe(visible);
+ return this;
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/ProjectLinkItem.java b/tests/src/test/java/org/sonarqube/pageobjects/ProjectLinkItem.java
new file mode 100644
index 00000000000..c652e018e0d
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/ProjectLinkItem.java
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects;
+
+import com.codeborne.selenide.SelenideElement;
+import org.openqa.selenium.NoSuchElementException;
+
+public class ProjectLinkItem {
+
+ private final SelenideElement elt;
+
+ public ProjectLinkItem(SelenideElement elt) {
+ this.elt = elt;
+ }
+
+ public SelenideElement getName() {
+ return elt.$(".js-name");
+ }
+
+ public SelenideElement getType() {
+ try {
+ return elt.$(".js-type");
+ } catch (NoSuchElementException e) {
+ return null;
+ }
+ }
+
+ public SelenideElement getUrl() {
+ return elt.$(".js-url");
+ }
+
+ public SelenideElement getDeleteButton() {
+ return elt.$(".js-delete-button");
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/ProjectLinksPage.java b/tests/src/test/java/org/sonarqube/pageobjects/ProjectLinksPage.java
new file mode 100644
index 00000000000..a4adbf396f3
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/ProjectLinksPage.java
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects;
+
+import com.codeborne.selenide.Condition;
+import com.codeborne.selenide.ElementsCollection;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static com.codeborne.selenide.Selenide.$;
+import static com.codeborne.selenide.Selenide.$$;
+
+public class ProjectLinksPage {
+
+ public ProjectLinksPage() {
+ $("#project-links").should(Condition.exist);
+ }
+
+ public ElementsCollection getLinks() {
+ return $$("#project-links tr[data-name]");
+ }
+
+ public List<ProjectLinkItem> getLinksAsItems() {
+ return getLinks()
+ .stream()
+ .map(ProjectLinkItem::new)
+ .collect(Collectors.toList());
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/ProjectPermissionsPage.java b/tests/src/test/java/org/sonarqube/pageobjects/ProjectPermissionsPage.java
new file mode 100644
index 00000000000..954ad779a07
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/ProjectPermissionsPage.java
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects;
+
+import static com.codeborne.selenide.Condition.cssClass;
+import static com.codeborne.selenide.Condition.exist;
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+
+public class ProjectPermissionsPage {
+
+ public ProjectPermissionsPage() {
+ $("#project-permissions-page").should(exist);
+ }
+
+ public ProjectPermissionsPage shouldBePublic() {
+ $("#visibility-public .icon-radio.is-checked").shouldBe(visible);
+ return this;
+ }
+
+ public ProjectPermissionsPage shouldBePrivate() {
+ $("#visibility-private .icon-radio.is-checked").shouldBe(visible);
+ return this;
+ }
+
+ public ProjectPermissionsPage turnToPublic() {
+ $("#visibility-public").click();
+ $("#confirm-turn-to-public").click();
+ shouldBePublic();
+ return this;
+ }
+
+ public ProjectPermissionsPage turnToPrivate() {
+ $("#visibility-private").click();
+ shouldBePrivate();
+ return this;
+ }
+
+ public ProjectPermissionsPage shouldNotAllowPrivate() {
+ $("#visibility-private").shouldHave(cssClass("text-muted"));
+ $(".upgrade-organization-box").shouldBe(visible);
+ return this;
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/ProjectQualityGatePage.java b/tests/src/test/java/org/sonarqube/pageobjects/ProjectQualityGatePage.java
new file mode 100644
index 00000000000..58e28acaacf
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/ProjectQualityGatePage.java
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects;
+
+import com.codeborne.selenide.SelenideElement;
+
+import static com.codeborne.selenide.Condition.exist;
+import static com.codeborne.selenide.Selenide.$;
+
+public class ProjectQualityGatePage {
+
+ public ProjectQualityGatePage() {
+ $("#project-quality-gate").should(exist);
+ }
+
+ public SelenideElement getSelectedQualityGate() {
+ return $(".Select-value-label");
+ }
+
+ public void assertNotSelected() {
+ $(".Select-placeholder").should(exist);
+ $(".Select-value-label").shouldNot(exist);
+ }
+
+ public void setQualityGate(String name) {
+ $(".Select-input input").val(name).pressEnter();
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/ProjectsManagementPage.java b/tests/src/test/java/org/sonarqube/pageobjects/ProjectsManagementPage.java
new file mode 100644
index 00000000000..ea5ff5c06db
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/ProjectsManagementPage.java
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects;
+
+import static com.codeborne.selenide.Condition.exist;
+import static com.codeborne.selenide.Condition.text;
+import static com.codeborne.selenide.Selenide.$;
+import static com.codeborne.selenide.Selenide.$$;
+
+public class ProjectsManagementPage {
+
+ public ProjectsManagementPage() {
+ $("#projects-management-page").should(exist);
+ }
+
+ public ProjectsManagementPage shouldHaveProjectsCount(int count) {
+ $$("#projects-management-page-projects tr").shouldHaveSize(count);
+ return this;
+ }
+
+ public ProjectsManagementPage shouldHaveProject(String key) {
+ $("#projects-management-page-projects").shouldHave(text(key));
+ return this;
+ }
+
+ public ProjectsManagementPage createProject(String key, String name, String visibility) {
+ $("#create-project").click();
+ $("#create-project-name").val(key);
+ $("#create-project-key").val(name);
+ $("#visibility-" + visibility).click();
+ $("#create-project-submit").submit();
+ return this;
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/RuleItem.java b/tests/src/test/java/org/sonarqube/pageobjects/RuleItem.java
new file mode 100644
index 00000000000..b311f3555b8
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/RuleItem.java
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects;
+
+import com.codeborne.selenide.SelenideElement;
+
+public class RuleItem {
+
+ private final SelenideElement elt;
+
+ public RuleItem(SelenideElement elt) {
+ this.elt = elt;
+ }
+
+ public SelenideElement getTitle() {
+ return elt.$(".coding-rule-title");
+ }
+
+ public SelenideElement getMetadata() {
+ return elt.$(".coding-rule-meta");
+ }
+
+
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/RulesPage.java b/tests/src/test/java/org/sonarqube/pageobjects/RulesPage.java
new file mode 100644
index 00000000000..35eda0b68e3
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/RulesPage.java
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects;
+
+import com.codeborne.selenide.Condition;
+import com.codeborne.selenide.ElementsCollection;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.openqa.selenium.By;
+
+import static com.codeborne.selenide.Selenide.$;
+import static com.codeborne.selenide.Selenide.$$;
+
+public class RulesPage extends Navigation {
+
+ public RulesPage() {
+ $(By.cssSelector(".coding-rules")).should(Condition.exist);
+ }
+
+ public ElementsCollection getRules() {
+ return $$(".coding-rules .coding-rule");
+ }
+
+ public List<RuleItem> getRulesAsItems() {
+ return getRules()
+ .stream()
+ .map(elt -> new RuleItem(elt))
+ .collect(Collectors.toList());
+ }
+
+ public int getTotal() {
+ // warning - number is localized
+ return Integer.parseInt($("#coding-rules-total").text());
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/SelenideConfig.java b/tests/src/test/java/org/sonarqube/pageobjects/SelenideConfig.java
new file mode 100644
index 00000000000..32c6850f917
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/SelenideConfig.java
@@ -0,0 +1,70 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects;
+
+import com.codeborne.selenide.Configuration;
+import com.codeborne.selenide.WebDriverRunner;
+import com.sonar.orchestrator.Orchestrator;
+import java.util.stream.Collectors;
+import org.openqa.selenium.WebDriver;
+
+import static java.util.Arrays.stream;
+
+public class SelenideConfig {
+
+ private enum Browser {
+ firefox("(v46 and lower)"),
+ marionette("(recent Firefox, require Geckodriver)"),
+ chrome("(require Chromedriver)");
+
+ private final String label;
+
+ Browser(String label) {
+ this.label = label;
+ }
+
+ static Browser of(String s) {
+ try {
+ return Browser.valueOf(s);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Invalid browser: " + s + ". Supported values are " +
+ stream(values()).map(b -> b.name() + " " + b.label).collect(Collectors.joining(", ")));
+ }
+ }
+ }
+
+ public static WebDriver configure(Orchestrator orchestrator) {
+ String browserKey = orchestrator.getConfiguration().getString("orchestrator.browser", Browser.firefox.name());
+ Browser browser = Browser.of(browserKey);
+ Configuration.browser = browser.name();
+ Configuration.baseUrl = orchestrator.getServer().getUrl();
+ Configuration.timeout = 8_000;
+ Configuration.reportsFolder = "target/screenshots";
+ Configuration.screenshots = true;
+ Configuration.captureJavascriptErrors = true;
+ Configuration.savePageSource = true;
+ Configuration.browserSize = "1280x1024";
+ return getWebDriver();
+ }
+
+ static WebDriver getWebDriver() {
+ return WebDriverRunner.getWebDriver();
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/ServerIdPage.java b/tests/src/test/java/org/sonarqube/pageobjects/ServerIdPage.java
new file mode 100644
index 00000000000..e65171c8425
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/ServerIdPage.java
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects;
+
+import com.codeborne.selenide.SelenideElement;
+
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+
+public class ServerIdPage {
+
+ public ServerIdPage() {
+ $("#server-id-page").shouldBe(visible);
+ }
+
+ public SelenideElement serverIdInput() {
+ return $("#server-id-result").shouldBe(visible);
+ }
+
+ private SelenideElement organizationInput() {
+ return $("#server-id-organization").shouldBe(visible);
+ }
+
+ private SelenideElement ipAddressInput() {
+ return $("#server-id-ip").shouldBe(visible);
+ }
+
+ public ServerIdPage assertError() {
+ $(".process-spinner-failed").shouldBe(visible);
+ return this;
+ }
+
+ public ServerIdPage setOrganization(String organization) {
+ organizationInput().val(organization);
+ return this;
+ }
+
+ public ServerIdPage setIpAddress(String ipAddress) {
+ ipAddressInput().val(ipAddress);
+ return this;
+ }
+
+ public ServerIdPage submitForm() {
+ $("#server-id-form").submit();
+ return this;
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/issues/Issue.java b/tests/src/test/java/org/sonarqube/pageobjects/issues/Issue.java
new file mode 100644
index 00000000000..a2c6e64648e
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/issues/Issue.java
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects.issues;
+
+import com.codeborne.selenide.SelenideElement;
+
+import static com.codeborne.selenide.Condition.exist;
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+
+public class Issue {
+
+ private final SelenideElement elt;
+
+ public Issue(SelenideElement elt) {
+ this.elt = elt;
+ }
+
+ public Issue shouldAllowAssign() {
+ elt.find(".js-issue-assign").shouldBe(visible);
+ return this;
+ }
+
+ public Issue shouldAllowChangeType() {
+ elt.find(".js-issue-set-type").shouldBe(visible);
+ return this;
+ }
+
+ public Issue shouldNotAllowAssign() {
+ elt.find(".js-issue-assign").shouldNotBe(visible);
+ return this;
+ }
+
+ public Issue shouldNotAllowChangeType() {
+ elt.find(".js-issue-set-type").shouldNotBe(visible);
+ return this;
+ }
+
+ public Issue assigneeSearchResultCount(String query, Integer count) {
+ SelenideElement assignLink = elt.find(".js-issue-assign");
+ assignLink.click();
+ SelenideElement popupMenu = $(".bubble-popup ul.menu").shouldBe(visible);
+ $(".bubble-popup input.search-box-input").shouldBe(visible).val("").sendKeys(query);
+ popupMenu.$("li a[data-text='Not assigned']").shouldNot(exist);
+ popupMenu.$$("li").shouldHaveSize(count);
+ assignLink.click();
+ return this;
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/issues/IssuesPage.java b/tests/src/test/java/org/sonarqube/pageobjects/issues/IssuesPage.java
new file mode 100644
index 00000000000..d09c894f9d9
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/issues/IssuesPage.java
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects.issues;
+
+import com.codeborne.selenide.ElementsCollection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static com.codeborne.selenide.CollectionCondition.sizeGreaterThan;
+import static com.codeborne.selenide.Condition.exist;
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+import static com.codeborne.selenide.Selenide.$$;
+
+public class IssuesPage {
+
+ public IssuesPage() {
+ $(".issues").should(exist);
+ }
+
+ private ElementsCollection getIssuesElements() {
+ return $$(".issues .issue");
+ }
+
+ public List<Issue> getIssues() {
+ return getIssuesElements()
+ .stream()
+ .map(Issue::new)
+ .collect(Collectors.toList());
+ }
+
+ public Issue getFirstIssue() {
+ getIssuesElements().shouldHave(sizeGreaterThan(0));
+ return new Issue(getIssuesElements().first());
+ }
+
+ public IssuesPage bulkChangeOpen() {
+ $("#issues-bulk-change").shouldBe(visible).click();
+ $("#bulk-change-form").shouldBe(visible);
+ return this;
+ }
+
+ public IssuesPage bulkChangeAssigneeSearchCount(String query, Integer count) {
+ $("#issues-bulk-change-assignee .Select-input input").val(query);
+ $$("#issues-bulk-change-assignee .Select-option").shouldHaveSize(count);
+ $("#issues-bulk-change-assignee .Select-input input").pressEscape();
+ return this;
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/licenses/LicenseItem.java b/tests/src/test/java/org/sonarqube/pageobjects/licenses/LicenseItem.java
new file mode 100644
index 00000000000..61b7753cc26
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/licenses/LicenseItem.java
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects.licenses;
+
+import com.codeborne.selenide.SelenideElement;
+
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+
+public class LicenseItem {
+
+ private final SelenideElement elt;
+
+ public LicenseItem(SelenideElement elt) {
+ this.elt = elt;
+ }
+
+ public SelenideElement getName() {
+ return elt.find(".js-product");
+ }
+
+ public LicenseItem setLicense(String value) {
+ elt.find(".js-change").click();
+ $("#license-input").shouldBe(visible).val(value);
+ $(".js-modal-submit").click();
+ $("#license-input").shouldNotBe(visible);
+ return this;
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/licenses/LicensesPage.java b/tests/src/test/java/org/sonarqube/pageobjects/licenses/LicensesPage.java
new file mode 100644
index 00000000000..95e1f438f30
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/licenses/LicensesPage.java
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects.licenses;
+
+import com.codeborne.selenide.ElementsCollection;
+import com.codeborne.selenide.SelenideElement;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+import static com.codeborne.selenide.Selenide.$$;
+
+public class LicensesPage {
+
+ public LicensesPage() {
+ $("#licenses-page").shouldBe(visible);
+ }
+
+ public ElementsCollection getLicenses() {
+ return $$(".js-license");
+ }
+
+ public List<LicenseItem> getLicensesAsItems() {
+ return getLicenses()
+ .stream()
+ .map(LicenseItem::new)
+ .collect(Collectors.toList());
+ }
+
+ public LicenseItem getLicenseByKey(String key) {
+ SelenideElement element = $(".js-license[data-license-key=\"" + key + "\"]");
+ return new LicenseItem(element);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/organization/MemberItem.java b/tests/src/test/java/org/sonarqube/pageobjects/organization/MemberItem.java
new file mode 100644
index 00000000000..0de4be4c937
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/organization/MemberItem.java
@@ -0,0 +1,95 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+
+package org.sonarqube.pageobjects.organization;
+
+import com.codeborne.selenide.CollectionCondition;
+import com.codeborne.selenide.Condition;
+import com.codeborne.selenide.ElementsCollection;
+import com.codeborne.selenide.SelenideElement;
+
+import static com.codeborne.selenide.Selenide.$;
+
+public class MemberItem {
+ private final SelenideElement elt;
+
+ public MemberItem(SelenideElement elt) {
+ this.elt = elt;
+ }
+
+ public MemberItem shouldBeNamed(String login, String name) {
+ ElementsCollection tds = this.elt.$$("td");
+ tds.get(1).$("strong").shouldHave(Condition.text(name));
+ tds.get(1).$("span").shouldHave(Condition.text(login));
+ return this;
+ }
+
+ public MemberItem shouldHaveGroups(Integer groups) {
+ ElementsCollection tds = this.elt.$$("td");
+ tds.get(2).should(Condition.exist);
+ tds.get(2).shouldHave(Condition.text(groups.toString()));
+ return this;
+ }
+
+ public MemberItem shouldNotHaveActions() {
+ this.elt.$$("td").shouldHave(CollectionCondition.sizeLessThan(3));
+ return this;
+ }
+
+ public MemberItem removeMembership() {
+ ElementsCollection tds = this.elt.$$("td");
+ tds.shouldHave(CollectionCondition.sizeGreaterThan(3));
+ SelenideElement actionTd = tds.get(3);
+ actionTd.$("button").should(Condition.exist).click();
+ actionTd.$$(".dropdown-menu > li").get(2).shouldBe(Condition.visible).click();
+ SelenideElement modal = getModal("Remove user");
+ modal.$("button.button-red").shouldBe(Condition.visible).click();
+ return this;
+ }
+
+ public MemberItem manageGroupsOpen() {
+ ElementsCollection tds = this.elt.$$("td");
+ tds.shouldHave(CollectionCondition.sizeGreaterThan(3));
+ SelenideElement actionTd = tds.get(3);
+ actionTd.$("button").should(Condition.exist).click();
+ actionTd.$$(".dropdown-menu > li").get(0).shouldBe(Condition.visible).click();
+ getModal("Manage groups");
+ return this;
+ }
+
+ public MemberItem manageGroupsSelect(String group) {
+ SelenideElement modal = getModal("Manage groups");
+ modal.$$("li").find(Condition.text(group)).shouldBe(Condition.visible).click();
+ return this;
+ }
+
+ public MemberItem manageGroupsSave() {
+ SelenideElement modal = getModal("Manage groups");
+ modal.$("button[type='submit']").shouldBe(Condition.visible).click();
+ return this;
+ }
+
+ private SelenideElement getModal(String title) {
+ $(".modal-head").should(Condition.exist).shouldHave(Condition.text(title));
+ SelenideElement form = $(".ReactModalPortal form");
+ form.should(Condition.exist);
+ return form;
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/organization/MembersPage.java b/tests/src/test/java/org/sonarqube/pageobjects/organization/MembersPage.java
new file mode 100644
index 00000000000..76d091305f6
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/organization/MembersPage.java
@@ -0,0 +1,85 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+
+package org.sonarqube.pageobjects.organization;
+
+import com.codeborne.selenide.Condition;
+import com.codeborne.selenide.ElementsCollection;
+import com.codeborne.selenide.SelenideElement;
+
+import static com.codeborne.selenide.Condition.text;
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+import static com.codeborne.selenide.Selenide.$$;
+
+public class MembersPage {
+
+ public MembersPage() {
+ $(".nav-tabs a.active").shouldBe(visible).shouldHave(text("Members"));
+ }
+
+ public ElementsCollection getMembers() {
+ return $$("table.data tr");
+ }
+
+ public MemberItem getMembersByIdx(Integer idx) {
+ return new MemberItem(getMembers().get(idx));
+ }
+
+ public MembersPage shouldHaveTotal(int total) {
+ $(".panel-vertical > span > strong").shouldHave(text(String.valueOf(total)));
+ return this;
+ }
+
+ public MembersPage searchForMember(String query) {
+ $("input.search-box-input").shouldBe(visible).val("").sendKeys(query);
+ return this;
+ }
+
+ public MembersPage canAddMember() {
+ $(".page-actions").shouldBe(visible);
+ return this;
+ }
+
+ public MembersPage canNotAddMember() {
+ $(".page-actions").shouldNot(Condition.exist);
+ return this;
+ }
+
+ public MembersPage addMember(String login) {
+ this.canAddMember();
+ $(".page-actions button").click();
+
+ SelenideElement modal = this.getModal("Add user");
+ SelenideElement input = modal.$(".Select-input input");
+ input.val(login);
+ modal.$("div.Select-option.is-focused").should(Condition.exist);
+ input.pressEnter();
+ modal.$("button[type='submit']").click();
+ return this;
+ }
+
+ private SelenideElement getModal(String title) {
+ $(".modal-head").should(Condition.exist).shouldHave(text(title));
+ SelenideElement form = $(".ReactModalPortal form");
+ form.should(Condition.exist);
+ return form;
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/projects/FacetItem.java b/tests/src/test/java/org/sonarqube/pageobjects/projects/FacetItem.java
new file mode 100644
index 00000000000..ec3197c5a62
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/projects/FacetItem.java
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects.projects;
+
+import com.codeborne.selenide.Condition;
+import com.codeborne.selenide.SelenideElement;
+
+public class FacetItem {
+
+ private final SelenideElement elt;
+
+ public FacetItem(SelenideElement elt) {
+ this.elt = elt;
+ }
+
+ public FacetItem shouldHaveValue(String key, String value) {
+ this.elt.$(".facet[data-key=\"" + key + "\"] .facet-stat").shouldHave(Condition.text(value));
+ return this;
+ }
+
+ public void selectValue(String key) {
+ this.elt.$(".facet[data-key=\"" + key + "\"]").click();
+ }
+
+ public FacetItem selectOptionItem(String value) {
+ SelenideElement selectInput = this.elt.$(".Select-input input");
+ selectInput.val(value);
+ this.elt.$("div.Select-option.is-focused").should(Condition.exist);
+ selectInput.pressEnter();
+ return this;
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/projects/ProjectItem.java b/tests/src/test/java/org/sonarqube/pageobjects/projects/ProjectItem.java
new file mode 100644
index 00000000000..6261efa9129
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/projects/ProjectItem.java
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects.projects;
+
+import com.codeborne.selenide.Condition;
+import com.codeborne.selenide.SelenideElement;
+
+public class ProjectItem {
+
+ private final SelenideElement elt;
+
+ public ProjectItem(SelenideElement elt) {
+ this.elt = elt;
+ }
+
+ public ProjectItem shouldHaveMeasure(String metricKey, String value) {
+ this.elt.$(".project-card-measure[data-key=\"" + metricKey + "\"]").shouldHave(Condition.text(value));
+ return this;
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/projects/ProjectsPage.java b/tests/src/test/java/org/sonarqube/pageobjects/projects/ProjectsPage.java
new file mode 100644
index 00000000000..d019dcd74c8
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/projects/ProjectsPage.java
@@ -0,0 +1,120 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects.projects;
+
+import com.codeborne.selenide.Condition;
+import com.codeborne.selenide.ElementsCollection;
+import com.codeborne.selenide.SelenideElement;
+
+import static com.codeborne.selenide.Condition.text;
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+import static com.codeborne.selenide.Selenide.$$;
+import static com.codeborne.selenide.WebDriverRunner.url;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ProjectsPage {
+
+ public ProjectsPage() {
+ $("#projects-page").shouldBe(visible);
+ }
+
+ public ElementsCollection getProjects() {
+ return $$(".projects-list > .boxed-group");
+ }
+
+ public ElementsCollection getFacets() {
+ return $$(".search-navigator-facet-box");
+ }
+
+ public ProjectItem getProjectByKey(String projectKey) {
+ SelenideElement element = getProjects().find(Condition.attribute("data-key", projectKey));
+ return new ProjectItem(element);
+ }
+
+ public ProjectItem getProjectByIdx(Integer idx) {
+ return new ProjectItem(getProjects().get(idx));
+ }
+
+ public FacetItem getFacetByProperty(String facetProperty) {
+ SelenideElement element = getFacets().find(Condition.attribute("data-key", facetProperty));
+ return new FacetItem(element);
+ }
+
+ public ProjectsPage shouldHaveTotal(int total) {
+ // warning - number is localized
+ $("#projects-total").shouldHave(text(String.valueOf(total)));
+ return this;
+ }
+
+ public ProjectsPage shouldDisplayAllProjects() {
+ assertThat(url()).endsWith("/projects");
+ return this;
+ }
+
+ public ProjectsPage shouldDisplayAllProjectsWidthSort(String sort) {
+ assertThat(url()).endsWith("/projects?sort=" + sort);
+ return this;
+ }
+
+ public ProjectsPage shouldDisplayFavoriteProjects() {
+ assertThat(url()).endsWith("/projects/favorite");
+ return this;
+ }
+
+ public ProjectsPage selectAllProjects() {
+ $("#all-projects").click();
+ return shouldDisplayAllProjects();
+ }
+
+ public ProjectsPage selectFavoriteProjects() {
+ $("#favorite-projects").click();
+ return shouldDisplayFavoriteProjects();
+ }
+
+ public ProjectsPage searchProject(String search) {
+ SelenideElement searchInput = $(".projects-topbar-item-search input");
+ searchInput.setValue("").sendKeys(search);
+ return this;
+ }
+
+ public ProjectsPage changePerspective(String perspective) {
+ SelenideElement sortSelect = getOpenTopBar().$(".js-projects-perspective-select");
+ sortSelect.$(".Select-value").should(Condition.exist).click();
+ sortSelect.$(".Select-option[title='" + perspective + "']").should(Condition.exist).click();
+ return this;
+ }
+
+ public ProjectsPage sortProjects(String sort) {
+ SelenideElement sortSelect = getOpenTopBar().$(".js-projects-sorting-select");
+ sortSelect.$(".Select-value").should(Condition.exist).click();
+ sortSelect.$(".Select-option[title='" + sort + "']").should(Condition.exist).click();
+ return this;
+ }
+
+ public ProjectsPage invertSorting() {
+ getOpenTopBar().$(".js-projects-sorting-select a.button-icon").should(Condition.exist).click();
+ return this;
+ }
+
+ private SelenideElement getOpenTopBar() {
+ return $(".projects-topbar-items").should(Condition.exist);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/settings/PropertySetInput.java b/tests/src/test/java/org/sonarqube/pageobjects/settings/PropertySetInput.java
new file mode 100644
index 00000000000..4f1c7db3a32
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/settings/PropertySetInput.java
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects.settings;
+
+import com.codeborne.selenide.SelenideElement;
+
+import static com.codeborne.selenide.Condition.exist;
+
+public class PropertySetInput {
+
+ private final SelenideElement elt;
+
+ public PropertySetInput(SelenideElement elt) {
+ this.elt = elt;
+ }
+
+ public PropertySetInput setFieldValue(int index, String fieldKey, String value) {
+ elt.findAll("input[name$=\"[" + fieldKey + "]\"]").get(index).val(value);
+ return this;
+ }
+
+ public PropertySetInput setFieldValue(String fieldKey, String value) {
+ return setFieldValue(0, fieldKey, value);
+ }
+
+ public PropertySetInput save() {
+ elt.find(".js-save-changes").click();
+ elt.find(".js-save-changes").shouldNot(exist);
+ return this;
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/settings/SettingsPage.java b/tests/src/test/java/org/sonarqube/pageobjects/settings/SettingsPage.java
new file mode 100644
index 00000000000..1d1af886d1e
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/pageobjects/settings/SettingsPage.java
@@ -0,0 +1,84 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.pageobjects.settings;
+
+import com.codeborne.selenide.SelenideElement;
+import org.openqa.selenium.By;
+
+import static com.codeborne.selenide.Condition.cssClass;
+import static com.codeborne.selenide.Condition.exactValue;
+import static com.codeborne.selenide.Condition.exist;
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+
+public class SettingsPage {
+
+ public SettingsPage() {
+ $("#settings-page").shouldBe(visible);
+ }
+
+ public SettingsPage assertMenuContains(String categoryName) {
+ $(".side-tabs-menu").$(By.linkText(categoryName)).shouldBe(visible);
+ return this;
+ }
+
+ public SettingsPage assertSettingDisplayed(String settingKey) {
+ $(".settings-definition[data-key='" + settingKey + "']").shouldBe(visible);
+ return this;
+ }
+
+ public SettingsPage assertSettingNotDisplayed(String settingKey) {
+ $(".settings-definition[data-key='" + settingKey + "']").shouldNotBe(visible);
+ return this;
+ }
+
+ public SettingsPage openCategory(String categoryName) {
+ $(".side-tabs-menu").$(By.linkText(categoryName)).click();
+ return this;
+ }
+
+ public SettingsPage assertStringSettingValue(String settingKey, String value) {
+ $("input[name=\"settings[" + settingKey + "]\"]").shouldHave(exactValue(value));
+ return this;
+ }
+
+ public SettingsPage assertBooleanSettingValue(String settingKey, boolean value) {
+ SelenideElement toggle = $("button[name=\"settings[" + settingKey + "]\"]");
+ if (value) {
+ toggle.shouldHave(cssClass("boolean-toggle-on"));
+ } else {
+ toggle.shouldNotHave(cssClass("boolean-toggle-on"));
+ }
+ return this;
+ }
+
+ public SettingsPage setStringValue(String settingKey, String value) {
+ SelenideElement setting = $(".settings-definition[data-key=\"" + settingKey + "\"]");
+ setting.find("input").val(value);
+ setting.find(".js-save-changes").click();
+ setting.find(".js-save-changes").shouldNot(exist);
+ return this;
+ }
+
+ public PropertySetInput getPropertySetInput(String settingKey) {
+ SelenideElement setting = $(".settings-definition[data-key=\"" + settingKey + "\"]");
+ return new PropertySetInput(setting);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/Category1Suite.java b/tests/src/test/java/org/sonarqube/tests/Category1Suite.java
new file mode 100644
index 00000000000..b48b1283b8a
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/Category1Suite.java
@@ -0,0 +1,133 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.projectAdministration.ProjectVisibilityTest;
+import org.sonarqube.tests.user.UsersPageTest;
+import org.sonarqube.tests.authorisation.ExecuteAnalysisPermissionTest;
+import org.sonarqube.tests.authorisation.IssuePermissionTest;
+import org.sonarqube.tests.authorisation.PermissionSearchTest;
+import org.sonarqube.tests.authorisation.ProvisioningPermissionTest;
+import org.sonarqube.tests.authorisation.QualityProfileAdminPermissionTest;
+import org.sonarqube.tests.complexity.ComplexityMeasuresTest;
+import org.sonarqube.tests.customMeasure.CustomMeasuresTest;
+import org.sonarqube.tests.i18n.I18nTest;
+import org.sonarqube.tests.measure.MeasuresWsTest;
+import org.sonarqube.tests.measure.ProjectDashboardTest;
+import org.sonarqube.tests.measure.ProjectMeasuresPageTest;
+import org.sonarqube.tests.measure.DifferentialPeriodsTest;
+import org.sonarqube.tests.measure.SincePreviousVersionHistoryTest;
+import org.sonarqube.tests.measure.SinceXDaysHistoryTest;
+import org.sonarqube.tests.measure.TimeMachineTest;
+import org.sonarqube.tests.projectAdministration.BackgroundTasksTest;
+import org.sonarqube.tests.projectAdministration.BulkDeletionTest;
+import org.sonarqube.tests.projectAdministration.ProjectAdministrationTest;
+import org.sonarqube.tests.projectAdministration.ProjectLinksPageTest;
+import org.sonarqube.tests.projectSearch.ProjectsPageTest;
+import org.sonarqube.tests.qualityGate.QualityGateNotificationTest;
+import org.sonarqube.tests.qualityGate.QualityGateTest;
+import org.sonarqube.tests.qualityGate.QualityGateUiTest;
+import org.sonarqube.tests.settings.DeprecatedPropertiesWsTest;
+import org.sonarqube.tests.settings.EmailsTest;
+import org.sonarqube.tests.settings.PropertySetsTest;
+import org.sonarqube.tests.settings.SettingsTest;
+import org.sonarqube.tests.sourceCode.EncodingTest;
+import org.sonarqube.tests.sourceCode.HighlightingTest;
+import org.sonarqube.tests.sourceCode.ProjectCodeTest;
+import org.junit.ClassRule;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+import static util.ItUtils.pluginArtifact;
+import static util.ItUtils.xooPlugin;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+ // administration
+ UsersPageTest.class,
+ ProjectVisibilityTest.class,
+ // project administration
+ BulkDeletionTest.class,
+ ProjectAdministrationTest.class,
+ ProjectLinksPageTest.class,
+ BackgroundTasksTest.class,
+ // settings
+ DeprecatedPropertiesWsTest.class,
+ EmailsTest.class,
+ PropertySetsTest.class,
+ SettingsTest.class,
+ // i18n
+ I18nTest.class,
+ // quality gate
+ QualityGateTest.class,
+ QualityGateUiTest.class,
+ QualityGateNotificationTest.class,
+ // authorisation
+ ExecuteAnalysisPermissionTest.class,
+ IssuePermissionTest.class,
+ PermissionSearchTest.class,
+ ProvisioningPermissionTest.class,
+ QualityProfileAdminPermissionTest.class,
+ // custom measure
+ CustomMeasuresTest.class,
+ // measure
+ ProjectMeasuresPageTest.class,
+ ProjectDashboardTest.class,
+ ProjectsPageTest.class,
+ MeasuresWsTest.class,
+ // measure history
+ DifferentialPeriodsTest.class,
+ SincePreviousVersionHistoryTest.class,
+ SinceXDaysHistoryTest.class,
+ TimeMachineTest.class,
+ // source code
+ EncodingTest.class,
+ HighlightingTest.class,
+ ProjectCodeTest.class,
+ // complexity
+ ComplexityMeasuresTest.class
+})
+public class Category1Suite {
+
+ @ClassRule
+ public static final Orchestrator ORCHESTRATOR = Orchestrator.builderEnv()
+ .setServerProperty("sonar.notifications.delay", "1")
+ .addPlugin(pluginArtifact("property-sets-plugin"))
+ .addPlugin(pluginArtifact("sonar-subcategories-plugin"))
+
+ // Used in I18nTest
+ .addPlugin(pluginArtifact("l10n-fr-pack"))
+
+ // 1 second. Required for notification test.
+ .setServerProperty("sonar.notifications.delay", "1")
+
+ // Used in SettingsTest.global_property_change_extension_point
+ .addPlugin(pluginArtifact("global-property-change-plugin"))
+
+ // Used in SettingsTest.should_get_settings_default_value
+ .addPlugin(pluginArtifact("server-plugin"))
+
+ .addPlugin(pluginArtifact("posttask-plugin"))
+
+ .addPlugin(xooPlugin())
+ .build();
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/Category2Suite.java b/tests/src/test/java/org/sonarqube/tests/Category2Suite.java
new file mode 100644
index 00000000000..c101253c84a
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/Category2Suite.java
@@ -0,0 +1,114 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.issue.AutoAssignTest;
+import org.sonarqube.tests.issue.CommonRulesTest;
+import org.sonarqube.tests.issue.CustomRulesTest;
+import org.sonarqube.tests.issue.IssueActionTest;
+import org.sonarqube.tests.issue.IssueBulkChangeTest;
+import org.sonarqube.tests.issue.IssueChangelogTest;
+import org.sonarqube.tests.issue.IssueCreationTest;
+import org.sonarqube.tests.issue.IssueFilterExtensionTest;
+import org.sonarqube.tests.issue.IssueFilterOnCommonRulesTest;
+import org.sonarqube.tests.issue.IssueFilterTest;
+import org.sonarqube.tests.issue.IssueMeasureTest;
+import org.sonarqube.tests.issue.IssueNotificationsTest;
+import org.sonarqube.tests.issue.IssuePurgeTest;
+import org.sonarqube.tests.issue.IssueSearchTest;
+import org.sonarqube.tests.issue.IssueTrackingTest;
+import org.sonarqube.tests.issue.IssueWorkflowTest;
+import org.sonarqube.tests.issue.IssuesPageTest;
+import org.sonarqube.tests.issue.NewIssuesMeasureTest;
+import org.sonarqube.tests.qualityModel.MaintainabilityMeasureTest;
+import org.sonarqube.tests.qualityModel.MaintainabilityRatingMeasureTest;
+import org.sonarqube.tests.qualityModel.NewDebtRatioMeasureTest;
+import org.sonarqube.tests.qualityModel.ReliabilityMeasureTest;
+import org.sonarqube.tests.qualityModel.SecurityMeasureTest;
+import org.sonarqube.tests.qualityModel.TechnicalDebtInIssueChangelogTest;
+import org.sonarqube.tests.qualityModel.TechnicalDebtMeasureVariationTest;
+import org.sonarqube.tests.qualityModel.TechnicalDebtTest;
+import org.sonarqube.tests.scm.ScmTest;
+import org.sonarqube.tests.test.CoverageTest;
+import org.sonarqube.tests.test.CoverageTrackingTest;
+import org.sonarqube.tests.test.NewCoverageTest;
+import org.sonarqube.tests.test.TestExecutionTest;
+import org.junit.ClassRule;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+import static util.ItUtils.pluginArtifact;
+import static util.ItUtils.xooPlugin;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+ // test
+ CoverageTrackingTest.class,
+ CoverageTest.class,
+ NewCoverageTest.class,
+ TestExecutionTest.class,
+ // scm
+ ScmTest.class,
+ // issue
+ AutoAssignTest.class,
+ CommonRulesTest.class,
+ CustomRulesTest.class,
+ IssueActionTest.class,
+ IssueBulkChangeTest.class,
+ IssueChangelogTest.class,
+ IssueCreationTest.class,
+ IssueFilterOnCommonRulesTest.class,
+ IssueFilterTest.class,
+ IssueFilterExtensionTest.class,
+ IssueMeasureTest.class,
+ IssueNotificationsTest.class,
+ IssuePurgeTest.class,
+ IssueSearchTest.class,
+ IssueTrackingTest.class,
+ IssueWorkflowTest.class,
+ NewIssuesMeasureTest.class,
+ // debt
+ MaintainabilityMeasureTest.class,
+ MaintainabilityRatingMeasureTest.class,
+ NewDebtRatioMeasureTest.class,
+ ReliabilityMeasureTest.class,
+ SecurityMeasureTest.class,
+ TechnicalDebtInIssueChangelogTest.class,
+ TechnicalDebtMeasureVariationTest.class,
+ TechnicalDebtTest.class,
+ // ui
+ IssuesPageTest.class
+})
+public class Category2Suite {
+
+ @ClassRule
+ public static final Orchestrator ORCHESTRATOR = Orchestrator.builderEnv()
+ .addPlugin(xooPlugin())
+
+ // issue
+ .addPlugin(pluginArtifact("issue-filter-plugin"))
+
+ // 1 second. Required for notification test.
+ .setServerProperty("sonar.notifications.delay", "1")
+
+ .build();
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/Category3Suite.java b/tests/src/test/java/org/sonarqube/tests/Category3Suite.java
new file mode 100644
index 00000000000..25dd138a78e
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/Category3Suite.java
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.analysis.ExtensionLifecycleTest;
+import org.sonarqube.tests.analysis.FavoriteTest;
+import org.sonarqube.tests.analysis.IssueJsonReportTest;
+import org.sonarqube.tests.analysis.IssuesModeTest;
+import org.sonarqube.tests.analysis.LinksTest;
+import org.sonarqube.tests.analysis.MultiLanguageTest;
+import org.sonarqube.tests.analysis.PermissionTest;
+import org.sonarqube.tests.analysis.ProjectBuilderTest;
+import org.sonarqube.tests.analysis.ReportDumpTest;
+import org.sonarqube.tests.analysis.SSLTest;
+import org.sonarqube.tests.analysis.ScannerTest;
+import org.sonarqube.tests.analysis.SettingsEncryptionTest;
+import org.sonarqube.tests.analysis.TempFolderTest;
+import org.sonarqube.tests.measure.DecimalScaleMetricTest;
+import org.sonarqube.tests.plugins.VersionPluginTest;
+import org.sonarqube.tests.webhook.WebhooksTest;
+import org.junit.ClassRule;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+import static util.ItUtils.pluginArtifact;
+import static util.ItUtils.xooPlugin;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+ // analysis
+ PermissionTest.class,
+ ExtensionLifecycleTest.class,
+ LinksTest.class,
+ ProjectBuilderTest.class,
+ TempFolderTest.class,
+ MultiLanguageTest.class,
+ IssueJsonReportTest.class,
+ ScannerTest.class,
+ IssuesModeTest.class,
+ VersionPluginTest.class,
+ SettingsEncryptionTest.class,
+ ReportDumpTest.class,
+ SSLTest.class,
+ FavoriteTest.class,
+ // measures
+ DecimalScaleMetricTest.class,
+ WebhooksTest.class
+})
+public class Category3Suite {
+
+ @ClassRule
+ public static final Orchestrator ORCHESTRATOR = Orchestrator.builderEnv()
+ .addPlugin(xooPlugin())
+ .setOrchestratorProperty("javaVersion", "LATEST_RELEASE").addPlugin("java")
+
+ // Used by SettingsEncryptionTest
+ .addPlugin(pluginArtifact("settings-encryption-plugin"))
+
+ // Used by IssuesModeTest
+ .addPlugin(pluginArtifact("access-secured-props-plugin"))
+
+ // used by TempFolderTest and DecimalScaleMetricTest
+ .addPlugin(pluginArtifact("batch-plugin"))
+
+ // used by ExtensionLifecycleTest
+ .addPlugin(pluginArtifact("extension-lifecycle-plugin"))
+
+ // used by ProjectBuilderTest
+ .addPlugin(pluginArtifact("project-builder-plugin"))
+
+ .build();
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/Category4Suite.java b/tests/src/test/java/org/sonarqube/tests/Category4Suite.java
new file mode 100644
index 00000000000..79eebb60fa6
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/Category4Suite.java
@@ -0,0 +1,124 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.analysis.FileExclusionsTest;
+import org.sonarqube.tests.analysis.IssueExclusionsTest;
+import org.sonarqube.tests.component.ComponentsWsTest;
+import org.sonarqube.tests.component.ProjectsWsTest;
+import org.sonarqube.tests.dbCleaner.PurgeTest;
+import org.sonarqube.tests.duplication.CrossProjectDuplicationsOnRemoveFileTest;
+import org.sonarqube.tests.duplication.CrossProjectDuplicationsTest;
+import org.sonarqube.tests.duplication.DuplicationsTest;
+import org.sonarqube.tests.duplication.NewDuplicationsTest;
+import org.sonarqube.tests.organization.RootUserTest;
+import org.sonarqube.tests.projectEvent.EventTest;
+import org.sonarqube.tests.projectEvent.ProjectActivityPageTest;
+import org.sonarqube.tests.qualityProfile.QualityProfilesUiTest;
+import org.sonarqube.tests.serverSystem.HttpHeadersTest;
+import org.sonarqube.tests.serverSystem.LogsTest;
+import org.sonarqube.tests.serverSystem.PingTest;
+import org.sonarqube.tests.serverSystem.ServerSystemTest;
+import org.sonarqube.tests.ui.SourceViewerTest;
+import org.sonarqube.tests.ui.UiTest;
+import org.sonarqube.tests.ui.UiExtensionsTest;
+import org.sonarqube.tests.user.BaseIdentityProviderTest;
+import org.sonarqube.tests.user.FavoritesWsTest;
+import org.sonarqube.tests.user.ForceAuthenticationTest;
+import org.sonarqube.tests.user.LocalAuthenticationTest;
+import org.sonarqube.tests.user.MyAccountPageTest;
+import org.sonarqube.tests.user.OAuth2IdentityProviderTest;
+import org.sonarqube.tests.ws.WsLocalCallTest;
+import org.sonarqube.tests.ws.WsTest;
+import org.junit.ClassRule;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+import static util.ItUtils.pluginArtifact;
+import static util.ItUtils.xooPlugin;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+ // organization
+ RootUserTest.class,
+ // server system
+ ServerSystemTest.class,
+ PingTest.class,
+ // user
+ MyAccountPageTest.class,
+ FavoritesWsTest.class,
+ // authentication
+ ForceAuthenticationTest.class,
+ LocalAuthenticationTest.class,
+ BaseIdentityProviderTest.class,
+ OAuth2IdentityProviderTest.class,
+ // component search
+ ProjectsWsTest.class,
+ ComponentsWsTest.class,
+ // analysis exclusion
+ FileExclusionsTest.class,
+ IssueExclusionsTest.class,
+ // duplication
+ CrossProjectDuplicationsTest.class,
+ CrossProjectDuplicationsOnRemoveFileTest.class,
+ DuplicationsTest.class,
+ NewDuplicationsTest.class,
+ // db cleaner
+ PurgeTest.class,
+ // project event
+ EventTest.class,
+ ProjectActivityPageTest.class,
+ // http
+ HttpHeadersTest.class,
+ // ui
+ UiTest.class,
+ SourceViewerTest.class,
+ // ui extensions
+ UiExtensionsTest.class,
+ WsLocalCallTest.class,
+ WsTest.class,
+ // quality profiles
+ QualityProfilesUiTest.class,
+ LogsTest.class
+})
+public class Category4Suite {
+
+ @ClassRule
+ public static final Orchestrator ORCHESTRATOR = Orchestrator.builderEnv()
+ .addPlugin(xooPlugin())
+
+ // Used in BaseIdentityProviderTest
+ .addPlugin(pluginArtifact("base-auth-plugin"))
+
+ // Used in OAuth2IdentityProviderTest
+ .addPlugin(pluginArtifact("oauth2-auth-plugin"))
+
+ // Used in UiExtensionsTest
+ .addPlugin(pluginArtifact("ui-extensions-plugin"))
+
+ // Used by WsLocalCallTest
+ .addPlugin(pluginArtifact("ws-plugin"))
+
+ // Used by LogsTest
+ .setServerProperty("sonar.web.accessLogs.pattern", LogsTest.ACCESS_LOGS_PATTERN)
+
+ .build();
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/Category5Suite.java b/tests/src/test/java/org/sonarqube/tests/Category5Suite.java
new file mode 100644
index 00000000000..283aea49471
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/Category5Suite.java
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests;
+
+import org.sonarqube.tests.serverSystem.ClusterTest;
+import org.sonarqube.tests.serverSystem.RestartTest;
+import org.sonarqube.tests.serverSystem.ServerSystemRestartingOrchestrator;
+import org.sonarqube.tests.settings.LicensesPageTest;
+import org.sonarqube.tests.settings.SettingsTestRestartingOrchestrator;
+import org.sonarqube.tests.updateCenter.UpdateCenterTest;
+import org.sonarqube.tests.user.OnboardingTest;
+import org.sonarqube.tests.user.RealmAuthenticationTest;
+import org.sonarqube.tests.user.SsoAuthenticationTest;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+/**
+ * This suite is reserved to the tests that start their own instance of Orchestrator.
+ * Indeed multiple instances of Orchestrator can't be started in parallel, so this
+ * suite does not declare a shared Orchestrator.
+ */
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+ ClusterTest.class,
+ ServerSystemRestartingOrchestrator.class,
+ RestartTest.class,
+ SettingsTestRestartingOrchestrator.class,
+ LicensesPageTest.class,
+ // update center
+ UpdateCenterTest.class,
+ RealmAuthenticationTest.class,
+ SsoAuthenticationTest.class,
+ OnboardingTest.class
+})
+public class Category5Suite {
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/Category6Suite.java b/tests/src/test/java/org/sonarqube/tests/Category6Suite.java
new file mode 100644
index 00000000000..96983945555
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/Category6Suite.java
@@ -0,0 +1,75 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.issue.IssueTagsTest;
+import org.sonarqube.tests.issue.OrganizationIssueAssignTest;
+import org.sonarqube.tests.organization.BillingTest;
+import org.sonarqube.tests.organization.OrganizationMembershipTest;
+import org.sonarqube.tests.organization.OrganizationMembershipUiTest;
+import org.sonarqube.tests.organization.OrganizationTest;
+import org.sonarqube.tests.organization.PersonalOrganizationTest;
+import org.sonarqube.tests.organization.RootUserOnOrganizationTest;
+import org.sonarqube.tests.projectSearch.LeakProjectsPageTest;
+import org.sonarqube.tests.projectSearch.SearchProjectsTest;
+import org.sonarqube.tests.qualityProfile.BuiltInQualityProfilesTest;
+import org.sonarqube.tests.qualityProfile.CustomQualityProfilesTest;
+import org.sonarqube.tests.qualityProfile.OrganizationQualityProfilesUiTest;
+import org.sonarqube.tests.ui.OrganizationUiExtensionsTest;
+import org.sonarqube.tests.user.OrganizationIdentityProviderTest;
+import org.junit.ClassRule;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+import static util.ItUtils.pluginArtifact;
+import static util.ItUtils.xooPlugin;
+
+/**
+ * This category is used only when organizations feature is activated
+ */
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+ OrganizationIdentityProviderTest.class,
+ OrganizationIssueAssignTest.class,
+ OrganizationMembershipTest.class,
+ OrganizationMembershipUiTest.class,
+ OrganizationQualityProfilesUiTest.class,
+ OrganizationTest.class,
+ RootUserOnOrganizationTest.class,
+ OrganizationUiExtensionsTest.class,
+ PersonalOrganizationTest.class,
+ BuiltInQualityProfilesTest.class,
+ CustomQualityProfilesTest.class,
+ BillingTest.class,
+ IssueTagsTest.class,
+ LeakProjectsPageTest.class,
+ SearchProjectsTest.class
+})
+public class Category6Suite {
+
+ @ClassRule
+ public static final Orchestrator ORCHESTRATOR = Orchestrator.builderEnv()
+ .addPlugin(xooPlugin())
+ .addPlugin(pluginArtifact("base-auth-plugin"))
+ .addPlugin(pluginArtifact("fake-billing-plugin"))
+ .addPlugin(pluginArtifact("ui-extensions-plugin"))
+ .build();
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/GroupTester.java b/tests/src/test/java/org/sonarqube/tests/GroupTester.java
new file mode 100644
index 00000000000..14568da774b
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/GroupTester.java
@@ -0,0 +1,102 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+import org.sonarqube.ws.Organizations;
+import org.sonarqube.ws.WsUserGroups;
+import org.sonarqube.ws.WsUsers;
+import org.sonarqube.ws.WsUsers.GroupsWsResponse.Group;
+import org.sonarqube.ws.client.user.GroupsRequest;
+import org.sonarqube.ws.client.usergroup.AddUserWsRequest;
+import org.sonarqube.ws.client.usergroup.CreateWsRequest;
+
+import static java.util.Arrays.stream;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class GroupTester {
+
+ private static final AtomicInteger ID_GENERATOR = new AtomicInteger();
+
+ private final Session session;
+
+ GroupTester(Session session) {
+ this.session = session;
+ }
+
+ @SafeVarargs
+ public final WsUserGroups.Group generate(@Nullable Organizations.Organization organization, Consumer<CreateWsRequest.Builder>... populators) {
+ int id = ID_GENERATOR.getAndIncrement();
+ CreateWsRequest.Builder request = CreateWsRequest.builder()
+ .setName("Group" + id)
+ .setDescription("Description " + id)
+ .setOrganization(organization != null ? organization.getKey() : null);
+ stream(populators).forEach(p -> p.accept(request));
+ return session.wsClient().userGroups().create(request.build()).getGroup();
+ }
+
+ public List<Group> getGroupsOfUser(@Nullable Organizations.Organization organization, String userLogin) {
+ GroupsRequest request = GroupsRequest.builder()
+ .setOrganization(organization != null ? organization.getKey() : null)
+ .setLogin(userLogin)
+ .build();
+ WsUsers.GroupsWsResponse response = session.users().service().groups(request);
+ return response.getGroupsList();
+ }
+
+ public GroupTester addMemberToGroups(Organizations.Organization organization, String userLogin, String... groups) {
+ for (String group : groups) {
+ AddUserWsRequest request = AddUserWsRequest.builder()
+ .setLogin(userLogin)
+ .setOrganization(organization.getKey())
+ .setName(group)
+ .build();
+ session.wsClient().userGroups().addUser(request);
+ }
+ return this;
+ }
+
+ public GroupTester assertThatUserIsMemberOf(@Nullable Organizations.Organization organization, String userLogin, String expectedGroup, String... otherExpectedGroups) {
+ List<String> groups = getGroupsOfUser(organization, userLogin)
+ .stream()
+ .map(Group::getName)
+ .collect(Collectors.toList());
+
+ assertThat(groups).contains(expectedGroup);
+ if (otherExpectedGroups.length > 0) {
+ assertThat(groups).contains(otherExpectedGroups);
+ }
+ return this;
+ }
+
+ public GroupTester assertThatUserIsOnlyMemberOf(@Nullable Organizations.Organization organization, String userLogin, String... expectedGroups) {
+ Set<String> groups = getGroupsOfUser(organization, userLogin)
+ .stream()
+ .map(Group::getName)
+ .collect(Collectors.toSet());
+ assertThat(groups).containsExactlyInAnyOrder(expectedGroups);
+ return this;
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/OrganizationTester.java b/tests/src/test/java/org/sonarqube/tests/OrganizationTester.java
new file mode 100644
index 00000000000..2072e87822a
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/OrganizationTester.java
@@ -0,0 +1,134 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import javax.annotation.Nullable;
+import org.sonarqube.ws.Organizations;
+import org.sonarqube.ws.WsUsers;
+import org.sonarqube.ws.client.HttpException;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.organization.CreateWsRequest;
+import org.sonarqube.ws.client.organization.OrganizationService;
+import org.sonarqube.ws.client.organization.SearchMembersWsRequest;
+import org.sonarqube.ws.client.organization.SearchWsRequest;
+import org.sonarqube.ws.client.user.GroupsRequest;
+
+import static java.util.Arrays.stream;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class OrganizationTester {
+
+ private static final AtomicInteger ID_GENERATOR = new AtomicInteger();
+
+ private final Session session;
+
+ OrganizationTester(Session session) {
+ this.session = session;
+ }
+
+ void enableSupport() {
+ session.wsClient().wsConnector().call(new PostRequest("api/organizations/enable_support"));
+ }
+
+ void deleteNonGuardedOrganizations() {
+ service().search(SearchWsRequest.builder().build()).getOrganizationsList()
+ .stream()
+ .filter(o -> !o.getKey().equals("default-organization"))
+ .forEach(organization -> service().delete(organization.getKey()));
+ }
+
+ @SafeVarargs
+ public final Organizations.Organization generate(Consumer<CreateWsRequest.Builder>... populators) {
+ int id = ID_GENERATOR.getAndIncrement();
+ CreateWsRequest.Builder request = new CreateWsRequest.Builder()
+ .setKey("org" + id)
+ .setName("Org " + id)
+ .setDescription("Description " + id)
+ .setUrl("http://test" + id);
+ stream(populators).forEach(p -> p.accept(request));
+ return service().create(request.build()).getOrganization();
+ }
+
+ public OrganizationTester addMember(Organizations.Organization organization, WsUsers.CreateWsResponse.User user) {
+ service().addMember(organization.getKey(), user.getLogin());
+ return this;
+ }
+
+ public OrganizationTester assertThatOrganizationDoesNotExist(String organizationKey) {
+ SearchWsRequest request = new SearchWsRequest.Builder().setOrganizations(organizationKey).build();
+ Organizations.SearchWsResponse searchWsResponse = service().search(request);
+ assertThat(searchWsResponse.getOrganizationsList()).isEmpty();
+ return this;
+ }
+
+ public OrganizationTester assertThatMemberOf(Organizations.Organization organization, WsUsers.CreateWsResponse.User user) {
+ return assertThatMemberOf(organization, user.getLogin());
+ }
+
+ public OrganizationTester assertThatMemberOf(Organizations.Organization organization, String userLogin) {
+ verifyOrganizationMembership(organization, userLogin, true);
+ verifyMembersGroupMembership(userLogin, organization, true);
+ return this;
+ }
+
+ public OrganizationTester assertThatNotMemberOf(Organizations.Organization organization, WsUsers.CreateWsResponse.User user) {
+ return assertThatNotMemberOf(organization, user.getLogin());
+ }
+
+ public OrganizationTester assertThatNotMemberOf(Organizations.Organization organization, String userLogin) {
+ verifyOrganizationMembership(organization, userLogin, false);
+ try {
+ verifyMembersGroupMembership(userLogin, organization, false);
+ } catch (HttpException e) {
+ // do not fail if user does not exist
+ if (e.code() != 404) {
+ throw e;
+ }
+ }
+ return this;
+ }
+
+ private void verifyOrganizationMembership(@Nullable Organizations.Organization organization, String userLogin, boolean isMember) {
+ List<Organizations.User> users = service().searchMembers(new SearchMembersWsRequest()
+ .setQuery(userLogin)
+ .setSelected("selected")
+ .setOrganization(organization != null ? organization.getKey() : null))
+ .getUsersList();
+ assertThat(users).hasSize(isMember ? 1 : 0);
+ }
+
+ private void verifyMembersGroupMembership(String userLogin, @Nullable Organizations.Organization organization, boolean isMember) {
+ List<WsUsers.GroupsWsResponse.Group> groups = session.wsClient().users().groups(GroupsRequest.builder()
+ .setLogin(userLogin)
+ .setOrganization(organization != null ? organization.getKey() : null)
+ .setQuery("Members")
+ .setSelected("selected")
+ .build())
+ .getGroupsList();
+ assertThat(groups).hasSize(isMember ? 1 : 0);
+ }
+
+ public OrganizationService service() {
+ return session.wsClient().organizations();
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/ProjectTester.java b/tests/src/test/java/org/sonarqube/tests/ProjectTester.java
new file mode 100644
index 00000000000..781500aa8c0
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/ProjectTester.java
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import javax.annotation.Nullable;
+import org.sonarqube.ws.Organizations;
+import org.sonarqube.ws.WsProjects;
+import org.sonarqube.ws.client.project.CreateRequest;
+import org.sonarqube.ws.client.project.DeleteRequest;
+import org.sonarqube.ws.client.project.ProjectsService;
+import org.sonarqube.ws.client.project.SearchWsRequest;
+
+import static java.util.Arrays.stream;
+import static java.util.Collections.singletonList;
+
+public class ProjectTester {
+
+ private static final AtomicInteger ID_GENERATOR = new AtomicInteger();
+
+ private final Session session;
+
+ ProjectTester(Session session) {
+ this.session = session;
+ }
+
+ void deleteAll() {
+ ProjectsService service = session.wsClient().projects();
+ service.search(SearchWsRequest.builder().setQualifiers(singletonList("TRK")).build()).getComponentsList().forEach(p -> {
+ service.delete(DeleteRequest.builder().setKey(p.getKey()).build());
+ });
+ }
+
+ @SafeVarargs
+ public final WsProjects.CreateWsResponse.Project generate(@Nullable Organizations.Organization organization, Consumer<CreateRequest.Builder>... populators) {
+ int id = ID_GENERATOR.getAndIncrement();
+ CreateRequest.Builder request = CreateRequest.builder()
+ .setKey("key" + id)
+ .setName("Name " + id)
+ .setOrganization(organization != null ? organization.getKey() : null);
+ stream(populators).forEach(p -> p.accept(request));
+
+ return session.wsClient().projects().create(request.build()).getProject();
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/QProfileTester.java b/tests/src/test/java/org/sonarqube/tests/QProfileTester.java
new file mode 100644
index 00000000000..764b80d6955
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/QProfileTester.java
@@ -0,0 +1,118 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import org.sonarqube.ws.Common;
+import org.sonarqube.ws.Organizations.Organization;
+import org.sonarqube.ws.QualityProfiles.CreateWsResponse.QualityProfile;
+import org.sonarqube.ws.Rules;
+import org.sonarqube.ws.client.HttpException;
+import org.sonarqube.ws.client.qualityprofile.ActivateRuleWsRequest;
+import org.sonarqube.ws.client.qualityprofile.CreateRequest;
+import org.sonarqube.ws.client.qualityprofile.QualityProfilesService;
+import org.sonarqube.ws.client.rule.SearchWsRequest;
+
+import static java.util.Arrays.asList;
+import static java.util.Arrays.stream;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class QProfileTester {
+ private static final AtomicInteger ID_GENERATOR = new AtomicInteger();
+
+ private final Session session;
+
+ QProfileTester(Session session) {
+ this.session = session;
+ }
+
+ public QualityProfilesService service() {
+ return session.wsClient().qualityProfiles();
+ }
+
+ @SafeVarargs
+ public final QualityProfile createXooProfile(Organization organization, Consumer<CreateRequest.Builder>... populators) {
+ int id = ID_GENERATOR.getAndIncrement();
+ CreateRequest.Builder request = CreateRequest.builder()
+ .setOrganizationKey(organization.getKey())
+ .setLanguage("xoo")
+ .setProfileName("Profile" + id);
+ stream(populators).forEach(p -> p.accept(request));
+ return service().create(request.build()).getProfile();
+ }
+
+ public QProfileTester activateRule(QualityProfile profile, String ruleKey) {
+ return activateRule(profile.getKey(), ruleKey);
+ }
+
+ public QProfileTester activateRule(String profileKey, String ruleKey) {
+ ActivateRuleWsRequest request = ActivateRuleWsRequest.builder()
+ .setProfileKey(profileKey)
+ .setRuleKey(ruleKey)
+ .build();
+ service().activateRule(request);
+ return this;
+ }
+
+ public QProfileTester deactivateRule(QualityProfile profile, String ruleKey) {
+ service().deactivateRule(profile.getKey(), ruleKey);
+ return this;
+ }
+
+ public QProfileTester assertThatNumberOfActiveRulesEqualsTo(QualityProfile profile, int expectedActiveRules) {
+ return assertThatNumberOfActiveRulesEqualsTo(profile.getKey(), expectedActiveRules);
+ }
+
+ public QProfileTester assertThatNumberOfActiveRulesEqualsTo(String profileKey, int expectedActiveRules) {
+ try {
+ List<String> facetIds = asList("active_severities", "repositories", "languages", "severities", "statuses", "types");
+ SearchWsRequest request = new SearchWsRequest()
+ .setQProfile(profileKey)
+ .setActivation(true)
+ .setFacets(facetIds)
+ .setFields(singletonList("actives"));
+ Rules.SearchResponse response = session.wsClient().rules().search(request);
+
+ // assume that expectedActiveRules fits in first page of results
+ assertThat(response.getRulesCount()).isEqualTo(expectedActiveRules);
+ assertThat(response.getTotal()).isEqualTo(expectedActiveRules);
+ assertThat(response.getActives().getActives()).as(response.toString()).hasSize(expectedActiveRules);
+
+ // verify facets
+ assertThat(response.getFacets().getFacetsCount()).isEqualTo(facetIds.size());
+ response.getFacets().getFacetsList().forEach(facet -> {
+ long total = facet.getValuesList().stream()
+ .mapToLong(Common.FacetValue::getCount)
+ .sum();
+ assertThat(total).as("Facet " + facet.getProperty()).isEqualTo((long) expectedActiveRules);
+ });
+ } catch (HttpException e) {
+ if (expectedActiveRules == 0 && e.code() == 404) {
+ // profile does not exist, do nothing
+ return this;
+ }
+ throw e;
+ }
+ return this;
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/Session.java b/tests/src/test/java/org/sonarqube/tests/Session.java
new file mode 100644
index 00000000000..6889f873b7e
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/Session.java
@@ -0,0 +1,38 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests;
+
+import org.sonarqube.ws.client.WsClient;
+
+public interface Session {
+
+ WsClient wsClient();
+
+ GroupTester groups();
+
+ OrganizationTester organizations();
+
+ ProjectTester projects();
+
+ QProfileTester qProfiles();
+
+ UserTester users();
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/Tester.java b/tests/src/test/java/org/sonarqube/tests/Tester.java
new file mode 100644
index 00000000000..f6a8f55162a
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/Tester.java
@@ -0,0 +1,201 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests;
+
+import com.sonar.orchestrator.Orchestrator;
+import javax.annotation.Nullable;
+import org.junit.rules.ExternalResource;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.pageobjects.Navigation;
+import util.selenium.Selenese;
+
+import static util.ItUtils.newUserWsClient;
+
+/**
+ * This JUnit rule wraps an {@link Orchestrator} instance and provides :
+ * <ul>
+ * <li>enabling the organization feature by default</li>
+ * <li>clean-up of organizations between tests</li>
+ * <li>clean-up of users between tests</li>
+ * <li>clean-up of session when opening a browser (cookies, local storage)</li>
+ * <li>quick access to {@link WsClient} instances</li>
+ * <li>helpers to generate organizations and users</li>
+ * </ul>
+ *
+ * Recommendation is to define a {@code @Rule} instance. If not possible, then
+ * {@code @ClassRule} must be used through a {@link org.junit.rules.RuleChain}
+ * around {@link Orchestrator}.
+ */
+public class Tester extends ExternalResource implements Session {
+
+ private final Orchestrator orchestrator;
+
+ // configuration before startup
+ private boolean disableOrganizations = false;
+
+ // initialized in #before()
+ private boolean beforeCalled = false;
+ private Session rootSession;
+
+ public Tester(Orchestrator orchestrator) {
+ this.orchestrator = orchestrator;
+ }
+
+ public Tester disableOrganizations() {
+ verifyNotStarted();
+ disableOrganizations = true;
+ return this;
+ }
+
+ @Override
+ protected void before() {
+ verifyNotStarted();
+ rootSession = new SessionImpl(orchestrator, "admin", "admin");
+
+ if (!disableOrganizations) {
+ organizations().enableSupport();
+ }
+
+ beforeCalled = true;
+ }
+
+ @Override
+ protected void after() {
+ if (!disableOrganizations) {
+ organizations().deleteNonGuardedOrganizations();
+ }
+ users().deleteAll();
+ projects().deleteAll();
+ }
+
+ public Session asAnonymous() {
+ return as(null, null);
+ }
+
+ public Session as(String login) {
+ return as(login, login);
+ }
+
+ public Session as(String login, String password) {
+ verifyStarted();
+ return new SessionImpl(orchestrator, login, password);
+ }
+
+ /**
+ * Open a new browser session. Cookies are deleted.
+ */
+ public Navigation openBrowser() {
+ verifyStarted();
+ return Navigation.create(orchestrator);
+ }
+
+ /**
+ * @deprecated use Selenide tests with {@link #openBrowser()}
+ */
+ @Deprecated
+ public Tester runHtmlTests(String... htmlTests) {
+ Selenese.runSelenese(orchestrator, htmlTests);
+ return this;
+ }
+
+ private void verifyNotStarted() {
+ if (beforeCalled) {
+ throw new IllegalStateException("Orchestrator should not be already started");
+ }
+ }
+
+ private void verifyStarted() {
+ if (!beforeCalled) {
+ throw new IllegalStateException("Orchestrator is not started yet");
+ }
+ }
+
+ /**
+ * Web service client configured with root access
+ */
+ @Override
+ public WsClient wsClient() {
+ verifyStarted();
+ return rootSession.wsClient();
+ }
+
+ @Override
+ public GroupTester groups() {
+ return rootSession.groups();
+ }
+
+ @Override
+ public OrganizationTester organizations() {
+ return rootSession.organizations();
+ }
+
+ @Override
+ public ProjectTester projects() {
+ return rootSession.projects();
+ }
+
+ @Override
+ public QProfileTester qProfiles() {
+ return rootSession.qProfiles();
+ }
+
+ @Override
+ public UserTester users() {
+ return rootSession.users();
+ }
+
+ private static class SessionImpl implements Session {
+ private final WsClient client;
+
+ private SessionImpl(Orchestrator orchestrator, @Nullable String login, @Nullable String password) {
+ this.client = newUserWsClient(orchestrator, login, password);
+ }
+
+ @Override
+ public WsClient wsClient() {
+ return client;
+ }
+
+ @Override
+ public GroupTester groups() {
+ return new GroupTester(this);
+ }
+
+ @Override
+ public OrganizationTester organizations() {
+ return new OrganizationTester(this);
+ }
+
+ @Override
+ public ProjectTester projects() {
+ return new ProjectTester(this);
+ }
+
+ @Override
+ public QProfileTester qProfiles() {
+ return new QProfileTester(this);
+ }
+
+ @Override
+ public UserTester users() {
+ return new UserTester(this);
+ }
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/UserTester.java b/tests/src/test/java/org/sonarqube/tests/UserTester.java
new file mode 100644
index 00000000000..9be2a5d512c
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/UserTester.java
@@ -0,0 +1,101 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import org.sonarqube.ws.Organizations;
+import org.sonarqube.ws.WsUsers;
+import org.sonarqube.ws.WsUsers.CreateWsResponse.User;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.user.CreateRequest;
+import org.sonarqube.ws.client.user.SearchRequest;
+import org.sonarqube.ws.client.user.UsersService;
+import org.sonarqube.ws.client.usergroup.AddUserWsRequest;
+
+import static java.util.Arrays.stream;
+
+public class UserTester {
+
+ private static final AtomicInteger ID_GENERATOR = new AtomicInteger();
+
+ private final Session session;
+
+ UserTester(Session session) {
+ this.session = session;
+ }
+
+ void deleteAll() {
+ session.wsClient().users().search(SearchRequest.builder().build()).getUsersList()
+ .stream()
+ .filter(u -> !"admin".equals(u.getLogin()))
+ .forEach(u -> {
+ PostRequest request = new PostRequest("api/users/deactivate").setParam("login", u.getLogin());
+ session.wsClient().wsConnector().call(request);
+ });
+ }
+
+ @SafeVarargs
+ public final User generate(Consumer<CreateRequest.Builder>... populators) {
+ int id = ID_GENERATOR.getAndIncrement();
+ String login = "login" + id;
+ CreateRequest.Builder request = CreateRequest.builder()
+ .setLogin(login)
+ .setPassword(login)
+ .setName("name" + id)
+ .setEmail(id + "@test.com");
+ stream(populators).forEach(p -> p.accept(request));
+ return service().create(request.build()).getUser();
+ }
+
+ @SafeVarargs
+ public final User generateAdministrator(Consumer<CreateRequest.Builder>... populators) {
+ User user = generate(populators);
+ session.wsClient().permissions().addUser(new org.sonarqube.ws.client.permission.AddUserWsRequest().setLogin(user.getLogin()).setPermission("admin"));
+ session.wsClient().userGroups().addUser(org.sonarqube.ws.client.usergroup.AddUserWsRequest.builder().setLogin(user.getLogin()).setName("sonar-administrators").build());
+ return user;
+ }
+
+ @SafeVarargs
+ public final User generateAdministrator(Organizations.Organization organization, Consumer<CreateRequest.Builder>... populators) {
+ User user = generate(populators);
+ session.wsClient().organizations().addMember(organization.getKey(), user.getLogin());
+ session.wsClient().userGroups().addUser(AddUserWsRequest.builder()
+ .setOrganization(organization.getKey())
+ .setLogin(user.getLogin())
+ .setName("Owners")
+ .build());
+ return user;
+ }
+
+ public UsersService service() {
+ return session.wsClient().users();
+ }
+
+ public Optional<WsUsers.SearchWsResponse.User> getByLogin(String login) {
+ List<WsUsers.SearchWsResponse.User> users = session.wsClient().users().search(SearchRequest.builder().setQuery(login).build()).getUsersList();
+ if (users.size() == 1) {
+ return Optional.of(users.get(0));
+ }
+ return Optional.empty();
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/analysis/ExtensionLifecycleTest.java b/tests/src/test/java/org/sonarqube/tests/analysis/ExtensionLifecycleTest.java
new file mode 100644
index 00000000000..09c9a18fdea
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/analysis/ExtensionLifecycleTest.java
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.analysis;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.MavenBuild;
+import org.sonarqube.tests.Category3Suite;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import util.ItUtils;
+
+public class ExtensionLifecycleTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category3Suite.ORCHESTRATOR;
+
+ @Before
+ public void cleanup() {
+ orchestrator.resetData();
+ }
+
+ @Test
+ public void testInstantiationStrategyAndLifecycleOfBatchExtensions() {
+ MavenBuild build = MavenBuild.create(ItUtils.projectPom("analysis/extension-lifecycle"))
+ .setCleanSonarGoals()
+ .setProperty("extension.lifecycle", "true")
+ .setProperty("sonar.dynamicAnalysis", "false");
+
+ // Build fails if the extensions provided in the extension-lifecycle-plugin are not correctly
+ // managed.
+ orchestrator.executeBuild(build);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/analysis/FavoriteTest.java b/tests/src/test/java/org/sonarqube/tests/analysis/FavoriteTest.java
new file mode 100644
index 00000000000..f94f6aa2833
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/analysis/FavoriteTest.java
@@ -0,0 +1,124 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.analysis;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category3Suite;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.Favorites;
+import org.sonarqube.ws.Favorites.Favorite;
+import org.sonarqube.ws.WsPermissions;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.favorite.SearchRequest;
+import org.sonarqube.ws.client.permission.AddProjectCreatorToTemplateWsRequest;
+import org.sonarqube.ws.client.permission.RemoveProjectCreatorFromTemplateWsRequest;
+import org.sonarqube.ws.client.permission.SearchTemplatesWsRequest;
+
+import static com.sonar.orchestrator.container.Server.ADMIN_LOGIN;
+import static com.sonar.orchestrator.container.Server.ADMIN_PASSWORD;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.projectDir;
+
+public class FavoriteTest {
+ @ClassRule
+ public static Orchestrator orchestrator = Category3Suite.ORCHESTRATOR;
+ private static WsClient adminWsClient;
+
+ private static final String PROJECT_KEY = "sample";
+
+ @Before
+ public void setUp() {
+ orchestrator.resetData();
+ }
+
+ @After
+ public void tearDown() {
+ removeProjectCreatorPermission();
+ }
+
+ @BeforeClass
+ public static void classSetUp() {
+ adminWsClient = newAdminWsClient(orchestrator);
+ }
+
+ @Test
+ public void project_as_favorite_when_authenticated_and_first_analysis_and_a_project_creator_permission() {
+ SonarScanner sampleProject = createScannerWithUserCredentials();
+ addProjectCreatorPermission();
+
+ orchestrator.executeBuild(sampleProject);
+
+ Favorites.SearchResponse response = adminWsClient.favorites().search(new SearchRequest());
+ assertThat(response.getFavoritesList()).extracting(Favorite::getKey).contains(PROJECT_KEY);
+ }
+
+ @Test
+ public void no_project_as_favorite_when_no_project_creator_permission() {
+ SonarScanner sampleProject = createScannerWithUserCredentials();
+
+ orchestrator.executeBuild(sampleProject);
+
+ Favorites.SearchResponse response = adminWsClient.favorites().search(new SearchRequest());
+ assertThat(response.getFavoritesList()).extracting(Favorite::getKey).doesNotContain(PROJECT_KEY);
+ }
+
+ @Test
+ public void no_project_as_favorite_when_second_analysis() {
+ SonarScanner sampleProject = SonarScanner.create(projectDir("shared/xoo-sample"));
+ orchestrator.executeBuild(sampleProject);
+ sampleProject = createScannerWithUserCredentials();
+ addProjectCreatorPermission();
+
+ orchestrator.executeBuild(sampleProject);
+
+ Favorites.SearchResponse response = adminWsClient.favorites().search(new SearchRequest());
+ assertThat(response.getFavoritesList()).extracting(Favorite::getKey).doesNotContain(PROJECT_KEY);
+ }
+
+ private static SonarScanner createScannerWithUserCredentials() {
+ return SonarScanner.create(projectDir("shared/xoo-sample"))
+ .setProperty("sonar.login", ADMIN_LOGIN)
+ .setProperty("sonar.password", ADMIN_PASSWORD);
+ }
+
+ private void addProjectCreatorPermission() {
+ WsPermissions.SearchTemplatesWsResponse permissionTemplates = adminWsClient.permissions().searchTemplates(new SearchTemplatesWsRequest());
+ assertThat(permissionTemplates.getDefaultTemplatesCount()).isEqualTo(1);
+ adminWsClient.permissions().addProjectCreatorToTemplate(AddProjectCreatorToTemplateWsRequest.builder()
+ .setTemplateId(permissionTemplates.getDefaultTemplates(0).getTemplateId())
+ .setPermission("admin")
+ .build());
+ }
+
+ private void removeProjectCreatorPermission() {
+ WsPermissions.SearchTemplatesWsResponse permissionTemplates = adminWsClient.permissions().searchTemplates(new SearchTemplatesWsRequest());
+ assertThat(permissionTemplates.getDefaultTemplatesCount()).isEqualTo(1);
+ adminWsClient.permissions().removeProjectCreatorFromTemplate(RemoveProjectCreatorFromTemplateWsRequest.builder()
+ .setTemplateId(permissionTemplates.getDefaultTemplates(0).getTemplateId())
+ .setPermission("admin")
+ .build());
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/analysis/FileExclusionsTest.java b/tests/src/test/java/org/sonarqube/tests/analysis/FileExclusionsTest.java
new file mode 100644
index 00000000000..c5e11147036
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/analysis/FileExclusionsTest.java
@@ -0,0 +1,139 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.analysis;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category4Suite;
+import java.util.List;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.WsComponents.Component;
+import org.sonarqube.ws.client.component.TreeWsRequest;
+import util.ItUtils;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.getMeasuresAsDoubleByMetricKey;
+import static util.ItUtils.newWsClient;
+
+public class FileExclusionsTest {
+ static final String PROJECT = "exclusions";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+
+ @Before
+ public void resetData() {
+ orchestrator.resetData();
+ }
+
+ @Test
+ public void exclude_source_files() {
+ scan(
+ "sonar.global.exclusions", "**/*Ignore*.xoo",
+ "sonar.exclusions", "**/*Exclude*.xoo,src/main/xoo/org/sonar/tests/packageToExclude/**",
+ "sonar.test.exclusions", "**/ClassTwoTest.xoo");
+
+ Map<String, Double> measures = getMeasuresAsDouble("ncloc", "files", "directories");
+ assertThat(measures.get("files").intValue()).isEqualTo(4);
+ assertThat(measures.get("ncloc").intValue()).isEqualTo(60);
+ assertThat(measures.get("directories").intValue()).isEqualTo(2);
+ }
+
+ /**
+ * SONAR-2444 / SONAR-3758
+ */
+ @Test
+ public void exclude_test_files() {
+ scan(
+ "sonar.global.exclusions", "**/*Ignore*.xoo",
+ "sonar.exclusions", "**/*Exclude*.xoo,org/sonar/tests/packageToExclude/**",
+ "sonar.test.exclusions", "**/ClassTwoTest.xoo");
+
+ List<Component> testFiles = getComponents("UTS");
+ assertThat(testFiles).hasSize(2);
+ assertThat(testFiles).extracting(Component::getName).doesNotContain("ClassTwoTest.xoo");
+ }
+
+ /**
+ * SONAR-1896
+ */
+ @Test
+ public void include_source_files() {
+ scan(
+ "sonar.dynamicAnalysis", "false",
+ "sonar.inclusions", "**/*One.xoo,**/*Two.xoo");
+
+ assertThat(getMeasuresAsDouble("files").get("files")).isEqualTo(2);
+
+ List<Component> sourceFiles = getComponents("FIL");
+ assertThat(sourceFiles).hasSize(2);
+ assertThat(sourceFiles).extracting(Component::getName).containsOnly("ClassOne.xoo", "ClassTwo.xoo");
+ }
+
+ /**
+ * SONAR-1896
+ */
+ @Test
+ public void include_test_files() {
+ scan("sonar.test.inclusions", "src/test/xoo/**/*One*.xoo,src/test/xoo/**/*Two*.xoo");
+
+ assertThat(getMeasuresAsDouble("tests").get("tests")).isEqualTo(2);
+
+ List<Component> testFiles = getComponents("UTS");
+ assertThat(testFiles).hasSize(2);
+ assertThat(testFiles).extracting(Component::getName).containsOnly("ClassOneTest.xoo", "ClassTwoTest.xoo");
+ }
+
+ /**
+ * SONAR-2760
+ */
+ @Test
+ public void include_and_exclude_files_by_absolute_path() {
+ scan(
+ // includes everything except ClassOnDefaultPackage
+ "sonar.inclusions", "file:**/src/main/xoo/org/**/*.xoo",
+
+ // exclude ClassThree and ClassToExclude
+ "sonar.exclusions", "file:**/src/main/xoo/org/**/packageToExclude/*.xoo,file:**/src/main/xoo/org/**/*Exclude.xoo");
+
+ List<Component> sourceFiles = getComponents("FIL");
+ assertThat(sourceFiles).hasSize(4);
+ assertThat(sourceFiles).extracting(Component::getName).containsOnly("ClassOne.xoo", "ClassToIgnoreGlobally.xoo", "ClassTwo.xoo", "NoSonarComment.xoo");
+ }
+
+ static Map<String, Double> getMeasuresAsDouble(String... metricKeys) {
+ return getMeasuresAsDoubleByMetricKey(orchestrator, PROJECT, metricKeys);
+ }
+
+ private void scan(String... properties) {
+ SonarScanner build = SonarScanner
+ .create(ItUtils.projectDir("exclusions/exclusions"))
+ .setProperties(properties);
+ orchestrator.executeBuild(build);
+ }
+
+ public static List<Component> getComponents(String qualifier) {
+ return newWsClient(orchestrator).components().tree(new TreeWsRequest().setBaseComponentKey(PROJECT).setQualifiers(singletonList(qualifier))).getComponentsList();
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/analysis/IssueExclusionsTest.java b/tests/src/test/java/org/sonarqube/tests/analysis/IssueExclusionsTest.java
new file mode 100644
index 00000000000..615a5a2aeb6
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/analysis/IssueExclusionsTest.java
@@ -0,0 +1,263 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.analysis;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.BuildResult;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category4Suite;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.getMeasuresAsDoubleByMetricKey;
+
+public class IssueExclusionsTest {
+
+ private static final String PROJECT_KEY = "com.sonarsource.it.samples:multi-modules-exclusions";
+ private static final String PROJECT_DIR = "exclusions/xoo-multi-modules";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+
+ @Before
+ public void resetData() {
+ orchestrator.resetData();
+ }
+
+ @Test
+ public void should_not_exclude_anything() {
+ scan();
+
+ checkIssueCountBySeverity(67, 2, 57, 4, 0, 4);
+ }
+
+ @Test
+ public void should_ignore_all_files() {
+ scan(
+ "sonar.issue.ignore.multicriteria", "1",
+ "sonar.issue.ignore.multicriteria.1.resourceKey", "**/*.xoo",
+ "sonar.issue.ignore.multicriteria.1.ruleKey", "*");
+
+ checkIssueCountBySeverity(4, 0, 0, 0, 0, 4);
+ }
+
+ @Test
+ public void should_enforce_only_on_one_file() {
+ scan(
+ "sonar.issue.enforce.multicriteria", "1",
+ "sonar.issue.enforce.multicriteria.1.resourceKey", "**/HelloA1.xoo",
+ "sonar.issue.enforce.multicriteria.1.ruleKey", "*");
+
+ checkIssueCountBySeverity(
+ 1 /* tag */ + 18 /* lines in HelloA1.xoo */ + 1 /* file */,
+ 0 + 1,
+ 0 + 18,
+ 0 + 1,
+ 0,
+ 0);
+ }
+
+ @Test
+ public void should_enforce_on_two_files_with_same_rule() {
+ scan(
+ "sonar.issue.enforce.multicriteria", "1,2",
+ "sonar.issue.enforce.multicriteria.1.resourceKey", "**/HelloA1.xoo",
+ "sonar.issue.enforce.multicriteria.1.ruleKey", "*",
+ "sonar.issue.enforce.multicriteria.2.resourceKey", "**/HelloA2.xoo",
+ "sonar.issue.enforce.multicriteria.2.ruleKey", "*");
+
+ checkIssueCountBySeverity(
+ 2 /* tags */ + 18 /* lines in HelloA1.xoo */ + 15 /* lines in HelloA2.xoo */ + 2 /* files */,
+ 0 + 2,
+ 0 + 18 + 15,
+ 0 + 2,
+ 0,
+ 0);
+ }
+
+ @Test
+ public void should_enforce_on_two_files_with_different_rule() {
+ scan(
+ "sonar.issue.enforce.multicriteria", "1,2",
+ "sonar.issue.enforce.multicriteria.1.resourceKey", "**/HelloA1.xoo",
+ "sonar.issue.enforce.multicriteria.1.ruleKey", "xoo:OneIssuePerLine",
+ "sonar.issue.enforce.multicriteria.2.resourceKey", "**/HelloA2.xoo",
+ "sonar.issue.enforce.multicriteria.2.ruleKey", "xoo:HasTag");
+
+ checkIssueCountBySeverity(
+ 1 /* tag in HelloA2 */ + 18 /* lines in HelloA1.xoo */ + 4 /* files */ + 4 /* modules */,
+ 0 + 1,
+ 0 + 18,
+ 4,
+ 0,
+ 4);
+ }
+
+ @Test
+ public void should_ignore_files_with_regexp() {
+ scan(
+ "sonar.issue.ignore.allfile", "1",
+ "sonar.issue.ignore.allfile.1.fileRegexp", "EXTERMINATE-ALL-ISSUES");
+
+ checkIssueCountBySeverity(
+ 67 - 1 /* tag */ - 18 /* lines in HelloA1.xoo */ - 1 /* file */,
+ 2 - 1,
+ 57 - 18,
+ 4 - 1,
+ 0,
+ 4);
+ }
+
+ @Test
+ public void should_ignore_block_with_regexp() {
+ scan(
+ "sonar.issue.ignore.block", "1",
+ "sonar.issue.ignore.block.1.beginBlockRegexp", "MUTE-SONAR",
+ "sonar.issue.ignore.block.1.endBlockRegexp", "UNMUTE-SONAR");
+
+ checkIssueCountBySeverity(
+ 67 - 1 /* tag */ - 5 /* lines in HelloA2.xoo */,
+ 2 - 1,
+ 57 - 5,
+ 4,
+ 0,
+ 4);
+ }
+
+ @Test
+ public void should_ignore_to_end_of_file() {
+ scan(
+ "sonar.issue.ignore.block", "1",
+ "sonar.issue.ignore.block.1.beginBlockRegexp", "MUTE-SONAR",
+ "sonar.issue.ignore.block.1.endBlockRegexp", "");
+
+ checkIssueCountBySeverity(
+ 67 - 1 /* tag */ - 7 /* remaining lines in HelloA2.xoo */,
+ 2 - 1,
+ 57 - 7,
+ 4,
+ 0,
+ 4);
+ }
+
+ @Test
+ public void should_ignore_one_per_line_on_single_package() {
+ scan(
+ "sonar.issue.ignore.multicriteria", "1",
+ "sonar.issue.ignore.multicriteria.1.resourceKey", "**/com/sonar/it/samples/modules/a1/*",
+ "sonar.issue.ignore.multicriteria.1.ruleKey", "xoo:OneIssuePerLine");
+
+ checkIssueCountBySeverity(
+ 67 - 18 /* lines in HelloA1.xoo */,
+ 2,
+ 57 - 18,
+ 4,
+ 0,
+ 4);
+ }
+
+ @Test
+ public void should_apply_exclusions_from_multiple_sources() {
+ scan(
+ "sonar.issue.ignore.allfile", "1",
+ "sonar.issue.ignore.allfile.1.fileRegexp", "EXTERMINATE-ALL-ISSUES",
+ "sonar.issue.ignore.block", "1",
+ "sonar.issue.ignore.block.1.beginBlockRegexp", "MUTE-SONAR",
+ "sonar.issue.ignore.block.1.endBlockRegexp", "UNMUTE-SONAR",
+ "sonar.issue.ignore.multicriteria", "1",
+ "sonar.issue.ignore.multicriteria.1.resourceKey", "**/com/sonar/it/samples/modules/b1/*",
+ "sonar.issue.ignore.multicriteria.1.ruleKey", "xoo:OneIssuePerLine");
+
+ checkIssueCountBySeverity(
+ 67 - 1 /* tag in HelloA1.xoo */ - 1 /* tag in HelloA2.xoo */
+ - 18 /* lines in HelloA1.xoo */ - 5 /* lines in HelloA2.xoo */ - 12 /* lines in HelloB1.xoo */
+ - 1 /* HelloA1.xoo file */,
+ 0,
+ 57 - 18 - 5 - 12,
+ 4 - 1,
+ 0,
+ 4);
+ }
+
+ @Test
+ public void should_log_missing_resource_key() {
+ checkAnalysisFails(
+ "sonar.issue.ignore.multicriteria", "1",
+ "sonar.issue.ignore.multicriteria.1.resourceKey", "",
+ "sonar.issue.ignore.multicriteria.1.ruleKey", "*");
+ }
+
+ @Test
+ public void should_log_missing_rule_key() {
+ checkAnalysisFails(
+ "sonar.issue.ignore.multicriteria", "1",
+ "sonar.issue.ignore.multicriteria.1.resourceKey", "*",
+ "sonar.issue.ignore.multicriteria.1.ruleKey", "");
+ }
+
+ @Test
+ public void should_log_missing_block_start() {
+ checkAnalysisFails(
+ "sonar.issue.ignore.block", "1",
+ "sonar.issue.ignore.block.1.beginBlockRegexp", "",
+ "sonar.issue.ignore.block.1.endBlockRegexp", "UNMUTE-SONAR");
+ }
+
+ @Test
+ public void should_log_missing_whole_file_regexp() {
+ checkAnalysisFails(
+ "sonar.issue.ignore.allfile", "1",
+ "sonar.issue.ignore.allfile.1.fileRegexp", "");
+ }
+
+ protected BuildResult scan(String... properties) {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/exclusions/IssueExclusionsTest/with-many-rules.xml"));
+ orchestrator.getServer().provisionProject("com.sonarsource.it.samples:multi-modules-exclusions",
+ "Sonar :: Integration Tests :: Multi-modules With Exclusions");
+ orchestrator.getServer().associateProjectToQualityProfile("com.sonarsource.it.samples:multi-modules-exclusions", "xoo", "with-many-rules");
+
+ SonarScanner scan = SonarScanner.create(ItUtils.projectDir(PROJECT_DIR))
+ .setProperty("sonar.cpd.exclusions", "**/*")
+ .setProperties(properties)
+ .setProperty("sonar.verbose", "true");
+ return orchestrator.executeBuildQuietly(scan);
+ }
+
+ private void checkIssueCountBySeverity(int total, int taggedXoo, int perLine, int perFile, int blocker, int perModule) {
+ Map<String, Double> measures = getMeasuresAsDoubleByMetricKey(orchestrator, PROJECT_KEY, "violations", "info_violations", "minor_violations", "major_violations",
+ "blocker_violations", "critical_violations");
+ assertThat(measures.get("violations").intValue()).isEqualTo(total);
+ assertThat(measures.get("info_violations").intValue()).isEqualTo(taggedXoo); // Has tag 'xoo'
+ assertThat(measures.get("minor_violations").intValue()).isEqualTo(perLine); // One per line
+ assertThat(measures.get("major_violations").intValue()).isEqualTo(perFile); // One per file
+ assertThat(measures.get("blocker_violations").intValue()).isEqualTo(blocker);
+ assertThat(measures.get("critical_violations").intValue()).isEqualTo(perModule); // One per module
+ }
+
+ private void checkAnalysisFails(String... properties) {
+ BuildResult buildResult = scan(properties);
+ assertThat(buildResult.getStatus()).isNotEqualTo(0);
+ assertThat(buildResult.getLogs().indexOf("IllegalStateException")).isGreaterThan(0);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/analysis/IssueJsonReportTest.java b/tests/src/test/java/org/sonarqube/tests/analysis/IssueJsonReportTest.java
new file mode 100644
index 00000000000..13ccd9ac4c5
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/analysis/IssueJsonReportTest.java
@@ -0,0 +1,313 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.analysis;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.BuildResult;
+import com.sonar.orchestrator.build.SonarScanner;
+import com.sonar.orchestrator.locator.FileLocation;
+import com.sonar.orchestrator.locator.ResourceLocation;
+import org.sonarqube.tests.Category3Suite;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.WsResponse;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newWsClient;
+
+public class IssueJsonReportTest {
+
+ private static final String SONAR_VERSION_PLACEHOLDER = "<SONAR_VERSION>";
+ private static final String RESOURCE_PATH = "/analysis/IssueJsonReportTest/";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category3Suite.ORCHESTRATOR;
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Before
+ public void resetData() {
+ orchestrator.resetData();
+ }
+
+ @Test
+ public void issue_line() throws IOException {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource(RESOURCE_PATH + "one-issue-per-line.xml"));
+ orchestrator.getServer().provisionProject("sample", "xoo-sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");
+
+ File projectDir = ItUtils.projectDir("shared/xoo-sample");
+ SonarScanner runner = SonarScanner.create(projectDir,
+ "sonar.analysis.mode", "issues",
+ "sonar.verbose", "true",
+ "sonar.report.export.path", "sonar-report.json");
+ BuildResult result = orchestrator.executeBuild(runner);
+ assertThat(ItUtils.countIssuesInJsonReport(result, true)).isEqualTo(17);
+
+ JSONObject obj = ItUtils.getJSONReport(result);
+ JSONArray issues = (JSONArray) obj.get("issues");
+ for (Object issue : issues) {
+ JSONObject jsonIssue = (JSONObject) issue;
+ assertThat(jsonIssue.get("startLine")).isNotNull();
+ assertThat(jsonIssue.get("line")).isEqualTo(jsonIssue.get("startLine"));
+ assertThat(jsonIssue.get("endLine")).isEqualTo(jsonIssue.get("startLine"));
+
+ assertThat(jsonIssue.get("endOffset")).isNotNull();
+ assertThat(jsonIssue.get("startOffset")).isNotNull();
+ }
+
+ List<Long> lineNumbers = new ArrayList<>(16);
+ for (long i = 1L; i < 18; i++) {
+ lineNumbers.add(i);
+ }
+ assertThat(issues).extracting("startLine").containsAll(lineNumbers);
+ assertThat(issues).extracting("endLine").containsAll(lineNumbers);
+ }
+
+ @Test
+ public void precise_issue_location() throws IOException {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource(RESOURCE_PATH + "multiline.xml"));
+ orchestrator.getServer().provisionProject("sample-multiline", "xoo-sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample-multiline", "xoo", "multiline");
+
+ File projectDir = ItUtils.projectDir("shared/xoo-precise-issues");
+ SonarScanner runner = SonarScanner.create(projectDir,
+ "sonar.analysis.mode", "issues",
+ "sonar.verbose", "true",
+ "sonar.report.export.path", "sonar-report.json");
+ BuildResult result = orchestrator.executeBuild(runner);
+ assertThat(ItUtils.countIssuesInJsonReport(result, true)).isEqualTo(2);
+
+ JSONObject obj = ItUtils.getJSONReport(result);
+ JSONArray issues = (JSONArray) obj.get("issues");
+
+ JSONObject issue1 = (JSONObject) issues.get(0);
+ JSONObject issue2 = (JSONObject) issues.get(1);
+
+ assertThat(issue1.get("startLine")).isIn(6L);
+ assertThat(issue1.get("line")).isIn(6L);
+ assertThat(issue1.get("endLine")).isIn(6L);
+ assertThat(issue1.get("startOffset")).isIn(27L);
+ assertThat(issue1.get("endOffset")).isIn(32L);
+
+ assertThat(issue2.get("startLine")).isIn(9L);
+ assertThat(issue2.get("line")).isIn(9L);
+ assertThat(issue2.get("endLine")).isIn(15L);
+ assertThat(issue2.get("startOffset")).isIn(20L);
+ assertThat(issue2.get("endOffset")).isIn(2L);
+
+ }
+
+ @Test
+ public void test_json_report_no_server_analysis() throws Exception {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource(RESOURCE_PATH + "one-issue-per-line.xml"));
+ orchestrator.getServer().provisionProject("sample", "tracking");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");
+
+ File projectDir = ItUtils.projectDir("analysis/tracking/v1");
+ SonarScanner issuesModeScan = SonarScanner.create(projectDir)
+ .setProperty("sonar.analysis.mode", "issues")
+ .setProperty("sonar.userHome", temp.newFolder().getAbsolutePath())
+ .setProperty("sonar.report.export.path", "sonar-report.json")
+ .setProperty("sonar.projectDate", "2013-05-02");
+ orchestrator.executeBuild(issuesModeScan);
+
+ File report = new File(projectDir, ".sonar/sonar-report.json");
+ assertThat(report).isFile().exists();
+
+ String json = sanitize(FileUtils.readFileToString(report));
+ String expectedJson = expected("no-server-analysis.json");
+ JSONAssert.assertEquals(expectedJson, json, false);
+ }
+
+ @Test
+ public void test_json_report() throws Exception {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource(RESOURCE_PATH + "one-issue-per-line.xml"));
+ orchestrator.getServer().provisionProject("sample", "tracking");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");
+
+ SonarScanner scan = SonarScanner.create(ItUtils.projectDir("analysis/tracking/v1"))
+ .setProperty("sonar.projectDate", "2013-05-01");
+ orchestrator.executeBuild(scan);
+
+ // Issues mode scan -> 2 new issues and 13 existing issues
+ File projectDir = ItUtils.projectDir("analysis/tracking/v2");
+ SonarScanner issuesModeScan = SonarScanner.create(projectDir)
+ .setProperty("sonar.analysis.mode", "issues")
+ .setProperty("sonar.userHome", temp.newFolder().getAbsolutePath())
+ .setProperty("sonar.report.export.path", "sonar-report.json")
+ .setProperty("sonar.projectDate", "2013-05-02");
+ orchestrator.executeBuild(issuesModeScan);
+
+ File report = new File(projectDir, ".sonar/sonar-report.json");
+ assertThat(report).isFile().exists();
+
+ String json = sanitize(FileUtils.readFileToString(report));
+ String expectedJson = expected("report-on-single-module.json");
+ JSONAssert.assertEquals(expectedJson, json, false);
+ }
+
+ @Test
+ public void test_json_report_on_branch() throws Exception {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource(getResource("one-issue-per-line.xml").getPath()));
+ orchestrator.getServer().provisionProject("sample:mybranch", "Sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample:mybranch", "xoo", "one-issue-per-line");
+
+ SonarScanner scan = SonarScanner.create(ItUtils.projectDir("analysis/tracking/v1"))
+ .setProperty("sonar.projectDate", "2013-05-01")
+ .setProperty("sonar.branch", "mybranch");
+ orchestrator.executeBuild(scan);
+
+ // issues mode scan -> 2 new issues and 13 existing issues
+ File projectDir = ItUtils.projectDir("analysis/tracking/v2");
+ SonarScanner issuesModeScan = SonarScanner.create(projectDir)
+ .setProperty("sonar.analysis.mode", "issues")
+ .setProperty("sonar.userHome", temp.newFolder().getAbsolutePath())
+ .setProperty("sonar.report.export.path", "sonar-report.json")
+ .setProperty("sonar.issuesReport.console.enable", "true")
+ .setProperty("sonar.projectDate", "2013-05-02")
+ .setProperty("sonar.verbose", "true")
+ .setProperty("sonar.branch", "mybranch");
+ orchestrator.executeBuild(issuesModeScan);
+
+ File report = new File(projectDir, ".sonar/sonar-report.json");
+ assertThat(report).isFile().exists();
+
+ String json = sanitize(FileUtils.readFileToString(report));
+ String expectedJson = expected("report-on-single-module-branch.json");
+ JSONAssert.assertEquals(expectedJson, json, false);
+ }
+
+ /**
+ * Multi-modules project but Eclipse scans only a single module
+ */
+ @Test
+ public void test_json_report_on_sub_module() throws Exception {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource(RESOURCE_PATH + "one-issue-per-line.xml"));
+ orchestrator.getServer().provisionProject("com.sonarsource.it.samples:multi-modules-sample", "Multi-module sample");
+ orchestrator.getServer().associateProjectToQualityProfile("com.sonarsource.it.samples:multi-modules-sample", "xoo", "one-issue-per-line");
+
+ File rootDir = ItUtils.projectDir("shared/xoo-multi-modules-sample");
+ SonarScanner scan = SonarScanner.create(rootDir)
+ .setProperty("sonar.projectDate", "2013-05-01");
+ orchestrator.executeBuild(scan);
+
+ // Issues mode scan on a module -> no new issues
+ File moduleDir = ItUtils.projectDir("shared/xoo-multi-modules-sample/module_a/module_a1");
+ SonarScanner issuesModeScan = SonarScanner.create(moduleDir)
+ .setProperty("sonar.projectKey", "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1")
+ .setProperty("sonar.projectVersion", "1.0-SNAPSHOT")
+ .setProperty("sonar.projectName", "ModuleA1")
+ .setProperty("sonar.sources", "src/main/xoo")
+ .setProperty("sonar.language", "xoo")
+ .setProperty("sonar.analysis.mode", "issues")
+ .setProperty("sonar.userHome", temp.newFolder().getAbsolutePath())
+ .setProperty("sonar.report.export.path", "sonar-report.json")
+ .setProperty("sonar.projectDate", "2013-05-02");
+ orchestrator.executeBuild(issuesModeScan);
+
+ File report = new File(moduleDir, ".sonar/sonar-report.json");
+ assertThat(report).isFile().exists();
+
+ String json = sanitize(FileUtils.readFileToString(report));
+ // SONAR-5218 All issues are updated as their root project id has changed (it's now the sub module)
+ String expectedJson = expected("report-on-sub-module.json");
+ JSONAssert.assertEquals(expectedJson, json, false);
+ }
+
+ /**
+ * Multi-modules project
+ */
+ @Test
+ public void test_json_report_on_root_module() throws Exception {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource(RESOURCE_PATH + "one-issue-per-line.xml"));
+ orchestrator.getServer().provisionProject("com.sonarsource.it.samples:multi-modules-sample", "Sonar :: Integration Tests :: Multi-modules Sample");
+ orchestrator.getServer().associateProjectToQualityProfile("com.sonarsource.it.samples:multi-modules-sample", "xoo", "one-issue-per-line");
+
+ File rootDir = ItUtils.projectDir("shared/xoo-multi-modules-sample");
+ SonarScanner scan = SonarScanner.create(rootDir)
+ .setProperty("sonar.projectDate", "2013-05-01");
+ orchestrator.executeBuild(scan);
+
+ // issues mode scan -> no new issues
+ SonarScanner issuesModeScan = SonarScanner.create(rootDir)
+ .setProperty("sonar.analysis.mode", "issues")
+ .setProperty("sonar.userHome", temp.newFolder().getAbsolutePath())
+ .setProperty("sonar.report.export.path", "sonar-report.json")
+ .setProperty("sonar.projectDate", "2013-05-02");
+ orchestrator.executeBuild(issuesModeScan);
+
+ File report = new File(rootDir, ".sonar/sonar-report.json");
+ assertThat(report).isFile().exists();
+
+ String json = sanitize(FileUtils.readFileToString(report));
+ String expectedJson = expected("report-on-root-module.json");
+ JSONAssert.assertEquals(expectedJson, json, false);
+ }
+
+ private String expected(String path) throws IOException {
+ try (WsResponse response = newWsClient(orchestrator).wsConnector().call(new GetRequest("api/server/version"))) {
+ String version = response.content();
+
+ return sanitize(IOUtils.toString(getResourceInputStream(path)))
+ .replaceAll(Pattern.quote(SONAR_VERSION_PLACEHOLDER), version);
+ }
+ }
+
+ @Test
+ public void issueSanityCheck() {
+ assertThat(sanitize("s\"0150F1EBDB8E000003\"f")).isEqualTo("s<ISSUE_KEY>f");
+ }
+
+ private static String sanitize(String s) {
+ // sanitize issue uuid keys
+ s = s.replaceAll("\"[a-zA-Z_0-9\\-]{15,20}\"", "<ISSUE_KEY>");
+
+ return ItUtils.sanitizeTimezones(s);
+ }
+
+ private InputStream getResourceInputStream(String resource) throws FileNotFoundException {
+ ResourceLocation res = getResource(resource);
+ return getClass().getResourceAsStream(res.getPath());
+ }
+
+ private ResourceLocation getResource(String resource) {
+ return FileLocation.ofClasspath(RESOURCE_PATH + resource);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/analysis/IssuesModeTest.java b/tests/src/test/java/org/sonarqube/tests/analysis/IssuesModeTest.java
new file mode 100644
index 00000000000..527f0970a7b
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/analysis/IssuesModeTest.java
@@ -0,0 +1,461 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.analysis;
+
+import com.google.common.collect.Maps;
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.BuildFailureException;
+import com.sonar.orchestrator.build.BuildResult;
+import com.sonar.orchestrator.build.SonarScanner;
+import com.sonar.orchestrator.build.SonarScannerInstaller;
+import com.sonar.orchestrator.config.FileSystem;
+import com.sonar.orchestrator.version.Version;
+import org.sonarqube.tests.Category3Suite;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import javax.annotation.Nullable;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.ObjectUtils;
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.wsclient.SonarClient;
+import org.sonar.wsclient.issue.Issue;
+import org.sonar.wsclient.issue.IssueClient;
+import org.sonar.wsclient.issue.IssueQuery;
+import org.sonar.wsclient.issue.Issues;
+import org.sonar.wsclient.user.UserParameters;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static util.ItUtils.getComponent;
+
+public class IssuesModeTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category3Suite.ORCHESTRATOR;
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Before
+ public void deleteData() throws IOException {
+ orchestrator.resetData();
+ }
+
+ @Test
+ public void issues_analysis_on_new_project() throws IOException {
+ restoreProfile("one-issue-per-line.xml");
+ orchestrator.getServer().provisionProject("sample", "xoo-sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");
+ SonarScanner runner = configureRunnerIssues("shared/xoo-sample", null, "sonar.verbose", "true");
+ BuildResult result = orchestrator.executeBuild(runner);
+ assertThat(ItUtils.countIssuesInJsonReport(result, true)).isEqualTo(17);
+ }
+
+ @Test
+ public void invalid_incremental_mode() throws IOException {
+ restoreProfile("one-issue-per-line.xml");
+ orchestrator.getServer().provisionProject("sample", "xoo-sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");
+ SonarScanner runner = configureRunner("shared/xoo-sample");
+ runner.setProperty("sonar.analysis.mode", "incremental");
+
+ thrown.expect(BuildFailureException.class);
+ BuildResult res = orchestrator.executeBuild(runner);
+
+ assertThat(res.getLogs()).contains("Invalid analysis mode: incremental. This mode was removed in SonarQube 5.2");
+ }
+
+ @Test
+ public void project_key_with_slash() throws IOException {
+ restoreProfile("one-issue-per-line.xml");
+ setDefaultQualityProfile("xoo", "one-issue-per-line");
+
+ SonarScanner runner = configureRunner("shared/xoo-sample");
+ runner.setProjectKey("sample/project");
+ runner.setProperty("sonar.analysis.mode", "issues");
+ BuildResult result = orchestrator.executeBuild(runner);
+ assertThat(ItUtils.countIssuesInJsonReport(result, true)).isEqualTo(17);
+ }
+
+ // SONAR-6931
+ @Test
+ public void only_scan_changed_files_qps() throws IOException {
+ restoreProfile("one-issue-per-line.xml");
+ orchestrator.getServer().provisionProject("sample", "xoo-sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");
+
+ SonarScanner runner = configureRunner("shared/xoo-sample", "sonar.verbose", "true");
+ BuildResult result = orchestrator.executeBuild(runner);
+ List<Issue> serverIssues = ItUtils.getAllServerIssues(orchestrator);
+ for (Issue i : serverIssues) {
+ assertThat(i.status()).isEqualTo("OPEN");
+ }
+ assertThat(serverIssues).hasSize(17);
+
+ // change quality profile
+ restoreProfile("with-many-rules.xml");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "with-many-rules");
+
+ // do it again, scanning nothing (all files should be unchanged)
+ runner = configureRunnerIssues("shared/xoo-sample", null,
+ "sonar.verbose", "true");
+ result = orchestrator.executeBuild(runner);
+ assertThat(result.getLogs()).contains("Scanning only changed files");
+ assertThat(result.getLogs()).contains("'One Issue Per Line' skipped because there is no related file in current project");
+ ItUtils.assertIssuesInJsonReport(result, 0, 0, 17);
+ }
+
+ // SONAR-6931
+ @Test
+ public void only_scan_changed_files_transitions() throws IOException {
+ restoreProfile("one-issue-per-line.xml");
+ orchestrator.getServer().provisionProject("sample", "xoo-sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");
+
+ SonarScanner runner = configureRunner("shared/xoo-sample", "sonar.verbose", "true");
+ BuildResult result = orchestrator.executeBuild(runner);
+ List<Issue> serverIssues = ItUtils.getAllServerIssues(orchestrator);
+ for (Issue i : serverIssues) {
+ assertThat(i.status()).isEqualTo("OPEN");
+ }
+ assertThat(serverIssues).hasSize(17);
+
+ // resolve 2 issues
+ IssueClient issueClient = orchestrator.getServer().wsClient("admin", "admin").issueClient();
+ issueClient.doTransition(serverIssues.get(0).key(), "wontfix");
+ issueClient.doTransition(serverIssues.get(1).key(), "wontfix");
+
+ // do it again, scanning nothing (all files should be unchanged)
+ runner = configureRunnerIssues("shared/xoo-sample", null,
+ "sonar.verbose", "true");
+ result = orchestrator.executeBuild(runner);
+ assertThat(result.getLogs()).contains("Scanning only changed files");
+ assertThat(result.getLogs()).contains("'One Issue Per Line' skipped because there is no related file in current project");
+ ItUtils.assertIssuesInJsonReport(result, 0, 0, 15);
+ }
+
+ // SONAR-6931
+ @Test
+ public void only_scan_changed_files_on_change() throws IOException {
+ restoreProfile("one-issue-per-line.xml");
+ orchestrator.getServer().provisionProject("sample", "xoo-sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");
+
+ SonarScanner runner = configureRunner("shared/xoo-sample", "sonar.verbose", "true");
+ BuildResult result = orchestrator.executeBuild(runner);
+
+ // change QP
+ restoreProfile("with-many-rules.xml");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "with-many-rules");
+
+ // now change file hash in a temporary location
+ File tmpProjectDir = temp.newFolder();
+ FileUtils.copyDirectory(ItUtils.projectDir("shared/xoo-sample"), tmpProjectDir);
+ File srcFile = new File(tmpProjectDir, "src/main/xoo/sample/Sample.xoo");
+ FileUtils.write(srcFile, "\n", StandardCharsets.UTF_8, true);
+
+ // scan again, with different QP
+ runner = SonarScanner.create(tmpProjectDir,
+ "sonar.working.directory", temp.newFolder().getAbsolutePath(),
+ "sonar.analysis.mode", "issues",
+ "sonar.report.export.path", "sonar-report.json",
+ "sonar.userHome", temp.newFolder().getAbsolutePath(),
+ "sonar.verbose", "true",
+ "sonar.scanChangedFilesOnly", "true");
+ result = orchestrator.executeBuild(runner);
+ assertThat(result.getLogs()).contains("Scanning only changed files");
+ assertThat(result.getLogs()).doesNotContain("'One Issue Per Line' skipped because there is no related file in current project");
+ ItUtils.assertIssuesInJsonReport(result, 3, 0, 17);
+ }
+
+ // SONAR-8518
+ @Test
+ public void should_support_sonar_profile_prop() throws IOException {
+ restoreProfile("one-issue-per-line.xml");
+ orchestrator.getServer().provisionProject("sample", "xoo-sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "empty");
+
+ SonarScanner runner = configureRunner("shared/xoo-sample",
+ "sonar.verbose", "true",
+ "sonar.analysis.mode", "issues",
+ "sonar.profile", "one-issue-per-line");
+ BuildResult result = orchestrator.executeBuild(runner);
+ ItUtils.assertIssuesInJsonReport(result, 17, 0, 0);
+ }
+
+ // SONAR-5715
+ @Test
+ public void test_issues_mode_on_project_with_space_in_filename() throws IOException {
+ restoreProfile("with-many-rules.xml");
+ orchestrator.getServer().provisionProject("sample", "xoo-sample-with-spaces");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "with-many-rules");
+
+ SonarScanner runner = configureRunner("analysis/xoo-sample-with-spaces/v2");
+ orchestrator.executeBuild(runner);
+ assertThat(getComponent(orchestrator, "sample:my sources/main/xoo/sample/My Sample.xoo")).isNotNull();
+
+ runner = configureRunnerIssues("analysis/xoo-sample-with-spaces/v2", null);
+ BuildResult result = orchestrator.executeBuild(runner);
+ // Analysis is not persisted in database
+ assertThat(getComponent(orchestrator, "com.sonarsource.it.samples:simple-sample")).isNull();
+ assertThat(result.getLogs()).contains("Issues");
+ assertThat(result.getLogs()).contains("ANALYSIS SUCCESSFUL");
+ }
+
+ @Test
+ public void should_not_fail_on_resources_that_have_existed_before() throws IOException {
+ restoreProfile("with-many-rules.xml");
+ orchestrator.getServer().provisionProject("sample", "xoo-history");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "with-many-rules");
+
+ // First real scan with source
+ SonarScanner runner = configureRunner("shared/xoo-history-v2");
+ orchestrator.executeBuild(runner);
+ assertThat(getComponent(orchestrator, "sample:src/main/xoo/sample/ClassAdded.xoo")).isNotNull();
+
+ // Second scan should remove ClassAdded.xoo
+ runner = configureRunner("shared/xoo-history-v1");
+ orchestrator.executeBuild(runner);
+ assertThat(getComponent(orchestrator, "sample:src/main/xoo/sample/ClassAdded.xoo")).isNull();
+
+ // Re-add ClassAdded.xoo in local workspace
+ runner = configureRunnerIssues("shared/xoo-history-v2", null);
+ BuildResult result = orchestrator.executeBuild(runner);
+
+ assertThat(getComponent(orchestrator, "sample:src/main/xoo/sample/ClassAdded.xoo")).isNull();
+ assertThat(result.getLogs()).contains("Issues");
+ assertThat(result.getLogs()).contains("ANALYSIS SUCCESSFUL");
+ }
+
+ @Test
+ public void should_fail_if_plugin_access_secured_properties() throws IOException {
+ // Test access from task (ie BatchSettings)
+ SonarScanner runner = configureRunnerIssues("shared/xoo-sample", null,
+ "accessSecuredFromTask", "true");
+ BuildResult result = orchestrator.executeBuildQuietly(runner);
+
+ assertThat(result.getLogs()).contains("Access to the secured property 'foo.bar.secured' is not possible in issues mode. "
+ + "The SonarQube plugin which requires this property must be deactivated in issues mode.");
+
+ // Test access from sensor (ie ModuleSettings)
+ runner = configureRunnerIssues("shared/xoo-sample", null,
+ "accessSecuredFromSensor", "true");
+ result = orchestrator.executeBuildQuietly(runner);
+
+ assertThat(result.getLogs()).contains("Access to the secured property 'foo.bar.secured' is not possible in issues mode. "
+ + "The SonarQube plugin which requires this property must be deactivated in issues mode.");
+ }
+
+ // SONAR-4602
+ @Test
+ public void no_issues_mode_cache_by_default() throws Exception {
+ File homeDir = runFirstAnalysisAndFlagIssueAsWontFix();
+
+ // Second issues mode using same cache dir but cache disabled by default
+ SonarScanner runner = configureRunnerIssues("shared/xoo-sample", homeDir);
+ BuildResult result = orchestrator.executeBuild(runner);
+
+ // False positive is not returned
+ assertThat(ItUtils.countIssuesInJsonReport(result, false)).isEqualTo(16);
+ }
+
+ private File runFirstAnalysisAndFlagIssueAsWontFix() throws IOException {
+ restoreProfile("one-issue-per-line.xml");
+ orchestrator.getServer().provisionProject("sample", "xoo-sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");
+
+ // First run (publish mode)
+ SonarScanner runner = configureRunner("shared/xoo-sample");
+ orchestrator.executeBuild(runner);
+
+ // First issues mode
+ File homeDir = temp.newFolder();
+ runner = configureRunnerIssues("shared/xoo-sample", homeDir);
+ BuildResult result = orchestrator.executeBuild(runner);
+
+ // 17 issues
+ assertThat(ItUtils.countIssuesInJsonReport(result, false)).isEqualTo(17);
+
+ // Flag one issue as false positive
+ JSONObject obj = ItUtils.getJSONReport(result);
+ String key = ((JSONObject) ((JSONArray) obj.get("issues")).get(0)).get("key").toString();
+ orchestrator.getServer().adminWsClient().issueClient().doTransition(key, "falsepositive");
+ return homeDir;
+ }
+
+ // SONAR-6522
+ @Test
+ public void load_user_name_in_json_report() throws Exception {
+ restoreProfile("one-issue-per-line.xml");
+ orchestrator.getServer().provisionProject("sample", "xoo-sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");
+
+ // First run (publish mode)
+ SonarScanner runner = configureRunner("shared/xoo-sample");
+ orchestrator.executeBuild(runner);
+
+ SonarClient client = orchestrator.getServer().adminWsClient();
+
+ Issues issues = client.issueClient().find(IssueQuery.create());
+ Issue issue = issues.list().get(0);
+
+ UserParameters creationParameters = UserParameters.create().login("julien").name("Julien H")
+ .password("password").passwordConfirmation("password");
+ client.userClient().create(creationParameters);
+
+ // Assign issue
+ client.issueClient().assign(issue.key(), "julien");
+
+ // Issues
+ runner = configureRunnerIssues("shared/xoo-sample", null, "sonar.login", "julien", "sonar.password", "password");
+ BuildResult result = orchestrator.executeBuild(runner);
+
+ JSONObject obj = ItUtils.getJSONReport(result);
+
+ Map<String, String> userNameByLogin = Maps.newHashMap();
+ final JSONArray users = (JSONArray) obj.get("users");
+ if (users != null) {
+ for (Object user : users) {
+ String login = ObjectUtils.toString(((JSONObject) user).get("login"));
+ String name = ObjectUtils.toString(((JSONObject) user).get("name"));
+ userNameByLogin.put(login, name);
+ }
+ }
+ assertThat(userNameByLogin.get("julien")).isEqualTo("julien");
+
+ for (Object issueJson : (JSONArray) obj.get("issues")) {
+ JSONObject jsonObject = (JSONObject) issueJson;
+ if (issue.key().equals(jsonObject.get("key"))) {
+ assertThat(jsonObject.get("assignee")).isEqualTo("julien");
+ return;
+ }
+ }
+ fail("Issue not found");
+ }
+
+ @Test
+ public void concurrent_issue_mode_on_existing_project() throws Exception {
+ restoreProfile("one-issue-per-line.xml");
+ orchestrator.getServer().provisionProject("sample", "xoo-sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");
+
+ // use same working dir, because lock file is in it
+ String workDirPath = temp.newFolder().getAbsolutePath();
+ SonarScanner runner = configureRunner("shared/xoo-sample",
+ "sonar.working.directory", workDirPath);
+
+ orchestrator.executeBuild(runner);
+
+ runConcurrentIssues(workDirPath);
+ }
+
+ private void runConcurrentIssues(final String workDirPath) throws Exception {
+ // Install sonar-runner in advance to avoid concurrent unzip issues
+ FileSystem fileSystem = orchestrator.getConfiguration().fileSystem();
+ new SonarScannerInstaller(fileSystem).install(Version.create(SonarScanner.DEFAULT_SCANNER_VERSION), fileSystem.workspace(), true);
+ final int nThreads = 3;
+ ExecutorService executorService = Executors.newFixedThreadPool(nThreads);
+ List<Callable<BuildResult>> tasks = new ArrayList<>();
+ final File homeDir = temp.newFolder();
+ for (int i = 0; i < nThreads; i++) {
+ tasks.add(new Callable<BuildResult>() {
+
+ public BuildResult call() throws Exception {
+ SonarScanner runner = configureRunnerIssues("shared/xoo-sample", homeDir,
+ "sonar.it.enableWaitingSensor", "true",
+ "sonar.working.directory", workDirPath);
+ return orchestrator.executeBuild(runner);
+ }
+ });
+ }
+
+ boolean expectedError = false;
+ for (Future<BuildResult> result : executorService.invokeAll(tasks)) {
+ try {
+ result.get();
+ } catch (ExecutionException e) {
+ if (e.getCause() instanceof BuildFailureException) {
+ BuildFailureException bfe = (BuildFailureException) e.getCause();
+ assertThat(bfe.getResult().getLogs()).contains("Another SonarQube analysis is already in progress for this project");
+ expectedError = true;
+ } else {
+ throw e;
+ }
+ }
+ }
+ if (!expectedError) {
+ fail("At least one of the threads should have failed");
+ }
+ }
+
+ private void restoreProfile(String fileName) {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/analysis/IssuesModeTest/" + fileName));
+ }
+
+ private SonarScanner configureRunnerIssues(String projectDir, @Nullable File homeDir, String... props) throws IOException {
+ SonarScanner scanner = SonarScanner.create(ItUtils.projectDir(projectDir),
+ "sonar.working.directory", temp.newFolder().getAbsolutePath(),
+ "sonar.analysis.mode", "issues",
+ "sonar.report.export.path", "sonar-report.json");
+ if (homeDir != null) {
+ scanner.setProperty("sonar.userHome", homeDir.getAbsolutePath());
+ } else {
+ scanner.setProperty("sonar.userHome", temp.newFolder().getAbsolutePath());
+ }
+ scanner.setProperties(props);
+ return scanner;
+ }
+
+ private SonarScanner configureRunner(String projectDir, String... props) throws IOException {
+ SonarScanner runner = SonarScanner.create(ItUtils.projectDir(projectDir),
+ "sonar.working.directory", temp.newFolder().getAbsolutePath(),
+ "sonar.report.export.path", "sonar-report.json",
+ "sonar.userHome", temp.newFolder().getAbsolutePath());
+ runner.setProperties(props);
+ return runner;
+ }
+
+ private void setDefaultQualityProfile(String languageKey, String profileName) {
+ orchestrator.getServer().adminWsClient().post("api/qualityprofiles/set_default",
+ "language", languageKey,
+ "profileName", profileName);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/analysis/LinksTest.java b/tests/src/test/java/org/sonarqube/tests/analysis/LinksTest.java
new file mode 100644
index 00000000000..eb14836b2c1
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/analysis/LinksTest.java
@@ -0,0 +1,93 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.analysis;
+
+import com.google.common.collect.ImmutableMap;
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.MavenBuild;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category3Suite;
+import java.util.List;
+import java.util.Optional;
+import org.junit.After;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.WsProjectLinks;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.projectlinks.SearchWsRequest;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class LinksTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category3Suite.ORCHESTRATOR;
+
+ private static final String PROJECT_KEY = "com.sonarsource.it.samples:simple-sample";
+
+ @After
+ public void cleanProjectLinksTable() {
+ orchestrator.getServer().post("api/projects/delete", ImmutableMap.of("key", PROJECT_KEY));
+ }
+
+ /**
+ * SONAR-3676
+ */
+ @Test
+ public void shouldUseLinkProperties() {
+ SonarScanner runner = SonarScanner.create(ItUtils.projectDir("analysis/links-project"))
+ .setProperty("sonar.scm.disabled", "true");
+ orchestrator.executeBuild(runner);
+
+ verifyLinks();
+ }
+
+ /**
+ * SONAR-3676
+ */
+ @Test
+ public void shouldUseLinkPropertiesOverPomLinksInMaven() {
+ MavenBuild build = MavenBuild.create(ItUtils.projectPom("analysis/links-project"))
+ .setCleanPackageSonarGoals()
+ .setProperty("sonar.scm.disabled", "true");
+ orchestrator.executeBuild(build);
+
+ verifyLinks();
+ }
+
+ private void verifyLinks() {
+ WsClient wsClient = ItUtils.newWsClient(orchestrator);
+ List<WsProjectLinks.Link> links = wsClient.projectLinks().search(new SearchWsRequest().setProjectKey(PROJECT_KEY)).getLinksList();
+ verifyLink(links, "homepage", "http://www.simplesample.org_OVERRIDDEN");
+ verifyLink(links, "ci", "http://bamboo.ci.codehaus.org/browse/SIMPLESAMPLE");
+ verifyLink(links, "issue", "http://jira.codehaus.org/browse/SIMPLESAMPLE");
+ verifyLink(links, "scm", "https://github.com/SonarSource/simplesample");
+ verifyLink(links, "scm_dev", "scm:git:git@github.com:SonarSource/simplesample.git");
+ }
+
+ private void verifyLink(List<WsProjectLinks.Link> links, String expectedType, String expectedUrl) {
+ Optional<WsProjectLinks.Link> link = links.stream()
+ .filter(l -> l.getType().equals(expectedType))
+ .findFirst();
+ assertThat(link).isPresent();
+ assertThat(link.get().getUrl()).isEqualTo(expectedUrl);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/analysis/MultiLanguageTest.java b/tests/src/test/java/org/sonarqube/tests/analysis/MultiLanguageTest.java
new file mode 100644
index 00000000000..fa33ed7ea08
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/analysis/MultiLanguageTest.java
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.analysis;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.BuildResult;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category3Suite;
+import java.util.Map;
+import org.junit.After;
+import org.junit.ClassRule;
+import org.junit.Test;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.getMeasureAsDouble;
+import static util.ItUtils.getMeasuresAsDoubleByMetricKey;
+
+public class MultiLanguageTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category3Suite.ORCHESTRATOR;
+
+ @After
+ public void cleanDatabase() {
+ orchestrator.resetData();
+ }
+
+ /**
+ * SONAR-926
+ * SONAR-5069
+ */
+ @Test
+ public void test_sonar_runner_inspection() {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/analysis/MultiLanguageTest/one-issue-per-line.xml"));
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/analysis/MultiLanguageTest/one-issue-per-line-xoo2.xml"));
+
+ orchestrator.getServer().provisionProject("multi-language-sample", "multi-language-sample");
+
+ orchestrator.getServer().associateProjectToQualityProfile("multi-language-sample", "xoo", "one-issue-per-line");
+ orchestrator.getServer().associateProjectToQualityProfile("multi-language-sample", "xoo2", "one-issue-per-line-xoo2");
+
+ SonarScanner build = SonarScanner.create().setProjectDir(ItUtils.projectDir("analysis/xoo-multi-languages"));
+ BuildResult result = orchestrator.executeBuild(build);
+
+ // 4 files: 1 .xoo, 1.xoo2, 2 .measures
+ assertThat(result.getLogs()).contains("4 files indexed");
+ assertThat(result.getLogs()).contains("Quality profile for xoo: one-issue-per-line");
+ assertThat(result.getLogs()).contains("Quality profile for xoo2: one-issue-per-line-xoo2");
+
+ // modules
+ Map<String, Double> measures = getMeasuresAsDoubleByMetricKey(orchestrator, "multi-language-sample", "files", "violations");
+ assertThat(measures.get("files")).isEqualTo(2);
+ assertThat(measures.get("violations")).isEqualTo(26);
+
+ assertThat(getMeasureAsDouble(orchestrator, "multi-language-sample:src/sample/Sample.xoo", "violations")).isEqualTo(13);
+ assertThat(getMeasureAsDouble(orchestrator, "multi-language-sample:src/sample/Sample.xoo2", "violations")).isEqualTo(13);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/analysis/PermissionTest.java b/tests/src/test/java/org/sonarqube/tests/analysis/PermissionTest.java
new file mode 100644
index 00000000000..79663eec478
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/analysis/PermissionTest.java
@@ -0,0 +1,177 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.analysis;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.BuildResult;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category3Suite;
+import javax.annotation.Nullable;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.ws.WsUserTokens;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.permission.AddUserWsRequest;
+import org.sonarqube.ws.client.usertoken.GenerateWsRequest;
+import org.sonarqube.ws.client.usertoken.RevokeWsRequest;
+import org.sonarqube.ws.client.usertoken.UserTokensService;
+import util.user.UserRule;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.projectDir;
+import static util.ItUtils.resetSettings;
+import static util.ItUtils.setServerProperty;
+
+public class PermissionTest {
+
+ private static final String A_LOGIN = "a_login";
+ private static final String A_PASSWORD = "a_password";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category3Suite.ORCHESTRATOR;
+
+ @Rule
+ public UserRule userRule = UserRule.from(orchestrator);
+
+ private WsClient adminWsClient;
+ private UserTokensService userTokensWsClient;
+
+ @Before
+ public void setUp() {
+ orchestrator.resetData();
+
+ // enforce scanners to be authenticated
+ setServerProperty(orchestrator, "sonar.forceAuthentication", "true");
+
+ adminWsClient = newAdminWsClient(orchestrator);
+ userTokensWsClient = adminWsClient.userTokens();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ resetSettings(orchestrator, null, "sonar.forceAuthentication");
+ userRule.resetUsers();
+ }
+
+ @Test
+ public void scanner_can_authenticate_with_authentication_token() {
+ createUserWithProvisioningAndScanPermissions();
+
+ String tokenName = "For test";
+ WsUserTokens.GenerateWsResponse generateWsResponse = userTokensWsClient.generate(new GenerateWsRequest()
+ .setLogin(A_LOGIN)
+ .setName(tokenName));
+ SonarScanner sampleProject = SonarScanner.create(projectDir("shared/xoo-sample"));
+ sampleProject.setProperties(
+ "sonar.login", generateWsResponse.getToken(),
+ "sonar.password", "");
+
+ BuildResult buildResult = orchestrator.executeBuild(sampleProject);
+
+ assertThat(buildResult.isSuccess()).isTrue();
+ userTokensWsClient.revoke(new RevokeWsRequest().setLogin(A_LOGIN).setName(tokenName));
+ }
+
+ @Test
+ public void scanner_fails_if_authentication_token_is_not_valid() {
+ SonarScanner sampleProject = SonarScanner.create(projectDir("shared/xoo-sample"));
+ sampleProject.setProperties(
+ "sonar.login", "unknown-token",
+ "sonar.password", "");
+
+ BuildResult buildResult = orchestrator.executeBuildQuietly(sampleProject);
+
+ assertThat(buildResult.isSuccess()).isFalse();
+ }
+
+ /**
+ * SONAR-4211 Test Sonar Runner when server requires authentication
+ */
+ @Test
+ public void scanner_can_authenticate_with_login_password() {
+ createUserWithProvisioningAndScanPermissions();
+
+ orchestrator.getServer().provisionProject("sample", "xoo-sample");
+
+ BuildResult buildResult = scanQuietly("shared/xoo-sample",
+ "sonar.login", "",
+ "sonar.password", "");
+ assertThat(buildResult.getLastStatus()).isEqualTo(1);
+ assertThat(buildResult.getLogs()).contains(
+ "Not authorized. Analyzing this project requires to be authenticated. Please provide the values of the properties sonar.login and sonar.password.");
+
+ // SONAR-4048
+ buildResult = scanQuietly("shared/xoo-sample",
+ "sonar.login", "wrong_login",
+ "sonar.password", "wrong_password");
+ assertThat(buildResult.getLastStatus()).isEqualTo(1);
+ assertThat(buildResult.getLogs()).contains(
+ "Not authorized. Please check the properties sonar.login and sonar.password.");
+
+ buildResult = scan("shared/xoo-sample",
+ "sonar.login", A_LOGIN,
+ "sonar.password", A_PASSWORD);
+ assertThat(buildResult.getLastStatus()).isEqualTo(0);
+ }
+
+ @Test
+ public void run_scanner_with_user_having_scan_permission_only_on_project() throws Exception {
+ userRule.createUser(A_LOGIN, A_PASSWORD);
+ orchestrator.getServer().provisionProject("sample", "sample");
+ addUserPermission(A_LOGIN, "scan", "sample");
+
+ BuildResult buildResult = scanQuietly("shared/xoo-sample", "sonar.login", A_LOGIN, "sonar.password", A_PASSWORD);
+
+ assertThat(buildResult.isSuccess()).isTrue();
+ }
+
+ private void addUserPermission(String login, String permission, @Nullable String projectKey) {
+ adminWsClient.permissions().addUser(new AddUserWsRequest()
+ .setLogin(login)
+ .setPermission(permission)
+ .setProjectKey(projectKey));
+ }
+
+ private BuildResult scan(String projectPath, String... props) {
+ SonarScanner scanner = configureScanner(projectPath, props);
+ return orchestrator.executeBuild(scanner);
+ }
+
+ private BuildResult scanQuietly(String projectPath, String... props) {
+ SonarScanner scanner = configureScanner(projectPath, props);
+ return orchestrator.executeBuildQuietly(scanner);
+ }
+
+ private SonarScanner configureScanner(String projectPath, String... props) {
+ return SonarScanner.create(projectDir(projectPath))
+ .setProperties(props);
+ }
+
+ private void createUserWithProvisioningAndScanPermissions() {
+ userRule.createUser(A_LOGIN, A_PASSWORD);
+ addUserPermission(A_LOGIN, "provisioning", null);
+ addUserPermission(A_LOGIN, "scan", null);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/analysis/ProjectBuilderTest.java b/tests/src/test/java/org/sonarqube/tests/analysis/ProjectBuilderTest.java
new file mode 100644
index 00000000000..53b5a7059f9
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/analysis/ProjectBuilderTest.java
@@ -0,0 +1,86 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.analysis;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.MavenBuild;
+import org.sonarqube.tests.Category3Suite;
+import java.util.Map;
+import org.junit.ClassRule;
+import org.junit.Test;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.getComponent;
+import static util.ItUtils.getMeasuresAsDoubleByMetricKey;
+
+/**
+ * Test the extension point org.sonar.api.batch.bootstrap.ProjectBuilder
+ * <p/>
+ * A Sonar plugin can override the project definition injected by build-tool.
+ * Example: C# plugin loads project structure and modules from Visual Studio metadata file.
+ *
+ * @since 2.9
+ */
+public class ProjectBuilderTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category3Suite.ORCHESTRATOR;
+
+ @Test
+ public void shouldDefineProjectFromPlugin() {
+ MavenBuild build = MavenBuild.create(ItUtils.projectPom("analysis/project-builder"))
+ .setCleanSonarGoals()
+ .setProperty("sonar.enableProjectBuilder", "true")
+ .setProperty("sonar.dynamicAnalysis", "false");
+ orchestrator.executeBuild(build);
+
+ checkProject();
+ checkSubProject("project-builder-module-a");
+ checkSubProject("project-builder-module-b");
+ checkFile("project-builder-module-a", "src/HelloA.java");
+ checkFile("project-builder-module-b", "src/HelloB.java");
+ assertThat(getComponent(orchestrator, "com.sonarsource.it.projects.batch:project-builder-module-b:src/IgnoredFile.java")).isNull();
+ }
+
+ private void checkProject() {
+ // name has been changed by plugin
+ assertThat(getComponent(orchestrator, "com.sonarsource.it.projects.batch:project-builder").getName()).isEqualTo("Name changed by plugin");
+
+ Map<String, Double> measures = getMeasures("com.sonarsource.it.projects.batch:project-builder");
+ assertThat(measures.get("files")).isEqualTo(2);
+ assertThat(measures.get("lines")).isGreaterThan(10);
+ }
+
+ private void checkSubProject(String subProjectKey) {
+ Map<String, Double> measures = getMeasures("com.sonarsource.it.projects.batch:" + subProjectKey);
+ assertThat(measures.get("files")).isEqualTo(1);
+ assertThat(measures.get("lines")).isGreaterThan(5);
+ }
+
+ private void checkFile(String subProjectKey, String fileKey) {
+ Map<String, Double> measures = getMeasures("com.sonarsource.it.projects.batch:" + subProjectKey + ":" + fileKey);
+ assertThat(measures.get("lines")).isGreaterThan(5);
+ }
+
+ private Map<String, Double> getMeasures(String key) {
+ return getMeasuresAsDoubleByMetricKey(orchestrator, key, "lines", "files");
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/analysis/ReportDumpTest.java b/tests/src/test/java/org/sonarqube/tests/analysis/ReportDumpTest.java
new file mode 100644
index 00000000000..952a6a057e6
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/analysis/ReportDumpTest.java
@@ -0,0 +1,73 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.analysis;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import com.sonar.orchestrator.http.HttpResponse;
+import org.sonarqube.tests.Category3Suite;
+import java.io.File;
+import java.io.IOException;
+import java.io.StringReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Properties;
+import org.apache.commons.io.FileUtils;
+import org.junit.ClassRule;
+import org.junit.Test;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ReportDumpTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category3Suite.ORCHESTRATOR;
+
+ /**
+ * SONAR-6905
+ */
+ @Test
+ public void dump_metadata_of_uploaded_report() throws Exception {
+ File projectDir = ItUtils.projectDir("shared/xoo-sample");
+ orchestrator.executeBuild(SonarScanner.create(projectDir, "sonar.projectKey", "dump_metadata_of_uploaded_report", "sonar.projectName", "dump_metadata_of_uploaded_report"));
+
+ File metadata = new File(projectDir, ".sonar/report-task.txt");
+ assertThat(metadata).exists().isFile();
+
+ // verify properties
+ Properties props = new Properties();
+ props.load(new StringReader(FileUtils.readFileToString(metadata, StandardCharsets.UTF_8)));
+ assertThat(props).hasSize(6);
+ assertThat(props.getProperty("projectKey")).isEqualTo("dump_metadata_of_uploaded_report");
+ assertThat(props.getProperty("ceTaskId")).isNotEmpty();
+ String serverVersion = orchestrator.getServer().newHttpCall("api/server/version").execute().getBodyAsString();
+ assertThat(props.getProperty("serverVersion")).isEqualTo(serverVersion);
+ verifyUrl(props.getProperty("serverUrl"));
+ verifyUrl(props.getProperty("dashboardUrl"));
+ verifyUrl(props.getProperty("ceTaskUrl"));
+ }
+
+ private void verifyUrl(String url) throws IOException {
+ HttpResponse response = orchestrator.getServer().newHttpCall(url).execute();
+ assertThat(response.isSuccessful()).as(url).isTrue();
+ assertThat(response.getBodyAsString()).as(url).isNotEmpty();
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/analysis/SSLTest.java b/tests/src/test/java/org/sonarqube/tests/analysis/SSLTest.java
new file mode 100644
index 00000000000..be2f8804ae5
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/analysis/SSLTest.java
@@ -0,0 +1,197 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.analysis;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.BuildResult;
+import com.sonar.orchestrator.build.SonarScanner;
+import com.sonar.orchestrator.util.NetworkUtils;
+import org.sonarqube.tests.Category3Suite;
+import java.net.InetAddress;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.proxy.ProxyServlet;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SslConnectionFactory;
+import org.eclipse.jetty.server.handler.DefaultHandler;
+import org.eclipse.jetty.server.handler.HandlerCollection;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SSLTest {
+
+ private static final String CLIENT_KEYSTORE = "/analysis/SSLTest/clientkeystore.jks";
+ private static final String CLIENT_KEYSTORE_PWD = "clientp12pwd";
+
+ private static final String CLIENT_TRUSTSTORE = "/analysis/SSLTest/clienttruststore.jks";
+ private static final String CLIENT_TRUSTSTORE_PWD = "clienttruststorepwd";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category3Suite.ORCHESTRATOR;
+
+ private static Server server;
+ private static int httpsPort;
+
+ @Before
+ public void deleteData() {
+ orchestrator.resetData();
+ }
+
+ public static void startSSLTransparentReverseProxy(boolean requireClientAuth) throws Exception {
+ int httpPort = NetworkUtils.getNextAvailablePort(InetAddress.getLoopbackAddress());
+ httpsPort = NetworkUtils.getNextAvailablePort(InetAddress.getLoopbackAddress());
+
+ // Setup Threadpool
+ QueuedThreadPool threadPool = new QueuedThreadPool();
+ threadPool.setMaxThreads(500);
+
+ server = new Server(threadPool);
+
+ // HTTP Configuration
+ HttpConfiguration httpConfig = new HttpConfiguration();
+ httpConfig.setSecureScheme("https");
+ httpConfig.setSecurePort(httpsPort);
+ httpConfig.setSendServerVersion(true);
+ httpConfig.setSendDateHeader(false);
+
+ // Handler Structure
+ HandlerCollection handlers = new HandlerCollection();
+ handlers.setHandlers(new Handler[] {proxyHandler(), new DefaultHandler()});
+ server.setHandler(handlers);
+
+ ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(httpConfig));
+ http.setPort(httpPort);
+ server.addConnector(http);
+
+ Path serverKeyStore = Paths.get(SSLTest.class.getResource("/analysis/SSLTest/serverkeystore.jks").toURI()).toAbsolutePath();
+ String keyStorePassword = "serverkeystorepwd";
+ String serverKeyPassword = "serverp12pwd";
+ Path serverTrustStore = Paths.get(SSLTest.class.getResource("/analysis/SSLTest/servertruststore.jks").toURI()).toAbsolutePath();
+ String trustStorePassword = "servertruststorepwd";
+
+ // SSL Context Factory
+ SslContextFactory sslContextFactory = new SslContextFactory();
+ sslContextFactory.setKeyStorePath(serverKeyStore.toString());
+ sslContextFactory.setKeyStorePassword(keyStorePassword);
+ sslContextFactory.setKeyManagerPassword(serverKeyPassword);
+ sslContextFactory.setTrustStorePath(serverTrustStore.toString());
+ sslContextFactory.setTrustStorePassword(trustStorePassword);
+ sslContextFactory.setNeedClientAuth(requireClientAuth);
+ sslContextFactory.setExcludeCipherSuites("SSL_RSA_WITH_DES_CBC_SHA",
+ "SSL_DHE_RSA_WITH_DES_CBC_SHA", "SSL_DHE_DSS_WITH_DES_CBC_SHA",
+ "SSL_RSA_EXPORT_WITH_RC4_40_MD5",
+ "SSL_RSA_EXPORT_WITH_DES40_CBC_SHA",
+ "SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA",
+ "SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA");
+
+ // SSL HTTP Configuration
+ HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig);
+
+ // SSL Connector
+ ServerConnector sslConnector = new ServerConnector(server,
+ new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()),
+ new HttpConnectionFactory(httpsConfig));
+ sslConnector.setPort(httpsPort);
+ server.addConnector(sslConnector);
+
+ server.start();
+ }
+
+ private static ServletContextHandler proxyHandler() {
+ ServletContextHandler contextHandler = new ServletContextHandler();
+ contextHandler.setServletHandler(newServletHandler());
+ return contextHandler;
+ }
+
+ private static ServletHandler newServletHandler() {
+ ServletHandler handler = new ServletHandler();
+ ServletHolder holder = handler.addServletWithMapping(ProxyServlet.Transparent.class, "/*");
+ holder.setInitParameter("proxyTo", orchestrator.getServer().getUrl());
+ return handler;
+ }
+
+ @After
+ public void stopProxy() throws Exception {
+ if (server != null && server.isStarted()) {
+ server.stop();
+ }
+ }
+
+ @Test
+ public void simple_analysis_with_server_and_client_certificate() throws Exception {
+ startSSLTransparentReverseProxy(true);
+
+ SonarScanner sonarScanner = SonarScanner.create(ItUtils.projectDir("shared/xoo-sample"))
+ .setScannerVersion("2.7")
+ .setProperty("sonar.host.url", "https://localhost:" + httpsPort);
+
+ BuildResult buildResult = orchestrator.executeBuildQuietly(sonarScanner);
+ assertThat(buildResult.getLastStatus()).isNotEqualTo(0);
+ assertThat(buildResult.getLogs()).contains("javax.net.ssl.SSLHandshakeException");
+
+ Path clientTruststore = Paths.get(SSLTest.class.getResource(CLIENT_TRUSTSTORE).toURI()).toAbsolutePath();
+ String truststorePassword = CLIENT_TRUSTSTORE_PWD;
+ Path clientKeystore = Paths.get(SSLTest.class.getResource(CLIENT_KEYSTORE).toURI()).toAbsolutePath();
+ String keystorePassword = CLIENT_KEYSTORE_PWD;
+
+ buildResult = orchestrator.executeBuild(sonarScanner
+ .setEnvironmentVariable("SONAR_SCANNER_OPTS",
+ "-Djavax.net.ssl.trustStore=" + clientTruststore.toString() +
+ " -Djavax.net.ssl.trustStorePassword=" + truststorePassword +
+ " -Djavax.net.ssl.keyStore=" + clientKeystore.toString() +
+ " -Djavax.net.ssl.keyStorePassword=" + keystorePassword));
+ }
+
+ @Test
+ public void simple_analysis_with_server_certificate() throws Exception {
+ startSSLTransparentReverseProxy(false);
+
+ SonarScanner sonarScanner = SonarScanner.create(ItUtils.projectDir("shared/xoo-sample"))
+ .setScannerVersion("2.7")
+ .setProperty("sonar.host.url", "https://localhost:" + httpsPort);
+
+ BuildResult buildResult = orchestrator.executeBuildQuietly(sonarScanner);
+ assertThat(buildResult.getLastStatus()).isNotEqualTo(0);
+ assertThat(buildResult.getLogs()).contains("javax.net.ssl.SSLHandshakeException");
+
+ Path clientTruststore = Paths.get(SSLTest.class.getResource(CLIENT_TRUSTSTORE).toURI()).toAbsolutePath();
+ String truststorePassword = CLIENT_TRUSTSTORE_PWD;
+
+ buildResult = orchestrator.executeBuild(sonarScanner
+ .setEnvironmentVariable("SONAR_SCANNER_OPTS",
+ "-Djavax.net.ssl.trustStore=" + clientTruststore.toString() +
+ " -Djavax.net.ssl.trustStorePassword=" + truststorePassword));
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/analysis/ScannerTest.java b/tests/src/test/java/org/sonarqube/tests/analysis/ScannerTest.java
new file mode 100644
index 00000000000..a175bbf6294
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/analysis/ScannerTest.java
@@ -0,0 +1,395 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.analysis;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.BuildResult;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category3Suite;
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+import org.apache.commons.io.FileUtils;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.sonarqube.ws.client.component.SearchWsRequest;
+import util.ItUtils;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.getComponent;
+import static util.ItUtils.getComponentNavigation;
+import static util.ItUtils.getMeasureAsDouble;
+import static util.ItUtils.getMeasuresAsDoubleByMetricKey;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.resetSettings;
+import static util.ItUtils.setServerProperty;
+
+public class ScannerTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category3Suite.ORCHESTRATOR;
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Before
+ public void deleteData() {
+ orchestrator.resetData();
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/analysis/BatchTest/one-issue-per-line.xml"));
+ }
+
+ /**
+ * SONAR-3718
+ */
+ @Test
+ public void should_scan_branch_with_forward_slash() {
+ scan("shared/xoo-multi-modules-sample");
+ scan("shared/xoo-multi-modules-sample", "sonar.branch", "branch/0.x");
+
+ assertThat(newAdminWsClient(orchestrator).components().search(new SearchWsRequest().setQualifiers(singletonList("TRK"))).getComponentsList()).hasSize(2);
+ assertThat(getComponent(orchestrator, "com.sonarsource.it.samples:multi-modules-sample").getName()).isEqualTo("Sonar :: Integration Tests :: Multi-modules Sample");
+ assertThat(getComponent(orchestrator, "com.sonarsource.it.samples:multi-modules-sample:branch/0.x").getName())
+ .isEqualTo("Sonar :: Integration Tests :: Multi-modules Sample branch/0.x");
+ }
+
+ @Test
+ public void use_sonar_profile_without_provisioning_project() {
+ scan("shared/xoo-multi-modules-sample",
+ "sonar.profile", "one-issue-per-line",
+ "sonar.verbose", "true");
+ assertThat(getMeasureAsDouble(orchestrator, "com.sonarsource.it.samples:multi-modules-sample", "violations")).isEqualTo(61);
+ }
+
+ // SONAR-4680
+ @Test
+ public void module_should_load_own_settings_from_database() {
+ String rootModuleKey = "com.sonarsource.it.samples:multi-modules-sample";
+ orchestrator.getServer().provisionProject(rootModuleKey, "Sonar :: Integration Tests :: Multi-modules Sample");
+
+ String propKey = "myFakeProperty";
+ String moduleBKey = rootModuleKey + ":module_b";
+ resetSettings(orchestrator, rootModuleKey, propKey);
+
+ BuildResult result = scan("shared/xoo-multi-modules-sample", "sonar.showSettings", propKey);
+
+ assertThat(result.getLogs()).doesNotContain(rootModuleKey + ":" + propKey);
+ assertThat(result.getLogs()).doesNotContain(moduleBKey + ":" + propKey);
+
+ // Set property only on root project
+ setServerProperty(orchestrator, rootModuleKey, propKey, "project");
+
+ result = scan("shared/xoo-multi-modules-sample", "sonar.showSettings", propKey);
+
+ assertThat(result.getLogs()).contains(rootModuleKey + ":" + propKey + " = project");
+ assertThat(result.getLogs()).contains(moduleBKey + ":" + propKey + " = project");
+
+ // Override property on moduleB
+ setServerProperty(orchestrator, moduleBKey, propKey, "moduleB");
+
+ result = scan("shared/xoo-multi-modules-sample", "sonar.showSettings", propKey);
+
+ assertThat(result.getLogs()).contains(rootModuleKey + ":" + propKey + " = project");
+ assertThat(result.getLogs()).contains(moduleBKey + ":" + propKey + " = moduleB");
+ }
+
+ // SONAR-4680
+ @Test
+ public void module_should_load_settings_from_parent() {
+ String rootModuleKey = "com.sonarsource.it.samples:multi-modules-sample";
+ orchestrator.getServer().provisionProject(rootModuleKey, "Sonar :: Integration Tests :: Multi-modules Sample");
+
+ String propKey = "myFakeProperty";
+ String moduleBKey = rootModuleKey + ":module_b";
+
+ // Set property on provisioned project
+ setServerProperty(orchestrator, rootModuleKey, propKey, "project");
+
+ BuildResult result = scan("shared/xoo-multi-modules-sample", "sonar.showSettings", propKey);
+
+ assertThat(result.getLogs()).contains(rootModuleKey + ":" + propKey + " = project");
+ // Module should inherit from parent
+ assertThat(result.getLogs()).contains(moduleBKey + ":" + propKey + " = project");
+ }
+
+ /**
+ * SONAR-3024
+ */
+ @Test
+ public void should_support_source_files_with_same_deprecated_key() {
+ orchestrator.getServer().provisionProject("com.sonarsource.it.projects.batch:duplicate-source", "exclusions");
+ orchestrator.getServer().associateProjectToQualityProfile("com.sonarsource.it.projects.batch:duplicate-source", "xoo", "one-issue-per-line");
+ scan("analysis/duplicate-source");
+
+ Map<String, Double> measures = getMeasuresAsDoubleByMetricKey(orchestrator, "com.sonarsource.it.projects.batch:duplicate-source", "files", "directories");
+ // 2 main files and 1 test file all with same deprecated key
+ assertThat(measures.get("files")).isEqualTo(2);
+ assertThat(measures.get("directories")).isEqualTo(2);
+ }
+
+ /**
+ * SONAR-3125
+ */
+ @Test
+ public void should_display_explicit_message_when_no_plugin_language_available() {
+ orchestrator.getServer().provisionProject("sample", "xoo-sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");
+
+ BuildResult buildResult = scanQuietly("shared/xoo-sample",
+ "sonar.language", "foo",
+ "sonar.profile", "");
+ assertThat(buildResult.getLastStatus()).isEqualTo(1);
+ assertThat(buildResult.getLogs()).contains(
+ "You must install a plugin that supports the language 'foo'");
+ }
+
+ @Test
+ public void should_display_explicit_message_when_wrong_profile() {
+ orchestrator.getServer().provisionProject("sample", "xoo-sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");
+
+ BuildResult buildResult = scanQuietly("shared/xoo-sample",
+ "sonar.profile", "unknow");
+ assertThat(buildResult.getLastStatus()).isEqualTo(1);
+ assertThat(buildResult.getLogs()).contains(
+ "sonar.profile was set to 'unknow' but didn't match any profile for any language. Please check your configuration.");
+ }
+
+ @Test
+ public void should_create_project_without_name_version() {
+ // some of the sub-modules have a name defined, others don't
+ BuildResult buildResult = scan("shared/xoo-multi-module-sample-without-project-name-version");
+ assertThat(buildResult.isSuccess()).isTrue();
+
+ assertNameAndVersion("com.sonarsource.it.samples:multi-modules-sample", "com.sonarsource.it.samples:multi-modules-sample", "not provided");
+
+ assertNameAndVersion("com.sonarsource.it.samples:multi-modules-sample:module_b", "module_b", "not provided");
+ assertNameAndVersion("com.sonarsource.it.samples:multi-modules-sample:module_b:module_b1", "module_b1", "not provided");
+ assertNameAndVersion("com.sonarsource.it.samples:multi-modules-sample:module_b:module_b2", "Sub-module B2", "not provided");
+
+ assertNameAndVersion("com.sonarsource.it.samples:multi-modules-sample:module_a", "Module A", "not provided");
+ assertNameAndVersion("com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1", "Sub-module A1", "not provided");
+ assertNameAndVersion("com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2", "Sub-module A2", "not provided");
+ }
+
+ @Test
+ public void should_analyze_project_without_name_version() {
+ orchestrator.getServer().provisionProject("com.sonarsource.it.samples:multi-modules-sample", "My project name");
+ BuildResult buildResult = scan("shared/xoo-multi-module-sample-without-project-name-version",
+ "sonar.projectName", "My project name",
+ "sonar.projectVersion", "1.0");
+ assertThat(buildResult.isSuccess()).isTrue();
+
+ buildResult = scan("shared/xoo-multi-module-sample-without-project-name-version");
+ assertThat(buildResult.isSuccess()).isTrue();
+
+ assertNameAndVersion("com.sonarsource.it.samples:multi-modules-sample", "My project name", "1.0");
+
+ assertNameAndVersion("com.sonarsource.it.samples:multi-modules-sample:module_b", "module_b", "1.0");
+ assertNameAndVersion("com.sonarsource.it.samples:multi-modules-sample:module_b:module_b1", "module_b1", "1.0");
+ assertNameAndVersion("com.sonarsource.it.samples:multi-modules-sample:module_b:module_b2", "Sub-module B2", "1.0");
+
+ assertNameAndVersion("com.sonarsource.it.samples:multi-modules-sample:module_a", "Module A", "1.0");
+ assertNameAndVersion("com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1", "Sub-module A1", "1.0");
+ assertNameAndVersion("com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2", "Sub-module A2", "1.0");
+ }
+
+ private void assertNameAndVersion(String projectKey, String expectedProjectName, String expectedProjectVersion) {
+ assertThat(getComponent(orchestrator, projectKey).getName()).isEqualTo(expectedProjectName);
+ assertThat(getComponentNavigation(orchestrator, projectKey).getVersion()).isEqualTo(expectedProjectVersion);
+ }
+
+ @Test
+ public void should_honor_sonarUserHome() {
+ File userHome = temp.getRoot();
+
+ orchestrator.getServer().provisionProject("sample", "xoo-sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");
+
+ SonarScanner scanner = configureScanner("shared/xoo-sample",
+ "sonar.verbose", "true");
+ scanner.setEnvironmentVariable("SONAR_USER_HOME", "/dev/null");
+ BuildResult buildResult = orchestrator.executeBuildQuietly(scanner);
+ assertThat(buildResult.getLastStatus()).isEqualTo(1);
+
+ buildResult = scan("shared/xoo-sample",
+ "sonar.verbose", "true",
+ "sonar.userHome", userHome.getAbsolutePath());
+ assertThat(buildResult.isSuccess()).isTrue();
+ }
+
+ /**
+ * SONAR-2291
+ */
+ @Test
+ public void batch_should_cache_plugin_jars() throws IOException {
+ File userHome = temp.newFolder();
+
+ BuildResult result = scan("shared/xoo-sample",
+ "sonar.userHome", userHome.getAbsolutePath());
+
+ File cache = new File(userHome, "cache");
+ assertThat(cache).exists().isDirectory();
+ int cachedFiles = FileUtils.listFiles(cache, new String[] {"jar"}, true).size();
+ assertThat(cachedFiles).isGreaterThan(5);
+ assertThat(result.getLogs()).contains("User cache: " + cache.getAbsolutePath());
+ assertThat(result.getLogs()).contains("Download sonar-xoo-plugin-");
+
+ result = scan("shared/xoo-sample",
+ "sonar.userHome", userHome.getAbsolutePath());
+ assertThat(cachedFiles).isEqualTo(cachedFiles);
+ assertThat(result.getLogs()).contains("User cache: " + cache.getAbsolutePath());
+ assertThat(result.getLogs()).doesNotContain("Download sonar-xoo-plugin-");
+ }
+
+ @Test
+ public void batch_should_keep_report_verbose() {
+ orchestrator.getServer().provisionProject("sample", "xoo-sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");
+
+ scanQuietly("shared/xoo-sample", "sonar.verbose", "true");
+ File reportDir = new File(new File(ItUtils.projectDir("shared/xoo-sample"), ".sonar"), "batch-report");
+ assertThat(reportDir).isDirectory();
+ assertThat(reportDir.list()).isNotEmpty();
+ }
+
+ /**
+ * SONAR-4239
+ */
+ @Test
+ public void should_display_project_url_after_analysis() throws IOException {
+ orchestrator.getServer().provisionProject("com.sonarsource.it.samples:multi-modules-sample", "Sonar :: Integration Tests :: Multi-modules Sample");
+ orchestrator.getServer().associateProjectToQualityProfile("com.sonarsource.it.samples:multi-modules-sample", "xoo", "one-issue-per-line");
+ Assume.assumeTrue(orchestrator.getServer().version().isGreaterThanOrEquals("3.6"));
+
+ BuildResult result = scan("shared/xoo-multi-modules-sample");
+
+ assertThat(result.getLogs()).contains("/dashboard/index/com.sonarsource.it.samples:multi-modules-sample");
+
+ result = scan("shared/xoo-multi-modules-sample",
+ "sonar.branch", "mybranch");
+
+ assertThat(result.getLogs()).contains("/dashboard/index/com.sonarsource.it.samples:multi-modules-sample:mybranch");
+
+ try {
+ setServerProperty(orchestrator, null, "sonar.core.serverBaseURL", "http://foo:123/sonar");
+ result = scan("shared/xoo-multi-modules-sample");
+ assertThat(result.getLogs()).contains("http://foo:123/sonar/dashboard/index/com.sonarsource.it.samples:multi-modules-sample");
+ } finally {
+ resetSettings(orchestrator, null, "sonar.core.serverBaseURL");
+ }
+ }
+
+ /**
+ * SONAR-4188, SONAR-5178, SONAR-5915
+ */
+ @Test
+ public void should_display_explicit_message_when_invalid_project_key_or_branch() {
+ orchestrator.getServer().provisionProject("sample", "xoo-sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");
+
+ BuildResult buildResult = scanQuietly("shared/xoo-sample",
+ "sonar.projectKey", "ar g$l:");
+ assertThat(buildResult.getLastStatus()).isEqualTo(1);
+ assertThat(buildResult.getLogs()).contains("\"ar g$l:\" is not a valid project or module key")
+ .contains("Allowed characters");
+
+ // SONAR-4629
+ buildResult = scanQuietly("shared/xoo-sample",
+ "sonar.projectKey", "12345");
+ assertThat(buildResult.getLastStatus()).isEqualTo(1);
+ assertThat(buildResult.getLogs()).contains("\"12345\" is not a valid project or module key")
+ .contains("Allowed characters");
+
+ buildResult = scanQuietly("shared/xoo-sample",
+ "sonar.branch", "ar g$l:");
+ assertThat(buildResult.getLastStatus()).isEqualTo(1);
+ assertThat(buildResult.getLogs()).contains("\"ar g$l:\" is not a valid branch")
+ .contains("Allowed characters");
+ }
+
+ /**
+ * SONAR-4547
+ */
+ @Test
+ public void display_MessageException_without_stacktrace() throws Exception {
+ orchestrator.getServer().provisionProject("sample", "xoo-sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");
+ BuildResult result = scanQuietly("shared/xoo-sample", "raiseMessageException", "true");
+ assertThat(result.getLastStatus()).isNotEqualTo(0);
+ assertThat(result.getLogs())
+ // message
+ .contains("Error message from plugin")
+
+ // but not stacktrace
+ .doesNotContain("at com.sonarsource.RaiseMessageException");
+ }
+
+ /**
+ * SONAR-4751
+ */
+ @Test
+ public void file_extensions_are_case_insensitive() throws Exception {
+ orchestrator.getServer().provisionProject("case-sensitive-file-extensions", "Case Sensitive");
+ orchestrator.getServer().associateProjectToQualityProfile("case-sensitive-file-extensions", "xoo", "one-issue-per-line");
+ scan("analysis/case-sensitive-file-extensions");
+
+ Map<String, Double> measures = getMeasuresAsDoubleByMetricKey(orchestrator, "case-sensitive-file-extensions", "files", "ncloc");
+ assertThat(measures.get("files")).isEqualTo(2);
+ assertThat(measures.get("ncloc")).isEqualTo(5 + 2);
+ }
+
+ /**
+ * SONAR-4876
+ */
+ @Test
+ public void custom_module_key() {
+ orchestrator.getServer().provisionProject("com.sonarsource.it.samples:multi-modules-sample", "Sonar :: Integration Tests :: Multi-modules Sample");
+ orchestrator.getServer().associateProjectToQualityProfile("com.sonarsource.it.samples:multi-modules-sample", "xoo", "one-issue-per-line");
+ scan("analysis/custom-module-key");
+ assertThat(getComponent(orchestrator, "com.sonarsource.it.samples:moduleA")).isNotNull();
+ assertThat(getComponent(orchestrator, "com.sonarsource.it.samples:moduleB")).isNotNull();
+ }
+
+ private BuildResult scan(String projectPath, String... props) {
+ SonarScanner scanner = configureScanner(projectPath, props);
+ return orchestrator.executeBuild(scanner);
+ }
+
+ private BuildResult scanQuietly(String projectPath, String... props) {
+ SonarScanner scanner = configureScanner(projectPath, props);
+ return orchestrator.executeBuildQuietly(scanner);
+ }
+
+ private SonarScanner configureScanner(String projectPath, String... props) {
+ SonarScanner scanner = SonarScanner.create(ItUtils.projectDir(projectPath))
+ .setProperties(props);
+ return scanner;
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/analysis/SettingsEncryptionTest.java b/tests/src/test/java/org/sonarqube/tests/analysis/SettingsEncryptionTest.java
new file mode 100644
index 00000000000..8a530680550
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/analysis/SettingsEncryptionTest.java
@@ -0,0 +1,83 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.analysis;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.BuildFailureException;
+import com.sonar.orchestrator.build.BuildResult;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category3Suite;
+import java.io.File;
+import java.net.URL;
+import org.junit.ClassRule;
+import org.junit.Test;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SettingsEncryptionTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category3Suite.ORCHESTRATOR;
+
+ /**
+ * SONAR-2084
+ * SONAR-4061
+ */
+ @Test
+ public void testEncryptedProperty() throws Exception {
+ SonarScanner build = SonarScanner.create(ItUtils.projectDir("shared/xoo-sample"))
+ .setProperty("sonar.secretKeyPath", pathToValidSecretKey())
+ .setProperty("sonar.login", "admin")
+ // wrong password
+ .setProperty("sonar.password", "{aes}wrongencryption==")// wrong password
+ // "this is a secret" encrypted with the above secret key
+ .setProperty("encryptedProperty", "{aes}9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY=");
+ BuildResult result = orchestrator.executeBuildQuietly(build);
+ assertThat(result.getStatus()).isNotEqualTo(0);
+ assertThat(result.getLogs()).contains("Fail to decrypt the property sonar.password. Please check your secret key");
+
+ build = SonarScanner.create(ItUtils.projectDir("shared/xoo-sample"))
+ .setProperty("sonar.secretKeyPath", pathToValidSecretKey())
+ // "admin" encrypted with the above secret key
+ .setProperty("sonar.login", "{aes}evRHXHsEyPr5RjEuxUJcHA==")
+ .setProperty("sonar.password", "{aes}evRHXHsEyPr5RjEuxUJcHA==")
+ // "this is a secret" encrypted with the above secret key
+ .setProperty("encryptedProperty", "{aes}9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY=");
+ // no error
+ orchestrator.executeBuild(build);
+ }
+
+ /**
+ * SONAR-2084
+ */
+ @Test(expected = BuildFailureException.class)
+ public void failIfEncryptedPropertyButNoSecretKey() throws Exception {
+ // path to secret key is missing
+ SonarScanner build = SonarScanner.create(ItUtils.projectDir("shared/xoo-sample"))
+ .setProperty("encryptedProperty", "{aes}9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY=");
+ orchestrator.executeBuild(build);
+ }
+
+ private String pathToValidSecretKey() throws Exception {
+ URL resource = getClass().getResource("/analysis/SettingsEncryptionTest/sonar-secret.txt");
+ return new File(resource.toURI()).getCanonicalPath();
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/analysis/TempFolderTest.java b/tests/src/test/java/org/sonarqube/tests/analysis/TempFolderTest.java
new file mode 100644
index 00000000000..9843ce5ce32
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/analysis/TempFolderTest.java
@@ -0,0 +1,98 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.analysis;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.BuildResult;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category3Suite;
+import java.io.File;
+import java.io.IOException;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TempFolderTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category3Suite.ORCHESTRATOR;
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Before
+ public void deleteData() {
+ orchestrator.resetData();
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/analysis/TempFolderTest/one-issue-per-line.xml"));
+ orchestrator.getServer().provisionProject("sample", "Sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");
+ }
+
+ // SONAR-4748
+ @Test
+ public void should_create_in_temp_folder() throws IOException {
+ File projectDir = ItUtils.projectDir("shared/xoo-sample");
+ BuildResult result = scan();
+
+ assertThat(result.getLogs()).doesNotContain("Creating temp directory:");
+ assertThat(result.getLogs()).doesNotContain("Creating temp file:");
+
+ result = scan("sonar.createTempFiles", "true");
+ assertThat(result.getLogs()).contains(
+ "Creating temp directory: " + projectDir.getCanonicalPath() + File.separator + ".sonar" + File.separator + ".sonartmp" + File.separator + "sonar-it");
+ assertThat(result.getLogs()).contains(
+ "Creating temp file: " + projectDir.getCanonicalPath() + File.separator + ".sonar" + File.separator + ".sonartmp" + File.separator + "sonar-it");
+
+ // Verify temp folder is deleted after analysis
+ assertThat(new File(projectDir, ".sonar/.sonartmp/sonar-it")).doesNotExist();
+ }
+
+ // SONAR-4748
+ @Test
+ public void should_not_use_system_tmp_dir() throws Exception {
+ File tmp = temp.newFolder();
+ SonarScanner runner = configureScanner()
+ .setEnvironmentVariable("SONAR_RUNNER_OPTS", "-Djava.io.tmpdir=" + tmp.getAbsolutePath());
+ orchestrator.executeBuild(runner);
+
+ // temp directory is clean-up
+ assertThat(tmp.list()).isEmpty();
+ }
+
+ private BuildResult scan(String... props) {
+ SonarScanner runner = configureScanner(props);
+ return orchestrator.executeBuild(runner);
+ }
+
+ private SonarScanner configureScanner(String... props) {
+ return SonarScanner.create(ItUtils.projectDir("shared/xoo-sample"))
+ .setProperties(props);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/authorisation/ExecuteAnalysisPermissionTest.java b/tests/src/test/java/org/sonarqube/tests/authorisation/ExecuteAnalysisPermissionTest.java
new file mode 100644
index 00000000000..65f1a52bce1
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/authorisation/ExecuteAnalysisPermissionTest.java
@@ -0,0 +1,151 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.authorisation;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.BuildFailureException;
+import org.sonarqube.tests.Category1Suite;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonar.wsclient.SonarClient;
+import org.sonar.wsclient.user.UserParameters;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.permission.AddGroupWsRequest;
+import org.sonarqube.ws.client.permission.AddProjectCreatorToTemplateWsRequest;
+import org.sonarqube.ws.client.permission.RemoveGroupWsRequest;
+import org.sonarqube.ws.client.project.UpdateVisibilityRequest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.runProjectAnalysis;
+
+/**
+ * SONAR-4397
+ */
+public class ExecuteAnalysisPermissionTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ private final static String USER_LOGIN = "scanperm";
+ private final static String USER_PASSWORD = "thewhite";
+ private final static String PROJECT_KEY = "sample";
+
+ private static WsClient adminWsClient;
+ private static SonarClient oldAdminWsClient;
+
+ @Before
+ public void setUp() {
+ orchestrator.resetData();
+ oldAdminWsClient = orchestrator.getServer().adminWsClient();
+ oldAdminWsClient.userClient().create(UserParameters.create().login(USER_LOGIN).name(USER_LOGIN).password(USER_PASSWORD).passwordConfirmation(USER_PASSWORD));
+ orchestrator.getServer().provisionProject(PROJECT_KEY, "Sample");
+ adminWsClient = newAdminWsClient(orchestrator);
+ }
+
+ @After
+ public void tearDown() {
+ addGlobalPermission("anyone", "scan");
+ oldAdminWsClient.userClient().deactivate(USER_LOGIN);
+ }
+
+ @Test
+ public void should_fail_if_logged_but_no_scan_permission() throws Exception {
+ executeLoggedAnalysis();
+
+ removeGlobalPermission("anyone", "scan");
+ try {
+ // Execute logged analysis, but without the "Execute Analysis" permission
+ executeLoggedAnalysis();
+ fail();
+ } catch (BuildFailureException e) {
+ assertThat(e.getResult().getLogs()).contains(
+ "You're only authorized to execute a local (preview) SonarQube analysis without pushing the results to the SonarQube server. Please contact your SonarQube administrator.");
+ }
+
+ newAdminWsClient(orchestrator).projects().updateVisibility(UpdateVisibilityRequest.builder().setProject(PROJECT_KEY).setVisibility("private").build());
+ try {
+ // Execute anonymous analysis
+ executeAnonymousAnalysis();
+ fail();
+ } catch (BuildFailureException e) {
+ assertThat(e.getResult().getLogs()).contains(
+ "You're not authorized to execute any SonarQube analysis. Please contact your SonarQube administrator.");
+ }
+ }
+
+ @Test
+ public void no_need_for_browse_permission_to_scan() throws Exception {
+ // Do a first analysis, no error
+ executeAnonymousAnalysis();
+
+ // make project private
+ newAdminWsClient(orchestrator).projects().updateVisibility(UpdateVisibilityRequest.builder().setProject("sample").setVisibility("private").build());
+
+ // still no error
+ executeAnonymousAnalysis();
+ }
+
+ @Test
+ public void execute_analysis_with_scan_permission_only_on_project() throws Exception {
+ removeGlobalPermission("anyone", "scan");
+ addProjectPermission("anyone", PROJECT_KEY, "scan");
+
+ executeLoggedAnalysis();
+ }
+
+ @Test
+ public void execute_analysis_with_scan_on_default_template() {
+ removeGlobalPermission("anyone", "scan");
+ adminWsClient.permissions().addProjectCreatorToTemplate(AddProjectCreatorToTemplateWsRequest.builder()
+ .setPermission("scan")
+ .setTemplateId("default_template")
+ .build());
+
+ runProjectAnalysis(orchestrator, "shared/xoo-sample", "sonar.login", USER_LOGIN, "sonar.password", USER_PASSWORD, "sonar.projectKey", "ANOTHER_PROJECT_KEY");
+ }
+
+ private static void addProjectPermission(String groupName, String projectKey, String permission) {
+ adminWsClient.permissions().addGroup(new AddGroupWsRequest().setGroupName(groupName).setProjectKey(projectKey).setPermission(permission));
+ }
+
+ private static void addGlobalPermission(String groupName, String permission) {
+ adminWsClient.permissions().addGroup(new AddGroupWsRequest().setGroupName(groupName).setPermission(permission));
+ }
+
+ private static void removeProjectPermission(String groupName, String projectKey, String permission) {
+ adminWsClient.permissions().removeGroup(new RemoveGroupWsRequest().setGroupName(groupName).setProjectKey(projectKey).setPermission(permission));
+ }
+
+ private static void removeGlobalPermission(String groupName, String permission) {
+ adminWsClient.permissions().removeGroup(new RemoveGroupWsRequest().setGroupName(groupName).setPermission(permission));
+ }
+
+ private static void executeLoggedAnalysis() {
+ runProjectAnalysis(orchestrator, "shared/xoo-sample", "sonar.login", USER_LOGIN, "sonar.password", USER_PASSWORD);
+ }
+
+ private static void executeAnonymousAnalysis() {
+ runProjectAnalysis(orchestrator, "shared/xoo-sample");
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/authorisation/IssuePermissionTest.java b/tests/src/test/java/org/sonarqube/tests/authorisation/IssuePermissionTest.java
new file mode 100644
index 00000000000..21f39e7d636
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/authorisation/IssuePermissionTest.java
@@ -0,0 +1,276 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.authorisation;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category1Suite;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonar.wsclient.SonarClient;
+import org.sonar.wsclient.base.HttpException;
+import org.sonar.wsclient.issue.Issue;
+import org.sonar.wsclient.issue.IssueQuery;
+import org.sonar.wsclient.user.UserParameters;
+import org.sonarqube.ws.Issues;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.issue.BulkChangeRequest;
+import org.sonarqube.ws.client.permission.AddUserWsRequest;
+import org.sonarqube.ws.client.project.UpdateVisibilityRequest;
+import util.ItUtils;
+
+import static java.util.Arrays.asList;
+import static junit.framework.TestCase.fail;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.newUserWsClient;
+import static util.ItUtils.projectDir;
+
+public class IssuePermissionTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+ public WsClient adminWsClient = newAdminWsClient(orchestrator);
+
+ @Before
+ public void init() {
+ orchestrator.resetData();
+
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/authorisation/one-issue-per-line-profile.xml"));
+
+ orchestrator.getServer().provisionProject("privateProject", "PrivateProject");
+ newAdminWsClient(orchestrator).projects().updateVisibility(UpdateVisibilityRequest.builder().setProject("privateProject").setVisibility("private").build());
+ orchestrator.getServer().associateProjectToQualityProfile("privateProject", "xoo", "one-issue-per-line");
+ SonarScanner privateProject = SonarScanner.create(projectDir("shared/xoo-sample"))
+ .setProperty("sonar.projectKey", "privateProject")
+ .setProperty("sonar.projectName", "PrivateProject");
+ orchestrator.executeBuild(privateProject);
+
+ orchestrator.getServer().provisionProject("publicProject", "PublicProject");
+ orchestrator.getServer().associateProjectToQualityProfile("publicProject", "xoo", "one-issue-per-line");
+ SonarScanner publicProject = SonarScanner.create(projectDir("shared/xoo-sample"))
+ .setProperty("sonar.projectKey", "publicProject")
+ .setProperty("sonar.projectName", "PublicProject");
+
+
+ orchestrator.executeBuild(publicProject);
+ }
+
+ @Test
+ public void need_user_permission_on_project_to_see_issue() {
+ SonarClient client = orchestrator.getServer().adminWsClient();
+
+ String withBrowsePermission = "with-browse-permission";
+ String withoutBrowsePermission = "without-browse-permission";
+
+ try {
+ client.userClient().create(UserParameters.create().login(withBrowsePermission).name(withBrowsePermission)
+ .password("password").passwordConfirmation("password"));
+ addUserPermission(withBrowsePermission, "privateProject", "user");
+
+ client.userClient().create(UserParameters.create().login(withoutBrowsePermission).name(withoutBrowsePermission)
+ .password("password").passwordConfirmation("password"));
+
+ // Without user permission, a user cannot see issues on the project
+ assertThat(orchestrator.getServer().wsClient(withoutBrowsePermission, "password").issueClient().find(
+ IssueQuery.create().componentRoots("privateProject")).list()).isEmpty();
+
+ // With user permission, a user can see issues on the project
+ assertThat(orchestrator.getServer().wsClient(withBrowsePermission, "password").issueClient().find(
+ IssueQuery.create().componentRoots("privateProject")).list()).isNotEmpty();
+
+ } finally {
+ client.userClient().deactivate(withBrowsePermission);
+ client.userClient().deactivate(withoutBrowsePermission);
+ }
+ }
+
+ /**
+ * SONAR-4839
+ */
+ @Test
+ public void need_user_permission_on_project_to_see_issue_changelog() {
+ SonarClient client = orchestrator.getServer().adminWsClient();
+ Issue issue = client.issueClient().find(IssueQuery.create().componentRoots("privateProject")).list().get(0);
+ client.issueClient().assign(issue.key(), "admin");
+
+ String withBrowsePermission = "with-browse-permission";
+ String withoutBrowsePermission = "without-browse-permission";
+
+ try {
+ client.userClient().create(UserParameters.create().login(withBrowsePermission).name(withBrowsePermission)
+ .password("password").passwordConfirmation("password"));
+ addUserPermission(withBrowsePermission, "privateProject", "user");
+
+ client.userClient().create(UserParameters.create().login(withoutBrowsePermission).name(withoutBrowsePermission)
+ .password("password").passwordConfirmation("password"));
+
+ // Without user permission, a user cannot see issue changelog on the project
+ try {
+ changelog(issue.key(), withoutBrowsePermission, "password");
+ fail();
+ } catch (Exception e) {
+ assertThat(e).isInstanceOf(org.sonarqube.ws.client.HttpException.class).describedAs("403");
+ }
+
+ // Without user permission, a user cannot see issues on the project
+ assertThat(changelog(issue.key(), withBrowsePermission, "password").getChangelogList()).isNotEmpty();
+
+ } finally {
+ client.userClient().deactivate(withBrowsePermission);
+ client.userClient().deactivate(withoutBrowsePermission);
+ }
+ }
+
+ /**
+ * SONAR-2447
+ */
+ @Test
+ public void need_administer_issue_permission_on_project_to_set_severity() {
+ SonarClient client = orchestrator.getServer().adminWsClient();
+ Issue issueOnPrivateProject = client.issueClient().find(IssueQuery.create().componentRoots("privateProject")).list().get(0);
+ Issue issueOnPublicProject = client.issueClient().find(IssueQuery.create().componentRoots("publicProject")).list().get(0);
+
+ String user = "user";
+
+ try {
+ client.userClient().create(UserParameters.create().login(user).name(user).password("password").passwordConfirmation("password"));
+ addUserPermission(user, "publicProject", "issueadmin");
+
+ // Without issue admin permission, a user cannot set severity on the issue
+ try {
+ orchestrator.getServer().wsClient(user, "password").issueClient().setSeverity(issueOnPrivateProject.key(), "BLOCKER");
+ fail();
+ } catch (Exception e) {
+ assertThat(e).isInstanceOf(HttpException.class).describedAs("404");
+ }
+
+ // With issue admin permission, a user can set severity on the issue
+ assertThat(orchestrator.getServer().wsClient(user, "password").issueClient().setSeverity(issueOnPublicProject.key(), "BLOCKER").severity()).isEqualTo("BLOCKER");
+
+ } finally {
+ client.userClient().deactivate(user);
+ }
+ }
+
+ /**
+ * SONAR-2447
+ */
+ @Test
+ public void need_administer_issue_permission_on_project_to_flag_as_false_positive() {
+ SonarClient client = orchestrator.getServer().adminWsClient();
+ Issue issueOnPrivateProject = client.issueClient().find(IssueQuery.create().componentRoots("privateProject")).list().get(0);
+ Issue issueOnPublicProject = client.issueClient().find(IssueQuery.create().componentRoots("publicProject")).list().get(0);
+
+ String user = "user";
+
+ try {
+ client.userClient().create(UserParameters.create().login(user).name(user).password("password").passwordConfirmation("password"));
+ addUserPermission(user, "publicProject", "issueadmin");
+
+ // Without issue admin permission, a user cannot flag an issue as false positive
+ try {
+ orchestrator.getServer().wsClient(user, "password").issueClient().doTransition(issueOnPrivateProject.key(), "falsepositive");
+ fail();
+ } catch (Exception e) {
+ assertThat(e).isInstanceOf(HttpException.class).describedAs("404");
+ }
+
+ // With issue admin permission, a user can flag an issue as false positive
+ assertThat(orchestrator.getServer().wsClient(user, "password").issueClient().doTransition(issueOnPublicProject.key(), "falsepositive").status()).isEqualTo("RESOLVED");
+
+ } finally {
+ client.userClient().deactivate(user);
+ }
+ }
+
+ /**
+ * SONAR-2447
+ */
+ @Test
+ public void need_administer_issue_permission_on_project_to_bulk_change_severity_and_false_positive() {
+ SonarClient client = orchestrator.getServer().adminWsClient();
+ Issue issueOnPrivateProject = client.issueClient().find(IssueQuery.create().componentRoots("privateProject")).list().get(0);
+ Issue issueOnPublicProject = client.issueClient().find(IssueQuery.create().componentRoots("publicProject")).list().get(0);
+
+ String user = "user";
+
+ try {
+ client.userClient().create(UserParameters.create().login(user).name(user).password("password").passwordConfirmation("password"));
+ addUserPermission(user, "privateProject", "issueadmin");
+
+ Issues.BulkChangeWsResponse response = makeBlockerAndFalsePositive(user, issueOnPrivateProject, issueOnPublicProject);
+
+ // public project but no issueadmin permission on publicProject => issue visible but not updated
+ // no user permission on privateproject => issue invisible and not updated
+ assertThat(response.getTotal()).isEqualTo(1);
+ assertThat(response.getSuccess()).isEqualTo(0);
+ assertThat(response.getIgnored()).isEqualTo(1);
+
+ addUserPermission(user, "privateProject", "user");
+ response = makeBlockerAndFalsePositive(user, issueOnPrivateProject, issueOnPublicProject);
+
+ // public project but no issueadmin permission on publicProject => unsuccessful on issueOnPublicProject
+ // user and issueadmin permission on privateproject => successful and 1 more issue visible
+ assertThat(response.getTotal()).isEqualTo(2);
+ assertThat(response.getSuccess()).isEqualTo(1);
+ assertThat(response.getIgnored()).isEqualTo(1);
+
+ addUserPermission(user, "publicProject", "issueadmin");
+ response = makeBlockerAndFalsePositive(user, issueOnPrivateProject, issueOnPublicProject);
+
+ // public and issueadmin permission on publicProject => successful on issueOnPublicProject
+ // issueOnPrivateProject already in specified state => unsuccessful
+ assertThat(response.getTotal()).isEqualTo(2);
+ assertThat(response.getSuccess()).isEqualTo(1);
+ assertThat(response.getIgnored()).isEqualTo(1);
+
+ response = makeBlockerAndFalsePositive(user, issueOnPrivateProject, issueOnPublicProject);
+
+ // issueOnPublicProject and issueOnPrivateProject already in specified state => unsuccessful
+ assertThat(response.getTotal()).isEqualTo(2);
+ assertThat(response.getSuccess()).isEqualTo(0);
+ assertThat(response.getIgnored()).isEqualTo(2);
+ } finally {
+ client.userClient().deactivate(user);
+ }
+ }
+
+ private Issues.BulkChangeWsResponse makeBlockerAndFalsePositive(String user, Issue issueOnPrivateProject, Issue issueOnPublicProject) {
+ return newUserWsClient(orchestrator, user, "password").issues()
+ .bulkChange(BulkChangeRequest.builder().setIssues(asList(issueOnPrivateProject.key(), issueOnPublicProject.key()))
+ .setSetSeverity("BLOCKER")
+ .setDoTransition("falsepositive")
+ .build());
+ }
+
+ private void addUserPermission(String login, String projectKey, String permission) {
+ adminWsClient.permissions().addUser(
+ new AddUserWsRequest()
+ .setLogin(login)
+ .setProjectKey(projectKey)
+ .setPermission(permission));
+ }
+
+ private static Issues.ChangelogWsResponse changelog(String issueKey, String login, String password) {
+ return newUserWsClient(orchestrator, login, password).issues().changelog(issueKey);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/authorisation/PermissionSearchTest.java b/tests/src/test/java/org/sonarqube/tests/authorisation/PermissionSearchTest.java
new file mode 100644
index 00000000000..7698aa3bb8e
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/authorisation/PermissionSearchTest.java
@@ -0,0 +1,203 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.authorisation;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category1Suite;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.WsPermissions;
+import org.sonarqube.ws.WsPermissions.Permission;
+import org.sonarqube.ws.WsPermissions.SearchTemplatesWsResponse;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.permission.AddGroupToTemplateWsRequest;
+import org.sonarqube.ws.client.permission.AddGroupWsRequest;
+import org.sonarqube.ws.client.permission.AddProjectCreatorToTemplateWsRequest;
+import org.sonarqube.ws.client.permission.AddUserToTemplateWsRequest;
+import org.sonarqube.ws.client.permission.AddUserWsRequest;
+import org.sonarqube.ws.client.permission.CreateTemplateWsRequest;
+import org.sonarqube.ws.client.permission.GroupsWsRequest;
+import org.sonarqube.ws.client.permission.PermissionsService;
+import org.sonarqube.ws.client.permission.RemoveGroupFromTemplateWsRequest;
+import org.sonarqube.ws.client.permission.RemoveProjectCreatorFromTemplateWsRequest;
+import org.sonarqube.ws.client.permission.RemoveUserFromTemplateWsRequest;
+import org.sonarqube.ws.client.permission.SearchTemplatesWsRequest;
+import org.sonarqube.ws.client.permission.UsersWsRequest;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.projectDir;
+
+public class PermissionSearchTest {
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+ private static WsClient adminWsClient;
+ private static PermissionsService permissionsWsClient;
+
+ private static final String PROJECT_KEY = "sample";
+ private static final String LOGIN = "george.orwell";
+ private static final String GROUP_NAME = "1984";
+
+ @BeforeClass
+ public static void analyzeProject() {
+ orchestrator.resetData();
+
+ ItUtils.restoreProfile(orchestrator, PermissionSearchTest.class.getResource("/authorisation/one-issue-per-line-profile.xml"));
+
+ orchestrator.getServer().provisionProject(PROJECT_KEY, "Sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");
+ SonarScanner sampleProject = SonarScanner.create(projectDir("shared/xoo-sample"));
+ orchestrator.executeBuild(sampleProject);
+
+ adminWsClient = newAdminWsClient(orchestrator);
+ permissionsWsClient = adminWsClient.permissions();
+
+ createUser(LOGIN, "George Orwell");
+ createGroup(GROUP_NAME);
+ }
+
+ @AfterClass
+ public static void delete_data() {
+ deactivateUser(LOGIN);
+ deleteGroup(GROUP_NAME);
+ }
+
+ @Test
+ public void permission_web_services() {
+ permissionsWsClient.addUser(
+ new AddUserWsRequest()
+ .setPermission("admin")
+ .setLogin(LOGIN));
+ permissionsWsClient.addGroup(
+ new AddGroupWsRequest()
+ .setPermission("admin")
+ .setGroupName(GROUP_NAME));
+
+ WsPermissions.WsSearchGlobalPermissionsResponse searchGlobalPermissionsWsResponse = permissionsWsClient.searchGlobalPermissions();
+ assertThat(searchGlobalPermissionsWsResponse.getPermissionsList().get(0).getKey()).isEqualTo("admin");
+ assertThat(searchGlobalPermissionsWsResponse.getPermissionsList().get(0).getUsersCount()).isEqualTo(1);
+ // by default, a group has the global admin permission
+ assertThat(searchGlobalPermissionsWsResponse.getPermissionsList().get(0).getGroupsCount()).isEqualTo(2);
+
+ WsPermissions.UsersWsResponse users = permissionsWsClient
+ .users(new UsersWsRequest().setPermission("admin"));
+ assertThat(users.getUsersList()).extracting("login").contains(LOGIN);
+
+ WsPermissions.WsGroupsResponse groupsResponse = permissionsWsClient
+ .groups(new GroupsWsRequest()
+ .setPermission("admin"));
+ assertThat(groupsResponse.getGroupsList()).extracting("name").contains(GROUP_NAME);
+ }
+
+ @Test
+ public void template_permission_web_services() {
+ WsPermissions.CreateTemplateWsResponse createTemplateWsResponse = permissionsWsClient.createTemplate(
+ new CreateTemplateWsRequest()
+ .setName("my-new-template")
+ .setDescription("template-used-in-tests"));
+ assertThat(createTemplateWsResponse.getPermissionTemplate().getName()).isEqualTo("my-new-template");
+
+ permissionsWsClient.addUserToTemplate(
+ new AddUserToTemplateWsRequest()
+ .setPermission("admin")
+ .setTemplateName("my-new-template")
+ .setLogin(LOGIN));
+
+ permissionsWsClient.addGroupToTemplate(
+ new AddGroupToTemplateWsRequest()
+ .setPermission("admin")
+ .setTemplateName("my-new-template")
+ .setGroupName(GROUP_NAME));
+
+ permissionsWsClient.addProjectCreatorToTemplate(
+ AddProjectCreatorToTemplateWsRequest.builder()
+ .setPermission("admin")
+ .setTemplateName("my-new-template")
+ .build());
+
+ SearchTemplatesWsResponse searchTemplatesWsResponse = permissionsWsClient.searchTemplates(
+ new SearchTemplatesWsRequest()
+ .setQuery("my-new-template"));
+ assertThat(searchTemplatesWsResponse.getPermissionTemplates(0).getName()).isEqualTo("my-new-template");
+ assertThat(searchTemplatesWsResponse.getPermissionTemplates(0).getPermissions(0).getKey()).isEqualTo("admin");
+ assertThat(searchTemplatesWsResponse.getPermissionTemplates(0).getPermissions(0).getUsersCount()).isEqualTo(1);
+ assertThat(searchTemplatesWsResponse.getPermissionTemplates(0).getPermissions(0).getGroupsCount()).isEqualTo(1);
+ assertThat(searchTemplatesWsResponse.getPermissionTemplates(0).getPermissions(0).getWithProjectCreator()).isTrue();
+
+ permissionsWsClient.removeGroupFromTemplate(
+ new RemoveGroupFromTemplateWsRequest()
+ .setPermission("admin")
+ .setTemplateName("my-new-template")
+ .setGroupName(GROUP_NAME));
+
+ permissionsWsClient.removeUserFromTemplate(
+ new RemoveUserFromTemplateWsRequest()
+ .setPermission("admin")
+ .setTemplateName("my-new-template")
+ .setLogin(LOGIN));
+
+ permissionsWsClient.removeProjectCreatorFromTemplate(
+ RemoveProjectCreatorFromTemplateWsRequest.builder()
+ .setPermission("admin")
+ .setTemplateName("my-new-template")
+ .build()
+ );
+
+ SearchTemplatesWsResponse clearedSearchTemplatesWsResponse = permissionsWsClient.searchTemplates(
+ new SearchTemplatesWsRequest()
+ .setQuery("my-new-template"));
+ assertThat(clearedSearchTemplatesWsResponse.getPermissionTemplates(0).getPermissionsList())
+ .extracting(Permission::getUsersCount, Permission::getGroupsCount, Permission::getWithProjectCreator)
+ .hasSize(5)
+ .containsOnly(tuple(0, 0, false));
+ }
+
+ private static void createUser(String login, String name) {
+ adminWsClient.wsConnector().call(
+ new PostRequest("api/users/create")
+ .setParam("login", login)
+ .setParam("name", name)
+ .setParam("password", "123456"));
+ }
+
+ private static void deactivateUser(String login) {
+ adminWsClient.wsConnector().call(
+ new PostRequest("api/users/deactivate")
+ .setParam("login", login));
+ }
+
+ private static void createGroup(String groupName) {
+ adminWsClient.wsConnector().call(
+ new PostRequest("api/user_groups/create")
+ .setParam("name", groupName));
+ }
+
+ private static void deleteGroup(String groupName) {
+ adminWsClient.wsConnector().call(
+ new PostRequest("api/user_groups/delete")
+ .setParam("name", groupName));
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/authorisation/PermissionTemplatesPageTest.java b/tests/src/test/java/org/sonarqube/tests/authorisation/PermissionTemplatesPageTest.java
new file mode 100644
index 00000000000..f94a50bb0b3
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/authorisation/PermissionTemplatesPageTest.java
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.authorisation;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category1Suite;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import static util.selenium.Selenese.runSelenese;
+
+public class PermissionTemplatesPageTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ @Test
+ public void should_display_page() throws Exception {
+ runSelenese(orchestrator,
+ "/authorisation/PermissionTemplatesPageTest/should_display_page.html",
+ "/authorisation/PermissionTemplatesPageTest/should_create.html");
+ }
+
+ @Test
+ public void should_manage_project_creators() throws Exception {
+ runSelenese(orchestrator, "/authorisation/PermissionTemplatesPageTest/should_manage_project_creators.html");
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/authorisation/ProvisioningPermissionTest.java b/tests/src/test/java/org/sonarqube/tests/authorisation/ProvisioningPermissionTest.java
new file mode 100644
index 00000000000..8d3b7ac007a
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/authorisation/ProvisioningPermissionTest.java
@@ -0,0 +1,150 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.authorisation;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category1Suite;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonarqube.ws.WsProjects.CreateWsResponse.Project;
+import org.sonarqube.ws.client.HttpException;
+import org.sonarqube.ws.client.permission.AddGroupWsRequest;
+import org.sonarqube.ws.client.permission.AddUserWsRequest;
+import org.sonarqube.ws.client.permission.PermissionsService;
+import org.sonarqube.ws.client.permission.RemoveGroupWsRequest;
+import org.sonarqube.ws.client.permission.RemoveUserWsRequest;
+import org.sonarqube.ws.client.project.CreateRequest;
+import util.user.UserRule;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.newUserWsClient;
+import static util.selenium.Selenese.runSelenese;
+
+public class ProvisioningPermissionTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ @ClassRule
+ public static UserRule userRule = UserRule.from(orchestrator);
+
+ private static final String PASSWORD = "password";
+
+ private static final String ADMIN_WITH_PROVISIONING = "admin-with-provisioning";
+ private static final String ADMIN_WITHOUT_PROVISIONING = "admin-without-provisioning";
+ private static final String USER_WITH_PROVISIONING = "user-with-provisioning";
+ private static final String USER_WITHOUT_PROVISIONING = "user-without-provisioning";
+
+ private static PermissionsService permissionsWsClient;
+
+ @BeforeClass
+ public static void init() {
+ permissionsWsClient = newAdminWsClient(orchestrator).permissions();
+
+ // remove default permission "provisioning" from anyone();
+ permissionsWsClient.removeGroup(new RemoveGroupWsRequest().setGroupName("anyone").setPermission("provisioning"));
+
+ userRule.createUser(ADMIN_WITH_PROVISIONING, PASSWORD);
+ addUserPermission(ADMIN_WITH_PROVISIONING, "admin");
+ addUserPermission(ADMIN_WITH_PROVISIONING, "provisioning");
+
+ userRule.createUser(ADMIN_WITHOUT_PROVISIONING, PASSWORD);
+ addUserPermission(ADMIN_WITHOUT_PROVISIONING, "admin");
+ removeUserPermission(ADMIN_WITHOUT_PROVISIONING, "provisioning");
+
+ userRule.createUser(USER_WITH_PROVISIONING, PASSWORD);
+ addUserPermission(USER_WITH_PROVISIONING, "provisioning");
+
+ userRule.createUser(USER_WITHOUT_PROVISIONING, PASSWORD);
+ removeUserPermission(USER_WITHOUT_PROVISIONING, "provisioning");
+ }
+
+ @AfterClass
+ public static void restoreData() throws Exception {
+ userRule.resetUsers();
+ permissionsWsClient.addGroup(new AddGroupWsRequest().setGroupName("anyone").setPermission("provisioning"));
+ }
+
+ /**
+ * SONAR-3871
+ * SONAR-4709
+ */
+ @Test
+ public void organization_administrator_cannot_provision_project_if_he_doesnt_have_provisioning_permission() {
+ runSelenese(orchestrator, "/authorisation/ProvisioningPermissionTest/should-not-be-able-to-provision-project.html");
+ }
+
+ /**
+ * SONAR-3871
+ * SONAR-4709
+ */
+ @Test
+ public void organization_administrator_can_provision_project_if_he_has_provisioning_permission() {
+ runSelenese(orchestrator, "/authorisation/ProvisioningPermissionTest/should-be-able-to-provision-project.html");
+ }
+
+ /**
+ * SONAR-3871
+ * SONAR-4709
+ */
+ @Test
+ public void user_can_provision_project_through_ws_if_he_has_provisioning_permission() {
+ final String newKey = "new-project";
+ final String newName = "New Project";
+
+ Project created = newUserWsClient(orchestrator, USER_WITH_PROVISIONING, PASSWORD).projects()
+ .create(CreateRequest.builder().setKey(newKey).setName(newName).build())
+ .getProject();
+
+ assertThat(created).isNotNull();
+ assertThat(created.getKey()).isEqualTo(newKey);
+ assertThat(created.getName()).isEqualTo(newName);
+ }
+
+ /**
+ * SONAR-3871
+ * SONAR-4709
+ */
+ @Test
+ public void user_cannot_provision_project_through_ws_if_he_doesnt_have_provisioning_permission() {
+ thrown.expect(HttpException.class);
+ thrown.expectMessage("403");
+
+ newUserWsClient(orchestrator, USER_WITHOUT_PROVISIONING, PASSWORD).projects()
+ .create(CreateRequest.builder().setKey("new-project").setName("New Project").build())
+ .getProject();
+ }
+
+ private static void addUserPermission(String login, String permission) {
+ permissionsWsClient.addUser(new AddUserWsRequest().setLogin(login).setPermission(permission));
+ }
+
+ private static void removeUserPermission(String login, String permission) {
+ permissionsWsClient.removeUser(new RemoveUserWsRequest().setLogin(login).setPermission(permission));
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/authorisation/QualityProfileAdminPermissionTest.java b/tests/src/test/java/org/sonarqube/tests/authorisation/QualityProfileAdminPermissionTest.java
new file mode 100644
index 00000000000..65193eb1bee
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/authorisation/QualityProfileAdminPermissionTest.java
@@ -0,0 +1,83 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.authorisation;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category1Suite;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.permission.AddUserWsRequest;
+import util.user.UserRule;
+
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.runProjectAnalysis;
+import static util.selenium.Selenese.runSelenese;
+
+/**
+ * SONAR-4210
+ */
+public class QualityProfileAdminPermissionTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ @ClassRule
+ public static UserRule userRule = UserRule.from(orchestrator);
+
+ private static WsClient adminWsClient;
+
+ @BeforeClass
+ public static void init() {
+ orchestrator.resetData();
+ adminWsClient = newAdminWsClient(orchestrator);
+ runProjectAnalysis(orchestrator, "shared/xoo-sample");
+ }
+
+ @AfterClass
+ public static void clearUsers() throws Exception {
+ userRule.resetUsers();
+ }
+
+ @Test
+ public void permission_should_grant_access_to_profile() {
+ userRule.createUser("not_profileadm", "userpwd");
+ userRule.createUser("profileadm", "papwd");
+ adminWsClient.permissions().addUser(new AddUserWsRequest().setLogin("profileadm").setPermission("profileadmin"));
+ createProfile("xoo", "foo");
+
+ runSelenese(orchestrator,
+ // Verify normal user is not allowed to do any modification
+ "/authorisation/QualityProfileAdminPermissionTest/normal-user.html",
+ // Verify profile admin is allowed to do modifications
+ "/authorisation/QualityProfileAdminPermissionTest/profile-admin.html");
+ }
+
+ private static void createProfile(String language, String name) {
+ adminWsClient.wsConnector().call(
+ new PostRequest("api/qualityprofiles/create")
+ .setParam("language", language)
+ .setParam("name", name));
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/ce/CeWsTest.java b/tests/src/test/java/org/sonarqube/tests/ce/CeWsTest.java
new file mode 100644
index 00000000000..e6faf6ec58c
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/ce/CeWsTest.java
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.ce;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.BuildResult;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category4Suite;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.WsCe;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.ce.ActivityWsRequest;
+import util.ItUtils;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.projectDir;
+
+public class CeWsTest {
+ @ClassRule
+ public static final Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+
+ private WsClient wsClient;
+ private String taskUuid;
+
+ @Before
+ public void inspectProject() {
+ orchestrator.resetData();
+ BuildResult buildResult = orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")));
+ this.taskUuid = ItUtils.extractCeTaskId(buildResult);
+ this.wsClient = ItUtils.newAdminWsClient(orchestrator);
+ }
+
+ @Test
+ public void activity() {
+ WsCe.ActivityResponse response = wsClient.ce().activity(new ActivityWsRequest()
+ .setStatus(newArrayList("SUCCESS"))
+ .setType("REPORT")
+ .setOnlyCurrents(true)
+ .setPage(1)
+ .setPageSize(100));
+
+ assertThat(response).isNotNull();
+ assertThat(response.getTasksCount()).isGreaterThan(0);
+ WsCe.Task firstTask = response.getTasks(0);
+ assertThat(firstTask.getId()).isNotEmpty();
+ }
+
+ @Test
+ public void task() {
+ WsCe.TaskResponse taskResponse = wsClient.ce().task(taskUuid);
+
+ assertThat(taskResponse.hasTask()).isTrue();
+ WsCe.Task task = taskResponse.getTask();
+ assertThat(task.getId()).isEqualTo(taskUuid);
+ assertThat(task.hasErrorMessage()).isFalse();
+ assertThat(task.hasHasScannerContext()).isTrue();
+ assertThat(task.getScannerContext()).isNotNull();
+ }
+
+ @Test
+ public void task_types() {
+ WsCe.TaskTypesWsResponse response = wsClient.ce().taskTypes();
+
+ assertThat(response).isNotNull();
+ assertThat(response.getTaskTypesCount()).isGreaterThan(0);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/complexity/ComplexityMeasuresTest.java b/tests/src/test/java/org/sonarqube/tests/complexity/ComplexityMeasuresTest.java
new file mode 100644
index 00000000000..3eb28da0ccc
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/complexity/ComplexityMeasuresTest.java
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.complexity;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category1Suite;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.entry;
+import static util.ItUtils.getMeasuresAsDoubleByMetricKey;
+import static util.ItUtils.projectDir;
+
+// TODO complete the test with other complexity metrics
+public class ComplexityMeasuresTest {
+
+ private static final String PROJECT = "com.sonarsource.it.samples:multi-modules-sample";
+ private static final String MODULE = "com.sonarsource.it.samples:multi-modules-sample:module_a";
+ private static final String SUB_MODULE = "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1";
+ private static final String DIRECTORY = "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1";
+ private static final String FILE = "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo";
+
+ private static final String COMPLEXITY_METRIC = "complexity";
+ private static final String COGNITIVE_COMPLEXITY_METRIC = "cognitive_complexity";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ @BeforeClass
+ public static void inspectProject() {
+ orchestrator.resetData();
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-multi-modules-sample")));
+ }
+
+ @Test
+ public void compute_complexity_metrics_on_file() {
+ assertThat(getMeasuresAsDoubleByMetricKey(orchestrator, FILE, COMPLEXITY_METRIC, COGNITIVE_COMPLEXITY_METRIC)).containsOnly(
+ entry(COMPLEXITY_METRIC, 3d),
+ entry(COGNITIVE_COMPLEXITY_METRIC, 4d));
+ }
+
+ @Test
+ public void compute_complexity_metrics_on_directory() {
+ assertThat(getMeasuresAsDoubleByMetricKey(orchestrator, DIRECTORY, COMPLEXITY_METRIC, COGNITIVE_COMPLEXITY_METRIC)).containsOnly(
+ entry(COMPLEXITY_METRIC, 3d),
+ entry(COGNITIVE_COMPLEXITY_METRIC, 4d));
+ }
+
+ @Test
+ public void compute_complexity_metrics_on_sub_module() {
+ assertThat(getMeasuresAsDoubleByMetricKey(orchestrator, SUB_MODULE, COMPLEXITY_METRIC, COGNITIVE_COMPLEXITY_METRIC)).containsOnly(
+ entry(COMPLEXITY_METRIC, 3d),
+ entry(COGNITIVE_COMPLEXITY_METRIC, 4d));
+ }
+
+ @Test
+ public void compute_complexity_metrics_on_module() {
+ assertThat(getMeasuresAsDoubleByMetricKey(orchestrator, MODULE, COMPLEXITY_METRIC, COGNITIVE_COMPLEXITY_METRIC)).containsOnly(
+ entry(COMPLEXITY_METRIC, 7d),
+ entry(COGNITIVE_COMPLEXITY_METRIC, 9d));
+ }
+
+ @Test
+ public void compute_complexity_metrics_on_project() {
+ assertThat(getMeasuresAsDoubleByMetricKey(orchestrator, PROJECT, COMPLEXITY_METRIC, COGNITIVE_COMPLEXITY_METRIC)).containsOnly(
+ entry(COMPLEXITY_METRIC, 13d),
+ entry(COGNITIVE_COMPLEXITY_METRIC, 17d));
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/component/ComponentsWsTest.java b/tests/src/test/java/org/sonarqube/tests/component/ComponentsWsTest.java
new file mode 100644
index 00000000000..69cae2a8d34
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/component/ComponentsWsTest.java
@@ -0,0 +1,75 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.component;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category4Suite;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonarqube.ws.WsComponents;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.component.SearchWsRequest;
+import org.sonarqube.ws.client.component.ShowWsRequest;
+import util.ItUtils;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.projectDir;
+
+public class ComponentsWsTest {
+ @ClassRule
+ public static final Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+ private static final String FILE_KEY = "sample:src/main/xoo/sample/Sample.xoo";
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ WsClient wsClient;
+
+ @Before
+ public void inspectProject() {
+ orchestrator.resetData();
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")));
+
+ wsClient = ItUtils.newAdminWsClient(orchestrator);
+ }
+
+ @Test
+ public void show() {
+ WsComponents.ShowWsResponse response = wsClient.components().show(new ShowWsRequest().setKey(FILE_KEY));
+
+ assertThat(response).isNotNull();
+ assertThat(response.getComponent().getKey()).isEqualTo(FILE_KEY);
+ assertThat(response.getAncestorsList()).isNotEmpty();
+ }
+
+ @Test
+ public void search() {
+ WsComponents.SearchWsResponse response = wsClient.components().search(new SearchWsRequest()
+ .setQualifiers(singletonList("FIL")));
+
+ assertThat(response).isNotNull();
+ assertThat(response.getComponents(0).getKey()).isEqualTo(FILE_KEY);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/component/ProjectsWsTest.java b/tests/src/test/java/org/sonarqube/tests/component/ProjectsWsTest.java
new file mode 100644
index 00000000000..75d5f5bae16
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/component/ProjectsWsTest.java
@@ -0,0 +1,125 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.component;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category4Suite;
+import java.io.IOException;
+import org.apache.commons.io.IOUtils;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.util.EntityUtils;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonarqube.ws.WsComponents;
+import org.sonarqube.ws.WsProjects.BulkUpdateKeyWsResponse;
+import org.sonarqube.ws.WsProjects.BulkUpdateKeyWsResponse.Key;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.component.ShowWsRequest;
+import org.sonarqube.ws.client.project.BulkUpdateKeyWsRequest;
+import org.sonarqube.ws.client.project.UpdateKeyWsRequest;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.projectDir;
+
+public class ProjectsWsTest {
+
+ @ClassRule
+ public static final Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+ private static final String PROJECT_KEY = "sample";
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private WsClient wsClient;
+
+ @Before
+ public void inspectProject() {
+ orchestrator.resetData();
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")));
+
+ wsClient = ItUtils.newAdminWsClient(orchestrator);
+ }
+
+ /**
+ * SONAR-3105
+ */
+ @Test
+ public void projects_web_service() throws IOException {
+ SonarScanner build = SonarScanner.create(projectDir("shared/xoo-sample"));
+ orchestrator.executeBuild(build);
+
+ String url = orchestrator.getServer().getUrl() + "/api/projects/index?key=sample&versions=true";
+ HttpClient httpclient = new DefaultHttpClient();
+ try {
+ HttpGet get = new HttpGet(url);
+ HttpResponse response = httpclient.execute(get);
+
+ assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200);
+ String content = IOUtils.toString(response.getEntity().getContent());
+ assertThat(content).doesNotContain("error");
+ assertThat(content).contains("sample");
+ EntityUtils.consume(response.getEntity());
+
+ } finally {
+ httpclient.getConnectionManager().shutdown();
+ }
+ }
+
+ @Test
+ public void update_key() {
+ String newProjectKey = "another_project_key";
+ WsComponents.Component project = wsClient.components().show(new ShowWsRequest().setKey(PROJECT_KEY)).getComponent();
+ assertThat(project.getKey()).isEqualTo(PROJECT_KEY);
+
+ wsClient.projects().updateKey(UpdateKeyWsRequest.builder()
+ .setKey(PROJECT_KEY)
+ .setNewKey(newProjectKey)
+ .build());
+
+ assertThat(wsClient.components().show(new ShowWsRequest().setId(project.getId())).getComponent().getKey()).isEqualTo(newProjectKey);
+ }
+
+ @Test
+ public void bulk_update_key() {
+ String newProjectKey = "another_project_key";
+ WsComponents.Component project = wsClient.components().show(new ShowWsRequest().setKey(PROJECT_KEY)).getComponent();
+ assertThat(project.getKey()).isEqualTo(PROJECT_KEY);
+
+ BulkUpdateKeyWsResponse result = wsClient.projects().bulkUpdateKey(BulkUpdateKeyWsRequest.builder()
+ .setKey(PROJECT_KEY)
+ .setFrom(PROJECT_KEY)
+ .setTo(newProjectKey)
+ .build());
+
+ assertThat(wsClient.components().show(new ShowWsRequest().setId(project.getId())).getComponent().getKey()).isEqualTo(newProjectKey);
+ assertThat(result.getKeysCount()).isEqualTo(1);
+ assertThat(result.getKeys(0))
+ .extracting(Key::getKey, Key::getNewKey, Key::getDuplicate)
+ .containsOnlyOnce(PROJECT_KEY, newProjectKey, false);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/customMeasure/CustomMeasuresTest.java b/tests/src/test/java/org/sonarqube/tests/customMeasure/CustomMeasuresTest.java
new file mode 100644
index 00000000000..206f3eb35dd
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/customMeasure/CustomMeasuresTest.java
@@ -0,0 +1,117 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.customMeasure;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category1Suite;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.projectDir;
+
+public class CustomMeasuresTest {
+
+ private static final String PROJECT_KEY = "sample";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ @Before
+ public void deleteProjects() {
+ orchestrator.resetData();
+ }
+
+ @Test
+ public void custom_measures_should_be_integrated_during_project_analysis() {
+ analyzeProject();
+ setBurnedBudget(1200.3);
+ setTeamSize(4);
+
+ assertThat(getMeasureAsDouble("team_size")).isNull();
+ assertThat(getMeasureAsDouble("burned_budget")).isNull();
+
+ analyzeProject();
+
+ assertThat(getMeasureAsDouble("burned_budget")).isEqualTo(1200.3);
+ assertThat(getMeasureAsDouble("team_size")).isEqualTo(4d);
+ }
+
+ @Test
+ public void should_update_value() {
+ analyzeProject();
+ setTeamSize(4);
+ analyzeProject();
+ updateTeamSize(15);
+ assertThat(getMeasureAsDouble("team_size")).isEqualTo(4d);
+ analyzeProject();// the value is available when the project is analyzed again
+ assertThat(getMeasureAsDouble("team_size")).isEqualTo(15d);
+ }
+
+ @Test
+ public void should_delete_custom_measure() {
+ analyzeProject();
+ setTeamSize(4);
+ analyzeProject();
+ deleteCustomMeasure("team_size");
+ assertThat(getMeasureAsDouble("team_size")).isEqualTo(4d);// the value is still available. It will be removed during next
+ // analyzed
+
+ analyzeProject();
+ assertThat(getMeasureAsDouble("team_size")).isNull();
+ }
+
+ private void analyzeProject() {
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")));
+ }
+
+ private void setTeamSize(int i) {
+ orchestrator.getServer().adminWsClient().post("api/custom_measures/create", "projectKey", PROJECT_KEY, "metricKey", "team_size", "value", String.valueOf(i));
+ }
+
+ private void updateTeamSize(int i) {
+ String response = orchestrator.getServer().adminWsClient().get("api/custom_measures/search", "projectKey", PROJECT_KEY, "metricKey", "team_size");
+ Matcher jsonObjectMatcher = Pattern.compile(".*?\"id\"\\s*:\\s*\"(.*?)\".*", Pattern.MULTILINE).matcher(response);
+ jsonObjectMatcher.find();
+ String customMeasureId = jsonObjectMatcher.group(1);
+ orchestrator.getServer().adminWsClient().post("api/custom_measures/update", "id", customMeasureId, "value", String.valueOf(i));
+ }
+
+ private void setBurnedBudget(double d) {
+ orchestrator.getServer().adminWsClient().post("api/custom_measures/create", "projectKey", PROJECT_KEY, "metricKey", "burned_budget", "value", String.valueOf(d));
+ }
+
+ private void deleteCustomMeasure(String metricKey) {
+ String response = orchestrator.getServer().adminWsClient().get("api/custom_measures/search", "projectKey", PROJECT_KEY, "metricKey", metricKey);
+ Matcher jsonObjectMatcher = Pattern.compile(".*?\"id\"\\s*:\\s*\"(.*?)\".*", Pattern.MULTILINE).matcher(response);
+ jsonObjectMatcher.find();
+ String customMeasureId = jsonObjectMatcher.group(1);
+ orchestrator.getServer().adminWsClient().post("api/custom_measures/delete", "id", customMeasureId);
+ }
+
+ private Double getMeasureAsDouble(String metricKey) {
+ return ItUtils.getMeasureAsDouble(orchestrator, PROJECT_KEY, metricKey);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/dbCleaner/PurgeTest.java b/tests/src/test/java/org/sonarqube/tests/dbCleaner/PurgeTest.java
new file mode 100644
index 00000000000..dc4bb0fcc78
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/dbCleaner/PurgeTest.java
@@ -0,0 +1,377 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.dbCleaner;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.BuildResult;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category4Suite;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.lang.time.DateFormatUtils;
+import org.apache.commons.lang.time.DateUtils;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ErrorCollector;
+import org.sonarqube.ws.WsMeasures;
+import org.sonarqube.ws.WsMeasures.SearchHistoryResponse.HistoryValue;
+import org.sonarqube.ws.client.measure.SearchHistoryRequest;
+import util.ItUtils;
+
+import static java.util.Collections.singletonList;
+import static org.apache.commons.lang.time.DateUtils.addDays;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static util.ItUtils.formatDate;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.runProjectAnalysis;
+import static util.ItUtils.setServerProperty;
+
+@Ignore
+public class PurgeTest {
+
+ private static final String COUNT_FILE_MEASURES = "project_measures pm, projects p where p.uuid = pm.component_uuid and p.scope='FIL'";
+ private static final String COUNT_DIR_MEASURES = "project_measures pm, projects p where p.uuid = pm.component_uuid and p.scope='DIR'";
+ private static final String PROJECT_KEY = "com.sonarsource.it.samples:multi-modules-sample";
+ private static final String PROJECT_SAMPLE_PATH = "dbCleaner/xoo-multi-modules-sample";
+
+ private static final String ONE_DAY_AGO = DateFormatUtils.ISO_DATE_FORMAT.format(addDays(new Date(), -1));
+ private static final String TWO_DAYS_AGO = DateFormatUtils.ISO_DATE_FORMAT.format(addDays(new Date(), -2));
+
+ @ClassRule
+ public static final Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+
+ @Rule
+ public ErrorCollector collector = new ErrorCollector();
+
+ @Before
+ public void deleteProjectData() {
+ orchestrator.resetData();
+
+ orchestrator.getServer().provisionProject(PROJECT_KEY, PROJECT_KEY);
+
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/dbCleaner/one-issue-per-line-profile.xml"));
+
+ setServerProperty(orchestrator, "sonar.dbcleaner.cleanDirectory", null);
+ setServerProperty(orchestrator, "sonar.dbcleaner.hoursBeforeKeepingOnlyOneSnapshotByDay", null);
+ setServerProperty(orchestrator, "sonar.dbcleaner.weeksBeforeKeepingOnlyOneSnapshotByWeek", null);
+ }
+
+ @Test
+ public void test_evolution_of_number_of_rows_when_scanning_two_times_the_same_project() {
+ Date today = new Date();
+ Date yesterday = DateUtils.addDays(today, -1);
+
+ scan(PROJECT_SAMPLE_PATH, DateFormatUtils.ISO_DATE_FORMAT.format(yesterday));
+
+ // count components
+ collector.checkThat("Wrong number of projects", count("projects where qualifier in ('TRK','BRC')"), equalTo(7));
+ collector.checkThat("Wrong number of directories", count("projects where qualifier in ('DIR')"), equalTo(4));
+ collector.checkThat("Wrong number of files", count("projects where qualifier in ('FIL')"), equalTo(4));
+ collector.checkThat("Wrong number of unit test files", count("projects where qualifier in ('UTS')"), equalTo(0));
+
+ int measuresOnTrk = 45;
+ int measuresOnBrc = 222;
+ int measuresOnDir = 141;
+ int measuresOnFil = 69;
+
+ // count measures 
+ assertMeasuresCountForQualifier("TRK", measuresOnTrk);
+ assertMeasuresCountForQualifier("BRC", measuresOnBrc);
+ assertMeasuresCountForQualifier("DIR", measuresOnDir);
+ assertMeasuresCountForQualifier("FIL", measuresOnFil);
+
+ // No new_* metrics measure should be recorded the first time
+ collector.checkThat(
+ "Wrong number of measure of new_ metrics",
+ count("project_measures, metrics where metrics.id = project_measures.metric_id and metrics.name like 'new_%'"),
+ equalTo(0));
+
+ int expectedMeasures = measuresOnTrk + measuresOnBrc + measuresOnDir + measuresOnFil;
+ collector.checkThat("Wrong number of measures", count("project_measures"), equalTo(expectedMeasures));
+ collector.checkThat("Wrong number of measure data", count("project_measures where measure_data is not null"), equalTo(0));
+
+ // count other tables that are constant between 2 scans
+ int expectedIssues = 52;
+
+ collector.checkThat("Wrong number of issues", count("issues"), equalTo(expectedIssues));
+
+ // must be a different date, else a single snapshot is kept per day
+ scan(PROJECT_SAMPLE_PATH, DateFormatUtils.ISO_DATE_FORMAT.format(today));
+
+ int newMeasuresOnTrk = 58;
+ int newMeasuresOnBrc = 304;
+ int newMeasuresOnDir = 56;
+ int newMeasuresOnFil = 0;
+
+ assertMeasuresCountForQualifier("TRK", measuresOnTrk + newMeasuresOnTrk);
+ assertMeasuresCountForQualifier("BRC", measuresOnBrc + newMeasuresOnBrc);
+ assertMeasuresCountForQualifier("DIR", measuresOnDir + newMeasuresOnDir);
+ assertMeasuresCountForQualifier("FIL", measuresOnFil + newMeasuresOnFil);
+
+ // Measures on new_* metrics should be recorded
+ collector.checkThat(
+ "Wrong number of measure of new_ metrics",
+ count("project_measures, metrics where metrics.id = project_measures.metric_id and metrics.name like 'new_%'"),
+ equalTo(154));
+
+ // added measures relate to project and new_* metrics
+ expectedMeasures += newMeasuresOnTrk + newMeasuresOnBrc + newMeasuresOnDir + newMeasuresOnFil;
+ collector.checkThat("Wrong number of measures after second analysis", count("project_measures"), equalTo(expectedMeasures));
+ collector.checkThat("Wrong number of measure data", count("project_measures where measure_data is not null"), equalTo(0));
+ collector.checkThat("Wrong number of issues", count("issues"), equalTo(expectedIssues));
+ }
+
+ /**
+ * SONAR-3378
+ */
+ @Test
+ public void should_keep_all_snapshots_the_first_day() {
+ // analyse once
+ scan(PROJECT_SAMPLE_PATH);
+ // analyse twice
+ scan(PROJECT_SAMPLE_PATH);
+ // and check we have 2 snapshots
+ assertThat(count("snapshots s where s.component_uuid=(select p.uuid from projects p where p.kee='com.sonarsource.it.samples:multi-modules-sample')")).isEqualTo(2);
+ }
+
+ /**
+ * SONAR-2807 & SONAR-3378 & SONAR-4710
+ */
+ @Test
+ public void should_keep_only_one_snapshot_per_day() {
+ scan(PROJECT_SAMPLE_PATH);
+
+ int snapshotsCount = count("snapshots");
+ int measuresCount = count("project_measures");
+ // Using the "sonar.dbcleaner.hoursBeforeKeepingOnlyOneSnapshotByDay" property set to '0' is the way
+ // to keep only 1 snapshot per day
+ setServerProperty(orchestrator, "sonar.dbcleaner.hoursBeforeKeepingOnlyOneSnapshotByDay", "0");
+ scan(PROJECT_SAMPLE_PATH);
+ assertThat(count("snapshots")).as("Different number of snapshots").isEqualTo(snapshotsCount);
+
+ int measureOnNewMetrics = count("project_measures, metrics where metrics.id = project_measures.metric_id and metrics.name like 'new_%'");
+ // Number of measures should be the same as previous, with the measures on new metrics
+ assertThat(count("project_measures")).as("Different number of measures").isEqualTo(measuresCount + measureOnNewMetrics);
+ }
+
+ /**
+ * SONAR-7175
+ */
+ @Test
+ public void keep_latest_snapshot() {
+ // Keep all snapshots from last 4 weeks
+ setServerProperty(orchestrator, "sonar.dbcleaner.weeksBeforeKeepingOnlyOneSnapshotByWeek", "4");
+
+ Date oneWeekAgo = addDays(new Date(), -7);
+
+ // Execute an analysis wednesday last week
+ Calendar lastWednesday = Calendar.getInstance();
+ lastWednesday.setTime(oneWeekAgo);
+ lastWednesday.set(Calendar.DAY_OF_WEEK, Calendar.WEDNESDAY);
+ String lastWednesdayFormatted = formatDate(lastWednesday.getTime());
+ runProjectAnalysis(orchestrator, PROJECT_SAMPLE_PATH, "sonar.projectDate", lastWednesdayFormatted);
+
+ // Execute an analysis thursday last week
+ Calendar lastThursday = Calendar.getInstance();
+ lastThursday.setTime(oneWeekAgo);
+ lastThursday.set(Calendar.DAY_OF_WEEK, Calendar.THURSDAY);
+ String lastThursdayFormatted = formatDate(lastThursday.getTime());
+ runProjectAnalysis(orchestrator, PROJECT_SAMPLE_PATH, "sonar.projectDate", lastThursdayFormatted);
+
+ // Now only keep 1 snapshot per week
+ setServerProperty(orchestrator, "sonar.dbcleaner.weeksBeforeKeepingOnlyOneSnapshotByWeek", "0");
+
+ // Execute an analysis today to execute the purge of previous weeks snapshots
+ runProjectAnalysis(orchestrator, PROJECT_SAMPLE_PATH);
+
+ // Check that only analysis from last thursday is kept (as it's the last one from previous week)
+ WsMeasures.SearchHistoryResponse response = newAdminWsClient(orchestrator).measures().searchHistory(SearchHistoryRequest.builder()
+ .setComponent(PROJECT_KEY)
+ .setMetrics(singletonList("ncloc"))
+ .build());
+ assertThat(response.getMeasuresCount()).isEqualTo(1);
+ assertThat(response.getMeasuresList().get(0).getHistoryList()).extracting(HistoryValue::getDate).doesNotContain(lastWednesdayFormatted, lastThursdayFormatted);
+ }
+
+ /**
+ * SONAR-3120
+ */
+ @Test
+ public void should_delete_removed_modules() {
+ scan("dbCleaner/modules/before");
+ assertExists("com.sonarsource.it.samples:multi-modules-sample:module_b");
+ assertExists("com.sonarsource.it.samples:multi-modules-sample:module_b:module_b1");
+
+ // we want the previous snapshot to be purged
+ setServerProperty(orchestrator, "sonar.dbcleaner.hoursBeforeKeepingOnlyOneSnapshotByDay", "0");
+
+ scan("dbCleaner/modules/after");
+ assertDisabled("com.sonarsource.it.samples:multi-modules-sample:module_b");
+ assertDisabled("com.sonarsource.it.samples:multi-modules-sample:module_b:module_b1");
+ assertExists("com.sonarsource.it.samples:multi-modules-sample:module_c:module_c1");
+ }
+
+ /**
+ * SONAR-3120
+ */
+ @Test
+ public void should_delete_removed_files() {
+ String fileKey = "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo";
+ scan("dbCleaner/files/before");
+ assertExists(fileKey);
+
+ scan("dbCleaner/files/after");
+ assertDisabled(fileKey);
+ assertExists("com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/NewHelloA1.xoo");
+ }
+
+ /**
+ * SONAR-2754
+ */
+ @Test
+ public void should_delete_historical_data_of_directories_by_default() {
+ scan(PROJECT_SAMPLE_PATH, TWO_DAYS_AGO);
+
+ int fileMeasures = count(COUNT_FILE_MEASURES);
+ int dirMeasures = count(COUNT_DIR_MEASURES);
+
+ scan(PROJECT_SAMPLE_PATH, ONE_DAY_AGO);
+
+ // second analysis with new_* metrics
+ assertThat(count(COUNT_FILE_MEASURES)).isLessThan(2 * fileMeasures);
+ assertThat(count(COUNT_DIR_MEASURES)).isLessThan(2 * dirMeasures);
+ }
+
+ /**
+ * SONAR-2754
+ */
+ @Test
+ public void should_not_delete_historical_data_of_directories() {
+ scan(PROJECT_SAMPLE_PATH, TWO_DAYS_AGO);
+
+ int fileMeasures = count(COUNT_FILE_MEASURES);
+ int dirMeasures = count(COUNT_DIR_MEASURES);
+
+ setServerProperty(orchestrator, "sonar.dbcleaner.cleanDirectory", "false");
+
+ scan(PROJECT_SAMPLE_PATH, ONE_DAY_AGO);
+
+ // second analysis as NEW_* metrics
+ assertThat(count(COUNT_FILE_MEASURES)).isLessThan(2 * fileMeasures);
+ assertThat(count(COUNT_DIR_MEASURES)).isGreaterThan(2 * dirMeasures);
+ }
+
+ /**
+ * SONAR-2061
+ */
+ @Test
+ public void should_delete_historical_data_of_flagged_metrics() {
+ scan(PROJECT_SAMPLE_PATH, TWO_DAYS_AGO);
+
+ // historical data of complexity_in_classes is supposed to be deleted (see CoreMetrics)
+ String selectNcloc = "project_measures where metric_id in (select id from metrics where name='ncloc')";
+ String selectComplexityInClasses = "project_measures where metric_id in (select id from metrics where name='complexity_in_classes')";
+ int nclocCount = count(selectNcloc);
+ int complexitInClassesCount = count(selectComplexityInClasses);
+
+ scan(PROJECT_SAMPLE_PATH, ONE_DAY_AGO);
+ assertThat(count(selectNcloc)).isGreaterThan(nclocCount);
+ assertThat(count(selectComplexityInClasses)).isEqualTo(complexitInClassesCount);
+ }
+
+ private void assertDisabled(String key) {
+ assertThat(enabledStatusOfComponent(key)).isFalse();
+ }
+
+ private void assertExists(String key) {
+ assertThat(enabledStatusOfComponent(key)).isTrue();
+ }
+
+ private Boolean enabledStatusOfComponent(String key) {
+ return orchestrator.getDatabase().executeSql("select enabled from projects p where p.kee='" + key + "'")
+ .stream()
+ .findFirst()
+ .map(PurgeTest::toBoolean)
+ .orElse(null);
+ }
+
+ private static Boolean toBoolean(Map<String, String> s) {
+ String value = s.get("ENABLED");
+ if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("t") || value.equals("1")) {
+ return true;
+ }
+ if (value.equalsIgnoreCase("false") || value.equalsIgnoreCase("f") || value.equals("0")) {
+ return false;
+ }
+ throw new IllegalArgumentException("Unsupported value can not be converted to boolean " + value);
+ }
+
+ private BuildResult scan(String path, String date) {
+ return scan(path, "sonar.projectDate", date);
+ }
+
+ private BuildResult scan(String path, String... extraProperties) {
+ SonarScanner runner = configureRunner(path, extraProperties);
+ return orchestrator.executeBuild(runner);
+ }
+
+ private SonarScanner configureRunner(String projectPath, String... props) {
+ orchestrator.getServer().associateProjectToQualityProfile(PROJECT_KEY, "xoo", "one-issue-per-line-profile");
+ return SonarScanner.create(ItUtils.projectDir(projectPath)).setProperties(props);
+ }
+
+ private int count(String condition) {
+ return orchestrator.getDatabase().countSql("select count(1) from " + condition);
+ }
+
+ private void assertMeasuresCountForQualifier(String qualifier, int count) {
+ int result = countMeasures(qualifier);
+ if (result != count) {
+ logMeasures("GOT", qualifier);
+ }
+ collector.checkThat("Wrong number of measures for qualifier " + qualifier, result, equalTo(count));
+ }
+
+ private int countMeasures(String qualifier) {
+ String sql = "SELECT count(1) FROM project_measures pm, projects p, metrics m where p.uuid=pm.component_uuid and pm.metric_id=m.id and p.qualifier='" + qualifier + "'";
+ return orchestrator.getDatabase().countSql(sql);
+ }
+
+ private void logMeasures(String title, String qualifier) {
+ String sql = "SELECT m.name as metricName, pm.value as value, pm.text_value as textValue, pm.variation_value_1, pm.variation_value_2, pm.variation_value_3 "
+ +
+ "FROM project_measures pm, projects p, metrics m " +
+ "WHERE pm.component_uuid=p.uuid and pm.metric_id=m.id and p.qualifier='"
+ + qualifier + "'";
+ List<Map<String, String>> rows = orchestrator.getDatabase().executeSql(sql);
+
+ System.out.println("---- " + title + " - measures on qualifier " + qualifier);
+ for (Map<String, String> row : rows) {
+ System.out.println(" " + row);
+ }
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/duplication/CrossModuleDuplicationsTest.java b/tests/src/test/java/org/sonarqube/tests/duplication/CrossModuleDuplicationsTest.java
new file mode 100644
index 00000000000..a4588635e37
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/duplication/CrossModuleDuplicationsTest.java
@@ -0,0 +1,143 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.duplication;
+
+import com.google.common.collect.ImmutableMap;
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category4Suite;
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+import org.apache.commons.io.FileUtils;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.getMeasuresAsDoubleByMetricKey;
+
+public class CrossModuleDuplicationsTest {
+ private static final String PROJECT_KEY = "cross-module";
+ private static final String PROJECT_DIR = "duplications/" + PROJECT_KEY;
+ private File projectDir;
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @BeforeClass
+ public static void analyzeProjects() {
+
+ }
+
+ @Before
+ public void setUpProject() throws IOException {
+ orchestrator.resetData();
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/duplication/xoo-duplication-profile.xml"));
+
+ FileUtils.copyDirectory(ItUtils.projectDir(PROJECT_DIR), temp.getRoot());
+ projectDir = temp.getRoot();
+ }
+
+ @Test
+ public void testDuplications() throws IOException {
+ analyzeProject(projectDir, PROJECT_KEY, true);
+ verifyDuplicationMeasures(PROJECT_KEY, 2, 54, 2, 56.3);
+
+ // File1 is the one duplicated in both modules
+ verifyDuplicationMeasures(PROJECT_KEY + ":module1:src/main/xoo/sample/File1.xoo", 1, 27, 1, 75);
+ verifyDuplicationMeasures(PROJECT_KEY + ":module2:src/main/xoo/sample/File1.xoo", 1, 27, 1, 75);
+ }
+
+ @Test
+ // SONAR-6184
+ public void testGhostDuplication() throws IOException {
+ analyzeProject(projectDir, PROJECT_KEY, true);
+
+ verifyDuplicationMeasures(PROJECT_KEY + ":module1", 1, 27, 1, 45);
+ verifyDuplicationMeasures(PROJECT_KEY + ":module2", 1, 27, 1, 75);
+
+ // move File2 from module1 to module2
+ File src = FileUtils.getFile(projectDir, "module1", "src", "main", "xoo", "sample", "File2.xoo");
+ File dst = FileUtils.getFile(projectDir, "module2", "src", "main", "xoo", "sample", "File2.xoo");
+ FileUtils.moveFile(src, dst);
+
+ src = new File(src.getParentFile(), "File2.xoo.measures");
+ dst = new File(dst.getParentFile(), "File2.xoo.measures");
+ FileUtils.moveFile(src, dst);
+
+ // duplication should remain unchanged (except for % of duplication)
+ analyzeProject(projectDir, PROJECT_KEY, false);
+ verifyDuplicationMeasures(PROJECT_KEY + ":module1", 1, 27, 1, 75);
+ verifyDuplicationMeasures(PROJECT_KEY + ":module2", 1, 27, 1, 45);
+ }
+
+ @Test
+ // SONAR-6184
+ public void testDuplicationFix() throws IOException {
+ analyzeProject(projectDir, PROJECT_KEY, true);
+
+ verifyDuplicationMeasures(PROJECT_KEY + ":module1", 1, 27, 1, 45);
+ verifyDuplicationMeasures(PROJECT_KEY + ":module2", 1, 27, 1, 75);
+
+ // remove File1 from module1
+ File f1 = FileUtils.getFile(projectDir, "module1", "src", "main", "xoo", "sample", "File1.xoo");
+ File f1m = FileUtils.getFile(projectDir, "module2", "src", "main", "xoo", "sample", "File1.xoo.measures");
+ f1.delete();
+ f1m.delete();
+
+ // duplication should be 0
+ analyzeProject(projectDir, PROJECT_KEY, false);
+ verifyDuplicationMeasures(PROJECT_KEY + ":module1", 0, 0, 0, 0);
+ verifyDuplicationMeasures(PROJECT_KEY + ":module2", 0, 0, 0, 0);
+ }
+
+ private static SonarScanner analyzeProject(File projectDir, String projectKey, boolean create, String... additionalProperties) {
+ if (create) {
+ orchestrator.getServer().provisionProject(projectKey, projectKey);
+ orchestrator.getServer().associateProjectToQualityProfile(projectKey, "xoo", "xoo-duplication-profile");
+ }
+
+ SonarScanner sonarRunner = SonarScanner.create(projectDir);
+ ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
+
+ for (int i = 0; i < additionalProperties.length; i += 2) {
+ builder.put(additionalProperties[i], additionalProperties[i + 1]);
+ }
+ SonarScanner scan = sonarRunner.setDebugLogs(true).setProperties(builder.build());
+ orchestrator.executeBuild(scan);
+ return scan;
+ }
+
+ private static void verifyDuplicationMeasures(String componentKey, int duplicatedBlocks, int duplicatedLines, int duplicatedFiles, double duplicatedLinesDensity) {
+ Map<String, Double> measures = getMeasuresAsDoubleByMetricKey(orchestrator, componentKey, "duplicated_lines", "duplicated_blocks", "duplicated_files", "duplicated_lines_density");
+ assertThat(measures.get("duplicated_blocks").intValue()).isEqualTo(duplicatedBlocks);
+ assertThat(measures.get("duplicated_lines").intValue()).isEqualTo(duplicatedLines);
+ assertThat(measures.get("duplicated_files").intValue()).isEqualTo(duplicatedFiles);
+ assertThat(measures.get("duplicated_lines_density")).isEqualTo(duplicatedLinesDensity);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/duplication/CrossProjectDuplicationsOnRemoveFileTest.java b/tests/src/test/java/org/sonarqube/tests/duplication/CrossProjectDuplicationsOnRemoveFileTest.java
new file mode 100644
index 00000000000..8813c2cb053
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/duplication/CrossProjectDuplicationsOnRemoveFileTest.java
@@ -0,0 +1,91 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.duplication;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category4Suite;
+import org.apache.commons.io.IOUtils;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.skyscreamer.jsonassert.JSONAssert.assertEquals;
+import static util.ItUtils.getComponent;
+import static util.ItUtils.runProjectAnalysis;
+import static util.selenium.Selenese.runSelenese;
+
+public class CrossProjectDuplicationsOnRemoveFileTest {
+
+ static final String ORIGIN_PROJECT = "origin-project";
+ static final String DUPLICATE_PROJECT = "duplicate-project";
+ static final String DUPLICATE_FILE = DUPLICATE_PROJECT + ":src/main/xoo/sample/File1.xoo";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+
+ @BeforeClass
+ public static void analyzeProjects() {
+ orchestrator.resetData();
+ ItUtils.restoreProfile(orchestrator, CrossProjectDuplicationsOnRemoveFileTest.class.getResource("/duplication/xoo-duplication-profile.xml"));
+
+ analyzeProject(ORIGIN_PROJECT, "duplications/cross-project/origin");
+ analyzeProject(DUPLICATE_PROJECT, "duplications/cross-project/duplicate");
+
+ // Remove origin project
+ orchestrator.getServer().adminWsClient().post("api/projects/bulk_delete", "keys", ORIGIN_PROJECT);
+ assertThat(getComponent(orchestrator, ORIGIN_PROJECT)).isNull();
+ }
+
+ @Test
+ public void duplications_show_ws_does_not_contain_key_of_deleted_file() throws Exception {
+ String duplication = orchestrator.getServer().adminWsClient().get("api/duplications/show", "key", DUPLICATE_FILE);
+
+ assertEquals(IOUtils.toString(CrossProjectDuplicationsTest.class.getResourceAsStream(
+ "/duplication/CrossProjectDuplicationsOnRemoveFileTest/duplications_on_removed_file-expected.json"), "UTF-8"),
+ duplication, false);
+
+ // Only one file should be reference, so the reference 2 on origin-project must not exist
+ assertThat(duplication).doesNotContain("\"2\"");
+ assertThat(duplication).doesNotContain("origin-project");
+ }
+
+ /**
+ * SONAR-3277
+ */
+ @Test
+ public void display_message_in_viewer_when_duplications_with_deleted_files_are_found() throws Exception {
+ // TODO stas, please replace this IT by a medium test
+ runSelenese(orchestrator,
+ "/duplication/CrossProjectDuplicationsOnRemoveFileTest/duplications-with-deleted-project.html");
+ }
+
+ private static void analyzeProject(String projectKey, String path) {
+ orchestrator.getServer().provisionProject(projectKey, projectKey);
+ orchestrator.getServer().associateProjectToQualityProfile(projectKey, "xoo", "xoo-duplication-profile");
+
+ runProjectAnalysis(orchestrator, path,
+ "sonar.cpd.cross_project", "true",
+ "sonar.projectKey", projectKey,
+ "sonar.projectName", projectKey);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/duplication/CrossProjectDuplicationsTest.java b/tests/src/test/java/org/sonarqube/tests/duplication/CrossProjectDuplicationsTest.java
new file mode 100644
index 00000000000..81708b22d2c
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/duplication/CrossProjectDuplicationsTest.java
@@ -0,0 +1,171 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.duplication;
+
+import com.google.common.collect.ObjectArrays;
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category4Suite;
+import java.util.Map;
+import org.apache.commons.io.IOUtils;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.issue.SearchWsRequest;
+import util.ItUtils;
+import util.issue.IssueRule;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.skyscreamer.jsonassert.JSONAssert.assertEquals;
+import static util.ItUtils.getMeasureAsDouble;
+import static util.ItUtils.getMeasuresAsDoubleByMetricKey;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.runProjectAnalysis;
+import static util.ItUtils.setServerProperty;
+import static util.selenium.Selenese.runSelenese;
+
+public class CrossProjectDuplicationsTest {
+
+ static final String ORIGIN_PROJECT = "origin-project";
+ static final String DUPLICATE_PROJECT = "duplicate-project";
+ static final String PROJECT_WITH_EXCLUSION = "project-with-exclusion";
+ static final String PROJECT_WITHOUT_ENOUGH_TOKENS = "project_without_enough_tokens";
+
+ static final String DUPLICATE_FILE = DUPLICATE_PROJECT + ":src/main/xoo/sample/File1.xoo";
+ static final String BRANCH = "with-branch";
+
+ static final String ORIGIN_PATH = "duplications/cross-project/origin";
+ static final String DUPLICATE_PATH = "duplications/cross-project/duplicate";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+
+ @ClassRule
+ public static final IssueRule issueRule = IssueRule.from(orchestrator);
+
+ @BeforeClass
+ public static void analyzeProjects() {
+ orchestrator.resetData();
+ ItUtils.restoreProfile(orchestrator, CrossProjectDuplicationsTest.class.getResource("/duplication/xoo-duplication-profile.xml"));
+
+ analyzeProject(ORIGIN_PROJECT, ORIGIN_PATH);
+ analyzeProject(DUPLICATE_PROJECT, DUPLICATE_PATH);
+ analyzeProjectWithBranch(DUPLICATE_PROJECT, DUPLICATE_PATH, BRANCH);
+ analyzeProject(PROJECT_WITH_EXCLUSION, DUPLICATE_PATH, "sonar.cpd.exclusions", "**/File*");
+
+ // Set minimum tokens to a big value in order to not get duplications
+ setServerProperty(orchestrator, "sonar.cpd.xoo.minimumTokens", "1000");
+ analyzeProject(PROJECT_WITHOUT_ENOUGH_TOKENS, DUPLICATE_PATH);
+ }
+
+ @AfterClass
+ public static void resetServerProperties() throws Exception {
+ setServerProperty(orchestrator, "sonar.cpd.xoo.minimumTokens", null);
+ }
+
+ @Test
+ public void origin_project_has_no_duplication_as_it_has_not_been_analyzed_twice() throws Exception {
+ assertProjectHasNoDuplication(ORIGIN_PROJECT);
+ }
+
+ @Test
+ public void duplicate_project_has_duplication_as_it_has_been_analyzed_twice() throws Exception {
+ Map<String, Double> measures = getMeasuresAsDoubleByMetricKey(orchestrator, DUPLICATE_PROJECT, "duplicated_lines", "duplicated_blocks", "duplicated_files", "duplicated_lines_density");
+ assertThat(measures.get("duplicated_lines").intValue()).isEqualTo(27);
+ assertThat(measures.get("duplicated_blocks").intValue()).isEqualTo(1);
+ assertThat(measures.get("duplicated_files").intValue()).isEqualTo(1);
+ assertThat(measures.get("duplicated_lines_density")).isEqualTo(45d);
+ }
+
+ @Test
+ public void issue_on_duplicated_blocks_is_generated_on_file() throws Exception {
+ assertThat(issueRule.search(new SearchWsRequest().setComponentKeys(singletonList(DUPLICATE_FILE)).setRules(singletonList("common-xoo:DuplicatedBlocks"))).getIssuesList())
+ .hasSize(1);
+ }
+
+ @Test
+ public void verify_sources_lines_ws_duplication_information() throws Exception {
+ verifyWsResultOnDuplicateFile("api/sources/lines", "sources_lines_duplication-expected.json");
+ }
+
+ @Test
+ public void verify_duplications_show_ws() throws Exception {
+ verifyWsResultOnDuplicateFile("api/duplications/show", "duplications_show-expected.json");
+ }
+
+ @Test
+ public void project_with_branch_has_no_duplication() throws Exception {
+ assertProjectHasNoDuplication(DUPLICATE_PROJECT + ":" + BRANCH);
+ }
+
+ @Test
+ public void project_with_exclusion_has_no_duplication() throws Exception {
+ assertProjectHasNoDuplication(PROJECT_WITH_EXCLUSION);
+ }
+
+ @Test
+ public void project_without_enough_tokens_has_duplication() throws Exception {
+ assertProjectHasNoDuplication(PROJECT_WITHOUT_ENOUGH_TOKENS);
+ }
+
+ @Test
+ public void verify_viewer() {
+ runSelenese(orchestrator, "/duplication/CrossProjectDuplicationsTest/cross-project-duplications-viewer.html");
+ }
+
+ private static void analyzeProject(String projectKey, String path, String... additionalProperties) {
+ initProject(projectKey);
+ executeAnalysis(projectKey, path, additionalProperties);
+ }
+
+ private static void analyzeProjectWithBranch(String projectKey, String path, String branch) {
+ initProject(projectKey + ":" + branch);
+ executeAnalysis(projectKey, path, "sonar.branch", branch);
+ }
+
+ private static void initProject(String effectiveProjectKey) {
+ orchestrator.getServer().provisionProject(effectiveProjectKey, effectiveProjectKey);
+ orchestrator.getServer().associateProjectToQualityProfile(effectiveProjectKey, "xoo", "xoo-duplication-profile");
+ }
+
+ private static void executeAnalysis(String projectKey, String path, String... additionalProperties) {
+ runProjectAnalysis(orchestrator, path,
+ ObjectArrays.concat(
+ new String[] {
+ "sonar.cpd.cross_project", "true",
+ "sonar.projectKey", projectKey,
+ "sonar.projectName", projectKey
+ },
+ additionalProperties, String.class));
+ }
+
+ private static void assertProjectHasNoDuplication(String projectKey) {
+ assertThat(getMeasureAsDouble(orchestrator, projectKey, "duplicated_lines")).isZero();
+ }
+
+ private static void verifyWsResultOnDuplicateFile(String ws, String expectedFilePath) throws Exception {
+ String duplication = newAdminWsClient(orchestrator).wsConnector().call(new GetRequest(ws).setParam("key", DUPLICATE_FILE)).content();
+ assertEquals(IOUtils.toString(CrossProjectDuplicationsTest.class.getResourceAsStream("/duplication/CrossProjectDuplicationsTest/" + expectedFilePath), "UTF-8"), duplication,
+ false);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/duplication/DuplicationsTest.java b/tests/src/test/java/org/sonarqube/tests/duplication/DuplicationsTest.java
new file mode 100644
index 00000000000..710fd8de0d5
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/duplication/DuplicationsTest.java
@@ -0,0 +1,189 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.duplication;
+
+import com.google.common.collect.ObjectArrays;
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category4Suite;
+import java.util.Map;
+import org.apache.commons.io.IOUtils;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.WsResponse;
+import org.sonarqube.ws.client.issue.SearchWsRequest;
+import util.ItUtils;
+import util.issue.IssueRule;
+
+import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.skyscreamer.jsonassert.JSONAssert.assertEquals;
+import static util.ItUtils.getMeasuresAsDoubleByMetricKey;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.runProjectAnalysis;
+import static util.ItUtils.setServerProperty;
+
+public class DuplicationsTest {
+
+ static final String DUPLICATIONS = "file-duplications";
+ static final String DUPLICATIONS_WITH_EXCLUSIONS = "file-duplications-with-exclusions";
+ static final String WITHOUT_ENOUGH_TOKENS = "project_without_enough_tokens";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+
+ @ClassRule
+ public static final IssueRule issueRule = IssueRule.from(orchestrator);
+
+ private static WsClient adminWsClient;
+
+ @BeforeClass
+ public static void analyzeProjects() {
+ orchestrator.resetData();
+
+ ItUtils.restoreProfile(orchestrator, DuplicationsTest.class.getResource("/duplication/xoo-duplication-profile.xml"));
+ analyzeProject(DUPLICATIONS);
+ analyzeProject(DUPLICATIONS_WITH_EXCLUSIONS, "sonar.cpd.exclusions", "**/File*");
+
+ // Set minimum tokens to a big value in order to not get duplications
+ setServerProperty(orchestrator, "sonar.cpd.xoo.minimumTokens", "1000");
+ analyzeProject(WITHOUT_ENOUGH_TOKENS);
+
+ adminWsClient = newAdminWsClient(orchestrator);
+ }
+
+ @AfterClass
+ public static void resetProperties() throws Exception {
+ setServerProperty(orchestrator, "sonar.cpd.xoo.minimumTokens", null);
+ }
+
+ private static Map<String, Double> getMeasures(String key) {
+ return getMeasuresAsDoubleByMetricKey(orchestrator, key, "duplicated_lines", "duplicated_blocks", "duplicated_files", "duplicated_lines_density");
+ }
+
+ private static void verifyDuplicationMeasures(String componentKey, int duplicatedBlocks, int duplicatedLines, int duplicatedFiles, double duplicatedLinesDensity) {
+ Map<String, Double> measures = getMeasures(componentKey);
+ assertThat(measures.get("duplicated_blocks").intValue()).isEqualTo(duplicatedBlocks);
+ assertThat(measures.get("duplicated_lines").intValue()).isEqualTo(duplicatedLines);
+ assertThat(measures.get("duplicated_files").intValue()).isEqualTo(duplicatedFiles);
+ assertThat(measures.get("duplicated_lines_density")).isEqualTo(duplicatedLinesDensity);
+ }
+
+ private static void analyzeProject(String projectKey, String... additionalProperties) {
+ orchestrator.getServer().provisionProject(projectKey, projectKey);
+ orchestrator.getServer().associateProjectToQualityProfile(projectKey, "xoo", "xoo-duplication-profile");
+
+ runProjectAnalysis(orchestrator, "duplications/file-duplications",
+ ObjectArrays.concat(
+ new String[] {
+ "sonar.projectKey", projectKey,
+ "sonar.projectName", projectKey
+ },
+ additionalProperties, String.class));
+ }
+
+ private static void verifyWsResultOnDuplicateFile(String fileKey, String ws, String expectedFilePath) throws Exception {
+ String duplication = orchestrator.getServer().adminWsClient().get(ws, "key", fileKey);
+ assertEquals(IOUtils.toString(CrossProjectDuplicationsTest.class.getResourceAsStream("/duplication/DuplicationsTest/" + expectedFilePath), "UTF-8"), duplication,
+ false);
+ }
+
+ @Test
+ public void duplicated_lines_within_same_file() {
+ verifyDuplicationMeasures(DUPLICATIONS + ":src/main/xoo/duplicated_lines_within_same_file/DuplicatedLinesInSameFile.xoo",
+ 2,
+ 30 * 2, // 2 blocks with 30 lines
+ 1,
+ 84.5);
+ }
+
+ @Test
+ public void duplicated_same_lines_within_3_classes() {
+ verifyDuplicationMeasures(DUPLICATIONS + ":src/main/xoo/duplicated_same_lines_within_3_files/File1.xoo", 1, 33, 1, 78.6);
+ verifyDuplicationMeasures(DUPLICATIONS + ":src/main/xoo/duplicated_same_lines_within_3_files/File2.xoo", 1, 31, 1, 75.6);
+ verifyDuplicationMeasures(DUPLICATIONS + ":src/main/xoo/duplicated_same_lines_within_3_files/File3.xoo", 1, 31, 1, 70.5);
+ verifyDuplicationMeasures(DUPLICATIONS + ":src/main/xoo/duplicated_same_lines_within_3_files", 3, 95, 3, 74.8);
+ }
+
+ @Test
+ public void duplicated_lines_within_directory() {
+ verifyDuplicationMeasures(DUPLICATIONS + ":src/main/xoo/duplicated_lines_within_dir/DuplicatedLinesInSameDirectory1.xoo", 1, 30, 1, 28.3);
+ verifyDuplicationMeasures(DUPLICATIONS + ":src/main/xoo/duplicated_lines_within_dir/DuplicatedLinesInSameDirectory2.xoo", 1, 30, 1, 41.7);
+ verifyDuplicationMeasures(DUPLICATIONS + ":src/main/xoo/duplicated_lines_within_dir", 2, 60, 2, 33.7);
+ }
+
+ @Test
+ public void duplicated_lines_with_other_directory() {
+ verifyDuplicationMeasures(DUPLICATIONS + ":src/main/xoo/duplicated_lines_with_other_dir1/DuplicatedLinesWithOtherDirectory.xoo", 1, 39, 1, 92.9);
+ verifyDuplicationMeasures(DUPLICATIONS + ":src/main/xoo/duplicated_lines_with_other_dir1", 1, 39, 1, 92.9);
+
+ verifyDuplicationMeasures(DUPLICATIONS + ":src/main/xoo/duplicated_lines_with_other_dir2/DuplicatedLinesWithOtherDirectory.xoo", 1, 39, 1, 92.9);
+ verifyDuplicationMeasures(DUPLICATIONS + ":src/main/xoo/duplicated_lines_with_other_dir2", 1, 39, 1, 92.9);
+ }
+
+ @Test
+ public void duplication_measures_on_project() {
+ verifyDuplicationMeasures(DUPLICATIONS, 9, 293, 8, 63.7);
+ }
+
+ @Test
+ public void project_without_enough_tokens_has_duplication() {
+ verifyDuplicationMeasures(WITHOUT_ENOUGH_TOKENS, 0, 0, 0, 0d);
+ }
+
+ /**
+ * SONAR-3108
+ */
+ @Test
+ public void use_duplication_exclusions() {
+ verifyDuplicationMeasures(DUPLICATIONS_WITH_EXCLUSIONS, 6, 198, 5, 43d);
+ }
+
+ @Test
+ public void issues_on_duplicated_blocks_are_generated_on_each_file() throws Exception {
+ assertThat(issueRule.search(new SearchWsRequest().setRules(singletonList("common-xoo:DuplicatedBlocks"))).getIssuesList()).hasSize(13);
+ }
+
+ @Test
+ public void verify_sources_lines_ws_duplication_information() throws Exception {
+ verifyWsResultOnDuplicateFile(DUPLICATIONS + ":src/main/xoo/duplicated_lines_within_same_file/DuplicatedLinesInSameFile.xoo",
+ "api/sources/lines", "sources_lines_duplication-expected.json");
+ }
+
+ @Test
+ public void verify_duplications_show_ws() throws Exception {
+ verifyWsResultOnDuplicateFile(DUPLICATIONS + ":src/main/xoo/duplicated_lines_within_same_file/DuplicatedLinesInSameFile.xoo",
+ "api/duplications/show", "duplications_show-expected.json");
+ }
+
+ // SONAR-9441
+ @Test
+ public void fail_properly_when_no_parameter() {
+ WsResponse result = adminWsClient.wsConnector().call(new GetRequest("api/duplications/show"));
+
+ assertThat(result.code()).isEqualTo(HTTP_BAD_REQUEST);
+ assertThat(result.content()).contains("Either 'uuid' or 'key' must be provided, not both");
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/duplication/NewDuplicationsTest.java b/tests/src/test/java/org/sonarqube/tests/duplication/NewDuplicationsTest.java
new file mode 100644
index 00000000000..c16b3441f5d
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/duplication/NewDuplicationsTest.java
@@ -0,0 +1,84 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.duplication;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category4Suite;
+import java.util.Map;
+import org.assertj.core.data.Offset;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.WsMeasures;
+
+import static java.lang.Double.parseDouble;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.getMeasuresWithVariationsByMetricKey;
+import static util.ItUtils.runProjectAnalysis;
+
+public class NewDuplicationsTest {
+
+ static final Offset<Double> DEFAULT_OFFSET = Offset.offset(0.1d);
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+
+ @BeforeClass
+ public static void analyzeProjects() {
+ orchestrator.resetData();
+
+ runProjectAnalysis(orchestrator, "duplications/new-duplications-v1",
+ "sonar.projectDate", "2015-02-01",
+ "sonar.scm.disabled", "false");
+ runProjectAnalysis(orchestrator, "duplications/new-duplications-v2",
+ "sonar.scm.disabled", "false");
+ }
+
+ @Test
+ public void new_duplications_on_project() throws Exception {
+ Map<String, WsMeasures.Measure> measures = getMeasures("new-duplications");
+ assertThat(parseDouble(measures.get("new_lines").getPeriods().getPeriodsValue(0).getValue())).isEqualTo(83d, DEFAULT_OFFSET);
+ assertThat(parseDouble(measures.get("new_duplicated_lines").getPeriods().getPeriodsValue(0).getValue())).isEqualTo(71d, DEFAULT_OFFSET);
+ assertThat(parseDouble(measures.get("new_duplicated_lines_density").getPeriods().getPeriodsValue(0).getValue())).isEqualTo(85.5d, DEFAULT_OFFSET);
+ assertThat(parseDouble(measures.get("new_duplicated_blocks").getPeriods().getPeriodsValue(0).getValue())).isEqualTo(12d, DEFAULT_OFFSET);
+ }
+
+ @Test
+ public void new_duplications_on_directory() throws Exception {
+ Map<String, WsMeasures.Measure> measures = getMeasures("new-duplications:src/main/xoo/duplicated_lines_with_other_dir1");
+ assertThat(parseDouble(measures.get("new_lines").getPeriods().getPeriodsValue(0).getValue())).isEqualTo(24d, DEFAULT_OFFSET);
+ assertThat(parseDouble(measures.get("new_duplicated_lines").getPeriods().getPeriodsValue(0).getValue())).isEqualTo(24d, DEFAULT_OFFSET);
+ assertThat(parseDouble(measures.get("new_duplicated_lines_density").getPeriods().getPeriodsValue(0).getValue())).isEqualTo(100d, DEFAULT_OFFSET);
+ assertThat(parseDouble(measures.get("new_duplicated_blocks").getPeriods().getPeriodsValue(0).getValue())).isEqualTo(7d, DEFAULT_OFFSET);
+ }
+
+ @Test
+ public void new_duplications_on_file() throws Exception {
+ Map<String, WsMeasures.Measure> measures = getMeasures("new-duplications:src/main/xoo/duplicated_lines_within_same_file/DuplicatedLinesInSameFile.xoo");
+ assertThat(parseDouble(measures.get("new_lines").getPeriods().getPeriodsValue(0).getValue())).isEqualTo(41d, DEFAULT_OFFSET);
+ assertThat(parseDouble(measures.get("new_duplicated_lines").getPeriods().getPeriodsValue(0).getValue())).isEqualTo(29d, DEFAULT_OFFSET);
+ assertThat(parseDouble(measures.get("new_duplicated_lines_density").getPeriods().getPeriodsValue(0).getValue())).isEqualTo(70.7d, DEFAULT_OFFSET);
+ assertThat(parseDouble(measures.get("new_duplicated_blocks").getPeriods().getPeriodsValue(0).getValue())).isEqualTo(2d, DEFAULT_OFFSET);
+ }
+
+ private static Map<String, WsMeasures.Measure> getMeasures(String key) {
+ return getMeasuresWithVariationsByMetricKey(orchestrator, key, "new_lines", "new_duplicated_lines", "new_duplicated_lines_density", "new_duplicated_blocks");
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/i18n/I18nTest.java b/tests/src/test/java/org/sonarqube/tests/i18n/I18nTest.java
new file mode 100644
index 00000000000..29b3580e0eb
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/i18n/I18nTest.java
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.i18n;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category1Suite;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static util.ItUtils.projectDir;
+import static util.selenium.Selenese.runSelenese;
+
+public class I18nTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ @Before
+ public void cleanDatabase() {
+ orchestrator.resetData();
+ }
+
+ /**
+ * TODO This test should use a fake widget that display a fake metric with decimals instead of using provided metric
+ * Ignored because there is not a good idea to force a display language by GET parameter
+ * The displayed language is based on browser/system locale
+ */
+ @Test
+ @Ignore
+ public void test_localization() {
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")));
+
+ runSelenese(orchestrator,
+ "/i18n/default-locale-is-english.html",
+ "/i18n/french-locale.html",
+ "/i18n/french-pack.html",
+ "/i18n/locale-with-france-country.html",
+ "/i18n/locale-with-swiss-country.html");
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/issue/AbstractIssueTest.java b/tests/src/test/java/org/sonarqube/tests/issue/AbstractIssueTest.java
new file mode 100644
index 00000000000..4f85822c7d6
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/issue/AbstractIssueTest.java
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.issue;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category2Suite;
+import java.util.List;
+import org.junit.ClassRule;
+import org.sonar.wsclient.issue.Issue;
+import org.sonar.wsclient.issue.IssueClient;
+import org.sonar.wsclient.issue.IssueQuery;
+import org.sonar.wsclient.issue.Issues;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public abstract class AbstractIssueTest {
+
+ @ClassRule
+ public static final Orchestrator ORCHESTRATOR = Category2Suite.ORCHESTRATOR;
+
+ static IssueClient adminIssueClient() {
+ return ORCHESTRATOR.getServer().adminWsClient().issueClient();
+ }
+
+ static IssueClient issueClient() {
+ return ORCHESTRATOR.getServer().wsClient().issueClient();
+ }
+
+ static Issue searchRandomIssue() {
+ List<Issue> issues = searchIssues(IssueQuery.create());
+ assertThat(issues).isNotEmpty();
+ return issues.get(0);
+ }
+
+ static Issues search(IssueQuery issueQuery) {
+ issueQuery.urlParams().put("additionalFields", "_all");
+ return issueClient().find(issueQuery);
+ }
+
+ static List<Issue> searchIssues() {
+ return searchIssues(IssueQuery.create());
+ }
+
+ static List<Issue> searchIssues(IssueQuery issueQuery) {
+ return issueClient().find(issueQuery).list();
+ }
+
+ static List<Issue> searchIssuesByProject(String projectKey) {
+ return search(IssueQuery.create().componentRoots(projectKey)).list();
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/issue/AutoAssignTest.java b/tests/src/test/java/org/sonarqube/tests/issue/AutoAssignTest.java
new file mode 100644
index 00000000000..7b6f2caf80a
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/issue/AutoAssignTest.java
@@ -0,0 +1,171 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.issue;
+
+import java.util.Arrays;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.wsclient.issue.Issue;
+import org.sonar.wsclient.issue.IssueQuery;
+import org.sonarqube.ws.WsUsers;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.user.CreateRequest;
+import org.sonarqube.ws.client.user.SearchRequest;
+import util.ProjectAnalysis;
+import util.ProjectAnalysisRule;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.setServerProperty;
+
+public class AutoAssignTest extends AbstractIssueTest {
+
+ @Rule
+ public final ProjectAnalysisRule projectAnalysisRule = ProjectAnalysisRule.from(ORCHESTRATOR);
+
+ ProjectAnalysis projectAnalysis;
+
+ @Before
+ public void setup() {
+ String qualityProfileKey = projectAnalysisRule.registerProfile("/issue/IssueActionTest/xoo-one-issue-per-line-profile.xml");
+ String projectKey = projectAnalysisRule.registerProject("issue/AutoAssignTest");
+ projectAnalysis = projectAnalysisRule.newProjectAnalysis(projectKey)
+ .withQualityProfile(qualityProfileKey)
+ .withProperties("sonar.scm.disabled", "false", "sonar.scm.provider", "xoo");
+ }
+
+ @After
+ public void resetData() throws Exception {
+ newAdminWsClient(ORCHESTRATOR).wsConnector().call(new PostRequest("api/projects/delete").setParam("project", "AutoAssignTest"));
+ deleteAllUsers();
+
+ // Reset default assignee
+ setServerProperty(ORCHESTRATOR, "sonar.issues.defaultAssigneeLogin", null);
+ }
+
+ @Test
+ public void auto_assign_issues_to_user() throws Exception {
+ // verify that login matches, case-sensitive
+ createUser("user1", "User 1", "user1@email.com");
+ createUser("USER2", "User 2", "user2@email.com");
+ // verify that name is not used to match, whatever the case
+ createUser("user3", "User 3", "user3@email.com");
+ createUser("user4", "USER 4", "user4@email.com");
+ // verify that email matches, case-insensitive
+ createUser("user5", "User 5", "user5@email.com");
+ createUser("user6", "User 6", "USER6@email.COM");
+ // verify that SCM account matches, case-insensitive
+ createUser("user7", "User 7", "user7@email.com", "user7ScmAccount");
+ createUser("user8", "User 8", "user8@email.com", "user8SCMaccOUNT");
+
+ projectAnalysis.run();
+
+ List<Issue> issues = search(IssueQuery.create().components("AutoAssignTest:src/sample.xoo").sort("FILE_LINE")).list();
+ // login match, case-sensitive
+ verifyIssueAssignee(issues, 1, "user1");
+ verifyIssueAssignee(issues, 2, null);
+ // user name is not used to match
+ verifyIssueAssignee(issues, 3, null);
+ verifyIssueAssignee(issues, 4, null);
+ // email match, case-insensitive
+ verifyIssueAssignee(issues, 5, "user5");
+ verifyIssueAssignee(issues, 6, "user6");
+ // SCM account match, case-insensitive
+ verifyIssueAssignee(issues, 7, "user7");
+ verifyIssueAssignee(issues, 8, "user8");
+ }
+
+ private static void verifyIssueAssignee(List<Issue> issues, int line, @Nullable String expectedAssignee) {
+ assertThat(issues.get(line - 1).assignee()).isEqualTo(expectedAssignee);
+ }
+
+ @Test
+ public void auto_assign_issues_to_default_assignee() throws Exception {
+ createUser("user1", "User 1", "user1@email.com");
+ createUser("user2", "User 2", "user2@email.com");
+ setServerProperty(ORCHESTRATOR, "sonar.issues.defaultAssigneeLogin", "user2");
+ projectAnalysis.run();
+
+ // user1 is assigned to his issues. All other issues are assigned to the default assignee.
+ assertThat(search(IssueQuery.create().assignees("user1")).list()).hasSize(1);
+ assertThat(search(IssueQuery.create().assignees("user2")).list()).hasSize(8);
+ // No unassigned issues
+ assertThat(search(IssueQuery.create().assigned(false)).list()).isEmpty();
+ }
+
+ /**
+ * SONAR-7098
+ *
+ * Given two versions of same project:
+ * v1: issue, but no SCM data
+ * v2: old issue and SCM data
+ * Expected: all issues should be associated with authors
+ */
+ @Test
+ public void update_author_and_assignee_when_scm_is_activated() {
+ createUser("user1", "User 1", "user1@email.com");
+
+ // Run a first analysis without SCM
+ projectAnalysis.withProperties("sonar.scm.disabled", "true").run();
+ List<Issue> issues = searchIssues();
+ assertThat(issues).isNotEmpty();
+
+ // No author and assignee are set
+ for (Issue issue : issues) {
+ assertThat(issue.author()).isEmpty();
+ }
+ assertThat(search(IssueQuery.create().assigned(true)).list()).isEmpty();
+
+ // Run a second analysis with SCM
+ projectAnalysis.run();
+ issues = searchIssues();
+ assertThat(issues).isNotEmpty();
+
+ // Authors and assignees are set
+ for (Issue issue : issues) {
+ assertThat(issue.author()).isNotEmpty();
+ }
+ assertThat(search(IssueQuery.create().assignees("user1")).list()).hasSize(1);
+ }
+
+ private static void createUser(String login, String name, String email, String... scmAccounts) {
+ newAdminWsClient(ORCHESTRATOR).users().create(
+ CreateRequest.builder()
+ .setLogin(login)
+ .setName(name)
+ .setEmail(email)
+ .setPassword("xxxxxxx")
+ .setScmAccounts(Arrays.asList(scmAccounts))
+ .build());
+ }
+
+ private static void deleteAllUsers() {
+ WsClient wsClient = newAdminWsClient(ORCHESTRATOR);
+ WsUsers.SearchWsResponse searchResponse = wsClient.users().search(SearchRequest.builder().build());
+ searchResponse.getUsersList().forEach(user -> {
+ wsClient.wsConnector().call(new PostRequest("api/users/deactivate").setParam("login", user.getLogin()));
+ });
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/issue/CommonRulesTest.java b/tests/src/test/java/org/sonarqube/tests/issue/CommonRulesTest.java
new file mode 100644
index 00000000000..69cddb89b7a
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/issue/CommonRulesTest.java
@@ -0,0 +1,95 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.issue;
+
+import java.util.List;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.sonarqube.ws.Issues.Issue;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.issue.SearchWsRequest;
+import util.ItUtils;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.runProjectAnalysis;
+
+public class CommonRulesTest extends AbstractIssueTest {
+
+ public static final String FILE_KEY = "common-rules-project:src/Sample.xoo";
+ public static final String TEST_FILE_KEY = "common-rules-project:test/SampleTest.xoo";
+
+ private static WsClient adminWsClient;
+
+ @BeforeClass
+ public static void setUp() {
+ ORCHESTRATOR.resetData();
+ ItUtils.restoreProfile(ORCHESTRATOR, CommonRulesTest.class.getResource("/issue/CommonRulesTest/xoo-common-rules-profile.xml"));
+ ORCHESTRATOR.getServer().provisionProject("common-rules-project", "Sample");
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile("common-rules-project", "xoo", "xoo-common-rules");
+ runProjectAnalysis(ORCHESTRATOR, "issue/common-rules",
+ "sonar.cpd.xoo.minimumTokens", "2",
+ "sonar.cpd.xoo.minimumLines", "2");
+
+ adminWsClient = newAdminWsClient(ORCHESTRATOR);
+ }
+
+ @Test
+ public void test_rule_on_duplicated_blocks() {
+ List<Issue> issues = findIssues(FILE_KEY, "common-xoo:DuplicatedBlocks");
+ assertThat(issues).hasSize(1);
+ }
+
+ @Test
+ public void test_rule_on_comments() {
+ List<Issue> issues = findIssues(FILE_KEY, "common-xoo:InsufficientCommentDensity");
+ assertThat(issues.size()).isEqualTo(1);
+ }
+
+ @Test
+ public void test_rule_on_coverage() {
+ List<Issue> issues = findIssues(FILE_KEY, "common-xoo:InsufficientBranchCoverage");
+ assertThat(issues.size()).isEqualTo(1);
+
+ issues = findIssues(FILE_KEY, "common-xoo:InsufficientLineCoverage");
+ assertThat(issues.size()).isEqualTo(1);
+ }
+
+ @Test
+ public void test_rule_on_skipped_tests() {
+ List<Issue> issues = findIssues(TEST_FILE_KEY, "common-xoo:SkippedUnitTests");
+ assertThat(issues.size()).isEqualTo(1);
+ }
+
+ @Test
+ public void test_rule_on_test_errors() {
+ List<Issue> issues = findIssues(TEST_FILE_KEY, "common-xoo:FailedUnitTests");
+ assertThat(issues.size()).isEqualTo(1);
+ }
+
+ private List<Issue> findIssues(String componentKey, String ruleKey) {
+ return adminWsClient.issues().search(
+ new SearchWsRequest()
+ .setComponents(singletonList(componentKey))
+ .setRules(singletonList(ruleKey)))
+ .getIssuesList();
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/issue/CustomRulesTest.java b/tests/src/test/java/org/sonarqube/tests/issue/CustomRulesTest.java
new file mode 100644
index 00000000000..cae6130cea6
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/issue/CustomRulesTest.java
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.issue;
+
+import java.util.List;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.wsclient.issue.Issue;
+import util.ProjectAnalysis;
+import util.ProjectAnalysisRule;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CustomRulesTest extends AbstractIssueTest {
+
+ @Rule
+ public final ProjectAnalysisRule projectAnalysisRule = ProjectAnalysisRule.from(ORCHESTRATOR);
+
+ private ProjectAnalysis xooSampleAnalysis;
+
+ @Before
+ public void setup() {
+ String profileKey = projectAnalysisRule.registerProfile("/issue/CustomRulesTest/custom.xml");
+ String projectKey = projectAnalysisRule.registerProject("shared/xoo-sample");
+ this.xooSampleAnalysis = projectAnalysisRule.newProjectAnalysis(projectKey)
+ .withQualityProfile(profileKey);
+ }
+
+ @Test
+ public void analyzeProjectWithCustomRules() throws Exception {
+ ORCHESTRATOR.getServer().adminWsClient().post("api/rules/create",
+ "template_key", "xoo:TemplateRule",
+ "custom_key", "MyCustomRule",
+ "markdown_description", "My description",
+ "name", "My custom rule",
+ "severity", "BLOCKER",
+ "params", "line=2");
+
+ xooSampleAnalysis.run();
+
+ List<Issue> issues = searchIssues();
+ assertThat(issues).hasSize(1);
+
+ Issue issue = issues.get(0);
+ assertThat(issue.ruleKey()).isEqualTo("xoo:MyCustomRule");
+ assertThat(issue.line()).isEqualTo(2);
+ // Overriden in quality profile
+ assertThat(issue.severity()).isEqualTo("CRITICAL");
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/issue/IssueActionTest.java b/tests/src/test/java/org/sonarqube/tests/issue/IssueActionTest.java
new file mode 100644
index 00000000000..6ef4ae9573e
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/issue/IssueActionTest.java
@@ -0,0 +1,200 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.issue;
+
+import java.util.List;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.ws.Issues;
+import org.sonarqube.ws.Issues.Issue;
+import org.sonarqube.ws.client.issue.AddCommentRequest;
+import org.sonarqube.ws.client.issue.AssignRequest;
+import org.sonarqube.ws.client.issue.EditCommentRequest;
+import org.sonarqube.ws.client.issue.IssuesService;
+import org.sonarqube.ws.client.issue.SearchWsRequest;
+import org.sonarqube.ws.client.issue.SetSeverityRequest;
+import util.ProjectAnalysis;
+import util.ProjectAnalysisRule;
+import util.issue.IssueRule;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static org.sonarqube.ws.Common.Severity.BLOCKER;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.toDatetime;
+
+public class IssueActionTest extends AbstractIssueTest {
+
+ @Rule
+ public final ProjectAnalysisRule projectAnalysisRule = ProjectAnalysisRule.from(ORCHESTRATOR);
+
+ @ClassRule
+ public static final IssueRule issueRule = IssueRule.from(ORCHESTRATOR);
+
+ private ProjectAnalysis projectAnalysis;
+ private IssuesService issuesService;
+
+ private Issue randomIssue;
+
+ @Before
+ public void setup() {
+ String qualityProfileKey = projectAnalysisRule.registerProfile("/issue/IssueActionTest/xoo-one-issue-per-line-profile.xml");
+ String projectKey = projectAnalysisRule.registerProject("shared/xoo-sample");
+
+ this.projectAnalysis = projectAnalysisRule.newProjectAnalysis(projectKey).withQualityProfile(qualityProfileKey);
+ this.projectAnalysis.run();
+ this.issuesService = newAdminWsClient(ORCHESTRATOR).issues();
+ this.randomIssue = issueRule.getRandomIssue();
+ }
+
+ @Test
+ public void no_comments_by_default() throws Exception {
+ assertThat(randomIssue.getComments().getCommentsList()).isEmpty();
+ }
+
+ @Test
+ public void add_comment() throws Exception {
+ Issues.Comment comment = issuesService.addComment(new AddCommentRequest(randomIssue.getKey(), "this is my *comment*")).getIssue().getComments().getComments(0);
+ assertThat(comment.getKey()).isNotNull();
+ assertThat(comment.getHtmlText()).isEqualTo("this is my <strong>comment</strong>");
+ assertThat(comment.getLogin()).isEqualTo("admin");
+ assertThat(comment.getCreatedAt()).isNotNull();
+
+ // reload issue
+ Issue reloaded = issueRule.getByKey(randomIssue.getKey());
+ assertThat(reloaded.getComments().getCommentsList()).hasSize(1);
+ assertThat(reloaded.getComments().getComments(0).getKey()).isEqualTo(comment.getKey());
+ assertThat(reloaded.getComments().getComments(0).getHtmlText()).isEqualTo("this is my <strong>comment</strong>");
+ assertThat(toDatetime(reloaded.getUpdateDate())).isAfter(toDatetime(randomIssue.getUpdateDate()));
+ }
+
+ /**
+ * SONAR-4450
+ */
+ @Test
+ public void should_reject_blank_comment() throws Exception {
+ try {
+ issuesService.addComment(new AddCommentRequest(randomIssue.getKey(), " "));
+ fail();
+ } catch (org.sonarqube.ws.client.HttpException ex) {
+ assertThat(ex.code()).isEqualTo(400);
+ }
+
+ Issue reloaded = issueRule.getByKey(randomIssue.getKey());
+ assertThat(reloaded.getComments().getCommentsList()).isEmpty();
+ }
+
+ @Test
+ public void edit_comment() throws Exception {
+ Issues.Comment comment = issuesService.addComment(new AddCommentRequest(randomIssue.getKey(), "this is my *comment*")).getIssue().getComments().getComments(0);
+ Issues.Comment editedComment = issuesService.editComment(new EditCommentRequest(comment.getKey(), "new *comment*")).getIssue().getComments().getComments(0);
+ assertThat(editedComment.getHtmlText()).isEqualTo("new <strong>comment</strong>");
+
+ // reload issue
+ Issue reloaded = issueRule.getByKey(randomIssue.getKey());
+ assertThat(reloaded.getComments().getCommentsList()).hasSize(1);
+ assertThat(reloaded.getComments().getComments(0).getHtmlText()).isEqualTo("new <strong>comment</strong>");
+ }
+
+ @Test
+ public void delete_comment() throws Exception {
+ Issues.Comment comment = issuesService.addComment(new AddCommentRequest(randomIssue.getKey(), "this is my *comment*")).getIssue().getComments().getComments(0);
+ Issue issue = issuesService.deleteComment(comment.getKey()).getIssue();
+ assertThat(issue.getComments().getCommentsList()).isEmpty();
+
+ // reload issue
+ Issue reloaded = issueRule.getByKey(randomIssue.getKey());
+ assertThat(reloaded.getComments().getCommentsList()).isEmpty();
+ }
+
+ /**
+ * SONAR-4352
+ */
+ @Test
+ public void change_severity() {
+ String componentKey = "sample";
+
+ // there are no blocker issues
+ assertThat(searchIssuesBySeverities(componentKey, "BLOCKER")).isEmpty();
+
+ // increase the severity of an issue
+ issuesService.setSeverity(new SetSeverityRequest(randomIssue.getKey(), "BLOCKER"));
+
+ assertThat(searchIssuesBySeverities(componentKey, "BLOCKER")).hasSize(1);
+
+ projectAnalysis.run();
+ Issue reloaded = issueRule.getByKey(randomIssue.getKey());
+ assertThat(reloaded.getSeverity()).isEqualTo(BLOCKER);
+ assertThat(reloaded.getStatus()).isEqualTo("OPEN");
+ assertThat(reloaded.hasResolution()).isFalse();
+ assertThat(reloaded.getCreationDate()).isEqualTo(randomIssue.getCreationDate());
+ assertThat(toDatetime(reloaded.getCreationDate())).isBefore(toDatetime(reloaded.getUpdateDate()));
+ }
+
+ /**
+ * SONAR-4287
+ */
+ @Test
+ public void assign() {
+ assertThat(randomIssue.hasAssignee()).isFalse();
+ Issues.SearchWsResponse response = issueRule.search(new SearchWsRequest().setIssues(singletonList(randomIssue.getKey())));
+ assertThat(response.getUsers().getUsersList()).isEmpty();
+
+ issuesService.assign(new AssignRequest(randomIssue.getKey(), "admin"));
+ assertThat(issueRule.search(new SearchWsRequest().setAssignees(singletonList("admin"))).getIssuesList()).hasSize(1);
+
+ projectAnalysis.run();
+ Issue reloaded = issueRule.getByKey(randomIssue.getKey());
+ assertThat(reloaded.getAssignee()).isEqualTo("admin");
+ assertThat(reloaded.getCreationDate()).isEqualTo(randomIssue.getCreationDate());
+
+ response = issueRule.search(new SearchWsRequest().setIssues(singletonList(randomIssue.getKey())).setAdditionalFields(singletonList("users")));
+ assertThat(response.getUsers().getUsersList().stream().filter(user -> "admin".equals(user.getLogin())).findFirst()).isPresent();
+ assertThat(response.getUsers().getUsersList().stream().filter(user -> "Administrator".equals(user.getName())).findFirst()).isPresent();
+
+ // unassign
+ issuesService.assign(new AssignRequest(randomIssue.getKey(), null));
+ reloaded = issueRule.getByKey(randomIssue.getKey());
+ assertThat(reloaded.hasAssignee()).isFalse();
+ assertThat(issueRule.search(new SearchWsRequest().setAssignees(singletonList("admin"))).getIssuesList()).isEmpty();
+ }
+
+ /**
+ * SONAR-4287
+ */
+ @Test
+ public void fail_assign_if_assignee_does_not_exist() {
+ assertThat(randomIssue.hasAssignee()).isFalse();
+ try {
+ issuesService.assign(new AssignRequest(randomIssue.getKey(), "unknown"));
+ fail();
+ } catch (org.sonarqube.ws.client.HttpException ex) {
+ assertThat(ex.code()).isEqualTo(404);
+ }
+ }
+
+ private static List<Issue> searchIssuesBySeverities(String projectKey, String severity) {
+ return issueRule.search(new SearchWsRequest().setProjectKeys(singletonList(projectKey)).setSeverities(singletonList(severity))).getIssuesList();
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/issue/IssueBulkChangeTest.java b/tests/src/test/java/org/sonarqube/tests/issue/IssueBulkChangeTest.java
new file mode 100644
index 00000000000..b62cf49a22c
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/issue/IssueBulkChangeTest.java
@@ -0,0 +1,272 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.issue;
+
+import com.google.common.collect.FluentIterable;
+import java.util.List;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.wsclient.base.HttpException;
+import org.sonarqube.ws.Issues;
+import org.sonarqube.ws.Issues.BulkChangeWsResponse;
+import org.sonarqube.ws.client.issue.BulkChangeRequest;
+import org.sonarqube.ws.client.issue.IssuesService;
+import org.sonarqube.ws.client.issue.SearchWsRequest;
+import util.ProjectAnalysis;
+import util.ProjectAnalysisRule;
+import util.issue.IssueRule;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonarqube.ws.Common.Severity.BLOCKER;
+import static org.sonarqube.ws.Issues.Issue;
+import static util.ItUtils.newAdminWsClient;
+
+/**
+ * SONAR-4421
+ */
+public class IssueBulkChangeTest extends AbstractIssueTest {
+
+ private static final int BULK_EDITED_ISSUE_COUNT = 3;
+ private static final String COMMENT_AS_MARKDOWN = "this is my *comment*";
+ private static final String COMMENT_AS_HTML = "this is my <strong>comment</strong>";
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @ClassRule
+ public static IssueRule issueRule = IssueRule.from(ORCHESTRATOR);
+
+ @Rule
+ public final ProjectAnalysisRule projectAnalysisRule = ProjectAnalysisRule.from(ORCHESTRATOR);
+
+ private IssuesService issuesService;
+ private ProjectAnalysis xooSampleLittleIssuesAnalysis;
+
+ @Before
+ public void setUp() throws Exception {
+ String qualityProfileKey = projectAnalysisRule.registerProfile("/issue/IssueBulkChangeTest/one-issue-per-line-profile.xml");
+ String projectKey = projectAnalysisRule.registerProject("shared/xoo-sample");
+ this.xooSampleLittleIssuesAnalysis = projectAnalysisRule.newProjectAnalysis(projectKey)
+ .withQualityProfile(qualityProfileKey);
+ this.issuesService = newAdminWsClient(ORCHESTRATOR).issues();
+ }
+
+ @Test
+ public void should_change_severity() {
+ xooSampleLittleIssuesAnalysis.run();
+
+ String newSeverity = "BLOCKER";
+ String[] issueKeys = searchIssueKeys(BULK_EDITED_ISSUE_COUNT);
+ BulkChangeWsResponse bulkChange = bulkChangeSeverityOfIssues(issueKeys, newSeverity);
+
+ assertThat(bulkChange.getSuccess()).isEqualTo(BULK_EDITED_ISSUE_COUNT);
+ assertIssueSeverity(issueKeys, newSeverity);
+ }
+
+ @Test
+ public void should_do_transition() {
+ xooSampleLittleIssuesAnalysis.run();
+ String[] issueKeys = searchIssueKeys(BULK_EDITED_ISSUE_COUNT);
+ BulkChangeWsResponse bulkChangeResponse = bulkTransitionStatusOfIssues(issueKeys, "confirm");
+
+ assertThat(bulkChangeResponse.getSuccess()).isEqualTo(BULK_EDITED_ISSUE_COUNT);
+ assertIssueStatus(issueKeys, "CONFIRMED");
+ }
+
+ @Test
+ public void should_assign() {
+ xooSampleLittleIssuesAnalysis.run();
+
+ String[] issueKeys = searchIssueKeys(BULK_EDITED_ISSUE_COUNT);
+ BulkChangeWsResponse bulkChangeResponse = buldChangeAssigneeOfIssues(issueKeys, "admin");
+
+ assertThat(bulkChangeResponse.getSuccess()).isEqualTo(BULK_EDITED_ISSUE_COUNT);
+ for (Issue issue : issueRule.getByKeys(issueKeys)) {
+ assertThat(issue.getAssignee()).isEqualTo("admin");
+ }
+ }
+
+ @Test
+ public void should_setSeverity_add_comment_in_single_WS_call() {
+ xooSampleLittleIssuesAnalysis.run();
+
+ String newSeverity = "BLOCKER";
+ String[] issueKeys = searchIssueKeys(BULK_EDITED_ISSUE_COUNT);
+
+ BulkChangeWsResponse bulkChangeResponse = issuesService.bulkChange(BulkChangeRequest.builder()
+ .setIssues(asList(issueKeys))
+ .setSetSeverity(newSeverity)
+ .setComment(COMMENT_AS_MARKDOWN)
+ .build());
+
+ assertThat(bulkChangeResponse.getSuccess()).isEqualTo(BULK_EDITED_ISSUE_COUNT);
+ for (Issue issue : issueRule.getByKeys(issueKeys)) {
+ assertThat(issue.getComments().getCommentsList()).hasSize(1);
+ assertThat(issue.getComments().getComments(0).getHtmlText()).isEqualTo(COMMENT_AS_HTML);
+ }
+ }
+
+ @Test
+ public void should_apply_bulk_change_on_many_actions() {
+ xooSampleLittleIssuesAnalysis.run();
+ String[] issueKeys = searchIssueKeys(BULK_EDITED_ISSUE_COUNT);
+
+ BulkChangeWsResponse bulkChangeResponse = issuesService.bulkChange(BulkChangeRequest.builder()
+ .setIssues(asList(issueKeys))
+ .setDoTransition("confirm")
+ .setAssign("admin")
+ .setSetSeverity("BLOCKER")
+ .setComment(COMMENT_AS_MARKDOWN)
+ .build());
+
+ assertThat(bulkChangeResponse.getSuccess()).isEqualTo(BULK_EDITED_ISSUE_COUNT);
+ for (Issue issue : issueRule.getByKeys(issueKeys)) {
+ assertThat(issue.getStatus()).isEqualTo("CONFIRMED");
+ assertThat(issue.getAssignee()).isEqualTo("admin");
+ assertThat(issue.getSeverity()).isEqualTo(BLOCKER);
+ assertThat(issue.getComments().getCommentsList()).hasSize(1);
+ assertThat(issue.getComments().getComments(0).getHtmlText()).isEqualTo(COMMENT_AS_HTML);
+ }
+ }
+
+ @Test
+ public void should_not_apply_bulk_change_if_not_logged() {
+ xooSampleLittleIssuesAnalysis.run();
+
+ String newSeverity = "BLOCKER";
+ String[] issueKeys = searchIssueKeys(BULK_EDITED_ISSUE_COUNT);
+
+ try {
+ issuesService.bulkChange(createBulkChangeSeverityOfIssuesQuery(issueKeys, newSeverity));
+ } catch (Exception e) {
+ assertHttpException(e, 401);
+ }
+ }
+
+ @Test
+ public void should_not_apply_bulk_change_if_no_change_to_do() {
+ xooSampleLittleIssuesAnalysis.run();
+
+ String newSeverity = "BLOCKER";
+ String[] issueKeys = searchIssueKeys(BULK_EDITED_ISSUE_COUNT);
+
+ // Apply the bulk change a first time
+ BulkChangeWsResponse bulkChangeResponse = bulkChangeSeverityOfIssues(issueKeys, newSeverity);
+ assertThat(bulkChangeResponse.getSuccess()).isEqualTo(BULK_EDITED_ISSUE_COUNT);
+
+ // Re apply the same bulk change -> no issue should be changed
+ bulkChangeResponse = bulkChangeSeverityOfIssues(issueKeys, newSeverity);
+ assertThat(bulkChangeResponse.getSuccess()).isEqualTo(0);
+ assertThat(bulkChangeResponse.getIgnored()).isEqualTo(BULK_EDITED_ISSUE_COUNT);
+ }
+
+ @Test
+ public void should_not_apply_bulk_change_if_no_action() {
+ xooSampleLittleIssuesAnalysis.run();
+
+ try {
+ int limit = BULK_EDITED_ISSUE_COUNT;
+ String[] issueKeys = searchIssueKeys(limit);
+ issuesService.bulkChange(BulkChangeRequest.builder().setIssues(asList(issueKeys)).build());
+ } catch (Exception e) {
+ assertHttpException(e, 400);
+ }
+ }
+
+ @Test
+ public void should_add_comment_only_on_issues_that_will_be_changed() {
+ xooSampleLittleIssuesAnalysis.run();
+ int nbIssues = BULK_EDITED_ISSUE_COUNT;
+ String[] issueKeys = searchIssueKeys(nbIssues);
+
+ // Confirm an issue
+ adminIssueClient().doTransition(searchIssues().iterator().next().key(), "confirm");
+
+ // Apply a bulk change on unconfirm transition
+ BulkChangeWsResponse bulkChangeResponse = issuesService.bulkChange(BulkChangeRequest.builder().setIssues(asList(issueKeys))
+ .setDoTransition("unconfirm")
+ .setComment("this is my comment")
+ .build());
+ assertThat(bulkChangeResponse.getSuccess()).isEqualTo(1);
+
+ int nbIssuesWithComment = 0;
+ for (Issues.Issue issue : issueRule.getByKeys(issueKeys)) {
+ if (!issue.getComments().getCommentsList().isEmpty()) {
+ nbIssuesWithComment++;
+ }
+ }
+ // Only one issue should have the comment
+ assertThat(nbIssuesWithComment).isEqualTo(1);
+ }
+
+ private static void assertIssueSeverity(String[] issueKeys, String expectedSeverity) {
+ for (Issues.Issue issue : issueRule.getByKeys(issueKeys)) {
+ assertThat(issue.getSeverity().name()).isEqualTo(expectedSeverity);
+ }
+ }
+
+ private static void assertIssueStatus(String[] issueKeys, String expectedStatus) {
+ for (Issues.Issue issue : issueRule.getByKeys(issueKeys)) {
+ assertThat(issue.getStatus()).isEqualTo(expectedStatus);
+ }
+ }
+
+ private static void assertHttpException(Exception e, int expectedCode) {
+ assertThat(e).isInstanceOf(HttpException.class);
+ assertThat(((HttpException) e).status()).isEqualTo(expectedCode);
+ }
+
+ private BulkChangeWsResponse bulkChangeSeverityOfIssues(String[] issueKeys, String newSeverity) {
+ BulkChangeRequest bulkChangeQuery = createBulkChangeSeverityOfIssuesQuery(issueKeys, newSeverity);
+ return issuesService.bulkChange(bulkChangeQuery);
+ }
+
+ private static BulkChangeRequest createBulkChangeSeverityOfIssuesQuery(String[] issueKeys, String newSeverity) {
+ BulkChangeRequest.Builder request = BulkChangeRequest.builder().setSetSeverity(newSeverity);
+ if (issueKeys != null && issueKeys.length > 0) {
+ request.setIssues(asList(issueKeys));
+ }
+ return request.build();
+ }
+
+ private BulkChangeWsResponse bulkTransitionStatusOfIssues(String[] issueKeys, String newSeverity) {
+ return issuesService.bulkChange(BulkChangeRequest.builder().setIssues(asList(issueKeys)).setDoTransition(newSeverity).build());
+ }
+
+ private BulkChangeWsResponse buldChangeAssigneeOfIssues(String[] issueKeys, String newAssignee) {
+ return issuesService.bulkChange(BulkChangeRequest.builder().setIssues(asList(issueKeys)).setAssign(newAssignee).build());
+ }
+
+ private static String[] searchIssueKeys(int limit) {
+ return getIssueKeys(issueRule.search(new SearchWsRequest()).getIssuesList(), limit);
+ }
+
+ private static String[] getIssueKeys(List<Issues.Issue> issues, int nbIssues) {
+ return FluentIterable.from(issues)
+ .limit(nbIssues)
+ .transform(Issue::getKey)
+ .toArray(String.class);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/issue/IssueChangelogTest.java b/tests/src/test/java/org/sonarqube/tests/issue/IssueChangelogTest.java
new file mode 100644
index 00000000000..d96536d1bd1
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/issue/IssueChangelogTest.java
@@ -0,0 +1,124 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.issue;
+
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.wsclient.issue.Issue;
+import org.sonarqube.ws.Issues;
+import org.sonarqube.ws.Issues.ChangelogWsResponse.Changelog;
+import org.sonarqube.ws.client.WsClient;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.runProjectAnalysis;
+
+public class IssueChangelogTest extends AbstractIssueTest {
+
+ private static WsClient adminClient;
+
+ @Before
+ public void prepareData() {
+ ORCHESTRATOR.resetData();
+ ItUtils.restoreProfile(ORCHESTRATOR, getClass().getResource("/issue/IssueChangelogTest/one-issue-per-line-profile.xml"));
+ ORCHESTRATOR.getServer().provisionProject("sample", "sample");
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");
+ adminClient = newAdminWsClient(ORCHESTRATOR);
+ }
+
+ @Test
+ public void update_changelog_when_assigning_issue_by_user() throws Exception {
+ runProjectAnalysis(ORCHESTRATOR, "shared/xoo-sample");
+ Issue issue = searchRandomIssue();
+ assertIssueHasNoChange(issue.key());
+
+ adminIssueClient().assign(issue.key(), "admin");
+
+ List<Changelog> changes = changelog(issue.key()).getChangelogList();
+ assertThat(changes).hasSize(1);
+ Changelog change = changes.get(0);
+ assertThat(change.getUser()).isEqualTo("admin");
+ assertThat(change.getCreationDate()).isNotNull();
+ assertThat(change.getDiffsList())
+ .extracting(Changelog.Diff::getKey, Changelog.Diff::hasOldValue, Changelog.Diff::getNewValue)
+ .containsOnly(tuple("assignee", false, "Administrator"));
+ }
+
+ @Test
+ public void update_changelog_when_reopening_unresolved_issue_by_scan() throws Exception {
+ runProjectAnalysis(ORCHESTRATOR, "shared/xoo-sample");
+ Issue issue = searchRandomIssue();
+ assertIssueHasNoChange(issue.key());
+
+ // re analyse the project after resolving an issue in order to reopen it
+ adminIssueClient().doTransition(issue.key(), "resolve");
+ runProjectAnalysis(ORCHESTRATOR, "shared/xoo-sample");
+
+ List<Changelog> changes = changelog(issue.key()).getChangelogList();
+ assertThat(changes).hasSize(2);
+
+ // Change done by the user (first change is be the oldest one)
+ Changelog change1 = changes.get(0);
+ assertThat(change1.getUser()).isEqualTo("admin");
+ assertThat(change1.getCreationDate()).isNotNull();
+ assertThat(change1.getDiffsList())
+ .extracting(Changelog.Diff::getKey, Changelog.Diff::getOldValue, Changelog.Diff::getNewValue)
+ .containsOnly(tuple("resolution", "", "FIXED"), tuple("status", "OPEN", "RESOLVED"));
+
+ // Change done by scan
+ Changelog change2 = changes.get(1);
+ assertThat(change2.hasUser()).isFalse();
+ assertThat(change2.getCreationDate()).isNotNull();
+ assertThat(change2.getDiffsList())
+ .extracting(Changelog.Diff::getKey, Changelog.Diff::getOldValue, Changelog.Diff::getNewValue)
+ .containsOnly(tuple("resolution", "", ""), tuple("status", "RESOLVED", "REOPENED"));
+ }
+
+ @Test
+ public void display_file_name_in_changelog_during_file_move() {
+ // version 1
+ runProjectAnalysis(ORCHESTRATOR, "issue/xoo-tracking-v1");
+
+ // version 2
+ runProjectAnalysis(ORCHESTRATOR, "issue/xoo-tracking-v3");
+
+ Issue issue = searchRandomIssue();
+ List<Changelog> changes = changelog(issue.key()).getChangelogList();
+ assertThat(changes).hasSize(1);
+ Changelog change = changes.get(0);
+ assertThat(change.hasUser()).isFalse();
+ assertThat(change.getCreationDate()).isNotNull();
+ assertThat(change.getDiffsList())
+ .extracting(Changelog.Diff::getKey, Changelog.Diff::getOldValue, Changelog.Diff::getNewValue)
+ .containsOnly(tuple("file", "src/main/xoo/sample/Sample.xoo", "src/main/xoo/sample/Sample2.xoo"));
+ }
+
+ private void assertIssueHasNoChange(String issueKey) {
+ assertThat(changelog(issueKey).getChangelogList()).isEmpty();
+ }
+
+ private static Issues.ChangelogWsResponse changelog(String issueKey) {
+ return adminClient.issues().changelog(issueKey);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/issue/IssueCreationDateTest.java b/tests/src/test/java/org/sonarqube/tests/issue/IssueCreationDateTest.java
new file mode 100644
index 00000000000..47ce6e5ac9f
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/issue/IssueCreationDateTest.java
@@ -0,0 +1,378 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.issue;
+
+import com.sonar.orchestrator.build.SonarScanner;
+import com.sonar.orchestrator.container.Server;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.wsclient.issue.Issue;
+import org.sonar.wsclient.issue.IssueQuery;
+import org.sonarqube.ws.ProjectAnalyses;
+import org.sonarqube.ws.client.projectanalysis.SearchRequest;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.projectDir;
+
+/**
+ * @see <a href="https://jira.sonarsource.com/browse/MMF-567">MMF-567</a>
+ */
+public class IssueCreationDateTest extends AbstractIssueTest {
+
+ private static final String ISSUE_STATUS_OPEN = "OPEN";
+
+ private static final String LANGUAGE_XOO = "xoo";
+
+ private static final String DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ";
+ private static final String DATE_FORMAT = "yyyy-MM-dd";
+
+ private static final String SAMPLE_PROJECT_KEY = "creation-date-sample";
+ private static final String SAMPLE_PROJECT_NAME = "Creation date sample";
+ private static final String SAMPLE_QUALITY_PROFILE_NAME = "creation-date-quality-profile";
+ private static final String SAMPLE_EXPLICIT_DATE_1 = todayMinusDays(2);
+ private static final String SAMPLE_EXPLICIT_DATE_2 = todayMinusDays(1);
+
+ private Server server = ORCHESTRATOR.getServer();
+
+ @Before
+ public void resetData() {
+ ORCHESTRATOR.resetData();
+ server.provisionProject(SAMPLE_PROJECT_KEY, SAMPLE_PROJECT_NAME);
+ }
+
+ @Test
+ public void should_use_scm_date_for_new_issues_if_scm_is_available() {
+ analysis(QProfile.ONE_RULE, SourceCode.INITIAL, ScannerFeature.SCM);
+
+ assertNumberOfIssues(3);
+ assertIssueCreationDate(Component.OnlyInInitial, IssueCreationDate.OnlyInInitial_R1);
+ assertIssueCreationDate(Component.ForeverAndModified, IssueCreationDate.ForeverAndModified_R1);
+ assertIssueCreationDate(Component.ForeverAndUnmodified, IssueCreationDate.ForeverAndUnmodified_R1);
+ }
+
+ @Test
+ public void should_use_analysis_date_for_new_issues_if_scm_is_not_available() {
+ analysis(QProfile.ONE_RULE, SourceCode.INITIAL);
+
+ assertNumberOfIssues(3);
+ assertIssueCreationDates(COMPONENTS_OF_SOURCE_INITIAL, IssueCreationDate.FIRST_ANALYSIS);
+ }
+
+ @Test
+ public void use_explicit_project_date_if_scm_is_not_available() {
+ analysis(QProfile.ONE_RULE, SourceCode.INITIAL, ScannerFeature.EXPLICIT_DATE_1);
+
+ assertNumberOfIssues(3);
+ assertIssueCreationDates(COMPONENTS_OF_SOURCE_INITIAL, IssueCreationDate.EXPLICIT_DATE_1);
+ }
+
+ @Test
+ public void use_scm_date_even_if_explicit_project_date_is_set() {
+ analysis(QProfile.ONE_RULE, SourceCode.INITIAL, ScannerFeature.SCM, ScannerFeature.EXPLICIT_DATE_1);
+
+ assertNumberOfIssues(3);
+ assertIssueCreationDate(Component.OnlyInInitial, IssueCreationDate.OnlyInInitial_R1);
+ assertIssueCreationDate(Component.ForeverAndModified, IssueCreationDate.ForeverAndModified_R1);
+ assertIssueCreationDate(Component.ForeverAndUnmodified, IssueCreationDate.ForeverAndUnmodified_R1);
+ }
+
+ @Test
+ public void no_rules_no_issues_if_scm_is_available() {
+ analysis(QProfile.NO_RULES, SourceCode.INITIAL, ScannerFeature.SCM);
+
+ assertNoIssue();
+ }
+
+ @Test
+ public void no_rules_no_issues_if_scm_is_not_available() {
+ analysis(QProfile.NO_RULES, SourceCode.INITIAL);
+
+ assertNoIssue();
+ }
+
+ @Test
+ public void use_scm_date_for_issues_raised_by_new_rules_if_scm_is_newly_available() {
+ analysis(QProfile.NO_RULES, SourceCode.INITIAL);
+ analysis(QProfile.ONE_RULE, SourceCode.CHANGED, ScannerFeature.SCM);
+
+ assertNumberOfIssues(3);
+ assertIssueCreationDate(Component.ForeverAndModified, IssueCreationDate.ForeverAndModified_R2);
+ assertIssueCreationDate(Component.ForeverAndUnmodified, IssueCreationDate.ForeverAndUnmodified_R1);
+ assertIssueCreationDate(Component.OnlyInChanged, IssueCreationDate.OnlyInChanged_R1);
+ }
+
+ @Test
+ public void use_scm_date_for_issues_raised_by_new_rules_if_scm_is_available_and_ever_has_been_available() {
+ analysis(QProfile.NO_RULES, SourceCode.INITIAL, ScannerFeature.SCM);
+ analysis(QProfile.ONE_RULE, SourceCode.CHANGED, ScannerFeature.SCM);
+
+ assertNumberOfIssues(3);
+ assertIssueCreationDate(Component.ForeverAndModified, IssueCreationDate.ForeverAndModified_R2);
+ assertIssueCreationDate(Component.ForeverAndUnmodified, IssueCreationDate.ForeverAndUnmodified_R1);
+ assertIssueCreationDate(Component.OnlyInChanged, IssueCreationDate.OnlyInChanged_R1);
+ }
+
+ @Test
+ public void use_analysis_date_for_issues_raised_by_new_rules_if_scm_is_not_available() {
+ analysis(QProfile.NO_RULES, SourceCode.INITIAL);
+ analysis(QProfile.ONE_RULE, SourceCode.CHANGED);
+
+ assertNumberOfIssues(3);
+ Stream.of(COMPONENTS_OF_SOURCE_CHANGED)
+ .forEach(component -> {
+ assertIssueCreationDate(component, IssueCreationDate.LATEST_ANALYSIS);
+ });
+ }
+
+ @Test
+ public void keep_the_date_of_an_existing_issue_even_if_the_blame_information_changes() {
+ analysis(QProfile.ONE_RULE, SourceCode.INITIAL, ScannerFeature.SCM);
+ analysis(QProfile.ONE_RULE, SourceCode.CHANGED, ScannerFeature.SCM);
+
+ assertNumberOfIssues(3);
+ assertIssueCreationDate(Component.ForeverAndModified, IssueCreationDate.ForeverAndModified_R1);
+ assertIssueCreationDate(Component.ForeverAndUnmodified, IssueCreationDate.ForeverAndUnmodified_R1);
+
+ // this file is new to the second analysis
+ assertIssueCreationDate(Component.OnlyInChanged, IssueCreationDate.LATEST_ANALYSIS);
+ }
+
+ @Test
+ public void ignore_explicit_date_for_issues_related_to_new_rules_if_scm_is_available() {
+ analysis(QProfile.NO_RULES, SourceCode.INITIAL, ScannerFeature.SCM, ScannerFeature.EXPLICIT_DATE_1);
+ analysis(QProfile.ONE_RULE, SourceCode.CHANGED, ScannerFeature.SCM, ScannerFeature.EXPLICIT_DATE_2);
+
+ assertNumberOfIssues(3);
+ assertIssueCreationDate(Component.ForeverAndModified, IssueCreationDate.ForeverAndModified_R2);
+ assertIssueCreationDate(Component.ForeverAndUnmodified, IssueCreationDate.ForeverAndUnmodified_R1);
+ assertIssueCreationDate(Component.OnlyInChanged, IssueCreationDate.OnlyInChanged_R1);
+ }
+
+ @Test
+ public void use_explicit_date_for_issues_related_to_new_rules_if_scm_is_not_available() {
+ analysis(QProfile.NO_RULES, SourceCode.INITIAL, ScannerFeature.EXPLICIT_DATE_1);
+ analysis(QProfile.ONE_RULE, SourceCode.CHANGED, ScannerFeature.EXPLICIT_DATE_2);
+
+ assertNumberOfIssues(3);
+ assertIssueCreationDates(COMPONENTS_OF_SOURCE_CHANGED, IssueCreationDate.EXPLICIT_DATE_2);
+ }
+
+ private void analysis(QProfile qProfile, SourceCode sourceCode, ScannerFeature... scm) {
+ ItUtils.restoreProfile(ORCHESTRATOR, getClass().getResource(qProfile.path));
+ server.associateProjectToQualityProfile(SAMPLE_PROJECT_KEY, LANGUAGE_XOO, SAMPLE_QUALITY_PROFILE_NAME);
+
+ SonarScanner scanner = SonarScanner.create(projectDir(sourceCode.path));
+ Arrays.stream(scm).forEach(s -> s.configure(scanner));
+ ORCHESTRATOR.executeBuild(scanner);
+ }
+
+ private static void assertNoIssue() {
+ assertNumberOfIssues(0);
+ }
+
+ private static void assertNumberOfIssues(int number) {
+ assertThat(getIssues(issueQuery())).hasSize(number);
+ }
+
+ private static void assertIssueCreationDate(Component component, IssueCreationDate expectedDate) {
+ assertIssueCreationDates(new Component[] {component}, expectedDate);
+ }
+
+ private static void assertIssueCreationDates(Component[] components, IssueCreationDate expectedDate) {
+ String[] keys = Arrays.stream(components).map(Component::getKey).toArray(String[]::new);
+ List<Issue> issues = getIssues(issueQuery().components(keys));
+ Date[] dates = Arrays.stream(components).map(x -> expectedDate.getDate()).toArray(Date[]::new);
+
+ assertThat(issues)
+ .extracting(Issue::creationDate)
+ .containsExactly(dates);
+ }
+
+ private static List<Issue> getIssues(IssueQuery query) {
+ return issueClient().find(query).list();
+ }
+
+ private static IssueQuery issueQuery() {
+ return IssueQuery.create().statuses(ISSUE_STATUS_OPEN);
+ }
+
+ private static Date dateTimeParse(String expectedDate) {
+ try {
+ return new SimpleDateFormat(DATETIME_FORMAT).parse(expectedDate);
+ } catch (ParseException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static Date dateParse(String expectedDate) {
+ try {
+ return new SimpleDateFormat(DATE_FORMAT).parse(expectedDate);
+ } catch (ParseException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static String todayMinusDays(int numberOfDays) {
+ return DateTimeFormatter.ofPattern(DATE_FORMAT).format(LocalDate.now().atStartOfDay().minusDays(numberOfDays));
+ }
+
+ private enum SourceCode {
+ INITIAL("issue/creationDateSampleInitial"),
+ CHANGED("issue/creationDateSampleChanged"),
+ ;
+
+ private final String path;
+
+ private SourceCode(String path) {
+ this.path = path;
+ }
+ }
+
+ private enum Component {
+ OnlyInInitial("creation-date-sample:src/main/xoo/sample/OnlyInInitial.xoo"),
+ ForeverAndModified("creation-date-sample:src/main/xoo/sample/ForeverAndModified.xoo"),
+ ForeverAndUnmodified("creation-date-sample:src/main/xoo/sample/ForeverAndUnmodified.xoo"),
+ OnlyInChanged("creation-date-sample:src/main/xoo/sample/OnlyInChanged.xoo"),
+ ;
+ private final String key;
+
+ private Component(String key) {
+ this.key = key;
+ }
+
+ public String getKey() {
+ return key;
+ }
+ }
+
+ private static final Component[] COMPONENTS_OF_SOURCE_INITIAL = {Component.OnlyInInitial, Component.ForeverAndModified, Component.ForeverAndUnmodified};
+ private static final Component[] COMPONENTS_OF_SOURCE_CHANGED = {Component.ForeverAndModified, Component.ForeverAndUnmodified, Component.OnlyInChanged};
+
+ private enum QProfile {
+ ONE_RULE("/issue/IssueCreationDateTest/one-rule.xml"),
+ NO_RULES("/issue/IssueCreationDateTest/no-rules.xml"),
+ ;
+
+ private final String path;
+
+ private QProfile(String path) {
+ this.path = path;
+ }
+ }
+
+ private enum ScannerFeature {
+ SCM {
+ @Override
+ void configure(SonarScanner scanner) {
+ scanner
+ .setProperty("sonar.scm.provider", "xoo")
+ .setProperty("sonar.scm.disabled", "false");
+ }
+ },
+ EXPLICIT_DATE_1 {
+ @Override
+ void configure(SonarScanner scanner) {
+ scanner
+ .setProperty("sonar.projectDate", SAMPLE_EXPLICIT_DATE_1);
+ }
+ },
+ EXPLICIT_DATE_2 {
+ @Override
+ void configure(SonarScanner scanner) {
+ scanner
+ .setProperty("sonar.projectDate", SAMPLE_EXPLICIT_DATE_2);
+ }
+ },
+ ;
+
+ void configure(SonarScanner scanner) {
+ }
+ }
+
+ private enum IssueCreationDate {
+ OnlyInInitial_R1(dateTimeParse("2001-01-01T00:00:00+0000")),
+ ForeverAndUnmodified_R1(dateTimeParse("2002-01-01T00:00:00+0000")),
+ ForeverAndModified_R1(dateTimeParse("2003-01-01T00:00:00+0000")),
+ ForeverAndModified_R2(dateTimeParse("2004-01-01T00:00:00+0000")),
+ OnlyInChanged_R1(dateTimeParse("2005-01-01T00:00:00+0000")),
+ EXPLICIT_DATE_1(dateParse(SAMPLE_EXPLICIT_DATE_1)),
+ EXPLICIT_DATE_2(dateParse(SAMPLE_EXPLICIT_DATE_2)),
+ FIRST_ANALYSIS {
+ @Override
+ Date getDate() {
+ return getAnalysisDate(l -> {
+ if (l.isEmpty()) {
+ return Optional.empty();
+ }
+ return Optional.of(l.get(l.size() - 1));
+ });
+ }
+ },
+ LATEST_ANALYSIS {
+ @Override
+ Date getDate() {
+ return getAnalysisDate(l -> {
+ if (l.size() > 0) {
+ return Optional.of(l.get(0));
+ }
+ return Optional.empty();
+ });
+ }
+ },
+ ;
+
+ private final Date date;
+
+ private IssueCreationDate() {
+ this.date = null;
+ }
+
+ private IssueCreationDate(Date date) {
+ this.date = date;
+ }
+
+ Date getDate() {
+ return date;
+ }
+
+ private static Date getAnalysisDate(Function<List<ProjectAnalyses.Analysis>, Optional<ProjectAnalyses.Analysis>> chooseItem) {
+ return Optional.of(
+ ItUtils.newWsClient(ORCHESTRATOR)
+ .projectAnalysis()
+ .search(SearchRequest.builder().setProject(SAMPLE_PROJECT_KEY).build())
+ .getAnalysesList())
+ .flatMap(chooseItem)
+ .map(ProjectAnalyses.Analysis::getDate)
+ .map(IssueCreationDateTest::dateTimeParse)
+ .orElseThrow(() -> new IllegalStateException("There is no analysis"));
+ }
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/issue/IssueCreationTest.java b/tests/src/test/java/org/sonarqube/tests/issue/IssueCreationTest.java
new file mode 100644
index 00000000000..b6afaf17304
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/issue/IssueCreationTest.java
@@ -0,0 +1,78 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.issue;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.wsclient.issue.Issue;
+import org.sonar.wsclient.issue.IssueQuery;
+import org.sonar.wsclient.issue.Issues;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.runProjectAnalysis;
+
+public class IssueCreationTest extends AbstractIssueTest {
+
+ private static final String SAMPLE_PROJECT_KEY = "sample";
+
+ @Before
+ public void resetData() {
+ ORCHESTRATOR.resetData();
+ }
+
+ /**
+ * See SONAR-4785
+ */
+ @Test
+ public void use_rule_name_if_issue_has_no_message() {
+ ORCHESTRATOR.getServer().provisionProject(SAMPLE_PROJECT_KEY, SAMPLE_PROJECT_KEY);
+ ItUtils.restoreProfile(ORCHESTRATOR, getClass().getResource("/issue/IssueCreationTest/with-custom-message.xml"));
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile(SAMPLE_PROJECT_KEY, "xoo", "with-custom-message");
+
+ // First analysis, the issue is generated with a message
+ runProjectAnalysis(ORCHESTRATOR, "shared/xoo-sample", "sonar.customMessage.message", "a message");
+ Issue issue = issueClient().find(IssueQuery.create()).list().get(0);
+ assertThat(issue.message()).isEqualTo("a message");
+
+ // Second analysis, the issue is generated without any message, the name of the rule is used
+ runProjectAnalysis(ORCHESTRATOR, "shared/xoo-sample");
+ issue = issueClient().find(IssueQuery.create()).list().get(0);
+ assertThat(issue.message()).isEqualTo("Issue With Custom Message");
+ }
+
+ @Test
+ public void plugin_can_override_profile_severity() throws Exception {
+ ORCHESTRATOR.getServer().provisionProject(SAMPLE_PROJECT_KEY, SAMPLE_PROJECT_KEY);
+
+ // The rule "OneBlockerIssuePerFile" is enabled with severity "INFO"
+ ItUtils.restoreProfile(ORCHESTRATOR, getClass().getResource("/issue/IssueCreationTest/override-profile-severity.xml"));
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile(SAMPLE_PROJECT_KEY, "xoo", "override-profile-severity");
+
+ // But it's hardcoded "blocker" when plugin generates the issue
+ runProjectAnalysis(ORCHESTRATOR, "shared/xoo-sample");
+
+ Issues issues = search(IssueQuery.create().rules("xoo:OneBlockerIssuePerFile"));
+ assertThat(issues.size()).isGreaterThan(0);
+ for (Issue issue : issues.list()) {
+ assertThat(issue.severity()).isEqualTo("BLOCKER");
+ }
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/issue/IssueFilterExtensionTest.java b/tests/src/test/java/org/sonarqube/tests/issue/IssueFilterExtensionTest.java
new file mode 100644
index 00000000000..7dc46462f63
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/issue/IssueFilterExtensionTest.java
@@ -0,0 +1,119 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.issue;
+
+import java.util.List;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.wsclient.issue.Issue;
+import org.sonar.wsclient.issue.IssueQuery;
+import util.ProjectAnalysis;
+import util.ProjectAnalysisRule;
+import util.issue.IssueRule;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.getMeasureAsDouble;
+
+/**
+ * Tests the extension point IssueFilter
+ */
+public class IssueFilterExtensionTest extends AbstractIssueTest {
+
+ @Rule
+ public final ProjectAnalysisRule projectAnalysisRule = ProjectAnalysisRule.from(ORCHESTRATOR);
+
+ @ClassRule
+ public static final IssueRule issueRule = IssueRule.from(ORCHESTRATOR);
+
+ private final String manyRuleProfileKey = projectAnalysisRule.registerProfile("/issue/IssueFilterExtensionTest/xoo-with-many-rules.xml");
+ private final String xooMultiModuleProjectKey = projectAnalysisRule.registerProject("shared/xoo-multi-modules-sample");
+ private final ProjectAnalysis analysis = projectAnalysisRule.newProjectAnalysis(xooMultiModuleProjectKey)
+ .withQualityProfile(manyRuleProfileKey);
+
+ @Test
+ public void should_filter_files() throws Exception {
+ analysis.withProperties("sonar.exclusions", "**/HelloA1.xoo").run();
+
+ List<Issue> issues = searchIssues();
+ assertThat(issues).isNotEmpty();
+ for (Issue issue : issues) {
+ // verify exclusion to avoid false positive
+ assertThat(issue.componentKey()).doesNotContain("HelloA1");
+ }
+
+ assertThat(getMeasureAsDouble(ORCHESTRATOR, xooMultiModuleProjectKey, "violations").intValue()).isEqualTo(issues.size());
+ }
+
+ @Test
+ public void should_filter_issues() {
+ // first analysis without issue-filter
+ analysis.run();
+
+ // Issue filter removes issues on lines < 5
+ // Deprecated violation filter removes issues detected by PMD
+ List<Issue> unresolvedIssues = searchResolvedIssues(xooMultiModuleProjectKey);
+ int issuesBeforeLine5 = countIssuesBeforeLine5(unresolvedIssues);
+ int pmdIssues = countModuleIssues(unresolvedIssues);
+ assertThat(issuesBeforeLine5).isGreaterThan(0);
+ assertThat(pmdIssues).isGreaterThan(0);
+
+ // Enable issue filters
+ analysis.withProperties("enableIssueFilters", "true").run();
+
+ unresolvedIssues = searchResolvedIssues(xooMultiModuleProjectKey);
+ List<Issue> resolvedIssues = searchUnresolvedIssues(xooMultiModuleProjectKey);
+ assertThat(countIssuesBeforeLine5(unresolvedIssues)).isZero();
+ assertThat(countModuleIssues(unresolvedIssues)).isZero();
+ assertThat(countModuleIssues(resolvedIssues)).isGreaterThan(0);
+ for (Issue issue : resolvedIssues) {
+ // SONAR-6364 no line number on closed issues
+ assertThat(issue.line()).isNull();
+ }
+ }
+
+ private static List<Issue> searchUnresolvedIssues(String projectKey) {
+ return searchIssues(IssueQuery.create().componentRoots(projectKey).resolved(true));
+ }
+
+ private static List<Issue> searchResolvedIssues(String projectKey) {
+ return searchIssues(IssueQuery.create().componentRoots(projectKey).resolved(false));
+ }
+
+ private static int countModuleIssues(List<Issue> issues) {
+ int count = 0;
+ for (Issue issue : issues) {
+ if (issue.ruleKey().equals("xoo:OneIssuePerModule")) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ private static int countIssuesBeforeLine5(List<Issue> issues) {
+ int count = 0;
+ for (Issue issue : issues) {
+ if (issue.line() != null && issue.line() < 5) {
+ count++;
+ }
+ }
+ return count;
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/issue/IssueFilterOnCommonRulesTest.java b/tests/src/test/java/org/sonarqube/tests/issue/IssueFilterOnCommonRulesTest.java
new file mode 100644
index 00000000000..8012aec2ef7
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/issue/IssueFilterOnCommonRulesTest.java
@@ -0,0 +1,126 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.issue;
+
+import java.util.List;
+import org.apache.commons.lang.ArrayUtils;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonarqube.ws.Issues;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.issue.SearchWsRequest;
+import util.ItUtils;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.runProjectAnalysis;
+import static util.ItUtils.setServerProperties;
+
+public class IssueFilterOnCommonRulesTest extends AbstractIssueTest {
+
+ private static WsClient adminWsClient;
+
+ private static final String PROJECT_KEY = "common-rules-project";
+ private static final String PROJECT_DIR = "issue/common-rules";
+
+ private static final String FILE_KEY = "common-rules-project:src/Sample.xoo";
+
+ @Before
+ public void resetData() {
+ ORCHESTRATOR.resetData();
+ ItUtils.restoreProfile(ORCHESTRATOR, getClass().getResource("/issue/IssueFilterOnCommonRulesTest/xoo-common-rules-profile.xml"));
+ ORCHESTRATOR.getServer().provisionProject(PROJECT_KEY, "Sample");
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile(PROJECT_KEY, "xoo", "xoo-common-rules");
+
+ adminWsClient = newAdminWsClient(ORCHESTRATOR);
+ }
+
+ @Test
+ public void ignore_all() {
+ executeAnalysis("sonar.issue.ignore.multicriteria", "1",
+ "sonar.issue.ignore.multicriteria.1.ruleKey", "*",
+ "sonar.issue.ignore.multicriteria.1.resourceKey", "**/*.xoo");
+
+ assertThat(findAllIssues()).hasSize(0);
+ }
+
+ @Test
+ public void ignore_some_rule_and_file() {
+ executeAnalysis(
+ "sonar.issue.ignore.multicriteria", "1,2",
+ "sonar.issue.ignore.multicriteria.1.ruleKey", "common-xoo:DuplicatedBlocks",
+ "sonar.issue.ignore.multicriteria.1.resourceKey", "**/Sample.xoo",
+ "sonar.issue.ignore.multicriteria.2.ruleKey", "common-xoo:SkippedUnitTests",
+ "sonar.issue.ignore.multicriteria.2.resourceKey", "**/SampleTest.xoo");
+
+ assertThat(findAllIssues()).hasSize(4);
+ assertThat(findIssuesByRuleKey("common-xoo:DuplicatedBlocks")).isEmpty();
+ assertThat(findIssuesByRuleKey("common-xoo:SkippedUnitTests")).isEmpty();
+ }
+
+ @Test
+ public void enforce_one_file() {
+ executeAnalysis(
+ "sonar.issue.enforce.multicriteria", "1",
+ "sonar.issue.enforce.multicriteria.1.ruleKey", "*",
+ // Only issues on this file will be accepted
+ "sonar.issue.enforce.multicriteria.1.resourceKey", "**/Sample.xoo");
+
+ assertThat(findAllIssues()).hasSize(4);
+ }
+
+ @Test
+ public void enforce_on_rules() {
+ executeAnalysis(
+ "sonar.issue.enforce.multicriteria", "1,2",
+ "sonar.issue.enforce.multicriteria.1.ruleKey", "common-xoo:DuplicatedBlocks",
+ "sonar.issue.enforce.multicriteria.1.resourceKey", "**/Sample.xoo",
+ // This rule should only be applied on a file that do not exist => no issue for this rule
+ "sonar.issue.enforce.multicriteria.2.ruleKey", "common-xoo:InsufficientCommentDensity",
+ "sonar.issue.enforce.multicriteria.2.resourceKey", "**/OtherFile.xoo");
+
+ assertThat(findAllIssues()).hasSize(5);
+ assertThat(findIssuesByRuleKey("common-xoo:DuplicatedBlocks")).hasSize(1);
+ assertThat(findIssuesByRuleKey("common-xoo:InsufficientCommentDensity")).isEmpty();
+ }
+
+ private void executeAnalysis(String... serverProperties) {
+ String[] cpdProperties = new String[] {
+ "sonar.cpd.xoo.minimumTokens", "2",
+ "sonar.cpd.xoo.minimumLines", "2"
+ };
+ setServerProperties(ORCHESTRATOR, PROJECT_KEY, (String[]) ArrayUtils.addAll(serverProperties, cpdProperties));
+ runProjectAnalysis(ORCHESTRATOR, PROJECT_DIR);
+ }
+
+ private List<Issues.Issue> findIssuesByRuleKey(String ruleKey) {
+ return adminWsClient.issues().search(
+ new SearchWsRequest()
+ .setComponents(singletonList(FILE_KEY))
+ .setRules(singletonList(ruleKey)))
+ .getIssuesList();
+ }
+
+ private List<Issues.Issue> findAllIssues() {
+ return adminWsClient.issues().search(new SearchWsRequest()).getIssuesList();
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/issue/IssueFilterTest.java b/tests/src/test/java/org/sonarqube/tests/issue/IssueFilterTest.java
new file mode 100644
index 00000000000..bc906d6093d
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/issue/IssueFilterTest.java
@@ -0,0 +1,115 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.issue;
+
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.getMeasuresAsDoubleByMetricKey;
+import static util.ItUtils.runProjectAnalysis;
+import static util.ItUtils.setServerProperties;
+
+public class IssueFilterTest extends AbstractIssueTest {
+
+ private static final String PROJECT_KEY = "com.sonarsource.it.samples:multi-modules-exclusions";
+ private static final String PROJECT_DIR = "exclusions/xoo-multi-modules";
+
+ @Before
+ public void resetData() {
+ ORCHESTRATOR.resetData();
+
+ ItUtils.restoreProfile(ORCHESTRATOR, getClass().getResource("/issue/IssueFilterTest/with-many-rules.xml"));
+ ORCHESTRATOR.getServer().provisionProject(PROJECT_KEY, "project");
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile(PROJECT_KEY, "xoo", "with-many-rules");
+ }
+
+ @Test
+ public void ignore_all_files() {
+ executeAnalysis(
+ "sonar.issue.ignore.multicriteria", "1",
+ "sonar.issue.ignore.multicriteria.1.resourceKey", "**/*.xoo",
+ "sonar.issue.ignore.multicriteria.1.ruleKey", "*");
+
+ checkIssueCountBySeverity(4, 0, 0, 4);
+ }
+
+ @Test
+ public void enforce_only_on_one_file() {
+ executeAnalysis(
+ "sonar.issue.enforce.multicriteria", "1",
+ "sonar.issue.enforce.multicriteria.1.resourceKey", "**/HelloA1.xoo",
+ "sonar.issue.enforce.multicriteria.1.ruleKey", "*");
+
+ checkIssueCountBySeverity(
+ 1 /* tag */ + 18 /* lines in HelloA1.xoo */ + 1 /* file */,
+ 0 + 1,
+ 0,
+ 0);
+ }
+
+ @Test
+ public void enforce_on_two_files_with_same_rule() {
+ executeAnalysis(
+ "sonar.issue.enforce.multicriteria", "1,2",
+ "sonar.issue.enforce.multicriteria.1.resourceKey", "**/HelloA1.xoo",
+ "sonar.issue.enforce.multicriteria.1.ruleKey", "*",
+ "sonar.issue.enforce.multicriteria.2.resourceKey", "**/HelloA2.xoo",
+ "sonar.issue.enforce.multicriteria.2.ruleKey", "*");
+
+ checkIssueCountBySeverity(
+ 2 /* tags */ + 18 /* lines in HelloA1.xoo */ + 15 /* lines in HelloA2.xoo */ + 2 /* files */,
+ 0 + 2,
+ 0,
+ 0);
+ }
+
+ @Test
+ public void enforce_on_two_files_with_different_rules() {
+ executeAnalysis(
+ "sonar.issue.enforce.multicriteria", "1,2",
+ "sonar.issue.enforce.multicriteria.1.resourceKey", "**/HelloA1.xoo",
+ "sonar.issue.enforce.multicriteria.1.ruleKey", "xoo:OneIssuePerLine",
+ "sonar.issue.enforce.multicriteria.2.resourceKey", "**/HelloA2.xoo",
+ "sonar.issue.enforce.multicriteria.2.ruleKey", "xoo:HasTag");
+
+ checkIssueCountBySeverity(
+ 1 /* tag in HelloA2 */ + 18 /* lines in HelloA1.xoo */ + 4 /* files */ + 4 /* modules */,
+ 4,
+ 0,
+ 4);
+ }
+
+ private void executeAnalysis(String... serverProperties) {
+ setServerProperties(ORCHESTRATOR, PROJECT_KEY, serverProperties);
+ runProjectAnalysis(ORCHESTRATOR, PROJECT_DIR);
+ }
+
+ private void checkIssueCountBySeverity(int total, int perFile, int perCommonRule, int perModule) {
+ Map<String, Double> measures = getMeasuresAsDoubleByMetricKey(ORCHESTRATOR, PROJECT_KEY, "violations", "major_violations", "blocker_violations", "critical_violations");
+ assertThat(measures.get("violations").intValue()).isEqualTo(total);
+ assertThat(measures.get("major_violations").intValue()).isEqualTo(perFile); // One per file
+ assertThat(measures.get("blocker_violations").intValue()).isEqualTo(perCommonRule); // On per common rule
+ assertThat(measures.get("critical_violations").intValue()).isEqualTo(perModule); // One per module
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/issue/IssueMeasureTest.java b/tests/src/test/java/org/sonarqube/tests/issue/IssueMeasureTest.java
new file mode 100644
index 00000000000..8b91bb76477
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/issue/IssueMeasureTest.java
@@ -0,0 +1,130 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.issue;
+
+import java.util.List;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.wsclient.issue.Issue;
+import org.sonar.wsclient.issue.IssueQuery;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.getMeasuresAsDoubleByMetricKey;
+import static util.ItUtils.runProjectAnalysis;
+
+public class IssueMeasureTest extends AbstractIssueTest {
+
+ private static final String MULTI_MODULE_SAMPLE_PROJECT_KEY = "com.sonarsource.it.samples:multi-modules-sample";
+ private static final String SAMPLE_PROJECT_KEY = "sample";
+
+ @Before
+ public void resetData() {
+ ORCHESTRATOR.resetData();
+ }
+
+ @Test
+ public void issues_by_severity_measures() {
+ ORCHESTRATOR.getServer().provisionProject(MULTI_MODULE_SAMPLE_PROJECT_KEY, MULTI_MODULE_SAMPLE_PROJECT_KEY);
+ ItUtils.restoreProfile(ORCHESTRATOR, getClass().getResource("/issue/with-many-rules.xml"));
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile(MULTI_MODULE_SAMPLE_PROJECT_KEY, "xoo", "with-many-rules");
+ runProjectAnalysis(ORCHESTRATOR, "shared/xoo-multi-modules-sample");
+
+ assertThat(search(IssueQuery.create().componentRoots(MULTI_MODULE_SAMPLE_PROJECT_KEY)).paging().total()).isEqualTo(136);
+
+ Map<String, Double> measures = getMeasuresAsDoubleByMetricKey(ORCHESTRATOR, MULTI_MODULE_SAMPLE_PROJECT_KEY, "violations", "info_violations", "minor_violations",
+ "major_violations",
+ "blocker_violations", "critical_violations");
+ assertThat(measures.get("violations")).isEqualTo(136);
+ assertThat(measures.get("info_violations")).isEqualTo(2);
+ assertThat(measures.get("minor_violations")).isEqualTo(61);
+ assertThat(measures.get("major_violations")).isEqualTo(65);
+ assertThat(measures.get("blocker_violations")).isEqualTo(4);
+ assertThat(measures.get("critical_violations")).isEqualTo(4);
+ }
+
+ /**
+ * SONAR-4330
+ * SONAR-7555
+ */
+ @Test
+ public void issues_by_resolution_and_status_measures() {
+ ORCHESTRATOR.getServer().provisionProject(SAMPLE_PROJECT_KEY, SAMPLE_PROJECT_KEY);
+ ItUtils.restoreProfile(ORCHESTRATOR, getClass().getResource("/issue/one-issue-per-line-profile.xml"));
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile(SAMPLE_PROJECT_KEY, "xoo", "one-issue-per-line-profile");
+ runProjectAnalysis(ORCHESTRATOR, "shared/xoo-sample");
+
+ List<Issue> issues = searchIssuesByProject(SAMPLE_PROJECT_KEY);
+ assertThat(issues).hasSize(17);
+
+ // 1 is a false-positive, 1 is a won't fix, 1 is confirmed, 1 is reopened, and the remaining ones stays open
+ adminIssueClient().doTransition(issues.get(0).key(), "falsepositive");
+ adminIssueClient().doTransition(issues.get(1).key(), "wontfix");
+ adminIssueClient().doTransition(issues.get(2).key(), "confirm");
+ adminIssueClient().doTransition(issues.get(3).key(), "resolve");
+ adminIssueClient().doTransition(issues.get(3).key(), "reopen");
+
+ // Re analyze the project to compute measures
+ runProjectAnalysis(ORCHESTRATOR, "shared/xoo-sample");
+
+ Map<String, Double> measures = getMeasuresAsDoubleByMetricKey(ORCHESTRATOR, SAMPLE_PROJECT_KEY, "false_positive_issues", "wont_fix_issues", "open_issues", "reopened_issues",
+ "confirmed_issues");
+ assertThat(measures.get("false_positive_issues")).isEqualTo(1);
+ assertThat(measures.get("wont_fix_issues")).isEqualTo(1);
+ assertThat(measures.get("open_issues")).isEqualTo(13);
+ assertThat(measures.get("reopened_issues")).isEqualTo(1);
+ assertThat(measures.get("confirmed_issues")).isEqualTo(1);
+ }
+
+ @Test
+ public void no_issue_are_computed_on_empty_profile() {
+ ORCHESTRATOR.getServer().provisionProject(SAMPLE_PROJECT_KEY, SAMPLE_PROJECT_KEY);
+
+ // no active rules
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile(SAMPLE_PROJECT_KEY, "xoo", "empty");
+ runProjectAnalysis(ORCHESTRATOR, "shared/xoo-sample");
+
+ assertThat(searchIssuesByProject(SAMPLE_PROJECT_KEY)).isEmpty();
+
+ Map<String, Double> measures = getMeasuresAsDoubleByMetricKey(ORCHESTRATOR, SAMPLE_PROJECT_KEY, "violations", "blocker_violations");
+ assertThat(measures.get("violations")).isEqualTo(0);
+ assertThat(measures.get("blocker_violations")).isEqualTo(0);
+ }
+
+ /**
+ * SONAR-3746
+ */
+ @Test
+ public void issues_measures_on_test_files() {
+ String projectKey = "sample-with-tests";
+ String testKey = "sample-with-tests:src/test/xoo/sample/SampleTest.xoo";
+
+ ORCHESTRATOR.getServer().provisionProject(projectKey, projectKey);
+ ItUtils.restoreProfile(ORCHESTRATOR, getClass().getResource("/issue/one-issue-per-file-profile.xml"));
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "xoo", "one-issue-per-file-profile");
+ runProjectAnalysis(ORCHESTRATOR, "shared/xoo-sample-with-tests");
+
+ // Store current number of issues
+ Map<String, Double> measures = getMeasuresAsDoubleByMetricKey(ORCHESTRATOR, testKey, "violations");
+ assertThat(measures.get("violations")).isEqualTo(1);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/issue/IssueNotificationsTest.java b/tests/src/test/java/org/sonarqube/tests/issue/IssueNotificationsTest.java
new file mode 100644
index 00000000000..49e4cee8afc
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/issue/IssueNotificationsTest.java
@@ -0,0 +1,236 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.issue;
+
+import java.util.Iterator;
+import javax.mail.internet.MimeMessage;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonar.wsclient.issue.Issue;
+import org.sonar.wsclient.issue.IssueClient;
+import org.sonar.wsclient.issue.IssueQuery;
+import org.sonar.wsclient.issue.Issues;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.issue.BulkChangeRequest;
+import org.sonarqube.ws.client.issue.IssuesService;
+import org.subethamail.wiser.Wiser;
+import org.subethamail.wiser.WiserMessage;
+import util.ItUtils;
+import util.user.UserRule;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.newUserWsClient;
+import static util.ItUtils.resetEmailSettings;
+import static util.ItUtils.runProjectAnalysis;
+import static util.ItUtils.setServerProperty;
+
+public class IssueNotificationsTest extends AbstractIssueTest {
+
+ private final static String PROJECT_KEY = "sample";
+ private final static String USER_LOGIN = "tester";
+ private final static String USER_PASSWORD = "tester";
+ private static final String USER_EMAIL = "tester@example.org";
+
+ private static Wiser smtpServer;
+
+ private IssueClient issueClient;
+
+ private IssuesService issuesService;
+
+ @ClassRule
+ public static UserRule userRule = UserRule.from(ORCHESTRATOR);
+
+ @BeforeClass
+ public static void before() throws Exception {
+ smtpServer = new Wiser(0);
+ smtpServer.start();
+ System.out.println("SMTP Server port: " + smtpServer.getServer().getPort());
+
+ // Configure Sonar
+ resetEmailSettings(ORCHESTRATOR);
+ setServerProperty(ORCHESTRATOR, "email.smtp_host.secured", "localhost");
+ setServerProperty(ORCHESTRATOR, "email.smtp_port.secured", Integer.toString(smtpServer.getServer().getPort()));
+
+ // Send test email to the test user
+ newAdminWsClient(ORCHESTRATOR).wsConnector().call(new PostRequest("api/emails/send")
+ .setParam("to", USER_EMAIL)
+ .setParam("message", "This is a test message from SonarQube"))
+ .failIfNotSuccessful();
+
+ // We need to wait until all notifications will be delivered
+ waitUntilAllNotificationsAreDelivered(1);
+
+ Iterator<WiserMessage> emails = smtpServer.getMessages().iterator();
+
+ MimeMessage message = emails.next().getMimeMessage();
+ assertThat(message.getHeader("To", null)).isEqualTo("<" + USER_EMAIL + ">");
+ assertThat((String) message.getContent()).contains("This is a test message from SonarQube");
+
+ assertThat(emails.hasNext()).isFalse();
+ }
+
+ @AfterClass
+ public static void stop() {
+ if (smtpServer != null) {
+ smtpServer.stop();
+ }
+ userRule.deactivateUsers(USER_LOGIN);
+ setServerProperty(ORCHESTRATOR, "sonar.issues.defaultAssigneeLogin", null);
+ resetEmailSettings(ORCHESTRATOR);
+ }
+
+ @Before
+ public void prepare() {
+ ORCHESTRATOR.resetData();
+
+ // Create test user
+ userRule.createUser(USER_LOGIN, "Tester", USER_EMAIL, USER_LOGIN);
+
+ smtpServer.getMessages().clear();
+ issueClient = ORCHESTRATOR.getServer().adminWsClient().issueClient();
+ issuesService = newAdminWsClient(ORCHESTRATOR).issues();
+
+ setServerProperty(ORCHESTRATOR, "sonar.issues.defaultAssigneeLogin", null);
+ ItUtils.restoreProfile(ORCHESTRATOR, getClass().getResource("/issue/one-issue-per-line-profile.xml"));
+ ORCHESTRATOR.getServer().provisionProject(PROJECT_KEY, "Sample");
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile(PROJECT_KEY, "xoo", "one-issue-per-line-profile");
+
+ // Add notifications to the test user
+ WsClient wsClient = newUserWsClient(ORCHESTRATOR, USER_LOGIN, USER_PASSWORD);
+ wsClient.wsConnector().call(new PostRequest("api/notifications/add")
+ .setParam("type", "NewIssues")
+ .setParam("channel", "EmailNotificationChannel"))
+ .failIfNotSuccessful();
+ wsClient.wsConnector().call(new PostRequest("api/notifications/add")
+ .setParam("type", "ChangesOnMyIssue")
+ .setParam("channel", "EmailNotificationChannel"))
+ .failIfNotSuccessful();
+ wsClient.wsConnector().call(new PostRequest("api/notifications/add")
+ .setParam("type", "SQ-MyNewIssues")
+ .setParam("channel", "EmailNotificationChannel"))
+ .failIfNotSuccessful();
+
+ }
+
+ @Test
+ public void notifications_for_new_issues_and_issue_changes() throws Exception {
+ runProjectAnalysis(ORCHESTRATOR, "shared/xoo-sample", "sonar.projectDate", "2015-12-15");
+
+ // change assignee
+ Issues issues = issueClient.find(IssueQuery.create().componentRoots(PROJECT_KEY));
+ Issue issue = issues.list().get(0);
+ issueClient.assign(issue.key(), USER_LOGIN);
+
+ waitUntilAllNotificationsAreDelivered(2);
+
+ Iterator<WiserMessage> emails = smtpServer.getMessages().iterator();
+
+ assertThat(emails.hasNext()).isTrue();
+ MimeMessage message = emails.next().getMimeMessage();
+ assertThat(message.getHeader("To", null)).isEqualTo("<tester@example.org>");
+ assertThat((String) message.getContent()).contains("Sample");
+ assertThat((String) message.getContent()).contains("17 new issues (new debt: 17min)");
+ assertThat((String) message.getContent()).contains("Severity");
+ assertThat((String) message.getContent()).contains("One Issue Per Line (xoo): 17");
+ assertThat((String) message.getContent()).contains(
+ "See it in SonarQube: http://localhost:9000/project/issues?id=sample&createdAt=2015-12-15T00%3A00%3A00%2B");
+
+ assertThat(emails.hasNext()).isTrue();
+ message = emails.next().getMimeMessage();
+ assertThat(message.getHeader("To", null)).isEqualTo("<tester@example.org>");
+ assertThat((String) message.getContent()).contains("sample/Sample.xoo");
+ assertThat((String) message.getContent()).contains("Assignee changed to Tester");
+ assertThat((String) message.getContent()).contains(
+ "See it in SonarQube: http://localhost:9000/project/issues?id=sample&issues=" + issue.key() + "&open=" + issue.key());
+
+ assertThat(emails.hasNext()).isFalse();
+ }
+
+ @Test
+ public void notifications_for_personalized_emails() throws Exception {
+ setServerProperty(ORCHESTRATOR, "sonar.issues.defaultAssigneeLogin", USER_LOGIN);
+ runProjectAnalysis(ORCHESTRATOR, "issue/xoo-with-scm", "sonar.scm.provider", "xoo", "sonar.scm.disabled", "false");
+
+ waitUntilAllNotificationsAreDelivered(2);
+
+ Iterator<WiserMessage> emails = smtpServer.getMessages().iterator();
+ emails.next();
+ // the second email sent is the personalized one
+ MimeMessage message = emails.next().getMimeMessage();
+
+ assertThat(message.getHeader("To", null)).isEqualTo("<tester@example.org>");
+ assertThat(message.getSubject()).contains("You have 13 new issues");
+ }
+
+ /**
+ * SONAR-4606
+ */
+ @Test
+ public void notifications_for_bulk_change_ws() throws Exception {
+ runProjectAnalysis(ORCHESTRATOR, "shared/xoo-sample", "sonar.projectDate", "2015-12-15");
+
+ Issues issues = issueClient.find(IssueQuery.create().componentRoots(PROJECT_KEY));
+ Issue issue = issues.list().get(0);
+
+ // bulk change without notification by default
+ issuesService.bulkChange(BulkChangeRequest.builder()
+ .setIssues(singletonList(issue.key()))
+ .setAssign(USER_LOGIN)
+ .setSetSeverity("MINOR")
+ .build());
+
+ // bulk change with notification
+ issuesService.bulkChange(BulkChangeRequest.builder()
+ .setIssues(singletonList(issue.key()))
+ .setSetSeverity("BLOCKER")
+ .setSendNotifications(true)
+ .build());
+
+ waitUntilAllNotificationsAreDelivered(2);
+
+ Iterator<WiserMessage> emails = smtpServer.getMessages().iterator();
+
+ emails.next();
+ MimeMessage message = emails.next().getMimeMessage();
+ assertThat(message.getHeader("To", null)).isEqualTo("<tester@example.org>");
+ assertThat((String) message.getContent()).contains("sample/Sample.xoo");
+ assertThat((String) message.getContent()).contains("Severity: BLOCKER (was MINOR)");
+ assertThat((String) message.getContent()).contains(
+ "See it in SonarQube: http://localhost:9000/project/issues?id=sample&issues=" + issue.key() + "&open=" + issue.key());
+
+ assertThat(emails.hasNext()).isFalse();
+ }
+
+ private static void waitUntilAllNotificationsAreDelivered(int expectedNumberOfEmails) throws InterruptedException {
+ for (int i = 0; i < 10; i++) {
+ if (smtpServer.getMessages().size() == expectedNumberOfEmails) {
+ break;
+ }
+ Thread.sleep(1_000);
+ }
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/issue/IssuePurgeTest.java b/tests/src/test/java/org/sonarqube/tests/issue/IssuePurgeTest.java
new file mode 100644
index 00000000000..63a033713ac
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/issue/IssuePurgeTest.java
@@ -0,0 +1,187 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.issue;
+
+import java.util.List;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.wsclient.issue.Issue;
+import org.sonar.wsclient.issue.IssueQuery;
+import org.sonar.wsclient.issue.Issues;
+import util.ProjectAnalysis;
+import util.ProjectAnalysisRule;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class IssuePurgeTest extends AbstractIssueTest {
+
+ @Rule
+ public final ProjectAnalysisRule projectAnalysisRule = ProjectAnalysisRule.from(ORCHESTRATOR);
+
+ private ProjectAnalysis xooSampleAnalysis;
+ private ProjectAnalysis xooMultiModuleAnalysis;
+
+ @Before
+ public void setUp() throws Exception {
+ String manyRulesProfile = projectAnalysisRule.registerProfile("/issue/IssuePurgeTest/with-many-rules.xml");
+ String xooSampleProjectKey = projectAnalysisRule.registerProject("shared/xoo-sample");
+ this.xooSampleAnalysis = projectAnalysisRule.newProjectAnalysis(xooSampleProjectKey)
+ .withQualityProfile(manyRulesProfile);
+ String xooMultiModuleProjectKey = projectAnalysisRule.registerProject("shared/xoo-multi-modules-sample");
+ this.xooMultiModuleAnalysis = projectAnalysisRule.newProjectAnalysis(xooMultiModuleProjectKey)
+ .withQualityProfile(manyRulesProfile);
+ }
+
+ /**
+ * SONAR-4308
+ */
+ @Test
+ public void purge_old_closed_issues() throws Exception {
+ projectAnalysisRule.setServerProperty("sonar.dbcleaner.daysBeforeDeletingClosedIssues", "5000");
+
+ // Generate some issues
+ xooSampleAnalysis.withProperties(
+ "sonar.dynamicAnalysis", "false",
+ "sonar.projectDate", "2014-10-01")
+ .run();
+
+ // All the issues are open
+ List<Issue> issuesList = searchIssues();
+ for (Issue issue : issuesList) {
+ assertThat(issue.resolution()).isNull();
+ }
+
+ // Second scan with empty profile -> all issues are resolved and closed
+ // -> Not deleted because less than 5000 days long
+ xooSampleAnalysis
+ .withXooEmptyProfile()
+ .withProperties(
+ "sonar.dynamicAnalysis", "false",
+ "sonar.projectDate", "2014-10-15")
+ .run();
+ issuesList = searchIssues();
+ assertThat(issuesList).isNotEmpty();
+ for (Issue issue : issuesList) {
+ assertThat(issue.resolution()).isNotNull();
+ assertThat(issue.status()).isEqualTo("CLOSED");
+ }
+
+ // Third scan -> closed issues are deleted
+ projectAnalysisRule.setServerProperty("sonar.dbcleaner.daysBeforeDeletingClosedIssues", "1");
+
+ xooSampleAnalysis.withXooEmptyProfile()
+ .withProperties(
+ "sonar.dynamicAnalysis", "false",
+ "sonar.projectDate", "2014-10-20")
+ .run();
+ Issues issues = issueClient().find(IssueQuery.create());
+ assertThat(issues.list()).isEmpty();
+ assertThat(issues.paging().total()).isZero();
+ }
+
+ /**
+ * SONAR-7108
+ */
+ @Test
+ public void purge_old_closed_issues_when_zero_closed_issues_wanted() throws Exception {
+ projectAnalysisRule.setServerProperty("sonar.dbcleaner.daysBeforeDeletingClosedIssues", "5000");
+
+ // Generate some issues
+ xooSampleAnalysis.withProperties(
+ "sonar.dynamicAnalysis", "false",
+ "sonar.projectDate", "2014-10-01")
+ .run();
+
+ // All the issues are open
+ List<Issue> issueList = searchIssues();
+ for (Issue issue : issueList) {
+ assertThat(issue.resolution()).isNull();
+ }
+
+ // Second scan with empty profile -> all issues are resolved and closed
+ // -> Not deleted because less than 5000 days long
+ xooSampleAnalysis
+ .withXooEmptyProfile()
+ .withProperties(
+ "sonar.dynamicAnalysis", "false",
+ "sonar.projectDate", "2014-10-15")
+ .run();
+ issueList = searchIssues();
+ assertThat(issueList).isNotEmpty();
+ for (Issue issue : issueList) {
+ assertThat(issue.resolution()).isNotNull();
+ assertThat(issue.status()).isEqualTo("CLOSED");
+ }
+
+ // Third scan -> closed issues are deleted
+ projectAnalysisRule.setServerProperty("sonar.dbcleaner.daysBeforeDeletingClosedIssues", "0");
+
+ xooSampleAnalysis.withXooEmptyProfile()
+ .withProperties(
+ "sonar.dynamicAnalysis", "false",
+ "sonar.projectDate", "2014-10-20")
+ .run();
+
+ Issues issues = issueClient().find(IssueQuery.create());
+ assertThat(issues.list()).isEmpty();
+ assertThat(issues.paging().total()).isZero();
+ }
+
+ /**
+ * SONAR-5200
+ */
+ @Test
+ public void resolve_issues_when_removing_module() throws Exception {
+ // Generate some issues
+ xooMultiModuleAnalysis
+ .withProperties("sonar.dynamicAnalysis", "false")
+ .run();
+
+ // All the issues are open
+ List<Issue> issues = searchIssues();
+ for (Issue issue : issues) {
+ assertThat(issue.resolution()).isNull();
+ }
+ Issue issue = issues.get(0);
+
+ int issuesOnModuleB = searchIssues(IssueQuery.create().componentRoots("com.sonarsource.it.samples:multi-modules-sample:module_b")).size();
+ assertThat(issuesOnModuleB).isEqualTo(28);
+
+ // Second scan without module B -> issues on module B are resolved as removed and closed
+ xooMultiModuleAnalysis
+ .withProperties(
+ "sonar.dynamicAnalysis", "false",
+ "sonar.modules", "module_a")
+ .run();
+
+ // Resolved should should all be mark as REMOVED and affect to module b
+ List<Issue> reloadedIssues = searchIssues(IssueQuery.create().resolved(true));
+ assertThat(reloadedIssues).hasSize(issuesOnModuleB);
+ for (Issue reloadedIssue : reloadedIssues) {
+ assertThat(reloadedIssue.resolution()).isEqualTo("FIXED");
+ assertThat(reloadedIssue.status()).isEqualTo("CLOSED");
+ assertThat(reloadedIssue.componentKey()).contains("com.sonarsource.it.samples:multi-modules-sample:module_b");
+ assertThat(reloadedIssue.updateDate().before(issue.updateDate())).isFalse();
+ assertThat(reloadedIssue.closeDate()).isNotNull();
+ assertThat(reloadedIssue.closeDate().before(reloadedIssue.creationDate())).isFalse();
+ }
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/issue/IssueSearchTest.java b/tests/src/test/java/org/sonarqube/tests/issue/IssueSearchTest.java
new file mode 100644
index 00000000000..1ad0be32cd9
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/issue/IssueSearchTest.java
@@ -0,0 +1,277 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.issue;
+
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import org.apache.commons.lang.time.DateUtils;
+import org.assertj.core.api.Fail;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.sonar.wsclient.base.HttpException;
+import org.sonar.wsclient.base.Paging;
+import org.sonar.wsclient.issue.Issue;
+import org.sonar.wsclient.issue.IssueQuery;
+import org.sonar.wsclient.issue.Issues;
+import org.sonarqube.ws.Common;
+import org.sonarqube.ws.client.issue.SearchWsRequest;
+import util.ItUtils;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonarqube.ws.Issues.SearchWsResponse;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.runProjectAnalysis;
+import static util.ItUtils.setServerProperty;
+import static util.ItUtils.toDate;
+
+public class IssueSearchTest extends AbstractIssueTest {
+
+ private static final String PROJECT_KEY = "com.sonarsource.it.samples:multi-modules-sample";
+ private static final String PROJECT_KEY2 = "com.sonarsource.it.samples:multi-modules-sample2";
+
+ private static int DEFAULT_PAGINATED_RESULTS = 100;
+ private static int TOTAL_NB_ISSUES = 272;
+
+ @BeforeClass
+ public static void prepareData() {
+ ORCHESTRATOR.resetData();
+
+ ItUtils.restoreProfile(ORCHESTRATOR, IssueSearchTest.class.getResource("/issue/with-many-rules.xml"));
+
+ // Launch 2 analysis to have more than 100 issues in total
+ ORCHESTRATOR.getServer().provisionProject(PROJECT_KEY, PROJECT_KEY);
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile(PROJECT_KEY, "xoo", "with-many-rules");
+ runProjectAnalysis(ORCHESTRATOR, "shared/xoo-multi-modules-sample");
+
+ ORCHESTRATOR.getServer().provisionProject(PROJECT_KEY2, PROJECT_KEY2);
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile(PROJECT_KEY2, "xoo", "with-many-rules");
+ runProjectAnalysis(ORCHESTRATOR, "shared/xoo-multi-modules-sample", "sonar.projectKey", PROJECT_KEY2);
+
+ // Assign a issue to test search by assignee
+ adminIssueClient().assign(searchRandomIssue().key(), "admin");
+
+ // Resolve a issue to test search by status and by resolution
+ adminIssueClient().doTransition(searchRandomIssue().key(), "resolve");
+ }
+
+ @Before
+ public void resetProperties() throws Exception {
+ setServerProperty(ORCHESTRATOR, "sonar.forceAuthentication", "false");
+ }
+
+ @Test
+ public void search_all_issues() {
+ assertThat(search(IssueQuery.create()).list()).hasSize(DEFAULT_PAGINATED_RESULTS);
+ }
+
+ @Test
+ public void search_issues_by_component_roots() {
+ assertThat(search(IssueQuery.create().componentRoots("com.sonarsource.it.samples:multi-modules-sample")).list()).hasSize(DEFAULT_PAGINATED_RESULTS);
+ assertThat(search(IssueQuery.create().componentRoots("com.sonarsource.it.samples:multi-modules-sample:module_a")).list()).hasSize(82);
+ assertThat(search(IssueQuery.create().componentRoots("com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1")).list()).hasSize(36);
+
+ assertThat(search(IssueQuery.create().componentRoots("unknown")).list()).isEmpty();
+ }
+
+ @Test
+ public void search_issues_by_components() {
+ assertThat(
+ search(IssueQuery.create().components("com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo")).list())
+ .hasSize(34);
+ assertThat(search(IssueQuery.create().components("unknown")).list()).isEmpty();
+ }
+
+ @Test
+ public void search_issues_by_severities() {
+ assertThat(search(IssueQuery.create().severities("BLOCKER")).list()).hasSize(8);
+ assertThat(search(IssueQuery.create().severities("CRITICAL")).list()).hasSize(8);
+ assertThat(search(IssueQuery.create().severities("MAJOR")).list()).hasSize(DEFAULT_PAGINATED_RESULTS);
+ assertThat(search(IssueQuery.create().severities("MINOR")).list()).hasSize(DEFAULT_PAGINATED_RESULTS);
+ assertThat(search(IssueQuery.create().severities("INFO")).list()).hasSize(4);
+ }
+
+ @Test
+ public void search_issues_by_statuses() {
+ assertThat(search(IssueQuery.create().statuses("OPEN")).list()).hasSize(DEFAULT_PAGINATED_RESULTS);
+ assertThat(search(IssueQuery.create().statuses("RESOLVED")).list()).hasSize(1);
+ assertThat(search(IssueQuery.create().statuses("CLOSED")).list()).isEmpty();
+ }
+
+ @Test
+ public void search_issues_by_resolutions() {
+ assertThat(search(IssueQuery.create().resolutions("FIXED")).list()).hasSize(1);
+ assertThat(search(IssueQuery.create().resolutions("FALSE-POSITIVE")).list()).isEmpty();
+ assertThat(search(IssueQuery.create().resolved(true)).list()).hasSize(1);
+ assertThat(search(IssueQuery.create().resolved(false)).paging().total()).isEqualTo(TOTAL_NB_ISSUES - 1);
+ }
+
+ @Test
+ public void search_issues_by_assignees() {
+ assertThat(search(IssueQuery.create().assignees("admin")).list()).hasSize(1);
+ assertThat(search(IssueQuery.create().assignees("unknown")).list()).isEmpty();
+ assertThat(search(IssueQuery.create().assigned(true)).list()).hasSize(1);
+ assertThat(search(IssueQuery.create().assigned(false)).paging().total()).isEqualTo(TOTAL_NB_ISSUES - 1);
+ }
+
+ @Test
+ public void search_issues_by_rules() {
+ assertThat(search(IssueQuery.create().rules("xoo:OneIssuePerLine")).list()).hasSize(DEFAULT_PAGINATED_RESULTS);
+ assertThat(search(IssueQuery.create().rules("xoo:OneIssuePerFile")).list()).hasSize(8);
+
+ try {
+ search(IssueQuery.create().rules("unknown"));
+ Assert.fail();
+ } catch (org.sonar.wsclient.base.HttpException e) {
+ assertThat(e.status()).isEqualTo(400);
+ }
+ }
+
+ /**
+ * SONAR-2981
+ */
+ @Test
+ public void search_issues_by_dates() {
+ // issues have been created today
+ Date today = toDate(new SimpleDateFormat("yyyy-MM-dd").format(new Date()));
+ Date past = toDate("2013-01-01");
+ Date future = toDate("2020-12-31");
+
+ // createdAfter in the future => bad request
+ try {
+ search(IssueQuery.create().createdAfter(future)).list();
+ Fail.fail("Expecting 400 from issues search WS");
+ } catch (HttpException exception) {
+ assertThat(exception.getMessage()).contains("Start bound cannot be in the future");
+ }
+
+ // after date
+ assertThat(search(IssueQuery.create().createdAfter(today)).list().size()).isGreaterThan(0);
+ assertThat(search(IssueQuery.create().createdAfter(past)).list().size()).isGreaterThan(0);
+
+ // before
+ assertThat(search(IssueQuery.create().createdBefore(future)).list().size()).isGreaterThan(0);
+ assertThat(search(IssueQuery.create().createdBefore(past)).list()).isEmpty();
+
+ // before and after
+ assertThat(search(IssueQuery.create().createdBefore(future).createdAfter(past)).list().size()).isGreaterThan(0);
+
+ // createdAfter > createdBefore => bad request
+ try {
+ search(IssueQuery.create().createdBefore(past).createdAfter(today)).list();
+ Fail.fail("Expecting 400 from issues search WS");
+ } catch (HttpException exception) {
+ assertThat(exception.getMessage()).contains("Start bound cannot be larger or equal to end bound");
+ }
+
+ }
+
+ /**
+ * SONAR-5132
+ */
+ @Test
+ public void search_issues_by_languages() {
+ assertThat(search(IssueQuery.create().languages("xoo")).list()).hasSize(DEFAULT_PAGINATED_RESULTS);
+ assertThat(search(IssueQuery.create().languages("foo")).list()).isEmpty();
+ }
+
+ @Test
+ public void paginate_results() {
+ Issues issues = search(IssueQuery.create().pageSize(20).pageIndex(2));
+
+ assertThat(issues.list()).hasSize(20);
+ Paging paging = issues.paging();
+ assertThat(paging.pageIndex()).isEqualTo(2);
+ assertThat(paging.pageSize()).isEqualTo(20);
+ assertThat(paging.total()).isEqualTo(TOTAL_NB_ISSUES);
+
+ // SONAR-3257
+ // return max page size results when using negative page size value
+ assertThat(search(IssueQuery.create().pageSize(0)).list()).hasSize(TOTAL_NB_ISSUES);
+ assertThat(search(IssueQuery.create().pageSize(-1)).list()).hasSize(TOTAL_NB_ISSUES);
+ }
+
+ @Test
+ public void sort_results() {
+ List<Issue> issues = search(IssueQuery.create().sort("SEVERITY").asc(false)).list();
+ assertThat(issues.get(0).severity()).isEqualTo("BLOCKER");
+ assertThat(issues.get(8).severity()).isEqualTo("CRITICAL");
+ assertThat(issues.get(17).severity()).isEqualTo("MAJOR");
+ }
+
+ /**
+ * SONAR-4563
+ */
+ @Test
+ public void search_by_exact_creation_date() {
+ final Issue issue = search(IssueQuery.create()).list().get(0);
+ assertThat(issue.creationDate()).isNotNull();
+
+ // search the issue key with the same date
+ assertThat(search(IssueQuery.create().issues().issues(issue.key()).createdAt(issue.creationDate())).list()).hasSize(1);
+
+ // search issue key with 1 second more and less should return nothing
+ assertThat(search(IssueQuery.create().issues().issues(issue.key()).createdAt(DateUtils.addSeconds(issue.creationDate(), 1))).size()).isEqualTo(0);
+ assertThat(search(IssueQuery.create().issues().issues(issue.key()).createdAt(DateUtils.addSeconds(issue.creationDate(), -1))).size()).isEqualTo(0);
+
+ // search with future and past dates that do not match any issues
+ assertThat(search(IssueQuery.create().createdAt(toDate("2020-01-01"))).size()).isEqualTo(0);
+ assertThat(search(IssueQuery.create().createdAt(toDate("2010-01-01"))).size()).isEqualTo(0);
+ }
+
+ @Test
+ public void return_issue_type() throws Exception {
+ List<org.sonarqube.ws.Issues.Issue> issues = searchByRuleKey("xoo:OneBugIssuePerLine");
+ assertThat(issues).isNotEmpty();
+ org.sonarqube.ws.Issues.Issue issue = issues.get(0);
+ assertThat(issue.getType()).isEqualTo(Common.RuleType.BUG);
+
+ issues = searchByRuleKey("xoo:OneVulnerabilityIssuePerModule");
+ assertThat(issues).isNotEmpty();
+ issue = issues.get(0);
+ assertThat(issue.getType()).isEqualTo(Common.RuleType.VULNERABILITY);
+
+ issues = searchByRuleKey("xoo:OneIssuePerLine");
+ assertThat(issues).isNotEmpty();
+ issue = issues.get(0);
+ assertThat(issue.getType()).isEqualTo(Common.RuleType.CODE_SMELL);
+ }
+
+ @Test
+ public void search_issues_by_types() throws IOException {
+ assertThat(searchIssues(new SearchWsRequest().setTypes(singletonList("CODE_SMELL"))).getPaging().getTotal()).isEqualTo(142);
+ assertThat(searchIssues(new SearchWsRequest().setTypes(singletonList("BUG"))).getPaging().getTotal()).isEqualTo(122);
+ assertThat(searchIssues(new SearchWsRequest().setTypes(singletonList("VULNERABILITY"))).getPaging().getTotal()).isEqualTo(8);
+ }
+
+ private List<org.sonarqube.ws.Issues.Issue> searchByRuleKey(String... ruleKey) throws IOException {
+ return searchIssues(new SearchWsRequest().setRules(asList(ruleKey))).getIssuesList();
+ }
+
+ private SearchWsResponse searchIssues(SearchWsRequest request) throws IOException {
+ return newAdminWsClient(ORCHESTRATOR).issues().search(request);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/issue/IssueTagsTest.java b/tests/src/test/java/org/sonarqube/tests/issue/IssueTagsTest.java
new file mode 100644
index 00000000000..aa1f987c565
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/issue/IssueTagsTest.java
@@ -0,0 +1,142 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.issue;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category6Suite;
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.Organizations.Organization;
+import org.sonarqube.ws.WsUsers.CreateWsResponse.User;
+import org.sonarqube.ws.client.issue.SearchWsRequest;
+import org.sonarqube.ws.client.permission.AddUserWsRequest;
+import org.sonarqube.ws.client.project.CreateRequest;
+import util.ItUtils;
+
+import static java.util.Arrays.asList;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newProjectKey;
+import static util.ItUtils.projectDir;
+import static util.ItUtils.restoreProfile;
+
+/**
+ * Tests WS api/issues/tags
+ */
+public class IssueTagsTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category6Suite.ORCHESTRATOR;
+
+ @Rule
+ public Tester tester = new Tester(orchestrator);
+
+ private Organization organization;
+
+ @Before
+ public void setUp() {
+ organization = tester.organizations().generate();
+ }
+
+ @Test
+ public void getTags() {
+ restoreProfile(orchestrator, IssueTagsTest.class.getResource("/issue/one-issue-per-line-profile.xml"), organization.getKey());
+ String projectKey = newProjectKey();
+ tester.wsClient().projects().create(
+ CreateRequest.builder()
+ .setKey(projectKey)
+ .setOrganization(organization.getKey())
+ .setName(randomAlphabetic(10))
+ .setVisibility("private")
+ .build());
+ analyzeProject(projectKey);
+
+ String issue = tester.wsClient().issues().search(new SearchWsRequest()).getIssues(0).getKey();
+ tester.wsClient().issues().setTags(issue, "bla", "blubb");
+
+ String[] publicTags = {"bad-practice", "convention", "pitfall"};
+ String[] privateTags = {"bad-practice", "bla", "blubb", "convention", "pitfall"};
+ String defaultOrganization = null;
+
+ // anonymous must not see custom tags of private project
+ {
+ String anonymous = null;
+ assertTags(anonymous, organization.getKey(), publicTags);
+ assertTags(anonymous, defaultOrganization, publicTags);
+ }
+
+ // stranger must not see custom tags of private project
+ {
+ User stranger = tester.users().generate();
+ assertTags(stranger.getLogin(), organization.getKey(), publicTags);
+ assertTags(stranger.getLogin(), defaultOrganization, publicTags);
+ }
+
+ // member with user permission must be able to see custom tags of private project, if he provides the organization parameter
+ {
+ User member = tester.users().generate();
+ addMemberToOrganization(member);
+ grantUserPermission(projectKey, member);
+ assertTags(member.getLogin(), organization.getKey(), privateTags);
+ assertTags(member.getLogin(), defaultOrganization, publicTags);
+ }
+ }
+
+ private void addMemberToOrganization(User member) {
+ tester.organizations().service().addMember(organization.getKey(), member.getLogin());
+ }
+
+ private void grantUserPermission(String projectKey, User member) {
+ tester.wsClient().permissions().addUser(
+ new AddUserWsRequest()
+ .setLogin(member.getLogin())
+ .setPermission("user")
+ .setProjectKey(projectKey));
+ }
+
+ private void assertTags(@Nullable String userLogin, @Nullable String organization, String... expectedTags) {
+ assertThat(
+ (List<String>) ItUtils.jsonToMap(
+ tester.as(userLogin)
+ .wsClient()
+ .issues()
+ .getTags(organization)
+ .content())
+ .get("tags")).containsExactly(
+ expectedTags);
+ }
+
+ private void analyzeProject(String projectKey) {
+ List<String> keyValueProperties = new ArrayList<>(asList(
+ "sonar.projectKey", projectKey,
+ "sonar.organization", organization.getKey(),
+ "sonar.profile", "one-issue-per-line-profile",
+ "sonar.login", "admin", "sonar.password", "admin",
+ "sonar.scm.disabled", "false"));
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample"), keyValueProperties.toArray(new String[0])));
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/issue/IssueTrackingTest.java b/tests/src/test/java/org/sonarqube/tests/issue/IssueTrackingTest.java
new file mode 100644
index 00000000000..27a9ed0e80f
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/issue/IssueTrackingTest.java
@@ -0,0 +1,220 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.issue;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonarqube.ws.Issues.Issue;
+import org.sonarqube.ws.Issues.SearchWsResponse;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.issue.SearchWsRequest;
+import util.ItUtils;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.runProjectAnalysis;
+import static util.ItUtils.toDate;
+
+public class IssueTrackingTest extends AbstractIssueTest {
+
+ private static final String SAMPLE_PROJECT_KEY = "sample";
+
+ private static final String OLD_DATE = "2014-03-01";
+ private static final Date NEW_DATE = new Date();
+ private static final String NEW_DATE_STR = new SimpleDateFormat("yyyy-MM-dd").format(NEW_DATE);
+
+ private static WsClient adminClient;
+
+ @Before
+ public void prepareData() {
+ ORCHESTRATOR.resetData();
+ ItUtils.restoreProfile(ORCHESTRATOR, getClass().getResource("/issue/issue-on-tag-foobar.xml"));
+ ItUtils.restoreProfile(ORCHESTRATOR, getClass().getResource("/issue/IssueTrackingTest/one-issue-per-module-profile.xml"));
+ ORCHESTRATOR.getServer().provisionProject(SAMPLE_PROJECT_KEY, SAMPLE_PROJECT_KEY);
+ adminClient = newAdminWsClient(ORCHESTRATOR);
+ }
+
+ @Test
+ public void close_issues_on_removed_components() throws Exception {
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile(SAMPLE_PROJECT_KEY, "xoo", "issue-on-tag-foobar");
+
+ // version 1
+ runProjectAnalysis(ORCHESTRATOR, "issue/xoo-tracking-v1",
+ "sonar.projectDate", OLD_DATE);
+
+ List<Issue> issues = searchUnresolvedIssuesByComponent("sample:src/main/xoo/sample/Sample.xoo");
+ assertThat(issues).hasSize(1);
+
+ // version 2
+ runProjectAnalysis(ORCHESTRATOR, "issue/xoo-tracking-v1",
+ "sonar.projectDate", NEW_DATE_STR,
+ "sonar.exclusions", "**/*.xoo");
+
+ issues = searchIssues(new SearchWsRequest().setProjectKeys(singletonList("sample"))).getIssuesList();
+ assertThat(issues).hasSize(1);
+ assertThat(issues.get(0).getStatus()).isEqualTo("CLOSED");
+ assertThat(issues.get(0).getResolution()).isEqualTo("FIXED");
+ }
+
+ /**
+ * SONAR-3072
+ */
+ @Test
+ public void track_issues_based_on_blocks_recognition() throws Exception {
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile(SAMPLE_PROJECT_KEY, "xoo", "issue-on-tag-foobar");
+
+ // version 1
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile(SAMPLE_PROJECT_KEY, "xoo", "issue-on-tag-foobar");
+ runProjectAnalysis(ORCHESTRATOR, "issue/xoo-tracking-v1",
+ "sonar.projectDate", OLD_DATE);
+
+ List<Issue> issues = searchUnresolvedIssuesByComponent("sample:src/main/xoo/sample/Sample.xoo");
+ assertThat(issues).hasSize(1);
+ Date issueDate = toDate(issues.iterator().next().getCreationDate());
+
+ // version 2
+ runProjectAnalysis(ORCHESTRATOR, "issue/xoo-tracking-v2",
+ "sonar.projectDate", NEW_DATE_STR);
+
+ issues = searchUnresolvedIssuesByComponent("sample:src/main/xoo/sample/Sample.xoo");
+ assertThat(issues).hasSize(3);
+
+ // issue created during the first scan and moved during the second scan
+ assertThat(toDate(getIssueOnLine(6, "xoo:HasTag", issues).getCreationDate())).isEqualTo(issueDate);
+
+ // issues created during the second scan
+ assertThat(toDate(getIssueOnLine(10, "xoo:HasTag", issues).getCreationDate())).isAfter(issueDate);
+ assertThat(toDate(getIssueOnLine(14, "xoo:HasTag", issues).getCreationDate())).isAfter(issueDate);
+ }
+
+ /**
+ * SONAR-4310
+ */
+ @Test
+ public void track_existing_unchanged_issues_on_module() throws Exception {
+ // The custom rule on module is enabled
+
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile(SAMPLE_PROJECT_KEY, "xoo", "one-issue-per-module");
+ runProjectAnalysis(ORCHESTRATOR, "shared/xoo-sample");
+
+ // Only one issue is created
+ assertThat(searchIssues(new SearchWsRequest()).getIssuesList()).hasSize(1);
+ Issue issue = getRandomIssue();
+
+ // Re analysis of the same project
+ runProjectAnalysis(ORCHESTRATOR, "shared/xoo-sample");
+
+ // No new issue should be created
+ assertThat(searchIssues(new SearchWsRequest()).getIssuesList()).hasSize(1);
+
+ // The issue on module should stay open and be the same from the first analysis
+ Issue reloadIssue = getIssueByKey(issue.getKey());
+ assertThat(reloadIssue.getCreationDate()).isEqualTo(issue.getCreationDate());
+ assertThat(reloadIssue.getStatus()).isEqualTo("OPEN");
+ assertThat(reloadIssue.hasResolution()).isFalse();
+ }
+
+ /**
+ * SONAR-4310
+ */
+ @Test
+ public void track_existing_unchanged_issues_on_multi_modules() throws Exception {
+ // The custom rule on module is enabled
+ ORCHESTRATOR.getServer().provisionProject("com.sonarsource.it.samples:multi-modules-sample", "com.sonarsource.it.samples:multi-modules-sample");
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile("com.sonarsource.it.samples:multi-modules-sample", "xoo", "one-issue-per-module");
+ runProjectAnalysis(ORCHESTRATOR, "shared/xoo-multi-modules-sample");
+
+ // One issue by module are created
+ List<Issue> issues = searchIssues(new SearchWsRequest()).getIssuesList();
+ assertThat(issues).hasSize(4);
+
+ // Re analysis of the same project
+ runProjectAnalysis(ORCHESTRATOR, "shared/xoo-multi-modules-sample");
+
+ // No new issue should be created
+ assertThat(searchIssues(new SearchWsRequest()).getIssuesList()).hasSize(issues.size());
+
+ // Issues on modules should stay open and be the same from the first analysis
+ for (Issue issue : issues) {
+ Issue reloadIssue = getIssueByKey(issue.getKey());
+ assertThat(reloadIssue.getStatus()).isEqualTo("OPEN");
+ assertThat(reloadIssue.hasResolution()).isFalse();
+ assertThat(reloadIssue.getCreationDate()).isEqualTo(issue.getCreationDate());
+ assertThat(reloadIssue.getUpdateDate()).isEqualTo(issue.getUpdateDate());
+ }
+ }
+
+ @Test
+ public void track_file_moves_based_on_identical_content() {
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile(SAMPLE_PROJECT_KEY, "xoo", "issue-on-tag-foobar");
+
+ // version 1
+ runProjectAnalysis(ORCHESTRATOR, "issue/xoo-tracking-v1",
+ "sonar.projectDate", OLD_DATE);
+
+ List<Issue> issues = searchUnresolvedIssuesByComponent("sample:src/main/xoo/sample/Sample.xoo");
+ assertThat(issues).hasSize(1);
+ Issue issueOnSample = issues.iterator().next();
+
+ // version 2
+ runProjectAnalysis(ORCHESTRATOR, "issue/xoo-tracking-v3",
+ "sonar.projectDate", NEW_DATE_STR);
+
+ assertThat(searchUnresolvedIssuesByComponent("sample:src/main/xoo/sample/Sample.xoo")).isEmpty();
+
+ issues = searchUnresolvedIssuesByComponent("sample:src/main/xoo/sample/Sample2.xoo");
+ assertThat(issues).hasSize(1);
+ Issue issueOnSample2 = issues.get(0);
+ assertThat(issueOnSample2.getKey()).isEqualTo(issueOnSample.getKey());
+ assertThat(issueOnSample2.getCreationDate()).isEqualTo(issueOnSample.getCreationDate());
+ assertThat(issueOnSample2.getUpdateDate()).isNotEqualTo(issueOnSample.getUpdateDate());
+ assertThat(issueOnSample2.getStatus()).isEqualTo("OPEN");
+ }
+
+ private Issue getIssueOnLine(int line, String rule, List<Issue> issues) {
+ return issues.stream()
+ .filter(issue -> issue.getRule().equals(rule))
+ .filter(issue -> issue.getLine() == line)
+ .findFirst().orElseThrow(IllegalArgumentException::new);
+ }
+
+ private List<Issue> searchUnresolvedIssuesByComponent(String componentKey) {
+ return searchIssues(new SearchWsRequest().setComponentKeys(singletonList(componentKey)).setResolved(false)).getIssuesList();
+ }
+
+ private static Issue getRandomIssue() {
+ return searchIssues(new SearchWsRequest()).getIssues(0);
+ }
+
+ private static Issue getIssueByKey(String issueKey) {
+ SearchWsResponse search = searchIssues(new SearchWsRequest().setIssues(singletonList(issueKey)));
+ assertThat(search.getTotal()).isEqualTo(1);
+ return search.getIssues(0);
+ }
+
+ private static SearchWsResponse searchIssues(SearchWsRequest request) {
+ return adminClient.issues().search(request);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/issue/IssueWorkflowTest.java b/tests/src/test/java/org/sonarqube/tests/issue/IssueWorkflowTest.java
new file mode 100644
index 00000000000..bfcd4afca84
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/issue/IssueWorkflowTest.java
@@ -0,0 +1,314 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.issue;
+
+import java.util.List;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.ws.Issues;
+import org.sonarqube.ws.Issues.Issue;
+import org.sonarqube.ws.client.issue.DoTransitionRequest;
+import org.sonarqube.ws.client.issue.IssuesService;
+import org.sonarqube.ws.client.issue.SearchWsRequest;
+import util.ProjectAnalysis;
+import util.ProjectAnalysisRule;
+import util.issue.IssueRule;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.toDatetime;
+
+public class IssueWorkflowTest extends AbstractIssueTest {
+
+ @Rule
+ public final ProjectAnalysisRule projectAnalysisRule = ProjectAnalysisRule.from(ORCHESTRATOR);
+
+ @ClassRule
+ public static final IssueRule issueRule = IssueRule.from(ORCHESTRATOR);
+
+ private ProjectAnalysis analysisWithIssues;
+ private ProjectAnalysis analysisWithoutIssues;
+ private IssuesService issuesService;
+
+ private Issue issue;
+
+ @Before
+ public void before() {
+ issuesService = newAdminWsClient(ORCHESTRATOR).issues();
+ String oneIssuePerFileProfileKey = projectAnalysisRule.registerProfile("/issue/IssueWorkflowTest/xoo-one-issue-per-line-profile.xml");
+ String analyzedProjectKey = projectAnalysisRule.registerProject("issue/workflow");
+ analysisWithIssues = projectAnalysisRule.newProjectAnalysis(analyzedProjectKey).withQualityProfile(oneIssuePerFileProfileKey);
+ analysisWithoutIssues = analysisWithIssues.withXooEmptyProfile();
+ analysisWithIssues.run();
+
+ issue = issueRule.getRandomIssue();
+ }
+
+ /**
+ * Issue on a disabled rule (uninstalled plugin or rule deactivated from quality profile) must
+ * be CLOSED with resolution REMOVED
+ */
+ @Test
+ public void issue_is_closed_as_removed_when_rule_is_disabled() throws Exception {
+ SearchWsRequest ruleSearchRequest = new SearchWsRequest().setRules(singletonList("xoo:OneIssuePerLine"));
+ List<Issue> issues = issueRule.search(ruleSearchRequest).getIssuesList();
+ assertThat(issues).isNotEmpty();
+
+ // re-analyze with profile "empty". The rule is disabled so the issues must be closed
+ analysisWithoutIssues.run();
+ issues = issueRule.search(ruleSearchRequest).getIssuesList();
+ assertThat(issues).isNotEmpty();
+ for (Issue issue : issues) {
+ assertThat(issue.getStatus()).isEqualTo("CLOSED");
+ assertThat(issue.getResolution()).isEqualTo("REMOVED");
+ }
+ }
+
+ /**
+ * SONAR-4329
+ */
+ @Test
+ public void user_should_confirm_issue() {
+ // mark as confirmed
+ issuesService.doTransition(new DoTransitionRequest(issue.getKey(), "confirm"));
+
+ Issue confirmed = issueRule.getByKey(issue.getKey());
+ assertThat(confirmed.getStatus()).isEqualTo("CONFIRMED");
+ assertThat(confirmed.hasResolution()).isFalse();
+ assertThat(confirmed.getCreationDate()).isEqualTo(issue.getCreationDate());
+
+ // user unconfirm the issue
+ assertThat(transitions(confirmed.getKey())).contains("unconfirm");
+ issuesService.doTransition(new DoTransitionRequest(confirmed.getKey(), "unconfirm"));
+
+ Issue unconfirmed = issueRule.getByKey(issue.getKey());
+ assertThat(unconfirmed.getStatus()).isEqualTo("REOPENED");
+ assertThat(unconfirmed.hasResolution()).isFalse();
+ assertThat(unconfirmed.getCreationDate()).isEqualTo(confirmed.getCreationDate());
+ }
+
+ /**
+ * SONAR-4329
+ */
+ @Test
+ public void user_should_mark_as_false_positive_confirmed_issue() {
+ // mark as confirmed
+ issuesService.doTransition(new DoTransitionRequest(issue.getKey(), "confirm"));
+
+ Issue confirmed = issueRule.getByKey(issue.getKey());
+ assertThat(confirmed.getStatus()).isEqualTo("CONFIRMED");
+ assertThat(confirmed.hasResolution()).isFalse();
+ assertThat(confirmed.getCreationDate()).isEqualTo(issue.getCreationDate());
+
+ // user mark the issue as false-positive
+ assertThat(transitions(confirmed.getKey())).contains("falsepositive");
+ issuesService.doTransition(new DoTransitionRequest(confirmed.getKey(), "falsepositive"));
+
+ Issue falsePositive = issueRule.getByKey(issue.getKey());
+ assertThat(falsePositive.getStatus()).isEqualTo("RESOLVED");
+ assertThat(falsePositive.getResolution()).isEqualTo("FALSE-POSITIVE");
+ assertThat(falsePositive.getCreationDate()).isEqualTo(confirmed.getCreationDate());
+ }
+
+ /**
+ * SONAR-4329
+ */
+ @Test
+ public void scan_should_close_no_more_existing_confirmed() {
+ // mark as confirmed
+ issuesService.doTransition(new DoTransitionRequest(issue.getKey(), "confirm"));
+ Issue falsePositive = issueRule.getByKey(issue.getKey());
+ assertThat(falsePositive.getStatus()).isEqualTo("CONFIRMED");
+ assertThat(falsePositive.hasResolution()).isFalse();
+ assertThat(falsePositive.getCreationDate()).isEqualTo(issue.getCreationDate());
+
+ // scan without any rules -> confirmed is closed
+ analysisWithoutIssues.run();
+ Issue closed = issueRule.getByKey(issue.getKey());
+ assertThat(closed.getStatus()).isEqualTo("CLOSED");
+ assertThat(closed.getResolution()).isEqualTo("REMOVED");
+ assertThat(closed.getCreationDate()).isEqualTo(issue.getCreationDate());
+ }
+
+ /**
+ * SONAR-4288
+ */
+ @Test
+ public void scan_should_reopen_unresolved_issue_but_marked_as_resolved() {
+ // mark as resolved
+ issuesService.doTransition(new DoTransitionRequest(issue.getKey(), "resolve"));
+ Issue resolvedIssue = issueRule.getByKey(issue.getKey());
+ assertThat(resolvedIssue.getStatus()).isEqualTo("RESOLVED");
+ assertThat(resolvedIssue.getResolution()).isEqualTo("FIXED");
+ assertThat(resolvedIssue.getCreationDate()).isEqualTo(issue.getCreationDate());
+
+ // re-execute scan, with the same Q profile -> the issue has not been fixed
+ analysisWithIssues.run();
+
+ // reload issue
+ Issue reopenedIssue = issueRule.getByKey(issue.getKey());
+
+ // the issue has been reopened
+ assertThat(reopenedIssue.getStatus()).isEqualTo("REOPENED");
+ assertThat(reopenedIssue.hasResolution()).isFalse();
+ assertThat(reopenedIssue.getCreationDate()).isEqualTo(issue.getCreationDate());
+ assertThat(toDatetime(reopenedIssue.getUpdateDate())).isAfter(toDatetime(issue.getUpdateDate()));
+ }
+
+ /**
+ * SONAR-4288
+ */
+ @Test
+ public void scan_should_close_resolved_issue() {
+ // mark as resolved
+ issuesService.doTransition(new DoTransitionRequest(issue.getKey(), "resolve"));
+ Issue resolvedIssue = issueRule.getByKey(issue.getKey());
+ assertThat(resolvedIssue.getStatus()).isEqualTo("RESOLVED");
+ assertThat(resolvedIssue.getResolution()).isEqualTo("FIXED");
+ assertThat(resolvedIssue.getCreationDate()).isEqualTo(issue.getCreationDate());
+ assertThat(resolvedIssue.hasCloseDate()).isFalse();
+
+ // re-execute scan without rules -> the issue is removed with resolution "REMOVED"
+ analysisWithoutIssues.run();
+
+ // reload issue
+ Issue closedIssue = issueRule.getByKey(issue.getKey());
+ assertThat(closedIssue.getStatus()).isEqualTo("CLOSED");
+ assertThat(closedIssue.getResolution()).isEqualTo("REMOVED");
+ assertThat(closedIssue.getCreationDate()).isEqualTo(issue.getCreationDate());
+ assertThat(toDatetime(closedIssue.getUpdateDate())).isAfter(toDatetime(resolvedIssue.getUpdateDate()));
+ assertThat(closedIssue.hasCloseDate()).isTrue();
+ assertThat(toDatetime(closedIssue.getCloseDate())).isAfter(toDatetime(closedIssue.getCreationDate()));
+ }
+
+ /**
+ * SONAR-4288
+ */
+ @Test
+ public void user_should_reopen_issue_marked_as_resolved() {
+ // user marks issue as resolved
+ issuesService.doTransition(new DoTransitionRequest(issue.getKey(), "resolve"));
+ Issue resolved = issueRule.getByKey(issue.getKey());
+ assertThat(resolved.getStatus()).isEqualTo("RESOLVED");
+ assertThat(resolved.getResolution()).isEqualTo("FIXED");
+ assertThat(resolved.getCreationDate()).isEqualTo(issue.getCreationDate());
+
+ // user reopens the issue
+ assertThat(transitions(resolved.getKey())).contains("reopen");
+ adminIssueClient().doTransition(resolved.getKey(), "reopen");
+
+ Issue reopened = issueRule.getByKey(resolved.getKey());
+ assertThat(reopened.getStatus()).isEqualTo("REOPENED");
+ assertThat(reopened.hasResolution()).isFalse();
+ assertThat(reopened.getCreationDate()).isEqualTo(resolved.getCreationDate());
+ }
+
+ /**
+ * SONAR-4286
+ */
+ @Test
+ public void scan_should_not_reopen_or_close_false_positives() {
+ // user marks issue as false-positive
+ issuesService.doTransition(new DoTransitionRequest(issue.getKey(), "falsepositive"));
+
+ Issue falsePositive = issueRule.getByKey(issue.getKey());
+ assertThat(falsePositive.getStatus()).isEqualTo("RESOLVED");
+ assertThat(falsePositive.getResolution()).isEqualTo("FALSE-POSITIVE");
+ assertThat(falsePositive.getCreationDate()).isEqualTo(issue.getCreationDate());
+
+ // re-execute the same scan
+ analysisWithIssues.run();
+
+ // refresh
+ Issue reloaded = issueRule.getByKey(falsePositive.getKey());
+ assertThat(reloaded.getStatus()).isEqualTo("RESOLVED");
+ assertThat(reloaded.getResolution()).isEqualTo("FALSE-POSITIVE");
+ assertThat(reloaded.getCreationDate()).isEqualTo(issue.getCreationDate());
+ assertThat(toDatetime(reloaded.getUpdateDate())).isEqualTo(toDatetime(falsePositive.getUpdateDate()));
+ }
+
+ /**
+ * SONAR-4286
+ */
+ @Test
+ public void scan_should_close_no_more_existing_false_positive() {
+ // user marks as false-positive
+ issuesService.doTransition(new DoTransitionRequest(issue.getKey(), "falsepositive"));
+ Issue falsePositive = issueRule.getByKey(issue.getKey());
+ assertThat(falsePositive.getStatus()).isEqualTo("RESOLVED");
+ assertThat(falsePositive.getResolution()).isEqualTo("FALSE-POSITIVE");
+ assertThat(falsePositive.getCreationDate()).isEqualTo(issue.getCreationDate());
+
+ // scan without any rules -> false-positive is closed
+ analysisWithoutIssues.run();
+ Issue closed = issueRule.getByKey(issue.getKey());
+ assertThat(closed.getStatus()).isEqualTo("CLOSED");
+ assertThat(closed.getResolution()).isEqualTo("REMOVED");
+ assertThat(closed.getCreationDate()).isEqualTo(issue.getCreationDate());
+ }
+
+ /**
+ * SONAR-4286
+ */
+ @Test
+ public void user_should_reopen_false_positive() {
+ // user marks as false-positive
+ issuesService.doTransition(new DoTransitionRequest(issue.getKey(), "falsepositive"));
+
+ Issue falsePositive = issueRule.getByKey(issue.getKey());
+ assertThat(falsePositive.getStatus()).isEqualTo("RESOLVED");
+ assertThat(falsePositive.getResolution()).isEqualTo("FALSE-POSITIVE");
+ assertThat(falsePositive.getCreationDate()).isEqualTo(issue.getCreationDate());
+
+ // user reopens the issue
+ assertThat(transitions(falsePositive.getKey())).contains("reopen");
+ adminIssueClient().doTransition(falsePositive.getKey(), "reopen");
+
+ Issue reopened = issueRule.getByKey(issue.getKey());
+ assertThat(reopened.getStatus()).isEqualTo("REOPENED");
+ assertThat(reopened.hasResolution()).isFalse();
+ assertThat(reopened.getCreationDate()).isEqualTo(falsePositive.getCreationDate());
+ }
+
+ @Test
+ public void user_should_not_reopen_closed_issue() {
+ issuesService.doTransition(new DoTransitionRequest(issue.getKey(), "resolve"));
+
+ // re-execute scan without rules -> the issue is closed
+ analysisWithoutIssues.run();
+
+ // user try to reopen the issue
+ assertThat(transitions(issue.getKey())).isEmpty();
+ }
+
+ private List<String> transitions(String issueKey) {
+ Issues.SearchWsResponse response = searchIssues(new SearchWsRequest().setIssues(singletonList(issueKey)).setAdditionalFields(singletonList("transitions")));
+ assertThat(response.getTotal()).isEqualTo(1);
+ return response.getIssues(0).getTransitions().getTransitionsList();
+ }
+
+ private Issues.SearchWsResponse searchIssues(SearchWsRequest request) {
+ return newAdminWsClient(ORCHESTRATOR).issues().search(request);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/issue/IssuesPageTest.java b/tests/src/test/java/org/sonarqube/tests/issue/IssuesPageTest.java
new file mode 100644
index 00000000000..cc3829b78e2
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/issue/IssuesPageTest.java
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.issue;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category2Suite;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.pageobjects.Navigation;
+import org.sonarqube.pageobjects.issues.Issue;
+import org.sonarqube.pageobjects.issues.IssuesPage;
+import util.ItUtils;
+import util.user.UserRule;
+
+import static util.ItUtils.runProjectAnalysis;
+
+public class IssuesPageTest {
+ private static final String PROJECT_KEY = "sample";
+
+ @ClassRule
+ public static Orchestrator ORCHESTRATOR = Category2Suite.ORCHESTRATOR;
+
+ @Rule
+ public UserRule userRule = UserRule.from(ORCHESTRATOR);
+
+ public Navigation nav = Navigation.create(ORCHESTRATOR);
+
+ private String adminUser;
+
+ @BeforeClass
+ public static void prepareData() {
+ ORCHESTRATOR.resetData();
+
+ ItUtils.restoreProfile(ORCHESTRATOR, IssuesPageTest.class.getResource("/issue/with-many-rules.xml"));
+
+ ORCHESTRATOR.getServer().provisionProject(PROJECT_KEY, PROJECT_KEY);
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile(PROJECT_KEY, "xoo", "with-many-rules");
+ runProjectAnalysis(ORCHESTRATOR, "shared/xoo-multi-modules-sample");
+ }
+
+ @Before
+ public void before() {
+ adminUser = userRule.createAdminUser();
+ }
+
+ @Test
+ public void should_display_actions() {
+ IssuesPage page = nav.logIn().submitCredentials(adminUser).openIssues();
+ Issue issue = page.getFirstIssue();
+ issue.shouldAllowAssign().shouldAllowChangeType();
+ }
+
+ @Test
+ public void should_not_display_actions() {
+ Navigation nav = Navigation.create(ORCHESTRATOR);
+ IssuesPage page = nav.openIssues();
+ Issue issue = page.getFirstIssue();
+ issue.shouldNotAllowAssign().shouldNotAllowChangeType();
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/issue/NewIssuesMeasureTest.java b/tests/src/test/java/org/sonarqube/tests/issue/NewIssuesMeasureTest.java
new file mode 100644
index 00000000000..40a5de1ab18
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/issue/NewIssuesMeasureTest.java
@@ -0,0 +1,144 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.issue;
+
+import com.sonar.orchestrator.build.SonarScanner;
+import java.util.Map;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.wsclient.issue.IssueQuery;
+import org.sonarqube.ws.WsMeasures;
+import org.sonarqube.ws.WsMeasures.Measure;
+import util.ItUtils;
+
+import static java.lang.Integer.parseInt;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.getLeakPeriodValue;
+import static util.ItUtils.getMeasuresWithVariationsByMetricKey;
+import static util.ItUtils.projectDir;
+import static util.ItUtils.setServerProperty;
+
+/**
+ * SONAR-4564
+ */
+public class NewIssuesMeasureTest extends AbstractIssueTest {
+
+ @AfterClass
+ public static void resetPeriod() {
+ ItUtils.resetPeriod(ORCHESTRATOR);
+ }
+
+ @Before
+ public void cleanUpAnalysisData() {
+ ORCHESTRATOR.resetData();
+ }
+
+ @Test
+ public void new_issues_measures() throws Exception {
+ setServerProperty(ORCHESTRATOR, "sonar.leak.period", "previous_analysis");
+ ORCHESTRATOR.getServer().provisionProject("sample", "Sample");
+
+ // Execute an analysis in the past with no issue to have a past snapshot
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile("sample", "xoo", "empty");
+ ORCHESTRATOR.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")).setProperty("sonar.projectDate", "2013-01-01"));
+
+ // Execute a analysis now with some issues
+ ItUtils.restoreProfile(ORCHESTRATOR, getClass().getResource("/issue/one-issue-per-line-profile.xml"));
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line-profile");
+ ORCHESTRATOR.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")));
+
+ assertThat(ORCHESTRATOR.getServer().wsClient().issueClient().find(IssueQuery.create()).list()).isNotEmpty();
+ assertThat(getLeakPeriodValue(ORCHESTRATOR, "sample:src/main/xoo/sample/Sample.xoo", "new_violations")).isEqualTo(17);
+
+ // second analysis, with exactly the same profile -> no new issues
+ ORCHESTRATOR.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")));
+
+ assertThat(ORCHESTRATOR.getServer().wsClient().issueClient().find(IssueQuery.create()).list()).isNotEmpty();
+ assertThat(getLeakPeriodValue(ORCHESTRATOR, "sample:src/main/xoo/sample/Sample.xoo", "new_violations")).isZero();
+ }
+
+ @Test
+ public void new_issues_measures_should_be_zero_on_project_when_no_new_issues_since_x_days() throws Exception {
+ setServerProperty(ORCHESTRATOR, "sonar.leak.period", "30");
+ ORCHESTRATOR.getServer().provisionProject("sample", "Sample");
+ ItUtils.restoreProfile(ORCHESTRATOR, getClass().getResource("/issue/one-issue-per-line-profile.xml"));
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line-profile");
+
+ ORCHESTRATOR.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample"))
+ // Analyse a project in the past, with a date older than 30 last days (second period)
+ .setProperty("sonar.projectDate", "2013-01-01"));
+ ORCHESTRATOR.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")));
+
+ // new issues measures should be to 0 on project on 2 periods as new issues has been created
+ assertThat(getLeakPeriodValue(ORCHESTRATOR, "sample", "new_violations")).isZero();
+ }
+
+ /**
+ * SONAR-3647
+ */
+ @Test
+ public void new_issues_measures_consistent_with_variations() throws Exception {
+ setServerProperty(ORCHESTRATOR, "sonar.leak.period", "previous_analysis");
+ ORCHESTRATOR.getServer().provisionProject("sample", "Sample");
+ ItUtils.restoreProfile(ORCHESTRATOR, getClass().getResource("/issue/one-issue-per-line-profile.xml"));
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line-profile");
+
+ // Execute an analysis in the past to have a past snapshot
+ // version 1
+ ORCHESTRATOR.executeBuilds(SonarScanner.create(projectDir("shared/xoo-history-v1")));
+
+ // version 2 with 2 new violations and 3 more ncloc
+ ORCHESTRATOR.executeBuilds(SonarScanner.create(projectDir("shared/xoo-history-v2")));
+
+ assertThat(ORCHESTRATOR.getServer().wsClient().issueClient().find(IssueQuery.create()).list()).isNotEmpty();
+
+ Map<String, Measure> measures = getMeasuresWithVariationsByMetricKey(ORCHESTRATOR, "sample", "new_violations", "violations", "ncloc");
+ assertThat(measures.get("new_violations").getPeriods().getPeriodsValueList()).extracting(WsMeasures.PeriodValue::getValue).containsOnly("17");
+
+ Measure violations = measures.get("violations");
+ assertThat(parseInt(violations.getValue())).isEqualTo(43);
+ assertThat(violations.getPeriods().getPeriodsValueList()).extracting(periodValue -> parseInt(periodValue.getValue())).containsOnly(17);
+
+ Measure ncloc = measures.get("ncloc");
+ assertThat(parseInt(ncloc.getValue())).isEqualTo(40);
+ assertThat(ncloc.getPeriods().getPeriodsValueList()).extracting(periodValue -> parseInt(periodValue.getValue())).containsOnly(16);
+ }
+
+ @Test
+ public void new_issues_measures_should_be_correctly_calculated_when_adding_a_new_module() throws Exception {
+ setServerProperty(ORCHESTRATOR, "sonar.leak.period", "previous_analysis");
+ ORCHESTRATOR.getServer().provisionProject("com.sonarsource.it.samples:multi-modules-sample", "com.sonarsource.it.samples:multi-modules-sample");
+
+ // First analysis without module b
+ ItUtils.restoreProfile(ORCHESTRATOR, getClass().getResource("/issue/NewIssuesMeasureTest/profile1.xml"));
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile("com.sonarsource.it.samples:multi-modules-sample", "xoo", "profile1");
+ ORCHESTRATOR.executeBuild(SonarScanner.create(projectDir("shared/xoo-multi-modules-sample"))
+ .setProperties("sonar.modules", "module_a"));
+
+ // Second analysis with module b and with a new rule activated to have new issues on module a since last analysis
+ ItUtils.restoreProfile(ORCHESTRATOR, getClass().getResource("/issue/NewIssuesMeasureTest/profile2.xml"));
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile("com.sonarsource.it.samples:multi-modules-sample", "xoo", "profile2");
+ ORCHESTRATOR.executeBuild(SonarScanner.create(projectDir("shared/xoo-multi-modules-sample")));
+
+ assertThat(getLeakPeriodValue(ORCHESTRATOR, "com.sonarsource.it.samples:multi-modules-sample", "new_violations")).isEqualTo(65);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/issue/OrganizationIssueAssignTest.java b/tests/src/test/java/org/sonarqube/tests/issue/OrganizationIssueAssignTest.java
new file mode 100644
index 00000000000..436e466454d
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/issue/OrganizationIssueAssignTest.java
@@ -0,0 +1,219 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+
+package org.sonarqube.tests.issue;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category6Suite;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.Issues;
+import org.sonarqube.ws.Issues.Issue;
+import org.sonarqube.ws.Organizations;
+import org.sonarqube.ws.WsUsers.CreateWsResponse.User;
+import org.sonarqube.ws.client.issue.AssignRequest;
+import org.sonarqube.ws.client.issue.BulkChangeRequest;
+import org.sonarqube.ws.client.issue.SearchWsRequest;
+import org.sonarqube.ws.client.project.CreateRequest;
+import org.sonarqube.ws.client.qualityprofile.AddProjectRequest;
+import org.sonarqube.pageobjects.issues.IssuesPage;
+import util.issue.IssueRule;
+
+import static java.lang.String.format;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.expectHttpError;
+import static util.ItUtils.restoreProfile;
+import static util.ItUtils.runProjectAnalysis;
+import static util.ItUtils.setServerProperty;
+
+public class OrganizationIssueAssignTest {
+
+ private final static String SAMPLE_PROJECT_KEY = "sample";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category6Suite.ORCHESTRATOR;
+ @Rule
+ public Tester tester = new Tester(orchestrator);
+
+ @Rule
+ public IssueRule issueRule = IssueRule.from(orchestrator);
+
+ private Organizations.Organization org1;
+ private Organizations.Organization org2;
+ private User user;
+
+ @Before
+ public void setUp() throws Exception {
+ org1 = tester.organizations().generate();
+ org2 = tester.organizations().generate();
+ user = tester.users().generate();
+ restoreProfile(orchestrator, getClass().getResource("/organization/IssueAssignTest/one-issue-per-file-profile.xml"), org1.getKey());
+ }
+
+ @Test
+ public void auto_assign_issues_to_user_if_default_assignee_is_member_of_project_organization() {
+ tester.organizations().addMember(org1, user);
+
+ provisionProject(SAMPLE_PROJECT_KEY, org1.getKey());
+ setServerProperty(orchestrator, "sample", "sonar.issues.defaultAssigneeLogin", user.getLogin());
+
+ analyseProject(SAMPLE_PROJECT_KEY, org1.getKey());
+
+ assertThat(issueRule.getRandomIssue().getAssignee()).isEqualTo(user.getLogin());
+ }
+
+ @Test
+ public void does_not_auto_assign_issues_to_user_if_default_assignee_is_not_member_of_project_organization() {
+ tester.organizations().addMember(org2, user);
+ provisionProject(SAMPLE_PROJECT_KEY, org1.getKey());
+ setServerProperty(orchestrator, "sample", "sonar.issues.defaultAssigneeLogin", user.getLogin());
+
+ analyseProject(SAMPLE_PROJECT_KEY, org1.getKey());
+
+ assertThat(issueRule.getRandomIssue().hasAssignee()).isFalse();
+ }
+
+ @Test
+ public void assign_issue_to_user_being_member_of_same_organization_as_project_issue_organization() {
+ tester.organizations().addMember(org1, user);
+ provisionAndAnalyseProject(SAMPLE_PROJECT_KEY, org1.getKey());
+ Issue issue = issueRule.getRandomIssue();
+
+ assignIssueTo(issue, user);
+
+ assertThat(issueRule.getByKey(issue.getKey()).getAssignee()).isEqualTo(user.getLogin());
+ }
+
+ @Test
+ public void fail_to_assign_issue_to_user_not_being_member_of_same_organization_as_project_issue_organization() {
+ tester.organizations().addMember(org2, user);
+ provisionAndAnalyseProject(SAMPLE_PROJECT_KEY, org1.getKey());
+ Issue issue = issueRule.getRandomIssue();
+
+ expectHttpError(400,
+ format("User '%s' is not member of organization '%s'", user.getLogin(), org1.getKey()),
+ () -> assignIssueTo(issue, user));
+ }
+
+ @Test
+ public void bulk_assign_issues_to_user_being_only_member_of_same_organization_as_project_issue_organization() {
+ restoreProfile(orchestrator, getClass().getResource("/organization/IssueAssignTest/one-issue-per-file-profile.xml"), org2.getKey());
+ // User is only member of org1, not of org2
+ tester.organizations().addMember(org1, user);
+ provisionAndAnalyseProject(SAMPLE_PROJECT_KEY, org1.getKey());
+ provisionAndAnalyseProject("sample2", org2.getKey());
+ List<String> issues = issueRule.search(new org.sonarqube.ws.client.issue.SearchWsRequest()).getIssuesList().stream().map(Issue::getKey).collect(Collectors.toList());
+
+ Issues.BulkChangeWsResponse response = tester.wsClient().issues()
+ .bulkChange(BulkChangeRequest.builder().setIssues(issues).setAssign(user.getLogin()).build());
+
+ assertThat(response.getIgnored()).isGreaterThan(0);
+ assertThat(issueRule.search(new SearchWsRequest().setProjectKeys(singletonList("sample"))).getIssuesList()).extracting(Issue::getAssignee)
+ .containsOnly(user.getLogin());
+ assertThat(issueRule.search(new SearchWsRequest().setProjectKeys(singletonList("sample2"))).getIssuesList()).extracting(Issue::hasAssignee)
+ .containsOnly(false);
+ }
+
+ @Test
+ public void single_assign_search_show_only_members_in_global_issues() {
+ tester.organizations().addMember(org1, user);
+ User otherUser = tester.users().generate();
+ provisionAndAnalyseProject(SAMPLE_PROJECT_KEY, org1.getKey());
+ IssuesPage page = tester.openBrowser().logIn().submitCredentials(user.getLogin()).openIssues();
+ page.getFirstIssue()
+ .shouldAllowAssign()
+ .assigneeSearchResultCount(otherUser.getLogin(), 0)
+ .assigneeSearchResultCount(user.getLogin(), 1);
+ }
+
+ @Test
+ public void bulk_assign_search_only_members_of_organization_in_project_issues() {
+ tester.organizations().addMember(org1, user);
+ User otherUser = tester.users().generate();
+
+ provisionAndAnalyseProject(SAMPLE_PROJECT_KEY, org1.getKey());
+ IssuesPage page = tester.openBrowser()
+ .logIn().submitCredentials(user.getLogin())
+ .openComponentIssues(SAMPLE_PROJECT_KEY);
+ page
+ .bulkChangeOpen()
+ .bulkChangeAssigneeSearchCount(user.getLogin(), 1)
+ .bulkChangeAssigneeSearchCount(otherUser.getLogin(), 0);
+ }
+
+ @Test
+ public void bulk_assign_search_all_users_in_global_issues() {
+ tester.organizations().addMember(org1, user);
+ User otherUser = tester.users().generate();
+ provisionAndAnalyseProject(SAMPLE_PROJECT_KEY, org1.getKey());
+ IssuesPage page = tester.openBrowser()
+ .logIn().submitCredentials(user.getLogin())
+ .openIssues();
+ page
+ .bulkChangeOpen()
+ .bulkChangeAssigneeSearchCount(user.getLogin(), 1)
+ .bulkChangeAssigneeSearchCount(otherUser.getLogin(), 1);
+ }
+
+ private void provisionAndAnalyseProject(String projectKey, String organization) {
+ provisionProject(projectKey, organization);
+ analyseProject(projectKey, organization);
+ }
+
+ private void provisionProject(String projectKey, String organization) {
+ tester.wsClient().projects().create(
+ CreateRequest.builder()
+ .setKey(projectKey)
+ .setName(projectKey)
+ .setOrganization(organization)
+ .build());
+ }
+
+ private void analyseProject(String projectKey, String organization) {
+ addQualityProfileToProject(organization, projectKey);
+ runProjectAnalysis(orchestrator, "issue/xoo-with-scm",
+ "sonar.projectKey", projectKey,
+ "sonar.organization", organization,
+ "sonar.login", "admin",
+ "sonar.password", "admin",
+ "sonar.scm.disabled", "false",
+ "sonar.scm.provider", "xoo");
+ }
+
+ private void addQualityProfileToProject(String organization, String projectKey) {
+ tester.wsClient().qualityProfiles().addProject(
+ AddProjectRequest.builder()
+ .setProjectKey(projectKey)
+ .setOrganization(organization)
+ .setLanguage("xoo")
+ .setProfileName("one-issue-per-file-profile")
+ .build());
+ }
+
+ private Issues.Operation assignIssueTo(Issue issue, User u) {
+ return tester.wsClient().issues().assign(new AssignRequest(issue.getKey(), u.getLogin()));
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/issueFilter/ToDoTest.java b/tests/src/test/java/org/sonarqube/tests/issueFilter/ToDoTest.java
new file mode 100644
index 00000000000..9b49441ec18
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/issueFilter/ToDoTest.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.issueFilter;
+
+public class ToDoTest {
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/lite/LiteSuite.java b/tests/src/test/java/org/sonarqube/tests/lite/LiteSuite.java
new file mode 100644
index 00000000000..b362cd9dfb3
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/lite/LiteSuite.java
@@ -0,0 +1,31 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.lite;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+ LiteTest.class
+})
+public class LiteSuite {
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/lite/LiteTest.java b/tests/src/test/java/org/sonarqube/tests/lite/LiteTest.java
new file mode 100644
index 00000000000..e60adfdbc2b
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/lite/LiteTest.java
@@ -0,0 +1,108 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.lite;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.Issues;
+import org.sonarqube.ws.WsComponents;
+import org.sonarqube.ws.WsMeasures;
+import org.sonarqube.ws.client.component.TreeWsRequest;
+import org.sonarqube.ws.client.issue.IssuesService;
+import org.sonarqube.ws.client.issue.SearchWsRequest;
+import org.sonarqube.ws.client.measure.ComponentTreeWsRequest;
+import org.sonarqube.ws.client.measure.ComponentWsRequest;
+import org.sonarqube.ws.client.measure.MeasuresService;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.runProjectAnalysis;
+import static util.ItUtils.xooPlugin;
+
+public class LiteTest {
+
+ private static final String PROJECT_KEY = "com.sonarsource.it.samples:multi-modules-sample";
+
+ private static Orchestrator orchestrator = Orchestrator.builderEnv()
+ .setOrchestratorProperty("sonar.web.context", "/sonarqube")
+ .addPlugin(xooPlugin())
+ .build();
+
+ private static Tester tester = new Tester(orchestrator);
+
+ @ClassRule
+ public static RuleChain ruleChain = RuleChain.outerRule(orchestrator)
+ .around(tester);
+
+ @BeforeClass
+ public static void setUp() {
+ runProjectAnalysis(orchestrator, "shared/xoo-multi-modules-sample");
+ }
+
+ @Test
+ public void call_issues_ws() {
+ // all issues
+ IssuesService issuesService = tester.wsClient().issues();
+ Issues.SearchWsResponse response = issuesService.search(new SearchWsRequest());
+ assertThat(response.getIssuesCount()).isGreaterThan(0);
+
+ // project issues
+ response = issuesService.search(new SearchWsRequest().setProjectKeys(singletonList(PROJECT_KEY)));
+ assertThat(response.getIssuesCount()).isGreaterThan(0);
+ }
+
+ @Test
+ public void call_components_ws() {
+ // files in project
+ WsComponents.TreeWsResponse tree = tester.wsClient().components().tree(new TreeWsRequest()
+ .setBaseComponentKey(PROJECT_KEY)
+ .setQualifiers(singletonList("FIL")));
+ assertThat(tree.getComponentsCount()).isEqualTo(4);
+ tree.getComponentsList().forEach(c -> {
+ assertThat(c.getQualifier()).isEqualTo("FIL");
+ assertThat(c.getName()).endsWith(".xoo");
+ });
+ }
+
+ @Test
+ public void call_measures_ws() {
+ // project measures
+ MeasuresService measuresService = tester.wsClient().measures();
+ WsMeasures.ComponentWsResponse component = measuresService.component(new ComponentWsRequest()
+ .setComponentKey(PROJECT_KEY)
+ .setMetricKeys(asList("lines", "ncloc", "files")));
+ assertThat(component.getComponent().getMeasuresCount()).isEqualTo(3);
+
+ // file measures
+ WsMeasures.ComponentTreeWsResponse tree = measuresService.componentTree(new ComponentTreeWsRequest()
+ .setBaseComponentKey(PROJECT_KEY)
+ .setQualifiers(singletonList("FIL"))
+ .setMetricKeys(asList("lines", "ncloc")));
+ assertThat(tree.getComponentsCount()).isEqualTo(4);
+ tree.getComponentsList().forEach(c -> {
+ assertThat(c.getMeasuresList()).extracting(m -> m.getMetric()).containsOnly("lines", "ncloc");
+ });
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/measure/DecimalScaleMetricTest.java b/tests/src/test/java/org/sonarqube/tests/measure/DecimalScaleMetricTest.java
new file mode 100644
index 00000000000..448c9554402
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/measure/DecimalScaleMetricTest.java
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.measure;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category3Suite;
+import org.junit.ClassRule;
+import org.junit.Test;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.getMeasureAsDouble;
+
+/**
+ * SONAR-6939
+ */
+public class DecimalScaleMetricTest {
+
+ /**
+ * Requires the plugin "batch-plugin"
+ */
+ @ClassRule
+ public static Orchestrator orchestrator = Category3Suite.ORCHESTRATOR;
+
+ @Test
+ public void override_decimal_scale_of_numeric_metric() {
+ String projectKey = "DecimalScaleMetricTest.override_decimal_scale_of_numeric_metric";
+ // see DecimalScaleMetric
+ String metricKey = "decimal_scale";
+ ItUtils.runProjectAnalysis(orchestrator, "shared/xoo-sample",
+ "sonar.projectKey", projectKey,
+ "sonar.scanner.feedDecimalScaleMetric", String.valueOf(true));
+
+ assertThat(getMeasureAsDouble(orchestrator, projectKey, metricKey)).isEqualTo(0.0001);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/measure/DifferentialPeriodsTest.java b/tests/src/test/java/org/sonarqube/tests/measure/DifferentialPeriodsTest.java
new file mode 100644
index 00000000000..0597e0e8e97
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/measure/DifferentialPeriodsTest.java
@@ -0,0 +1,177 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.measure;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category1Suite;
+import java.util.Date;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.pageobjects.Navigation;
+import util.ItUtils;
+import util.user.UserRule;
+
+import static org.apache.commons.lang.time.DateUtils.addDays;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.formatDate;
+import static util.ItUtils.getLeakPeriodValue;
+import static util.ItUtils.getMeasuresAsDoubleByMetricKey;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.resetPeriod;
+import static util.ItUtils.runProjectAnalysis;
+import static util.ItUtils.setServerProperty;
+
+public class DifferentialPeriodsTest {
+
+ static final String PROJECT_KEY = "sample";
+ static final String MULTI_MODULE_PROJECT_KEY = "com.sonarsource.it.samples:multi-modules-sample";
+
+ static WsClient CLIENT;
+
+ @ClassRule
+ public static final Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ @Rule
+ public UserRule userRule = UserRule.from(orchestrator);
+
+ private String adminUser;
+
+ @BeforeClass
+ public static void createWsClient() throws Exception {
+ CLIENT = newAdminWsClient(orchestrator);
+ }
+
+ @Before
+ public void cleanUpAnalysisData() {
+ orchestrator.resetData();
+ adminUser = userRule.createAdminUser();
+ }
+
+ @After
+ public void reset() throws Exception {
+ resetPeriod(orchestrator);
+ }
+
+ /**
+ * SONAR-7093
+ */
+ @Test
+ public void ensure_leak_period_defined_at_project_level_is_taken_into_account() throws Exception {
+ orchestrator.getServer().provisionProject(PROJECT_KEY, PROJECT_KEY);
+
+ // Set a global property and a project property to ensure project property is used
+ setServerProperty(orchestrator, "sonar.leak.period", "previous_analysis");
+ setServerProperty(orchestrator, PROJECT_KEY, "sonar.leak.period", "30");
+
+ // Execute an analysis in the past to have a past snapshot without any issues
+ orchestrator.getServer().associateProjectToQualityProfile(PROJECT_KEY, "xoo", "empty");
+ runProjectAnalysis(orchestrator, "shared/xoo-sample", "sonar.projectDate", formatDate(addDays(new Date(), -15)));
+
+ // Second analysis -> issues will be created
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/measure/one-issue-per-line-profile.xml"));
+ orchestrator.getServer().associateProjectToQualityProfile(PROJECT_KEY, "xoo", "one-issue-per-line");
+ runProjectAnalysis(orchestrator, "shared/xoo-sample");
+
+ // Third analysis -> There's no new issue from previous analysis
+ runProjectAnalysis(orchestrator, "shared/xoo-sample");
+
+ // Project should have 17 new issues for leak period
+ assertThat(getLeakPeriodValue(orchestrator, PROJECT_KEY, "violations")).isEqualTo(17);
+
+ // Check on ui that it's possible to define leak period on project
+ Navigation.create(orchestrator).openHome().logIn().submitCredentials(adminUser).openSettings("sample")
+ .assertSettingDisplayed("sonar.leak.period");
+ }
+
+ /**
+ * SONAR-7237
+ */
+ @Test
+ public void ensure_differential_measures_are_computed_when_adding_new_component_after_period() throws Exception {
+ orchestrator.getServer().provisionProject(MULTI_MODULE_PROJECT_KEY, MULTI_MODULE_PROJECT_KEY);
+ setServerProperty(orchestrator, MULTI_MODULE_PROJECT_KEY, "sonar.leak.period", "30");
+
+ // Execute an analysis 60 days ago without module b
+ orchestrator.getServer().associateProjectToQualityProfile(MULTI_MODULE_PROJECT_KEY, "xoo", "empty");
+ runProjectAnalysis(orchestrator, "shared/xoo-multi-modules-sample",
+ "sonar.projectDate", formatDate(addDays(new Date(), -60)),
+ "sonar.modules", "module_a");
+
+ // Second analysis, 20 days ago, issues will be created
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/measure/one-issue-per-line-profile.xml"));
+ orchestrator.getServer().associateProjectToQualityProfile(MULTI_MODULE_PROJECT_KEY, "xoo", "one-issue-per-line");
+ runProjectAnalysis(orchestrator, "shared/xoo-multi-modules-sample",
+ "sonar.projectDate", formatDate(addDays(new Date(), -20)),
+ "sonar.modules", "module_a,module_b");
+
+ // Variation on module b should exist
+ assertThat(getLeakPeriodValue(orchestrator, MULTI_MODULE_PROJECT_KEY + ":module_b", "ncloc")).isEqualTo(24);
+ }
+
+ @Test
+ public void compute_no_new_lines_measures_when_changes_but_no_scm() throws Exception {
+ orchestrator.getServer().provisionProject(MULTI_MODULE_PROJECT_KEY, MULTI_MODULE_PROJECT_KEY);
+ setServerProperty(orchestrator, MULTI_MODULE_PROJECT_KEY, "sonar.leak.period", "previous_analysis");
+
+ // Execute an analysis 60 days ago without module b
+ orchestrator.getServer().associateProjectToQualityProfile(MULTI_MODULE_PROJECT_KEY, "xoo", "empty");
+ runProjectAnalysis(orchestrator, "shared/xoo-multi-modules-sample",
+ "sonar.projectDate", formatDate(addDays(new Date(), -60)),
+ "sonar.modules", "module_a");
+
+ // Second analysis, 20 days ago
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/measure/one-issue-per-line-profile.xml"));
+ orchestrator.getServer().associateProjectToQualityProfile(MULTI_MODULE_PROJECT_KEY, "xoo", "one-issue-per-line");
+ runProjectAnalysis(orchestrator, "shared/xoo-multi-modules-sample",
+ "sonar.projectDate", formatDate(addDays(new Date(), -20)),
+ "sonar.modules", "module_a,module_b");
+
+ // No new lines measure
+ assertNoMeasures(MULTI_MODULE_PROJECT_KEY, "new_lines", "new_lines_to_cover");
+ }
+
+ @Test
+ public void compute_zero_new_lines_measures_when_no_changes_and_scm_available() throws Exception {
+ String projectKey = "sample-scm";
+ orchestrator.getServer().provisionProject(projectKey, projectKey);
+ setServerProperty(orchestrator, projectKey, "sonar.leak.period", "previous_analysis");
+
+ // Execute an analysis 60 days ago
+ runProjectAnalysis(orchestrator, "scm/xoo-sample-with-scm", "sonar.projectDate", formatDate(addDays(new Date(), -60)),
+ "sonar.scm.provider", "xoo", "sonar.scm.disabled", "false");
+
+ // Second analysis, 20 days ago
+ runProjectAnalysis(orchestrator, "scm/xoo-sample-with-scm", "sonar.projectDate", formatDate(addDays(new Date(), -20)),
+ "sonar.scm.provider", "xoo", "sonar.scm.disabled", "false");
+
+ // New lines measures is zero
+ assertThat(getLeakPeriodValue(orchestrator, projectKey, "new_lines")).isEqualTo(0);
+ assertThat(getLeakPeriodValue(orchestrator, projectKey, "new_lines_to_cover")).isEqualTo(0);
+ }
+
+ private void assertNoMeasures(String projectKey, String... metrics) {
+ assertThat(getMeasuresAsDoubleByMetricKey(orchestrator, projectKey, metrics)).isEmpty();
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/measure/MeasuresWsTest.java b/tests/src/test/java/org/sonarqube/tests/measure/MeasuresWsTest.java
new file mode 100644
index 00000000000..53e20f39bc0
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/measure/MeasuresWsTest.java
@@ -0,0 +1,174 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.measure;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category1Suite;
+import java.util.List;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.WsMeasures;
+import org.sonarqube.ws.WsMeasures.ComponentTreeWsResponse;
+import org.sonarqube.ws.WsMeasures.ComponentWsResponse;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.measure.ComponentTreeWsRequest;
+import org.sonarqube.ws.client.measure.ComponentWsRequest;
+import util.ItUtils;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.projectDir;
+import static util.ItUtils.setServerProperty;
+
+public class MeasuresWsTest {
+ @ClassRule
+ public static final Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ private static final String FILE_KEY = "sample:src/main/xoo/sample/Sample.xoo";
+ private static final String DIR_KEY = "sample:src/main/xoo/sample";
+ WsClient wsClient;
+
+ @BeforeClass
+ public static void initPeriod() throws Exception {
+ setServerProperty(orchestrator, "sonar.leak.period", "previous_analysis");
+ }
+
+ @AfterClass
+ public static void resetPeriod() throws Exception {
+ ItUtils.resetPeriod(orchestrator);
+ }
+
+ @Before
+ public void inspectProject() {
+ orchestrator.resetData();
+
+ wsClient = ItUtils.newAdminWsClient(orchestrator);
+ }
+
+ @Test
+ public void component_tree() {
+ scanXooSample();
+
+ ComponentTreeWsResponse response = wsClient.measures().componentTree(new ComponentTreeWsRequest()
+ .setBaseComponentKey("sample")
+ .setMetricKeys(singletonList("ncloc"))
+ .setAdditionalFields(newArrayList("metrics", "periods")));
+
+ assertThat(response).isNotNull();
+ assertThat(response.getBaseComponent().getKey()).isEqualTo("sample");
+ assertThat(response.getMetrics().getMetricsList()).extracting("key").containsOnly("ncloc");
+ List<WsMeasures.Component> components = response.getComponentsList();
+ assertThat(components).hasSize(2).extracting("key").containsOnly(DIR_KEY, FILE_KEY);
+ assertThat(components.get(0).getMeasuresList().get(0).getValue()).isEqualTo("13");
+ }
+
+ /**
+ * @see SONAR-7958
+ */
+ @Test
+ public void component_tree_supports_module_move_down() {
+ String projectKey = "sample";
+ String newModuleKey = "sample:new_module";
+ String moduleAKey = "module_a";
+ String dirKey = "module_a:src/main/xoo/sample";
+ String fileKey = "module_a:src/main/xoo/sample/Sample.xoo";
+
+ scanXooSampleModuleMoveV1();
+
+ verifyComponentTreeWithChildren(projectKey, moduleAKey);
+ verifyComponentTreeWithChildren(moduleAKey, dirKey);
+ verifyComponentTreeWithChildren(dirKey, fileKey);
+
+ scanXooSampleModuleMoveV2();
+
+ verifyComponentTreeWithChildren(projectKey, newModuleKey);
+ verifyComponentTreeWithChildren(newModuleKey, moduleAKey);
+ verifyComponentTreeWithChildren(moduleAKey, dirKey);
+ verifyComponentTreeWithChildren(dirKey, fileKey);
+ }
+
+ /**
+ * @see SONAR-7958
+ */
+ @Test
+ public void component_tree_supports_module_move_up() {
+ String projectKey = "sample";
+ String newModuleKey = "sample:new_module";
+ String moduleAKey = "module_a";
+ String dirKey = "module_a:src/main/xoo/sample";
+ String fileKey = "module_a:src/main/xoo/sample/Sample.xoo";
+
+ scanXooSampleModuleMoveV2();
+
+ verifyComponentTreeWithChildren(projectKey, newModuleKey);
+ verifyComponentTreeWithChildren(newModuleKey, moduleAKey);
+ verifyComponentTreeWithChildren(moduleAKey, dirKey);
+ verifyComponentTreeWithChildren(dirKey, fileKey);
+
+ scanXooSampleModuleMoveV1();
+
+ verifyComponentTreeWithChildren(projectKey, moduleAKey);
+ verifyComponentTreeWithChildren(moduleAKey, dirKey);
+ verifyComponentTreeWithChildren(dirKey, fileKey);
+ }
+
+ private void verifyComponentTreeWithChildren(String baseComponentKey, String... childKeys) {
+ ComponentTreeWsResponse response = wsClient.measures().componentTree(new ComponentTreeWsRequest()
+ .setBaseComponentKey(baseComponentKey)
+ .setMetricKeys(singletonList("ncloc"))
+ .setStrategy("children"));
+
+ assertThat(response.getBaseComponent().getKey()).isEqualTo(baseComponentKey);
+ assertThat(response.getComponentsList())
+ .extracting("key").containsOnly(childKeys);
+ }
+
+ @Test
+ public void component() {
+ scanXooSample();
+
+ ComponentWsResponse response = wsClient.measures().component(new ComponentWsRequest()
+ .setComponentKey("sample")
+ .setMetricKeys(singletonList("ncloc"))
+ .setAdditionalFields(newArrayList("metrics", "periods")));
+
+ WsMeasures.Component component = response.getComponent();
+ assertThat(component.getKey()).isEqualTo("sample");
+ assertThat(component.getMeasuresList()).isNotEmpty();
+ assertThat(response.getMetrics().getMetricsList()).extracting("key").containsOnly("ncloc");
+ }
+
+ private void scanXooSample() {
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")));
+ }
+
+ private void scanXooSampleModuleMoveV1() {
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample-module-move-v1")));
+ }
+
+ private void scanXooSampleModuleMoveV2() {
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample-module-move-v2")));
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/measure/ProjectDashboardTest.java b/tests/src/test/java/org/sonarqube/tests/measure/ProjectDashboardTest.java
new file mode 100644
index 00000000000..584938877da
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/measure/ProjectDashboardTest.java
@@ -0,0 +1,140 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.measure;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category1Suite;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.openqa.selenium.Keys;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.pageobjects.Navigation;
+import org.sonarqube.pageobjects.ProjectDashboardPage;
+import util.user.UserRule;
+
+import static com.codeborne.selenide.Condition.hasText;
+import static com.codeborne.selenide.Condition.text;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.projectDir;
+import static util.selenium.Selenese.runSelenese;
+
+public class ProjectDashboardTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ @Rule
+ public UserRule userRule = UserRule.from(orchestrator);
+
+ private Navigation nav = Navigation.create(orchestrator);
+
+ private static WsClient wsClient;
+ private String adminUser;
+
+ @Before
+ public void setUp() throws Exception {
+ wsClient = newAdminWsClient(orchestrator);
+ orchestrator.resetData();
+ adminUser = userRule.createAdminUser();
+ }
+
+ @Test
+ public void after_first_analysis() throws Exception {
+ executeBuild("shared/xoo-sample", "project-for-overview", "Project For Overview");
+
+ runSelenese(orchestrator, "/measure/ProjectDashboardTest/test_project_overview_after_first_analysis.html");
+ }
+
+ @Test
+ public void display_size() {
+ executeBuild("shared/xoo-sample", "sample", "Sample");
+
+ ProjectDashboardPage page = Navigation.create(orchestrator).openProjectDashboard("sample");
+
+ page.getLinesOfCode().should(hasText("13"));
+ page.getLanguageDistribution().should(hasText("Xoo"), hasText("13"));
+ }
+
+ @Test
+ public void display_tags_without_edit() {
+ executeBuild("shared/xoo-sample", "sample", "Sample");
+
+ // Add some tags to the project
+ wsClient.wsConnector().call(
+ new PostRequest("api/project_tags/set")
+ .setParam("project", "sample")
+ .setParam("tags", "foo,bar,baz"));
+
+ ProjectDashboardPage page = Navigation.create(orchestrator).openProjectDashboard("sample");
+ page
+ .shouldHaveTags("foo", "bar", "baz")
+ .shouldNotBeEditable();
+ }
+
+ @Test
+ public void display_tags_with_edit() {
+ executeBuild("shared/xoo-sample", "sample-with-tags", "Sample with tags");
+ // Add some tags to another project to have them in the list
+ wsClient.wsConnector().call(
+ new PostRequest("api/project_tags/set")
+ .setParam("project", "sample-with-tags")
+ .setParam("tags", "foo,bar,baz"));
+
+ executeBuild("shared/xoo-sample", "sample", "Sample");
+ ProjectDashboardPage page = nav.logIn().submitCredentials(adminUser).openProjectDashboard("sample");
+ page
+ .shouldHaveTags("No tags")
+ .shouldBeEditable()
+ .openTagEditor()
+ .getTagAtIdx(2).click();
+ page
+ .shouldHaveTags("foo")
+ .sendKeysToTagsInput("test")
+ .getTagAtIdx(0).should(hasText("+ test")).click();
+ page
+ .shouldHaveTags("foo", "test")
+ .getTagAtIdx(1).should(hasText("test"));
+ page
+ .sendKeysToTagsInput(Keys.ENTER)
+ .shouldHaveTags("test");
+ }
+
+ @Test
+ @Ignore("there is no more place to show the error")
+ public void display_a_nice_error_when_requesting_unknown_project() {
+ Navigation nav = Navigation.create(orchestrator);
+ nav.open("/dashboard/index?id=unknown");
+ nav.getErrorMessage().should(text("The requested project does not exist. Either it has never been analyzed successfully or it has been deleted."));
+ // TODO verify that on global homepage
+ }
+
+ private void executeBuild(String projectLocation, String projectKey, String projectName) {
+ orchestrator.executeBuild(
+ SonarScanner.create(projectDir(projectLocation))
+ .setProjectKey(projectKey)
+ .setProjectName(projectName));
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/measure/ProjectMeasuresPageTest.java b/tests/src/test/java/org/sonarqube/tests/measure/ProjectMeasuresPageTest.java
new file mode 100644
index 00000000000..46ee9aafd11
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/measure/ProjectMeasuresPageTest.java
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.measure;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category1Suite;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.pageobjects.Navigation;
+
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+import static com.codeborne.selenide.Selenide.$$;
+import static util.ItUtils.projectDir;
+import static util.selenium.Selenese.runSelenese;
+
+public class ProjectMeasuresPageTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ @BeforeClass
+ public static void inspectProject() {
+ orchestrator.executeBuild(
+ SonarScanner
+ .create(projectDir("shared/xoo-sample"))
+ .setProperty("sonar.projectKey", "project-measures-page-test-project")
+ .setProperty("sonar.projectName", "ProjectMeasuresPageTest Project"));
+
+ // one more time
+ orchestrator.executeBuild(
+ SonarScanner
+ .create(projectDir("shared/xoo-sample"))
+ .setProperty("sonar.projectKey", "project-measures-page-test-project")
+ .setProperty("sonar.projectName", "ProjectMeasuresPageTest Project"));
+ }
+
+ @Test
+ public void should_display_measures_page() {
+ runSelenese(orchestrator, "/measure/ProjectMeasuresPageTest/should_display_measures_page.html");
+ }
+
+ @Test
+ public void should_drilldown_on_list_view() {
+ runSelenese(orchestrator, "/measure/ProjectMeasuresPageTest/should_drilldown_on_list_view.html");
+ }
+
+ @Test
+ public void should_drilldown_on_tree_view() {
+ runSelenese(orchestrator, "/measure/ProjectMeasuresPageTest/should_drilldown_on_tree_view.html");
+ }
+
+ @Test
+ public void should_show_history() {
+ Navigation nav = Navigation.create(orchestrator);
+ nav.open("/component_measures/metric/reliability_rating/history?id=project-measures-page-test-project");
+ $(".line-chart").shouldBe(visible);
+ $$(".line-chart-tick-x").shouldHaveSize(5);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/measure/SincePreviousVersionHistoryTest.java b/tests/src/test/java/org/sonarqube/tests/measure/SincePreviousVersionHistoryTest.java
new file mode 100644
index 00000000000..5dd7e15ca23
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/measure/SincePreviousVersionHistoryTest.java
@@ -0,0 +1,133 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.measure;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category1Suite;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.time.DateUtils;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import util.ItUtils;
+
+import static java.lang.Integer.parseInt;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonarqube.ws.WsMeasures.Measure;
+import static org.sonarqube.ws.WsMeasures.PeriodValue;
+import static util.ItUtils.getLeakPeriodValue;
+import static util.ItUtils.getMeasureWithVariation;
+import static util.ItUtils.projectDir;
+import static util.ItUtils.setServerProperty;
+
+public class SincePreviousVersionHistoryTest {
+
+ private static final String PROJECT = "com.sonarsource.it.samples:multi-modules-sample";
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ @BeforeClass
+ public static void initPeriod() throws Exception {
+ setServerProperty(orchestrator, "sonar.leak.period", "previous_version");
+ }
+
+ @AfterClass
+ public static void resetPeriod() throws Exception {
+ ItUtils.resetPeriod(orchestrator);
+ }
+
+ private static void analyzeProject(String version) {
+ analyzeProject(version, null, null);
+ }
+
+ private static void analyzeProjectWithExclusions(String version, String exclusions) {
+ analyzeProject(version, exclusions, null);
+ }
+
+ private static void analyzeProjectWithDate(String version, String date) {
+ analyzeProject(version, null, date);
+ }
+
+ private static void analyzeProject(String version, @Nullable String exclusions, @Nullable String date) {
+ SonarScanner build = SonarScanner.create(projectDir("shared/xoo-multi-modules-sample"))
+ .setProperties("sonar.projectVersion", version);
+ if (exclusions != null) {
+ build.setProperties("sonar.exclusions", exclusions);
+ }
+ if (date != null) {
+ build.setProperty("sonar.projectDate", date);
+ }
+ orchestrator.executeBuild(build);
+ }
+
+ public static String toStringDate(Date date) {
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+ return sdf.format(date);
+ }
+
+ @Before
+ public void resetData() throws Exception {
+ orchestrator.resetData();
+ }
+
+ /**
+ * SONAR-2496
+ */
+ @Test
+ public void test_since_previous_version_period() {
+ analyzeProjectWithExclusions("0.9", "**/*2.xoo");
+ analyzeProject("1.0-SNAPSHOT");
+ analyzeProject("1.0-SNAPSHOT");
+
+ Measure measure = getMeasureWithVariation(orchestrator, PROJECT, "files");
+
+ // There are 4 files
+ assertThat(parseInt(measure.getValue())).isEqualTo(4);
+ // 2 files were added since the first analysis which was version 0.9
+ assertThat(measure.getPeriods().getPeriodsValueList()).extracting(PeriodValue::getValue).contains("2");
+ }
+
+ /**
+ * SONAR-6356
+ */
+ @Test
+ public void since_previous_version_should_use_first_analysis_when_no_version_found() {
+ Date now = new Date();
+
+ // Analyze project by excluding some files
+ analyzeProject("1.0-SNAPSHOT", "**/*2.xoo", toStringDate(DateUtils.addDays(now, -2)));
+ // No difference measure after first analysis
+ assertThat(getLeakPeriodValue(orchestrator, PROJECT, "files")).isNull();
+
+ analyzeProjectWithDate("1.0-SNAPSHOT", toStringDate(DateUtils.addDays(now, -1)));
+ // No new version, first analysis is used -> 2 new files
+ assertThat(getLeakPeriodValue(orchestrator, PROJECT, "files")).isEqualTo(2);
+
+ analyzeProjectWithDate("1.0-SNAPSHOT", toStringDate(now));
+ // Still no new version, first analysis is used -> 2 new files
+ assertThat(getLeakPeriodValue(orchestrator, PROJECT, "files")).isEqualTo(2);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/measure/SinceXDaysHistoryTest.java b/tests/src/test/java/org/sonarqube/tests/measure/SinceXDaysHistoryTest.java
new file mode 100644
index 00000000000..b37942b817e
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/measure/SinceXDaysHistoryTest.java
@@ -0,0 +1,119 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.measure;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category1Suite;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.time.DateUtils;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.WsMeasures;
+import util.ItUtils;
+
+import static java.lang.Integer.parseInt;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.getMeasureWithVariation;
+import static util.ItUtils.projectDir;
+import static util.ItUtils.setServerProperty;
+
+public class SinceXDaysHistoryTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ private static final String PROJECT = "multi-files-sample";
+
+ @BeforeClass
+ public static void analyseProjectWithHistory() {
+ initPeriod();
+
+ orchestrator.resetData();
+ ItUtils.restoreProfile(orchestrator, SinceXDaysHistoryTest.class.getResource("/measure/one-issue-per-line-profile.xml"));
+ orchestrator.getServer().provisionProject(PROJECT, PROJECT);
+ orchestrator.getServer().associateProjectToQualityProfile(PROJECT, "xoo", "one-issue-per-line");
+
+ // Execute a analysis in the past before since 30 days period -> 0 issue, 0 file
+ analyzeProject("2013-01-01", "**/File1*,**/File2*,**/File3*,**/File4*");
+
+ // Execute a analysis 20 days ago, after since 30 days period -> 16 issues, 1 file
+ analyzeProject(getPastDate(20), "**/File2*,**/File3*,**/File4*");
+
+ // Execute a analysis 10 days ago, after since 30 days period -> 28 issues, 2 files
+ analyzeProject(getPastDate(10), "**/File3*,**/File4*");
+
+ // Execute a analysis in the present with all modules -> 52 issues, 4 files
+ analyzeProject();
+ }
+
+ private static void initPeriod() {
+ setServerProperty(orchestrator, "sonar.leak.period", "30");
+ }
+
+ @AfterClass
+ public static void resetPeriods() throws Exception {
+ ItUtils.resetPeriod(orchestrator);
+ }
+
+ @Test
+ public void check_files_variation() throws Exception {
+ checkMeasure("files", 3);
+ }
+
+ @Test
+ public void check_issues_variation() throws Exception {
+ checkMeasure("violations", 45);
+ }
+
+ @Test
+ public void check_new_issues_measures() throws Exception {
+ checkMeasure("new_violations", 45);
+ }
+
+ private void checkMeasure(String metric, int variation) {
+ WsMeasures.Measure measure = getMeasureWithVariation(orchestrator, PROJECT, metric);
+ assertThat(measure.getPeriods().getPeriodsValueList()).extracting(periodValue -> parseInt(periodValue.getValue())).containsOnly(variation);
+ }
+
+ private static void analyzeProject() {
+ analyzeProject(null, null);
+ }
+
+ private static void analyzeProject(@Nullable String date, @Nullable String exclusions) {
+ SonarScanner runner = SonarScanner.create(projectDir("measureHistory/xoo-multi-files-sample"));
+ if (date != null) {
+ runner.setProperty("sonar.projectDate", date);
+ }
+ if (exclusions != null) {
+ runner.setProperties("sonar.exclusions", exclusions);
+ }
+ orchestrator.executeBuild(runner);
+ }
+
+ private static String getPastDate(int nbPastDays) {
+ return new SimpleDateFormat("yyyy-MM-dd").format(DateUtils.addDays(new Date(), nbPastDays * -1));
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/measure/TimeMachineTest.java b/tests/src/test/java/org/sonarqube/tests/measure/TimeMachineTest.java
new file mode 100644
index 00000000000..90dbd6f9009
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/measure/TimeMachineTest.java
@@ -0,0 +1,163 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.measure;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.BuildResult;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category1Suite;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Map;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.WsMeasures.Measure;
+import org.sonarqube.ws.WsMeasures.SearchHistoryResponse;
+import org.sonarqube.ws.WsMeasures.SearchHistoryResponse.HistoryValue;
+import org.sonarqube.ws.client.measure.MeasuresService;
+import org.sonarqube.ws.client.measure.SearchHistoryRequest;
+import util.ItUtils;
+import util.ItUtils.ComponentNavigation;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.formatDate;
+import static util.ItUtils.getComponentNavigation;
+import static util.ItUtils.getMeasuresByMetricKey;
+import static util.ItUtils.getMeasuresWithVariationsByMetricKey;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.projectDir;
+import static util.ItUtils.setServerProperty;
+
+public class TimeMachineTest {
+
+ private static final String PROJECT = "sample";
+ private static final String FIRST_ANALYSIS_DATE = "2014-10-19";
+ private static final String SECOND_ANALYSIS_DATE = "2014-11-13";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+ private static MeasuresService wsMeasures;
+
+ @BeforeClass
+ public static void initialize() {
+ orchestrator.resetData();
+ initPeriod();
+ ItUtils.restoreProfile(orchestrator, TimeMachineTest.class.getResource("/measure/one-issue-per-line-profile.xml"));
+ orchestrator.getServer().provisionProject("sample", "Sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");
+ analyzeProject("measure/xoo-history-v1", FIRST_ANALYSIS_DATE);
+ analyzeProject("measure/xoo-history-v2", SECOND_ANALYSIS_DATE);
+
+ wsMeasures = newAdminWsClient(orchestrator).measures();
+ }
+
+ private static void initPeriod() {
+ setServerProperty(orchestrator, "sonar.leak.period", "previous_analysis");
+ }
+
+ @AfterClass
+ public static void resetPeriod() throws Exception {
+ ItUtils.resetPeriod(orchestrator);
+ }
+
+ private static BuildResult analyzeProject(String path, String date) {
+ return orchestrator.executeBuild(SonarScanner.create(projectDir(path), "sonar.projectDate", date));
+ }
+
+ @Test
+ public void projectIsAnalyzed() {
+ ComponentNavigation component = getComponentNavigation(orchestrator, PROJECT);
+ assertThat(component.getVersion()).isEqualTo("1.0-SNAPSHOT");
+ assertThat(component.getDate().getMonth()).isEqualTo(10); // November
+ }
+
+ @Test
+ public void testHistoryOfIssues() {
+ SearchHistoryResponse response = searchHistory("blocker_violations", "critical_violations", "info_violations", "major_violations", "minor_violations");
+ assertThat(response.getPaging().getTotal()).isEqualTo(2);
+
+ assertHistory(response, "blocker_violations", "0", "0");
+ assertHistory(response, "critical_violations", "0", "0");
+ assertHistory(response, "info_violations", "0", "0");
+ assertHistory(response, "major_violations", "0", "0");
+ assertHistory(response, "minor_violations", "26", "43");
+ }
+
+ @Test
+ public void testHistoryOfMeasures() {
+ SearchHistoryResponse response = searchHistory("lines", "ncloc");
+
+ assertThat(response.getPaging().getTotal()).isEqualTo(2);
+ assertHistory(response, "lines", "26", "43");
+ assertHistory(response, "ncloc", "24", "40");
+ }
+
+ @Test
+ public void noDataForInterval() {
+ Date now = new Date();
+
+ SearchHistoryResponse response = wsMeasures.searchHistory(SearchHistoryRequest.builder()
+ .setComponent(PROJECT)
+ .setMetrics(singletonList("lines"))
+ .setFrom(formatDate(now))
+ .setTo(formatDate(now))
+ .build());
+
+ assertThat(response.getPaging().getTotal()).isEqualTo(0);
+ assertThat(response.getMeasures(0).getHistoryList()).isEmpty();
+ }
+
+ /**
+ * SONAR-4962
+ */
+ @Test
+ public void measure_variations_are_only_meaningful_when_additional_fields_contains_periods() {
+ Map<String, Measure> measures = getMeasuresWithVariationsByMetricKey(orchestrator, PROJECT, "violations", "new_violations");
+ assertThat(measures.get("violations")).isNotNull();
+ assertThat(measures.get("new_violations")).isNotNull();
+ SearchHistoryResponse response = searchHistory("new_violations");
+ assertThat(response.getMeasures(0).getHistoryCount()).isGreaterThan(0);
+
+ measures = getMeasuresByMetricKey(orchestrator, PROJECT, "violations", "new_violations");
+ assertThat(measures.get("violations")).isNotNull();
+ assertThat(measures.get("new_violations")).isNull();
+ }
+
+ private static SearchHistoryResponse searchHistory(String... metrics) {
+ return wsMeasures.searchHistory(SearchHistoryRequest.builder()
+ .setComponent(PROJECT)
+ .setMetrics(Arrays.asList(metrics))
+ .build());
+ }
+
+ private static void assertHistory(SearchHistoryResponse response, String metric, String... expectedMeasures) {
+ for (SearchHistoryResponse.HistoryMeasure measures : response.getMeasuresList()) {
+ if (metric.equals(measures.getMetric())) {
+ assertThat(measures.getHistoryList()).extracting(HistoryValue::getValue).containsExactly(expectedMeasures);
+ return;
+ }
+ }
+
+ throw new IllegalArgumentException("Metric not found");
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/organization/BillingTest.java b/tests/src/test/java/org/sonarqube/tests/organization/BillingTest.java
new file mode 100644
index 00000000000..22d209954e3
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/organization/BillingTest.java
@@ -0,0 +1,238 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.organization;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.BuildResult;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category6Suite;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.Organizations;
+import org.sonarqube.ws.WsUsers.CreateWsResponse.User;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.WsResponse;
+import org.sonarqube.ws.client.organization.UpdateProjectVisibilityWsRequest;
+import org.sonarqube.ws.client.project.CreateRequest;
+import org.sonarqube.ws.client.project.UpdateVisibilityRequest;
+import org.sonarqube.pageobjects.Navigation;
+import util.ItUtils;
+
+import static java.lang.String.format;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonarqube.ws.WsCe.TaskResponse;
+import static util.ItUtils.expectHttpError;
+import static util.ItUtils.newProjectKey;
+import static util.ItUtils.projectDir;
+import static util.ItUtils.resetSettings;
+import static util.ItUtils.setServerProperty;
+
+public class BillingTest {
+
+ private static final String PROPERTY_PREVENT_ANALYSIS = "sonar.billing.preventProjectAnalysis";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category6Suite.ORCHESTRATOR;
+
+ @Rule
+ public Tester tester = new Tester(orchestrator);
+
+ private Organizations.Organization organization;
+ private User orgAdministrator;
+
+ @Before
+ @After
+ public void reset() {
+ resetSettings(orchestrator, null, PROPERTY_PREVENT_ANALYSIS, "sonar.billing.preventUpdatingProjectsVisibilityToPrivate");
+ }
+
+ @Before
+ public void setUp() {
+ organization = tester.organizations().generate();
+ orgAdministrator = tester.users().generateAdministrator(organization);
+ }
+
+ @Test
+ public void execute_successfully_ce_analysis_on_organization() {
+ setServerProperty(orchestrator, PROPERTY_PREVENT_ANALYSIS, "false");
+
+ String taskUuid = executeAnalysis(newProjectKey());
+
+ TaskResponse taskResponse = tester.wsClient().ce().task(taskUuid);
+ assertThat(taskResponse.getTask().hasErrorMessage()).isFalse();
+ }
+
+ @Test
+ public void fail_to_execute_ce_analysis_on_organization() {
+ setServerProperty(orchestrator, PROPERTY_PREVENT_ANALYSIS, "true");
+
+ String taskUuid = executeAnalysis(newProjectKey());
+
+ TaskResponse taskResponse = tester.wsClient().ce().task(taskUuid);
+ assertThat(taskResponse.getTask().hasErrorMessage()).isTrue();
+ assertThat(taskResponse.getTask().getErrorMessage()).contains(format("Organization %s cannot perform analysis", organization.getKey()));
+ }
+
+ @Test
+ public void api_navigation_organization_returns_canUpdateProjectsVisibilityToPrivate() {
+ User user = tester.users().generate();
+ tester.organizations().addMember(organization, user);
+
+ setServerProperty(orchestrator, "sonar.billing.preventUpdatingProjectsVisibilityToPrivate", "false");
+ assertWsResponseAsAdmin(new GetRequest("api/navigation/organization").setParam("organization", organization.getKey()),
+ "\"canUpdateProjectsVisibilityToPrivate\":true");
+
+ setServerProperty(orchestrator, "sonar.billing.preventUpdatingProjectsVisibilityToPrivate", "true");
+ assertWsResponseAsAdmin(new GetRequest("api/navigation/organization").setParam("organization", organization.getKey()),
+ "\"canUpdateProjectsVisibilityToPrivate\":false");
+
+ setServerProperty(orchestrator, "sonar.billing.preventUpdatingProjectsVisibilityToPrivate", "true");
+ assertWsResponseAsUser(new GetRequest("api/navigation/organization").setParam("organization", organization.getKey()),
+ "\"canUpdateProjectsVisibilityToPrivate\":false", user);
+ }
+
+ @Test
+ public void api_navigation_component_returns_canUpdateProjectVisibilityToPrivate() {
+ String projectKey = createPublicProject();
+
+ setServerProperty(orchestrator, "sonar.billing.preventUpdatingProjectsVisibilityToPrivate", "false");
+ assertWsResponseAsAdmin(new GetRequest("api/navigation/component").setParam("componentKey", projectKey),
+ "\"canUpdateProjectVisibilityToPrivate\":true");
+
+ setServerProperty(orchestrator, "sonar.billing.preventUpdatingProjectsVisibilityToPrivate", "true");
+ assertWsResponseAsAdmin(new GetRequest("api/navigation/component").setParam("componentKey", projectKey),
+ "\"canUpdateProjectVisibilityToPrivate\":false");
+ }
+
+ @Test
+ public void does_not_fail_to_update_default_projects_visibility_to_private() {
+ setServerProperty(orchestrator, "sonar.billing.preventUpdatingProjectsVisibilityToPrivate", "false");
+
+ tester.wsClient().organizations().updateProjectVisibility(UpdateProjectVisibilityWsRequest.builder()
+ .setOrganization(organization.getKey())
+ .setProjectVisibility("private")
+ .build());
+
+ assertWsResponseAsAdmin(new GetRequest("api/navigation/organization").setParam("organization", organization.getKey()),
+ "\"projectVisibility\":\"private\"");
+ }
+
+ @Test
+ public void fail_to_update_organization_default_visibility_to_private() {
+ setServerProperty(orchestrator, "sonar.billing.preventUpdatingProjectsVisibilityToPrivate", "true");
+
+ expectHttpError(400,
+ format("Organization %s cannot use private project", organization.getKey()),
+ () -> tester.wsClient().organizations()
+ .updateProjectVisibility(UpdateProjectVisibilityWsRequest.builder().setOrganization(organization.getKey()).setProjectVisibility("private").build()));
+ }
+
+ @Test
+ public void does_not_fail_to_update_project_visibility_to_private() {
+ String projectKey = createPublicProject();
+ setServerProperty(orchestrator, "sonar.billing.preventUpdatingProjectsVisibilityToPrivate", "false");
+
+ tester.wsClient().projects().updateVisibility(UpdateVisibilityRequest.builder().setProject(projectKey).setVisibility("private").build());
+
+ assertWsResponseAsAdmin(new GetRequest("api/navigation/component").setParam("componentKey", projectKey), "\"visibility\":\"private\"");
+ }
+
+ @Test
+ public void fail_to_update_project_visibility_to_private() {
+ String projectKey = createPublicProject();
+ setServerProperty(orchestrator, "sonar.billing.preventUpdatingProjectsVisibilityToPrivate", "true");
+
+ expectHttpError(400,
+ format("Organization %s cannot use private project", organization.getKey()),
+ () -> tester.wsClient().projects().updateVisibility(UpdateVisibilityRequest.builder().setProject(projectKey).setVisibility("private").build()));
+ }
+
+ @Test
+ public void does_not_fail_to_create_private_project() {
+ String projectKey = newProjectKey();
+ setServerProperty(orchestrator, "sonar.billing.preventUpdatingProjectsVisibilityToPrivate", "false");
+
+ tester.wsClient().projects().create(CreateRequest.builder().setKey(projectKey).setName(projectKey).setOrganization(organization.getKey()).setVisibility("public").build());
+
+ assertWsResponseAsAdmin(new GetRequest("api/navigation/component").setParam("componentKey", projectKey), "\"visibility\":\"public\"");
+ }
+
+ @Test
+ public void fail_to_create_private_project() {
+ String projectKey = newProjectKey();
+ setServerProperty(orchestrator, "sonar.billing.preventUpdatingProjectsVisibilityToPrivate", "true");
+
+ expectHttpError(400,
+ format("Organization %s cannot use private project", organization.getKey()),
+ () -> tester.wsClient().projects()
+ .create(CreateRequest.builder().setKey(projectKey).setName(projectKey).setOrganization(organization.getKey()).setVisibility("private").build()));
+ }
+
+ @Test
+ public void ui_does_not_allow_to_turn_project_to_private() {
+ String projectKey = createPublicProject();
+ setServerProperty(orchestrator, "sonar.billing.preventUpdatingProjectsVisibilityToPrivate", "true");
+
+ Navigation.create(orchestrator)
+ .logIn().submitCredentials(orgAdministrator.getLogin())
+ .openProjectPermissions(projectKey)
+ .shouldBePublic()
+ .shouldNotAllowPrivate();
+ }
+
+ @Test
+ public void ui_allows_to_turn_project_to_private() {
+ String projectKey = createPublicProject();
+ setServerProperty(orchestrator, "sonar.billing.preventUpdatingProjectsVisibilityToPrivate", "false");
+
+ tester.openBrowser()
+ .logIn().submitCredentials(orgAdministrator.getLogin())
+ .openProjectPermissions(projectKey)
+ .shouldBePublic()
+ .turnToPrivate();
+ }
+
+ private String createPublicProject() {
+ return tester.projects().generate(organization).getKey();
+ }
+
+ private String executeAnalysis(String projectKey) {
+ BuildResult buildResult = orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample"),
+ "sonar.organization", organization.getKey(),
+ "sonar.projectKey", projectKey,
+ "sonar.login", orgAdministrator.getLogin(),
+ "sonar.password", orgAdministrator.getLogin()));
+ return ItUtils.extractCeTaskId(buildResult);
+ }
+
+ private void assertWsResponseAsAdmin(GetRequest request, String expectedContent) {
+ WsResponse response = tester.wsClient().wsConnector().call(request).failIfNotSuccessful();
+ assertThat(response.content()).contains(expectedContent);
+ }
+
+ private void assertWsResponseAsUser(GetRequest request, String expectedContent, User user) {
+ WsResponse response = tester.as(user.getLogin()).wsClient().wsConnector().call(request).failIfNotSuccessful();
+ assertThat(response.content()).contains(expectedContent);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/organization/OrganizationMembershipTest.java b/tests/src/test/java/org/sonarqube/tests/organization/OrganizationMembershipTest.java
new file mode 100644
index 00000000000..256b30b7271
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/organization/OrganizationMembershipTest.java
@@ -0,0 +1,133 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+
+package org.sonarqube.tests.organization;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category6Suite;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.Organizations.Organization;
+import org.sonarqube.ws.WsUsers.CreateWsResponse.User;
+import org.sonarqube.ws.client.HttpException;
+import org.sonarqube.ws.client.permission.AddUserWsRequest;
+
+import static util.ItUtils.setServerProperty;
+
+public class OrganizationMembershipTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category6Suite.ORCHESTRATOR;
+
+ @Rule
+ public Tester tester = new Tester(orchestrator);
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @BeforeClass
+ public static void setUp() {
+ setServerProperty(orchestrator, "sonar.organizations.anyoneCanCreate", "true");
+ }
+
+ @AfterClass
+ public static void tearDown() {
+ setServerProperty(orchestrator, "sonar.organizations.anyoneCanCreate", null);
+ }
+
+ @Test
+ public void new_user_should_not_become_member_of_default_organization() {
+ User user = tester.users().generate();
+ tester.organizations().assertThatNotMemberOf(null, user);
+ }
+
+ @Test
+ public void add_and_remove_member() {
+ Organization organization = tester.organizations().generate();
+ User user = tester.users().generate();
+
+ addMembership(organization, user);
+ tester.organizations().assertThatMemberOf(organization, user);
+
+ removeMembership(organization, user);
+ tester.organizations().assertThatNotMemberOf(organization, user);
+ }
+
+ @Test
+ public void remove_organization_admin_member() {
+ Organization organization = tester.organizations().generate();
+ User user = tester.users().generate();
+ addMembership(organization, user);
+
+ tester.wsClient().permissions().addUser(new AddUserWsRequest().setLogin(user.getLogin()).setPermission("admin").setOrganization(organization.getKey()));
+ tester.organizations().assertThatMemberOf(organization, user);
+
+ removeMembership(organization, user);
+ tester.organizations().assertThatNotMemberOf(organization, user);
+ }
+
+ @Test
+ public void fail_to_remove_organization_admin_member_when_last_admin() {
+ Organization organization = tester.organizations().generate();
+ User user = tester.users().generate();
+ addMembership(organization, user);
+
+ tester.wsClient().permissions().addUser(new AddUserWsRequest().setLogin(user.getLogin()).setPermission("admin").setOrganization(organization.getKey()));
+ tester.organizations().assertThatMemberOf(organization, user);
+ // Admin is the creator of the organization so he was granted with admin permission
+ tester.wsClient().organizations().removeMember(organization.getKey(), "admin");
+
+ expectedException.expect(HttpException.class);
+ expectedException.expectMessage("The last administrator member cannot be removed");
+ removeMembership(organization, user);
+ }
+
+ @Test
+ public void remove_user_remove_its_membership() {
+ Organization organization = tester.organizations().generate();
+ User user = tester.users().generate();
+ addMembership(organization, user);
+
+ tester.users().service().deactivate(user.getLogin());
+ tester.organizations().assertThatNotMemberOf(organization, user);
+ }
+
+ @Test
+ public void user_creating_an_organization_becomes_member_of_this_organization() {
+ User user = tester.users().generate();
+
+ Organization organization = tester.as(user.getLogin()).organizations().generate();
+
+ tester.organizations().assertThatMemberOf(organization, user);
+ }
+
+ private void addMembership(Organization organization, User user) {
+ tester.organizations().addMember(organization, user);
+ }
+
+ private void removeMembership(Organization organization, User user) {
+ tester.wsClient().organizations().removeMember(organization.getKey(), user.getLogin());
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/organization/OrganizationMembershipUiTest.java b/tests/src/test/java/org/sonarqube/tests/organization/OrganizationMembershipUiTest.java
new file mode 100644
index 00000000000..eb6b6bb639e
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/organization/OrganizationMembershipUiTest.java
@@ -0,0 +1,179 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+
+package org.sonarqube.tests.organization;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category6Suite;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.tests.OrganizationTester;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.Organizations.Organization;
+import org.sonarqube.ws.WsUsers.CreateWsResponse.User;
+import org.sonarqube.pageobjects.organization.MembersPage;
+
+import static util.ItUtils.setServerProperty;
+
+public class OrganizationMembershipUiTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category6Suite.ORCHESTRATOR;
+
+ @Rule
+ public Tester tester = new Tester(orchestrator);
+
+ private User root;
+
+ @Before
+ public void setUp() {
+ setServerProperty(orchestrator, "sonar.organizations.anyoneCanCreate", "true");
+ root = tester.users().generate();
+ tester.wsClient().roots().setRoot(root.getLogin());
+ }
+
+ @After
+ public void tearDown() {
+ setServerProperty(orchestrator, "sonar.organizations.anyoneCanCreate", null);
+ }
+
+ @Test
+ public void should_display_members_page() {
+ Organization organization = tester.organizations().generate();
+ User member1 = tester.users().generate(p -> p.setName("foo"));
+ addMember(organization, member1);
+ User member2 = tester.users().generate(p -> p.setName("bar"));
+ addMember(organization, member2);
+ User nonMember = tester.users().generate();
+
+ MembersPage page = tester.openBrowser().openOrganizationMembers(organization.getKey());
+ page
+ .canNotAddMember()
+ .shouldHaveTotal(3);
+ page.getMembersByIdx(0).shouldBeNamed("admin", "Administrator");
+ page.getMembersByIdx(1)
+ .shouldBeNamed(member2.getLogin(), member2.getName());
+ page.getMembersByIdx(2)
+ .shouldBeNamed(member1.getLogin(), member1.getName())
+ .shouldNotHaveActions();
+ }
+
+ @Test
+ public void search_for_members() {
+ Organization organization = tester.organizations().generate();
+ User member1 = tester.users().generate(p -> p.setName("foo"));
+ addMember(organization, member1);
+ User member2 = tester.users().generate(p -> p.setName("sameprefixuser1"));
+ addMember(organization, member2);
+ // Created to verify that only the user part of the org is returned
+ User userWithSameNamePrefix = tester.users().generate(p -> p.setName(member2.getName() + "sameprefixuser2"));
+
+ MembersPage page = tester.openBrowser().openOrganizationMembers(organization.getKey());
+ page
+ .searchForMember("sameprefixuser")
+ .shouldHaveTotal(1);
+ page.getMembersByIdx(0).shouldBeNamed(member2.getLogin(), member2.getName());
+ page
+ .searchForMember(member1.getLogin())
+ .shouldHaveTotal(1);
+ page.getMembersByIdx(0).shouldBeNamed(member1.getLogin(), member1.getName());
+ }
+
+ @Test
+ public void admin_can_add_members() {
+ Organization organization = tester.organizations().generate();
+ User user1 = tester.users().generate(u -> u.setLogin("foo"));
+ User user2 = tester.users().generate();
+
+ MembersPage page = tester.openBrowser()
+ .logIn().submitCredentials(root.getLogin())
+ .openOrganizationMembers(organization.getKey());
+ page
+ .shouldHaveTotal(1)
+ .addMember(user1.getLogin())
+ .shouldHaveTotal(2);
+ page.getMembersByIdx(0).shouldBeNamed("admin", "Administrator").shouldHaveGroups(2);
+ page.getMembersByIdx(1).shouldBeNamed(user1.getLogin(), user1.getName()).shouldHaveGroups(1);
+ }
+
+ @Test
+ public void admin_can_remove_members() {
+ Organization organization = tester.organizations().generate();
+ User user1 = tester.users().generate();
+ addMember(organization, user1);
+ User user2 = tester.users().generate();
+ addMember(organization, user2);
+
+ MembersPage page = tester.openBrowser()
+ .logIn().submitCredentials(root.getLogin())
+ .openOrganizationMembers(organization.getKey());
+ page.shouldHaveTotal(3)
+ .getMembersByIdx(1).removeMembership();
+ page.shouldHaveTotal(2);
+ }
+
+ @Test
+ public void admin_can_manage_groups() {
+ Organization organization = tester.organizations().generate();
+ User user = tester.users().generate(u -> u.setLogin("foo"));
+ addMember(organization, user);
+
+ MembersPage page = tester.openBrowser()
+ .logIn().submitCredentials(root.getLogin())
+ .openOrganizationMembers(organization.getKey());
+ // foo user
+ page.getMembersByIdx(1)
+ .manageGroupsOpen()
+ .manageGroupsSelect("owners")
+ .manageGroupsSave()
+ .shouldHaveGroups(2);
+ // admin user
+ page.getMembersByIdx(0)
+ .manageGroupsOpen()
+ .manageGroupsSelect("owners")
+ .manageGroupsSave()
+ .shouldHaveGroups(1);
+ }
+
+ @Test
+ public void groups_count_should_be_updated_when_a_member_was_just_added() {
+ Organization organization = tester.organizations().generate();
+ User user = tester.users().generate();
+
+ MembersPage page = tester.openBrowser()
+ .logIn().submitCredentials(root.getLogin())
+ .openOrganizationMembers(organization.getKey());
+ page
+ .addMember(user.getLogin())
+ .getMembersByIdx(1)
+ .shouldHaveGroups(1)
+ .manageGroupsOpen()
+ .manageGroupsSelect("owners")
+ .manageGroupsSave()
+ .shouldHaveGroups(2);
+ }
+
+ private OrganizationTester addMember(Organization organization, User member1) {
+ return tester.organizations().addMember(organization, member1);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/organization/OrganizationTest.java b/tests/src/test/java/org/sonarqube/tests/organization/OrganizationTest.java
new file mode 100644
index 00000000000..7ea671d5928
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/organization/OrganizationTest.java
@@ -0,0 +1,370 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.organization;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.BuildFailureException;
+import org.sonarqube.tests.Category6Suite;
+import java.util.List;
+import java.util.Locale;
+import org.junit.After;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonarqube.tests.OrganizationTester;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.Organizations.Organization;
+import org.sonarqube.ws.QualityProfiles;
+import org.sonarqube.ws.Rules;
+import org.sonarqube.ws.WsComponents;
+import org.sonarqube.ws.WsUserGroups.Group;
+import org.sonarqube.ws.WsUsers;
+import org.sonarqube.ws.WsUsers.CreateWsResponse.User;
+import org.sonarqube.ws.client.component.ComponentsService;
+import org.sonarqube.ws.client.organization.CreateWsRequest;
+import org.sonarqube.ws.client.organization.OrganizationService;
+import org.sonarqube.ws.client.organization.SearchWsRequest;
+import org.sonarqube.ws.client.organization.UpdateWsRequest;
+import org.sonarqube.ws.client.permission.AddUserWsRequest;
+import org.sonarqube.ws.client.permission.PermissionsService;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static util.ItUtils.expectBadRequestError;
+import static util.ItUtils.expectForbiddenError;
+import static util.ItUtils.expectNotFoundError;
+import static util.ItUtils.expectUnauthorizedError;
+import static util.ItUtils.runProjectAnalysis;
+import static util.ItUtils.setServerProperty;
+
+public class OrganizationTest {
+
+ private static final String SETTING_ANYONE_CAN_CREATE_ORGANIZATIONS = "sonar.organizations.anyoneCanCreate";
+ private static final String DEFAULT_ORGANIZATION_KEY = "default-organization";
+ private static final String NAME = "Foo Company";
+ // private static final String KEY = "foo-company";
+ private static final String DESCRIPTION = "the description of Foo company";
+ private static final String URL = "https://www.foo.fr";
+ private static final String AVATAR_URL = "https://www.foo.fr/corporate_logo.png";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category6Suite.ORCHESTRATOR;
+ @Rule
+ public Tester tester = new Tester(orchestrator);
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @After
+ public void tearDown() {
+ setServerProperty(orchestrator, SETTING_ANYONE_CAN_CREATE_ORGANIZATIONS, null);
+ }
+
+ @Test
+ public void default_organization_should_exist() {
+ Organization defaultOrg = tester.organizations().service().search(SearchWsRequest.builder().build())
+ .getOrganizationsList()
+ .stream()
+ .filter(Organization::getGuarded)
+ .findFirst()
+ .orElseThrow(IllegalStateException::new);
+ assertThat(defaultOrg.getKey()).isEqualTo(DEFAULT_ORGANIZATION_KEY);
+ assertThat(defaultOrg.getName()).isEqualTo("Default Organization");
+ }
+
+ @Test
+ public void default_organization_can_not_be_deleted() {
+ expectBadRequestError(() -> tester.organizations().service().delete(DEFAULT_ORGANIZATION_KEY));
+ }
+
+ @Test
+ public void create_update_and_delete_organizations() {
+ OrganizationService service = tester.organizations().service();
+
+ Organization org = tester.organizations().generate(o -> o
+ .setName(NAME)
+ .setDescription(DESCRIPTION)
+ .setUrl(URL)
+ .setAvatar(AVATAR_URL)
+ .build());
+ assertThat(org.getName()).isEqualTo(NAME);
+ assertThat(org.getDescription()).isEqualTo(DESCRIPTION);
+ assertThat(org.getUrl()).isEqualTo(URL);
+ assertThat(org.getAvatar()).isEqualTo(AVATAR_URL);
+
+ verifyOrganization(org, NAME, DESCRIPTION, URL, AVATAR_URL);
+ assertThatBuiltInQualityProfilesExist(org);
+
+ // update by key
+ service.update(new UpdateWsRequest.Builder()
+ .setKey(org.getKey())
+ .setName("new name")
+ .setDescription("new description")
+ .setUrl("new url")
+ .setAvatar("new avatar url")
+ .build());
+ verifyOrganization(org, "new name", "new description", "new url", "new avatar url");
+
+ // remove optional fields
+ service.update(new UpdateWsRequest.Builder()
+ .setKey(org.getKey())
+ .setName("new name 2")
+ .setDescription("")
+ .setUrl("")
+ .setAvatar("")
+ .build());
+ verifyOrganization(org, "new name 2", null, null, null);
+
+ // delete organization
+ service.delete(org.getKey());
+ assertThatOrganizationDoesNotExit(org);
+ assertThatQualityProfilesDoNotExist(org);
+
+ // create again
+ service.create(new CreateWsRequest.Builder()
+ .setName(NAME)
+ .setKey(org.getKey())
+ .build())
+ .getOrganization();
+ verifyOrganization(org, NAME, null, null, null);
+ }
+
+ @Test
+ public void create_generates_key_from_name() {
+ // create organization without key
+ String name = "Foo Company to keyize";
+ String expectedKey = "foo-company-to-keyize";
+ Organization createdOrganization = tester.organizations().service().create(new CreateWsRequest.Builder()
+ .setName(name)
+ .build())
+ .getOrganization();
+ assertThat(createdOrganization.getKey()).isEqualTo(expectedKey);
+ verifyOrganization(createdOrganization, name, null, null, null);
+ }
+
+ @Test
+ public void anonymous_user_cannot_administrate_organization() {
+ Organization org = tester.organizations().generate();
+ OrganizationTester anonymousTester = tester.asAnonymous().organizations();
+
+ expectForbiddenError(() -> anonymousTester.generate());
+ expectUnauthorizedError(() -> anonymousTester.service().update(new UpdateWsRequest.Builder().setKey(org.getKey()).setName("new name").build()));
+ expectUnauthorizedError(() -> anonymousTester.service().delete(org.getKey()));
+ }
+
+ @Test
+ public void logged_in_user_cannot_administrate_organization() {
+ Organization org = tester.organizations().generate();
+ User user = tester.users().generate();
+ OrganizationTester userTester = tester.as(user.getLogin()).organizations();
+
+ expectForbiddenError(() -> userTester.generate());
+ expectForbiddenError(() -> userTester.service().update(new UpdateWsRequest.Builder().setKey(org.getKey()).setName("new name").build()));
+ expectForbiddenError(() -> userTester.service().delete(org.getKey()));
+ }
+
+ @Test
+ public void logged_in_user_can_administrate_organization_if_root() {
+ User user = tester.users().generate();
+ OrganizationTester asUser = tester.as(user.getLogin()).organizations();
+
+ tester.wsClient().roots().setRoot(user.getLogin());
+ Organization org = asUser.generate();
+
+ // delete org, attempt recreate when no root anymore and ensure it can't anymore
+ asUser.service().delete(org.getKey());
+
+ tester.wsClient().roots().unsetRoot(user.getLogin());
+ expectForbiddenError(() -> asUser.generate());
+ }
+
+ @Test
+ public void an_organization_member_can_analyze_project() {
+ Organization organization = tester.organizations().generate();
+ User user = tester.users().generate();
+ Group group = tester.groups().generate(organization);
+ // users.removeGroups("sonar-users");
+ tester.organizations().service().addMember(organization.getKey(), user.getLogin());
+ addPermissionsToUser(organization.getKey(), user.getLogin(), "provisioning", "scan");
+
+ runProjectAnalysis(orchestrator, "shared/xoo-sample",
+ "sonar.organization", organization.getKey(),
+ "sonar.login", user.getLogin(),
+ "sonar.password", user.getLogin());
+ ComponentsService componentsService = tester.as(user.getLogin()).wsClient().components();
+ assertThat(searchSampleProject(organization.getKey(), componentsService).getComponentsList()).hasSize(1);
+ }
+
+ @Test
+ public void by_default_anonymous_cannot_analyse_project_on_organization() {
+ Organization organization = tester.organizations().generate();
+
+ try {
+ runProjectAnalysis(orchestrator, "shared/xoo-sample",
+ "sonar.organization", organization.getKey());
+ fail();
+ } catch (BuildFailureException e) {
+ assertThat(e.getResult().getLogs()).contains("Insufficient privileges");
+ }
+
+ ComponentsService componentsService = tester.wsClient().components();
+ assertThat(searchSampleProject(organization.getKey(), componentsService).getComponentsCount()).isEqualTo(0);
+ }
+
+ @Test
+ public void by_default_anonymous_can_browse_project_on_organization() {
+ Organization organization = tester.organizations().generate();
+
+ runProjectAnalysis(orchestrator, "shared/xoo-sample", "sonar.organization", organization.getKey(), "sonar.login", "admin", "sonar.password", "admin");
+
+ ComponentsService componentsService = tester.asAnonymous().wsClient().components();
+ assertThat(searchSampleProject(organization.getKey(), componentsService).getComponentsList()).hasSize(1);
+ }
+
+ private void addPermissionsToUser(String orgKeyAndName, String login, String permission, String... otherPermissions) {
+ PermissionsService permissionsService = tester.wsClient().permissions();
+ permissionsService.addUser(new AddUserWsRequest().setLogin(login).setOrganization(orgKeyAndName).setPermission(permission));
+ for (String otherPermission : otherPermissions) {
+ permissionsService.addUser(new AddUserWsRequest().setLogin(login).setOrganization(orgKeyAndName).setPermission(otherPermission));
+ }
+ }
+
+ @Test
+ public void deleting_an_organization_deletes_its_projects() {
+ Organization organization = tester.organizations().generate();
+
+ runProjectAnalysis(orchestrator, "shared/xoo-sample",
+ "sonar.organization", organization.getKey(),
+ "sonar.login", "admin",
+ "sonar.password", "admin");
+ ComponentsService componentsService = tester.wsClient().components();
+ assertThat(searchSampleProject(organization.getKey(), componentsService).getComponentsList()).hasSize(1);
+
+ tester.organizations().service().delete(organization.getKey());
+
+ expectNotFoundError(() -> searchSampleProject(organization.getKey(), componentsService));
+ assertThatOrganizationDoesNotExit(organization);
+ }
+
+ @Test
+ public void return_groups_belonging_to_a_user_on_an_organization() throws Exception {
+ Organization organization = tester.organizations().generate();
+ User user = tester.users().generate();
+ tester.organizations().service().addMember(organization.getKey(), user.getLogin());
+
+ Group group = tester.groups().generate(organization);
+ tester.groups().addMemberToGroups(organization, user.getLogin(), group.getName());
+
+ List<WsUsers.GroupsWsResponse.Group> memberOfGroups = tester.groups().getGroupsOfUser(organization, user.getLogin());
+
+ assertThat(memberOfGroups).extracting(WsUsers.GroupsWsResponse.Group::getName)
+ .containsExactlyInAnyOrder(group.getName(), "Members");
+ }
+
+ @Test
+ public void anonymous_cannot_create_organizations_even_if_anyone_is_allowed_to() {
+ setServerProperty(orchestrator, SETTING_ANYONE_CAN_CREATE_ORGANIZATIONS, "true");
+
+ expectUnauthorizedError(() -> tester.asAnonymous().organizations().generate());
+ }
+
+ @Test
+ public void logged_in_user_can_create_organizations_if_anyone_is_allowed_to() {
+ setServerProperty(orchestrator, SETTING_ANYONE_CAN_CREATE_ORGANIZATIONS, "true");
+ User user = tester.users().generate();
+
+ Organization organization = tester.as(user.getLogin()).organizations().generate();
+
+ assertThat(organization.getName()).isNotEmpty();
+ assertThat(organization.getKey()).isNotEmpty();
+ assertThat(organization.getGuarded()).isFalse();
+
+ List<Organization> reloadedOrgs = tester.organizations().service().search(SearchWsRequest.builder().build()).getOrganizationsList();
+ assertThat(reloadedOrgs)
+ .filteredOn(o -> o.getKey().equals(organization.getKey()))
+ .hasSize(1);
+ }
+
+ private WsComponents.SearchWsResponse searchSampleProject(String organizationKey, ComponentsService componentsService) {
+ return componentsService
+ .search(new org.sonarqube.ws.client.component.SearchWsRequest()
+ .setOrganization(organizationKey)
+ .setQualifiers(singletonList("TRK"))
+ .setQuery("sample"));
+ }
+
+ private void assertThatOrganizationDoesNotExit(Organization org) {
+ SearchWsRequest request = new SearchWsRequest.Builder().setOrganizations(org.getKey()).build();
+ assertThat(tester.organizations().service().search(request).getOrganizationsList()).isEmpty();
+ }
+
+ private void verifyOrganization(Organization createdOrganization, String name, String description, String url,
+ String avatarUrl) {
+ SearchWsRequest request = new SearchWsRequest.Builder().setOrganizations(createdOrganization.getKey()).build();
+ List<Organization> result = tester.organizations().service().search(request).getOrganizationsList();
+ assertThat(result).hasSize(1);
+ Organization searchedOrganization = result.get(0);
+ assertThat(searchedOrganization.getKey()).isEqualTo(createdOrganization.getKey());
+ assertThat(searchedOrganization.getName()).isEqualTo(name);
+ if (description == null) {
+ assertThat(searchedOrganization.hasDescription()).isFalse();
+ } else {
+ assertThat(searchedOrganization.getDescription()).isEqualTo(description);
+ }
+ if (url == null) {
+ assertThat(searchedOrganization.hasUrl()).isFalse();
+ } else {
+ assertThat(searchedOrganization.getUrl()).isEqualTo(url);
+ }
+ if (avatarUrl == null) {
+ assertThat(searchedOrganization.hasAvatar()).isFalse();
+ } else {
+ assertThat(searchedOrganization.getAvatar()).isEqualTo(avatarUrl);
+ }
+ }
+
+ private void assertThatBuiltInQualityProfilesExist(Organization org) {
+ org.sonarqube.ws.client.qualityprofile.SearchWsRequest profilesRequest = new org.sonarqube.ws.client.qualityprofile.SearchWsRequest()
+ .setOrganizationKey(org.getKey());
+ QualityProfiles.SearchWsResponse response = tester.wsClient().qualityProfiles().search(profilesRequest);
+ assertThat(response.getProfilesCount()).isGreaterThan(0);
+
+ response.getProfilesList().forEach(p -> {
+ assertThat(p.getIsInherited()).isFalse();
+ assertThat(p.getProjectCount()).isEqualTo(0);
+ assertThat(p.getIsBuiltIn()).isTrue();
+ if (p.getName().toLowerCase(Locale.ENGLISH).contains("empty")) {
+ assertThat(p.getActiveRuleCount()).isEqualTo(0);
+ } else {
+ assertThat(p.getActiveRuleCount()).isGreaterThan(0);
+ // that allows to check the Elasticsearch index of active rules
+ Rules.SearchResponse activeRulesResponse = tester.wsClient().rules().search(new org.sonarqube.ws.client.rule.SearchWsRequest().setActivation(true).setQProfile(p.getKey()));
+ assertThat(activeRulesResponse.getTotal()).as("profile " + p.getName()).isEqualTo(p.getActiveRuleCount());
+ assertThat(activeRulesResponse.getRulesCount()).isEqualTo((int) p.getActiveRuleCount());
+ }
+ });
+ }
+
+ private void assertThatQualityProfilesDoNotExist(Organization org) {
+ expectNotFoundError(() -> tester.wsClient().qualityProfiles().search(
+ new org.sonarqube.ws.client.qualityprofile.SearchWsRequest().setOrganizationKey(org.getKey())));
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/organization/PersonalOrganizationTest.java b/tests/src/test/java/org/sonarqube/tests/organization/PersonalOrganizationTest.java
new file mode 100644
index 00000000000..5f6320c6397
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/organization/PersonalOrganizationTest.java
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+
+package org.sonarqube.tests.organization;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category6Suite;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.Organizations;
+import org.sonarqube.ws.WsUsers;
+import org.sonarqube.ws.client.organization.SearchWsRequest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.setServerProperty;
+
+public class PersonalOrganizationTest {
+
+ private static final String SETTING_CREATE_PERSONAL_ORG = "sonar.organizations.createPersonalOrg";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category6Suite.ORCHESTRATOR;
+
+ @Rule
+ public Tester tester = new Tester(orchestrator);
+
+ @Before
+ public void setUp() {
+ setServerProperty(orchestrator, SETTING_CREATE_PERSONAL_ORG, "true");
+ }
+
+ @After
+ public void tearDown() {
+ setServerProperty(orchestrator, SETTING_CREATE_PERSONAL_ORG, null);
+ }
+
+ @Test
+ public void personal_organizations_are_created_for_new_users() {
+ WsUsers.CreateWsResponse.User user = tester.users().generate();
+
+ List<Organizations.Organization> existing = tester.wsClient().organizations().search(SearchWsRequest.builder().build()).getOrganizationsList();
+ assertThat(existing)
+ .filteredOn(o -> o.getGuarded())
+ .filteredOn(o -> o.getKey().equals(user.getLogin()))
+ .hasSize(1)
+ .matches(l -> l.get(0).getName().equals(user.getName()));
+
+ tester.organizations().assertThatMemberOf(existing.get(0), user);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/organization/RootUserOnOrganizationTest.java b/tests/src/test/java/org/sonarqube/tests/organization/RootUserOnOrganizationTest.java
new file mode 100644
index 00000000000..e462fb709c5
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/organization/RootUserOnOrganizationTest.java
@@ -0,0 +1,91 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.organization;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category6Suite;
+import java.sql.SQLException;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.tests.Session;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.WsRoot;
+import org.sonarqube.ws.WsUsers;
+import util.user.UserRule;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.expectBadRequestError;
+import static util.ItUtils.expectForbiddenError;
+
+public class RootUserOnOrganizationTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category6Suite.ORCHESTRATOR;
+
+ @Rule
+ public Tester tester = new Tester(orchestrator);
+
+ @Test
+ public void system_administrator_is_flagged_as_root_when_he_enables_organization_support() {
+ assertThat(tester.wsClient().roots().search().getRootsList())
+ .extracting(WsRoot.Root::getLogin)
+ .containsExactly(UserRule.ADMIN_LOGIN);
+ }
+
+ @Test
+ public void a_root_can_flag_other_user_as_root() {
+ WsUsers.CreateWsResponse.User user = tester.users().generate();
+ tester.wsClient().roots().setRoot(user.getLogin());
+
+ assertThat(tester.wsClient().roots().search().getRootsList())
+ .extracting(WsRoot.Root::getLogin)
+ .containsExactlyInAnyOrder(UserRule.ADMIN_LOGIN, user.getLogin());
+ }
+
+ @Test
+ public void last_root_can_not_be_unset_root() throws SQLException {
+ expectBadRequestError(() -> tester.wsClient().roots().unsetRoot(UserRule.ADMIN_LOGIN));
+ }
+
+ @Test
+ public void root_can_be_set_and_unset_via_web_services() {
+ WsUsers.CreateWsResponse.User user1 = tester.users().generate();
+ WsUsers.CreateWsResponse.User user2 = tester.users().generate();
+ Session user1Session = tester.as(user1.getLogin());
+ Session user2Session = tester.as(user2.getLogin());
+
+ // non root can not set or unset root another user not itself
+ expectForbiddenError(() -> user1Session.wsClient().roots().setRoot(user2.getLogin()));
+ expectForbiddenError(() -> user1Session.wsClient().roots().setRoot(user1.getLogin()));
+ expectForbiddenError(() -> user1Session.wsClient().roots().unsetRoot(user1.getLogin()));
+ expectForbiddenError(() -> user2Session.wsClient().roots().unsetRoot(user1.getLogin()));
+ expectForbiddenError(() -> user2Session.wsClient().roots().unsetRoot(user2.getLogin()));
+ // admin (the first root) sets root1 as root
+ tester.wsClient().roots().setRoot(user1.getLogin());
+ // root1 can set root root2
+ user1Session.wsClient().roots().setRoot(user2.getLogin());
+ // root2 can unset root root1
+ user2Session.wsClient().roots().unsetRoot(user1.getLogin());
+ // root2 can unset root itself as it's not the last root
+ user2Session.wsClient().roots().unsetRoot(user2.getLogin());
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/organization/RootUserTest.java b/tests/src/test/java/org/sonarqube/tests/organization/RootUserTest.java
new file mode 100644
index 00000000000..517fc94d0f1
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/organization/RootUserTest.java
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.organization;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category4Suite;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.tests.Tester;
+
+import static util.ItUtils.expectForbiddenError;
+
+public class RootUserTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+
+ @Rule
+ public Tester tester = new Tester(orchestrator).disableOrganizations();
+
+ @Test
+ public void nobody_is_root_by_default_when_organizations_are_disabled() {
+ // anonymous
+ expectForbiddenError(() -> tester.wsClient().roots().search());
+
+ // admin
+ expectForbiddenError(() -> tester.wsClient().roots().search());
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/performance/AbstractPerfTest.java b/tests/src/test/java/org/sonarqube/tests/performance/AbstractPerfTest.java
new file mode 100644
index 00000000000..2cb2519fdc4
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/performance/AbstractPerfTest.java
@@ -0,0 +1,104 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.performance;
+
+import com.sonar.orchestrator.build.SonarScanner;
+import com.sonar.orchestrator.locator.FileLocation;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Properties;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.hamcrest.CustomMatcher;
+import org.junit.Rule;
+import org.junit.rules.ErrorCollector;
+import org.junit.rules.TestName;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public abstract class AbstractPerfTest {
+ static final double ACCEPTED_DURATION_VARIATION_IN_PERCENTS = 8.0;
+
+ @Rule
+ public TestName testName = new TestName();
+
+ protected void assertDurationAround(long duration, long expectedDuration) {
+ double variation = 100.0 * (0.0 + duration - expectedDuration) / expectedDuration;
+ System.out.printf("Test %s : executed in %d ms (%.2f %% from target)\n", testName.getMethodName(), duration, variation);
+ assertThat(Math.abs(variation)).as(String.format("Expected %d ms, got %d ms", expectedDuration, duration)).isLessThan(ACCEPTED_DURATION_VARIATION_IN_PERCENTS);
+ }
+
+ protected void assertDurationAround(ErrorCollector collector, long duration, long expectedDuration) {
+ double variation = 100.0 * (0.0 + duration - expectedDuration) / expectedDuration;
+ System.out.printf("Test %s : executed in %d ms (%.2f %% from target)\n", testName.getMethodName(), duration, variation);
+ collector.checkThat(String.format("Expected %d ms, got %d ms", expectedDuration, duration), Math.abs(variation), new CustomMatcher<Double>("a value less than "
+ + ACCEPTED_DURATION_VARIATION_IN_PERCENTS) {
+ @Override
+ public boolean matches(Object item) {
+ return ((item instanceof Double) && ((Double) item).compareTo(ACCEPTED_DURATION_VARIATION_IN_PERCENTS) < 0);
+ }
+ });
+ }
+
+ protected void assertDurationLessThan(long duration, long maxDuration) {
+ System.out.printf("Test %s : %d ms (max allowed is %d)\n", testName.getMethodName(), duration, maxDuration);
+ assertThat(duration).as(String.format("Expected less than %d ms, got %d ms", maxDuration, duration)).isLessThanOrEqualTo(maxDuration);
+ }
+
+ protected void assertDurationLessThan(ErrorCollector collector, long duration, final long maxDuration) {
+ System.out.printf("Test %s : %d ms (max allowed is %d)\n", testName.getMethodName(), duration, maxDuration);
+ collector.checkThat(String.format("Expected less than %d ms, got %d ms", maxDuration, duration), duration, new CustomMatcher<Long>("a value less than "
+ + maxDuration) {
+ @Override
+ public boolean matches(Object item) {
+ return ((item instanceof Long) && ((Long) item).compareTo(maxDuration) < 0);
+ }
+ });
+ }
+
+ protected Properties readProfiling(File baseDir, String moduleKey) throws IOException {
+ File profilingFile = new File(baseDir, ".sonar/profiling/" + moduleKey + "-profiler.properties");
+ Properties props = new Properties();
+ FileInputStream in = FileUtils.openInputStream(profilingFile);
+ try {
+ props.load(in);
+ } finally {
+ IOUtils.closeQuietly(in);
+ }
+ return props;
+ }
+
+ /**
+ * New batch analysis with most features disabled by default (empty QP, no CPD, no SCM, ...)
+ */
+ public static SonarScanner newScanner(String sonarRunnerOpts, String... props) {
+ SonarScanner scanner = SonarScanner.create()
+ .setProperties(
+ "sonar.scm.disabled", "true",
+ "sonar.cpd.exclusions", "**")
+ .setProperties(props);
+ scanner
+ .setEnvironmentVariable("SONAR_RUNNER_OPTS", sonarRunnerOpts)
+ .setEnvironmentVariable("SONAR_SCANNER_OPTS", sonarRunnerOpts)
+ .setProjectDir(FileLocation.of("projects/performance/xoo-sample").getFile());
+ return scanner;
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/performance/MavenLogs.java b/tests/src/test/java/org/sonarqube/tests/performance/MavenLogs.java
new file mode 100644
index 00000000000..ee9cc7a5c80
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/performance/MavenLogs.java
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.performance;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.commons.lang.StringUtils;
+
+public class MavenLogs {
+
+ /**
+ * Total time: 6.015s
+ * Total time: 3:14.025s
+ */
+ public static Long extractTotalTime(String logs) {
+ Pattern pattern = Pattern.compile("^.*Total time: (\\d*:)?(\\d+).(\\d+)s.*$", Pattern.DOTALL);
+ Matcher matcher = pattern.matcher(logs);
+ if (matcher.matches()) {
+ String minutes = StringUtils.defaultIfBlank(StringUtils.removeEnd(matcher.group(1), ":"), "0");
+ String seconds = StringUtils.defaultIfBlank(matcher.group(2), "0");
+ String millis = StringUtils.defaultIfBlank(matcher.group(3), "0");
+
+ return (Long.parseLong(minutes) * 60000) + (Long.parseLong(seconds) * 1000) + Long.parseLong(millis);
+ }
+ throw new IllegalStateException("Maven logs do not contain \"Total time\"");
+ }
+
+ /**
+ * Final Memory: 68M/190M
+ */
+ public static Long extractEndMemory(String logs) {
+ return extractLong(logs, ".*Final Memory: (\\d+)M/[\\d]+M.*");
+ }
+
+ public static Long extractMaxMemory(String logs) {
+ return extractLong(logs, ".*Final Memory: [\\d]+M/(\\d+)M.*");
+ }
+
+ private static Long extractLong(String logs, String format) {
+ Pattern pattern = Pattern.compile(format);
+ Matcher matcher = pattern.matcher(logs);
+ if (matcher.matches()) {
+ String s = matcher.group(1);
+ return Long.parseLong(s);
+ }
+ return null;
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/performance/MavenLogsTest.java b/tests/src/test/java/org/sonarqube/tests/performance/MavenLogsTest.java
new file mode 100644
index 00000000000..b9df51bbebc
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/performance/MavenLogsTest.java
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.performance;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class MavenLogsTest {
+ @Test
+ public void testExtractTotalTime() throws Exception {
+ assertThat(MavenLogs.extractTotalTime(" \n Total time: 6.015s \n ")).isEqualTo(6015);
+ assertThat(MavenLogs.extractTotalTime(" \n Total time: 3:14.025s\n ")).isEqualTo(194025);
+ }
+
+ @Test
+ public void testMaxMemory() throws Exception {
+ assertThat(MavenLogs.extractMaxMemory(" Final Memory: 68M/190M ")).isEqualTo(190);
+ }
+
+ @Test
+ public void testEndMemory() throws Exception {
+ assertThat(MavenLogs.extractEndMemory(" Final Memory: 68M/190M ")).isEqualTo(68);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/performance/PerfRule.java b/tests/src/test/java/org/sonarqube/tests/performance/PerfRule.java
new file mode 100644
index 00000000000..7b12d79bd38
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/performance/PerfRule.java
@@ -0,0 +1,123 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.performance;
+
+import com.google.common.base.Joiner;
+import org.hamcrest.CustomMatcher;
+import org.junit.rules.ErrorCollector;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public abstract class PerfRule extends ErrorCollector {
+
+ private final int runCount;
+ private final List<List<Long>> recordedResults = new ArrayList<List<Long>>();
+
+ private int currentRun;
+ private String testName;
+
+ public PerfRule(int runCount) {
+ this.runCount = runCount;
+ }
+
+ @Override
+ public Statement apply(final Statement base, Description description) {
+ this.testName = description.getMethodName();
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ for (currentRun = 1; currentRun <= runCount; currentRun++) {
+ recordedResults.add(new ArrayList<Long>());
+ beforeEachRun();
+ base.evaluate();
+ }
+ verify();
+ }
+
+ };
+ }
+
+ protected abstract void beforeEachRun();
+
+ public void assertDurationAround(long duration, long expectedDuration) {
+ currentResults().add(duration);
+ if (isLastRun()) {
+ long meanDuration = computeAverageDurationOfCurrentStep();
+ double variation = 100.0 * (0.0 + meanDuration - expectedDuration) / expectedDuration;
+ checkThat(String.format("Expected %d ms in average, got %d ms [%s]", expectedDuration, meanDuration, Joiner.on(",").join(getAllResultsOfCurrentStep())), Math.abs(variation),
+ new CustomMatcher<Double>(
+ "a value less than "
+ + AbstractPerfTest.ACCEPTED_DURATION_VARIATION_IN_PERCENTS) {
+ @Override
+ public boolean matches(Object item) {
+ return ((item instanceof Double) && ((Double) item).compareTo(AbstractPerfTest.ACCEPTED_DURATION_VARIATION_IN_PERCENTS) < 0);
+ }
+ });
+ }
+ }
+
+ private Long[] getAllResultsOfCurrentStep() {
+ Long[] result = new Long[runCount];
+ for (int i = 0; i < runCount; i++) {
+ result[i] = recordedResults.get(i).get(currentResults().size() - 1);
+ }
+ return result;
+ }
+
+ private long computeAverageDurationOfCurrentStep() {
+ Long[] result = getAllResultsOfCurrentStep();
+ // Compute a truncated mean by ignoring greater value
+ Arrays.sort(result);
+ long meanDuration = 0;
+ for (int i = 0; i < (runCount - 1); i++) {
+ meanDuration += result[i];
+ }
+ meanDuration /= (runCount - 1);
+ return meanDuration;
+ }
+
+ private List<Long> currentResults() {
+ return recordedResults.get(currentRun - 1);
+ }
+
+ public void assertDurationLessThan(long duration, final long maxDuration) {
+ currentResults().add(duration);
+ if (isLastRun()) {
+ long meanDuration = computeAverageDurationOfCurrentStep();
+ checkThat(String.format("Expected less than %d ms in average, got %d ms [%s]", maxDuration, meanDuration, Joiner.on(",").join(getAllResultsOfCurrentStep())), meanDuration,
+ new CustomMatcher<Long>("a value less than "
+ + maxDuration) {
+ @Override
+ public boolean matches(Object item) {
+ return ((item instanceof Long) && ((Long) item).compareTo(maxDuration) < 0);
+ }
+ });
+ }
+ }
+
+ private boolean isLastRun() {
+ return currentRun == runCount;
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/performance/ServerLogs.java b/tests/src/test/java/org/sonarqube/tests/performance/ServerLogs.java
new file mode 100644
index 00000000000..ece4f4a26a7
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/performance/ServerLogs.java
@@ -0,0 +1,93 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.performance;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.container.Server;
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.commons.io.Charsets;
+import org.apache.commons.io.FileUtils;
+
+public class ServerLogs {
+
+ public static Date extractDate(String line) {
+ String pattern = "yyyy.MM.dd HH:mm:ss";
+ SimpleDateFormat format = new SimpleDateFormat(pattern);
+ if (line.length() > 19) {
+ try {
+ return format.parse(line.substring(0, 19));
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+ return null;
+ }
+
+ public static Date extractFirstDate(List<String> lines) {
+ for (String line : lines) {
+ Date d = ServerLogs.extractDate(line);
+ if (d != null) {
+ return d;
+ }
+ }
+ return null;
+ }
+
+ public static void clear(Orchestrator orch) throws IOException {
+ Server server = orch.getServer();
+ if (server != null) {
+ for (File file : new File[]{server.getAppLogs(), server.getWebLogs(), server.getCeLogs(), server.getEsLogs()}) {
+ if (file != null) {
+ FileUtils.write(file, "", false);
+ }
+ }
+ }
+ }
+
+ /**
+ * 2015.09.29 16:57:45 INFO ce[o.s.s.c.q.CeWorkerRunnableImpl] Executed task | project=com.github.kevinsawicki:http-request-parent | id=AVAZm9oHIXrp54OmOeQe | time=2283ms
+ */
+ public static Long extractComputationTotalTime(Orchestrator orchestrator) throws IOException {
+ File report = orchestrator.getServer().getCeLogs();
+ List<String> logsLines = FileUtils.readLines(report, Charsets.UTF_8);
+ return extractComputationTotalTime(logsLines);
+ }
+
+ static Long extractComputationTotalTime(List<String> logs) {
+ Pattern pattern = Pattern.compile(".*INFO.*Executed task.* \\| time=(\\d+)ms.*");
+ for (int i = logs.size() - 1; i >= 0; i--) {
+ String line = logs.get(i);
+ Matcher matcher = pattern.matcher(line);
+ if (matcher.matches()) {
+ String duration = matcher.group(1);
+ return Long.parseLong(duration);
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/performance/ServerLogsTest.java b/tests/src/test/java/org/sonarqube/tests/performance/ServerLogsTest.java
new file mode 100644
index 00000000000..7ace69e4a52
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/performance/ServerLogsTest.java
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.performance;
+
+import com.google.common.collect.Lists;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ServerLogsTest {
+ @Test
+ public void logs_with_different_computations_take_the_last_one() throws Exception {
+ assertThat(ServerLogs.extractComputationTotalTime(Lists.newArrayList(
+ "2015.09.29 16:57:45 INFO web[][o.s.s.c.q.CeWorkerRunnableImpl] Executed task | project=com.github.kevinsawicki:http-request-parent | id=AVAZm9oHIXrp54OmOeQe | time=2283ms",
+ "2015.09.29 16:57:45 INFO web[][o.s.s.c.q.CeWorkerRunnableImpl] Executed task | project=com.github.kevinsawicki:http-request-parent | id=AVAZm9oHIXrp54OmOeQe | time=1234ms")))
+ .isEqualTo(1234L);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/performance/scanner/BootstrappingTest.java b/tests/src/test/java/org/sonarqube/tests/performance/scanner/BootstrappingTest.java
new file mode 100644
index 00000000000..a8e3c2376a8
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/performance/scanner/BootstrappingTest.java
@@ -0,0 +1,150 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.performance.scanner;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.BuildResult;
+import com.sonar.orchestrator.build.SonarScanner;
+import java.io.File;
+import java.io.IOException;
+import org.apache.commons.io.FileUtils;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonarqube.tests.performance.AbstractPerfTest;
+import org.sonarqube.tests.performance.MavenLogs;
+import org.sonarqube.tests.performance.PerfRule;
+
+@Ignore("Timeout on computation side")
+public class BootstrappingTest extends AbstractPerfTest {
+
+ @Rule
+ public PerfRule perfRule = new PerfRule(4) {
+ @Override
+ protected void beforeEachRun() {
+ orchestrator.resetData();
+ }
+ };
+
+ @ClassRule
+ public static TemporaryFolder temp = new TemporaryFolder();
+
+ @ClassRule
+ public static Orchestrator orchestrator = ScannerPerformanceSuite.ORCHESTRATOR;
+
+ private static File manyFlatModulesBaseDir;
+ private static File manyNestedModulesBaseDir;
+
+ @BeforeClass
+ public static void setUp() throws Exception {
+ // Execute a first analysis to prevent any side effects with cache of plugin JAR files
+ orchestrator.executeBuild(newScanner("-Xmx512m -server", "sonar.profile", "one-xoo-issue-per-line"));
+
+ manyFlatModulesBaseDir = prepareProjectWithManyFlatModules(100);
+ manyNestedModulesBaseDir = prepareProjectWithManyNestedModules(50);
+ }
+
+ @Test
+ public void analyzeProjectWith100FlatModules() throws IOException {
+
+ SonarScanner scanner = SonarScanner.create()
+ .setProperties(
+ "sonar.projectKey", "many-flat-modules",
+ "sonar.projectName", "Many Flat Modules",
+ "sonar.projectVersion", "1.0",
+ "sonar.sources", "",
+ "sonar.showProfiling", "true");
+ scanner
+ .setEnvironmentVariable("SONAR_RUNNER_OPTS", "-Xmx512m -server")
+ .setProjectDir(manyFlatModulesBaseDir);
+
+ BuildResult result = orchestrator.executeBuild(scanner);
+ // First analysis
+ perfRule.assertDurationAround(MavenLogs.extractTotalTime(result.getLogs()), 22800L);
+
+ result = orchestrator.executeBuild(scanner);
+ // Second analysis is longer since we load project referential
+ perfRule.assertDurationAround(MavenLogs.extractTotalTime(result.getLogs()), 27200L);
+ }
+
+ private static File prepareProjectWithManyFlatModules(int SIZE) throws IOException {
+ File baseDir = temp.newFolder();
+ File projectProps = new File(baseDir, "sonar-project.properties");
+
+ StringBuilder moduleListBuilder = new StringBuilder(SIZE * ("module".length() + 2));
+
+ for (int i = 1; i <= SIZE; i++) {
+ moduleListBuilder.append("module").append(i);
+ File moduleDir = new File(baseDir, "module" + i);
+ moduleDir.mkdir();
+ if (i != SIZE) {
+ moduleListBuilder.append(",");
+ }
+ }
+
+ FileUtils.write(projectProps, "sonar.modules=", true);
+ FileUtils.write(projectProps, moduleListBuilder.toString(), true);
+ FileUtils.write(projectProps, "\n", true);
+ return baseDir;
+ }
+
+ @Test
+ public void analyzeProjectWith50NestedModules() throws IOException {
+ SonarScanner scanner = SonarScanner.create()
+ .setProperties(
+ "sonar.projectKey", "many-nested-modules",
+ "sonar.projectName", "Many Nested Modules",
+ "sonar.projectVersion", "1.0",
+ "sonar.sources", "",
+ "sonar.showProfiling", "true");
+ scanner.setEnvironmentVariable("SONAR_RUNNER_OPTS", "-Xmx512m -server");
+ scanner.setProjectDir(manyNestedModulesBaseDir);
+
+ BuildResult result = orchestrator.executeBuild(scanner);
+ // First analysis
+ perfRule.assertDurationAround(MavenLogs.extractTotalTime(result.getLogs()), 8900L);
+
+ result = orchestrator.executeBuild(scanner);
+ // Second analysis
+ perfRule.assertDurationAround(MavenLogs.extractTotalTime(result.getLogs()), 9300L);
+ }
+
+ private static File prepareProjectWithManyNestedModules(int SIZE) throws IOException {
+ File baseDir = temp.newFolder();
+ File currentDir = baseDir;
+
+ for (int i = 1; i <= SIZE; i++) {
+ File projectProps = new File(currentDir, "sonar-project.properties");
+ FileUtils.write(projectProps, "sonar.modules=module" + i + "\n", true);
+ if (i >= 1) {
+ FileUtils.write(projectProps, "sonar.moduleKey=module" + (i - 1), true);
+ }
+ File moduleDir = new File(currentDir, "module" + i);
+ moduleDir.mkdir();
+ currentDir = moduleDir;
+ }
+ FileUtils.write(new File(currentDir, "sonar-project.properties"), "sonar.moduleKey=module" + SIZE, true);
+ return baseDir;
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/performance/scanner/DuplicationTest.java b/tests/src/test/java/org/sonarqube/tests/performance/scanner/DuplicationTest.java
new file mode 100644
index 00000000000..4729fb331d8
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/performance/scanner/DuplicationTest.java
@@ -0,0 +1,96 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.performance.scanner;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.MavenBuild;
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ErrorCollector;
+import org.junit.rules.TemporaryFolder;
+import org.sonarqube.tests.performance.AbstractPerfTest;
+import org.sonarqube.ws.WsMeasures;
+import org.sonarqube.ws.client.HttpConnector;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.WsClientFactories;
+import org.sonarqube.ws.client.measure.ComponentWsRequest;
+
+import static java.lang.Double.parseDouble;
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class DuplicationTest extends AbstractPerfTest {
+
+ @Rule
+ public ErrorCollector collector = new ErrorCollector();
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @ClassRule
+ public static final Orchestrator orchestrator = ScannerPerformanceSuite.ORCHESTRATOR;
+
+ @BeforeClass
+ public static void setUp() throws IOException {
+ // Execute a first analysis to prevent any side effects with cache of plugin JAR files
+ orchestrator.executeBuild(newScanner("-Xmx512m -server", "sonar.profile", "one-xoo-issue-per-line"));
+ }
+
+ @Before
+ public void cleanDatabase() {
+ orchestrator.resetData();
+ }
+
+ /**
+ * SONAR-3060
+ */
+ @Test
+ public void hugeJavaFile() {
+ MavenBuild build = MavenBuild.create(new File("projects/performance/huge-file/pom.xml"))
+ .setEnvironmentVariable("MAVEN_OPTS", "-Xmx1024m")
+ .setProperty("sonar.sourceEncoding", "UTF-8")
+ .setCleanSonarGoals();
+ orchestrator.executeBuild(build);
+ Map<String, Double> measure = getMeasures("com.sonarsource.it.samples:huge-file:src/main/java/huge/HugeFile.java");
+ assertThat(measure.get("duplicated_lines")).isGreaterThan(50000.0);
+ }
+
+ private Map<String, Double> getMeasures(String key) {
+ return newWsClient().measures().component(new ComponentWsRequest()
+ .setComponentKey(key)
+ .setMetricKeys(asList("duplicated_lines", "duplicated_blocks", "duplicated_files", "duplicated_lines_density")))
+ .getComponent().getMeasuresList()
+ .stream()
+ .collect(Collectors.toMap(WsMeasures.Measure::getMetric, measure -> parseDouble(measure.getValue())));
+ }
+
+ private WsClient newWsClient() {
+ return WsClientFactories.getDefault().newClient(HttpConnector.newBuilder()
+ .url(orchestrator.getServer().getUrl())
+ .build());
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/performance/scanner/FileSystemTest.java b/tests/src/test/java/org/sonarqube/tests/performance/scanner/FileSystemTest.java
new file mode 100644
index 00000000000..fca5799f3ea
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/performance/scanner/FileSystemTest.java
@@ -0,0 +1,106 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.performance.scanner;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarRunner;
+import java.io.File;
+import java.io.IOException;
+import java.util.Properties;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.StringUtils;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonarqube.tests.performance.AbstractPerfTest;
+import org.sonarqube.tests.performance.PerfRule;
+
+public class FileSystemTest extends AbstractPerfTest {
+
+ @Rule
+ public PerfRule perfRule = new PerfRule(4) {
+ @Override
+ protected void beforeEachRun() {
+ orchestrator.resetData();
+ }
+ };
+
+ @ClassRule
+ public static TemporaryFolder temp = new TemporaryFolder();
+
+ @ClassRule
+ public static Orchestrator orchestrator = ScannerPerformanceSuite.ORCHESTRATOR;
+
+ private static File baseDir;
+
+ @BeforeClass
+ public static void setUp() throws IOException {
+ // Execute a first analysis to prevent any side effects with cache of plugin JAR files
+ orchestrator.executeBuild(newScanner("-Xmx512m -server", "sonar.profile", "one-xoo-issue-per-line"));
+ baseDir = prepareProject();
+ }
+
+ @Before
+ public void cleanDatabase() {
+ orchestrator.resetData();
+ }
+
+ @Test
+ public void indexProjectWith1000BigFilesXmx128() throws IOException {
+ run(128, 30000L);
+ }
+
+ private void run(int xmx, long expectedDuration) throws IOException {
+ SonarRunner runner = SonarRunner.create()
+ .setProperties(
+ "sonar.projectKey", "filesystemXmx" + xmx,
+ "sonar.projectName", "filesystem xmx" + xmx,
+ "sonar.projectVersion", "1.0",
+ "sonar.sources", "src",
+ "sonar.analysis.mode", "issues",
+ "sonar.preloadFileMetadata", "true",
+ "sonar.showProfiling", "true")
+ .setEnvironmentVariable("SONAR_RUNNER_OPTS", "-Xmx" + xmx + "m -server")
+ .setProjectDir(baseDir);
+
+ orchestrator.executeBuild(runner);
+
+ Properties prof = readProfiling(baseDir, "filesystemXmx" + xmx);
+ perfRule.assertDurationAround(Long.valueOf(prof.getProperty("Index filesystem")), expectedDuration);
+ }
+
+ private static File prepareProject() throws IOException {
+ File baseDir = temp.newFolder();
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ int nbFiles = 1000;
+ int lines = 10000;
+ for (int nb = 1; nb <= nbFiles; nb++) {
+ File xooFile = new File(srcDir, "sample" + nb + ".xoo");
+ FileUtils.write(xooFile, StringUtils.repeat(StringUtils.repeat("a", 100) + "\n", lines));
+ }
+ return baseDir;
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/performance/scanner/HighlightingTest.java b/tests/src/test/java/org/sonarqube/tests/performance/scanner/HighlightingTest.java
new file mode 100644
index 00000000000..f995244763b
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/performance/scanner/HighlightingTest.java
@@ -0,0 +1,107 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.performance.scanner;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.BuildResult;
+import com.sonar.orchestrator.build.SonarScanner;
+import java.io.File;
+import java.io.IOException;
+import java.util.Properties;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.StringUtils;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonarqube.tests.performance.AbstractPerfTest;
+import org.sonarqube.tests.performance.MavenLogs;
+import org.sonarqube.tests.performance.PerfRule;
+
+public class HighlightingTest extends AbstractPerfTest {
+
+ @Rule
+ public PerfRule perfRule = new PerfRule(4) {
+ @Override
+ protected void beforeEachRun() {
+ orchestrator.resetData();
+ }
+ };
+
+ @ClassRule
+ public static TemporaryFolder temp = new TemporaryFolder();
+
+ @ClassRule
+ public static Orchestrator orchestrator = ScannerPerformanceSuite.ORCHESTRATOR;
+
+ @BeforeClass
+ public static void setUp() throws IOException {
+ // Execute a first analysis to prevent any side effects with cache of plugin JAR files
+ orchestrator.executeBuild(newScanner("-Xmx512m -server", "sonar.profile", "one-xoo-issue-per-line"));
+ }
+
+ @Before
+ public void cleanDatabase() {
+ orchestrator.resetData();
+ }
+
+ @Test
+ public void computeSyntaxHighlightingOnBigFiles() throws IOException {
+ File baseDir = temp.newFolder();
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ int nbFiles = 100;
+ int ruleCount = 100000;
+ int nblines = 1000;
+ int linesize = ruleCount / nblines;
+ for (int nb = 1; nb <= nbFiles; nb++) {
+ File xooFile = new File(srcDir, "sample" + nb + ".xoo");
+ File xoohighlightingFile = new File(srcDir, "sample" + nb + ".xoo.highlighting");
+ FileUtils.write(xooFile, StringUtils.repeat(StringUtils.repeat("a", linesize) + "\n", nblines));
+ StringBuilder sb = new StringBuilder(16 * ruleCount);
+ for (int i = 0; i < ruleCount; i++) {
+ sb.append(i).append(":").append(i + 1).append(":s\n");
+ }
+ FileUtils.write(xoohighlightingFile, sb.toString());
+ }
+
+ SonarScanner scanner = SonarScanner.create()
+ .setScannerVersion("2.4")
+ .setProperties(
+ "sonar.projectKey", "highlighting",
+ "sonar.projectName", "highlighting",
+ "sonar.projectVersion", "1.0",
+ "sonar.sources", "src",
+ "sonar.showProfiling", "true");
+ scanner.setEnvironmentVariable("SONAR_RUNNER_OPTS", "-Xmx512m -server")
+ .setProjectDir(baseDir);
+
+ BuildResult result = orchestrator.executeBuild(scanner);
+ System.out.println("Total time: " + MavenLogs.extractTotalTime(result.getLogs()));
+ perfRule.assertDurationAround(MavenLogs.extractTotalTime(result.getLogs()), 25700L);
+
+ Properties prof = readProfiling(baseDir, "highlighting");
+ perfRule.assertDurationAround(Long.valueOf(prof.getProperty("Xoo Highlighting Sensor")), 9700L);
+
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/performance/scanner/IssuesModeTest.java b/tests/src/test/java/org/sonarqube/tests/performance/scanner/IssuesModeTest.java
new file mode 100644
index 00000000000..03f811529e1
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/performance/scanner/IssuesModeTest.java
@@ -0,0 +1,73 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.performance.scanner;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import java.io.File;
+import java.io.IOException;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonarqube.tests.performance.AbstractPerfTest;
+import org.sonarqube.tests.performance.PerfRule;
+
+public class IssuesModeTest extends AbstractPerfTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = ScannerPerformanceSuite.ORCHESTRATOR;
+
+ @ClassRule
+ public static TemporaryFolder temp = new TemporaryFolder();
+
+ @Rule
+ public PerfRule perfRule = new PerfRule(4) {
+ @Override
+ protected void beforeEachRun() {
+ orchestrator.resetData();
+ }
+ };
+
+ @Before
+ public void cleanDatabase() {
+ orchestrator.resetData();
+ }
+
+ @Test
+ public void issues_mode_scan_xoo_project() throws IOException {
+ File userHome = temp.newFolder();
+ orchestrator.getServer().provisionProject("sample", "xoo-sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-xoo-issue-per-line");
+ SonarScanner runner = newScanner(
+ "-Xmx512m -server",
+ "sonar.analysis.mode", "issues",
+ "sonar.userHome", userHome.getAbsolutePath(),
+ "sonar.showProfiling", "true")
+ .setScannerVersion("2.8");
+ long start = System.currentTimeMillis();
+ orchestrator.executeBuild(runner, false);
+ long duration = System.currentTimeMillis() - start;
+ System.out.println("Issues analysis: " + duration + "ms");
+
+ perfRule.assertDurationAround(duration, 4300L);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/performance/scanner/MemoryTest.java b/tests/src/test/java/org/sonarqube/tests/performance/scanner/MemoryTest.java
new file mode 100644
index 00000000000..2b9b162d72e
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/performance/scanner/MemoryTest.java
@@ -0,0 +1,120 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.performance.scanner;
+
+import com.google.common.base.Strings;
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.BuildResult;
+import com.sonar.orchestrator.build.SonarScanner;
+import com.sonar.orchestrator.http.HttpMethod;
+import java.io.File;
+import java.io.IOException;
+import org.apache.commons.io.FileUtils;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonarqube.tests.performance.AbstractPerfTest;
+import org.sonarqube.tests.performance.MavenLogs;
+import org.sonarqube.tests.performance.PerfRule;
+
+public class MemoryTest extends AbstractPerfTest {
+
+ @Rule
+ public PerfRule perfRule = new PerfRule(4) {
+ @Override
+ protected void beforeEachRun() {
+ orchestrator.resetData();
+ }
+ };
+
+ @ClassRule
+ public static TemporaryFolder temp = new TemporaryFolder();
+
+ @ClassRule
+ public static Orchestrator orchestrator = ScannerPerformanceSuite.ORCHESTRATOR;
+
+ @Before
+ public void cleanDatabase() {
+ orchestrator.resetData();
+ }
+
+ int DEPTH = 4;
+
+ @Test
+ public void should_not_fail_with_limited_xmx_memory_and_no_coverage_per_test() {
+ orchestrator.executeBuild(
+ newScanner("-Xmx80m -server -XX:-HeapDumpOnOutOfMemoryError"));
+ }
+
+ // Property on root module is duplicated in each module so it may be big
+ @Test
+ public void analyzeProjectWithManyModulesAndBigProperties() throws IOException {
+
+ File baseDir = temp.newFolder();
+
+ prepareModule(baseDir, "moduleA", 1);
+ prepareModule(baseDir, "moduleB", 1);
+ prepareModule(baseDir, "moduleC", 1);
+
+ FileUtils.write(new File(baseDir, "sonar-project.properties"), "sonar.modules=moduleA,moduleB,moduleC\n", true);
+ FileUtils.write(new File(baseDir, "sonar-project.properties"), "sonar.myBigProp=" + Strings.repeat("A", 10000), true);
+
+ SonarScanner scanner = SonarScanner.create()
+ .setScannerVersion("2.8")
+ .setProperties(
+ "sonar.projectKey", "big-module-tree",
+ "sonar.projectName", "Big Module Tree",
+ "sonar.projectVersion", "1.0",
+ "sonar.sources", "",
+ "sonar.showProfiling", "true");
+ scanner.setEnvironmentVariable("SONAR_SCANNER_OPTS", "-Xmx512m -server")
+ .setProjectDir(baseDir);
+
+ BuildResult result = orchestrator.executeBuild(scanner);
+ perfRule.assertDurationAround(MavenLogs.extractTotalTime(result.getLogs()), 6190L);
+
+ // Second execution with a property on server side
+ orchestrator.getServer().newHttpCall("/api/settings/set")
+ .setMethod(HttpMethod.POST)
+ .setAdminCredentials()
+ .setParam("key", "sonar.anotherBigProp")
+ .setParam("value", Strings.repeat("B", 1000))
+ .setParam("component", "big-module-tree")
+ .execute();
+ result = orchestrator.executeBuild(scanner);
+ perfRule.assertDurationAround(MavenLogs.extractTotalTime(result.getLogs()), 6120L);
+ }
+
+ private void prepareModule(File parentDir, String moduleName, int depth) throws IOException {
+ File moduleDir = new File(parentDir, moduleName);
+ moduleDir.mkdir();
+ File projectProps = new File(moduleDir, "sonar-project.properties");
+ FileUtils.write(projectProps, "sonar.moduleKey=" + moduleName + "\n", true);
+ if (depth < DEPTH) {
+ FileUtils.write(projectProps, "sonar.modules=" + moduleName + "A," + moduleName + "B," + moduleName + "C\n", true);
+ prepareModule(moduleDir, moduleName + "A", depth + 1);
+ prepareModule(moduleDir, moduleName + "B", depth + 1);
+ prepareModule(moduleDir, moduleName + "C", depth + 1);
+ }
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/performance/scanner/ScannerPerformanceSuite.java b/tests/src/test/java/org/sonarqube/tests/performance/scanner/ScannerPerformanceSuite.java
new file mode 100644
index 00000000000..bfb8fb2e3b4
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/performance/scanner/ScannerPerformanceSuite.java
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.performance.scanner;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.locator.FileLocation;
+import java.io.File;
+import java.io.IOException;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+import org.sonarqube.tests.performance.AbstractPerfTest;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+ BootstrappingTest.class,
+ DuplicationTest.class,
+ FileSystemTest.class,
+ HighlightingTest.class,
+ IssuesModeTest.class,
+ MemoryTest.class
+})
+public class ScannerPerformanceSuite {
+
+ @ClassRule
+ public static final Orchestrator ORCHESTRATOR = Orchestrator
+ .builderEnv()
+ .addPlugin(FileLocation.byWildcardMavenFilename(new File("../plugins/sonar-xoo-plugin/target"), "sonar-xoo-plugin-*.jar"))
+ // should not be so high, but required as long embedded h2 is used -> requires more memory on server
+ .setServerProperty("sonar.web.javaOpts", "-Xmx1G -XX:+HeapDumpOnOutOfMemoryError")
+ // Needed by DuplicationTest::hugeJavaFile
+ .setOrchestratorProperty("javaVersion", "LATEST_RELEASE").addPlugin("java")
+ .restoreProfileAtStartup(FileLocation.ofClasspath("/one-xoo-issue-per-line.xml"))
+ .build();
+
+ @BeforeClass
+ public static void setUp() throws IOException {
+ // Execute a first analysis to prevent any side effects with cache of plugin JAR files
+ ORCHESTRATOR.executeBuild(AbstractPerfTest.newScanner("-Xmx512m -server", "sonar.profile", "one-xoo-issue-per-line"));
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/performance/server/ComputeEnginePerfTest.java b/tests/src/test/java/org/sonarqube/tests/performance/server/ComputeEnginePerfTest.java
new file mode 100644
index 00000000000..e6376641a37
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/performance/server/ComputeEnginePerfTest.java
@@ -0,0 +1,125 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.performance.server;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import com.sonar.orchestrator.locator.FileLocation;
+import java.io.File;
+import java.io.IOException;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.StringUtils;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonarqube.tests.performance.AbstractPerfTest;
+import org.sonarqube.tests.performance.ServerLogs;
+
+public class ComputeEnginePerfTest extends AbstractPerfTest {
+ private static int MAX_HEAP_SIZE_IN_MEGA = 600;
+
+ @ClassRule
+ public static TemporaryFolder temp = new TemporaryFolder();
+
+ @ClassRule
+ public static Orchestrator orchestrator = Orchestrator
+ .builderEnv()
+ .addPlugin(FileLocation.byWildcardMavenFilename(new File("../plugins/sonar-xoo-plugin/target"), "sonar-xoo-plugin-*.jar"))
+ .setServerProperty(
+ "sonar.web.javaOpts",
+ String.format("-Xms%dm -Xmx%dm -XX:+HeapDumpOnOutOfMemoryError -Djava.awt.headless=true", MAX_HEAP_SIZE_IN_MEGA, MAX_HEAP_SIZE_IN_MEGA))
+ .restoreProfileAtStartup(FileLocation.ofClasspath("/one-xoo-issue-per-line.xml"))
+ .build();
+
+ private static File bigProjectBaseDir;
+
+ @BeforeClass
+ public static void classSetUp() throws IOException {
+ bigProjectBaseDir = createProject(4, 10, 20);
+ }
+
+ @Before
+ public void before() throws Exception {
+ orchestrator.resetData();
+ }
+
+ @Test
+ public void analyse_big_project() throws Exception {
+ SonarScanner scanner = SonarScanner.create()
+ .setProperties(
+ "sonar.projectKey", "big-project",
+ "sonar.projectName", "Big Project",
+ "sonar.projectVersion", "1.0",
+ "sonar.sources", "src",
+ "sonar.profile", "one-xoo-issue-per-line")
+ .setProjectDir(bigProjectBaseDir);
+
+ orchestrator.executeBuild(scanner);
+
+ assertComputationDurationAround(350_000L);
+ }
+
+ private void assertComputationDurationAround(long expectedDuration) throws IOException {
+ Long duration = ServerLogs.extractComputationTotalTime(orchestrator);
+
+ assertDurationAround(duration, expectedDuration);
+ }
+
+ private static File createProject(int dirDepth, int nbDirByLayer, int nbIssuesByFile) throws IOException {
+ File rootDir = temp.newFolder();
+ File projectProperties = new File(rootDir, "sonar-project.properties");
+
+ StringBuilder moduleListBuilder = new StringBuilder(nbDirByLayer * ("module".length() + 2));
+
+ for (int i = 1; i <= nbDirByLayer; i++) {
+ moduleListBuilder.append("module").append(i);
+ File moduleDir = new File(rootDir, "module" + i + "/src");
+ moduleDir.mkdirs();
+ if (i != nbDirByLayer) {
+ moduleListBuilder.append(",");
+ }
+
+ createProjectFiles(moduleDir, dirDepth - 1, nbDirByLayer, nbIssuesByFile);
+ }
+
+ FileUtils.write(projectProperties, "sonar.modules=", true);
+ FileUtils.write(projectProperties, moduleListBuilder.toString(), true);
+ FileUtils.write(projectProperties, "\n", true);
+ FileUtils.write(projectProperties, "sonar.source=src", true);
+
+ return rootDir;
+ }
+
+ private static void createProjectFiles(File dir, int depth, int nbFilesByDir, int nbIssuesByFile) throws IOException {
+ dir.mkdir();
+ for (int i = 1; i <= nbFilesByDir; i++) {
+ File xooFile = new File(dir, "file" + i + ".xoo");
+ String line = xooFile.getAbsolutePath() + i + "\n";
+ FileUtils.write(xooFile, StringUtils.repeat(line, nbIssuesByFile));
+ File xooMeasureFile = new File(dir, "file" + i + ".xoo.measures");
+ FileUtils.write(xooMeasureFile, "lines:" + nbIssuesByFile);
+ if (depth > 1) {
+ createProjectFiles(new File(dir, "dir" + i), depth - 1, nbFilesByDir, nbIssuesByFile);
+ }
+ }
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/performance/server/ServerPerfTest.java b/tests/src/test/java/org/sonarqube/tests/performance/server/ServerPerfTest.java
new file mode 100644
index 00000000000..7aa915ce982
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/performance/server/ServerPerfTest.java
@@ -0,0 +1,111 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.performance.server;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.locator.FileLocation;
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.Timeout;
+import org.sonarqube.tests.performance.AbstractPerfTest;
+import org.sonarqube.tests.performance.ServerLogs;
+
+import static org.apache.commons.io.FileUtils.readLines;
+
+public class ServerPerfTest extends AbstractPerfTest {
+ private static final int TIMEOUT_3_MINUTES = 1000 * 60 * 3;
+
+ @Rule
+ public Timeout timeout = new Timeout(TIMEOUT_3_MINUTES);
+
+ // ES + TOMCAT
+ @Test
+ public void server_startup_and_shutdown() throws Exception {
+ String defaultWebJavaOptions = "-Xmx768m -XX:+HeapDumpOnOutOfMemoryError -Djava.awt.headless=true -Dfile.encoding=UTF-8";
+ Orchestrator orchestrator = Orchestrator.builderEnv()
+ .addPlugin(FileLocation.byWildcardMavenFilename(new File("../plugins/sonar-xoo-plugin/target"), "sonar-xoo-plugin-*.jar"))
+
+ // See http://wiki.apache.org/tomcat/HowTo/FasterStartUp
+ // Sometimes source of entropy is too small and Tomcat spends ~20 seconds on the step :
+ // "Creation of SecureRandom instance for session ID generation using [SHA1PRNG]"
+ // Using /dev/urandom fixes the issue on linux
+ .setServerProperty("sonar.web.javaOpts", defaultWebJavaOptions + " -Djava.security.egd=file:/dev/./urandom")
+ .build();
+ try {
+ ServerLogs.clear(orchestrator);
+ orchestrator.start();
+
+ // compare dates of first and last log
+ long firstLogDate = ServerLogs.extractFirstDate(readLines(orchestrator.getServer().getAppLogs())).getTime();
+ long startedAtDate = extractStartedAtDate(orchestrator);
+ assertDurationAround(startedAtDate - firstLogDate, 25_000);
+
+ ServerLogs.clear(orchestrator);
+ orchestrator.stop();
+
+ List<String> lines = readLines(orchestrator.getServer().getAppLogs());
+ long firstStopLogDate = ServerLogs.extractFirstDate(lines).getTime();
+ long stopDate = extractStopDate(lines);
+ assertDurationLessThan(stopDate - firstStopLogDate, 10_000);
+
+ } finally {
+ orchestrator.stop();
+ }
+ }
+
+ private static long extractStartedAtDate(Orchestrator orchestrator) throws IOException {
+ Date startedAtDate = extractStartedDate(readLines(orchestrator.getServer().getCeLogs()));
+ // if SQ never starts, the test will fail with timeout
+ while (startedAtDate == null) {
+ try {
+ Thread.sleep(100);
+ startedAtDate = extractStartedDate(readLines(orchestrator.getServer().getCeLogs()));
+ } catch (InterruptedException e) {
+ // ignored
+ Thread.currentThread().interrupt();
+ }
+ }
+ return startedAtDate.getTime();
+ }
+
+ private static Date extractStartedDate(List<String> lines) {
+ Collections.reverse(lines);
+ Date end = null;
+ for (String line : lines) {
+ if (line.contains("Compute Engine is operational")) {
+ end = ServerLogs.extractDate(line);
+ break;
+ }
+ }
+ return end;
+ }
+
+ private static long extractStopDate(List<String> lines) throws IOException {
+ Collections.reverse(lines);
+ Date end = ServerLogs.extractFirstDate(lines);
+ return end.getTime();
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/performance/server/ServerPerformanceSuite.java b/tests/src/test/java/org/sonarqube/tests/performance/server/ServerPerformanceSuite.java
new file mode 100644
index 00000000000..c4c6c66787e
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/performance/server/ServerPerformanceSuite.java
@@ -0,0 +1,31 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.performance.server;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+ ComputeEnginePerfTest.class,
+ ServerPerfTest.class
+})
+public class ServerPerformanceSuite {
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/plugins/PluginsSuite.java b/tests/src/test/java/org/sonarqube/tests/plugins/PluginsSuite.java
new file mode 100644
index 00000000000..de977606df9
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/plugins/PluginsSuite.java
@@ -0,0 +1,31 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.plugins;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+ PluginsTest.class,
+})
+public class PluginsSuite {
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/plugins/PluginsTest.java b/tests/src/test/java/org/sonarqube/tests/plugins/PluginsTest.java
new file mode 100644
index 00000000000..45b9262ca3e
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/plugins/PluginsTest.java
@@ -0,0 +1,202 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.plugins;
+
+import com.google.common.collect.Sets;
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.OrchestratorBuilder;
+import com.sonar.orchestrator.build.BuildResult;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.plugins.checks.AbapCheck;
+import org.sonarqube.tests.plugins.checks.CCheck;
+import org.sonarqube.tests.plugins.checks.Check;
+import org.sonarqube.tests.plugins.checks.CobolCheck;
+import org.sonarqube.tests.plugins.checks.CppCheck;
+import org.sonarqube.tests.plugins.checks.FlexCheck;
+import org.sonarqube.tests.plugins.checks.GroovyCheck;
+import org.sonarqube.tests.plugins.checks.JavaCheck;
+import org.sonarqube.tests.plugins.checks.JavascriptCheck;
+import org.sonarqube.tests.plugins.checks.PhpCheck;
+import org.sonarqube.tests.plugins.checks.PliCheck;
+import org.sonarqube.tests.plugins.checks.PythonCheck;
+import org.sonarqube.tests.plugins.checks.RpgCheck;
+import org.sonarqube.tests.plugins.checks.SwiftCheck;
+import org.sonarqube.tests.plugins.checks.Validation;
+import org.sonarqube.tests.plugins.checks.VbCheck;
+import org.sonarqube.tests.plugins.checks.WebCheck;
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ErrorCollector;
+
+import static com.sonar.orchestrator.locator.FileLocation.byWildcardMavenFilename;
+import static org.assertj.core.api.Assertions.fail;
+
+/**
+ * Verify that latest releases of the plugins available in update center
+ * are correctly supported.
+ */
+public class PluginsTest {
+
+ private static final Set<String> LICENSED_PLUGINS = Sets.newHashSet(
+ "abap", "cobol", "cpp", "objc", "pli", "plsql", "rpg",
+ "swift", "vb", "vbnet");
+
+ private static final List<Check> CHECKS = Arrays.asList(
+ new AbapCheck(),
+ new CCheck(), new CppCheck(),
+ new CobolCheck(),
+ // FIXME css plugin is temporary disabled as for the moment incompatible with the web plugin
+ // new CssCheck(),
+ new FlexCheck(),
+ new GroovyCheck(),
+ new JavaCheck(),
+ new JavascriptCheck(),
+ new PhpCheck(),
+ new PliCheck(),
+ new PythonCheck(),
+ new RpgCheck(),
+ new SwiftCheck(),
+ new VbCheck(),
+ new WebCheck());
+
+ private static Orchestrator ORCHESTRATOR;
+
+ @BeforeClass
+ public static void startServer() {
+ OrchestratorBuilder builder = Orchestrator.builderEnv()
+ .setZipFile(byWildcardMavenFilename(new File("../sonar-application/target"), "sonar*.zip").getFile());
+
+ // FIXME JSON plugin is temporarily disabled as for the moment the github repo doesn't exist anymore installPlugin(builder, "JSON");;
+ installPlugin(builder, "Sonargraph");
+ installPlugin(builder, "abap");
+ // FIXME AEM Rules plugin is disabled because it is no more compatible with SonarQube 6.4 (ClassNotFoundException: com.google.common.base.Functions) installPlugin(builder, "aemrules");
+ installPlugin(builder, "android");
+ installPlugin(builder, "authbitbucket");
+ installPlugin(builder, "authgithub");
+ installPlugin(builder, "checkstyle");
+ installPlugin(builder, "clover");
+ installPlugin(builder, "cobol");
+ installPlugin(builder, "codecrackercsharp");
+ installPlugin(builder, "cpp");
+ installPlugin(builder, "csharp");
+ // FIXME css plugin is temporarily disabled as for the moment incompatible with the web plugin installPlugin(builder, "css");
+ // FIXME erlang plugin is temporarily disabled because it is not compatible with SQ 6.4 until usage of Colorizer API is removed
+ // FIXME findbugs plugin is temporarily disabled because it is not compatible with SQ 6.4 until usage of Colorizer API is removed
+ installPlugin(builder, "flex");
+ installPlugin(builder, "github");
+ installPlugin(builder, "googleanalytics");
+ installPlugin(builder, "groovy");
+ installPlugin(builder, "java");
+ // FIXME javaProperties plugin is temporarily disabled as for the moment the github repo doesn't exist anymore installPlugin(builder, "javaProperties");
+ installPlugin(builder, "javascript");
+ installPlugin(builder, "jdepend");
+ installPlugin(builder, "l10nde");
+ installPlugin(builder, "l10nel");
+ installPlugin(builder, "l10nes");
+ installPlugin(builder, "l10nfr");
+ installPlugin(builder, "l10nit");
+ installPlugin(builder, "l10nja");
+ installPlugin(builder, "l10nko");
+ installPlugin(builder, "l10npt");
+ installPlugin(builder, "l10nru");
+ installPlugin(builder, "l10nzh");
+ installPlugin(builder, "ldap");
+ installPlugin(builder, "lua");
+ installPlugin(builder, "php");
+ installPlugin(builder, "pitest");
+ installPlugin(builder, "pli");
+ installPlugin(builder, "plsql");
+ installPlugin(builder, "pmd");
+ // FIXME puppet plugin is temporarily disabled because it is not compatible with SQ 6.4 until usage of Colorizer API is removed
+ installPlugin(builder, "python");
+ installPlugin(builder, "rci");
+ installPlugin(builder, "rpg");
+ installPlugin(builder, "scmclearcase");
+ installPlugin(builder, "scmcvs");
+ installPlugin(builder, "scmgit");
+ installPlugin(builder, "scmjazzrtc");
+ installPlugin(builder, "scmmercurial");
+ installPlugin(builder, "scmperforce");
+ installPlugin(builder, "scmsvn");
+ installPlugin(builder, "scmtfvc");
+ installPlugin(builder, "softvis3d");
+ installPlugin(builder, "sonargraphintegration");
+ installPlugin(builder, "status");
+ installPlugin(builder, "swift");
+ installPlugin(builder, "vb");
+ installPlugin(builder, "vbnet");
+ installPlugin(builder, "web");
+ installPlugin(builder, "xanitizer");
+ installPlugin(builder, "xml");
+
+ activateLicenses(builder);
+ ORCHESTRATOR = builder.build();
+ ORCHESTRATOR.start();
+ }
+
+ @Rule
+ public ErrorCollector errorCollector = new ErrorCollector();
+
+ @Test
+ public void analysis_of_project_with_all_supported_languages() {
+ SonarScanner analysis = newAnalysis();
+ BuildResult result = ORCHESTRATOR.executeBuildQuietly(analysis);
+ if (result.getLastStatus() != 0) {
+ fail(result.getLogs());
+ }
+ for (Check check : CHECKS) {
+ System.out.println(check.getClass().getSimpleName() + "...");
+ check.validate(new Validation(ORCHESTRATOR, errorCollector));
+ }
+ }
+
+ @Test
+ public void preview_analysis_of_project_with_all_supported_languages() {
+ SonarScanner analysis = newAnalysis();
+ analysis.setProperty("sonar.analysis.mode", "issues");
+ BuildResult result = ORCHESTRATOR.executeBuildQuietly(analysis);
+ if (result.getLastStatus() != 0) {
+ fail(result.getLogs());
+ }
+ }
+
+ private static SonarScanner newAnalysis() {
+ SonarScanner analysis = SonarScanner.create(Project.basedir());
+
+ // required to bypass usage of build-wrapper
+ analysis.setProperties("sonar.cfamily.build-wrapper-output.bypass", "true");
+ return analysis;
+ }
+
+ private static void activateLicenses(OrchestratorBuilder builder) {
+ LICENSED_PLUGINS.forEach(builder::activateLicense);
+ }
+
+ private static void installPlugin(OrchestratorBuilder builder, String pluginKey) {
+ builder.setOrchestratorProperty(pluginKey + "Version", "LATEST_RELEASE");
+ builder.addPlugin(pluginKey);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/plugins/Project.java b/tests/src/test/java/org/sonarqube/tests/plugins/Project.java
new file mode 100644
index 00000000000..e6bfdffb015
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/plugins/Project.java
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.plugins;
+
+import com.google.common.base.Function;
+import java.io.File;
+import java.util.Collection;
+import javax.annotation.Nullable;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang.StringUtils;
+import util.ItUtils;
+
+import static com.google.common.collect.FluentIterable.from;
+
+public class Project {
+
+ public static File basedir() {
+ return ItUtils.projectDir("plugins/project");
+ }
+
+ public static Iterable<String> allFilesInDir(final String dirPath) {
+ Collection<File> files = FileUtils.listFiles(new File(basedir(), dirPath), null, true);
+ return from(files).transform(new Function<File, String>() {
+ @Nullable
+ public String apply(File file) {
+ // transforms /absolute/path/to/src/java/Foo.java to src/java/Foo.java
+ String filePath = FilenameUtils.separatorsToUnix(file.getPath());
+ return dirPath + StringUtils.substringAfterLast(filePath, dirPath);
+ }
+ });
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/plugins/VersionPluginTest.java b/tests/src/test/java/org/sonarqube/tests/plugins/VersionPluginTest.java
new file mode 100644
index 00000000000..38b91d3f7ac
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/plugins/VersionPluginTest.java
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.plugins;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category3Suite;
+import java.io.IOException;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.WsClient;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newAdminWsClient;
+
+public class VersionPluginTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category3Suite.ORCHESTRATOR;
+ private static WsClient wsClient;
+
+ @BeforeClass
+ public static void init_ws_cient() {
+ wsClient = newAdminWsClient(orchestrator);
+ }
+
+ @Before
+ public void deleteData() throws IOException {
+ orchestrator.resetData();
+ }
+
+ @Test
+ public void check_functional_version() {
+ assertThat(wsClient.wsConnector().call(new GetRequest("api/plugins/installed")).content()).contains("1.0.2 (build 42)");
+
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/plugins/checks/AbapCheck.java b/tests/src/test/java/org/sonarqube/tests/plugins/checks/AbapCheck.java
new file mode 100644
index 00000000000..a2fc7080198
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/plugins/checks/AbapCheck.java
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.plugins.checks;
+
+public class AbapCheck implements Check {
+
+ public static final String DIR = "src/abap";
+
+ @Override
+ public void validate(Validation validation) {
+ validation.mustHaveNonEmptySource(DIR);
+ validation.mustHaveIssues(DIR);
+ validation.mustHaveSize(DIR);
+ validation.mustHaveComments(DIR);
+ validation.mustHaveComplexity(DIR);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/plugins/checks/CCheck.java b/tests/src/test/java/org/sonarqube/tests/plugins/checks/CCheck.java
new file mode 100644
index 00000000000..0465a417995
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/plugins/checks/CCheck.java
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.plugins.checks;
+
+public class CCheck implements Check {
+
+ public static final String DIR = "src/c";
+
+ @Override
+ public void validate(Validation validation) {
+ validation.mustHaveNonEmptySource(DIR);
+ validation.mustHaveIssues(DIR);
+ validation.mustHaveSize(DIR);
+ validation.mustHaveComments(DIR);
+ validation.mustHaveComplexity(DIR);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/plugins/checks/Check.java b/tests/src/test/java/org/sonarqube/tests/plugins/checks/Check.java
new file mode 100644
index 00000000000..e85542a6ea7
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/plugins/checks/Check.java
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.plugins.checks;
+
+public interface Check {
+ void validate(Validation validation);
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/plugins/checks/CobolCheck.java b/tests/src/test/java/org/sonarqube/tests/plugins/checks/CobolCheck.java
new file mode 100644
index 00000000000..dc23032edde
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/plugins/checks/CobolCheck.java
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.plugins.checks;
+
+public class CobolCheck implements Check {
+
+ public static final String DIR = "src/cobol";
+
+ @Override
+ public void validate(Validation validation) {
+ validation.mustHaveNonEmptySource(DIR);
+ validation.mustHaveIssues(DIR);
+ validation.mustHaveSize(DIR);
+ validation.mustHaveComments(DIR);
+ validation.mustHaveComplexity(DIR);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/plugins/checks/CppCheck.java b/tests/src/test/java/org/sonarqube/tests/plugins/checks/CppCheck.java
new file mode 100644
index 00000000000..9cb5359317f
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/plugins/checks/CppCheck.java
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.plugins.checks;
+
+public class CppCheck implements Check {
+
+ public static final String DIR = "src/cpp";
+
+ @Override
+ public void validate(Validation validation) {
+ validation.mustHaveNonEmptySource(DIR);
+ validation.mustHaveIssues(DIR);
+ validation.mustHaveSize(DIR);
+ validation.mustHaveComments(DIR);
+ validation.mustHaveComplexity(DIR);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/plugins/checks/CssCheck.java b/tests/src/test/java/org/sonarqube/tests/plugins/checks/CssCheck.java
new file mode 100644
index 00000000000..b2d10039ae2
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/plugins/checks/CssCheck.java
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.plugins.checks;
+
+public class CssCheck implements Check {
+
+ public static final String DIR = "src/css";
+
+ @Override
+ public void validate(Validation validation) {
+ validation.mustHaveNonEmptySource(DIR);
+ validation.mustHaveIssues(DIR);
+ validation.mustHaveSize(DIR);
+ validation.mustHaveComments(DIR);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/plugins/checks/FlexCheck.java b/tests/src/test/java/org/sonarqube/tests/plugins/checks/FlexCheck.java
new file mode 100644
index 00000000000..c2443b49363
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/plugins/checks/FlexCheck.java
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.plugins.checks;
+
+public class FlexCheck implements Check {
+
+ public static final String DIR = "src/flex";
+
+ @Override
+ public void validate(Validation validation) {
+ validation.mustHaveNonEmptySource(DIR);
+ validation.mustHaveSize(DIR);
+ validation.mustHaveComments(DIR);
+ validation.mustHaveComplexity(DIR);
+ validation.mustHaveIssues(DIR + "/HasIssues.as");
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/plugins/checks/GroovyCheck.java b/tests/src/test/java/org/sonarqube/tests/plugins/checks/GroovyCheck.java
new file mode 100644
index 00000000000..f8bd3b0d9d5
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/plugins/checks/GroovyCheck.java
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.plugins.checks;
+
+public class GroovyCheck implements Check {
+
+ public static final String DIR = "src/groovy";
+
+ @Override
+ public void validate(Validation validation) {
+ validation.mustHaveNonEmptySource(DIR);
+ validation.mustHaveIssues(DIR);
+ validation.mustHaveSize(DIR);
+ validation.mustHaveComments(DIR);
+ validation.mustHaveComplexity(DIR);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/plugins/checks/JavaCheck.java b/tests/src/test/java/org/sonarqube/tests/plugins/checks/JavaCheck.java
new file mode 100644
index 00000000000..f5c7f3f86fe
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/plugins/checks/JavaCheck.java
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.plugins.checks;
+
+public class JavaCheck implements Check {
+
+ public static final String DIR = "src/java";
+
+ @Override
+ public void validate(Validation validation) {
+ validation.mustHaveNonEmptySource(DIR);
+ validation.mustHaveIssues(DIR);
+ validation.mustHaveSize(DIR);
+ validation.mustHaveComments(DIR);
+ validation.mustHaveComplexity(DIR);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/plugins/checks/JavascriptCheck.java b/tests/src/test/java/org/sonarqube/tests/plugins/checks/JavascriptCheck.java
new file mode 100644
index 00000000000..40f74d64943
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/plugins/checks/JavascriptCheck.java
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.plugins.checks;
+
+public class JavascriptCheck implements Check {
+
+ public static final String SRC_DIR = "src/js";
+
+ @Override
+ public void validate(Validation validation) {
+ validation.mustHaveNonEmptySource(SRC_DIR);
+ validation.mustHaveSize(SRC_DIR);
+ validation.mustHaveComments(SRC_DIR);
+ validation.mustHaveComplexity(SRC_DIR);
+ validation.mustHaveIssues(SRC_DIR + "/HasIssues.js");
+ validation.mustHaveMeasuresGreaterThan(SRC_DIR + "/Person.js", 0, "coverage");
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/plugins/checks/PhpCheck.java b/tests/src/test/java/org/sonarqube/tests/plugins/checks/PhpCheck.java
new file mode 100644
index 00000000000..f1b5e36aa36
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/plugins/checks/PhpCheck.java
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.plugins.checks;
+
+public class PhpCheck implements Check {
+
+ public static final String DIR = "src/php";
+
+ @Override
+ public void validate(Validation validation) {
+ validation.mustHaveNonEmptySource(DIR);
+ validation.mustHaveIssues(DIR);
+ validation.mustHaveSize(DIR);
+ validation.mustHaveComments(DIR);
+ validation.mustHaveComplexity(DIR);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/plugins/checks/PliCheck.java b/tests/src/test/java/org/sonarqube/tests/plugins/checks/PliCheck.java
new file mode 100644
index 00000000000..3d08a1baaec
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/plugins/checks/PliCheck.java
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.plugins.checks;
+
+public class PliCheck implements Check {
+
+ public static final String DIR = "src/pli";
+
+ @Override
+ public void validate(Validation validation) {
+ validation.mustHaveNonEmptySource(DIR);
+ validation.mustHaveSize(DIR);
+ validation.mustHaveComments(DIR);
+ validation.mustHaveComplexity(DIR);
+ validation.mustHaveIssues(DIR + "/hasissues.pli");
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/plugins/checks/PythonCheck.java b/tests/src/test/java/org/sonarqube/tests/plugins/checks/PythonCheck.java
new file mode 100644
index 00000000000..d7f901ac817
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/plugins/checks/PythonCheck.java
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.plugins.checks;
+
+import org.sonarqube.tests.plugins.Project;
+
+public class PythonCheck implements Check {
+
+ public static final String DIR = "src/python";
+
+ @Override
+ public void validate(Validation validation) {
+ // all files have size measures, even empty __init__.py
+ validation.mustHaveSize(DIR);
+
+ for (String filePath : Project.allFilesInDir(DIR)) {
+ if (filePath.endsWith("__init__.py")) {
+ validation.mustHaveSource(filePath);
+ } else {
+ validation.mustHaveNonEmptySource(filePath);
+ validation.mustHaveComments(filePath);
+ validation.mustHaveComplexity(filePath);
+ }
+ }
+
+ validation.mustHaveIssues(DIR + "/hasissues.py");
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/plugins/checks/RpgCheck.java b/tests/src/test/java/org/sonarqube/tests/plugins/checks/RpgCheck.java
new file mode 100644
index 00000000000..eb0010e801f
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/plugins/checks/RpgCheck.java
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.plugins.checks;
+
+public class RpgCheck implements Check {
+
+ public static final String DIR = "src/rpg";
+
+ @Override
+ public void validate(Validation validation) {
+ validation.mustHaveNonEmptySource(DIR);
+ validation.mustHaveIssues(DIR);
+ validation.mustHaveSize(DIR);
+ validation.mustHaveComments(DIR);
+ validation.mustHaveComplexity(DIR);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/plugins/checks/SwiftCheck.java b/tests/src/test/java/org/sonarqube/tests/plugins/checks/SwiftCheck.java
new file mode 100644
index 00000000000..8360ff253d1
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/plugins/checks/SwiftCheck.java
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.plugins.checks;
+
+public class SwiftCheck implements Check {
+
+ public static final String DIR = "src/swift";
+
+ @Override
+ public void validate(Validation validation) {
+ validation.mustHaveNonEmptySource(DIR);
+ validation.mustHaveIssues(DIR);
+ validation.mustHaveSize(DIR);
+ validation.mustHaveComplexity(DIR);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/plugins/checks/Validation.java b/tests/src/test/java/org/sonarqube/tests/plugins/checks/Validation.java
new file mode 100644
index 00000000000..9b2f9a4ea21
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/plugins/checks/Validation.java
@@ -0,0 +1,163 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.plugins.checks;
+
+import com.google.common.base.Joiner;
+import com.google.gson.Gson;
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.plugins.Project;
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+import org.hamcrest.Matchers;
+import org.junit.rules.ErrorCollector;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.WsResponse;
+
+import static java.util.Arrays.asList;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static util.ItUtils.getMeasuresAsDoubleByMetricKey;
+import static util.ItUtils.newAdminWsClient;
+
+/**
+ *
+ * TODO must have syntax highlighting
+ * TODO must have duplications
+ * TODO must have issues with debt
+ * TODO must have tests
+ * TODO must have coverage
+ */
+public class Validation {
+
+ private final Orchestrator orchestrator;
+ private final ErrorCollector errorCollector;
+
+ public Validation(Orchestrator orchestrator, ErrorCollector errorCollector) {
+ this.orchestrator = orchestrator;
+ this.errorCollector = errorCollector;
+ }
+
+ public void mustHaveIssues(String path) {
+ // TODO use the WS api/issues
+ mustHaveMeasuresGreaterThan(path, 1, "violations");
+ }
+
+ public void mustHaveComments(String path) {
+ mustHaveMeasuresGreaterThan(path, 0, "comment_lines", "comment_lines_density");
+ }
+
+ public void mustHaveComplexity(String path) {
+ mustHaveMeasuresGreaterThan(path, 0, "complexity");
+ }
+
+ public void mustHaveSize(String path) {
+ mustHaveMeasuresGreaterThan(path, 0, "ncloc", "lines");
+ }
+
+ public void mustHaveMeasuresGreaterThan(String path, int min, String... metricKeys) {
+ for (String filePath : toFiles(path)) {
+ fileMustHaveMeasures(filePath, metricKeys, min);
+ }
+ }
+
+ private void fileMustHaveMeasures(String filePath, String[] metricKeys, int min) {
+ String componentKey = filePathToKey(filePath);
+ Map<String, Double> measures = getMeasuresAsDoubleByMetricKey(orchestrator, componentKey, metricKeys);
+ errorCollector.checkThat("Measures " + Joiner.on(",").join(metricKeys) + " are set on file " + filePath, componentKey, notNullValue());
+ if (!measures.isEmpty()) {
+ for (String metricKey : metricKeys) {
+ Double measure = measures.get(metricKey);
+ errorCollector.checkThat("Measure " + metricKey + " is set on file " + filePath, measure, notNullValue());
+ if (measure != null) {
+ errorCollector.checkThat("Measure " + metricKey + " is positive on file " + filePath, measure.intValue(), Matchers.greaterThanOrEqualTo(min));
+ }
+ }
+ }
+ }
+
+ /**
+ * Checks that each source file of the given directory is uploaded to server.
+ * @param path relative path to source directory or source file
+ */
+ public void mustHaveNonEmptySource(String path) {
+ mustHaveSourceWithAtLeast(path, 1);
+ }
+
+ public void mustHaveSource(String path) {
+ mustHaveSourceWithAtLeast(path, 0);
+ }
+
+ private void mustHaveSourceWithAtLeast(String path, int minLines) {
+ for (String filePath : toFiles(path)) {
+ WsResponse response = newAdminWsClient(orchestrator).wsConnector().call(new GetRequest("api/sources/lines").setParam("key", filePathToKey(filePath)));
+ errorCollector.checkThat("Source is set on file " + filePath, response.isSuccessful(), is(true));
+ Sources source = Sources.parse(response.content());
+ if (source != null) {
+ errorCollector.checkThat("Source is empty on file " + filePath, source.getSources().size(), Matchers.greaterThanOrEqualTo(minLines));
+ }
+ }
+ }
+
+ private Iterable<String> toFiles(String path) {
+ File fileOrDir = new File(Project.basedir(), path);
+ if (!fileOrDir.exists()) {
+ throw new IllegalArgumentException("Path does not exist: " + fileOrDir);
+ }
+ if (fileOrDir.isDirectory()) {
+ return Project.allFilesInDir(path);
+ }
+ return asList(path);
+ }
+
+ private String filePathToKey(String filePath) {
+ return "all-langs:" + filePath;
+ }
+
+ public static class Sources {
+
+ private List<Source> sources;
+
+ private Sources(List<Source> sources) {
+ this.sources = sources;
+ }
+
+ public List<Source> getSources() {
+ return sources;
+ }
+
+ public static Sources parse(String json) {
+ Gson gson = new Gson();
+ return gson.fromJson(json, Sources.class);
+ }
+
+ public static class Source {
+ private final String line;
+
+ private Source(String line) {
+ this.line = line;
+ }
+
+ public String getLine() {
+ return line;
+ }
+ }
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/plugins/checks/VbCheck.java b/tests/src/test/java/org/sonarqube/tests/plugins/checks/VbCheck.java
new file mode 100644
index 00000000000..4438277fd49
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/plugins/checks/VbCheck.java
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.plugins.checks;
+
+public class VbCheck implements Check {
+
+ public static final String DIR = "src/vb";
+
+ @Override
+ public void validate(Validation validation) {
+ validation.mustHaveNonEmptySource(DIR);
+ validation.mustHaveIssues(DIR);
+ validation.mustHaveSize(DIR);
+ validation.mustHaveComments(DIR);
+ validation.mustHaveComplexity(DIR);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/plugins/checks/WebCheck.java b/tests/src/test/java/org/sonarqube/tests/plugins/checks/WebCheck.java
new file mode 100644
index 00000000000..8c999254cdd
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/plugins/checks/WebCheck.java
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.plugins.checks;
+
+public class WebCheck implements Check {
+
+ public static final String DIR = "src/web";
+
+ @Override
+ public void validate(Validation validation) {
+ validation.mustHaveNonEmptySource(DIR);
+ validation.mustHaveIssues(DIR);
+ validation.mustHaveSize(DIR);
+ validation.mustHaveComments(DIR);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/previewAnalysis/ToDoTest.java b/tests/src/test/java/org/sonarqube/tests/previewAnalysis/ToDoTest.java
new file mode 100644
index 00000000000..67122882ba2
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/previewAnalysis/ToDoTest.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.previewAnalysis;
+
+public class ToDoTest {
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/projectAdministration/BackgroundTasksTest.java b/tests/src/test/java/org/sonarqube/tests/projectAdministration/BackgroundTasksTest.java
new file mode 100644
index 00000000000..0c4e916ebf7
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/projectAdministration/BackgroundTasksTest.java
@@ -0,0 +1,115 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.projectAdministration;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category1Suite;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.pageobjects.BackgroundTaskItem;
+import org.sonarqube.pageobjects.BackgroundTasksPage;
+import org.sonarqube.pageobjects.Navigation;
+import util.user.UserRule;
+
+import static com.codeborne.selenide.CollectionCondition.sizeGreaterThan;
+import static util.ItUtils.projectDir;
+import static util.selenium.Selenese.runSelenese;
+
+public class BackgroundTasksTest {
+
+ private static final String ADMIN_USER_LOGIN = "admin-user";
+
+ @ClassRule
+ public static Orchestrator ORCHESTRATOR = Category1Suite.ORCHESTRATOR;
+
+ @Rule
+ public UserRule userRule = UserRule.from(ORCHESTRATOR);
+
+ private Navigation nav = Navigation.create(ORCHESTRATOR);
+
+ @BeforeClass
+ public static void beforeClass() {
+ executeBuild("test-project", "Test Project");
+ executeBuild("test-project-2", "Another Test Project");
+ }
+
+ @Before
+ public void before() {
+ userRule.createAdminUser(ADMIN_USER_LOGIN, ADMIN_USER_LOGIN);
+ }
+
+ @After
+ public void deleteAdminUser() {
+ userRule.resetUsers();
+ }
+
+ @Test
+ public void should_not_display_failing_and_search_and_filter_elements_on_project_level_page() throws Exception {
+ runSelenese(ORCHESTRATOR, "/projectAdministration/BackgroundTasksTest/should_not_display_failing_and_search_and_filter_elements_on_project_level_page.html");
+ }
+
+ @Test
+ public void display_scanner_context() {
+ nav.logIn().submitCredentials(ADMIN_USER_LOGIN);
+ BackgroundTasksPage page = nav.openBackgroundTasksPage();
+
+ page.getTasks().shouldHave(sizeGreaterThan(0));
+ BackgroundTaskItem task = page.getTasksAsItems().get(0);
+ task.openActions()
+ .openScannerContext()
+ .assertScannerContextContains("SonarQube plugins:")
+ .assertScannerContextContains("Global properties:");
+ }
+
+ @Test
+ public void display_error_stacktrace() {
+ Navigation nav = Navigation.create(ORCHESTRATOR);
+ executeBuild("test-project", "Test Project", "2010-01-01");
+
+ nav.logIn().submitCredentials(ADMIN_USER_LOGIN);
+ BackgroundTasksPage page = nav.openBackgroundTasksPage();
+
+ page.getTasks().shouldHave(sizeGreaterThan(0));
+ BackgroundTaskItem task = page.getTasksAsItems().get(0);
+ task.openActions()
+ .openErrorStacktrace()
+ .assertErrorStacktraceContains("Date of analysis cannot be older than the date of the last known analysis");
+ }
+
+ private static void executeBuild(String projectKey, String projectName) {
+ ORCHESTRATOR.executeBuild(
+ SonarScanner.create(projectDir("shared/xoo-sample"))
+ .setProjectKey(projectKey)
+ .setProjectName(projectName));
+ }
+
+ private static void executeBuild(String projectKey, String projectName, String date) {
+ ORCHESTRATOR.executeBuild(
+ SonarScanner.create(projectDir("shared/xoo-sample"))
+ .setProjectKey(projectKey)
+ .setProjectName(projectName)
+ .setProperty("sonar.projectDate", date));
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/projectAdministration/BulkDeletionTest.java b/tests/src/test/java/org/sonarqube/tests/projectAdministration/BulkDeletionTest.java
new file mode 100644
index 00000000000..e31020a3700
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/projectAdministration/BulkDeletionTest.java
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.projectAdministration;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category1Suite;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import util.user.UserRule;
+
+import static util.ItUtils.projectDir;
+import static util.selenium.Selenese.runSelenese;
+
+public class BulkDeletionTest {
+
+ private static final String ADMIN_USER_LOGIN = "admin-user";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ @Rule
+ public UserRule userRule = UserRule.from(orchestrator);
+
+ @Before
+ public void deleteData() {
+ orchestrator.resetData();
+ userRule.createAdminUser(ADMIN_USER_LOGIN, ADMIN_USER_LOGIN);
+ }
+
+ @After
+ public void deleteAdminUser() {
+ userRule.resetUsers();
+ }
+
+ /**
+ * SONAR-2614, SONAR-3805
+ */
+ @Test
+ public void test_bulk_deletion_on_selected_projects() throws Exception {
+ // we must have several projects to test the bulk deletion
+ executeBuild("cameleon-1", "Sample-Project");
+ executeBuild("cameleon-2", "Foo-Application");
+ executeBuild("cameleon-3", "Bar-Sonar-Plugin");
+
+ runSelenese(orchestrator, "/projectAdministration/BulkDeletionTest/bulk-delete-filter-projects.html");
+ }
+
+ private void executeBuild(String projectKey, String projectName) {
+ orchestrator.executeBuild(
+ SonarScanner.create(projectDir("shared/xoo-sample"))
+ .setProjectKey(projectKey)
+ .setProjectName(projectName));
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectAdministrationTest.java b/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectAdministrationTest.java
new file mode 100644
index 00000000000..7d46eb61e7c
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectAdministrationTest.java
@@ -0,0 +1,220 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.projectAdministration;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category1Suite;
+import java.io.UnsupportedEncodingException;
+import java.sql.SQLException;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.time.DateFormatUtils;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.wsclient.SonarClient;
+import org.sonar.wsclient.base.HttpException;
+import org.sonar.wsclient.user.UserParameters;
+import org.sonarqube.pageobjects.Navigation;
+import org.sonarqube.pageobjects.settings.SettingsPage;
+import util.user.UserRule;
+
+import static org.apache.commons.lang.time.DateUtils.addDays;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.getComponent;
+import static util.ItUtils.projectDir;
+import static util.selenium.Selenese.runSelenese;
+
+public class ProjectAdministrationTest {
+ private static final String DELETE_WS_ENDPOINT = "api/projects/bulk_delete";
+
+ // take some day in the past
+ private static final String ANALYSIS_DATE = DateFormatUtils.ISO_DATE_FORMAT.format(addDays(new Date(), -1));
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Rule
+ public UserRule userRule = UserRule.from(orchestrator);
+
+ private Navigation nav = Navigation.create(orchestrator);
+
+ private static final String PROJECT_KEY = "sample";
+ private static final String FILE_KEY = "sample:src/main/xoo/sample/Sample.xoo";
+ private String adminUser;
+
+ @Before
+ public void deleteAnalysisData() throws SQLException {
+ orchestrator.resetData();
+ adminUser = userRule.createAdminUser();
+ }
+
+ @Test
+ public void delete_project_by_web_service() {
+ scanSampleWithDate(ANALYSIS_DATE);
+
+ assertThat(getComponent(orchestrator, PROJECT_KEY)).isNotNull();
+ assertThat(getComponent(orchestrator, FILE_KEY)).isNotNull();
+
+ orchestrator.getServer().adminWsClient().post(DELETE_WS_ENDPOINT, "keys", PROJECT_KEY);
+
+ assertThat(getComponent(orchestrator, PROJECT_KEY)).isNull();
+ assertThat(getComponent(orchestrator, FILE_KEY)).isNull();
+ }
+
+ @Test
+ public void fail_when_trying_to_delete_a_file() {
+ expectedException.expect(HttpException.class);
+ scanSampleWithDate(ANALYSIS_DATE);
+
+ assertThat(getComponent(orchestrator, PROJECT_KEY)).isNotNull();
+ assertThat(getComponent(orchestrator, FILE_KEY)).isNotNull();
+
+ // it's forbidden to delete only some files
+ orchestrator.getServer().adminWsClient().post(DELETE_WS_ENDPOINT, "keys", FILE_KEY);
+ }
+
+ @Test
+ public void fail_when_insufficient_privilege() {
+ expectedException.expect(HttpException.class);
+ scanSampleWithDate(ANALYSIS_DATE);
+
+ assertThat(getComponent(orchestrator, PROJECT_KEY)).isNotNull();
+
+ // use wsClient() instead of adminWsClient()
+ orchestrator.getServer().wsClient().post(DELETE_WS_ENDPOINT, "keys", PROJECT_KEY);
+ }
+
+ /**
+ * Test updated for SONAR-3570 and SONAR-5923
+ */
+ @Test
+ public void project_deletion() {
+ String projectAdminUser = "project-deletion-with-admin-permission-on-project";
+ SonarClient wsClient = orchestrator.getServer().adminWsClient();
+ try {
+ SonarScanner scan = SonarScanner.create(projectDir("shared/xoo-sample"));
+ orchestrator.executeBuild(scan);
+
+ // Create user having admin permission on previously analysed project
+ wsClient.userClient().create(
+ UserParameters.create().login(projectAdminUser).name(projectAdminUser).password("password").passwordConfirmation("password"));
+
+ wsClient.post("api/permissions/add_user",
+ "login", projectAdminUser,
+ "projectKey", "sample",
+ "permission", "admin");
+
+ runSelenese(orchestrator, "/projectAdministration/ProjectAdministrationTest/project-deletion/project-deletion.html");
+ } finally {
+ wsClient.userClient().deactivate(projectAdminUser);
+ }
+ }
+
+ // SONAR-4203
+ @Test
+ @Ignore("refactor with wsClient")
+ public void delete_version_of_multimodule_project() {
+ GregorianCalendar today = new GregorianCalendar();
+ SonarScanner build = SonarScanner.create(projectDir("shared/xoo-multi-modules-sample"))
+ .setProperty("sonar.dynamicAnalysis", "false")
+ .setProperty("sonar.projectDate", (today.get(Calendar.YEAR) - 1) + "-01-01");
+ orchestrator.executeBuild(build);
+
+ // The analysis must be run once again to have an history so that it is possible
+ // to set/delete version on old snapshot
+ build.setProperty("sonar.projectDate", today.get(Calendar.YEAR) + "-01-01");
+ orchestrator.executeBuild(build);
+
+ // There are 7 modules
+ assertThat(count("events where category='Version'")).as("Different number of events").isEqualTo(1);
+
+ runSelenese(orchestrator, "/projectAdministration/ProjectAdministrationTest/project-administration/multimodule-project-modify-version.html");
+
+ assertThat(count("events where category='Version'")).as("Different number of events").isEqualTo(2);
+
+ runSelenese(orchestrator, "/projectAdministration/ProjectAdministrationTest/project-administration/multimodule-project-delete-version.html");
+
+ assertThat(count("events where category='Version'")).as("Different number of events").isEqualTo(1);
+ }
+
+ @Test
+ public void display_project_settings() throws UnsupportedEncodingException {
+ scanSample(null, null);
+
+ SettingsPage page = nav.logIn().submitCredentials(adminUser).openSettings("sample")
+ .assertMenuContains("Analysis Scope")
+ .assertMenuContains("Category 1")
+ .assertMenuContains("DEV")
+ .assertMenuContains("project-only")
+ .assertMenuContains("Xoo")
+ .assertSettingDisplayed("sonar.dbcleaner.daysBeforeDeletingClosedIssues");
+
+ page.openCategory("project-only")
+ .assertSettingDisplayed("prop_only_on_project");
+
+ page.openCategory("General")
+ .assertStringSettingValue("sonar.dbcleaner.daysBeforeDeletingClosedIssues", "30")
+ .assertStringSettingValue("sonar.leak.period", "previous_version")
+ .assertBooleanSettingValue("sonar.dbcleaner.cleanDirectory", true)
+ .setStringValue("sonar.dbcleaner.daysBeforeDeletingClosedIssues", "1")
+ .assertStringSettingValue("sonar.dbcleaner.daysBeforeDeletingClosedIssues", "1");
+ }
+
+ @Test
+ public void display_module_settings() throws UnsupportedEncodingException {
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-multi-modules-sample")));
+
+ nav.logIn().submitCredentials(adminUser)
+ .openSettings("com.sonarsource.it.samples:multi-modules-sample:module_a")
+ .assertMenuContains("Analysis Scope")
+ .assertSettingDisplayed("sonar.coverage.exclusions");
+ }
+
+ private void scanSampleWithDate(String date) {
+ scanSample(date, null);
+ }
+
+ private void scanSample(@Nullable String date, @Nullable String profile) {
+ SonarScanner scan = SonarScanner.create(projectDir("shared/xoo-sample"))
+ .setProperty("sonar.cpd.exclusions", "**/*");
+ if (date != null) {
+ scan.setProperty("sonar.projectDate", date);
+ }
+ if (profile != null) {
+ scan.setProfile(profile);
+ }
+ orchestrator.executeBuild(scan);
+ }
+
+ private int count(String condition) {
+ return orchestrator.getDatabase().countSql("select count(1) from " + condition);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectKeyPageTest.java b/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectKeyPageTest.java
new file mode 100644
index 00000000000..a1d7f61aba1
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectKeyPageTest.java
@@ -0,0 +1,182 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.projectAdministration;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category1Suite;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.pageobjects.Navigation;
+import org.sonarqube.pageobjects.ProjectKeyPage;
+
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+import static com.codeborne.selenide.WebDriverRunner.url;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.projectDir;
+
+public class ProjectKeyPageTest {
+
+ @ClassRule
+ public static Orchestrator ORCHESTRATOR = Category1Suite.ORCHESTRATOR;
+
+ private static WsClient wsClient;
+
+ @BeforeClass
+ public static void setUp() {
+ wsClient = newAdminWsClient(ORCHESTRATOR);
+ }
+
+ @Before
+ public void cleanUp() {
+ ORCHESTRATOR.resetData();
+ }
+
+ private Navigation nav = Navigation.create(ORCHESTRATOR);
+
+ @Test
+ public void change_key_when_no_modules() {
+ createProject("sample");
+
+ ProjectKeyPage page = openPage("sample");
+ page.assertSimpleUpdate().trySimpleUpdate("another");
+
+ assertThat(url()).endsWith("/project/key?id=another");
+ }
+
+ @Test
+ public void fail_to_change_key_when_no_modules() {
+ createProject("sample");
+ createProject("another");
+
+ ProjectKeyPage page = openPage("sample");
+ page.assertSimpleUpdate().trySimpleUpdate("another");
+
+ $(".alert.alert-danger").shouldBe(visible);
+ assertThat(url()).endsWith("/project/key?id=sample");
+ }
+
+ @Test
+ public void change_key_of_multi_modules_project() {
+ analyzeProject("shared/xoo-multi-modules-sample", "sample");
+
+ ProjectKeyPage page = openPage("sample");
+ page.openFineGrainedUpdate().tryFineGrainedUpdate("sample", "another");
+
+ assertThat(url()).endsWith("/project/key?id=another");
+ }
+
+ @Test
+ public void fail_to_change_key_of_multi_modules_project() {
+ analyzeProject("shared/xoo-multi-modules-sample", "sample");
+ createProject("another");
+
+ ProjectKeyPage page = openPage("sample");
+ page.openFineGrainedUpdate().tryFineGrainedUpdate("sample", "another");
+
+ $(".alert.alert-danger").shouldBe(visible);
+ assertThat(url()).endsWith("/project/key?id=sample");
+ }
+
+ @Test
+ public void change_key_of_module_of_multi_modules_project() {
+ analyzeProject("shared/xoo-multi-modules-sample", "sample");
+
+ ProjectKeyPage page = openPage("sample");
+ page.openFineGrainedUpdate().tryFineGrainedUpdate("sample:module_a:module_a1", "another");
+
+ $("#update-key-confirmation-form").shouldNotBe(visible);
+
+ nav.openProjectKey("another");
+ assertThat(url()).endsWith("/project/key?id=another");
+ }
+
+ @Test
+ public void fail_to_change_key_of_module_of_multi_modules_project() {
+ analyzeProject("shared/xoo-multi-modules-sample", "sample");
+ createProject("another");
+
+ ProjectKeyPage page = openPage("sample");
+ page.openFineGrainedUpdate().tryFineGrainedUpdate("sample:module_a:module_a1", "another");
+
+ $(".alert.alert-danger").shouldBe(visible);
+ }
+
+ @Test
+ public void bulk_change() {
+ analyzeProject("shared/xoo-multi-modules-sample", "sample");
+
+ ProjectKeyPage page = openPage("sample");
+ page.assertBulkChange().simulateBulkChange("sample", "another");
+
+ $("#bulk-update-results").shouldBe(visible);
+ page.assertBulkChangeSimulationResult("sample", "another")
+ .assertBulkChangeSimulationResult("sample:module_a:module_a1", "another:module_a:module_a1");
+
+ page.confirmBulkUpdate().assertSuccessfulBulkUpdate();
+ }
+
+ @Test
+ public void fail_to_bulk_change_because_no_changed_key() {
+ analyzeProject("shared/xoo-multi-modules-sample", "sample");
+
+ ProjectKeyPage page = openPage("sample");
+ page.assertBulkChange().simulateBulkChange("random", "another");
+
+ $("#bulk-update-nothing").shouldBe(visible);
+ $("#bulk-update-results").shouldNotBe(visible);
+ }
+
+ @Test
+ public void fail_to_bulk_change_because_of_duplications() {
+ analyzeProject("shared/xoo-multi-modules-sample", "sample");
+
+ ProjectKeyPage page = openPage("sample");
+ page.assertBulkChange().simulateBulkChange("module_a1", "module_a2");
+
+ $("#bulk-update-duplicate").shouldBe(visible);
+ $("#bulk-update-results").shouldBe(visible);
+
+ page.assertBulkChangeSimulationResult("sample:module_a:module_a1", "sample:module_a:module_a2")
+ .assertDuplicated("sample:module_a:module_a1");
+ }
+
+ private ProjectKeyPage openPage(String projectKey) {
+ nav.logIn().submitCredentials("admin", "admin");
+ return nav.openProjectKey(projectKey);
+ }
+
+ private static void createProject(String projectKey) {
+ wsClient.wsConnector().call(new PostRequest("api/projects/create")
+ .setParam("key", projectKey)
+ .setParam("name", projectKey));
+ }
+
+ private static void analyzeProject(String path, String projectKey) {
+ ORCHESTRATOR.executeBuild(SonarScanner.create(projectDir(path))
+ .setProjectKey(projectKey));
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectLinksPageTest.java b/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectLinksPageTest.java
new file mode 100644
index 00000000000..5cb04c3b4e8
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectLinksPageTest.java
@@ -0,0 +1,158 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.projectAdministration;
+
+import com.codeborne.selenide.Condition;
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category1Suite;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.ws.WsProjectLinks.CreateWsResponse;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.projectlinks.CreateWsRequest;
+import org.sonarqube.ws.client.projectlinks.DeleteWsRequest;
+import org.sonarqube.pageobjects.Navigation;
+import org.sonarqube.pageobjects.ProjectLinkItem;
+import org.sonarqube.pageobjects.ProjectLinksPage;
+import util.user.UserRule;
+
+import static com.codeborne.selenide.Condition.hasText;
+import static com.codeborne.selenide.Selenide.$;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.projectDir;
+
+public class ProjectLinksPageTest {
+
+ @ClassRule
+ public static Orchestrator ORCHESTRATOR = Category1Suite.ORCHESTRATOR;
+
+ private Navigation nav = Navigation.create(ORCHESTRATOR);
+
+ @Rule
+ public UserRule userRule = UserRule.from(ORCHESTRATOR);
+
+ private static WsClient wsClient;
+ private long customLinkId;
+ private String adminUser;
+
+ @BeforeClass
+ public static void setUp() {
+ wsClient = newAdminWsClient(ORCHESTRATOR);
+
+ ORCHESTRATOR.resetData();
+ ORCHESTRATOR.executeBuild(
+ SonarScanner.create(projectDir("shared/xoo-sample"))
+ .setProperty("sonar.links.homepage", "http://example.com"));
+ }
+
+ @Before
+ public void prepare() {
+ customLinkId = Long.parseLong(createCustomLink().getLink().getId());
+ adminUser = userRule.createAdminUser();
+ }
+
+ @After
+ public void clean() {
+ deleteLink(customLinkId);
+ }
+
+ @Test
+ public void should_list_links() {
+ ProjectLinksPage page = openPage();
+
+ page.getLinks().shouldHaveSize(2);
+
+ List<ProjectLinkItem> links = page.getLinksAsItems();
+ ProjectLinkItem homepageLink = links.get(0);
+ ProjectLinkItem customLink = links.get(1);
+
+ homepageLink.getName().should(hasText("Home"));
+ homepageLink.getType().should(hasText("sonar.links.homepage"));
+ homepageLink.getUrl().should(hasText("http://example.com"));
+ homepageLink.getDeleteButton().shouldNot(Condition.present);
+
+ customLink.getName().should(hasText("Custom"));
+ customLink.getType().shouldNot(Condition.present);
+ customLink.getUrl().should(hasText("http://example.org/custom"));
+ customLink.getDeleteButton().shouldBe(Condition.visible);
+ }
+
+ @Test
+ public void should_create_link() {
+ ProjectLinksPage page = openPage();
+
+ page.getLinks().shouldHaveSize(2);
+
+ $("#create-project-link").click();
+ $("#create-link-name").setValue("Test");
+ $("#create-link-url").setValue("http://example.com/test");
+ $("#create-link-confirm").click();
+
+ page.getLinks().shouldHaveSize(3);
+
+ ProjectLinkItem testLink = page.getLinksAsItems().get(2);
+
+ testLink.getName().should(hasText("Test"));
+ testLink.getType().shouldNot(Condition.present);
+ testLink.getUrl().should(hasText("http://example.com/test"));
+ testLink.getDeleteButton().shouldBe(Condition.visible);
+ }
+
+ @Test
+ public void should_delete_link() {
+ ProjectLinksPage page = openPage();
+
+ page.getLinks().shouldHaveSize(2);
+
+ List<ProjectLinkItem> links = page.getLinksAsItems();
+ ProjectLinkItem customLink = links.get(1);
+
+ customLink.getDeleteButton().click();
+ $("#delete-link-confirm").click();
+
+ page.getLinks().shouldHaveSize(1);
+ }
+
+ private CreateWsResponse createCustomLink() {
+ return wsClient.projectLinks().create(new CreateWsRequest()
+ .setProjectKey("sample")
+ .setName("Custom")
+ .setUrl("http://example.org/custom"));
+ }
+
+ private void deleteLink(long id) {
+ try {
+ wsClient.projectLinks().delete(new DeleteWsRequest().setId(id));
+ } catch (Exception e) {
+ // fail silently
+ }
+ }
+
+ private ProjectLinksPage openPage() {
+ nav.logIn().submitCredentials(adminUser, adminUser);
+ return nav.openProjectLinks("sample");
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectPermissionsTest.java b/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectPermissionsTest.java
new file mode 100644
index 00000000000..2184a9a9506
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectPermissionsTest.java
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.projectAdministration;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category1Suite;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.pageobjects.Navigation;
+import org.sonarqube.pageobjects.ProjectPermissionsPage;
+import util.user.UserRule;
+
+import static util.ItUtils.projectDir;
+import static util.selenium.Selenese.runSelenese;
+
+public class ProjectPermissionsTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ @Rule
+ public UserRule userRule = UserRule.from(orchestrator);
+
+ private Navigation nav = Navigation.create(orchestrator);
+ private String adminUser;
+
+ @BeforeClass
+ public static void beforeClass() {
+ executeBuild("project-permissions-project", "Test Project");
+ executeBuild("project-permissions-project-2", "Another Test Project");
+ }
+
+ @Before
+ public void before() {
+ adminUser = userRule.createAdminUser();
+ }
+
+ @Test
+ public void test_project_permissions_page_shows_only_single_project() throws Exception {
+ runSelenese(orchestrator, "/projectAdministration/ProjectPermissionsTest/test_project_permissions_page_shows_only_single_project.html");
+ }
+
+ @Test
+ public void change_project_visibility() {
+ ProjectPermissionsPage page = nav.logIn().submitCredentials(adminUser).openProjectPermissions("project-permissions-project");
+ page
+ .shouldBePublic()
+ .turnToPrivate()
+ .turnToPublic();
+ }
+
+ private static void executeBuild(String projectKey, String projectName) {
+ orchestrator.executeBuild(
+ SonarScanner.create(projectDir("shared/xoo-sample"))
+ .setProjectKey(projectKey)
+ .setProjectName(projectName)
+ );
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectQualityGatePageTest.java b/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectQualityGatePageTest.java
new file mode 100644
index 00000000000..9fd03897d08
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectQualityGatePageTest.java
@@ -0,0 +1,147 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.projectAdministration;
+
+import com.codeborne.selenide.Condition;
+import com.codeborne.selenide.SelenideElement;
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category1Suite;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.sonar.wsclient.qualitygate.QualityGate;
+import org.sonar.wsclient.qualitygate.QualityGateClient;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.qualitygate.SelectWsRequest;
+import org.sonarqube.pageobjects.Navigation;
+import org.sonarqube.pageobjects.ProjectQualityGatePage;
+
+import static util.ItUtils.newAdminWsClient;
+
+public class ProjectQualityGatePageTest {
+
+ @ClassRule
+ public static Orchestrator ORCHESTRATOR = Category1Suite.ORCHESTRATOR;
+
+ private Navigation nav = Navigation.create(ORCHESTRATOR);
+
+ private static WsClient wsClient;
+
+ @BeforeClass
+ public static void prepare() {
+ wsClient = newAdminWsClient(ORCHESTRATOR);
+ }
+
+ @Before
+ public void setUp() {
+ ORCHESTRATOR.resetData();
+
+ wsClient.wsConnector().call(new PostRequest("api/projects/create")
+ .setParam("name", "Sample")
+ .setParam("key", "sample"));
+ }
+
+ @Test
+ public void should_display_default() {
+ QualityGate customQualityGate = createCustomQualityGate("should_display_default");
+ qualityGateClient().setDefault(customQualityGate.id());
+
+ ProjectQualityGatePage page = openPage();
+ SelenideElement selectedQualityGate = page.getSelectedQualityGate();
+ selectedQualityGate.should(Condition.hasText("Default"));
+ selectedQualityGate.should(Condition.hasText(customQualityGate.name()));
+ }
+
+ @Test
+ public void should_display_custom() {
+ QualityGate customQualityGate = createCustomQualityGate("should_display_custom");
+ associateWithQualityGate(customQualityGate);
+
+ ProjectQualityGatePage page = openPage();
+ SelenideElement selectedQualityGate = page.getSelectedQualityGate();
+ selectedQualityGate.shouldNot(Condition.hasText("Default"));
+ selectedQualityGate.should(Condition.hasText(customQualityGate.name()));
+ }
+
+ @Test
+ public void should_display_none() {
+ qualityGateClient().unsetDefault();
+
+ ProjectQualityGatePage page = openPage();
+ page.assertNotSelected();
+ }
+
+ @Test
+ public void should_set_custom() {
+ QualityGate customQualityGate = createCustomQualityGate("should_set_custom");
+
+ ProjectQualityGatePage page = openPage();
+ page.setQualityGate(customQualityGate.name());
+
+ SelenideElement selectedQualityGate = page.getSelectedQualityGate();
+ selectedQualityGate.should(Condition.hasText(customQualityGate.name()));
+ }
+
+ @Test
+ public void should_set_default() {
+ QualityGate customQualityGate = createCustomQualityGate("should_set_default");
+ qualityGateClient().setDefault(customQualityGate.id());
+
+ ProjectQualityGatePage page = openPage();
+ page.setQualityGate(customQualityGate.name());
+
+ SelenideElement selectedQualityGate = page.getSelectedQualityGate();
+ selectedQualityGate.should(Condition.hasText("Default"));
+ selectedQualityGate.should(Condition.hasText(customQualityGate.name()));
+ }
+
+ @Test
+ @Ignore("find a way to select None")
+ public void should_set_none() {
+ qualityGateClient().unsetDefault();
+ QualityGate customQualityGate = createCustomQualityGate("should_set_none");
+ associateWithQualityGate(customQualityGate);
+
+ ProjectQualityGatePage page = openPage();
+ page.setQualityGate("");
+
+ page.assertNotSelected();
+ }
+
+ private ProjectQualityGatePage openPage() {
+ nav.logIn().submitCredentials("admin", "admin");
+ return nav.openProjectQualityGate("sample");
+ }
+
+ private static QualityGate createCustomQualityGate(String name) {
+ return qualityGateClient().create(name);
+ }
+
+ private void associateWithQualityGate(QualityGate qualityGate) {
+ wsClient.qualityGates().associateProject(new SelectWsRequest().setProjectKey("sample").setGateId(qualityGate.id()));
+ }
+
+ private static QualityGateClient qualityGateClient() {
+ return ORCHESTRATOR.getServer().adminWsClient().qualityGateClient();
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectVisibilityTest.java b/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectVisibilityTest.java
new file mode 100644
index 00000000000..45146bbe410
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectVisibilityTest.java
@@ -0,0 +1,98 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.projectAdministration;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category1Suite;
+import java.sql.SQLException;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.ws.WsComponents;
+import org.sonarqube.ws.client.component.SearchProjectsRequest;
+import org.sonarqube.ws.client.permission.RemoveGroupWsRequest;
+import org.sonarqube.ws.client.project.UpdateVisibilityRequest;
+import org.sonarqube.pageobjects.Navigation;
+import org.sonarqube.pageobjects.ProjectsManagementPage;
+import util.user.UserRule;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.projectDir;
+
+public class ProjectVisibilityTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ @Rule
+ public UserRule userRule = UserRule.from(orchestrator);
+
+ private Navigation nav = Navigation.create(orchestrator);
+
+ private String adminUser;
+
+ @Before
+ public void initData() throws SQLException {
+ orchestrator.resetData();
+ adminUser = userRule.createAdminUser();
+ }
+
+ @Test
+ public void return_all_projects_even_when_no_permission() throws Exception {
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")).setProperties("sonar.projectKey", "sample1"));
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")).setProperties("sonar.projectKey", "sample2"));
+ newAdminWsClient(orchestrator).projects().updateVisibility(UpdateVisibilityRequest.builder().setProject("sample2").setVisibility("private").build());
+ // Remove 'Admin' permission for admin group on project 2 -> No one can access or admin this project, expect System Admin
+ newAdminWsClient(orchestrator).permissions().removeGroup(new RemoveGroupWsRequest().setProjectKey("sample2").setGroupName("sonar-administrators").setPermission("admin"));
+
+ nav.logIn().submitCredentials(adminUser).openProjectsManagement()
+ .shouldHaveProject("sample1")
+ .shouldHaveProject("sample2");
+ }
+
+ @Test
+ public void create_public_project() {
+ createProjectAndVerify("public");
+ }
+
+ @Test
+ public void create_private_project() {
+ createProjectAndVerify("private");
+ }
+
+ private void createProjectAndVerify(String visibility) {
+ ProjectsManagementPage page = nav.logIn().submitCredentials(adminUser, adminUser).openProjectsManagement();
+ page
+ .shouldHaveProjectsCount(0)
+ .createProject("foo", "foo", visibility)
+ .shouldHaveProjectsCount(1);
+
+ WsComponents.SearchProjectsWsResponse response = newAdminWsClient(orchestrator).components().searchProjects(
+ SearchProjectsRequest.builder().build());
+ assertThat(response.getComponentsCount()).isEqualTo(1);
+ assertThat(response.getComponents(0).getKey()).isEqualTo("foo");
+ assertThat(response.getComponents(0).getName()).isEqualTo("foo");
+ assertThat(response.getComponents(0).getVisibility()).isEqualTo(visibility);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/projectEvent/EventTest.java b/tests/src/test/java/org/sonarqube/tests/projectEvent/EventTest.java
new file mode 100644
index 00000000000..4941929515c
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/projectEvent/EventTest.java
@@ -0,0 +1,97 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.projectEvent;
+
+import com.google.common.collect.Lists;
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category4Suite;
+import java.util.List;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.sonar.wsclient.services.Event;
+import org.sonar.wsclient.services.EventQuery;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.WsConnector;
+import org.sonarqube.ws.client.WsResponse;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.projectDir;
+
+@Ignore("refactor using wsClient")
+public class EventTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+
+ @Before
+ public void setUp() throws Exception {
+ orchestrator.resetData();
+ }
+
+ @Test
+ public void old_ws_events_does_not_allow_creating_events_on_modules() {
+ SonarScanner sampleProject = SonarScanner.create(projectDir("shared/xoo-multi-modules-sample"));
+ orchestrator.executeBuild(sampleProject);
+
+ WsConnector wsConnector = ItUtils.newAdminWsClient(orchestrator).wsConnector();
+ WsResponse response = wsConnector.call(newCreateEventRequest("com.sonarsource.it.samples:multi-modules-sample", "bar"));
+ assertThat(response.code())
+ .isEqualTo(200);
+
+ assertThat(wsConnector.call(newCreateEventRequest("com.sonarsource.it.samples:multi-modules-sample:module_a", "bar")).code())
+ .isEqualTo(400);
+ }
+
+ private static PostRequest newCreateEventRequest(String componentKey, String eventName) {
+ return new PostRequest("/api/events")
+ .setParam("resource", componentKey)
+ .setParam("name", eventName)
+ .setParam("category", "Foo");
+ }
+
+ /**
+ * SONAR-3308
+ */
+ @Test
+ public void keep_only_one_event_per_version_in_project_history() throws Exception {
+ // first analyse the 1.0-SNAPSHOT version
+ executeAnalysis();
+ // then analyse the 1.0 version
+ executeAnalysis("sonar.projectVersion", "1.0");
+ // and do this all over again
+ executeAnalysis();
+ executeAnalysis("sonar.projectVersion", "1.0");
+
+ // there should be only 1 "0.1-SNAPSHOT" event and only 1 "0.1" event
+ List<Event> events = orchestrator.getServer().getWsClient().findAll(new EventQuery().setResourceKey("sample"));
+ assertThat(events.size()).isEqualTo(2);
+ List<String> eventNames = Lists.newArrayList(events.get(0).getName(), events.get(1).getName());
+ assertThat(eventNames).contains("1.0", "1.0-SNAPSHOT");
+ }
+
+ private static void executeAnalysis(String... properties) {
+ SonarScanner sampleProject = SonarScanner.create(projectDir("shared/xoo-sample")).setProperties(properties);
+ orchestrator.executeBuild(sampleProject);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/projectEvent/ProjectActivityPageTest.java b/tests/src/test/java/org/sonarqube/tests/projectEvent/ProjectActivityPageTest.java
new file mode 100644
index 00000000000..5a10c83e607
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/projectEvent/ProjectActivityPageTest.java
@@ -0,0 +1,99 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.projectEvent;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category4Suite;
+import java.util.List;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.pageobjects.Navigation;
+import org.sonarqube.pageobjects.ProjectActivityPage;
+import org.sonarqube.pageobjects.ProjectAnalysisItem;
+import util.user.UserRule;
+
+import static util.ItUtils.projectDir;
+
+public class ProjectActivityPageTest {
+
+ @ClassRule
+ public static Orchestrator ORCHESTRATOR = Category4Suite.ORCHESTRATOR;
+
+ @Rule
+ public UserRule userRule = UserRule.from(ORCHESTRATOR);
+
+ private Navigation nav = Navigation.create(ORCHESTRATOR);
+
+ @Before
+ public void setUp() throws Exception {
+ ORCHESTRATOR.resetData();
+ }
+
+ @Test
+ public void should_list_snapshots() {
+ analyzeProject("shared/xoo-history-v1", "2014-10-19");
+ analyzeProject("shared/xoo-history-v2", "2014-11-13");
+
+ ProjectActivityPage page = openPage();
+ page.getAnalyses().shouldHaveSize(2);
+
+ List<ProjectAnalysisItem> analyses = page.getAnalysesAsItems();
+ analyses.get(0)
+ .shouldHaveEventWithText("1.0-SNAPSHOT")
+ .shouldNotHaveDeleteButton();
+
+ analyses.get(1)
+ .shouldHaveEventWithText("0.9-SNAPSHOT")
+ .shouldHaveDeleteButton();
+ }
+
+ @Test
+ public void add_change_delete_custom_event() {
+ analyzeProject();
+ openPage().getLastAnalysis()
+ .addCustomEvent("foo")
+ .changeLastEvent("bar")
+ .deleteLastEvent();
+ }
+
+ @Test
+ public void delete_analysis() {
+ analyzeProject();
+ analyzeProject();
+ openPage().getFirstAnalysis().delete();
+ }
+
+ private ProjectActivityPage openPage() {
+ String userAdmin = userRule.createAdminUser();
+ nav.logIn().submitCredentials(userAdmin, userAdmin);
+ return nav.openProjectActivity("sample");
+ }
+
+ private static void analyzeProject() {
+ ORCHESTRATOR.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")));
+ }
+
+ private static void analyzeProject(String path, String date) {
+ ORCHESTRATOR.executeBuild(SonarScanner.create(projectDir(path)).setProperties("sonar.projectDate", date));
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/projectSearch/LeakProjectsPageTest.java b/tests/src/test/java/org/sonarqube/tests/projectSearch/LeakProjectsPageTest.java
new file mode 100644
index 00000000000..78c56f8bea2
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/projectSearch/LeakProjectsPageTest.java
@@ -0,0 +1,119 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.projectSearch;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category6Suite;
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.Organizations.Organization;
+import org.sonarqube.pageobjects.projects.ProjectsPage;
+
+import static com.codeborne.selenide.WebDriverRunner.url;
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newProjectKey;
+import static util.ItUtils.projectDir;
+import static util.ItUtils.resetSettings;
+import static util.ItUtils.restoreProfile;
+import static util.ItUtils.setServerProperty;
+
+public class LeakProjectsPageTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category6Suite.ORCHESTRATOR;
+
+ @Rule
+ public Tester tester = new Tester(orchestrator);
+
+ private Organization organization;
+
+ @BeforeClass
+ public static void beforeClass() {
+ setServerProperty(orchestrator, "sonar.leak.period", "previous_analysis");
+ }
+
+ @AfterClass
+ public static void tearDown() {
+ resetSettings(orchestrator, null, "sonar.leak.period");
+ }
+
+ @Before
+ public void setUp() {
+ organization = tester.organizations().generate();
+ restoreProfile(orchestrator, SearchProjectsTest.class.getResource("/projectSearch/SearchProjectsTest/with-many-rules.xml"), organization.getKey());
+ }
+
+ @Test
+ public void should_display_leak_information() {
+ // This project has 0% duplication on new code
+ String projectKey2 = newProjectKey();
+ analyzeProject(projectKey2, "projectSearch/xoo-history-v1", "2016-12-31");
+ analyzeProject(projectKey2, "projectSearch/xoo-history-v2", null);
+
+ // This project has no duplication on new code
+ String projectKey1 = newProjectKey();
+ analyzeProject(projectKey1, "shared/xoo-sample", "2016-12-31");
+ analyzeProject(projectKey1, "shared/xoo-sample", null);
+
+ // Check the facets and project cards
+ ProjectsPage page = tester.openBrowser().openProjects(organization.getKey());
+ page.changePerspective("Leak");
+ assertThat(url()).endsWith("/projects?view=leak");
+ page.shouldHaveTotal(2);
+ page.getProjectByKey(projectKey2)
+ .shouldHaveMeasure("new_reliability_rating", "0A")
+ .shouldHaveMeasure("new_security_rating", "0A")
+ .shouldHaveMeasure("new_maintainability_rating", "17A")
+ .shouldHaveMeasure("new_coverage", "–")
+ .shouldHaveMeasure("new_duplicated_lines_density", "0.0%")
+ .shouldHaveMeasure("new_lines", "17");
+ page.getFacetByProperty("new_duplications")
+ .shouldHaveValue("1", "1")
+ .shouldHaveValue("2", "0")
+ .shouldHaveValue("3", "0")
+ .shouldHaveValue("4", "0")
+ .shouldHaveValue("5", "0")
+ .shouldHaveValue("6", "1");
+ }
+
+ private void analyzeProject(String projectKey, String relativePath, @Nullable String analysisDate) {
+ List<String> keyValueProperties = new ArrayList<>(asList(
+ "sonar.projectKey", projectKey,
+ "sonar.organization", organization.getKey(),
+ "sonar.profile", "with-many-rules",
+ "sonar.login", "admin", "sonar.password", "admin",
+ "sonar.scm.disabled", "false"));
+ if (analysisDate != null) {
+ keyValueProperties.add("sonar.projectDate");
+ keyValueProperties.add(analysisDate);
+ }
+ orchestrator.executeBuild(SonarScanner.create(projectDir(relativePath), keyValueProperties.toArray(new String[0])));
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/projectSearch/ProjectsPageTest.java b/tests/src/test/java/org/sonarqube/tests/projectSearch/ProjectsPageTest.java
new file mode 100644
index 00000000000..dde5ace24cb
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/projectSearch/ProjectsPageTest.java
@@ -0,0 +1,203 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.projectSearch;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category1Suite;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.WsUsers;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.project.DeleteRequest;
+import org.sonarqube.pageobjects.Navigation;
+import org.sonarqube.pageobjects.projects.ProjectsPage;
+
+import static com.codeborne.selenide.Selenide.clearBrowserLocalStorage;
+import static com.codeborne.selenide.WebDriverRunner.url;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.projectDir;
+
+public class ProjectsPageTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ private static final String PROJECT_KEY = "key-foo";
+ private static Tester tester = new Tester(orchestrator).disableOrganizations();
+
+ @ClassRule
+ public static RuleChain ruleChain = RuleChain.outerRule(orchestrator)
+ .around(tester);
+
+ @BeforeClass
+ public static void setUp() {
+ orchestrator.resetData();
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")).setProjectKey(PROJECT_KEY));
+ orchestrator.executeBuild(SonarScanner.create(projectDir("duplications/file-duplications")).setProjectKey("key-bar"));
+ }
+
+ @AfterClass
+ public static void tearDown() {
+ tester.wsClient().projects().delete(DeleteRequest.builder().setKey(PROJECT_KEY).build());
+ tester.wsClient().projects().delete(DeleteRequest.builder().setKey("key-bar").build());
+ }
+
+ @Before
+ public void before() {
+ clearBrowserLocalStorage();
+ }
+
+ @Test
+ public void should_display_projects() {
+ ProjectsPage page = tester.openBrowser().openProjects();
+ page.shouldHaveTotal(2);
+ page.getProjectByKey(PROJECT_KEY)
+ .shouldHaveMeasure("reliability_rating", "A")
+ .shouldHaveMeasure("security_rating", "A")
+ .shouldHaveMeasure("sqale_rating", "A")
+ .shouldHaveMeasure("duplicated_lines_density", "0.0%")
+ .shouldHaveMeasure("ncloc", "13")
+ .shouldHaveMeasure("ncloc", "Xoo");
+ }
+
+ @Test
+ public void should_display_facets() {
+ ProjectsPage page = tester.openBrowser().openProjects();
+ page.getFacetByProperty("duplications")
+ .shouldHaveValue("1", "1")
+ .shouldHaveValue("2", "1")
+ .shouldHaveValue("3", "1")
+ .shouldHaveValue("4", "1")
+ .shouldHaveValue("5", "1")
+ .shouldHaveValue("6", "0");
+ }
+
+ @Test
+ public void should_filter_using_facet() {
+ ProjectsPage page = tester.openBrowser().openProjects();
+ page.shouldHaveTotal(2);
+ page.getFacetByProperty("duplications").selectValue("3");
+ page.shouldHaveTotal(1);
+ }
+
+ @Test
+ public void should_open_default_page() {
+ // default page can be "All Projects" or "Favorite Projects" depending on your last choice
+ Navigation nav = tester.openBrowser();
+ ProjectsPage page = nav.openProjects();
+
+ // all projects for anonymous user with default sorting to analysis date
+ page.shouldHaveTotal(2).shouldDisplayAllProjectsWidthSort("-analysis_date");
+
+ // all projects by default for logged in user
+ WsUsers.CreateWsResponse.User administrator = tester.users().generateAdministrator();
+ page = nav.logIn().submitCredentials(administrator.getLogin()).openProjects();
+ page.shouldHaveTotal(2).shouldDisplayAllProjects();
+
+ // favorite one project
+ WsClient administratorWsClient = tester.as(administrator.getLogin()).wsClient();
+ administratorWsClient.favorites().add(PROJECT_KEY);
+ page = nav.openProjects();
+ page.shouldHaveTotal(1).shouldDisplayFavoriteProjects();
+
+ // un-favorite this project
+ administratorWsClient.favorites().remove(PROJECT_KEY);
+ page = nav.openProjects();
+ page.shouldHaveTotal(2).shouldDisplayAllProjects();
+
+ // select favorite
+ page.selectFavoriteProjects();
+ page = nav.openProjects();
+ page.shouldHaveTotal(0).shouldDisplayFavoriteProjects();
+
+ // select all
+ page.selectAllProjects();
+ page = nav.openProjects();
+ page.shouldHaveTotal(2).shouldDisplayAllProjects();
+ }
+
+ @Test
+ public void should_add_language_to_facet() {
+ ProjectsPage page = tester.openBrowser().openProjects();
+ page.getFacetByProperty("languages")
+ .selectOptionItem("xoo2")
+ .shouldHaveValue("xoo2", "0");
+ }
+
+ @Test
+ public void should_add_tag_to_facet() {
+ // Add some tags to this project
+ tester.wsClient().wsConnector().call(
+ new PostRequest("api/project_tags/set")
+ .setParam("project", PROJECT_KEY)
+ .setParam("tags", "aa,bb,cc,dd,ee,ff,gg,hh,ii,jj,zz"));
+
+ ProjectsPage page = tester.openBrowser().openProjects();
+ page.getFacetByProperty("tags")
+ .shouldHaveValue("aa", "1")
+ .shouldHaveValue("ii", "1")
+ .selectOptionItem("zz")
+ .shouldHaveValue("zz", "1");
+ }
+
+ @Test
+ public void should_switch_between_perspectives() {
+ WsUsers.CreateWsResponse.User administrator = tester.users().generateAdministrator();
+ ProjectsPage page = tester.openBrowser()
+ .logIn().submitCredentials(administrator.getLogin())
+ .openProjects();
+ page.changePerspective("Risk");
+ assertThat(url()).endsWith("/projects?view=visualizations&visualization=risk");
+ page.changePerspective("Leak");
+ assertThat(url()).endsWith("/projects?view=leak");
+ }
+
+ @Test
+ public void should_sort_by_facet() {
+ ProjectsPage page = tester.openBrowser().openProjects();
+ page.sortProjects("Duplications");
+ page.getProjectByIdx(0).shouldHaveMeasure("duplicated_lines_density", "63.7%");
+ page.invertSorting();
+ page.getProjectByIdx(0).shouldHaveMeasure("duplicated_lines_density", "0.0%");
+ }
+
+ @Test
+ public void should_search_for_project() {
+ ProjectsPage page = tester.openBrowser().openProjects();
+ page.searchProject("s").shouldHaveTotal(2);
+ page.searchProject("sam").shouldHaveTotal(1);
+ }
+
+ @Test
+ public void should_search_for_project_and_keep_other_filters() {
+ ProjectsPage page = tester.openBrowser().openProjects();
+ page.shouldHaveTotal(2);
+ page.getFacetByProperty("duplications").selectValue("3");
+ page.shouldHaveTotal(1);
+ page.searchProject("sample").shouldHaveTotal(0);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/projectSearch/SearchProjectsTest.java b/tests/src/test/java/org/sonarqube/tests/projectSearch/SearchProjectsTest.java
new file mode 100644
index 00000000000..8383935cab8
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/projectSearch/SearchProjectsTest.java
@@ -0,0 +1,306 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.projectSearch;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category6Suite;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.assertj.core.groups.Tuple;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.Common;
+import org.sonarqube.ws.Organizations.Organization;
+import org.sonarqube.ws.WsComponents.Component;
+import org.sonarqube.ws.WsComponents.SearchProjectsWsResponse;
+import org.sonarqube.ws.client.component.SearchProjectsRequest;
+import org.sonarqube.ws.client.project.CreateRequest;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.groups.Tuple.tuple;
+import static util.ItUtils.concat;
+import static util.ItUtils.newProjectKey;
+import static util.ItUtils.projectDir;
+import static util.ItUtils.restoreProfile;
+import static util.ItUtils.sanitizeTimezones;
+import static util.ItUtils.setServerProperty;
+
+/**
+ * Tests WS api/components/search_projects
+ */
+public class SearchProjectsTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category6Suite.ORCHESTRATOR;
+
+ @Rule
+ public Tester tester = new Tester(orchestrator);
+
+ private Organization organization;
+
+ @Before
+ public void setUp() {
+ organization = tester.organizations().generate();
+ restoreProfile(orchestrator, SearchProjectsTest.class.getResource("/projectSearch/SearchProjectsTest/with-many-rules.xml"), organization.getKey());
+ }
+
+ @Test
+ public void filter_projects_by_measure_values() throws Exception {
+ String projectKey = newProjectKey();
+ analyzeProject(projectKey, "shared/xoo-sample");
+
+ verifyFilterMatches(projectKey, "ncloc > 1");
+ verifyFilterMatches(projectKey, "ncloc > 1 and duplicated_lines_density <= 100");
+ verifyFilterDoesNotMatch("ncloc <= 1");
+ }
+
+ @Test
+ public void find_projects_with_no_data() throws Exception {
+ String projectKey = newProjectKey();
+ analyzeProject(projectKey, "shared/xoo-sample");
+
+ verifyFilterMatches(projectKey, "coverage = NO_DATA");
+ verifyFilterDoesNotMatch("ncloc = NO_DATA");
+ }
+
+ @Test
+ public void provisioned_projects_should_be_included_to_results() throws Exception {
+ String projectKey = newProjectKey();
+ tester.wsClient().projects().create(CreateRequest.builder().setKey(projectKey).setName(projectKey).setOrganization(organization.getKey()).build());
+
+ SearchProjectsWsResponse response = searchProjects(SearchProjectsRequest.builder().setOrganization(organization.getKey()).build());
+
+ assertThat(response.getComponentsList()).extracting(Component::getKey).containsOnly(projectKey);
+ }
+
+ @Test
+ public void return_leak_period_date() throws Exception {
+ setServerProperty(orchestrator, "sonar.leak.period", "previous_version");
+ // This project has a leak period
+ String projectKey1 = newProjectKey();
+ analyzeProject(projectKey1, "shared/xoo-sample", "sonar.projectDate", "2016-12-31");
+ analyzeProject(projectKey1, "shared/xoo-sample");
+ // This project has only one analysis, so no leak period
+ String projectKey2 = newProjectKey();
+ analyzeProject(projectKey2, "shared/xoo-sample");
+ // This project is provisioned, so has no leak period
+ String projectKey3 = newProjectKey();
+ tester.wsClient().projects().create(CreateRequest.builder().setKey(projectKey3).setName(projectKey3).setOrganization(organization.getKey()).build());
+
+ SearchProjectsWsResponse response = searchProjects(
+ SearchProjectsRequest.builder().setAdditionalFields(singletonList("leakPeriodDate")).setOrganization(organization.getKey()).build());
+
+ assertThat(response.getComponentsList()).extracting(Component::getKey, Component::hasLeakPeriodDate)
+ .containsOnly(
+ tuple(projectKey1, true),
+ tuple(projectKey2, false),
+ tuple(projectKey3, false));
+ Component project1 = response.getComponentsList().stream().filter(component -> component.getKey().equals(projectKey1)).findFirst()
+ .orElseThrow(() -> new IllegalStateException("Project1 is not found"));
+ assertThat(sanitizeTimezones(project1.getLeakPeriodDate())).isEqualTo("2016-12-31T00:00:00+0000");
+ }
+
+ @Test
+ public void filter_by_text_query() throws IOException {
+ analyzeProject("project1", "shared/xoo-sample", "sonar.projectName", "apachee");
+ analyzeProject("project2", "shared/xoo-sample", "sonar.projectName", "Apache");
+ analyzeProject("project3", "shared/xoo-multi-modules-sample", "sonar.projectName", "Apache Foundation");
+ analyzeProject("project4", "shared/xoo-multi-modules-sample", "sonar.projectName", "Windows");
+
+ // Search only by text query
+ assertThat(searchProjects("query = \"apache\"").getComponentsList()).extracting(Component::getKey).containsExactly("project2", "project3", "project1");
+ assertThat(searchProjects("query = \"pAch\"").getComponentsList()).extracting(Component::getKey).containsExactly("project2", "project3", "project1");
+ assertThat(searchProjects("query = \"hee\"").getComponentsList()).extracting(Component::getKey).containsExactly("project1");
+ assertThat(searchProjects("query = \"project1\"").getComponentsList()).extracting(Component::getKey).containsExactly("project1");
+ assertThat(searchProjects("query = \"unknown\"").getComponentsList()).isEmpty();
+
+ // Search by metric criteria and text query
+ assertThat(searchProjects(SearchProjectsRequest.builder().setFilter("query = \"pAch\" AND ncloc > 50").build()).getComponentsList())
+ .extracting(Component::getKey).containsExactly("project3");
+ assertThat(searchProjects(SearchProjectsRequest.builder().setFilter("query = \"nd\" AND ncloc > 50").build()).getComponentsList())
+ .extracting(Component::getKey).containsExactly("project3", "project4");
+ assertThat(searchProjects(SearchProjectsRequest.builder().setFilter("query = \"unknown\" AND ncloc > 50").build()).getComponentsList()).isEmpty();
+ ;
+
+ // Check facets
+ assertThat(searchProjects(SearchProjectsRequest.builder().setFilter("query = \"apache\"").setFacets(singletonList("ncloc")).build()).getFacets().getFacets(0).getValuesList())
+ .extracting(Common.FacetValue::getVal, Common.FacetValue::getCount)
+ .containsOnly(tuple("*-1000.0", 3L), tuple("1000.0-10000.0", 0L), tuple("10000.0-100000.0", 0L), tuple("100000.0-500000.0", 0L), tuple("500000.0-*", 0L));
+ assertThat(searchProjects(SearchProjectsRequest.builder().setFilter("query = \"unknown\"").setFacets(singletonList("ncloc")).build()).getFacets().getFacets(0)
+ .getValuesList()).extracting(Common.FacetValue::getVal, Common.FacetValue::getCount)
+ .containsOnly(tuple("*-1000.0", 0L), tuple("1000.0-10000.0", 0L), tuple("10000.0-100000.0", 0L), tuple("100000.0-500000.0", 0L), tuple("500000.0-*", 0L));
+ }
+
+ @Test
+ public void should_return_facets() throws Exception {
+ analyzeProject(newProjectKey(), "shared/xoo-sample");
+ analyzeProject(newProjectKey(), "shared/xoo-multi-modules-sample");
+
+ SearchProjectsWsResponse response = searchProjects(SearchProjectsRequest.builder().setOrganization(organization.getKey()).setFacets(asList(
+ "alert_status",
+ "coverage",
+ "duplicated_lines_density",
+ "languages",
+ "ncloc",
+ "reliability_rating",
+ "security_rating",
+ "sqale_rating",
+ "tags")).build());
+
+ checkFacet(response, "alert_status",
+ tuple("OK", 2L),
+ tuple("WARN", 0L),
+ tuple("ERROR", 0L));
+ checkFacet(response, "coverage",
+ tuple("NO_DATA", 2L),
+ tuple("*-30.0", 0L),
+ tuple("30.0-50.0", 0L),
+ tuple("50.0-70.0", 0L),
+ tuple("70.0-80.0", 0L),
+ tuple("80.0-*", 0L));
+ checkFacet(response, "duplicated_lines_density",
+ tuple("NO_DATA", 0L),
+ tuple("*-3.0", 2L),
+ tuple("3.0-5.0", 0L),
+ tuple("5.0-10.0", 0L),
+ tuple("10.0-20.0", 0L),
+ tuple("20.0-*", 0L));
+ checkFacet(response, "languages",
+ tuple("xoo", 2L));
+ checkFacet(response, "ncloc",
+ tuple("*-1000.0", 2L),
+ tuple("1000.0-10000.0", 0L),
+ tuple("10000.0-100000.0", 0L),
+ tuple("100000.0-500000.0", 0L),
+ tuple("500000.0-*", 0L));
+ checkFacet(response, "reliability_rating",
+ tuple("1", 2L),
+ tuple("2", 0L),
+ tuple("3", 0L),
+ tuple("4", 0L),
+ tuple("5", 0L));
+ checkFacet(response, "security_rating",
+ tuple("1", 2L),
+ tuple("2", 0L),
+ tuple("3", 0L),
+ tuple("4", 0L),
+ tuple("5", 0L));
+ checkFacet(response, "sqale_rating",
+ tuple("1", 0L),
+ tuple("2", 0L),
+ tuple("3", 0L),
+ tuple("4", 2L),
+ tuple("5", 0L));
+ checkFacet(response, "tags");
+ }
+
+ @Test
+ public void should_return_facets_on_leak() throws Exception {
+ setServerProperty(orchestrator, "sonar.leak.period", "previous_analysis");
+ // This project has no duplication on new code
+ String projectKey1 = newProjectKey();
+ analyzeProject(projectKey1, "shared/xoo-sample", "sonar.projectDate", "2016-12-31");
+ analyzeProject(projectKey1, "shared/xoo-sample");
+ // This project has 0% duplication on new code
+ String projectKey2 = newProjectKey();
+ analyzeProject(projectKey2, "projectSearch/xoo-history-v1", "sonar.projectDate", "2016-12-31");
+ analyzeProject(projectKey2, "projectSearch/xoo-history-v2");
+
+ SearchProjectsWsResponse response = searchProjects(SearchProjectsRequest.builder().setOrganization(organization.getKey()).setFacets(asList(
+ "new_reliability_rating", "new_security_rating", "new_maintainability_rating", "new_coverage", "new_duplicated_lines_density", "new_lines")).build());
+
+ checkFacet(response, "new_reliability_rating",
+ tuple("1", 2L),
+ tuple("2", 0L),
+ tuple("3", 0L),
+ tuple("4", 0L),
+ tuple("5", 0L));
+ checkFacet(response, "new_security_rating",
+ tuple("1", 2L),
+ tuple("2", 0L),
+ tuple("3", 0L),
+ tuple("4", 0L),
+ tuple("5", 0L));
+ checkFacet(response, "new_maintainability_rating",
+ tuple("1", 2L),
+ tuple("2", 0L),
+ tuple("3", 0L),
+ tuple("4", 0L),
+ tuple("5", 0L));
+ checkFacet(response, "new_coverage",
+ tuple("NO_DATA", 2L),
+ tuple("*-30.0", 0L),
+ tuple("30.0-50.0", 0L),
+ tuple("50.0-70.0", 0L),
+ tuple("70.0-80.0", 0L),
+ tuple("80.0-*", 0L));
+ checkFacet(response, "new_duplicated_lines_density",
+ tuple("NO_DATA", 1L),
+ tuple("*-3.0", 1L),
+ tuple("3.0-5.0", 0L),
+ tuple("5.0-10.0", 0L),
+ tuple("10.0-20.0", 0L),
+ tuple("20.0-*", 0L));
+ checkFacet(response, "new_lines",
+ tuple("*-1000.0", 1L),
+ tuple("1000.0-10000.0", 0L),
+ tuple("10000.0-100000.0", 0L),
+ tuple("100000.0-500000.0", 0L),
+ tuple("500000.0-*", 0L));
+ }
+
+ private void checkFacet(SearchProjectsWsResponse response, String facetKey, Tuple... values) {
+ Common.Facet facet = response.getFacets().getFacetsList().stream().filter(f -> f.getProperty().equals(facetKey)).findAny().get();
+ assertThat(facet.getValuesList()).extracting(Common.FacetValue::getVal, Common.FacetValue::getCount).containsExactlyInAnyOrder(values);
+ }
+
+ private void analyzeProject(String projectKey, String relativePath, String... properties) {
+ List<String> keyValueProperties = new ArrayList<>(asList(
+ "sonar.projectKey", projectKey,
+ "sonar.organization", organization.getKey(),
+ "sonar.profile", "with-many-rules",
+ "sonar.login", "admin", "sonar.password", "admin",
+ "sonar.scm.disabled", "false"));
+ orchestrator.executeBuild(SonarScanner.create(projectDir(relativePath), concat(keyValueProperties.toArray(new String[0]), properties)));
+ }
+
+ private SearchProjectsWsResponse searchProjects(String filter) throws IOException {
+ return searchProjects(SearchProjectsRequest.builder().setOrganization(organization.getKey()).setFilter(filter).build());
+ }
+
+ private SearchProjectsWsResponse searchProjects(SearchProjectsRequest request) throws IOException {
+ return tester.wsClient().components().searchProjects(request);
+ }
+
+ private void verifyFilterMatches(String projectKey, String filter) throws IOException {
+ assertThat(searchProjects(filter).getComponentsList()).extracting(Component::getKey).containsOnly(projectKey);
+ }
+
+ private void verifyFilterDoesNotMatch(String filter) throws IOException {
+ assertThat(searchProjects(filter).getComponentsCount()).isZero();
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/qualityGate/QualityGateNotificationTest.java b/tests/src/test/java/org/sonarqube/tests/qualityGate/QualityGateNotificationTest.java
new file mode 100644
index 00000000000..69e24dddbda
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/qualityGate/QualityGateNotificationTest.java
@@ -0,0 +1,160 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.qualityGate;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category1Suite;
+import java.util.Iterator;
+import javax.mail.internet.MimeMessage;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonar.wsclient.qualitygate.NewCondition;
+import org.sonar.wsclient.qualitygate.QualityGate;
+import org.sonar.wsclient.qualitygate.QualityGateClient;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.WsClient;
+import org.subethamail.wiser.Wiser;
+import org.subethamail.wiser.WiserMessage;
+import util.user.UserRule;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonarqube.ws.WsMeasures.Measure;
+import static util.ItUtils.getMeasure;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.newUserWsClient;
+import static util.ItUtils.projectDir;
+import static util.ItUtils.resetEmailSettings;
+import static util.ItUtils.resetPeriod;
+import static util.ItUtils.setServerProperty;
+
+public class QualityGateNotificationTest {
+
+ private static long DEFAULT_QUALITY_GATE;
+
+ private static final String PROJECT_KEY = "sample";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ @ClassRule
+ public static UserRule userRule = UserRule.from(orchestrator);
+
+ private static Wiser smtpServer;
+
+ @BeforeClass
+ public static void init() throws Exception {
+ DEFAULT_QUALITY_GATE = qgClient().list().defaultGate().id();
+
+ setServerProperty(orchestrator, "sonar.leak.period", "previous_analysis");
+ resetEmailSettings(orchestrator);
+
+ smtpServer = new Wiser(0);
+ smtpServer.start();
+ }
+
+ @AfterClass
+ public static void resetData() throws Exception {
+ qgClient().setDefault(DEFAULT_QUALITY_GATE);
+
+ resetPeriod(orchestrator);
+ resetEmailSettings(orchestrator);
+
+ if (smtpServer != null) {
+ smtpServer.stop();
+ }
+ }
+
+ @Before
+ public void cleanUp() {
+ orchestrator.resetData();
+ }
+
+ @Test
+ public void status_on_metric_variation_and_send_notifications() throws Exception {
+ setServerProperty(orchestrator, "email.smtp_host.secured", "localhost");
+ setServerProperty(orchestrator, "email.smtp_port.secured", Integer.toString(smtpServer.getServer().getPort()));
+
+ // Create user, who will receive notifications for new violations
+ userRule.createUser("tester", "Tester", "tester@example.org", "tester");
+ // Send test email to the test user
+ newAdminWsClient(orchestrator).wsConnector().call(new PostRequest("api/emails/send")
+ .setParam("to", "test@example.org")
+ .setParam("message", "This is a test message from SonarQube"))
+ .failIfNotSuccessful();
+ // Add notifications to the test user
+ WsClient wsClient = newUserWsClient(orchestrator, "tester", "tester");
+ wsClient.wsConnector().call(new PostRequest("api/notifications/add")
+ .setParam("type", "NewAlerts")
+ .setParam("channel", "EmailNotificationChannel"))
+ .failIfNotSuccessful();
+
+ // Create quality gate with conditions on variations
+ QualityGate simple = qgClient().create("SimpleWithDifferential");
+ qgClient().setDefault(simple.id());
+ qgClient().createCondition(NewCondition.create(simple.id()).metricKey("ncloc").period(1).operator("EQ").warningThreshold("0"));
+
+ SonarScanner analysis = SonarScanner.create(projectDir("qualitygate/xoo-sample"));
+ orchestrator.executeBuild(analysis);
+ assertThat(getGateStatusMeasure().getValue()).isEqualTo("OK");
+
+ orchestrator.executeBuild(analysis);
+ assertThat(getGateStatusMeasure().getValue()).isEqualTo("WARN");
+
+ qgClient().unsetDefault();
+ qgClient().destroy(simple.id());
+
+ waitUntilAllNotificationsAreDelivered(smtpServer);
+
+ Iterator<WiserMessage> emails = smtpServer.getMessages().iterator();
+
+ MimeMessage message = emails.next().getMimeMessage();
+ assertThat(message.getHeader("To", null)).isEqualTo("<test@example.org>");
+ assertThat((String) message.getContent()).contains("This is a test message from SonarQube");
+
+ assertThat(emails.hasNext()).isTrue();
+ message = emails.next().getMimeMessage();
+ assertThat(message.getHeader("To", null)).isEqualTo("<tester@example.org>");
+ assertThat((String) message.getContent()).contains("Quality gate status: Orange (was Green)");
+ assertThat((String) message.getContent()).contains("Quality gate threshold: Lines of Code variation = 0 since previous analysis");
+ assertThat((String) message.getContent()).contains("/dashboard?id=sample");
+ assertThat(emails.hasNext()).isFalse();
+ }
+
+ private Measure getGateStatusMeasure() {
+ return getMeasure(orchestrator, PROJECT_KEY, "alert_status");
+ }
+
+ private static QualityGateClient qgClient() {
+ return orchestrator.getServer().adminWsClient().qualityGateClient();
+ }
+
+ private static void waitUntilAllNotificationsAreDelivered(Wiser smtpServer) throws InterruptedException {
+ for (int i = 0; i < 10; i++) {
+ if (smtpServer.getMessages().size() == 2) {
+ break;
+ }
+ Thread.sleep(1_000);
+ }
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/qualityGate/QualityGateOnRatingMeasuresTest.java b/tests/src/test/java/org/sonarqube/tests/qualityGate/QualityGateOnRatingMeasuresTest.java
new file mode 100644
index 00000000000..db5b8e92bdc
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/qualityGate/QualityGateOnRatingMeasuresTest.java
@@ -0,0 +1,124 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.qualityGate;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category1Suite;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonar.wsclient.qualitygate.QualityGateClient;
+import org.sonarqube.ws.WsMeasures;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.qualitygate.CreateConditionRequest;
+import org.sonarqube.ws.client.qualitygate.QualityGatesService;
+import org.sonarqube.ws.client.qualitygate.SelectWsRequest;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.getMeasure;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.resetSettings;
+import static util.ItUtils.runProjectAnalysis;
+import static util.ItUtils.setServerProperty;
+
+public class QualityGateOnRatingMeasuresTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ private static final String PROJECT_KEY = "sample";
+
+ static WsClient wsClient;
+
+ static QualityGatesService QUALITY_GATES;
+
+ Long qualityGateId;
+
+ @BeforeClass
+ public static void init() {
+ wsClient = newAdminWsClient(orchestrator);
+ QUALITY_GATES = wsClient.qualityGates();
+ }
+
+ @Before
+ public void prepareData() {
+ orchestrator.resetData();
+ qualityGateId = QUALITY_GATES.create("QualityGate").getId();
+ orchestrator.getServer().provisionProject(PROJECT_KEY, PROJECT_KEY);
+ QUALITY_GATES.associateProject(new SelectWsRequest().setGateId(qualityGateId).setProjectKey(PROJECT_KEY));
+ }
+
+ @After
+ public void resetData() throws Exception {
+ qgClient().destroy(qualityGateId);
+ resetSettings(orchestrator, null, "sonar.leak.period");
+ }
+
+ @Test
+ public void generate_warning_qgate_on_rating_metric() throws Exception {
+ QUALITY_GATES.createCondition(CreateConditionRequest.builder()
+ .setQualityGateId(qualityGateId.intValue())
+ .setMetricKey("security_rating")
+ .setOperator("GT")
+ .setWarning("3")
+ .build());
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/qualityGate/QualityGateOnRatingMeasuresTest/with-many-rules.xml"));
+ orchestrator.getServer().associateProjectToQualityProfile(PROJECT_KEY, "xoo", "with-many-rules");
+
+ runProjectAnalysis(orchestrator, "qualitygate/xoo-sample");
+
+ assertThat(getGateStatusMeasure().getValue()).isEqualTo("WARN");
+ }
+
+ @Test
+ public void generate_error_qgate_on_rating_metric_on_leak_period() throws Exception {
+ setServerProperty(orchestrator, "sonar.leak.period", "previous_analysis");
+ QUALITY_GATES.createCondition(CreateConditionRequest.builder()
+ .setQualityGateId(qualityGateId.intValue())
+ .setMetricKey("new_security_rating")
+ .setOperator("GT")
+ .setError("3")
+ .setPeriod(1)
+ .build());
+
+ // Run first analysis with empty quality gate -> quality gate is green
+ orchestrator.getServer().associateProjectToQualityProfile(PROJECT_KEY, "xoo", "empty");
+ runProjectAnalysis(orchestrator, "qualitygate/xoo-sample");
+ assertThat(getGateStatusMeasure().getValue()).isEqualTo("OK");
+
+ // Run second analysis with some rules that makes Security Rating to E -> quality gate is red
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/qualityGate/QualityGateOnRatingMeasuresTest/with-many-rules.xml"));
+ orchestrator.getServer().associateProjectToQualityProfile(PROJECT_KEY, "xoo", "with-many-rules");
+ runProjectAnalysis(orchestrator, "qualitygate/xoo-sample");
+ assertThat(getGateStatusMeasure().getValue()).isEqualTo("ERROR");
+ }
+
+ private WsMeasures.Measure getGateStatusMeasure() {
+ return getMeasure(orchestrator, PROJECT_KEY, "alert_status");
+ }
+
+ private static QualityGateClient qgClient() {
+ return orchestrator.getServer().adminWsClient().qualityGateClient();
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/qualityGate/QualityGateTest.java b/tests/src/test/java/org/sonarqube/tests/qualityGate/QualityGateTest.java
new file mode 100644
index 00000000000..fc6a8adc0a0
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/qualityGate/QualityGateTest.java
@@ -0,0 +1,405 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.qualityGate;
+
+import com.google.gson.Gson;
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.BuildResult;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category1Suite;
+import java.io.File;
+import java.io.IOException;
+import java.io.StringReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+import org.apache.commons.io.Charsets;
+import org.apache.commons.io.FileUtils;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonar.wsclient.qualitygate.NewCondition;
+import org.sonar.wsclient.qualitygate.QualityGate;
+import org.sonar.wsclient.qualitygate.QualityGateClient;
+import org.sonarqube.ws.MediaTypes;
+import org.sonarqube.ws.WsCe;
+import org.sonarqube.ws.WsMeasures.Measure;
+import org.sonarqube.ws.WsQualityGates.ProjectStatusWsResponse;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.WsResponse;
+import org.sonarqube.ws.client.qualitygate.ProjectStatusWsRequest;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.groups.Tuple.tuple;
+import static util.ItUtils.concat;
+import static util.ItUtils.extractCeTaskId;
+import static util.ItUtils.getMeasure;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.newProjectKey;
+import static util.ItUtils.projectDir;
+
+public class QualityGateTest {
+
+ private static final String TASK_STATUS_SUCCESS = "SUCCESS";
+ private static final String QG_STATUS_NO_QG = "null";
+ private static final String QG_STATUS_OK = "OK";
+ private static final String QG_STATUS_ERROR = "ERROR";
+ private static final String QG_STATUS_WARN = "WARN";
+
+ private static long DEFAULT_QUALITY_GATE;
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+ private static WsClient wsClient;
+
+ @BeforeClass
+ public static void startOrchestrator() {
+ wsClient = newAdminWsClient(orchestrator);
+ DEFAULT_QUALITY_GATE = qgClient().list().defaultGate().id();
+ }
+
+ @AfterClass
+ public static void restoreDefaultQualitGate() throws Exception {
+ qgClient().setDefault(DEFAULT_QUALITY_GATE);
+ }
+
+ @Test
+ public void do_not_compute_status_if_no_gate() throws IOException {
+ String projectKey = newProjectKey();
+ BuildResult buildResult = executeAnalysis(projectKey);
+
+ verifyQGStatusInPostTask(buildResult, projectKey, TASK_STATUS_SUCCESS, QG_STATUS_NO_QG);
+
+ assertThat(getGateStatusMeasure(projectKey)).isNull();
+ }
+
+ @Test
+ public void status_ok_if_empty_gate() throws IOException {
+ QualityGate empty = qgClient().create("Empty");
+ qgClient().setDefault(empty.id());
+
+ try {
+ String projectKey = newProjectKey();
+ BuildResult buildResult = executeAnalysis(projectKey);
+
+ verifyQGStatusInPostTask(buildResult, projectKey, TASK_STATUS_SUCCESS, QG_STATUS_OK);
+
+ assertThat(getGateStatusMeasure(projectKey).getValue()).isEqualTo("OK");
+ } finally {
+ qgClient().unsetDefault();
+ qgClient().destroy(empty.id());
+ }
+ }
+
+ @Test
+ public void test_status_ok() throws IOException {
+ QualityGate simple = qgClient().create("SimpleWithHighThreshold");
+ qgClient().setDefault(simple.id());
+ qgClient().createCondition(NewCondition.create(simple.id()).metricKey("ncloc").operator("GT").warningThreshold("40"));
+
+ try {
+ String projectKey = newProjectKey();
+ BuildResult buildResult = executeAnalysis(projectKey);
+
+ verifyQGStatusInPostTask(buildResult, projectKey, TASK_STATUS_SUCCESS, QG_STATUS_OK);
+
+ assertThat(getGateStatusMeasure(projectKey).getValue()).isEqualTo("OK");
+ } finally {
+ qgClient().unsetDefault();
+ qgClient().destroy(simple.id());
+ }
+ }
+
+ @Test
+ public void test_status_warning() throws IOException {
+ QualityGate simple = qgClient().create("SimpleWithLowThreshold");
+ qgClient().setDefault(simple.id());
+ qgClient().createCondition(NewCondition.create(simple.id()).metricKey("ncloc").operator("GT").warningThreshold("10"));
+
+ try {
+ String projectKey = newProjectKey();
+ BuildResult buildResult = executeAnalysis(projectKey);
+
+ verifyQGStatusInPostTask(buildResult, projectKey, TASK_STATUS_SUCCESS, QG_STATUS_WARN);
+
+ assertThat(getGateStatusMeasure(projectKey).getValue()).isEqualTo("WARN");
+ } finally {
+ qgClient().unsetDefault();
+ qgClient().destroy(simple.id());
+ }
+ }
+
+ @Test
+ public void test_status_error() throws IOException {
+ QualityGate simple = qgClient().create("SimpleWithLowThreshold");
+ qgClient().setDefault(simple.id());
+ qgClient().createCondition(NewCondition.create(simple.id()).metricKey("ncloc").operator("GT").errorThreshold("10"));
+
+ try {
+ String projectKey = newProjectKey();
+ BuildResult buildResult = executeAnalysis(projectKey);
+
+ verifyQGStatusInPostTask(buildResult, projectKey, TASK_STATUS_SUCCESS, QG_STATUS_ERROR);
+
+ assertThat(getGateStatusMeasure(projectKey).getValue()).isEqualTo("ERROR");
+ } finally {
+ qgClient().unsetDefault();
+ qgClient().destroy(simple.id());
+ }
+ }
+
+ @Test
+ public void use_server_settings_instead_of_default_gate() throws IOException {
+ QualityGate alert = qgClient().create("AlertWithLowThreshold");
+ qgClient().createCondition(NewCondition.create(alert.id()).metricKey("ncloc").operator("GT").warningThreshold("10"));
+ QualityGate error = qgClient().create("ErrorWithLowThreshold");
+ qgClient().createCondition(NewCondition.create(error.id()).metricKey("ncloc").operator("GT").errorThreshold("10"));
+
+ qgClient().setDefault(alert.id());
+ String projectKey = newProjectKey();
+ orchestrator.getServer().provisionProject(projectKey, projectKey);
+ associateQualityGateToProject(error.id(), projectKey);
+
+ try {
+ BuildResult buildResult = executeAnalysis(projectKey);
+
+ verifyQGStatusInPostTask(buildResult, projectKey, TASK_STATUS_SUCCESS, QG_STATUS_ERROR);
+
+ assertThat(getGateStatusMeasure(projectKey).getValue()).isEqualTo("ERROR");
+ } finally {
+ qgClient().unsetDefault();
+ qgClient().destroy(alert.id());
+ qgClient().destroy(error.id());
+ }
+ }
+
+ @Test
+ public void conditions_on_multiple_metric_types() throws IOException {
+ QualityGate allTypes = qgClient().create("AllMetricTypes");
+ qgClient().createCondition(NewCondition.create(allTypes.id()).metricKey("ncloc").operator("GT").warningThreshold("10"));
+ qgClient().createCondition(NewCondition.create(allTypes.id()).metricKey("duplicated_lines_density").operator("GT").warningThreshold("20"));
+ qgClient().setDefault(allTypes.id());
+
+ try {
+ String projectKey = newProjectKey();
+ BuildResult buildResult = executeAnalysis(projectKey, "sonar.cpd.xoo.minimumLines", "2", "sonar.cpd.xoo.minimumTokens", "5");
+
+ verifyQGStatusInPostTask(buildResult, projectKey, TASK_STATUS_SUCCESS, QG_STATUS_WARN);
+
+ Measure alertStatus = getGateStatusMeasure(projectKey);
+ assertThat(alertStatus.getValue()).isEqualTo("WARN");
+
+ String qualityGateDetailJson = getMeasure(orchestrator, projectKey, "quality_gate_details").getValue();
+ assertThat(QualityGateDetails.parse(qualityGateDetailJson).getConditions())
+ .extracting(QualityGateDetails.Conditions::getMetric, QualityGateDetails.Conditions::getOp, QualityGateDetails.Conditions::getWarning)
+ .contains(tuple("ncloc", "GT", "10"), tuple("duplicated_lines_density", "GT", "20"));
+ } finally {
+ qgClient().unsetDefault();
+ qgClient().destroy(allTypes.id());
+ }
+ }
+
+ @Test
+ public void ad_hoc_build_break_strategy() throws IOException {
+ QualityGate simple = qgClient().create("SimpleWithLowThresholdForBuildBreakStrategy");
+ qgClient().setDefault(simple.id());
+ qgClient().createCondition(NewCondition.create(simple.id()).metricKey("ncloc").operator("GT").errorThreshold("7"));
+
+ try {
+ String projectKey = newProjectKey();
+ BuildResult buildResult = executeAnalysis(projectKey);
+
+ verifyQGStatusInPostTask(buildResult, projectKey, TASK_STATUS_SUCCESS, QG_STATUS_ERROR);
+
+ String taskId = getTaskIdInLocalReport(projectDir("qualitygate/xoo-sample"));
+ String analysisId = getAnalysisId(taskId);
+
+ ProjectStatusWsResponse projectStatusWsResponse = wsClient.qualityGates().projectStatus(new ProjectStatusWsRequest().setAnalysisId(analysisId));
+ ProjectStatusWsResponse.ProjectStatus projectStatus = projectStatusWsResponse.getProjectStatus();
+ assertThat(projectStatus.getStatus()).isEqualTo(ProjectStatusWsResponse.Status.ERROR);
+ assertThat(projectStatus.getConditionsCount()).isEqualTo(1);
+ ProjectStatusWsResponse.Condition condition = projectStatus.getConditionsList().get(0);
+ assertThat(condition.getMetricKey()).isEqualTo("ncloc");
+ assertThat(condition.getErrorThreshold()).isEqualTo("7");
+ } finally {
+ qgClient().unsetDefault();
+ qgClient().destroy(simple.id());
+ }
+ }
+
+ @Test
+ public void does_not_fail_when_condition_is_on_removed_metric() throws IOException {
+ String customMetricKey = randomAlphabetic(10);
+ createCustomIntMetric(orchestrator, customMetricKey);
+ QualityGate simple = qgClient().create("OnCustomMetric");
+ qgClient().setDefault(simple.id());
+ qgClient().createCondition(NewCondition.create(simple.id()).metricKey(customMetricKey).operator("GT").warningThreshold("40"));
+ try {
+ deleteCustomMetric(orchestrator, customMetricKey);
+ String projectKey = newProjectKey();
+ BuildResult buildResult = executeAnalysis(projectKey);
+
+ verifyQGStatusInPostTask(buildResult, projectKey, TASK_STATUS_SUCCESS, QG_STATUS_OK);
+
+ assertThat(getGateStatusMeasure(projectKey).getValue()).isEqualTo("OK");
+ } finally {
+ deleteCustomMetric(orchestrator, customMetricKey);
+ qgClient().unsetDefault();
+ qgClient().destroy(simple.id());
+ }
+ }
+
+ private BuildResult executeAnalysis(String projectKey, String... keyValueProperties) {
+ return orchestrator.executeBuild(SonarScanner.create(
+ projectDir("qualitygate/xoo-sample"), concat(keyValueProperties, "sonar.projectKey", projectKey)));
+ }
+
+ private void verifyQGStatusInPostTask(BuildResult buildResult, String projectKey, String taskStatus, String qgStatus) throws IOException {
+ List<String> logsLines = FileUtils.readLines(orchestrator.getServer().getCeLogs(), Charsets.UTF_8);
+ List<String> postTaskLogLines = extractPosttaskPluginLogs(extractCeTaskId(buildResult), logsLines);
+
+ assertThat(postTaskLogLines).hasSize(1);
+ assertThat(postTaskLogLines.iterator().next())
+ .contains("CeTask[" + taskStatus + "]")
+ .contains("Project[" + projectKey + "]")
+ .contains("QualityGate[" + qgStatus + "]");
+ }
+
+ private String getAnalysisId(String taskId) throws IOException {
+ WsResponse activity = wsClient
+ .wsConnector()
+ .call(new GetRequest("api/ce/task")
+ .setParam("id", taskId)
+ .setMediaType(MediaTypes.PROTOBUF));
+ WsCe.TaskResponse activityWsResponse = WsCe.TaskResponse.parseFrom(activity.contentStream());
+ return activityWsResponse.getTask().getAnalysisId();
+ }
+
+ private String getTaskIdInLocalReport(File projectDirectory) throws IOException {
+ File metadata = new File(projectDirectory, ".sonar/report-task.txt");
+ assertThat(metadata).exists().isFile();
+ // verify properties
+ Properties props = new Properties();
+ props.load(new StringReader(FileUtils.readFileToString(metadata, StandardCharsets.UTF_8)));
+ assertThat(props.getProperty("ceTaskId")).isNotEmpty();
+
+ return props.getProperty("ceTaskId");
+ }
+
+ private Measure getGateStatusMeasure(String projectKey) {
+ return getMeasure(orchestrator, projectKey, "alert_status");
+ }
+
+ private static QualityGateClient qgClient() {
+ return orchestrator.getServer().adminWsClient().qualityGateClient();
+ }
+
+ private static void associateQualityGateToProject(long qGateId, String projectKey) {
+ newAdminWsClient(orchestrator).wsConnector()
+ .call(new PostRequest("api/qualitygates/select")
+ .setParam("gateId", qGateId)
+ .setParam("projectKey", projectKey))
+ .failIfNotSuccessful();
+ }
+
+ private static List<String> extractPosttaskPluginLogs(String taskUuid, Iterable<String> ceLogs) {
+ return StreamSupport.stream(ceLogs.spliterator(), false)
+ .filter(s -> s.contains("POSTASKPLUGIN: finished()"))
+ .filter(s -> s.contains(taskUuid))
+ .collect(Collectors.toList());
+ }
+
+ private static void createCustomIntMetric(Orchestrator orchestrator, String metricKey) {
+ newAdminWsClient(orchestrator).wsConnector().call(new PostRequest("api/metrics/create")
+ .setParam("key", metricKey)
+ .setParam("name", metricKey)
+ .setParam("type", "INT"))
+ .failIfNotSuccessful();
+ }
+
+ private static void deleteCustomMetric(Orchestrator orchestrator, String metricKey) {
+ newAdminWsClient(orchestrator).wsConnector().call(new PostRequest("api/metrics/delete")
+ .setParam("keys", metricKey))
+ .failIfNotSuccessful();
+ }
+
+ static class QualityGateDetails {
+
+ private String level;
+
+ private List<Conditions> conditions = new ArrayList<>();
+
+ String getLevel() {
+ return level;
+ }
+
+ List<Conditions> getConditions() {
+ return conditions;
+ }
+
+ public static QualityGateDetails parse(String json) {
+ Gson gson = new Gson();
+ return gson.fromJson(json, QualityGateDetails.class);
+ }
+
+ public static class Conditions {
+ private final String metric;
+ private final String op;
+ private final String warning;
+ private final String actual;
+ private final String level;
+
+ private Conditions(String metric, String op, String values, String actual, String level) {
+ this.metric = metric;
+ this.op = op;
+ this.warning = values;
+ this.actual = actual;
+ this.level = level;
+ }
+
+ String getMetric() {
+ return metric;
+ }
+
+ String getOp() {
+ return op;
+ }
+
+ String getWarning() {
+ return warning;
+ }
+
+ String getActual() {
+ return actual;
+ }
+
+ String getLevel() {
+ return level;
+ }
+ }
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/qualityGate/QualityGateUiTest.java b/tests/src/test/java/org/sonarqube/tests/qualityGate/QualityGateUiTest.java
new file mode 100644
index 00000000000..4404dc7ca80
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/qualityGate/QualityGateUiTest.java
@@ -0,0 +1,125 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.qualityGate;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category1Suite;
+import java.util.Date;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.time.DateFormatUtils;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonar.wsclient.qualitygate.NewCondition;
+import org.sonar.wsclient.qualitygate.QualityGate;
+import org.sonar.wsclient.qualitygate.QualityGateClient;
+import org.sonar.wsclient.qualitygate.QualityGateCondition;
+import org.sonar.wsclient.qualitygate.UpdateCondition;
+import org.sonarqube.pageobjects.Navigation;
+import org.sonarqube.pageobjects.ProjectActivityPage;
+
+import static org.apache.commons.lang.time.DateUtils.addDays;
+import static util.ItUtils.projectDir;
+import static util.ItUtils.resetPeriod;
+import static util.ItUtils.setServerProperty;
+import static util.selenium.Selenese.runSelenese;
+
+public class QualityGateUiTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ private static long DEFAULT_QUALITY_GATE;
+
+ @BeforeClass
+ public static void initPeriod() throws Exception {
+ setServerProperty(orchestrator, "sonar.leak.period", "previous_analysis");
+ DEFAULT_QUALITY_GATE = qgClient().list().defaultGate().id();
+ }
+
+ @AfterClass
+ public static void resetData() throws Exception {
+ resetPeriod(orchestrator);
+ qgClient().setDefault(DEFAULT_QUALITY_GATE);
+ }
+
+ @Before
+ public void cleanUp() {
+ orchestrator.resetData();
+ }
+
+ /**
+ * SONAR-3326
+ */
+ @Test
+ public void display_alerts_correctly_in_history_page() {
+ QualityGateClient qgClient = qgClient();
+ QualityGate qGate = qgClient.create("AlertsForHistory");
+ qgClient.setDefault(qGate.id());
+
+ String firstAnalysisDate = DateFormatUtils.ISO_DATE_FORMAT.format(addDays(new Date(), -2));
+ String secondAnalysisDate = DateFormatUtils.ISO_DATE_FORMAT.format(addDays(new Date(), -1));
+
+ // with this configuration, project should have an Orange alert
+ QualityGateCondition lowThresholds = qgClient.createCondition(NewCondition.create(qGate.id()).metricKey("lines").operator("GT").warningThreshold("5").errorThreshold("50"));
+ scanSampleWithDate(firstAnalysisDate);
+ // with this configuration, project should have a Green alert
+ qgClient.updateCondition(UpdateCondition.create(lowThresholds.id()).metricKey("lines").operator("GT").warningThreshold("5000").errorThreshold("5000"));
+ scanSampleWithDate(secondAnalysisDate);
+
+ Navigation nav = Navigation.create(orchestrator);
+ ProjectActivityPage page = nav.openProjectActivity("sample");
+ page
+ .assertFirstAnalysisOfTheDayHasText(secondAnalysisDate, "Green (was Orange)")
+ .assertFirstAnalysisOfTheDayHasText(firstAnalysisDate, "Orange");
+
+ qgClient.unsetDefault();
+ qgClient.destroy(qGate.id());
+ }
+
+ @Test
+ public void should_display_quality_gates_page() {
+ runSelenese(orchestrator, "/qualityGate/QualityGateUiTest/should_display_quality_gates_page.html");
+ }
+
+ private void scanSampleWithDate(String date) {
+ scanSample(date, null);
+ }
+
+ private void scanSample(@Nullable String date, @Nullable String profile) {
+ SonarScanner scan = SonarScanner.create(projectDir("shared/xoo-sample"))
+ .setProperty("sonar.cpd.exclusions", "**/*");
+ if (date != null) {
+ scan.setProperty("sonar.projectDate", date);
+ }
+ if (profile != null) {
+ scan.setProfile(profile);
+ }
+ orchestrator.executeBuild(scan);
+ }
+
+ private static QualityGateClient qgClient() {
+ return orchestrator.getServer().adminWsClient().qualityGateClient();
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/qualityModel/DebtConfigurationRule.java b/tests/src/test/java/org/sonarqube/tests/qualityModel/DebtConfigurationRule.java
new file mode 100644
index 00000000000..ee4648373b7
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/qualityModel/DebtConfigurationRule.java
@@ -0,0 +1,114 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.qualityModel;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableSet;
+import com.sonar.orchestrator.Orchestrator;
+import java.util.Set;
+import org.junit.rules.ExternalResource;
+
+import static com.google.common.base.Preconditions.checkState;
+import static util.ItUtils.setServerProperty;
+
+/**
+ * This rule should be used when dealing with technical debt properties, in order to always be sure that the properties are correctly reset between each tests.
+ */
+public class DebtConfigurationRule extends ExternalResource {
+
+ private static final String DEV_COST_PROPERTY = "sonar.technicalDebt.developmentCost";
+ private static final String RATING_GRID_PROPERTY = "sonar.technicalDebt.ratingGrid";
+
+ private static final String DEV_COST_LANGUAGE_PROPERTY = "languageSpecificParameters";
+ private static final String DEV_COST_LANGUAGE_NAME_PROPERTY = DEV_COST_LANGUAGE_PROPERTY + ".0.language";
+ private static final String DEV_COST_LANGUAGE_COST_PROPERTY = DEV_COST_LANGUAGE_PROPERTY + ".0.man_days";
+
+ private static final Joiner COMA_JOINER = Joiner.on(",");
+
+ private static final Set<String> DEV_COST_PROPERTIES = ImmutableSet.of(
+ DEV_COST_PROPERTY,
+ DEV_COST_LANGUAGE_PROPERTY,
+ DEV_COST_LANGUAGE_NAME_PROPERTY,
+ DEV_COST_LANGUAGE_COST_PROPERTY,
+ RATING_GRID_PROPERTY);
+
+ private final Orchestrator orchestrator;
+
+ private DebtConfigurationRule(Orchestrator orchestrator) {
+ this.orchestrator = orchestrator;
+ }
+
+ public static DebtConfigurationRule create(Orchestrator orchestrator) {
+ return new DebtConfigurationRule(orchestrator);
+ }
+
+ @Override
+ protected void before() throws Throwable {
+ reset();
+ }
+
+ @Override
+ protected void after() {
+ reset();
+ }
+
+ public void reset() {
+ resetDevelopmentCost();
+ resetRatingGrid();
+ }
+
+ public DebtConfigurationRule updateDevelopmentCost(int developmentCost) {
+ setProperty(DEV_COST_PROPERTY, Integer.toString(developmentCost));
+ return this;
+ }
+
+ public DebtConfigurationRule updateLanguageDevelopmentCost(String language, int developmentCost) {
+ setServerProperty(orchestrator, DEV_COST_LANGUAGE_PROPERTY, "0");
+ setServerProperty(orchestrator, DEV_COST_LANGUAGE_NAME_PROPERTY, language);
+ setServerProperty(orchestrator, DEV_COST_LANGUAGE_COST_PROPERTY, Integer.toString(developmentCost));
+ return this;
+ }
+
+ public void resetDevelopmentCost() {
+ for (String property : DEV_COST_PROPERTIES) {
+ resetProperty(property);
+ }
+ }
+
+ public DebtConfigurationRule updateRatingGrid(Double... ratingGrid) {
+ checkState(ratingGrid.length == 4, "Rating grid must contains 4 values");
+ setProperty(RATING_GRID_PROPERTY, COMA_JOINER.join(ratingGrid));
+ return this;
+ }
+
+ public DebtConfigurationRule resetRatingGrid() {
+ resetProperty(RATING_GRID_PROPERTY);
+ return this;
+ }
+
+ private void setProperty(String property, String value) {
+ setServerProperty(orchestrator, property, value);
+ }
+
+ private void resetProperty(String property) {
+ setProperty(property, null);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/qualityModel/MaintainabilityMeasureTest.java b/tests/src/test/java/org/sonarqube/tests/qualityModel/MaintainabilityMeasureTest.java
new file mode 100644
index 00000000000..13c310481f2
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/qualityModel/MaintainabilityMeasureTest.java
@@ -0,0 +1,111 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.qualityModel;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category2Suite;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.getMeasureAsDouble;
+import static util.ItUtils.projectDir;
+
+/**
+ * SONAR-4715
+ */
+public class MaintainabilityMeasureTest {
+
+ private static final String PROJECT = "com.sonarsource.it.samples:multi-modules-sample";
+ private static final String MODULE = "com.sonarsource.it.samples:multi-modules-sample:module_a";
+ private static final String SUB_MODULE = "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1";
+ private static final String DIRECTORY = "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1";
+ private static final String FILE = "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo";
+
+ private static final String CODE_SMELLS_METRIC = "code_smells";
+ private static final String MAINTAINABILITY_REMEDIATION_EFFORT_METRIC = "sqale_index";
+ private static final String MAINTAINABILITY_RATING_METRIC = "sqale_rating";
+ private static final String EFFORT_TO_REACH_MAINTAINABILITY_RATING_A_METRIC = "effort_to_reach_maintainability_rating_a";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category2Suite.ORCHESTRATOR;
+
+ @Rule
+ public DebtConfigurationRule debtConfiguration = DebtConfigurationRule.create(orchestrator);
+
+ @Before
+ public void init() {
+ orchestrator.resetData();
+
+ // Set rating grid values to not depend from default value
+ debtConfiguration.updateRatingGrid(0.1d, 0.2d, 0.5d, 1d);
+
+ orchestrator.getServer().provisionProject(PROJECT, PROJECT);
+ }
+
+ @Test
+ public void verify_maintainability_measures_when_code_smells_rules_activated() {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/qualityModel/with-many-rules.xml"));
+ orchestrator.getServer().associateProjectToQualityProfile(PROJECT, "xoo", "with-many-rules");
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-multi-modules-sample")));
+
+ assertThat(getMeasureAsDouble(orchestrator, PROJECT, CODE_SMELLS_METRIC)).isEqualTo(71);
+ assertThat(getMeasureAsDouble(orchestrator, PROJECT, MAINTAINABILITY_REMEDIATION_EFFORT_METRIC)).isEqualTo(445);
+ assertThat(getMeasureAsDouble(orchestrator, PROJECT, MAINTAINABILITY_RATING_METRIC)).isEqualTo(3);
+ assertThat(getMeasureAsDouble(orchestrator, PROJECT, EFFORT_TO_REACH_MAINTAINABILITY_RATING_A_METRIC)).isEqualTo(292);
+
+ assertThat(getMeasureAsDouble(orchestrator, MODULE, CODE_SMELLS_METRIC)).isEqualTo(43);
+ assertThat(getMeasureAsDouble(orchestrator, MODULE, MAINTAINABILITY_REMEDIATION_EFFORT_METRIC)).isEqualTo(231);
+ assertThat(getMeasureAsDouble(orchestrator, MODULE, MAINTAINABILITY_RATING_METRIC)).isEqualTo(3);
+ assertThat(getMeasureAsDouble(orchestrator, MODULE, EFFORT_TO_REACH_MAINTAINABILITY_RATING_A_METRIC)).isEqualTo(150);
+
+ assertThat(getMeasureAsDouble(orchestrator, SUB_MODULE, CODE_SMELLS_METRIC)).isEqualTo(19);
+ assertThat(getMeasureAsDouble(orchestrator, SUB_MODULE, MAINTAINABILITY_REMEDIATION_EFFORT_METRIC)).isEqualTo(113);
+ assertThat(getMeasureAsDouble(orchestrator, SUB_MODULE, MAINTAINABILITY_RATING_METRIC)).isEqualTo(3);
+ assertThat(getMeasureAsDouble(orchestrator, SUB_MODULE, EFFORT_TO_REACH_MAINTAINABILITY_RATING_A_METRIC)).isEqualTo(77);
+
+ assertThat(getMeasureAsDouble(orchestrator, DIRECTORY, CODE_SMELLS_METRIC)).isEqualTo(18);
+ assertThat(getMeasureAsDouble(orchestrator, DIRECTORY, MAINTAINABILITY_REMEDIATION_EFFORT_METRIC)).isEqualTo(28);
+ assertThat(getMeasureAsDouble(orchestrator, DIRECTORY, MAINTAINABILITY_RATING_METRIC)).isEqualTo(1);
+ assertThat(getMeasureAsDouble(orchestrator, DIRECTORY, EFFORT_TO_REACH_MAINTAINABILITY_RATING_A_METRIC)).isZero();
+
+ assertThat(getMeasureAsDouble(orchestrator, FILE, CODE_SMELLS_METRIC)).isEqualTo(18);
+ assertThat(getMeasureAsDouble(orchestrator, FILE, MAINTAINABILITY_REMEDIATION_EFFORT_METRIC)).isEqualTo(28);
+ assertThat(getMeasureAsDouble(orchestrator, FILE, MAINTAINABILITY_RATING_METRIC)).isEqualTo(1);
+ assertThat(getMeasureAsDouble(orchestrator, FILE, EFFORT_TO_REACH_MAINTAINABILITY_RATING_A_METRIC)).isZero();
+ }
+
+ @Test
+ public void verify_reliability_measures_when_no_code_smells_rule() {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/qualityModel/without-type-code-smells.xml"));
+ orchestrator.getServer().associateProjectToQualityProfile(PROJECT, "xoo", "without-type-code-smells");
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-multi-modules-sample")));
+
+ assertThat(getMeasureAsDouble(orchestrator, PROJECT, CODE_SMELLS_METRIC)).isZero();
+ assertThat(getMeasureAsDouble(orchestrator, PROJECT, MAINTAINABILITY_REMEDIATION_EFFORT_METRIC)).isZero();
+ assertThat(getMeasureAsDouble(orchestrator, PROJECT, MAINTAINABILITY_RATING_METRIC)).isEqualTo(1);
+ assertThat(getMeasureAsDouble(orchestrator, PROJECT, EFFORT_TO_REACH_MAINTAINABILITY_RATING_A_METRIC)).isZero();
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/qualityModel/MaintainabilityRatingMeasureTest.java b/tests/src/test/java/org/sonarqube/tests/qualityModel/MaintainabilityRatingMeasureTest.java
new file mode 100644
index 00000000000..01834898461
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/qualityModel/MaintainabilityRatingMeasureTest.java
@@ -0,0 +1,164 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.qualityModel;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category2Suite;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.getMeasureAsDouble;
+import static util.ItUtils.projectDir;
+
+/**
+ * SONAR-4715
+ */
+public class MaintainabilityRatingMeasureTest {
+
+ private static final String PROJECT = "com.sonarsource.it.samples:multi-modules-sample";
+ private static final String MODULE = "com.sonarsource.it.samples:multi-modules-sample:module_a";
+ private static final String SUB_MODULE = "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1";
+ private static final String DIRECTORY = "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1";
+ private static final String FILE = "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category2Suite.ORCHESTRATOR;
+
+ @Rule
+ public DebtConfigurationRule debtConfiguration = DebtConfigurationRule.create(orchestrator);
+
+ @Before
+ public void init() {
+ orchestrator.resetData();
+
+ // Set rating grid values to not depend from default value
+ debtConfiguration.updateRatingGrid(0.1d, 0.2d, 0.5d, 1d);
+ }
+
+ @Test
+ public void sqale_rating_measures() {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/qualityModel/with-many-rules.xml"));
+ orchestrator.getServer().provisionProject(PROJECT, PROJECT);
+ orchestrator.getServer().associateProjectToQualityProfile(PROJECT, "xoo", "with-many-rules");
+
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-multi-modules-sample")));
+
+ assertThat(getMeasureAsDouble(orchestrator, PROJECT, "sqale_rating")).isEqualTo(3);
+ assertThat(getMeasureAsDouble(orchestrator, MODULE, "sqale_rating")).isEqualTo(3);
+ assertThat(getMeasureAsDouble(orchestrator, SUB_MODULE, "sqale_rating")).isEqualTo(3);
+ assertThat(getMeasureAsDouble(orchestrator, DIRECTORY, "sqale_rating")).isEqualTo(1);
+ assertThat(getMeasureAsDouble(orchestrator, FILE, "sqale_rating")).isEqualTo(1);
+ }
+
+ @Test
+ public void sqale_debt_ratio_measures() {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/qualityModel/with-many-rules.xml"));
+ orchestrator.getServer().provisionProject(PROJECT, PROJECT);
+ orchestrator.getServer().associateProjectToQualityProfile(PROJECT, "xoo", "with-many-rules");
+
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-multi-modules-sample")));
+
+ assertThat(getMeasureAsDouble(orchestrator, PROJECT, "sqale_debt_ratio")).isEqualTo(29.1d);
+ assertThat(getMeasureAsDouble(orchestrator, MODULE, "sqale_debt_ratio")).isEqualTo(28.5d);
+ assertThat(getMeasureAsDouble(orchestrator, SUB_MODULE, "sqale_debt_ratio")).isEqualTo(31.4d);
+ assertThat(getMeasureAsDouble(orchestrator, DIRECTORY, "sqale_debt_ratio")).isEqualTo(7.8d);
+ assertThat(getMeasureAsDouble(orchestrator, FILE, "sqale_debt_ratio")).isEqualTo(7.8d);
+ }
+
+ @Test
+ public void use_development_cost_parameter() throws Exception {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/qualityModel/one-issue-per-line.xml"));
+ orchestrator.getServer().provisionProject("sample", "sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");
+
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")));
+
+ assertThat(getMeasureAsDouble(orchestrator, "sample", "sqale_rating")).isEqualTo(1);
+
+ debtConfiguration.updateDevelopmentCost(2);
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")));
+
+ assertThat(getMeasureAsDouble(orchestrator, "sample", "sqale_rating")).isEqualTo(4);
+ }
+
+ @Test
+ public void use_language_specific_parameters() throws Exception {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/qualityModel/one-issue-per-line.xml"));
+ orchestrator.getServer().provisionProject(PROJECT, PROJECT);
+ orchestrator.getServer().associateProjectToQualityProfile(PROJECT, "xoo", "one-issue-per-line");
+
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-multi-modules-sample")));
+
+ assertThat(getMeasureAsDouble(orchestrator, PROJECT, "sqale_rating")).isEqualTo(1);
+
+ debtConfiguration.updateLanguageDevelopmentCost("xoo", 1);
+ orchestrator.executeBuild(
+ SonarScanner.create(projectDir("shared/xoo-multi-modules-sample"))
+ .setProfile("one-issue-per-line"));
+
+ assertThat(getMeasureAsDouble(orchestrator, PROJECT, "sqale_rating")).isEqualTo(5);
+ }
+
+ @Test
+ public void use_rating_grid_parameter() throws Exception {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/qualityModel/one-issue-per-line.xml"));
+ orchestrator.getServer().provisionProject("sample", "sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");
+
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")));
+
+ assertThat(getMeasureAsDouble(orchestrator, "sample", "sqale_rating")).isEqualTo(1);
+
+ debtConfiguration.updateRatingGrid(0.001d, 0.005d, 0.01d, 0.015d);
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")));
+
+ assertThat(getMeasureAsDouble(orchestrator, "sample", "sqale_rating")).isEqualTo(5);
+ }
+
+ @Test
+ public void effort_to_reach_maintainability_rating_a() {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/qualityModel/with-many-rules.xml"));
+ orchestrator.getServer().provisionProject(PROJECT, PROJECT);
+ orchestrator.getServer().associateProjectToQualityProfile(PROJECT, "xoo", "with-many-rules");
+
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-multi-modules-sample")));
+
+ assertThat(getMeasureAsDouble(orchestrator, PROJECT, "sqale_rating")).isEqualTo(3);
+ assertThat(getMeasureAsDouble(orchestrator, PROJECT, "effort_to_reach_maintainability_rating_a")).isEqualTo(292);
+
+ assertThat(getMeasureAsDouble(orchestrator, MODULE, "sqale_rating")).isEqualTo(3);
+ assertThat(getMeasureAsDouble(orchestrator, MODULE, "effort_to_reach_maintainability_rating_a")).isEqualTo(150);
+
+ assertThat(getMeasureAsDouble(orchestrator, SUB_MODULE, "sqale_rating")).isEqualTo(3);
+ assertThat(getMeasureAsDouble(orchestrator, SUB_MODULE, "effort_to_reach_maintainability_rating_a")).isEqualTo(77);
+
+ assertThat(getMeasureAsDouble(orchestrator, DIRECTORY, "sqale_rating")).isEqualTo(1);
+ assertThat(getMeasureAsDouble(orchestrator, DIRECTORY, "effort_to_reach_maintainability_rating_a")).isZero();
+
+ assertThat(getMeasureAsDouble(orchestrator, FILE, "sqale_rating")).isEqualTo(1);
+ assertThat(getMeasureAsDouble(orchestrator, FILE, "effort_to_reach_maintainability_rating_a")).isZero();
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/qualityModel/NewDebtRatioMeasureTest.java b/tests/src/test/java/org/sonarqube/tests/qualityModel/NewDebtRatioMeasureTest.java
new file mode 100644
index 00000000000..90192354c83
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/qualityModel/NewDebtRatioMeasureTest.java
@@ -0,0 +1,142 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.qualityModel;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category2Suite;
+import java.util.Date;
+import javax.annotation.Nullable;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import util.ItUtils;
+
+import static org.apache.commons.lang.time.DateUtils.addDays;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.within;
+import static util.ItUtils.formatDate;
+import static util.ItUtils.getLeakPeriodValue;
+import static util.ItUtils.resetPeriod;
+import static util.ItUtils.setServerProperty;
+import static util.ItUtils.toDate;
+
+/**
+ * SONAR-5876
+ */
+public class NewDebtRatioMeasureTest {
+
+ private static final String NEW_DEBT_RATIO_METRIC_KEY = "new_sqale_debt_ratio";
+
+ private static final Date FIRST_COMMIT_DATE = toDate("2016-09-01");
+ private static final Date SECOND_COMMIT_DATE = toDate("2016-09-17");
+ private static final Date THIRD_COMMIT_DATE = toDate("2016-09-20");
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category2Suite.ORCHESTRATOR;
+
+ @AfterClass
+ public static void reset() throws Exception {
+ resetPeriod(orchestrator);
+ }
+
+ @Before
+ public void cleanUpAnalysisData() {
+ orchestrator.resetData();
+ }
+
+ @Test
+ public void new_debt_ratio_is_computed_from_new_debt_and_new_ncloc_count_per_file() throws Exception {
+ setServerProperty(orchestrator, "sonar.leak.period", "previous_analysis");
+
+ // run analysis on the day of after the first commit, with 'one-issue-per-line' profile
+ defineQualityProfile("one-issue-per-line");
+ provisionSampleProject();
+ setSampleProjectQualityProfile("one-issue-per-line");
+ runSampleProjectAnalysis("v1", "sonar.projectDate", formatDate(addDays(FIRST_COMMIT_DATE, 1)));
+
+ // first analysis, no previous snapshot => periods not resolved => no value
+ assertNoNewDebtRatio();
+
+ // run analysis on the day after of second commit 'one-issue-per-line' profile*
+ // => 3 new issues will be created
+ runSampleProjectAnalysis("v2", "sonar.projectDate", formatDate(addDays(SECOND_COMMIT_DATE, 1)));
+ assertNewDebtRatio(4.44);
+
+ // run analysis on the day after of third commit 'one-issue-per-line' profile*
+ // => 4 new issues will be created
+ runSampleProjectAnalysis("v3", "sonar.projectDate", formatDate(addDays(THIRD_COMMIT_DATE, 1)));
+ assertNewDebtRatio(4.17);
+ }
+
+ @Test
+ public void compute_new_debt_ratio_using_number_days_in_leak_period() throws Exception {
+ setServerProperty(orchestrator, "sonar.leak.period", "30");
+
+ // run analysis on the day of after the first commit, with 'one-issue-per-line' profile
+ defineQualityProfile("one-issue-per-line");
+ provisionSampleProject();
+ setSampleProjectQualityProfile("one-issue-per-line");
+ runSampleProjectAnalysis("v1", "sonar.projectDate", formatDate(addDays(FIRST_COMMIT_DATE, 1)));
+
+ // first analysis, no previous snapshot => periods not resolved => no value
+ assertNoNewDebtRatio();
+
+ // run analysis on the day after of second commit 'one-issue-per-line' profile*
+ // => 3 new issues will be created
+ runSampleProjectAnalysis("v2", "sonar.projectDate", formatDate(addDays(SECOND_COMMIT_DATE, 1)));
+ assertNewDebtRatio(4.44);
+
+ // run analysis on the day after of third commit 'one-issue-per-line' profile*
+ // => previous 3 issues plus 4 new issues will be taking into account
+ runSampleProjectAnalysis("v3", "sonar.projectDate", formatDate(addDays(THIRD_COMMIT_DATE, 1)));
+ assertNewDebtRatio(4.28);
+ }
+
+ private void assertNoNewDebtRatio() {
+ assertThat(getLeakPeriodValue(orchestrator, "sample:src/main/xoo/sample/Sample.xoo", NEW_DEBT_RATIO_METRIC_KEY)).isZero();
+ }
+
+ private void assertNewDebtRatio(@Nullable Double valuePeriod) {
+ assertThat(getLeakPeriodValue(orchestrator, "sample:src/main/xoo/sample/Sample.xoo", NEW_DEBT_RATIO_METRIC_KEY)).isEqualTo(valuePeriod, within(0.01));
+ }
+
+ private void setSampleProjectQualityProfile(String qualityProfileKey) {
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", qualityProfileKey);
+ }
+
+ private void provisionSampleProject() {
+ orchestrator.getServer().provisionProject("sample", "sample");
+ }
+
+ private void defineQualityProfile(String qualityProfileKey) {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/measure/" + qualityProfileKey + ".xml"));
+ }
+
+ private void runSampleProjectAnalysis(String projectVersion, String... properties) {
+ ItUtils.runVerboseProjectAnalysis(
+ NewDebtRatioMeasureTest.orchestrator,
+ "measure/xoo-new-debt-ratio-" + projectVersion,
+ ItUtils.concat(properties,
+ // disable standard scm support so that it does not interfere with Xoo Scm sensor
+ "sonar.scm.disabled", "false"));
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/qualityModel/ReliabilityMeasureTest.java b/tests/src/test/java/org/sonarqube/tests/qualityModel/ReliabilityMeasureTest.java
new file mode 100644
index 00000000000..3afaa189ea9
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/qualityModel/ReliabilityMeasureTest.java
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.qualityModel;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category2Suite;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.WsMeasures;
+import util.ItUtils;
+
+import static java.lang.Double.parseDouble;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.getMeasuresByMetricKey;
+import static util.ItUtils.projectDir;
+
+public class ReliabilityMeasureTest {
+
+ private static final String PROJECT = "com.sonarsource.it.samples:multi-modules-sample";
+ private static final String MODULE = "com.sonarsource.it.samples:multi-modules-sample:module_a";
+ private static final String SUB_MODULE = "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1";
+ private static final String DIRECTORY = "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1";
+ private static final String FILE = "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo";
+
+ private static final String BUGS_METRIC = "bugs";
+ private static final String RELIABILITY_REMEDIATION_EFFORT_METRIC = "reliability_remediation_effort";
+ private static final String RELIABILITY_RATING_METRIC = "reliability_rating";
+
+ private static final String[] METRICS = new String[] {BUGS_METRIC, RELIABILITY_RATING_METRIC, RELIABILITY_REMEDIATION_EFFORT_METRIC};
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category2Suite.ORCHESTRATOR;
+
+ @Before
+ public void init() {
+ orchestrator.resetData();
+
+ orchestrator.getServer().provisionProject(PROJECT, PROJECT);
+ }
+
+ @Test
+ public void verify_reliability_measures_when_bug_rules_activated() {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/qualityModel/with-many-rules.xml"));
+ orchestrator.getServer().associateProjectToQualityProfile(PROJECT, "xoo", "with-many-rules");
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-multi-modules-sample")));
+
+ assertMeasures(PROJECT, 61, 305, 4);
+ assertMeasures(MODULE, 37, 185, 4);
+ assertMeasures(SUB_MODULE, 16, 80, 4);
+ assertMeasures(DIRECTORY, 16, 80, 4);
+ assertMeasures(FILE, 16, 80, 4);
+ }
+
+ @Test
+ public void verify_reliability_measures_when_no_bug_rule() {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/qualityModel/without-type-bug.xml"));
+ orchestrator.getServer().associateProjectToQualityProfile(PROJECT, "xoo", "without-type-bug");
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-multi-modules-sample")));
+
+ assertMeasures(PROJECT, 0, 0, 1);
+ }
+
+ private void assertMeasures(String componentKey, int expectedBugs, int expectedReliabilityRemediationEffort, int expectedReliabilityRating) {
+ Map<String, WsMeasures.Measure> measures = getMeasuresByMetricKey(orchestrator, componentKey, METRICS);
+ assertThat(parseDouble(measures.get(BUGS_METRIC).getValue())).isEqualTo(expectedBugs);
+ assertThat(parseDouble(measures.get(RELIABILITY_REMEDIATION_EFFORT_METRIC).getValue())).isEqualTo(expectedReliabilityRemediationEffort);
+ assertThat(parseDouble(measures.get(RELIABILITY_RATING_METRIC).getValue())).isEqualTo(expectedReliabilityRating);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/qualityModel/SecurityMeasureTest.java b/tests/src/test/java/org/sonarqube/tests/qualityModel/SecurityMeasureTest.java
new file mode 100644
index 00000000000..5aa1d0b1428
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/qualityModel/SecurityMeasureTest.java
@@ -0,0 +1,89 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.qualityModel;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category2Suite;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.WsMeasures;
+import util.ItUtils;
+
+import static java.lang.Double.parseDouble;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.getMeasuresByMetricKey;
+import static util.ItUtils.projectDir;
+
+public class SecurityMeasureTest {
+
+ private static final String PROJECT = "com.sonarsource.it.samples:multi-modules-sample";
+ private static final String MODULE = "com.sonarsource.it.samples:multi-modules-sample:module_a";
+ private static final String SUB_MODULE = "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1";
+ private static final String DIRECTORY = "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1";
+ private static final String FILE = "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo";
+
+ private static final String VULNERABILITIES_METRIC = "vulnerabilities";
+ private static final String SECURITY_REMEDIATION_EFFORT_METRIC = "security_remediation_effort";
+ private static final String SECURITY_RATING_METRIC = "security_rating";
+
+ private static final String[] METRICS = new String[] {VULNERABILITIES_METRIC, SECURITY_REMEDIATION_EFFORT_METRIC, SECURITY_RATING_METRIC};
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category2Suite.ORCHESTRATOR;
+
+ @Before
+ public void init() {
+ orchestrator.resetData();
+
+ orchestrator.getServer().provisionProject(PROJECT, PROJECT);
+ }
+
+ @Test
+ public void verify_security_measures_when_vulnerability_rules_activated() {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/qualityModel/with-many-rules.xml"));
+ orchestrator.getServer().associateProjectToQualityProfile(PROJECT, "xoo", "with-many-rules");
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-multi-modules-sample")));
+
+ assertMeasures(PROJECT, 4, 340, 5);
+ assertMeasures(MODULE, 2, 170, 5);
+ assertMeasures(SUB_MODULE, 1, 85, 5);
+ assertMeasures(DIRECTORY, 0, 0, 1);
+ assertMeasures(FILE, 0, 0, 1);
+ }
+
+ @Test
+ public void verify_security_measures_when_no_vulnerability_rule() {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/qualityModel/without-type-vulnerability.xml"));
+ orchestrator.getServer().associateProjectToQualityProfile(PROJECT, "xoo", "without-type-vulnerability");
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-multi-modules-sample")));
+
+ assertMeasures(PROJECT, 0, 0, 1);
+ }
+
+ private void assertMeasures(String componentKey, int expectedVulnerabilities, int expectedReliabilityRemediationEffort, int expectedReliabilityRating) {
+ Map<String, WsMeasures.Measure> measures = getMeasuresByMetricKey(orchestrator, componentKey, METRICS);
+ assertThat(parseDouble(measures.get(VULNERABILITIES_METRIC).getValue())).isEqualTo(expectedVulnerabilities);
+ assertThat(parseDouble(measures.get(SECURITY_REMEDIATION_EFFORT_METRIC).getValue())).isEqualTo(expectedReliabilityRemediationEffort);
+ assertThat(parseDouble(measures.get(SECURITY_RATING_METRIC).getValue())).isEqualTo(expectedReliabilityRating);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/qualityModel/TechnicalDebtInIssueChangelogTest.java b/tests/src/test/java/org/sonarqube/tests/qualityModel/TechnicalDebtInIssueChangelogTest.java
new file mode 100644
index 00000000000..cb181386895
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/qualityModel/TechnicalDebtInIssueChangelogTest.java
@@ -0,0 +1,84 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.qualityModel;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category2Suite;
+import java.util.List;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.wsclient.issue.Issue;
+import org.sonar.wsclient.issue.IssueClient;
+import org.sonar.wsclient.issue.IssueQuery;
+import org.sonarqube.ws.Issues;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.projectDir;
+
+/**
+ * SONAR-4834
+ */
+public class TechnicalDebtInIssueChangelogTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category2Suite.ORCHESTRATOR;
+
+ @Rule
+ public DebtConfigurationRule debtConfiguration = DebtConfigurationRule.create(orchestrator);
+
+ @Before
+ public void deleteAnalysisData() {
+ orchestrator.resetData();
+ }
+
+ @Test
+ public void display_debt_in_issue_changelog() throws Exception {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/qualityModel/one-issue-per-file.xml"));
+ orchestrator.getServer().provisionProject("sample", "sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-file");
+
+ // Execute a first analysis to have a past snapshot
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")));
+
+ // Second analysis, existing issues on OneIssuePerFile will have their technical debt updated with the effort to fix
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample"))
+ .setProperties("sonar.oneIssuePerFile.effortToFix", "10"));
+
+ IssueClient issueClient = orchestrator.getServer().wsClient().issueClient();
+ Issue issue = issueClient.find(IssueQuery.create()).list().get(0);
+
+ List<Issues.ChangelogWsResponse.Changelog> changes = changelog(issue.key()).getChangelogList();
+ assertThat(changes).hasSize(1);
+ assertThat(changes.get(0).getDiffsList())
+ .extracting(Issues.ChangelogWsResponse.Changelog.Diff::getKey, Issues.ChangelogWsResponse.Changelog.Diff::getOldValue, Issues.ChangelogWsResponse.Changelog.Diff::getNewValue)
+ .containsOnly(tuple("effort", "10", "100"));
+ }
+
+ private static Issues.ChangelogWsResponse changelog(String issueKey) {
+ return newAdminWsClient(orchestrator).issues().changelog(issueKey);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/qualityModel/TechnicalDebtMeasureVariationTest.java b/tests/src/test/java/org/sonarqube/tests/qualityModel/TechnicalDebtMeasureVariationTest.java
new file mode 100644
index 00000000000..5ea52641e4c
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/qualityModel/TechnicalDebtMeasureVariationTest.java
@@ -0,0 +1,154 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.qualityModel;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category2Suite;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.getLeakPeriodValue;
+import static util.ItUtils.setServerProperty;
+
+/**
+ * SONAR-4776
+ */
+public class TechnicalDebtMeasureVariationTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category2Suite.ORCHESTRATOR;
+
+ @AfterClass
+ public static void resetPeriod() throws Exception {
+ ItUtils.resetPeriod(orchestrator);
+ }
+
+ @Before
+ public void cleanUpAnalysisData() {
+ orchestrator.resetData();
+ }
+
+ @Test
+ public void new_technical_debt_measures_from_new_issues() throws Exception {
+ setServerProperty(orchestrator, "sonar.leak.period", "previous_analysis");
+
+ // Execute an analysis in the past to have a past snapshot without any issues
+ provisionSampleProject();
+ setSampleProjectQualityProfile("empty");
+ runSampleProjectAnalysis("sonar.projectDate", "2013-01-01");
+
+ // Second analysis -> issues will be created
+ defineQualityProfile("one-issue-per-line");
+ setSampleProjectQualityProfile("one-issue-per-line");
+ runSampleProjectAnalysis();
+
+ // New technical debt only comes from new issues
+ assertThat(getLeakPeriodValue(orchestrator, "sample:src/main/xoo/sample/Sample.xoo", "new_technical_debt")).isEqualTo(17d);
+
+ // Third analysis, with exactly the same profile -> no new issues so no new technical debt
+ runSampleProjectAnalysis();
+ // No variation => measure is 0 (best value)
+ assertThat(getLeakPeriodValue(orchestrator, "sample:src/main/xoo/sample/Sample.xoo", "new_technical_debt")).isZero();
+ }
+
+ @Test
+ public void new_technical_debt_measures_from_technical_debt_update_since_previous_analysis() throws Exception {
+ setServerProperty(orchestrator, "sonar.leak.period", "previous_analysis");
+
+ // Execute twice analysis
+ defineQualityProfile("one-issue-per-file");
+ provisionSampleProject();
+ setSampleProjectQualityProfile("one-issue-per-file");
+ runSampleProjectAnalysis();
+ runSampleProjectAnalysis();
+
+ // Third analysis, existing issues on OneIssuePerFile will have their technical debt updated with the effort to fix
+ runSampleProjectAnalysis("sonar.oneIssuePerFile.effortToFix", "10");
+ assertThat(getLeakPeriodValue(orchestrator, "sample:src/main/xoo/sample/Sample.xoo", "new_technical_debt")).isEqualTo(90);
+
+ // Fourth analysis, with exactly the same profile -> no new issues so no new technical debt since previous analysis
+ runSampleProjectAnalysis("sonar.oneIssuePerFile.effortToFix", "10");
+ assertThat(getLeakPeriodValue(orchestrator, "sample:src/main/xoo/sample/Sample.xoo", "new_technical_debt")).isZero();
+ }
+
+ @Test
+ public void new_technical_debt_measures_from_technical_debt_update_since_30_days() throws Exception {
+ setServerProperty(orchestrator, "sonar.leak.period", "30");
+
+ // Execute an analysis in the past to have a past snapshot without any issues
+ provisionSampleProject();
+ setSampleProjectQualityProfile("empty");
+ runSampleProjectAnalysis("sonar.projectDate", "2013-01-01");
+
+ // Second analysis -> issues will be created
+ String profileXmlFile = "one-issue-per-file";
+ defineQualityProfile(profileXmlFile);
+ setSampleProjectQualityProfile("one-issue-per-file");
+ runSampleProjectAnalysis();
+
+ // Third analysis, existing issues on OneIssuePerFile will have their technical debt updated with the effort to fix
+ runSampleProjectAnalysis("sonar.oneIssuePerFile.effortToFix", "10");
+ assertThat(getLeakPeriodValue(orchestrator, "sample:src/main/xoo/sample/Sample.xoo", "new_technical_debt")).isEqualTo(90);
+
+ // Fourth analysis, with exactly the same profile -> no new issues so no new technical debt since previous analysis but still since 30
+ // days
+ runSampleProjectAnalysis("sonar.oneIssuePerFile.effortToFix", "10");
+ assertThat(getLeakPeriodValue(orchestrator, "sample:src/main/xoo/sample/Sample.xoo", "new_technical_debt")).isEqualTo(90);
+ }
+
+ /**
+ * SONAR-5059
+ */
+ @Test
+ public void new_technical_debt_measures_should_never_be_negative() throws Exception {
+ setServerProperty(orchestrator, "sonar.leak.period", "previous_analysis");
+
+ // Execute an analysis with a big effort to fix
+ defineQualityProfile("one-issue-per-file");
+ provisionSampleProject();
+ setSampleProjectQualityProfile("one-issue-per-file");
+ runSampleProjectAnalysis("sonar.oneIssuePerFile.effortToFix", "100");
+
+ // Execute a second analysis with a smaller effort to fix -> Added technical debt should be 0, not negative
+ runSampleProjectAnalysis("sonar.oneIssuePerFile.effortToFix", "10");
+ assertThat(getLeakPeriodValue(orchestrator, "sample:src/main/xoo/sample/Sample.xoo", "new_technical_debt")).isZero();
+ }
+
+ private void setSampleProjectQualityProfile(String qualityProfileKey) {
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", qualityProfileKey);
+ }
+
+ private void provisionSampleProject() {
+ orchestrator.getServer().provisionProject("sample", "sample");
+ }
+
+ private void defineQualityProfile(String qualityProfileKey) {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/measure/" + qualityProfileKey + ".xml"));
+ }
+
+ private void runSampleProjectAnalysis(String... properties) {
+ ItUtils.runVerboseProjectAnalysis(TechnicalDebtMeasureVariationTest.orchestrator, "shared/xoo-sample", properties);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/qualityModel/TechnicalDebtTest.java b/tests/src/test/java/org/sonarqube/tests/qualityModel/TechnicalDebtTest.java
new file mode 100644
index 00000000000..6eb57cc0ea3
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/qualityModel/TechnicalDebtTest.java
@@ -0,0 +1,70 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.qualityModel;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category2Suite;
+import java.util.List;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.wsclient.issue.Issue;
+import org.sonar.wsclient.issue.IssueQuery;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.projectDir;
+
+public class TechnicalDebtTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category2Suite.ORCHESTRATOR;
+
+ @Rule
+ public DebtConfigurationRule debtConfiguration = DebtConfigurationRule.create(orchestrator);
+
+ @Before
+ public void deleteAnalysisData() {
+ orchestrator.resetData();
+ }
+
+ /**
+ * SONAR-4716
+ */
+ @Test
+ public void technical_debt_on_issue() throws Exception {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource("/qualityModel/one-issue-per-line.xml"));
+ orchestrator.getServer().provisionProject("sample", "sample");
+ orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");
+
+ // Generate some issues
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")));
+
+ // All the issues should have a technical debt
+ List<Issue> issues = orchestrator.getServer().wsClient().issueClient().find(IssueQuery.create()).list();
+ assertThat(issues).isNotEmpty();
+ for (Issue issue : issues) {
+ assertThat(issue.debt()).isEqualTo("1min");
+ }
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/qualityProfile/BuiltInQualityProfilesTest.java b/tests/src/test/java/org/sonarqube/tests/qualityProfile/BuiltInQualityProfilesTest.java
new file mode 100644
index 00000000000..82719d800f3
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/qualityProfile/BuiltInQualityProfilesTest.java
@@ -0,0 +1,157 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.qualityProfile;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category6Suite;
+import java.util.function.Predicate;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.tests.Session;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.Organizations.Organization;
+import org.sonarqube.ws.QualityProfiles;
+import org.sonarqube.ws.QualityProfiles.CreateWsResponse;
+import org.sonarqube.ws.QualityProfiles.SearchWsResponse;
+import org.sonarqube.ws.QualityProfiles.SearchWsResponse.QualityProfile;
+import org.sonarqube.ws.WsUsers.CreateWsResponse.User;
+import org.sonarqube.ws.client.qualityprofile.ChangeParentRequest;
+import org.sonarqube.ws.client.qualityprofile.CopyRequest;
+import org.sonarqube.ws.client.qualityprofile.QualityProfilesService;
+import org.sonarqube.ws.client.qualityprofile.SearchWsRequest;
+import org.sonarqube.ws.client.qualityprofile.SetDefaultRequest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static util.ItUtils.expectBadRequestError;
+
+public class BuiltInQualityProfilesTest {
+ private static final String RULE_ONE_BUG_PER_LINE = "xoo:OneBugIssuePerLine";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category6Suite.ORCHESTRATOR;
+
+ @Rule
+ public Tester tester = new Tester(orchestrator);
+
+ @Test
+ public void built_in_profiles_are_available_in_new_organization() {
+ Organization org = tester.organizations().generate();
+ SearchWsResponse result = tester.qProfiles().service().search(new SearchWsRequest().setOrganizationKey(org.getKey()));
+
+ assertThat(result.getProfilesList())
+ .extracting(QualityProfile::getName, QualityProfile::getLanguage, QualityProfile::getIsBuiltIn, QualityProfile::getIsDefault)
+ .containsExactlyInAnyOrder(
+ tuple("Basic", "xoo", true, true),
+ tuple("empty", "xoo", true, false),
+ tuple("Basic", "xoo2", true, true));
+ }
+
+ @Test
+ public void built_in_profiles_are_available_in_default_organization() {
+ SearchWsResponse result = tester.qProfiles().service().search(new SearchWsRequest().setOrganizationKey("default-organization"));
+
+ assertThat(result.getProfilesList())
+ .extracting(QualityProfile::getOrganization, QualityProfile::getName, QualityProfile::getLanguage, QualityProfile::getIsBuiltIn, QualityProfile::getIsDefault)
+ .containsExactlyInAnyOrder(
+ tuple("default-organization", "Basic", "xoo", true, true),
+ tuple("default-organization", "empty", "xoo", true, false),
+ tuple("default-organization", "Basic", "xoo2", true, true));
+ }
+
+ @Test
+ public void cannot_delete_built_in_profile_even_when_not_the_default_profile() {
+ Organization org = tester.organizations().generate();
+ QualityProfile builtInProfile = getProfile(org, p -> p.getIsBuiltIn() && p.getIsDefault() && "Basic".equals(p.getName()) && "xoo".equals(p.getLanguage()));
+
+ CreateWsResponse.QualityProfile profileInOrg = tester.qProfiles().createXooProfile(org);
+ tester.qProfiles().service().setDefault(new SetDefaultRequest(profileInOrg.getKey()));
+
+ expectBadRequestError(() -> tester.qProfiles().service().delete(builtInProfile.getKey()));
+ }
+
+ @Test
+ public void built_in_profile_cannot_be_modified() {
+ Organization org = tester.organizations().generate();
+ QualityProfile builtInProfile = getProfile(org, p -> p.getIsBuiltIn() && p.getIsDefault() && "Basic".equals(p.getName()) && "xoo".equals(p.getLanguage()));
+
+ QualityProfilesService service = tester.qProfiles().service();
+ expectBadRequestError(() -> tester.qProfiles().activateRule(builtInProfile.getKey(), RULE_ONE_BUG_PER_LINE));
+ expectBadRequestError(() -> service.deactivateRule(builtInProfile.getKey(), RULE_ONE_BUG_PER_LINE));
+ expectBadRequestError(() -> service.delete(builtInProfile.getKey()));
+ }
+
+ @Test
+ public void copy_built_in_profile_to_a_custom_profile() {
+ Organization org = tester.organizations().generate();
+ User administrator = tester.users().generateAdministrator(org);
+ QualityProfile builtInProfile = getProfile(org, p -> p.getIsBuiltIn() && "Basic".equals(p.getName()) && "xoo".equals(p.getLanguage()));
+ Session adminSession = tester.as(administrator.getLogin());
+
+ QualityProfiles.CopyWsResponse copyResponse = adminSession.qProfiles().service().copy(new CopyRequest(builtInProfile.getKey(), "My copy"));
+
+ assertThat(copyResponse.getIsDefault()).isFalse();
+ assertThat(copyResponse.getKey()).isNotEmpty().isNotEqualTo(builtInProfile.getKey());
+ assertThat(copyResponse.getLanguage()).isEqualTo(builtInProfile.getLanguage());
+ assertThat(copyResponse.getName()).isEqualTo("My copy");
+ assertThat(copyResponse.getIsInherited()).isFalse();
+
+ QualityProfile copy = getProfile(org, p -> "My copy".equals(p.getName()) && "xoo".equals(p.getLanguage()));
+ assertThat(copy.getIsBuiltIn()).isFalse();
+ assertThat(copy.getIsDefault()).isFalse();
+ assertThat(builtInProfile.getActiveRuleCount()).isGreaterThan(0);
+ adminSession.qProfiles().assertThatNumberOfActiveRulesEqualsTo(copy.getKey(), (int) builtInProfile.getActiveRuleCount());
+ }
+
+ @Test
+ public void can_inherit_and_disinherit_from_built_in_profile_to_a_custom_profile() {
+ Organization org = tester.organizations().generate();
+ User administrator = tester.users().generateAdministrator(org);
+ QualityProfile builtInProfile = getProfile(org, p -> p.getIsBuiltIn() && "Basic".equals(p.getName()) && "xoo".equals(p.getLanguage()));
+ Session adminSession = tester.as(administrator.getLogin());
+
+ QualityProfiles.CopyWsResponse copyResponse = adminSession.qProfiles().service().copy(new CopyRequest(builtInProfile.getKey(), "My copy"));
+ adminSession.qProfiles().service().changeParent(
+ ChangeParentRequest.builder().setParentKey(builtInProfile.getKey()).setProfileKey(copyResponse.getKey()).build());
+
+ QualityProfile inheritedQualityPropfile = getProfile(org, p -> p.getKey().equals(copyResponse.getKey()));
+
+ assertThat(inheritedQualityPropfile.getParentKey()).isEqualTo(builtInProfile.getKey());
+ assertThat(inheritedQualityPropfile.getParentName()).isEqualTo(builtInProfile.getName());
+
+ // Remove inheritance
+ adminSession.qProfiles().service().changeParent(
+ new ChangeParentRequest(ChangeParentRequest.builder().setProfileKey(inheritedQualityPropfile.getKey())));
+
+ inheritedQualityPropfile = getProfile(org, p -> p.getKey().equals(copyResponse.getKey()));
+
+ assertThat(inheritedQualityPropfile.getParentKey()).isEmpty();
+ assertThat(inheritedQualityPropfile.getParentName()).isEmpty();
+ }
+
+ private QualityProfile getProfile(Organization organization, Predicate<QualityProfile> filter) {
+ return tester.qProfiles().service().search(new SearchWsRequest()
+ .setOrganizationKey(organization.getKey())).getProfilesList()
+ .stream()
+ .filter(filter)
+ .findAny().orElseThrow(IllegalStateException::new);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/qualityProfile/CustomQualityProfilesTest.java b/tests/src/test/java/org/sonarqube/tests/qualityProfile/CustomQualityProfilesTest.java
new file mode 100644
index 00000000000..0be4b07d90f
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/qualityProfile/CustomQualityProfilesTest.java
@@ -0,0 +1,328 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.qualityProfile;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category6Suite;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.tests.QProfileTester;
+import org.sonarqube.tests.Session;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.Organizations.Organization;
+import org.sonarqube.ws.QualityProfiles;
+import org.sonarqube.ws.QualityProfiles.CreateWsResponse.QualityProfile;
+import org.sonarqube.ws.WsUsers.CreateWsResponse.User;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.qualityprofile.AddProjectRequest;
+import org.sonarqube.ws.client.qualityprofile.ChangeParentRequest;
+import org.sonarqube.ws.client.qualityprofile.CopyRequest;
+import org.sonarqube.ws.client.qualityprofile.CreateRequest;
+import org.sonarqube.ws.client.qualityprofile.SearchWsRequest;
+import org.sonarqube.ws.client.qualityprofile.SetDefaultRequest;
+import util.ItUtils;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.expectForbiddenError;
+import static util.ItUtils.expectMissingError;
+import static util.ItUtils.expectUnauthorizedError;
+import static util.ItUtils.projectDir;
+
+public class CustomQualityProfilesTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category6Suite.ORCHESTRATOR;
+
+ @Rule
+ public Tester tester = new Tester(orchestrator);
+
+ @Test
+ public void activation_of_rules_is_isolated_among_organizations() {
+ // create two profiles with same names in two organizations
+ Organization org1 = tester.organizations().generate();
+ Organization org2 = tester.organizations().generate();
+ QualityProfile profileInOrg1 = tester.qProfiles().createXooProfile(org1, p -> p.setProfileName("foo"));
+ QualityProfile profileInOrg2 = tester.qProfiles().createXooProfile(org2, p -> p.setProfileName("foo"));
+
+ tester.qProfiles()
+ .assertThatNumberOfActiveRulesEqualsTo(profileInOrg1, 0)
+ .assertThatNumberOfActiveRulesEqualsTo(profileInOrg2, 0);
+
+ tester.qProfiles()
+ .activateRule(profileInOrg1, "xoo:OneIssuePerLine")
+ .assertThatNumberOfActiveRulesEqualsTo(profileInOrg1, 1)
+ .assertThatNumberOfActiveRulesEqualsTo(profileInOrg2, 0);
+
+ tester.qProfiles()
+ .activateRule(profileInOrg1, "xoo:OneIssuePerFile")
+ .assertThatNumberOfActiveRulesEqualsTo(profileInOrg1, 2)
+ .assertThatNumberOfActiveRulesEqualsTo(profileInOrg2, 0);
+
+ tester.qProfiles()
+ .deactivateRule(profileInOrg1, "xoo:OneIssuePerFile")
+ .assertThatNumberOfActiveRulesEqualsTo(profileInOrg1, 1)
+ .assertThatNumberOfActiveRulesEqualsTo(profileInOrg2, 0);
+
+ tester.qProfiles()
+ .activateRule(profileInOrg2, "xoo:OneIssuePerFile")
+ .assertThatNumberOfActiveRulesEqualsTo(profileInOrg1, 1)
+ .assertThatNumberOfActiveRulesEqualsTo(profileInOrg2, 1);
+
+ delete(profileInOrg1);
+ tester.qProfiles()
+ .assertThatNumberOfActiveRulesEqualsTo(profileInOrg1, 0)
+ .assertThatNumberOfActiveRulesEqualsTo(profileInOrg2, 1);
+ }
+
+ @Test
+ public void an_organization_administrator_can_manage_the_profiles_of_organization() {
+ Organization org = tester.organizations().generate();
+ User user = tester.users().generateAdministrator(org);
+
+ QProfileTester adminSession = tester.as(user.getLogin()).qProfiles();
+ QualityProfile profile = adminSession.createXooProfile(org);
+ adminSession.assertThatNumberOfActiveRulesEqualsTo(profile, 0);
+
+ adminSession
+ .activateRule(profile, "xoo:OneIssuePerFile")
+ .assertThatNumberOfActiveRulesEqualsTo(profile, 1);
+
+ adminSession.service().delete(profile.getKey());
+ adminSession.assertThatNumberOfActiveRulesEqualsTo(profile, 0);
+ }
+
+ @Test
+ public void deleting_an_organization_delete_all_profiles_on_this_organization() {
+ Organization org = tester.organizations().generate();
+ User user = tester.users().generateAdministrator(org);
+
+ QProfileTester adminSession = tester.as(user.getLogin()).qProfiles();
+ // Profile
+ QualityProfile parentProfile = adminSession.createXooProfile(org);
+
+ // Copied profile
+ QualityProfiles.SearchWsResponse.QualityProfile builtInProfile = getProfile(org, p -> p.getIsBuiltIn() && "Basic".equals(p.getName()) && "xoo".equals(p.getLanguage()));
+ QualityProfiles.CopyWsResponse copyResponse = adminSession.service().copy(new CopyRequest(builtInProfile.getKey(), "My copy"));
+
+ // Inherited profile from custom
+ QualityProfile inheritedProfile1 = adminSession.service().create(
+ CreateRequest.builder()
+ .setLanguage(parentProfile.getLanguage())
+ .setOrganizationKey(org.getKey())
+ .setProfileName("inherited_profile")
+ .build())
+ .getProfile();
+
+ adminSession.service().changeParent(
+ ChangeParentRequest.builder().setParentKey(parentProfile.getKey()).setProfileKey(inheritedProfile1.getKey()).build());
+
+ // Inherited profile from builtIn
+ QualityProfile inheritedProfile2 = adminSession.service().create(
+ CreateRequest.builder()
+ .setLanguage(parentProfile.getLanguage())
+ .setOrganizationKey(org.getKey())
+ .setProfileName("inherited_profile2")
+ .build())
+ .getProfile();
+
+ adminSession.service().changeParent(
+ ChangeParentRequest.builder().setParentKey(builtInProfile.getKey()).setProfileKey(inheritedProfile2.getKey()).build());
+
+ tester.organizations().service().delete(org.getKey());
+
+ expectMissingError(() -> tester.qProfiles().service().search(new SearchWsRequest()
+ .setOrganizationKey(org.getKey())));
+
+ tester.qProfiles().service().search(new SearchWsRequest()).getProfilesList()
+ .forEach(p -> {
+ assertThat(p.getOrganization()).isNotEqualTo(org.getKey());
+ assertThat(p.getKey()).isNotIn(parentProfile.getKey(), copyResponse.getKey(), inheritedProfile1.getKey(), inheritedProfile2.getKey());
+ });
+ }
+
+ @Test
+ public void an_organization_administrator_cannot_manage_the_profiles_of_other_organizations() {
+ Organization org1 = tester.organizations().generate();
+ Organization org2 = tester.organizations().generate();
+ QualityProfile profileInOrg2 = tester.qProfiles().createXooProfile(org2);
+ User adminOfOrg1 = tester.users().generateAdministrator(org1);
+
+ QProfileTester adminSession = tester.as(adminOfOrg1.getLogin()).qProfiles();
+
+ expectForbiddenError(() -> adminSession.createXooProfile(org2));
+ expectForbiddenError(() -> adminSession.service().delete(profileInOrg2.getKey()));
+ expectForbiddenError(() -> adminSession.activateRule(profileInOrg2, "xoo:OneIssuePerFile"));
+ expectForbiddenError(() -> adminSession.deactivateRule(profileInOrg2, "xoo:OneIssuePerFile"));
+ }
+
+ private void delete(QualityProfile profile) {
+ tester.qProfiles().service().delete(profile.getKey());
+ }
+
+ @Test
+ public void anonymous_cannot_manage_the_profiles_of_an_organization() {
+ Organization org = tester.organizations().generate();
+ QualityProfile profile = tester.qProfiles().createXooProfile(org);
+
+ Session anonymousSession = tester.asAnonymous();
+
+ expectUnauthorizedError(() -> anonymousSession.qProfiles().createXooProfile(org));
+ expectUnauthorizedError(() -> anonymousSession.qProfiles().service().delete(profile.getKey()));
+ expectUnauthorizedError(() -> anonymousSession.qProfiles().activateRule(profile, "xoo:OneIssuePerFile"));
+ expectUnauthorizedError(() -> anonymousSession.qProfiles().deactivateRule(profile, "xoo:OneIssuePerFile"));
+ }
+
+ @Test
+ public void root_can_manage_the_profiles_of_any_organization() {
+ Organization org = tester.organizations().generate();
+
+ User orgAdmin = tester.users().generateAdministrator(org);
+ Session adminSession = tester.as(orgAdmin.getLogin());
+ QualityProfile profile = adminSession.qProfiles().createXooProfile(org);
+
+ // root can activate rule and delete the profile
+ tester.qProfiles()
+ .activateRule(profile, "xoo:OneIssuePerFile")
+ .assertThatNumberOfActiveRulesEqualsTo(profile, 1);
+ tester.qProfiles().service().delete(profile.getKey());
+ tester.qProfiles().assertThatNumberOfActiveRulesEqualsTo(profile, 0);
+ }
+
+ @Test
+ public void can_inherit_and_disinherit_and__from_another_custom_profile() {
+ Organization org = tester.organizations().generate();
+ User user = tester.users().generateAdministrator(org);
+
+ Session adminSession = tester.as(user.getLogin());
+ QualityProfile parentProfile = adminSession.qProfiles().createXooProfile(org);
+ QualityProfile inheritedProfile = adminSession.qProfiles().service().create(
+ CreateRequest.builder()
+ .setLanguage(parentProfile.getLanguage())
+ .setOrganizationKey(org.getKey())
+ .setProfileName("inherited_profile")
+ .build())
+ .getProfile();
+
+ adminSession.qProfiles().service().changeParent(
+ ChangeParentRequest.builder().setParentKey(parentProfile.getKey()).setProfileKey(inheritedProfile.getKey()).build());
+
+ QualityProfiles.SearchWsResponse.QualityProfile inheritedQualityPropfile = getProfile(org, p -> p.getKey().equals(inheritedProfile.getKey()));
+
+ assertThat(inheritedQualityPropfile.getParentKey()).isEqualTo(parentProfile.getKey());
+ assertThat(inheritedQualityPropfile.getParentName()).isEqualTo(parentProfile.getName());
+
+ // Remove inheritance
+ adminSession.qProfiles().service().changeParent(
+ new ChangeParentRequest(ChangeParentRequest.builder().setProfileKey(inheritedQualityPropfile.getKey())));
+
+ inheritedQualityPropfile = getProfile(org, p -> p.getKey().equals(inheritedProfile.getKey()));
+
+ assertThat(inheritedQualityPropfile.getParentKey()).isEmpty();
+ assertThat(inheritedQualityPropfile.getParentName()).isEmpty();
+ }
+
+ @Test
+ public void analysis_must_use_default_profile() {
+ Organization org = tester.organizations().generate();
+ User admin = tester.users().generateAdministrator(org);
+
+ Session adminSession = tester.as(admin.getLogin());
+
+ String projectKey = randomAlphanumeric(10);
+ String projectName = randomAlphanumeric(10);
+ orchestrator.executeBuild(
+ SonarScanner.create(projectDir("shared/xoo-sample"),
+ "sonar.login", admin.getLogin(),
+ "sonar.password", admin.getLogin(),
+ "sonar.organization", org.getKey())
+ .setProjectKey(projectKey)
+ .setProjectName(projectName));
+
+ QualityProfiles.SearchWsResponse.QualityProfile defaultProfile = getProfile(org, p -> "xoo".equals(p.getLanguage()) &&
+ p.getIsDefault());
+ assertThatQualityProfileIsUsedFor(projectKey, defaultProfile.getKey());
+
+ QualityProfile newXooProfile = adminSession.qProfiles().createXooProfile(org);
+ adminSession.qProfiles().service().setDefault(new SetDefaultRequest(newXooProfile.getKey()));
+
+ orchestrator.executeBuild(
+ SonarScanner.create(projectDir("shared/xoo-sample"),
+ "sonar.login", admin.getLogin(),
+ "sonar.password", admin.getLogin(),
+ "sonar.organization", org.getKey())
+ .setProjectKey(projectKey)
+ .setProjectName(projectName));
+
+ assertThatQualityProfileIsUsedFor(projectKey, newXooProfile.getKey());
+ }
+
+ @Test
+ public void analysis_must_use_associated_profile() {
+ Organization org = tester.organizations().generate();
+ User admin = tester.users().generateAdministrator(org);
+ String projectKey = randomAlphanumeric(10);
+ String projectName = randomAlphanumeric(10);
+ Session adminSession = tester.as(admin.getLogin());
+ QualityProfile newXooProfile = adminSession.qProfiles().createXooProfile(org);
+
+ adminSession.wsClient().wsConnector().call(new PostRequest("api/projects/create")
+ .setParam("project", projectKey)
+ .setParam("name", projectName)
+ .setParam("organization", org.getKey()));
+
+ adminSession.qProfiles().service().addProject(AddProjectRequest.builder()
+ .setProfileKey(newXooProfile.getKey())
+ .setProjectKey(projectKey)
+ .build());
+
+ orchestrator.executeBuild(
+ SonarScanner.create(projectDir("shared/xoo-sample"),
+ "sonar.login", admin.getLogin(),
+ "sonar.password", admin.getLogin(),
+ "sonar.organization", org.getKey())
+ .setProjectKey(projectKey)
+ .setProjectName(projectName));
+
+ assertThatQualityProfileIsUsedFor(projectKey, newXooProfile.getKey());
+ }
+
+ private void assertThatQualityProfileIsUsedFor(String projectKey, String qualityProfileKey) {
+ GetRequest request = new GetRequest("api/navigation/component")
+ .setParam("componentKey", projectKey);
+ Map components = ItUtils.jsonToMap(tester.wsClient().wsConnector().call(request).content());
+
+ assertThat(((Map) ((List) components.get("qualityProfiles")).get(0)).get("key")).isEqualTo(qualityProfileKey);
+ }
+
+ private QualityProfiles.SearchWsResponse.QualityProfile getProfile(Organization organization, Predicate<QualityProfiles.SearchWsResponse.QualityProfile> filter) {
+ return tester.qProfiles().service().search(new SearchWsRequest()
+ .setOrganizationKey(organization.getKey())).getProfilesList()
+ .stream()
+ .filter(filter)
+ .findAny().orElseThrow(IllegalStateException::new);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/qualityProfile/OrganizationQualityProfilesUiTest.java b/tests/src/test/java/org/sonarqube/tests/qualityProfile/OrganizationQualityProfilesUiTest.java
new file mode 100644
index 00000000000..1d9b9c1e420
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/qualityProfile/OrganizationQualityProfilesUiTest.java
@@ -0,0 +1,181 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.qualityProfile;
+
+import com.codeborne.selenide.Condition;
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category6Suite;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.Organizations;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.qualityprofile.AddProjectRequest;
+import org.sonarqube.ws.client.qualityprofile.ChangeParentRequest;
+import org.sonarqube.pageobjects.Navigation;
+
+import static com.codeborne.selenide.Selenide.$;
+import static util.ItUtils.projectDir;
+
+public class OrganizationQualityProfilesUiTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category6Suite.ORCHESTRATOR;
+
+ @Rule
+ public Tester tester = new Tester(orchestrator);
+
+ private Organizations.Organization organization;
+
+ @Before
+ public void setUp() {
+ // key and name are overridden for HTML Selenese tests
+ organization = tester.organizations().generate(o -> o.setKey("test-org").setName("test-org"));
+ tester.users().generateAdministrator(organization, u -> u.setLogin("admin2").setPassword("admin2"));
+ createProfile("xoo", "sample");
+ inheritProfile("xoo", "sample", "Basic");
+ analyzeProject("shared/xoo-sample");
+ addProfileToProject("xoo", "sample", "sample");
+ }
+
+ @Test
+ public void testNoGlobalPage() {
+ Navigation nav = tester.openBrowser();
+ nav.open("/profiles");
+ $(".page-wrapper-simple").should(Condition.visible);
+ }
+
+ @Test
+ public void testHomePage() {
+ tester.runHtmlTests(
+ "/organization/OrganizationQualityProfilesUiTest/should_display_list.html",
+ "/organization/OrganizationQualityProfilesUiTest/should_open_from_list.html",
+ "/organization/OrganizationQualityProfilesUiTest/should_filter_by_language.html");
+ }
+
+ @Test
+ public void testProfilePage() {
+ tester.runHtmlTests(
+ "/organization/OrganizationQualityProfilesUiTest/should_display_profile_rules.html",
+ "/organization/OrganizationQualityProfilesUiTest/should_display_profile_inheritance.html",
+ "/organization/OrganizationQualityProfilesUiTest/should_display_profile_projects.html",
+ "/organization/OrganizationQualityProfilesUiTest/should_display_profile_exporters.html");
+ }
+
+ @Test
+ public void testNotFound() {
+ Navigation nav = tester.openBrowser();
+ nav.open("/organizations/" + organization.getKey() + "/quality_profiles/show?key=unknown");
+ $(".quality-profile-not-found").should(Condition.visible);
+
+ nav.open("/organizations/" + organization.getKey() + "/quality_profiles/show?language=xoo&name=unknown");
+ $(".quality-profile-not-found").should(Condition.visible);
+ }
+
+ @Test
+ public void testProfileChangelog() {
+ tester.runHtmlTests(
+ "/organization/OrganizationQualityProfilesUiTest/should_display_changelog.html");
+ }
+
+ @Ignore("to be replaced by selenide test in order to inject profile key")
+ @Test
+ public void testComparison() {
+ tester.runHtmlTests("/organization/OrganizationQualityProfilesUiTest/should_compare.html");
+ }
+
+ @Test
+ public void testCreation() {
+ tester.runHtmlTests("/organization/OrganizationQualityProfilesUiTest/should_create.html");
+ }
+
+ @Test
+ public void testDeletion() {
+ tester.runHtmlTests("/organization/OrganizationQualityProfilesUiTest/should_delete.html");
+ }
+
+ @Test
+ public void testCopying() {
+ tester.runHtmlTests("/organization/OrganizationQualityProfilesUiTest/should_copy.html");
+ }
+
+ @Test
+ public void testRenaming() {
+ tester.runHtmlTests("/organization/OrganizationQualityProfilesUiTest/should_rename.html");
+ }
+
+ @Test
+ public void testSettingDefault() {
+ tester.runHtmlTests("/organization/OrganizationQualityProfilesUiTest/should_set_default.html");
+ }
+
+ @Test
+ public void testRestoration() {
+ deleteProfile("xoo", "empty");
+
+ tester.runHtmlTests("/organization/OrganizationQualityProfilesUiTest/should_restore.html");
+ }
+
+ private void createProfile(String language, String name) {
+ tester.wsClient().wsConnector().call(
+ new PostRequest("api/qualityprofiles/create")
+ .setParam("language", language)
+ .setParam("name", name)
+ .setParam("organization", organization.getKey()));
+ }
+
+ private void inheritProfile(String language, String name, String parentName) {
+ tester.wsClient().qualityProfiles().changeParent(ChangeParentRequest.builder()
+ .setLanguage(language)
+ .setProfileName(name)
+ .setParentName(parentName)
+ .setOrganization(organization.getKey())
+ .build());
+ }
+
+ private void analyzeProject(String path) {
+ orchestrator.executeBuild(SonarScanner.create(projectDir(path)).setProperties(
+ "sonar.organization", organization.getKey(),
+ "sonar.login", "admin",
+ "sonar.password", "admin"));
+ }
+
+ private void addProfileToProject(String language, String profileName, String projectKey) {
+ tester.wsClient().qualityProfiles().addProject(AddProjectRequest.builder()
+ .setLanguage(language)
+ .setProfileName(profileName)
+ .setProjectKey(projectKey)
+ .setOrganization(organization.getKey())
+ .build());
+ }
+
+ private void deleteProfile(String language, String name) {
+ tester.wsClient().wsConnector().call(
+ new PostRequest("api/qualityprofiles/delete")
+ .setParam("language", language)
+ .setParam("profileName", name)
+ .setParam("organization", organization.getKey()));
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/qualityProfile/QualityProfilesUiTest.java b/tests/src/test/java/org/sonarqube/tests/qualityProfile/QualityProfilesUiTest.java
new file mode 100644
index 00000000000..25c126037bb
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/qualityProfile/QualityProfilesUiTest.java
@@ -0,0 +1,196 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.qualityProfile;
+
+import com.codeborne.selenide.Condition;
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category4Suite;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.qualityprofile.AddProjectRequest;
+import org.sonarqube.ws.client.qualityprofile.ChangeParentRequest;
+import org.sonarqube.ws.client.qualityprofile.CreateRequest;
+import org.sonarqube.pageobjects.Navigation;
+import util.user.UserRule;
+
+import static com.codeborne.selenide.Selenide.$;
+import static util.ItUtils.projectDir;
+
+public class QualityProfilesUiTest {
+
+ private static final String ADMIN_USER_LOGIN = "admin-user";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+
+ @Rule
+ public UserRule userRule = UserRule.from(orchestrator);
+
+ private static WsClient adminWsClient;
+
+ @Rule
+ public Tester tester = new Tester(orchestrator).disableOrganizations();
+
+ @Before
+ public void initAdminUser() throws Exception {
+ userRule.createAdminUser(ADMIN_USER_LOGIN, ADMIN_USER_LOGIN);
+ }
+
+ @After
+ public void deleteAdminUser() {
+ userRule.resetUsers();
+ }
+
+ @Before
+ public void createSampleProfile() {
+ createProfile("xoo", "sample");
+ inheritProfile("xoo", "sample", "Basic");
+ analyzeProject("shared/xoo-sample");
+ addProfileToProject("xoo", "sample", "sample");
+ }
+
+ @After
+ public void tearDown() {
+ setDefault("xoo", "Basic");
+ deleteProfile("xoo", "sample");
+ deleteProfile("xoo", "new name");
+ }
+
+ @Test
+ public void testHomePage() {
+ tester.runHtmlTests(
+ "/qualityProfile/QualityProfilesUiTest/should_display_list.html",
+ "/qualityProfile/QualityProfilesUiTest/should_open_from_list.html",
+ "/qualityProfile/QualityProfilesUiTest/should_filter_by_language.html");
+ }
+
+ @Test
+ public void testProfilePage() {
+ tester.runHtmlTests(
+ "/qualityProfile/QualityProfilesUiTest/should_display_profile_rules.html",
+ "/qualityProfile/QualityProfilesUiTest/should_display_profile_inheritance.html",
+ "/qualityProfile/QualityProfilesUiTest/should_display_profile_projects.html",
+ "/qualityProfile/QualityProfilesUiTest/should_display_profile_exporters.html");
+ }
+
+ @Test
+ public void testNotFound() {
+ Navigation nav = tester.openBrowser();
+
+ nav.open("/profiles/show?key=unknown");
+ $(".quality-profile-not-found").should(Condition.visible);
+
+ nav.open("/profiles/show?language=xoo&name=unknown");
+ $(".quality-profile-not-found").should(Condition.visible);
+ }
+
+ @Test
+ public void testProfileChangelog() {
+ tester.runHtmlTests(
+ "/qualityProfile/QualityProfilesUiTest/should_display_changelog.html");
+ }
+
+ @Ignore("find a way to know profile key inside selenium tests")
+ @Test
+ public void testComparison() {
+ tester.runHtmlTests("/qualityProfile/QualityProfilesUiTest/should_compare.html");
+ }
+
+ @Test
+ public void testCreation() {
+ tester.runHtmlTests("/qualityProfile/QualityProfilesUiTest/should_create.html");
+ }
+
+ @Test
+ public void testDeletion() {
+ tester.runHtmlTests("/qualityProfile/QualityProfilesUiTest/should_delete.html");
+ }
+
+ @Test
+ public void testCopying() {
+ tester.runHtmlTests("/qualityProfile/QualityProfilesUiTest/should_copy.html");
+ }
+
+ @Test
+ public void testRenaming() {
+ tester.runHtmlTests("/qualityProfile/QualityProfilesUiTest/should_rename.html");
+ }
+
+ @Test
+ public void testSettingDefault() {
+ tester.runHtmlTests("/qualityProfile/QualityProfilesUiTest/should_set_default.html");
+ }
+
+ @Test
+ public void testRestore() {
+ deleteProfile("xoo", "empty");
+
+ tester.runHtmlTests("/qualityProfile/QualityProfilesUiTest/should_restore.html");
+ }
+
+ private void createProfile(String language, String name) {
+ tester.wsClient().qualityProfiles().create(CreateRequest.builder()
+ .setLanguage(language)
+ .setProfileName(name)
+ .build());
+ }
+
+ private void inheritProfile(String language, String name, String parentName) {
+ tester.wsClient().qualityProfiles().changeParent(ChangeParentRequest.builder()
+ .setLanguage(language)
+ .setProfileName(name)
+ .setParentName(parentName)
+ .build());
+ }
+
+ private static void analyzeProject(String path) {
+ orchestrator.executeBuild(SonarScanner.create(projectDir(path)));
+ }
+
+ private void addProfileToProject(String language, String profileName, String projectKey) {
+ tester.wsClient().qualityProfiles().addProject(AddProjectRequest.builder()
+ .setLanguage(language)
+ .setProfileName(profileName)
+ .setProjectKey(projectKey)
+ .build());
+ }
+
+ private void deleteProfile(String language, String name) {
+ tester.wsClient().wsConnector().call(
+ new PostRequest("api/qualityprofiles/delete")
+ .setParam("language", language)
+ .setParam("profileName", name));
+ }
+
+ private void setDefault(String language, String name) {
+ tester.wsClient().wsConnector().call(
+ new PostRequest("api/qualityprofiles/set_default")
+ .setParam("language", language)
+ .setParam("profileName", name));
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/rule/RuleTagsTest.java b/tests/src/test/java/org/sonarqube/tests/rule/RuleTagsTest.java
new file mode 100644
index 00000000000..16e9397c713
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/rule/RuleTagsTest.java
@@ -0,0 +1,110 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.rule;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category6Suite;
+import java.util.List;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.Organizations;
+import org.sonarqube.ws.client.PostRequest;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class RuleTagsTest {
+
+ private static Orchestrator orchestrator = Category6Suite.ORCHESTRATOR;
+ private static Tester tester = new Tester(orchestrator);
+
+ @ClassRule
+ public static TestRule chain = RuleChain.outerRule(orchestrator)
+ .around(tester);
+
+ private static Organizations.Organization organization1;
+ private static Organizations.Organization organization2;
+
+ @BeforeClass
+ public static void setUp() {
+ organization1 = tester.organizations().generate();
+ organization2 = tester.organizations().generate();
+ }
+
+ @Test
+ public void should_not_show_tags_of_other_organization() {
+ updateTag("foo-tag", organization1);
+ updateTag("bar-tag", organization2);
+ assertThat(showRuleTags(organization1)).containsExactly("foo-tag");
+ assertThat(showRuleTags(organization2)).containsExactly("bar-tag");
+ }
+
+ @Test
+ public void should_not_list_tags_of_other_organization() {
+ updateTag("foo-tag", organization1);
+ updateTag("bar-tag", organization2);
+ assertThat(listTags(organization1))
+ .contains("foo-tag")
+ .doesNotContain("bar-tag");
+ }
+
+ @Test
+ public void should_not_show_removed_tags() {
+ updateTag("foo-tag", organization1);
+ assertThat(showRuleTags(organization1)).contains("foo-tag");
+
+ updateTag("", organization1);
+ assertThat(showRuleTags(organization1)).isEmpty();
+ }
+
+ @Test
+ public void should_not_list_removed_tags() {
+ updateTag("foo-tag", organization1);
+ assertThat(listTags(organization1)).contains("foo-tag");
+
+ updateTag("", organization1);
+ assertThat(listTags(organization1)).doesNotContain("foo-tag");
+ }
+
+ private List<String> listTags(Organizations.Organization organization) {
+ String json = orchestrator.getServer().newHttpCall("/api/rules/tags")
+ .setParam("organization", organization.getKey())
+ .execute()
+ .getBodyAsString();
+ return (List<String>) ItUtils.jsonToMap(json).get("tags");
+ }
+
+ private List<String> showRuleTags(Organizations.Organization organization) {
+ return tester.wsClient().rules().show(organization.getKey(), "xoo:OneIssuePerFile")
+ .getRule().getTags().getTagsList();
+ }
+
+ private void updateTag(String tag, Organizations.Organization organization) {
+ tester.wsClient().wsConnector().call(new PostRequest("/api/rules/update")
+ .setParam("organization", organization.getKey())
+ .setParam("key", "xoo:OneIssuePerFile")
+ .setParam("tags", tag))
+ .failIfNotSuccessful();
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/scm/ScmTest.java b/tests/src/test/java/org/sonarqube/tests/scm/ScmTest.java
new file mode 100644
index 00000000000..16cc5fbe942
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/scm/ScmTest.java
@@ -0,0 +1,139 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.scm;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.BuildResult;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category2Suite;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.commons.lang.builder.EqualsBuilder;
+import org.apache.commons.lang.builder.HashCodeBuilder;
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.commons.lang.builder.ToStringStyle;
+import org.assertj.core.data.MapEntry;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonar.wsclient.jsonsimple.JSONArray;
+import org.sonar.wsclient.jsonsimple.JSONObject;
+import org.sonar.wsclient.jsonsimple.JSONValue;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.projectDir;
+
+public class ScmTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category2Suite.ORCHESTRATOR;
+
+ private static final SimpleDateFormat DATETIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
+
+ @Before
+ public void delete_data() {
+ orchestrator.resetData();
+ }
+
+ @Test
+ public void scm_optimization() throws Exception {
+ SonarScanner build = SonarScanner.create(projectDir("scm/xoo-sample-with-scm"))
+ .setProperty("sonar.scm.provider", "xoo")
+ .setProperty("sonar.scm.disabled", "false");
+
+ // First run
+ BuildResult buildResult = orchestrator.executeBuild(build);
+
+ assertThat(getScmData("sample-scm:src/main/xoo/sample/Sample.xoo"))
+ .containsExactly(
+ MapEntry.entry(1, new LineData("1", "2013-01-04T00:00:00+0000", "jhenry")),
+ MapEntry.entry(3, new LineData("2", "2013-01-04T00:00:00+0000", "jhenry")),
+ MapEntry.entry(4, new LineData("1", "2013-01-04T00:00:00+0000", "jhenry")),
+ MapEntry.entry(8, new LineData("3", "2014-01-04T00:00:00+0000", "toto")));
+
+ assertThat(buildResult.getLogs()).containsSequence("1 files to be analyzed", "1/1 files analyzed");
+
+ // Second run with same file should not trigger blame but SCM data are copied from previous analysis
+ buildResult = orchestrator.executeBuild(build);
+
+ assertThat(getScmData("sample-scm:src/main/xoo/sample/Sample.xoo"))
+ .containsExactly(
+ MapEntry.entry(1, new LineData("1", "2013-01-04T00:00:00+0000", "jhenry")),
+ MapEntry.entry(3, new LineData("2", "2013-01-04T00:00:00+0000", "jhenry")),
+ MapEntry.entry(4, new LineData("1", "2013-01-04T00:00:00+0000", "jhenry")),
+ MapEntry.entry(8, new LineData("3", "2014-01-04T00:00:00+0000", "toto")));
+
+ assertThat(buildResult.getLogs()).doesNotContain("1 files to be analyzed");
+ assertThat(buildResult.getLogs()).doesNotContain("1/1 files analyzed");
+
+ // Now if SCM is explicitely disabled it should clear SCM data on server side
+ buildResult = orchestrator.executeBuild(build.setProperty("sonar.scm.disabled", "true"));
+
+ assertThat(getScmData("sample-scm:src/main/xoo/sample/Sample.xoo")).isEmpty();
+
+ assertThat(buildResult.getLogs()).doesNotContain("1 files to be analyzed");
+ assertThat(buildResult.getLogs()).doesNotContain("1/1 files analyzed");
+ }
+
+ private class LineData {
+
+ final String revision;
+ final Date date;
+ final String author;
+
+ public LineData(String revision, String datetime, String author) throws ParseException {
+ this.revision = revision;
+ this.date = DATETIME_FORMAT.parse(datetime);
+ this.author = author;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return EqualsBuilder.reflectionEquals(this, obj);
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder().append(revision).append(date).append(author).toHashCode();
+ }
+
+ @Override
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this, ToStringStyle.SIMPLE_STYLE);
+ }
+ }
+
+ private Map<Integer, LineData> getScmData(String fileKey) throws ParseException {
+ Map<Integer, LineData> result = new HashMap<>();
+ String json = orchestrator.getServer().adminWsClient().get("api/sources/scm", "key", fileKey);
+ JSONObject obj = (JSONObject) JSONValue.parse(json);
+ JSONArray array = (JSONArray) obj.get("scm");
+ for (Object anArray : array) {
+ JSONArray item = (JSONArray) anArray;
+ String datetime = (String) item.get(2);
+ result.put(((Long) item.get(0)).intValue(), new LineData((String) item.get(3), datetime, (String) item.get(1)));
+ }
+ return result;
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/serverSystem/ClusterTest.java b/tests/src/test/java/org/sonarqube/tests/serverSystem/ClusterTest.java
new file mode 100644
index 00000000000..ff110377844
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/serverSystem/ClusterTest.java
@@ -0,0 +1,215 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.serverSystem;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableMap;
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.server.StartupLogWatcher;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.StringUtils;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.sonarqube.ws.Issues;
+import org.sonarqube.ws.Settings;
+import org.sonarqube.ws.client.rule.SearchWsRequest;
+import org.sonarqube.ws.client.setting.ValuesRequest;
+import util.ItUtils;
+
+import static org.apache.commons.lang3.StringUtils.containsIgnoreCase;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newWsClient;
+
+@Ignore("temporarily ignored")
+public class ClusterTest {
+
+ private static final String CONF_FILE_PATH = "conf/sonar.properties";
+
+ /**
+ * SONAR-7899
+ */
+ @Test
+ public void secondary_nodes_do_not_write_to_datastores_at_startup() throws Exception {
+ // start "startup leader", which creates and populates datastores
+ Orchestrator orchestrator = Orchestrator.builderEnv()
+ .setServerProperty("sonar.cluster.enabled", "true")
+ .setServerProperty("sonar.cluster.name", "secondary_nodes_do_not_write_to_datastores_at_startup")
+ .setServerProperty("sonar.cluster.web.startupLeader", "true")
+ .setServerProperty("sonar.log.level", "TRACE")
+ .addPlugin(ItUtils.xooPlugin())
+ .build();
+ orchestrator.start();
+
+ expectLog(orchestrator, "Cluster enabled (startup leader)");
+ expectWriteOperations(orchestrator, true);
+ // verify that datastores are populated by requesting rules
+ assertThat(newWsClient(orchestrator).rules().search(new SearchWsRequest()).getTotal()).isGreaterThan(0);
+
+ FileUtils.write(orchestrator.getServer().getWebLogs(), "", false);
+ updateSonarPropertiesFile(orchestrator, ImmutableMap.of("sonar.cluster.web.startupLeader", "false"));
+ orchestrator.restartServer();
+
+ expectLog(orchestrator, "Cluster enabled (startup follower)");
+ expectWriteOperations(orchestrator, false);
+
+ orchestrator.stop();
+ }
+
+ @Test
+ public void start_cluster_of_elasticsearch_and_web_nodes() throws IOException {
+ Orchestrator elasticsearch = null;
+ Orchestrator web = null;
+
+ try {
+ ElasticsearchStartupWatcher esWatcher = new ElasticsearchStartupWatcher();
+ elasticsearch = Orchestrator.builderEnv()
+ .setServerProperty("sonar.cluster.enabled", "true")
+ .setServerProperty("sonar.cluster.name", "start_cluster_of_elasticsearch_and_web_nodes")
+ .setServerProperty("sonar.cluster.web.disabled", "true")
+ .setServerProperty("sonar.cluster.ce.disabled", "true")
+ .setStartupLogWatcher(esWatcher)
+ .build();
+ elasticsearch.start();
+ assertThat(esWatcher.port).isGreaterThan(0);
+ assertThat(FileUtils.readFileToString(elasticsearch.getServer().getAppLogs())).doesNotContain("Process[web]");
+
+ web = Orchestrator.builderEnv()
+ .setServerProperty("sonar.cluster.enabled", "true")
+ .setServerProperty("sonar.cluster.name", "start_cluster_of_elasticsearch_and_web_nodes")
+ .setServerProperty("sonar.cluster.web.startupLeader", "true")
+ .setServerProperty("sonar.cluster.search.disabled", "true")
+ .setServerProperty("sonar.cluster.search.hosts", "localhost:" + esWatcher.port)
+ // no need for compute engine in this test. Disable it for faster test.
+ .setServerProperty("sonar.cluster.ce.disabled", "true")
+ // override the default watcher provided by Orchestrator
+ // which waits for Compute Engine to be up
+ .setStartupLogWatcher(log -> log.contains("SonarQube is up"))
+ .build();
+ web.start();
+
+ String coreId = getPropertyValue(web, "sonar.core.id");
+ String startTime = getPropertyValue(web, "sonar.core.startTime");
+
+ assertThat(FileUtils.readFileToString(web.getServer().getAppLogs())).doesNotContain("Process[es]");
+ // call a web service that requires Elasticsearch
+ Issues.SearchWsResponse wsResponse = newWsClient(web).issues().search(new org.sonarqube.ws.client.issue.SearchWsRequest());
+ assertThat(wsResponse.getIssuesCount()).isEqualTo(0);
+
+ web.restartServer();
+
+ // sonar core id must not change after restart
+ assertThat(getPropertyValue(web, "sonar.core.id")).isEqualTo(coreId);
+ // startTime must change at each startup
+ assertThat(getPropertyValue(web, "sonar.core.startTime")).isNotEqualTo(startTime);
+ } finally {
+ if (web != null) {
+ web.stop();
+ }
+ if (elasticsearch != null) {
+ elasticsearch.stop();
+ }
+ }
+ }
+
+ private static String getPropertyValue(Orchestrator web, String property) {
+ Settings.ValuesWsResponse response = ItUtils.newAdminWsClient(web).settings().values(ValuesRequest.builder().setKeys(property).build());
+ List<Settings.Setting> settingsList = response.getSettingsList();
+ if (settingsList.isEmpty()) {
+ return null;
+ }
+ assertThat(settingsList).hasSize(1);
+ return settingsList.iterator().next().getValue();
+ }
+
+ private static class ElasticsearchStartupWatcher implements StartupLogWatcher {
+ private final Pattern pattern = Pattern.compile("Elasticsearch listening on .*:(\\d+)");
+ private int port = -1;
+
+ @Override
+ public boolean isStarted(String log) {
+ Matcher matcher = pattern.matcher(log);
+ if (matcher.find()) {
+ port = Integer.parseInt(matcher.group(1));
+ }
+ return log.contains("Process[es] is up");
+ }
+ }
+
+ private static void expectLog(Orchestrator orchestrator, String expectedLog) throws IOException {
+ File logFile = orchestrator.getServer().getWebLogs();
+ try (Stream<String> lines = Files.lines(logFile.toPath())) {
+ assertThat(lines.anyMatch(s -> StringUtils.containsIgnoreCase(s, expectedLog))).isTrue();
+ }
+ }
+
+ private static void expectWriteOperations(Orchestrator orchestrator, boolean expected) throws IOException {
+ try (Stream<String> lines = Files.lines(orchestrator.getServer().getWebLogs().toPath())) {
+ List<String> writeOperations = lines.filter(ClusterTest::isWriteOperation).collect(Collectors.toList());
+ if (expected) {
+ assertThat(writeOperations).isNotEmpty();
+ } else {
+ assertThat(writeOperations).as("Unexpected write operations: " + Joiner.on('\n').join(writeOperations)).isEmpty();
+
+ }
+ }
+ }
+
+ private static boolean isWriteOperation(String log) {
+ return isDbWriteOperation(log) || isEsWriteOperation(log);
+ }
+
+ private static boolean isDbWriteOperation(String log) {
+ return log.contains("web[][sql]") && (containsIgnoreCase(log, "sql=insert") ||
+ containsIgnoreCase(log, "sql=update") ||
+ containsIgnoreCase(log, "sql=delete") ||
+ containsIgnoreCase(log, "sql=create"));
+ }
+
+ private static boolean isEsWriteOperation(String log) {
+ return log.contains("web[][es]") && (containsIgnoreCase(log, "Create index") ||
+ containsIgnoreCase(log, "Create type") ||
+ containsIgnoreCase(log, "put mapping request") ||
+ containsIgnoreCase(log, "refresh request") ||
+ containsIgnoreCase(log, "index request"));
+ }
+
+ private static void updateSonarPropertiesFile(Orchestrator orchestrator, Map<String, String> props) throws IOException {
+ Properties propsFile = new Properties();
+ try (FileInputStream conf = FileUtils.openInputStream(new File(orchestrator.getServer().getHome(), CONF_FILE_PATH))) {
+ propsFile.load(conf);
+ propsFile.putAll(props);
+ }
+ try (FileOutputStream conf = FileUtils.openOutputStream(new File(orchestrator.getServer().getHome(), CONF_FILE_PATH))) {
+ propsFile.store(conf, "");
+ }
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/serverSystem/HttpHeadersTest.java b/tests/src/test/java/org/sonarqube/tests/serverSystem/HttpHeadersTest.java
new file mode 100644
index 00000000000..7ac7d9ba0cc
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/serverSystem/HttpHeadersTest.java
@@ -0,0 +1,159 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.serverSystem;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category4Suite;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import okhttp3.CacheControl;
+import okhttp3.Response;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import static com.google.common.io.Files.getFileExtension;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.call;
+
+public class HttpHeadersTest {
+
+ @ClassRule
+ public static final Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+
+ private static String JS_HASH;
+
+ @BeforeClass
+ public static void setUp() throws Exception {
+ JS_HASH = getJsHash();
+ }
+
+ @Test
+ public void verify_headers_of_base_url() throws Exception {
+ Response response = call(orchestrator.getServer().getUrl() + "/");
+
+ verifySecurityHeaders(response);
+ verifyContentType(response, "text/html;charset=utf-8");
+
+ // SONAR-6964
+ assertNoCacheInBrowser(response);
+ }
+
+ @Test
+ public void verify_headers_of_ws() throws Exception {
+ Response response = call(orchestrator.getServer().getUrl() + "/api/issues/search");
+
+ verifySecurityHeaders(response);
+ verifyContentType(response, "application/json");
+ assertNoCacheInBrowser(response);
+ }
+
+ @Test
+ public void verify_headers_of_images() throws Exception {
+ Response response = call(orchestrator.getServer().getUrl() + "/images/logo.svg");
+
+ verifySecurityHeaders(response);
+ verifyContentType(response, "image/svg+xml");
+ assertCacheInBrowser(response);
+ }
+
+ @Test
+ public void verify_headers_of_css() throws Exception {
+ Response response = call(orchestrator.getServer().getUrl() + "/css/sonar." + JS_HASH + ".css");
+
+ verifySecurityHeaders(response);
+ verifyContentType(response, "text/css");
+ assertCacheInBrowser(response);
+ }
+
+ @Test
+ public void verify_headers_of_js() throws Exception {
+ Response response = call(orchestrator.getServer().getUrl() + "/js/app." + JS_HASH + ".js");
+
+ verifySecurityHeaders(response);
+ verifyContentType(response, "application/javascript");
+ }
+
+ @Test
+ public void verify_headers_of_images_provided_by_plugins() throws Exception {
+ Response response = call(orchestrator.getServer().getUrl() + "/static/uiextensionsplugin/cute.jpg");
+
+ verifySecurityHeaders(response);
+ verifyContentType(response, "image/jpeg");
+ }
+
+ @Test
+ public void verify_headers_of_js_provided_by_plugins() throws Exception {
+ Response response = call(orchestrator.getServer().getUrl() + "/static/uiextensionsplugin/extension.js");
+
+ verifySecurityHeaders(response);
+ verifyContentType(response, "application/javascript");
+ }
+
+ @Test
+ public void verify_headers_of_html_provided_by_plugins() throws Exception {
+ Response response = call(orchestrator.getServer().getUrl() + "/static/uiextensionsplugin/file.html");
+
+ verifySecurityHeaders(response);
+ verifyContentType(response, "text/html");
+ }
+
+ private static void assertCacheInBrowser(Response httpResponse) {
+ CacheControl cacheControl = httpResponse.cacheControl();
+ assertThat(cacheControl.mustRevalidate()).isFalse();
+ assertThat(cacheControl.noCache()).isFalse();
+ assertThat(cacheControl.noStore()).isFalse();
+ }
+
+ private static void assertNoCacheInBrowser(Response httpResponse) {
+ CacheControl cacheControl = httpResponse.cacheControl();
+ assertThat(cacheControl.mustRevalidate()).isTrue();
+ assertThat(cacheControl.noCache()).isTrue();
+ assertThat(cacheControl.noStore()).isTrue();
+ }
+
+ /**
+ * SONAR-8247
+ */
+ private static void verifySecurityHeaders(Response httpResponse) {
+ assertThat(httpResponse.isSuccessful()).as("Code is %s", httpResponse.code()).isTrue();
+ assertThat(httpResponse.headers().get("X-Frame-Options")).isEqualTo("SAMEORIGIN");
+ assertThat(httpResponse.headers().get("X-XSS-Protection")).isEqualTo("1; mode=block");
+ assertThat(httpResponse.headers().get("X-Content-Type-Options")).isEqualTo("nosniff");
+ }
+
+ private static void verifyContentType(Response httpResponse, String expectedContentType) {
+ assertThat(httpResponse.headers().get("Content-Type")).isEqualTo(expectedContentType);
+ }
+
+ /**
+ * Every JS and CSS files contains a hash between the file name and the extension.
+ */
+ private static String getJsHash() throws IOException {
+ File cssFolder = new File(orchestrator.getServer().getHome(), "web/css");
+ String fileName = Files.list(cssFolder.toPath())
+ .map(path -> path.toFile().getName())
+ .filter(name -> getFileExtension(name).equals("css"))
+ .findFirst()
+ .orElseThrow(() -> new IllegalStateException("sonar.css hasn't been found"));
+ return fileName.replace("sonar.", "").replace(".css", "");
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/serverSystem/LogsTest.java b/tests/src/test/java/org/sonarqube/tests/serverSystem/LogsTest.java
new file mode 100644
index 00000000000..e9eb13360e9
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/serverSystem/LogsTest.java
@@ -0,0 +1,148 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.serverSystem;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category4Suite;
+import java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.input.ReversedLinesFileReader;
+import org.assertj.core.util.Files;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.issue.SearchWsRequest;
+import util.ItUtils;
+
+import static java.lang.String.format;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.projectDir;
+
+public class LogsTest {
+
+ public static final String ACCESS_LOGS_PATTERN = "\"%reqAttribute{ID}\" \"%reqAttribute{LOGIN}\" \"%r\" %s";
+ private static final String PATH = "/called/from/LogsTest";
+
+ @ClassRule
+ public static final Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+
+ @Before
+ public void cleanDatabase() {
+ orchestrator.resetData();
+ }
+
+ /**
+ * SONAR-7581
+ */
+ @Test
+ public void verify_login_in_access_logs() throws Exception {
+ // log "-" for anonymous
+ sendHttpRequest(ItUtils.newWsClient(orchestrator), PATH);
+ assertThat(accessLogsFile()).isFile().exists();
+ verifyLastAccessLogLine("-", PATH, 200);
+
+ sendHttpRequest(ItUtils.newAdminWsClient(orchestrator), PATH);
+ verifyLastAccessLogLine("admin", PATH, 200);
+ }
+
+ @Test
+ public void verify_request_id_in_access_logs() throws IOException {
+ sendHttpRequest(ItUtils.newWsClient(orchestrator), PATH);
+ String lastAccessLog = readLastAccessLog();
+ assertThat(lastAccessLog).doesNotStartWith("\"\"").startsWith("\"");
+ int firstQuote = lastAccessLog.indexOf('"');
+ String requestId = lastAccessLog.substring(firstQuote + 1, lastAccessLog.indexOf('"', firstQuote + 1));
+ assertThat(requestId.length()).isGreaterThanOrEqualTo(20);
+ }
+
+ @Test
+ public void info_log_in_sonar_log_file_when_SQ_is_done_starting() throws IOException {
+ List<String> logs = FileUtils.readLines(orchestrator.getServer().getAppLogs());
+ String sqIsUpMessage = "SonarQube is up";
+ assertThat(logs.stream().filter(str -> str.contains(sqIsUpMessage)).findFirst()).describedAs("message is there").isNotEmpty();
+ assertThat(logs.get(logs.size() - 1)).describedAs("message is the last line of logs").contains(sqIsUpMessage);
+ }
+
+ @Test
+ public void test_ws_change_log_level() throws IOException {
+ generateSqlAndEsLogsInWebAndCe();
+
+ assertThat(logLevelsOf(orchestrator.getServer().getWebLogs())).doesNotContain("DEBUG", "TRACE");
+ assertThat(logLevelsOf(orchestrator.getServer().getCeLogs())).doesNotContain("DEBUG", "TRACE");
+
+ orchestrator.getServer().adminWsClient().post("api/system/change_log_level", "level", "TRACE");
+
+ generateSqlAndEsLogsInWebAndCe();
+
+ // there is hardly DEBUG logs, but we are sure there must be TRACE logs for SQL and ES requests
+ assertThat(logLevelsOf(orchestrator.getServer().getWebLogs())).contains("TRACE");
+ assertThat(logLevelsOf(orchestrator.getServer().getCeLogs())).contains("TRACE");
+
+ // reset log files to empty and level to INFO
+ orchestrator.getServer().adminWsClient().post("api/system/change_log_level", "level", "INFO");
+ FileUtils.write(orchestrator.getServer().getWebLogs(), "");
+ FileUtils.write(orchestrator.getServer().getCeLogs(), "");
+
+ generateSqlAndEsLogsInWebAndCe();
+
+ assertThat(logLevelsOf(orchestrator.getServer().getWebLogs())).doesNotContain("DEBUG", "TRACE");
+ assertThat(logLevelsOf(orchestrator.getServer().getCeLogs())).doesNotContain("DEBUG", "TRACE");
+ }
+
+ private void generateSqlAndEsLogsInWebAndCe() {
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")));
+ ItUtils.newAdminWsClient(orchestrator).issues().search(new SearchWsRequest()
+ .setProjectKeys(Collections.singletonList("sample")));
+ }
+
+ private Collection<String> logLevelsOf(File webLogs) {
+ return Files.linesOf(webLogs, "UTF-8").stream()
+ .filter(str -> str.length() >= 25)
+ .map(str -> str.substring(20, 25))
+ .map(String::trim)
+ .collect(Collectors.toSet());
+ }
+
+ private void verifyLastAccessLogLine(String login, String path, int status) throws IOException {
+ assertThat(readLastAccessLog()).endsWith(format("\"%s\" \"GET %s HTTP/1.1\" %d", login, path, status));
+ }
+
+ private String readLastAccessLog() throws IOException {
+ try (ReversedLinesFileReader tailer = new ReversedLinesFileReader(accessLogsFile())) {
+ return tailer.readLine();
+ }
+ }
+
+ private void sendHttpRequest(WsClient client, String path) {
+ client.wsConnector().call(new GetRequest(path));
+ }
+
+ private File accessLogsFile() {
+ return new File(orchestrator.getServer().getHome(), "logs/access.log");
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/serverSystem/PingTest.java b/tests/src/test/java/org/sonarqube/tests/serverSystem/PingTest.java
new file mode 100644
index 00000000000..b1276875ff7
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/serverSystem/PingTest.java
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.serverSystem;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.http.HttpResponse;
+import org.sonarqube.tests.Category4Suite;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class PingTest {
+
+ @ClassRule
+ public static final Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+
+ @Test
+ public void ping_answers_pong() throws Exception {
+ HttpResponse response = orchestrator.getServer().newHttpCall("/api/system/ping").execute();
+
+ assertThat(response.getBodyAsString()).isEqualTo("pong");
+ assertThat(response.getHeader("Content-Type")).isEqualTo("text/plain");
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/serverSystem/RestartTest.java b/tests/src/test/java/org/sonarqube/tests/serverSystem/RestartTest.java
new file mode 100644
index 00000000000..ee69d37febb
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/serverSystem/RestartTest.java
@@ -0,0 +1,135 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.serverSystem;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.SystemUtils;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.DisableOnDebug;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.WsResponse;
+import org.sonarqube.ws.client.permission.AddUserWsRequest;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.newWsClient;
+
+/**
+ * This class starts a new orchestrator on each test case
+ */
+public class RestartTest {
+
+ private Orchestrator orchestrator;
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+ @Rule
+ public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(900));
+
+ @After
+ public void stop() {
+ if (orchestrator != null) {
+ orchestrator.stop();
+ }
+ }
+
+ @Test
+ public void restart_in_prod_mode_requires_sysadmin_permission_and_restarts() throws Exception {
+ // server classloader locks Jar files on Windows
+ if (!SystemUtils.IS_OS_WINDOWS) {
+ orchestrator = Orchestrator.builderEnv()
+ .setOrchestratorProperty("orchestrator.keepWorkspace", "true")
+ .build();
+ orchestrator.start();
+
+ verifyFailWith403(() -> newWsClient(orchestrator).system().restart());
+
+ createNonSystemAdministrator("john", "doe");
+ verifyFailWith403(() -> ItUtils.newUserWsClient(orchestrator, "john", "doe").system().restart());
+
+ createSystemAdministrator("big", "boss");
+ ItUtils.newUserWsClient(orchestrator, "big", "boss").system().restart();
+ WsResponse wsResponse = newAdminWsClient(orchestrator).wsConnector().call(new GetRequest("/api/system/status")).failIfNotSuccessful();
+ assertThat(wsResponse.content()).contains("RESTARTING");
+
+ // we just wait five seconds, for a lack of a better approach to waiting for the restart process to start in SQ
+ Thread.sleep(5000);
+
+ assertThat(FileUtils.readFileToString(orchestrator.getServer().getWebLogs()))
+ .contains("SonarQube restart requested by big");
+ }
+ }
+
+ /**
+ * SONAR-4843
+ */
+ @Test
+ public void restart_on_dev_mode() throws Exception {
+ // server classloader locks Jar files on Windows
+ if (!SystemUtils.IS_OS_WINDOWS) {
+ orchestrator = Orchestrator.builderEnv()
+ .setServerProperty("sonar.web.dev", "true")
+ .build();
+ orchestrator.start();
+
+ newAdminWsClient(orchestrator).system().restart();
+ assertThat(FileUtils.readFileToString(orchestrator.getServer().getWebLogs()))
+ .contains("Fast restarting WebServer...")
+ .contains("WebServer restarted");
+ }
+ }
+
+ private static void verifyFailWith403(Runnable runnable) {
+ try {
+ runnable.run();
+ fail();
+ } catch (Exception e) {
+ assertThat(e.getMessage()).contains("403");
+ }
+ }
+
+ private void createSystemAdministrator(String login, String password) {
+ WsClient wsClient = newAdminWsClient(orchestrator);
+ createNonSystemAdministrator(wsClient, login, password);
+ wsClient.permissions().addUser(new AddUserWsRequest().setLogin(login).setPermission("admin"));
+ }
+
+ private void createNonSystemAdministrator(String login, String password) {
+ createNonSystemAdministrator(newAdminWsClient(orchestrator), login, password);
+ }
+
+ private static void createNonSystemAdministrator(WsClient wsClient, String login, String password) {
+ wsClient.wsConnector().call(
+ new PostRequest("api/users/create")
+ .setParam("login", login)
+ .setParam("name", login)
+ .setParam("password", password));
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/serverSystem/ServerSystemRestartingOrchestrator.java b/tests/src/test/java/org/sonarqube/tests/serverSystem/ServerSystemRestartingOrchestrator.java
new file mode 100644
index 00000000000..02e7887b6cf
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/serverSystem/ServerSystemRestartingOrchestrator.java
@@ -0,0 +1,111 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.serverSystem;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.locator.FileLocation;
+import java.io.File;
+import java.util.Map;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.WsResponse;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static util.ItUtils.newWsClient;
+
+/**
+ * This class start a new orchestrator on each test case
+ */
+public class ServerSystemRestartingOrchestrator {
+
+ Orchestrator orchestrator;
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @After
+ public void stop() {
+ if (orchestrator != null) {
+ orchestrator.stop();
+ }
+ }
+
+ /**
+ * SONAR-3516
+ */
+ @Test
+ public void check_minimal_sonar_version_at_startup() throws Exception {
+ try {
+ orchestrator = Orchestrator.builderEnv()
+ .addPlugin(FileLocation.of(new File(ServerSystemRestartingOrchestrator.class.getResource("/serverSystem/ServerSystemTest/incompatible-plugin-1.0.jar").toURI())))
+ .build();
+ orchestrator.start();
+ fail();
+ } catch (Exception e) {
+ assertThat(FileUtils.readFileToString(orchestrator.getServer().getWebLogs())).contains(
+ "Plugin incompatible-plugin [incompatibleplugin] requires at least SonarQube 100");
+ }
+ }
+
+ @Test
+ public void support_install_dir_with_whitespaces() throws Exception {
+ String dirName = "target/has space";
+ FileUtils.deleteDirectory(new File(dirName));
+ orchestrator = Orchestrator.builderEnv()
+ .setOrchestratorProperty("orchestrator.workspaceDir", dirName)
+ .build();
+ orchestrator.start();
+
+ WsResponse statusResponse = newWsClient(orchestrator).wsConnector().call(new GetRequest("api/system/status"));
+ Map<String, Object> json = ItUtils.jsonToMap(statusResponse.content());
+ assertThat(json.get("status")).isEqualTo("UP");
+ }
+
+ // SONAR-4748
+ @Test
+ public void should_create_in_temp_folder() throws Exception {
+ orchestrator = Orchestrator.builderEnv()
+ .addPlugin(ItUtils.pluginArtifact("server-plugin"))
+ .setServerProperty("sonar.createTempFiles", "true")
+ .build();
+ orchestrator.start();
+
+ File tempDir = new File(orchestrator.getServer().getHome(), "temp/tmp");
+
+ String logs = FileUtils.readFileToString(orchestrator.getServer().getWebLogs());
+ assertThat(logs).contains("Creating temp directory: " + tempDir.getAbsolutePath() + File.separator + "sonar-it");
+ assertThat(logs).contains("Creating temp file: " + tempDir.getAbsolutePath() + File.separator + "sonar-it");
+
+ // Verify temp folder is created
+ assertThat(new File(tempDir, "sonar-it")).isDirectory().exists();
+
+ orchestrator.stop();
+
+ // Verify temp folder is deleted after shutdown
+ assertThat(new File(tempDir, "sonar-it")).doesNotExist();
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/serverSystem/ServerSystemTest.java b/tests/src/test/java/org/sonarqube/tests/serverSystem/ServerSystemTest.java
new file mode 100644
index 00000000000..ad4adf46519
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/serverSystem/ServerSystemTest.java
@@ -0,0 +1,223 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.serverSystem;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category4Suite;
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+import okhttp3.Response;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.json.simple.JSONValue;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.ws.MediaTypes;
+import org.sonarqube.ws.ServerId.ShowWsResponse;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.WsResponse;
+import org.sonarqube.pageobjects.Navigation;
+import org.sonarqube.pageobjects.ServerIdPage;
+import util.ItUtils;
+import util.user.UserRule;
+
+import static org.apache.commons.lang.StringUtils.startsWithAny;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static util.ItUtils.call;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.newWsClient;
+import static util.selenium.Selenese.runSelenese;
+
+public class ServerSystemTest {
+
+ private static final String ADMIN_USER_LOGIN = "admin-user";
+
+ @ClassRule
+ public static final Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+
+ @Rule
+ public UserRule userRule = UserRule.from(orchestrator);
+
+ @Before
+ public void initAdminUser() {
+ userRule.createAdminUser(ADMIN_USER_LOGIN, ADMIN_USER_LOGIN);
+ }
+
+ @After
+ public void deleteAdminUser() {
+ userRule.resetUsers();
+ }
+
+ @Test
+ public void get_sonarqube_version() {
+ Map<String, Object> json = callStatus();
+
+ String version = (String) json.get("version");
+ if (!startsWithAny(version, new String[] {"6."})) {
+ fail("Bad version: " + version);
+ }
+ }
+
+ @Test
+ public void get_server_status() {
+ Map<String, Object> json = callStatus();
+ assertThat(json.get("status")).isEqualTo("UP");
+ }
+
+ @Test
+ public void generate_server_id() throws IOException {
+ Navigation nav = Navigation.create(orchestrator).openHome().logIn().submitCredentials(ADMIN_USER_LOGIN);
+ String validIpAddress = getValidIpAddress();
+
+ nav.openServerId()
+ .setOrganization("Name with invalid chars like $")
+ .setIpAddress(validIpAddress)
+ .submitForm()
+ .assertError();
+
+ nav.openServerId()
+ .setOrganization("DEMO")
+ .setIpAddress("invalid_address")
+ .submitForm()
+ .assertError();
+
+ ServerIdPage page = nav.openServerId()
+ .setOrganization("DEMO")
+ .setIpAddress(validIpAddress)
+ .submitForm();
+
+ String serverId = page.serverIdInput().val();
+ assertThat(serverId).isNotEmpty();
+ }
+
+ private Map<String, Object> callStatus() {
+ WsResponse statusResponse = newWsClient(orchestrator).wsConnector().call(new GetRequest("api/system/status"));
+ return ItUtils.jsonToMap(statusResponse.content());
+ }
+
+ @Test
+ public void display_system_info() {
+ runSelenese(orchestrator, "/serverSystem/ServerSystemTest/system_info.html");
+ }
+
+ @Test
+ public void download_system_info() throws Exception {
+ waitForComputeEngineToBeUp(orchestrator);
+
+ WsResponse response = newAdminWsClient(orchestrator).wsConnector().call(
+ new GetRequest("api/system/info"));
+
+ assertThat(response.code()).isEqualTo(200);
+
+ assertThat(response.content()).contains(
+ // SONAR-7436 monitor ES and CE
+ "\"Compute Engine Database Connection\":", "\"Compute Engine State\":", "\"Compute Engine Tasks\":",
+ "\"Elasticsearch\":", "\"State\":\"GREEN\"",
+
+ // SONAR-7271 get settings
+ "\"Settings\":", "\"sonar.jdbc.url\":", "\"sonar.path.data\":");
+ }
+
+ private static void waitForComputeEngineToBeUp(Orchestrator orchestrator) throws IOException {
+ for (int i = 0; i < 10_000; i++) {
+ File logs = orchestrator.getServer().getCeLogs();
+ if (FileUtils.readFileToString(logs).contains("Compute Engine is operational")) {
+ return;
+ }
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ // ignored
+ }
+ }
+ throw new IllegalStateException("Compute Engine is not operational");
+ }
+
+ /**
+ * See http://jira.codehaus.org/browse/SONAR-2727
+ */
+ @Test
+ public void display_warnings_when_using_h2() {
+ if (orchestrator.getConfiguration().getString("sonar.jdbc.dialect").equals("h2")) {
+ runSelenese(orchestrator, "/serverSystem/ServerSystemTest/derby-warning.html");
+ }
+ }
+
+ /**
+ * See http://jira.codehaus.org/browse/SONAR-2840
+ */
+ @Test
+ public void hide_jdbc_settings_to_non_admin() {
+ runSelenese(orchestrator, "/serverSystem/ServerSystemTest/hide-jdbc-settings.html");
+ }
+
+ @Test
+ public void http_response_should_be_gzipped() throws IOException {
+ String url = orchestrator.getServer().getUrl() + "/api/rules/search";
+ Response metricsResponse = call(url);
+ assertThat(metricsResponse.isSuccessful()).as("Response code is %s", metricsResponse.code()).isTrue();
+ assertThat(metricsResponse.header("Content-Encoding")).isNull();
+
+ Response homeResponse = call(url, "Accept-Encoding", "gzip, deflate");
+ assertThat(homeResponse.isSuccessful()).as("Response code is %s", metricsResponse.code()).isTrue();
+ assertThat(homeResponse.header("Content-Encoding")).isEqualToIgnoringCase("gzip");
+ }
+
+ /**
+ * SONAR-3962
+ */
+ // TODO should be moved elsewhere
+ @Test
+ public void not_fail_with_url_ending_by_jsp() {
+ orchestrator.executeBuild(SonarScanner.create(ItUtils.projectDir("shared/xoo-sample"))
+ .setProperty("sonar.projectKey", "myproject.jsp"));
+ // Access dashboard
+ runSelenese(orchestrator, "/serverSystem/ServerSystemTest/url_ending_by_jsp.html");
+ }
+
+ /**
+ * SONAR-5197
+ */
+ // TODO should be moved elsewhere
+ @Test
+ public void api_ws_shortcut() throws Exception {
+ Response response = call(orchestrator.getServer().getUrl() + "/api");
+ assertThat(response.isSuccessful()).as("Response code is %s", response.code()).isTrue();
+ String json = IOUtils.toString(response.body().byteStream());
+ Map jsonAsMap = (Map) JSONValue.parse(json);
+ assertThat(jsonAsMap.get("webServices")).isNotNull();
+ }
+
+ private String getValidIpAddress() throws IOException {
+ WsClient adminWsClient = newAdminWsClient(orchestrator);
+ ShowWsResponse response = ShowWsResponse.parseFrom(adminWsClient.wsConnector().call(
+ new GetRequest("api/server_id/show").setMediaType(MediaTypes.PROTOBUF)).contentStream());
+ assertThat(response.getValidIpAddressesCount()).isGreaterThan(0);
+ return response.getValidIpAddresses(0);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/settings/DeprecatedPropertiesWsTest.java b/tests/src/test/java/org/sonarqube/tests/settings/DeprecatedPropertiesWsTest.java
new file mode 100644
index 00000000000..4a0bd037333
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/settings/DeprecatedPropertiesWsTest.java
@@ -0,0 +1,417 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.settings;
+
+import com.google.common.base.Throwables;
+import com.google.gson.Gson;
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category1Suite;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import okhttp3.Credentials;
+import okhttp3.FormBody;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.WsResponse;
+import org.sonarqube.ws.client.setting.SetRequest;
+import org.sonarqube.ws.client.setting.SettingsService;
+import util.user.UserRule;
+
+import static java.net.URLEncoder.encode;
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.newUserWsClient;
+import static util.ItUtils.newWsClient;
+import static util.ItUtils.resetSettings;
+import static util.ItUtils.runProjectAnalysis;
+
+public class DeprecatedPropertiesWsTest {
+
+ private final static String PROJECT_KEY = "com.sonarsource.it.samples:multi-modules-sample";
+ private static final String MODULE_KEY = "com.sonarsource.it.samples:multi-modules-sample:module_a";
+ private static final String SUB_MODULE_KEY = "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1";
+
+ private static final String PROJECT_SETTING_KEY = "sonar.dbcleaner.hoursBeforeKeepingOnlyOneSnapshotByDay";
+
+ private static String USER_LOGIN = "john";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ @ClassRule
+ public static UserRule userRule = UserRule.from(orchestrator);
+
+ static WsClient adminWsClient;
+ static WsClient userWsClient;
+ static WsClient anonymousWsClient;
+
+ static SettingsService adminSettingsService;
+
+ @BeforeClass
+ public static void init() throws Exception {
+ orchestrator.resetData();
+ userRule.createUser(USER_LOGIN, "password");
+ adminWsClient = newAdminWsClient(orchestrator);
+ userWsClient = newUserWsClient(orchestrator, USER_LOGIN, "password");
+ anonymousWsClient = newWsClient(orchestrator);
+ adminSettingsService = newAdminWsClient(orchestrator).settings();
+ runProjectAnalysis(orchestrator, "shared/xoo-multi-modules-sample");
+ }
+
+ @AfterClass
+ public static void resetAfterClass() throws Exception {
+ doResetSettings();
+ userRule.deactivateUsers(USER_LOGIN);
+ }
+
+ @Before
+ public void resetBefore() throws Exception {
+ doResetSettings();
+ }
+
+ private static void doResetSettings() {
+ resetSettings(orchestrator, null, "some-property", "custom-property", "int", "multi", "boolean", "hidden", "not_defined", "setting.secured", "setting.license.secured", "list",
+ "undefined");
+ resetSettings(orchestrator, PROJECT_KEY, PROJECT_SETTING_KEY, "sonar.coverage.exclusions", "project.setting");
+ }
+
+ @Test
+ public void get_default_global_value() throws Exception {
+ assertThat(getProperty("some-property", null).getValue()).isEqualTo("aDefaultValue");
+ }
+
+ @Test
+ public void get_global_value() throws Exception {
+ setProperty("some-property", "value", null);
+
+ assertThat(getProperty("some-property", null).getValue()).isEqualTo("value");
+ }
+
+ @Test
+ public void get_multi_values() throws Exception {
+ setProperty("multi", asList("value1", "value2", "value,3"), null);
+
+ Properties.Property setting = getProperty("multi", null);
+ assertThat(setting.getValue()).isEqualTo("value1,value2,value%2C3");
+ assertThat(setting.getValues()).containsOnly("value1", "value2", "value,3");
+ }
+
+ @Test
+ public void get_hidden_setting() throws Exception {
+ setProperty("hidden", "value", null);
+
+ assertThat(getProperty("hidden", null).getValue()).isEqualTo("value");
+ }
+
+ @Test
+ public void get_secured_setting() throws Exception {
+ setProperty("setting.secured", "value", null);
+
+ assertThat(getProperty("setting.secured", null).getValue()).isEqualTo("value");
+ }
+
+ @Test
+ public void get_license_setting() throws Exception {
+ setProperty("setting.license.secured", "value", null);
+
+ assertThat(getProperty("setting.license.secured", null).getValue()).isEqualTo("value");
+ }
+
+ @Test
+ public void get_not_defined_setting() throws Exception {
+ setProperty("not_defined", "value", null);
+
+ assertThat(getProperty("not_defined", null).getValue()).isEqualTo("value");
+ }
+
+ @Test
+ public void secured_setting_not_returned_to_not_admin() throws Exception {
+ setProperty("setting.secured", "value", null);
+
+ // Admin can see the secured setting
+ assertThat(getProperties(null)).extracting(Properties.Property::getKey).contains("setting.secured");
+
+ // Not admin cannot see the secured setting
+ assertThat(getProperties(userWsClient, null)).extracting(Properties.Property::getKey).doesNotContain("setting.secured");
+ assertThat(getProperties(anonymousWsClient, null)).extracting(Properties.Property::getKey).doesNotContain("setting.secured");
+ }
+
+ @Test
+ public void license_setting_not_returned_to_not_logged() throws Exception {
+ setProperty("setting.license.secured", "value", null);
+
+ // Admin and user can see the license setting
+ assertThat(getProperties(null)).extracting(Properties.Property::getKey).contains("setting.license.secured");
+ assertThat(getProperties(userWsClient, null)).extracting(Properties.Property::getKey).contains("setting.license.secured");
+
+ // Anonymous cannot see the license setting
+ assertThat(getProperties(anonymousWsClient, null)).extracting(Properties.Property::getKey).doesNotContain("setting.license.secured");
+ }
+
+ @Test
+ public void get_all_global_settings() throws Exception {
+ List<Properties.Property> properties = getProperties(null);
+ assertThat(properties).isNotEmpty();
+ assertThat(properties).extracting("key")
+ .contains("sonar.core.id", "some-property", "boolean")
+ .doesNotContain("hidden");
+ }
+
+ @Test
+ public void get_default_component_value() throws Exception {
+ // Check default value is returned
+ assertThat(getProperty(PROJECT_SETTING_KEY, PROJECT_KEY).getValue()).isEqualTo("24");
+ assertThat(getProperty(PROJECT_SETTING_KEY, MODULE_KEY).getValue()).isEqualTo("24");
+ assertThat(getProperty(PROJECT_SETTING_KEY, SUB_MODULE_KEY).getValue()).isEqualTo("24");
+ }
+
+ @Test
+ public void get_global_component_value() throws Exception {
+ // Check global value is returned
+ setProperty(PROJECT_SETTING_KEY, "30", null);
+ assertThat(getProperty(PROJECT_SETTING_KEY, PROJECT_KEY).getValue()).isEqualTo("30");
+ assertThat(getProperty(PROJECT_SETTING_KEY, MODULE_KEY).getValue()).isEqualTo("30");
+ assertThat(getProperty(PROJECT_SETTING_KEY, SUB_MODULE_KEY).getValue()).isEqualTo("30");
+ }
+
+ @Test
+ public void get_component_value() throws Exception {
+ setProperty("sonar.coverage.exclusions", asList("file"), PROJECT_KEY);
+
+ assertThat(getProperty("sonar.coverage.exclusions", PROJECT_KEY).getValue()).isEqualTo("file");
+ }
+
+ @Test
+ public void get_global_value_when_component_is_unknown() throws Exception {
+ setProperty("some-property", "value", null);
+
+ assertThat(getProperty("some-property", PROJECT_KEY).getValue()).isEqualTo("value");
+ }
+
+ @Test
+ public void get_all_component_settings() throws Exception {
+ List<Properties.Property> properties = getProperties(PROJECT_KEY);
+ assertThat(properties).isNotEmpty();
+ assertThat(properties).extracting("key")
+ .contains("sonar.dbcleaner.cleanDirectory", "sonar.dbcleaner.weeksBeforeDeletingAllSnapshots")
+ .doesNotContain("hidden");
+ }
+
+ @Test
+ public void get_global_value_using_id_parameter() throws Exception {
+ setProperty("some-property", "value", null);
+
+ assertThat(getProperty(adminWsClient, "some-property", null, true).getValue()).isEqualTo("value");
+ }
+
+ @Test
+ public void put_property() throws Exception {
+ putProperty("some-property", "some-value", null, false);
+
+ assertThat(getProperty("some-property", null).getValue()).isEqualTo("some-value");
+ }
+
+ @Test
+ public void put_property_using_id_parameter() throws Exception {
+ putProperty("some-property", "some-value", null, true);
+
+ assertThat(getProperty("some-property", null).getValue()).isEqualTo("some-value");
+ }
+
+ @Test
+ public void put_property_on_project() throws Exception {
+ putProperty("project.setting", "some-value", PROJECT_KEY, false);
+
+ assertThat(getProperty("project.setting", PROJECT_KEY).getValue()).isEqualTo("some-value");
+ }
+
+ @Test
+ public void put_property_for_undefined_setting() throws Exception {
+ putProperty("undefined", "some-value", null, false);
+
+ assertThat(getProperty("undefined", null).getValue()).isEqualTo("some-value");
+ }
+
+ @Test
+ public void put_property_multi_values() throws Exception {
+ putProperty("multi", "value1,value2,value3", null, false);
+
+ Properties.Property setting = getProperty("multi", null);
+ assertThat(setting.getValue()).isEqualTo("value1,value2,value3");
+ assertThat(setting.getValues()).containsOnly("value1", "value2", "value3");
+ }
+
+ @Test
+ public void fail_with_error_400_when_put_property_without_id() throws Exception {
+ Response response = putProperty("", "some-value", null, false);
+ assertThat(response.code()).isEqualTo(400);
+ }
+
+ @Test
+ public void delete_property() throws Exception {
+ setProperty("custom-property", "value", null);
+
+ deleteProperty("custom-property", null, false);
+
+ assertThat(getProperty("custom-property", null)).isNull();
+ }
+
+ @Test
+ public void delete_property_using_id_parameter() throws Exception {
+ setProperty("custom-property", "value", null);
+
+ deleteProperty("custom-property", null, true);
+
+ assertThat(getProperty("custom-property", null)).isNull();
+ }
+
+ @Test
+ public void delete_property_on_project() throws Exception {
+ setProperty("project.setting", "value", PROJECT_KEY);
+
+ deleteProperty("project.setting", PROJECT_KEY, false);
+
+ assertThat(getProperty("project.setting", PROJECT_KEY)).isNull();
+ }
+
+ private static void setProperty(String key, String value, @Nullable String componentKey) {
+ adminSettingsService.set(SetRequest.builder().setKey(key).setValue(value).setComponent(componentKey).build());
+ }
+
+ private static void setProperty(String key, List<String> values, @Nullable String componentKey) {
+ adminSettingsService.set(SetRequest.builder().setKey(key).setValues(values).setComponent(componentKey).build());
+ }
+
+ private static List<Properties.Property> getProperties(@Nullable String componentKey) {
+ return getProperties(adminWsClient, componentKey);
+ }
+
+ private static List<Properties.Property> getProperties(WsClient wsClient, @Nullable String componentKey) {
+ WsResponse response = wsClient.wsConnector()
+ .call(new GetRequest("api/properties")
+ .setParam("resource", componentKey))
+ .failIfNotSuccessful();
+ return asList(Properties.parse(response.content()));
+ }
+
+ private static Properties.Property getProperty(String key, @Nullable String componentKey) throws UnsupportedEncodingException {
+ return getProperty(adminWsClient, key, componentKey, false);
+ }
+
+ @CheckForNull
+ private static Properties.Property getProperty(WsClient wsClient, String key, @Nullable String componentKey, boolean useIdParameter) throws UnsupportedEncodingException {
+ GetRequest getRequest = useIdParameter ? new GetRequest("api/properties").setParam("id", encode(key, "UTF-8")).setParam("resource", componentKey)
+ : new GetRequest("api/properties/" + encode(key, "UTF-8")).setParam("resource", componentKey);
+ WsResponse response = wsClient.wsConnector()
+ .call(getRequest)
+ .failIfNotSuccessful();
+ Properties.Property[] properties = Properties.parse(response.content());
+ return Arrays.stream(properties).findFirst().orElseGet(() -> null);
+ }
+
+ private static Response putProperty(String key, String value, @Nullable String componentKey, boolean useIdParameter) throws UnsupportedEncodingException {
+ String url = useIdParameter ? orchestrator.getServer().getUrl() + "/api/properties?id=" + encode(key, "UTF-8") + "&value=" + value
+ : orchestrator.getServer().getUrl() + "/api/properties/" + encode(key, "UTF-8") + "?value=" + value;
+ url += componentKey != null ? "&resource=" + componentKey : "";
+ return call(new Request.Builder()
+ .put(new FormBody.Builder().build())
+ .url(url));
+ }
+
+ private static Response deleteProperty(String key, @Nullable String componentKey, boolean useIdParameter) throws UnsupportedEncodingException {
+ String url = useIdParameter ? orchestrator.getServer().getUrl() + "/api/properties?id=" + encode(key, "UTF-8")
+ : orchestrator.getServer().getUrl() + "/api/properties/" + encode(key, "UTF-8");
+ url += componentKey != null ? "?resource=" + componentKey : "";
+ return call(new Request.Builder()
+ .delete(new FormBody.Builder().build())
+ .url(url));
+ }
+
+ private static Response call(Request.Builder requestBuilder) {
+ try {
+ requestBuilder.header("Authorization", Credentials.basic("admin", "admin"));
+ return new OkHttpClient.Builder()
+ .connectTimeout(30, TimeUnit.SECONDS)
+ .readTimeout(30, TimeUnit.SECONDS)
+ .build()
+ .newCall(requestBuilder.build())
+ .execute();
+ } catch (IOException e) {
+ throw Throwables.propagate(e);
+ }
+ }
+
+ public static class Properties {
+
+ private List<Property> properties;
+
+ private Properties(List<Property> properties) {
+ this.properties = properties;
+ }
+
+ public List<Property> getProperties() {
+ return properties;
+ }
+
+ public static Property[] parse(String json) {
+ Gson gson = new Gson();
+ return gson.fromJson(json, Property[].class);
+ }
+
+ public static class Property {
+ private final String key;
+ private final String value;
+ private final String[] values;
+
+ private Property(String key, String value, String[] values) {
+ this.key = key;
+ this.value = value;
+ this.values = values;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public String[] getValues() {
+ return values;
+ }
+ }
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/settings/EmailsTest.java b/tests/src/test/java/org/sonarqube/tests/settings/EmailsTest.java
new file mode 100644
index 00000000000..86967abb6c2
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/settings/EmailsTest.java
@@ -0,0 +1,140 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.settings;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category1Suite;
+import java.util.Iterator;
+import javax.annotation.Nullable;
+import javax.mail.internet.MimeMessage;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.Settings;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.setting.SettingsService;
+import org.sonarqube.ws.client.setting.ValuesRequest;
+import org.subethamail.wiser.Wiser;
+import org.subethamail.wiser.WiserMessage;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.groups.Tuple.tuple;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.resetEmailSettings;
+import static util.ItUtils.setServerProperty;
+
+public class EmailsTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ static Wiser SMTP_SERVER;
+ static WsClient ADMIN_WS_CLIENT;
+ static SettingsService SETTINGS;
+
+ @BeforeClass
+ public static void before() throws Exception {
+ ADMIN_WS_CLIENT = newAdminWsClient(orchestrator);
+ SETTINGS = ADMIN_WS_CLIENT.settings();
+
+ SMTP_SERVER = new Wiser(0);
+ SMTP_SERVER.start();
+ System.out.println("SMTP Server port: " + SMTP_SERVER.getServer().getPort());
+ }
+
+ @AfterClass
+ public static void stop() {
+ if (SMTP_SERVER != null) {
+ SMTP_SERVER.stop();
+ }
+ resetEmailSettings(orchestrator);
+ }
+
+ @Before
+ public void prepare() {
+ orchestrator.resetData();
+ SMTP_SERVER.getMessages().clear();
+ resetEmailSettings(orchestrator);
+ }
+
+ @Test
+ public void update_email_settings() throws Exception {
+ updateEmailSettings("localhost", "42", "noreply@email.com", "[EMAIL]", "ssl", "john", "123456");
+
+ Settings.ValuesWsResponse response = SETTINGS.values(ValuesRequest.builder()
+ .setKeys("email.smtp_host.secured", "email.smtp_port.secured", "email.smtp_secure_connection.secured", "email.smtp_username.secured", "email.smtp_password.secured",
+ "email.from", "email.prefix")
+ .build());
+
+ assertThat(response.getSettingsList()).extracting(Settings.Setting::getKey, Settings.Setting::getValue)
+ .containsOnly(
+ tuple("email.smtp_host.secured", "localhost"),
+ tuple("email.smtp_port.secured", "42"),
+ tuple("email.smtp_secure_connection.secured", "ssl"),
+ tuple("email.smtp_username.secured", "john"),
+ tuple("email.smtp_password.secured", "123456"),
+ tuple("email.from", "noreply@email.com"),
+ tuple("email.prefix", "[EMAIL]"));
+ }
+
+ @Test
+ public void send_test_email() throws Exception {
+ updateEmailSettings("localhost", Integer.toString(SMTP_SERVER.getServer().getPort()), null, null, null, null, null);
+
+ sendEmail("test@example.org", "Test Message from SonarQube", "This is a test message from SonarQube");
+
+ // We need to wait until all notifications will be delivered
+ waitUntilAllNotificationsAreDelivered();
+ Iterator<WiserMessage> emails = SMTP_SERVER.getMessages().iterator();
+ MimeMessage message = emails.next().getMimeMessage();
+ assertThat(message.getHeader("To", null)).isEqualTo("<test@example.org>");
+ assertThat(message.getSubject()).contains("Test Message from SonarQube");
+ assertThat((String) message.getContent()).contains("This is a test message from SonarQube");
+ assertThat(emails.hasNext()).isFalse();
+ }
+
+ private static void waitUntilAllNotificationsAreDelivered() throws InterruptedException {
+ Thread.sleep(10000);
+ }
+
+ private static void updateEmailSettings(@Nullable String host, @Nullable String port, @Nullable String from, @Nullable String prefix, @Nullable String secure,
+ @Nullable String username, @Nullable String password) {
+ setServerProperty(orchestrator, "email.smtp_host.secured", host);
+ setServerProperty(orchestrator, "email.smtp_port.secured", port);
+ setServerProperty(orchestrator, "email.smtp_secure_connection.secured", secure);
+ setServerProperty(orchestrator, "email.smtp_username.secured", username);
+ setServerProperty(orchestrator, "email.smtp_password.secured", password);
+ setServerProperty(orchestrator, "email.from", from);
+ setServerProperty(orchestrator, "email.prefix", prefix);
+ }
+
+ private static void sendEmail(String to, String subject, String message) {
+ ADMIN_WS_CLIENT.wsConnector().call(
+ new PostRequest("/api/emails/send")
+ .setParam("to", to)
+ .setParam("subject", subject)
+ .setParam("message", message))
+ .failIfNotSuccessful();
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/settings/LicensesPageTest.java b/tests/src/test/java/org/sonarqube/tests/settings/LicensesPageTest.java
new file mode 100644
index 00000000000..5f0ebe0078d
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/settings/LicensesPageTest.java
@@ -0,0 +1,93 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.settings;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.ws.Settings.ValuesWsResponse;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.setting.ValuesRequest;
+import org.sonarqube.pageobjects.Navigation;
+import org.sonarqube.pageobjects.licenses.LicenseItem;
+import org.sonarqube.pageobjects.licenses.LicensesPage;
+import util.user.UserRule;
+
+import static com.codeborne.selenide.Condition.text;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.pluginArtifact;
+
+public class LicensesPageTest {
+ private static Orchestrator orchestrator;
+ private static WsClient wsClient;
+
+ @Rule
+ public UserRule userRule = UserRule.from(orchestrator);
+
+ private String adminUser;
+
+ @BeforeClass
+ public static void start() {
+ orchestrator = Orchestrator.builderEnv()
+ .addPlugin(pluginArtifact("license-plugin"))
+ .build();
+ orchestrator.start();
+
+ wsClient = newAdminWsClient(orchestrator);
+ }
+
+ @AfterClass
+ public static void stop() {
+ if (orchestrator != null) {
+ orchestrator.stop();
+ }
+ }
+
+ @Before
+ public void before() {
+ adminUser = userRule.createAdminUser();
+ }
+
+ @Test
+ public void display_licenses() {
+ LicensesPage page = Navigation.create(orchestrator).logIn().submitCredentials(adminUser).openLicenses();
+
+ page.getLicenses().shouldHaveSize(2);
+ page.getLicensesAsItems().get(0).getName().shouldHave(text("Typed property"));
+ page.getLicensesAsItems().get(1).getName().shouldHave(text("Property without license type"));
+ }
+
+ @Test
+ public void change_licenses() {
+ String EXAMPLE_LICENSE = "TmFtZTogRGV2ZWxvcHBlcnMKUGx1Z2luOiBhdXRvY29udHJvbApFeHBpcmVzOiAyMDEyLTA0LTAxCktleTogNjI5N2MxMzEwYzg2NDZiZTE5MDU1MWE4ZmZmYzk1OTBmYzEyYTIyMgo=";
+
+ LicensesPage page = Navigation.create(orchestrator).logIn().submitCredentials(adminUser).openLicenses();
+ LicenseItem licenseItem = page.getLicenseByKey("typed.license.secured");
+ licenseItem.setLicense(EXAMPLE_LICENSE);
+
+ ValuesWsResponse response = wsClient.settings()
+ .values(ValuesRequest.builder().setKeys("typed.license.secured").build());
+ assertThat(response.getSettings(0).getValue()).isEqualTo(EXAMPLE_LICENSE);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/settings/PropertySetsTest.java b/tests/src/test/java/org/sonarqube/tests/settings/PropertySetsTest.java
new file mode 100644
index 00000000000..d30b68168e3
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/settings/PropertySetsTest.java
@@ -0,0 +1,153 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.settings;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category1Suite;
+import java.io.UnsupportedEncodingException;
+import java.util.List;
+import java.util.Map;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.ws.Settings;
+import org.sonarqube.ws.client.setting.SetRequest;
+import org.sonarqube.ws.client.setting.SettingsService;
+import org.sonarqube.ws.client.setting.ValuesRequest;
+import org.sonarqube.pageobjects.Navigation;
+import org.sonarqube.pageobjects.settings.SettingsPage;
+import util.user.UserRule;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.entry;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.resetSettings;
+
+public class PropertySetsTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ @Rule
+ public UserRule userRule = UserRule.from(orchestrator);
+
+ private Navigation nav = Navigation.create(orchestrator);
+
+ static SettingsService SETTINGS;
+ private String adminUser;
+
+ @BeforeClass
+ public static void initSettingsService() throws Exception {
+ SETTINGS = newAdminWsClient(orchestrator).settings();
+ }
+
+ @Before
+ public void before() {
+ adminUser = userRule.createAdminUser();
+ }
+
+ @After
+ public void reset_settings() throws Exception {
+ resetSettings(orchestrator, null, "sonar.demo", "sonar.autogenerated", "sonar.test.jira.servers");
+ }
+
+ @Test
+ public void support_property_sets() throws UnsupportedEncodingException {
+ SettingsPage page = nav.logIn().submitCredentials(adminUser).openSettings(null).openCategory("DEV")
+ .assertSettingDisplayed("sonar.test.jira.servers");
+
+ page.getPropertySetInput("sonar.test.jira.servers")
+ .setFieldValue("key", "jira1")
+ .setFieldValue("url", "http://jira")
+ .setFieldValue("port", "12345")
+ .save();
+
+ assertPropertySet("sonar.test.jira.servers", asList(
+ entry("key", "jira1"),
+ entry("url", "http://jira"),
+ entry("port", "12345")));
+ }
+
+ @Test
+ public void support_property_sets_with_auto_generated_keys() throws UnsupportedEncodingException {
+ SettingsPage page = nav.logIn().submitCredentials(adminUser).openSettings(null).openCategory("DEV")
+ .assertSettingDisplayed("sonar.autogenerated");
+
+ page.getPropertySetInput("sonar.autogenerated")
+ .setFieldValue(0, "value", "FIRST")
+ .setFieldValue(1, "value", "SECOND")
+ .setFieldValue(2, "value", "THIRD")
+ .save();
+
+ assertPropertySet("sonar.autogenerated",
+ asList(entry("value", "FIRST")),
+ asList(entry("value", "SECOND")),
+ asList(entry("value", "THIRD")));
+ }
+
+ @Test
+ public void edit_property_set() {
+ SETTINGS.set(SetRequest.builder()
+ .setKey("sonar.test.jira.servers")
+ .setFieldValues(newArrayList(
+ "{\"key\":\"jira1\", \"url\":\"http://jira1\", \"port\":\"12345\"}",
+ "{\"key\":\"jira2\", \"url\":\"http://jira2\", \"port\":\"54321\"}"))
+ .build());
+
+ assertPropertySet("sonar.test.jira.servers",
+ asList(entry("key", "jira1"), entry("url", "http://jira1"), entry("port", "12345")),
+ asList(entry("key", "jira2"), entry("url", "http://jira2"), entry("port", "54321")));
+ }
+
+ @Test
+ public void delete_property_set() throws Exception {
+ SETTINGS.set(SetRequest.builder()
+ .setKey("sonar.test.jira.servers")
+ .setFieldValues(newArrayList("{\"url\":\"http://jira1\"}", "{\"port\":\"12345\"}"))
+ .build());
+
+ resetSettings(orchestrator, null, "sonar.test.jira.servers");
+
+ assertThat(SETTINGS.values(ValuesRequest.builder().setKeys("sonar.test.jira.servers").build()).getSettingsList()).isEmpty();
+ }
+
+ private void assertPropertySet(String baseSettingKey, List<Map.Entry<String, String>>... fieldsValues) {
+ Settings.Setting setting = getSetting(baseSettingKey);
+ assertThat(setting.getFieldValues().getFieldValuesList()).hasSize(fieldsValues.length);
+ int index = 0;
+ for (Settings.FieldValues.Value fieldValue : setting.getFieldValues().getFieldValuesList()) {
+ assertThat(fieldValue.getValue()).containsOnly(fieldsValues[index].toArray(new Map.Entry[] {}));
+ index++;
+ }
+ }
+
+ private Settings.Setting getSetting(String key) {
+ Settings.ValuesWsResponse response = SETTINGS.values(ValuesRequest.builder().setKeys(key).build());
+ List<Settings.Setting> settings = response.getSettingsList();
+ assertThat(settings).hasSize(1);
+ return settings.get(0);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/settings/SettingsTest.java b/tests/src/test/java/org/sonarqube/tests/settings/SettingsTest.java
new file mode 100644
index 00000000000..de7d213db2b
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/settings/SettingsTest.java
@@ -0,0 +1,208 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.settings;
+
+import com.google.common.collect.ImmutableMap;
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category1Suite;
+import java.io.IOException;
+import java.util.List;
+import javax.annotation.CheckForNull;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.Settings;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.permission.AddGroupWsRequest;
+import org.sonarqube.ws.client.permission.AddUserWsRequest;
+import org.sonarqube.ws.client.permission.RemoveGroupWsRequest;
+import org.sonarqube.ws.client.setting.ResetRequest;
+import org.sonarqube.ws.client.setting.SetRequest;
+import org.sonarqube.ws.client.setting.SettingsService;
+import org.sonarqube.ws.client.setting.ValuesRequest;
+import util.user.UserRule;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.sonarqube.ws.Settings.Setting;
+import static org.sonarqube.ws.Settings.ValuesWsResponse;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.newUserWsClient;
+import static util.ItUtils.newWsClient;
+import static util.ItUtils.resetSettings;
+
+public class SettingsTest {
+
+ /**
+ * This setting is defined by server-plugin
+ */
+ private final static String PLUGIN_SETTING_KEY = "some-property";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ @ClassRule
+ public static UserRule userRule = UserRule.from(orchestrator);
+
+ private static WsClient adminWsClient;
+ private static SettingsService anonymousSettingsService;
+ private static SettingsService userSettingsService;
+ private static SettingsService scanSettingsService;
+ private static SettingsService adminSettingsService;
+
+ @BeforeClass
+ public static void initSettingsService() throws Exception {
+ userRule.createUser("setting-user", "setting-user");
+ userRule.createUser("scanner-user", "scanner-user");
+ adminWsClient = newAdminWsClient(orchestrator);
+ // Remove 'Execute Analysis' permission from anyone
+ adminWsClient.permissions().removeGroup(new RemoveGroupWsRequest().setGroupName("anyone").setPermission("scan"));
+
+ // Anonymous user, without 'Execute Analysis' permission
+ anonymousSettingsService = newWsClient(orchestrator).settings();
+
+ // Authenticated user, without 'Execute Analysis' permission
+ userSettingsService = newUserWsClient(orchestrator, "setting-user", "setting-user").settings();
+
+ // User with 'Execute Analysis' permission
+ adminWsClient.permissions().addUser(new AddUserWsRequest().setLogin("scanner-user").setPermission("scan"));
+ scanSettingsService = newUserWsClient(orchestrator, "scanner-user", "scanner-user").settings();
+
+ // User with 'Administer System' permission but without 'Execute Analysis' permission
+ adminSettingsService = adminWsClient.settings();
+ }
+
+ @AfterClass
+ public static void tearDown() throws Exception {
+ userRule.deactivateUsers("setting-user", "scanner-user");
+ // Restore 'Execute Analysis' permission to anyone
+ adminWsClient.permissions().addGroup(new AddGroupWsRequest().setGroupName("anyone").setPermission("scan"));
+ }
+
+ @After
+ public void reset_settings() throws Exception {
+ resetSettings(orchestrator, null, PLUGIN_SETTING_KEY, "globalPropertyChange.received", "hidden", "setting.secured", "setting.license.secured");
+ }
+
+ /**
+ * SONAR-3320
+ */
+ @Test
+ public void global_property_change_extension_point() throws IOException {
+ adminSettingsService.set(SetRequest.builder().setKey("globalPropertyChange.received").setValue("NEWVALUE").build());
+ assertThat(FileUtils.readFileToString(orchestrator.getServer().getWebLogs()))
+ .contains("Received change: [key=globalPropertyChange.received, newValue=NEWVALUE]");
+ }
+
+ @Test
+ public void get_default_value() {
+ Setting setting = getSetting(PLUGIN_SETTING_KEY, anonymousSettingsService);
+ assertThat(setting.getValue()).isEqualTo("aDefaultValue");
+ assertThat(setting.getInherited()).isTrue();
+ }
+
+ @Test
+ public void set_setting() {
+ adminSettingsService.set(SetRequest.builder().setKey(PLUGIN_SETTING_KEY).setValue("some value").build());
+
+ String value = getSetting(PLUGIN_SETTING_KEY, anonymousSettingsService).getValue();
+ assertThat(value).isEqualTo("some value");
+ }
+
+ @Test
+ public void remove_setting() {
+ adminSettingsService.set(SetRequest.builder().setKey(PLUGIN_SETTING_KEY).setValue("some value").build());
+ adminSettingsService.set(SetRequest.builder().setKey("sonar.links.ci").setValue("http://localhost").build());
+
+ adminSettingsService.reset(ResetRequest.builder().setKeys(PLUGIN_SETTING_KEY, "sonar.links.ci").build());
+ assertThat(getSetting(PLUGIN_SETTING_KEY, anonymousSettingsService).getValue()).isEqualTo("aDefaultValue");
+ assertThat(getSetting("sonar.links.ci", anonymousSettingsService)).isNull();
+ }
+
+ @Test
+ public void hidden_setting() {
+ adminSettingsService.set(SetRequest.builder().setKey("hidden").setValue("test").build());
+ assertThat(getSetting("hidden", anonymousSettingsService).getValue()).isEqualTo("test");
+ }
+
+ @Test
+ public void secured_setting() {
+ adminSettingsService.set(SetRequest.builder().setKey("setting.secured").setValue("test").build());
+ assertThat(getSetting("setting.secured", anonymousSettingsService)).isNull();
+// assertThat(getSetting("setting.secured", userSettingsService)).isNull();
+ assertThat(getSetting("setting.secured", scanSettingsService).getValue()).isEqualTo("test");
+ assertThat(getSetting("setting.secured", adminSettingsService).getValue()).isEqualTo("test");
+ }
+
+ @Test
+ public void license_setting() {
+ adminSettingsService.set(SetRequest.builder().setKey("setting.license.secured").setValue("test").build());
+ assertThat(getSetting("setting.license.secured", anonymousSettingsService)).isNull();
+ assertThat(getSetting("setting.license.secured", userSettingsService).getValue()).isEqualTo("test");
+ assertThat(getSetting("setting.license.secured", scanSettingsService).getValue()).isEqualTo("test");
+ assertThat(getSetting("setting.license.secured", adminSettingsService).getValue()).isEqualTo("test");
+ }
+
+ @Test
+ public void multi_values_setting() throws Exception {
+ adminSettingsService.set(SetRequest.builder().setKey("multi").setValues(asList("value1", "value2", "value3")).build());
+ assertThat(getSetting("multi", anonymousSettingsService).getValues().getValuesList()).containsOnly("value1", "value2", "value3");
+ }
+
+ @Test
+ public void property_set_setting() throws Exception {
+ adminSettingsService.set(SetRequest.builder().setKey("sonar.jira").setFieldValues(asList(
+ "{\"key\":\"jira1\", \"url\":\"http://jira1\", \"port\":\"12345\", \"type\":\"A\"}",
+ "{\"key\":\"jira2\", \"url\":\"http://jira2\", \"port\":\"54321\"}"))
+ .build());
+
+ assertThat(getSetting("sonar.jira", anonymousSettingsService).getFieldValues().getFieldValuesList()).extracting(Settings.FieldValues.Value::getValue).containsOnly(
+ ImmutableMap.of("key", "jira1", "url", "http://jira1", "port", "12345", "type", "A"),
+ ImmutableMap.of("key", "jira2", "url", "http://jira2", "port", "54321"));
+ }
+
+ @Test
+ public void return_defined_settings_when_no_key_provided() throws Exception {
+ adminSettingsService.set(SetRequest.builder().setKey(PLUGIN_SETTING_KEY).setValue("some value").build());
+ adminSettingsService.set(SetRequest.builder().setKey("hidden").setValue("test").build());
+
+ assertThat(adminSettingsService.values(ValuesRequest.builder().build()).getSettingsList())
+ .extracting(Setting::getKey)
+ .contains(PLUGIN_SETTING_KEY, "hidden", "sonar.forceAuthentication",
+ // Settings for scanner
+ "sonar.core.startTime");
+
+ assertThat(adminSettingsService.values(ValuesRequest.builder().build()).getSettingsList())
+ .extracting(Setting::getKey, Setting::getValue)
+ .contains(tuple(PLUGIN_SETTING_KEY, "some value"), tuple("hidden", "test"));
+ }
+
+ @CheckForNull
+ private static Setting getSetting(String key, SettingsService settingsService) {
+ ValuesWsResponse response = settingsService.values(ValuesRequest.builder().setKeys(key).build());
+ List<Settings.Setting> settings = response.getSettingsList();
+ return settings.isEmpty() ? null : settings.get(0);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/settings/SettingsTestRestartingOrchestrator.java b/tests/src/test/java/org/sonarqube/tests/settings/SettingsTestRestartingOrchestrator.java
new file mode 100644
index 00000000000..60d690257e9
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/settings/SettingsTestRestartingOrchestrator.java
@@ -0,0 +1,115 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.settings;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import java.io.UnsupportedEncodingException;
+import java.net.URL;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonarqube.pageobjects.EncryptionPage;
+import org.sonarqube.pageobjects.Navigation;
+import util.user.UserRule;
+
+import static com.codeborne.selenide.Condition.visible;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.pluginArtifact;
+import static util.ItUtils.projectDir;
+import static util.ItUtils.xooPlugin;
+
+/**
+ * This class start a new orchestrator on each test case
+ */
+public class SettingsTestRestartingOrchestrator {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private Orchestrator orchestrator;
+
+ private UserRule userRule;
+
+ @After
+ public void stop() {
+ if (orchestrator != null) {
+ userRule.resetUsers();
+ orchestrator.stop();
+ }
+ }
+
+ @Test
+ public void test_settings() throws UnsupportedEncodingException {
+ URL secretKeyUrl = getClass().getResource("/settings/SettingsTest/sonar-secret.txt");
+ orchestrator = Orchestrator.builderEnv()
+ .addPlugin(pluginArtifact("settings-plugin"))
+ .addPlugin(pluginArtifact("license-plugin"))
+ .setServerProperty("sonar.secretKeyPath", secretKeyUrl.getFile())
+ .build();
+ startOrchestrator();
+
+ String adminUser = userRule.createAdminUser();
+ Navigation nav = Navigation.create(orchestrator).openHome().logIn().submitCredentials(adminUser);
+
+ nav.openSettings(null)
+ .assertMenuContains("General")
+ .assertSettingDisplayed("sonar.dbcleaner.cleanDirectory")
+ .assertSettingNotDisplayed("settings.extension.hidden")
+ .assertSettingNotDisplayed("settings.extension.global");
+
+ EncryptionPage encryptionPage = nav.openEncryption();
+ assertThat(encryptionPage.encryptValue("clear")).isEqualTo("{aes}4aQbfYe1lrEjiRzv/ETbyg==");
+ encryptionPage.generateNewKey();
+ encryptionPage.generationForm().shouldBe(visible).submit();
+ encryptionPage.generationForm().shouldNotBe(visible);
+ encryptionPage.newSecretKey().shouldBe(visible);
+ }
+
+ @Test
+ public void property_relocation() throws UnsupportedEncodingException {
+ orchestrator = Orchestrator.builderEnv()
+ .addPlugin(pluginArtifact("property-relocation-plugin"))
+ .addPlugin(xooPlugin())
+ .setServerProperty("sonar.deprecatedKey", "true")
+ .build();
+ startOrchestrator();
+
+ SonarScanner withDeprecatedKey = SonarScanner.create(projectDir("shared/xoo-sample"))
+ .setProperty("sonar.deprecatedKey", "true");
+ SonarScanner withNewKey = SonarScanner.create(projectDir("shared/xoo-sample"))
+ .setProperty("sonar.newKey", "true");
+ // should not fail
+ orchestrator.executeBuilds(withDeprecatedKey, withNewKey);
+
+ String adminUser = userRule.createAdminUser();
+ Navigation.create(orchestrator).openHome().logIn().submitCredentials(adminUser).openSettings(null)
+ .assertMenuContains("General")
+ .assertSettingDisplayed("sonar.newKey")
+ .assertSettingNotDisplayed("sonar.deprecatedKey");
+ }
+
+ private void startOrchestrator() {
+ orchestrator.start();
+ userRule = UserRule.from(orchestrator);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/sourceCode/EncodingTest.java b/tests/src/test/java/org/sonarqube/tests/sourceCode/EncodingTest.java
new file mode 100644
index 00000000000..985a361fb90
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/sourceCode/EncodingTest.java
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.sourceCode;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category1Suite;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.tests.Tester;
+
+import static util.ItUtils.runProjectAnalysis;
+
+public class EncodingTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ @Rule
+ public Tester tester = new Tester(orchestrator).disableOrganizations();
+
+ @Test
+ public void support_japanese_charset() {
+ runProjectAnalysis(orchestrator, "sourceCode/japanese-charset", "sonar.sourceEncoding", "Shift_JIS");
+
+ tester.runHtmlTests("/sourceCode/EncodingTest/japanese_sources.html");
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/sourceCode/HighlightingTest.java b/tests/src/test/java/org/sonarqube/tests/sourceCode/HighlightingTest.java
new file mode 100644
index 00000000000..6e682a74176
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/sourceCode/HighlightingTest.java
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.sourceCode;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category1Suite;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.tests.Tester;
+
+import static util.ItUtils.runProjectAnalysis;
+
+public class HighlightingTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ @Rule
+ public Tester tester = new Tester(orchestrator).disableOrganizations();
+
+ @Test
+ public void highlight_source_code_and_symbols_usage() {
+ runProjectAnalysis(orchestrator, "highlighting/xoo-sample-with-highlighting-v2");
+
+ // SONAR-3893 & SONAR-4247
+ tester.runHtmlTests("/sourceCode/HighlightingTest/syntax-highlighting.html");
+
+ // SONAR-4249 & SONAR-4250
+ tester.runHtmlTests("/sourceCode/HighlightingTest/symbol-usages-highlighting.html");
+ }
+
+ // Check that E/S index is updated when file content is unchanged but plugin generates different syntax/symbol highlighting
+ @Test
+ public void update_highlighting_even_when_code_unchanged() {
+ runProjectAnalysis(orchestrator, "highlighting/xoo-sample-with-highlighting-v1");
+
+ tester.runHtmlTests("/sourceCode/HighlightingTest/syntax-highlighting-v1.html");
+
+ runProjectAnalysis(orchestrator, "highlighting/xoo-sample-with-highlighting-v2");
+
+ tester.runHtmlTests("/sourceCode/HighlightingTest/syntax-highlighting-v2.html");
+ tester.runHtmlTests("/sourceCode/HighlightingTest/symbol-usages-highlighting.html");
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/sourceCode/ProjectCodeTest.java b/tests/src/test/java/org/sonarqube/tests/sourceCode/ProjectCodeTest.java
new file mode 100644
index 00000000000..b79114be2ac
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/sourceCode/ProjectCodeTest.java
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.sourceCode;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category1Suite;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.tests.Tester;
+
+import static util.ItUtils.projectDir;
+
+public class ProjectCodeTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ @Rule
+ public Tester tester = new Tester(orchestrator).disableOrganizations();
+
+ @Test
+ public void test_project_code_page() {
+ executeBuild("shared/xoo-sample", "project-for-code", "Project For Code");
+
+ tester.runHtmlTests(
+ "/sourceCode/ProjectCodeTest/test_project_code_page.html",
+ "/sourceCode/ProjectCodeTest/search.html",
+ "/sourceCode/ProjectCodeTest/permalink.html");
+ }
+
+ @Test
+ public void code_page_should_expand_root_dir() {
+ executeBuild("shared/xoo-sample-with-root-dir", "project-for-code-root-dir", "Project For Code");
+
+ tester.runHtmlTests("/sourceCode/ProjectCodeTest/code_page_should_expand_root_dir.html");
+ }
+
+ private void executeBuild(String projectLocation, String projectKey, String projectName) {
+ orchestrator.executeBuild(
+ SonarScanner.create(projectDir(projectLocation))
+ .setProjectKey(projectKey)
+ .setProjectName(projectName));
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/test/CoverageTest.java b/tests/src/test/java/org/sonarqube/tests/test/CoverageTest.java
new file mode 100644
index 00000000000..1680429d0b0
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/test/CoverageTest.java
@@ -0,0 +1,221 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.test;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category2Suite;
+import java.io.File;
+import java.util.Map;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.filefilter.TrueFileFilter;
+import org.apache.commons.lang.StringUtils;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.sonarqube.tests.Tester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.getMeasuresAsDoubleByMetricKey;
+import static util.ItUtils.projectDir;
+
+public class CoverageTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category2Suite.ORCHESTRATOR;
+
+ private static final String[] ALL_COVERAGE_METRICS = new String[] {
+ "line_coverage", "lines_to_cover", "uncovered_lines", "branch_coverage", "conditions_to_cover", "uncovered_conditions", "coverage",
+ "it_line_coverage", "it_lines_to_cover", "it_uncovered_lines", "it_branch_coverage", "it_conditions_to_cover", "it_uncovered_conditions", "it_coverage",
+ "overall_line_coverage", "overall_lines_to_cover", "overall_uncovered_lines", "overall_branch_coverage", "overall_conditions_to_cover", "overall_uncovered_conditions",
+ "overall_coverage"
+ };
+
+ @Rule
+ public Tester tester = new Tester(orchestrator).disableOrganizations();
+
+ @Test
+ public void coverage() throws Exception {
+ orchestrator.executeBuilds(SonarScanner.create(projectDir("testing/xoo-sample-ut-coverage")));
+
+ Map<String, Double> measures = getMeasuresAsDoubleByMetricKey(orchestrator, "sample-ut-coverage", ALL_COVERAGE_METRICS);
+ assertThat(measures.get("line_coverage")).isEqualTo(50.0);
+ assertThat(measures.get("lines_to_cover")).isEqualTo(4d);
+ assertThat(measures.get("uncovered_lines")).isEqualTo(2d);
+ assertThat(measures.get("branch_coverage")).isEqualTo(50.0);
+ assertThat(measures.get("conditions_to_cover")).isEqualTo(2d);
+ assertThat(measures.get("uncovered_conditions")).isEqualTo(1d);
+ assertThat(measures.get("coverage")).isEqualTo(50.0);
+
+ assertThat(measures.get("it_coverage")).isNull();
+
+ assertThat(measures.get("overall_coverage")).isNull();
+
+ String coverage = cleanupScmAndDuplication(orchestrator.getServer().adminWsClient().get("api/sources/lines", "key", "sample-ut-coverage:src/main/xoo/sample/Sample.xoo"));
+ // Use strict checking to be sure IT coverage is not present
+ JSONAssert.assertEquals(IOUtils.toString(this.getClass().getResourceAsStream("/test/CoverageTest/unit_test_coverage-expected.json"), "UTF-8"), coverage, true);
+
+ verifyComputeEngineTempDirIsEmpty();
+ }
+
+ private String cleanupScmAndDuplication(String coverage) {
+ coverage = StringUtils.remove(coverage, ",\"scmAuthor\":\"\"");
+ coverage = StringUtils.remove(coverage, ",\"scmRevision\":\"\"");
+ coverage = StringUtils.remove(coverage, ",\"duplicated\":false");
+ return coverage;
+ }
+
+ @Test
+ public void coverage_no_condition() throws Exception {
+ orchestrator.executeBuilds(SonarScanner.create(projectDir("testing/xoo-sample-ut-coverage-no-condition")));
+
+ Map<String, Double> measures = getMeasuresAsDoubleByMetricKey(orchestrator, "sample-ut-coverage", ALL_COVERAGE_METRICS);
+ assertThat(measures.get("line_coverage")).isEqualTo(50.0);
+ assertThat(measures.get("lines_to_cover")).isEqualTo(4d);
+ assertThat(measures.get("uncovered_lines")).isEqualTo(2d);
+ assertThat(measures.get("branch_coverage")).isNull();
+ assertThat(measures.get("conditions_to_cover")).isNull();
+ assertThat(measures.get("uncovered_conditions")).isNull();
+ assertThat(measures.get("coverage")).isEqualTo(50.0);
+
+ assertThat(measures.get("it_coverage")).isNull();
+
+ assertThat(measures.get("overall_coverage")).isNull();
+
+ String coverage = cleanupScmAndDuplication(orchestrator.getServer().adminWsClient().get("api/sources/lines", "key", "sample-ut-coverage:src/main/xoo/sample/Sample.xoo"));
+ // Use strict checking to be sure IT coverage is not present
+ JSONAssert.assertEquals(IOUtils.toString(this.getClass().getResourceAsStream("/test/CoverageTest/unit_test_coverage_no_condition-expected.json"), "UTF-8"), coverage,
+ true);
+
+ verifyComputeEngineTempDirIsEmpty();
+ }
+
+ @Test
+ public void it_coverage_imported_as_coverage() throws Exception {
+ orchestrator.executeBuilds(SonarScanner.create(projectDir("testing/xoo-sample-it-coverage")));
+
+ Map<String, Double> measures = getMeasuresAsDoubleByMetricKey(orchestrator, "sample-it-coverage", ALL_COVERAGE_METRICS);
+
+ // Since SQ 6.2 all coverage reports are merged as coverage
+
+ assertThat(measures.get("line_coverage")).isEqualTo(50.0);
+ assertThat(measures.get("lines_to_cover")).isEqualTo(4d);
+ assertThat(measures.get("uncovered_lines")).isEqualTo(2d);
+ assertThat(measures.get("branch_coverage")).isEqualTo(50.0);
+ assertThat(measures.get("conditions_to_cover")).isEqualTo(2d);
+ assertThat(measures.get("uncovered_conditions")).isEqualTo(1d);
+ assertThat(measures.get("coverage")).isEqualTo(50.0);
+
+ assertThat(measures.get("it_coverage")).isNull();
+
+ assertThat(measures.get("overall_coverage")).isNull();
+
+ String coverage = cleanupScmAndDuplication(orchestrator.getServer().adminWsClient().get("api/sources/lines", "key", "sample-it-coverage:src/main/xoo/sample/Sample.xoo"));
+ // Use strict checking to be sure UT coverage is not present
+ JSONAssert.assertEquals(IOUtils.toString(this.getClass().getResourceAsStream("/test/CoverageTest/it_coverage-expected.json"), "UTF-8"), coverage, true);
+
+ verifyComputeEngineTempDirIsEmpty();
+ }
+
+ @Test
+ public void ut_and_it_coverage_merged_in_coverage() throws Exception {
+ orchestrator.executeBuilds(SonarScanner.create(projectDir("testing/xoo-sample-overall-coverage")));
+
+ // Since SQ 6.2 all coverage reports are merged as coverage
+
+ Map<String, Double> measures = getMeasuresAsDoubleByMetricKey(orchestrator, "sample-overall-coverage", ALL_COVERAGE_METRICS);
+ assertThat(measures.get("line_coverage")).isEqualTo(75.0);
+ assertThat(measures.get("lines_to_cover")).isEqualTo(4);
+ assertThat(measures.get("uncovered_lines")).isEqualTo(1);
+ assertThat(measures.get("branch_coverage")).isEqualTo(50.0);
+ assertThat(measures.get("conditions_to_cover")).isEqualTo(4);
+ assertThat(measures.get("uncovered_conditions")).isEqualTo(2);
+ assertThat(measures.get("coverage")).isEqualTo(62.5);
+
+ assertThat(measures.get("it_coverage")).isNull();
+
+ assertThat(measures.get("overall_coverage")).isNull();
+
+ String coverage = cleanupScmAndDuplication(orchestrator.getServer().adminWsClient().get("api/sources/lines", "key", "sample-overall-coverage:src/main/xoo/sample/Sample.xoo"));
+ // Use strict checking to be sure no extra coverage is present
+ JSONAssert.assertEquals(IOUtils.toString(this.getClass().getResourceAsStream("/test/CoverageTest/ut_and_it_coverage-expected.json"), "UTF-8"), coverage, true);
+
+ verifyComputeEngineTempDirIsEmpty();
+ }
+
+ /**
+ * SONAR-766
+ */
+ @Test
+ public void should_compute_coverage_on_project() {
+ orchestrator.executeBuilds(SonarScanner.create(projectDir("testing/xoo-half-covered")));
+
+ assertThat(getMeasuresAsDoubleByMetricKey(orchestrator, "xoo-half-covered", ALL_COVERAGE_METRICS).get("coverage")).isEqualTo(50.0);
+
+ verifyComputeEngineTempDirIsEmpty();
+ }
+
+ /**
+ * SONAR-766
+ */
+ @Test
+ public void should_ignore_coverage_on_full_path() {
+ orchestrator.executeBuilds(SonarScanner.create(projectDir("testing/xoo-half-covered"))
+ .setProperty("sonar.coverage.exclusions", "src/main/xoo/org/sonar/tests/halfcovered/UnCovered.xoo"));
+
+ assertThat(getMeasuresAsDoubleByMetricKey(orchestrator, "xoo-half-covered", ALL_COVERAGE_METRICS).get("coverage")).isEqualTo(100.0);
+
+ verifyComputeEngineTempDirIsEmpty();
+ }
+
+ /**
+ * SONAR-766
+ */
+ @Test
+ public void should_ignore_coverage_on_pattern() {
+ orchestrator.executeBuilds(SonarScanner.create(projectDir("testing/xoo-half-covered"))
+ .setProperty("sonar.coverage.exclusions", "**/UnCovered*"));
+
+ assertThat(getMeasuresAsDoubleByMetricKey(orchestrator, "xoo-half-covered", ALL_COVERAGE_METRICS).get("coverage")).isEqualTo(100.0);
+
+ verifyComputeEngineTempDirIsEmpty();
+ }
+
+ /**
+ * SONAR-766
+ */
+ @Test
+ public void should_not_have_coverage_at_all() {
+ orchestrator.executeBuilds(SonarScanner.create(projectDir("testing/xoo-half-covered"))
+ .setProperty("sonar.coverage.exclusions", "**/*"));
+
+ assertThat(getMeasuresAsDoubleByMetricKey(orchestrator, "xoo-half-covered", ALL_COVERAGE_METRICS).get("coverage")).isNull();
+
+ verifyComputeEngineTempDirIsEmpty();
+ }
+
+ private void verifyComputeEngineTempDirIsEmpty() {
+ File ceTempDirectory = new File(new File(orchestrator.getServer().getHome(), "temp"), "ce");
+ assertThat(FileUtils.listFiles(ceTempDirectory, TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE)).isEmpty();
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/test/CoverageTrackingTest.java b/tests/src/test/java/org/sonarqube/tests/test/CoverageTrackingTest.java
new file mode 100644
index 00000000000..f6dd9b95355
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/test/CoverageTrackingTest.java
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.test;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category2Suite;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.commons.io.IOUtils;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.sonarqube.tests.Tester;
+
+import static util.ItUtils.projectDir;
+
+public class CoverageTrackingTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category2Suite.ORCHESTRATOR;
+
+ @Rule
+ public Tester tester = new Tester(orchestrator).disableOrganizations();
+
+ @Test
+ public void test_coverage_per_test() throws Exception {
+ orchestrator.executeBuilds(SonarScanner.create(projectDir("testing/xoo-sample-with-coverage-per-test")));
+
+ String tests = orchestrator.getServer().adminWsClient().get("api/tests/list", "testFileKey", "sample-with-tests:src/test/xoo/sample/SampleTest.xoo");
+ JSONAssert.assertEquals(IOUtils.toString(this.getClass().getResourceAsStream("/test/CoverageTrackingTest/tests-expected.json"), "UTF-8"), tests, false);
+
+ String covered_files = orchestrator.getServer().adminWsClient()
+ .get("api/tests/covered_files", "testId", extractSuccessfulTestId(tests));
+ JSONAssert
+ .assertEquals(IOUtils.toString(this.getClass().getResourceAsStream("/test/CoverageTrackingTest/covered_files-expected.json"), "UTF-8"), covered_files, false);
+ }
+
+ private String extractSuccessfulTestId(String json) {
+ Matcher jsonObjectMatcher = Pattern.compile(".*\\{((.*?)success(.*?))\\}.*", Pattern.MULTILINE).matcher(json);
+ jsonObjectMatcher.find();
+
+ Matcher idMatcher = Pattern.compile(".*\"id\"\\s*?:\\s*?\"(\\S*?)\".*", Pattern.MULTILINE).matcher(jsonObjectMatcher.group(1));
+ return idMatcher.find() ? idMatcher.group(1) : "";
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/test/NewCoverageTest.java b/tests/src/test/java/org/sonarqube/tests/test/NewCoverageTest.java
new file mode 100644
index 00000000000..6a331207e32
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/test/NewCoverageTest.java
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.test;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category2Suite;
+import java.util.Map;
+import org.assertj.core.data.Offset;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.WsMeasures;
+
+import static java.lang.Double.parseDouble;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.getMeasuresWithVariationsByMetricKey;
+import static util.ItUtils.projectDir;
+
+public class NewCoverageTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category2Suite.ORCHESTRATOR;
+
+ private static final String PROJECT_KEY = "sample-new-coverage";
+
+ private static final Offset<Double> DEFAULT_OFFSET = Offset.offset(0.1d);
+
+ private static final String[] ALL_NEW_COVERAGE_METRICS = new String[] {
+ "new_coverage", "new_line_coverage", "new_branch_coverage"
+ };
+
+ @BeforeClass
+ public static void analyze_project() {
+ orchestrator.resetData();
+
+ orchestrator.executeBuilds(SonarScanner.create(projectDir("testing/xoo-sample-new-coverage-v1"))
+ .setProperty("sonar.projectDate", "2015-02-01")
+ .setProperty("sonar.scm.disabled", "false"));
+ orchestrator.executeBuilds(SonarScanner.create(projectDir("testing/xoo-sample-new-coverage-v2"))
+ .setProperty("sonar.scm.disabled", "false"));
+ }
+
+ @Test
+ public void new_coverage() throws Exception {
+ Map<String, WsMeasures.Measure> measures = getMeasuresWithVariationsByMetricKey(orchestrator, PROJECT_KEY, ALL_NEW_COVERAGE_METRICS);
+ assertThat(parseDouble(measures.get("new_coverage").getPeriods().getPeriodsValue(0).getValue())).isEqualTo(66.6d, DEFAULT_OFFSET);
+ assertThat(parseDouble(measures.get("new_line_coverage").getPeriods().getPeriodsValue(0).getValue())).isEqualTo(100d, DEFAULT_OFFSET);
+ assertThat(parseDouble(measures.get("new_branch_coverage").getPeriods().getPeriodsValue(0).getValue())).isEqualTo(42.8, DEFAULT_OFFSET);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/test/TestExecutionTest.java b/tests/src/test/java/org/sonarqube/tests/test/TestExecutionTest.java
new file mode 100644
index 00000000000..bb5edbb55f3
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/test/TestExecutionTest.java
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.test;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category2Suite;
+import java.util.Map;
+import org.apache.commons.io.IOUtils;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.getMeasuresAsDoubleByMetricKey;
+import static util.ItUtils.projectDir;
+
+public class TestExecutionTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category2Suite.ORCHESTRATOR;
+
+ @Before
+ public void delete_data() {
+ orchestrator.resetData();
+ }
+
+ @Test
+ public void test_execution_details() throws Exception {
+ orchestrator.executeBuilds(SonarScanner.create(projectDir("testing/xoo-sample-with-tests-execution-details")));
+
+ Map<String, Double> measures = getMeasuresAsDoubleByMetricKey(orchestrator, "sample-with-tests",
+ "test_success_density", "test_failures", "test_errors", "tests", "skipped_tests", "test_execution_time");
+ assertThat(measures.get("test_success_density")).isEqualTo(33.3);
+ assertThat(measures.get("test_failures")).isEqualTo(1);
+ assertThat(measures.get("test_errors")).isEqualTo(1);
+ assertThat(measures.get("tests")).isEqualTo(3);
+ assertThat(measures.get("skipped_tests")).isEqualTo(1);
+ assertThat(measures.get("test_execution_time")).isEqualTo(8);
+
+ String json = orchestrator.getServer().adminWsClient().get("api/tests/list", "testFileKey", "sample-with-tests:src/test/xoo/sample/SampleTest.xoo");
+ JSONAssert.assertEquals(IOUtils.toString(this.getClass().getResourceAsStream("/test/TestExecutionTest/expected.json"), "UTF-8"), json, false);
+ }
+
+ @Test
+ public void test_execution_measures() throws Exception {
+ orchestrator.executeBuilds(SonarScanner.create(projectDir("testing/xoo-sample-with-tests-execution-measures")));
+
+ Map<String, Double> measures = getMeasuresAsDoubleByMetricKey(orchestrator, "sample-with-tests",
+ "test_success_density", "test_failures", "test_errors", "tests", "skipped_tests", "test_execution_time");
+ assertThat(measures.get("test_success_density")).isEqualTo(33.3);
+ assertThat(measures.get("test_failures")).isEqualTo(1);
+ assertThat(measures.get("test_errors")).isEqualTo(1);
+ assertThat(measures.get("tests")).isEqualTo(3);
+ assertThat(measures.get("skipped_tests")).isEqualTo(1);
+ assertThat(measures.get("test_execution_time")).isEqualTo(8);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/ui/OrganizationUiExtensionsTest.java b/tests/src/test/java/org/sonarqube/tests/ui/OrganizationUiExtensionsTest.java
new file mode 100644
index 00000000000..5198fc89062
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/ui/OrganizationUiExtensionsTest.java
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.ui;
+
+import com.codeborne.selenide.Condition;
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category6Suite;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.Organizations.Organization;
+import org.sonarqube.ws.WsUsers.CreateWsResponse.User;
+
+import static com.codeborne.selenide.Condition.text;
+import static com.codeborne.selenide.Selenide.$;
+import static com.codeborne.selenide.WebDriverRunner.url;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class OrganizationUiExtensionsTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category6Suite.ORCHESTRATOR;
+
+ @Rule
+ public Tester tester = new Tester(orchestrator);
+
+ @Test
+ public void organization_page() {
+ Organization organization = tester.organizations().generate();
+ tester.openBrowser().open("/organizations/" + organization.getKey() + "/projects");
+
+ $("#organization-navigation-more").click();
+ $(By.linkText("Organization Page")).shouldBe(Condition.visible).click();
+
+ assertThat(url()).contains("uiextensionsplugin/organization_page");
+ $("body").shouldHave(text("uiextensionsplugin/organization_page"));
+ }
+
+ @Test
+ public void organization_admin_page() {
+ Organization organization = tester.organizations().generate();
+ User administrator = tester.users().generateAdministrator(organization);
+ tester.openBrowser()
+ .logIn().submitCredentials(administrator.getLogin())
+ .open("/organizations/" + organization.getKey() + "/projects");
+
+ $("#context-navigation a.navbar-admin-link").click();
+ $(By.linkText("Organization Admin Page")).shouldBe(Condition.visible).click();
+
+ assertThat(url()).contains("uiextensionsplugin/organization_admin_page");
+ $("body").shouldHave(text("uiextensionsplugin/organization_admin_page"));
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/ui/SourceViewerTest.java b/tests/src/test/java/org/sonarqube/tests/ui/SourceViewerTest.java
new file mode 100644
index 00000000000..64ff1451740
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/ui/SourceViewerTest.java
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.ui;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category4Suite;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.pageobjects.Navigation;
+
+import static com.codeborne.selenide.Condition.exist;
+import static com.codeborne.selenide.Selenide.$;
+import static util.ItUtils.projectDir;
+
+public class SourceViewerTest {
+
+ @ClassRule
+ public static final Orchestrator ORCHESTRATOR = Category4Suite.ORCHESTRATOR;
+
+ private Navigation nav = Navigation.create(ORCHESTRATOR);
+
+ @BeforeClass
+ public static void beforeClass() {
+ ORCHESTRATOR.resetData();
+ analyzeSampleProject();
+ }
+
+ @Test
+ public void line_permalink() {
+ nav.open("/component?id=sample%3Asrc%2Fmain%2Fxoo%2Fsample%2FSample.xoo&line=6");
+ $(".source-line").should(exist);
+ $(".source-line-highlighted[data-line-number=\"6\"]").should(exist);
+ }
+
+ private static void analyzeSampleProject() {
+ ORCHESTRATOR.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")));
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/ui/UiExtensionsTest.java b/tests/src/test/java/org/sonarqube/tests/ui/UiExtensionsTest.java
new file mode 100644
index 00000000000..db79e355e35
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/ui/UiExtensionsTest.java
@@ -0,0 +1,115 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.ui;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category4Suite;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.WsProjects;
+import org.sonarqube.ws.WsUsers.CreateWsResponse.User;
+import org.sonarqube.ws.client.project.CreateRequest;
+import util.ItUtils;
+
+import static com.codeborne.selenide.Condition.text;
+import static com.codeborne.selenide.Selenide.$;
+import static com.codeborne.selenide.WebDriverRunner.url;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class UiExtensionsTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+
+ @Rule
+ public Tester tester = new Tester(orchestrator).disableOrganizations();
+
+ @Test
+ public void test_static_files() {
+ tester.runHtmlTests("/ui/UiExtensionsTest/static-files.html");
+ }
+
+ @Test
+ public void test_global_page() {
+ tester.openBrowser().open("/about");
+
+ // on about page
+ $("#global-navigation-more").click();
+ $(By.linkText("Global Page")).click();
+
+ assertThat(url()).contains("/uiextensionsplugin/global_page");
+ $("body").shouldHave(text("uiextensionsplugin/global_page"));
+ }
+
+ @Test
+ public void test_global_administration_page() {
+ User administrator = tester.users().generateAdministrator();
+ tester.openBrowser()
+ .logIn().submitCredentials(administrator.getLogin())
+ .open("/about");
+
+ $(".navbar-admin-link").click();
+ $("#settings-navigation-configuration").click();
+ $(By.linkText("Global Admin Page")).click();
+
+ assertThat(url()).contains("uiextensionsplugin/global_admin_page");
+ $("body").shouldHave(text("uiextensionsplugin/global_admin_page"));
+ }
+
+ @Test
+ public void test_project_page() {
+ WsProjects.CreateWsResponse.Project project = createSampleProject();
+
+ tester.openBrowser().open("/dashboard?id=" + project.getKey());
+
+ $("#component-navigation-more").click();
+ $(By.linkText("Project Page")).click();
+
+ assertThat(url()).contains("uiextensionsplugin/project_page");
+ $("body").shouldHave(text("uiextensionsplugin/project_page"));
+ }
+
+ @Test
+ public void test_project_administration_page() {
+ WsProjects.CreateWsResponse.Project project = createSampleProject();
+ User administrator = tester.users().generateAdministrator();
+
+ tester.openBrowser()
+ .logIn().submitCredentials(administrator.getLogin())
+ .open("/dashboard?id=" + project.getKey());
+
+ $("#component-navigation-admin").click();
+ $(By.linkText("Project Admin Page")).click();
+
+ assertThat(url()).contains("uiextensionsplugin/project_admin_page");
+ $("body").shouldHave(text("uiextensionsplugin/project_admin_page"));
+ }
+
+ private WsProjects.CreateWsResponse.Project createSampleProject() {
+ String projectKey = ItUtils.newProjectKey();
+ return tester.wsClient().projects().create(CreateRequest.builder()
+ .setKey(projectKey)
+ .setName("Name of " + projectKey)
+ .build()).getProject();
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/ui/UiTest.java b/tests/src/test/java/org/sonarqube/tests/ui/UiTest.java
new file mode 100644
index 00000000000..ff4b65c0f14
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/ui/UiTest.java
@@ -0,0 +1,155 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.ui;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category4Suite;
+import java.util.Map;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.WsResponse;
+import org.sonarqube.pageobjects.Navigation;
+import util.ItUtils;
+
+import static com.codeborne.selenide.Condition.hasText;
+import static com.codeborne.selenide.Condition.text;
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+import static com.codeborne.selenide.WebDriverRunner.url;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.projectDir;
+import static util.ItUtils.resetSettings;
+import static util.ItUtils.setServerProperty;
+
+public class UiTest {
+
+ @ClassRule
+ public static final Orchestrator ORCHESTRATOR = Category4Suite.ORCHESTRATOR;
+
+ private Navigation nav = Navigation.create(ORCHESTRATOR);
+
+ @Before
+ @After
+ public void resetData() throws Exception {
+ resetSettings(ORCHESTRATOR, null, "sonar.forceAuthentication");
+ }
+
+ @Test
+ public void footer_contains_information() {
+ nav.getFooter()
+ .should(hasText("Documentation"))
+ .should(hasText("SonarSource SA"));
+ }
+
+ @Test
+ public void footer_contains_version() {
+ WsResponse status = ItUtils.newAdminWsClient(ORCHESTRATOR).wsConnector().call(new GetRequest("api/navigation/global"));
+ Map<String, Object> statusMap = ItUtils.jsonToMap(status.content());
+
+ nav.getFooter().should(hasText((String) statusMap.get("version")));
+ }
+
+ @Test
+ public void footer_doesnt_contains_version_on_login_page() {
+ WsResponse status = ItUtils.newAdminWsClient(ORCHESTRATOR).wsConnector().call(new GetRequest("api/navigation/global"));
+ Map<String, Object> statusMap = ItUtils.jsonToMap(status.content());
+
+ nav.openLogin();
+ nav.getFooter().shouldNot(hasText((String) statusMap.get("version")));
+ }
+
+ @Test
+ public void footer_doesnt_contains_about_when_not_logged_in() {
+ setServerProperty(ORCHESTRATOR, "sonar.forceAuthentication", "true");
+ nav.openLogin();
+ nav.getFooter()
+ .shouldNot(hasText("About"))
+ .shouldNot(hasText("Web API"));
+ }
+
+ @Test
+ public void many_page_transitions() {
+ analyzeSampleProject();
+
+ nav.open("/about");
+
+ // on about page
+ $(".about-page-projects-link")
+ .shouldBe(visible)
+ .shouldHave(text("1"))
+ .click();
+
+ // on projects page
+ assertThat(url()).contains("/projects");
+ $(".project-card-name")
+ .shouldBe(visible)
+ .shouldHave(text("Sample"))
+ .find("a")
+ .click();
+
+ // on project dashboard
+ assertThat(url()).contains("/dashboard?id=sample");
+ $(".overview-quality-gate")
+ .shouldBe(visible)
+ .shouldHave(text("Passed"));
+ $("a[href=\"/project/issues?id=sample&resolved=false&types=CODE_SMELL\"]")
+ .shouldBe(visible)
+ .shouldHave(text("0"))
+ .click();
+
+ // on project issues page
+ assertThat(url()).contains("/project/issues?id=sample&resolved=false&types=CODE_SMELL");
+ $("[data-property=\"resolutions\"] .facet.active").shouldBe(visible);
+
+ $("#global-navigation").find("a[href=\"/profiles\"]").click();
+
+ // on quality profiles page
+ assertThat(url()).contains("/profiles");
+ $("table[data-language=xoo]").find("tr[data-name=Basic]").find(".quality-profiles-table-name")
+ .shouldBe(visible)
+ .shouldHave(text("Basic"))
+ .find("a")
+ .click();
+
+ // on profile page
+ assertThat(url()).contains("/profiles/show");
+ $(".quality-profile-inheritance")
+ .shouldBe(visible)
+ .shouldHave(text("active rules"));
+ }
+
+ @Test
+ public void markdown_help() {
+ String tags[] = {"strong", "a", "ul", "ol", "h1", "code", "pre", "blockquote"};
+
+ nav.open("/markdown/help");
+ for (String tag : tags) {
+ $(tag).shouldBe(visible);
+ }
+ }
+
+ private static void analyzeSampleProject() {
+ ORCHESTRATOR.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")));
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/updateCenter/UpdateCenterTest.java b/tests/src/test/java/org/sonarqube/tests/updateCenter/UpdateCenterTest.java
new file mode 100644
index 00000000000..a8f4d95b825
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/updateCenter/UpdateCenterTest.java
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.updateCenter;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.junit.After;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import util.user.UserRule;
+
+import static util.ItUtils.pluginArtifact;
+import static util.selenium.Selenese.runSelenese;
+
+/**
+ * This class start its own orchestrator
+ */
+public class UpdateCenterTest {
+
+ @ClassRule
+ public static final Orchestrator orchestrator = Orchestrator.builderEnv()
+ .setServerProperty("sonar.updatecenter.url", UpdateCenterTest.class.getResource("/updateCenter/UpdateCenterTest/update-center.properties").toString())
+ .addPlugin(pluginArtifact("sonar-fake-plugin"))
+ .build();
+ @Rule
+ public UserRule userRule = UserRule.from(orchestrator);
+
+ @After
+ public void tearDown() throws Exception {
+ userRule.resetUsers();
+ }
+
+ @Test
+ public void test_console() {
+ runSelenese(orchestrator, "/updateCenter/installed-plugins.html");
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/upgrade/MssqlConfig.java b/tests/src/test/java/org/sonarqube/tests/upgrade/MssqlConfig.java
new file mode 100644
index 00000000000..2d22485ef43
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/upgrade/MssqlConfig.java
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.upgrade;
+
+import com.sonar.orchestrator.config.Configuration;
+import com.sonar.orchestrator.version.Version;
+import org.apache.commons.lang.StringUtils;
+
+import static java.util.Objects.requireNonNull;
+
+public class MssqlConfig {
+
+ /**
+ * Versions prior to 5.2 support only jTDS driver. Versions greater than or equal to 5.2
+ * support only MS driver. The problem is that the test is configured with only
+ * the MS URL, so it must be changed at runtime for versions < 5.2.
+ */
+ public static String fixUrl(Configuration conf, Version sqVersion) {
+ String jdbcUrl = requireNonNull(conf.getString("sonar.jdbc.url"), "No JDBC url configured");
+ if (jdbcUrl.startsWith("jdbc:sqlserver:") && !sqVersion.isGreaterThanOrEquals("5.2")) {
+ // Job is configured with the new Microsoft driver, which is not supported by old versions of SQ
+ String host = StringUtils.substringBetween(jdbcUrl, "jdbc:sqlserver://", ";databaseName=");
+ String db = StringUtils.substringAfter(jdbcUrl, "databaseName=");
+ jdbcUrl = "jdbc:jtds:sqlserver://" + host + "/" + db;
+ System.out.println("Replaced JDBC url to: " + jdbcUrl);
+ return jdbcUrl;
+ }
+ return jdbcUrl;
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/upgrade/SelenideConfig.java b/tests/src/test/java/org/sonarqube/tests/upgrade/SelenideConfig.java
new file mode 100644
index 00000000000..b67e99b5735
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/upgrade/SelenideConfig.java
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.upgrade;
+
+import com.codeborne.selenide.Configuration;
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Objects.requireNonNull;
+
+enum SelenideConfig {
+ INSTANCE;
+
+ private static final Set<String> SUPPORTED_BROWSERS = ImmutableSet.of("firefox");
+
+ SelenideConfig() {
+ Configuration.reportsFolder = "target/screenshots";
+ }
+
+ public SelenideConfig setBrowser(String browser) {
+ checkArgument(SUPPORTED_BROWSERS.contains(requireNonNull(browser)), "Browser is not supported: %s", browser);
+ Configuration.browser = browser;
+ return this;
+ }
+
+ public SelenideConfig setBaseUrl(String s) {
+ Configuration.baseUrl = requireNonNull(s);
+ return this;
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/upgrade/ServerMigrationCall.java b/tests/src/test/java/org/sonarqube/tests/upgrade/ServerMigrationCall.java
new file mode 100644
index 00000000000..8b55198410e
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/upgrade/ServerMigrationCall.java
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.upgrade;
+
+import com.sonar.orchestrator.Orchestrator;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import org.sonar.wsclient.jsonsimple.JSONObject;
+
+public class ServerMigrationCall extends WsCallAndWait<ServerMigrationResponse> {
+
+ public ServerMigrationCall(Orchestrator orchestrator) {
+ super(orchestrator, "/api/system/migrate_db");
+ }
+
+ @Override
+ @Nonnull
+ protected ServerMigrationResponse parse(JSONObject jsonObject) {
+ try {
+ return new ServerMigrationResponse(
+ ServerMigrationResponse.Status.valueOf((String) jsonObject.get("state")),
+ (String) jsonObject.get("message"),
+ parseDate((String) jsonObject.get("createdAt")));
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to parse JSON response", e);
+ }
+ }
+
+ @CheckForNull
+ private Date parseDate(@Nullable String createdAt) throws ParseException {
+ if (createdAt == null || createdAt.isEmpty()) {
+ return null;
+ }
+ return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(createdAt);
+ }
+
+ @Override
+ protected boolean shouldWait(ServerMigrationResponse response) {
+ return response.getStatus() == ServerMigrationResponse.Status.MIGRATION_NEEDED
+ || response.getStatus() == ServerMigrationResponse.Status.MIGRATION_RUNNING;
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/upgrade/ServerMigrationResponse.java b/tests/src/test/java/org/sonarqube/tests/upgrade/ServerMigrationResponse.java
new file mode 100644
index 00000000000..1a24b0c5445
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/upgrade/ServerMigrationResponse.java
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.upgrade;
+
+import java.util.Date;
+import javax.annotation.CheckForNull;
+
+class ServerMigrationResponse {
+ private final Status status;
+ private final String message;
+ private final Date date;
+
+ ServerMigrationResponse(Status status, String message, Date date) {
+ this.status = status;
+ this.message = message;
+ this.date = date;
+ }
+
+ public Status getStatus() {
+ return status;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ @CheckForNull
+ public Date getDate() {
+ return date;
+ }
+
+ public enum Status {
+ NO_MIGRATION, MIGRATION_NEEDED, MIGRATION_RUNNING, MIGRATION_SUCCEEDED, MIGRATION_FAILED
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/upgrade/ServerStatusCall.java b/tests/src/test/java/org/sonarqube/tests/upgrade/ServerStatusCall.java
new file mode 100644
index 00000000000..d764cfd0bac
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/upgrade/ServerStatusCall.java
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.upgrade;
+
+import com.sonar.orchestrator.Orchestrator;
+import javax.annotation.Nonnull;
+import org.sonar.wsclient.jsonsimple.JSONObject;
+
+public class ServerStatusCall extends WsCallAndWait<ServerStatusResponse> {
+ protected ServerStatusCall(Orchestrator orchestrator) {
+ super(orchestrator, "/api/system/status");
+ }
+
+ @Nonnull
+ @Override
+ protected ServerStatusResponse parse(JSONObject jsonObject) {
+ return new ServerStatusResponse(
+ (String) jsonObject.get("id"),
+ (String) jsonObject.get("version"),
+ ServerStatusResponse.Status.valueOf((String) jsonObject.get("status"))
+ );
+ }
+
+ @Override
+ protected boolean shouldWait(ServerStatusResponse serverStatusResponse) {
+ ServerStatusResponse.Status status = serverStatusResponse.getStatus();
+ return status == ServerStatusResponse.Status.STARTING || status == ServerStatusResponse.Status.DB_MIGRATION_RUNNING;
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/upgrade/ServerStatusResponse.java b/tests/src/test/java/org/sonarqube/tests/upgrade/ServerStatusResponse.java
new file mode 100644
index 00000000000..0ffcc3cfb4a
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/upgrade/ServerStatusResponse.java
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.upgrade;
+
+public class ServerStatusResponse {
+ private final String id;
+ private final String version;
+ private final Status status;
+
+ public ServerStatusResponse(String id, String version, Status status) {
+ this.id = id;
+ this.version = version;
+ this.status = status;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ public Status getStatus() {
+ return status;
+ }
+
+ public enum Status {
+ UP, DOWN, STARTING, DB_MIGRATION_NEEDED, DB_MIGRATION_RUNNING
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/upgrade/UpgradeSuite.java b/tests/src/test/java/org/sonarqube/tests/upgrade/UpgradeSuite.java
new file mode 100644
index 00000000000..efb4694299f
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/upgrade/UpgradeSuite.java
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.upgrade;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+ UpgradeTest.class
+})
+public class UpgradeSuite {
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/upgrade/UpgradeTest.java b/tests/src/test/java/org/sonarqube/tests/upgrade/UpgradeTest.java
new file mode 100644
index 00000000000..83cf0c1ac91
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/upgrade/UpgradeTest.java
@@ -0,0 +1,281 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.upgrade;
+
+import com.codeborne.selenide.Selenide;
+import com.codeborne.selenide.WebDriverRunner;
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.OrchestratorBuilder;
+import com.sonar.orchestrator.build.MavenBuild;
+import com.sonar.orchestrator.container.Server;
+import com.sonar.orchestrator.locator.FileLocation;
+import com.sonar.orchestrator.version.Version;
+import java.io.File;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Collections;
+import org.apache.commons.io.IOUtils;
+import org.junit.After;
+import org.junit.Test;
+import org.sonar.wsclient.services.ResourceQuery;
+import org.sonarqube.ws.WsMeasures.Measure;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.HttpConnector;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.WsClientFactories;
+import org.sonarqube.ws.client.WsResponse;
+import org.sonarqube.ws.client.measure.ComponentWsRequest;
+
+import static com.codeborne.selenide.Condition.hasText;
+import static com.codeborne.selenide.Selenide.$;
+import static java.lang.Integer.parseInt;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class UpgradeTest {
+
+ private static final String PROJECT_KEY = "org.apache.struts:struts-parent";
+ private static final String LATEST_JAVA_RELEASE = "LATEST_RELEASE";
+ private static final Version VERSION_5_2 = Version.create("5.2");
+ private static final Version VERSION_5_6_1 = Version.create("5.6.1");
+ private static final Version VERSION_CURRENT = Version.create("DEV");
+
+ private Orchestrator orchestrator;
+
+ @After
+ public void stop() {
+ if (orchestrator != null) {
+ orchestrator.stop();
+ orchestrator = null;
+ }
+ }
+
+ @Test
+ public void test_upgrade_from_5_6_1() {
+ testDatabaseUpgrade(VERSION_5_6_1);
+ }
+
+ @Test
+ public void test_upgrade_from_5_2_via_5_6_1() {
+ testDatabaseUpgrade(VERSION_5_2, VERSION_5_6_1);
+ }
+
+ private void testDatabaseUpgrade(Version fromVersion, Version... intermediaryVersions) {
+ startOldVersionServer(fromVersion, false);
+ scanProject();
+ int files = countFiles(PROJECT_KEY);
+ assertThat(files).isGreaterThan(0);
+ stopServer();
+
+ Arrays.stream(intermediaryVersions).forEach((sqVersion) -> {
+ startOldVersionServer(sqVersion, true);
+ upgrade(sqVersion);
+ verifyAnalysis(files);
+ stopServer();
+ });
+
+ startDevServer();
+ upgrade(VERSION_CURRENT);
+ verifyAnalysis(files);
+ stopServer();
+ }
+
+ private void verifyAnalysis(int expectedNumberOfFiles) {
+ assertThat(countFiles(PROJECT_KEY)).isEqualTo(expectedNumberOfFiles);
+ scanProject();
+ assertThat(countFiles(PROJECT_KEY)).isEqualTo(expectedNumberOfFiles);
+ browseWebapp();
+ }
+
+ private void upgrade(Version sqVersion) {
+ checkSystemStatus(sqVersion, ServerStatusResponse.Status.DB_MIGRATION_NEEDED);
+ if (sqVersion.equals(VERSION_CURRENT)) {
+ checkUrlsBeforeUpgrade();
+ }
+ ServerMigrationResponse serverMigrationResponse = new ServerMigrationCall(orchestrator).callAndWait();
+ assertThat(serverMigrationResponse.getStatus())
+ .describedAs("Migration status of version " + sqVersion + " should be MIGRATION_SUCCEEDED")
+ .isEqualTo(ServerMigrationResponse.Status.MIGRATION_SUCCEEDED);
+ checkSystemStatus(sqVersion, ServerStatusResponse.Status.UP);
+ checkUrlsAfterUpgrade();
+ }
+
+ private void checkSystemStatus(Version sqVersion, ServerStatusResponse.Status serverStatus) {
+ ServerStatusResponse serverStatusResponse = new ServerStatusCall(orchestrator).callAndWait();
+
+ assertThat(serverStatusResponse.getStatus())
+ .describedAs("Server status of version " + sqVersion + " should be " + serverStatus)
+ .isEqualTo(serverStatus);
+ }
+
+ private void checkUrlsBeforeUpgrade() {
+ // These urls should be available when system requires a migration
+ checkUrlIsReturningOk("/api/system/status");
+ checkUrlIsReturningOk("/api/system/db_migration_status");
+ checkUrlIsReturningOk("/api/webservices/list");
+
+ // These urls should not be available when system requires a migration
+ checkUrlIsReturningNotFound("/api/issues/search?projectKeys=org.apache.struts%3Astruts-core");
+ checkUrlIsReturningNotFound("/api/components/tree?baseComponentKey=org.apache.struts%3Astruts-core");
+ checkUrlIsReturningNotFound("/api/measures/component_tree?baseComponentKey=org.apache.struts%3Astruts-core&metricKeys=ncloc,files,violations");
+ checkUrlIsReturningNotFound("/api/qualityprofiles/search");
+
+ // These page should all redirect to maintenance page
+ checkUrlIsRedirectedToMaintenancePage("/");
+ checkUrlIsRedirectedToMaintenancePage("/issues/index");
+ checkUrlIsRedirectedToMaintenancePage("/dashboard/index/org.apache.struts:struts-parent");
+ checkUrlIsRedirectedToMaintenancePage("/issues");
+ checkUrlIsRedirectedToMaintenancePage(
+ "/component/index?id=org.apache.struts%3Astruts-core%3Asrc%2Fmain%2Fjava%2Forg%2Fapache%2Fstruts%2Fchain%2Fcommands%2Fgeneric%2FWrappingLookupCommand.java");
+ checkUrlIsRedirectedToMaintenancePage("/profiles");
+ }
+
+ private void checkUrlsAfterUpgrade() {
+ checkUrlIsReturningOk("/api/system/status");
+ checkUrlIsReturningOk("/api/system/db_migration_status");
+ checkUrlIsReturningOk("/api/webservices/list");
+ checkUrlIsReturningOk("/api/l10n/index");
+
+ checkUrlIsReturningOk("/api/issues/search?projectKeys=org.apache.struts%3Astruts-core");
+ checkUrlIsReturningOk("/api/components/tree?baseComponentKey=org.apache.struts%3Astruts-core");
+ checkUrlIsReturningOk("/api/measures/component_tree?baseComponentKey=org.apache.struts%3Astruts-core&metricKeys=ncloc,files,violations");
+ checkUrlIsReturningOk("/api/qualityprofiles/search");
+ }
+
+ private void browseWebapp() {
+ testUrl("/");
+ testUrl("/api/issues/search?projectKeys=org.apache.struts%3Astruts-core");
+ testUrl("/api/components/tree?baseComponentKey=org.apache.struts%3Astruts-core");
+ testUrl("/api/measures/component_tree?baseComponentKey=org.apache.struts%3Astruts-core&metricKeys=ncloc,files,violations");
+ testUrl("/api/qualityprofiles/search");
+ testUrl("/issues/index");
+ testUrl("/dashboard/index/org.apache.struts:struts-parent");
+ testUrl("/issues");
+ testUrl("/component/index?id=org.apache.struts%3Astruts-core%3Asrc%2Fmain%2Fjava%2Forg%2Fapache%2Fstruts%2Fchain%2Fcommands%2Fgeneric%2FWrappingLookupCommand.java");
+ testUrl("/profiles");
+ }
+
+ private void startOldVersionServer(Version sqVersion, boolean keepDatabase) {
+ OrchestratorBuilder builder = Orchestrator.builderEnv()
+ .setSonarVersion(sqVersion.toString())
+ .setOrchestratorProperty("orchestrator.keepDatabase", String.valueOf(keepDatabase))
+ .setOrchestratorProperty("javaVersion", "3.14")
+ .addPlugin("java")
+ .setStartupLogWatcher(log -> log.contains("Process[web] is up"));
+ orchestrator = builder.build();
+ orchestrator.start();
+ initSelenide(orchestrator);
+ }
+
+ private void startDevServer() {
+ OrchestratorBuilder builder = Orchestrator.builderEnv()
+ .setZipFile(FileLocation.byWildcardMavenFilename(new File("../sonar-application/target"), "sonar*.zip").getFile())
+ .setOrchestratorProperty("orchestrator.keepDatabase", "true")
+ .setOrchestratorProperty("javaVersion", LATEST_JAVA_RELEASE)
+ .addPlugin("java")
+ .setStartupLogWatcher(log -> log.contains("Database must be upgraded"));
+ orchestrator = builder.build();
+ orchestrator.start();
+ initSelenide(orchestrator);
+ }
+
+ private void stopServer() {
+ if (orchestrator != null) {
+ orchestrator.stop();
+ }
+ }
+
+ private void scanProject() {
+ MavenBuild build = MavenBuild.create(new File("projects/struts-1.3.9-diet/pom.xml"))
+ .setCleanSonarGoals()
+ // exclude pom.xml, otherwise it will be published in SQ 6.3+ and not in previous versions, resulting in a different number of components
+ .setProperty("sonar.exclusions", "**/pom.xml")
+ .setProperty("sonar.dynamicAnalysis", "false")
+ .setProperty("sonar.scm.disabled", "true")
+ .setProperty("sonar.cpd.cross_project", "true");
+ orchestrator.executeBuild(build);
+ }
+
+ private int countFiles(String key) {
+ if (orchestrator.getConfiguration().getSonarVersion().isGreaterThanOrEquals("5.4")) {
+ Measure measure = newWsClient(orchestrator).measures().component(new ComponentWsRequest().setComponentKey(key).setMetricKeys(Collections.singletonList("files")))
+ .getComponent().getMeasures(0);
+ return parseInt(measure.getValue());
+ }
+ return orchestrator.getServer().getWsClient().find(ResourceQuery.createForMetrics(key, "files")).getMeasureIntValue("files");
+ }
+
+ private void testUrl(String path) {
+ HttpURLConnection connection = null;
+ try {
+ URL url = new URL(orchestrator.getServer().getUrl() + path);
+ connection = (HttpURLConnection) url.openConnection();
+ connection.connect();
+ assertThat(connection.getResponseCode()).as("Fail to load " + path).isEqualTo(HttpURLConnection.HTTP_OK);
+
+ String content = IOUtils.toString(connection.getInputStream());
+ assertThat(content).as("Fail to load " + path).doesNotContain("something went wrong");
+ assertThat(content).as("Fail to load " + path).doesNotContain("The page you were looking for doesn't exist");
+ assertThat(content).as("Fail to load " + path).doesNotContain("Unauthorized access");
+
+ } catch (IOException e) {
+ throw new IllegalStateException("Error with " + path, e);
+
+ } finally {
+ if (connection != null) {
+ connection.disconnect();
+ }
+ }
+ }
+
+ private void checkUrlIsReturningOk(String url) {
+ newWsClient(orchestrator).wsConnector().call(new GetRequest(url)).failIfNotSuccessful();
+ }
+
+ private void checkUrlIsReturningNotFound(String url) {
+ WsResponse response = newWsClient(orchestrator).wsConnector().call(new GetRequest(url));
+ assertThat(response.code()).isEqualTo(HttpURLConnection.HTTP_NOT_FOUND);
+ }
+
+ private void checkUrlIsRedirectedToMaintenancePage(String url) {
+ shouldBeRedirectToMaintenance(url);
+ }
+
+ private static WsClient newWsClient(Orchestrator orchestrator) {
+ Server server = orchestrator.getServer();
+ return WsClientFactories.getDefault().newClient(HttpConnector.newBuilder()
+ .url(server.getUrl())
+ .build());
+ }
+
+ private static void initSelenide(Orchestrator orchestrator) {
+ String browser = orchestrator.getConfiguration().getString("orchestrator.browser", "firefox");
+ SelenideConfig.INSTANCE
+ .setBrowser(browser)
+ .setBaseUrl(orchestrator.getServer().getUrl());
+ WebDriverRunner.getWebDriver().manage().deleteAllCookies();
+ }
+
+ private void shouldBeRedirectToMaintenance(String relativeUrl) {
+ Selenide.open(relativeUrl);
+ $("#content").should(hasText("SonarQube is under maintenance"));
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/upgrade/WsCallAndWait.java b/tests/src/test/java/org/sonarqube/tests/upgrade/WsCallAndWait.java
new file mode 100644
index 00000000000..4e4fa645ea2
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/upgrade/WsCallAndWait.java
@@ -0,0 +1,102 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.upgrade;
+
+import com.sonar.orchestrator.Orchestrator;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+import org.sonar.wsclient.jsonsimple.JSONObject;
+import org.sonar.wsclient.jsonsimple.parser.JSONParser;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Throwables.propagate;
+
+public abstract class WsCallAndWait<RESPONSE> {
+ private static final long TIMEOUT_5_MINUTES = 5L * 60 * 1000;
+ private static final long DELAY_3_SECONDS = 3L * 1000;
+
+ private final Orchestrator orchestrator;
+ private final String targetRelativeUrl;
+ private final long timeout;
+ private final long delay;
+
+ protected WsCallAndWait(Orchestrator orchestrator, String targetRelativeUrl, long timeout, long delay) {
+ this.orchestrator = orchestrator;
+ this.targetRelativeUrl = checkNotNull(targetRelativeUrl);
+ this.timeout = timeout;
+ this.delay = delay;
+ }
+
+ protected WsCallAndWait(Orchestrator orchestrator, String targetRelativeUrl) {
+ this(orchestrator, targetRelativeUrl, TIMEOUT_5_MINUTES, DELAY_3_SECONDS);
+ }
+
+ @Nonnull
+ public RESPONSE call() {
+ String response = orchestrator.getServer().wsClient().post(targetRelativeUrl);
+ JSONObject jsonObject = toJsonObject(response);
+ try {
+ return parse(jsonObject);
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to parse JSON response", e);
+ }
+ }
+
+ @CheckForNull
+ public RESPONSE callAndWait() {
+ long endAt = System.currentTimeMillis() + timeout;
+
+ while (System.currentTimeMillis() < endAt) {
+ RESPONSE response = call();
+ if (shouldWait(response)) {
+ sleepQuietly(delay);
+ } else {
+ return response;
+ }
+ }
+ return null;
+ }
+
+ private void sleepQuietly(long rateInMs) {
+ try {
+ Thread.sleep(rateInMs);
+ } catch (InterruptedException e) {
+ propagate(e);
+ }
+ }
+
+ private JSONObject toJsonObject(String s) {
+ try {
+ JSONParser parser = new JSONParser();
+ Object o = parser.parse(s);
+ if (o instanceof JSONObject) {
+ return (JSONObject) o;
+ }
+ throw new RuntimeException("Can not parse response from server migration WS (not a JSON object)");
+ } catch (Exception e) {
+ throw new IllegalStateException("Invalid JSON: " + s, e);
+ }
+ }
+
+ @Nonnull
+ protected abstract RESPONSE parse(JSONObject jsonObject);
+
+ protected abstract boolean shouldWait(RESPONSE response);
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/user/BaseIdentityProviderTest.java b/tests/src/test/java/org/sonarqube/tests/user/BaseIdentityProviderTest.java
new file mode 100644
index 00000000000..65cd095d285
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/user/BaseIdentityProviderTest.java
@@ -0,0 +1,324 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.user;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category4Suite;
+import java.io.File;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.user.CreateRequest;
+import org.sonarqube.pageobjects.Navigation;
+import util.user.UserRule;
+import util.user.Users;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.guava.api.Assertions.assertThat;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.resetSettings;
+import static util.ItUtils.setServerProperty;
+import static util.selenium.Selenese.runSelenese;
+
+/**
+ * TODO : Add missing ITs
+ * - display multiple identity provider plugins (probably in another class)
+ */
+public class BaseIdentityProviderTest {
+
+ @ClassRule
+ public static Orchestrator ORCHESTRATOR = Category4Suite.ORCHESTRATOR;
+
+ @ClassRule
+ public static UserRule userRule = UserRule.from(ORCHESTRATOR);
+
+ static String FAKE_PROVIDER_KEY = "fake-base-id-provider";
+
+ static String USER_LOGIN = "john";
+ static String USER_PROVIDER_ID = "fake-john";
+ static String USER_NAME = "John";
+ static String USER_EMAIL = "john@email.com";
+
+ static String USER_NAME_UPDATED = "John Doe";
+ static String USER_EMAIL_UPDATED = "john.doe@email.com";
+
+ static String GROUP1 = "group1";
+ static String GROUP2 = "group2";
+ static String GROUP3 = "group3";
+
+ static WsClient adminWsClient;
+
+ @BeforeClass
+ public static void setUp() {
+ ORCHESTRATOR.resetData();
+ adminWsClient = newAdminWsClient(ORCHESTRATOR);
+ }
+
+ @Before
+ @After
+ public void resetData() throws Exception {
+ userRule.resetUsers();
+ userRule.removeGroups(GROUP1, GROUP2, GROUP3);
+ resetSettings(ORCHESTRATOR, null,
+ "sonar.auth.fake-base-id-provider.enabled",
+ "sonar.auth.fake-base-id-provider.user",
+ "sonar.auth.fake-base-id-provider.throwUnauthorizedMessage",
+ "sonar.auth.fake-base-id-provider.enabledGroupsSync",
+ "sonar.auth.fake-base-id-provider.groups",
+ "sonar.auth.fake-base-id-provider.allowsUsersToSignUp");
+ }
+
+ @Test
+ public void create_new_user_when_authenticate() throws Exception {
+ enablePlugin();
+ setUserCreatedByAuthPlugin(USER_LOGIN, USER_PROVIDER_ID, USER_NAME, USER_EMAIL);
+
+ userRule.verifyUserDoesNotExist(USER_LOGIN);
+
+ // First connection, user is created
+ authenticateWithFakeAuthProvider();
+
+ userRule.verifyUserExists(USER_LOGIN, USER_NAME, USER_EMAIL, false);
+ }
+
+ @Test
+ public void authenticate_user_through_ui() throws Exception {
+ enablePlugin();
+ setUserCreatedByAuthPlugin(USER_LOGIN, USER_PROVIDER_ID, USER_NAME, USER_EMAIL);
+
+ Navigation.create(ORCHESTRATOR).openLogin().useOAuth2().shouldBeLoggedIn();
+
+ userRule.verifyUserExists(USER_LOGIN, USER_NAME, USER_EMAIL);
+ }
+
+ @Test
+ public void display_unauthorized_page_when_authentication_failed() throws Exception {
+ enablePlugin();
+ // As this property is null, the plugin will throw an exception
+ setServerProperty(ORCHESTRATOR, "sonar.auth.fake-base-id-provider.user", null);
+
+ runSelenese(ORCHESTRATOR, "/user/BaseIdentityProviderTest/display_unauthorized_page_when_authentication_failed.html");
+
+ userRule.verifyUserDoesNotExist(USER_LOGIN);
+ }
+
+ @Test
+ public void fail_when_email_already_exists() throws Exception {
+ enablePlugin();
+ setUserCreatedByAuthPlugin(USER_LOGIN, USER_PROVIDER_ID, USER_NAME, USER_EMAIL);
+ userRule.createUser("another", "Another", USER_EMAIL, "another");
+
+ runSelenese(ORCHESTRATOR, "/user/BaseIdentityProviderTest/fail_when_email_already_exists.html");
+
+ File logFile = ORCHESTRATOR.getServer().getWebLogs();
+ assertThat(FileUtils.readFileToString(logFile))
+ .doesNotContain("You can't sign up because email 'john@email.com' is already used by an existing user. This means that you probably already registered with another account");
+ }
+
+ @Test
+ public void fail_to_authenticate_when_not_allowed_to_sign_up() throws Exception {
+ enablePlugin();
+ setUserCreatedByAuthPlugin(USER_LOGIN, USER_PROVIDER_ID, USER_NAME, USER_EMAIL);
+ setServerProperty(ORCHESTRATOR, "sonar.auth.fake-base-id-provider.allowsUsersToSignUp", "false");
+
+ runSelenese(ORCHESTRATOR, "/user/BaseIdentityProviderTest/fail_to_authenticate_when_not_allowed_to_sign_up.html");
+
+ userRule.verifyUserDoesNotExist(USER_LOGIN);
+ }
+
+ @Test
+ public void update_existing_user_when_authenticate() throws Exception {
+ enablePlugin();
+ setUserCreatedByAuthPlugin(USER_LOGIN, USER_PROVIDER_ID, USER_NAME, USER_EMAIL);
+
+ // First connection, user is created
+ authenticateWithFakeAuthProvider();
+
+ setUserCreatedByAuthPlugin(USER_LOGIN, USER_PROVIDER_ID, USER_NAME_UPDATED, USER_EMAIL_UPDATED);
+
+ // Second connection, user should be updated
+ authenticateWithFakeAuthProvider();
+
+ userRule.verifyUserExists(USER_LOGIN, USER_NAME_UPDATED, USER_EMAIL_UPDATED);
+ }
+
+ @Test
+ public void reactivate_disabled_user() throws Exception {
+ enablePlugin();
+ setUserCreatedByAuthPlugin(USER_LOGIN, USER_PROVIDER_ID, USER_NAME, USER_EMAIL);
+
+ userRule.verifyUserDoesNotExist(USER_LOGIN);
+
+ // First connection, user is created
+ authenticateWithFakeAuthProvider();
+
+ Optional<Users.User> user = userRule.getUserByLogin(USER_LOGIN);
+ assertThat(user).isPresent();
+
+ // Disable user
+ userRule.deactivateUsers(USER_LOGIN);
+
+ // Second connection, user is reactivated
+ authenticateWithFakeAuthProvider();
+ userRule.verifyUserExists(USER_LOGIN, USER_NAME, USER_EMAIL);
+ }
+
+ @Test
+ public void not_authenticate_when_plugin_is_disabled() throws Exception {
+ setServerProperty(ORCHESTRATOR, "sonar.auth.fake-base-id-provider.enabled", "false");
+ setUserCreatedByAuthPlugin(USER_LOGIN, USER_PROVIDER_ID, USER_NAME, USER_EMAIL);
+
+ authenticateWithFakeAuthProvider();
+
+ // User is not created as nothing plugin is disabled
+ userRule.verifyUserDoesNotExist(USER_LOGIN);
+
+ // TODO Add Selenium test to check login form
+ }
+
+ @Test
+ public void display_message_in_ui_but_not_in_log_when_unauthorized_exception() throws Exception {
+ enablePlugin();
+ setUserCreatedByAuthPlugin(USER_LOGIN, USER_PROVIDER_ID, USER_NAME, USER_EMAIL);
+ setServerProperty(ORCHESTRATOR, "sonar.auth.fake-base-id-provider.throwUnauthorizedMessage", "true");
+
+ runSelenese(ORCHESTRATOR,
+ "/user/BaseIdentityProviderTest/display_message_in_ui_but_not_in_log_when_unauthorized_exception.html");
+
+ File logFile = ORCHESTRATOR.getServer().getWebLogs();
+ assertThat(FileUtils.readFileToString(logFile)).doesNotContain("A functional error has happened");
+ assertThat(FileUtils.readFileToString(logFile)).doesNotContain("UnauthorizedException");
+
+ userRule.verifyUserDoesNotExist(USER_LOGIN);
+ }
+
+ @Test
+ public void synchronize_groups_for_new_user() throws Exception {
+ enablePlugin();
+ userRule.createGroup(GROUP1);
+ userRule.createGroup(GROUP2);
+ setUserCreatedByAuthPlugin(USER_LOGIN, USER_PROVIDER_ID, USER_NAME, USER_EMAIL);
+ // Group3 doesn't exist in DB, user won't belong to this group
+ setGroupsReturnedByAuthPlugin(GROUP1, GROUP2, GROUP3);
+
+ authenticateWithFakeAuthProvider();
+
+ userRule.verifyUserGroupMembership(USER_LOGIN, GROUP1, GROUP2, "sonar-users");
+ }
+
+ @Test
+ public void synchronize_groups_for_existing_user() throws Exception {
+ enablePlugin();
+ userRule.createGroup(GROUP1);
+ userRule.createGroup(GROUP2);
+ userRule.createGroup(GROUP3);
+ userRule.createUser(USER_LOGIN, "password");
+ userRule.associateGroupsToUser(USER_LOGIN, GROUP1, GROUP2);
+ setUserCreatedByAuthPlugin(USER_LOGIN, USER_PROVIDER_ID, USER_NAME, USER_EMAIL);
+ // Group1 is not returned by the plugin, user won't belong anymore to this group
+ setGroupsReturnedByAuthPlugin(GROUP2, GROUP3);
+
+ authenticateWithFakeAuthProvider();
+
+ userRule.verifyUserGroupMembership(USER_LOGIN, GROUP2, GROUP3, "sonar-users");
+ }
+
+ @Test
+ public void remove_user_groups_when_groups_provided_by_plugin_are_empty() throws Exception {
+ enablePlugin();
+ userRule.createGroup(GROUP1);
+ userRule.createUser(USER_LOGIN, "password");
+ userRule.associateGroupsToUser(USER_LOGIN, GROUP1);
+ setUserCreatedByAuthPlugin(USER_LOGIN, USER_PROVIDER_ID, USER_NAME, USER_EMAIL);
+ // No group is returned by the plugin
+ setGroupsReturnedByAuthPlugin();
+
+ authenticateWithFakeAuthProvider();
+
+ // User is not member to any group
+ userRule.verifyUserGroupMembership(USER_LOGIN, "sonar-users");
+ }
+
+ @Test
+ public void allow_user_login_with_2_characters() throws Exception {
+ enablePlugin();
+ String login = "jo";
+ setUserCreatedByAuthPlugin(login, USER_PROVIDER_ID, USER_NAME, USER_EMAIL);
+ userRule.verifyUserDoesNotExist(login);
+
+ // First connection, user is created
+ authenticateWithFakeAuthProvider();
+
+ userRule.verifyUserExists(login, USER_NAME, USER_EMAIL, false);
+ }
+
+ @Test
+ public void provision_user_before_authentication() {
+ enablePlugin();
+ setUserCreatedByAuthPlugin(USER_LOGIN, USER_PROVIDER_ID, USER_NAME, USER_EMAIL);
+
+ // Provision none local user in database
+ newAdminWsClient(ORCHESTRATOR).users().create(CreateRequest.builder()
+ .setLogin(USER_LOGIN)
+ .setName(USER_NAME)
+ .setEmail(USER_EMAIL)
+ .setLocal(false)
+ .build());
+ assertThat(userRule.getUserByLogin(USER_LOGIN).get())
+ .extracting(Users.User::isLocal, Users.User::getExternalIdentity, Users.User::getExternalProvider)
+ .containsOnly(false, USER_LOGIN, "sonarqube");
+
+ // Authenticate with external system -> It will update external provider info
+ authenticateWithFakeAuthProvider();
+
+ assertThat(userRule.getUserByLogin(USER_LOGIN).get())
+ .extracting(Users.User::isLocal, Users.User::getExternalIdentity, Users.User::getExternalProvider)
+ .containsOnly(false, USER_PROVIDER_ID, FAKE_PROVIDER_KEY);
+ }
+
+ private static void enablePlugin() {
+ setServerProperty(ORCHESTRATOR, "sonar.auth.fake-base-id-provider.enabled", "true");
+ }
+
+ private static void setUserCreatedByAuthPlugin(String login, String providerId, String name, String email) {
+ setServerProperty(ORCHESTRATOR, "sonar.auth.fake-base-id-provider.user", login + "," + providerId + "," + name + "," + email);
+ }
+
+ private static void setGroupsReturnedByAuthPlugin(String... groups) {
+ setServerProperty(ORCHESTRATOR, "sonar.auth.fake-base-id-provider.enabledGroupsSync", "true");
+ if (groups.length > 0) {
+ setServerProperty(ORCHESTRATOR, "sonar.auth.fake-base-id-provider.groups", Joiner.on(",").join(groups));
+ }
+ }
+
+ private static void authenticateWithFakeAuthProvider() {
+ adminWsClient.wsConnector().call(
+ new GetRequest("/sessions/init/" + FAKE_PROVIDER_KEY))
+ .failIfNotSuccessful();
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/user/FavoritesWsTest.java b/tests/src/test/java/org/sonarqube/tests/user/FavoritesWsTest.java
new file mode 100644
index 00000000000..fbcd3181ad3
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/user/FavoritesWsTest.java
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.user;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category4Suite;
+import java.util.List;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.Favorites.Favorite;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.favorite.SearchRequest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.projectDir;
+
+/**
+ * TODO This test should not require an analysis, only provisioning the project should be enough
+ */
+public class FavoritesWsTest {
+
+ @ClassRule
+ public static final Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+ private static WsClient adminClient;
+
+ @Before
+ public void inspectProject() {
+ orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")));
+ adminClient = newAdminWsClient(orchestrator);
+ }
+
+ @Test
+ public void favorites_web_service() {
+ // GET (nothing)
+ List<Favorite> favorites = adminClient.favorites().search(new SearchRequest()).getFavoritesList();
+ assertThat(favorites).isEmpty();
+
+ // POST (create favorites)
+ adminClient.favorites().add("sample");
+ adminClient.favorites().add("sample:src/main/xoo/sample/Sample.xoo");
+
+ // GET (created favorites)
+ favorites = adminClient.favorites().search(new SearchRequest()).getFavoritesList();
+ assertThat(favorites.stream().map(Favorite::getKey)).containsOnly("sample", "sample:src/main/xoo/sample/Sample.xoo");
+
+ // DELETE (a favorite)
+ adminClient.favorites().remove("sample");
+ favorites = adminClient.favorites().search(new SearchRequest()).getFavoritesList();
+ assertThat(favorites.stream().map(Favorite::getKey)).containsOnly("sample:src/main/xoo/sample/Sample.xoo");
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/user/ForceAuthenticationTest.java b/tests/src/test/java/org/sonarqube/tests/user/ForceAuthenticationTest.java
new file mode 100644
index 00000000000..1f0ae8f71ee
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/user/ForceAuthenticationTest.java
@@ -0,0 +1,132 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.user;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category4Suite;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.WsUsers.CreateWsResponse.User;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.WsConnector;
+import org.sonarqube.ws.client.WsRequest;
+import org.sonarqube.ws.client.WsResponse;
+import org.sonarqube.pageobjects.Navigation;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonarqube.ws.client.WsRequest.Method.GET;
+import static org.sonarqube.ws.client.WsRequest.Method.POST;
+import static util.ItUtils.resetSettings;
+import static util.ItUtils.setServerProperty;
+
+public class ForceAuthenticationTest {
+
+ @ClassRule
+ public static final Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+
+ @Rule
+ public Tester tester = new Tester(orchestrator).disableOrganizations();
+ private User user;
+
+ @Before
+ public void setUp() {
+ setServerProperty(orchestrator, "sonar.forceAuthentication", "true");
+ user = tester.users().generate();
+ }
+
+ @After
+ public void tearDown() {
+ resetSettings(orchestrator, null, "sonar.forceAuthentication");
+ }
+
+ @Test
+ public void batch_ws_does_not_require_authentication() {
+ WsConnector anonymousConnector = tester.asAnonymous().wsClient().wsConnector();
+ WsResponse batchIndex = anonymousConnector.call(new GetRequest("/batch/index")).failIfNotSuccessful();
+ String batchIndexContent = batchIndex.content();
+
+ assertThat(batchIndexContent).isNotEmpty();
+ String jar = batchIndexContent.split("\\|")[0];
+
+ assertThat(anonymousConnector.call(
+ new GetRequest("/batch/file").setParam("name", jar)).failIfNotSuccessful().contentStream()).isNotNull();
+
+ // As sonar-runner is still using deprecated /batch/key, we have to also verify it
+ assertThat(anonymousConnector.call(new GetRequest("/batch/" + jar)).failIfNotSuccessful().contentStream()).isNotNull();
+ }
+
+ @Test
+ public void authentication_ws_does_not_require_authentication() {
+ WsConnector anonymousConnector = tester.asAnonymous().wsClient().wsConnector();
+ assertThat(anonymousConnector.call(new PostRequest("/api/authentication/login")
+ .setParam("login", user.getLogin())
+ .setParam("password", user.getLogin())).isSuccessful()).isTrue();
+ verifyPathDoesNotRequiresAuthentication("/api/authentication/logout", POST);
+ }
+
+ @Test
+ public void check_ws_not_requiring_authentication() {
+ verifyPathDoesNotRequiresAuthentication("/api/system/db_migration_status", GET);
+ verifyPathDoesNotRequiresAuthentication("/api/system/status", GET);
+ verifyPathDoesNotRequiresAuthentication("/api/system/migrate_db", POST);
+ verifyPathDoesNotRequiresAuthentication("/api/users/identity_providers", GET);
+ verifyPathDoesNotRequiresAuthentication("/api/l10n/index", GET);
+ }
+
+ @Test
+ public void check_ws_requiring_authentication() {
+ verifyPathRequiresAuthentication("/api/issues/search", GET);
+ verifyPathRequiresAuthentication("/api/rules/search", GET);
+ }
+
+ @Test
+ public void redirect_to_login_page() {
+ User administrator = tester.users().generateAdministrator();
+ Navigation page = tester.openBrowser().openHome();
+ page.shouldBeRedirectedToLogin();
+ page.openLogin().submitCredentials(administrator.getLogin()).shouldBeLoggedIn();
+ page.logOut().shouldBeRedirectedToLogin();
+ }
+
+ private void verifyPathRequiresAuthentication(String path, WsRequest.Method method) {
+ assertThat(call(tester.asAnonymous().wsClient(), path, method).code()).isEqualTo(401);
+ WsResponse wsResponse = call(tester.wsClient(), path, method);
+ assertThat(wsResponse.isSuccessful()).as("code is %s on path %s", wsResponse.code(), path).isTrue();
+ }
+
+ private void verifyPathDoesNotRequiresAuthentication(String path, WsRequest.Method method) {
+ WsResponse wsResponse = call(tester.asAnonymous().wsClient(), path, method);
+ assertThat(wsResponse.isSuccessful()).as("code is %s on path %s", wsResponse.code(), path).isTrue();
+ wsResponse = call(tester.wsClient(), path, method);
+ assertThat(wsResponse.isSuccessful()).as("code is %s on path %s", wsResponse.code(), path).isTrue();
+ }
+
+ private WsResponse call(WsClient client, String path, WsRequest.Method method) {
+ WsRequest request = method.equals(GET) ? new GetRequest(path) : new PostRequest(path);
+ return client.wsConnector().call(request);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/user/LocalAuthenticationTest.java b/tests/src/test/java/org/sonarqube/tests/user/LocalAuthenticationTest.java
new file mode 100644
index 00000000000..3f53ba7145b
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/user/LocalAuthenticationTest.java
@@ -0,0 +1,234 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.user;
+
+import com.codeborne.selenide.Condition;
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category4Suite;
+import java.util.UUID;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.WsUserTokens;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.HttpConnector;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.WsClientFactories;
+import org.sonarqube.ws.client.WsResponse;
+import org.sonarqube.ws.client.permission.AddUserWsRequest;
+import org.sonarqube.ws.client.usertoken.GenerateWsRequest;
+import org.sonarqube.ws.client.usertoken.RevokeWsRequest;
+import org.sonarqube.ws.client.usertoken.SearchWsRequest;
+import org.sonarqube.ws.client.usertoken.UserTokensService;
+import org.sonarqube.pageobjects.LoginPage;
+import org.sonarqube.pageobjects.Navigation;
+
+import static java.lang.String.format;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.resetSettings;
+import static util.ItUtils.setServerProperty;
+import static util.selenium.Selenese.runSelenese;
+
+public class LocalAuthenticationTest {
+
+ private static final String ADMIN_USER_LOGIN = "admin-user";
+
+ private static final String LOGIN = "george.orwell";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+
+ @Rule
+ public Tester tester = new Tester(orchestrator).disableOrganizations();
+
+ @Before
+ public void setUp() {
+ tester.users().generate(u -> u.setLogin(LOGIN).setPassword("123456"));
+ addUserPermission(LOGIN, "admin");
+
+ tester.users().generate(u -> u.setLogin("simple-user").setPassword("password"));
+ tester.users().generateAdministrator(u -> u.setLogin(ADMIN_USER_LOGIN).setPassword(ADMIN_USER_LOGIN));
+ }
+
+ @After
+ public void resetProperties() throws Exception {
+ resetSettings(orchestrator, null, "sonar.forceAuthentication");
+ }
+
+ @Test
+ public void log_in_with_correct_credentials_then_log_out() {
+ Navigation nav = tester.openBrowser();
+ nav.shouldNotBeLoggedIn();
+ nav.logIn().submitCredentials(LOGIN, "123456").shouldBeLoggedIn();
+ nav.logOut().shouldNotBeLoggedIn();
+ }
+
+ @Test
+ public void log_in_with_wrong_credentials() {
+ Navigation nav = tester.openBrowser();
+ LoginPage page = nav
+ .logIn()
+ .submitWrongCredentials(LOGIN, "wrong");
+ page.getErrorMessage().shouldHave(Condition.text("Authentication failed"));
+
+ nav.openHome();
+ nav.shouldNotBeLoggedIn();
+ }
+
+ @Test
+ public void basic_authentication_based_on_login_and_password() {
+ String userId = UUID.randomUUID().toString();
+ String login = format("login-%s", userId);
+ String name = format("name-%s", userId);
+ String password = "!ascii-only:-)@";
+ tester.users().generate(u -> u.setLogin(login).setName(name).setPassword(password));
+
+ // authenticate
+ WsClient wsClient = tester.as(login, password).wsClient();
+ WsResponse response = wsClient.wsConnector().call(new GetRequest("api/authentication/validate"));
+ assertThat(response.content()).isEqualTo("{\"valid\":true}");
+ }
+
+ @Test
+ public void basic_authentication_based_on_token() {
+ String tokenName = "Validate token based authentication";
+ UserTokensService tokensService = tester.wsClient().userTokens();
+ WsUserTokens.GenerateWsResponse generateWsResponse = tokensService.generate(new GenerateWsRequest()
+ .setLogin(LOGIN)
+ .setName(tokenName));
+ WsClient wsClient = WsClientFactories.getDefault().newClient(HttpConnector.newBuilder()
+ .url(orchestrator.getServer().getUrl())
+ .token(generateWsResponse.getToken()).build());
+
+ WsResponse response = wsClient.wsConnector().call(new GetRequest("api/authentication/validate"));
+
+ assertThat(response.content()).isEqualTo("{\"valid\":true}");
+
+ WsUserTokens.SearchWsResponse searchResponse = tokensService.search(new SearchWsRequest().setLogin(LOGIN));
+ assertThat(searchResponse.getUserTokensCount()).isEqualTo(1);
+ tokensService.revoke(new RevokeWsRequest().setLogin(LOGIN).setName(tokenName));
+ searchResponse = tokensService.search(new SearchWsRequest().setLogin(LOGIN));
+ assertThat(searchResponse.getUserTokensCount()).isEqualTo(0);
+ }
+
+ @Test
+ @Ignore
+ public void web_login_form_should_support_utf8_passwords() {
+ // TODO selenium
+ }
+
+ @Test
+ public void basic_authentication_does_not_support_utf8_passwords() {
+ String login = "user_with_utf8_password";
+ // see http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt
+ String password = "κόσμε";
+
+ // create user with a UTF-8 password
+ tester.users().generate(u -> u.setLogin(login).setPassword(password));
+
+ // authenticate
+ assertThat(checkAuthenticationWithAuthenticateWebService(login, password)).isFalse();
+ }
+
+ @Test
+ public void allow_user_login_with_2_characters() throws Exception {
+ tester.users().generate(u -> u.setLogin("jo").setPassword("password"));
+
+ assertThat(checkAuthenticationWithAuthenticateWebService("jo", "password")).isTrue();
+ }
+
+ @Test
+ public void authentication_through_ui() {
+ runSelenese(orchestrator,
+ "/user/LocalAuthenticationTest/login_successful.html",
+ "/user/LocalAuthenticationTest/login_wrong_password.html",
+ "/user/LocalAuthenticationTest/should_not_be_unlogged_when_going_to_login_page.html",
+ "/user/LocalAuthenticationTest/redirect_to_login_when_not_enough_privilege.html",
+ // SONAR-2132
+ "/user/LocalAuthenticationTest/redirect_to_original_url_after_direct_login.html",
+ "/user/LocalAuthenticationTest/redirect_to_original_url_with_parameters_after_direct_login.html",
+ // SONAR-2009
+ "/user/LocalAuthenticationTest/redirect_to_original_url_after_indirect_login.html");
+
+ setServerProperty(orchestrator, "sonar.forceAuthentication", "true");
+
+ runSelenese(orchestrator,
+ // SONAR-3473
+ "/user/LocalAuthenticationTest/force-authentication.html");
+ }
+
+ @Test
+ public void authentication_with_authentication_ws() {
+ assertThat(checkAuthenticationWithAuthenticateWebService("admin", "admin")).isTrue();
+ assertThat(checkAuthenticationWithAuthenticateWebService("wrong", "admin")).isFalse();
+ assertThat(checkAuthenticationWithAuthenticateWebService("admin", "wrong")).isFalse();
+ assertThat(checkAuthenticationWithAuthenticateWebService(null, null)).isTrue();
+
+ setServerProperty(orchestrator, "sonar.forceAuthentication", "true");
+
+ assertThat(checkAuthenticationWithAuthenticateWebService("admin", "admin")).isTrue();
+ assertThat(checkAuthenticationWithAuthenticateWebService("wrong", "admin")).isFalse();
+ assertThat(checkAuthenticationWithAuthenticateWebService("admin", "wrong")).isFalse();
+ assertThat(checkAuthenticationWithAuthenticateWebService(null, null)).isFalse();
+ }
+
+ /**
+ * SONAR-7640
+ */
+ @Test
+ public void authentication_with_any_ws() throws Exception {
+ assertThat(checkAuthenticationWithAnyWS("admin", "admin").code()).isEqualTo(200);
+ assertThat(checkAuthenticationWithAnyWS("wrong", "admin").code()).isEqualTo(401);
+ assertThat(checkAuthenticationWithAnyWS("admin", "wrong").code()).isEqualTo(401);
+ assertThat(checkAuthenticationWithAnyWS("admin", null).code()).isEqualTo(401);
+ assertThat(checkAuthenticationWithAnyWS(null, null).code()).isEqualTo(200);
+
+ setServerProperty(orchestrator, "sonar.forceAuthentication", "true");
+
+ assertThat(checkAuthenticationWithAnyWS("admin", "admin").code()).isEqualTo(200);
+ assertThat(checkAuthenticationWithAnyWS("wrong", "admin").code()).isEqualTo(401);
+ assertThat(checkAuthenticationWithAnyWS("admin", "wrong").code()).isEqualTo(401);
+ assertThat(checkAuthenticationWithAnyWS("admin", null).code()).isEqualTo(401);
+ assertThat(checkAuthenticationWithAnyWS(null, null).code()).isEqualTo(401);
+ }
+
+ private boolean checkAuthenticationWithAuthenticateWebService(String login, String password) {
+ String result = tester.as(login, password).wsClient().wsConnector().call(new PostRequest("/api/authentication/validate")).content();
+ return result.contains("{\"valid\":true}");
+ }
+
+ private WsResponse checkAuthenticationWithAnyWS(String login, String password) {
+ WsClient wsClient = WsClientFactories.getDefault().newClient(HttpConnector.newBuilder().url(orchestrator.getServer().getUrl()).credentials(login, password).build());
+ // Call any WS
+ return wsClient.wsConnector().call(new GetRequest("api/rules/search"));
+ }
+
+ private void addUserPermission(String login, String permission) {
+ tester.wsClient().permissions().addUser(new AddUserWsRequest()
+ .setLogin(login)
+ .setPermission(permission));
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/user/MyAccountPageTest.java b/tests/src/test/java/org/sonarqube/tests/user/MyAccountPageTest.java
new file mode 100644
index 00000000000..de2c4a992d3
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/user/MyAccountPageTest.java
@@ -0,0 +1,131 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.user;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category4Suite;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.WsUsers.CreateWsResponse.User;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.pageobjects.Navigation;
+
+import static com.codeborne.selenide.Condition.text;
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+import static util.ItUtils.projectDir;
+import static util.selenium.Selenese.runSelenese;
+
+public class MyAccountPageTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+
+ @Rule
+ public Tester tester = new Tester(orchestrator).disableOrganizations();
+
+ private User administrator;
+
+ @Before
+ public void initUser() {
+ administrator = tester.users().generateAdministrator();
+ createUser("account-user", "User With Account", "user@example.com");
+ }
+
+ @Test
+ public void should_display_user_details() throws Exception {
+ Navigation nav = tester.openBrowser();
+ nav.openLogin().submitCredentials("account-user", "password").shouldBeLoggedIn();
+ nav.open("/account");
+ $("#name").shouldHave(text("User With Account"));
+ $("#login").shouldHave(text("account-user"));
+ $("#email").shouldHave(text("user@example.com"));
+ $("#groups").shouldHave(text("sonar-users"));
+ $("#scm-accounts").shouldHave(text("user@example.com"));
+ $("#avatar").shouldBe(visible);
+ }
+
+ @Test
+ public void should_change_password() throws Exception {
+ Navigation nav = tester.openBrowser();
+ nav.openLogin().submitCredentials("account-user", "password").shouldBeLoggedIn();
+ nav.open("/account/security");
+ $("#old_password").val("password");
+ $("#password").val("new_password");
+ $("#password_confirmation").val("new_password");
+ $("#change-password").click();
+ $(".alert-success").shouldBe(visible);
+ nav.logOut().logIn().submitCredentials("account-user", "new_password").shouldBeLoggedIn();
+ }
+
+ @Test
+ public void should_display_projects() throws Exception {
+ // first, try on empty instance
+ runSelenese(orchestrator, "/user/MyAccountPageTest/should_display_no_projects.html");
+
+ // then, analyze a project
+ analyzeProject("sample");
+ grantAdminPermission("account-user", "sample");
+
+ runSelenese(orchestrator, "/user/MyAccountPageTest/should_display_projects.html");
+ }
+
+ @Test
+ public void notifications() {
+ Navigation nav = tester.openBrowser();
+ nav.logIn().submitCredentials(administrator.getLogin()).openNotifications()
+ .addGlobalNotification("ChangesOnMyIssue")
+ .addGlobalNotification("NewIssues")
+ .removeGlobalNotification("ChangesOnMyIssue");
+
+ nav.openNotifications()
+ .shouldHaveGlobalNotification("NewIssues")
+ .shouldNotHaveGlobalNotification("ChangesOnMyIssue");
+ }
+
+ private void createUser(String login, String name, String email) {
+ tester.wsClient().wsConnector().call(
+ new PostRequest("api/users/create")
+ .setParam("login", login)
+ .setParam("name", name)
+ .setParam("email", email)
+ .setParam("password", "password"));
+ }
+
+ private static void analyzeProject(String projectKey) {
+ SonarScanner build = SonarScanner.create(projectDir("qualitygate/xoo-sample"))
+ .setProjectKey(projectKey)
+ .setProperty("sonar.projectDescription", "Description of a project")
+ .setProperty("sonar.links.homepage", "http://example.com");
+ orchestrator.executeBuild(build);
+ }
+
+ private void grantAdminPermission(String login, String projectKey) {
+ tester.wsClient().wsConnector().call(
+ new PostRequest("api/permissions/add_user")
+ .setParam("login", login)
+ .setParam("projectKey", projectKey)
+ .setParam("permission", "admin"));
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/user/OAuth2IdentityProviderTest.java b/tests/src/test/java/org/sonarqube/tests/user/OAuth2IdentityProviderTest.java
new file mode 100644
index 00000000000..642a7351ccd
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/user/OAuth2IdentityProviderTest.java
@@ -0,0 +1,220 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.user;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category4Suite;
+import java.io.File;
+import java.net.HttpURLConnection;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.WsUsers.SearchWsResponse.User;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.WsResponse;
+import org.sonarqube.ws.client.user.CreateRequest;
+import org.sonarqube.pageobjects.Navigation;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.resetSettings;
+import static util.ItUtils.setServerProperty;
+import static util.selenium.Selenese.runSelenese;
+
+/**
+ * There's only tests specific to OAuth2 in this class
+ */
+public class OAuth2IdentityProviderTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+
+ private static String FAKE_PROVIDER_KEY = "fake-oauth2-id-provider";
+
+ private static String USER_LOGIN = "john";
+ private static String USER_PROVIDER_ID = "fake-john";
+ private static String USER_NAME = "John";
+ private static String USER_EMAIL = "john@email.com";
+
+ @Rule
+ public Tester tester = new Tester(orchestrator).disableOrganizations();
+
+ private MockWebServer fakeServerAuthProvider;
+ private String fakeServerAuthProviderUrl;
+
+ @Before
+ public void setUp() throws Exception {
+ fakeServerAuthProvider = new MockWebServer();
+ fakeServerAuthProvider.start();
+ fakeServerAuthProviderUrl = fakeServerAuthProvider.url("").url().toString();
+ resetData();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ resetData();
+ fakeServerAuthProvider.shutdown();
+ }
+
+ private void resetData() {
+ resetSettings(orchestrator, null,
+ "sonar.auth.fake-oauth2-id-provider.enabled",
+ "sonar.auth.fake-oauth2-id-provider.url",
+ "sonar.auth.fake-oauth2-id-provider.user",
+ "sonar.auth.fake-oauth2-id-provider.throwUnauthorizedMessage",
+ "sonar.auth.fake-oauth2-id-provider.allowsUsersToSignUp");
+ }
+
+ @Test
+ public void create_user_when_authenticating_for_the_first_time() {
+ simulateRedirectionToCallback();
+ enablePlugin();
+
+ authenticateWithFakeAuthProvider();
+
+ verifyUser(USER_LOGIN, USER_NAME, USER_EMAIL);
+ }
+
+ private void verifyUser(String login, String name, String email) {
+ User user = tester.users().getByLogin(login).orElseThrow(IllegalStateException::new);
+ assertThat(user.getLogin()).isEqualTo(login);
+ assertThat(user.getName()).isEqualTo(name);
+ assertThat(user.getEmail()).isEqualTo(email);
+ assertThat(user.getActive()).isTrue();
+ }
+
+ @Test
+ public void authenticate_user_through_ui() throws Exception {
+ simulateRedirectionToCallback();
+ enablePlugin();
+
+ Navigation nav = tester.openBrowser();
+ nav.openLogin().useOAuth2().shouldBeLoggedIn();
+
+ verifyUser(USER_LOGIN, USER_NAME, USER_EMAIL);
+ }
+
+ @Test
+ public void display_unauthorized_page_when_authentication_failed_in_callback() throws Exception {
+ simulateRedirectionToCallback();
+ enablePlugin();
+
+ // As this property is null, the plugin will throw an exception
+ setServerProperty(orchestrator, "sonar.auth.fake-oauth2-id-provider.user", null);
+
+ runSelenese(orchestrator, "/user/OAuth2IdentityProviderTest/display_unauthorized_page_when_authentication_failed.html");
+
+ assertThatUserDoesNotExist(USER_LOGIN);
+ }
+
+ @Test
+ public void fail_to_authenticate_when_not_allowed_to_sign_up() throws Exception {
+ simulateRedirectionToCallback();
+ enablePlugin();
+ setServerProperty(orchestrator, "sonar.auth.fake-oauth2-id-provider.allowsUsersToSignUp", "false");
+
+ runSelenese(orchestrator, "/user/OAuth2IdentityProviderTest/fail_to_authenticate_when_not_allowed_to_sign_up.html");
+
+ assertThatUserDoesNotExist(USER_LOGIN);
+ }
+
+ @Test
+ public void display_message_in_ui_but_not_in_log_when_unauthorized_exception_in_callback() throws Exception {
+ simulateRedirectionToCallback();
+ enablePlugin();
+ setServerProperty(orchestrator, "sonar.auth.fake-oauth2-id-provider.throwUnauthorizedMessage", "true");
+
+ tester.runHtmlTests("/user/OAuth2IdentityProviderTest/display_message_in_ui_but_not_in_log_when_unauthorized_exception.html");
+
+ File logFile = orchestrator.getServer().getWebLogs();
+ assertThat(FileUtils.readFileToString(logFile)).doesNotContain("A functional error has happened");
+ assertThat(FileUtils.readFileToString(logFile)).doesNotContain("UnauthorizedException");
+
+ assertThatUserDoesNotExist(USER_LOGIN);
+ }
+
+ @Test
+ public void fail_when_email_already_exists() throws Exception {
+ simulateRedirectionToCallback();
+ enablePlugin();
+ tester.users().generate(u -> u.setLogin("another").setName("Another").setEmail(USER_EMAIL).setPassword("another"));
+
+ tester.runHtmlTests("/user/OAuth2IdentityProviderTest/fail_when_email_already_exists.html");
+
+ File logFile = orchestrator.getServer().getWebLogs();
+ assertThat(FileUtils.readFileToString(logFile))
+ .doesNotContain("You can't sign up because email 'john@email.com' is already used by an existing user. This means that you probably already registered with another account");
+ }
+
+ @Test
+ public void provision_user_before_authentication() {
+ simulateRedirectionToCallback();
+ enablePlugin();
+
+ // Provision none local user in database
+ tester.wsClient().users().create(CreateRequest.builder()
+ .setLogin(USER_LOGIN)
+ .setName(USER_NAME)
+ .setEmail(USER_EMAIL)
+ .setLocal(false)
+ .build());
+ User user = tester.users().getByLogin(USER_LOGIN).get();
+ assertThat(user.getLocal()).isFalse();
+ assertThat(user.getExternalIdentity()).isEqualTo(USER_LOGIN);
+ assertThat(user.getExternalProvider()).isEqualTo("sonarqube");
+
+ // Authenticate with external system -> It will update external provider info
+ authenticateWithFakeAuthProvider();
+
+ user = tester.users().getByLogin(USER_LOGIN).get();
+ assertThat(user.getLocal()).isFalse();
+ assertThat(user.getExternalIdentity()).isEqualTo(USER_PROVIDER_ID);
+ assertThat(user.getExternalProvider()).isEqualTo(FAKE_PROVIDER_KEY);
+ }
+
+ private void authenticateWithFakeAuthProvider() {
+ WsResponse response = tester.wsClient().wsConnector().call(
+ new GetRequest(("/sessions/init/" + FAKE_PROVIDER_KEY)));
+ assertThat(response.code()).isEqualTo(200);
+ }
+
+ private void simulateRedirectionToCallback() {
+ fakeServerAuthProvider.enqueue(new MockResponse()
+ .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
+ .addHeader("Location: " + orchestrator.getServer().getUrl() + "/oauth2/callback/" + FAKE_PROVIDER_KEY)
+ .setBody("Redirect to SonarQube"));
+ }
+
+ private void enablePlugin() {
+ setServerProperty(orchestrator, "sonar.auth.fake-oauth2-id-provider.enabled", "true");
+ setServerProperty(orchestrator, "sonar.auth.fake-oauth2-id-provider.url", fakeServerAuthProviderUrl);
+ setServerProperty(orchestrator, "sonar.auth.fake-oauth2-id-provider.user", USER_LOGIN + "," + USER_PROVIDER_ID + "," + USER_NAME + "," + USER_EMAIL);
+ }
+
+ private void assertThatUserDoesNotExist(String login) {
+ assertThat(tester.users().getByLogin(login)).isEmpty();
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/user/OnboardingTest.java b/tests/src/test/java/org/sonarqube/tests/user/OnboardingTest.java
new file mode 100644
index 00000000000..205e17d10ab
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/user/OnboardingTest.java
@@ -0,0 +1,161 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.user;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.WsUsers.CreateWsResponse.User;
+import org.sonarqube.ws.client.WsClient;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.resetSettings;
+import static util.ItUtils.setServerProperty;
+
+public class OnboardingTest {
+
+ private static final String ONBOARDING_TUTORIAL_SHOW_TO_NEW_USERS = "sonar.onboardingTutorial.showToNewUsers";
+
+ @ClassRule
+ public static final Orchestrator orchestrator = Orchestrator.builderEnv().build();
+
+ @Rule
+ public Tester tester = new Tester(orchestrator).disableOrganizations();
+
+ @Before
+ public void setUp() {
+ resetSettings(orchestrator, null, ONBOARDING_TUTORIAL_SHOW_TO_NEW_USERS);
+ }
+
+ @After
+ public void reset() {
+ resetSettings(orchestrator, null, ONBOARDING_TUTORIAL_SHOW_TO_NEW_USERS);
+ }
+
+ @Test
+ public void by_default_new_user_does_not_see_onboarding_tutorial() {
+ User user = tester.users().generate();
+
+ verifyTutorial(user, false);
+ }
+
+ @Test
+ public void new_user_see_onboarding_tutorial_when_show_onboarding_setting_is_enabled() {
+ setShownOnboardingSetting(true);
+ User user = tester.users().generate();
+
+ verifyTutorial(user, true);
+ }
+
+ @Test
+ public void new_user_does_not_see_onboarding_tutorial_when_show_onboarding_setting_is_disabled() {
+ setShownOnboardingSetting(false);
+ User user = tester.users().generate();
+
+ verifyTutorial(user, false);
+ }
+
+ @Test
+ public void new_user_does_not_see_onboarding_tutorial_when_show_onboarding_setting_is_enabled_after_user_creation() {
+ setShownOnboardingSetting(false);
+ // User is created when show onboading is disabled
+ User user = tester.users().generate();
+ setShownOnboardingSetting(true);
+
+ // The user doesn't see the tutorial as he was created when the show onboading setting was disabled
+ verifyTutorial(user, false);
+ }
+
+ @Test
+ public void skip_onboarding_tutorial() {
+ setShownOnboardingSetting(true);
+ User user = tester.users().generate();
+
+ tester.as(user.getLogin()).wsClient().users().skipOnboardingTutorial();
+
+ verifyTutorial(user, false);
+ }
+
+ @Test
+ public void skip_onboarding_tutorial_when_show_onboarding_setting_is_disabled() {
+ setShownOnboardingSetting(true);
+ User user = tester.users().generate();
+
+ tester.as(user.getLogin()).wsClient().users().skipOnboardingTutorial();
+
+ verifyTutorial(user, false);
+ }
+
+ @Test
+ public void anonymous_user_does_not_see_onboarding_tutorial() {
+ setShownOnboardingSetting(true);
+
+ // anonymous should not see the onboarding tutorial
+ verifyTutorialForAnonymous(false);
+
+ // anonymous should not be able to skip the tutorial
+ ItUtils.expectHttpError(401, () -> tester.asAnonymous().wsClient().users().skipOnboardingTutorial());
+ }
+
+ @Test
+ public void admin_user_see_onboarding_tutorial() {
+
+ assertThat(tester.wsClient().users().current().getShowOnboardingTutorial()).isEqualTo(true);
+
+ // Onboarding setting has no effect as admin is created at startup
+ setShownOnboardingSetting(false);
+ assertThat(tester.wsClient().users().current().getShowOnboardingTutorial()).isEqualTo(true);
+
+ setShownOnboardingSetting(true);
+ assertThat(tester.wsClient().users().current().getShowOnboardingTutorial()).isEqualTo(true);
+ }
+
+ @Test
+ public void reactivated_user_should_see_the_onboarding_tutorial() {
+ setShownOnboardingSetting(true);
+ User user = tester.users().generate();
+ tester.as(user.getLogin()).wsClient().users().skipOnboardingTutorial();
+ verifyTutorial(user, false);
+
+ tester.wsClient().users().deactivate(user.getLogin());
+ User reactivatedUser = tester.users().generate(u -> u.setLogin(user.getLogin()).setName(user.getName()).setPassword(user.getLogin()));
+
+ verifyTutorial(reactivatedUser, true);
+ }
+
+ private static void setShownOnboardingSetting(boolean showTutorial) {
+ setServerProperty(orchestrator, ONBOARDING_TUTORIAL_SHOW_TO_NEW_USERS, String.valueOf(showTutorial));
+ }
+
+ private void verifyTutorial(User user, boolean expectedTutorial) {
+ WsClient wsClient = tester.as(user.getLogin()).wsClient();
+ assertThat(wsClient.users().current().getShowOnboardingTutorial()).isEqualTo(expectedTutorial);
+ }
+
+ private void verifyTutorialForAnonymous(boolean expectedTutorial) {
+ WsClient wsClient = tester.asAnonymous().wsClient();
+ assertThat(wsClient.users().current().getShowOnboardingTutorial()).isEqualTo(expectedTutorial);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/user/OrganizationIdentityProviderTest.java b/tests/src/test/java/org/sonarqube/tests/user/OrganizationIdentityProviderTest.java
new file mode 100644
index 00000000000..767c76f70db
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/user/OrganizationIdentityProviderTest.java
@@ -0,0 +1,119 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.user;
+
+import com.google.common.base.Joiner;
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category6Suite;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.WsUserGroups.Group;
+import org.sonarqube.ws.WsUsers.CreateWsResponse.User;
+import org.sonarqube.ws.client.GetRequest;
+
+import static util.ItUtils.resetSettings;
+import static util.ItUtils.setServerProperty;
+
+public class OrganizationIdentityProviderTest {
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category6Suite.ORCHESTRATOR;
+
+ @Rule
+ public Tester tester = new Tester(orchestrator);
+
+
+ @Before
+ public void setUp() {
+ // enable the fake authentication plugin
+ setServerProperty(orchestrator, "sonar.auth.fake-base-id-provider.enabled", "true");
+ }
+
+ @After
+ public void tearDown() {
+ resetSettings(orchestrator, null, "sonar.auth.fake-base-id-provider.enabled", "sonar.auth.fake-base-id-provider.user",
+ "sonar.auth.fake-base-id-provider.throwUnauthorizedMessage", "sonar.auth.fake-base-id-provider.enabledGroupsSync", "sonar.auth.fake-base-id-provider.groups",
+ "sonar.auth.fake-base-id-provider.allowsUsersToSignUp");
+ }
+
+ @Test
+ public void default_group_is_not_added_for_new_user_when_organizations_are_enabled() {
+ Group group = tester.groups().generate(null);
+ enableUserCreationByAuthPlugin("aLogin");
+ setGroupsReturnedByAuthPlugin(group.getName());
+
+ authenticateWithFakeAuthProvider();
+
+ // No default group membership
+ tester.groups().assertThatUserIsOnlyMemberOf(null, "aLogin", group.getName());
+ }
+
+ @Test
+ public void default_group_is_not_sync_for_existing_user_when_organizations_are_enabled() {
+ Group group = tester.groups().generate(null);
+ User user = tester.users().generate();
+ enableUserCreationByAuthPlugin(user.getLogin());
+ setGroupsReturnedByAuthPlugin(group.getName());
+
+ authenticateWithFakeAuthProvider();
+
+ // No default group membership
+ tester.groups().assertThatUserIsOnlyMemberOf(null, user.getLogin(), group.getName());
+ }
+
+ @Test
+ public void remove_default_group_when_organizations_are_enabled() {
+ Group group = tester.groups().generate(null);
+ User user = tester.users().generate();
+ // Add user as member of default organization
+ tester.wsClient().organizations().addMember("default-organization", user.getLogin());
+ tester.groups().assertThatUserIsMemberOf(null, user.getLogin(), "Members");
+ enableUserCreationByAuthPlugin(user.getLogin());
+ // No group is returned by the plugin
+ setGroupsReturnedByAuthPlugin();
+
+ authenticateWithFakeAuthProvider();
+
+ // No default group membership
+ tester.groups().assertThatUserIsOnlyMemberOf(null, user.getLogin());
+ }
+
+ private static void enableUserCreationByAuthPlugin(String login) {
+ setServerProperty(orchestrator, "sonar.auth.fake-base-id-provider.user", login + ",fake-john,John,john@email.com");
+ }
+
+ private static void setGroupsReturnedByAuthPlugin(String... groups) {
+ setServerProperty(orchestrator, "sonar.auth.fake-base-id-provider.enabledGroupsSync", "true");
+ if (groups.length > 0) {
+ setServerProperty(orchestrator, "sonar.auth.fake-base-id-provider.groups", Joiner.on(",").join(groups));
+ }
+ }
+
+ private void authenticateWithFakeAuthProvider() {
+ tester.wsClient().wsConnector().call(
+ new GetRequest("/sessions/init/fake-base-id-provider"))
+ .failIfNotSuccessful();
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/user/RealmAuthenticationTest.java b/tests/src/test/java/org/sonarqube/tests/user/RealmAuthenticationTest.java
new file mode 100644
index 00000000000..5e4170fceb1
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/user/RealmAuthenticationTest.java
@@ -0,0 +1,389 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.user;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.sonar.orchestrator.Orchestrator;
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.wsclient.Host;
+import org.sonar.wsclient.Sonar;
+import org.sonar.wsclient.base.HttpException;
+import org.sonar.wsclient.connectors.HttpClient4Connector;
+import org.sonar.wsclient.services.AuthenticationQuery;
+import org.sonar.wsclient.user.UserParameters;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.WsResponse;
+import org.sonarqube.ws.client.user.CreateRequest;
+import util.user.UserRule;
+import util.user.Users;
+
+import static java.net.HttpURLConnection.HTTP_OK;
+import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.newUserWsClient;
+import static util.ItUtils.pluginArtifact;
+import static util.ItUtils.resetSettings;
+import static util.ItUtils.setServerProperty;
+import static util.selenium.Selenese.runSelenese;
+
+/**
+ * Test REALM authentication.
+ *
+ * It starts its own server as it's using a different authentication system
+ */
+public class RealmAuthenticationTest {
+
+ private static final String TECH_USER = "techUser";
+ private static final String USER_LOGIN = "tester";
+ private static final String ADMIN_USER_LOGIN = "admin-user";
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ /**
+ * Property from security-plugin for user management.
+ */
+ private static final String USERS_PROPERTY = "sonar.fakeauthenticator.users";
+
+ @ClassRule
+ public static final Orchestrator orchestrator = Orchestrator.builderEnv()
+ .addPlugin(pluginArtifact("security-plugin"))
+ .setServerProperty("sonar.security.realm", "FakeRealm")
+ .build();
+
+ @Rule
+ public UserRule userRule = UserRule.from(orchestrator);
+
+ @Before
+ @After
+ public void resetData() throws Exception {
+ resetSettings(orchestrator, null, USERS_PROPERTY, "sonar.security.updateUserAttributes");
+ }
+
+ @Before
+ public void initAdminUser() throws Exception {
+ userRule.createAdminUser(ADMIN_USER_LOGIN, ADMIN_USER_LOGIN);
+ }
+
+ @After
+ public void deleteAdminUser() {
+ userRule.resetUsers();
+ }
+
+ /**
+ * SONAR-3137, SONAR-2292
+ * Restriction on password length (minimum 4 characters) should be disabled, when external system enabled.
+ */
+ @Test
+ public void shouldSynchronizeDetailsAndGroups() {
+ // Given clean Sonar installation and no users in external system
+ String username = USER_LOGIN;
+ String password = "123";
+ Map<String, String> users = Maps.newHashMap();
+
+ // When user created in external system
+ users.put(username + ".password", password);
+ users.put(username + ".name", "Tester Testerovich");
+ users.put(username + ".email", "tester@example.org");
+ users.put(username + ".groups", "sonar-user");
+ updateUsersInExtAuth(users);
+ // Then
+ verifyAuthenticationIsOk(username, password);
+
+ // with external details and groups
+ runSelenese(orchestrator, "/user/ExternalAuthenticationTest/external-user-details.html");
+
+ // SONAR-4462
+ runSelenese(orchestrator, "/user/ExternalAuthenticationTest/system-info.html");
+ }
+
+ /**
+ * SONAR-4034
+ */
+ @Test
+ public void shouldUpdateDetailsByDefault() {
+ // Given clean Sonar installation and no users in external system
+ String username = USER_LOGIN;
+ String password = "123";
+ Map<String, String> users = Maps.newHashMap();
+
+ // When user created in external system
+ users.put(username + ".password", password);
+ users.put(username + ".name", "Tester Testerovich");
+ users.put(username + ".email", "tester@example.org");
+ users.put(username + ".groups", "sonar-user");
+ updateUsersInExtAuth(users);
+ // Then
+ verifyAuthenticationIsOk(username, password);
+
+ // with external details and groups
+ // TODO replace by WS ? Or with new Selenese utils
+ runSelenese(orchestrator, "/user/ExternalAuthenticationTest/external-user-details.html");
+
+ // Now update user details
+ users.put(username + ".name", "Tester2 Testerovich");
+ users.put(username + ".email", "tester2@example.org");
+ updateUsersInExtAuth(users);
+ // Then
+ verifyAuthenticationIsOk(username, password);
+
+ // with external details and groups updated
+ runSelenese(orchestrator, "/user/ExternalAuthenticationTest/external-user-details2.html");
+ }
+
+ /**
+ * SONAR-3138
+ */
+ @Test
+ public void shouldNotFallback() {
+ // Given clean Sonar installation and no users in external system
+ String login = USER_LOGIN;
+ String password = "1234567";
+ Map<String, String> users = Maps.newHashMap();
+
+ // When user created in external system
+ users.put(login + ".password", password);
+ updateUsersInExtAuth(users);
+ // Then
+ verifyAuthenticationIsOk(login, password);
+
+ // When external system does not work
+ users.remove(login + ".password");
+ updateUsersInExtAuth(users);
+ // Then
+ verifyAuthenticationIsNotOk(login, password);
+ }
+
+ /**
+ * SONAR-4543
+ */
+ @Test
+ public void adminIsLocalAccountByDefault() {
+ // Given clean Sonar installation and no users in external system
+ String login = "admin";
+ String localPassword = "admin";
+ String remotePassword = "nimda";
+ Map<String, String> users = Maps.newHashMap();
+
+ // When admin created in external system with a different password
+ users.put(login + ".password", remotePassword);
+ updateUsersInExtAuth(users);
+
+ // Then this is local DB that should be used
+ verifyAuthenticationIsNotOk(login, remotePassword);
+ verifyAuthenticationIsOk(login, localPassword);
+ }
+
+ /**
+ * SONAR-1334, SONAR-3185 (createUsers=true is default)
+ */
+ @Test
+ public void shouldCreateNewUsers() {
+ // Given clean Sonar installation and no users in external system
+ String username = USER_LOGIN;
+ String password = "1234567";
+ Map<String, String> users = Maps.newHashMap();
+
+ // When user not exists in external system
+ // Then
+ verifyAuthenticationIsNotOk(username, password);
+
+ // When user created in external system
+ users.put(username + ".password", password);
+ updateUsersInExtAuth(users);
+ // Then
+ verifyAuthenticationIsOk(username, password);
+ verifyAuthenticationIsNotOk(username, "wrong");
+ }
+
+ // SONAR-3258
+ @Test
+ public void shouldAutomaticallyReactivateDeletedUser() throws Exception {
+ // Given clean Sonar installation and no users in external system
+
+ // Let's create and delete the user "tester" in Sonar DB
+ runSelenese(orchestrator, "/user/ExternalAuthenticationTest/create-and-delete-user.html");
+
+ // And now update the security with the user that was deleted
+ String login = USER_LOGIN;
+ String password = "1234567";
+ Map<String, String> users = Maps.newHashMap();
+ users.put(login + ".password", password);
+ updateUsersInExtAuth(users);
+ // check that the deleted/deactivated user "tester" has been reactivated and can now log in
+ verifyAuthenticationIsOk(login, password);
+ }
+
+ /**
+ * SONAR-7036
+ */
+ @Test
+ public void update_password_of_technical_user() throws Exception {
+ // Create user in external authentication
+ updateUsersInExtAuth(ImmutableMap.of(USER_LOGIN + ".password", USER_LOGIN));
+ verifyAuthenticationIsOk(USER_LOGIN, USER_LOGIN);
+
+ // Create technical user in db
+ createUserInDb(TECH_USER, "old_password");
+ assertThat(checkAuthenticationThroughWebService(TECH_USER, "old_password")).isTrue();
+
+ // Updating password of technical user is allowed
+ updateUserPasswordInDb(TECH_USER, "new_password");
+ assertThat(checkAuthenticationThroughWebService(TECH_USER, "new_password")).isTrue();
+
+ // But updating password of none local user is not allowed
+ try {
+ updateUserPasswordInDb(USER_LOGIN, "new_password");
+ fail();
+ } catch (HttpException e) {
+ verifyHttpException(e, 400);
+ }
+ }
+
+ /**
+ * SONAR-7640
+ */
+ @Test
+ public void authentication_with_ws() throws Exception {
+ // Given clean Sonar installation and no users in external system
+ String login = USER_LOGIN;
+ String password = "1234567";
+ Map<String, String> users = Maps.newHashMap();
+
+ // When user created in external system
+ users.put(login + ".password", password);
+ updateUsersInExtAuth(users);
+
+ verifyAuthenticationIsOk(login, password);
+ verifyAuthenticationIsNotOk("wrong", password);
+ verifyAuthenticationIsNotOk(login, "wrong");
+ verifyAuthenticationIsNotOk(login, null);
+ verifyAuthenticationIsOk(null, null);
+
+ setServerProperty(orchestrator, "sonar.forceAuthentication", "true");
+
+ verifyAuthenticationIsOk(login, password);
+ verifyAuthenticationIsNotOk("wrong", password);
+ verifyAuthenticationIsNotOk(login, "wrong");
+ verifyAuthenticationIsNotOk(login, null);
+ verifyAuthenticationIsNotOk(null, null);
+ }
+
+ @Test
+ public void allow_user_login_with_2_characters() {
+ String username = "jo";
+ String password = "1234567";
+ updateUsersInExtAuth(ImmutableMap.of(username + ".password", password));
+
+ verifyAuthenticationIsOk(username, password);
+ }
+
+ @Test
+ public void provision_user_before_authentication() {
+ newAdminWsClient(orchestrator).users().create(CreateRequest.builder()
+ .setLogin(USER_LOGIN)
+ .setName("Tester Testerovich")
+ .setEmail("tester@example.org")
+ .setLocal(false)
+ .build());
+ // The user is created in SonarQube but doesn't exist yet in external authentication system
+ verifyAuthenticationIsNotOk(USER_LOGIN, "123");
+
+ updateUsersInExtAuth(ImmutableMap.of(
+ USER_LOGIN + ".password", "123",
+ USER_LOGIN + ".name", "Tester Testerovich",
+ USER_LOGIN + ".email", "tester@example.org"));
+
+ verifyAuthenticationIsOk(USER_LOGIN, "123");
+ assertThat(userRule.getUserByLogin(USER_LOGIN).get())
+ .extracting(Users.User::isLocal, Users.User::getExternalIdentity, Users.User::getExternalProvider)
+ .containsOnly(false, USER_LOGIN, "sonarqube");
+ }
+
+ private void verifyHttpException(Exception e, int expectedCode) {
+ assertThat(e).isInstanceOf(HttpException.class);
+ HttpException exception = (HttpException) e;
+ assertThat(exception.status()).isEqualTo(expectedCode);
+ }
+
+ private boolean checkAuthenticationThroughWebService(String login, String password) {
+ return createWsClient(login, password).find(new AuthenticationQuery()).isValid();
+ }
+
+ /**
+ * Updates information about users in security-plugin.
+ */
+ private static void updateUsersInExtAuth(Map<String, String> users) {
+ setServerProperty(orchestrator, USERS_PROPERTY, format(users));
+ }
+
+ private void createUserInDb(String login, String password) {
+ orchestrator.getServer().adminWsClient().userClient().create(UserParameters.create().login(login).name(login)
+ .password(password).passwordConfirmation(password));
+ }
+
+ private void updateUserPasswordInDb(String login, String newPassword) {
+ orchestrator.getServer().adminWsClient().post("/api/users/change_password", "login", login, "password", newPassword);
+ }
+
+ /**
+ * Utility method to create {@link Sonar} with specified {@code username} and {@code password}.
+ * Orchestrator does not provide such method.
+ */
+ private Sonar createWsClient(String username, String password) {
+ return new Sonar(new HttpClient4Connector(new Host(orchestrator.getServer().getUrl(), username, password)));
+ }
+
+ @CheckForNull
+ private static String format(Map<String, String> map) {
+ if (map.isEmpty()) {
+ return null;
+ }
+ StringBuilder sb = new StringBuilder();
+ for (Map.Entry<String, String> entry : map.entrySet()) {
+ sb.append(entry.getKey()).append('=').append(entry.getValue()).append('\n');
+ }
+ return sb.toString();
+ }
+
+ private void verifyAuthenticationIsOk(String login, String password) {
+ assertThat(checkAuthenticationWithWebService(login, password).code()).isEqualTo(HTTP_OK);
+ }
+
+ private void verifyAuthenticationIsNotOk(String login, String password) {
+ assertThat(checkAuthenticationWithWebService(login, password).code()).isEqualTo(HTTP_UNAUTHORIZED);
+ }
+
+ private WsResponse checkAuthenticationWithWebService(String login, String password) {
+ // Call any WS
+ return newUserWsClient(orchestrator, login, password).wsConnector().call(new GetRequest("api/rules/search"));
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/user/SsoAuthenticationTest.java b/tests/src/test/java/org/sonarqube/tests/user/SsoAuthenticationTest.java
new file mode 100644
index 00000000000..2860b09dfb7
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/user/SsoAuthenticationTest.java
@@ -0,0 +1,162 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.user;
+
+import com.sonar.orchestrator.Orchestrator;
+import java.net.URLEncoder;
+import java.util.List;
+import javax.annotation.Nullable;
+import okhttp3.Response;
+import org.apache.commons.io.FileUtils;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import util.user.UserRule;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.call;
+
+/**
+ * Test SSO authentication (using HTTP headers).
+ * <p>
+ * It starts its own server as it's using a different authentication system
+ */
+public class SsoAuthenticationTest {
+
+ private static final String LOGIN_HEADER = "H-Login";
+ private static final String NAME_HEADER = "H-Name";
+ private static final String EMAIL_HEADER = "H-Email";
+ private static final String GROUPS_HEADER = "H-Groups";
+
+ static final String USER_LOGIN = "tester";
+ static final String USER_NAME = "Tester";
+ static final String USER_EMAIL = "tester@email.com";
+
+ static final String GROUP_1 = "group-1";
+ static final String GROUP_2 = "group-2";
+ static final String GROUP_3 = "group-3";
+
+ @ClassRule
+ public static final Orchestrator orchestrator = Orchestrator.builderEnv()
+ .setServerProperty("sonar.web.sso.enable", "true")
+ .setServerProperty("sonar.web.sso.loginHeader", LOGIN_HEADER)
+ .setServerProperty("sonar.web.sso.nameHeader", NAME_HEADER)
+ .setServerProperty("sonar.web.sso.emailHeader", EMAIL_HEADER)
+ .setServerProperty("sonar.web.sso.groupsHeader", GROUPS_HEADER)
+ .build();
+
+ @ClassRule
+ public static UserRule USER_RULE = UserRule.from(orchestrator);
+
+ @Before
+ public void resetData() throws Exception {
+ USER_RULE.resetUsers();
+ }
+
+ @Test
+ public void authenticate() {
+ doCall(USER_LOGIN, USER_NAME, USER_EMAIL, null);
+
+ USER_RULE.verifyUserExists(USER_LOGIN, USER_NAME, USER_EMAIL);
+ }
+
+ @Test
+ public void authenticate_with_only_login() throws Exception {
+ doCall(USER_LOGIN, null, null, null);
+
+ USER_RULE.verifyUserExists(USER_LOGIN, USER_LOGIN, null);
+ }
+
+ @Test
+ public void update_user_when_headers_are_updated() {
+ doCall(USER_LOGIN, USER_NAME, USER_EMAIL, null);
+ USER_RULE.verifyUserExists(USER_LOGIN, USER_NAME, USER_EMAIL);
+
+ // As we don't keep the JWT token is the test, the user is updated
+ doCall(USER_LOGIN, "new name", "new email", null);
+ USER_RULE.verifyUserExists(USER_LOGIN, "new name", "new email");
+ }
+
+ @Test
+ public void authenticate_with_groups() {
+ doCall(USER_LOGIN, null, null, GROUP_1);
+
+ USER_RULE.verifyUserGroupMembership(USER_LOGIN, GROUP_1, "sonar-users");
+ }
+
+ @Test
+ public void synchronize_groups_when_authenticating_existing_user() throws Exception {
+ USER_RULE.createGroup(GROUP_1);
+ USER_RULE.createGroup(GROUP_2);
+ USER_RULE.createGroup(GROUP_3);
+ USER_RULE.createUser(USER_LOGIN, "password");
+ USER_RULE.associateGroupsToUser(USER_LOGIN, GROUP_1, GROUP_2);
+
+ doCall(USER_LOGIN, null, null, GROUP_2 + "," + GROUP_3);
+
+ USER_RULE.verifyUserGroupMembership(USER_LOGIN, GROUP_2, GROUP_3, "sonar-users");
+ }
+
+ @Test
+ public void authentication_with_local_user_is_possible_when_no_header() throws Exception {
+ USER_RULE.createUser(USER_LOGIN, "password");
+
+ checkLocalAuthentication(USER_LOGIN, "password");
+ }
+
+ @Test
+ public void display_message_in_ui_but_not_in_log_when_unauthorized_exception() throws Exception {
+ Response response = doCall("invalid login $", null, null, null);
+
+ assertThat(response.code()).isEqualTo(200);
+ assertThat(response.request().url().toString()).contains("sessions/unauthorized");
+
+ List<String> logsLines = FileUtils.readLines(orchestrator.getServer().getWebLogs(), UTF_8);
+ assertThat(logsLines).doesNotContain("org.sonar.server.exceptions.BadRequestException: Use only letters, numbers, and .-_@ please.");
+ USER_RULE.verifyUserDoesNotExist(USER_LOGIN);
+ }
+
+ @Test
+ public void fail_when_email_already_exists() throws Exception {
+ USER_RULE.createUser("another", "Another", USER_EMAIL, "another");
+
+ Response response = doCall(USER_LOGIN, USER_NAME, USER_EMAIL, null);
+
+ String expectedError = "You can't sign up because email 'tester@email.com' is already used by an existing user. This means that you probably already registered with another account";
+ assertThat(response.code()).isEqualTo(200);
+ assertThat(response.request().url().toString()).contains(URLEncoder.encode(expectedError, UTF_8.name()));
+ assertThat(FileUtils.readLines(orchestrator.getServer().getWebLogs(), UTF_8)).doesNotContain(expectedError);
+ }
+
+ private static Response doCall(String login, @Nullable String name, @Nullable String email, @Nullable String groups) {
+ return call(orchestrator.getServer().getUrl(),
+ LOGIN_HEADER, login,
+ NAME_HEADER, name,
+ EMAIL_HEADER, email,
+ GROUPS_HEADER, groups);
+ }
+
+ private boolean checkLocalAuthentication(String login, String password) {
+ String result = orchestrator.getServer().wsClient(login, password).get("/api/authentication/validate");
+ return result.contains("{\"valid\":true}");
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/user/UsersPageTest.java b/tests/src/test/java/org/sonarqube/tests/user/UsersPageTest.java
new file mode 100644
index 00000000000..b983b0e69e0
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/user/UsersPageTest.java
@@ -0,0 +1,84 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.user;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category1Suite;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.ws.WsUsers;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.user.GroupsRequest;
+import util.selenium.Selenese;
+import util.user.UserRule;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newAdminWsClient;
+
+public class UsersPageTest {
+
+ private static final String ADMIN_USER_LOGIN = "admin-user";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+ @Rule
+ public UserRule userRule = UserRule.from(orchestrator);
+
+ private WsClient adminClient = newAdminWsClient(orchestrator);
+
+ @Before
+ public void initAdminUser() throws Exception {
+ userRule.createAdminUser(ADMIN_USER_LOGIN, ADMIN_USER_LOGIN);
+ }
+
+ @After
+ public void deleteAdminUser() {
+ userRule.resetUsers();
+ }
+
+ @Test
+ public void generate_and_revoke_user_token() {
+ Selenese.runSelenese(orchestrator, "/user/UsersPageTest/generate_and_revoke_user_token.html");
+ }
+
+ @Test
+ public void admin_should_change_his_own_password() {
+ Selenese.runSelenese(orchestrator, "/user/UsersPageTest/admin_should_change_its_own_password.html");
+ }
+
+ @Test
+ public void return_groups_belonging_to_a_user() {
+ String login = randomAlphabetic(10);
+ String group = randomAlphabetic(10);
+ userRule.createUser(login, login);
+ userRule.createGroup(group);
+ userRule.associateGroupsToUser(login, group);
+
+ List<WsUsers.GroupsWsResponse.Group> result = adminClient.users().groups(GroupsRequest.builder().setLogin(login).build()).getGroupsList();
+
+ assertThat(result).extracting(WsUsers.GroupsWsResponse.Group::getName).contains(group);
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/webhook/ExternalServer.java b/tests/src/test/java/org/sonarqube/tests/webhook/ExternalServer.java
new file mode 100644
index 00000000000..33301c85e3b
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/webhook/ExternalServer.java
@@ -0,0 +1,97 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.webhook;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.junit.rules.ExternalResource;
+
+/**
+ * This web server listens to requests sent by webhooks
+ */
+class ExternalServer extends ExternalResource {
+ private final Server jetty;
+ private final List<PayloadRequest> payloads = new ArrayList<>();
+
+ ExternalServer() {
+ jetty = new Server(0);
+ jetty.setHandler(new AbstractHandler() {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException {
+
+ if ("POST".equalsIgnoreCase(request.getMethod())) {
+ String json = request.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
+ Map<String, String> httpHeaders = new HashMap<>();
+ Enumeration<String> headerNames = request.getHeaderNames();
+ while (headerNames.hasMoreElements()) {
+ String key = headerNames.nextElement();
+ httpHeaders.put(key, request.getHeader(key));
+ }
+ payloads.add(new PayloadRequest(target, httpHeaders, json));
+ }
+
+ response.setStatus(target.equals("/fail") ? 500 : 200);
+ baseRequest.setHandled(true);
+ }
+ });
+ }
+
+ @Override
+ protected void before() throws Throwable {
+ jetty.start();
+ }
+
+ @Override
+ protected void after() {
+ try {
+ jetty.stop();
+ } catch (Exception e) {
+ throw new IllegalStateException("Cannot stop Jetty");
+ }
+ }
+
+ List<PayloadRequest> getPayloadRequests() {
+ return payloads;
+ }
+
+ List<PayloadRequest> getPayloadRequestsOnPath(String path) {
+ return payloads.stream().filter(p -> p.getPath().equals(path)).collect(Collectors.toList());
+ }
+
+ String urlFor(String path) {
+ return jetty.getURI().resolve(path).toString();
+ }
+
+ void clear() {
+ payloads.clear();
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/webhook/PayloadRequest.java b/tests/src/test/java/org/sonarqube/tests/webhook/PayloadRequest.java
new file mode 100644
index 00000000000..e641e71648d
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/webhook/PayloadRequest.java
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.webhook;
+
+import java.util.Map;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Request received by {@link ExternalServer}
+ */
+class PayloadRequest {
+ private final String path;
+ private final Map<String, String> httpHeaders;
+ private final String json;
+
+ PayloadRequest(String path, Map<String, String> httpHeaders, String json) {
+ this.path = requireNonNull(path);
+ this.httpHeaders = requireNonNull(httpHeaders);
+ this.json = requireNonNull(json);
+ }
+
+ Map<String, String> getHttpHeaders() {
+ return httpHeaders;
+ }
+
+ String getJson() {
+ return json;
+ }
+
+ String getPath() {
+ return path;
+ }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/webhook/WebhooksTest.java b/tests/src/test/java/org/sonarqube/tests/webhook/WebhooksTest.java
new file mode 100644
index 00000000000..975a5dc9366
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/webhook/WebhooksTest.java
@@ -0,0 +1,290 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.webhook;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category3Suite;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+import org.apache.commons.lang3.StringUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.Webhooks;
+import org.sonarqube.ws.client.HttpException;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.project.DeleteRequest;
+import org.sonarqube.ws.client.setting.ResetRequest;
+import org.sonarqube.ws.client.setting.SetRequest;
+import org.sonarqube.ws.client.webhook.DeliveriesRequest;
+import util.ItUtils;
+
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.IntStream.range;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.jsonToMap;
+import static util.ItUtils.runProjectAnalysis;
+
+public class WebhooksTest {
+
+ private static final String PROJECT_KEY = "my-project";
+ private static final String PROJECT_NAME = "My Project";
+ private static final String GLOBAL_WEBHOOK_PROPERTY = "sonar.webhooks.global";
+ private static final String PROJECT_WEBHOOK_PROPERTY = "sonar.webhooks.project";
+
+ @ClassRule
+ public static Orchestrator orchestrator = Category3Suite.ORCHESTRATOR;
+
+ @ClassRule
+ public static ExternalServer externalServer = new ExternalServer();
+
+ private WsClient adminWs = ItUtils.newAdminWsClient(orchestrator);
+
+ @Before
+ public void setUp() throws Exception {
+ externalServer.clear();
+ }
+
+ @Before
+ @After
+ public void reset() throws Exception {
+ disableGlobalWebhooks();
+ try {
+ // delete project and related properties/webhook deliveries
+ adminWs.projects().delete(DeleteRequest.builder().setKey(PROJECT_KEY).build());
+ } catch (HttpException e) {
+ // ignore because project may not exist
+ }
+ }
+
+ @Test
+ public void call_multiple_global_and_project_webhooks_when_analysis_is_done() {
+ orchestrator.getServer().provisionProject(PROJECT_KEY, PROJECT_NAME);
+ enableGlobalWebhooks(
+ new Webhook("Jenkins", externalServer.urlFor("/jenkins")),
+ new Webhook("HipChat", externalServer.urlFor("/hipchat")));
+ enableProjectWebhooks(PROJECT_KEY,
+ new Webhook("Burgr", externalServer.urlFor("/burgr")));
+
+ analyseProject();
+
+ // the same payload has been sent to three servers
+ assertThat(externalServer.getPayloadRequests()).hasSize(3);
+ PayloadRequest request = externalServer.getPayloadRequests().get(0);
+ for (int i = 1; i < 3; i++) {
+ PayloadRequest r = externalServer.getPayloadRequests().get(i);
+ assertThat(request.getJson()).isEqualTo(r.getJson());
+ }
+
+ // verify HTTP headers
+ assertThat(request.getHttpHeaders().get("X-SonarQube-Project")).isEqualTo(PROJECT_KEY);
+
+ // verify content of payload
+ Map<String, Object> payload = jsonToMap(request.getJson());
+ assertThat(payload.get("status")).isEqualTo("SUCCESS");
+ assertThat(payload.get("analysedAt")).isNotNull();
+ Map<String, String> project = (Map<String, String>) payload.get("project");
+ assertThat(project.get("key")).isEqualTo(PROJECT_KEY);
+ assertThat(project.get("name")).isEqualTo(PROJECT_NAME);
+ Map<String, Object> gate = (Map<String, Object>) payload.get("qualityGate");
+ assertThat(gate.get("name")).isEqualTo("SonarQube way");
+ assertThat(gate.get("status")).isEqualTo("OK");
+ assertThat(gate.get("conditions")).isNotNull();
+
+ // verify list of persisted deliveries (api/webhooks/deliveries)
+ List<Webhooks.Delivery> deliveries = getPersistedDeliveries();
+ assertThat(deliveries).hasSize(3);
+ for (Webhooks.Delivery delivery : deliveries) {
+ assertThatPersistedDeliveryIsValid(delivery);
+ assertThat(delivery.getSuccess()).isTrue();
+ assertThat(delivery.getHttpStatus()).isEqualTo(200);
+ assertThat(delivery.getName()).isIn("Jenkins", "HipChat", "Burgr");
+ assertThat(delivery.hasErrorStacktrace()).isFalse();
+ // payload is available only in api/webhooks/delivery to avoid loading multiple DB CLOBs
+ assertThat(delivery.hasPayload()).isFalse();
+ }
+
+ // verify detail of persisted delivery (api/webhooks/delivery)
+ Webhooks.Delivery detail = getDetailOfPersistedDelivery(deliveries.get(0));
+ assertThatPersistedDeliveryIsValid(detail);
+ assertThat(detail.getPayload()).isEqualTo(request.getJson());
+ }
+
+ @Test
+ public void persist_delivery_as_failed_if_external_server_returns_an_error_code() {
+ enableGlobalWebhooks(
+ new Webhook("Fail", externalServer.urlFor("/fail")),
+ new Webhook("HipChat", externalServer.urlFor("/hipchat")));
+
+ analyseProject();
+
+ // all webhooks are called, even if one returns an error code
+ assertThat(externalServer.getPayloadRequests()).hasSize(2);
+
+ // verify persisted deliveries
+ Webhooks.Delivery failedDelivery = getPersistedDeliveryByName("Fail");
+ assertThatPersistedDeliveryIsValid(failedDelivery);
+ assertThat(failedDelivery.getSuccess()).isFalse();
+ assertThat(failedDelivery.getHttpStatus()).isEqualTo(500);
+
+ Webhooks.Delivery successfulDelivery = getPersistedDeliveryByName("HipChat");
+ assertThatPersistedDeliveryIsValid(successfulDelivery);
+ assertThat(successfulDelivery.getSuccess()).isTrue();
+ assertThat(successfulDelivery.getHttpStatus()).isEqualTo(200);
+ }
+
+ /**
+ * Restrict calls to ten webhooks per type (global or project)
+ */
+ @Test
+ public void do_not_become_a_denial_of_service_attacker() {
+ orchestrator.getServer().provisionProject(PROJECT_KEY, PROJECT_NAME);
+
+ List<Webhook> globalWebhooks = range(0, 15).mapToObj(i -> new Webhook("G" + i, externalServer.urlFor("/global"))).collect(Collectors.toList());
+ enableGlobalWebhooks(globalWebhooks.toArray(new Webhook[globalWebhooks.size()]));
+ List<Webhook> projectWebhooks = range(0, 15).mapToObj(i -> new Webhook("P" + i, externalServer.urlFor("/project"))).collect(Collectors.toList());
+ enableProjectWebhooks(PROJECT_KEY, projectWebhooks.toArray(new Webhook[projectWebhooks.size()]));
+
+ analyseProject();
+
+ // only the first ten global webhooks and ten project webhooks are called
+ assertThat(externalServer.getPayloadRequests()).hasSize(10 + 10);
+ assertThat(externalServer.getPayloadRequestsOnPath("/global")).hasSize(10);
+ assertThat(externalServer.getPayloadRequestsOnPath("/project")).hasSize(10);
+
+ // verify persisted deliveries
+ assertThat(getPersistedDeliveries()).hasSize(10 + 10);
+ }
+
+ @Test
+ public void persist_delivery_as_failed_if_webhook_url_is_malformed() {
+ enableGlobalWebhooks(new Webhook("Jenkins", "this_is_not_an_url"));
+
+ analyseProject();
+
+ assertThat(externalServer.getPayloadRequests()).isEmpty();
+
+ // verify persisted deliveries
+ Webhooks.Delivery delivery = getPersistedDeliveryByName("Jenkins");
+ Webhooks.Delivery detail = getDetailOfPersistedDelivery(delivery);
+
+ assertThat(detail.getSuccess()).isFalse();
+ assertThat(detail.hasHttpStatus()).isFalse();
+ assertThat(detail.hasDurationMs()).isFalse();
+ assertThat(detail.getPayload()).isNotEmpty();
+ assertThat(detail.getErrorStacktrace())
+ .contains("java.lang.IllegalArgumentException")
+ .contains("unexpected url")
+ .contains("this_is_not_an_url");
+ }
+
+ @Test
+ public void ignore_webhook_if_url_is_missing() {
+ // property sets, as used to define webhooks, do
+ // not allow to validate values yet
+ enableGlobalWebhooks(new Webhook("Jenkins", null));
+
+ analyseProject();
+
+ assertThat(externalServer.getPayloadRequests()).isEmpty();
+ assertThat(getPersistedDeliveries()).isEmpty();
+ }
+
+ private void analyseProject() {
+ runProjectAnalysis(orchestrator, "shared/xoo-sample",
+ "sonar.projectKey", PROJECT_KEY,
+ "sonar.projectName", PROJECT_NAME);
+ }
+
+ private List<Webhooks.Delivery> getPersistedDeliveries() {
+ DeliveriesRequest deliveriesReq = DeliveriesRequest.builder().setComponentKey(PROJECT_KEY).build();
+ return adminWs.webhooks().deliveries(deliveriesReq).getDeliveriesList();
+ }
+
+ private Webhooks.Delivery getPersistedDeliveryByName(String webhookName) {
+ List<Webhooks.Delivery> deliveries = getPersistedDeliveries();
+ return deliveries.stream().filter(d -> d.getName().equals(webhookName)).findFirst().get();
+ }
+
+ private Webhooks.Delivery getDetailOfPersistedDelivery(Webhooks.Delivery delivery) {
+ Webhooks.Delivery detail = adminWs.webhooks().delivery(delivery.getId()).getDelivery();
+ return requireNonNull(detail);
+ }
+
+ private void assertThatPersistedDeliveryIsValid(Webhooks.Delivery delivery) {
+ assertThat(delivery.getId()).isNotEmpty();
+ assertThat(delivery.getName()).isNotEmpty();
+ assertThat(delivery.hasSuccess()).isTrue();
+ assertThat(delivery.getHttpStatus()).isGreaterThanOrEqualTo(200);
+ assertThat(delivery.getDurationMs()).isGreaterThanOrEqualTo(0);
+ assertThat(delivery.getAt()).isNotEmpty();
+ assertThat(delivery.getComponentKey()).isEqualTo(PROJECT_KEY);
+ assertThat(delivery.getUrl()).startsWith(externalServer.urlFor("/"));
+ }
+
+ private void enableGlobalWebhooks(Webhook... webhooks) {
+ enableWebhooks(null, GLOBAL_WEBHOOK_PROPERTY, webhooks);
+ }
+
+ private void enableProjectWebhooks(String projectKey, Webhook... webhooks) {
+ enableWebhooks(projectKey, PROJECT_WEBHOOK_PROPERTY, webhooks);
+ }
+
+ private void enableWebhooks(@Nullable String projectKey, String property, Webhook... webhooks) {
+ List<String> webhookIds = new ArrayList<>();
+ for (int i = 0; i < webhooks.length; i++) {
+ Webhook webhook = webhooks[i];
+ String id = String.valueOf(i + 1);
+ webhookIds.add(id);
+ setProperty(projectKey, property + "." + id + ".name", webhook.name);
+ setProperty(projectKey, property + "." + id + ".url", webhook.url);
+ }
+ setProperty(projectKey, property, StringUtils.join(webhookIds, ","));
+ }
+
+ private void disableGlobalWebhooks() {
+ setProperty(null, GLOBAL_WEBHOOK_PROPERTY, null);
+ }
+
+ private void setProperty(@Nullable String componentKey, String key, @Nullable String value) {
+ if (value == null) {
+ ResetRequest req = ResetRequest.builder().setKeys(key).setComponent(componentKey).build();
+ adminWs.settings().reset(req);
+ } else {
+ SetRequest req = SetRequest.builder().setKey(key).setValue(value).setComponent(componentKey).build();
+ adminWs.settings().set(req);
+ }
+ }
+
+ private static class Webhook {
+ private final String name;
+ private final String url;
+
+ Webhook(@Nullable String name, @Nullable String url) {
+ this.name = name;
+ this.url = url;
+ }
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/ws/RoutesTest.java b/tests/src/test/java/org/sonarqube/tests/ws/RoutesTest.java
new file mode 100644
index 00000000000..6db3a4d30b6
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/ws/RoutesTest.java
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.ws;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category4Suite;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.WsResponse;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newWsClient;
+
+public class RoutesTest {
+
+ @ClassRule
+ public static final Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+
+ @Test
+ public void redirect_profiles_export_to_api_qualityprofiles_export() {
+ WsResponse response = newWsClient(orchestrator).wsConnector().call(new GetRequest("profiles/export?language=xoo&format=XooFakeExporter"));
+ assertThat(response.isSuccessful()).isTrue();
+ assertThat(response.requestUrl()).endsWith("/api/qualityprofiles/export?language=xoo&format=XooFakeExporter");
+ assertThat(response.content()).isEqualTo("xoo -> Basic -> 1");
+
+ // Check 'name' parameter is taken into account
+ assertThat(newWsClient(orchestrator).wsConnector().call(new GetRequest("profiles/export?language=xoo&name=empty&format=XooFakeExporter")).content()).isEqualTo("xoo -> empty -> 0");
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/ws/WsLocalCallTest.java b/tests/src/test/java/org/sonarqube/tests/ws/WsLocalCallTest.java
new file mode 100644
index 00000000000..8e822e286dd
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/ws/WsLocalCallTest.java
@@ -0,0 +1,73 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.ws;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category4Suite;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.HttpConnector;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.WsClientFactories;
+import org.sonarqube.ws.client.WsResponse;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newWsClient;
+
+/**
+ * Tests the ability for a web service to call another web services.
+ */
+public class WsLocalCallTest {
+
+ @ClassRule
+ public static final Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+
+ @Test
+ public void gets_protobuf() {
+ WsResponse response = newWsClient(orchestrator).wsConnector().call(new GetRequest("local_ws_call/protobuf_data"));
+ assertThat(response.isSuccessful()).isTrue();
+ }
+
+ @Test
+ public void gets_json() {
+ WsResponse response = newWsClient(orchestrator).wsConnector().call(new GetRequest("local_ws_call/json_data"));
+ assertThat(response.isSuccessful()).isTrue();
+ }
+
+ @Test
+ public void propagates_authorization_rights() {
+ WsClient wsClient = WsClientFactories.getDefault().newClient(HttpConnector.newBuilder()
+ .url(orchestrator.getServer().getUrl())
+ .credentials("admin", "admin")
+ .build());
+ WsResponse response = wsClient.wsConnector().call(new GetRequest("local_ws_call/require_permission"));
+ assertThat(response.isSuccessful()).isTrue();
+ }
+
+ @Test
+ public void fails_if_requires_permissions() {
+ WsResponse response = newWsClient(orchestrator).wsConnector().call(new GetRequest("local_ws_call/require_permission"));
+
+ // this is not the unauthorized code as plugin forces it to 500
+ assertThat(response.code()).isEqualTo(500);
+ }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/ws/WsTest.java b/tests/src/test/java/org/sonarqube/tests/ws/WsTest.java
new file mode 100644
index 00000000000..50fdd7758c5
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/ws/WsTest.java
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package org.sonarqube.tests.ws;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.sonarqube.tests.Category4Suite;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.WsResponse;
+
+import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newWsClient;
+
+public class WsTest {
+
+ @ClassRule
+ public static final Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+
+ @Test
+ public void gets_protobuf() {
+ WsResponse response = newWsClient(orchestrator).wsConnector().call(new GetRequest("api/issues/search.protobuf"));
+ assertThat(response.isSuccessful()).isTrue();
+ assertThat(response.contentType()).contains("application/x-protobuf");
+ }
+
+ @Test
+ public void gets_json() {
+ WsResponse response = newWsClient(orchestrator).wsConnector().call(new GetRequest("api/issues/search.json"));
+ assertThat(response.isSuccessful()).isTrue();
+ assertThat(response.contentType()).contains("application/json");
+ }
+
+ /**
+ * SONAR-7484
+ */
+ @Test
+ public void fail_on_unknown_extension() {
+ WsResponse response = newWsClient(orchestrator).wsConnector().call(new GetRequest("api/issues/search.unknown"));
+ assertThat(response.isSuccessful()).isFalse();
+ assertThat(response.code()).isEqualTo(HTTP_BAD_REQUEST);
+ }
+
+}
diff --git a/tests/src/test/java/util/ItUtils.java b/tests/src/test/java/util/ItUtils.java
new file mode 100644
index 00000000000..3ef0d275549
--- /dev/null
+++ b/tests/src/test/java/util/ItUtils.java
@@ -0,0 +1,534 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package util;
+
+import com.google.common.base.Splitter;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableMap;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.BuildResult;
+import com.sonar.orchestrator.build.SonarRunner;
+import com.sonar.orchestrator.container.Server;
+import com.sonar.orchestrator.locator.FileLocation;
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import org.apache.commons.io.FileUtils;
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.JSONValue;
+import org.junit.Assert;
+import org.sonar.wsclient.issue.Issue;
+import org.sonar.wsclient.issue.IssueClient;
+import org.sonar.wsclient.issue.IssueQuery;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.WsComponents.Component;
+import org.sonarqube.ws.WsMeasures;
+import org.sonarqube.ws.WsMeasures.Measure;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.HttpConnector;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.WsClientFactories;
+import org.sonarqube.ws.client.component.ShowWsRequest;
+import org.sonarqube.ws.client.measure.ComponentWsRequest;
+import org.sonarqube.ws.client.qualityprofile.RestoreWsRequest;
+import org.sonarqube.ws.client.setting.ResetRequest;
+import org.sonarqube.ws.client.setting.SetRequest;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.sonar.orchestrator.container.Server.ADMIN_LOGIN;
+import static com.sonar.orchestrator.container.Server.ADMIN_PASSWORD;
+import static java.lang.Double.parseDouble;
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static java.util.Locale.ENGLISH;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+
+public class ItUtils {
+ public static final Splitter LINE_SPLITTER = Splitter.on(System.getProperty("line.separator"));
+
+ private ItUtils() {
+ }
+
+ public static FileLocation xooPlugin() {
+ return FileLocation.byWildcardMavenFilename(new File("../plugins/sonar-xoo-plugin/target"), "sonar-xoo-plugin-*.jar");
+ }
+
+ public static List<Issue> getAllServerIssues(Orchestrator orchestrator) {
+ IssueClient issueClient = orchestrator.getServer().wsClient().issueClient();
+ return issueClient.find(IssueQuery.create()).list();
+ }
+
+ /**
+ * @deprecated replaced by {@link Tester#wsClient()}
+ */
+ @Deprecated
+ public static WsClient newAdminWsClient(Orchestrator orchestrator) {
+ return newUserWsClient(orchestrator, ADMIN_LOGIN, ADMIN_PASSWORD);
+ }
+
+ /**
+ * @deprecated replaced by {@link Tester#wsClient()}
+ */
+ @Deprecated
+ public static WsClient newWsClient(Orchestrator orchestrator) {
+ return newUserWsClient(orchestrator, null, null);
+ }
+
+ /**
+ * @deprecated replaced by {@link Tester#wsClient()}
+ */
+ @Deprecated
+ public static WsClient newUserWsClient(Orchestrator orchestrator, @Nullable String login, @Nullable String password) {
+ Server server = orchestrator.getServer();
+ return WsClientFactories.getDefault().newClient(HttpConnector.newBuilder()
+ .url(server.getUrl())
+ .credentials(login, password)
+ .build());
+ }
+
+ /**
+ * Locate the directory of sample project
+ *
+ * @param relativePath path related to the directory it/projects, for example "qualitygate/xoo-sample"
+ */
+ public static File projectDir(String relativePath) {
+ File dir = new File("projects/" + relativePath);
+ if (!dir.exists() || !dir.isDirectory()) {
+ throw new IllegalStateException("Directory does not exist: " + dir.getAbsolutePath());
+ }
+ return dir;
+ }
+
+ /**
+ * Locate the artifact of a fake plugin stored in it/plugins.
+ *
+ * @param dirName the directory of it/plugins, for example "sonar-fake-plugin".
+ * It assumes that version is 1.0-SNAPSHOT
+ */
+ public static FileLocation pluginArtifact(String dirName) {
+ return FileLocation.byWildcardMavenFilename(new File("plugins/" + dirName + "/target"), dirName + "-*.jar");
+ }
+
+ /**
+ * Locate the pom file of a sample project
+ *
+ * @param projectName project path related to the directory it/projects, for example "qualitygate/xoo-sample"
+ */
+ public static File projectPom(String projectName) {
+ File pom = new File(projectDir(projectName), "pom.xml");
+ if (!pom.exists() || !pom.isFile()) {
+ throw new IllegalStateException("pom file does not exist: " + pom.getAbsolutePath());
+ }
+ return pom;
+ }
+
+ public static String sanitizeTimezones(String s) {
+ return s.replaceAll("[\\+\\-]\\d\\d\\d\\d", "+0000");
+ }
+
+ public static JSONObject getJSONReport(BuildResult result) {
+ Pattern pattern = Pattern.compile("Export issues to (.*?).json");
+ Matcher m = pattern.matcher(result.getLogs());
+ if (m.find()) {
+ String s = m.group(1);
+ File path = new File(s + ".json");
+ assertThat(path).exists();
+ try {
+ return (JSONObject) JSONValue.parse(FileUtils.readFileToString(path));
+ } catch (IOException e) {
+ throw new RuntimeException("Unable to read JSON report", e);
+ }
+ }
+ fail("Unable to locate json report");
+ return null;
+ }
+
+ public static int countIssuesInJsonReport(BuildResult result, boolean onlyNews) {
+ JSONObject obj = getJSONReport(result);
+ JSONArray issues = (JSONArray) obj.get("issues");
+ int count = 0;
+ for (Object issue : issues) {
+ JSONObject jsonIssue = (JSONObject) issue;
+ if (!onlyNews || (Boolean) jsonIssue.get("isNew")) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ public static void assertIssuesInJsonReport(BuildResult result, int newIssues, int resolvedIssues, int existingIssues) {
+ JSONObject obj = getJSONReport(result);
+ JSONArray issues = (JSONArray) obj.get("issues");
+ int countNew = 0;
+ int countResolved = 0;
+ int countExisting = 0;
+
+ for (Object issue : issues) {
+ JSONObject jsonIssue = (JSONObject) issue;
+
+ if ((Boolean) jsonIssue.get("isNew")) {
+ countNew++;
+ } else if (jsonIssue.get("resolution") != null) {
+ countResolved++;
+ } else {
+ countExisting++;
+ }
+ }
+ assertThat(countNew).isEqualTo(newIssues);
+ assertThat(countResolved).isEqualTo(resolvedIssues);
+ assertThat(countExisting).isEqualTo(existingIssues);
+ }
+
+ public static SonarRunner runVerboseProjectAnalysis(Orchestrator orchestrator, String projectRelativePath, String... properties) {
+ return runProjectAnalysis(orchestrator, projectRelativePath, true, properties);
+ }
+
+ public static SonarRunner runProjectAnalysis(Orchestrator orchestrator, String projectRelativePath, String... properties) {
+ return runProjectAnalysis(orchestrator, projectRelativePath, false, properties);
+ }
+
+ private static SonarRunner runProjectAnalysis(Orchestrator orchestrator, String projectRelativePath, boolean enableDebugLogs, String... properties) {
+ SonarRunner sonarRunner = SonarRunner.create(projectDir(projectRelativePath));
+ ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
+ for (int i = 0; i < properties.length; i += 2) {
+ builder.put(properties[i], properties[i + 1]);
+ }
+ SonarRunner scan = sonarRunner.setDebugLogs(enableDebugLogs).setProperties(builder.build());
+ orchestrator.executeBuild(scan);
+ return scan;
+ }
+
+ public static void setServerProperty(Orchestrator orchestrator, String key, @Nullable String value) {
+ setServerProperty(orchestrator, null, key, value);
+ }
+
+ public static void setServerProperty(Orchestrator orchestrator, @Nullable String componentKey, String key, @Nullable String value) {
+ if (value == null) {
+ newAdminWsClient(orchestrator).settings().reset(ResetRequest.builder().setKeys(key).setComponent(componentKey).build());
+ } else {
+ newAdminWsClient(orchestrator).settings().set(SetRequest.builder().setKey(key).setValue(value).setComponent(componentKey).build());
+ }
+ }
+
+ public static void setServerProperties(Orchestrator orchestrator, @Nullable String componentKey, String... properties) {
+ for (int i = 0; i < properties.length; i += 2) {
+ setServerProperty(orchestrator, componentKey, properties[i], properties[i + 1]);
+ }
+ }
+
+ public static void resetSettings(Orchestrator orchestrator, @Nullable String componentKey, String... keys) {
+ if (keys.length > 0) {
+ newAdminWsClient(orchestrator).settings().reset(ResetRequest.builder().setKeys(keys).setComponent(componentKey).build());
+ }
+ }
+
+ public static void resetEmailSettings(Orchestrator orchestrator) {
+ resetSettings(orchestrator, null, "email.smtp_host.secured", "email.smtp_port.secured", "email.smtp_secure_connection.secured", "email.smtp_username.secured",
+ "email.smtp_password.secured", "email.from", "email.prefix");
+ }
+
+ public static void resetPeriod(Orchestrator orchestrator) {
+ resetSettings(orchestrator, null, "sonar.leak.period");
+ }
+
+ @CheckForNull
+ public static Measure getMeasure(Orchestrator orchestrator, String componentKey, String metricKey) {
+ return getMeasuresByMetricKey(orchestrator, componentKey, metricKey).get(metricKey);
+ }
+
+ @CheckForNull
+ public static Double getMeasureAsDouble(Orchestrator orchestrator, String componentKey, String metricKey) {
+ Measure measure = getMeasure(orchestrator, componentKey, metricKey);
+ return (measure == null) ? null : Double.parseDouble(measure.getValue());
+ }
+
+ public static Map<String, Measure> getMeasuresByMetricKey(Orchestrator orchestrator, String componentKey, String... metricKeys) {
+ return getStreamMeasures(orchestrator, componentKey, metricKeys)
+ .filter(Measure::hasValue)
+ .collect(Collectors.toMap(Measure::getMetric, Function.identity()));
+ }
+
+ public static Map<String, Double> getMeasuresAsDoubleByMetricKey(Orchestrator orchestrator, String componentKey, String... metricKeys) {
+ return getStreamMeasures(orchestrator, componentKey, metricKeys)
+ .filter(Measure::hasValue)
+ .collect(Collectors.toMap(Measure::getMetric, measure -> parseDouble(measure.getValue())));
+ }
+
+ private static Stream<Measure> getStreamMeasures(Orchestrator orchestrator, String componentKey, String... metricKeys) {
+ return newWsClient(orchestrator).measures().component(new ComponentWsRequest()
+ .setComponentKey(componentKey)
+ .setMetricKeys(asList(metricKeys)))
+ .getComponent().getMeasuresList()
+ .stream();
+ }
+
+ @CheckForNull
+ public static Measure getMeasureWithVariation(Orchestrator orchestrator, String componentKey, String metricKey) {
+ WsMeasures.ComponentWsResponse response = newWsClient(orchestrator).measures().component(new ComponentWsRequest()
+ .setComponentKey(componentKey)
+ .setMetricKeys(singletonList(metricKey))
+ .setAdditionalFields(singletonList("periods")));
+ List<Measure> measures = response.getComponent().getMeasuresList();
+ return measures.size() == 1 ? measures.get(0) : null;
+ }
+
+ @CheckForNull
+ public static Map<String, Measure> getMeasuresWithVariationsByMetricKey(Orchestrator orchestrator, String componentKey, String... metricKeys) {
+ return newWsClient(orchestrator).measures().component(new ComponentWsRequest()
+ .setComponentKey(componentKey)
+ .setMetricKeys(asList(metricKeys))
+ .setAdditionalFields(singletonList("periods"))).getComponent().getMeasuresList()
+ .stream()
+ .collect(Collectors.toMap(Measure::getMetric, Function.identity()));
+ }
+
+ /**
+ * Return leak period value
+ */
+ @CheckForNull
+ public static Double getLeakPeriodValue(Orchestrator orchestrator, String componentKey, String metricKey) {
+ List<WsMeasures.PeriodValue> periodsValueList = getMeasureWithVariation(orchestrator, componentKey, metricKey).getPeriods().getPeriodsValueList();
+ return periodsValueList.size() > 0 ? Double.parseDouble(periodsValueList.get(0).getValue()) : null;
+ }
+
+ @CheckForNull
+ public static Component getComponent(Orchestrator orchestrator, String componentKey) {
+ try {
+ return newWsClient(orchestrator).components().show(new ShowWsRequest().setKey((componentKey))).getComponent();
+ } catch (org.sonarqube.ws.client.HttpException e) {
+ if (e.code() == 404) {
+ return null;
+ }
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @CheckForNull
+ public static ComponentNavigation getComponentNavigation(Orchestrator orchestrator, String componentKey) {
+ // Waiting for SONAR-7745 to have version in api/components/show, we use internal api/navigation/component WS to get the component
+ // version
+ String content = newWsClient(orchestrator).wsConnector().call(new GetRequest("api/navigation/component").setParam("componentKey", componentKey)).failIfNotSuccessful()
+ .content();
+ return ComponentNavigation.parse(content);
+ }
+
+ public static void restoreProfile(Orchestrator orchestrator, URL resource) {
+ restoreProfile(orchestrator, resource, null);
+ }
+
+ public static void restoreProfile(Orchestrator orchestrator, URL resource, String organization) {
+ URI uri;
+ try {
+ uri = resource.toURI();
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException("Cannot find quality profile xml file '" + resource + "' in classpath");
+ }
+ newAdminWsClient(orchestrator)
+ .qualityProfiles()
+ .restoreProfile(
+ RestoreWsRequest.builder()
+ .setBackup(new File(uri))
+ .setOrganization(organization)
+ .build());
+ }
+
+ public static String newOrganizationKey() {
+ return randomAlphabetic(32).toLowerCase(ENGLISH);
+ }
+
+ public static String newProjectKey() {
+ return "key-" + randomAlphabetic(100);
+ }
+
+ public static class ComponentNavigation {
+ private String version;
+ private String analysisDate;
+
+ public String getVersion() {
+ return version;
+ }
+
+ public Date getDate() {
+ return toDatetime(analysisDate);
+ }
+
+ public static ComponentNavigation parse(String json) {
+ Gson gson = new Gson();
+ return gson.fromJson(json, ComponentNavigation.class);
+ }
+ }
+
+ /**
+ * Concatenates a vararg to a String array.
+ *
+ * Useful when using {@link #runVerboseProjectAnalysis(Orchestrator, String, String...)}, eg.:
+ * <pre>
+ * ItUtils.runProjectAnalysis(orchestrator, "project_name",
+ * ItUtils.concat(properties, "sonar.scm.disabled", "false")
+ * );
+ * </pre>
+ */
+ public static String[] concat(String[] properties, String... str) {
+ if (properties == null || properties.length == 0) {
+ return str;
+ }
+ return Stream.concat(Arrays.stream(properties), Arrays.stream(str))
+ .toArray(String[]::new);
+ }
+
+ public static Date toDate(String sDate) {
+ try {
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+ return sdf.parse(sDate);
+ } catch (ParseException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static Date toDatetime(String sDate) {
+ try {
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
+ return sdf.parse(sDate);
+ } catch (ParseException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static String formatDate(Date d) {
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+ return sdf.format(d);
+ }
+
+ public static String extractCeTaskId(BuildResult buildResult) {
+ List<String> taskIds = extractCeTaskIds(buildResult);
+ checkState(taskIds.size() == 1, "More than one task id retrieved from logs");
+ return taskIds.iterator().next();
+ }
+
+ private static List<String> extractCeTaskIds(BuildResult buildResult) {
+ String logs = buildResult.getLogs();
+ return StreamSupport.stream(LINE_SPLITTER.split(logs).spliterator(), false)
+ .filter(s -> s.contains("More about the report processing at"))
+ .map(s -> s.substring(s.length() - 20, s.length()))
+ .collect(Collectors.toList());
+ }
+
+ public static Map<String, Object> jsonToMap(String json) {
+ Gson gson = new Gson();
+ Type type = new TypeToken<Map<String, Object>>() {
+ }.getType();
+ return gson.fromJson(json, type);
+ }
+
+ /**
+ * @deprecated replaced by {@code orchestrator.getServer().newHttpCall()}
+ */
+ @Deprecated
+ public static Response call(String url, String... headers) {
+ Request.Builder requestBuilder = new Request.Builder().get().url(url);
+ for (int i = 0; i < headers.length; i += 2) {
+ String headerName = headers[i];
+ String headerValue = headers[i + 1];
+ if (headerValue != null) {
+ requestBuilder.addHeader(headerName, headerValue);
+ }
+ }
+ try {
+ return new OkHttpClient.Builder()
+ .connectTimeout(30, TimeUnit.SECONDS)
+ .readTimeout(30, TimeUnit.SECONDS)
+ .build()
+ .newCall(requestBuilder.build())
+ .execute();
+ } catch (IOException e) {
+ throw Throwables.propagate(e);
+ }
+ }
+
+ public static void expectBadRequestError(Runnable runnable) {
+ expectHttpError(400, runnable);
+ }
+
+ public static void expectMissingError(Runnable runnable) {
+ expectHttpError(404, runnable);
+ }
+ /**
+ * Missing permissions
+ */
+ public static void expectForbiddenError(Runnable runnable) {
+ expectHttpError(403, runnable);
+ }
+
+ /**
+ * Not authenticated
+ */
+ public static void expectUnauthorizedError(Runnable runnable) {
+ expectHttpError(401, runnable);
+ }
+
+ public static void expectNotFoundError(Runnable runnable) {
+ expectHttpError(404, runnable);
+ }
+
+ public static void expectHttpError(int expectedCode, Runnable runnable) {
+ try {
+ runnable.run();
+ Assert.fail("Ws call should have failed");
+ } catch (org.sonarqube.ws.client.HttpException e) {
+ assertThat(e.code()).isEqualTo(expectedCode);
+ }
+ }
+
+ public static void expectHttpError(int expectedCode, String expectedMessage, Runnable runnable) {
+ try {
+ runnable.run();
+ Assert.fail("Ws call should have failed");
+ } catch (org.sonarqube.ws.client.HttpException e) {
+ assertThat(e.code()).isEqualTo(expectedCode);
+ assertThat(e.getMessage()).contains(expectedMessage);
+ }
+ }
+}
diff --git a/tests/src/test/java/util/LoadedProfiles.java b/tests/src/test/java/util/LoadedProfiles.java
new file mode 100644
index 00000000000..71c76803901
--- /dev/null
+++ b/tests/src/test/java/util/LoadedProfiles.java
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package util;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+final class LoadedProfiles {
+ private final Map<String, Profile> profileStatesPerProfileKey = new HashMap<>();
+
+ public LoadedProfiles() {
+ init();
+ }
+
+ public String loadProfile(String relativePathToProfile) {
+ try {
+ URL resource = getClass().getResource(relativePathToProfile);
+ Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(resource.openStream());
+
+ String profileKey = null;
+ String languageKey = null;
+
+ Element documentElement = document.getDocumentElement();
+ checkArgument("profile".equals(documentElement.getNodeName()), "%s is not a quality profile file. Root node is not %s", resource.toURI().toString());
+ NodeList childNodes = documentElement.getChildNodes();
+ for (int i = 0; i < childNodes.getLength(); i++) {
+ Node childNode = childNodes.item(i);
+ if ("name".equals(childNode.getNodeName())) {
+ profileKey = childNode.getChildNodes().item(0).getNodeValue();
+ } else if ("language".equals(childNode.getNodeName())) {
+ languageKey = childNode.getChildNodes().item(0).getNodeValue();
+ }
+ }
+ checkArgument(profileKey != null, "Quality profile file %s is missing profile key", resource.toURI().toString());
+ checkArgument(languageKey != null, "Quality profile file %s is missing language key", resource.toURI().toString());
+ this.profileStatesPerProfileKey.put(profileKey, new Profile(profileKey, languageKey, relativePathToProfile));
+
+ return profileKey;
+ } catch (URISyntaxException | SAXException | IOException | ParserConfigurationException e) {
+ throw new RuntimeException("Can not load quality profile " + relativePathToProfile, e);
+ }
+ }
+
+ public Profile getState(String qualityProfileKey) {
+ Profile profile = this.profileStatesPerProfileKey.get(qualityProfileKey);
+ checkArgument(profile != null, "Quality Profile with key %s is unknown to %s", qualityProfileKey, ProjectAnalysisRule.class.getSimpleName());
+ return profile;
+ }
+
+ public void reset() {
+ this.profileStatesPerProfileKey.clear();
+ init();
+ }
+
+ private void init() {
+ this.profileStatesPerProfileKey.put(Profile.XOO_EMPTY_PROFILE.getProfileKey(), Profile.XOO_EMPTY_PROFILE);
+ }
+}
diff --git a/tests/src/test/java/util/LoadedProjects.java b/tests/src/test/java/util/LoadedProjects.java
new file mode 100644
index 00000000000..eb3565f5f84
--- /dev/null
+++ b/tests/src/test/java/util/LoadedProjects.java
@@ -0,0 +1,82 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package util;
+
+import com.google.common.base.Throwables;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+final class LoadedProjects {
+
+ static final String SONAR_PROJECT_PROPERTIES_FILE_NAME = "sonar-project.properties";
+
+ private final Map<String, ProjectState> projectStatePerProjectKey = new HashMap<>();
+ private final Set<String> knownProjects = new HashSet<>();
+
+ public void reset() {
+ this.projectStatePerProjectKey.clear();
+ this.knownProjects.clear();
+ }
+
+ public String load(String projectRelativePath) {
+ checkState(!knownProjects.contains(projectRelativePath), "Project at location %s already loaded", projectRelativePath);
+
+ File projectDir = ItUtils.projectDir(projectRelativePath);
+ Properties sonarProjectProperties = loadProjectProperties(projectDir);
+ ProjectState projectState = new ProjectState(projectDir, sonarProjectProperties);
+
+ register(projectRelativePath, projectState);
+
+ return projectState.getProjectKey();
+ }
+
+ public ProjectState getProjectState(String projectKey) {
+ ProjectState projectState = this.projectStatePerProjectKey.get(projectKey);
+ checkArgument(projectState != null, "Project with key %s is unknown to %s", projectKey, ProjectAnalysisRule.class.getSimpleName());
+ return projectState;
+ }
+
+ private void register(String projectRelativePath, ProjectState projectState) {
+ this.projectStatePerProjectKey.put(projectState.getProjectKey(), projectState);
+ this.knownProjects.add(projectRelativePath);
+ }
+
+ private static Properties loadProjectProperties(File projectDir) {
+ File sonarPropertiesFile = new File(projectDir, SONAR_PROJECT_PROPERTIES_FILE_NAME);
+ checkArgument(sonarPropertiesFile.exists(), "Can not locate %s in project %s", SONAR_PROJECT_PROPERTIES_FILE_NAME, projectDir.getAbsolutePath());
+
+ Properties properties = new Properties();
+ try {
+ properties.load(new FileReader(sonarPropertiesFile));
+ } catch (IOException e) {
+ Throwables.propagate(e);
+ }
+ return properties;
+ }
+}
diff --git a/tests/src/test/java/util/Profile.java b/tests/src/test/java/util/Profile.java
new file mode 100644
index 00000000000..f9f6a78c52a
--- /dev/null
+++ b/tests/src/test/java/util/Profile.java
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package util;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+final class Profile {
+ static final Profile XOO_EMPTY_PROFILE = new Profile("empty", "xoo", "n/a");
+
+ private final String profileKey;
+ private final String languageKey;
+ private final String relativePath;
+
+ Profile(String profileKey, String languageKey, String relativePath) {
+ this.profileKey = profileKey;
+ this.languageKey = languageKey;
+ this.relativePath = relativePath;
+ }
+
+ public String getProfileKey() {
+ return profileKey;
+ }
+
+ public String getLanguageKey() {
+ return languageKey;
+ }
+
+ public String getRelativePath() {
+ return relativePath;
+ }
+}
diff --git a/tests/src/test/java/util/ProjectAnalysis.java b/tests/src/test/java/util/ProjectAnalysis.java
new file mode 100644
index 00000000000..e60f4c37704
--- /dev/null
+++ b/tests/src/test/java/util/ProjectAnalysis.java
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package util;
+
+public interface ProjectAnalysis {
+ /**
+ * Creates a new ProjectAnalysis which will use the specified quality profile.
+ *
+ * @throws IllegalArgumentException if the quality profile with the specified key has not been loaded into the Rule
+ * @see {@link ProjectAnalysisRule#registerProfile(String)}
+ */
+ ProjectAnalysis withQualityProfile(String qualityProfileKey);
+
+ /**
+ * Creates a new ProjectAnalysis which will use the built-in Xoo empty profile.
+ */
+ ProjectAnalysis withXooEmptyProfile();
+
+ /**
+ * Creates a new ProjectAnalysis which will have debug logs enabled (or not).
+ */
+ ProjectAnalysis withDebugLogs(boolean enabled);
+
+ /**
+ * Creates a new ProjectAnalysis which will have the specified properties.
+ */
+ ProjectAnalysis withProperties(String... properties);
+
+ /**
+ * Execute the current ProjectAnalysis.
+ * This method can be called any number of time and will run the same analysis again and again.
+ */
+ void run();
+}
diff --git a/tests/src/test/java/util/ProjectAnalysisRule.java b/tests/src/test/java/util/ProjectAnalysisRule.java
new file mode 100644
index 00000000000..89debd0c066
--- /dev/null
+++ b/tests/src/test/java/util/ProjectAnalysisRule.java
@@ -0,0 +1,220 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package util;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableMap;
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarRunner;
+import java.util.HashSet;
+import java.util.Set;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.junit.rules.ExternalResource;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.Objects.requireNonNull;
+import static util.ItUtils.resetSettings;
+
+/**
+ * Rule wrapping around an {@link Orchestrator} instance which handle:
+ * <ul>
+ * <li>automatic reset of Orchestrator data after each method when used as a {@link org.junit.Rule},
+ * after each class when used as a {@link org.junit.ClassRule}</li>
+ * <li>automatic reset of server properties after each method when used as a {@link org.junit.Rule},
+ * after each class when used as a {@link org.junit.ClassRule}</li>
+ * <li>associating project with a specific Quality Profile before running an analysis</li>
+ * <li>provisioning a project before its first analysis so that a Quality Profile can be associated to it</li>
+ * <li>"restoring" a Quality Profile before an analysis with a specific Quality Profile</li>
+ * </ul>
+ *
+ * This Rule has preparatory methods ({@link #registerProfile(String)} and {@link #registerProject(String)}) which
+ * will allow consequent calls to the rule methods to be based solely on Quality Profile and Project keys. In addition,
+ * these methods returns the Quality Profile and Project key to avoid information duplication (the only magic string
+ * the IT developer has to know is the relative path of the Project or the Quality Profile).
+ *
+ * To run an analysis, use method {@link #newProjectAnalysis(String)} to create a {@link ProjectAnalysis}
+ * object. This object has a {@link ProjectAnalysis#run()} method which will start the analysis.
+ * {@link ProjectAnalysis} can safely be reused to run the same analysis multiple times. In addition, these objects are
+ * immutable. Any call to one of their method which would modify their state will create a new instance which can also
+ * be reused at will.
+ */
+public class ProjectAnalysisRule extends ExternalResource {
+
+ private final Orchestrator orchestrator;
+ private final LoadedProfiles loadedProfiles = new LoadedProfiles();
+ private final LoadedProjects loadedProjects = new LoadedProjects();
+ private final Set<String> serverProperties = new HashSet<>();
+
+ private ProjectAnalysisRule(Orchestrator orchestrator) {
+ this.orchestrator = orchestrator;
+ }
+
+ public static ProjectAnalysisRule from(Orchestrator orchestrator) {
+ return new ProjectAnalysisRule(requireNonNull(orchestrator, "Orchestrator instance can not be null"));
+ }
+
+ /**
+ * @param relativePathToProfile eg.: "/issue/suite/IssueFilterExtensionTest/xoo-with-many-rules.xml"
+ *
+ * @return the quality profile key
+ */
+ public String registerProfile(String relativePathToProfile) {
+ return this.loadedProfiles.loadProfile(relativePathToProfile);
+ }
+
+ /**
+ * @param projectRelativePath path relative to it/projects, eg. "shared/xoo-multi-modules-sample"
+ *
+ * @return the project key
+ */
+ public String registerProject(String projectRelativePath) {
+ return this.loadedProjects.load(projectRelativePath);
+ }
+
+ public ProjectAnalysis newProjectAnalysis(String projectKey) {
+ ProjectState projectState = this.loadedProjects.getProjectState(projectKey);
+
+ return new ProjectAnalysisImpl(projectState, null, false);
+ }
+
+ @Override
+ protected void before() throws Throwable {
+ orchestrator.resetData();
+ }
+
+ @Override
+ protected void after() {
+ resetServerProperties();
+ resetRuleState();
+ }
+
+ private void resetServerProperties() {
+ resetSettings(orchestrator, null, serverProperties.toArray(new String[] {}));
+ }
+
+ public void setServerPropertyImpl(String key, @Nullable String value) {
+ ItUtils.setServerProperty(orchestrator, key, value);
+ }
+
+ public ProjectAnalysisRule setServerProperty(String key, String value) {
+ setServerPropertyImpl(key, value);
+ this.serverProperties.add(key);
+ return this;
+ }
+
+ @Immutable
+ private final class ProjectAnalysisImpl implements ProjectAnalysis {
+ private final ProjectState projectState;
+ @CheckForNull
+ private final Profile qualityProfile;
+ private final boolean debugLogs;
+ @CheckForNull
+ private final String[] properties;
+
+ private ProjectAnalysisImpl(ProjectState projectState, @Nullable Profile qualityProfile, boolean debugLogs, String... properties) {
+ this.projectState = projectState;
+ this.qualityProfile = qualityProfile;
+ this.debugLogs = debugLogs;
+ this.properties = properties;
+ }
+
+ @Override
+ public ProjectAnalysis withQualityProfile(String qualityProfileKey) {
+ checkNotNull(qualityProfileKey, "Specified Quality Profile Key can not be null");
+ if (this.qualityProfile != null && this.qualityProfile.getProfileKey().equals(qualityProfileKey)) {
+ return this;
+ }
+
+ return new ProjectAnalysisImpl(this.projectState, loadedProfiles.getState(qualityProfileKey), this.debugLogs, this.properties);
+ }
+
+ @Override
+ public ProjectAnalysis withXooEmptyProfile() {
+ if (this.qualityProfile == Profile.XOO_EMPTY_PROFILE) {
+ return this;
+ }
+ return new ProjectAnalysisImpl(this.projectState, Profile.XOO_EMPTY_PROFILE, this.debugLogs, this.properties);
+ }
+
+ @Override
+ public ProjectAnalysis withDebugLogs(boolean enabled) {
+ if (this.debugLogs == enabled) {
+ return this;
+ }
+ return new ProjectAnalysisImpl(this.projectState, this.qualityProfile, enabled, this.properties);
+ }
+
+ @Override
+ public ProjectAnalysis withProperties(String... properties) {
+ checkArgument(
+ properties == null || properties.length % 2 == 0,
+ "there must be an even number of String parameters (got %s): key/value pairs must be complete", properties == null ? 0 : properties.length);
+ return new ProjectAnalysisImpl(this.projectState, this.qualityProfile, this.debugLogs, properties);
+ }
+
+ @Override
+ public void run() {
+ provisionIfNecessary();
+ setQualityProfileIfNecessary();
+ runAnalysis();
+ }
+
+ private void setQualityProfileIfNecessary() {
+ if (this.qualityProfile != null) {
+ if (this.qualityProfile != Profile.XOO_EMPTY_PROFILE) {
+ ItUtils.restoreProfile(orchestrator, getClass().getResource(this.qualityProfile.getRelativePath()));
+ }
+ orchestrator.getServer().associateProjectToQualityProfile(
+ this.projectState.getProjectKey(),
+ this.qualityProfile.getLanguageKey(),
+ this.qualityProfile.getProfileKey());
+ }
+ }
+
+ private void provisionIfNecessary() {
+ if (this.qualityProfile != null && !projectState.isProvisioned()) {
+ String projectKey = projectState.getProjectKey();
+ orchestrator.getServer().provisionProject(projectKey, MoreObjects.firstNonNull(projectState.getProjectName(), projectKey));
+ projectState.setProvisioned(true);
+ }
+ }
+
+ private SonarRunner runAnalysis() {
+ SonarRunner sonarRunner = SonarRunner.create(projectState.getProjectDir());
+ ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
+ for (int i = 0; i < this.properties.length; i += 2) {
+ builder.put(this.properties[i], this.properties[i + 1]);
+ }
+ SonarRunner scan = sonarRunner.setDebugLogs(this.debugLogs).setProperties(builder.build());
+ orchestrator.executeBuild(scan);
+ return scan;
+ }
+ }
+
+ private void resetRuleState() {
+ this.loadedProjects.reset();
+ this.loadedProfiles.reset();
+ this.serverProperties.clear();
+ }
+
+}
diff --git a/tests/src/test/java/util/ProjectState.java b/tests/src/test/java/util/ProjectState.java
new file mode 100644
index 00000000000..cfa310326b4
--- /dev/null
+++ b/tests/src/test/java/util/ProjectState.java
@@ -0,0 +1,71 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package util;
+
+import java.io.File;
+import java.util.Properties;
+
+import static com.google.common.base.Preconditions.checkState;
+import static util.LoadedProjects.SONAR_PROJECT_PROPERTIES_FILE_NAME;
+
+final class ProjectState {
+ private static final String SONAR_PROJECT_KEY_PROPERTY_NAME = "sonar.projectKey";
+ private static final String SONAR_PROJECT_NAME_PROPERTY_NAME = "sonar.projectName";
+
+ private final File projectDir;
+ private final Properties properties;
+ private boolean provisioned = false;
+
+ ProjectState(File projectDir, Properties properties) {
+ this.projectDir = projectDir;
+ this.properties = properties;
+ }
+
+ public File getProjectDir() {
+ return projectDir;
+ }
+
+ public Properties getProperties() {
+ return properties;
+ }
+
+ public String getProjectKey() {
+ return getProperty(SONAR_PROJECT_KEY_PROPERTY_NAME);
+ }
+
+ public String getProjectName() {
+ return getProperty(SONAR_PROJECT_NAME_PROPERTY_NAME);
+ }
+
+ private String getProperty(String propertyName) {
+ String value = this.properties.getProperty(propertyName);
+ checkState(value != null, "Property %s is missing in %s file in project directory %s",
+ propertyName, SONAR_PROJECT_PROPERTIES_FILE_NAME, projectDir.getAbsolutePath());
+ return value;
+ }
+
+ public boolean isProvisioned() {
+ return provisioned;
+ }
+
+ public void setProvisioned(boolean provisioned) {
+ this.provisioned = provisioned;
+ }
+}
diff --git a/tests/src/test/java/util/issue/IssueRule.java b/tests/src/test/java/util/issue/IssueRule.java
new file mode 100644
index 00000000000..7857d3cabdc
--- /dev/null
+++ b/tests/src/test/java/util/issue/IssueRule.java
@@ -0,0 +1,79 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package util.issue;
+
+import com.sonar.orchestrator.Orchestrator;
+import java.util.List;
+import org.junit.rules.ExternalResource;
+import org.sonarqube.ws.Issues.Issue;
+import org.sonarqube.ws.Issues.SearchWsResponse;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.issue.SearchWsRequest;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static java.util.Objects.requireNonNull;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newAdminWsClient;
+
+public class IssueRule extends ExternalResource {
+
+ private final Orchestrator orchestrator;
+
+ private WsClient adminWsClient;
+
+ private IssueRule(Orchestrator orchestrator) {
+ this.orchestrator = orchestrator;
+ }
+
+ public static IssueRule from(Orchestrator orchestrator) {
+ return new IssueRule(requireNonNull(orchestrator, "Orchestrator instance can not be null"));
+ }
+
+ public SearchWsResponse search(SearchWsRequest request) {
+ return adminWsClient().issues().search(request);
+ }
+
+ public Issue getRandomIssue() {
+ List<Issue> issues = search(new SearchWsRequest()).getIssuesList();
+ assertThat(issues).isNotEmpty();
+ return issues.get(0);
+ }
+
+ public Issue getByKey(String issueKey) {
+ List<Issue> issues = search(new SearchWsRequest().setIssues(singletonList(issueKey)).setAdditionalFields(singletonList("_all"))).getIssuesList();
+ assertThat(issues).hasSize(1);
+ return issues.iterator().next();
+ }
+
+ public List<Issue> getByKeys(String... issueKeys) {
+ List<Issue> issues = search(new SearchWsRequest().setIssues(asList(issueKeys)).setAdditionalFields(singletonList("_all"))).getIssuesList();
+ assertThat(issues).hasSize(issueKeys.length);
+ return issues;
+ }
+
+ private WsClient adminWsClient() {
+ if (adminWsClient == null) {
+ adminWsClient = newAdminWsClient(orchestrator);
+ }
+ return adminWsClient;
+ }
+
+}
diff --git a/tests/src/test/java/util/selenium/ByCssSelectorOrByNameOrById.java b/tests/src/test/java/util/selenium/ByCssSelectorOrByNameOrById.java
new file mode 100644
index 00000000000..364ba779d6e
--- /dev/null
+++ b/tests/src/test/java/util/selenium/ByCssSelectorOrByNameOrById.java
@@ -0,0 +1,107 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package util.selenium;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.List;
+import org.openqa.selenium.By;
+import org.openqa.selenium.SearchContext;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.internal.FindsByCssSelector;
+import org.openqa.selenium.internal.FindsById;
+import org.openqa.selenium.internal.FindsByName;
+
+public class ByCssSelectorOrByNameOrById extends By implements Serializable {
+ private static final long serialVersionUID = -3910258723099459239L;
+
+ private final String selector;
+
+ public ByCssSelectorOrByNameOrById(String selector) {
+ this.selector = selector;
+ }
+
+ @Override
+ public WebElement findElement(SearchContext context) {
+ WebElement element;
+
+ if (validCssSelector(selector)) {
+ element = ((FindsByCssSelector) context).findElementByCssSelector(quoteCss(selector));
+ if (element != null) {
+ return element;
+ }
+ }
+
+ element = ((FindsByName) context).findElementByName(selector);
+ if (element != null) {
+ return element;
+ }
+
+ element = ((FindsById) context).findElementById(selector);
+ if (element != null) {
+ return element;
+ }
+
+ return null;
+ }
+
+ @Override
+ public List<WebElement> findElements(SearchContext context) {
+ List<WebElement> elements;
+
+ if (validCssSelector(selector)) {
+ elements = ((FindsByCssSelector) context).findElementsByCssSelector(quoteCss(selector));
+ if ((elements != null) && (!elements.isEmpty())) {
+ return elements;
+ }
+ }
+
+ elements = ((FindsByName) context).findElementsByName(selector);
+ if ((elements != null) && (!elements.isEmpty())) {
+ return elements;
+ }
+
+ elements = ((FindsById) context).findElementsById(selector);
+ if ((elements != null) && (!elements.isEmpty())) {
+ return elements;
+ }
+
+ return Collections.emptyList();
+ }
+
+ protected boolean validCssSelector(String selector) {
+ return !selector.endsWith("[]");
+ }
+
+ protected String quoteCss(String selector) {
+ if (selector.startsWith(".")) {
+ return selector;
+ }
+ if (selector.startsWith("#")) {
+ return selector.replaceAll("(\\w)[.]", "$1\\\\.");
+ }
+ return selector;
+ }
+
+ @Override
+ public String toString() {
+ return selector;
+ }
+}
diff --git a/tests/src/test/java/util/selenium/Consumer.java b/tests/src/test/java/util/selenium/Consumer.java
new file mode 100644
index 00000000000..69009893a97
--- /dev/null
+++ b/tests/src/test/java/util/selenium/Consumer.java
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package util.selenium;
+
+public interface Consumer<T> {
+ void accept(T t);
+}
diff --git a/tests/src/test/java/util/selenium/ElementFilter.java b/tests/src/test/java/util/selenium/ElementFilter.java
new file mode 100644
index 00000000000..86b42cfa826
--- /dev/null
+++ b/tests/src/test/java/util/selenium/ElementFilter.java
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package util.selenium;
+
+import com.google.common.base.Function;
+import java.util.Collection;
+import org.openqa.selenium.WebElement;
+
+class ElementFilter {
+ private static final ElementFilter ANY = new ElementFilter("", new Function<Collection<WebElement>, Collection<WebElement>>() {
+ @Override
+ public Collection<WebElement> apply(Collection<WebElement> input) {
+ return input;
+ }
+ });
+
+ private final String description;
+ private final Function<Collection<WebElement>, Collection<WebElement>> filter;
+
+ ElementFilter(String description, Function<Collection<WebElement>, Collection<WebElement>> filter) {
+ this.description = description;
+ this.filter = filter;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public Function<Collection<WebElement>, Collection<WebElement>> getFilter() {
+ return filter;
+ }
+
+ public static ElementFilter any() {
+ return ANY;
+ }
+
+ public ElementFilter and(final ElementFilter second) {
+ if (ANY == this) {
+ return second;
+ }
+ if (ANY == second) {
+ return this;
+ }
+ return new ElementFilter(description + ',' + second.description, new Function<Collection<WebElement>, Collection<WebElement>>() {
+ @Override
+ public Collection<WebElement> apply(Collection<WebElement> stream) {
+ return second.filter.apply(filter.apply(stream));
+ }
+ });
+ }
+}
diff --git a/tests/src/test/java/util/selenium/Failure.java b/tests/src/test/java/util/selenium/Failure.java
new file mode 100644
index 00000000000..8ba36543820
--- /dev/null
+++ b/tests/src/test/java/util/selenium/Failure.java
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package util.selenium;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class Failure {
+ private static final String PREFIX = Failure.class.getPackage().getName() + ".";
+
+ private Failure() {
+ // Static class
+ }
+
+ public static AssertionError create(String message) {
+ AssertionError error = new AssertionError(message);
+ removeSimpleleniumFromStackTrace(error);
+ return error;
+ }
+
+ private static void removeSimpleleniumFromStackTrace(Throwable throwable) {
+ List<StackTraceElement> filtered = new ArrayList<>();
+
+ for (StackTraceElement element : throwable.getStackTrace()) {
+ if (!element.getClassName().contains(PREFIX)) {
+ filtered.add(element);
+ }
+ }
+
+ throwable.setStackTrace(filtered.toArray(new StackTraceElement[filtered.size()]));
+ }
+}
diff --git a/tests/src/test/java/util/selenium/LazyDomElement.java b/tests/src/test/java/util/selenium/LazyDomElement.java
new file mode 100644
index 00000000000..27c5199f820
--- /dev/null
+++ b/tests/src/test/java/util/selenium/LazyDomElement.java
@@ -0,0 +1,174 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package util.selenium;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.base.Supplier;
+import com.google.common.collect.FluentIterable;
+import java.util.Collection;
+import java.util.List;
+import java.util.NoSuchElementException;
+import javax.annotation.Nullable;
+import org.openqa.selenium.By;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.interactions.Actions;
+import org.openqa.selenium.support.ui.Select;
+
+class LazyDomElement {
+ private final WebDriver driver;
+ private final By selector;
+ private final ElementFilter filter;
+ private final Retry retry;
+
+ LazyDomElement(WebDriver driver, By selector) {
+ this(driver, selector, Retry._30_SECONDS);
+ }
+
+ LazyDomElement(WebDriver driver, By selector, Retry retry) {
+ this(driver, selector, ElementFilter.any(), retry);
+ }
+
+ private LazyDomElement(WebDriver driver, By selector, ElementFilter filter, Retry retry) {
+ this.driver = driver;
+ this.selector = selector;
+ this.filter = filter;
+ this.retry = retry;
+ }
+
+ public LazyDomElement withText(final String text) {
+ String fullDescription = " with text [" + text + "]";
+
+ return with(new ElementFilter(fullDescription, new Function<Collection<WebElement>, Collection<WebElement>>() {
+ @Override
+ public Collection<WebElement> apply(Collection<WebElement> stream) {
+ return FluentIterable.from(stream).filter(new Predicate<WebElement>() {
+ @Override
+ public boolean apply(@Nullable WebElement element) {
+// return Objects.equals(element.getText(), text);
+ return element.getText().contains(text);
+ }
+ }).toList();
+ }
+ }));
+ }
+
+ public LazyShould should() {
+ return new LazyShould(this, Retry._30_SECONDS, true);
+ }
+
+ public void fill(final CharSequence text) {
+ execute("fill(" + text + ")", new Consumer<WebElement>() {
+ @Override
+ public void accept(WebElement element) {
+ element.clear();
+ element.sendKeys(text);
+ }
+ });
+ }
+
+ public void pressEnter() {
+ execute("pressEnter", new Consumer<WebElement>() {
+ @Override
+ public void accept(WebElement element) {
+ element.sendKeys(Keys.ENTER);
+ }
+ });
+ }
+
+ public void select(final String text) {
+ executeSelect("select(" + text + ")", new Consumer<Select>() {
+ @Override
+ public void accept(Select select) {
+ select.selectByVisibleText(text);
+ }
+ });
+ }
+
+ public void executeSelect(String description, final Consumer<Select> selectOnElement) {
+ execute(description, new Consumer<WebElement>() {
+ @Override
+ public void accept(WebElement element) {
+ selectOnElement.accept(new Select(element));
+ }
+ });
+ }
+
+ public void click() {
+ execute("click", new Consumer<WebElement>() {
+ @Override
+ public void accept(WebElement element) {
+ new Actions(driver).moveToElement(element);
+ element.click();
+ }
+ });
+ }
+
+ public void check() {
+ execute("check", new Consumer<WebElement>() {
+ @Override
+ public void accept(WebElement element) {
+ if (!element.isSelected()) {
+ element.click();
+ }
+ }
+ });
+ }
+
+ public void execute(Consumer<WebElement> action) {
+ execute("execute(" + action + ")", action);
+ }
+
+ private LazyDomElement with(ElementFilter filter) {
+ return new LazyDomElement(driver, selector, this.filter.and(filter), retry);
+ }
+
+ private void execute(String message, Consumer<WebElement> action) {
+ System.out.println(" - " + Text.toString(selector) + filter.getDescription() + "." + message);
+
+ Supplier<Optional<WebElement>> findOne = new Supplier<Optional<WebElement>>() {
+ @Override
+ public Optional<WebElement> get() {
+ List<WebElement> elements = stream();
+ if (elements.isEmpty()) {
+ return Optional.empty();
+ }
+ return Optional.of(elements.get(0));
+ }
+ };
+
+ try {
+ retry.execute(findOne, action);
+ } catch (NoSuchElementException e) {
+ throw new AssertionError("Element not found: " + Text.toString(selector));
+ }
+ }
+
+ List<WebElement> stream() {
+ return FluentIterable.from(filter.getFilter().apply(driver.findElements(selector))).toList();
+ }
+
+ @Override
+ public String toString() {
+ return Text.toString(selector) + filter.getDescription();
+ }
+}
diff --git a/tests/src/test/java/util/selenium/LazyShould.java b/tests/src/test/java/util/selenium/LazyShould.java
new file mode 100644
index 00000000000..c880a7d944b
--- /dev/null
+++ b/tests/src/test/java/util/selenium/LazyShould.java
@@ -0,0 +1,190 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package util.selenium;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.base.Supplier;
+import com.google.common.collect.FluentIterable;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.regex.Pattern;
+import javax.annotation.Nullable;
+import org.openqa.selenium.WebElement;
+
+class LazyShould {
+ private final LazyDomElement element;
+ private final Retry retry;
+ private final boolean ok;
+
+ LazyShould(LazyDomElement element, Retry retry, boolean ok) {
+ this.element = element;
+ this.retry = retry;
+ this.ok = ok;
+ }
+
+ public LazyShould beDisplayed() {
+ return verify(
+ isOrNot("displayed"),
+ new Predicate<List<WebElement>>() {
+ @Override
+ public boolean apply(List<WebElement> elements) {
+ return !elements.isEmpty() && FluentIterable.from(elements).allMatch(new Predicate<WebElement>() {
+ @Override
+ public boolean apply(WebElement element) {
+ return element.isDisplayed();
+ }
+ });
+ }
+ },
+ new Function<List<WebElement>, String>() {
+ @Override
+ public String apply(List<WebElement> elements) {
+ return "It is " + statuses(elements, new Function<WebElement, String>() {
+ @Override
+ public String apply(WebElement element) {
+ return displayedStatus(element);
+ }
+ });
+ }
+ });
+ }
+
+ public LazyShould match(final Pattern regexp) {
+ return verify(
+ doesOrNot("match") + " (" + regexp.pattern() + ")",
+ new Predicate<List<WebElement>>() {
+ @Override
+ public boolean apply(List<WebElement> elements) {
+ return !elements.isEmpty() && FluentIterable.from(elements).anyMatch(new Predicate<WebElement>() {
+ @Override
+ public boolean apply(WebElement element) {
+ return regexp.matcher(WebElementHelper.text(element)).matches();
+ }
+ });
+ }
+ },
+ new Function<List<WebElement>, String>() {
+ @Override
+ public String apply(List<WebElement> elements) {
+ return "It contains " + statuses(elements, new Function<WebElement, String>() {
+ @Nullable
+ @Override
+ public String apply(@Nullable WebElement element) {
+ return WebElementHelper.text(element);
+ }
+ });
+ }
+ });
+ }
+
+ public LazyShould contain(final String text) {
+ return verify(
+ doesOrNot("contain") + "(" + text + ")",
+ new Predicate<List<WebElement>>() {
+ @Override
+ public boolean apply(List<WebElement> elements) {
+ return FluentIterable.from(elements).anyMatch(new Predicate<WebElement>() {
+ @Override
+ public boolean apply(@Nullable WebElement element) {
+ if (text.startsWith("exact:")) {
+ return WebElementHelper.text(element).equals(text.substring(6));
+ }
+ return WebElementHelper.text(element).contains(text);
+ }
+ });
+ }
+ },
+ new Function<List<WebElement>, String>() {
+ @Override
+ public String apply(List<WebElement> elements) {
+ return "It contains " + statuses(elements, new Function<WebElement, String>() {
+ @Override
+ public String apply(WebElement element) {
+ return WebElementHelper.text(element);
+ }
+ });
+ }
+ });
+ }
+
+ public LazyShould exist() {
+ return verify(
+ doesOrNot("exist"),
+ new Predicate<List<WebElement>>() {
+ @Override
+ public boolean apply(List<WebElement> elements) {
+ return !elements.isEmpty();
+ }
+ },
+ new Function<List<WebElement>, String>() {
+ @Override
+ public String apply(List<WebElement> elements) {
+ return "It contains " + Text.plural(elements.size(), "element");
+ }
+ });
+ }
+
+ private static String displayedStatus(WebElement element) {
+ return element.isDisplayed() ? "displayed" : "not displayed";
+ }
+
+ private LazyShould verify(String message, Predicate<List<WebElement>> predicate, Function<List<WebElement>, String> toErrorMessage) {
+ String verification = "verify that " + element + " " + message;
+ System.out.println(" -> " + verification);
+
+ try {
+ if (!retry.verify(new Supplier<List<WebElement>>() {
+ @Override
+ public List<WebElement> get() {
+ return LazyShould.this.findElements();
+ }
+ }, ok ? predicate : Predicates.not(predicate))) {
+ throw Failure.create("Failed to " + verification + ". " + toErrorMessage.apply(findElements()));
+ }
+ } catch (NoSuchElementException e) {
+ throw Failure.create("Element not found. Failed to " + verification);
+ }
+
+ return ok ? this : not();
+ }
+
+ private List<WebElement> findElements() {
+ return element.stream();
+ }
+
+ private static String statuses(List<WebElement> elements, Function<WebElement, String> toStatus) {
+ return "(" + FluentIterable.from(elements).transform(toStatus).join(Joiner.on(";")) + ")";
+ }
+
+ public LazyShould not() {
+ return new LazyShould(element, retry, !ok);
+ }
+
+ private String doesOrNot(String verb) {
+ return Text.doesOrNot(!ok, verb);
+ }
+
+ private String isOrNot(String state) {
+ return Text.isOrNot(!ok, state);
+ }
+}
diff --git a/tests/src/test/java/util/selenium/Optional.java b/tests/src/test/java/util/selenium/Optional.java
new file mode 100644
index 00000000000..918d488a85f
--- /dev/null
+++ b/tests/src/test/java/util/selenium/Optional.java
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package util.selenium;
+
+import java.util.NoSuchElementException;
+
+public final class Optional<T> {
+ private static final Optional<?> EMPTY = new Optional<>();
+
+ private final T value;
+
+ private Optional() {
+ this.value = null;
+ }
+
+ public static <T> Optional<T> empty() {
+ @SuppressWarnings("unchecked")
+ Optional<T> t = (Optional<T>) EMPTY;
+ return t;
+ }
+
+ private Optional(T value) {
+ this.value = value;
+ }
+
+ public static <T> Optional<T> of(T value) {
+ return new Optional<>(value);
+ }
+
+ public T get() {
+ if (value == null) {
+ throw new NoSuchElementException("No value present");
+ }
+ return value;
+ }
+
+ public boolean isPresent() {
+ return value != null;
+ }
+}
diff --git a/tests/src/test/java/util/selenium/Retry.java b/tests/src/test/java/util/selenium/Retry.java
new file mode 100644
index 00000000000..7cc8a20897c
--- /dev/null
+++ b/tests/src/test/java/util/selenium/Retry.java
@@ -0,0 +1,152 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package util.selenium;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Supplier;
+import java.util.NoSuchElementException;
+import java.util.concurrent.TimeUnit;
+import org.openqa.selenium.InvalidElementStateException;
+import org.openqa.selenium.NotFoundException;
+import org.openqa.selenium.StaleElementReferenceException;
+import org.openqa.selenium.WebDriverException;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+class Retry {
+ public static final Retry _30_SECONDS = new Retry(30, SECONDS);
+
+ private final long timeoutInMs;
+
+ Retry(long duration, TimeUnit timeUnit) {
+ this.timeoutInMs = timeUnit.toMillis(duration);
+ }
+
+ <T> void execute(Supplier<Optional<T>> target, Consumer<T> action) {
+ WebDriverException lastError = null;
+
+ boolean retried = false;
+
+ long start = System.currentTimeMillis();
+ while ((System.currentTimeMillis() - start) < timeoutInMs) {
+ try {
+ Optional<T> targetElement = target.get();
+ if (targetElement.isPresent()) {
+ action.accept(targetElement.get());
+ if (retried) {
+ System.out.println();
+ }
+ return;
+ }
+ } catch (StaleElementReferenceException e) {
+ // ignore
+ } catch (WebDriverException e) {
+ lastError = e;
+ }
+
+ retried = true;
+ System.out.print(".");
+ }
+
+ if (retried) {
+ System.out.println();
+ }
+
+ if (lastError != null) {
+ throw lastError;
+ }
+ throw new NoSuchElementException("Not found");
+ }
+
+ <T> void execute(Runnable action) {
+ WebDriverException lastError = null;
+
+ boolean retried = false;
+
+ long start = System.currentTimeMillis();
+ while ((System.currentTimeMillis() - start) < timeoutInMs) {
+ try {
+ action.run();
+ if (retried) {
+ System.out.println();
+ }
+ return;
+ } catch (StaleElementReferenceException e) {
+ // ignore
+ } catch (WebDriverException e) {
+ lastError = e;
+ }
+
+ retried = true;
+ System.out.print(".");
+ }
+
+ if (retried) {
+ System.out.println();
+ }
+
+ if (lastError != null) {
+ throw lastError;
+ }
+ throw new NoSuchElementException("Not found");
+ }
+
+ <T> boolean verify(Supplier<T> targetSupplier, Predicate<T> predicate) throws NoSuchElementException {
+ Error error = Error.KO;
+
+ boolean retried = false;
+
+ long start = System.currentTimeMillis();
+ while ((System.currentTimeMillis() - start) < timeoutInMs) {
+ try {
+ if (predicate.apply(targetSupplier.get())) {
+ if (retried) {
+ System.out.println();
+ }
+ return true;
+ }
+
+ error = Error.KO;
+ } catch (InvalidElementStateException e) {
+ error = Error.KO;
+ } catch (NotFoundException e) {
+ error = Error.NOT_FOUND;
+ } catch (StaleElementReferenceException e) {
+ // ignore
+ }
+
+ retried = true;
+ System.out.print(".");
+ }
+
+ if (retried) {
+ System.out.println();
+ }
+
+ if (error == Error.NOT_FOUND) {
+ throw new NoSuchElementException("Not found");
+ }
+ return false;
+ }
+
+ enum Error {
+ NOT_FOUND, KO
+ }
+}
diff --git a/tests/src/test/java/util/selenium/Selenese.java b/tests/src/test/java/util/selenium/Selenese.java
new file mode 100644
index 00000000000..187e33d9024
--- /dev/null
+++ b/tests/src/test/java/util/selenium/Selenese.java
@@ -0,0 +1,91 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package util.selenium;
+
+import com.sonar.orchestrator.Orchestrator;
+import java.io.File;
+import javax.annotation.Nullable;
+import org.apache.commons.io.FileUtils;
+import org.sonarqube.tests.Tester;
+
+/**
+ * Selenium HTML tests, generally written with Selenium IDE
+ * @deprecated replaced by {@link Tester}
+ */
+@Deprecated
+public final class Selenese {
+
+ private File[] htmlTests;
+
+ public Selenese(Builder builder) {
+ this.htmlTests = builder.htmlTests;
+ }
+
+ public File[] getHtmlTests() {
+ return htmlTests;
+ }
+
+ /**
+ * @deprecated replaced by {@link Tester#runHtmlTests(String...)}
+ */
+ @Deprecated
+ public static void runSelenese(Orchestrator orchestrator, String... htmlFiles) {
+ Selenese selenese = new Builder()
+ .setHtmlTests(htmlFiles)
+ .build();
+ new SeleneseRunner().runOn(selenese, orchestrator);
+ }
+
+ private static final class Builder {
+ private File[] htmlTests;
+
+ private Builder() {
+ }
+
+ public Builder setHtmlTests(File... htmlTests) {
+ this.htmlTests = htmlTests;
+ return this;
+ }
+
+ public Builder setHtmlTests(String... htmlTestPaths) {
+ this.htmlTests = new File[htmlTestPaths.length];
+ for (int index = 0; index < htmlTestPaths.length; index++) {
+ htmlTests[index] = FileUtils.toFile(getClass().getResource(htmlTestPaths[index]));
+ }
+ return this;
+ }
+
+ public Selenese build() {
+ if (htmlTests == null || htmlTests.length == 0) {
+ throw new IllegalArgumentException("HTML suite or tests are missing");
+ }
+ for (File htmlTest : htmlTests) {
+ checkPresence(htmlTest);
+ }
+ return new Selenese(this);
+ }
+
+ private static void checkPresence(@Nullable File file) {
+ if (file == null || !file.isFile() || !file.exists()) {
+ throw new IllegalArgumentException("HTML file does not exist: " + file);
+ }
+ }
+ }
+}
diff --git a/tests/src/test/java/util/selenium/SeleneseRunner.java b/tests/src/test/java/util/selenium/SeleneseRunner.java
new file mode 100644
index 00000000000..96cc4e609a2
--- /dev/null
+++ b/tests/src/test/java/util/selenium/SeleneseRunner.java
@@ -0,0 +1,440 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package util.selenium;
+
+import com.sonar.orchestrator.Orchestrator;
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Pattern;
+import org.assertj.core.util.Strings;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.openqa.selenium.By;
+import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.NotFoundException;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.logging.LogEntries;
+import org.openqa.selenium.logging.LogEntry;
+import org.openqa.selenium.logging.LogType;
+import org.sonarqube.pageobjects.SelenideConfig;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.requireNonNull;
+import static java.util.regex.Pattern.DOTALL;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.selenium.Retry._30_SECONDS;
+
+class SeleneseRunner {
+
+ private Map<String, String> variables;
+ private String baseUrl;
+ private WebDriver driver;
+
+ void runOn(Selenese selenese, Orchestrator orchestrator) {
+ this.variables = new HashMap<>();
+ this.baseUrl = orchestrator.getServer().getUrl();
+ this.driver = SelenideConfig.configure(orchestrator);
+
+ driver.manage().deleteAllCookies();
+
+ for (File file : selenese.getHtmlTests()) {
+ System.out.println();
+ System.out.println("============ " + file.getName() + " ============");
+ Document doc = parse(file);
+ for (Element table : doc.getElementsByTag("table")) {
+ for (Element tbody : table.getElementsByTag("tbody")) {
+ for (Element tr : tbody.getElementsByTag("tr")) {
+ String action = tr.child(0).text();
+ String param1 = tr.child(1).text();
+ String param2 = tr.child(2).text();
+
+ try {
+ action(action, param1, param2);
+ } catch (AssertionError e) {
+ analyzeLog(driver);
+ throw e;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private static void analyzeLog(WebDriver driver) {
+ LogEntries logEntries = driver.manage().logs().get(LogType.BROWSER);
+ for (LogEntry entry : logEntries) {
+ System.out.println(new Date(entry.getTimestamp()) + " " + entry.getLevel() + " " + entry.getMessage());
+ }
+ }
+
+ private static Document parse(File file) {
+ try {
+ return Jsoup.parse(file, UTF_8.name());
+ } catch (IOException e) {
+ throw new RuntimeException("Unable to parse file: " + file, e);
+ }
+ }
+
+ public SeleneseRunner action(String action, String param1, String param2) {
+ switch (action) {
+ case "open":
+ open(param1);
+ return this;
+ case "type":
+ type(param1, param2);
+ return this;
+ case "keyPressAndWait":
+ keyPressAndWait(param1, param2);
+ return this;
+ case "select":
+ select(param1, param2);
+ return this;
+ case "clickAndWait":
+ case "click":
+ click(param1);
+ return this;
+ case "check":
+ check(param1);
+ return this;
+ case "selectFrame":
+ selectFrame(param1);
+ return this;
+ case "assertElementPresent":
+ assertElementPresent(param1);
+ return this;
+ case "assertElementNotPresent":
+ assertElementNotPresent(param1);
+ return this;
+ case "storeText":
+ storeText(param1, param2);
+ return this;
+ case "storeEval":
+ storeEval(param1, param2);
+ return this;
+ case "store":
+ store(param1, param2);
+ return this;
+ case "assertText":
+ case "waitForText":
+ assertText(param1, param2);
+ return this;
+ case "assertNotText":
+ case "waitForNotText":
+ assertNotText(param1, param2);
+ return this;
+ case "assertTextPresent":
+ assertTextPresent(param1);
+ return this;
+ case "assertTextNotPresent":
+ assertTextNotPresent(param1);
+ return this;
+ case "assertLocation":
+ assertLocation(param1);
+ return this;
+ case "verifyHtmlSource":
+ verifyHtmlSource(param1);
+ return this;
+ case "waitForElementPresent":
+ waitForElementPresent(param1, param2);
+ return this;
+ case "waitForElementNotPresent":
+ waitForElementNotPresent(param1, param2);
+ return this;
+ case "waitForVisible":
+ waitForVisible(param1);
+ return this;
+ case "waitForXpathCount":
+ waitForXpathCount(param1, Integer.parseInt(param2));
+ return this;
+ case "assertValue":
+ case "waitForValue":
+ case "verifyValue":
+ assertInputValue(param1, param2);
+ return this;
+ case "assertConfirmation":
+ confirm(param1);
+ return this;
+ case "setTimeout":
+ case "pause":
+ // Ignore
+ return this;
+ }
+
+ throw new IllegalArgumentException("Unsupported action: " + action);
+ }
+
+ private void open(String url) {
+ if (url.startsWith("/sonar/")) {
+ goTo(url.substring(6));
+ } else {
+ goTo(url);
+ }
+ }
+
+ private void goTo(String url) {
+ requireNonNull(url, "The url cannot be null");
+
+ url = replacePlaceholders(url);
+
+ URI uri = URI.create(url.replace(" ", "%20").replace("|", "%7C"));
+ if (!uri.isAbsolute()) {
+ url = baseUrl + url;
+ }
+
+ System.out.println("goTo " + url);
+ driver.get(url);
+ System.out.println(" - current url " + driver.getCurrentUrl());
+ }
+
+ private LazyDomElement find(String selector) {
+ selector = replacePlaceholders(selector);
+
+ if (selector.startsWith("link=") || selector.startsWith("Link=")) {
+ return find("a").withText(selector.substring(5));
+ }
+
+ By by;
+ if (selector.startsWith("//")) {
+ by = new By.ByXPath(selector);
+ } else if (selector.startsWith("xpath=")) {
+ by = new By.ByXPath(selector.substring(6));
+ } else if (selector.startsWith("id=")) {
+ by = new By.ById(selector.substring(3));
+ } else if (selector.startsWith("name=")) {
+ by = new By.ByName(selector.substring(5));
+ } else if (selector.startsWith("css=")) {
+ by = new By.ByCssSelector(selector.substring(4));
+ } else if (selector.startsWith("class=")) {
+ by = new By.ByCssSelector("." + selector.substring(6));
+ } else {
+ by = new ByCssSelectorOrByNameOrById(selector);
+ }
+
+ return new LazyDomElement(driver, by);
+ }
+
+ private void click(String selector) {
+ find(selector).click();
+ }
+
+ private void check(String selector) {
+ find(selector).check();
+ }
+
+ private void selectFrame(final String id) {
+ if ("relative=parent".equals(id)) {
+ return;
+ }
+
+ System.out.println(" - selectFrame(" + id + ")");
+ _30_SECONDS.execute(new Runnable() {
+ @Override
+ public void run() {
+ driver.switchTo().frame(id);
+ }
+ });
+ }
+
+ private void type(String selector, String text) {
+ find(selector).fill(replacePlaceholders(text));
+ }
+
+ private void keyPressAndWait(String selector, String key) {
+ if (!key.equals("\\13")) {
+ throw new IllegalArgumentException("Invalid key: " + key);
+ }
+ find(selector).pressEnter();
+ }
+
+ private void select(String selector, String text) {
+ if (text.startsWith("label=")) {
+ find(selector).select(text.substring(6));
+ } else {
+ find(selector).select(text);
+ }
+ }
+
+ private void assertElementPresent(String selector) {
+ find(selector).should().beDisplayed();
+ }
+
+ private void assertElementNotPresent(String selector) {
+ find(selector).should().not().beDisplayed();
+ }
+
+ private void storeText(String selector, String name) {
+ find(selector).execute(new ExtractVariable(name));
+ }
+
+ private void storeEval(final String expression, final String name) {
+ // Retry until it's not null and doesn't fail
+ _30_SECONDS.execute(new Runnable() {
+ @Override
+ public void run() {
+ Object result = ((JavascriptExecutor) driver).executeScript("return " + expression);
+ if (result == null) {
+ throw new NotFoundException(expression);
+ }
+ String value = result.toString();
+ variables.put(name, value);
+ }
+ });
+ }
+
+ private void store(String expression, String name) {
+ if (expression.startsWith("javascript{") && expression.endsWith("}")) {
+ storeEval(expression.substring(11, expression.length() - 1), name);
+ } else {
+ throw new IllegalArgumentException("Invalid store expression: " + expression);
+ }
+ }
+
+ private class ExtractVariable implements Consumer<WebElement> {
+ private final String name;
+
+ ExtractVariable(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public void accept(WebElement webElement) {
+ variables.put(name, webElement.getText());
+ }
+
+ public String toString() {
+ return "read value into " + name;
+ }
+ }
+
+ private void assertText(String selector, String pattern) {
+ pattern = replacePlaceholders(pattern);
+
+ if (pattern.startsWith("exact:")) {
+ String expectedText = pattern.substring(6);
+ find(selector).withText(expectedText).should().exist();
+ return;
+ }
+
+ if (pattern.startsWith("regexp:")) {
+ find(selector).should().match(regex(pattern));
+ return;
+ }
+
+ find(selector).should().match(glob(pattern));
+ }
+
+ private void assertNotText(String selector, String pattern) {
+ pattern = replacePlaceholders(pattern);
+
+ if (pattern.startsWith("exact:")) {
+ String expectedText = pattern.substring(6);
+ find(selector).withText(expectedText).should().not().exist();
+ return;
+ }
+
+ if (pattern.startsWith("regexp:")) {
+ find(selector).should().not().match(regex(pattern));
+ return;
+ }
+
+ find(selector).should().not().match(glob(pattern));
+ }
+
+ private static Pattern glob(String pattern) {
+ String regexp = pattern.replaceFirst("glob:", "");
+ regexp = regexp.replaceAll("([\\]\\[\\\\{\\}$\\(\\)\\|\\^\\+.])", "\\\\$1");
+ regexp = regexp.replaceAll("\\*", ".*");
+ regexp = regexp.replaceAll("\\?", ".");
+ return Pattern.compile(regexp, DOTALL | Pattern.CASE_INSENSITIVE);
+ }
+
+ private static Pattern regex(String pattern) {
+ String regexp = pattern.replaceFirst("regexp:", ".*") + ".*";
+ return Pattern.compile(regexp, DOTALL | Pattern.CASE_INSENSITIVE);
+ }
+
+ private void assertTextPresent(String text) {
+ find("body").should().contain(text);
+ }
+
+ private void assertTextNotPresent(String text) {
+ find("body").should().not().contain(text);
+ }
+
+ private void waitForElementPresent(String selector, String text) {
+ if (Strings.isNullOrEmpty(text)) {
+ find(selector).should().exist();
+ } else {
+ find(selector).withText(text).should().exist();
+ }
+ }
+
+ private void waitForElementNotPresent(String selector, String text) {
+ if (Strings.isNullOrEmpty(text)) {
+ find(selector).should().not().exist();
+ } else {
+ find(selector).withText(text).should().not().exist();
+ }
+ }
+
+ private void waitForVisible(String selector) {
+ find(selector).should().beDisplayed();
+ }
+
+ private void assertInputValue(String selector, String text) {
+ find(selector).should().contain(text);
+ }
+
+ private void waitForXpathCount(String selector, int expectedCount) {
+ assertThat(find(selector).stream().size()).isEqualTo(expectedCount);
+ }
+
+ private void confirm(final String message) {
+ System.out.println(" - confirm(" + message + ")");
+
+ _30_SECONDS.execute(new Runnable() {
+ @Override
+ public void run() {
+ driver.switchTo().alert().accept();
+ }
+ });
+ }
+
+ private void assertLocation(String urlPattern) {
+ assertThat(driver.getCurrentUrl()).matches(glob(urlPattern));
+ }
+
+ private void verifyHtmlSource(String expect) {
+ assertThat(driver.getPageSource()).matches(glob(expect));
+ }
+
+ private String replacePlaceholders(String text) {
+ for (Map.Entry<String, String> entry : variables.entrySet()) {
+ text = text.replace("${" + entry.getKey() + "}", entry.getValue());
+ }
+ return text;
+ }
+}
diff --git a/tests/src/test/java/util/selenium/Text.java b/tests/src/test/java/util/selenium/Text.java
new file mode 100644
index 00000000000..5f3b8d259ce
--- /dev/null
+++ b/tests/src/test/java/util/selenium/Text.java
@@ -0,0 +1,58 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package util.selenium;
+
+import com.google.common.base.Joiner;
+import org.openqa.selenium.By;
+
+public abstract class Text {
+ private Text() {
+ // Static utility class
+ }
+
+ public static String doesOrNot(boolean not, String verb) {
+ if (!verb.contains(" ")) {
+ if (not) {
+ return "doesn't " + verb;
+ } else if (verb.endsWith("h")) {
+ return verb + "es";
+ } else {
+ return verb + "s";
+ }
+ }
+
+ String[] verbs = verb.split(" ");
+ verbs[0] = doesOrNot(not, verbs[0]);
+
+ return Joiner.on(" ").join(verbs);
+ }
+
+ public static String isOrNot(boolean not, String state) {
+ return (not ? "is not " : "is ") + state;
+ }
+
+ public static String plural(int n, String word) {
+ return (n + " " + word) + (n <= 1 ? "" : "s");
+ }
+
+ public static String toString(By selector) {
+ return selector.toString().replace("By.selector: ", "").replace("By.cssSelector: ", "");
+ }
+}
diff --git a/tests/src/test/java/util/selenium/WebElementHelper.java b/tests/src/test/java/util/selenium/WebElementHelper.java
new file mode 100644
index 00000000000..edb9d9dccb5
--- /dev/null
+++ b/tests/src/test/java/util/selenium/WebElementHelper.java
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package util.selenium;
+
+import org.openqa.selenium.WebElement;
+
+class WebElementHelper {
+ WebElementHelper() {
+ // Static class
+ }
+
+ public static String text(WebElement element) {
+ String text = element.getText();
+ if (!"".equals(text)) {
+ return nullToEmpty(text);
+ }
+
+ return nullToEmpty(element.getAttribute("value"));
+ }
+
+ private static String nullToEmpty(String text) {
+ return (text == null) ? "" : text;
+ }
+}
diff --git a/tests/src/test/java/util/user/GroupManagement.java b/tests/src/test/java/util/user/GroupManagement.java
new file mode 100644
index 00000000000..41b362f3e0a
--- /dev/null
+++ b/tests/src/test/java/util/user/GroupManagement.java
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package util.user;
+
+import java.util.List;
+import java.util.Optional;
+import javax.annotation.Nullable;
+
+/**
+ * @deprecated replaced by {@link org.sonarqube.tests.Tester}
+ */
+@Deprecated
+public interface GroupManagement {
+ void createGroup(String name);
+
+ void createGroup(String name, @Nullable String description);
+
+ void removeGroups(List<String> groupNames);
+
+ void removeGroups(String... groupNames);
+
+ Optional<Groups.Group> getGroupByName(String name);
+
+ Groups getGroups();
+
+ void verifyUserGroupMembership(String userLogin, String... groups);
+
+ Groups getUserGroups(String userLogin);
+
+ void associateGroupsToUser(String userLogin, String... groups);
+}
diff --git a/tests/src/test/java/util/user/Groups.java b/tests/src/test/java/util/user/Groups.java
new file mode 100644
index 00000000000..7b87547bc17
--- /dev/null
+++ b/tests/src/test/java/util/user/Groups.java
@@ -0,0 +1,58 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package util.user;
+
+import com.google.gson.Gson;
+import java.util.List;
+
+/**
+ * @deprecated replaced by {@link org.sonarqube.tests.Tester}
+ */
+@Deprecated
+
+public class Groups {
+
+ private List<Group> groups;
+
+ private Groups(List<Group> groups) {
+ this.groups = groups;
+ }
+
+ public List<Group> getGroups() {
+ return groups;
+ }
+
+ public static Groups parse(String json) {
+ Gson gson = new Gson();
+ return gson.fromJson(json, Groups.class);
+ }
+
+ public static class Group {
+ private final String name;
+
+ private Group(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+ }
+}
diff --git a/tests/src/test/java/util/user/UserRule.java b/tests/src/test/java/util/user/UserRule.java
new file mode 100644
index 00000000000..fe93e631c01
--- /dev/null
+++ b/tests/src/test/java/util/user/UserRule.java
@@ -0,0 +1,395 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package util.user;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
+import com.sonar.orchestrator.Orchestrator;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import org.junit.rules.ExternalResource;
+import org.sonarqube.ws.Organizations;
+import org.sonarqube.ws.WsUsers;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.WsResponse;
+import org.sonarqube.ws.client.permission.AddUserWsRequest;
+import org.sonarqube.ws.client.user.CreateRequest;
+import org.sonarqube.ws.client.user.SearchRequest;
+import org.sonarqube.ws.client.user.UsersService;
+import util.selenium.Consumer;
+
+import static java.util.Arrays.asList;
+import static java.util.Arrays.stream;
+import static java.util.Objects.requireNonNull;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.guava.api.Assertions.assertThat;
+import static util.ItUtils.newAdminWsClient;
+
+/**
+ * @deprecated replaced by {@link org.sonarqube.tests.Tester}
+ */
+@Deprecated
+public class UserRule extends ExternalResource implements GroupManagement {
+
+ public static final String ADMIN_LOGIN = "admin";
+ private static final AtomicInteger ID_GENERATOR = new AtomicInteger();
+ private final Orchestrator orchestrator;
+
+ private WsClient adminWsClient;
+ private final GroupManagement defaultOrganizationGroupManagement;
+
+ public UserRule(Orchestrator orchestrator) {
+ this.orchestrator = orchestrator;
+ this.defaultOrganizationGroupManagement = new GroupManagementImpl(null);
+ }
+
+ public static UserRule from(Orchestrator orchestrator) {
+ return new UserRule(requireNonNull(orchestrator, "Orchestrator instance cannot be null"));
+ }
+
+ @Override
+ protected void after() {
+ deactivateAllUsers();
+ // TODO delete groups
+ }
+
+ // *****************
+ // Users
+ // *****************
+
+ public void resetUsers() {
+ for (Users.User user : getUsers().getUsers()) {
+ String userLogin = user.getLogin();
+ if (!userLogin.equals(ADMIN_LOGIN)) {
+ deactivateUsers(userLogin);
+ }
+ }
+ }
+
+ public Users.User verifyUserExists(String login, String name, @Nullable String email) {
+ Optional<Users.User> user = getUserByLogin(login);
+ assertThat(user).as("User with login '%s' hasn't been found", login).isPresent();
+ assertThat(user.get().getLogin()).isEqualTo(login);
+ assertThat(user.get().getName()).isEqualTo(name);
+ assertThat(user.get().getEmail()).isEqualTo(email);
+ return user.get();
+ }
+
+ public void verifyUserExists(String login, String name, @Nullable String email, boolean local) {
+ Users.User user = verifyUserExists(login, name, email);
+ assertThat(user.isLocal()).isEqualTo(local);
+ }
+
+ public void verifyUserDoesNotExist(String login) {
+ assertThat(getUserByLogin(login)).as("Unexpected user with login '%s' has been found", login).isAbsent();
+ }
+
+ public WsUsers.CreateWsResponse.User createUser(String login, String name, @Nullable String email, String password) {
+ CreateRequest.Builder request = CreateRequest.builder()
+ .setLogin(login)
+ .setName(name)
+ .setEmail(email)
+ .setPassword(password);
+ return adminWsClient().users().create(request.build()).getUser();
+ }
+
+ /**
+ * Create user with randomly generated values. By default password is the login.
+ */
+ @SafeVarargs
+ public final WsUsers.CreateWsResponse.User generate(Consumer<CreateRequest.Builder>... populators) {
+ int id = ID_GENERATOR.getAndIncrement();
+ String login = "login" + id;
+ CreateRequest.Builder request = CreateRequest.builder()
+ .setLogin(login)
+ .setName("name" + id)
+ .setEmail(id + "@test.com")
+ .setPassword(login);
+ stream(populators).forEach(p -> p.accept(request));
+ return adminWsClient().users().create(request.build()).getUser();
+ }
+
+ public void createUser(String login, String password) {
+ createUser(login, login, null, password);
+ }
+
+ public WsUsers.CreateWsResponse.User createAdministrator(Organizations.Organization organization, String password) {
+ WsUsers.CreateWsResponse.User user = generate(p -> p.setPassword(password));
+ adminWsClient.organizations().addMember(organization.getKey(), user.getLogin());
+ forOrganization(organization.getKey()).associateGroupsToUser(user.getLogin(), "Owners");
+ return user;
+ }
+
+ /**
+ * Create a new admin user with random login, having password same as login
+ */
+ public String createAdminUser() {
+ String login = randomAlphabetic(10).toLowerCase();
+ return createAdminUser(login, login);
+ }
+
+ public String createAdminUser(String login, String password) {
+ createUser(login, password);
+ adminWsClient.permissions().addUser(new AddUserWsRequest().setLogin(login).setPermission("admin"));
+ adminWsClient.userGroups().addUser(org.sonarqube.ws.client.usergroup.AddUserWsRequest.builder().setLogin(login).setName("sonar-administrators").build());
+ return login;
+ }
+
+ /**
+ * Create a new root user with random login, having password same as login
+ */
+ public String createRootUser() {
+ String login = randomAlphabetic(10).toLowerCase();
+ return createRootUser(login, login);
+ }
+
+ public String createRootUser(String login, String password) {
+ createUser(login, password);
+ setRoot(login);
+ return login;
+ }
+
+ public void setRoot(String login) {
+ adminWsClient().roots().setRoot(login);
+ }
+
+ public void unsetRoot(String login) {
+ adminWsClient().roots().unsetRoot(login);
+ }
+
+ public Optional<Users.User> getUserByLogin(String login) {
+ return FluentIterable.from(getUsers().getUsers()).firstMatch(new MatchUserLogin(login));
+ }
+
+ public Users getUsers() {
+ WsResponse response = adminWsClient().wsConnector().call(
+ new GetRequest("api/users/search"))
+ .failIfNotSuccessful();
+ return Users.parse(response.content());
+ }
+
+ public void deactivateUsers(List<String> userLogins) {
+ for (String userLogin : userLogins) {
+ if (getUserByLogin(userLogin).isPresent()) {
+ adminWsClient().wsConnector().call(new PostRequest("api/users/deactivate").setParam("login", userLogin)).failIfNotSuccessful();
+ }
+ }
+ }
+
+ public void deactivateUsers(String... userLogins) {
+ deactivateUsers(asList(userLogins));
+ }
+
+ public void deactivateAllUsers() {
+ UsersService service = newAdminWsClient(orchestrator).users();
+ List<String> logins = service.search(SearchRequest.builder().build()).getUsersList()
+ .stream()
+ .filter(u -> !u.getLogin().equals("admin"))
+ .map(u -> u.getLogin())
+ .collect(Collectors.toList());
+ deactivateUsers(logins);
+ }
+
+ // *****************
+ // User groups
+ // *****************
+
+ public GroupManagement forOrganization(String organizationKey) {
+ return new GroupManagementImpl(organizationKey);
+ }
+
+ private final class GroupManagementImpl implements GroupManagement {
+ @CheckForNull
+ private final String organizationKey;
+
+ private GroupManagementImpl(@Nullable String organizationKey) {
+ this.organizationKey = organizationKey;
+ }
+
+ @Override
+ public void createGroup(String name) {
+ createGroup(name, null);
+ }
+
+ @Override
+ public void createGroup(String name, @Nullable String description) {
+ PostRequest request = new PostRequest("api/user_groups/create")
+ .setParam("name", name)
+ .setParam("description", description);
+ addOrganizationParam(request);
+ adminWsClient().wsConnector().call(request).failIfNotSuccessful();
+ }
+
+ private void addOrganizationParam(PostRequest request) {
+ request.setParam("organization", organizationKey);
+ }
+
+ private void addOrganizationParam(GetRequest request) {
+ request.setParam("organization", organizationKey);
+ }
+
+ @Override
+ public void removeGroups(List<String> groupNames) {
+ for (String groupName : groupNames) {
+ if (getGroupByName(groupName).isPresent()) {
+ PostRequest request = new PostRequest("api/user_groups/delete")
+ .setParam("name", groupName);
+ addOrganizationParam(request);
+ adminWsClient().wsConnector().call(request).failIfNotSuccessful();
+ }
+ }
+ }
+
+ @Override
+ public void removeGroups(String... groupNames) {
+ removeGroups(asList(groupNames));
+ }
+
+ @Override
+ public java.util.Optional<Groups.Group> getGroupByName(String name) {
+ return getGroups().getGroups().stream().filter(new MatchGroupName(name)::apply).findFirst();
+ }
+
+ @Override
+ public Groups getGroups() {
+ GetRequest request = new GetRequest("api/user_groups/search");
+ addOrganizationParam(request);
+ WsResponse response = adminWsClient().wsConnector().call(request).failIfNotSuccessful();
+ return Groups.parse(response.content());
+ }
+
+ @Override
+ public void verifyUserGroupMembership(String userLogin, String... expectedGroups) {
+ Groups userGroup = getUserGroups(userLogin);
+ List<String> userGroupName = userGroup.getGroups().stream().map(Groups.Group::getName).collect(Collectors.toList());
+ assertThat(userGroupName).containsOnly(expectedGroups);
+ }
+
+ @Override
+ public Groups getUserGroups(String userLogin) {
+ GetRequest request = new GetRequest("api/users/groups")
+ .setParam("login", userLogin)
+ .setParam("selected", "selected");
+ addOrganizationParam(request);
+ WsResponse response = adminWsClient().wsConnector().call(request).failIfNotSuccessful();
+ return Groups.parse(response.content());
+ }
+
+ @Override
+ public void associateGroupsToUser(String userLogin, String... groups) {
+ for (String group : groups) {
+ PostRequest request = new PostRequest("api/user_groups/add_user")
+ .setParam("login", userLogin)
+ .setParam("name", group);
+ addOrganizationParam(request);
+ adminWsClient().wsConnector().call(request).failIfNotSuccessful();
+ }
+ }
+ }
+
+ @Override
+ public void createGroup(String name) {
+ defaultOrganizationGroupManagement.createGroup(name);
+ }
+
+ @Override
+ public void createGroup(String name, @Nullable String description) {
+ defaultOrganizationGroupManagement.createGroup(name, description);
+ }
+
+ @Override
+ public void removeGroups(List<String> groupNames) {
+ defaultOrganizationGroupManagement.removeGroups(groupNames);
+ }
+
+ @Override
+ public void removeGroups(String... groupNames) {
+ defaultOrganizationGroupManagement.removeGroups(groupNames);
+ }
+
+ @Override
+ public java.util.Optional<Groups.Group> getGroupByName(String name) {
+ return defaultOrganizationGroupManagement.getGroupByName(name);
+ }
+
+ @Override
+ public Groups getGroups() {
+ return defaultOrganizationGroupManagement.getGroups();
+ }
+
+ @Override
+ public void verifyUserGroupMembership(String userLogin, String... groups) {
+ defaultOrganizationGroupManagement.verifyUserGroupMembership(userLogin, groups);
+ }
+
+ @Override
+ public Groups getUserGroups(String userLogin) {
+ return defaultOrganizationGroupManagement.getUserGroups(userLogin);
+ }
+
+ @Override
+ public void associateGroupsToUser(String userLogin, String... groups) {
+ defaultOrganizationGroupManagement.associateGroupsToUser(userLogin, groups);
+ }
+
+ private WsClient adminWsClient() {
+ if (adminWsClient == null) {
+ adminWsClient = newAdminWsClient(orchestrator);
+ }
+ return adminWsClient;
+ }
+
+ private class MatchUserLogin implements Predicate<Users.User> {
+ private final String login;
+
+ private MatchUserLogin(String login) {
+ this.login = login;
+ }
+
+ @Override
+ public boolean apply(@Nonnull Users.User user) {
+ String login = user.getLogin();
+ return login != null && login.equals(this.login) && user.isActive();
+ }
+ }
+
+ private class MatchGroupName implements Predicate<Groups.Group> {
+ private final String groupName;
+
+ private MatchGroupName(String groupName) {
+ this.groupName = groupName;
+ }
+
+ @Override
+ public boolean apply(@Nonnull Groups.Group group) {
+ String groupName = group.getName();
+ return groupName != null && groupName.equals(this.groupName);
+ }
+ }
+
+}
diff --git a/tests/src/test/java/util/user/Users.java b/tests/src/test/java/util/user/Users.java
new file mode 100644
index 00000000000..2f0a252e6c7
--- /dev/null
+++ b/tests/src/test/java/util/user/Users.java
@@ -0,0 +1,108 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ */
+package util.user;
+
+import com.google.gson.Gson;
+import java.util.List;
+
+public class Users {
+
+ private List<User> users;
+
+ private Users(List<User> users) {
+ this.users = users;
+ }
+
+ public List<User> getUsers() {
+ return users;
+ }
+
+ public static Users parse(String json) {
+ Gson gson = new Gson();
+ return gson.fromJson(json, Users.class);
+ }
+
+ public static class User {
+ private final String login;
+ private final String name;
+ private final String email;
+ private final String externalIdentity;
+ private final String externalProvider;
+ private final List<String> groups;
+ private final List<String> scmAccounts;
+ private final boolean active;
+ private final boolean local;
+ private int tokensCount;
+
+ private User(String login, String name, String email, String externalIdentity, String externalProvider, List<String> groups, List<String> scmAccounts,
+ boolean active, boolean local, int tokensCount) {
+ this.login = login;
+ this.name = name;
+ this.externalIdentity = externalIdentity;
+ this.externalProvider = externalProvider;
+ this.email = email;
+ this.groups = groups;
+ this.scmAccounts = scmAccounts;
+ this.active = active;
+ this.tokensCount = tokensCount;
+ this.local = local;
+ }
+
+ public String getLogin() {
+ return login;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public List<String> getGroups() {
+ return groups;
+ }
+
+ public List<String> getScmAccounts() {
+ return scmAccounts;
+ }
+
+ public boolean isActive() {
+ return active;
+ }
+
+ public boolean isLocal() {
+ return local;
+ }
+
+ public int getTokensCount() {
+ return tokensCount;
+ }
+
+ public String getExternalIdentity() {
+ return externalIdentity;
+ }
+
+ public String getExternalProvider() {
+ return externalProvider;
+ }
+ }
+}
diff --git a/tests/src/test/resources/analysis/BatchTest/one-issue-per-line.xml b/tests/src/test/resources/analysis/BatchTest/one-issue-per-line.xml
new file mode 100644
index 00000000000..7bb4ed5593a
--- /dev/null
+++ b/tests/src/test/resources/analysis/BatchTest/one-issue-per-line.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?><!-- Generated by Sonar -->
+<profile>
+ <name>one-issue-per-line</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MAJOR</priority>
+ </rule>
+ </rules>
+</profile> \ No newline at end of file
diff --git a/tests/src/test/resources/analysis/IssueJsonReportTest/multiline.xml b/tests/src/test/resources/analysis/IssueJsonReportTest/multiline.xml
new file mode 100644
index 00000000000..778866e91c2
--- /dev/null
+++ b/tests/src/test/resources/analysis/IssueJsonReportTest/multiline.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?><!-- Generated by Sonar -->
+<profile>
+ <name>multiline</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>MultilineIssue</key>
+ <priority>MAJOR</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/analysis/IssueJsonReportTest/no-server-analysis.json b/tests/src/test/resources/analysis/IssueJsonReportTest/no-server-analysis.json
new file mode 100644
index 00000000000..4e1b18df6ee
--- /dev/null
+++ b/tests/src/test/resources/analysis/IssueJsonReportTest/no-server-analysis.json
@@ -0,0 +1,212 @@
+{
+ "components": [
+ {
+ "key": "sample"
+ },
+ {
+ "key": "sample:src/main/xoo/sample/Sample.xoo",
+ "moduleKey": "sample",
+ "path": "src/main/xoo/sample/Sample.xoo",
+ "status": "ADDED"
+ },
+ {
+ "key": "sample:src/main/xoo/sample",
+ "moduleKey": "sample",
+ "path": "src/main/xoo/sample"
+ }
+ ],
+ "issues": [
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-02T00:00:00+0200",
+ "endLine": 2,
+ "endOffset": 0,
+ "isNew": true,
+ "key": "<ISSUE_KEY>",
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 2,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-02T00:00:00+0200",
+ "endLine": 6,
+ "endOffset": 14,
+ "isNew": true,
+ "key": "<ISSUE_KEY>",
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 6,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-02T00:00:00+0200",
+ "endLine": 1,
+ "endOffset": 15,
+ "isNew": true,
+ "key": "<ISSUE_KEY>",
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 1,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-02T00:00:00+0200",
+ "endLine": 5,
+ "endOffset": 23,
+ "isNew": true,
+ "key": "<ISSUE_KEY>",
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 5,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-02T00:00:00+0200",
+ "endLine": 8,
+ "endOffset": 1,
+ "isNew": true,
+ "key": "<ISSUE_KEY>",
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 8,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-02T00:00:00+0200",
+ "endLine": 9,
+ "endOffset": 28,
+ "isNew": true,
+ "key": "<ISSUE_KEY>",
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 9,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-02T00:00:00+0200",
+ "endLine": 7,
+ "endOffset": 2,
+ "isNew": true,
+ "key": "<ISSUE_KEY>",
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 7,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-02T00:00:00+0200",
+ "endLine": 4,
+ "endOffset": 1,
+ "isNew": true,
+ "key": "<ISSUE_KEY>",
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 4,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-02T00:00:00+0200",
+ "endLine": 11,
+ "endOffset": 2,
+ "isNew": true,
+ "key": "<ISSUE_KEY>",
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 11,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-02T00:00:00+0200",
+ "endLine": 3,
+ "endOffset": 21,
+ "isNew": true,
+ "key": "<ISSUE_KEY>",
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 3,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-02T00:00:00+0200",
+ "endLine": 13,
+ "endOffset": 0,
+ "isNew": true,
+ "key": "<ISSUE_KEY>",
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 13,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-02T00:00:00+0200",
+ "endLine": 12,
+ "endOffset": 1,
+ "isNew": true,
+ "key": "<ISSUE_KEY>",
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 12,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-02T00:00:00+0200",
+ "endLine": 10,
+ "endOffset": 17,
+ "isNew": true,
+ "key": "<ISSUE_KEY>",
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 10,
+ "startOffset": 0,
+ "status": "OPEN"
+ }
+ ],
+ "rules": [
+ {
+ "key": "xoo:OneIssuePerLine",
+ "name": "One Issue Per Line",
+ "repository": "xoo",
+ "rule": "OneIssuePerLine"
+ }
+ ],
+ "users": [],
+ "version": "<SONAR_VERSION>"
+}
diff --git a/tests/src/test/resources/analysis/IssueJsonReportTest/one-issue-per-line.xml b/tests/src/test/resources/analysis/IssueJsonReportTest/one-issue-per-line.xml
new file mode 100644
index 00000000000..7bb4ed5593a
--- /dev/null
+++ b/tests/src/test/resources/analysis/IssueJsonReportTest/one-issue-per-line.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?><!-- Generated by Sonar -->
+<profile>
+ <name>one-issue-per-line</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MAJOR</priority>
+ </rule>
+ </rules>
+</profile> \ No newline at end of file
diff --git a/tests/src/test/resources/analysis/IssueJsonReportTest/report-on-root-module.json b/tests/src/test/resources/analysis/IssueJsonReportTest/report-on-root-module.json
new file mode 100644
index 00000000000..ad3c0218cdb
--- /dev/null
+++ b/tests/src/test/resources/analysis/IssueJsonReportTest/report-on-root-module.json
@@ -0,0 +1,880 @@
+{
+ "components": [
+ {
+ "key": "com.sonarsource.it.samples:multi-modules-sample"
+ },
+ {
+ "key": "com.sonarsource.it.samples:multi-modules-sample:module_a",
+ "path": "module_a"
+ },
+ {
+ "key": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1",
+ "path": "module_a1"
+ },
+ {
+ "key": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2",
+ "path": "module_a2"
+ },
+ {
+ "key": "com.sonarsource.it.samples:multi-modules-sample:module_b",
+ "path": "module_b"
+ },
+ {
+ "key": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b1",
+ "path": "module_b1"
+ },
+ {
+ "key": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b2",
+ "path": "module_b2"
+ },
+ {
+ "key": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "moduleKey": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1",
+ "path": "src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "status": "SAME"
+ },
+ {
+ "key": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2:src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo",
+ "moduleKey": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2",
+ "path": "src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo",
+ "status": "SAME"
+ },
+ {
+ "key": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b1:src/main/xoo/com/sonar/it/samples/modules/b1/HelloB1.xoo",
+ "moduleKey": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b1",
+ "path": "src/main/xoo/com/sonar/it/samples/modules/b1/HelloB1.xoo",
+ "status": "SAME"
+ },
+ {
+ "key": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b2:src/main/xoo/com/sonar/it/samples/modules/b2/HelloB2.xoo",
+ "moduleKey": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b2",
+ "path": "src/main/xoo/com/sonar/it/samples/modules/b2/HelloB2.xoo",
+ "status": "SAME"
+ },
+ {
+ "key": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1",
+ "moduleKey": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1",
+ "path": "src/main/xoo/com/sonar/it/samples/modules/a1"
+ },
+ {
+ "key": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2:src/main/xoo/com/sonar/it/samples/modules/a2",
+ "moduleKey": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2",
+ "path": "src/main/xoo/com/sonar/it/samples/modules/a2"
+ },
+ {
+ "key": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b1:src/main/xoo/com/sonar/it/samples/modules/b1",
+ "moduleKey": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b1",
+ "path": "src/main/xoo/com/sonar/it/samples/modules/b1"
+ },
+ {
+ "key": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b2:src/main/xoo/com/sonar/it/samples/modules/b2",
+ "moduleKey": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b2",
+ "path": "src/main/xoo/com/sonar/it/samples/modules/b2"
+ }
+ ],
+ "issues": [
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 9,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 9,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 9,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 10,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 10,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 10,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 11,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 11,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 11,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 12,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 12,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 12,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 13,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 13,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 13,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 14,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 14,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 14,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 15,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 15,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 15,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 16,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 16,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 16,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 1,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 1,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 1,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 2,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 2,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 2,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 3,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 3,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 3,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 4,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 4,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 4,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 5,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 5,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 5,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 7,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 7,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 7,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 6,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 6,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 6,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 8,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 8,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 8,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2:src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 1,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 1,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 1,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2:src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 2,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 2,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 2,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2:src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 3,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 3,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 3,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2:src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 4,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 4,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 4,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2:src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 5,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 5,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 5,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2:src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 6,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 6,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 6,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2:src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 7,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 7,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 7,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2:src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 8,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 8,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 8,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2:src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 9,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 9,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 9,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2:src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 10,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 10,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 10,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2:src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 11,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 11,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 11,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2:src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 12,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 12,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 12,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2:src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 13,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 13,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 13,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2:src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 14,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 14,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 14,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2:src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 15,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 15,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 15,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2:src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 16,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 16,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 16,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2:src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 17,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 17,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 17,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2:src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 18,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 18,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 18,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2:src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 19,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 19,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 19,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2:src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 20,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 20,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 20,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a2:src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 21,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 21,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 21,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b1:src/main/xoo/com/sonar/it/samples/modules/b1/HelloB1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 1,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 1,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 1,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b1:src/main/xoo/com/sonar/it/samples/modules/b1/HelloB1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 2,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 2,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 2,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b1:src/main/xoo/com/sonar/it/samples/modules/b1/HelloB1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 3,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 3,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 3,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b1:src/main/xoo/com/sonar/it/samples/modules/b1/HelloB1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 4,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 4,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 4,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b1:src/main/xoo/com/sonar/it/samples/modules/b1/HelloB1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 5,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 5,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 5,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b1:src/main/xoo/com/sonar/it/samples/modules/b1/HelloB1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 6,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 6,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 6,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b1:src/main/xoo/com/sonar/it/samples/modules/b1/HelloB1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 7,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 7,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 7,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b1:src/main/xoo/com/sonar/it/samples/modules/b1/HelloB1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 8,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 8,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 8,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b1:src/main/xoo/com/sonar/it/samples/modules/b1/HelloB1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 9,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 9,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 9,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b1:src/main/xoo/com/sonar/it/samples/modules/b1/HelloB1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 10,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 10,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 10,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b1:src/main/xoo/com/sonar/it/samples/modules/b1/HelloB1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 11,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 11,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 11,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b1:src/main/xoo/com/sonar/it/samples/modules/b1/HelloB1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 12,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 12,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 12,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b2:src/main/xoo/com/sonar/it/samples/modules/b2/HelloB2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 1,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 1,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 1,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b2:src/main/xoo/com/sonar/it/samples/modules/b2/HelloB2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 2,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 2,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 2,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b2:src/main/xoo/com/sonar/it/samples/modules/b2/HelloB2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 3,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 3,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 3,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b2:src/main/xoo/com/sonar/it/samples/modules/b2/HelloB2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 4,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 4,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 4,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b2:src/main/xoo/com/sonar/it/samples/modules/b2/HelloB2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 5,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 5,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 5,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b2:src/main/xoo/com/sonar/it/samples/modules/b2/HelloB2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 6,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 6,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 6,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b2:src/main/xoo/com/sonar/it/samples/modules/b2/HelloB2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 7,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 7,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 7,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b2:src/main/xoo/com/sonar/it/samples/modules/b2/HelloB2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 8,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 8,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 8,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b2:src/main/xoo/com/sonar/it/samples/modules/b2/HelloB2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 9,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 9,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 9,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b2:src/main/xoo/com/sonar/it/samples/modules/b2/HelloB2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 10,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 10,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 10,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b2:src/main/xoo/com/sonar/it/samples/modules/b2/HelloB2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 12,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 12,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 12,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_b:module_b2:src/main/xoo/com/sonar/it/samples/modules/b2/HelloB2.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 11,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 11,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 11,
+ "status": "OPEN"
+ }
+ ],
+ "rules": [
+ {
+ "key": "xoo:OneIssuePerLine",
+ "name": "One Issue Per Line",
+ "repository": "xoo",
+ "rule": "OneIssuePerLine"
+ }
+ ],
+ "users": [],
+ "version": "<SONAR_VERSION>"
+}
diff --git a/tests/src/test/resources/analysis/IssueJsonReportTest/report-on-single-module-branch.json b/tests/src/test/resources/analysis/IssueJsonReportTest/report-on-single-module-branch.json
new file mode 100644
index 00000000000..aa066bbbf2f
--- /dev/null
+++ b/tests/src/test/resources/analysis/IssueJsonReportTest/report-on-single-module-branch.json
@@ -0,0 +1,255 @@
+{
+ "components": [
+ {
+ "key": "sample:mybranch"
+ },
+ {
+ "key": "sample:mybranch:src/main/xoo/sample/Sample.xoo",
+ "moduleKey": "sample:mybranch",
+ "path": "src/main/xoo/sample/Sample.xoo",
+ "status": "CHANGED"
+ },
+ {
+ "key": "sample:mybranch:src/main/xoo/sample",
+ "moduleKey": "sample:mybranch",
+ "path": "src/main/xoo/sample"
+ }
+ ],
+ "issues": [
+ {
+ "component": "sample:mybranch:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 1,
+ "endOffset": 15,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 1,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 1,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:mybranch:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 2,
+ "endOffset": 0,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 2,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 2,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:mybranch:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 3,
+ "endOffset": 21,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 3,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 3,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:mybranch:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 4,
+ "endOffset": 1,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 4,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 4,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:mybranch:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 5,
+ "endOffset": 23,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 5,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 5,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:mybranch:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 6,
+ "endOffset": 14,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 6,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 6,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:mybranch:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 8,
+ "endOffset": 2,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 8,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 8,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:mybranch:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 9,
+ "endOffset": 1,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 9,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 9,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:mybranch:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 10,
+ "endOffset": 28,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 10,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 10,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:mybranch:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 12,
+ "endOffset": 17,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 12,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 12,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:mybranch:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 13,
+ "endOffset": 2,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 13,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 13,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:mybranch:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 14,
+ "endOffset": 1,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 14,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 14,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:mybranch:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 15,
+ "endOffset": 0,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 15,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 15,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:mybranch:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-02T00:00:00+0200",
+ "endLine": 11,
+ "endOffset": 28,
+ "isNew": true,
+ "key": "<ISSUE_KEY>",
+ "line": 11,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 11,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:mybranch:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-02T00:00:00+0200",
+ "endLine": 7,
+ "endOffset": 28,
+ "isNew": true,
+ "key": "<ISSUE_KEY>",
+ "line": 7,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 7,
+ "startOffset": 0,
+ "status": "OPEN"
+ }
+ ],
+ "rules": [
+ {
+ "key": "xoo:OneIssuePerLine",
+ "name": "One Issue Per Line",
+ "repository": "xoo",
+ "rule": "OneIssuePerLine"
+ }
+ ],
+ "users": [],
+ "version": "<SONAR_VERSION>"
+}
diff --git a/tests/src/test/resources/analysis/IssueJsonReportTest/report-on-single-module.json b/tests/src/test/resources/analysis/IssueJsonReportTest/report-on-single-module.json
new file mode 100644
index 00000000000..6c3aab91f02
--- /dev/null
+++ b/tests/src/test/resources/analysis/IssueJsonReportTest/report-on-single-module.json
@@ -0,0 +1,255 @@
+{
+ "components": [
+ {
+ "key": "sample"
+ },
+ {
+ "key": "sample:src/main/xoo/sample/Sample.xoo",
+ "moduleKey": "sample",
+ "path": "src/main/xoo/sample/Sample.xoo",
+ "status": "CHANGED"
+ },
+ {
+ "key": "sample:src/main/xoo/sample",
+ "moduleKey": "sample",
+ "path": "src/main/xoo/sample"
+ }
+ ],
+ "issues": [
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-02T00:00:00+0200",
+ "endLine": 11,
+ "endOffset": 28,
+ "isNew": true,
+ "key": "<ISSUE_KEY>",
+ "line": 11,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 11,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-02T00:00:00+0200",
+ "endLine": 7,
+ "endOffset": 28,
+ "isNew": true,
+ "key": "<ISSUE_KEY>",
+ "line": 7,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 7,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 1,
+ "endOffset": 15,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 1,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 1,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 2,
+ "endOffset": 0,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 2,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 2,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 3,
+ "endOffset": 21,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 3,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 3,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 4,
+ "endOffset": 1,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 4,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 4,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 5,
+ "endOffset": 23,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 5,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 5,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 6,
+ "endOffset": 14,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 6,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 6,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 8,
+ "endOffset": 2,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 8,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 8,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 9,
+ "endOffset": 1,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 9,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 9,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 10,
+ "endOffset": 28,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 10,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 10,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 12,
+ "endOffset": 17,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 12,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 12,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 13,
+ "endOffset": 2,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 13,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 13,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 14,
+ "endOffset": 1,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 14,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 14,
+ "startOffset": 0,
+ "status": "OPEN"
+ },
+ {
+ "component": "sample:src/main/xoo/sample/Sample.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 15,
+ "endOffset": 0,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 15,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 15,
+ "startOffset": 0,
+ "status": "OPEN"
+ }
+ ],
+ "rules": [
+ {
+ "key": "xoo:OneIssuePerLine",
+ "name": "One Issue Per Line",
+ "repository": "xoo",
+ "rule": "OneIssuePerLine"
+ }
+ ],
+ "users": [],
+ "version": "<SONAR_VERSION>"
+}
diff --git a/tests/src/test/resources/analysis/IssueJsonReportTest/report-on-sub-module.json b/tests/src/test/resources/analysis/IssueJsonReportTest/report-on-sub-module.json
new file mode 100644
index 00000000000..84a68c4d5a4
--- /dev/null
+++ b/tests/src/test/resources/analysis/IssueJsonReportTest/report-on-sub-module.json
@@ -0,0 +1,238 @@
+{
+ "components": [
+ {
+ "key": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1"
+ },
+ {
+ "key": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "moduleKey": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1",
+ "path": "src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "status": "SAME"
+ },
+ {
+ "key": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1",
+ "moduleKey": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1",
+ "path": "src/main/xoo/com/sonar/it/samples/modules/a1"
+ }
+ ],
+ "issues": [
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 1,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 1,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 1,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 2,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 2,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 2,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 3,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 3,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 3,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 4,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 4,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 4,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 5,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 5,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 5,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 6,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 6,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 6,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 7,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 7,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 7,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 8,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 8,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 8,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 9,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 9,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 9,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 10,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 10,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 10,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 11,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 11,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 11,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 12,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 12,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 12,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 13,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 13,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 13,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 14,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 14,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 14,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 15,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 15,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 15,
+ "status": "OPEN"
+ },
+ {
+ "component": "com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo",
+ "creationDate": "2013-05-01T00:00:00+0200",
+ "endLine": 16,
+ "isNew": false,
+ "key": "<ISSUE_KEY>",
+ "line": 16,
+ "message": "This issue is generated on each line",
+ "rule": "xoo:OneIssuePerLine",
+ "severity": "MAJOR",
+ "startLine": 16,
+ "status": "OPEN"
+ }
+ ],
+ "rules": [
+ {
+ "key": "xoo:OneIssuePerLine",
+ "name": "One Issue Per Line",
+ "repository": "xoo",
+ "rule": "OneIssuePerLine"
+ }
+ ],
+ "users": [],
+ "version": "<SONAR_VERSION>"
+}
diff --git a/tests/src/test/resources/analysis/IssuesModeTest/empty.xml b/tests/src/test/resources/analysis/IssuesModeTest/empty.xml
new file mode 100644
index 00000000000..8bab61d6c85
--- /dev/null
+++ b/tests/src/test/resources/analysis/IssuesModeTest/empty.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?><!-- Generated by Sonar -->
+<profile>
+ <name>empty</name>
+ <language>xoo</language>
+ <rules>
+ </rules>
+</profile> \ No newline at end of file
diff --git a/tests/src/test/resources/analysis/IssuesModeTest/one-issue-per-line-empty.xml b/tests/src/test/resources/analysis/IssuesModeTest/one-issue-per-line-empty.xml
new file mode 100644
index 00000000000..0ba34f10dbd
--- /dev/null
+++ b/tests/src/test/resources/analysis/IssuesModeTest/one-issue-per-line-empty.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?><!-- Generated by Sonar -->
+<profile>
+ <name>one-issue-per-line</name>
+ <language>xoo</language>
+ <rules>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/analysis/IssuesModeTest/one-issue-per-line.xml b/tests/src/test/resources/analysis/IssuesModeTest/one-issue-per-line.xml
new file mode 100644
index 00000000000..7bb4ed5593a
--- /dev/null
+++ b/tests/src/test/resources/analysis/IssuesModeTest/one-issue-per-line.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?><!-- Generated by Sonar -->
+<profile>
+ <name>one-issue-per-line</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MAJOR</priority>
+ </rule>
+ </rules>
+</profile> \ No newline at end of file
diff --git a/tests/src/test/resources/analysis/IssuesModeTest/with-many-rules.xml b/tests/src/test/resources/analysis/IssuesModeTest/with-many-rules.xml
new file mode 100644
index 00000000000..f3d0baf0616
--- /dev/null
+++ b/tests/src/test/resources/analysis/IssuesModeTest/with-many-rules.xml
@@ -0,0 +1,32 @@
+<profile>
+ <name>with-many-rules</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MINOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerFile</key>
+ <priority>MAJOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerModule</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>HasTag</key>
+ <priority>INFO</priority>
+ <parameters>
+ <parameter>
+ <key>tag</key>
+ <value>xoo</value>
+ </parameter>
+ </parameters>
+ </rule>
+ </rules>
+</profile> \ No newline at end of file
diff --git a/tests/src/test/resources/analysis/MultiLanguageTest/one-issue-per-line-xoo2.xml b/tests/src/test/resources/analysis/MultiLanguageTest/one-issue-per-line-xoo2.xml
new file mode 100644
index 00000000000..67215f91a52
--- /dev/null
+++ b/tests/src/test/resources/analysis/MultiLanguageTest/one-issue-per-line-xoo2.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?><!-- Generated by Sonar -->
+<profile>
+ <name>one-issue-per-line-xoo2</name>
+ <language>xoo2</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo2</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MAJOR</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/analysis/MultiLanguageTest/one-issue-per-line.xml b/tests/src/test/resources/analysis/MultiLanguageTest/one-issue-per-line.xml
new file mode 100644
index 00000000000..7bb4ed5593a
--- /dev/null
+++ b/tests/src/test/resources/analysis/MultiLanguageTest/one-issue-per-line.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?><!-- Generated by Sonar -->
+<profile>
+ <name>one-issue-per-line</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MAJOR</priority>
+ </rule>
+ </rules>
+</profile> \ No newline at end of file
diff --git a/tests/src/test/resources/analysis/SSLTest/README b/tests/src/test/resources/analysis/SSLTest/README
new file mode 100644
index 00000000000..fb535a907c6
--- /dev/null
+++ b/tests/src/test/resources/analysis/SSLTest/README
@@ -0,0 +1,184 @@
+$ openssl req -config ./openssl.cnf -newkey rsa:2048 -nodes -keyform PEM -keyout ca.key -x509 -days 3650 -extensions certauth -outform PEM -out ca.cer
+Generating a 2048 bit RSA private key
+..............................................+++
+........................................................................................................+++
+writing new private key to 'ca.key'
+-----
+You are about to be asked to enter information that will be incorporated
+into your certificate request.
+What you are about to enter is what is called a Distinguished Name or a DN.
+There are quite a few fields but you can leave some blank
+For some fields there will be a default value,
+If you enter '.', the field will be left blank.
+-----
+Country [FR]:
+Locality [Poitiers]:
+Organization [SonarSource]:
+Common Name []:CA for SonarQube ITs
+
+$ openssl genrsa -out server.key 2048
+Generating RSA private key, 2048 bit long modulus
+........................+++
+............................+++
+e is 65537 (0x10001)
+
+$ openssl req -config ./openssl.cnf -new -key server.key -out server.req
+You are about to be asked to enter information that will be incorporated
+into your certificate request.
+What you are about to enter is what is called a Distinguished Name or a DN.
+There are quite a few fields but you can leave some blank
+For some fields there will be a default value,
+If you enter '.', the field will be left blank.
+-----
+Country [FR]:
+Locality [Poitiers]:
+Organization [SonarSource]:
+Common Name []:localhost
+
+$ openssl x509 -req -in server.req -CA ca.cer -CAkey ca.key -set_serial 100 -extfile openssl.cnf -extensions server -days 3650 -outform PEM -out server.cer
+Signature ok
+subject=/C=FR/L=Poitiers/O=SonarSource/CN=localhost
+Getting CA Private Key
+
+$ openssl genrsa -out client.key 2048
+Generating RSA private key, 2048 bit long modulus
+...................................+++
+.....................................................+++
+e is 65537 (0x10001)
+
+$ openssl req -config ./openssl.cnf -new -key client.key -out client.req
+You are about to be asked to enter information that will be incorporated
+into your certificate request.
+What you are about to enter is what is called a Distinguished Name or a DN.
+There are quite a few fields but you can leave some blank
+For some fields there will be a default value,
+If you enter '.', the field will be left blank.
+-----
+Country [FR]:
+Locality [Poitiers]:
+Organization [SonarSource]:
+Common Name []:Julien Henry
+
+$ openssl x509 -req -in client.req -CA ca.cer -CAkey ca.key -set_serial 101 -extfile openssl.cnf -extensions client -days 3650 -outform PEM -out client.cer
+Signature ok
+subject=/C=FR/L=Poitiers/O=SonarSource/CN=Julien Henry
+Getting CA Private Key
+
+$ openssl pkcs12 -export -inkey client.key -in client.cer -out client.p12
+Enter Export Password: clientp12pwd
+Verifying - Enter Export Password: clientp12pwd
+
+$ openssl pkcs12 -inkey server.key -in server.cer -export -out server.p12
+Enter Export Password: serverp12pwd
+Verifying - Enter Export Password: serverp12pwd
+
+$ keytool -importkeystore -srckeystore server.p12 -srcstoretype PKCS12 -destkeystore serverkeystore.jks
+Entrez le mot de passe du fichier de clés de destination : serverkeystorepwd
+Ressaisissez le nouveau mot de passe : serverkeystorepwd
+Entrez le mot de passe du fichier de clés source : serverp12pwd
+L'entrée de l'alias 1 a été importée.
+Commande d'import exécutée : 1 entrées importées, échec ou annulation de 0 entrées
+
+$ keytool -import -file ca.cer -keystore servertruststore.jks
+Entrez le mot de passe du fichier de clés : servertruststorepwd
+Ressaisissez le nouveau mot de passe : servertruststorepwd
+Propriétaire : CN=Test CA, O=SonarSource, L=Poitiers, C=FR
+Emetteur : CN=Test CA, O=SonarSource, L=Poitiers, C=FR
+Numéro de série : dabbebc7bce2fc6a
+Valide du : Wed Aug 31 14:42:15 CEST 2016 au : Sat Aug 29 14:42:15 CEST 2026
+Empreintes du certificat :
+ MD5: 69:36:AE:65:51:CD:F4:C3:83:77:DE:45:AE:49:01:1A
+ SHA1 : 77:92:45:84:18:FC:34:7A:2A:B0:EC:3D:22:48:15:1A:19:71:1D:B3
+ SHA256 : 99:03:89:84:6E:E3:D3:B7:12:2D:70:7E:49:88:49:41:52:1C:89:3A:9B:C0:83:D1:C5:44:D4:93:FB:42:C8:07
+ Nom de l'algorithme de signature : SHA1withRSA
+ Version : 3
+
+Extensions :
+
+#1: ObjectId: 2.5.29.35 Criticality=false
+AuthorityKeyIdentifier [
+KeyIdentifier [
+0000: 3A 61 C1 86 AD BE FC 15 82 B3 59 FF 00 28 5E F9 :a........Y..(^.
+0010: B5 5A 87 04 .Z..
+]
+[CN=Test CA, O=SonarSource, L=Poitiers, C=FR]
+SerialNumber: [ dabbebc7 bce2fc6a]
+]
+
+#2: ObjectId: 2.5.29.19 Criticality=false
+BasicConstraints:[
+ CA:true
+ PathLen:2147483647
+]
+
+#3: ObjectId: 2.5.29.31 Criticality=false
+CRLDistributionPoints [
+ [DistributionPoint:
+ [URIName: http://testca.local/ca.crl]
+]]
+
+#4: ObjectId: 2.5.29.14 Criticality=false
+SubjectKeyIdentifier [
+KeyIdentifier [
+0000: 3A 61 C1 86 AD BE FC 15 82 B3 59 FF 00 28 5E F9 :a........Y..(^.
+0010: B5 5A 87 04 .Z..
+]
+]
+
+Faire confiance à ce certificat ? [non] : oui
+Certificat ajouté au fichier de clés
+
+$ keytool -import -file server.cer -keystore clienttruststore.jks
+Entrez le mot de passe du fichier de clés : clienttruststorepwd
+Ressaisissez le nouveau mot de passe : clienttruststorepwd
+Propriétaire : CN=localhost, O=SonarSource, L=Poitiers, C=FR
+Emetteur : CN=Test CA, O=SonarSource, L=Poitiers, C=FR
+Numéro de série : 64
+Valide du : Wed Aug 31 14:45:30 CEST 2016 au : Thu Aug 31 14:45:30 CEST 2017
+Empreintes du certificat :
+ MD5: 40:52:F4:5E:67:C3:68:B6:00:7D:70:C0:1E:8E:75:2E
+ SHA1 : 83:3F:4F:AC:4E:E6:F4:06:14:01:E6:5B:F2:63:34:94:68:12:8C:3A
+ SHA256 : 7C:03:9A:CA:0D:B5:57:A5:66:56:75:09:23:45:9E:D5:CC:6C:72:14:0B:4B:9B:E8:29:3F:78:4C:9F:D6:77:E2
+ Nom de l'algorithme de signature : SHA256withRSA
+ Version : 3
+
+Extensions :
+
+#1: ObjectId: 2.5.29.19 Criticality=false
+BasicConstraints:[
+ CA:false
+ PathLen: undefined
+]
+
+#2: ObjectId: 2.5.29.31 Criticality=false
+CRLDistributionPoints [
+ [DistributionPoint:
+ [URIName: http://testca.local/ca.crl]
+]]
+
+#3: ObjectId: 2.5.29.37 Criticality=false
+ExtendedKeyUsages [
+ serverAuth
+]
+
+#4: ObjectId: 2.5.29.15 Criticality=false
+KeyUsage [
+ DigitalSignature
+ Key_Encipherment
+ Data_Encipherment
+]
+
+#5: ObjectId: 2.16.840.1.113730.1.1 Criticality=false
+NetscapeCertType [
+ SSL server
+]
+
+Faire confiance à ce certificat ? [non] : oui
+Certificat ajouté au fichier de clés
+
+$ keytool -importkeystore -srckeystore client.p12 -srcstoretype PKCS12 -destkeystore clientkeystore.jks
+Entrez le mot de passe du fichier de clés de destination : clientp12pwd
+Ressaisissez le nouveau mot de passe : clientp12pwd
+Entrez le mot de passe du fichier de clés source : clientp12pwd
+L'entrée de l'alias 1 a été importée.
+Commande d'import exécutée : 1 entrées importées, échec ou annulation de 0 entrées
diff --git a/tests/src/test/resources/analysis/SSLTest/clientkeystore.jks b/tests/src/test/resources/analysis/SSLTest/clientkeystore.jks
new file mode 100644
index 00000000000..653f3c7f093
--- /dev/null
+++ b/tests/src/test/resources/analysis/SSLTest/clientkeystore.jks
Binary files differ
diff --git a/tests/src/test/resources/analysis/SSLTest/clienttruststore.jks b/tests/src/test/resources/analysis/SSLTest/clienttruststore.jks
new file mode 100644
index 00000000000..2529245e1c6
--- /dev/null
+++ b/tests/src/test/resources/analysis/SSLTest/clienttruststore.jks
Binary files differ
diff --git a/tests/src/test/resources/analysis/SSLTest/openssl.cnf b/tests/src/test/resources/analysis/SSLTest/openssl.cnf
new file mode 100644
index 00000000000..4a5148fd4d7
--- /dev/null
+++ b/tests/src/test/resources/analysis/SSLTest/openssl.cnf
@@ -0,0 +1,38 @@
+[ req ]
+default_md = sha1
+distinguished_name = req_distinguished_name
+
+[ req_distinguished_name ]
+countryName = Country
+countryName_default = FR
+countryName_min = 2
+countryName_max = 2
+localityName = Locality
+localityName_default = Poitiers
+organizationName = Organization
+organizationName_default = SonarSource
+commonName = Common Name
+commonName_max = 64
+
+[ certauth ]
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always,issuer:always
+basicConstraints = CA:true
+crlDistributionPoints = @crl
+
+[ server ]
+basicConstraints = CA:FALSE
+keyUsage = digitalSignature, keyEncipherment, dataEncipherment
+extendedKeyUsage = serverAuth
+nsCertType = server
+crlDistributionPoints = @crl
+
+[ client ]
+basicConstraints = CA:FALSE
+keyUsage = digitalSignature, keyEncipherment, dataEncipherment
+extendedKeyUsage = clientAuth
+nsCertType = client
+crlDistributionPoints = @crl
+
+[ crl ]
+URI=http://testca.local/ca.crl \ No newline at end of file
diff --git a/tests/src/test/resources/analysis/SSLTest/serverkeystore.jks b/tests/src/test/resources/analysis/SSLTest/serverkeystore.jks
new file mode 100644
index 00000000000..319c899278a
--- /dev/null
+++ b/tests/src/test/resources/analysis/SSLTest/serverkeystore.jks
Binary files differ
diff --git a/tests/src/test/resources/analysis/SSLTest/servertruststore.jks b/tests/src/test/resources/analysis/SSLTest/servertruststore.jks
new file mode 100644
index 00000000000..a3aee3b8a9d
--- /dev/null
+++ b/tests/src/test/resources/analysis/SSLTest/servertruststore.jks
Binary files differ
diff --git a/tests/src/test/resources/analysis/SettingsEncryptionTest/sonar-secret.txt b/tests/src/test/resources/analysis/SettingsEncryptionTest/sonar-secret.txt
new file mode 100644
index 00000000000..65b98c522da
--- /dev/null
+++ b/tests/src/test/resources/analysis/SettingsEncryptionTest/sonar-secret.txt
@@ -0,0 +1 @@
+0PZz+G+f8mjr3sPn4+AhHg== \ No newline at end of file
diff --git a/tests/src/test/resources/analysis/TempFolderTest/one-issue-per-line.xml b/tests/src/test/resources/analysis/TempFolderTest/one-issue-per-line.xml
new file mode 100644
index 00000000000..7bb4ed5593a
--- /dev/null
+++ b/tests/src/test/resources/analysis/TempFolderTest/one-issue-per-line.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?><!-- Generated by Sonar -->
+<profile>
+ <name>one-issue-per-line</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MAJOR</priority>
+ </rule>
+ </rules>
+</profile> \ No newline at end of file
diff --git a/tests/src/test/resources/authorisation/PermissionTemplatesPageTest/should_create.html b/tests/src/test/resources/authorisation/PermissionTemplatesPageTest/should_create.html
new file mode 100644
index 00000000000..0e67e0f4fab
--- /dev/null
+++ b/tests/src/test/resources/authorisation/PermissionTemplatesPageTest/should_create.html
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>should_create</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/logout</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>password</td>
+ <td>root-user</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>login</td>
+ <td>root-user</td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>name=commit</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/permission_templates</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.page-actions button</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>click</td>
+ <td>css=.page-actions button</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=#permission-template-name</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>css=#permission-template-name</td>
+ <td>Custom</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>css=#permission-template-description</td>
+ <td>Description</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>css=#permission-template-project-key-pattern</td>
+ <td>.*</td>
+ </tr>
+ <tr>
+ <td>click</td>
+ <td>css=#permission-template-submit</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=tr[data-name=&quot;Custom&quot;]</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>assertText</td>
+ <td>css=tr[data-name=&quot;Custom&quot;] .js-name</td>
+ <td>*Custom*</td>
+ </tr>
+ <tr>
+ <td>assertText</td>
+ <td>css=tr[data-name=&quot;Custom&quot;] .js-description</td>
+ <td>*Description*</td>
+ </tr>
+ <tr>
+ <td>assertText</td>
+ <td>css=tr[data-name=&quot;Custom&quot;] .js-project-key-pattern</td>
+ <td>*.*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/authorisation/PermissionTemplatesPageTest/should_display_page.html b/tests/src/test/resources/authorisation/PermissionTemplatesPageTest/should_display_page.html
new file mode 100644
index 00000000000..8d9640f25b6
--- /dev/null
+++ b/tests/src/test/resources/authorisation/PermissionTemplatesPageTest/should_display_page.html
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>should_display_page</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/logout</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/permission_templates</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=tr[data-id=&quot;default_template&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=tr[data-id=&quot;default_template&quot;] .js-name</td>
+ <td>*Default template*</td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=tr[data-id=&quot;default_template&quot;] .js-defaults</td>
+ <td>*Projects*</td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=tr[data-id=&quot;default_template&quot;] .js-description</td>
+ <td>*This permission template will be used*</td>
+</tr>
+<tr>
+ <td>assertElementPresent</td>
+ <td>css=td[data-permission=&quot;user&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertElementPresent</td>
+ <td>css=td[data-permission=&quot;codeviewer&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertElementPresent</td>
+ <td>css=td[data-permission=&quot;issueadmin&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertElementPresent</td>
+ <td>css=td[data-permission=&quot;admin&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertElementPresent</td>
+ <td>css=td[data-permission=&quot;scan&quot;]</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/authorisation/PermissionTemplatesPageTest/should_manage_project_creators.html b/tests/src/test/resources/authorisation/PermissionTemplatesPageTest/should_manage_project_creators.html
new file mode 100644
index 00000000000..3d1c172a783
--- /dev/null
+++ b/tests/src/test/resources/authorisation/PermissionTemplatesPageTest/should_manage_project_creators.html
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>should_manage_project_creators</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/logout</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin-user</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin-user</td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>name=commit</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/permission_templates</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=td[data-permission=&quot;user&quot;]</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>click</td>
+ <td>css=td[data-permission=&quot;user&quot;] .js-update-users</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=#grant-to-project-creators</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>assertElementPresent</td>
+ <td>css=#grant-to-project-creators:not(:checked)</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>click</td>
+ <td>css=#grant-to-project-creators</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>click</td>
+ <td>css=.js-modal-close</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=td[data-permission=&quot;user&quot;] .js-project-creators</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>click</td>
+ <td>css=td[data-permission=&quot;user&quot;] .js-update-users</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=#grant-to-project-creators</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>assertElementPresent</td>
+ <td>css=#grant-to-project-creators:checked</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>click</td>
+ <td>css=#grant-to-project-creators</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>click</td>
+ <td>css=.js-modal-close</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementNotPresent</td>
+ <td>css=td[data-permission=&quot;user&quot;] .js-project-creators</td>
+ <td></td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/authorisation/ProvisioningPermissionTest/should-be-able-to-provision-project.html b/tests/src/test/resources/authorisation/ProvisioningPermissionTest/should-be-able-to-provision-project.html
new file mode 100644
index 00000000000..c5db3b4898a
--- /dev/null
+++ b/tests/src/test/resources/authorisation/ProvisioningPermissionTest/should-be-able-to-provision-project.html
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>user-cannot-administrate-profile</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/logout</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin-with-provisioning</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>password</td>
+ <td>password</td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/projects_admin</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=#projects-type__ALL</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>assertText</td>
+ <td>css=.page-actions button</td>
+ <td>*Create Project*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/authorisation/ProvisioningPermissionTest/should-not-be-able-to-provision-project.html b/tests/src/test/resources/authorisation/ProvisioningPermissionTest/should-not-be-able-to-provision-project.html
new file mode 100644
index 00000000000..270daf67cd9
--- /dev/null
+++ b/tests/src/test/resources/authorisation/ProvisioningPermissionTest/should-not-be-able-to-provision-project.html
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>user-cannot-administrate-profile</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/logout</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin-without-provisioning</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>password</td>
+ <td>password</td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/projects_admin</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=#projects-type__ALL</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>assertNotText</td>
+ <td>css=.page-actions button</td>
+ <td>*Create Project*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/authorisation/QualityProfileAdminPermissionTest/normal-user.html b/tests/src/test/resources/authorisation/QualityProfileAdminPermissionTest/normal-user.html
new file mode 100644
index 00000000000..b9190a27027
--- /dev/null
+++ b/tests/src/test/resources/authorisation/QualityProfileAdminPermissionTest/normal-user.html
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>user-cannot-administrate-profile</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/logout</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>login</td>
+ <td>not_profileadm</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>password</td>
+ <td>userpwd</td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/profiles</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profiles-table-row[data-name=&quot;foo&quot;]</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>click</td>
+ <td>css=.quality-profiles-table-row[data-name=&quot;foo&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>css=.quality-profile-header</td>
+ <td>*foo*</td>
+ </tr>
+ <tr>
+ <td>assertElementNotPresent</td>
+ <td>css=.js-change-parent</td>
+ <td></td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/authorisation/QualityProfileAdminPermissionTest/profile-admin.html b/tests/src/test/resources/authorisation/QualityProfileAdminPermissionTest/profile-admin.html
new file mode 100644
index 00000000000..86d6b26ece8
--- /dev/null
+++ b/tests/src/test/resources/authorisation/QualityProfileAdminPermissionTest/profile-admin.html
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>user-can-administrate-profile</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/logout</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>profileadm</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>papwd</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profiles-table-row[data-name=&quot;foo&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.quality-profiles-table-row[data-name=&quot;foo&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=.quality-profile-header</td>
+ <td>*foo*</td>
+</tr>
+<tr>
+ <td>assertElementPresent</td>
+ <td>css=.js-change-parent</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/project/quality_profiles?id=sample</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>id=content</td>
+ <td>*Quality Profiles*</td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>id=content</td>
+ <td>*Xoo*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/authorisation/one-issue-per-line-profile.xml b/tests/src/test/resources/authorisation/one-issue-per-line-profile.xml
new file mode 100644
index 00000000000..521adc7e06f
--- /dev/null
+++ b/tests/src/test/resources/authorisation/one-issue-per-line-profile.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<profile>
+ <name>one-issue-per-line</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MINOR</priority>
+ </rule>
+ </rules>
+</profile> \ No newline at end of file
diff --git a/tests/src/test/resources/dbCleaner/one-issue-per-line-profile.xml b/tests/src/test/resources/dbCleaner/one-issue-per-line-profile.xml
new file mode 100644
index 00000000000..2006b6fb365
--- /dev/null
+++ b/tests/src/test/resources/dbCleaner/one-issue-per-line-profile.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0"?><!-- Generated by Sonar -->
+<profile>
+ <name>one-issue-per-line-profile</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MAJOR</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/duplication/CrossProjectDuplicationsOnRemoveFileTest/duplications-with-deleted-project.html b/tests/src/test/resources/duplication/CrossProjectDuplicationsOnRemoveFileTest/duplications-with-deleted-project.html
new file mode 100644
index 00000000000..37a48368f83
--- /dev/null
+++ b/tests/src/test/resources/duplication/CrossProjectDuplicationsOnRemoveFileTest/duplications-with-deleted-project.html
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>duplications-with-deleted-project</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">duplications-with-deleted-project</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/component/index?id=duplicate-project%3Asrc%2Fmain%2Fxoo%2Fsample%2FFile1.xoo</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>css=.source-viewer</td>
+ <td>*src/main/xoo/sample/File1.xoo*</td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>css=.source-table</td>
+ <td>*File1*</td>
+ </tr>
+ <tr>
+ <td>click</td>
+ <td>css=.source-line-duplicated</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.bubble-popup</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>assertElementPresent</td>
+ <td>css=.bubble-popup .alert</td>
+ <td></td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/duplication/CrossProjectDuplicationsOnRemoveFileTest/duplications_on_removed_file-expected.json b/tests/src/test/resources/duplication/CrossProjectDuplicationsOnRemoveFileTest/duplications_on_removed_file-expected.json
new file mode 100644
index 00000000000..37f0e402120
--- /dev/null
+++ b/tests/src/test/resources/duplication/CrossProjectDuplicationsOnRemoveFileTest/duplications_on_removed_file-expected.json
@@ -0,0 +1,25 @@
+{
+ "duplications": [
+ {
+ "blocks": [
+ {
+ "from": 9,
+ "size": 27
+ },
+ {
+ "from": 9,
+ "size": 27,
+ "_ref": "1"
+ }
+ ]
+ }
+ ],
+ "files": {
+ "1": {
+ "key": "duplicate-project:src/main/xoo/sample/File1.xoo",
+ "name": "src/main/xoo/sample/File1.xoo",
+ "project": "duplicate-project",
+ "projectName": "duplicate-project"
+ }
+ }
+}
diff --git a/tests/src/test/resources/duplication/CrossProjectDuplicationsTest/cross-project-duplications-viewer.html b/tests/src/test/resources/duplication/CrossProjectDuplicationsTest/cross-project-duplications-viewer.html
new file mode 100644
index 00000000000..689594fdbb1
--- /dev/null
+++ b/tests/src/test/resources/duplication/CrossProjectDuplicationsTest/cross-project-duplications-viewer.html
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>cross-project-duplications-viewer</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">cross-project-duplications-viewer</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/component/index?id=duplicate-project%3Asrc%2Fmain%2Fxoo%2Fsample%2FFile1.xoo</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.source-line</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>css=.source-viewer-header</td>
+ <td>*75.0%*</td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.source-line-duplicated</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>css=[data-line-number='3']</td>
+ <td>*File1*</td>
+ </tr>
+ <tr>
+ <td>click</td>
+ <td>css=.source-line-duplicated</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.bubble-popup</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>css=.bubble-popup</td>
+ <td>glob:*origin-project*File1.xoo*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/duplication/CrossProjectDuplicationsTest/duplications_show-expected.json b/tests/src/test/resources/duplication/CrossProjectDuplicationsTest/duplications_show-expected.json
new file mode 100644
index 00000000000..a9b0c623f53
--- /dev/null
+++ b/tests/src/test/resources/duplication/CrossProjectDuplicationsTest/duplications_show-expected.json
@@ -0,0 +1,32 @@
+{
+ "duplications": [
+ {
+ "blocks": [
+ {
+ "from": 9,
+ "size": 27,
+ "_ref": "1"
+ },
+ {
+ "from": 9,
+ "size": 27,
+ "_ref": "2"
+ }
+ ]
+ }
+ ],
+ "files": {
+ "2": {
+ "key": "origin-project:src/main/xoo/sample/File1.xoo",
+ "name": "src/main/xoo/sample/File1.xoo",
+ "project": "origin-project",
+ "projectName": "origin-project"
+ },
+ "1": {
+ "key": "duplicate-project:src/main/xoo/sample/File1.xoo",
+ "name": "src/main/xoo/sample/File1.xoo",
+ "project": "duplicate-project",
+ "projectName": "duplicate-project"
+ }
+ }
+}
diff --git a/tests/src/test/resources/duplication/CrossProjectDuplicationsTest/sources_lines_duplication-expected.json b/tests/src/test/resources/duplication/CrossProjectDuplicationsTest/sources_lines_duplication-expected.json
new file mode 100644
index 00000000000..0ce4c68a243
--- /dev/null
+++ b/tests/src/test/resources/duplication/CrossProjectDuplicationsTest/sources_lines_duplication-expected.json
@@ -0,0 +1,184 @@
+{
+ "sources": [
+ {
+ "line": 1,
+ "code": "package sample;",
+ "duplicated": false
+ },
+ {
+ "line": 2,
+ "code": "",
+ "duplicated": false
+ },
+ {
+ "line": 3,
+ "code": "public class File1 {",
+ "duplicated": false
+ },
+ {
+ "line": 4,
+ "code": "",
+ "duplicated": false
+ },
+ {
+ "line": 5,
+ "code": " public File1() {",
+ "duplicated": false
+ },
+ {
+ "line": 6,
+ "code": " }",
+ "duplicated": false
+ },
+ {
+ "line": 7,
+ "code": "",
+ "duplicated": false
+ },
+ {
+ "line": 8,
+ "code": " public void test() {",
+ "duplicated": false
+ },
+ {
+ "line": 9,
+ "code": " char[] charList = new char[30];",
+ "duplicated": true
+ },
+ {
+ "line": 10,
+ "code": " for (int i = 0; i &lt; 10; i++) {",
+ "duplicated": true
+ },
+ {
+ "line": 11,
+ "code": " charList[i] = 'a';",
+ "duplicated": true
+ },
+ {
+ "line": 12,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 13,
+ "code": " for (int i = 0; i &lt; 10; i++) {",
+ "duplicated": true
+ },
+ {
+ "line": 14,
+ "code": " charList[i] = 'a';",
+ "duplicated": true
+ },
+ {
+ "line": 15,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 16,
+ "code": " int intergerToBeIncremented = 0;",
+ "duplicated": true
+ },
+ {
+ "line": 17,
+ "code": " while (intergerToBeIncremented &lt; 100) {",
+ "duplicated": true
+ },
+ {
+ "line": 18,
+ "code": " intergerToBeIncremented++;",
+ "duplicated": true
+ },
+ {
+ "line": 19,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 20,
+ "code": " int intergerToBeIncremented2 = 0;",
+ "duplicated": true
+ },
+ {
+ "line": 21,
+ "code": " while (intergerToBeIncremented2 &lt; 100) {",
+ "duplicated": true
+ },
+ {
+ "line": 22,
+ "code": " intergerToBeIncremented2++;",
+ "duplicated": true
+ },
+ {
+ "line": 23,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 24,
+ "code": " String temp = \"\";",
+ "duplicated": true
+ },
+ {
+ "line": 25,
+ "code": " for (int i=0; i&lt;10; i++){",
+ "duplicated": true
+ },
+ {
+ "line": 26,
+ "code": " temp += \"say something\"+i;",
+ "duplicated": true
+ },
+ {
+ "line": 27,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 28,
+ "code": " for (int i=0; i&lt;20; i++){",
+ "duplicated": true
+ },
+ {
+ "line": 29,
+ "code": " temp += \"say nothing\"+i;",
+ "duplicated": true
+ },
+ {
+ "line": 30,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 31,
+ "code": " for (int i=0; i&lt;30; i++){",
+ "duplicated": true
+ },
+ {
+ "line": 32,
+ "code": " temp += \"always say nothing\"+i;",
+ "duplicated": true
+ },
+ {
+ "line": 33,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 34,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 35,
+ "code": "}",
+ "duplicated": true
+ },
+ {
+ "line": 36,
+ "code": "",
+ "duplicated": false
+ }
+ ]
+}
diff --git a/tests/src/test/resources/duplication/DuplicationsTest/duplications_show-expected.json b/tests/src/test/resources/duplication/DuplicationsTest/duplications_show-expected.json
new file mode 100644
index 00000000000..83b69f2be1d
--- /dev/null
+++ b/tests/src/test/resources/duplication/DuplicationsTest/duplications_show-expected.json
@@ -0,0 +1,26 @@
+{
+ "duplications": [
+ {
+ "blocks": [
+ {
+ "from": 9,
+ "size": 29,
+ "_ref": "1"
+ },
+ {
+ "from": 40,
+ "size": 31,
+ "_ref": "1"
+ }
+ ]
+ }
+ ],
+ "files": {
+ "1": {
+ "key": "file-duplications:src/main/xoo/duplicated_lines_within_same_file/DuplicatedLinesInSameFile.xoo",
+ "name": "src/main/xoo/duplicated_lines_within_same_file/DuplicatedLinesInSameFile.xoo",
+ "project": "file-duplications",
+ "projectName": "file-duplications"
+ }
+ }
+}
diff --git a/tests/src/test/resources/duplication/DuplicationsTest/sources_lines_duplication-expected.json b/tests/src/test/resources/duplication/DuplicationsTest/sources_lines_duplication-expected.json
new file mode 100644
index 00000000000..f6ccc926f29
--- /dev/null
+++ b/tests/src/test/resources/duplication/DuplicationsTest/sources_lines_duplication-expected.json
@@ -0,0 +1,359 @@
+{
+ "sources": [
+ {
+ "line": 1,
+ "code": "package duplicated_lines_within_same_file;",
+ "duplicated": false
+ },
+ {
+ "line": 2,
+ "code": "",
+ "duplicated": false
+ },
+ {
+ "line": 3,
+ "code": "public class DuplicatedLinesInSameFile {",
+ "duplicated": false
+ },
+ {
+ "line": 4,
+ "code": "",
+ "duplicated": false
+ },
+ {
+ "line": 5,
+ "code": " public DuplicatedLinesInSameFile() {",
+ "duplicated": false
+ },
+ {
+ "line": 6,
+ "code": " }",
+ "duplicated": false
+ },
+ {
+ "line": 7,
+ "code": "",
+ "duplicated": false
+ },
+ {
+ "line": 8,
+ "code": " public void duplicatedMethodInSameFile1() {",
+ "duplicated": false
+ },
+ {
+ "line": 9,
+ "code": " String temp = \"\";",
+ "duplicated": true
+ },
+ {
+ "line": 10,
+ "code": " for (int i=0; i&lt;10; i++){",
+ "duplicated": true
+ },
+ {
+ "line": 11,
+ "code": " temp += \"say something\"+i;",
+ "duplicated": true
+ },
+ {
+ "line": 12,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 13,
+ "code": " for (int i=0; i&lt;20; i++){",
+ "duplicated": true
+ },
+ {
+ "line": 14,
+ "code": " temp += \"say nothing\"+i;",
+ "duplicated": true
+ },
+ {
+ "line": 15,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 16,
+ "code": " for (int i=0; i&lt;30; i++){",
+ "duplicated": true
+ },
+ {
+ "line": 17,
+ "code": " temp += \"always say nothing\"+i;",
+ "duplicated": true
+ },
+ {
+ "line": 18,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 19,
+ "code": " for (int i=0; i&lt;40; i++){",
+ "duplicated": true
+ },
+ {
+ "line": 20,
+ "code": " temp += \"really nothing to say \"+i;",
+ "duplicated": true
+ },
+ {
+ "line": 21,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 22,
+ "code": " for (int i=0; i&lt;50; i++){",
+ "duplicated": true
+ },
+ {
+ "line": 23,
+ "code": " temp += \"really really nothing to say \"+i;",
+ "duplicated": true
+ },
+ {
+ "line": 24,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 25,
+ "code": " for (int i=0; i&lt;60; i++){",
+ "duplicated": true
+ },
+ {
+ "line": 26,
+ "code": " temp += \".. \"+i;",
+ "duplicated": true
+ },
+ {
+ "line": 27,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 28,
+ "code": " for (int i=0; i&lt;70; i++){",
+ "duplicated": true
+ },
+ {
+ "line": 29,
+ "code": " temp += \"you say something? \"+i;",
+ "duplicated": true
+ },
+ {
+ "line": 30,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 31,
+ "code": " for (int i=0; i&lt;80; i++){",
+ "duplicated": true
+ },
+ {
+ "line": 32,
+ "code": " temp += \"ah no...\"+i;",
+ "duplicated": true
+ },
+ {
+ "line": 33,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 34,
+ "code": " for (int i=0; i&lt;90; i++){",
+ "duplicated": true
+ },
+ {
+ "line": 35,
+ "code": " temp += \"bye\"+i;",
+ "duplicated": true
+ },
+ {
+ "line": 36,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 37,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 38,
+ "code": "",
+ "duplicated": false
+ },
+ {
+ "line": 39,
+ "code": " public void duplicatedMethodInSameFile2() {",
+ "duplicated": false
+ },
+ {
+ "line": 40,
+ "code": " String temp = \"\";",
+ "duplicated": true
+ },
+ {
+ "line": 41,
+ "code": " for (int i=0; i&lt;10; i++){",
+ "duplicated": true
+ },
+ {
+ "line": 42,
+ "code": " temp += \"say something\"+i;",
+ "duplicated": true
+ },
+ {
+ "line": 43,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 44,
+ "code": " for (int i=0; i&lt;20; i++){",
+ "duplicated": true
+ },
+ {
+ "line": 45,
+ "code": " temp += \"say nothing\"+i;",
+ "duplicated": true
+ },
+ {
+ "line": 46,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 47,
+ "code": " for (int i=0; i&lt;30; i++){",
+ "duplicated": true
+ },
+ {
+ "line": 48,
+ "code": " temp += \"always say nothing\"+i;",
+ "duplicated": true
+ },
+ {
+ "line": 49,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 50,
+ "code": " for (int i=0; i&lt;40; i++){",
+ "duplicated": true
+ },
+ {
+ "line": 51,
+ "code": " temp += \"really nothing to say \"+i;",
+ "duplicated": true
+ },
+ {
+ "line": 52,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 53,
+ "code": " for (int i=0; i&lt;50; i++){",
+ "duplicated": true
+ },
+ {
+ "line": 54,
+ "code": " temp += \"really really nothing to say \"+i;",
+ "duplicated": true
+ },
+ {
+ "line": 55,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 56,
+ "code": " for (int i=0; i&lt;60; i++){",
+ "duplicated": true
+ },
+ {
+ "line": 57,
+ "code": " temp += \".. \"+i;",
+ "duplicated": true
+ },
+ {
+ "line": 58,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 59,
+ "code": " for (int i=0; i&lt;70; i++){",
+ "duplicated": true
+ },
+ {
+ "line": 60,
+ "code": " temp += \"you say something? \"+i;",
+ "duplicated": true
+ },
+ {
+ "line": 61,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 62,
+ "code": " for (int i=0; i&lt;80; i++){",
+ "duplicated": true
+ },
+ {
+ "line": 63,
+ "code": " temp += \"ah no...\"+i;",
+ "duplicated": true
+ },
+ {
+ "line": 64,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 65,
+ "code": " for (int i=0; i&lt;90; i++){",
+ "duplicated": true
+ },
+ {
+ "line": 66,
+ "code": " temp += \"bye\"+i;",
+ "duplicated": true
+ },
+ {
+ "line": 67,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 68,
+ "code": " }",
+ "duplicated": true
+ },
+ {
+ "line": 69,
+ "code": "",
+ "duplicated": true
+ },
+ {
+ "line": 70,
+ "code": "}",
+ "duplicated": true
+ },
+ {
+ "line": 71,
+ "code": "",
+ "duplicated": false
+ }
+ ]
+}
diff --git a/tests/src/test/resources/duplication/xoo-duplication-profile.xml b/tests/src/test/resources/duplication/xoo-duplication-profile.xml
new file mode 100644
index 00000000000..999b64a8251
--- /dev/null
+++ b/tests/src/test/resources/duplication/xoo-duplication-profile.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0"?><!-- Generated by Sonar -->
+<profile>
+ <name>xoo-duplication-profile</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>common-xoo</repositoryKey>
+ <key>DuplicatedBlocks</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/exclusions/IssueExclusionsTest/with-many-rules.xml b/tests/src/test/resources/exclusions/IssueExclusionsTest/with-many-rules.xml
new file mode 100644
index 00000000000..f3d0baf0616
--- /dev/null
+++ b/tests/src/test/resources/exclusions/IssueExclusionsTest/with-many-rules.xml
@@ -0,0 +1,32 @@
+<profile>
+ <name>with-many-rules</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MINOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerFile</key>
+ <priority>MAJOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerModule</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>HasTag</key>
+ <priority>INFO</priority>
+ <parameters>
+ <parameter>
+ <key>tag</key>
+ <value>xoo</value>
+ </parameter>
+ </parameters>
+ </rule>
+ </rules>
+</profile> \ No newline at end of file
diff --git a/tests/src/test/resources/i18n/default-locale-is-english.html b/tests/src/test/resources/i18n/default-locale-is-english.html
new file mode 100644
index 00000000000..978ceaa382e
--- /dev/null
+++ b/tests/src/test/resources/i18n/default-locale-is-english.html
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>default_locale_is_english</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">default_locale_is_english</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/dashboard/index?id=sample&name=Dashboard&locale=foo</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>css=#content</td>
+ <td>regexp:.*Jan.*|.*Feb.*|.*Mar.*|.*Apr.*|.*May.*|.*Jun.*|.*Jul.*|.*Aug.*|.*Sep.*|.*Oct.*|.*Nov.*|.*Dec.*</td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>css=#content</td>
+ <td>*0.0%*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/i18n/french-locale.html b/tests/src/test/resources/i18n/french-locale.html
new file mode 100644
index 00000000000..389f9e04ec4
--- /dev/null
+++ b/tests/src/test/resources/i18n/french-locale.html
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>french</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">french</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/dashboard/index?id=sample&name=Dashboard&locale=fr</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>css=#content</td>
+ <td>regexp:.*jan.*|.*fév.*|.*mar.*|.*avr.*|.*mai.*|.*juin.*|.*juil.*|.*août.*|.*sept.*|.*oct.*|.*nov.*|.*déc.*</td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>css=#content</td>
+ <td>*0,0%*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/i18n/french-pack.html b/tests/src/test/resources/i18n/french-pack.html
new file mode 100644
index 00000000000..3f801c9a6ec
--- /dev/null
+++ b/tests/src/test/resources/i18n/french-pack.html
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>french-pack</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">french-pack</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/?locale=fr</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>css=.navbar</td>
+ <td>glob:*Se connecter*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/i18n/locale-with-france-country.html b/tests/src/test/resources/i18n/locale-with-france-country.html
new file mode 100644
index 00000000000..e84051475eb
--- /dev/null
+++ b/tests/src/test/resources/i18n/locale-with-france-country.html
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>french-france</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">french-france</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/dashboard/index?id=sample&name=Dashboard&locale=fr-FR</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>css=#content</td>
+ <td>regexp:.*jan.*|.*fév.*|.*mar.*|.*avr.*|.*mai.*|.*juin.*|.*juil.*|.*août.*|.*sept.*|.*oct.*|.*nov.*|.*déc.*</td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>css=#content</td>
+ <td>*0,0%*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/i18n/locale-with-swiss-country.html b/tests/src/test/resources/i18n/locale-with-swiss-country.html
new file mode 100644
index 00000000000..be1453e002f
--- /dev/null
+++ b/tests/src/test/resources/i18n/locale-with-swiss-country.html
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>french-switzerland</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">french-switzerland</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/dashboard/index?id=sample&name=Dashboard&locale=fr-CH</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>css=#content</td>
+ <td>regexp:.*jan.*|.*fév.*|.*mar.*|.*avr.*|.*mai.*|.*juin.*|.*juil.*|.*août.*|.*sept.*|.*oct.*|.*nov.*|.*déc.*</td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>css=#content</td>
+ <td>*0.0%*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/issue/CommonRulesTest/xoo-common-rules-profile.xml b/tests/src/test/resources/issue/CommonRulesTest/xoo-common-rules-profile.xml
new file mode 100644
index 00000000000..9803527b94f
--- /dev/null
+++ b/tests/src/test/resources/issue/CommonRulesTest/xoo-common-rules-profile.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0"?><!-- Generated by Sonar -->
+<profile>
+ <name>xoo-common-rules</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>common-xoo</repositoryKey>
+ <key>DuplicatedBlocks</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ <rule>
+ <repositoryKey>common-xoo</repositoryKey>
+ <key>InsufficientBranchCoverage</key>
+ <priority>CRITICAL</priority>
+ <parameters>
+ <parameter>
+ <key>minimumBranchCoverageRatio</key>
+ <value>90</value>
+ </parameter>
+ </parameters>
+ </rule>
+ <rule>
+ <repositoryKey>common-xoo</repositoryKey>
+ <key>InsufficientCommentDensity</key>
+ <priority>CRITICAL</priority>
+ <parameters>
+ <parameter>
+ <key>minimumCommentDensity</key>
+ <value>90.0</value>
+ </parameter>
+ </parameters>
+ </rule>
+ <rule>
+ <repositoryKey>common-xoo</repositoryKey>
+ <key>InsufficientLineCoverage</key>
+ <priority>CRITICAL</priority>
+ <parameters>
+ <parameter>
+ <key>minimumLineCoverageRatio</key>
+ <value>90</value>
+ </parameter>
+ </parameters>
+ </rule>
+ <rule>
+ <repositoryKey>common-xoo</repositoryKey>
+ <key>FailedUnitTests</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ <rule>
+ <repositoryKey>common-xoo</repositoryKey>
+ <key>SkippedUnitTests</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/issue/CustomRulesTest/custom.xml b/tests/src/test/resources/issue/CustomRulesTest/custom.xml
new file mode 100644
index 00000000000..b04d126115d
--- /dev/null
+++ b/tests/src/test/resources/issue/CustomRulesTest/custom.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?><!-- Generated by Sonar -->
+<profile>
+ <name>Custom</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>MyCustomRule</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ </rules>
+</profile> \ No newline at end of file
diff --git a/tests/src/test/resources/issue/IssueActionTest/xoo-one-issue-per-line-profile.xml b/tests/src/test/resources/issue/IssueActionTest/xoo-one-issue-per-line-profile.xml
new file mode 100644
index 00000000000..608f80cae96
--- /dev/null
+++ b/tests/src/test/resources/issue/IssueActionTest/xoo-one-issue-per-line-profile.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0"?><!-- Generated by Sonar -->
+<profile>
+ <name>xoo-one-issue-per-line-profile</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/issue/IssueBulkChangeTest/one-issue-per-line-profile.xml b/tests/src/test/resources/issue/IssueBulkChangeTest/one-issue-per-line-profile.xml
new file mode 100644
index 00000000000..365aa896a73
--- /dev/null
+++ b/tests/src/test/resources/issue/IssueBulkChangeTest/one-issue-per-line-profile.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<profile>
+ <name>one-issue-per-line</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MINOR</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/issue/IssueChangelogTest/one-issue-per-line-profile.xml b/tests/src/test/resources/issue/IssueChangelogTest/one-issue-per-line-profile.xml
new file mode 100644
index 00000000000..365aa896a73
--- /dev/null
+++ b/tests/src/test/resources/issue/IssueChangelogTest/one-issue-per-line-profile.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<profile>
+ <name>one-issue-per-line</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MINOR</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/issue/IssueCreationDateTest/no-rules.xml b/tests/src/test/resources/issue/IssueCreationDateTest/no-rules.xml
new file mode 100644
index 00000000000..62ec52d9d10
--- /dev/null
+++ b/tests/src/test/resources/issue/IssueCreationDateTest/no-rules.xml
@@ -0,0 +1,6 @@
+<profile>
+ <name>creation-date-quality-profile</name>
+ <language>xoo</language>
+ <rules>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/issue/IssueCreationDateTest/one-rule.xml b/tests/src/test/resources/issue/IssueCreationDateTest/one-rule.xml
new file mode 100644
index 00000000000..c1d7d44f02c
--- /dev/null
+++ b/tests/src/test/resources/issue/IssueCreationDateTest/one-rule.xml
@@ -0,0 +1,11 @@
+<profile>
+ <name>creation-date-quality-profile</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerFile</key>
+ <priority>MAJOR</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/issue/IssueCreationTest/override-profile-severity.xml b/tests/src/test/resources/issue/IssueCreationTest/override-profile-severity.xml
new file mode 100644
index 00000000000..d048760be60
--- /dev/null
+++ b/tests/src/test/resources/issue/IssueCreationTest/override-profile-severity.xml
@@ -0,0 +1,11 @@
+<profile>
+ <name>override-profile-severity</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneBlockerIssuePerFile</key>
+ <priority>INFO</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/issue/IssueCreationTest/with-custom-message.xml b/tests/src/test/resources/issue/IssueCreationTest/with-custom-message.xml
new file mode 100644
index 00000000000..323e50985cd
--- /dev/null
+++ b/tests/src/test/resources/issue/IssueCreationTest/with-custom-message.xml
@@ -0,0 +1,11 @@
+<profile>
+ <name>with-custom-message</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>CustomMessage</key>
+ <priority>MAJOR</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/issue/IssueFilterExtensionTest/xoo-with-many-rules.xml b/tests/src/test/resources/issue/IssueFilterExtensionTest/xoo-with-many-rules.xml
new file mode 100644
index 00000000000..a08c9cdd246
--- /dev/null
+++ b/tests/src/test/resources/issue/IssueFilterExtensionTest/xoo-with-many-rules.xml
@@ -0,0 +1,32 @@
+<profile>
+ <name>with-many-rules</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MINOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerFile</key>
+ <priority>MAJOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerModule</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>HasTag</key>
+ <priority>INFO</priority>
+ <parameters>
+ <parameter>
+ <key>tag</key>
+ <value>xoo</value>
+ </parameter>
+ </parameters>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/issue/IssueFilterOnCommonRulesTest/xoo-common-rules-profile.xml b/tests/src/test/resources/issue/IssueFilterOnCommonRulesTest/xoo-common-rules-profile.xml
new file mode 100644
index 00000000000..9803527b94f
--- /dev/null
+++ b/tests/src/test/resources/issue/IssueFilterOnCommonRulesTest/xoo-common-rules-profile.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0"?><!-- Generated by Sonar -->
+<profile>
+ <name>xoo-common-rules</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>common-xoo</repositoryKey>
+ <key>DuplicatedBlocks</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ <rule>
+ <repositoryKey>common-xoo</repositoryKey>
+ <key>InsufficientBranchCoverage</key>
+ <priority>CRITICAL</priority>
+ <parameters>
+ <parameter>
+ <key>minimumBranchCoverageRatio</key>
+ <value>90</value>
+ </parameter>
+ </parameters>
+ </rule>
+ <rule>
+ <repositoryKey>common-xoo</repositoryKey>
+ <key>InsufficientCommentDensity</key>
+ <priority>CRITICAL</priority>
+ <parameters>
+ <parameter>
+ <key>minimumCommentDensity</key>
+ <value>90.0</value>
+ </parameter>
+ </parameters>
+ </rule>
+ <rule>
+ <repositoryKey>common-xoo</repositoryKey>
+ <key>InsufficientLineCoverage</key>
+ <priority>CRITICAL</priority>
+ <parameters>
+ <parameter>
+ <key>minimumLineCoverageRatio</key>
+ <value>90</value>
+ </parameter>
+ </parameters>
+ </rule>
+ <rule>
+ <repositoryKey>common-xoo</repositoryKey>
+ <key>FailedUnitTests</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ <rule>
+ <repositoryKey>common-xoo</repositoryKey>
+ <key>SkippedUnitTests</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/issue/IssueFilterTest/with-many-rules.xml b/tests/src/test/resources/issue/IssueFilterTest/with-many-rules.xml
new file mode 100644
index 00000000000..366a3ab7576
--- /dev/null
+++ b/tests/src/test/resources/issue/IssueFilterTest/with-many-rules.xml
@@ -0,0 +1,43 @@
+<profile>
+ <name>with-many-rules</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MINOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerFile</key>
+ <priority>MAJOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerModule</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>HasTag</key>
+ <priority>INFO</priority>
+ <parameters>
+ <parameter>
+ <key>tag</key>
+ <value>xoo</value>
+ </parameter>
+ </parameters>
+ </rule>
+ <rule>
+ <repositoryKey>common-xoo</repositoryKey>
+ <key>InsufficientLineCoverage</key>
+ <priority>BLOCKER</priority>
+ <parameters>
+ <parameter>
+ <key>minimumLineCoverageRatio</key>
+ <value>90</value>
+ </parameter>
+ </parameters>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/issue/IssuePurgeTest/with-many-rules.xml b/tests/src/test/resources/issue/IssuePurgeTest/with-many-rules.xml
new file mode 100644
index 00000000000..f3d0baf0616
--- /dev/null
+++ b/tests/src/test/resources/issue/IssuePurgeTest/with-many-rules.xml
@@ -0,0 +1,32 @@
+<profile>
+ <name>with-many-rules</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MINOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerFile</key>
+ <priority>MAJOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerModule</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>HasTag</key>
+ <priority>INFO</priority>
+ <parameters>
+ <parameter>
+ <key>tag</key>
+ <value>xoo</value>
+ </parameter>
+ </parameters>
+ </rule>
+ </rules>
+</profile> \ No newline at end of file
diff --git a/tests/src/test/resources/issue/IssueTrackingTest/one-issue-per-module-profile.xml b/tests/src/test/resources/issue/IssueTrackingTest/one-issue-per-module-profile.xml
new file mode 100644
index 00000000000..8b863ce9a46
--- /dev/null
+++ b/tests/src/test/resources/issue/IssueTrackingTest/one-issue-per-module-profile.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<profile>
+ <name>one-issue-per-module</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerModule</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ </rules>
+</profile> \ No newline at end of file
diff --git a/tests/src/test/resources/issue/IssueWorkflowTest/xoo-one-issue-per-line-profile.xml b/tests/src/test/resources/issue/IssueWorkflowTest/xoo-one-issue-per-line-profile.xml
new file mode 100644
index 00000000000..608f80cae96
--- /dev/null
+++ b/tests/src/test/resources/issue/IssueWorkflowTest/xoo-one-issue-per-line-profile.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0"?><!-- Generated by Sonar -->
+<profile>
+ <name>xoo-one-issue-per-line-profile</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/issue/NewIssuesMeasureTest/profile1.xml b/tests/src/test/resources/issue/NewIssuesMeasureTest/profile1.xml
new file mode 100644
index 00000000000..3acc7f6a4ed
--- /dev/null
+++ b/tests/src/test/resources/issue/NewIssuesMeasureTest/profile1.xml
@@ -0,0 +1,27 @@
+<profile>
+ <name>profile1</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerFile</key>
+ <priority>MAJOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerModule</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>HasTag</key>
+ <priority>INFO</priority>
+ <parameters>
+ <parameter>
+ <key>tag</key>
+ <value>xoo</value>
+ </parameter>
+ </parameters>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/issue/NewIssuesMeasureTest/profile2.xml b/tests/src/test/resources/issue/NewIssuesMeasureTest/profile2.xml
new file mode 100644
index 00000000000..70e0f8cd25a
--- /dev/null
+++ b/tests/src/test/resources/issue/NewIssuesMeasureTest/profile2.xml
@@ -0,0 +1,32 @@
+<profile>
+ <name>profile2</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MINOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerFile</key>
+ <priority>MAJOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerModule</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>HasTag</key>
+ <priority>INFO</priority>
+ <parameters>
+ <parameter>
+ <key>tag</key>
+ <value>xoo</value>
+ </parameter>
+ </parameters>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/issue/issue-on-tag-foobar.xml b/tests/src/test/resources/issue/issue-on-tag-foobar.xml
new file mode 100644
index 00000000000..d49b8f7c6c5
--- /dev/null
+++ b/tests/src/test/resources/issue/issue-on-tag-foobar.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<profile>
+ <name>issue-on-tag-foobar</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>HasTag</key>
+ <priority>MAJOR</priority>
+ <parameters>
+ <parameter>
+ <key>tag</key>
+ <value>foobar</value>
+ </parameter>
+ </parameters>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/issue/one-issue-per-file-profile.xml b/tests/src/test/resources/issue/one-issue-per-file-profile.xml
new file mode 100644
index 00000000000..141921f02e7
--- /dev/null
+++ b/tests/src/test/resources/issue/one-issue-per-file-profile.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0"?><!-- Generated by Sonar -->
+<profile>
+ <name>one-issue-per-file-profile</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerFile</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/issue/one-issue-per-line-profile.xml b/tests/src/test/resources/issue/one-issue-per-line-profile.xml
new file mode 100644
index 00000000000..28cebcc2380
--- /dev/null
+++ b/tests/src/test/resources/issue/one-issue-per-line-profile.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0"?><!-- Generated by Sonar -->
+<profile>
+ <name>one-issue-per-line-profile</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/issue/with-many-rules.xml b/tests/src/test/resources/issue/with-many-rules.xml
new file mode 100644
index 00000000000..f97ef70cefd
--- /dev/null
+++ b/tests/src/test/resources/issue/with-many-rules.xml
@@ -0,0 +1,42 @@
+<profile>
+ <name>with-many-rules</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MINOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerFile</key>
+ <priority>MAJOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerModule</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>HasTag</key>
+ <priority>INFO</priority>
+ <parameters>
+ <parameter>
+ <key>tag</key>
+ <value>xoo</value>
+ </parameter>
+ </parameters>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneBugIssuePerLine</key>
+ <priority>MAJOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneVulnerabilityIssuePerModule</key>
+ <priority>BLOCKER</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/logback-test.xml b/tests/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..599faa969f0
--- /dev/null
+++ b/tests/src/test/resources/logback-test.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<configuration debug="false">
+ <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
+
+ <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <pattern>
+ %d{yyyy.MM.dd HH:mm:ss} %-5level %msg%n
+ </pattern>
+ </encoder>
+ </appender>
+
+ <root>
+ <!-- Don't set to DEBUG or it will show the dev licenses into the console!!!! -->
+ <level value="INFO"/>
+ <appender-ref ref="CONSOLE"/>
+ </root>
+
+</configuration>
diff --git a/tests/src/test/resources/measure/ProjectDashboardTest/test_project_overview_after_first_analysis.html b/tests/src/test/resources/measure/ProjectDashboardTest/test_project_overview_after_first_analysis.html
new file mode 100644
index 00000000000..ac440b184aa
--- /dev/null
+++ b/tests/src/test/resources/measure/ProjectDashboardTest/test_project_overview_after_first_analysis.html
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>test_project_overview_after_first_analysis</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">test_project_overview_after_first_analysis</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/dashboard?id=project-for-overview</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>id=content</td>
+ <td>*Quality Gate*Passed*</td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>id=overview-code-smells</td>
+ <td>*0*A*Debt*0*Code Smells*</td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>id=content</td>
+ <td>*0.0%*Duplications*0*Duplicated Blocks*</td>
+ </tr>
+ <tr>
+ <td>assertNotText</td>
+ <td>id=content</td>
+ <td>*Coverage*</td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>id=content</td>
+ <td>*Quality Gate*SonarQube way*</td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>id=content</td>
+ <td>*Quality Profiles*Xoo*Basic*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/measure/ProjectMeasuresPageTest/should_display_measures_page.html b/tests/src/test/resources/measure/ProjectMeasuresPageTest/should_display_measures_page.html
new file mode 100644
index 00000000000..4c5a77a9545
--- /dev/null
+++ b/tests/src/test/resources/measure/ProjectMeasuresPageTest/should_display_measures_page.html
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>should_display_measure_drilldown</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/component_measures?id=project-measures-page-test-project</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>id=measure-code_smells</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>id=measure-code_smells-value</td>
+ <td>*0*</td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>id=measure-new_code_smells-leak</td>
+ <td>*0*</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=#measure-code_smells &gt; a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*Code Smells*0*</td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*src/main/xoo/sample*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/measure/ProjectMeasuresPageTest/should_drilldown_on_list_view.html b/tests/src/test/resources/measure/ProjectMeasuresPageTest/should_drilldown_on_list_view.html
new file mode 100644
index 00000000000..e68d8b87dfb
--- /dev/null
+++ b/tests/src/test/resources/measure/ProjectMeasuresPageTest/should_drilldown_on_list_view.html
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>should_drilldown_on_list_view</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/component_measures/metric/ncloc/list?id=project-measures-page-test-project</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>id=component-measures-component-link-project-measures-page-test-project:src/main/xoo/sample/Sample.xoo</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>id=component-measures-component-link-project-measures-page-test-project:src/main/xoo/sample/Sample.xoo</td>
+ <td>*src/main/xoo/sample/Sample.xoo*</td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>id=component-measures-component-measure-project-measures-page-test-project:src/main/xoo/sample/Sample.xoo-ncloc</td>
+ <td>*13*</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>id=component-measures-component-link-project-measures-page-test-project:src/main/xoo/sample/Sample.xoo</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.source-line-code</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.component-measures-breadcrumbs</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>id=component-measures-breadcrumb-project-measures-page-test-project</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=.component-measures-breadcrumbs</td>
+ <td>*Sample.xoo*13*</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>id=component-measures-breadcrumb-project-measures-page-test-project</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>id=component-measures-component-link-project-measures-page-test-project:src/main/xoo/sample/Sample.xoo</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/measure/ProjectMeasuresPageTest/should_drilldown_on_tree_view.html b/tests/src/test/resources/measure/ProjectMeasuresPageTest/should_drilldown_on_tree_view.html
new file mode 100644
index 00000000000..489c9d93afc
--- /dev/null
+++ b/tests/src/test/resources/measure/ProjectMeasuresPageTest/should_drilldown_on_tree_view.html
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>should_drilldown_on_tree_view</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/component_measures/metric/ncloc/tree?id=project-measures-page-test-project</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>id=component-measures-component-link-project-measures-page-test-project:src/main/xoo/sample</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>id=component-measures-component-link-project-measures-page-test-project:src/main/xoo/sample</td>
+ <td>*src/main/xoo/sample*</td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>id=component-measures-component-measure-project-measures-page-test-project:src/main/xoo/sample-ncloc</td>
+ <td>*13*</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>id=component-measures-component-link-project-measures-page-test-project:src/main/xoo/sample</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>id=component-measures-component-link-project-measures-page-test-project:src/main/xoo/sample/Sample.xoo</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>id=component-measures-component-link-project-measures-page-test-project:src/main/xoo/sample/Sample.xoo</td>
+ <td>*src/main/xoo/sample/Sample.xoo*</td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>id=component-measures-component-measure-project-measures-page-test-project:src/main/xoo/sample/Sample.xoo-ncloc</td>
+ <td>*13*</td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=.component-measures-breadcrumbs</td>
+ <td>*ProjectMeasuresPageTest Project*src/main/xoo/sample*13*</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>id=component-measures-component-link-project-measures-page-test-project:src/main/xoo/sample/Sample.xoo</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.source-line-code</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=.component-measures-breadcrumbs</td>
+ <td>*ProjectMeasuresPageTest Project*src/main/xoo/sample*13*Sample.xoo*13*</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>id=component-measures-breadcrumb-project-measures-page-test-project</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>id=component-measures-component-link-project-measures-page-test-project:src/main/xoo/sample</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/measure/one-issue-per-file.xml b/tests/src/test/resources/measure/one-issue-per-file.xml
new file mode 100644
index 00000000000..7193ebfd779
--- /dev/null
+++ b/tests/src/test/resources/measure/one-issue-per-file.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<profile>
+ <name>one-issue-per-file</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerFile</key>
+ <priority>MINOR</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/measure/one-issue-per-line-profile.xml b/tests/src/test/resources/measure/one-issue-per-line-profile.xml
new file mode 100644
index 00000000000..521adc7e06f
--- /dev/null
+++ b/tests/src/test/resources/measure/one-issue-per-line-profile.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<profile>
+ <name>one-issue-per-line</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MINOR</priority>
+ </rule>
+ </rules>
+</profile> \ No newline at end of file
diff --git a/tests/src/test/resources/measure/one-issue-per-line.xml b/tests/src/test/resources/measure/one-issue-per-line.xml
new file mode 100644
index 00000000000..365aa896a73
--- /dev/null
+++ b/tests/src/test/resources/measure/one-issue-per-line.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<profile>
+ <name>one-issue-per-line</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MINOR</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/one-xoo-issue-per-line.xml b/tests/src/test/resources/one-xoo-issue-per-line.xml
new file mode 100644
index 00000000000..b2f49460d34
--- /dev/null
+++ b/tests/src/test/resources/one-xoo-issue-per-line.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?><!-- Generated by Sonar -->
+<profile>
+ <name>one-xoo-issue-per-line</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MAJOR</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/organization/IssueAssignTest/one-issue-per-file-profile.xml b/tests/src/test/resources/organization/IssueAssignTest/one-issue-per-file-profile.xml
new file mode 100644
index 00000000000..141921f02e7
--- /dev/null
+++ b/tests/src/test/resources/organization/IssueAssignTest/one-issue-per-file-profile.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0"?><!-- Generated by Sonar -->
+<profile>
+ <name>one-issue-per-file-profile</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerFile</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_compare.html b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_compare.html
new file mode 100644
index 00000000000..b8ea98d1690
--- /dev/null
+++ b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_compare.html
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_display_changelog</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_display_changelog</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/organizations/test-org/quality_profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profiles-table-name a[href^=&quot;/organizations/test-org/quality_profiles/show?key=xoo-sample&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.quality-profiles-table-name a[href^=&quot;/organizations/test-org/quality_profiles/show?key=xoo-sample&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-header .dropdown-toggle</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.quality-profile-header .dropdown-toggle</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=#quality-profile-compare</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-profile-comparison .Select</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.js-profile-comparison .Select-control</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_copy.html b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_copy.html
new file mode 100644
index 00000000000..1ed2b8a9018
--- /dev/null
+++ b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_copy.html
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_create</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_create</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/sessions/login</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin2</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin2</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/organizations/test-org/quality_profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-header .dropdown-toggle</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.quality-profile-header .dropdown-toggle</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=#quality-profile-copy</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>css=#copy-profile-name</td>
+ <td>copied</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=#copy-profile-submit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=.quality-profile-header</td>
+ <td>*copied*</td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=.quality-profile-rules</td>
+ <td>*1*Bugs*0*Vulnerabilities*0*Code Smells*1*</td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/organizations/test-org/quality_profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;copied&quot;]</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_create.html b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_create.html
new file mode 100644
index 00000000000..a6716b8c06a
--- /dev/null
+++ b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_create.html
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_create</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_create</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/sessions/login</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin2</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin2</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/organizations/test-org/quality_profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=#quality-profiles-create</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=#quality-profiles-create</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=#create-profile-name</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertElementPresent</td>
+ <td>css=#create-profile-form-backup-XooProfileImporter</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>css=#create-profile-name</td>
+ <td>test</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=#create-profile-submit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-header</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=.quality-profile-header</td>
+ <td>*test*</td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=.quality-profile-rules</td>
+ <td>*Bugs*0*Vulnerabilities*0*Code Smells*0*</td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/organizations/test-org/quality_profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profiles-table-row[data-name=&quot;test&quot;]</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_delete.html b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_delete.html
new file mode 100644
index 00000000000..9610b0db66c
--- /dev/null
+++ b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_delete.html
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_create</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_create</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/sessions/login</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin2</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin2</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/organizations/test-org/quality_profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-header .dropdown-toggle</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.quality-profile-header .dropdown-toggle</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=#quality-profile-delete</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=#delete-profile-submit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=#delete-profile-submit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementNotPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/organizations/test-org/quality_profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertElementNotPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;]</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_display_changelog.html b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_display_changelog.html
new file mode 100644
index 00000000000..12536be7cbf
--- /dev/null
+++ b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_display_changelog.html
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_display_changelog</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_display_changelog</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/organizations/test-org/quality_profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=a[href^=&quot;/organizations/test-org/quality_profiles/changelog&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=a[href^=&quot;/organizations/test-org/quality_profiles/changelog&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-profile-changelog-event</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=.js-profile-changelog-event</td>
+ <td>*System*Activated*Has Tag*Major*tag*xoo*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_display_list.html b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_display_list.html
new file mode 100644
index 00000000000..4d9242c77de
--- /dev/null
+++ b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_display_list.html
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>should_display_list</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_display_list</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/organizations/test-org/quality_profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profiles-table</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertElementPresent</td>
+ <td>css=.quality-profiles-table .data[data-language=&quot;xoo&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertElementPresent</td>
+ <td>css=.quality-profiles-table .data[data-language=&quot;xoo2&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;] .quality-profiles-table-name</td>
+ <td>*Basic*</td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;] .quality-profiles-table-projects</td>
+ <td>*Default*</td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;] .quality-profiles-table-rules</td>
+ <td>*1*</td>
+</tr>
+<tr>
+ <td>assertElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;] .quality-profiles-table-rules a[href^=&quot;/organizations/test-org/rules#qprofile&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=table[data-language=&quot;xoo2&quot;] tr[data-name=&quot;Basic&quot;] .quality-profiles-table-name</td>
+ <td>*Basic*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_display_profile_exporters.html b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_display_profile_exporters.html
new file mode 100644
index 00000000000..b26d162f2e7
--- /dev/null
+++ b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_display_profile_exporters.html
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_display_profile_projects.html</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_display_profile_projects.html</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/organizations/test-org/quality_profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-exporters</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-exporters [data-key=&quot;XooFakeExporter&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-exporters a[href^=&quot;/api/qualityprofiles/export&quot;]</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_display_profile_inheritance.html b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_display_profile_inheritance.html
new file mode 100644
index 00000000000..c403fbf5196
--- /dev/null
+++ b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_display_profile_inheritance.html
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_display_profile_inheritance.html</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_display_profile_inheritance.html</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/organizations/test-org/quality_profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-inheritance</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-inheritance-ancestor</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=.js-inheritance-ancestor</td>
+ <td>*Basic*1*</td>
+</tr>
+<tr>
+ <td>assertElementPresent</td>
+ <td>css=.js-inheritance-current</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=.js-inheritance-current</td>
+ <td>*sample*1*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_display_profile_projects.html b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_display_profile_projects.html
new file mode 100644
index 00000000000..062014eb239
--- /dev/null
+++ b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_display_profile_projects.html
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_display_profile_projects.html</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_display_profile_projects.html</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/organizations/test-org/quality_profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-projects</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-profile-project</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=.js-profile-project</td>
+ <td>*Sample*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_display_profile_rules.html b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_display_profile_rules.html
new file mode 100644
index 00000000000..6b497fc7b58
--- /dev/null
+++ b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_display_profile_rules.html
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_display_profile_rules.html</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_display_profile_rules.html</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/organizations/test-org/quality_profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-rules</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=.quality-profile-rules</td>
+ <td>*Active*1*Bugs*0*Vulnerabilities*0*Code Smells*1*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_filter_by_language.html b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_filter_by_language.html
new file mode 100644
index 00000000000..a913a70a34f
--- /dev/null
+++ b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_filter_by_language.html
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>should_filter_by_language</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_filter_by_language</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/organizations/test-org/quality_profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profiles-table .data[data-language=&quot;xoo&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertElementPresent</td>
+ <td>css=.quality-profiles-table .data[data-language=&quot;xoo2&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=.js-language-filter</td>
+ <td>*All*</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.js-language-filter</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-language-filter-option[data-language=&quot;xoo2&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.js-language-filter-option[data-language=&quot;xoo2&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementNotPresent</td>
+ <td>css=.quality-profiles-table[data-language=&quot;xoo&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertElementPresent</td>
+ <td>css=.quality-profiles-table .data[data-language=&quot;xoo2&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=.js-language-filter</td>
+ <td>*Xoo2*</td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/organizations/test-org/quality_profiles?language=xoo2</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profiles-table .data[data-language=&quot;xoo2&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertElementNotPresent</td>
+ <td>css=.quality-profiles-table[data-language=&quot;xoo&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=.js-language-filter</td>
+ <td>*Xoo2*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_open_from_list.html b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_open_from_list.html
new file mode 100644
index 00000000000..2e753ba1cd7
--- /dev/null
+++ b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_open_from_list.html
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>should_open_from_list</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_open_from_list</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/organizations/test-org/quality_profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-header</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-rules</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-inheritance</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-projects</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_rename.html b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_rename.html
new file mode 100644
index 00000000000..5da4d40ca0c
--- /dev/null
+++ b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_rename.html
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_create</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_create</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/sessions/login</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin2</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin2</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/organizations/test-org/quality_profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-header .dropdown-toggle</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.quality-profile-header .dropdown-toggle</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=#quality-profile-rename</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=#rename-profile-name</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>css=#rename-profile-name</td>
+ <td>new name</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=#rename-profile-submit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=.quality-profile-header</td>
+ <td>*new name*</td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/organizations/test-org/quality_profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;new name&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_restore.html b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_restore.html
new file mode 100644
index 00000000000..dd2d21ba31f
--- /dev/null
+++ b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_restore.html
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_create</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_create</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/sessions/login</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin2</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin2</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/organizations/test-org/quality_profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-more-admin-actions</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.js-more-admin-actions</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=#quality-profiles-restore</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=#restore-profile-backup</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=#restore-profile-submit</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_set_default.html b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_set_default.html
new file mode 100644
index 00000000000..c267bf702e3
--- /dev/null
+++ b/tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_set_default.html
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_create</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_create</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/sessions/login</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin2</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin2</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/organizations/test-org/quality_profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-header .dropdown-toggle</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.quality-profile-header .dropdown-toggle</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=#quality-profile-set-as-default</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=.quality-profile-projects</td>
+ <td>*Default*</td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/organizations/test-org/quality_profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profiles-table-row[data-name=&quot;sample&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=.quality-profiles-table-row[data-name=&quot;sample&quot;] .quality-profiles-table-projects</td>
+ <td>*Default*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/projectAdministration/BackgroundTasksTest/should_not_display_failing_and_search_and_filter_elements_on_project_level_page.html b/tests/src/test/resources/projectAdministration/BackgroundTasksTest/should_not_display_failing_and_search_and_filter_elements_on_project_level_page.html
new file mode 100644
index 00000000000..e28cdbba827
--- /dev/null
+++ b/tests/src/test/resources/projectAdministration/BackgroundTasksTest/should_not_display_failing_and_search_and_filter_elements_on_project_level_page.html
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_not_display_failing_and_search_and_filter_elements_on_project_level_page</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">should_not_display_failing_and_search_and_filter_elements_on_project_level_page</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/logout</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/sessions/login</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin-user</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin-user</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>commit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/project/background_tasks?id=test-project</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.background-tasks .badge</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertNotText</td>
+ <td>css=.background-tasks</td>
+ <td>*Another Test Project*</td>
+</tr>
+<tr>
+ <td>assertElementNotPresent</td>
+ <td>css=.js-search</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertNotText</td>
+ <td>css=#content</td>
+ <td>*still failing*</td>
+</tr>
+<tr>
+ <td>assertElementNotPresent</td>
+ <td>css=.icon-filter</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/projectAdministration/BulkDeletionTest/bulk-delete-filter-projects.html b/tests/src/test/resources/projectAdministration/BulkDeletionTest/bulk-delete-filter-projects.html
new file mode 100644
index 00000000000..b6256e49b35
--- /dev/null
+++ b/tests/src/test/resources/projectAdministration/BulkDeletionTest/bulk-delete-filter-projects.html
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>bulk-delete-filter-projects</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/logout</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/sessions/login</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin-user</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin-user</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/projects_admin</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*Bar-Sonar-Plugin*Foo-Application*Sample-Project*</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>css=.search-box-input</td>
+ <td>s</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.search-box-submit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*Bar-Sonar-Plugin*Sample-Project*</td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*cameleon-3*cameleon-1*</td>
+</tr>
+<tr>
+ <td>assertTextNotPresent</td>
+ <td>content</td>
+ <td>*Foo-Application*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-administration/multimodule-project-delete-version.html b/tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-administration/multimodule-project-delete-version.html
new file mode 100644
index 00000000000..496917b9e4c
--- /dev/null
+++ b/tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-administration/multimodule-project-delete-version.html
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>multimodule-project-delete-version</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">project-modify-versions</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/logout</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/sessions/login</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin</td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/project/history/com.sonarsource.it.samples:multi-modules-sample</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>link=Remove</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>assertConfirmation</td>
+ <td>Are you sure you want to remove 'RELEASE' from this snapshot?</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>infomsg</td>
+ <td>glob:*Version 'RELEASE' was removed from current project*</td>
+ </tr>
+ <tr>
+ <td>assertNotText</td>
+ <td>version_1</td>
+ <td>glob:*RELEASE*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-administration/multimodule-project-modify-version.html b/tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-administration/multimodule-project-modify-version.html
new file mode 100644
index 00000000000..f336f7e93b9
--- /dev/null
+++ b/tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-administration/multimodule-project-modify-version.html
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>multimodule-project-modify-version</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">project-modify-versions</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/logout</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/sessions/login</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin-user</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin-user</td>
+ </tr>
+ <tr>
+ <td>click</td>
+ <td>commit</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/project/history/com.sonarsource.it.samples:multi-modules-sample</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>assertElementNotPresent</td>
+ <td>link=Remove</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>click</td>
+ <td>version_1_change</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>version_name_1</td>
+ <td>RELEASE</td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>save_version_1</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>infomsg</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>infomsg</td>
+ <td>Version 'RELEASE' was created for current project.</td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>version_1</td>
+ <td>glob:*RELEASE*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-deletion/project-deletion.html b/tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-deletion/project-deletion.html
new file mode 100644
index 00000000000..9345f6e3f75
--- /dev/null
+++ b/tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-deletion/project-deletion.html
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>project-deletion</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">project-deletion</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/logout</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/sessions/login</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>project-deletion-with-admin-permission-on-project</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>password</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/project/deletion?id=sample</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=#delete-project</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=#delete-project</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=#delete-project-confirm</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=#delete-project-confirm</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementNotPresent</td>
+ <td>css=#delete-project-confirm</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/dashboard?id=sample</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.process-spinner-failed</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/projectAdministration/ProjectPermissionsTest/test_project_permissions_page_shows_only_single_project.html b/tests/src/test/resources/projectAdministration/ProjectPermissionsTest/test_project_permissions_page_shows_only_single_project.html
new file mode 100644
index 00000000000..cbb6595ff20
--- /dev/null
+++ b/tests/src/test/resources/projectAdministration/ProjectPermissionsTest/test_project_permissions_page_shows_only_single_project.html
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>test_project_overview_after_first_analysis</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">test_project_overview_after_first_analysis</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/logout</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/sessions/login</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin</td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/project_roles?id=project-permissions-project</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=#projects</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>assertNotText</td>
+ <td>css=#projects</td>
+ <td>*Another Test Project*</td>
+ </tr>
+ <tr>
+ <td>assertElementNotPresent</td>
+ <td>css=footer</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>assertElementNotPresent</td>
+ <td>css=.search-box</td>
+ <td></td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/projectComparison/ProjectComparisonTest/should-add-metrics.html b/tests/src/test/resources/projectComparison/ProjectComparisonTest/should-add-metrics.html
new file mode 100644
index 00000000000..ab7758bd71a
--- /dev/null
+++ b/tests/src/test/resources/projectComparison/ProjectComparisonTest/should-add-metrics.html
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>should-add-metrics</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/comparison/index</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>assertNotText</td>
+ <td>comparison-page</td>
+ <td>*Major issues*</td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>new_metric</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>typeAndWait</td>
+ <td>new_metric</td>
+ <td>major_violations</td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>comparison-page</td>
+ <td>*Lines of Code*Complexity*Comments (%)*Duplicated Lines (%)*Issues*Coverage*Major Issues*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/projectComparison/ProjectComparisonTest/should-add-projects.html b/tests/src/test/resources/projectComparison/ProjectComparisonTest/should-add-projects.html
new file mode 100644
index 00000000000..18c882331ad
--- /dev/null
+++ b/tests/src/test/resources/projectComparison/ProjectComparisonTest/should-add-projects.html
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>should-add-projects</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/comparison/index</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>new_resource</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>new_resource</td>
+ <td>project-comparison-test-project</td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>//option[text()='1.0-SNAPSHOT']</td>
+ <td></td>
+</tr>
+<tr>
+ <td>selectAndWait</td>
+ <td>new_version</td>
+ <td>1.0-SNAPSHOT</td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>id=content</td>
+ <td>*ProjectComparisonTest Project*1.0-SNAPSHOT*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/projectComparison/ProjectComparisonTest/should-display-basic-set-of-metrics.html b/tests/src/test/resources/projectComparison/ProjectComparisonTest/should-display-basic-set-of-metrics.html
new file mode 100644
index 00000000000..3a8e1256198
--- /dev/null
+++ b/tests/src/test/resources/projectComparison/ProjectComparisonTest/should-display-basic-set-of-metrics.html
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>should-display-basic-set-of-metrics</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/comparison/index</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>id=content</td>
+ <td>*Lines of Code*Complexity*Comments (%)*Duplicated Lines (%)*Issues*Coverage*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/projectComparison/ProjectComparisonTest/should-move-and-remove-metrics.html b/tests/src/test/resources/projectComparison/ProjectComparisonTest/should-move-and-remove-metrics.html
new file mode 100644
index 00000000000..3b89b4af128
--- /dev/null
+++ b/tests/src/test/resources/projectComparison/ProjectComparisonTest/should-move-and-remove-metrics.html
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>should-move-and-remove-metrics</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/comparison/index</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>comparison-page</td>
+ <td>*Lines of Code*Complexity*Comments (%)*Duplicated Lines (%)*Issues*Coverage*</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>down-0</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>comparison-page</td>
+ <td>*Complexity*Lines of Code*Comments (%)*Duplicated Lines (%)*Issues*Coverage*</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>up-5</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>comparison-page</td>
+ <td>*Complexity*Lines of Code*Comments (%)*Duplicated Lines (%)*Coverage*Issues*</td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>comparison-page</td>
+ <td>*%*</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>del-m-2</td>
+ <td></td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>del-m-2</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>comparison-page</td>
+ <td>*Complexity*Lines of Code*Coverage*Issues*</td>
+</tr>
+<tr>
+ <td>assertNotText</td>
+ <td>comparison-page</td>
+ <td>*%*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/projectComparison/ProjectComparisonTest/should-move-and-remove-projects.html b/tests/src/test/resources/projectComparison/ProjectComparisonTest/should-move-and-remove-projects.html
new file mode 100644
index 00000000000..f7d71dbfd2f
--- /dev/null
+++ b/tests/src/test/resources/projectComparison/ProjectComparisonTest/should-move-and-remove-projects.html
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>should-move-and-remove-projects</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/comparison/index</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>new_resource</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>new_resource</td>
+ <td>project-comparison-test-project</td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>//option[text()='1.0-SNAPSHOT']</td>
+ <td></td>
+</tr>
+<tr>
+ <td>selectAndWait</td>
+ <td>new_version</td>
+ <td>1.0-SNAPSHOT</td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>id=content</td>
+ <td>*ProjectComparisonTest Project*1.0-SNAPSHOT*</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>del-r-0</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertNotText</td>
+ <td>id=content</td>
+ <td>*Sample*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/projectComparison/ProjectComparisonTest/should-not-add-differential-metrics.html b/tests/src/test/resources/projectComparison/ProjectComparisonTest/should-not-add-differential-metrics.html
new file mode 100644
index 00000000000..ff433b66005
--- /dev/null
+++ b/tests/src/test/resources/projectComparison/ProjectComparisonTest/should-not-add-differential-metrics.html
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>should-not-add-differential-metrics</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/comparison/index</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>comparison-page</td>
+ <td>*Lines of Code*Complexity*Comments (%)*Duplicated Lines (%)*Issues*Coverage*</td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>new_metric</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>typeAndWait</td>
+ <td>new_metric</td>
+ <td>new_violations</td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>comparison-page</td>
+ <td>*Lines of Code*Complexity*Comments (%)*Duplicated Lines (%)*Issues*Coverage*</td>
+ </tr>
+ <tr>
+ <td>assertNotText</td>
+ <td>comparison-page</td>
+ <td>*New Issues*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/projectSearch/SearchProjectsTest/with-many-rules.xml b/tests/src/test/resources/projectSearch/SearchProjectsTest/with-many-rules.xml
new file mode 100644
index 00000000000..366a3ab7576
--- /dev/null
+++ b/tests/src/test/resources/projectSearch/SearchProjectsTest/with-many-rules.xml
@@ -0,0 +1,43 @@
+<profile>
+ <name>with-many-rules</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MINOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerFile</key>
+ <priority>MAJOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerModule</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>HasTag</key>
+ <priority>INFO</priority>
+ <parameters>
+ <parameter>
+ <key>tag</key>
+ <value>xoo</value>
+ </parameter>
+ </parameters>
+ </rule>
+ <rule>
+ <repositoryKey>common-xoo</repositoryKey>
+ <key>InsufficientLineCoverage</key>
+ <priority>BLOCKER</priority>
+ <parameters>
+ <parameter>
+ <key>minimumLineCoverageRatio</key>
+ <value>90</value>
+ </parameter>
+ </parameters>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/qualityGate/QualityGateOnRatingMeasuresTest/with-many-rules.xml b/tests/src/test/resources/qualityGate/QualityGateOnRatingMeasuresTest/with-many-rules.xml
new file mode 100644
index 00000000000..6668eff9096
--- /dev/null
+++ b/tests/src/test/resources/qualityGate/QualityGateOnRatingMeasuresTest/with-many-rules.xml
@@ -0,0 +1,42 @@
+<profile>
+ <name>with-many-rules</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MINOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerFile</key>
+ <priority>MAJOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerModule</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>HasTag</key>
+ <priority>INFO</priority>
+ <parameters>
+ <parameter>
+ <key>tag</key>
+ <value>xoo</value>
+ </parameter>
+ </parameters>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneBugIssuePerLine</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneVulnerabilityIssuePerModule</key>
+ <priority>BLOCKER</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/qualityGate/QualityGateUiTest/should-display-alerts-correctly-history-page.html b/tests/src/test/resources/qualityGate/QualityGateUiTest/should-display-alerts-correctly-history-page.html
new file mode 100644
index 00000000000..951ae58d38a
--- /dev/null
+++ b/tests/src/test/resources/qualityGate/QualityGateUiTest/should-display-alerts-correctly-history-page.html
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ SonarQube, open source software quality management tool.
+ ~ Copyright (C) 2008-2016 SonarSource
+ ~ mailto:contact AT sonarsource DOT com
+ ~
+ ~ SonarQube is free software; you can redistribute it and/or
+ ~ modify it under the terms of the GNU Lesser General Public
+ ~ License as published by the Free Software Foundation; either
+ ~ version 3 of the License, or (at your option) any later version.
+ ~
+ ~ SonarQube 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
+ ~ Lesser General Public License for more details.
+ ~
+ ~ You should have received a copy of the GNU Lesser 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.
+ -->
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>should-display-alerts-correctly-history-page</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">should-display-alerts-correctly-history-page</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/login</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin</td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/dashboard/index/sample</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>click</td>
+ <td>css=#context-navigation .navbar-admin-link</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>link=History</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>link=History</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>assertElementPresent</td>
+ <td>//img[@title='Quality Gate Status: Green (was Orange). ']</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>assertElementPresent</td>
+ <td>//img[@title='Quality Gate Status: Orange. Lines > 5']</td>
+ <td></td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/qualityGate/QualityGateUiTest/should_display_quality_gates_page.html b/tests/src/test/resources/qualityGate/QualityGateUiTest/should_display_quality_gates_page.html
new file mode 100644
index 00000000000..b0975df675d
--- /dev/null
+++ b/tests/src/test/resources/qualityGate/QualityGateUiTest/should_display_quality_gates_page.html
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ SonarQube, open source software quality management tool.
+ ~ Copyright (C) 2008-2016 SonarSource
+ ~ mailto:contact AT sonarsource DOT com
+ ~
+ ~ SonarQube is free software; you can redistribute it and/or
+ ~ modify it under the terms of the GNU Lesser General Public
+ ~ License as published by the Free Software Foundation; either
+ ~ version 3 of the License, or (at your option) any later version.
+ ~
+ ~ SonarQube 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
+ ~ Lesser General Public License for more details.
+ ~
+ ~ You should have received a copy of the GNU Lesser 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.
+ -->
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>should_display_quality_gates_page</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">should_display_quality_gates_page</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/quality_gates</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-gates-results a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.quality-gates-results a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>id=quality-gate-conditions</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/qualityGate/notifications/activate_notification_channels.html b/tests/src/test/resources/qualityGate/notifications/activate_notification_channels.html
new file mode 100644
index 00000000000..40de82c178a
--- /dev/null
+++ b/tests/src/test/resources/qualityGate/notifications/activate_notification_channels.html
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>reate_user_with_email</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">create_user_with_email</td>
+ </tr>
+ </thead>
+ <tbody>
+<tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>tester</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>tester</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/sonar/account/notifications</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>id=global_notifs_NewAlerts_EmailNotificationChannel</td>
+ <td></td>
+</tr>
+<tr>
+ <td>check</td>
+ <td>id=global_notifs_NewAlerts_EmailNotificationChannel</td>
+ <td></td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>id=submit-notifications</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/qualityGate/notifications/email_configuration.html b/tests/src/test/resources/qualityGate/notifications/email_configuration.html
new file mode 100644
index 00000000000..7b43cf8ee03
--- /dev/null
+++ b/tests/src/test/resources/qualityGate/notifications/email_configuration.html
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>email_configuration</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>setTimeout</td>
+ <td>300000</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin</td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/email_configuration</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>assertValue</td>
+ <td>smtp_host</td>
+ <td>localhost</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>to_address</td>
+ <td>test@example.org</td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>submit_test</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForVisible</td>
+ <td>info</td>
+ <td></td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/qualityModel/has-hello-tag.xml b/tests/src/test/resources/qualityModel/has-hello-tag.xml
new file mode 100644
index 00000000000..88cf4f9273e
--- /dev/null
+++ b/tests/src/test/resources/qualityModel/has-hello-tag.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<profile>
+ <name>has-tag</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>HasTag</key>
+ <priority>INFO</priority>
+ <parameters>
+ <parameter>
+ <key>tag</key>
+ <value>hello</value>
+ </parameter>
+ </parameters>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/qualityModel/one-day-debt-per-file.xml b/tests/src/test/resources/qualityModel/one-day-debt-per-file.xml
new file mode 100644
index 00000000000..cdebd7554c9
--- /dev/null
+++ b/tests/src/test/resources/qualityModel/one-day-debt-per-file.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<profile>
+ <name>one-day-debt-per-file</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneDayDebtPerFile</key>
+ <priority>MINOR</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/qualityModel/one-issue-per-file.xml b/tests/src/test/resources/qualityModel/one-issue-per-file.xml
new file mode 100644
index 00000000000..7193ebfd779
--- /dev/null
+++ b/tests/src/test/resources/qualityModel/one-issue-per-file.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<profile>
+ <name>one-issue-per-file</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerFile</key>
+ <priority>MINOR</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/qualityModel/one-issue-per-line.xml b/tests/src/test/resources/qualityModel/one-issue-per-line.xml
new file mode 100644
index 00000000000..521adc7e06f
--- /dev/null
+++ b/tests/src/test/resources/qualityModel/one-issue-per-line.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<profile>
+ <name>one-issue-per-line</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MINOR</priority>
+ </rule>
+ </rules>
+</profile> \ No newline at end of file
diff --git a/tests/src/test/resources/qualityModel/with-many-rules.xml b/tests/src/test/resources/qualityModel/with-many-rules.xml
new file mode 100644
index 00000000000..6668eff9096
--- /dev/null
+++ b/tests/src/test/resources/qualityModel/with-many-rules.xml
@@ -0,0 +1,42 @@
+<profile>
+ <name>with-many-rules</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MINOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerFile</key>
+ <priority>MAJOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerModule</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>HasTag</key>
+ <priority>INFO</priority>
+ <parameters>
+ <parameter>
+ <key>tag</key>
+ <value>xoo</value>
+ </parameter>
+ </parameters>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneBugIssuePerLine</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneVulnerabilityIssuePerModule</key>
+ <priority>BLOCKER</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/qualityModel/without-type-bug.xml b/tests/src/test/resources/qualityModel/without-type-bug.xml
new file mode 100644
index 00000000000..89c92cf10fa
--- /dev/null
+++ b/tests/src/test/resources/qualityModel/without-type-bug.xml
@@ -0,0 +1,26 @@
+<profile>
+ <name>without-type-bug</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MINOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerFile</key>
+ <priority>MAJOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerModule</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneVulnerabilityIssuePerModule</key>
+ <priority>BLOCKER</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/qualityModel/without-type-code-smells.xml b/tests/src/test/resources/qualityModel/without-type-code-smells.xml
new file mode 100644
index 00000000000..4bbb6f88ed7
--- /dev/null
+++ b/tests/src/test/resources/qualityModel/without-type-code-smells.xml
@@ -0,0 +1,16 @@
+<profile>
+ <name>without-type-code-smells</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneBugIssuePerLine</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneVulnerabilityIssuePerModule</key>
+ <priority>BLOCKER</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/qualityModel/without-type-vulnerability.xml b/tests/src/test/resources/qualityModel/without-type-vulnerability.xml
new file mode 100644
index 00000000000..c5fb823bd7c
--- /dev/null
+++ b/tests/src/test/resources/qualityModel/without-type-vulnerability.xml
@@ -0,0 +1,26 @@
+<profile>
+ <name>without-type-vulnerability</name>
+ <language>xoo</language>
+ <rules>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerLine</key>
+ <priority>MINOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerFile</key>
+ <priority>MAJOR</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneIssuePerModule</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ <rule>
+ <repositoryKey>xoo</repositoryKey>
+ <key>OneBugIssuePerLine</key>
+ <priority>CRITICAL</priority>
+ </rule>
+ </rules>
+</profile>
diff --git a/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_compare.html b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_compare.html
new file mode 100644
index 00000000000..c2339d690f6
--- /dev/null
+++ b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_compare.html
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_display_changelog</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_display_changelog</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profiles-table-name a[href^=&quot;/profiles/show?key=xoo-sample&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.quality-profiles-table-name a[href^=&quot;/profiles/show?key=xoo-sample&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-header .dropdown-toggle</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.quality-profile-header .dropdown-toggle</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=#quality-profile-compare</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-profile-comparison .Select</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.js-profile-comparison .Select-control</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_copy.html b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_copy.html
new file mode 100644
index 00000000000..d2f0a12a8b3
--- /dev/null
+++ b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_copy.html
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_create</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_create</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/sessions/login</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin-user</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin-user</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-header .dropdown-toggle</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.quality-profile-header .dropdown-toggle</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=#quality-profile-copy</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>css=#copy-profile-name</td>
+ <td>copied</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=#copy-profile-submit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=.quality-profile-header</td>
+ <td>*copied*</td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=.quality-profile-rules</td>
+ <td>*1*Bugs*0*Vulnerabilities*0*Code Smells*1*</td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;copied&quot;]</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_create.html b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_create.html
new file mode 100644
index 00000000000..7fefabfbd4a
--- /dev/null
+++ b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_create.html
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_create</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/sessions/login</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin-user</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin-user</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=#quality-profiles-create</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=#quality-profiles-create</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=#create-profile-name</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertElementPresent</td>
+ <td>css=#create-profile-form-backup-XooProfileImporter</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>css=#create-profile-name</td>
+ <td>test</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=#create-profile-submit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-header</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=.quality-profile-header</td>
+ <td>*test*</td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=.quality-profile-rules</td>
+ <td>*Bugs*0*Vulnerabilities*0*Code Smells*0*</td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profiles-table-row[data-name=&quot;test&quot;]</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_delete.html b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_delete.html
new file mode 100644
index 00000000000..85e85c0391c
--- /dev/null
+++ b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_delete.html
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_create</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/sessions/login</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin-user</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin-user</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-header .dropdown-toggle</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.quality-profile-header .dropdown-toggle</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=#quality-profile-delete</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=#delete-profile-submit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=#delete-profile-submit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementNotPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertElementNotPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;]</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_display_changelog.html b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_display_changelog.html
new file mode 100644
index 00000000000..8614c2fdab9
--- /dev/null
+++ b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_display_changelog.html
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_display_changelog</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_display_changelog</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=a[href^=&quot;/profiles/changelog&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=a[href^=&quot;/profiles/changelog&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-profile-changelog-event</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=.js-profile-changelog-event</td>
+ <td>*System*Activated*Has Tag*Major*tag*xoo*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_display_list.html b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_display_list.html
new file mode 100644
index 00000000000..66f83c9af47
--- /dev/null
+++ b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_display_list.html
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>should_display_list</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_display_list</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profiles-table</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertElementPresent</td>
+ <td>css=.quality-profiles-table .data[data-language=&quot;xoo&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertElementPresent</td>
+ <td>css=.quality-profiles-table .data[data-language=&quot;xoo2&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;] .quality-profiles-table-name</td>
+ <td>*Basic*</td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;] .quality-profiles-table-projects</td>
+ <td>*Default*</td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;] .quality-profiles-table-rules</td>
+ <td>*1*</td>
+</tr>
+<tr>
+ <td>assertElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;] .quality-profiles-table-rules a[href^=&quot;/coding_rules#qprofile&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=table[data-language=&quot;xoo2&quot;] tr[data-name=&quot;Basic&quot;] .quality-profiles-table-name</td>
+ <td>*Basic*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_display_profile_exporters.html b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_display_profile_exporters.html
new file mode 100644
index 00000000000..ea8b1d321f1
--- /dev/null
+++ b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_display_profile_exporters.html
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_display_profile_projects.html</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_display_profile_projects.html</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-exporters</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-exporters [data-key=&quot;XooFakeExporter&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-exporters a[href^=&quot;/api/qualityprofiles/export&quot;]</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_display_profile_inheritance.html b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_display_profile_inheritance.html
new file mode 100644
index 00000000000..501ae5b5df8
--- /dev/null
+++ b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_display_profile_inheritance.html
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_display_profile_inheritance.html</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_display_profile_inheritance.html</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-inheritance</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-inheritance-ancestor</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=.js-inheritance-ancestor</td>
+ <td>*Basic*1*</td>
+</tr>
+<tr>
+ <td>assertElementPresent</td>
+ <td>css=.js-inheritance-current</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=.js-inheritance-current</td>
+ <td>*sample*1*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_display_profile_projects.html b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_display_profile_projects.html
new file mode 100644
index 00000000000..ff38495e318
--- /dev/null
+++ b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_display_profile_projects.html
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_display_profile_projects.html</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_display_profile_projects.html</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-projects</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-profile-project</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=.js-profile-project</td>
+ <td>*Sample*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_display_profile_rules.html b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_display_profile_rules.html
new file mode 100644
index 00000000000..eb5ccf3194b
--- /dev/null
+++ b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_display_profile_rules.html
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_display_profile_rules.html</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_display_profile_rules.html</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-rules</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=.quality-profile-rules</td>
+ <td>*Active*1*Bugs*0*Vulnerabilities*0*Code Smells*1*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_filter_by_language.html b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_filter_by_language.html
new file mode 100644
index 00000000000..cfe5321b816
--- /dev/null
+++ b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_filter_by_language.html
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>should_filter_by_language</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_filter_by_language</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profiles-table .data[data-language=&quot;xoo&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertElementPresent</td>
+ <td>css=.quality-profiles-table .data[data-language=&quot;xoo2&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=.js-language-filter</td>
+ <td>*All*</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.js-language-filter</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-language-filter-option[data-language=&quot;xoo2&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.js-language-filter-option[data-language=&quot;xoo2&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementNotPresent</td>
+ <td>css=.quality-profiles-table[data-language=&quot;xoo&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertElementPresent</td>
+ <td>css=.quality-profiles-table .data[data-language=&quot;xoo2&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=.js-language-filter</td>
+ <td>*Xoo2*</td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/profiles?language=xoo2</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profiles-table .data[data-language=&quot;xoo2&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertElementNotPresent</td>
+ <td>css=.quality-profiles-table[data-language=&quot;xoo&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=.js-language-filter</td>
+ <td>*Xoo2*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_open_from_list.html b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_open_from_list.html
new file mode 100644
index 00000000000..de19c00a017
--- /dev/null
+++ b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_open_from_list.html
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>should_open_from_list</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_open_from_list</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-header</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-rules</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-inheritance</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-projects</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_rename.html b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_rename.html
new file mode 100644
index 00000000000..dfa3138f8b1
--- /dev/null
+++ b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_rename.html
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_create</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">should_create</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/sessions/login</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin-user</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin-user</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-header .dropdown-toggle</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.quality-profile-header .dropdown-toggle</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=#quality-profile-rename</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=#rename-profile-name</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>css=#rename-profile-name</td>
+ <td>new name</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=#rename-profile-submit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=.quality-profile-header</td>
+ <td>*new name*</td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;new name&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_restore.html b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_restore.html
new file mode 100644
index 00000000000..8def7727001
--- /dev/null
+++ b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_restore.html
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_create</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/sessions/login</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin-user</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin-user</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-more-admin-actions</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.js-more-admin-actions</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=#quality-profiles-restore</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=#restore-profile-backup</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=#restore-profile-submit</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_set_default.html b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_set_default.html
new file mode 100644
index 00000000000..6306820ba6a
--- /dev/null
+++ b/tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_set_default.html
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_create</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/sessions/login</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin-user</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin-user</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;sample&quot;] .quality-profiles-table-name a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profile-header .dropdown-toggle</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.quality-profile-header .dropdown-toggle</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=#quality-profile-set-as-default</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=.quality-profile-projects</td>
+ <td>*Default*</td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/profiles</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.quality-profiles-table-row[data-name=&quot;sample&quot;]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=.quality-profiles-table-row[data-name=&quot;sample&quot;] .quality-profiles-table-projects</td>
+ <td>*Default*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/serverSystem/HttpsTest/keystore.jks b/tests/src/test/resources/serverSystem/HttpsTest/keystore.jks
new file mode 100644
index 00000000000..13234b49dc2
--- /dev/null
+++ b/tests/src/test/resources/serverSystem/HttpsTest/keystore.jks
Binary files differ
diff --git a/tests/src/test/resources/serverSystem/HttpsTest/keystore.jks.txt b/tests/src/test/resources/serverSystem/HttpsTest/keystore.jks.txt
new file mode 100644
index 00000000000..ce4a4adfc39
--- /dev/null
+++ b/tests/src/test/resources/serverSystem/HttpsTest/keystore.jks.txt
@@ -0,0 +1,3 @@
+keytool -genkey -alias tests -keyalg RSA -keystore keystore.jks
+keystore password: thepassword
+key password for <tests>: thetests \ No newline at end of file
diff --git a/tests/src/test/resources/serverSystem/ServerSystemTest/derby-warning.html b/tests/src/test/resources/serverSystem/ServerSystemTest/derby-warning.html
new file mode 100644
index 00000000000..c2e13c56aad
--- /dev/null
+++ b/tests/src/test/resources/serverSystem/ServerSystemTest/derby-warning.html
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>derby-warning</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">derby-warning</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>assertElementPresent</td>
+ <td>evaluation_warning</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>footer</td>
+ <td>glob:*evaluation*</td>
+ </tr>
+
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/serverSystem/ServerSystemTest/hide-jdbc-settings.html b/tests/src/test/resources/serverSystem/ServerSystemTest/hide-jdbc-settings.html
new file mode 100644
index 00000000000..311d000f9cd
--- /dev/null
+++ b/tests/src/test/resources/serverSystem/ServerSystemTest/hide-jdbc-settings.html
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ SonarQube, open source software quality management tool.
+ ~ Copyright (C) 2008-2016 SonarSource
+ ~ mailto:contact AT sonarsource DOT com
+ ~
+ ~ SonarQube is free software; you can redistribute it and/or
+ ~ modify it under the terms of the GNU Lesser General Public
+ ~ License as published by the Free Software Foundation; either
+ ~ version 3 of the License, or (at your option) any later version.
+ ~
+ ~ SonarQube 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
+ ~ Lesser General Public License for more details.
+ ~
+ ~ You should have received a copy of the GNU Lesser 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.
+ -->
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>hide-jdbc-settings</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">hide-jdbc-settings</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/setup/index</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>assertTextNotPresent</td>
+ <td>jdbc:</td>
+ <td></td>
+ </tr>
+
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/serverSystem/ServerSystemTest/incompatible-plugin-1.0.jar b/tests/src/test/resources/serverSystem/ServerSystemTest/incompatible-plugin-1.0.jar
new file mode 100644
index 00000000000..541131c5093
--- /dev/null
+++ b/tests/src/test/resources/serverSystem/ServerSystemTest/incompatible-plugin-1.0.jar
Binary files differ
diff --git a/tests/src/test/resources/serverSystem/ServerSystemTest/system_info.html b/tests/src/test/resources/serverSystem/ServerSystemTest/system_info.html
new file mode 100644
index 00000000000..e304586c59c
--- /dev/null
+++ b/tests/src/test/resources/serverSystem/ServerSystemTest/system_info.html
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>system_info</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">system_info</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/logout</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin-user</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin-user</td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/system/index</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>id=content</td>
+ <td>*Official Distribution*</td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>id=content</td>
+ <td>*Database Version*</td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>id=content</td>
+ <td>*Pool Active Connections*</td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>id=content</td>
+ <td>*Start Time*</td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>id=content</td>
+ <td>*Processors*</td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>id=content</td>
+ <td>*java.class.path*java.specification.version*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/serverSystem/ServerSystemTest/url_ending_by_jsp.html b/tests/src/test/resources/serverSystem/ServerSystemTest/url_ending_by_jsp.html
new file mode 100644
index 00000000000..12056550fc2
--- /dev/null
+++ b/tests/src/test/resources/serverSystem/ServerSystemTest/url_ending_by_jsp.html
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ SonarQube, open source software quality management tool.
+ ~ Copyright (C) 2008-2016 SonarSource
+ ~ mailto:contact AT sonarsource DOT com
+ ~
+ ~ SonarQube is free software; you can redistribute it and/or
+ ~ modify it under the terms of the GNU Lesser General Public
+ ~ License as published by the Free Software Foundation; either
+ ~ version 3 of the License, or (at your option) any later version.
+ ~
+ ~ SonarQube 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
+ ~ Lesser General Public License for more details.
+ ~
+ ~ You should have received a copy of the GNU Lesser 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.
+ -->
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/dashboard/index/myproject.jsp</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=#content</td>
+ <td></td>
+ </tr>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/settings/SettingsTest/sonar-secret.txt b/tests/src/test/resources/settings/SettingsTest/sonar-secret.txt
new file mode 100644
index 00000000000..65b98c522da
--- /dev/null
+++ b/tests/src/test/resources/settings/SettingsTest/sonar-secret.txt
@@ -0,0 +1 @@
+0PZz+G+f8mjr3sPn4+AhHg== \ No newline at end of file
diff --git a/tests/src/test/resources/sourceCode/EncodingTest/japanese_sources.html b/tests/src/test/resources/sourceCode/EncodingTest/japanese_sources.html
new file mode 100644
index 00000000000..d873fba3939
--- /dev/null
+++ b/tests/src/test/resources/sourceCode/EncodingTest/japanese_sources.html
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>japanese_sources</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">japanese_sources</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/component/index?id=japanese-charset%3Asrc%2Fmain%2Fxoo%2Fcom%2Ftest%2FAppDuplication.xoo</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.source-viewer</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=.source-table</td>
+ <td>glob:*public class AppDuplication*</td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=.source-table</td>
+ <td>glob:*証明書パス構築に失敗しました。*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/sourceCode/HighlightingTest/symbol-usages-highlighting.html b/tests/src/test/resources/sourceCode/HighlightingTest/symbol-usages-highlighting.html
new file mode 100644
index 00000000000..42e1fc154f2
--- /dev/null
+++ b/tests/src/test/resources/sourceCode/HighlightingTest/symbol-usages-highlighting.html
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>highlight-symbol-usages</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">highlight-symbol-usages</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/component/index?id=sample-with-highlighting%3Asrc%2Fmain%2Fxoo%2Fsample%2FSample.xoo</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.source-line</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForXpathCount</td>
+ <td>//span[contains(@class, 'sym')]</td>
+ <td>3</td>
+</tr>
+<tr>
+ <td>waitForXpathCount</td>
+ <td>//span[contains(@class, 'sym highlighted')]</td>
+ <td>0</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>//span[contains(@class, 'sym')][2]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForXpathCount</td>
+ <td>//span[contains(@class, 'sym highlighted')]</td>
+ <td>2</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/sourceCode/HighlightingTest/syntax-highlighting-v1.html b/tests/src/test/resources/sourceCode/HighlightingTest/syntax-highlighting-v1.html
new file mode 100644
index 00000000000..7a1e36cf816
--- /dev/null
+++ b/tests/src/test/resources/sourceCode/HighlightingTest/syntax-highlighting-v1.html
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>java-syntax-highlighting</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">highlight-syntax-v1</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/component/index?id=sample-with-highlighting%3Asrc%2Fmain%2Fxoo%2Fsample%2FSample.xoo</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.source-line</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>verifyHtmlSource</td>
+ <td>glob:*&lt;span class="k"&gt;package&lt;/span&gt;*</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>verifyHtmlSource</td>
+ <td>glob:*&lt;span class="k"&gt;public&lt;/span&gt;*</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>verifyHtmlSource</td>
+ <td>glob:*&lt;span class="k"&gt;class&lt;/span&gt;*</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>verifyHtmlSource</td>
+ <!-- Here class is wrong and will be fixed during next analysis -->
+ <td>glob:*&lt;span class="s"&gt;return&lt;/span&gt;*</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>verifyHtmlSource</td>
+ <td>glob:*&lt;span class="s"&gt;"hello"&lt;/span&gt;*</td>
+ <td></td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/sourceCode/HighlightingTest/syntax-highlighting-v2.html b/tests/src/test/resources/sourceCode/HighlightingTest/syntax-highlighting-v2.html
new file mode 100644
index 00000000000..7f473dd9e43
--- /dev/null
+++ b/tests/src/test/resources/sourceCode/HighlightingTest/syntax-highlighting-v2.html
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>java-syntax-highlighting</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">highlight-syntax-v2</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/component/index?id=sample-with-highlighting%3Asrc%2Fmain%2Fxoo%2Fsample%2FSample.xoo</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.source-line</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>verifyHtmlSource</td>
+ <td>glob:*&lt;span class="k"&gt;package&lt;/span&gt;*</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>verifyHtmlSource</td>
+ <td>glob:*&lt;span class="k"&gt;public&lt;/span&gt;*</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>verifyHtmlSource</td>
+ <td>glob:*&lt;span class="k"&gt;class&lt;/span&gt;*</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>verifyHtmlSource</td>
+ <td>glob:*&lt;span class="k"&gt;return&lt;/span&gt;*</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>verifyHtmlSource</td>
+ <td>glob:*&lt;span class="s"&gt;"hello"&lt;/span&gt;*</td>
+ <td></td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/sourceCode/HighlightingTest/syntax-highlighting.html b/tests/src/test/resources/sourceCode/HighlightingTest/syntax-highlighting.html
new file mode 100644
index 00000000000..53e4602a978
--- /dev/null
+++ b/tests/src/test/resources/sourceCode/HighlightingTest/syntax-highlighting.html
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>java-syntax-highlighting</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">highlight-syntax</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/component/index?id=sample-with-highlighting%3Asrc%2Fmain%2Fxoo%2Fsample%2FSample.xoo</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.source-line</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>verifyHtmlSource</td>
+ <td>glob:*&lt;span class="k"&gt;package&lt;/span&gt;*</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>verifyHtmlSource</td>
+ <td>glob:*&lt;span class="k"&gt;public&lt;/span&gt;*</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>verifyHtmlSource</td>
+ <td>glob:*&lt;span class="k"&gt;class&lt;/span&gt;*</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>verifyHtmlSource</td>
+ <td>glob:*&lt;span class="k"&gt;return&lt;/span&gt;*</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>verifyHtmlSource</td>
+ <td>glob:*&lt;span class="s"&gt;"hello"&lt;/span&gt;*</td>
+ <td></td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/sourceCode/ProjectCodeTest/code_page_should_expand_root_dir.html b/tests/src/test/resources/sourceCode/ProjectCodeTest/code_page_should_expand_root_dir.html
new file mode 100644
index 00000000000..c9737d5fe25
--- /dev/null
+++ b/tests/src/test/resources/sourceCode/ProjectCodeTest/code_page_should_expand_root_dir.html
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>code_page_should_expand_root_dir</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">code_page_should_expand_root_dir</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/code?id=project-for-code-root-dir</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=#content</td>
+ <td>*Hello.xoo*src/main/xoo/sample*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/sourceCode/ProjectCodeTest/permalink.html b/tests/src/test/resources/sourceCode/ProjectCodeTest/permalink.html
new file mode 100644
index 00000000000..69364a69079
--- /dev/null
+++ b/tests/src/test/resources/sourceCode/ProjectCodeTest/permalink.html
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>test_project_code_page</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">test_project_code_page</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/code?id=project-for-code&amp;selected=project-for-code%3Asrc%2Fmain%2Fxoo%2Fsample%2FSample.xoo</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=#content</td>
+ <td>*public class Sample*</td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=.code-breadcrumbs</td>
+ <td>*Project For Code*src/main/xoo/sample*Sample.xoo*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/sourceCode/ProjectCodeTest/search.html b/tests/src/test/resources/sourceCode/ProjectCodeTest/search.html
new file mode 100644
index 00000000000..1594ee28cd5
--- /dev/null
+++ b/tests/src/test/resources/sourceCode/ProjectCodeTest/search.html
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>test_project_code_page</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">test_project_code_page</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/code?id=project-for-code</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=#content</td>
+ <td>*Project For Code*13*0*0*0.0%*</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>css=.search-box-input</td>
+ <td>xoo</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.search-box-submit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=#content</td>
+ <td>*Sample.xoo*</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.code-name-cell a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=#content</td>
+ <td>*public class Sample*</td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=.code-breadcrumbs</td>
+ <td>*Project For Code*src/main/xoo/sample*Sample.xoo*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/sourceCode/ProjectCodeTest/test_project_code_page.html b/tests/src/test/resources/sourceCode/ProjectCodeTest/test_project_code_page.html
new file mode 100644
index 00000000000..9c6b163fa44
--- /dev/null
+++ b/tests/src/test/resources/sourceCode/ProjectCodeTest/test_project_code_page.html
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>test_project_code_page</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">test_project_code_page</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/code?id=project-for-code</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=#content</td>
+ <td>*Project For Code*13*0*0*0.0%*</td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=#content</td>
+ <td>*src/main/xoo/sample*</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.code-name-cell a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=#content</td>
+ <td>*Sample.xoo*</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.code-breadcrumbs a</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForNotText</td>
+ <td>css=#content</td>
+ <td>*Sample.xoo*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/test/CoverageTest/it_coverage-expected.json b/tests/src/test/resources/test/CoverageTest/it_coverage-expected.json
new file mode 100644
index 00000000000..521b372b6ea
--- /dev/null
+++ b/tests/src/test/resources/test/CoverageTest/it_coverage-expected.json
@@ -0,0 +1,84 @@
+{
+ "sources": [
+ {
+ "line": 1,
+ "code": "package sample;"
+ },
+ {
+ "line": 2,
+ "code": ""
+ },
+ {
+ "line": 3,
+ "code": "public class Sample {"
+ },
+ {
+ "line": 4,
+ "code": "\t"
+ },
+ {
+ "line": 5,
+ "code": "\tpublic Sample(int i) {"
+ },
+ {
+ "line": 6,
+ "code": "\t\tint j = i++;",
+ "lineHits": 1,
+ "utLineHits": 1
+ },
+ {
+ "line": 7,
+ "code": "\t}"
+ },
+ {
+ "line": 8,
+ "code": "\t"
+ },
+ {
+ "line": 9,
+ "code": "\tprivate String myMethod() {"
+ },
+ {
+ "line": 10,
+ "code": "\t\tif (foo == bar) {",
+ "lineHits": 0,
+ "utLineHits": 0,
+ "conditions": 2,
+ "utConditions": 2,
+ "coveredConditions": 1,
+ "utCoveredConditions": 1
+ },
+ {
+ "line": 11,
+ "code": "\t\t\treturn \"hello\";",
+ "lineHits": 1,
+ "utLineHits": 1
+ },
+ {
+ "line": 12,
+ "code": "\t\t} else {"
+ },
+ {
+ "line": 13,
+ "code": "\t\t\tthrow new IllegalStateException();",
+ "lineHits": 0,
+ "utLineHits": 0
+ },
+ {
+ "line": 14,
+ "code": "\t\t}"
+ },
+ {
+ "line": 15,
+ "code": "\t}"
+ },
+ {
+ "line": 16,
+ "code": "}"
+ },
+ {
+ "line": 17,
+ "code": ""
+ }
+ ]
+}
diff --git a/tests/src/test/resources/test/CoverageTest/unit_test_coverage-expected.json b/tests/src/test/resources/test/CoverageTest/unit_test_coverage-expected.json
new file mode 100644
index 00000000000..c2f25a610a9
--- /dev/null
+++ b/tests/src/test/resources/test/CoverageTest/unit_test_coverage-expected.json
@@ -0,0 +1,84 @@
+{
+ "sources":[
+ {
+ "line": 1,
+ "code": "package sample;",
+ },
+ {
+ "line": 2,
+ "code": "",
+ },
+ {
+ "line": 3,
+ "code": "public class Sample {",
+ },
+ {
+ "line": 4,
+ "code": "\t",
+ },
+ {
+ "line": 5,
+ "code": "\tpublic Sample(int i) {",
+ },
+ {
+ "line": 6,
+ "code": "\t\tint j = i++;",
+ "lineHits": 1,
+ "utLineHits": 1
+ },
+ {
+ "line": 7,
+ "code": "\t}",
+ },
+ {
+ "line": 8,
+ "code": "\t",
+ },
+ {
+ "line": 9,
+ "code": "\tprivate String myMethod() {",
+ },
+ {
+ "line": 10,
+ "code": "\t\tif (foo == bar) {",
+ "lineHits": 0,
+ "utLineHits": 0,
+ "conditions": 2,
+ "utConditions": 2,
+ "coveredConditions": 1,
+ "utCoveredConditions": 1
+ },
+ {
+ "line": 11,
+ "code": "\t\t\treturn \"hello\";",
+ "lineHits": 1,
+ "utLineHits": 1
+ },
+ {
+ "line": 12,
+ "code": "\t\t} else {",
+ },
+ {
+ "line": 13,
+ "code": "\t\t\tthrow new IllegalStateException();",
+ "lineHits": 0,
+ "utLineHits": 0
+ },
+ {
+ "line": 14,
+ "code": "\t\t}",
+ },
+ {
+ "line": 15,
+ "code": "\t}",
+ },
+ {
+ "line": 16,
+ "code": "}",
+ },
+ {
+ "line": 17,
+ "code": "",
+ }
+ ]
+}
diff --git a/tests/src/test/resources/test/CoverageTest/unit_test_coverage_no_condition-expected.json b/tests/src/test/resources/test/CoverageTest/unit_test_coverage_no_condition-expected.json
new file mode 100644
index 00000000000..aabf73102ff
--- /dev/null
+++ b/tests/src/test/resources/test/CoverageTest/unit_test_coverage_no_condition-expected.json
@@ -0,0 +1,80 @@
+{
+ "sources": [
+ {
+ "line": 1,
+ "code": "package sample;",
+ },
+ {
+ "line": 2,
+ "code": "",
+ },
+ {
+ "line": 3,
+ "code": "public class Sample {",
+ },
+ {
+ "line": 4,
+ "code": "\t",
+ },
+ {
+ "line": 5,
+ "code": "\tpublic Sample(int i) {",
+ },
+ {
+ "line": 6,
+ "code": "\t\tint j = i++;",
+ "lineHits": 1,
+ "utLineHits": 1
+ },
+ {
+ "line": 7,
+ "code": "\t}",
+ },
+ {
+ "line": 8,
+ "code": "\t",
+ },
+ {
+ "line": 9,
+ "code": "\tprivate String myMethod() {",
+ },
+ {
+ "line": 10,
+ "code": "\t\tif (foo == bar) {",
+ "lineHits": 0,
+ "utLineHits": 0
+ },
+ {
+ "line": 11,
+ "code": "\t\t\treturn \"hello\";",
+ "lineHits": 1,
+ "utLineHits": 1
+ },
+ {
+ "line": 12,
+ "code": "\t\t} else {",
+ },
+ {
+ "line": 13,
+ "code": "\t\t\tthrow new IllegalStateException();",
+ "lineHits": 0,
+ "utLineHits": 0
+ },
+ {
+ "line": 14,
+ "code": "\t\t}",
+ },
+ {
+ "line": 15,
+ "code": "\t}",
+ },
+ {
+ "line": 16,
+ "code": "}",
+ },
+ {
+ "line": 17,
+ "code": "",
+ }
+ ]
+}
diff --git a/tests/src/test/resources/test/CoverageTest/ut_and_it_coverage-expected.json b/tests/src/test/resources/test/CoverageTest/ut_and_it_coverage-expected.json
new file mode 100644
index 00000000000..d2b26d19c61
--- /dev/null
+++ b/tests/src/test/resources/test/CoverageTest/ut_and_it_coverage-expected.json
@@ -0,0 +1,85 @@
+{
+ "sources": [
+ {
+ "line": 1,
+ "code": "package sample;"
+ },
+ {
+ "line": 2,
+ "code": ""
+ },
+ {
+ "line": 3,
+ "code": "public class Sample {"
+ },
+ {
+ "line": 4,
+ "code": "\t"
+ },
+ {
+ "line": 5,
+ "code": "\tpublic Sample(int i) {"
+ },
+ {
+ "line": 6,
+ "code": "\t\tint j = i++;",
+ "lineHits": 1,
+ "utLineHits": 1
+ },
+ {
+ "line": 7,
+ "code": "\t}"
+ },
+ {
+ "line": 8,
+ "code": "\t"
+ },
+ {
+ "line": 9,
+ "code": "\tprivate String myMethod() {"
+ },
+ {
+ "line": 10,
+ "code": "\t\tif (foo == bar &amp;&amp; biz &gt; 1) {",
+ "lineHits": 0,
+ "utLineHits": 0,
+ "conditions": 4,
+ "utConditions": 4,
+ "coveredConditions": 2,
+ "utCoveredConditions": 2
+ },
+ {
+ "line": 11,
+ "code": "\t\t\treturn \"hello\";",
+ "lineHits": 1,
+ "utLineHits": 1
+ },
+ {
+ "line": 12,
+ "code": "\t\t} else {"
+ },
+ {
+ "line": 13,
+ "code": "\t\t\tthrow new IllegalStateException();",
+ "lineHits": 1,
+ "utLineHits": 1
+ },
+ {
+ "line": 14,
+ "code": "\t\t}"
+ },
+ {
+ "line": 15,
+ "code": "\t}"
+ },
+ {
+ "line": 16,
+ "code": "}"
+ },
+ {
+ "line": 17,
+ "code": ""
+ }
+ ]
+
+}
diff --git a/tests/src/test/resources/test/CoverageTrackingTest/covered_files-expected.json b/tests/src/test/resources/test/CoverageTrackingTest/covered_files-expected.json
new file mode 100644
index 00000000000..5dda4f3dd11
--- /dev/null
+++ b/tests/src/test/resources/test/CoverageTrackingTest/covered_files-expected.json
@@ -0,0 +1,14 @@
+{
+ "files": [
+ {
+ "key": "sample-with-tests:src/main/xoo/sample/Sample.xoo",
+ "longName": "src/main/xoo/sample/Sample.xoo",
+ "coveredLines": 2
+ },
+ {
+ "key": "sample-with-tests:src/main/xoo/sample/Sample2.xoo",
+ "longName": "src/main/xoo/sample/Sample2.xoo",
+ "coveredLines": 1
+ }
+ ]
+}
diff --git a/tests/src/test/resources/test/CoverageTrackingTest/tests-expected.json b/tests/src/test/resources/test/CoverageTrackingTest/tests-expected.json
new file mode 100644
index 00000000000..121e6753074
--- /dev/null
+++ b/tests/src/test/resources/test/CoverageTrackingTest/tests-expected.json
@@ -0,0 +1,30 @@
+{
+ "tests": [
+ {
+ "name": "success",
+ "status": "OK",
+ "durationInMs": 4,
+ "coveredLines": 3
+ },
+ {
+ "name": "error",
+ "status": "ERROR",
+ "durationInMs": 2,
+ "coveredLines": 0,
+ "message": "Error",
+ "stacktrace": "The stack"
+ },
+ {
+ "name": "failure",
+ "status": "FAILURE",
+ "durationInMs": 2,
+ "coveredLines": 1,
+ "message": "Failure"
+ },
+ {
+ "name": "skipped",
+ "status": "SKIPPED",
+ "coveredLines": 0
+ }
+ ]
+}
diff --git a/tests/src/test/resources/test/TestExecutionTest/expected.json b/tests/src/test/resources/test/TestExecutionTest/expected.json
new file mode 100644
index 00000000000..843d28f5284
--- /dev/null
+++ b/tests/src/test/resources/test/TestExecutionTest/expected.json
@@ -0,0 +1,31 @@
+{
+ "tests": [
+ {
+ "name": "success",
+ "status": "OK",
+ "durationInMs": 4,
+ "coveredLines": 0
+ },
+ {
+ "name": "error",
+ "status": "ERROR",
+ "durationInMs": 2,
+ "coveredLines": 0,
+ "message": "Error",
+ "stacktrace": "The stack"
+ },
+ {
+ "name": "failure",
+ "status": "FAILURE",
+ "durationInMs": 2,
+ "coveredLines": 0,
+ "message": "Failure"
+ },
+ {
+ "name": "skipped",
+ "status": "SKIPPED",
+ "coveredLines": 0,
+ "durationInMs": 0
+ }
+ ]
+}
diff --git a/tests/src/test/resources/ui/UiExtensionsTest/static-files.html b/tests/src/test/resources/ui/UiExtensionsTest/static-files.html
new file mode 100644
index 00000000000..0acd31cc1fa
--- /dev/null
+++ b/tests/src/test/resources/ui/UiExtensionsTest/static-files.html
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>static-files</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/static/uiextensionsplugin/file.html</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>//body</td>
+ <td>Text from static resource</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/updateCenter/UpdateCenterTest/update-center.properties b/tests/src/test/resources/updateCenter/UpdateCenterTest/update-center.properties
new file mode 100644
index 00000000000..5882d43188f
--- /dev/null
+++ b/tests/src/test/resources/updateCenter/UpdateCenterTest/update-center.properties
@@ -0,0 +1,62 @@
+# THIS FILE IS USED BY THE UPDATE CENTER
+# DO NOT REMOVE OR RENAME
+#
+# Note : prefix all : by \
+#
+
+publicVersions=3.0,100.0
+
+3.0.description=Encryption of database password, TimeMachine available as widgets, New algorithm for tracking violations, 40 bugs and 40 improvements
+3.0.downloadUrl=http\://dist.sonar.codehaus.org/sonar-3.0.zip
+3.0.changelogUrl=http\://www.sonarsource.org/downloads/#3.0
+3.0.date=2012-04-17
+
+100.0.description=Hundred dot zero!
+100.0.downloadUrl=http\://dist.sonar.codehaus.org/sonar-100.0.zip
+100.0.changelogUrl=http\://www.sonarsource.org/downloads/#100.0
+100.0.date=2112-06-13
+
+
+plugins=fake,abap
+
+#--------------------------------------------------------------------------------------------------------------------------
+abap.homepageUrl=http\://www.sonarsource.com/products/plugins/languages/abap/
+abap.name=ABAP
+abap.category=Additional Languages
+abap.publicVersions=1.0,1.0.1,1.1,2.0.1
+
+abap.1.0.description=Initial version of the product
+abap.1.0.sqVersions=2.7,2.8,2.9,2.10,2.11,2.12,2.13,2.13.1,2.14,3.0,3.0.1,3.1,3.1.1
+abap.1.0.downloadUrl=
+abap.1.0.date=2011-07-29
+
+abap.1.0.1.description=Adjust computation of certain metrics
+abap.1.0.1.sqVersions=2.7,2.8,2.9,2.10,2.11,2.12,2.13,2.13.1,2.14,3.0,3.0.1,3.1,3.1.1
+abap.1.0.1.downloadUrl=
+abap.1.0.1.date=2011-08-28
+
+abap.1.1.description=Adjust computation of certain metrics
+abap.1.1.sqVersions=2.7,2.8,2.9,2.10,2.11,2.12,2.13,2.13.1,2.14,3.0,3.0.1,3.1,3.1.1
+abap.1.1.downloadUrl=
+abap.1.1.date=2012-01-05
+
+abap.2.0.1.description=This new version provides an ABAP source code extractor, adds 5 new rules and improves others
+abap.2.0.1.sqVersions=3.2,3.3,3.4,3.5,3.6,3.7,3.7.1,3.7.2,3.7.3,3.7.4,3.7.5,3.7.6,3.7.7,4.0,4.0.1,4.1,4.2,4.3,4.4,4.5,4.6,4.7,5.0,5.1,5.2,5.3,5.4,5.5,5.6,5.7,6.0,6.1,6.2,6.3,6.4,6.5,6.6
+abap.2.0.1.downloadUrl=
+abap.2.0.1.date=2012-06-25
+
+#--------------------------------------------------------------------------------------------------------------------------
+fake.category=Additional Metrics
+fake.publicVersions=1.0,1.1
+fake.name=Fake
+fake.description=Fake plugin for integration tests
+
+fake.1.0.description=Initial release
+fake.1.0.sqVersions=3.2,3.3,3.4,3.5,3.6,3.7
+fake.1.0.downloadUrl=
+fake.1.0.date=2011-05-06
+
+fake.1.1.description=Support sonarqube v100.0
+fake.1.1.sqVersions=3.2,3.3,3.4,3.5,3.6,3.7,3.7.1,3.7.2,3.7.3,3.7.4,3.7.5,3.7.6,3.7.7,4.0,4.0.1,4.1,4.2,4.3,4.4,4.5,4.6,4.7,5.0,5.1,5.2,5.3,5.4,5.5,5.6,5.7,6.0,6.1,6.2,6.3,6.4,6.5,6.6,100.0
+fake.1.1.downloadUrl=
+fake.1.1.date=2012-04-27
diff --git a/tests/src/test/resources/updateCenter/installed-plugins.html b/tests/src/test/resources/updateCenter/installed-plugins.html
new file mode 100644
index 00000000000..d1986f6f897
--- /dev/null
+++ b/tests/src/test/resources/updateCenter/installed-plugins.html
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>installed-plugins</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/logout</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/sessions/login</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>commit</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin</td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/updatecenter</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*Fake*</td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>id=update-center-plugins</td>
+ <td>*Fake*1.0-SNAPSHOT*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/user/BaseIdentityProviderTest/display_message_in_ui_but_not_in_log_when_unauthorized_exception.html b/tests/src/test/resources/user/BaseIdentityProviderTest/display_message_in_ui_but_not_in_log_when_unauthorized_exception.html
new file mode 100644
index 00000000000..b62763fb7c9
--- /dev/null
+++ b/tests/src/test/resources/user/BaseIdentityProviderTest/display_message_in_ui_but_not_in_log_when_unauthorized_exception.html
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>fail_to_authenticate_when_not_allowed_to_sign_up</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">french</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*Log in with Fake base identity provider*</td>
+ </tr>
+ <tr>
+ <td>click</td>
+ <td>css=.oauth-providers a</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>bd</td>
+ <td>*You're not authorized to access this page. Please contact the administrator.*</td>
+ </tr>
+ <tr>
+ <td>assertText</td>
+ <td>bd</td>
+ <td>*Reason : A functional error has happened*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/user/BaseIdentityProviderTest/display_unauthorized_page_when_authentication_failed.html b/tests/src/test/resources/user/BaseIdentityProviderTest/display_unauthorized_page_when_authentication_failed.html
new file mode 100644
index 00000000000..47a19a2df41
--- /dev/null
+++ b/tests/src/test/resources/user/BaseIdentityProviderTest/display_unauthorized_page_when_authentication_failed.html
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>display_unauthorized_page_when_authentication_failed</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">french</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*Log in with Fake base identity provider*</td>
+ </tr>
+ <tr>
+ <td>click</td>
+ <td>css=.oauth-providers a</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>bd</td>
+ <td>*You're not authorized to access this page. Please contact the administrator.*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/user/BaseIdentityProviderTest/fail_to_authenticate_when_not_allowed_to_sign_up.html b/tests/src/test/resources/user/BaseIdentityProviderTest/fail_to_authenticate_when_not_allowed_to_sign_up.html
new file mode 100644
index 00000000000..40c300bd701
--- /dev/null
+++ b/tests/src/test/resources/user/BaseIdentityProviderTest/fail_to_authenticate_when_not_allowed_to_sign_up.html
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>fail_to_authenticate_when_not_allowed_to_sign_up</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">french</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*Log in with Fake base identity provider*</td>
+ </tr>
+ <tr>
+ <td>click</td>
+ <td>css=.oauth-providers a</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>bd</td>
+ <td>*You're not authorized to access this page. Please contact the administrator.*Reason : 'fake-base-id-provider' users are not allowed to sign up*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/user/BaseIdentityProviderTest/fail_when_email_already_exists.html b/tests/src/test/resources/user/BaseIdentityProviderTest/fail_when_email_already_exists.html
new file mode 100644
index 00000000000..b6f7e600ac3
--- /dev/null
+++ b/tests/src/test/resources/user/BaseIdentityProviderTest/fail_when_email_already_exists.html
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>fail_when_email_already_exists</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">french</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*Log in with Fake base identity provider*</td>
+ </tr>
+ <tr>
+ <td>click</td>
+ <td>css=.oauth-providers a</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>bd</td>
+ <td>*You're not authorized to access this page. Please contact the administrator.*</td>
+ </tr>
+ <tr>
+ <td>assertText</td>
+ <td>bd</td>
+ <td>*You can't sign up because email 'john@email.com' is already used by an existing user. This means that you probably already registered with another account*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/user/ExternalAuthenticationTest/create-and-delete-user.html b/tests/src/test/resources/user/ExternalAuthenticationTest/create-and-delete-user.html
new file mode 100644
index 00000000000..f7d0dd6ffb6
--- /dev/null
+++ b/tests/src/test/resources/user/ExternalAuthenticationTest/create-and-delete-user.html
@@ -0,0 +1,144 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-/W3C/DTD XHTML 1.0 Strict/EN" "http:/www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http:/www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http:/selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>create-and-delete-users</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">create-and-delete-users</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin-user</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin-user</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/users</td>
+ <td></td>
+</tr>
+<tr>
+ <td>store</td>
+ <td>javascript{new Date().getTime()}</td>
+ <td>userId</td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*Create User*</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>id=users-create</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>id=create-user-form</td>
+ <td>*Create User*</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>id=create-user-login</td>
+ <td>${userId}</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>id=create-user-name</td>
+ <td>Name of ${userId}</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>id=create-user-password</td>
+ <td>password</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>id=create-user-submit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>id=users-list</td>
+ <td>*${userId}*</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=[data-login=&quot;${userId}&quot;] .js-user-deactivate</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>id=deactivate-user-form</td>
+ <td>*${userId}*</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>id=deactivate-user-submit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForNotText</td>
+ <td>id=users-list</td>
+ <td>*${userId}*</td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/sessions/logout</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>tester</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>password</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td>deactivate-user-form</td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>login_form</td>
+ <td>*Authentication failed*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/user/ExternalAuthenticationTest/external-user-details.html b/tests/src/test/resources/user/ExternalAuthenticationTest/external-user-details.html
new file mode 100644
index 00000000000..b2d712f516c
--- /dev/null
+++ b/tests/src/test/resources/user/ExternalAuthenticationTest/external-user-details.html
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-/W3C/DTD XHTML 1.0 Strict/EN" "http:/www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http:/www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http:/selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>external_user_details</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">external_user_details</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>login</td>
+ <td>tester</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>password</td>
+ <td>123</td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/account/</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>login</td>
+ <td>tester</td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>id=name</td>
+ <td>Tester Testerovich</td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>id=email</td>
+ <td>tester@example.org</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/user/ExternalAuthenticationTest/external-user-details2.html b/tests/src/test/resources/user/ExternalAuthenticationTest/external-user-details2.html
new file mode 100644
index 00000000000..d18ae08afb9
--- /dev/null
+++ b/tests/src/test/resources/user/ExternalAuthenticationTest/external-user-details2.html
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-/W3C/DTD XHTML 1.0 Strict/EN" "http:/www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http:/www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http:/selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>external_user_details</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">external_user_details</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>login</td>
+ <td>tester</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>password</td>
+ <td>123</td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/account</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>login</td>
+ <td>tester</td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>id=name</td>
+ <td>Tester2 Testerovich</td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>id=email</td>
+ <td>tester2@example.org</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/user/ExternalAuthenticationTest/system-info.html b/tests/src/test/resources/user/ExternalAuthenticationTest/system-info.html
new file mode 100644
index 00000000000..1daf46d205d
--- /dev/null
+++ b/tests/src/test/resources/user/ExternalAuthenticationTest/system-info.html
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-/W3C/DTD XHTML 1.0 Strict/EN" "http:/www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http:/www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http:/selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>external_user_details</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">external_user_details</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/system</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin</td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>id=content</td>
+ <td>*External User Authentication*FakeRealm*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/user/LocalAuthenticationTest/allow_users_to_sign_up.html b/tests/src/test/resources/user/LocalAuthenticationTest/allow_users_to_sign_up.html
new file mode 100644
index 00000000000..dab0c303ea5
--- /dev/null
+++ b/tests/src/test/resources/user/LocalAuthenticationTest/allow_users_to_sign_up.html
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>allow_users_to_sign_up</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*Log In to SonarQube*</td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>bd</td>
+ <td>*Not a member*</td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>link=Sign up</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>id=user_login</td>
+ <td>signuplogin</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>id=user_name</td>
+ <td>SignUpName</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>id=user_password</td>
+ <td>password</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>id=user_password_confirmation</td>
+ <td>password</td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>global-navigation</td>
+ <td>*Log in*</td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*Log In to SonarQube*</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>login</td>
+ <td>signuplogin</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>password</td>
+ <td>password</td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>css=.navbar</td>
+ <td>*SignUpName*</td>
+ </tr>
+ <tr>
+ <td>click</td>
+ <td>Link=SignUpName</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>css=.navbar</td>
+ <td>*Log out*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/user/LocalAuthenticationTest/force-authentication.html b/tests/src/test/resources/user/LocalAuthenticationTest/force-authentication.html
new file mode 100644
index 00000000000..4c5a751665d
--- /dev/null
+++ b/tests/src/test/resources/user/LocalAuthenticationTest/force-authentication.html
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>force-authentication</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">force-authentication</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/logout</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*Log In to SonarQube*</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin-user</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin-user</td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/sessions/logout</td>
+ <td></td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/user/LocalAuthenticationTest/login_successful.html b/tests/src/test/resources/user/LocalAuthenticationTest/login_successful.html
new file mode 100644
index 00000000000..18da805f66b
--- /dev/null
+++ b/tests/src/test/resources/user/LocalAuthenticationTest/login_successful.html
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>login_successful</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">login_successful</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*Log In to SonarQube*</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin-user</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin-user</td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>click</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>assertTextPresent</td>
+ <td>Log out</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>link=Log out</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>link=Log in</td>
+ <td>Log in</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/user/LocalAuthenticationTest/login_wrong_password.html b/tests/src/test/resources/user/LocalAuthenticationTest/login_wrong_password.html
new file mode 100644
index 00000000000..dc66db8ee9b
--- /dev/null
+++ b/tests/src/test/resources/user/LocalAuthenticationTest/login_wrong_password.html
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>login_wrong_password</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*Log In to SonarQube*</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin-user</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>password</td>
+ <td>wrong</td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>assertTextPresent</td>
+ <td>Authentication failed</td>
+ <td></td>
+ </tr>
+
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_login_when_not_enough_privilege.html b/tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_login_when_not_enough_privilege.html
new file mode 100644
index 00000000000..9b5a1645ace
--- /dev/null
+++ b/tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_login_when_not_enough_privilege.html
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>redirect-to-original-url-after-direct-login</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*Log In to SonarQube*</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>simple-user</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>password</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/settings</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*Log In to SonarQube*</td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>content</td>
+ <td>*You are not authorized to access this page. Please log in with more privileges and try again.*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_original_url_after_direct_login.html b/tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_original_url_after_direct_login.html
new file mode 100644
index 00000000000..c6519726f27
--- /dev/null
+++ b/tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_original_url_after_direct_login.html
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>redirect-to-original-url-after-direct-login</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/logout</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>global-navigation</td>
+ <td>*Log in*</td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/settings?category=general</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*Log In to SonarQube*</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin-user</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin-user</td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>assertLocation</td>
+ <td>glob:*/settings?category=general*</td>
+ <td></td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_original_url_after_indirect_login.html b/tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_original_url_after_indirect_login.html
new file mode 100644
index 00000000000..aecfd8af765
--- /dev/null
+++ b/tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_original_url_after_indirect_login.html
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/logout</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>global-navigation</td>
+ <td>*Log in*</td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/settings</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*Log In to SonarQube*</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin-user</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin-user</td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>assertLocation</td>
+ <td>*/settings</td>
+ <td></td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_original_url_with_parameters_after_direct_login.html b/tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_original_url_with_parameters_after_direct_login.html
new file mode 100644
index 00000000000..5359e3308e0
--- /dev/null
+++ b/tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_original_url_with_parameters_after_direct_login.html
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/logout</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>global-navigation</td>
+ <td>*Log in*</td>
+ </tr>
+ <tr>
+ <td>open</td>
+ <td>/projects?gate=OK&reliability=1&security=1</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>global-navigation</td>
+ <td>*Log in*</td>
+ </tr>
+ <tr>
+ <td>click</td>
+ <td>link=Log in</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*Log In to SonarQube*</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin-user</td>
+ </tr>
+ <tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin-user</td>
+ </tr>
+ <tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>assertLocation</td>
+ <td>*/projects?gate=OK&reliability=1&security=1</td>
+ <td></td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/user/LocalAuthenticationTest/should_not_be_unlogged_when_going_to_login_page.html b/tests/src/test/resources/user/LocalAuthenticationTest/should_not_be_unlogged_when_going_to_login_page.html
new file mode 100644
index 00000000000..af5b4362f0e
--- /dev/null
+++ b/tests/src/test/resources/user/LocalAuthenticationTest/should_not_be_unlogged_when_going_to_login_page.html
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>redirect-to-original-url-after-direct-login</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*Log In to SonarQube*</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>simple-user</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>password</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*Log In to SonarQube*</td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/user/MyAccountPageTest/should_display_no_projects.html b/tests/src/test/resources/user/MyAccountPageTest/should_display_no_projects.html
new file mode 100644
index 00000000000..2c2f0f026db
--- /dev/null
+++ b/tests/src/test/resources/user/MyAccountPageTest/should_display_no_projects.html
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_display_no_projects</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">should_display_no_projects</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/login</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>account-user</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>password</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/account/projects/</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=#account-projects</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertElementNotPresent</td>
+ <td>css=.account-projects-list</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/user/MyAccountPageTest/should_display_projects.html b/tests/src/test/resources/user/MyAccountPageTest/should_display_projects.html
new file mode 100644
index 00000000000..b199152803f
--- /dev/null
+++ b/tests/src/test/resources/user/MyAccountPageTest/should_display_projects.html
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>should_display_projects</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">should_display_projects</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/login</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>account-user</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>password</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/account/projects/</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.account-project-card</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=.account-project-name</td>
+ <td>*Sample*</td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=.account-project-quality-gate</td>
+ <td>*Passed*</td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=.account-project-key</td>
+ <td>*sample*</td>
+</tr>
+<tr>
+ <td>assertText</td>
+ <td>css=.account-project-description</td>
+ <td>*Description of a project*</td>
+</tr>
+<tr>
+ <td>assertElementPresent</td>
+ <td>css=.account-project-analysis</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertElementPresent</td>
+ <td>css=.account-project-links a[href=&quot;http://example.com&quot;]</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/user/OAuth2IdentityProviderTest/display_message_in_ui_but_not_in_log_when_unauthorized_exception.html b/tests/src/test/resources/user/OAuth2IdentityProviderTest/display_message_in_ui_but_not_in_log_when_unauthorized_exception.html
new file mode 100644
index 00000000000..6a38ed69063
--- /dev/null
+++ b/tests/src/test/resources/user/OAuth2IdentityProviderTest/display_message_in_ui_but_not_in_log_when_unauthorized_exception.html
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>fail_to_authenticate_when_not_allowed_to_sign_up</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">french</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*Log in with Fake oauth2 identity provider*</td>
+ </tr>
+ <tr>
+ <td>click</td>
+ <td>css=.oauth-providers a</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>bd</td>
+ <td>*You're not authorized to access this page. Please contact the administrator.*</td>
+ </tr>
+ <tr>
+ <td>assertText</td>
+ <td>bd</td>
+ <td>*Reason : A functional error has happened*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/user/OAuth2IdentityProviderTest/display_unauthorized_page_when_authentication_failed.html b/tests/src/test/resources/user/OAuth2IdentityProviderTest/display_unauthorized_page_when_authentication_failed.html
new file mode 100644
index 00000000000..b01d24aad4c
--- /dev/null
+++ b/tests/src/test/resources/user/OAuth2IdentityProviderTest/display_unauthorized_page_when_authentication_failed.html
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>display_unauthorized_page_when_authentication_failed</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">french</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*Log in with Fake oauth2 identity provider*</td>
+ </tr>
+ <tr>
+ <td>click</td>
+ <td>css=.oauth-providers a</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>bd</td>
+ <td>*You're not authorized to access this page. Please contact the administrator.*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/user/OAuth2IdentityProviderTest/fail_to_authenticate_when_not_allowed_to_sign_up.html b/tests/src/test/resources/user/OAuth2IdentityProviderTest/fail_to_authenticate_when_not_allowed_to_sign_up.html
new file mode 100644
index 00000000000..a3da2de8ed0
--- /dev/null
+++ b/tests/src/test/resources/user/OAuth2IdentityProviderTest/fail_to_authenticate_when_not_allowed_to_sign_up.html
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>fail_to_authenticate_when_not_allowed_to_sign_up</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">french</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*Log in with Fake oauth2 identity provider*</td>
+ </tr>
+ <tr>
+ <td>click</td>
+ <td>css=.oauth-providers a</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>bd</td>
+ <td>*You're not authorized to access this page. Please contact the administrator.*Reason : 'fake-oauth2-id-provider' users are not allowed to sign up*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/user/OAuth2IdentityProviderTest/fail_when_email_already_exists.html b/tests/src/test/resources/user/OAuth2IdentityProviderTest/fail_when_email_already_exists.html
new file mode 100644
index 00000000000..7d038ac592d
--- /dev/null
+++ b/tests/src/test/resources/user/OAuth2IdentityProviderTest/fail_when_email_already_exists.html
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>fail_when_email_already_exists</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">french</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>content</td>
+ <td>*Log in with Fake oauth2 identity provider*</td>
+ </tr>
+ <tr>
+ <td>click</td>
+ <td>css=.oauth-providers a</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>waitForText</td>
+ <td>bd</td>
+ <td>*You're not authorized to access this page. Please contact the administrator.*</td>
+ </tr>
+ <tr>
+ <td>assertText</td>
+ <td>bd</td>
+ <td>*You can't sign up because email 'john@email.com' is already used by an existing user. This means that you probably already registered with another account*</td>
+ </tr>
+ </tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/user/UsersPageTest/admin_should_change_its_own_password.html b/tests/src/test/resources/user/UsersPageTest/admin_should_change_its_own_password.html
new file mode 100644
index 00000000000..06e806cf259
--- /dev/null
+++ b/tests/src/test/resources/user/UsersPageTest/admin_should_change_its_own_password.html
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>admin_should_change_its_own_password</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr>
+<td rowspan="1" colspan="3">admin_should_change_its_own_password</td>
+</tr>
+</thead>
+<tbody>
+<tr>
+ <td>open</td>
+ <td>/sessions/logout</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/sessions/login</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin-user</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin-user</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/users</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=[data-login=admin-user]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=[data-login=admin-user] .js-user-change-password</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.modal</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>id=change-user-password-old-password</td>
+ <td>admin-user</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>id=change-user-password-password</td>
+ <td>new-admin-user</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>id=change-user-password-password-confirmation</td>
+ <td>new-admin-user</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>id=change-user-password-submit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertElementNotPresent</td>
+ <td>css=.modal</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/tests/src/test/resources/user/UsersPageTest/generate_and_revoke_user_token.html b/tests/src/test/resources/user/UsersPageTest/generate_and_revoke_user_token.html
new file mode 100644
index 00000000000..c81949c4d10
--- /dev/null
+++ b/tests/src/test/resources/user/UsersPageTest/generate_and_revoke_user_token.html
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="selenium.base" href="http://localhost:49506"/>
+ <title>generate_and_revoke_user_token</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+ <thead>
+ <tr>
+ <td rowspan="1" colspan="3">generate_and_revoke_user_token</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>open</td>
+ <td>/sessions/logout</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/sessions/new</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>login</td>
+ <td>admin-user</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>password</td>
+ <td>admin-user</td>
+</tr>
+<tr>
+ <td>clickAndWait</td>
+ <td>commit</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-authenticated</td>
+ <td></td>
+</tr>
+<tr>
+ <td>open</td>
+ <td>/users</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-user-tokens</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.js-user-tokens</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=.modal</td>
+ <td>*No tokens*</td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-generate-token-form input</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>css=.js-generate-token-form input</td>
+ <td>test-token</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.js-generate-token-form button</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.modal code</td>
+ <td></td>
+</tr>
+<tr>
+ <td>waitForText</td>
+ <td>css=.modal</td>
+ <td>*test-token*</td>
+</tr>
+<tr>
+ <td>waitForElementPresent</td>
+ <td>css=.js-revoke-token-form</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.js-revoke-token-form button</td>
+ <td></td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>css=.js-revoke-token-form button</td>
+ <td></td>
+</tr>
+<tr>
+ <td>assertElementNotPresent</td>
+ <td>css=.js-revoke-token-form</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>